diff --git a/docs/design/logging-redesign/PLAN.md b/docs/design/logging-redesign/PLAN.md new file mode 100644 index 0000000..9a0f05b --- /dev/null +++ b/docs/design/logging-redesign/PLAN.md @@ -0,0 +1,222 @@ +# Logging & category-system redesign — phased implementation plan + +**Status:** Phase 0 complete (planning + groundwork committed). Phases 1-8 not started. +**Owner handoff doc.** This is written so a *fresh* session can pick up any phase without re-deriving the codebase. Read this file top-to-bottom, then the one subsystem map relevant to your phase, then start. + +--- + +## 0. How to use this document (read first) + +1. **Source of truth for the design** is `handover/` in this folder: `README.md` (intent + component-library requirement), `GoFlo Theme Redesign.md` (palette hexes — already applied, see note below), `Log Screens Rethought.html` (pannable mockup canvas), and `screens/01-06-row.png` (rendered reference of each row). Open the PNGs to see the target. +2. **Codebase groundwork** is in `subsystem-maps/` — three stamped maps (logging screens, data model, theme/colour). Each carries the commit + build it was verified against and a staleness check. Trust them, but run the staleness check first if HEAD has moved. +3. **Pick up a phase** from §6. Each phase is self-contained, shippable on its own, and lists its files, data changes, acceptance criteria, and a feature-preservation checklist. +4. **When you finish a phase**, update §7 (Progress log) in the same PR: mark the phase done, record the commit, DB version, and any deviations from this plan. Keep the maps honest — if you changed a file a map describes, update the map or note the drift. +5. **Follow the golden rules in §1 on every phase.** They are what prevent feature loss and CI failures. + +--- + +## 1. Golden rules (non-negotiable, every phase) + +- **Never lose a feature.** The current app is feature-rich (§3). The mock shows an *end state*; it is not a licence to delete. Additive-first: build the new path alongside the old, prove parity, *then* remove. The removal phase (Phase 8) is the only place old screens get deleted, and only against the parity checklist. +- **Migrations are additive and versioned.** DB is currently version 23 (`GoFloDatabase.kt`). Every schema change adds a numbered migration (`N → N+1`), never edits an existing one, never uses `fallbackToDestructiveMigration`. Existing user data must survive untouched. +- **Colour is a role, never a literal.** Bind to `MaterialTheme.colorScheme.*` or a resolved category role token. The hexes in the mock are role stand-ins. The one exception is the `FIXED` role, which is a deliberate raw hex (already supported by `colorToken`'s hex path). +- **Every PR that touches app code adds a changelog fragment** at `changelog/unreleased/.json` (see CLAUDE.md). Do not edit `CHANGELOG.md` or `build.gradle.kts` versions directly. +- **Accessibility is enforced by CI.** `a11y_check.py` requires a `Role.` on every `.clickable{}`/`.combinedClickable{}`. Colour is never the only signal (chips carry a check, steps carry fill + label, timeline carries the time). Min tap target 44x44dp (design says pad to 48). Run `python3 a11y_check.py` before pushing. +- **WCAG is enforced.** Run `python3 wcag_check.py` after any colour change. Derived roles (quaternary/quinary/senary) are not in the palette tables — add an explicit spot-check for them. +- **No en dashes or em dashes in user-facing text** (CLAUDE.md). Hyphens in compounds are fine. This applies to strings the user sees, not this planning doc. +- **Privacy invariants hold.** No network, no new permissions without discussion, health data stays on device, PIN-locked widgets show a neutral placeholder. Medical disclaimer / privacy surfaces use the body font, never Comfortaa. +- **This environment cannot build** (no Android SDK/Gradle here). CI is the build check. Verify by inspection + the python checks; do not add "couldn't compile" disclaimers. + +--- + +## 2. What this redesign is + +A ground-up rethink of the per-day logging screens and the "What You Track" category system, resting on one idea: **a category stores a colour *role*, and belongs to an optional *group*; the group owns the role and a default input type.** From that follow: one unified log screen (period is a *state of the day*, not a separate destination), a small reusable component library that every screen is assembled from, and richer categories (icons already exist; add Yes/No + Time input types, generalised timelines, and per-category alarms surfaced on edit). + +The design's four load-bearing rules: +1. **Kill the slider for ratings.** A 1-5 rating is discrete rising tap-steps, not a drag track. Genuine continuous measures (weight, temp) keep a real slider. +2. **Words over numbers.** "Medium", "Still ongoing", "Barely noticeable". Numbers survive only as small step captions where a scale is genuinely numeric. +3. **One card per idea, not per field.** Dates = one list card with rows. Section titles = small uppercase labels with the current value on the right. +4. **Selected state is tonal, not outlined.** Chips fill with a container colour + check; unselected stay hairline. + +--- + +## 3. Feature-preservation inventory (the "do not lose these" list) + +Every item below exists today and must keep working through every phase. Tick against this list in each phase's preservation checklist. + +**Logging** +- Log a generic category value: chip multi-select (`default`), stepped slider (`numeric_slider`), free numeric input with unit (`numeric_free`), count stepper (`increment`), timed increment with per-tap timestamp + timeline (`increment` + `trackAgainstTime`). +- "Previously recorded (removed from options)" chips for stored labels no longer in the catalog. +- Notes per log (500-char cap). Edit an existing log. Delete a log. Date selection when `canEditDate`. +- Log a period day: start/end dates, ongoing (null end), flow (slider *or* chips with the 1→Spotting/2→Light/4→Heavy/else Medium mapping), symptoms (chips + inline "Add" via `AddSymptomDialog` that writes to the value catalog), pinned categories (`showInLogPeriod`), notes. +- Period episode/continuation logic: gap tolerance from prefs, episode day number, range vs open-ended, "Remove this day", "Delete Entire Period", "Disable period logging". +- Unsaved-changes discard/save guard on the period screen. +- **Period save fans out to the tracking system** (`syncFlowToTrackingLog`, `syncSymptomsToTrackingLog`, `syncPinnedCategoryLogs`) so period data shows in Stats/Flow/Symptoms. Preserve this on the unified screen. +- Side effects on save: widget refresh (`GoFloWidget.updateAllWidgets`), reminder refresh (`ReminderScheduler.refreshPredictionReminders`). + +**Categories & management** +- Create / rename / archive / unarchive / delete (system categories protected) / reorder categories. +- Per-category icon (20-icon catalogue), colour token (primary/secondary/tertiary/hex), input type, numeric range/unit/decimals, scale labels, allow-multiple, show-in-log-period, track-against-time, mode key. +- Value catalog CRUD (`TrackingValue` rows) incl. rename-with-history (`bulkRenameLogValues`) and seeded-value protection. +- System categories: Flow, Symptoms (seeded), Ovulation Test (mode-key preset). +- Tracking modes (`modeKey` presets in `ui/util/TrackingModes.kt` — suggested categories per mode). +- Quick-log config + Quick Log widget (`widget/QuickLogWidget.kt`). +- `ManageCategoryValues` screen (value editing). + +**Theme & colour** +- 12 named palettes x light/dark/system, HIGH_CONTRAST x2, BLUE_ORANGE, CUSTOM (user hues/argb + light/dark background), WCAG AAA variants for all 12. `AppTheme` names are DataStore keys — never rename. +- `ColorProfile` saved custom-palette slots. +- Category bubbles re-theme with the palette; calendar period dot = primary, ovulation = tertiaryContainer. + +**Other surfaces that read this data** (regression-test these when the model changes): Stats (`StatsViewModel`, `ChartDataComputer`), History, Home, `DayLogSheet`, Dashboard, data export (`data/export`), custom alarms (`CustomAlarm` linked to categories). + +--- + +## 4. Current state vs. the mock — gap analysis + +The handover's "as-built" understates the current app. Much of the "new" model **already exists**; treat this redesign as *unifying and re-skinning* more than *building from zero*. + +| Design concept | Already in the code | Genuinely new work | +|---|---|---| +| Per-category **icon** | `iconName` + `CategoryIcon` (20 icons) | Icon picker in the new create/edit flow (component reuse) | +| Colour as a **role** | `colorToken` = primary/secondary/tertiary **or** hex | **quaternary/quinary/senary** derived roles; `FIXED` = existing hex path | +| **Input type** | `categoryType`: default / numeric_slider / numeric_free / increment | **yes_no** and **time** types; a `MetricInput` façade | +| **Allow multiple / timeline** | `allowMultiple` + `trackAgainstTime` + `loggedAt` + `TimedIncrementSection` | Generalise timeline to all input types via one `Timeline` component | +| **Log with period** | `showInLogPeriod` pin | Reframe: the flag pins the category into the flow context on the unified screen | +| **Groups** | none | **`Group` entity + `groupId` on category** (the core new model) | +| **Unified log screen** | two separate screens (LogPeriod / LogCategory) | **one `LogScreen(date)`**; period becomes a day-state; header switcher to re-file | +| **What You Track home** | `ManageCategoriesScreen` (flat list) | Grouped/Ungrouped segmented view, group cards, add-to-group sheet | +| **Component library** | ad-hoc per-screen composables, duplicated input rendering | **~12 stateless primitives** in `ui/components/` (hard requirement) | +| **Alarms on category edit** | `CustomAlarm` system + `EditAlarm` screen exist | Surface reminders inside the category-edit flow | + +> **Note on the theme redesign (`GoFlo Theme Redesign.md`):** the palettes were largely applied in a prior change (see the `// Redesigned 2026-05` header in `Color.kt`), but some values in the code differ from the spec (e.g. Coral tertiary is a rose-magenta in code vs gold in the spec). Reconciling `Color.kt` exactly to that spec is a **separate, bounded task** and is intentionally *out of scope* for this logging plan. If desired, do it as its own PR with `wcag_check.py` as the gate. Do not fold it into a logging phase. + +--- + +## 5. Target architecture + +**Data model additions (additive):** +- New `Group` entity: `{ id, name, colorRole: String, defaultInputType: String, displayOrder: Int }`. New `GroupDao`, repository methods, and a migration adding the table. +- `TrackingCategory` gains `groupId: Long?` (nullable). No other column is required — `colorToken` already carries the role/hex, `categoryType` already carries the input type, `allowMultiple`/`showInLogPeriod`/`trackAgainstTime` already exist. +- **Colour inheritance rule (deviation from handover — see §6 Phase 2):** a category resolves its colour from its own `colorToken`; if that is the sentinel `"inherit"` and it has a `groupId`, it uses the group's `colorRole`. Existing categories keep their current `colorToken`, so **they do not turn grey** (the handover's "default to neutral surfaceVariant" would visually wipe existing category colours — we preserve them instead; only categories explicitly set to inherit-with-no-group render neutral). + +**Colour roles:** add `quaternary`/`quinary`/`senary` token strings resolved in `CategoryAppearance.kt` by deriving from the active `ColorScheme` (HSL, mirroring `buildCustomColorScheme`), provided via a `CompositionLocal` set up in `Theme.kt` so derivation happens once per theme. `FIXED` reuses the hex path. + +**Component library** (`ui/components/`, all stateless, parameterised, driven by a `role: Color`): `SectionHeader`, `ListCard`/`ListRow`, `StepScale`, `ChipToggle`/`ChipRow`, `ToneHero`, `SegmentedToggle`, `RolePicker`, `IconPicker`, `SwitchRow`, `Timeline`/`TimelineEntry`, `PrimarySaveBar`, and the `MetricInput(type, config, value, onChange)` façade that switches on input type. The log screen never branches on type itself — it renders `MetricInput`. + +**Unified log screen:** one `LogScreen(date)` composing the primitives. Off-period → mood hero leads (amber/secondary), flow not rendered, footer = quiet "Period started today" row. On-period → flow group slots in at top (blue/primary), mood compresses to a row, footer = filled status row ("Period ongoing · since · End"). Everything between is the same component in the same order. Title is a button → category-switch sheet (re-files the entry, preserves the entered value). + +--- + +## 6. The phases + +Each phase is a shippable PR. Order is deliberate: additive foundations first (roles, model, components), then the façade, then the big screen unification, then management, then creation/edit, then removal. **A later phase never starts by deleting an earlier path.** + +> **Each phase has a detailed build guide** in [`phases/`](phases/) with exact files, current line refs, code sketches, migration details, and acceptance + preservation checklists. Read the guide for your phase before starting; the summaries below are the index. +> +> - Phase 1 → [`phases/phase-1-color-roles.md`](phases/phase-1-color-roles.md) +> - Phase 2 → [`phases/phase-2-group-model.md`](phases/phase-2-group-model.md) +> - Phase 3 → [`phases/phase-3-component-library.md`](phases/phase-3-component-library.md) +> - Phase 4 → [`phases/phase-4-metricinput.md`](phases/phase-4-metricinput.md) +> - Phase 5 → [`phases/phase-5-unified-logscreen.md`](phases/phase-5-unified-logscreen.md) +> - Phase 6 → [`phases/phase-6-what-you-track.md`](phases/phase-6-what-you-track.md) +> - Phase 7 → [`phases/phase-7-create-edit-alarms.md`](phases/phase-7-create-edit-alarms.md) +> - Phase 8 → [`phases/phase-8-cleanup.md`](phases/phase-8-cleanup.md) + +### Phase 1 — Extended colour roles (additive, no migration) +**Goal:** categories can be assigned quaternary/quinary/senary in-theme roles and a fixed off-theme colour, all re-theming correctly. +**Do:** +- Add `QUATERNARY`, `QUINARY`, `SENARY` to `CategoryColor` (CategoryAppearance.kt) with keys `quaternary`/`quinary`/`senary`. +- Derive the three colours from the active `ColorScheme` (HSL hue-offset / interpolation, mirroring `buildCustomColorScheme`); provide them + their on-colours via a `CompositionLocal` installed in `Theme.kt`. Add branches to `String.toCategoryColor()` / `toCategoryOnColor()`. +- Extend the colour picker UI (in `ManageCategoriesScreen.kt`) to a `RolePicker` row offering the 6 roles + a Fixed-colour track (existing hex options). Match the "Pick a colour" mock (row 6). +**Data:** none (colorToken is free-form). +**Acceptance:** assign each new role to a category; it renders and re-themes across a light and a dark palette; on-colour passes contrast; `wcag_check.py` still green; a11y roles present on the picker (Role.RadioButton per the a11y table). Fixed colour does not change on theme switch. +**Preservation:** existing primary/secondary/tertiary/hex tokens unchanged; calendar/DayLogSheet still resolve colours. +**Risk:** low. Fully additive. + +### Phase 2 — Group data model (additive migration 23→24) +**Goal:** groups exist in the data layer with colour inheritance, no UI redesign yet. +**Do:** +- `Group` entity + `GroupDao` + repository methods (CRUD, list, reorder). Add to `@Database` entities, bump version to 24, add `MIGRATION_23_24` creating `groups` and adding nullable `groupId` to `tracking_categories` (`ALTER TABLE ADD COLUMN groupId INTEGER`). +- Implement the inheritance rule (§5): resolver falls back to the group's `colorRole` when `colorToken == "inherit"`. Add `"inherit"` handling; keep default `"secondary"` for existing rows. +- Minimal wiring: a category can be assigned/unassigned a group via repository (no full UI yet; a temporary entry point is fine, or defer UI to Phase 6). +**Data:** migration adds `groups` table + `groupId` column, all existing rows `groupId = null`, colours unchanged. +**Acceptance:** migration test (open a v23 DB, migrate, assert data intact + new column present); assigning a group changes an inherit-category's colour; existing categories keep their colours; app builds in CI. +**Preservation:** all category/log reads unaffected; Stats/History/export unchanged. +**Risk:** medium (schema). Mitigate with a migration test and the additive-column approach. + +### Phase 3 — Reusable component library (additive, no wiring) +**Goal:** the ~12 primitives exist with `@Preview`s, driven by params + a role colour. No screen consumes them yet (or only previews do). +**Do:** build `SectionHeader`, `ListCard`/`ListRow`, `StepScale`, `ChipToggle`/`ChipRow`, `ToneHero`, `SegmentedToggle`, `RolePicker` (from Phase 1, factored out), `IconPicker`, `SwitchRow`, `Timeline`/`TimelineEntry`, `PrimarySaveBar`. Each carries correct a11y (StepScale exposes as one control reporting "Flow, Medium, 3 of 4"; chips carry check + role; SwitchRow uses Role.Switch + stateDescription). Typography per the spec (Comfortaa on titles/hero words, applied explicitly; uppercase section labels 11sp +0.11em). +**Acceptance:** previews render each primitive in a light and dark theme at default and 200% font scale without clipping; `a11y_check.py` green. +**Preservation:** nothing removed; existing screens untouched. +**Risk:** low. + +### Phase 4 — MetricInput façade + new input types (Yes/No, Time) +**Goal:** one façade renders every input type; two new types added; the *category* log screen renders via the façade with no behaviour loss. +**Do:** +- Add `YES_NO` (`yes_no`) and `TIME` (`time`) to `CategoryType`. Decide storage: Yes/No stores "Yes"/"No" as the value label; Time stores `HH:mm` as the value label (reuse `loggedAt` semantics or the value string — document the choice). +- Build `MetricInput(type, config, value, onChange)` switching to `StepScale` (scale) / continuous `Slider` (numeric_free/genuine slider) / `Counter` (increment) / `SegmentedToggle` (yes_no) / `TimeField` (time) / `ChipRow` (default). Preserve the numeric_slider stepped behaviour, numeric_free unit + empty-blocks-save, increment ≤0-blocks-save, timed-increment timeline. +- Refactor `LogCategoryScreen` to render `MetricInput` instead of its inline `when`. Keep the screen otherwise identical (notes, save, date, delete). +**Data:** none (new categoryType strings are free-form; no migration). +**Acceptance:** every existing category type logs and edits exactly as before; Yes/No and Time categories can be created (via existing create flow or a temporary path) and logged; timed timeline still works; save-blocking rules intact. +**Preservation:** this is the riskiest for silent behaviour loss — walk every branch in map 01 §2 and confirm parity. Keep `LogPeriodScreen`'s `PinnedCategoryInput` working (it will be replaced in Phase 5; until then it may also delegate to `MetricInput`). +**Risk:** medium. Parity-driven. + +### Phase 5 — Unified `LogScreen` (the big one) +**Goal:** one screen logs a day; period is a state of the day; old screens still reachable until parity is signed off. +**Do:** +- Build `LogScreen(date)` + its ViewModel composing the primitives: mood hero (off-period, secondary) / flow group (on-period, primary), symptoms chips, grouped tracked-metric cards (a group of ≥2 → one `ListCard` of rows; a group of one → single-metric page), notes, period footer state row, `PrimarySaveBar`. Header title is a button → category/day switch sheet organised by group, tinted by role, preserving the entered value. +- Reproduce **all** period behaviour: flow mapping, symptom inline-add, pinned categories in the flow context, episode/continuation, remove-day, delete-episode, disable-period, unsaved-changes guard, the tracking-system fan-out, widget + reminder refresh. +- Route it in behind a flag / new route; keep `LogPeriod`/`LogCategory` routes alive. Entry points (calendar tap, FAB, quick log) can switch to `LogScreen` once parity holds. +**Data:** none expected (derives period state from `period_days`). +**Acceptance:** parity checklist against §3 Logging fully ticked; a day with and without an active period logs correctly and shows in Stats/History/widget; TalkBack traversal is top→bottom and the scale reads as one control. +**Preservation:** do not delete the old screens here. This phase adds; Phase 8 removes. +**Risk:** high. Largest surface. Consider sub-PRs (off-period first, then on-period, then header switcher). + +### Phase 6 — "What You Track" management home (Grouped/Ungrouped) +**Goal:** redesign `ManageCategoriesScreen` to the row-4 mock; groups are first-class in the UI. +**Do:** `SegmentedToggle` Grouped/Ungrouped; Grouped = group cards listing categories with inline "+ Add category to this group" and a "+ New group" pill; Ungrouped = neutral `surfaceVariant` categories each with "Add to group ›"; add-to-group sheet (adopt group role, or "New group…" pre-filled, past entries preserved); create-group flow. +**Acceptance:** create/edit groups; file/unfile categories; all existing management actions (archive, reorder, delete-with-history, system protection, tracking modes, quick-log, value editing) still reachable and working. +**Preservation:** `ManageCategoryValues`, tracking modes, quick-log config all intact. +**Risk:** medium. + +### Phase 7 — Category creation & edit (2-step) + scale settings + alarms +**Goal:** the row-5/row-6 create/edit flow. +**Do:** New category step 1 (name, `IconPicker`, `RolePicker`, input-type selector, allow-multiple `SwitchRow`, log-with-period `SwitchRow`) → step 2 shown only for Scale (min/max range, per-step word labels, allow-decimals). Edit adds a Reminders section (wire to the existing `CustomAlarm`/`EditAlarm` system) and a danger-zone delete-with-history row. Alarms appear on edit, not first creation. +**Acceptance:** create each input type end-to-end; scale step-2 persists range/labels/decimals; edit surfaces and schedules alarms via the existing system; delete removes category + history with confirmation. +**Preservation:** existing create/edit capabilities and `EditAlarm` scheduling behaviour unchanged; `categoryType` immutability rule honoured (or explicitly relaxed with a migration-safe plan if the design requires changing type post-creation — decide and document). +**Risk:** medium. + +### Phase 8 — Cleanup & removal (only after parity) +**Goal:** retire the superseded paths. +**Do:** once `LogScreen` is flagged on and parity signed off, remove `LogPeriodScreen`/`LogCategoryScreen` and the duplicated `PinnedCategoryInput`, consolidate the duplicated `DatePickerDialogWrapper`, drop dead routes. Final `a11y_check.py` + `wcag_check.py` + full regression pass over §3. Consolidate changelog fragments at the next release. +**Acceptance:** no references to removed screens; all §3 features still present via the new surfaces; CI green. +**Preservation:** this is the *checkpoint*, not a free-for-all: every removal is matched to a proven replacement. +**Risk:** medium (deletion). Gate on the parity checklist. + +--- + +## 7. Progress log (update in every phase PR) + +| Phase | Status | PR / commit | DB version after | Notes / deviations | +|---|---|---|---|---| +| 0 — Planning & groundwork | Done | this branch (`claude/design-handover-bdq9he`) | 23 | Subsystem maps stamped at `d07d947` / vc116. Theme-reconciliation split out as separate task. | +| 1 — Extended colour roles | Not started | | 23 | | +| 2 — Group data model | Not started | | (24) | Colour-inheritance deviates from handover's "grey by default" to preserve existing colours. | +| 3 — Component library | Not started | | | | +| 4 — MetricInput + Yes/No + Time | Not started | | | | +| 5 — Unified LogScreen | Not started | | | Consider sub-PRs. | +| 6 — What You Track home | Not started | | | | +| 7 — Create/edit + scale + alarms | Not started | | | Decide categoryType mutability. | +| 8 — Cleanup & removal | Not started | | | Gate on parity checklist. | + +--- + +## 8. Open decisions for a human (surface these, don't guess) + +1. **Colour default for ungrouped existing categories:** this plan keeps their current `colorToken` (no grey wipe), diverging from the handover's "neutral surfaceVariant by default". Confirm that is the desired behaviour, or accept a one-time optional "Organise your categories" nudge that offers (not forces) filing. +2. **`categoryType` mutability:** currently immutable after creation. The new edit flow implies changing type. Allowing it needs a value-migration story (e.g. scale↔count) or a documented "type is fixed once logged" constraint. Decide before Phase 7. +3. **Yes/No and Time storage encoding:** confirm storing as value-label strings ("Yes"/"No", "HH:mm") vs a dedicated column. Value-label keeps zero-migration; a column is cleaner for Stats. Decide before Phase 4. +4. **Theme-spec reconciliation** (`Color.kt` vs `GoFlo Theme Redesign.md`): in scope as a separate PR, or leave as-is? Not part of this logging plan. diff --git a/docs/design/logging-redesign/README.md b/docs/design/logging-redesign/README.md new file mode 100644 index 0000000..55e1494 --- /dev/null +++ b/docs/design/logging-redesign/README.md @@ -0,0 +1,22 @@ +# Logging & category-system redesign + +This folder holds the design handover for GoFlo's logging/category redesign and a phased plan to implement it across multiple sessions **without losing existing features**. + +## Start here + +1. **[`PLAN.md`](PLAN.md)** — the master plan. Golden rules, feature-preservation inventory, gap analysis, the 8 phases with acceptance criteria, a progress log, and open decisions. A fresh session should read this first. +2. **[`phases/`](phases/)** — one detailed build guide per phase (exact files, current line refs, code sketches, migration/data details, acceptance + preservation checklists, gotchas). Read the guide for your phase after the master plan. +3. **[`subsystem-maps/`](subsystem-maps/)** — stamped groundwork so you don't have to re-explore the codebase: + - [`01-logging-screens.md`](subsystem-maps/01-logging-screens.md) — the two current log screens, routes, input controls, save flows. + - [`02-category-data-model.md`](subsystem-maps/02-category-data-model.md) — entities, DB version + migration chain, DAOs, repository API. + - [`03-theme-color-machinery.md`](subsystem-maps/03-theme-color-machinery.md) — `colorSchemeFor`, palettes, colour-role resolution hook. + Each map is stamped with the **commit + build** it was verified against (`d07d947`, versionCode 116, DB v23) and a staleness check. If HEAD has moved, run the staleness check before trusting line numbers. +4. **[`handover/`](handover/)** — the original design bundle (verbatim): + - `README.md` — design intent + the reusable-component-library requirement. + - `GoFlo Theme Redesign.md` — the 12-palette colour spec (largely already applied; reconciliation is a separate task). + - `Log Screens Rethought.html` — the pannable mockup canvas (open in a browser). + - `screens/01-06-row.png` — rendered reference of each design row. + +## The one-paragraph version + +A category stores a colour **role** and an optional **group**; the group owns the role and a default input type. From that: three extra in-theme colour roles (quaternary/quinary/senary) plus a fixed off-theme colour; a small library of reusable Compose primitives that every screen is built from; and one unified log screen where a running period is a *state of the day*, not a separate screen. Most of the per-category model (icons, input types, allow-multiple, timeline, show-in-period) **already exists** in the code — see the gap analysis in `PLAN.md` §4. The work is unification and a few genuinely new pieces (the group model, the extra roles, the unified screen, the component library, Yes/No + Time input types), done additively so nothing currently shipping is lost. diff --git a/docs/design/logging-redesign/handover/GoFlo Theme Redesign.md b/docs/design/logging-redesign/handover/GoFlo Theme Redesign.md new file mode 100644 index 0000000..771061f --- /dev/null +++ b/docs/design/logging-redesign/handover/GoFlo Theme Redesign.md @@ -0,0 +1,504 @@ +# GoFlo theme redesign — handoff spec + +**Target file:** `app/src/main/java/com/mapgie/goflo/ui/theme/Color.kt` +**Scope:** replace the colour values of all 12 named palettes (the 3 "classic" and 9 "fun" / "bold" themes). Leave `HighContrastLight`, `HighContrastDark`, and `BlueOrange` alone — those are accessibility schemes with constraints of their own. +**Do not change:** the `AppTheme` enum names (they're persisted to DataStore), the `ThemeGroup` structure, the function signature of `colorSchemeFor()`, or `Typography.kt`. +**Do change:** every `previewArgb` value to match the new primary of its scheme. + +--- + +## Why this redesign + +The current palettes have great names — Summer Candy, All-Night Disco Party, Dragon Fire — but the colours don't deliver. Specific problems being fixed: + +| Palette | Problem in current values | +|---|---| +| **Coral** | Primary, secondary, tertiary are all red-orange. No hue contrast. | +| **Teal** | Primary, secondary, tertiary are all desaturated teals/blue-greys. | +| **Sage** | Same — three greens. | +| **Summer Candy** | Burnt-orange #BF360C + deep teal reads as autumn stew, not sherbet. | +| **Beach Vibes** | Terra cotta secondary is wrong climate. No sun, no sand. | +| **Peach Melba** | Brick #BF360C + magenta-wine #880E4F + forest green has no peach and no cream. | +| **All-Night Disco Party** | Tertiary is mustard #8B6A00. Discos are *neon*. | +| **Metal Chick** | All three colours are grey. No edge. | +| **Whimsy Whispers** | Secondary is a violet-grey that drags the whole palette into the mud. | +| **Colour Me Happy** | Two oranges fighting + a lime. Not the joyful primary chord the name promises. | +| **Dragon Fire** | Tertiary is electric indigo. Indigo is not on fire. | +| **Midnight Neon** light | Has no neon — it's lavender, teal and olive. | + +## Design principles applied + +1. **Three real hues per palette.** Primary, secondary and tertiary now live in different parts of the colour wheel — not three shades of the same orange. Each palette has a deliberate three-note chord. +2. **Backgrounds tinted to character.** Surface and surfaceVariant carry the lightest tint of the palette's dominant hue family, not a generic near-white. +3. **Containers stay in family.** primaryContainer is a desaturated tint of primary; same for secondary/tertiary. This was already the convention — preserved. +4. **WCAG AA preserved.** All primary/onPrimary, container/onContainer, and outline/surfaceVariant pairs target ≥4.5:1 for text and ≥3:1 for UI elements. Re-run `wcag_check.py` after applying. +5. **Names deliver.** Every palette now matches the noun in its name. Summer Candy is candy. Dragon Fire is fire. Midnight Neon glows in both modes. + +--- + +## The palettes + +Each section gives the **intent**, the **three-note chord**, then the full Material 3 token sets for light and dark. + +### 1. Coral *(classic)* + +**Intent:** Living coral reef — vivid coral against lagoon water under a high sun. +**Chord:** coral red · lagoon teal · sun-bleached gold. + +**Light** +``` +primary #BC4D3F onPrimary #FFFFFF +primaryContainer #FFDAD2 onPrimaryContainer #3F0900 +secondary #007E7A onSecondary #FFFFFF +secondaryContainer #B4ECE7 onSecondaryContainer #002523 +tertiary #956800 onTertiary #FFFFFF +tertiaryContainer #FFE08D onTertiaryContainer #251A00 +background #FFF6F2 onBackground #221915 +surface #FFF6F2 onSurface #221915 +surfaceVariant #F4DDD5 onSurfaceVariant #524340 +outline #856E68 +``` +`previewArgb` = `0xFFBC4D3FL` + +**Dark** +``` +primary #FFB4A4 onPrimary #5C1600 +primaryContainer #7C2A18 onPrimaryContainer #FFDAD0 +secondary #5BD8D0 onSecondary #003734 +secondaryContainer #00504C onSecondaryContainer #B4EEE9 +tertiary #FFD787 onTertiary #3D2D00 +tertiaryContainer #574200 onTertiaryContainer #FFE08D +background #1C110E onBackground #F1DED8 +surface #1C110E onSurface #F1DED8 +surfaceVariant #523F3A onSurfaceVariant #D7C2BC +outline #A28B86 +``` +`previewArgb` = `0xFFFFB4A4L` (unchanged) + +--- + +### 2. Teal *(classic — enum still named TURQUOISE)* + +**Intent:** Clear lagoon water with warm clay and ink-blue as counterpoints. +**Chord:** deep teal · terra-cotta · indigo. + +**Light** +``` +primary #00747C onPrimary #FFFFFF +primaryContainer #9DEFF6 onPrimaryContainer #002023 +secondary #B3572A onSecondary #FFFFFF +secondaryContainer #FFDBC8 onSecondaryContainer #381300 +tertiary #4B5BAC onTertiary #FFFFFF +tertiaryContainer #DEE1FF onTertiaryContainer #00115A +background #F0FBFC onBackground #161C1D +surface #F0FBFC onSurface #161C1D +surfaceVariant #DAE4E5 onSurfaceVariant #3F4949 +outline #6F7979 +``` +`previewArgb` = `0xFF00747CL` + +**Dark** +``` +primary #80D5DB onPrimary #003739 +primaryContainer #004F52 onPrimaryContainer #9DEFF6 +secondary #FFB28F onSecondary #5A1B00 +secondaryContainer #8C3B16 onSecondaryContainer #FFDBC8 +tertiary #BCC2FF onTertiary #1C257B +tertiaryContainer #353F93 onTertiaryContainer #DEE1FF +background #0E1818 onBackground #E0E3E3 +surface #0E1818 onSurface #E0E3E3 +surfaceVariant #3F4949 onSurfaceVariant #BEC8C9 +outline #8B9595 +``` +`previewArgb` = `0xFF80D5DBL` (unchanged) + +--- + +### 3. Sage *(classic — enum still named GREEN)* + +**Intent:** Herb garden at dawn — soft green with warm earth and honey. +**Chord:** sage green · terra-cotta clay · honey gold. + +**Light** +``` +primary #4F7D2B onPrimary #FFFFFF +primaryContainer #C8F09F onPrimaryContainer #112100 +secondary #B5532A onSecondary #FFFFFF +secondaryContainer #FFDBC8 onSecondaryContainer #3A1100 +tertiary #8C6212 onTertiary #FFFFFF +tertiaryContainer #FFDF9C onTertiaryContainer #2A1D00 +background #F7FBEE onBackground #1A1C16 +surface #F7FBEE onSurface #1A1C16 +surfaceVariant #DEE5D2 onSurfaceVariant #424A3B +outline #72796A +``` +`previewArgb` = `0xFF4F7D2BL` + +**Dark** +``` +primary #ACD888 onPrimary #1B3900 +primaryContainer #2D530B onPrimaryContainer #C8F09F +secondary #FFB28F onSecondary #5A1B00 +secondaryContainer #8B3914 onSecondaryContainer #FFDBC8 +tertiary #F4C16D onTertiary #422C00 +tertiaryContainer #5E4300 onTertiaryContainer #FFDF9C +background #14170F onBackground #E2E4D7 +surface #14170F onSurface #E2E4D7 +surfaceVariant #424A3B onSurfaceVariant #C2C9B6 +outline #919888 +``` +`previewArgb` = `0xFFACD888L` + +--- + +### 4. Summer Candy + +**Intent:** Bubblegum, sherbet and slushie. Sugary, saturated, joyful — not autumnal. +**Chord:** bubblegum raspberry · mint aqua · lemon gold. + +**Light** +``` +primary #D81B60 onPrimary #FFFFFF +primaryContainer #FFD8E5 onPrimaryContainer #40001B +secondary #007F76 onSecondary #FFFFFF +secondaryContainer #6FF5E5 onSecondaryContainer #00201D +tertiary #9A6700 onTertiary #FFFFFF +tertiaryContainer #FFE08D onTertiaryContainer #271B00 +background #FFF7FA onBackground #1E1316 +surface #FFF7FA onSurface #1E1316 +surfaceVariant #F4DDE5 onSurfaceVariant #534249 +outline #856D74 +``` +`previewArgb` = `0xFFD81B60L` + +**Dark** +``` +primary #FFB1CA onPrimary #650033 +primaryContainer #8E0049 onPrimaryContainer #FFD8E5 +secondary #6DDFD0 onSecondary #003A35 +secondaryContainer #00514B onSecondaryContainer #6FF5E5 +tertiary #FFCD66 onTertiary #3F2D00 +tertiaryContainer #5A4200 onTertiaryContainer #FFE08D +background #1D1014 onBackground #ECDFE3 +surface #1D1014 onSurface #ECDFE3 +surfaceVariant #534249 onSurfaceVariant #D6C1C8 +outline #A39095 +``` +`previewArgb` = `0xFFFFB1CAL` + +--- + +### 5. Beach Vibes + +**Intent:** Sand, sea, sky and sun. Bright, breezy, holiday-postcard. +**Chord:** clear sea blue · sun-gold sand · sea-foam green. (Terra cotta is gone.) + +**Light** +``` +primary #1265AF onPrimary #FFFFFF +primaryContainer #D5E3FF onPrimaryContainer #001C3D +secondary #9A6800 onSecondary #FFFFFF +secondaryContainer #FFE08D onSecondaryContainer #251A00 +tertiary #297F6C onTertiary #FFFFFF +tertiaryContainer #A8F0DC onTertiaryContainer #002019 +background #F8FAFE onBackground #181C22 +surface #F8FAFE onSurface #181C22 +surfaceVariant #E1E3EE onSurfaceVariant #444751 +outline #757782 +``` +`previewArgb` = `0xFF1265AFL` + +**Dark** +``` +primary #A8C8FF onPrimary #002E66 +primaryContainer #00478A onPrimaryContainer #D5E3FF +secondary #F4C16D onSecondary #422C00 +secondaryContainer #5E4300 onSecondaryContainer #FFE08D +tertiary #84D7BC onTertiary #003828 +tertiaryContainer #00513B onTertiaryContainer #A8F0DC +background #101820 onBackground #DDE3EB +surface #101820 onSurface #DDE3EB +surfaceVariant #444751 onSurfaceVariant #C5C7D2 +outline #93959F +``` +`previewArgb` = `0xFFA8C8FFL` + +--- + +### 6. Peach Melba + +**Intent:** The Escoffier dessert — peach halves, raspberry coulis, vanilla cream. Delicate, soft, dessert-shop. +**Chord:** apricot peach · raspberry · vanilla-caramel cream. + +**Light** +``` +primary #B35535 onPrimary #FFFFFF +primaryContainer #FFDBC9 onPrimaryContainer #3A1300 +secondary #B53369 onSecondary #FFFFFF +secondaryContainer #FFD9E3 onSecondaryContainer #3F0024 +tertiary #8A6926 onTertiary #FFFFFF +tertiaryContainer #FFE2A0 onTertiaryContainer #2A1F00 +background #FFF6F0 onBackground #201813 +surface #FFF6F0 onSurface #201813 +surfaceVariant #F4DED1 onSurfaceVariant #534439 +outline #867262 +``` +`previewArgb` = `0xFFB35535L` + +**Dark** +``` +primary #FFB694 onPrimary #571F00 +primaryContainer #7A3015 onPrimaryContainer #FFDBC9 +secondary #FFB1C9 onSecondary #65003A +secondaryContainer #8C1F4F onSecondaryContainer #FFD9E3 +tertiary #F4C16D onTertiary #432D00 +tertiaryContainer #5F4300 onTertiaryContainer #FFE2A0 +background #1E1410 onBackground #EDE0D8 +surface #1E1410 onSurface #EDE0D8 +surfaceVariant #534439 onSurfaceVariant #D7C3B5 +outline #A59185 +``` +`previewArgb` = `0xFFFFB694L` + +--- + +### 7. All-Night Disco Party + +**Intent:** Mirrorball, strobes, glitter. Actual neon, not mustard. +**Chord:** hot magenta · electric violet · glitter gold. + +**Light** +``` +primary #C1127A onPrimary #FFFFFF +primaryContainer #FFD6EA onPrimaryContainer #3D003C +secondary #6E1FB5 onSecondary #FFFFFF +secondaryContainer #ECDCFF onSecondaryContainer #270060 +tertiary #966900 onTertiary #FFFFFF +tertiaryContainer #FFE08C onTertiaryContainer #2A1D00 +background #FFF7FB onBackground #1E1020 +surface #FFF7FB onSurface #1E1020 +surfaceVariant #F0DEEC onSurfaceVariant #4F4452 +outline #807385 +``` +`previewArgb` = `0xFFC1127AL` + +**Dark** — full neon mode. +``` +primary #FF66B8 onPrimary #5C0040 +primaryContainer #890062 onPrimaryContainer #FFD6EA +secondary #D5B2FF onSecondary #3F0090 +secondaryContainer #5800B0 onSecondaryContainer #ECDCFF +tertiary #FFD350 onTertiary #3A2A00 +tertiaryContainer #523D00 onTertiaryContainer #FFE08C +background #170820 onBackground #ECDAEC +surface #170820 onSurface #ECDAEC +surfaceVariant #4F4452 onSurfaceVariant #D2C2D2 +outline #A092A2 +``` +`previewArgb` = `0xFFFF66B8L` + +--- + +### 8. Metal Chick + +**Intent:** Black leather and chrome with one shocking lipstick accent. Edge, attitude. +**Chord:** gunmetal · crimson-lipstick · brushed chrome. + +**Light** +``` +primary #2E2E3A onPrimary #FFFFFF +primaryContainer #DCDBE9 onPrimaryContainer #0E0E1C +secondary #C8235A onSecondary #FFFFFF +secondaryContainer #FFD8E0 onSecondaryContainer #3F001A +tertiary #6A6A78 onTertiary #FFFFFF +tertiaryContainer #E3E2F0 onTertiaryContainer #1C1C2A +background #F4F3F8 onBackground #1B1B22 +surface #F4F3F8 onSurface #1B1B22 +surfaceVariant #E2E0EC onSurfaceVariant #45434E +outline #76737E +``` +`previewArgb` = `0xFF2E2E3AL` + +**Dark** +``` +primary #C7C5D6 onPrimary #2F2D3D +primaryContainer #454354 onPrimaryContainer #DCDBE9 +secondary #FF8FB0 onSecondary #5C0028 +secondaryContainer #88133E onSecondaryContainer #FFD8E0 +tertiary #9D9DAC onTertiary #2F2F3E +tertiaryContainer #4A4A58 onTertiaryContainer #E3E2F0 +background #0E0E13 onBackground #E5E3ED +surface #0E0E13 onSurface #E5E3ED +surfaceVariant #45434E onSurfaceVariant #C8C5D0 +outline #918E99 +``` +`previewArgb` = `0xFFC7C5D6L` + +--- + +### 9. Whimsy Whispers + +**Intent:** Fairy-tale soft pastels — but actually saturated, not muddy. Soft-focus, dreamy. +**Chord:** lavender · blush rose · spearmint. + +**Light** +``` +primary #6E5DC4 onPrimary #FFFFFF +primaryContainer #E5DFFF onPrimaryContainer #1A0067 +secondary #BD4878 onSecondary #FFFFFF +secondaryContainer #FFD9E4 onSecondaryContainer #3D0024 +tertiary #2B7F68 onTertiary #FFFFFF +tertiaryContainer #A6F0D6 onTertiaryContainer #002016 +background #FBF8FF onBackground #1C1A24 +surface #FBF8FF onSurface #1C1A24 +surfaceVariant #E5DFF0 onSurfaceVariant #49454F +outline #7A7580 +``` +`previewArgb` = `0xFF6E5DC4L` + +**Dark** +``` +primary #C7BEFF onPrimary #260092 +primaryContainer #3D2EAE onPrimaryContainer #E5DFFF +secondary #FFB1C6 onSecondary #650033 +secondaryContainer #8E1F50 onSecondaryContainer #FFD9E4 +tertiary #80DAB8 onTertiary #003828 +tertiaryContainer #00513B onTertiaryContainer #A6F0D6 +background #16131F onBackground #E6E1F0 +surface #16131F onSurface #E6E1F0 +surfaceVariant #49454F onSurfaceVariant #CBC5D0 +outline #96909F +``` +`previewArgb` = `0xFFC7BEFFL` + +--- + +### 10. Colour Me Happy + +**Intent:** Kindergarten primary colours — tomato, sky, grass. Joyful, optimistic, clear. +**Chord:** tomato red · sky blue · grass green. + +**Light** +``` +primary #D63A26 onPrimary #FFFFFF +primaryContainer #FFDAD2 onPrimaryContainer #410000 +secondary #1872BD onSecondary #FFFFFF +secondaryContainer #D1E4FF onSecondaryContainer #001D36 +tertiary #428129 onTertiary #FFFFFF +tertiaryContainer #C6F0A1 onTertiaryContainer #0F2200 +background #FFFCF5 onBackground #1F1A14 +surface #FFFCF5 onSurface #1F1A14 +surfaceVariant #EFE3D5 onSurfaceVariant #4E443A +outline #80766A +``` +`previewArgb` = `0xFFD63A26L` + +**Dark** +``` +primary #FFB4A4 onPrimary #5C1500 +primaryContainer #882000 onPrimaryContainer #FFDAD2 +secondary #A0CAFF onSecondary #002F66 +secondaryContainer #00497D onSecondaryContainer #D1E4FF +tertiary #ACD688 onTertiary #1A3900 +tertiaryContainer #2D530B onTertiaryContainer #C6F0A1 +background #1A1612 onBackground #EDE0D2 +surface #1A1612 onSurface #EDE0D2 +surfaceVariant #4E443A onSurfaceVariant #D2C4B5 +outline #9F9184 +``` +`previewArgb` = `0xFFFFB4A4L` + +--- + +### 11. Dragon Fire + +**Intent:** Smaug's hoard burning — embers, molten metal, gold leaf. No more random indigo. +**Chord:** ember red · molten orange · furnace gold. + +**Light** +``` +primary #B0181F onPrimary #FFFFFF +primaryContainer #FFCFCC onPrimaryContainer #3F0001 +secondary #C04A0E onSecondary #FFFFFF +secondaryContainer #FFDBC9 onSecondaryContainer #3B1100 +tertiary #996800 onTertiary #FFFFFF +tertiaryContainer #FFDE8E onTertiaryContainer #2D1F00 +background #FFF8F4 onBackground #1E140F +surface #FFF8F4 onSurface #1E140F +surfaceVariant #F3DED5 onSurfaceVariant #524338 +outline #87715F +``` +`previewArgb` = `0xFFB0181FL` + +**Dark** +``` +primary #FF8A82 onPrimary #680000 +primaryContainer #960000 onPrimaryContainer #FFDAD4 +secondary #FFB28F onSecondary #561A00 +secondaryContainer #8E2D00 onSecondaryContainer #FFDBC9 +tertiary #FFCD66 onTertiary #422C00 +tertiaryContainer #5E4300 onTertiaryContainer #FFDE8E +background #1C0907 onBackground #F1DDD7 +surface #1C0907 onSurface #F1DDD7 +surfaceVariant #523F38 onSurfaceVariant #D7C2B8 +outline #A28D81 +``` +`previewArgb` = `0xFFFF8A82L` (unchanged) + +--- + +### 12. Midnight Neon + +**Intent:** Cyberpunk arcade. Glows in **both** modes — light mode is "neon sign at dusk", dark is "Akira after midnight". +**Chord:** neon magenta · electric cyan · acid lime. + +**Light** +``` +primary #C5128A onPrimary #FFFFFF +primaryContainer #FFD6E8 onPrimaryContainer #3D003C +secondary #006D90 onSecondary #FFFFFF +secondaryContainer #B8E9FF onSecondaryContainer #001E2F +tertiary #4C7A0E onTertiary #FFFFFF +tertiaryContainer #C2F088 onTertiaryContainer #0F2300 +background #FBF7FF onBackground #161020 +surface #FBF7FF onSurface #161020 +surfaceVariant #EDDEEC onSurfaceVariant #4D4350 +outline #7F7484 +``` +`previewArgb` = `0xFFC5128AL` + +**Dark** — go full glow. +``` +primary #FF55C8 onPrimary #5C0048 +primaryContainer #870068 onPrimaryContainer #FFD6EA +secondary #5EE0FF onSecondary #00374A +secondaryContainer #00516C onSecondaryContainer #B8E9FF +tertiary #C2F052 onTertiary #1D3300 +tertiaryContainer #2D4900 onTertiaryContainer #C2F088 +background #0A0A18 onBackground #EDE5FA +surface #0A0A18 onSurface #EDE5FA +surfaceVariant #4D4350 onSurfaceVariant #D0C3D0 +outline #9D90A4 +``` +`previewArgb` = `0xFFFF55C8L` + +--- + +## Implementation notes for Claude Code + +1. **Edit `Color.kt` in place.** Replace the colour `Color(0x…)` values inside each of these schemes: + `CoralLight`, `CoralDark`, `TurquoiseLight`, `TurquoiseDark`, `GreenLight`, `GreenDark`, `SummerCandyLight`, `SummerCandyDark`, `BeachVibesLight`, `BeachVibesDark`, `PeachMelbaLight`, `PeachMelbaDark`, `DiscoLight`, `DiscoDark`, `MetalChickLight`, `MetalChickDark`, `WhimsyLight`, `WhimsyDark`, `ColourHappyLight`, `ColourHappyDark`, `DragonFireLight`, `DragonFireDark`, `MidnightNeonLight`, `MidnightNeonDark`. +2. **Update `previewArgb`** on the matching `AppTheme` enum entries (and the corresponding `*_SYSTEM` rows — light system rows use the light primary, dark system rows the dark primary). Specifically the `*_SYSTEM` previews should match the LIGHT primary so the settings chip stays recognisable in either system mode — though current code uses dark primaries for system previews; keep whichever you prefer but be consistent. +3. **Leave alone:** `HighContrastLight`, `HighContrastDark`, `BlueOrange`, the `ThemeGroup` enum, the `AppTheme` *names*, and `colorSchemeFor()`. +4. **Run `wcag_check.py`** before committing. The values were chosen to clear 4.5:1 (text) / 3:1 (UI) but verify on the real script. If any pair fails by <0.2, darken/lighten the offending hex by ~3 % luminance toward black or white; the target hue should not shift. +5. **Don't change** the comment headers above each scheme (e.g. `// ── Classic light color schemes ──`) — keep the file structure recognisable. +6. **Drop the obsolete contrast comments** like `// Coral: primary darkened to #C15542 (WCAG AA audit 2026-05-23…)` — they refer to old values. Add a single line above each section: `// Redesigned 2026-05 — see GoFlo Theme Redesign.md`. +7. **CHANGELOG entry suggestion:** `Themes: rebuilt all 12 named palettes so each name's three-hue chord actually delivers (e.g. Summer Candy is now sherbet, Dragon Fire is now fire, Midnight Neon glows in both modes). Accessibility schemes unchanged.` + +## Quick QA checklist after Claude Code applies it + +- [ ] App builds; no missing references. +- [ ] Settings → Theme picker shows updated swatches next to each row. +- [ ] Open one light and one dark variant of each palette and verify the calendar (period dot uses primary, ovulation uses tertiaryContainer) reads correctly. +- [ ] FAB (uses primaryContainer / onPrimaryContainer) has clear text contrast in every theme. +- [ ] `wcag_check.py` passes. diff --git a/docs/design/logging-redesign/handover/Log Screens Rethought.html b/docs/design/logging-redesign/handover/Log Screens Rethought.html new file mode 100644 index 0000000..a8f6e5b --- /dev/null +++ b/docs/design/logging-redesign/handover/Log Screens Rethought.html @@ -0,0 +1,676 @@ + + + + + +GoFlo — logging screens, rethought + + + + + +
+
Today — as built
+
Every input is a different shape. Sliders shout a number nobody reads, the track's two-tone fill looks like a progress bar, and each control floats in its own mauve slab.
+
+
9:41▮▮▮
+

