mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
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:
1 parent
e7e123be01
commit
6164a0c5c2
12 files changed
+147
-59
No files matched your search
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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
@@ -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,
|
||||
})),
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user