From 75129c798d791a6f0f01b4742bbb82f091ad26ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 21 Jun 2026 10:57:16 +0200 Subject: [PATCH] refine(cycle): consolidate disclaimer, align columns, unify tiles Move the non-medical disclaimer to the cycle-tracking on-ramp and drop it from the repeating in-feature surfaces (calendar tab, predictions panel); delete the now-unused disclaimer component. Align the left ring column with the tab content rather than the tab strip. Raise the settings subheading sizes to the standard card-description scale. Reflow the reminder rows so a wrapping label no longer collides with its button at narrow widths. Unify the wheel and phase-education tile padding. Cite the symptothermal double-check primary source under the second fertility sign. --- messages/de.json | 2 + messages/en.json | 2 + messages/es.json | 2 + messages/fr.json | 2 + messages/it.json | 2 + messages/pl.json | 2 + .../__tests__/predictions-panel.test.tsx | 22 +---- src/components/cycle/cycle-disclaimer.tsx | 32 ------- src/components/cycle/cycle-settings.tsx | 89 ++++++++++++------- src/components/cycle/cycle-view.tsx | 28 ++---- src/components/cycle/predictions-panel.tsx | 25 +----- .../settings/cycle-tracking-card.tsx | 17 +++- 12 files changed, 100 insertions(+), 125 deletions(-) delete mode 100644 src/components/cycle/cycle-disclaimer.tsx diff --git a/messages/de.json b/messages/de.json index 7e8cc75f1..6065105a9 100644 --- a/messages/de.json +++ b/messages/de.json @@ -4708,6 +4708,7 @@ "cycleTracking": { "title": "Zyklus-Tracking", "description": "Verfolge deinen Menstruationszyklus, Vorhersagen, Symptome und Phasen-Insights. Standardmäßig an für als weiblich eingestellte Konten; für alle verfügbar.", + "disclaimer": "Vorhersagen und Phasen sind beschreibende Schätzungen aus deinen erfassten Daten – keine medizinische Beratung und keine Verhütungsmethode. Sie zeigen nie an, dass ungeschützter Geschlechtsverkehr sicher ist. Die Genauigkeit steigt, je mehr Zyklen du erfasst. Wende dich für Verhütung oder medizinische Entscheidungen an eine medizinische Fachperson.", "enable": "Zyklus-Tracking aktivieren", "manage": "Zyklus-Tracking öffnen", "error": "Zyklus-Tracking konnte nicht aktualisiert werden." @@ -6276,6 +6277,7 @@ "rawChartModeDescription": "Zeige nur deine erfassten Daten, ohne Prognosen oder Deutung.", "secondarySymptomLabel": "Zweites Fruchtbarkeitszeichen", "secondarySymptomDescription": "Das zweite Zeichen, das für die symptothermale Doppelkontrolle mit deiner Temperatur kombiniert wird. Die meisten beobachten den Zervixschleim; wechsle zur Gebärmutterhals-Beobachtung, wenn du das bevorzugst.", + "secondarySymptomCitation": "Mehr zur Methode (Studie)", "secondarySymptom": { "MUCUS": "Zervixschleim", "CERVIX": "Gebärmutterhals" diff --git a/messages/en.json b/messages/en.json index e5f7ce2d0..1b76e6254 100644 --- a/messages/en.json +++ b/messages/en.json @@ -4708,6 +4708,7 @@ "cycleTracking": { "title": "Cycle tracking", "description": "Track your menstrual cycle, predictions, symptoms, and phase insights. On by default for accounts set to female; available to everyone.", + "disclaimer": "Predictions and phases are descriptive estimates from your logged data — not medical advice and not a contraceptive method. They never indicate that it is safe to have unprotected sex. Accuracy improves as you log more cycles. For contraception or any medical decision, consult a healthcare professional.", "enable": "Enable cycle tracking", "manage": "Open cycle tracking", "error": "Couldn't update cycle tracking." @@ -6281,6 +6282,7 @@ "rawChartModeDescription": "Show only your logged data with no predictions or interpretation.", "secondarySymptomLabel": "Secondary fertility sign", "secondarySymptomDescription": "The second sign paired with your temperature for the symptothermal double-check. Most people track cervical mucus; switch to cervix observation if you prefer.", + "secondarySymptomCitation": "Read about the method (study)", "secondarySymptom": { "MUCUS": "Cervical mucus", "CERVIX": "Cervix observation" diff --git a/messages/es.json b/messages/es.json index 887273942..731b8359f 100644 --- a/messages/es.json +++ b/messages/es.json @@ -4708,6 +4708,7 @@ "cycleTracking": { "title": "Seguimiento del ciclo", "description": "Registra tu ciclo menstrual, predicciones, síntomas e información de fases. Activado por defecto en cuentas configuradas como femeninas; disponible para todos.", + "disclaimer": "Las predicciones y las fases son estimaciones descriptivas a partir de tus datos registrados, no son consejo médico ni un método anticonceptivo. Nunca indican que sea seguro tener relaciones sin protección. La precisión mejora a medida que registras más ciclos. Para la anticoncepción o cualquier decisión médica, consulta a un profesional sanitario.", "enable": "Activar el seguimiento del ciclo", "manage": "Abrir seguimiento del ciclo", "error": "No se pudo actualizar el seguimiento del ciclo." @@ -6276,6 +6277,7 @@ "rawChartModeDescription": "Muestra solo tus datos registrados, sin predicciones ni interpretación.", "secondarySymptomLabel": "Segundo signo de fertilidad", "secondarySymptomDescription": "El segundo signo que se combina con tu temperatura para la doble comprobación sintotérmica. La mayoría observa el moco cervical; cambia a la observación del cuello uterino si lo prefieres.", + "secondarySymptomCitation": "Más sobre el método (estudio)", "secondarySymptom": { "MUCUS": "Moco cervical", "CERVIX": "Cuello uterino" diff --git a/messages/fr.json b/messages/fr.json index 3785662a4..209719ae5 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -4708,6 +4708,7 @@ "cycleTracking": { "title": "Suivi du cycle", "description": "Suivez votre cycle menstruel, les prédictions, les symptômes et les analyses de phases. Activé par défaut pour les comptes définis comme féminins ; disponible pour tous.", + "disclaimer": "Les prévisions et les phases sont des estimations descriptives issues de tes données enregistrées : ce ne sont ni un avis médical ni une méthode de contraception. Elles n'indiquent jamais qu'un rapport non protégé est sans risque. La précision s'améliore à mesure que tu enregistres plus de cycles. Pour la contraception ou toute décision médicale, consulte un professionnel de santé.", "enable": "Activer le suivi du cycle", "manage": "Ouvrir le suivi du cycle", "error": "Impossible de mettre à jour le suivi du cycle." @@ -6276,6 +6277,7 @@ "rawChartModeDescription": "Affiche uniquement tes données enregistrées, sans prévision ni interprétation.", "secondarySymptomLabel": "Deuxième signe de fertilité", "secondarySymptomDescription": "Le deuxième signe associé à ta température pour la double vérification sympto-thermique. La plupart des personnes observent la glaire cervicale ; passe à l'observation du col de l'utérus si tu préfères.", + "secondarySymptomCitation": "En savoir plus sur la méthode (étude)", "secondarySymptom": { "MUCUS": "Glaire cervicale", "CERVIX": "Col de l'utérus" diff --git a/messages/it.json b/messages/it.json index f70d2af80..9261eb583 100644 --- a/messages/it.json +++ b/messages/it.json @@ -4708,6 +4708,7 @@ "cycleTracking": { "title": "Monitoraggio del ciclo", "description": "Monitora il ciclo mestruale, le previsioni, i sintomi e gli approfondimenti sulle fasi. Attivo per impostazione predefinita per gli account impostati come femminili; disponibile per tutti.", + "disclaimer": "Le previsioni e le fasi sono stime descrittive basate sui dati che registri: non sono un parere medico né un metodo contraccettivo. Non indicano mai che sia sicuro avere rapporti non protetti. La precisione migliora man mano che registri più cicli. Per la contraccezione o qualsiasi decisione medica, rivolgiti a un professionista sanitario.", "enable": "Attiva il monitoraggio del ciclo", "manage": "Apri il monitoraggio del ciclo", "error": "Impossibile aggiornare il monitoraggio del ciclo." @@ -6276,6 +6277,7 @@ "rawChartModeDescription": "Mostra solo i tuoi dati registrati, senza previsioni né interpretazioni.", "secondarySymptomLabel": "Secondo segno di fertilità", "secondarySymptomDescription": "Il secondo segno abbinato alla tua temperatura per il doppio controllo sintotermico. La maggior parte osserva il muco cervicale; passa all'osservazione della cervice se preferisci.", + "secondarySymptomCitation": "Scopri di più sul metodo (studio)", "secondarySymptom": { "MUCUS": "Muco cervicale", "CERVIX": "Cervice" diff --git a/messages/pl.json b/messages/pl.json index d1ae296e5..c8059bc23 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -4708,6 +4708,7 @@ "cycleTracking": { "title": "Śledzenie cyklu", "description": "Śledź swój cykl menstruacyjny, prognozy, objawy i analizy faz. Domyślnie włączone dla kont ustawionych jako kobiece; dostępne dla wszystkich.", + "disclaimer": "Prognozy i fazy to opisowe szacunki na podstawie zarejestrowanych danych — nie są poradą medyczną ani metodą antykoncepcji. Nigdy nie wskazują, że współżycie bez zabezpieczenia jest bezpieczne. Dokładność rośnie wraz z liczbą zarejestrowanych cykli. W sprawach antykoncepcji lub decyzji medycznych skonsultuj się z pracownikiem ochrony zdrowia.", "enable": "Włącz śledzenie cyklu", "manage": "Otwórz śledzenie cyklu", "error": "Nie udało się zaktualizować śledzenia cyklu." @@ -6276,6 +6277,7 @@ "rawChartModeDescription": "Pokazuj tylko zapisane dane, bez prognoz i interpretacji.", "secondarySymptomLabel": "Drugi objaw płodności", "secondarySymptomDescription": "Drugi objaw łączony z temperaturą w podwójnej kontroli objawowo-termicznej. Większość obserwuje śluz szyjkowy; przełącz na obserwację szyjki macicy, jeśli wolisz.", + "secondarySymptomCitation": "Więcej o metodzie (badanie)", "secondarySymptom": { "MUCUS": "Śluz szyjkowy", "CERVIX": "Szyjka macicy" diff --git a/src/components/cycle/__tests__/predictions-panel.test.tsx b/src/components/cycle/__tests__/predictions-panel.test.tsx index c1b91adb1..a10a12819 100644 --- a/src/components/cycle/__tests__/predictions-panel.test.tsx +++ b/src/components/cycle/__tests__/predictions-panel.test.tsx @@ -37,33 +37,19 @@ const emptyHistory: CycleHistoryResponse = { }; describe("", () => { - it("renders the next-period range and shows the disclaimer under the AVOID_PREGNANCY goal", () => { + it("renders the next-period range without repeating the non-medical disclaimer", () => { + // The disclaimer now lives once at the cycle-tracking on-ramp, never beneath + // the prediction surfaces, so the panel must not render it for any goal. const html = render( , - ); - expect(html).toContain('data-slot="cycle-disclaimer"'); - expect(html).toContain("Estimates only, not medical advice."); - // The range (not a single date) renders. - expect(html).toContain("–"); - }); - - it("suppresses the generic estimate disclaimer for non-AVOID_PREGNANCY goals", () => { - const html = render( - , ); expect(html).not.toContain('data-slot="cycle-disclaimer"'); expect(html).not.toContain("Estimates only, not medical advice."); - // The range still renders. + // The range (not a single date) renders. expect(html).toContain("–"); }); diff --git a/src/components/cycle/cycle-disclaimer.tsx b/src/components/cycle/cycle-disclaimer.tsx deleted file mode 100644 index 371fcd9eb..000000000 --- a/src/components/cycle/cycle-disclaimer.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; - -import { cn } from "@/lib/utils"; - -/** - * v1.15.0 — the fixed non-medical disclaimer for every prediction / fertility - * surface. Mirrors the GLP-1 drug-level-estimate disclaimer pattern - * (`DrugLevelChart`): a muted, border-left note that stays attached wherever - * an estimate renders. The copy is descriptive-not-a-contraceptive, - * not-medical-advice — never a green/safe claim. The text comes from the - * server (`cycle.prediction.disclaimer`, surfaced on the prediction DTO) so - * the web + iOS + the engine speak the same words. - */ -export function CycleDisclaimer({ - text, - className, -}: { - text: string; - className?: string; -}) { - return ( -

- {text} -

- ); -} diff --git a/src/components/cycle/cycle-settings.tsx b/src/components/cycle/cycle-settings.tsx index 2ee30642f..ce0aec3dd 100644 --- a/src/components/cycle/cycle-settings.tsx +++ b/src/components/cycle/cycle-settings.tsx @@ -194,7 +194,18 @@ export function CycleSettings({ profile }: { profile: CycleProfileDTO }) { ))}

- {t("cycle.settings.secondarySymptomDescription")} + {t("cycle.settings.secondarySymptomDescription")}{" "} + {/* Primary source for the symptothermal double-check method: the + German NFP prospective cohort (Frank-Herrmann et al., Human + Reproduction 2007). Plain anchor — no markdown renderer. */} + + {t("cycle.settings.secondarySymptomCitation")} +

@@ -220,21 +231,11 @@ export function CycleSettings({ profile }: { profile: CycleProfileDTO }) { {/* Reminders — the per-channel toggles live on the notifications page; point users there rather than duplicating the toggle state here. */} -
-
-

- {t("cycle.settings.reminders")} -

-

- {t("cycle.settings.remindersDescription")} -

-
- -
+ {/* Fertile-window reminder — surfaced ONLY under the conception goal. The toggle itself is the default-OFF per-channel CYCLE_FERTILE_SOON @@ -242,21 +243,11 @@ export function CycleSettings({ profile }: { profile: CycleProfileDTO }) { it, so the affordance never appears (and fertile language never shows) for the other goals (the inclusive-framing rule). */} {goal === "TRYING_TO_CONCEIVE" ? ( -
-
-

- {t("cycle.settings.fertileReminder")} -

-

- {t("cycle.settings.fertileReminderDescription")} -

-
- -
+ ) : null} {/* Cycle data export + delete live with the main Settings → Export and @@ -306,7 +297,7 @@ function ToggleRow({ -

{description}

+

{description}

+
+

{title}

+

{description}

+
+ + + ); +} + function PriorField({ id, label, diff --git a/src/components/cycle/cycle-view.tsx b/src/components/cycle/cycle-view.tsx index 43a63ac41..7489fd3c1 100644 --- a/src/components/cycle/cycle-view.tsx +++ b/src/components/cycle/cycle-view.tsx @@ -12,7 +12,6 @@ import { cn } from "@/lib/utils"; import { CycleRing } from "./cycle-ring"; import { BbtChart } from "./bbt-chart"; import { CycleCalendar } from "./cycle-calendar"; -import { CycleDisclaimer } from "./cycle-disclaimer"; import { CycleHistoryChart } from "./cycle-history-chart"; import { LogDaySheet } from "./log-day-sheet"; import { PhaseEducationCard } from "./phase-education-card"; @@ -118,22 +117,11 @@ export function CycleView() { const loading = calendar.isLoading && !calendar.data; const calendarError = calendar.isError && !calendar.data; const tileHue = wheel.phase ? PHASE_HUE[wheel.phase] : undefined; - // AVOID_PREGNANCY surfaces the fertile window, so it must show the stronger - // "not a contraceptive method" caveat. Prefer the server-resolved - // prediction.disclaimer (already goal-correct); fall back to the goal-derived - // key for the no-prediction calendar tab (QA H-1). const goal = calendar.data?.profile.goal; // Honesty-gate inputs for the phase-education card (Clue precedent): the // predictive phase framing only shows when prediction is on, not in // raw-chart mode, and at least three cycles are observed. const calProfile = calendar.data?.profile; - const disclaimerText = - calendar.data?.prediction?.disclaimer ?? - t( - goal === "AVOID_PREGNANCY" - ? "cycle.disclaimer" - : "cycle.prediction.disclaimer", - ); return (
@@ -162,8 +150,13 @@ export function CycleView() { {/* Desktop: ring (left) + tabs (right). Single column below lg. */}
- {/* Wheel + compact phase card — grouped so they stick together. */} -
+ {/* Wheel + compact phase card — grouped so they stick together. The + `lg:mt-15` (3.75rem) drops the column so the wheel tile's top edge + lines up with the calendar / predictions content rather than the + tab strip: TabsList `h-9` (2.25rem) + the Tabs root `gap-2` (0.5rem) + + the content's `mt-4` (1rem). Below `lg` the column stacks first and + the offset collapses to zero. */} +
{/* Wheel — the signature ring on the premium wellness-tile surface. `data-revealed` must sit on an ANCESTOR of `.wellness-tile-rise` — the keyframe selector is `[data-revealed="true"] @@ -180,7 +173,7 @@ export function CycleView() { : undefined } className={cn( - "wellness-tile flex flex-col items-center gap-3 rounded-xl px-6 py-6", + "wellness-tile flex flex-col items-center gap-3 rounded-xl px-5 py-5", play && "wellness-tile-rise", )} > @@ -321,9 +314,6 @@ export function CycleView() { )} - {!loading && !calendarError ? ( - - ) : null} @@ -339,8 +329,6 @@ export function CycleView() { prediction={calendar.data?.prediction ?? null} rawChartMode={calendar.data?.profile.rawChartMode ?? false} history={history.data} - fallbackDisclaimer={disclaimerText} - goal={goal} /> @@ -129,8 +112,6 @@ export function PredictionsPanel({ ) : null} )} - - {disclaimer ? : null} diff --git a/src/components/settings/cycle-tracking-card.tsx b/src/components/settings/cycle-tracking-card.tsx index c4469b068..ac197a3ca 100644 --- a/src/components/settings/cycle-tracking-card.tsx +++ b/src/components/settings/cycle-tracking-card.tsx @@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import Link from "next/link"; -import { CalendarHeart } from "lucide-react"; +import { CalendarHeart, Info } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; @@ -79,6 +79,21 @@ export function CycleTrackingCard({ className="mt-0.5 shrink-0" />
+ {/* The single, prominent non-medical disclaimer for the whole cycle + vertical. It lives here at the on-ramp — where expectations are + set — instead of repeating beneath every prediction surface. */} +
+
{enabled ? (