Log Period

+
+

Dates

Start: Aug 6, 2026
End: Ongoing
+

Flow

3
14
+

Symptoms

CrampsHeadacheBloatingFatigueBack PainMood SwingsBleeding (non-period)nausea+ Add
+

Rage

0
0Drag to set a value5
+
+
+
+ +
+
Log Period — rethought
+
One surface language: facts go in a list, judgements go on a tap-scale, states go on chips. Scales become five discrete rising steps — tappable, labelled in words, no numeral needed and no drag required.
+
+
9:41▮▮▮
+

Log period

Day 4 · started Aug 6
+
+
Dates
+
+
StartedAug 6, 2026
+
EndedStill ongoing
+
+ +
FlowMedium
+
+
+ + + + +
+
+ +
Symptoms2 today
+
+CrampsNauseaHeadacheBloatingFatigueBack painMood swings+ Add +
+ +
RageNot today
+
+
+ + + + + + +
+
CalmVolcanic
+
+
+
Save log
+
+
+ +
+
Single-metric log — rethought
+
The old screen said its own name three times and left two thirds of the page empty. Here the metric is the page: one tonal hero holds the reading, the date sits inline, notes fill the space that was blank.
+
+
9:41▮▮▮
+

Nose symptoms

Sat, Aug 22 · change
+
+
+
Barely noticeable
How bad is it right now?
+
+
+ + + + + +
+
Barely thereUnbearable
+
+
+
NotesOptional
+
Anything worth remembering later…
+
Last 7 days
+
+ + + + + + + +
+
+
Save
+
+
+ +
+
What changed, and why
+
Four rules that make every log screen fall into place.
+
+
1 · Kill the sliderA 1–5 rating is not a continuous quantity. Five discrete steps, each a real tap target, rising in height so magnitude is visible at a glance. Nobody has to drag, and the giant blue numeral disappears — the selected step is the readout.
+
2 · Words over numbers"Medium", "Still ongoing", "Barely noticeable". Numbers stay as small step captions where a scale genuinely is numeric.
+
3 · One card per idea, not per fieldStart and end dates are one list card with rows, not two floating outlined pills. Section titles become small uppercase labels with the current value on the right, so the screen can be read without touching it.
+
4 · Selected state is tonal, not outlinedChips fill with primaryContainer and gain a check when on; unselected chips stay hairline. Warm secondary is reserved for the mood-ish scales, so blue always means "flow / medical" and amber means "how I felt".
+
+
+ +
+
Daily log — no period running
+
Same vocabulary, different content: with no cycle active the page leads with how the day felt, keeps period out of the form, and offers starting one as a single quiet action at the bottom.
+
+
9:41▮▮▮
+

