fix(insights): keep every data-having pill in the default nav and reconcile review findings

The default insights layout shipped a curated visible subset (v1.15.11),
authored for the overview Vitals/Mobility grid. Once the tab strip began
gating its nav pills on the same layout, that 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 flash-then-vanish on first paint.

The grid renders a fixed mapped subset regardless of tile visibility, so
defaulting every tile visible does not bloat it; what it restores is the
nav's everything-with-data default. The user hides what they don't want.

Also:
- mood distribution + weekday charts: switch axis tick fill from the raw
  --dracula-fg (white-on-white on the light-mode card) to the shared
  --muted-foreground axis-text token; dark mode unchanged.
- drop the orphaned insights.editMode.manageTiles key from all six locales
  (replaced by manageSubPages) and add a manageSubPagesHint noting that
  hiding a detail page also removes its top-nav pill.
- pin the all-visible default with a layout test and a tab-strip
  default-nav regression test.
This commit is contained in:
Marc-André Bombeck committed 2026-06-07 21:01:26 +02:00
1 parent e7e123be01
commit 6164a0c5c2
12 files changed
+147 -59

No files matched your search

+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
@@ -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("<InsightsTabStrip> — 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(
<InsightsTabStrip
availability={availability}
visibleTileIds={visibleTileIds}
/>,
);
// 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<");
});
});
@@ -400,6 +400,15 @@ export function InsightsEditMode({
/>
{t("insights.editMode.manageSubPages")}
</button>
{/* 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. */}
<p
data-slot="insights-edit-tiles-nav-hint"
className="text-muted-foreground mt-1 text-xs"
>
{t("insights.editMode.manageSubPagesHint")}
</p>
{tilesOpen && (
// v1.15.14 W2 — the manager now lists EVERY sub-page
// slug grouped by category, the same `tiles` layout the
@@ -61,13 +61,25 @@ export function MoodDistributionChart({
<BarChart data={data} margin={{ top: 8, right: 12, bottom: 8, left: 0 }}>
<XAxis
dataKey="label"
tick={{ fontSize: 11, fill: "var(--dracula-fg)" }}
// v1.15.14 — theme-aware axis text. `--dracula-fg` (near-white) is
// not overridden in `:root.light` (Alucard white card) → white-on-
// white. `--muted-foreground` is the token every other in-app chart
// uses for axis ticks and is legible in both themes; dark mode is
// unchanged. The mood-hue bar Cells stay on `--dracula-*` — only the
// axis text legibility changes here.
tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
stroke="var(--dracula-comment)"
interval={0}
/>
<YAxis
allowDecimals={false}
tick={{ fontSize: 11, fill: "var(--dracula-fg)" }}
// v1.15.14 — theme-aware axis text. `--dracula-fg` (near-white) is
// not overridden in `:root.light` (Alucard white card) → white-on-
// white. `--muted-foreground` is the token every other in-app chart
// uses for axis ticks and is legible in both themes; dark mode is
// unchanged. The mood-hue bar Cells stay on `--dracula-*` — only the
// axis text legibility changes here.
tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
stroke="var(--dracula-comment)"
width={28}
/>
@@ -85,14 +85,22 @@ export function MoodWeekdayChart({ weekday }: { weekday: MoodWeekdayRow[] }) {
>
<XAxis
dataKey="label"
tick={{ fontSize: 11, fill: "var(--dracula-fg)" }}
// v1.15.14 — theme-aware axis text (see mood-distribution-chart).
// `--dracula-fg` was white-on-white on the light-mode card; switch
// to the shared `--muted-foreground` axis-tick token. Bar mood
// hues stay `--dracula-*`; dark mode is unchanged.
tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
stroke="var(--dracula-comment)"
interval={0}
/>
<YAxis
domain={[1, 5]}
ticks={[1, 2, 3, 4, 5]}
tick={{ fontSize: 11, fill: "var(--dracula-fg)" }}
// v1.15.14 — theme-aware axis text (see mood-distribution-chart).
// `--dracula-fg` was white-on-white on the light-mode card; switch
// to the shared `--muted-foreground` axis-tick token. Bar mood
// hues stay `--dracula-*`; dark mode is unchanged.
tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
stroke="var(--dracula-comment)"
width={24}
/>
+34
View File
@@ -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);
}
});
});
+26 -49
View File
@@ -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<InsightsTileId>([
"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,
})),
};