mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
refactor(dashboard): retire the legacy opt-in hero
The digest-driven Today hero is the single promoted hero at the top of the dashboard, so the old opt-in hero band is redundant — a user who had opted in and has Insights on saw two heroes. Remove the legacy hero and its heroVisible layout flag. - Drop the DashboardHero / DashboardHeroSkeleton mount from the dashboard page; the header now renders the greeting unconditionally (its showGreeting gate is gone with the hero that owned it). - Retire the heroVisible field from the dashboard layout (type, defaults, resolver, serializer) and the widgets PUT schema. The resolver is a whitelist constructor, so a stored heroVisible is dropped on read and never re-persisted — no stored layout breaks. - Delete the now-orphaned hero components (band, skeleton, ring carousel, briefing spotlight) and their tests; drop the retired dashboard.hero copy across all locales. - Remove the hero-visibility toggle from Settings → Dashboard. The score-ring picker no longer hangs off that flag and stays available, since the rings it configures still ride the server-computed dashboard snapshot. - Repoint the phone-width density guard at the Today hero and mock the daily digest so it renders. - Repoint the preserve-when-absent doc analogy off the removed field.
This commit is contained in:
1 parent
d6ad76b1e1
commit
1d592bef78
29 files changed
+183
-2719
No files matched your search
+49
-15
@@ -13,21 +13,21 @@ import { mockMoodInsights } from "./utils/mock-mood-insights";
|
||||
* Phone-width density guard (390×844 iPhone class, 360×780 small
|
||||
* Android class).
|
||||
*
|
||||
* Two invariants, per the report against the dashboard hero (a long
|
||||
* briefing-signal headline squeezed into a one-word-per-line column
|
||||
* beside a wide delta, the delta spilling past the tile edge at 360 px):
|
||||
* Two invariants, per the original overflow report (long headline text
|
||||
* squeezed into a one-word-per-line column beside a wide delta, the
|
||||
* delta spilling past the tile edge at 360 px):
|
||||
*
|
||||
* 1. No horizontal page overflow — `scrollWidth` never exceeds the
|
||||
* viewport width (1 px sub-pixel tolerance).
|
||||
* 2. Nothing escapes its tile — for every card / hero band / list
|
||||
* 2. Nothing escapes its tile — for every card / Today hero / list
|
||||
* row, no descendant's visible box crosses the tile's left or
|
||||
* right edge (clip-aware: content genuinely hidden by an inner
|
||||
* `overflow-hidden` wrapper below the tile is fine; content cut
|
||||
* off at the tile's own edge is the bug).
|
||||
*
|
||||
* The snapshot mock pins the worst case: hero band on, three score
|
||||
* rings, and a fresh briefing whose signal rows pair wrapping German
|
||||
* headlines with wide delta strings.
|
||||
* The mocks pin the worst case: the Today hero (its score face + a
|
||||
* worth-a-look rail) plus a fresh briefing whose rows pair wrapping
|
||||
* German headlines with wide delta strings.
|
||||
*
|
||||
* Desktop project only — the assertions are viewport-driven via
|
||||
* `setViewportSize` (the `ipad-viewport.spec.ts` pattern).
|
||||
@@ -51,7 +51,7 @@ interface TileEscape {
|
||||
function collectTileEscapes(): TileEscape[] {
|
||||
const escapes: TileEscape[] = [];
|
||||
const tiles = document.querySelectorAll(
|
||||
'[data-slot="card"], [data-slot="dashboard-hero"], [data-slot="list-row"]',
|
||||
'[data-slot="card"], [data-slot="today-hero"], [data-slot="list-row"]',
|
||||
);
|
||||
const TOLERANCE = 1;
|
||||
for (const tile of tiles) {
|
||||
@@ -113,10 +113,46 @@ test.describe("phone-width density guard", () => {
|
||||
await page.setViewportSize(viewport);
|
||||
await mockDashboardSnapshot(page, {
|
||||
summaries: POPULATED_SUMMARIES,
|
||||
heroVisible: true,
|
||||
briefing: LONG_HEADLINE_BRIEFING,
|
||||
scoreRings: MOCK_SCORE_RINGS,
|
||||
});
|
||||
// The Today hero reads the unified daily digest from
|
||||
// `/api/daily/digest`. Mock it with a deterministic digest (a
|
||||
// score + one worth-a-look item) so the hero paints without
|
||||
// reaching the real route — mirror of the dashboard.spec mock.
|
||||
await page.route(/\/api\/daily\/digest(\?|$)/, (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
generatedAt: new Date().toISOString(),
|
||||
phase: "final",
|
||||
sleepPending: false,
|
||||
score: { value: 82, band: "green", delta: 2 },
|
||||
topSignal: null,
|
||||
briefingLead: "Your week is trending steady.",
|
||||
line: "Your week is trending steady.",
|
||||
worthALook: [
|
||||
{
|
||||
kind: "sync_issue",
|
||||
title: "Sync needs attention",
|
||||
body: "Withings isn't syncing.",
|
||||
status: "warning",
|
||||
actions: [
|
||||
{
|
||||
labelKey: "daily.action.reconnect",
|
||||
intent: "sync.reconnect",
|
||||
href: "/settings/integrations",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
error: null,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
if (route === "/insights/mood") {
|
||||
await mockMoodInsights(page);
|
||||
}
|
||||
@@ -124,14 +160,12 @@ test.describe("phone-width density guard", () => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
if (route === "/") {
|
||||
// The guard must not pass vacuously — the hero band and its
|
||||
// long-headline spotlight rows have to actually render.
|
||||
// The guard must not pass vacuously — the Today hero and its
|
||||
// score face have to actually render.
|
||||
await expect(page.locator('[data-slot="today-hero"]')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-slot="dashboard-hero"]'),
|
||||
page.locator('[data-slot="today-hero-score"]'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-slot="dashboard-briefing-spotlight-row"]'),
|
||||
).toHaveCount(3);
|
||||
}
|
||||
|
||||
if (route === "/insights/mood") {
|
||||
|
||||
@@ -231,11 +231,9 @@ export interface MockSnapshotOptions {
|
||||
summaries?: Record<string, DataSummary>;
|
||||
/** Artificial latency in ms before the route fulfils (race specs). */
|
||||
delayMs?: number;
|
||||
/** Render the dashboard hero band (`layout.heroVisible` opt-in). */
|
||||
heroVisible?: boolean;
|
||||
/** Fresh (`ready`, non-stale) briefing for the hero spotlight rows. */
|
||||
/** Fresh (`ready`, non-stale) briefing for the dashboard briefing card. */
|
||||
briefing?: DailyBriefing;
|
||||
/** Selected hero score rings (renders beside the health-score ring). */
|
||||
/** Selected score rings carried on the snapshot (server-computed). */
|
||||
scoreRings?: DashboardScoreRing[];
|
||||
}
|
||||
|
||||
@@ -263,9 +261,7 @@ export function buildMockSnapshot(
|
||||
onboardingTourCompleted: true,
|
||||
greetingHour: 9,
|
||||
},
|
||||
layout: options.heroVisible
|
||||
? { ...DEFAULT_DASHBOARD_LAYOUT, heroVisible: true }
|
||||
: DEFAULT_DASHBOARD_LAYOUT,
|
||||
layout: DEFAULT_DASHBOARD_LAYOUT,
|
||||
layoutCatalogue: buildLayoutCatalogue(),
|
||||
metricStates: {},
|
||||
targetBands: mockTargetBands(),
|
||||
|
||||
Reference in new issue
Block a user