Log today

Sat, Aug 22 · cycle day 19
+
+
+
Pretty good
How did today feel overall?
+
+
+ + + + + +
+
RoughGreat
+
+
+
Symptoms1 today
+
+HeadacheCrampsBloatingFatigueNausea+ Add +
+
Tracking3 metrics
+
+
Nose symptomsBarely noticeable
+
RageNot today
+
SleepAdd
+
+
NotesOptional
+
Anything worth remembering later…
+
Period started today
+
+
Save today
+
+
+ +
+
Same screen, period running
+
Nothing is rearranged — a single flow group slots in at the top and the footer swaps to the period's own state. The form the user learned on quiet days stays the form they use on heavy ones.
+
+
9:41▮▮▮
+

Log today

Sat, Aug 22 · period day 4
+
+
FlowMedium
+
+
+ + + + +
+
+
+
Rough
Day overall
+
+
+ + + + + +
+
+
+
Symptoms2 today
+
+CrampsNauseaHeadacheBloatingFatigue+ Add +
+
Tracking3 metrics
+
+
Nose symptomsBarely noticeable
+
RageLevel 3
+
+
NotesOptional
+
Anything worth remembering later…
+
Period ongoing · since Aug 6End
+
+
Save today
+
+
+ +
+
The rule that ties them together
+
There is one log screen, not two. Period is a state the day is in, not a separate destination.
+
+
Off-period → mood leadsWith no cycle running, "how did today feel" is the only thing worth asking first. It gets the amber hero; flow simply isn't rendered.
+
On-period → flow leadsFlow takes the top slot in blue and the mood hero compresses to a single row. Everything below it — symptoms, tracked metrics, notes — is byte-for-byte the same component in the same order.
+
Start / end lives in the footerOff-period it's a hairline row, "Period started today". On-period it's a filled primaryContainer status row with End on the right. One row, two states — no separate "Log Period" screen to navigate to.
+
Colour carries meaningBlue = cycle and body facts. Amber = subjective feeling. A user glancing at a saved day can tell what kind of information they're looking at before reading a word.
+
+
+ +
+
Settings → Categories & groups
+
This is where the colour language is authored, not hardcoded. A group owns a colour role (primary / secondary / tertiary) — never a raw hex — so switching theme below re-colours every group at once.
+
+
9:41▮▮▮
+

Categories

Grouping & colour
+
+
App themeBeach Vibes
+
+
+
+
+
+
+
+
+
Your groups+ New group
+
+
Body & cycle
Flow · Cramps · Nose · primary
+
How I felt
Mood · Rage · secondary
+
Environment
Weather · Rainfall · Dampness · tertiary
+
Ungrouped
Sleep · Water — neutral until filed
+
+
+
+
+ +
+
A group is one card with rows
+
Weather, rainfall and dampness were three separate logs. Filed under one Environment group they render as a single tertiary-coloured card — each metric a row with its own scale behind the chevron.
+
+
9:41▮▮▮
+

Environment

Sat, Aug 22 · tap title to switch
+
+
Environment3 metrics
+
+
WeatherOvercast
+
RainfallLight
+
DampnessNot set
+
+
RainfallLight
+
+
+ + + + +
+
NotesOptional
+
Anything worth remembering later…
+
+
Save
+
+
+ +
+
Tap the header to switch
+
Logged the wrong thing? The title is a button. It opens a sheet of every category, organised by group and tinted by the group's role — so the colour you're about to log in is visible before you commit.
+
+
9:41▮▮▮
+

Log period

Switching category…
+
+
+
Body & cycle
+
Log period
+
Nose symptoms
+
How I felt
+
Overall mood
+
Rage
+
Environment
+
Weather · Rainfall · Dampness
+
+
The current entry is preserved — switching only changes what the same saved day is filed under.
+
+
+
+ +
+
The data model behind it
+
One nullable column carries all of this — and nothing existing has to be rewritten.
+
+
Category → groupId (nullable)Subjective vs objective isn't a hidden flag — it's which group you filed it in. A Group stores {name, colourRole, defaultScale}. The role is primary/secondary/tertiary, never a hex, so the theme picker recolours everything for free.
+
Migration is additiveAdd the column, default null. Every existing category keeps working, rendered neutral (surfaceVariant). A one-time optional "Organise your categories" nudge lets people file them. No destructive rewrite, no crash surface.
+
Grouping ≥2 → one cardA group with several categories renders as a single card of rows (Environment). A group of one renders as today's single-metric page. Same component, count-driven.
+
Header switch = re-file, not re-enterSwitching category from the title changes the entry's categoryId; the value the user already picked is preserved. Cheap because logging is one screen driven by the category, not per-category screens.
+
+
+ +
+
What You Track — Grouped
+
The management home. A Grouped / Ungrouped toggle switches the view; each group is a card you can add a category straight into, and one action makes a whole new group.
+
+
9:41▮▮▮
+

What you track

