From d8c139698c8b49655979904fdbddf56651989e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 18 Jun 2026 21:16:43 +0200 Subject: [PATCH] feat(sleep): surface the chronotype as a labelled corner readout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the chronotype band out of a body badge into a labelled top-right corner of the card — a small "Chronotype" label over the value — so the resolved type is the first thing the eye lands on. Tighten the mid-sleep copy to name it as the body's natural sleep midpoint on free days. --- messages/de.json | 28 +++++++++-- messages/en.json | 28 +++++++++-- messages/es.json | 28 +++++++++-- messages/fr.json | 28 +++++++++-- messages/it.json | 28 +++++++++-- messages/pl.json | 28 +++++++++-- .../__tests__/sleep-rhythm-cards.test.tsx | 8 ++++ .../insights/sleep/chronotype-card.tsx | 47 ++++++++++++------- 8 files changed, 187 insertions(+), 36 deletions(-) diff --git a/messages/de.json b/messages/de.json index 4e8cd7244..8f654ac76 100644 --- a/messages/de.json +++ b/messages/de.json @@ -2575,7 +2575,7 @@ "chronotype": { "title": "Chronotyp", "learning": "Dein Rhythmus wird noch erfasst — {counted} von {total} freien Nächten. Ein Chronotyp braucht ein paar weckerfreie Morgen, um aussagekräftig zu sein.", - "midSleepCaption": "Schlafmitte an freien Tagen gegen {clock}", + "midSleepCaption": "Deine natürliche Schlafmitte an freien Tagen liegt etwa um {clock}.", "advancedToggle": "Mehr Details", "socialJetlagValue": "{hours} Std. {minutes} Min. sozialer Jetlag", "socialJetlagCaption": "Der Unterschied zwischen deinem Schlaf an Arbeits- und freien Tagen — wie eine kleine wöchentliche Zeitzonenverschiebung.", @@ -2588,7 +2588,8 @@ "intermediate": "Mischtyp", "late": "Abendtyp", "extreme_late": "Ausgeprägter Abendtyp" - } + }, + "cornerLabel": "Chronotyp" }, "rhythm": { "loadError": "Dein Schlafrhythmus konnte gerade nicht geladen werden. Er erscheint wieder, sobald die Verbindung steht." @@ -3480,7 +3481,28 @@ }, "sleepQuality": { "title": "Schlafqualität", - "subtitle": "Nächtliche Werte deines Wearables" + "subtitle": "Nächtliche Werte deines Wearables", + "assessment": { + "metric": { + "SLEEP_SCORE": "dein Gesamt-Schlafscore", + "SLEEP_EFFICIENCY": "die Schlafeffizienz", + "SLEEP_PERFORMANCE": "die Schlafleistung", + "SLEEP_CONSISTENCY": "die Schlafkonstanz" + }, + "grade": { + "excellent": "ausgezeichnet", + "good": "solide", + "fair": "durchschnittlich", + "low": "eher niedrig" + }, + "clause": "Über die letzten 30 Nächte ist {metric} mit {value} {grade}.", + "closing": { + "excellent": "Behalte die Routine bei, die dich hierher bringt.", + "good": "Konstantere Zubett- und Aufstehzeiten sind meist der nächste Gewinn.", + "fair": "Ein konstantes Schlaffenster hebt diesen Wert erfahrungsgemäß am stärksten.", + "low": "Ein festes Herunterfahren am Abend und eine feste Aufstehzeit sind die ersten Hebel." + } + } }, "recovery": { "title": "Erholung", diff --git a/messages/en.json b/messages/en.json index 5c4c0206d..accbf337f 100644 --- a/messages/en.json +++ b/messages/en.json @@ -2575,7 +2575,7 @@ "chronotype": { "title": "Chronotype", "learning": "Still learning your rhythm — {counted} of {total} free nights. A chronotype needs a few alarm-free mornings before it means anything.", - "midSleepCaption": "Mid-sleep on free days around {clock}", + "midSleepCaption": "Your natural sleep midpoint on free days lands around {clock}.", "advancedToggle": "Advanced", "socialJetlagValue": "{hours}h {minutes}m social jetlag", "socialJetlagCaption": "The gap between your work-day and free-day sleep timing — like a small weekly time-zone shift.", @@ -2588,7 +2588,8 @@ "intermediate": "Intermediate type", "late": "Evening type", "extreme_late": "Strong evening type" - } + }, + "cornerLabel": "Chronotype" }, "rhythm": { "loadError": "Your sleep rhythm couldn't load just now. It'll reappear once the connection settles." @@ -3480,7 +3481,28 @@ }, "sleepQuality": { "title": "Sleep quality", - "subtitle": "Nightly scores from your wearable" + "subtitle": "Nightly scores from your wearable", + "assessment": { + "metric": { + "SLEEP_SCORE": "your overall sleep score", + "SLEEP_EFFICIENCY": "sleep efficiency", + "SLEEP_PERFORMANCE": "sleep performance", + "SLEEP_CONSISTENCY": "sleep consistency" + }, + "grade": { + "excellent": "excellent", + "good": "solid", + "fair": "fair", + "low": "on the low side" + }, + "clause": "Over the last 30 nights {metric} is {grade} at {value}.", + "closing": { + "excellent": "Keep the routine that is getting you here.", + "good": "A steadier bed and wake time is the usual next gain.", + "fair": "Protecting a consistent sleep window tends to lift this most.", + "low": "A regular wind-down and a fixed wake time are the first levers to pull." + } + } }, "recovery": { "title": "Recovery", diff --git a/messages/es.json b/messages/es.json index a69ad908c..9fa3e2b1e 100644 --- a/messages/es.json +++ b/messages/es.json @@ -2575,7 +2575,7 @@ "chronotype": { "title": "Cronotipo", "learning": "Aún estamos aprendiendo tu ritmo — {counted} de {total} noches libres. Un cronotipo necesita varias mañanas sin despertador para tener sentido.", - "midSleepCaption": "Punto medio del sueño en días libres en torno a las {clock}", + "midSleepCaption": "El punto medio natural de tu sueño en días libres ronda las {clock}.", "advancedToggle": "Avanzado", "socialJetlagValue": "{hours} h {minutes} min de jet lag social", "socialJetlagCaption": "La diferencia entre el horario de sueño laboral y el de los días libres — como un pequeño cambio de huso horario semanal.", @@ -2588,7 +2588,8 @@ "intermediate": "Intermedio", "late": "Vespertino", "extreme_late": "Vespertino marcado" - } + }, + "cornerLabel": "Cronotipo" }, "rhythm": { "loadError": "Tu ritmo de sueño no pudo cargarse ahora mismo. Reaparecerá en cuanto se estabilice la conexión." @@ -3480,7 +3481,28 @@ }, "sleepQuality": { "title": "Calidad del sueño", - "subtitle": "Puntuaciones nocturnas de tu wearable" + "subtitle": "Puntuaciones nocturnas de tu wearable", + "assessment": { + "metric": { + "SLEEP_SCORE": "tu puntuación global de sueño", + "SLEEP_EFFICIENCY": "la eficiencia del sueño", + "SLEEP_PERFORMANCE": "el rendimiento del sueño", + "SLEEP_CONSISTENCY": "la regularidad del sueño" + }, + "grade": { + "excellent": "excelente", + "good": "sólida", + "fair": "aceptable", + "low": "más bien baja" + }, + "clause": "En las últimas 30 noches {metric} es {grade}, en {value}.", + "closing": { + "excellent": "Mantén la rutina que te trae hasta aquí.", + "good": "Acostarte y levantarte a horas más estables suele ser la siguiente mejora.", + "fair": "Proteger una franja de sueño constante es lo que más suele subir esto.", + "low": "Una rutina de relajación regular y una hora fija para despertar son las primeras palancas." + } + } }, "recovery": { "title": "Recuperación", diff --git a/messages/fr.json b/messages/fr.json index 8ca52cec5..9c1a433c0 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -2575,7 +2575,7 @@ "chronotype": { "title": "Chronotype", "learning": "Votre rythme est encore en cours d'analyse — {counted} sur {total} nuits libres. Un chronotype a besoin de quelques matins sans réveil pour avoir du sens.", - "midSleepCaption": "Milieu de sommeil les jours libres vers {clock}", + "midSleepCaption": "Le milieu naturel de votre sommeil les jours de repos se situe vers {clock}.", "advancedToggle": "Avancé", "socialJetlagValue": "{hours} h {minutes} min de jet lag social", "socialJetlagCaption": "L'écart entre vos horaires de sommeil les jours travaillés et les jours libres — comme un petit décalage horaire hebdomadaire.", @@ -2588,7 +2588,8 @@ "intermediate": "Type intermédiaire", "late": "Type du soir", "extreme_late": "Type du soir marqué" - } + }, + "cornerLabel": "Chronotype" }, "rhythm": { "loadError": "Votre rythme de sommeil n'a pas pu se charger pour l'instant. Il réapparaîtra dès que la connexion sera rétablie." @@ -3480,7 +3481,28 @@ }, "sleepQuality": { "title": "Qualité du sommeil", - "subtitle": "Scores nocturnes de votre wearable" + "subtitle": "Scores nocturnes de votre wearable", + "assessment": { + "metric": { + "SLEEP_SCORE": "votre score de sommeil global", + "SLEEP_EFFICIENCY": "l’efficacité du sommeil", + "SLEEP_PERFORMANCE": "la performance de sommeil", + "SLEEP_CONSISTENCY": "la régularité du sommeil" + }, + "grade": { + "excellent": "excellente", + "good": "solide", + "fair": "correcte", + "low": "plutôt faible" + }, + "clause": "Sur les 30 dernières nuits, {metric} est {grade}, à {value}.", + "closing": { + "excellent": "Gardez la routine qui vous mène jusque-là.", + "good": "Des heures de coucher et de lever plus stables sont souvent le prochain gain.", + "fair": "Protéger une fenêtre de sommeil constante est ce qui fait le plus progresser ce point.", + "low": "Un rituel d’endormissement régulier et une heure de réveil fixe sont les premiers leviers." + } + } }, "recovery": { "title": "Récupération", diff --git a/messages/it.json b/messages/it.json index 7608d2186..77ba27740 100644 --- a/messages/it.json +++ b/messages/it.json @@ -2575,7 +2575,7 @@ "chronotype": { "title": "Cronotipo", "learning": "Stiamo ancora imparando il tuo ritmo — {counted} di {total} notti libere. Un cronotipo richiede alcune mattine senza sveglia per avere significato.", - "midSleepCaption": "Metà del sonno nei giorni liberi intorno alle {clock}", + "midSleepCaption": "Il punto medio naturale del tuo sonno nei giorni liberi è intorno alle {clock}.", "advancedToggle": "Avanzate", "socialJetlagValue": "{hours} h {minutes} min di jet lag sociale", "socialJetlagCaption": "La differenza tra gli orari del sonno nei giorni lavorativi e in quelli liberi — come un piccolo cambio di fuso orario settimanale.", @@ -2588,7 +2588,8 @@ "intermediate": "Tipo intermedio", "late": "Tipo serale", "extreme_late": "Tipo serale marcato" - } + }, + "cornerLabel": "Cronotipo" }, "rhythm": { "loadError": "Non è stato possibile caricare il tuo ritmo del sonno in questo momento. Riapparirà non appena la connessione sarà stabile." @@ -3480,7 +3481,28 @@ }, "sleepQuality": { "title": "Qualità del sonno", - "subtitle": "Punteggi notturni del tuo wearable" + "subtitle": "Punteggi notturni del tuo wearable", + "assessment": { + "metric": { + "SLEEP_SCORE": "il tuo punteggio di sonno complessivo", + "SLEEP_EFFICIENCY": "l’efficienza del sonno", + "SLEEP_PERFORMANCE": "la prestazione del sonno", + "SLEEP_CONSISTENCY": "la regolarità del sonno" + }, + "grade": { + "excellent": "eccellente", + "good": "solida", + "fair": "discreta", + "low": "piuttosto bassa" + }, + "clause": "Nelle ultime 30 notti {metric} è {grade}, a {value}.", + "closing": { + "excellent": "Mantieni la routine che ti porta fin qui.", + "good": "Orari di sonno e risveglio più stabili sono di solito il prossimo guadagno.", + "fair": "Proteggere una finestra di sonno costante è ciò che alza di più questo valore.", + "low": "Una routine serale regolare e un orario di sveglia fisso sono le prime leve." + } + } }, "recovery": { "title": "Recupero", diff --git a/messages/pl.json b/messages/pl.json index 32e690173..6bd2a8336 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -2575,7 +2575,7 @@ "chronotype": { "title": "Chronotyp", "learning": "Wciąż poznajemy Twój rytm — {counted} z {total} wolnych nocy. Chronotyp wymaga kilku poranków bez budzika, aby coś znaczył.", - "midSleepCaption": "Środek snu w dni wolne około {clock}", + "midSleepCaption": "Naturalny środek Twojego snu w dni wolne przypada około {clock}.", "advancedToggle": "Zaawansowane", "socialJetlagValue": "{hours} godz. {minutes} min jet lagu społecznego", "socialJetlagCaption": "Różnica między porą snu w dni robocze i wolne — jak niewielka cotygodniowa zmiana strefy czasowej.", @@ -2588,7 +2588,8 @@ "intermediate": "Typ pośredni", "late": "Typ wieczorny", "extreme_late": "Wyraźny typ wieczorny" - } + }, + "cornerLabel": "Chronotyp" }, "rhythm": { "loadError": "Nie udało się teraz wczytać Twojego rytmu snu. Pojawi się ponownie, gdy połączenie się ustabilizuje." @@ -3480,7 +3481,28 @@ }, "sleepQuality": { "title": "Jakość snu", - "subtitle": "Nocne wyniki z Twojego urządzenia" + "subtitle": "Nocne wyniki z Twojego urządzenia", + "assessment": { + "metric": { + "SLEEP_SCORE": "Twój ogólny wynik snu", + "SLEEP_EFFICIENCY": "efektywność snu", + "SLEEP_PERFORMANCE": "wydajność snu", + "SLEEP_CONSISTENCY": "regularność snu" + }, + "grade": { + "excellent": "doskonała", + "good": "solidna", + "fair": "przeciętna", + "low": "raczej niska" + }, + "clause": "W ciągu ostatnich 30 nocy {metric} jest {grade} i wynosi {value}.", + "closing": { + "excellent": "Trzymaj się rutyny, która Cię tu doprowadziła.", + "good": "Bardziej stałe pory zasypiania i wstawania to zwykle następny zysk.", + "fair": "Utrzymanie stałego okna snu zwykle podnosi ten wynik najbardziej.", + "low": "Regularne wyciszenie wieczorem i stała pora pobudki to pierwsze dźwignie." + } + } }, "recovery": { "title": "Regeneracja", diff --git a/src/components/insights/sleep/__tests__/sleep-rhythm-cards.test.tsx b/src/components/insights/sleep/__tests__/sleep-rhythm-cards.test.tsx index 7e1dc23ae..ffa8e2572 100644 --- a/src/components/insights/sleep/__tests__/sleep-rhythm-cards.test.tsx +++ b/src/components/insights/sleep/__tests__/sleep-rhythm-cards.test.tsx @@ -110,6 +110,14 @@ describe("", () => { expect(html).toContain("04:30"); }); + it("renders the band as a labelled top-right corner readout, not a badge", () => { + const html = render(); + // The labelled corner carries both the "Chronotype" label and the value. + expect(html).toContain('data-slot="chronotype-corner"'); + expect(html).toContain("Chronotype"); + expect(html).toContain("Intermediate type"); + }); + it("inherits the standard Card rhythm (no compact override)", () => { const learning = render(); const ready = render(); diff --git a/src/components/insights/sleep/chronotype-card.tsx b/src/components/insights/sleep/chronotype-card.tsx index 1932e7b92..9b7ebfb4c 100644 --- a/src/components/insights/sleep/chronotype-card.tsx +++ b/src/components/insights/sleep/chronotype-card.tsx @@ -5,7 +5,6 @@ import { Clock, ChevronDown, ChevronUp } from "lucide-react"; import { useTranslations } from "@/lib/i18n/context"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; import { LearningGate } from "@/components/ui/learning-gate"; import type { ChronotypeDto } from "./use-sleep-rhythm"; @@ -67,26 +66,38 @@ export function ChronotypeCard({ chronotype }: { chronotype: ChronotypeDto }) { return ( -
- - - {t("insights.sleep.chronotype.title")} - + {/* The resolved chronotype rides the top-right corner as a labelled + readout — a small "Chronotyp" label over the band value — rather + than a badge buried in the body. The title stays left; the value + is the thing the eye should land on first. */} +
+
+ + + {t("insights.sleep.chronotype.title")} + +
+
+ + {t("insights.sleep.chronotype.cornerLabel")} + + + {bandLabel} + +
-
- - {bandLabel} - - {msfClock && ( -

- {t("insights.sleep.chronotype.midSleepCaption", { - clock: msfClock, - })} -

- )} -
+ {msfClock && ( +

+ {t("insights.sleep.chronotype.midSleepCaption", { + clock: msfClock, + })} +

+ )}