diff --git a/messages/de.json b/messages/de.json index 291fb25a5..c09d62ccd 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1612,8 +1612,8 @@ "dragHandle": "Zum Sortieren ziehen", "show": "Einblenden", "hide": "Ausblenden", - "manageTiles": "Kacheln verwalten", "manageSubPages": "Detailseiten verwalten", + "manageSubPagesHint": "Ausgeblendete Seiten verschwinden auch aus der oberen Navigation.", "groupSleep": "Schlaf", "groupMood": "Stimmung", "groupEvents": "Ereignisse", diff --git a/messages/en.json b/messages/en.json index 0de52041d..a8d9cf9a4 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1614,8 +1614,8 @@ "dragHandle": "Drag to reorder", "show": "Show", "hide": "Hide", - "manageTiles": "Manage tiles", "manageSubPages": "Manage detail pages", + "manageSubPagesHint": "Hidden pages are also removed from the top navigation.", "groupSleep": "Sleep", "groupMood": "Mood", "groupEvents": "Events", diff --git a/messages/es.json b/messages/es.json index f35579dd7..d11008a7e 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1614,8 +1614,8 @@ "dragHandle": "Arrastra para reordenar", "show": "Mostrar", "hide": "Ocultar", - "manageTiles": "Gestionar tarjetas", "manageSubPages": "Gestionar páginas de detalle", + "manageSubPagesHint": "Las páginas ocultas también se quitan de la navegación superior.", "groupSleep": "Sueño", "groupMood": "Estado de ánimo", "groupEvents": "Eventos", diff --git a/messages/fr.json b/messages/fr.json index ab529b6d2..ab8aaf052 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1614,8 +1614,8 @@ "dragHandle": "Fais glisser pour réorganiser", "show": "Afficher", "hide": "Masquer", - "manageTiles": "Gérer les tuiles", "manageSubPages": "Gérer les pages de détail", + "manageSubPagesHint": "Les pages masquées disparaissent aussi de la navigation supérieure.", "groupSleep": "Sommeil", "groupMood": "Humeur", "groupEvents": "Événements", diff --git a/messages/it.json b/messages/it.json index b8a08ffd5..1df6a9855 100644 --- a/messages/it.json +++ b/messages/it.json @@ -1614,8 +1614,8 @@ "dragHandle": "Trascina per riordinare", "show": "Mostra", "hide": "Nascondi", - "manageTiles": "Gestisci riquadri", "manageSubPages": "Gestisci pagine di dettaglio", + "manageSubPagesHint": "Le pagine nascoste vengono rimosse anche dalla navigazione in alto.", "groupSleep": "Sonno", "groupMood": "Umore", "groupEvents": "Eventi", diff --git a/messages/pl.json b/messages/pl.json index 0cfce2803..bd5133174 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -1614,8 +1614,8 @@ "dragHandle": "Przeciągnij, aby zmienić kolejność", "show": "Pokaż", "hide": "Ukryj", - "manageTiles": "Zarządzaj kafelkami", "manageSubPages": "Zarządzaj stronami szczegółów", + "manageSubPagesHint": "Ukryte strony znikają również z górnej nawigacji.", "groupSleep": "Sen", "groupMood": "Nastrój", "groupEvents": "Zdarzenia", diff --git a/src/components/insights/__tests__/insights-tab-strip.test.tsx b/src/components/insights/__tests__/insights-tab-strip.test.tsx index 1c8967edd..3646d0efc 100644 --- a/src/components/insights/__tests__/insights-tab-strip.test.tsx +++ b/src/components/insights/__tests__/insights-tab-strip.test.tsx @@ -16,6 +16,7 @@ vi.mock("sonner", () => ({ toast: { success: vi.fn() } })); import { InsightsTabStrip } from "../insights-tab-strip"; import type { InsightInputs } from "@/lib/insights/metric-availability"; import type { DataSummary } from "@/lib/analytics/trends"; +import { DEFAULT_INSIGHTS_LAYOUT } from "@/lib/insights-layout"; function render(node: React.ReactNode, locale: "en" | "de" = "en") { return renderToStaticMarkup( @@ -339,4 +340,51 @@ describe(" — saved-layout visibility gate (v1.15.14 W2)", () expect(html).toContain('data-group="activity"'); expect(html).toContain(">Activity<"); }); + + // v1.15.14 — regression guard: the DEFAULT layout's visible set must equal + // the data-only nav set. The v1.15.11 curated default dropped ~20 pills once + // the strip began gating on the layout; making the default all-visible + // restores everything-with-data. Derive `visibleTileIds` straight from + // `DEFAULT_INSIGHTS_LAYOUT` (what a fresh / never-customized account + // resolves to) and assert the long-tail pills + group parents still show + // when their data is present. + it("default layout shows every data-having pill (nav-pill regression guard)", () => { + const visibleTileIds = new Set( + DEFAULT_INSIGHTS_LAYOUT.tiles + .filter((t) => t.visible) + .map((t) => t.id), + ); + const availability: InsightInputs = { + summaries: { + SLEEP_DURATION: fakeSummary(20), + ACTIVITY_STEPS: fakeSummary(4200), + ACTIVE_ENERGY_BURNED: fakeSummary(30), + FAT_MASS: fakeSummary(12), + MUSCLE_MASS: fakeSummary(12), + AUDIO_EXPOSURE_ENV: fakeSummary(40), + TIME_IN_DAYLIGHT: fakeSummary(15), + }, + hasMood: false, + hasMedication: false, + }; + const html = render( + , + ); + // Flat pill that previously regressed out of the default nav. + expect(html).toContain(">Sleep<"); + // Group parents that the curated default suppressed: Activity (steps / + // active-energy), Body (fat-mass / muscle-mass), Hearing (audio), + // Environment (daylight). + expect(html).toContain('data-group="activity"'); + expect(html).toContain(">Activity<"); + expect(html).toContain('data-group="body"'); + expect(html).toContain(">Body<"); + expect(html).toContain('data-group="hearing"'); + expect(html).toContain(">Hearing<"); + expect(html).toContain('data-group="environment"'); + expect(html).toContain(">Environment<"); + }); }); diff --git a/src/components/insights/insights-edit-mode.tsx b/src/components/insights/insights-edit-mode.tsx index 4078ebedf..6fe2599ec 100644 --- a/src/components/insights/insights-edit-mode.tsx +++ b/src/components/insights/insights-edit-mode.tsx @@ -400,6 +400,15 @@ export function InsightsEditMode({ /> {t("insights.editMode.manageSubPages")} + {/* v1.15.14 — flag that hiding a detail page also drops + its top-nav pill, so a user isn't surprised a metric + vanished from the navigation. */} +

+ {t("insights.editMode.manageSubPagesHint")} +

{tilesOpen && ( // v1.15.14 W2 — the manager now lists EVERY sub-page // slug grouped by category, the same `tiles` layout the diff --git a/src/components/insights/mood/mood-distribution-chart.tsx b/src/components/insights/mood/mood-distribution-chart.tsx index e9cdd5160..302c784a5 100644 --- a/src/components/insights/mood/mood-distribution-chart.tsx +++ b/src/components/insights/mood/mood-distribution-chart.tsx @@ -61,13 +61,25 @@ export function MoodDistributionChart({ diff --git a/src/components/insights/mood/mood-weekday-chart.tsx b/src/components/insights/mood/mood-weekday-chart.tsx index ab0e1777f..cebd23e81 100644 --- a/src/components/insights/mood/mood-weekday-chart.tsx +++ b/src/components/insights/mood/mood-weekday-chart.tsx @@ -85,14 +85,22 @@ export function MoodWeekdayChart({ weekday }: { weekday: MoodWeekdayRow[] }) { > diff --git a/src/lib/__tests__/insights-layout.test.ts b/src/lib/__tests__/insights-layout.test.ts index 12689687f..8813c96e3 100644 --- a/src/lib/__tests__/insights-layout.test.ts +++ b/src/lib/__tests__/insights-layout.test.ts @@ -11,6 +11,7 @@ import { describe, it, expect } from "vitest"; import { INSIGHTS_SECTION_IDS, + INSIGHTS_TILE_IDS, DEFAULT_INSIGHTS_LAYOUT, resolveInsightsLayout, serializeInsightsLayout, @@ -298,4 +299,37 @@ describe("resolveTileLayout — W2c per-tile decision", () => { expect(resolveTileLayout(DEFAULT_INSIGHTS_LAYOUT, id).visible).toBe(true); } }); + + // v1.15.14 — the default is ALL-VISIBLE. The v1.15.11 curated subset + // dropped ~20 nav pills once the tab strip began gating on the layout + // (sleep, steps, active-energy, walking-*, audio-*, daylight, the body- + // composition tiles, …). The grid renders a fixed mapped subset + // regardless of `tiles.visible`, so all-visible does NOT bloat it; what + // it restores is the nav's everything-with-data default. Pin it. + it("defaults EVERY tile visible (nav-pill regression guard)", () => { + expect(DEFAULT_INSIGHTS_LAYOUT.tiles.length).toBe(INSIGHTS_TILE_IDS.length); + for (const tile of DEFAULT_INSIGHTS_LAYOUT.tiles) { + expect(tile.visible).toBe(true); + } + // Spot-check the long-tail slugs that previously regressed out of nav. + for (const id of [ + "sleep", + "steps", + "active-energy", + "walking-distance", + "walking-speed", + "flights-climbed", + "environmental-audio", + "headphone-audio", + "daylight", + "breathing-disturbances", + "skin-temperature", + "body-water", + "fat-mass", + "muscle-mass", + "lean-body-mass", + ]) { + expect(resolveTileLayout(DEFAULT_INSIGHTS_LAYOUT, id).visible).toBe(true); + } + }); }); diff --git a/src/lib/insights-layout.ts b/src/lib/insights-layout.ts index 3a8eb7bc6..501136496 100644 --- a/src/lib/insights-layout.ts +++ b/src/lib/insights-layout.ts @@ -140,55 +140,30 @@ const INSIGHTS_LAYOUT_VERSION = 2; /** * Default layout mirrors the routed tab-strip order — vitals first, * then body composition, activity, sleep, cardiovascular, mood, then - * events. Core surfaces are default-visible; the long-tail HealthKit - * pills (oxygen saturation, body temperature, active energy, sleep, - * resting HR, HRV) stay default-invisible and the user opts in once - * the matching data starts to flow. Adding a new tile lands as an - * additional `{ id, visible: false, order: N }` row here and the - * resolver auto-merges it onto existing users' saved layouts. + * events. Every tile ships default-VISIBLE; the user hides what they + * don't want. Adding a new tile lands as an additional default-visible + * row here and the resolver auto-merges it onto existing users' saved + * layouts. + * + * v1.15.14 — the default is now ALL-VISIBLE. The v1.15.11 design shipped + * a curated subset (`DEFAULT_VISIBLE_TILE_IDS`) authored for the overview + * Vitals/Mobility GRID, which renders only a FIXED mapped set of tiles + * (the four derived re-frames + `SECTION_VITALS` + `SECTION_MOBILITY` — + * see `vitals-dashboard.tsx`). But once the tab strip began gating its + * nav pills on the same layout (v1.15.14 W2), that curated subset dropped + * ~20 nav pills for a fresh account with data in Sleep, Steps, + * Active-Energy, body composition, Walking/mobility, Audio, Daylight, + * etc. — pills that show today — and caused a "pills flash in then + * vanish" once the layout GET settled. + * + * The grid renders a fixed mapped subset regardless of `tiles.visible` + * (the long-tail slugs never appear in the grid), so defaulting EVERY + * tile visible does NOT bloat the grid — it stays its curated, data-gated + * set. What it restores is the tab strip's everything-with-data default: + * a fresh account sees every pill it has data for, and the user hides the + * ones they don't want. Data availability stays the floor everywhere — a + * visible tile with no readings still renders nothing. */ -// Core surfaces that ship default-visible for a fresh account. Every -// other tile (the long-tail HealthKit + body-composition + mobility + -// audio pills) stays default-invisible and the user opts in once the -// matching data starts to flow. Keeping this as an explicit set means -// the default-visibility decision is auditable in one place while the -// tile-id universe itself derives from `SUB_PAGE_SLUGS`. -const DEFAULT_VISIBLE_TILE_IDS = new Set([ - "overview", - "blood-pressure", - "pulse", - "weight", - "bmi", - "workouts", - "mood", - "medications", - // v1.15.11 W2c — the Vitals + Mobility grid on the overview surfaces every - // one of these tiles today, gated only on data availability. Now that the - // grid honours this layout's tile visibility, they must ship default-visible - // or a fresh account (no saved layout) would lose the tiles it shows today — - // a regression. They stay data-gated on top of visibility, so a tile with no - // readings still renders nothing exactly as before; widening the default set - // only ensures the layout never *suppresses* a tile the user already sees. - // The ids map to the grid's tile concepts: - // cardio-fitness → FitnessAge (VO₂max re-frame); vascular-age → - // VascularAge; hrv → HRV balance; resting-pulse / respiratory-rate / - // oxygen / body-temperature / blood-glucose → the per-vital baseline - // tiles; six-minute-walk / stair-ascent-speed / stair-descent-speed / - // wrist-temperature → the Mobility sub-grid. - "cardio-fitness", - "vascular-age", - "hrv", - "resting-pulse", - "respiratory-rate", - "oxygen", - "body-temperature", - "blood-glucose", - "six-minute-walk", - "stair-ascent-speed", - "stair-descent-speed", - "wrist-temperature", -]); - export const DEFAULT_INSIGHTS_LAYOUT: InsightsLayout = { version: INSIGHTS_LAYOUT_VERSION, // Every section ships default-visible; feature/data gates still apply @@ -203,9 +178,11 @@ export const DEFAULT_INSIGHTS_LAYOUT: InsightsLayout = { // `SUB_PAGE_SLUGS` order, which itself tracks the MeasurementCategory // overlay: vitals → body → activity → sleep → cardiovascular → mood → // events). The dense 0-based `order` mirrors the routed tab strip. + // Every tile defaults VISIBLE — see the module note above for why the + // v1.15.11 curated subset was a nav-pill regression. tiles: INSIGHTS_TILE_IDS.map((id, order) => ({ id, - visible: DEFAULT_VISIBLE_TILE_IDS.has(id), + visible: true, order, })), };