8 categories · 3 groups
+
+
+
+
Body & cycleEdit
+
Flow
+
Cramps
+
Nose symptoms
+
+ Add category to this group
+
+
+
How I feltEdit
+
Overall mood
+
Rage
+
+ Add category to this group
+
+
+
EnvironmentEdit
+
Weather
+
Rainfall
+
+ Add category to this group
+
+
+ New group
+
+
+
+ +
+
What You Track — Ungrouped
+
The same page, other tab. Loose categories render neutral; each offers Add to group to file it into an existing group (inheriting that group's colour), plus a new category can be made right here.
+
+
9:41▮▮▮
+

What you track

2 ungrouped
+
+
+
+
SleepAdd to group ›
+
WaterAdd to group ›
+
+
Ungrouped categories still log — they just show in surfaceVariant until you file them.
+
+ New category
+
+
+
+ +
+
New category — full option set
+
Name, icon, and colour in one pass, then how it behaves: input type, whether it can be logged many times a day, and whether it rides along with a period. Colour left as roles for now — we'll revisit.
+
+
9:41▮▮▮
+

New category

Step 1 of 2 · basics
Next ›
+
+
Name
Headache severity
+
Icon
+
+ +
+
Colourrole — revisit
+
+Body & cycle +How I felt +Environment +
+
Input type
+
+Scale 1–5 +Slider +Count +Yes / no +Time +
+
A 1–5 scale is discrete steps. A slider is a genuine continuous quantity (weight, temp). Different components — one control decides which renders.
+
Allow multiple per day
Log it several times — keeps the time of each
+
Log with period
Surface it in the flow context while a period runs
+
Scale, range and alarms live on step 2 — the first screen stays short.
+
+
+
+ +
+
Add to group (from Ungrouped)
+
Tapping Add to group on a loose category opens this — pick a group and it adopts that group's colour role immediately. "New group…" jumps to group creation with this category pre-filled.
+
+
9:41▮▮▮
+

Add "Sleep" to…

Currently ungrouped
+
+
+
Body & cycle
+
How I felt
+
Environment
+
+New group…
+
+
Sleep keeps all its past entries — filing changes its colour and where it appears, nothing else.
+
+
+
+ +
+
Multiple per day → a timeline
+
With "allow multiple" on, the log stops being one value and becomes a timeline of timestamped entries. Each tap appends a reading with the time it happened; the day's card shows them in order.
+
+
9:41▮▮▮
+

Headache

Sat, Aug 22 · 3 entries today
+
+
Today3 logged
+
+
08:10Level 2 · mild
+
13:40Level 4 · after lunch
+
19:05Level 1 · faded
+
+ Log another — now, 21:30
+
+
Add a readingLevel 1
+
+ + + + + +
+
+
Add entry
+
+
+ +
+
Input types render differently
+
The five input types are genuinely different controls — the category's type decides which appears. Scale stays as the stepped bars; the rest each get their own honest control.
+
+
9:41▮▮▮
+

Input types

One control per kind
+
+
Slider — continuous72.4 kg
+
+
50weight110
+
Count — tap to add6 glasses
+
+
+
6
+
+
glasses of water
+
Yes / noYes
+
+
Yes
+
No
+
Time23:15
+
23 : 15when it happened
+
+
+
+ +
+
How the new options interact
+
The four additions each map cleanly onto the model — no clashes once "log with period" is reframed.
+
+
IconStored per category, tinted with the category's colour role — so it recolours with the theme like everything else. Shows in the header switcher and on saved-day summaries.
+
Input type ≠ scaleA category stores {type, config}. Scale & slider are different components: discrete steps vs a real continuous value. "Kill the slider" only ever meant ratings — genuine measures keep one.
+
Allow multiple per dayFlips the store from one value/day to a list of timestamped entries — you capture what time something happened. The log screen swaps to the timeline component automatically.
+
Log with period — reframed, no clashIt no longer means a separate screen. In the unified log, period is a state of the day, so the flag just pins the category into the flow context while a period runs. Off-period it sits in its normal group.
+
+
+ +
+
New category — step 2, scale settings
+
Choosing type Scale reveals its own settings on the second screen: the words behind each step, a custom number range, and whether decimals are allowed.
+
+
9:41▮▮▮
+

Scale settings

Step 2 of 2 · Headache severity
Save
+
+
Range1 – 5
+
+
Min1
+
Max5
+
+
Step labelsoptional words
+
+
1Barely there
+
2Noticeable
+
3Distracting
+
4Add word…
+
5Unbearable
+
+
Allow decimals
Log values like 3.5 between steps
+
+
+
+ +
+
Category edit — alarms
+
Editing an existing category is the same two screens plus alarms — reminders to log it. They only appear on edit, not first creation, keeping step 1 short.
+
+
9:41▮▮▮
+

Headache

Edit category
Save
+
+
Reminders+ Add alarm
+
+
09:00
Every day
+
21:30
Weekdays
+
+
TypeScale 1–5
+
Scale settings
+
Danger zone
+
Delete category & its history
+
+
+
+ +
+
Colour — extended theme roles
+
Beyond primary/secondary/tertiary, we auto-derive 3 more in-theme roles from the active theme, so users get 5–6 harmonious choices that all re-theme. A truly clashing pick becomes a fixed colour — deliberately exempt from theme changes.
+
+
9:41▮▮▮
+

Pick a colour

Headache
+
+
In-theme rolesre-theme automatically
+
+Primary +Secondary +Tertiary +Quaternary +Quinary +Senary +
+
Fixed colourstays put on theme change
+
+
+
+
+
+
+
+
+
A fixed colour is treated as "don't change this" — it won't shift when the app theme changes. Everything else follows the theme.
+
+
+
+ + + diff --git a/docs/design/logging-redesign/handover/README.md b/docs/design/logging-redesign/handover/README.md new file mode 100644 index 0000000..3dd645a --- /dev/null +++ b/docs/design/logging-redesign/handover/README.md @@ -0,0 +1,156 @@ +# Handoff: GoFlo logging & category system redesign + +## Overview +A ground-up rethink of GoFlo's per-day logging screens and the "What You Track" category system. It replaces the current slider-driven, one-shape-per-input log screens with a single, consistent surface language, and introduces a **group / role** model that unifies category colour, the Settings theme picker, and category organisation. It also adds user-authored categories with icons, multiple input types, timelines, and alarms. + +## About the design files +The file in this bundle — `Log Screens Rethought.html` — is a **design reference created in HTML**. It is a pannable canvas of ~22 phone mockups plus annotation cards, not production code to copy. The task is to **recreate these designs in the existing GoFlo Android app** (Kotlin + Jetpack Compose + Material 3) using its established patterns — `Color.kt`, the `AppTheme` enum, `colorSchemeFor()`, DataStore persistence, Compose navigation. Do **not** port HTML/CSS; translate intent into Compose. + +`GoFlo Theme Redesign.md` is the companion spec for the 12 colour palettes and is the source of truth for token hex values. + +## Fidelity +**High-fidelity.** Colours, type scale, spacing, corner radii and interactions are final-intent. Recreate pixel-close using Compose Material 3 components and the app's colour scheme. Where the mock hardcodes a hex (e.g. `#1265AF`), that value stands in for a **theme role** — bind to `MaterialTheme.colorScheme.*`, never a literal. + +--- + +## The core model change (read this first) +Everything visual rests on one idea: **a category stores a colour *role*, never a hex.** + +- **Group**: `{ id, name, colorRole, defaultInputType }`. A group owns a role and a default. +- **Category**: gains `groupId` (**nullable**), `colorRole` (nullable — inherits group's when null), `icon`, `inputType`, `inputConfig`, `allowMultiplePerDay: Boolean`, `logWithPeriod: Boolean`. +- **colorRole** is an enum referencing the theme's tonal roles: `PRIMARY, SECONDARY, TERTIARY, QUATERNARY, QUINARY, SENARY`, plus `FIXED(argb)` for a deliberate off-theme colour. + - The first three map to existing M3 roles. **Quaternary/quinary/senary must be derived** from the active scheme (generate 3 extra harmonious tonal roles per theme, or extend each palette in `Color.kt`). They re-theme with everything else. + - `FIXED` stores a raw ARGB and is intentionally exempt from theme changes. +- **Migration is additive.** Add nullable columns; existing categories default to `groupId = null`, `colorRole = null` → render neutral (`surfaceVariant`) and keep working. Offer a one-time optional "Organise your categories" prompt. **No destructive rewrite.** + +Colour semantics in the mock: blue = body/cycle facts, amber = subjective feeling, indigo = environment. These are group choices, not hardcoded — the user assigns them. + +--- + +## Screens / views +The canvas is organised in rows. Left→right, top→bottom: + +### Row 1 — Log Period, rethought +- **As-built (reference of current state):** every input a different shape, giant blue numeral, two-tone drag track that reads like a progress bar, mauve slabs. +- **Rethought:** one surface language — **facts in a list card, judgements on a tap-scale, states on chips.** + - **Dates** = one white list card, two rows (Started / Ended), tabular values, chevrons. Not two outlined pills. + - **Flow** = 4 discrete **rising tap-steps** (Spot/Light/Med/Heavy), selected step filled `primary`, current value shown as a word in the section header. **No slider, no numeral.** + - **Symptoms** = chips; selected = filled `primaryContainer` + check; unselected = hairline outline. + - **Rage** = 6 warm (`secondary`) tap-steps, Calm→Volcanic end labels. + - Sticky **Save log** bar (`primary`, 52px, radius 26). +- **Single-metric log:** the metric *is* the page — tonal `primaryContainer` hero holds the reading as words ("Barely noticeable"), 5-step scale, notes fill former dead space, a small 7-day sparkline. + +### Row 2 — One log screen, two states +- **Off-period daily log:** mood leads in an **amber hero** ("How did today feel"); flow not rendered; symptoms, tracked metrics, notes below; a hairline "Period started today" footer row. +- **On-period:** a blue **Flow** group slots in at the top, mood hero compresses to one row, footer becomes a filled `primaryContainer` status row ("Period ongoing · since Aug 6 · End"). **Everything between is byte-for-byte identical** — one screen, period is a state of the day, not a separate destination. + +### Row 3 — Groups tie it together +- **Settings → Categories & groups:** theme swatch row on top; each group is a row with a colour dot + role label. Switching theme recolours every group. +- **Grouped multi-metric card:** Environment (Weather/Rainfall/Dampness) = **one card of rows**, not three separate logs. A group of one renders as the single-metric page. +- **Header switcher:** the screen title is a button → a sheet of categories **organised by group, tinted by role**. Switching re-files the entry; the value already entered is preserved. + +### Row 4 — What You Track (management home) +- **Grouped / Ungrouped** segmented toggle. +- **Grouped:** each group a card listing its categories, inline "+ Add category to this group", and a "+ New group" pill at the bottom. +- **Ungrouped:** loose categories in neutral `surfaceVariant`, each with "Add to group ›"; "+ New category" CTA. +- **Add to group** sheet: pick an existing group (adopts its role) or "New group…" pre-filled. Past entries preserved. + +### Row 5 — Category creation & input types +- **New category (step 1 of 2):** name, **icon** picker, colour (role chips), **input type** (Scale / Slider / Count / Yes-no / Time), **Allow multiple per day** toggle, **Log with period** toggle. Footer "Next ›". Step 1 stays short. +- **Timeline:** with *allow multiple* on, the day becomes a **list of timestamped entries** (08:10 · 13:40 · 19:05), each an appended reading with its time. Log screen swaps to this component automatically. +- **Input types render differently:** Slider = genuine continuous value (72.4 kg); Count = −/+ stepper; Yes/no = two-button segmented; Time = time field. Scale keeps the stepped bars. ("Kill the slider" applied only to ratings.) + +### Row 6 — Scale config, alarms, colour +- **Step 2 — Scale settings:** custom **min/max range**, per-step **word labels**, **Allow decimals** toggle. Only shown when type = Scale. +- **Category edit — alarms:** reminder times with day rules (Every day / Weekdays), on/off switches; link back into scale settings; delete-with-history danger row. Alarms appear on **edit**, not first creation. +- **Colour — extended theme roles:** primary/secondary/tertiary + 3 auto-derived in-theme roles (quaternary/quinary/senary), all re-theme; **Fixed colour** track for a deliberately clashing pick, treated as "don't change on theme switch". + +--- + +## Interactions & behaviour +- **Tap-steps:** single tap selects; the bar fills with the scale's role colour and its caption highlights. No drag. Steps rise in height left→right to encode magnitude. +- **Chips:** toggle selected/unselected; selected fills `primaryContainer` and gains a leading check. +- **Header title:** tappable → category-switch bottom sheet. Switching changes the entry's `categoryId`; the entered value is retained. +- **Grouped/Ungrouped toggle:** M3 segmented; view swaps in place. +- **Save bar:** sticky to the bottom, gradient fade over the scroll content. +- **Multiple-per-day:** primary action appends a timestamped entry; existing entries listed with an overflow (edit/delete) per row. +- **Alarms:** local notifications; per-alarm enable switch. + +## State / data +- Add nullable columns to Category (`groupId`, `colorRole`, `icon`, `inputType`, `inputConfig` JSON, `allowMultiplePerDay`, `logWithPeriod`). New `Group` table. +- `LogEntry`: for single-value categories, one row/day; for `allowMultiplePerDay`, many rows/day each with a timestamp. +- Period is derived state (is a period active on date D?) driving whether Flow renders and the footer variant — not a per-category screen. +- Theme selection already persists to DataStore (`AppTheme` enum); extend `colorSchemeFor()` to expose the 3 extra derived roles. + +## Design tokens (Beach Vibes light — see GoFlo Theme Redesign.md for all 12) +- `primary #1265AF` · `onPrimary #FFFFFF` · `primaryContainer #D5E3FF` · `onPrimaryContainer #001C3D` +- `secondary #9A6800` · `secondaryContainer #FFE08D` · `onSecondaryContainer #251A00` +- `tertiary #4B5BAC` (indigo, used for Environment group) +- `surface / background #F8FAFE` · `onSurface #181C22` · `surfaceVariant #E1E3EE` · `onSurfaceVariant #444751` · `outline #757782` +- Radii: cards 16–20, chips/pills full (19–26), phone frame 26. Save bar height 52. +- Type: Comfortaa (display / screen titles), Inter/system for body. Section labels 11px 700 uppercase +0.11em tracking. Values 14–15px 600. +- Elevation: cards `0 0 0 1px rgba(20,30,50,.06)` hairline; sheets add a soft drop shadow. + +## Design rules & guidelines +Apply these consistently — they are what make the screens feel like one system rather than 22 layouts. + +### Typography +- **Two families only.** Comfortaa (700) for screen titles and `ToneHero` words; Inter/system for everything else. Never introduce a third. +- **Scale (sp):** screen title 21 · hero word 22–24 · section value 14–15 (600) · body/list value 13.5–14.5 · **section label 11 (700, UPPERCASE, +0.11em tracking)** · step caption / helper 11–11.5 · subtitle under title 11.5. +- **Floor: nothing below 11sp**, and 11 only for non-essential captions. Body content is 13.5sp minimum. Respect the user's font-scale setting — use `sp`, never `dp`, for text. +- Weights: 400 body, 600 values/labels/emphasis, 700 titles + uppercase labels. Don't use 500. +- `letter-spacing` only on the uppercase labels (+0.11em) and slightly negative on titles (−0.02em). Body stays default. + +### Spacing, sizes, hit targets +- **Base unit 4dp.** Common gaps: 8 (chips, tight rows), 10–12 (within a card), 16 (screen horizontal padding), 20 (between groups). +- **Minimum touch target 48×48dp** — tap-steps, chips, counter buttons, switches, list rows all meet it even when the visible bar is shorter (pad the hit area). +- Corner radii: cards 16–20 · pills/chips full (height/2) · phone frame 26 · save bar 26 · icon tiles 12. +- Save bar 52dp, sticky, with a gradient fade of the surface colour over scrolling content. +- Elevation: cards use a **hairline** `1px surfaceVariant/6%` outline, not a drop shadow; only sheets/menus get a soft shadow. Keep the UI flat. +- List rows ≥ 52dp; divider is a 1px hairline at ~7% onSurface, never a full-contrast line. + +### Colour usage +- **Bind to roles, never hex.** `MaterialTheme.colorScheme.*` or a resolved `colorRole`. The hexes in the mock are role stand-ins. +- One role per meaning: a scale, its selected step, its header value, and its icon all share the category's role. Don't mix roles within one control. +- Selected = **tonal fill** (`primaryContainer`/role container), unselected = hairline outline. Never rely on outline-only to show selection. +- Reserve warm `secondary` for subjective/feeling categories, cool roles for facts — but this is the *user's* group choice, not hardcoded. + +### When to use small explanatory text (and when not) +- **Use it** for: an end-scale anchor pair (Calm ↔ Volcanic), a one-line consequence the user can't infer ("A fixed colour won't change with the theme", "Sleep keeps all its past entries"), and optional-field hints ("optional words"). +- **Don't** use it to restate a label ("Flow — set your flow"), to fill empty space, or on every field. If a control is self-evident, no helper. Aim for **at most one helper line per group**, and prefer showing the current value in the section header over explaining the control. +- Helper text is 11.5sp, `onSurfaceVariant`, never coloured unless it's a warning (`secondary`/amber on a tonal chip). + +### Accessibility +- **Contrast:** all text ≥ 4.5:1, large text/UI ≥ 3:1. The 12 palettes were tuned to pass — run `wcag_check.py` (see theme spec) after any colour change. +- **Never colour-only.** Selected states carry a check (chips) or fill + label emphasis (steps); the timeline shows the time, not just a coloured dot. A colour-blind or greyscale user must still parse every screen. +- **Touch:** 48dp minimum, 8dp minimum between independent targets. +- **Content descriptions** on icon-only controls (icon picker, counter ±, alarm toggle, header switcher caret). +- **Dynamic type:** layouts must reflow at the OS's largest font scale without clipping — test tap-steps and chips at 200%. +- **Motion:** step/chip selection is an instant tonal change or a ≤150ms fade; honour reduce-motion. No essential info conveyed by animation alone. +- **Focus & TalkBack:** logical traversal order top→bottom; a scale exposes as a single control reporting "Flow, Medium, 3 of 4", not four unlabelled buttons. + +## Files +- `Log Screens Rethought.html` — the full design canvas (open in a browser; pan/zoom). +- `GoFlo Theme Redesign.md` — the 12-palette colour spec and QA checklist. +- `screens/01–06-row.png` — rendered reference of each row (Log Period → colour/alarms), including the annotation cards. + +## Reusable component library (build once, reuse everywhere) +**This is a hard requirement: implement the design as a small set of stateless, parameterised Compose components — not per-screen one-offs.** Every screen in the mock is assembled from the same ~10 primitives. Reinventing any of these per screen is a defect. Put them in a shared `ui/components/` module and drive them entirely by parameters + a `colorRole`, so a category's role colour flows in from one place. + +Suggested primitives and their APIs (names indicative): +- `SectionHeader(label: String, value: String? = null, valueColor: Color = role)` — the uppercase label + right-aligned current value used above every group. +- `ListCard { rows }` + `ListRow(key, value, valueEmphasis, trailing = Chevron)` — the white rounded card with hairline-divided rows. Powers Dates, Tracking, alarms, step-labels, add-to-group. +- `StepScale(range, labels: List?, value, role, onSelect)` — the discrete rising tap-steps. **One component** serves flow, rage, severity, day-overall; range + labels + role are the only differences. No bespoke scales. +- `ChipToggle(text, selected, onToggle)` + `ChipRow` — filled `primaryContainer`+check when on, hairline when off. +- `ToneHero(word, caption, role, content)` — the tonal container that makes one metric the page (blue or amber by role). +- `SegmentedToggle(options, selected)` — Grouped/Ungrouped and Yes/No both use it. +- `RolePicker(roles, selected, onPick)` and `IconPicker` — reused in create + edit. +- `SwitchRow(title, subtitle, checked)` — allow-multiple, log-with-period, allow-decimals, alarm enable all share it. +- `Timeline(entries, onAppend)` + `TimelineEntry(time, value, sub)` — the multiple-per-day list. +- `PrimarySaveBar(label)` — the sticky bottom action. +- Input controls behind one `MetricInput(type, config, value, onChange)` façade that switches on `inputType` → `StepScale` / `ContinuousSlider` / `Counter` / `SegmentedToggle` / `TimeField`. **The log screen never branches on type itself** — it renders `MetricInput` and this façade picks the control. + +Because colour is a **role**, each primitive takes a `role` (or reads the category's) and resolves it through `colorSchemeFor()` — so theme switches and the quaternary/quinary/senary roles need zero per-component work. A single `LogScreen(category, date)` composes these primitives; there is no separate "Log Period" vs "Log X" screen. + +## Target codebase notes +Kotlin + Jetpack Compose + Material 3. Bind every colour to `MaterialTheme.colorScheme.*` or a derived role — never a literal hex from the mock. Reuse existing navigation, DataStore, and `AppTheme` machinery. Don't rename `AppTheme` entries (persisted) or change `colorSchemeFor()`'s signature; extend it. diff --git a/docs/design/logging-redesign/handover/screens/01-row.png b/docs/design/logging-redesign/handover/screens/01-row.png new file mode 100644 index 0000000..9ada62f Binary files /dev/null and b/docs/design/logging-redesign/handover/screens/01-row.png differ diff --git a/docs/design/logging-redesign/handover/screens/02-row.png b/docs/design/logging-redesign/handover/screens/02-row.png new file mode 100644 index 0000000..b2511e9 Binary files /dev/null and b/docs/design/logging-redesign/handover/screens/02-row.png differ diff --git a/docs/design/logging-redesign/handover/screens/03-row.png b/docs/design/logging-redesign/handover/screens/03-row.png new file mode 100644 index 0000000..9b72652 Binary files /dev/null and b/docs/design/logging-redesign/handover/screens/03-row.png differ diff --git a/docs/design/logging-redesign/handover/screens/04-row.png b/docs/design/logging-redesign/handover/screens/04-row.png new file mode 100644 index 0000000..120ef38 Binary files /dev/null and b/docs/design/logging-redesign/handover/screens/04-row.png differ diff --git a/docs/design/logging-redesign/handover/screens/05-row.png b/docs/design/logging-redesign/handover/screens/05-row.png new file mode 100644 index 0000000..165d411 Binary files /dev/null and b/docs/design/logging-redesign/handover/screens/05-row.png differ diff --git a/docs/design/logging-redesign/handover/screens/06-row.png b/docs/design/logging-redesign/handover/screens/06-row.png new file mode 100644 index 0000000..88668d7 Binary files /dev/null and b/docs/design/logging-redesign/handover/screens/06-row.png differ diff --git a/docs/design/logging-redesign/phases/KICKOFF.md b/docs/design/logging-redesign/phases/KICKOFF.md new file mode 100644 index 0000000..ac082a4 --- /dev/null +++ b/docs/design/logging-redesign/phases/KICKOFF.md @@ -0,0 +1,78 @@ +# Phase kickoff prompts + +Paste one of these into a **fresh session** to implement a phase. Replace `` and the phase name. Each phase is its own branch + PR off `main`. + +--- + +## Reusable template + +``` +You are implementing ONE phase of the GoFlo logging & category-system redesign. + +Repo: mapgie/GoFlo. Do all work on a new branch off main named +`claude/logging-redesign-phase-`. Open a draft PR when done. + +READ FIRST, IN THIS ORDER (do not skip): +1. docs/design/logging-redesign/PLAN.md — golden rules (§1), feature-preservation + inventory (§3), gap analysis (§4). These are binding. +2. docs/design/logging-redesign/phases/phase--*.md — your build guide: exact + files, code sketches, migration/data details, acceptance + preservation checklists. +3. The one subsystem map your phase touches (docs/design/logging-redesign/subsystem-maps/). + Run its staleness check first: the maps were stamped at commit d07d947 / DB v23 — + if HEAD or the DB version has moved, re-verify the files it describes before trusting + line numbers. + +HARD RULES: +- Additive-first. Do NOT delete or rewire any existing screen/feature unless your phase + guide's removal section explicitly says to (only Phase 8 removes). Every item in + PLAN.md §3 must still work when you finish. +- DB changes: additive, numbered migration only (check GoFloDatabase.kt's current + `version`), never fallbackToDestructiveMigration. Add a migration test. +- This environment can't build (no Android SDK) — CI is the build check. Verify by + inspection + run `python3 a11y_check.py` and `python3 wcag_check.py` before pushing. + Don't add "couldn't compile" disclaimers. +- Every app-code PR needs a changelog fragment at changelog/unreleased/.json + (the guide suggests one). No en/em dashes in user-facing text. Bind colour to roles, + never literal hex. Respect the a11y rules in .claude/CLAUDE.md (Role on every clickable). +- If your phase hits one of the "open decisions" in PLAN.md §8, STOP and ask me — don't guess. + +WHEN DONE: +- Tick your guide's acceptance + preservation checklists in the PR description. +- Update PLAN.md §7 progress log (status, PR/commit, DB version after, any deviations). +- If a file a subsystem map describes changed materially, update that map or note the drift. +- Open a draft PR; do not merge. + +Start by reading the three docs above, then tell me your plan for Phase before writing code. +``` + +--- + +## Phase 1 (ready to paste) + +``` +You are implementing Phase 1 (Extended colour roles) of the GoFlo logging redesign. + +Repo: mapgie/GoFlo. New branch off main: `claude/logging-redesign-phase-1`. Draft PR when done. + +READ FIRST: docs/design/logging-redesign/PLAN.md (§1 golden rules, §3 preservation, §4 gap +analysis), then docs/design/logging-redesign/phases/phase-1-color-roles.md (your build guide), +then docs/design/logging-redesign/subsystem-maps/03-theme-color-machinery.md (run its staleness +check first). + +Goal: add quaternary/quinary/senary in-theme colour roles (derived from the active ColorScheme +via HSL, provided through a CompositionLocal) plus a Fixed off-theme colour, and extend the +category colour picker. Fully additive, no DB migration. + +HARD RULES: additive only (don't touch existing primary/secondary/tertiary/hex behaviour); +don't change colorSchemeFor's signature (extend around it); derived on-colours must pass WCAG +contrast in all 12 palettes — run python3 wcag_check.py and add a spot-check for the new roles; +run python3 a11y_check.py (picker chips need Role.RadioButton, ≥48dp, not colour-only). Add a +changelog fragment. No en/em dashes in user-facing copy. + +When done: tick the guide's acceptance + preservation checklists in the PR, update PLAN.md §7, +open a draft PR (don't merge). + +Start by reading the three docs, then give me your plan before writing code. +``` + +> For later phases, copy the template and point it at the matching `phase--*.md`. Phase 5 is large — its guide suggests splitting into sub-PRs (5a-5d); you can run those as separate sessions too. diff --git a/docs/design/logging-redesign/phases/phase-1-color-roles.md b/docs/design/logging-redesign/phases/phase-1-color-roles.md new file mode 100644 index 0000000..6ac9bb5 --- /dev/null +++ b/docs/design/logging-redesign/phases/phase-1-color-roles.md @@ -0,0 +1,121 @@ +# Phase 1 — Extended colour roles + +**Goal:** categories can use three extra in-theme colour roles (quaternary/quinary/senary) that re-theme with the palette, plus a fixed off-theme colour. Fully additive, no DB migration. + +**Prerequisites:** none (first build phase). +**Read first:** `PLAN.md` §1, §5; `subsystem-maps/03-theme-color-machinery.md` (the whole thing); `handover/screens/06-row.png` ("Pick a colour" mock). + +--- + +## Why + +The whole redesign rests on "a category stores a colour *role*, never a hex." Today only `primary`/`secondary`/`tertiary` (+ raw hex) exist. The mock offers six in-theme roles plus a Fixed track. Material3's `ColorScheme` has no 4th-6th accent slot, so we derive them and expose them through a `CompositionLocal`. + +## Files to touch + +| File | Change | +|---|---| +| `ui/theme/Color.kt` | Add a derivation function producing 3 extra roles + on-colours from a `ColorScheme`. Add a small holder data class. | +| `ui/theme/Theme.kt` | Derive roles once per theme; provide via a new `CompositionLocal` wrapping `MaterialTheme`. | +| `ui/util/CategoryAppearance.kt` | Add enum entries + resolver branches for the 3 new tokens. | +| `ui/screens/categories/ManageCategoriesScreen.kt` | Colour picker becomes a role row of 6 + a Fixed track (factor a `RolePicker` here or inline; it is extracted into a shared component in Phase 3). | + +## Step-by-step + +### 1. Derivation (`Color.kt`) +Add near `buildCustomColorScheme` (mirror its HSL house style — do **not** add an HCT/TonalPalette dependency): + +```kotlin +/** Three extra in-theme accent roles derived from a ColorScheme, + WCAG-safe on-colours. */ +data class ExtendedRoles( + val quaternary: Color, val onQuaternary: Color, + val quinary: Color, val onQuinary: Color, + val senary: Color, val onSenary: Color, +) + +fun deriveExtendedRoles(scheme: ColorScheme): ExtendedRoles { + // Rotate hue off the three existing accents so the extras are harmonious but distinct. + // Pin S/L into the scheme's own accent band so contrast stays in range. + fun rotate(base: Color, degrees: Float): Color { + val hsl = FloatArray(3) + ColorUtils.colorToHSL(base.toArgb(), hsl) + hsl[0] = (hsl[0] + degrees + 360f) % 360f + return Color(ColorUtils.HSLToColor(hsl)) + } + val q = rotate(scheme.primary, 30f) + val qi = rotate(scheme.secondary, -30f) + val s = rotate(scheme.tertiary, 45f) + fun on(c: Color): Color = if (c.luminance() > 0.35f) Color(0xFF1C1B1F) else Color.White + return ExtendedRoles(q, on(q), qi, on(qi), s, on(s)) +} +``` +> The exact rotation is a judgement call — tune it so each role is visually separated from the base three in every palette. An alternative is interpolating between existing accents (`lerp`). Whatever you pick, verify contrast (step 5). + +### 2. Provide it (`Theme.kt`) +Add a CompositionLocal and wrap the existing `MaterialTheme(...)`: + +```kotlin +val LocalExtendedRoles = staticCompositionLocalOf { error("ExtendedRoles not provided") } +``` +Inside `GoFloTheme`, after `colorScheme` is chosen: +```kotlin +CompositionLocalProvider(LocalExtendedRoles provides deriveExtendedRoles(colorScheme)) { + MaterialTheme(colorScheme = colorScheme, typography = GoFloTypography, content = content) +} +``` +Keep the `SideEffect` status-bar logic as-is. + +### 3. Resolver (`CategoryAppearance.kt`) +Extend the enum: +```kotlin +enum class CategoryColor(val key: String, val displayName: String) { + PRIMARY("primary","Primary"), SECONDARY("secondary","Secondary"), TERTIARY("tertiary","Accent"), + QUATERNARY("quaternary","Quaternary"), QUINARY("quinary","Quinary"), SENARY("senary","Senary"), +} +``` +Extend both resolvers (they are `@Composable`, so they can read the local): +```kotlin +@Composable fun String.toCategoryColor(): Color { + val s = MaterialTheme.colorScheme + val ext = LocalExtendedRoles.current + return when (this) { + "primary" -> s.primary; "secondary" -> s.secondary; "tertiary" -> s.tertiary + "quaternary" -> ext.quaternary; "quinary" -> ext.quinary; "senary" -> ext.senary + else -> runCatching { Color(toLong(16)) }.getOrDefault(s.secondary) // FIXED = hex + } +} +``` +Mirror in `toCategoryOnColor()` with `onQuaternary`/`onQuinary`/`onSenary`. + +### 4. Picker UI (`ManageCategoriesScreen.kt`, existing "More colours" area ~line 1141) +Render a row of 6 role chips (selected = filled with the role colour + a check/ring) and, below, the existing fixed hex track labelled so the user knows it "stays put on theme change" (mock row 6). Each role chip: `Role.RadioButton` in `.semantics`, min 48dp, `contentDescription` naming the role. Selected state must not be colour-only (add a ring/check). + +### 5. Verify contrast +- Run `python3 wcag_check.py` (base palettes unaffected, must stay green). +- Add a spot-check: for each of the 12 light+dark schemes, derive the 3 roles and assert on-colour contrast ≥ 3:1 (icon) against the role. If a derived role fails in some palette, adjust the rotation/L clamp until all pass. Consider adding this as a small script (`wcag_check_roles.py`) so it is repeatable. + +## Data changes +None. `colorToken` is a free-form string; new tokens need no migration. + +## Acceptance criteria +- [ ] A category set to each of quaternary/quinary/senary renders that colour and **re-themes** when the palette changes (verify in one light + one dark palette). +- [ ] A category set to a Fixed colour does **not** change on theme switch. +- [ ] Derived on-colours pass contrast in all 12 palettes (spot-check script green). +- [ ] `wcag_check.py` and `a11y_check.py` both green. +- [ ] Picker chips carry `Role.RadioButton`, are ≥48dp, and selection is not colour-only. + +## Feature-preservation checklist +- [ ] Existing `primary`/`secondary`/`tertiary`/hex tokens resolve exactly as before. +- [ ] Calendar dots, `DayLogSheet` bubbles, Stats colour usage unchanged. +- [ ] The `CUSTOM` theme path (`buildCustomColorScheme`) still works — `deriveExtendedRoles` runs on the custom scheme too. + +## Gotchas +- `LocalExtendedRoles` is read inside `@Composable` resolvers only. Anything resolving a category colour **outside** composition (rare) needs the scheme passed explicitly. +- `staticCompositionLocalOf` won't recompose on change; theme changes swap the whole `GoFloTheme` subtree so that's fine. If you see stale colours, switch to `compositionLocalOf`. +- Comfortaa/typography untouched here. + +## Changelog fragment +`changelog/unreleased/extended-color-roles.json`: +```json +{ "bump": "minor", "added": ["Three extra in-theme colour roles for categories, plus a fixed off-theme colour option"] } +``` diff --git a/docs/design/logging-redesign/phases/phase-2-group-model.md b/docs/design/logging-redesign/phases/phase-2-group-model.md new file mode 100644 index 0000000..dce109a --- /dev/null +++ b/docs/design/logging-redesign/phases/phase-2-group-model.md @@ -0,0 +1,124 @@ +# Phase 2 — Group data model + +**Goal:** groups exist in the data layer with colour inheritance. Additive migration 23→24. No UI redesign yet (management UI is Phase 6). + +**Prerequisites:** Phase 1 (roles resolve, incl. the new tokens). +**Read first:** `subsystem-maps/02-category-data-model.md` (entities, migration chain, repository API); `PLAN.md` §5 (inheritance rule) and §8 (open decision #1). + +--- + +## Why + +A group owns a colour role and a default input type; a category optionally belongs to one. This is the only genuinely new table in the redesign. Everything additive so existing data survives untouched. + +## The inheritance rule (important deviation) + +The handover says existing categories should default to a neutral `surfaceVariant`. **We do not do that** — it would visually wipe the colours every existing category already has (`colorToken` is populated today). Instead: + +- A category resolves colour from its **own `colorToken`**. +- Introduce a sentinel token `"inherit"`. If `colorToken == "inherit"` **and** `groupId != null`, resolve to the **group's `colorRole`**. +- If `colorToken == "inherit"` **and** `groupId == null`, render neutral (`surfaceVariant`). +- Existing rows keep their real `colorToken` (e.g. `"primary"`), so they look identical after migration. Only categories the user explicitly sets to "inherit" follow the group. + +This keeps §8 decision #1 open (you can later offer an optional "Organise" nudge) without a destructive default. Confirm with the owner before shipping if unsure. + +## Files to touch + +| File | Change | +|---|---| +| `data/database/entities/Group.kt` | **New** entity. | +| `data/database/dao/GroupDao.kt` | **New** DAO. | +| `data/database/entities/TrackingCategory.kt` | Add `groupId: Long? = null`. | +| `data/database/GoFloDatabase.kt` | Add `Group` to `@Database`, bump `version = 24`, add `MIGRATION_23_24`, expose `groupDao()`. | +| `data/repository/TrackingRepository.kt` (or a new `GroupRepository`) | Group CRUD + assign/unassign category. | +| `ui/util/CategoryAppearance.kt` | Add `"inherit"` handling to the resolvers (needs the group's role passed in — see below). | +| `GoFloApplication.kt` | Wire the new DAO/repository if a separate repository is used. | + +## Step-by-step + +### 1. Entity (`Group.kt`) +```kotlin +@Entity(tableName = "groups") +data class Group( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + val name: String, + val colorRole: String = "primary", // a CategoryColor key (never a hex; groups are in-theme) + val defaultInputType: String = "default",// a CategoryType key + val displayOrder: Int = 0, +) +``` + +### 2. `TrackingCategory` — add nullable column +```kotlin +val groupId: Long? = null, +``` +Place it last to keep the constructor call-sites that use named args safe (they mostly do). Grep for positional constructor calls just in case. + +### 3. Migration (`GoFloDatabase.kt`) +Follow the additive-column pattern used by e.g. MIGRATION_11_12: +```kotlin +val MIGRATION_23_24 = object : Migration(23, 24) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("""CREATE TABLE IF NOT EXISTS `groups` ( + `id` INTEGER NOT NULL PRIMARY KEY AUTOGENERATE_PLACEHOLDER, + `name` TEXT NOT NULL, + `colorRole` TEXT NOT NULL DEFAULT 'primary', + `defaultInputType` TEXT NOT NULL DEFAULT 'default', + `displayOrder` INTEGER NOT NULL DEFAULT 0)""") + db.execSQL("ALTER TABLE tracking_categories ADD COLUMN groupId INTEGER") + } +} +``` +> Match Room's exact generated SQL: autoGenerate PK is `INTEGER PRIMARY KEY AUTOINCREMENT` only if you keep AUTOINCREMENT; Room by default uses `INTEGER PRIMARY KEY AUTOINCREMENT` when `autoGenerate = true`. **Confirm the generated schema** — the safe way is to let Room generate it once (temporarily set `exportSchema = true` locally) or copy the column definition style from an existing `CREATE TABLE` migration in this file. `groupId` is nullable so no default is needed. + +Register it in `.addMigrations(...)` and bump `version = 24`. Add `Group::class` to the `entities = [...]` array and a `abstract fun groupDao(): GroupDao`. + +### 4. DAO (`GroupDao.kt`) +```kotlin +@Dao interface GroupDao { + @Query("SELECT * FROM `groups` ORDER BY displayOrder, name") fun getAllGroups(): Flow> + @Query("SELECT * FROM `groups` ORDER BY displayOrder, name") suspend fun getAllGroupsOnce(): List + @Query("SELECT * FROM `groups` WHERE id = :id") suspend fun getGroupById(id: Long): Group? + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertGroup(group: Group): Long + @Update suspend fun updateGroup(group: Group) + @Delete suspend fun deleteGroup(group: Group) +} +``` +> Decide delete behaviour: deleting a group should **not** delete its categories. Either set their `groupId = null` in the same transaction, or `ON DELETE SET NULL` via an FK. Simplest: a repository method that nulls members then deletes the group. Do **not** cascade-delete categories. + +### 5. Repository methods +`getAllGroups()`, `addGroup(name, colorRole, defaultInputType)`, `renameGroup`, `updateGroupRole`, `reorderGroups`, `deleteGroup(unassignMembers=true)`, `assignCategoryToGroup(categoryId, groupId)`, `unassignCategory(categoryId)`. + +### 6. Resolver `"inherit"` support (`CategoryAppearance.kt`) +`toCategoryColor()` is `@Composable` and keyed only on the token string; it does not know the group. Two options: +- **A (recommended):** resolve the effective token *before* calling `toCategoryColor()` — at the call site you usually have the category and can look up its group's role. Add a helper `TrackingCategory.effectiveColorToken(groups): String` that returns `colorToken` unless it is `"inherit"`, in which case the group's `colorRole` (or `"surfaceVariant-sentinel"` when no group). +- **B:** pass an optional group role into the resolver. Messier; prefer A. + +Add a neutral branch so an inherit-with-no-group category renders `surfaceVariant`/`onSurfaceVariant`. + +## Data changes +Migration 23→24 adds the `groups` table and a nullable `groupId` column. Existing rows: `groupId = null`, colours unchanged. + +## Acceptance criteria +- [ ] **Migration test:** open a seeded v23 database, run `MIGRATION_23_24`, assert (a) `groups` exists, (b) `tracking_categories.groupId` exists and is null for all rows, (c) all pre-existing category/value/log data is intact. (See how other migrations are exercised in tests, if any; otherwise add one under `app/src/test` or `androidTest`.) +- [ ] A category with `colorToken = "inherit"` assigned to a group renders the group's role colour and re-themes. +- [ ] An inherit category with no group renders neutral. +- [ ] Existing categories look **identical** to before (no grey wipe). +- [ ] App builds in CI. + +## Feature-preservation checklist +- [ ] All existing category reads (`getActiveCategories`, `getCategoryById`, …) still compile and return the same data plus the new nullable field. +- [ ] Stats/History/Home/DayLogSheet/export unaffected (they don't need `groupId` yet). +- [ ] Deleting a group never deletes categories or their history. +- [ ] `saveLog` / value CRUD untouched. + +## Gotchas +- Room requires the migration's generated SQL to **exactly** match the entity schema or it throws `IllegalStateException` at first open after the bump. Copy the column-definition style from an existing migration in this file; verify types (`INTEGER`/`TEXT`) and `NOT NULL`/defaults. +- `groups` is a SQL keyword-adjacent name; always backtick it in raw SQL. +- Never `fallbackToDestructiveMigration` (CLAUDE.md). +- If any `TrackingCategory(...)` positional constructor exists, adding a field breaks it — grep and fix to named args. + +## Changelog fragment +```json +{ "bump": "minor", "added": ["Groups: categories can now belong to a group that owns a shared colour role"] } +``` diff --git a/docs/design/logging-redesign/phases/phase-3-component-library.md b/docs/design/logging-redesign/phases/phase-3-component-library.md new file mode 100644 index 0000000..beb8f19 --- /dev/null +++ b/docs/design/logging-redesign/phases/phase-3-component-library.md @@ -0,0 +1,58 @@ +# Phase 3 — Reusable component library + +**Goal:** build the ~12 stateless, parameterised Compose primitives every later screen is assembled from. With `@Preview`s. No screen consumes them yet (previews only). This is a **hard requirement** from the handover — per-screen one-offs are a defect. + +**Prerequisites:** Phase 1 (roles). Phase 2 not strictly required but helpful. +**Read first:** `handover/README.md` §"Reusable component library" (the API list); `handover/screens/01-06-row.png`; `PLAN.md` §1 (a11y rules), §2 (the four design rules). + +--- + +## Where + +All in `ui/components/` (new files, one per primitive or a small cluster). Each is stateless: state in, callbacks out. Each accents from a `role: Color` param (plus its on-colour) so a category's role flows in from one place — never reads `MaterialTheme.colorScheme.primary` directly for accent. + +## The primitives (names indicative; match the handover) + +| Component | API sketch | Notes / a11y | +|---|---|---| +| `SectionHeader` | `(label: String, value: String? = null, valueColor: Color = role)` | Uppercase label 11sp/700/+0.11em; value right-aligned. Above every group. | +| `ListCard` + `ListRow` | `ListCard { rows }`; `ListRow(key, value, valueEmphasis, trailing = Chevron, onClick)` | White rounded card, hairline dividers (1px ~7% onSurface). Powers Dates, Tracking, alarms, step-labels, add-to-group. Row ≥52dp. `Role.Button` when clickable. | +| `StepScale` | `(range: IntRange, labels: List?, value: Int?, role: Color, onColor: Color, onSelect: (Int) -> Unit, endLabels: Pair? = null)` | Discrete rising tap-steps; selected step filled `role`, caption highlighted. No drag. **Expose as ONE control**: `clearAndSetSemantics { role = Role.RadioButton; contentDescription = ", , of " }`. Rising heights encode magnitude; pad hit area to 48dp. Serves flow, rage, severity, day-overall. | +| `ChipToggle` + `ChipRow` | `ChipToggle(text, selected, role, onToggle)`; `ChipRow(...)` | Selected = filled role container + leading check; unselected = hairline. `Role.Checkbox`/`FilterChip` semantics. Reuse/replace `SelectableChip`. Never colour-only (check icon). | +| `ToneHero` | `(word: String, caption: String?, role: Color, onColor: Color, content: @Composable () -> Unit)` | Tonal container that makes one metric the page (blue primary or amber secondary by role). Word in **Comfortaa** (apply `ComfortaaFamily` explicitly — see map 03 §3). | +| `SegmentedToggle` | `(options: List, selected: Int, onSelect: (Int) -> Unit)` | M3 segmented buttons. Powers Grouped/Ungrouped and Yes/No. `Role.RadioButton` per segment. | +| `RolePicker` | `(roles: List, selected, onPick)` + a Fixed track | Extract from Phase 1's inline picker. `Role.RadioButton`, ≥48dp, ring/check not colour-only. | +| `IconPicker` | `(icons: List, selected, onPick)` | Grid; `Role.RadioButton`; `contentDescription` = icon displayName. | +| `SwitchRow` | `(title, subtitle: String?, checked, onCheckedChange)` | allow-multiple, log-with-period, allow-decimals, alarm-enable. `Role.Switch` + `stateDescription`. Set inner `Switch onClick = null` if the row handles the click (avoid double-focus, per CLAUDE.md a11y). | +| `Timeline` + `TimelineEntry` | `Timeline(entries, onAppend)`; `TimelineEntry(time, value, sub, onOverflow)` | Multiple-per-day list: `08:10 · Level 2 · mild`. Shows the **time**, not just a coloured dot (colour-blind safe). Per-row overflow (edit/delete). | +| `PrimarySaveBar` | `(label: String, onClick)` | Sticky bottom, 52dp, radius 26, `role`/primary. Gradient fade of surface over scrolling content. Icon-less; if icon-only variant, add `contentDescription`. | +| `MetricInput` (façade) | `(type: CategoryType, config: MetricConfig, value: MetricValue, onChange)` | **Stub the switch here** (returns each control) but full behaviour lands in Phase 4. Define `MetricConfig`/`MetricValue` value types now. | + +## Typography & spacing (apply consistently — this is what makes it one system) +- Two families only: **Comfortaa (700)** for screen titles + `ToneHero` words (apply `ComfortaaFamily` from `Type.kt` explicitly — it is not wired into `GoFloTypography`); **system/Inter** for everything else. +- Scale (sp): title 21 · hero word 22-24 · section value 14-15 (600) · body 13.5-14.5 · section label 11 (700 UPPERCASE +0.11em) · caption 11-11.5. Floor 11sp; body ≥13.5sp. **Use `sp`, never `dp`, for text.** +- Radii: cards 16-20 · pills/chips full · save bar 26 · icon tiles 12. Base unit 4dp; gaps 8/10-12/16/20. +- Elevation: cards use a **hairline** `1px surfaceVariant/6%` outline, not a drop shadow. Only sheets get a soft shadow. + +## Acceptance criteria +- [ ] Every primitive has an `@Preview` (light + dark) rendering correctly. +- [ ] Previews at **200% font scale** reflow without clipping (test `StepScale` and `ChipRow` especially — handover requirement). +- [ ] `a11y_check.py` green: every `.clickable{}` carries a `Role`; `StepScale` exposes as one control; `SwitchRow` announces state. +- [ ] No primitive reads a hardcoded accent — accent always comes from `role`. +- [ ] Motion: selection is instant or ≤150ms fade; honour reduce-motion. + +## Feature-preservation checklist +- [ ] Nothing existing is removed or rewired. `SelectableChip` may stay until Phase 4/5 migrate callers; do not delete it here. +- [ ] Existing screens compile unchanged. + +## Gotchas +- `ComfortaaFamily` is a downloadable Google Font — first render may fall back until fetched; provide a sane fallback stack. +- LESSONS.md has directly-relevant entries: inline links via `LinkAnnotation`, `liveRegion` for status text, icon-only controls need `contentDescription` on the container, `ModalBottomSheetProperties` needs all three params in this Material3 version. Read the "Android / Compose" section before building sheets/links. +- Keep components **stateless** — no `remember`ed domain state inside; hoist it. This is what lets Phase 5 compose them freely. + +## Changelog fragment +Component-library-only PRs have no direct user-facing change, but they touch `.kt`, so a fragment is still required by CI. Use: +```json +{ "bump": "patch", "changed": ["Internal: shared UI component library for the logging redesign (no user-facing change yet)"] } +``` +> If the project prefers not to surface internal notes in "What's New", coordinate with the owner; CI only requires a valid fragment, and the release dialog shows the 5 most recent entries. diff --git a/docs/design/logging-redesign/phases/phase-4-metricinput.md b/docs/design/logging-redesign/phases/phase-4-metricinput.md new file mode 100644 index 0000000..199a07c --- /dev/null +++ b/docs/design/logging-redesign/phases/phase-4-metricinput.md @@ -0,0 +1,83 @@ +# Phase 4 — MetricInput façade + new input types (Yes/No, Time) + +**Goal:** one façade renders every input type; add `yes_no` and `time`; refactor `LogCategoryScreen` to render through the façade with **zero behaviour loss**. This is the phase most at risk of silent feature loss — parity is the whole job. + +**Prerequisites:** Phase 3 (primitives exist). +**Read first:** `subsystem-maps/01-logging-screens.md` §2 and §4 (walk every branch); `handover/screens/05-row.png` and `06-row.png` (input types render differently); `PLAN.md` §8 decision #3 (storage encoding). + +--- + +## The five (now seven) input paths that must all keep working + +From map 01 §2, today's `LogCategoryScreen` renders: +1. `numeric_slider` → stepped `Slider` (steps = range-1, or 0 if `allowDecimals`), value readout, optional `scaleLabels` word. +2. `numeric_free` → `OutlinedTextField` decimal, unit label, **empty blocks save**. +3. `increment` + `trackAgainstTime` → per-tap immediate save with `HH:mm`, timeline list, per-entry delete, **no Save button**. +4. `increment` (untimed) → big count + add/decrement, **≤0 blocks save**. +5. `default` → `SelectableChip` multi-select + "previously recorded (removed)" chips. + +New: +6. `yes_no` → `SegmentedToggle` (Yes/No), stores "Yes"/"No". +7. `time` → `TimeField`, stores `HH:mm`. + +## Storage decision (resolve §8 #3 before coding) +Default recommendation, zero-migration: store the value as a `TrackingLogValue.valueLabel` string — "Yes"/"No" for yes_no, "HH:mm" for time. Stats already counts value labels (`getValueCountsForCategory`), so Yes/No charts for free. Time as a label is display-only in Stats (acceptable). Document whatever you choose at the top of the façade file. + +## Files to touch + +| File | Change | +|---|---| +| `ui/util/CategoryAppearance.kt` | Add `YES_NO("yes_no",…)`, `TIME("time",…)` to `CategoryType`. | +| `ui/components/MetricInput.kt` | Flesh out the façade `when(type)` → the right primitive; define `MetricConfig`/`MetricValue`. | +| `ui/components/` | Add `Counter` and `TimeField` if not built in Phase 3. | +| `ui/screens/log/LogCategoryScreen.kt` | Replace the inline `when` (map 01 §2) with a single `MetricInput(...)`. Keep notes/save/date/delete/timed-timeline behaviour. | +| `ui/screens/log/LogCategoryViewModel.kt` | Add value get/set for yes_no + time; keep save-blocking rules. | +| `ui/screens/log/LogPeriodScreen.kt` | Optional: point `PinnedCategoryInput` at `MetricInput` too, to kill the duplication early (it is fully replaced in Phase 5). | + +## The façade +```kotlin +@Composable +fun MetricInput( + type: CategoryType, config: MetricConfig, value: MetricValue, + role: Color, onColor: Color, onChange: (MetricValue) -> Unit, +) = when (type) { + CategoryType.NUMERIC_SLIDER -> StepScale(config.range, config.labels, value.int, role, onColor, { onChange(value.copy(int = it)) }, config.endLabels) + CategoryType.NUMERIC_FREE -> ContinuousInput(value.text, config.unit, config.allowDecimals, onChange = { onChange(value.copy(text = it)) }) + CategoryType.INCREMENT -> Counter(value.int ?: 0, onChange = { onChange(value.copy(int = it)) }) // timed variant → Timeline, handled by screen + CategoryType.YES_NO -> SegmentedToggle(listOf("Yes","No"), if (value.text == "Yes") 0 else 1) { onChange(value.copy(text = if (it==0) "Yes" else "No")) } + CategoryType.TIME -> TimeField(value.text) { onChange(value.copy(text = it)) } + CategoryType.DEFAULT -> ChipRow(config.options, value.selected, role, onColor, onToggle = { onChange(value.toggle(it)) }) +} +``` +> The **log screen never branches on type** after this — it renders `MetricInput`. The timed-increment timeline is a screen-level concern (it appends immediately); keep that branch in the screen but have it render the `Timeline` primitive. + +## Parity procedure (do this, don't skip) +For each of the 7 paths: create/pick a category of that type, log a value, edit it, and confirm the stored `TrackingLogValue` and behaviour match `main`. Specifically re-verify: +- slider steps/labels/decimals + readout word +- numeric_free empty-blocks-save + unit label +- increment ≤0-blocks-save +- timed increment: per-tap save, timestamp, timeline order, per-entry delete, no Save button +- default chips: multi-select + "previously recorded (removed)" chips render +- notes 500-char cap; date selection when `canEditDate`; edit vs new; delete + +## Acceptance criteria +- [ ] All 7 paths log + edit correctly; stored values identical to `main` for the 5 existing ones. +- [ ] Yes/No and Time categories can be created (via the current create flow or a temporary path — full create UI is Phase 7) and logged. +- [ ] `LogCategoryScreen` no longer contains a `categoryType` `when` for rendering (it calls `MetricInput`). +- [ ] `a11y_check.py` green. + +## Feature-preservation checklist +- [ ] Every bullet in the parity procedure passes. +- [ ] `LogPeriodScreen` still works (whether or not you rewired `PinnedCategoryInput`). +- [ ] Period fan-out (`syncFlowToTrackingLog` etc.) unaffected. +- [ ] Stats/History still read the same value labels. + +## Gotchas +- Timed increment is genuinely different (saves per tap, no Save button). Don't force it through the same "collect then save" flow; keep it screen-driven, rendering `Timeline`. +- `MetricValue` needs to represent: an int (slider/count), a text (free/yes_no/time), and a selected-set (chips). Model it so `onChange` round-trips without lossy conversions. +- Keep `categoryType` immutability for now (§8 #2 is a Phase 7 decision). + +## Changelog fragment +```json +{ "bump": "minor", "added": ["New category input types: Yes/No and Time"], "changed": ["Unified how category inputs are rendered (no change to existing categories)"] } +``` diff --git a/docs/design/logging-redesign/phases/phase-5-unified-logscreen.md b/docs/design/logging-redesign/phases/phase-5-unified-logscreen.md new file mode 100644 index 0000000..f1fe74f --- /dev/null +++ b/docs/design/logging-redesign/phases/phase-5-unified-logscreen.md @@ -0,0 +1,65 @@ +# Phase 5 — Unified `LogScreen` (the big one) + +**Goal:** one screen logs a day; a running period is a *state of the day*, not a separate destination. Old screens stay reachable until parity is signed off (removal is Phase 8). + +**Prerequisites:** Phases 1-4. +**Read first:** `subsystem-maps/01-logging-screens.md` (all of it — this phase must reproduce both screens' behaviour); `handover/screens/01-03-row.png` (the two states, the header switcher, the grouped card); `PLAN.md` §3 (preservation inventory) and §5. + +--- + +## The core idea (handover row 2-3) +There is **one** log screen. Off-period and on-period share the same components in the same order; only two things change: +- **Off-period:** mood hero leads (amber/`secondary` `ToneHero`, "How did today feel"); flow not rendered; footer = quiet hairline "Period started today ›" row. +- **On-period:** a blue/`primary` **Flow** `StepScale` group slots in at the top; the mood hero compresses to a single `StepScale` row; footer = filled `primaryContainer` status row ("Period ongoing · since Aug 6 · End"). +Everything between (symptoms, tracked metrics, notes) is byte-for-byte the same. + +## Suggested sub-PRs (this phase is large — split it) +1. **5a — off-period skeleton:** `LogScreen(date)` + VM rendering mood hero, symptoms, grouped tracked-metric cards, notes, save bar, quiet period footer. Behind a new route/flag; entry points still use the old screens. +2. **5b — on-period state:** flow group, compressed mood row, status footer, all period episode/continuation logic ported. +3. **5c — header switcher + grouped multi-metric card:** title-as-button sheet (categories by group, tinted by role, preserves entered value); a group of ≥2 renders one `ListCard` of rows, a group of one renders the single-metric page. +4. **5d — flip entry points:** calendar tap / FAB / quick-log open `LogScreen`; keep old routes registered. + +## Files + +| File | Change | +|---|---| +| `ui/screens/log/LogScreen.kt` + `LogViewModel.kt` | **New.** Compose the primitives; own the day model. | +| `ui/navigation/Screen.kt` | Add a `Log` route (e.g. `log_day?date={date}`). Keep `LogPeriod`/`LogCategory`. | +| `MainActivity.kt` | Register the new route; wire its VM factory. Flip entry points in 5d. | +| `data/repository/PeriodRepository.kt` / `TrackingRepository.kt` | Reuse existing methods; add read helpers if needed (e.g. "is a period active on date D"). No schema change expected. | + +## Behaviour that MUST be ported (from map 01 §4-5) +- **Flow:** slider *or* chips per the flow-category mode; the 1→Spotting / 2→Light / 4→Heavy / else Medium mapping. +- **Symptoms:** chips + inline "Add" (`AddSymptomDialog`) writing to the value catalog (`addValueToCategory`). +- **Pinned categories** (`showInLogPeriod`) render in the flow context while a period runs; in their normal group otherwise. +- **Episode/continuation:** gap tolerance from prefs, episode day number, range vs open-ended, "Remove this day" (`unlogPeriodDay`), "Delete Entire Period" (deletes episode + its per-day tracking logs), "Disable period logging". +- **Unsaved-changes guard:** discard/save dialog on back (LESSONS.md `SwipeToDismissBox`/guard patterns are relevant to how you intercept). +- **Save fan-out:** `syncFlowToTrackingLog`, `syncSymptomsToTrackingLog`, `syncPinnedCategoryLogs` → tracking system; plus `GoFloWidget.updateAllWidgets` and `ReminderScheduler.refreshPredictionReminders`. +- **Header switcher (re-file):** switching category changes the entry's `categoryId`; the entered value is preserved. "The current entry is preserved — switching only changes what the same saved day is filed under." + +## Acceptance criteria +- [ ] A day with **no** active period: mood hero + symptoms + tracked metrics + notes all log and persist; "Period started today" starts one. +- [ ] A day **with** an active period: flow leads, mood compresses, footer shows status + End; everything else identical to the off-period layout. +- [ ] Period data appears in Stats / Flow / Symptoms / widget exactly as the old screen produced. +- [ ] Header switcher re-files without losing the entered value. +- [ ] Grouped multi-metric card: ≥2 categories → one card of rows; 1 → single-metric page. +- [ ] TalkBack traversal top→bottom; `StepScale` reads as one control ("Flow, Medium, 3 of 4"). +- [ ] `a11y_check.py` green. + +## Feature-preservation checklist (tick every item in `PLAN.md` §3 "Logging") +- [ ] Old `LogPeriod` and `LogCategory` routes **still work** and are still registered (do not delete). +- [ ] All five (now seven) input types render via `MetricInput` inside `LogScreen`. +- [ ] Edit an existing period day / category log through the new screen matches old behaviour. +- [ ] Unsaved-changes guard present. +- [ ] Disable-period, remove-day, delete-episode all reachable. + +## Gotchas +- This is where "recreate from the mock" most tempts feature loss. Work from map 01 §3 as a **checklist**, not the mock. +- The mock omits some real behaviour (e.g. tolerance/continuation). Those live in `LogPeriodViewModel`; port them even though the mock doesn't show them. +- Keep the flag/route split until 5d so a broken new screen never blocks logging. +- Don't remove `PinnedCategoryInput` yet — Phase 8. + +## Changelog fragment +```json +{ "bump": "minor", "changed": ["Redesigned daily logging: one screen for the day, with an active period shown as a state of that day"] } +``` diff --git a/docs/design/logging-redesign/phases/phase-6-what-you-track.md b/docs/design/logging-redesign/phases/phase-6-what-you-track.md new file mode 100644 index 0000000..6180885 --- /dev/null +++ b/docs/design/logging-redesign/phases/phase-6-what-you-track.md @@ -0,0 +1,49 @@ +# Phase 6 — "What You Track" management home (Grouped/Ungrouped) + +**Goal:** redesign `ManageCategoriesScreen` to the row-4 mock; groups become first-class in the UI. All existing management actions survive. + +**Prerequisites:** Phases 1-3 (roles, group model, `SegmentedToggle`/`ListCard`/`RolePicker`). +**Read first:** `handover/screens/03-row.png` and `04-row.png` (Grouped/Ungrouped, add-to-group sheet); `subsystem-maps/02-category-data-model.md` §4 (repo API); the current `ManageCategoriesScreen.kt` (63KB — read it fully; it already hosts icon/colour pickers, archive, reorder, delete, tracking modes, quick-log). + +--- + +## Target UI (handover row 4) +- Top: `SegmentedToggle` **Grouped / Ungrouped** (view swaps in place). +- **Grouped:** each group is a `ListCard` titled with its colour dot + "Edit", listing its categories, an inline "+ Add category to this group", and a "+ New group" pill at the bottom. +- **Ungrouped:** loose categories in neutral `surfaceVariant`, each with "Add to group ›"; a "+ New category" CTA. Copy: "Ungrouped categories still log — they just show in surfaceVariant until you file them." +- **Add-to-group sheet** (row 4 right / row 5 far-right): pick an existing group (adopts its role immediately) or "New group…" (jumps to group creation with this category pre-filled). Past entries preserved: "filing changes its colour and where it appears, nothing else." + +## Files + +| File | Change | +|---|---| +| `ui/screens/categories/ManageCategoriesScreen.kt` | Restructure into the two-tab view; add group cards + add-to-group sheet + new-group flow. Reuse `SegmentedToggle`, `ListCard`, `RolePicker`. | +| `ui/screens/categories/ManageCategoriesViewModel.kt` | Add groups Flow, group CRUD, assign/unassign, tab state. | +| `ui/navigation/Screen.kt` / `MainActivity.kt` | If group create/edit is a separate destination, add a route; a bottom sheet in-place is also fine. | + +## Existing behaviour to keep reachable (map 02 §4) +Archive / unarchive, reorder, delete (system protected, delete-with-history), rename, icon + colour edit, numeric/scale settings entry, allow-multiple / show-in-period / track-against-time toggles, **tracking modes** (`modeKey` presets), **quick-log** config, and the link into **`ManageCategoryValues`** for value editing. None of these may disappear behind the redesign; re-home them into the group/category rows and the edit flow (edit-flow detail is Phase 7). + +## Acceptance criteria +- [ ] Create, rename, recolour (role), reorder, and delete groups; deleting a group unfiles its categories (never deletes them). +- [ ] File / unfile a category via the add-to-group sheet; an inherit-category adopts the group role live. +- [ ] Grouped and Ungrouped tabs both render correctly and swap in place. +- [ ] Every pre-existing management action is still reachable and works. +- [ ] `a11y_check.py` green (segmented = `Role.RadioButton`; rows = `Role.Button`; sheet dismiss = `Role.Button`). + +## Feature-preservation checklist +- [ ] Archive/unarchive, reorder, delete-with-history, system-category protection. +- [ ] Tracking modes still present and functional. +- [ ] Quick-log config + Quick Log widget unaffected. +- [ ] `ManageCategoryValues` still reachable; value CRUD + rename-with-history intact. +- [ ] Category counts in the header ("8 categories · 3 groups") reflect reality. + +## Gotchas +- `ManageCategoriesScreen.kt` is large and already dense. Prefer extracting the group-card and category-row into private composables (or shared primitives) over growing one function. +- Reorder within a group vs global order: decide whether `displayOrder` is per-group or global. Simplest: keep global `displayOrder` on categories, add `displayOrder` on groups; render grouped by `groupId` then category `displayOrder`. +- Don't regress the existing colour/icon pickers — they move into the Phase 7 edit flow, but until then keep them working here. + +## Changelog fragment +```json +{ "bump": "minor", "changed": ["Redesigned the What You Track screen with grouped and ungrouped views and inline group management"] } +``` diff --git a/docs/design/logging-redesign/phases/phase-7-create-edit-alarms.md b/docs/design/logging-redesign/phases/phase-7-create-edit-alarms.md new file mode 100644 index 0000000..e14a952 --- /dev/null +++ b/docs/design/logging-redesign/phases/phase-7-create-edit-alarms.md @@ -0,0 +1,61 @@ +# Phase 7 — Category creation & edit (2-step) + scale settings + alarms + +**Goal:** the row-5 / row-6 create and edit flows. Step 1 stays short; step 2 is scale-only; alarms and danger-zone appear on **edit**, not first creation. + +**Prerequisites:** Phases 1-4 (roles, group model, primitives, input types). +**Read first:** `handover/screens/04-row.png` (New category full option set), `05-row.png` (step 2 scale settings + category-edit alarms), `06-row.png` (colour roles). Current `ManageCategoriesScreen.kt` (hosts today's create/edit) and the `EditAlarm` screen + `CustomAlarm` system. + +--- + +## New category — step 1 (row 5) +Fields, in order: **Name**; **Icon** (`IconPicker`); **Colour** (`RolePicker` — 6 roles + Fixed); **Input type** (Scale / Slider / Count / Yes-no / Time via `SegmentedToggle` or a row of chips); **Allow multiple per day** (`SwitchRow` — "Log it several times — keeps the time of each"); **Log with period** (`SwitchRow` — "Surface it in the flow context while a period runs"). Footer "Next ›". Helper (one line): "Scale, range and alarms live on step 2 — the first screen stays short." + +Map to existing model: Scale → `numeric_slider`; Slider → `numeric_free` (genuine continuous) or a dedicated continuous type — **decide and document**; Count → `increment`; Yes-no → `yes_no`; Time → `time`. Allow-multiple → `allowMultiple`; Log-with-period → `showInLogPeriod`. Colour → `colorToken` (role key or hex, or `"inherit"` if filed in a group). + +## New category — step 2 (row 6, Scale only) +Shown only when type = Scale: **Range** (min/max), **Step labels** (per-step optional words → `scaleLabels` via `encodeScaleLabels`), **Allow decimals** (`SwitchRow` → `allowDecimals`). "Save". + +## Category edit (row 5 middle + row 6) +Same two screens **plus**: +- **Reminders** section: list of alarm times with day rules (Every day / Weekdays) and on/off `SwitchRow`s; "+ Add alarm". Wire to the **existing** `CustomAlarm` system + `EditAlarm` route (`Screen.EditAlarm.newForCategory(categoryId)` already exists). Do not build a parallel alarm system. +- Link into scale settings. +- **Danger zone:** "Delete category & its history" (confirm; system categories protected). +- Alarms appear on edit only. + +## Files + +| File | Change | +|---|---| +| `ui/screens/categories/` | New `CategoryEditScreen` (2-step, create + edit) or restructure the existing create/edit path. Reuse `IconPicker`/`RolePicker`/`SwitchRow`/`SegmentedToggle`/`ListCard`. | +| `ui/navigation/Screen.kt` / `MainActivity.kt` | Routes for create (step1→step2) and edit; wire `EditAlarm` linkage. | +| `data/repository/TrackingRepository.kt` | Reuse `addCategory(...)` and `updateCategoryFullSettings(...)`; both already accept the full field set (map 02 §4). | + +## Open decision (§8 #2): categoryType mutability +Today `categoryType` is immutable after creation. The edit flow shows a Type row. Either: +- **Keep immutable:** show type read-only on edit (simplest, safe). Or +- **Allow change:** needs a value-migration story (e.g. switching scale↔count reinterprets stored labels). If allowed, write a repository method that converts existing `TrackingLogValue`s or explicitly warns they'll be reinterpreted. **Decide with the owner before building.** Default to read-only-on-edit unless told otherwise. + +## Acceptance criteria +- [ ] Create a category of each of the 5 types end-to-end; correct fields persist (`addCategory`). +- [ ] Scale step-2 persists range, per-step labels (`scaleLabels`), and decimals. +- [ ] Edit surfaces Reminders; adding one schedules via the existing `CustomAlarm` system and fires. +- [ ] Delete-with-history works with confirmation; system categories cannot be deleted. +- [ ] Step 1 has at most one helper line per group (handover copy discipline). +- [ ] `a11y_check.py` green (icon/role pickers = `Role.RadioButton`; switches = `Role.Switch` + `stateDescription`; content descriptions on icon-only controls). + +## Feature-preservation checklist +- [ ] All fields the current create/edit path can set are still settable (icon, colour, type, numeric range/unit/decimals, scale labels, allow-multiple, show-in-period, track-against-time, mode key). +- [ ] Existing `CustomAlarm`/`EditAlarm` scheduling behaviour unchanged; no duplicate alarm system. +- [ ] Value catalog editing (`ManageCategoryValues`) still reachable for `default` categories. +- [ ] `modeKey` categories (tracking modes) still creatable/editable. + +## Gotchas +- Reuse `addCategory` / `updateCategoryFullSettings` — do not invent new persistence. +- Alarms use `SCHEDULE_EXACT_ALARM` / `POST_NOTIFICATIONS` (already granted). Don't add permissions. +- Medical-disclaimer/privacy surfaces (if touched) must use the body font, never Comfortaa. +- No en/em dashes in any of the new user-facing copy. + +## Changelog fragment +```json +{ "bump": "minor", "added": ["Redesigned category creation with icon, colour role, input type, and per-category reminders"] } +``` diff --git a/docs/design/logging-redesign/phases/phase-8-cleanup.md b/docs/design/logging-redesign/phases/phase-8-cleanup.md new file mode 100644 index 0000000..8cfd104 --- /dev/null +++ b/docs/design/logging-redesign/phases/phase-8-cleanup.md @@ -0,0 +1,45 @@ +# Phase 8 — Cleanup & removal (only after parity) + +**Goal:** retire the superseded paths now that the unified screen and new management flows are proven. This is the **only** phase that deletes code, and every removal must be matched to a proven replacement. + +**Prerequisites:** Phases 5, 6, 7 shipped and their acceptance criteria met. The unified `LogScreen` is flagged on and entry points use it. +**Read first:** `PLAN.md` §3 (run the whole inventory as a regression pass); all prior phase docs' preservation checklists. + +--- + +## Gate (do not start until all true) +- [ ] `LogScreen` is the entry point for calendar tap, FAB, and quick-log, and has been for a full test pass. +- [ ] Every item in `PLAN.md` §3 has a working home in the new UI. +- [ ] No open parity gaps recorded in the progress log. + +## Remove / consolidate +- `ui/screens/log/LogPeriodScreen.kt` + `LogPeriodViewModel.kt` — **only** once `LogScreen` reproduces all of it (map 01 §5). Move any still-unique helper (e.g. episode math) into `LogViewModel` or the repository first. +- `ui/screens/log/LogCategoryScreen.kt` + `LogCategoryViewModel.kt` — once `LogScreen` + `MetricInput` cover single-category logging (including edit-from-notification / edit-from-stats entry points). +- `PinnedCategoryInput` (in `LogPeriodScreen.kt`) — the duplicated input path; deleted with the file. +- The two private `DatePickerDialogWrapper` copies (map 01 §6) — consolidate into one shared composable if still used. +- Dead routes in `Screen.kt` (`LogPeriod`, `LogCategory`) and their `composable(...)` blocks in `MainActivity.kt` — remove after confirming nothing navigates to them (grep `Screen.LogPeriod`, `Screen.LogCategory`, `withId(`, `newEntry`, `editEntry`). + +## Do NOT remove +- Any repository method still used by the new screens (`saveLog`, `updateLogInPlace`, `syncFlowLogsForPeriod`, period episode methods, etc.). +- The `CustomAlarm` system, tracking modes, quick-log, `ManageCategoryValues`, export, Stats/History. +- `SelectableChip` if any non-redesign caller remains (grep first). + +## Final passes +- [ ] `python3 a11y_check.py` — green. +- [ ] `python3 wcag_check.py` — green (and the derived-roles spot-check from Phase 1). +- [ ] Full regression over `PLAN.md` §3: log off-period, log on-period, every input type, create/edit/delete category, groups, archive/reorder, tracking modes, quick-log widget, alarms, Stats, History, export, calendar, PIN-locked widget shows neutral placeholder. +- [ ] Grep for now-dead code: unused imports, `internal` helpers with no callers, string resources for removed screens. +- [ ] Update `LESSONS.md` if the unification surfaced a transferable lesson (CLAUDE.md rule). + +## Acceptance criteria +- [ ] No references to removed screens/routes anywhere (grep clean, CI compiles). +- [ ] All `PLAN.md` §3 features present via the new surfaces. +- [ ] CI green. + +## Changelog fragment +```json +{ "bump": "patch", "changed": ["Removed the superseded separate period and category logging screens now that the unified daily log replaces them"] } +``` + +## After this phase +Update `PLAN.md` §7 progress log to Done for all phases, record final DB version and the commit, and note any features intentionally changed (with owner sign-off) vs preserved. Consider promoting the redesign out of beta only via the manual `-beta.N` drop described in CLAUDE.md — not as part of this phase. diff --git a/docs/design/logging-redesign/subsystem-maps/01-logging-screens.md b/docs/design/logging-redesign/subsystem-maps/01-logging-screens.md new file mode 100644 index 0000000..6a5bdcb --- /dev/null +++ b/docs/design/logging-redesign/subsystem-maps/01-logging-screens.md @@ -0,0 +1,105 @@ +# Subsystem map: Per-day logging screens + +> **Mapped against** +> - Commit: `d07d947` (`d07d947f5b2463eaa08e6521d3228026c55b2bef`) +> - versionCode **116**, versionName **0.53.0-beta.1**, DB schema version **23** +> - Date: 2026-08-22 +> +> **Staleness check for future sessions:** run `git diff d07d947 -- app/src/main/java/com/mapgie/goflo/ui/screens/log/` before trusting the line numbers below. If any log-screen file changed, re-read it. The *shape* of the description (two separate screens, `categoryType` discriminator) is durable; exact line numbers drift. + +## Overview: two truly separate destinations + +"Log Period" and "Log Category" are **fully separate screens, routes, ViewModels, and repositories**. They share only two small helpers: the `LogEntryTopBar` composable and a private (duplicated) `DatePickerDialogWrapper`. Period logging is a bespoke multi-section day editor on `PeriodRepository`; category logging is a single generic input on `TrackingRepository`. + +| | Log Category | Log Period | +|---|---|---| +| Route | `log_category/{categoryId}?date={date}&logId={logId}` (`Screen.kt:60-68`) | `log_period?periodId={periodId}&startDate={startDate}` (`Screen.kt:12-17`) | +| ViewModel | `LogCategoryViewModel` (`TrackingRepository` only) | `LogPeriodViewModel` (`PeriodRepository` + optional `TrackingRepository`, `Application`, `AppPreferencesStore`) | +| Back callback param | `onNavigateBack` | `onBack` | + +## 1. Route arguments & navigation wiring + +**LogCategory** — `MainActivity.kt:567-592` +- Args: `categoryId: Long` (required), `date: String?` (nullable ISO, defaults null→today), `logId: Long` (default `-1L`, `-1` coerced to null via `.takeIf { it != -1L }`). +- Entry builders (`Screen.kt:63-67`): `newEntry(categoryId, date)`, `editEntry(categoryId, logId)`. New vs edit is distinguished purely by whether `logId` is present. +- VM key: `"log_cat_${categoryId}_${dateStr}_${logId}"`. + +**LogPeriod** — `MainActivity.kt:358-373` +- Args: `periodId: Long` (default `-1L`), `startDate: String?`. `periodId > 0` ⇒ edit existing episode; `-1` ⇒ new entry. +- Builders (`Screen.kt:13-16`): `withId(periodId, targetDate?)`, `newEntry`, `newEntryForDate(date)`. +- VM key: `"log_${periodId}_${startDate}"`. + +## 2. Input control types (the full catalog) + +Control selection is driven by `TrackingCategory.categoryType` (a `String`) plus flags `trackAgainstTime`, `allowDecimals`, `allowMultiple`, and range fields `numericMin`/`numericMax`/`numericUnit`/`scaleLabels`. + +### In LogCategoryScreen — branch at `LogCategoryScreen.kt:499-586` +```kotlin +val cat = state.category +when { + cat?.categoryType == "numeric_slider" -> NumericSliderSection(...) + cat?.categoryType == "numeric_free" -> NumericFreeInputSection(...) + cat?.categoryType == "increment" && cat.trackAgainstTime -> TimedIncrementSection(...) + cat?.categoryType == "increment" -> IncrementSection(...) + else -> /* text value chips (SelectableChip in a FlowRow) */ +} +``` +Five rendered controls: +1. **`numeric_slider`** → `NumericSliderSection` (`:144-230`). Material3 `Slider` bounded to `numericMin..numericMax`; `steps = 0` when `allowDecimals` else `range-1` whole steps; large value readout; optional per-value text label from `scaleLabels.decodeScaleLabels()`. +2. **`numeric_free`** → `NumericFreeInputSection` (`:232-262`). `OutlinedTextField`, `KeyboardType.Decimal`, label = `numericUnit` or "Value". +3. **`increment` + `trackAgainstTime`** → `TimedIncrementSection` (`:330-411`). "Log +1 now" button; each tap **saves a new log immediately** with an `HH:mm` timestamp; lists today's timestamped entries with per-entry delete. No notes/Save button. +4. **`increment`** (untimed) → `IncrementSection` (`:271-324`). Big count + "Add one" button and a decrement `IconButton`; count held in `numericValue`. +5. **else (text)** → `SelectableChip` chips in a `FlowRow` (`:534-585`), multi-select. Also renders "Previously recorded (removed from options)" chips for stored labels no longer in the catalog. + +Shared extras (only when NOT timed-increment, `:591-635`): optional "Track against time" `Checkbox`, a Notes `OutlinedTextField` (500-char cap), and the Save/Update `Button`. A `DateSelectorCard` (`:76-110`) appears at top only when `state.canEditDate`. + +### In LogPeriodScreen — pinned-category branch at `LogPeriodScreen.kt:483-616` +`PinnedCategoryInput` re-implements the **same** four-way `when(category.categoryType)` (`numeric_slider` / `numeric_free` / `increment` / else-chips). This is a **parallel, duplicated** rendering path — the two screens do not share input composables. The period increment control uses a "+1" button and, unlike the category screen, is not timed. + +## 3. How LogCategoryScreen decides which control to render + +Solely on `state.category?.categoryType`, with a secondary check on `trackAgainstTime` to split timed vs untimed increment. There is no separate "input type" enum — `categoryType` is the discriminator string. + +## 4. Exact save flow + +### LogCategoryViewModel.save() — `LogCategoryViewModel.kt:243-296` +1. Early return if timed-increment (those save per-tap). +2. Build `valuesToSave: Set` by type: `numeric_slider` → `numericValue ?: numericMin` formatted; `numeric_free` → trimmed text, **blocks save if empty**; `increment` → `numericValue.toInt()`, **blocks if ≤ 0**; else → `state.selectedValues` (chip set). +3. `loggedAt` = `HH:mm` if `trackTime` else `""`. +4. Persist: edit → `repository.updateLogInPlace(...)`; else → `repository.saveLog(date, categoryId, selectedValues, notes, allowMultiple, loggedAt)`. +5. `saved=true` → screen `LaunchedEffect` pops back. + +Other VM methods: `setNumericValue`, `setNumericFreeText`, `toggleValue`, `setNotes`, `setTrackTime`, `setDate`, `addTimedIncrement`, `deleteTimedEntry`, `delete`. + +### LogPeriodViewModel.save() — `LogPeriodViewModel.kt:353-394` +Heavier; the unit is a single **day**: +1. Editing → `logPeriodDay(date)` then `updateEpisode(id, start, end, notes, tolerance)`. +2. New with end date → `logPeriodRange(date, endDate, tolerance)`. +3. New single day → `logPeriodDay(date, tolerance)`. +4. If episode returned → `updateEpisodeMeta(id, notes, flowLevel)`. +5. **Mirror per-day values into the tracking system**: `syncFlowToTrackingLog`, `syncSymptomsToTrackingLog`, `syncPinnedCategoryLogs` — each calls `trackingRepository.saveLog(...)` so period data also appears under Flow/Symptoms/pinned categories in Stats. +6. Side effects: `GoFloWidget.updateAllWidgets`, `ReminderScheduler.refreshPredictionReminders`. +7. `saved=true` → screen pops. + +Extra period-only actions: `removeDay()` → `unlogPeriodDay`, `delete()` → deletes episode + its per-day tracking logs, `disablePeriodTracking()`. + +## 5. How period logging differs from generic category logging + +- **Composite fixed layout.** Period screen stacks: Day picker, Period-dates/End-date, dedicated **Flow** section (slider *or* chips), **Symptoms** section (chips + an "Add" `AssistChip` opening `AddSymptomDialog`), **pinned categories** (`state.pinnedCategories` each via `PinnedCategoryInput`), Notes, Save, edit-only "Remove this day" / "Delete Entire Period". The category screen renders exactly **one** input for **one** category. +- **Flow slider→label mapping.** 1→Spotting, 2→Light, 4→Heavy, else Medium (`LogPeriodViewModel.kt:299-308`, `:542-547`), unlike generic sliders which store the raw numeric string. +- **Episode/continuation logic.** Period VM computes `continuesEpisodeStart`, `episodeDayNumber`, gap `toleranceDays` from prefs, open-ended vs range periods — absent from category logging. +- **Unsaved-changes guard.** Period screen tracks `hasChanges` and shows a discard/save dialog on back; category screen has none. +- **Overflow menu** with "Disable period logging". +- **Symptoms added inline** via `AddSymptomDialog` → `addNewSymptomToLibrary` → `trackingRepository.addValueToCategory`; category screen cannot create new values (directs to Settings). +- **Repository split.** Period save writes to `PeriodRepository` (episode model) *and* fans out to `TrackingRepository`; category save writes only `TrackingRepository`. + +## 6. Shared pieces (redesign-relevant) + +- **`LogEntryTopBar`** (`LogEntryTopBar.kt:23-55`): `internal` composable, params `title, subtitle, onBack, actions`. Used by both screens. primaryContainer-coloured header. +- **`DatePickerDialogWrapper`**: defined **privately in each screen file** (`LogCategoryScreen.kt:114-134` vs `LogPeriodScreen.kt:626-650`; period version adds a `minDate`). Duplicated, not shared. +- **`SelectableChip`** (`ui.components`) and **`decodeScaleLabels()`** (`ui.util`) are shared. +- **`SectionLabel`** is private to `LogPeriodScreen.kt:620-622` only. + +## Key duplication to consolidate + +The four-way `categoryType` rendering exists in **two independent implementations**: `LogCategoryScreen`'s top-level `when` + its section composables, and `LogPeriodScreen`'s `PinnedCategoryInput`. The slider step/label/formatting logic is copy-pasted between them. A unified `MetricInput` façade consolidates these two paths — see the plan's Phase 4. diff --git a/docs/design/logging-redesign/subsystem-maps/02-category-data-model.md b/docs/design/logging-redesign/subsystem-maps/02-category-data-model.md new file mode 100644 index 0000000..8ad0dd6 --- /dev/null +++ b/docs/design/logging-redesign/subsystem-maps/02-category-data-model.md @@ -0,0 +1,101 @@ +# Subsystem map: Category / tracking data model & DB layer + +> **Mapped against** +> - Commit: `d07d947` (`d07d947f5b2463eaa08e6521d3228026c55b2bef`) +> - versionCode **116**, versionName **0.53.0-beta.1**, DB schema version **23** +> - Date: 2026-08-22 +> +> **Staleness check for future sessions:** the DB class is `data/database/GoFloDatabase.kt`. Confirm its `version = N` before writing a migration — if it is no longer **23**, someone added migrations after this map; read them and target `N → N+1`. Run `git diff d07d947 -- app/src/main/java/com/mapgie/goflo/data/` to see drift. + +All paths under `app/src/main/java/com/mapgie/goflo/`. **The DB class is `GoFloDatabase.kt`, not `AppDatabase.kt`.** + +## 1. Entity schemas + +### TrackingCategory — table `tracking_categories` (`entities/TrackingCategory.kt`) +The central category entity. **Note how much already exists** — icons, colour token, input type, allow-multiple, log-with-period, and time-tracking are all present today. + +| Field | Type | Default | Notes | +|---|---|---|---| +| `id` | `Long` | `0` autoGen PK | | +| `name` | `String` | — | user-visible name | +| `isSystem` | `Boolean` | `false` | Flow/Symptoms seeded true; can't delete (UI-enforced) | +| `systemKey` | `String` | `""` | stable key: `"flow"`, `"symptoms"`, else `""` | +| `displayOrder` | `Int` | `0` | | +| `iconName` | `String` | `"category"` | → `CategoryIcon.key` (20 curated icons) | +| `colorToken` | `String` | `"secondary"` | semantic token OR 8-char AARRGGBB hex | +| `categoryType` | `String` | `"default"` | input-type discriminator; **immutable after creation (current rule)** | +| `numericMin` | `Float` | `0f` | | +| `numericMax` | `Float` | `10f` | | +| `allowDecimals` | `Boolean` | `false` | | +| `numericUnit` | `String` | `""` | suffix e.g. "°C" | +| `scaleLabels` | `String` | `""` | newline `value=label` pairs, slider only | +| `isArchived` | `Boolean` | `false` | hides from logging UI | +| `allowMultiple` | `Boolean` | `false` | multiple logs per day | +| `showInLogPeriod` | `Boolean` | `false` | pin to Log Period screen | +| `trackAgainstTime` | `Boolean` | `false` | enables `loggedAt` time on logs | +| `modeKey` | `String` | `""` | links to a tracking-mode preset | + +Computed (not a column): `val isNumeric get() = categoryType != "default"`. + +### TrackingValue — table `tracking_values` +Catalog of selectable options for a category (this is how "Spot/Light/Med/Heavy" are stored). +- `id: Long` PK, `categoryId: Long` (FK → categories, `ON DELETE CASCADE`, indexed), `label: String`, `displayOrder: Int = 0`, `isSeeded: Boolean = false` (protects shipped values from deletion). + +### TrackingLog — table `tracking_logs` +Per-(date,category) entry (one row per pair, unless `allowMultiple`). +- `id: Long` PK, `date: String` (ISO), `categoryId: Long` (FK cascade; indices on `categoryId` and `date`), `notes: String = ""`, `loggedAt: String = ""` (HH:mm; empty when not time-tracked). + +### TrackingLogValue — table `tracking_log_values` +Selected values on a log — a **string snapshot**, NOT an FK to `TrackingValue` (so renames/deletes don't corrupt history). +- `id: Long` PK, `logId: Long` (FK → logs cascade, indexed), `valueLabel: String`. + +### ColorProfile — table `color_profiles` +Saved custom **app-theme** palette slots (unrelated to per-category colour). +- `id, name="", primaryArgb=0, secondaryArgb=0, tertiaryArgb=0, lightBackgroundArgb=0, darkBackgroundArgb=0` (0 = Auto). + +### PeriodEntry — table `periods` +Derived period *episode*. `id, startDate, endDate: String? = null, flowLevel="Medium", notes=""`. + +### PeriodDayEntry — table `period_days` +Per-day source of truth; `date` unique-indexed. `id, date`. + +### SymptomEntry — table `symptoms` +Legacy per-period symptom, FK → `PeriodEntry` cascade. `id, periodId, symptomType` (stored as display label after MIGRATION_14_15). + +## 2. Database config — `data/database/GoFloDatabase.kt` + +- **Current version: 23**, `exportSchema = false`. +- **@Database entities:** `PeriodEntry, PeriodDayEntry, SymptomEntry, TrackingCategory, TrackingValue, TrackingLog, TrackingLogValue, CustomAlarm, CustomAlarmCategory, ColorProfile`. +- Fresh installs seed via `onCreate → seedSystemCategories`: Flow (icon `water`, token `primary`, values Spotting/Light/Medium/Heavy) and Symptoms (icon `healing`, token `tertiary`, values Cramps/Headache/Bloating/Fatigue/Back Pain/Mood Swings/Bleeding (non-period)). Values seeded with `isSeeded=1`. +- `PRAGMA foreign_keys = ON` re-applied on every open. + +### Migration chain (all registered in `.addMigrations(...)`, chain 1→23) +1_2 create `custom_symptoms` (later dropped) · 2_3 create the 4 tracking tables + seed Flow/Symptoms · 3_4 add `iconName`+`colorArgb` · 4_5 rebuild → replace `colorArgb` with `colorToken` · 5_6 add `isNumeric,numericMin,numericMax,allowDecimals` · 6_7 rebuild → replace `isNumeric` with `categoryType`, add `numericUnit,isArchived` · 7_8 add `allowMultiple` · 8_9 add `showInLogPeriod` · 9_10 add `scaleLabels` · 10_11 add `systemKey` · 11_12 add `trackAgainstTime`+`loggedAt` · 12_13 insert "Bleeding (non-period)" · 13_14 add `isSeeded` · 14_15 convert enum names→labels, un-seed flow/symptom values, migrate custom_symptoms→values, drop custom_symptoms · 15_16 seed "Ovulation Test" · 16_17 add `modeKey` · 17_18 create `custom_alarms`+`custom_alarm_categories` · 18_19 demote Ovulation Test to non-system, `modeKey='ovulation_test'` · 19_20 `showInLogPeriod=1` for flow/symptoms · 20_21 create `color_profiles` · 21_22 add light/dark background argb · 22_23 create `period_days` (unique date), backfill from episode ranges. + +**Pattern to follow:** additive column adds use `ALTER TABLE ... ADD COLUMN`; type changes do a full table rebuild (create-new, copy, drop, rename). Never `fallbackToDestructiveMigration` (forbidden by CLAUDE.md). + +## 3. DAOs + +### TrackingCategoryDao +Categories: `getAllCategories()`/`getActiveCategories()` (isArchived=0) Flows; `getCategoryById`/`…Once`; `getAllCategoriesOnce`; lookups `getSystemCategoryByName/ByKey`, `getCategoryByName`, `getCategoryByModeKey`; `getShowInLogPeriodCategoriesOnce`. Mutations: `insertCategory` (REPLACE→Long), `updateCategory`, `deleteCategory`, `deleteAllCustomCategories`, `unarchiveAllSystemCategories`. +Values: `getValuesForCategory`/`…Once`, `insertValue` (IGNORE), `updateValue`, `deleteValue`, `bulkRenameLogValues(categoryId, oldLabel, newLabel)`. + +### TrackingLogDao +Logs: `getLogsForDate`/`…Once`; `getAllLogDates`; `getLogById`/`…Once`; `getLogForDateAndCategory` (LIMIT 1); `getLogsForDateAndCategory` (multiple, ordered by loggedAt); `insertLog` (REPLACE→Long), `updateLog`, `deleteLog`. Log values: `getLogValuesForLog`/`…Once`, `insertLogValue`, `deleteLogValuesForLog`. Stats/export: `getLogsForCategoryInRange`, `getValueCountsForCategory` (→ `ValueCount`), `getAllLogsInRange`, `getLogsForCategoriesInRange`, `getAllLogsForCategories`, `getLogValuesForLogs`, `getEarliest/LatestLogDate`, delete ranges/date/all. + +## 4. Repository — `data/repository/TrackingRepository.kt` +Constructor: `TrackingRepository(categoryDao, logDao, symptomDao?)`. Wrapper `TrackingLogWithValues(log, category, values: List)`. + +Category CRUD: `getAllCategories()/getActiveCategories(): Flow>`, `getAllCategoriesOnce()`, `getShowInLogPeriodCategoriesOnce()`, `getCategoryById(id)/…Once`, `getValuesForCategory(id)/…Once`, `addCategory(name, iconName, colorToken, categoryType, numericMin, numericMax, allowDecimals, numericUnit, scaleLabels, allowMultiple, showInLogPeriod, trackAgainstTime, modeKey): Long`, `renameCategory`, `updateCategoryAppearance(id, iconName, colorToken)`, `updateCategoryFullSettings(...)`, `updateTrackAgainstTime`, `updateNumericSettings`, `updateNumericUnit`, `updateShowInLogPeriod`, `updateAllowMultiple`, `updateFlowCategoryMode(id, useSlider)`, `archiveCategory`, `unarchiveCategory`, `deleteCategory` (guards `isSystem`), `reorderCategories`, `getExistingModeKeys`. Values: `addValueToCategory`, `deleteValue`, `renameValue(value, newLabel, fixHistorical)`. + +Log CRUD: `getLogsForDate(date): Flow>`, `getAllLogDates(): Flow>`, `saveLog(date, categoryId, selectedValues, notes, allowMultiple=false, loggedAt=""): Long` (upsert unless allowMultiple; deletes+re-inserts log values each time), `updateLogInPlace(...)`, `deleteLog`, `incrementLog(date, categoryId, delta=1): Int`, `getExistingLog`, `getLogsForDateAndCategory`, `getLogById`, stats/export accessors, `deleteAllLogs`, `resetCategoryConfiguration`, `deleteLogsForPeriod`, `syncFlowLogsForPeriod`. + +## 5. Answers to the model questions that matter for the redesign + +- **Category colour** = single `TrackingCategory.colorToken: String` (default `"secondary"`). Holds *either* a semantic token (`primary`/`secondary`/`tertiary`) *or* an 8-char AARRGGBB hex. Resolution in `ui/util/CategoryAppearance.kt`. **There is no "role" concept beyond these 3 tokens, and no group.** → The handover's `FIXED(argb)` role maps directly onto the existing hex path. The handover's `QUATERNARY/QUINARY/SENARY` are new tokens to add. +- **Selectable values** = `TrackingValue` rows (ordered, `isSeeded`). Numeric/slider categories use `numericMin/Max/allowDecimals/numericUnit/scaleLabels` instead. Logged value is snapshotted as `TrackingLogValue.valueLabel`. +- **Input type** already exists as `categoryType` (`default`/`numeric_slider`/`numeric_free`/`increment`), enum `CategoryType` in `CategoryAppearance.kt`. → The handover's Scale/Slider/Count map onto existing types; **Yes/No and Time are new**. +- **Icons** already exist (`iconName` → `CategoryIcon`, 20 icons). +- **Allow-multiple-per-day** already exists (`allowMultiple`, honoured in `saveLog`). +- **Timestamps / timeline** partially exist (`trackAgainstTime` + `loggedAt` + `TimedIncrementSection`). → The handover's generalised timeline extends this to all input types. +- **Groups / roles**: **NONE exist.** No group entity, no role field, no parent-child. Closest existing mechanisms: `modeKey`, `systemKey`, `showInLogPeriod`, `custom_alarm_categories` join table. A group/role model is entirely additive. diff --git a/docs/design/logging-redesign/subsystem-maps/03-theme-color-machinery.md b/docs/design/logging-redesign/subsystem-maps/03-theme-color-machinery.md new file mode 100644 index 0000000..401af2e --- /dev/null +++ b/docs/design/logging-redesign/subsystem-maps/03-theme-color-machinery.md @@ -0,0 +1,86 @@ +# Subsystem map: Theme / colour machinery + +> **Mapped against** +> - Commit: `d07d947` (`d07d947f5b2463eaa08e6521d3228026c55b2bef`) +> - versionCode **116**, versionName **0.53.0-beta.1**, DB schema version **23** +> - Date: 2026-08-22 +> +> **Staleness check for future sessions:** `colorSchemeFor`'s signature is load-bearing (the handover says extend, don't change it). Confirm it before wiring role derivation: `grep -n "fun colorSchemeFor" app/src/main/java/com/mapgie/goflo/ui/theme/Color.kt`. Run `git diff d07d947 -- app/src/main/java/com/mapgie/goflo/ui/theme/ app/src/main/java/com/mapgie/goflo/ui/util/CategoryAppearance.kt` for drift. + +All paths under `app/src/main/java/com/mapgie/goflo/`. + +## 1. `ui/theme/Color.kt` (~1520 lines) + +**Imports:** `ColorScheme`, `lightColorScheme`, `darkColorScheme`, `Color`, `Color.luminance`, `Color.toArgb`, `androidx.core.graphics.ColorUtils`. **No** `androidx.compose.material3.dynamiccolor`, `Hct`, `TonalPalette`, or `dynamicLight/DarkColorScheme` anywhere in the project. House style for computed colour is **HSL via `Color.hsl(...)` and `ColorUtils.colorToHSL`**, plus `lerp` — not HCT. + +**`AppTheme` enum:** constructor `(displayName, group, previewArgb: Long, isDark = false)`. ~45 entries across `ThemeGroup` LIGHT/DARK/SYSTEM/HIGH_CONTRAST/COLOR_BLIND. 12 palette families (Coral, Turquoise/"Teal", Green/"Sage", SummerCandy, BeachVibes, PeachMelba, Disco, MetalChick, Whimsy, ColourHappy, DragonFire, MidnightNeon) each with `_DARK` and `_SYSTEM` variants, plus `HIGH_CONTRAST_LIGHT/DARK`, `BLUE_ORANGE`, `CUSTOM`. **Entry names are persisted to DataStore — do not rename.** + +**`colorSchemeFor(...)` — the signature you must NOT break:** +```kotlin +fun colorSchemeFor(theme: AppTheme, systemIsDark: Boolean = false, wcag: Boolean = false): ColorScheme +``` +Public, top-level (~line 1281). If `!wcag` or theme is HIGH_CONTRAST_*/BLUE_ORANGE → delegates to `private fun standardColorSchemeFor(theme, systemIsDark)`; otherwise a `when(theme)` returns the `*Wcag` variant. Called from exactly one place: `GoFloTheme` in `Theme.kt`. + +**Private `val` ColorScheme count:** ~51. Standard (27): `{Palette}Light`/`{Palette}Dark` + `HighContrastLight/Dark` + `BlueOrange`. WCAG AAA (24): `{Palette}Light Wcag`/`{Palette}DarkWcag` for all 12 families. Each built with `lightColorScheme(...)`/`darkColorScheme(...)` specifying only standard Material3 roles. + +**Existing tonal-derivation helper — the pattern to mirror:** `buildCustomColorScheme(...)` (~lines 1383-1519), the ONLY place colours are computed: +```kotlin +fun buildCustomColorScheme( + primaryHue: Float, secondaryHue: Float, tertiaryHue: Float, + primaryArgb: Int = 0, secondaryArgb: Int = 0, tertiaryArgb: Int = 0, + backgroundArgb: Int = 0, isDark: Boolean, +): ColorScheme +``` +Derives containers/on-colours/surfaces via `Color.hsl(hue, s, l)`, computes WCAG-safe on-colours from relative luminance (`onArgb`, manual sRGB linearization), uses `ColorUtils.colorToHSL` for background overrides. **This is the reference for deriving quaternary/quinary/senary — HSL manipulation, not HCT/TonalPalette.** + +**Material3 "extra role" pattern:** None. `ColorScheme` exposes only primary/secondary/tertiary accents (plus error, surface family). There is no 4th/5th/6th accent slot. **Derived roles cannot live inside the returned `ColorScheme`** — use a parallel holder (data class / CompositionLocal) or derive on the fly at the resolution site. + +## 2. `ui/theme/Theme.kt` (~73 lines) + +`@Composable fun GoFloTheme(appTheme, wcag, customHues, customArgbs, customThemeMode, customLightBackgroundArgb, customDarkBackgroundArgb, content)`. +- Scheme choice: `appTheme == CUSTOM && customHues != null` → `buildCustomColorScheme(...)`, else `colorSchemeFor(appTheme, systemDark, wcag)`. +- `systemDark = isSystemInDarkTheme()`. +- Computes `effectivelyDark` to flip status-bar icon contrast via `WindowCompat...isAppearanceLightStatusBars` in a `SideEffect`. +- Applies: `MaterialTheme(colorScheme = colorScheme, typography = GoFloTypography, content = content)`. Does NOT read DataStore — the active theme is passed in by MainActivity. + +**Where to inject a role holder:** if roles are derived once per theme, compute them here and provide via a `CompositionLocalProvider` wrapping `MaterialTheme`, so `String.toCategoryColor()` can read them without re-deriving per call. + +## 3. `ui/theme/Type.kt` (~43 lines) + +- Downloadable Google Font provider (`com.google.android.gms.fonts`). +- `val ComfortaaFamily: FontFamily` — single `GoogleFont("Comfortaa")` at `FontWeight.Bold`. **`GoFloTypography` does not wire `ComfortaaFamily` into any TextStyle** (styles set only weight/size/lineHeight); the family is exported for use elsewhere (app title). The design calls for Comfortaa on screen titles and `ToneHero` words — those composables must apply `ComfortaaFamily` explicitly. +- `val GoFloTypography = Typography(...)`: headlineLarge/Medium, titleLarge/Medium, bodyLarge/Medium, labelLarge/Medium. + +## 4. Theme persistence — DataStore + +Store: `AppPreferencesStore` (in `data/preferences/ReminderPreferences.kt`). Backing store `preferencesDataStore(name = "goflo_prefs")`. +- Key: `val THEME = stringPreferencesKey("theme")`. Read: `preferences: Flow` maps `theme = prefs[Keys.THEME] ?: "CORAL"`. Write: `suspend fun setTheme(theme: String)`. +- Also holds CUSTOM theme fields: `customPrimaryHue/Argb`, `customSecondary*`, `customTertiary*`, `customLight/DarkBackgroundArgb`, `customThemeName`, `customThemeMode`. +- Consumers: `MainActivity` reads `preferencesStore.preferences`, `AppTheme.valueOf(appPrefs.theme)` (fallback CORAL) → `GoFloTheme(...)`. `SettingsViewModel.setTheme` wraps `store.setTheme`. Store exposed as `GoFloApplication.preferencesStore`. + +## 5. Category colour resolution at render time — the hook to extend + +Stored on `TrackingCategory.colorToken: String = "secondary"`. Resolution lives in **`ui/util/CategoryAppearance.kt`**: +- `enum class CategoryColor(key, displayName)`: only `PRIMARY`, `SECONDARY`, `TERTIARY`. **Add new roles here.** +- `@Composable fun String.toCategoryColor(): Color`: +```kotlin +val s = MaterialTheme.colorScheme +return when (this) { + "primary" -> s.primary + "secondary" -> s.secondary + "tertiary" -> s.tertiary + else -> runCatching { Color(toLong(16)) }.getOrDefault(s.secondary) // hex path = FIXED role +} +``` +- `@Composable fun String.toCategoryOnColor(): Color`: mirror for icon/text tint (onPrimary/… or luminance-based near-black/white for hex). +- Shade helpers: `ordinalShade(base, surface, index, total)`, `continuousShade(base, surface, fraction)` (use `lerp`). +- `CATEGORY_COLOR_OPTIONS: List`: fixed hex palette in the "More colours" picker (`ManageCategoriesScreen.kt`). + +Calendar (`ui/components/CalendarGrid.kt`) does NOT use category tokens — it reads `MaterialTheme.colorScheme.primary/tertiary/secondary` directly for period/ovulation dots. + +## Takeaways for adding quaternary/quinary/senary roles + +1. **Extend around `colorSchemeFor`, don't change its signature.** Material3 `ColorScheme` has no 4th-6th accent slot, so store derived roles in a parallel holder (CompositionLocal set up in `Theme.kt`) or derive at the resolution site in `CategoryAppearance.kt`. +2. **Derive with HSL** (`Color.hsl`, `ColorUtils.colorToHSL`, `lerp`) mirroring `buildCustomColorScheme` — no new HCT/TonalPalette dependency. A defensible derivation: rotate the hue of primary/secondary/tertiary by a fixed offset (or interpolate between the existing three accents) and pin saturation/lightness to the scheme's own accent band so contrast stays in range. +3. **On-colours must stay WCAG-safe.** Compute the on-colour from luminance (as the hex path already does) so derived roles pass ≥3:1 for icons / ≥4.5:1 for text. Re-run `wcag_check.py` (and note derived roles aren't in the palette tables, so add a spot-check). +4. **`FIXED` needs no new storage** — the existing hex path in `colorToken` already means "don't change on theme switch". Extended in-theme roles are just three new token strings (`quaternary`/`quinary`/`senary`) added to both resolver `when` branches and the `CategoryColor` enum. **No DB migration required to reference them** (colorToken is free-form).