From 6abc178c88f27f658d9c260be95d22834e1969d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 7 Jun 2026 14:15:22 +0200 Subject: [PATCH] Even out the Insights overview inter-section spacing The overview is a vertical stack on a single space-y-6 rhythm, but two seams read wrong: Trends to period-in-review felt too wide while the cycle summary to signals-of-the-day seam had no gap at all. The missing gap traced to the cycle-summary card wrapping itself in a display:contents box, which collapsed its margin so space-y skipped it. - Give the cycle summary a real section wrapper (heading above the card, matching every other section) so the inter-section margin applies. - Document space-y-6 as the single gap, top to bottom; every section is a real section or renders null, never a contents wrapper that strands a phantom margin. --- src/app/insights/page.tsx | 15 ++- .../cycle-insight-summary-card.test.tsx | 8 +- .../cycle/cycle-insight-summary-card.tsx | 103 ++++++++++-------- 3 files changed, 74 insertions(+), 52 deletions(-) diff --git a/src/app/insights/page.tsx b/src/app/insights/page.tsx index 71b67c6f2..70487d82b 100644 --- a/src/app/insights/page.tsx +++ b/src/app/insights/page.tsx @@ -366,6 +366,13 @@ export default function InsightsPage() { // wrap used `space-y-6` (24 px), so the overview read as two tiers. Unify // to `space-y-6` — it matches the vitals wrap and tightens the overview in // line with the "Insights gives away too much space" direction. + // + // v1.15.10 — `space-y-6` is now the SINGLE inter-section gap, top to + // bottom. Every section renders as a real `
` (or `null` — never a + // `display: contents` wrapper, which used to collapse the cycle-summary box + // so `space-y` skipped its margin and the cycle→signals seam read as + // zero-gap). Each section owns its OWN `SectionHeading` + `space-y-3` to its + // card, so the rhythm is even regardless of which sections are present.
coachLaunch.askCoach(prompt) - : undefined + coachLaunch ? (prompt) => coachLaunch.askCoach(prompt) : undefined } healthScore={analytics?.healthScore ?? undefined} /> @@ -392,9 +397,7 @@ export default function InsightsPage() { // v1.15.3 — the cycle ring rides the scores strip as a gated sibling // tile, only for a cycle-tracking account, so its calendar read never // fires otherwise (the same `/api/auth/me` gate the sidebar nav uses). - extraTile={ - user?.cycleTrackingEnabled ? : undefined - } + extraTile={user?.cycleTrackingEnabled ? : undefined} // v1.15.5 — when the cycle ring is shown it TAKES the Strain slot: // hide Strain so the strip stays compact instead of growing a sixth // tile. Strain stays visible for non-cycle accounts. diff --git a/src/components/cycle/__tests__/cycle-insight-summary-card.test.tsx b/src/components/cycle/__tests__/cycle-insight-summary-card.test.tsx index 60b10499c..f7a72f259 100644 --- a/src/components/cycle/__tests__/cycle-insight-summary-card.test.tsx +++ b/src/components/cycle/__tests__/cycle-insight-summary-card.test.tsx @@ -57,6 +57,8 @@ vi.mock("../use-cycle", async () => { ...actual, useCycleCalendar: () => calendarState.current, useCycleInsights: () => insightsState.current, + // The card now reads the profile lengths for the low-data idealized ring. + useCycleProfile: () => ({ data: undefined }), }; }); @@ -173,7 +175,11 @@ describe("", () => { }); it("renders nothing while the calendar read is still resolving", () => { - calendarState.current = { data: undefined, isLoading: true, isError: false }; + calendarState.current = { + data: undefined, + isLoading: true, + isError: false, + }; const html = render(); expect(html).toBe(""); }); diff --git a/src/components/cycle/cycle-insight-summary-card.tsx b/src/components/cycle/cycle-insight-summary-card.tsx index 1641c6419..a143755b4 100644 --- a/src/components/cycle/cycle-insight-summary-card.tsx +++ b/src/components/cycle/cycle-insight-summary-card.tsx @@ -2,13 +2,19 @@ import { useMemo } from "react"; import Link from "next/link"; -import { ArrowRight, Sparkles } from "lucide-react"; +import { ArrowRight, Droplets } from "lucide-react"; import { useTranslations } from "@/lib/i18n/context"; +import { SectionHeading } from "@/components/insights/section-heading"; import { CyclePhaseHeadline } from "./cycle-phase-crosstab"; import { PHASE_HUE } from "./phase-tokens"; import { deriveWheelState } from "./wheel-state"; -import { localYmd, useCycleCalendar, useCycleInsights } from "./use-cycle"; +import { + localYmd, + useCycleCalendar, + useCycleInsights, + useCycleProfile, +} from "./use-cycle"; /** * v1.15.2 — the gated cycle-insights summary card for the main Insights page. @@ -54,10 +60,18 @@ export function CycleInsightSummaryCard() { const calendar = useCycleCalendar(from, to); const insights = useCycleInsights(); + // Shared 5-min-cached profile read so a low-data tracker still gets the + // canonical four-phase ring (no extra request — shared cache key). + const profile = useCycleProfile(); const wheel = useMemo( - () => deriveWheelState(calendar.data?.days ?? [], today), - [calendar.data, today], + () => + deriveWheelState(calendar.data?.days ?? [], today, { + typicalCycleLength: profile.data?.typicalCycleLength, + typicalPeriodLength: profile.data?.typicalPeriodLength, + lutealPhaseLength: profile.data?.lutealPhaseLength, + }), + [calendar.data, today, profile.data], ); // Stay silent until the calendar read resolves, and on a hard error: the @@ -83,30 +97,29 @@ export function CycleInsightSummaryCard() { // node never matched, so the tile stayed stuck at the rise rule's // `opacity: 0` — invisible but still occupying its box, which read as an // oversized gap on the overview. The wrapper restores the entrance. -
-
-
- {/* Zone 1 — eyebrow + the current phase / cycle-day read. */} +
+ + {/* `data-revealed` must sit on an ANCESTOR of the `.wellness-tile-rise` + element — the keyframe selector is `[data-revealed="true"] + .wellness-tile-rise` (descendant combinator). */} +
+
+ {/* Zone 1 — the current phase / cycle-day read. */}
-
-
-

-
- {/* Zone 2 — the one headline phase finding (shared component owns the - FDR-gated copy AND the honest "not enough cycles yet" empty line, so - this can never show a broken state). */} -
- -
+ {/* Zone 2 — the one headline phase finding (shared component owns the + FDR-gated copy AND the honest "not enough cycles yet" empty line, so + this can never show a broken state). */} +
+ +
- {/* Zone 3 — the deep-link into the single source of truth: the cycle - insights tab. The cycle page reads `?tab=insights` and opens that tab. */} - - {t("cycle.insightsSummary.viewDetails")} -
-
+ {/* Zone 3 — the deep-link into the single source of truth: the cycle + insights tab. The cycle page reads `?tab=insights` and opens it. */} + + {t("cycle.insightsSummary.viewDetails")} +
+
+
); }