feat(topics): add the Topics preview area, its navigation entry and its switch

/topics shows a tile per topic the record holds data for, and names the
empty topics and the module-hidden ones once each. /topics/<topic>
shows the topic today, its signals side by side over 7, 30, 90 days or
a year against their usual range, how they move together, their trends,
its blocks, the single-metric pages behind it and what it is missing.
Every topic links into the metric pages, which stay as they are, and
each page ends on a neutral note that this is a preview.

Settings > System > Preview features is listed again and switches the
area on for the person reading. While it is on, the sidebar carries a
Topics entry with a Beta badge directly below Insights and the phone's
More sheet leads with a Topics tile; the bottom bar does not change.
The pages leave for Insights when the switch is off.

The charts load through the shared chart runtime. The sidebar rows move
into their own module and the More sheet tile into its own component,
so neither shell file grows; the lab trend wording is shared between the
marker page and the metabolism topic. Copy in all nine locales.
This commit is contained in:
Marc-André Bombeck committed 2026-10-23 08:18:53 +02:00
1 parent cc04e0944d
commit c68e833dcc
64 files changed
+4319 -219

No files matched your search

+2
View File
@@ -51,6 +51,8 @@ const WHOLE_RECORD_ROUTE_FAMILIES = [
"/achievements",
// v1.42 — the timeline lays every section side by side.
"/timeline",
// So does a topic page (preview).
"/topics",
] as const;
interface RouteFamily {
+9
View File
@@ -49,6 +49,7 @@ import { vaccinationKeys } from "./vaccinations";
import { workoutKeys } from "./workouts";
import { activityKeys } from "./activity";
import { provenanceKeys } from "./provenance";
import { topicKeys } from "./topics";
export const queryKeys = {
...authKeys,
@@ -64,6 +65,7 @@ export const queryKeys = {
...workoutKeys,
...activityKeys,
...provenanceKeys,
...topicKeys,
...settingsKeys,
...cycleKeys,
...measurementReminderKeys,
@@ -165,6 +167,7 @@ export const measurementDependentKeys = [
// write to this one evicts both roots.
queryKeys.dayRoot(),
queryKeys.timelineRoot(),
queryKeys.topicsRoot(),
];
/**
@@ -199,6 +202,7 @@ export const moodDependentKeys = [
// write to this one evicts both roots.
queryKeys.dayRoot(),
queryKeys.timelineRoot(),
queryKeys.topicsRoot(),
];
/**
@@ -254,6 +258,7 @@ export const medicationDependentKeys = [
// write to this one evicts both roots.
queryKeys.dayRoot(),
queryKeys.timelineRoot(),
queryKeys.topicsRoot(),
// v1.43 — a taken supplement dose adds to the day's micronutrients
// (`source = SUPPLEMENT`), so a dose write evicts the nutrient reads.
queryKeys.nutrientsRoot(),
@@ -274,6 +279,7 @@ export const cycleDependentKeys = [
// write to this one evicts both roots.
queryKeys.dayRoot(),
queryKeys.timelineRoot(),
queryKeys.topicsRoot(),
];
/**
@@ -297,6 +303,7 @@ export const illnessDependentKeys = [
// write to this one evicts both roots.
queryKeys.dayRoot(),
queryKeys.timelineRoot(),
queryKeys.topicsRoot(),
];
/**
@@ -330,6 +337,7 @@ export const encounterDependentKeys = [
// write to this one evicts both roots.
queryKeys.dayRoot(),
queryKeys.timelineRoot(),
queryKeys.topicsRoot(),
];
/**
@@ -357,6 +365,7 @@ export const vaccinationDependentKeys = [
// write to this one evicts both roots.
queryKeys.dayRoot(),
queryKeys.timelineRoot(),
queryKeys.topicsRoot(),
];
/**
+3
View File
@@ -44,6 +44,9 @@ export const settingsKeys = {
*/
moduleListLayout: () => ["settings", "module-list-layout"] as const,
/** The per-user preview-feature opt-ins behind `/api/settings/preview-features`. */
previewFeatures: () => ["settings", "preview-features"] as const,
/**
* v1.35.0 — which pillars count toward the Health Score, behind
* `GET/PATCH /api/auth/me/health-score-config`. Its own cell rather than
+11
View File
@@ -0,0 +1,11 @@
/**
* Query keys — the topic pages (preview).
* Part of the centralized factory; aggregated in `./index.ts`.
*/
export const topicKeys = {
/** Every topic read; a write to any record table the topics read evicts it. */
topicsRoot: () => ["topics"] as const,
topicsOverview: () => ["topics", "overview"] as const,
topicDetail: (topic: string, range: string) =>
["topics", "detail", topic, range] as const,
};
@@ -98,6 +98,7 @@ export const SETTINGS_DESTINATION_INVENTORY = {
export: PERSONAL,
advanced: PERSONAL,
privacy: PERSONAL,
preview: PERSONAL,
about: PERSONAL,
} as const satisfies Record<
SettingsSectionSlug,
+4 -4
View File
@@ -114,10 +114,10 @@ export const SETTINGS_SECTION_SLUGS = [
// pieces into one coherent privacy pane. Sits next to `advanced` (the
// destructive-action home) and before `about`.
"privacy",
// v1.44 — "Preview features" (per-user opt-ins, Settings > System) is not
// listed until the first preview it switches, the topic pages, ships: a
// switch that changes nothing is a promise the page cannot keep. The stored
// flag, `/api/settings/preview-features` and its backup round-trip stay.
// "Preview features": per-user opt-in to things finished enough to
// try and not yet the default (the Topics area first). Sits in the System
// group before About.
"preview",
"about",
] as const;