diff --git a/messages/de.json b/messages/de.json index 3dcc80763..b9a9a1292 100644 --- a/messages/de.json +++ b/messages/de.json @@ -3778,18 +3778,21 @@ "sections": { "account": { "title": "Konto", - "description": "Profil, Passwort, Passkeys und deine Onboarding-Tour." + "description": "Profil, Passwort, Passkeys und deine Onboarding-Tour.", + "subtitle": "Dein Profil, deine Identität und deine Anmeldedaten." }, "integrations": { "title": "Integrationen", - "description": "Verbundene Dienste, Zustellkanäle und die Quellen-Priorität an einem Ort." + "description": "Verbundene Dienste, Zustellkanäle und die Quellen-Priorität an einem Ort.", + "subtitle": "Datenquellen und Geräte verbinden und verwalten." }, "notifications": { "title": "Benachrichtigungen", "description": "Alle Erinnerungen an einem Ort — was wann erinnert wird. Die Zustellkanäle liegen unter Integrationen.", "channelsHint": "Telegram, ntfy, Web-Push, Webhook und E-Mail richtest du jetzt unter", "channelsHintLink": "Integrationen → Kanäle", - "inboxLink": "Posteingang öffnen" + "inboxLink": "Posteingang öffnen", + "subtitle": "Wähle, welche Erinnerungen du erhältst." }, "layout": { "title": "Layout", @@ -3802,15 +3805,18 @@ "insights": { "title": "Insights", "description": "Reihenfolge der Übersicht und der Navigations-Pills." - } + }, + "subtitle": "Passe an, wie Dashboard und Insights angeordnet sind." }, "dashboard": { "title": "Dashboard", - "description": "Kachel-Layout und Reihenfolge." + "description": "Kachel-Layout und Reihenfolge.", + "subtitle": "Ordne die Kacheln auf deinem Dashboard an." }, "thresholds": { "title": "Zielwerte", - "description": "Pro Metrik dein eigener Zielbereich für gesunde Werte." + "description": "Pro Metrik dein eigener Zielbereich für gesunde Werte.", + "subtitle": "Lege deine persönlichen Zielbereiche je Messgröße fest." }, "sources": { "title": "Quellen-Priorität", @@ -3869,19 +3875,23 @@ "scale": "Waage", "other": "Sonstige", "unknown": "Unbekannt" - } + }, + "subtitle": "Lege fest, welche Quelle gewinnt, wenn zwei dieselbe Messgröße liefern." }, "ai": { - "title": "KI-Auswertungen", - "description": "Provider, Modell und API-Key für die Auswertungen — dazu dein „Über mich“-Kontext." + "title": "KI-Anbieter", + "description": "Provider, Modell und API-Key für die Auswertungen — dazu dein „Über mich“-Kontext.", + "subtitle": "Wähle deinen KI-Anbieter und hinterlege deinen eigenen Schlüssel." }, "api": { "title": "API & Tokens", - "description": "Bearer-Tokens für eigene Skripte und Apps — erfasse Messwerte oder Einnahmen von überall." + "description": "Bearer-Tokens für eigene Skripte und Apps — erfasse Messwerte oder Einnahmen von überall.", + "subtitle": "API-Endpunkte und Zugriffstoken verwalten." }, "advanced": { "title": "Erweitert", - "description": "Opt-in-Schalter für den Forschungsmodus sowie der Notausgang zum unwiderruflichen Löschen aller Gesundheitsdaten." + "description": "Opt-in-Schalter für den Forschungsmodus sowie der Notausgang zum unwiderruflichen Löschen aller Gesundheitsdaten.", + "subtitle": "Research-Modus, Daten zurücksetzen und Konto löschen." }, "export": { "title": "Export & Import", @@ -3982,27 +3992,33 @@ "resultSummary": "{inserted} neu importiert und {updated} aktualisiert, {skipped} übersprungen.", "rowError": "Zeile {line}: {reason}" } - } + }, + "subtitle": "Exportiere deine Daten oder importiere aus einer Sicherung." }, "about": { "title": "Über diese App", - "description": "Version, Lizenz, Links." + "description": "Version, Lizenz, Links.", + "subtitle": "Version, Lizenz und Projektlinks." }, "sharing": { "title": "Freigabe", - "description": "Erstelle einen schreibgeschützten Link, um deine Gesundheitsakte mit einer Ärztin oder einem Arzt zu teilen." + "description": "Erstelle einen schreibgeschützten Link, um deine Gesundheitsakte mit einer Ärztin oder einem Arzt zu teilen.", + "subtitle": "Teile schreibgeschützten Zugriff auf deine Daten." }, "insights": { "title": "Insights", - "description": "Reihenfolge der Übersicht und der Navigations-Pills." + "description": "Reihenfolge der Übersicht und der Navigations-Pills.", + "subtitle": "Ordne die Übersichts-Bereiche und die Reihenfolge der Pills an." }, "medications": { "title": "Medikamente", - "description": "Listenansicht, Reihenfolge und Injektionsstellen deiner Medikamente." + "description": "Listenansicht, Reihenfolge und Injektionsstellen deiner Medikamente.", + "subtitle": "Verwalte Ansicht und Reihenfolge deiner Medikamentenliste." }, "mood": { "title": "Stimmungs-Tags", - "description": "Gruppen, Tags und archivierte Tags der Stimmungs-Auswahl." + "description": "Gruppen, Tags und archivierte Tags der Stimmungs-Auswahl.", + "subtitle": "Organisiere deine Stimmungs-Tags, Gruppen und Auswahlreihenfolge." }, "modules": { "title": "Module", @@ -4012,26 +4028,24 @@ "title": "Was du trackst", "description": "Optionale Bereiche. Deaktivierte Module verschwinden aus Navigation, Kacheln und Auswertungen — deine Daten bleiben erhalten." }, - "core": { - "title": "Immer aktiv", - "description": "Die Grundlagen von HealthLog. Diese Bereiche lassen sich nicht abschalten.", - "note": "Kernbereich — immer aktiv" - }, - "description": "Lege fest, was HealthLog für dich erfasst. Schalte einzelne Bereiche ein oder aus — die Navigation, Kacheln und Auswertungen passen sich sofort an.", "manageIn": "In {section} verwalten", - "operatorDisabled": "Serverweit deaktiviert" + "operatorDisabled": "Serverweit deaktiviert", + "subtitle": "Lege fest, welche Tracking-Bereiche HealthLog überall anzeigt." }, "gesundheitsakte": { "title": "Gesundheitsakte", - "description": "Vollständige Gesundheitsakte als PDF, FHIR R4 oder kombiniertes ZIP-Paket exportieren." + "description": "Vollständige Gesundheitsakte als PDF, FHIR R4 oder kombiniertes ZIP-Paket exportieren.", + "subtitle": "Exportiere eine vollständige, arzttaugliche Gesundheitsakte." }, "coach": { "title": "Coach", - "description": "Coach aktivieren, Ton und Umfang, gespeicherte Erinnerungen." + "description": "Coach aktivieren, Ton und Umfang, gespeicherte Erinnerungen.", + "subtitle": "Stelle den Coach, seine proaktiven Hinweise und sein Gedächtnis ein." }, "channels": { "title": "Kanäle", - "description": "Zustellung über Telegram, ntfy, Web-Push, Webhook und E-Mail." + "description": "Zustellung über Telegram, ntfy, Web-Push, Webhook und E-Mail.", + "subtitle": "Wohin Erinnerungen und Hinweise zugestellt werden." } }, "profile": "Profil", @@ -5291,7 +5305,12 @@ "subtitle": "Destruktive systemweite Operationen." }, "about": { - "title": "Über" + "title": "Über", + "subtitle": "Version, Lizenz und Update-Status." + }, + "module-availability": { + "title": "Modulverfügbarkeit", + "subtitle": "Module für jedes Konto auf diesem Server aktivieren oder deaktivieren." } }, "carrier": "Anbieter", @@ -6588,18 +6607,6 @@ "label": "Arztbericht", "description": "PDF- und FHIR-Export für deine Praxis." }, - "weight": { - "label": "Gewicht", - "description": "Gewicht und Körperzusammensetzung." - }, - "bloodPressure": { - "label": "Blutdruck", - "description": "Systolisch und diastolisch." - }, - "pulse": { - "label": "Puls", - "description": "Ruhepuls und Herzfrequenz." - }, "medications": { "label": "Medikamente", "description": "Pläne, Einnahmen und Compliance." diff --git a/messages/en.json b/messages/en.json index a87949920..8b8f063d4 100644 --- a/messages/en.json +++ b/messages/en.json @@ -3778,18 +3778,21 @@ "sections": { "account": { "title": "Account", - "description": "Profile, password, passkeys, and your onboarding tour." + "description": "Profile, password, passkeys, and your onboarding tour.", + "subtitle": "Your profile, identity, and sign-in details." }, "integrations": { "title": "Integrations", - "description": "Connected services, delivery channels, and source priority in one place." + "description": "Connected services, delivery channels, and source priority in one place.", + "subtitle": "Connect and manage your data sources and devices." }, "notifications": { "title": "Notifications", "description": "Every reminder in one place — what gets reminded and when. The delivery channels live under Integrations.", "channelsHint": "Telegram, ntfy, Web Push, Webhook, and Email are now set up under", "channelsHintLink": "Integrations → Channels", - "inboxLink": "Open the inbox" + "inboxLink": "Open the inbox", + "subtitle": "Choose which reminders you receive." }, "layout": { "title": "Layout", @@ -3802,15 +3805,18 @@ "insights": { "title": "Insights", "description": "Overview sections and navigation-pill order." - } + }, + "subtitle": "Personalize how your dashboard and insights are arranged." }, "dashboard": { "title": "Dashboard", - "description": "Tile layout and order." + "description": "Tile layout and order.", + "subtitle": "Arrange the tiles on your dashboard." }, "thresholds": { "title": "Targets", - "description": "Per metric: your own target range for healthy values." + "description": "Per metric: your own target range for healthy values.", + "subtitle": "Set your personal target ranges per metric." }, "sources": { "title": "Source priority", @@ -3869,19 +3875,23 @@ "scale": "Scale", "other": "Other", "unknown": "Unknown" - } + }, + "subtitle": "Decide which source wins when two report the same metric." }, "ai": { - "title": "AI Insights", - "description": "Provider, model, and API key for the insights — plus your “about me” context." + "title": "AI provider", + "description": "Provider, model, and API key for the insights — plus your “about me” context.", + "subtitle": "Choose your AI provider and bring your own key." }, "api": { "title": "API & Tokens", - "description": "Bearer tokens for your own scripts and apps — log measurements or medication intake from anywhere." + "description": "Bearer tokens for your own scripts and apps — log measurements or medication intake from anywhere.", + "subtitle": "Manage API endpoints and access tokens." }, "advanced": { "title": "Advanced", - "description": "Opt-in Research Mode toggle plus the account-wide danger zone for wiping every health record." + "description": "Opt-in Research Mode toggle plus the account-wide danger zone for wiping every health record.", + "subtitle": "Research mode, data reset, and account deletion." }, "export": { "title": "Export & Import", @@ -3982,27 +3992,33 @@ "resultSummary": "Imported {inserted} new and updated {updated}, {skipped} skipped.", "rowError": "Line {line}: {reason}" } - } + }, + "subtitle": "Export your data or import from a backup." }, "about": { "title": "About this app", - "description": "Version, license, links." + "description": "Version, license, links.", + "subtitle": "Version, license, and project links." }, "sharing": { "title": "Sharing", - "description": "Create a read-only link to share your health record with a clinician." + "description": "Create a read-only link to share your health record with a clinician.", + "subtitle": "Share read-only access to your data." }, "insights": { "title": "Insights", - "description": "Overview and navigation-pill order." + "description": "Overview and navigation-pill order.", + "subtitle": "Arrange your overview sections and pill order." }, "medications": { "title": "Medications", - "description": "List view, order, and injection sites for your medications." + "description": "List view, order, and injection sites for your medications.", + "subtitle": "Manage your medication list view and order." }, "mood": { "title": "Mood tags", - "description": "Groups, tags, and archived tags for the mood picker." + "description": "Groups, tags, and archived tags for the mood picker.", + "subtitle": "Organize your mood tags, groups, and picker order." }, "modules": { "title": "Modules", @@ -4012,26 +4028,24 @@ "title": "What you track", "description": "Optional areas. Disabled modules drop out of the navigation, tiles, and insights — your data stays untouched." }, - "core": { - "title": "Always on", - "description": "HealthLog's foundations. These areas can't be turned off.", - "note": "Core area — always on" - }, - "description": "Choose what HealthLog tracks for you. Turn individual areas on or off — the navigation, tiles, and insights adapt right away.", "manageIn": "Manage in {section}", - "operatorDisabled": "Disabled server-wide" + "operatorDisabled": "Disabled server-wide", + "subtitle": "Choose which tracking areas HealthLog shows across the app." }, "gesundheitsakte": { "title": "Health record", - "description": "Export your full health record as PDF, FHIR R4, or a combined ZIP package." + "description": "Export your full health record as PDF, FHIR R4, or a combined ZIP package.", + "subtitle": "Export a complete, clinician-ready health record." }, "coach": { "title": "Coach", - "description": "Enable the Coach, tone and scope, stored memories." + "description": "Enable the Coach, tone and scope, stored memories.", + "subtitle": "Tune the Coach, its proactive nudges, and its memory." }, "channels": { "title": "Channels", - "description": "Telegram, ntfy, Web Push, Webhook, and Email delivery." + "description": "Telegram, ntfy, Web Push, Webhook, and Email delivery.", + "subtitle": "Where reminders and alerts are delivered." } }, "profile": "Profile", @@ -5291,7 +5305,12 @@ "subtitle": "Destructive system-wide operations." }, "about": { - "title": "About" + "title": "About", + "subtitle": "Version, license, and update status." + }, + "module-availability": { + "title": "Module availability", + "subtitle": "Turn modules on or off for every account on this server." } }, "carrier": "Carrier", @@ -6588,18 +6607,6 @@ "label": "Doctor report", "description": "PDF and FHIR export for your practice." }, - "weight": { - "label": "Weight", - "description": "Weight and body composition." - }, - "bloodPressure": { - "label": "Blood pressure", - "description": "Systolic and diastolic." - }, - "pulse": { - "label": "Pulse", - "description": "Resting pulse and heart rate." - }, "medications": { "label": "Medications", "description": "Schedules, intakes, and compliance." diff --git a/messages/es.json b/messages/es.json index 99b6d7782..ad87143c8 100644 --- a/messages/es.json +++ b/messages/es.json @@ -3778,18 +3778,21 @@ "sections": { "account": { "title": "Cuenta", - "description": "Perfil, contraseña, claves de acceso y tu recorrido de incorporación." + "description": "Perfil, contraseña, claves de acceso y tu recorrido de incorporación.", + "subtitle": "Tu perfil, identidad y datos de acceso." }, "integrations": { "title": "Integraciones", - "description": "Servicios conectados, canales de entrega y prioridad de fuentes en un solo lugar." + "description": "Servicios conectados, canales de entrega y prioridad de fuentes en un solo lugar.", + "subtitle": "Conecta y gestiona tus fuentes de datos y dispositivos." }, "notifications": { "title": "Notificaciones", "description": "Todos los recordatorios en un solo lugar: qué se recuerda y cuándo. Los canales de entrega están en Integraciones.", "channelsHint": "Telegram, ntfy, Web Push, Webhook y correo se configuran ahora en", "channelsHintLink": "Integraciones → Canales", - "inboxLink": "Abrir la bandeja de entrada" + "inboxLink": "Abrir la bandeja de entrada", + "subtitle": "Elige qué recordatorios recibes." }, "layout": { "title": "Diseño", @@ -3802,15 +3805,18 @@ "insights": { "title": "Análisis", "description": "Orden de la vista general y de las pestañas de navegación." - } + }, + "subtitle": "Personaliza cómo se organizan tu panel e insights." }, "dashboard": { "title": "Panel", - "description": "Diseño y orden de los mosaicos." + "description": "Diseño y orden de los mosaicos.", + "subtitle": "Organiza los mosaicos de tu panel." }, "thresholds": { "title": "Objetivos", - "description": "Por métrica: tu propio rango objetivo de valores saludables." + "description": "Por métrica: tu propio rango objetivo de valores saludables.", + "subtitle": "Define tus rangos objetivo personales por métrica." }, "sources": { "title": "Prioridad de fuentes", @@ -3869,19 +3875,23 @@ "scale": "Báscula", "other": "Otro", "unknown": "Desconocido" - } + }, + "subtitle": "Decide qué fuente prevalece cuando dos informan la misma métrica." }, "ai": { - "title": "Análisis con IA", - "description": "Proveedor, modelo y clave API para los análisis, además de tu contexto «sobre mí»." + "title": "Proveedor de IA", + "description": "Proveedor, modelo y clave API para los análisis, además de tu contexto «sobre mí».", + "subtitle": "Elige tu proveedor de IA y usa tu propia clave." }, "api": { "title": "API & Tokens", - "description": "Tokens Bearer para tus propios scripts y apps — registra mediciones o tomas de medicación desde cualquier lugar." + "description": "Tokens Bearer para tus propios scripts y apps — registra mediciones o tomas de medicación desde cualquier lugar.", + "subtitle": "Gestiona los endpoints de la API y los tokens de acceso." }, "advanced": { "title": "Avanzado", - "description": "Activador opt-in del modo Investigación y zona de peligro para borrar irrevocablemente todos los registros de salud." + "description": "Activador opt-in del modo Investigación y zona de peligro para borrar irrevocablemente todos los registros de salud.", + "subtitle": "Modo investigación, restablecer datos y eliminar cuenta." }, "export": { "title": "Exportar e importar", @@ -3982,27 +3992,33 @@ "resultSummary": "{inserted} nuevas importadas y {updated} actualizadas, {skipped} omitidas.", "rowError": "Línea {line}: {reason}" } - } + }, + "subtitle": "Exporta tus datos o importa desde una copia de seguridad." }, "about": { "title": "Acerca de esta app", - "description": "Versión, licencia, enlaces." + "description": "Versión, licencia, enlaces.", + "subtitle": "Versión, licencia y enlaces del proyecto." }, "sharing": { "title": "Compartir", - "description": "Crea un enlace de solo lectura para compartir tu historial de salud con un profesional médico." + "description": "Crea un enlace de solo lectura para compartir tu historial de salud con un profesional médico.", + "subtitle": "Comparte acceso de solo lectura a tus datos." }, "insights": { "title": "Análisis", - "description": "Orden de la vista general y de las pestañas de navegación." + "description": "Orden de la vista general y de las pestañas de navegación.", + "subtitle": "Organiza las secciones de tu resumen y el orden de las píldoras." }, "medications": { "title": "Medicamentos", - "description": "Vista de lista, orden y zonas de inyección de tus medicamentos." + "description": "Vista de lista, orden y zonas de inyección de tus medicamentos.", + "subtitle": "Gestiona la vista y el orden de tu lista de medicamentos." }, "mood": { "title": "Etiquetas de ánimo", - "description": "Grupos, etiquetas y etiquetas archivadas del selector de ánimo." + "description": "Grupos, etiquetas y etiquetas archivadas del selector de ánimo.", + "subtitle": "Organiza tus etiquetas de ánimo, grupos y orden del selector." }, "modules": { "title": "Módulos", @@ -4012,26 +4028,24 @@ "title": "Qué registras", "description": "Áreas opcionales. Los módulos desactivados desaparecen de la navegación, los paneles y los análisis; tus datos se conservan." }, - "core": { - "title": "Siempre activos", - "description": "Los pilares de HealthLog. Estas áreas no se pueden desactivar.", - "note": "Área esencial — siempre activa" - }, - "description": "Elige qué registra HealthLog por ti. Activa o desactiva áreas concretas: la navegación, los paneles y los análisis se adaptan al instante.", "manageIn": "Gestionar en {section}", - "operatorDisabled": "Desactivado en todo el servidor" + "operatorDisabled": "Desactivado en todo el servidor", + "subtitle": "Elige qué áreas de seguimiento muestra HealthLog en la app." }, "gesundheitsakte": { "title": "Historial médico", - "description": "Exporta tu historial médico completo como PDF, FHIR R4 o un paquete ZIP combinado." + "description": "Exporta tu historial médico completo como PDF, FHIR R4 o un paquete ZIP combinado.", + "subtitle": "Exporta un historial de salud completo, listo para el médico." }, "coach": { "title": "Coach", - "description": "Activa el Coach, tono y alcance, recuerdos guardados." + "description": "Activa el Coach, tono y alcance, recuerdos guardados.", + "subtitle": "Ajusta el Coach, sus avisos proactivos y su memoria." }, "channels": { "title": "Canales", - "description": "Envío por Telegram, ntfy, Web Push, Webhook y correo electrónico." + "description": "Envío por Telegram, ntfy, Web Push, Webhook y correo electrónico.", + "subtitle": "Dónde se entregan los recordatorios y avisos." } }, "profile": "Perfil", @@ -5291,7 +5305,12 @@ "subtitle": "Operaciones destructivas en todo el sistema." }, "about": { - "title": "Acerca de" + "title": "Acerca de", + "subtitle": "Versión, licencia y estado de actualizaciones." + }, + "module-availability": { + "title": "Disponibilidad de módulos", + "subtitle": "Activa o desactiva módulos para todas las cuentas de este servidor." } }, "carrier": "Operador", @@ -6588,18 +6607,6 @@ "label": "Informe médico", "description": "Exportación PDF y FHIR para tu consulta." }, - "weight": { - "label": "Peso", - "description": "Peso y composición corporal." - }, - "bloodPressure": { - "label": "Presión arterial", - "description": "Sistólica y diastólica." - }, - "pulse": { - "label": "Pulso", - "description": "Pulso en reposo y frecuencia cardiaca." - }, "medications": { "label": "Medicamentos", "description": "Pautas, tomas y cumplimiento." diff --git a/messages/fr.json b/messages/fr.json index 85cb525e9..1c74cf1bf 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -3778,18 +3778,21 @@ "sections": { "account": { "title": "Compte", - "description": "Profil, mot de passe, clés d’accès et votre visite guidée." + "description": "Profil, mot de passe, clés d’accès et votre visite guidée.", + "subtitle": "Votre profil, votre identité et vos identifiants." }, "integrations": { "title": "Intégrations", - "description": "Services connectés, canaux de diffusion et priorité des sources au même endroit." + "description": "Services connectés, canaux de diffusion et priorité des sources au même endroit.", + "subtitle": "Connectez et gérez vos sources de données et appareils." }, "notifications": { "title": "Notifications", "description": "Tous les rappels au même endroit — quoi et quand. Les canaux de diffusion se trouvent dans Intégrations.", "channelsHint": "Telegram, ntfy, Web Push, Webhook et e-mail se configurent désormais dans", "channelsHintLink": "Intégrations → Canaux", - "inboxLink": "Ouvrir la boîte de réception" + "inboxLink": "Ouvrir la boîte de réception", + "subtitle": "Choisissez les rappels que vous recevez." }, "layout": { "title": "Disposition", @@ -3802,15 +3805,18 @@ "insights": { "title": "Analyses", "description": "Ordre de la vue d'ensemble et des onglets de navigation." - } + }, + "subtitle": "Personnalisez l'agencement de votre tableau de bord et de vos insights." }, "dashboard": { "title": "Tableau de bord", - "description": "Disposition et ordre des tuiles." + "description": "Disposition et ordre des tuiles.", + "subtitle": "Organisez les tuiles de votre tableau de bord." }, "thresholds": { "title": "Objectifs", - "description": "Par métrique : votre propre plage cible pour des valeurs saines." + "description": "Par métrique : votre propre plage cible pour des valeurs saines.", + "subtitle": "Définissez vos plages cibles personnelles par mesure." }, "sources": { "title": "Priorité des sources", @@ -3869,19 +3875,23 @@ "scale": "Balance", "other": "Autre", "unknown": "Inconnu" - } + }, + "subtitle": "Choisissez la source prioritaire en cas de doublon de mesure." }, "ai": { - "title": "Analyses IA", - "description": "Fournisseur, modèle et clé API pour les analyses, ainsi que votre contexte « à propos de moi »." + "title": "Fournisseur d'IA", + "description": "Fournisseur, modèle et clé API pour les analyses, ainsi que votre contexte « à propos de moi ».", + "subtitle": "Choisissez votre fournisseur d'IA et utilisez votre propre clé." }, "api": { "title": "API & Tokens", - "description": "Jetons Bearer pour vos propres scripts et applis — enregistrez des mesures ou des prises de médicaments depuis n’importe où." + "description": "Jetons Bearer pour vos propres scripts et applis — enregistrez des mesures ou des prises de médicaments depuis n’importe où.", + "subtitle": "Gérez les points d'accès API et les jetons d'accès." }, "advanced": { "title": "Avancé", - "description": "Bouton opt-in du mode Recherche et zone dangereuse pour effacer irrévocablement toutes les données de santé." + "description": "Bouton opt-in du mode Recherche et zone dangereuse pour effacer irrévocablement toutes les données de santé.", + "subtitle": "Mode recherche, réinitialisation des données et suppression du compte." }, "export": { "title": "Export et import", @@ -3982,27 +3992,33 @@ "resultSummary": "{inserted} nouvelles importées et {updated} mises à jour, {skipped} ignorées.", "rowError": "Ligne {line} : {reason}" } - } + }, + "subtitle": "Exportez vos données ou importez depuis une sauvegarde." }, "about": { "title": "À propos de cette app", - "description": "Version, licence, liens." + "description": "Version, licence, liens.", + "subtitle": "Version, licence et liens du projet." }, "sharing": { "title": "Partage", - "description": "Créez un lien en lecture seule pour partager votre dossier de santé avec un professionnel de santé." + "description": "Créez un lien en lecture seule pour partager votre dossier de santé avec un professionnel de santé.", + "subtitle": "Partagez un accès en lecture seule à vos données." }, "insights": { "title": "Analyses", - "description": "Ordre de la vue d'ensemble et des onglets de navigation." + "description": "Ordre de la vue d'ensemble et des onglets de navigation.", + "subtitle": "Organisez les sections d'aperçu et l'ordre des pastilles." }, "medications": { "title": "Médicaments", - "description": "Vue liste, ordre et sites d’injection de vos médicaments." + "description": "Vue liste, ordre et sites d’injection de vos médicaments.", + "subtitle": "Gérez l'affichage et l'ordre de votre liste de médicaments." }, "mood": { "title": "Étiquettes d’humeur", - "description": "Groupes, étiquettes et étiquettes archivées du sélecteur d’humeur." + "description": "Groupes, étiquettes et étiquettes archivées du sélecteur d’humeur.", + "subtitle": "Organisez vos tags d'humeur, groupes et ordre du sélecteur." }, "modules": { "title": "Modules", @@ -4012,26 +4028,24 @@ "title": "Ce que vous suivez", "description": "Domaines optionnels. Les modules désactivés disparaissent de la navigation, des tuiles et des analyses ; vos données sont conservées." }, - "core": { - "title": "Toujours actifs", - "description": "Les fondations de HealthLog. Ces domaines ne peuvent pas être désactivés.", - "note": "Domaine essentiel — toujours actif" - }, - "description": "Choisissez ce que HealthLog suit pour vous. Activez ou désactivez chaque domaine : la navigation, les tuiles et les analyses s'adaptent aussitôt.", "manageIn": "Gérer dans {section}", - "operatorDisabled": "Désactivé sur tout le serveur" + "operatorDisabled": "Désactivé sur tout le serveur", + "subtitle": "Choisissez les domaines de suivi affichés par HealthLog." }, "gesundheitsakte": { "title": "Dossier médical", - "description": "Exportez votre dossier médical complet au format PDF, FHIR R4 ou en paquet ZIP combiné." + "description": "Exportez votre dossier médical complet au format PDF, FHIR R4 ou en paquet ZIP combiné.", + "subtitle": "Exportez un dossier de santé complet, prêt pour le médecin." }, "coach": { "title": "Coach", - "description": "Activez le Coach, le ton et la portée, les souvenirs enregistrés." + "description": "Activez le Coach, le ton et la portée, les souvenirs enregistrés.", + "subtitle": "Réglez le Coach, ses suggestions proactives et sa mémoire." }, "channels": { "title": "Canaux", - "description": "Distribution via Telegram, ntfy, Web Push, Webhook et e-mail." + "description": "Distribution via Telegram, ntfy, Web Push, Webhook et e-mail.", + "subtitle": "Où sont envoyés les rappels et alertes." } }, "profile": "Profil", @@ -5291,7 +5305,12 @@ "subtitle": "Opérations destructrices à l'échelle du système." }, "about": { - "title": "À propos" + "title": "À propos", + "subtitle": "Version, licence et état des mises à jour." + }, + "module-availability": { + "title": "Disponibilité des modules", + "subtitle": "Activez ou désactivez les modules pour tous les comptes de ce serveur." } }, "carrier": "Opérateur", @@ -6588,18 +6607,6 @@ "label": "Rapport médical", "description": "Export PDF et FHIR pour votre cabinet." }, - "weight": { - "label": "Poids", - "description": "Poids et composition corporelle." - }, - "bloodPressure": { - "label": "Tension artérielle", - "description": "Systolique et diastolique." - }, - "pulse": { - "label": "Pouls", - "description": "Pouls au repos et fréquence cardiaque." - }, "medications": { "label": "Médicaments", "description": "Schémas, prises et observance." diff --git a/messages/it.json b/messages/it.json index 48507ed1d..8f3123811 100644 --- a/messages/it.json +++ b/messages/it.json @@ -3778,18 +3778,21 @@ "sections": { "account": { "title": "Account", - "description": "Profilo, password, passkey e il tour di benvenuto." + "description": "Profilo, password, passkey e il tour di benvenuto.", + "subtitle": "Il tuo profilo, la tua identità e i dati di accesso." }, "integrations": { "title": "Integrazioni", - "description": "Servizi collegati, canali di consegna e priorità delle fonti in un unico posto." + "description": "Servizi collegati, canali di consegna e priorità delle fonti in un unico posto.", + "subtitle": "Collega e gestisci le tue fonti di dati e i dispositivi." }, "notifications": { "title": "Notifiche", "description": "Tutti i promemoria in un unico posto — cosa e quando. I canali di recapito si trovano in Integrazioni.", "channelsHint": "Telegram, ntfy, Web Push, Webhook ed e-mail ora si configurano in", "channelsHintLink": "Integrazioni → Canali", - "inboxLink": "Apri la posta in arrivo" + "inboxLink": "Apri la posta in arrivo", + "subtitle": "Scegli quali promemoria ricevere." }, "layout": { "title": "Layout", @@ -3802,15 +3805,18 @@ "insights": { "title": "Approfondimenti", "description": "Ordine della panoramica e delle schede di navigazione." - } + }, + "subtitle": "Personalizza la disposizione di dashboard e insights." }, "dashboard": { "title": "Cruscotto", - "description": "Layout e ordine dei riquadri." + "description": "Layout e ordine dei riquadri.", + "subtitle": "Disponi i riquadri della tua dashboard." }, "thresholds": { "title": "Obiettivi", - "description": "Per metrica: il tuo intervallo target per valori sani." + "description": "Per metrica: il tuo intervallo target per valori sani.", + "subtitle": "Imposta i tuoi intervalli target personali per metrica." }, "sources": { "title": "Priorità delle fonti", @@ -3869,19 +3875,23 @@ "scale": "Bilancia", "other": "Altro", "unknown": "Sconosciuto" - } + }, + "subtitle": "Decidi quale fonte prevale quando due riportano la stessa metrica." }, "ai": { - "title": "Analisi IA", - "description": "Provider, modello e chiave API per gli approfondimenti, oltre al tuo contesto «su di me»." + "title": "Provider IA", + "description": "Provider, modello e chiave API per gli approfondimenti, oltre al tuo contesto «su di me».", + "subtitle": "Scegli il tuo provider IA e usa la tua chiave." }, "api": { "title": "API & Tokens", - "description": "Token Bearer per i tuoi script e le tue app — registra misurazioni o assunzioni di farmaci da ovunque." + "description": "Token Bearer per i tuoi script e le tue app — registra misurazioni o assunzioni di farmaci da ovunque.", + "subtitle": "Gestisci gli endpoint API e i token di accesso." }, "advanced": { "title": "Avanzate", - "description": "Interruttore opt-in della modalità Ricerca e zona pericolosa per cancellare in modo irrevocabile tutti i dati di salute." + "description": "Interruttore opt-in della modalità Ricerca e zona pericolosa per cancellare in modo irrevocabile tutti i dati di salute.", + "subtitle": "Modalità ricerca, ripristino dati ed eliminazione account." }, "export": { "title": "Esporta e importa", @@ -3982,27 +3992,33 @@ "resultSummary": "{inserted} nuove importate e {updated} aggiornate, {skipped} saltate.", "rowError": "Riga {line}: {reason}" } - } + }, + "subtitle": "Esporta i tuoi dati o importa da un backup." }, "about": { "title": "Informazioni su questa app", - "description": "Versione, licenza, link." + "description": "Versione, licenza, link.", + "subtitle": "Versione, licenza e link al progetto." }, "sharing": { "title": "Condivisione", - "description": "Crea un link di sola lettura per condividere la tua cartella clinica con un medico." + "description": "Crea un link di sola lettura per condividere la tua cartella clinica con un medico.", + "subtitle": "Condividi l'accesso in sola lettura ai tuoi dati." }, "insights": { "title": "Approfondimenti", - "description": "Ordine della panoramica e delle schede di navigazione." + "description": "Ordine della panoramica e delle schede di navigazione.", + "subtitle": "Disponi le sezioni della panoramica e l'ordine dei pill." }, "medications": { "title": "Farmaci", - "description": "Vista elenco, ordine e siti di iniezione dei tuoi farmaci." + "description": "Vista elenco, ordine e siti di iniezione dei tuoi farmaci.", + "subtitle": "Gestisci vista e ordine del tuo elenco di farmaci." }, "mood": { "title": "Tag dell’umore", - "description": "Gruppi, tag e tag archiviati del selettore dell’umore." + "description": "Gruppi, tag e tag archiviati del selettore dell’umore.", + "subtitle": "Organizza i tag dell'umore, i gruppi e l'ordine del selettore." }, "modules": { "title": "Moduli", @@ -4012,26 +4028,24 @@ "title": "Cosa monitori", "description": "Ambiti opzionali. I moduli disattivati spariscono da navigazione, riquadri e analisi; i tuoi dati restano intatti." }, - "core": { - "title": "Sempre attivi", - "description": "Le fondamenta di HealthLog. Questi ambiti non possono essere disattivati.", - "note": "Ambito di base — sempre attivo" - }, - "description": "Scegli cosa registra HealthLog per te. Attiva o disattiva i singoli ambiti: navigazione, riquadri e analisi si adattano subito.", "manageIn": "Gestisci in {section}", - "operatorDisabled": "Disattivato a livello di server" + "operatorDisabled": "Disattivato a livello di server", + "subtitle": "Scegli quali aree di monitoraggio HealthLog mostra nell'app." }, "gesundheitsakte": { "title": "Cartella clinica", - "description": "Esporta la tua cartella clinica completa in PDF, FHIR R4 o in un pacchetto ZIP combinato." + "description": "Esporta la tua cartella clinica completa in PDF, FHIR R4 o in un pacchetto ZIP combinato.", + "subtitle": "Esporta una cartella clinica completa, pronta per il medico." }, "coach": { "title": "Coach", - "description": "Attiva il Coach, tono e ambito, ricordi salvati." + "description": "Attiva il Coach, tono e ambito, ricordi salvati.", + "subtitle": "Regola il Coach, i suoi suggerimenti proattivi e la sua memoria." }, "channels": { "title": "Canali", - "description": "Recapito tramite Telegram, ntfy, Web Push, Webhook ed e-mail." + "description": "Recapito tramite Telegram, ntfy, Web Push, Webhook ed e-mail.", + "subtitle": "Dove vengono recapitati promemoria e avvisi." } }, "profile": "Profilo", @@ -5291,7 +5305,12 @@ "subtitle": "Operazioni distruttive a livello di sistema." }, "about": { - "title": "Informazioni" + "title": "Informazioni", + "subtitle": "Versione, licenza e stato degli aggiornamenti." + }, + "module-availability": { + "title": "Disponibilità dei moduli", + "subtitle": "Attiva o disattiva i moduli per tutti gli account su questo server." } }, "carrier": "Operatore", @@ -6588,18 +6607,6 @@ "label": "Referto medico", "description": "Esportazione PDF e FHIR per il tuo studio." }, - "weight": { - "label": "Peso", - "description": "Peso e composizione corporea." - }, - "bloodPressure": { - "label": "Pressione", - "description": "Sistolica e diastolica." - }, - "pulse": { - "label": "Battito", - "description": "Battito a riposo e frequenza cardiaca." - }, "medications": { "label": "Farmaci", "description": "Schemi, assunzioni e aderenza." diff --git a/messages/pl.json b/messages/pl.json index c60c651f4..5da1f13f1 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -3778,18 +3778,21 @@ "sections": { "account": { "title": "Konto", - "description": "Profil, hasło, klucze dostępu i przewodnik wprowadzający." + "description": "Profil, hasło, klucze dostępu i przewodnik wprowadzający.", + "subtitle": "Twój profil, tożsamość i dane logowania." }, "integrations": { "title": "Integracje", - "description": "Połączone usługi, kanały dostarczania i priorytet źródeł w jednym miejscu." + "description": "Połączone usługi, kanały dostarczania i priorytet źródeł w jednym miejscu.", + "subtitle": "Połącz i zarządzaj źródłami danych oraz urządzeniami." }, "notifications": { "title": "Powiadomienia", "description": "Wszystkie przypomnienia w jednym miejscu — co i kiedy. Kanały dostarczania znajdują się w Integracjach.", "channelsHint": "Telegram, ntfy, Web Push, Webhook i e-mail konfigurujesz teraz w", "channelsHintLink": "Integracje → Kanały", - "inboxLink": "Otwórz skrzynkę odbiorczą" + "inboxLink": "Otwórz skrzynkę odbiorczą", + "subtitle": "Wybierz, które przypomnienia otrzymujesz." }, "layout": { "title": "Układ", @@ -3802,15 +3805,18 @@ "insights": { "title": "Analizy", "description": "Kolejność przeglądu i pigułek nawigacyjnych." - } + }, + "subtitle": "Dostosuj układ pulpitu i analiz." }, "dashboard": { "title": "Pulpit", - "description": "Układ i kolejność kafelków." + "description": "Układ i kolejność kafelków.", + "subtitle": "Rozmieść kafelki na pulpicie." }, "thresholds": { "title": "Cele", - "description": "Dla każdej metryki: własny zakres docelowy zdrowych wartości." + "description": "Dla każdej metryki: własny zakres docelowy zdrowych wartości.", + "subtitle": "Ustaw osobiste zakresy docelowe dla każdej metryki." }, "sources": { "title": "Priorytet źródeł", @@ -3869,19 +3875,23 @@ "scale": "Waga", "other": "Inne", "unknown": "Nieznane" - } + }, + "subtitle": "Zdecyduj, które źródło wygrywa, gdy dwa podają tę samą metrykę." }, "ai": { - "title": "Analizy AI", - "description": "Dostawca, model i klucz API do analiz, a także Twój kontekst „o mnie”." + "title": "Dostawca AI", + "description": "Dostawca, model i klucz API do analiz, a także Twój kontekst „o mnie”.", + "subtitle": "Wybierz dostawcę AI i użyj własnego klucza." }, "api": { "title": "API & Tokens", - "description": "Tokeny Bearer dla własnych skryptów i aplikacji — zapisuj pomiary lub przyjęcia leków z dowolnego miejsca." + "description": "Tokeny Bearer dla własnych skryptów i aplikacji — zapisuj pomiary lub przyjęcia leków z dowolnego miejsca.", + "subtitle": "Zarządzaj punktami końcowymi API i tokenami dostępu." }, "advanced": { "title": "Zaawansowane", - "description": "Opt-in przełącznika trybu Badawczego oraz strefa zagrożenia umożliwiająca nieodwracalne wymazanie wszystkich danych zdrowotnych." + "description": "Opt-in przełącznika trybu Badawczego oraz strefa zagrożenia umożliwiająca nieodwracalne wymazanie wszystkich danych zdrowotnych.", + "subtitle": "Tryb badawczy, reset danych i usunięcie konta." }, "export": { "title": "Eksport i import", @@ -3982,27 +3992,33 @@ "resultSummary": "Zaimportowano {inserted} nowych i zaktualizowano {updated}, pominięto {skipped}.", "rowError": "Wiersz {line}: {reason}" } - } + }, + "subtitle": "Wyeksportuj dane lub zaimportuj z kopii zapasowej." }, "about": { "title": "O tej aplikacji", - "description": "Wersja, licencja, linki." + "description": "Wersja, licencja, linki.", + "subtitle": "Wersja, licencja i linki do projektu." }, "sharing": { "title": "Udostępnianie", - "description": "Utwórz link tylko do odczytu, aby udostępnić swoją dokumentację zdrowotną lekarzowi." + "description": "Utwórz link tylko do odczytu, aby udostępnić swoją dokumentację zdrowotną lekarzowi.", + "subtitle": "Udostępnij dostęp tylko do odczytu do swoich danych." }, "insights": { "title": "Analizy", - "description": "Kolejność przeglądu i pigułek nawigacyjnych." + "description": "Kolejność przeglądu i pigułek nawigacyjnych.", + "subtitle": "Rozmieść sekcje przeglądu i kolejność etykiet." }, "medications": { "title": "Leki", - "description": "Widok listy, kolejność i miejsca wstrzyknięć Twoich leków." + "description": "Widok listy, kolejność i miejsca wstrzyknięć Twoich leków.", + "subtitle": "Zarządzaj widokiem i kolejnością listy leków." }, "mood": { "title": "Tagi nastroju", - "description": "Grupy, tagi i zarchiwizowane tagi w wyborze nastroju." + "description": "Grupy, tagi i zarchiwizowane tagi w wyborze nastroju.", + "subtitle": "Uporządkuj tagi nastroju, grupy i kolejność wyboru." }, "modules": { "title": "Moduły", @@ -4012,26 +4028,24 @@ "title": "Co śledzisz", "description": "Obszary opcjonalne. Wyłączone moduły znikają z nawigacji, kafelków i analiz — Twoje dane pozostają nienaruszone." }, - "core": { - "title": "Zawsze aktywne", - "description": "Podstawy HealthLog. Tych obszarów nie można wyłączyć.", - "note": "Obszar podstawowy — zawsze aktywny" - }, - "description": "Wybierz, co HealthLog ma dla Ciebie śledzić. Włączaj i wyłączaj poszczególne obszary — nawigacja, kafelki i analizy dostosują się od razu.", "manageIn": "Zarządzaj w {section}", - "operatorDisabled": "Wyłączone na całym serwerze" + "operatorDisabled": "Wyłączone na całym serwerze", + "subtitle": "Wybierz, które obszary śledzenia pokazuje HealthLog w aplikacji." }, "gesundheitsakte": { "title": "Dokumentacja zdrowotna", - "description": "Wyeksportuj pełną dokumentację zdrowotną jako PDF, FHIR R4 lub łączony pakiet ZIP." + "description": "Wyeksportuj pełną dokumentację zdrowotną jako PDF, FHIR R4 lub łączony pakiet ZIP.", + "subtitle": "Eksportuj kompletną dokumentację zdrowotną gotową dla lekarza." }, "coach": { "title": "Coach", - "description": "Włącz Coacha, ton i zakres, zapisane wspomnienia." + "description": "Włącz Coacha, ton i zakres, zapisane wspomnienia.", + "subtitle": "Dostosuj Coacha, jego proaktywne podpowiedzi i pamięć." }, "channels": { "title": "Kanały", - "description": "Dostarczanie przez Telegram, ntfy, Web Push, Webhook i e-mail." + "description": "Dostarczanie przez Telegram, ntfy, Web Push, Webhook i e-mail.", + "subtitle": "Gdzie dostarczane są przypomnienia i powiadomienia." } }, "profile": "Profil", @@ -5291,7 +5305,12 @@ "subtitle": "Destrukcyjne operacje obejmujące cały system." }, "about": { - "title": "O programie" + "title": "O programie", + "subtitle": "Wersja, licencja i stan aktualizacji." + }, + "module-availability": { + "title": "Dostępność modułów", + "subtitle": "Włącz lub wyłącz moduły dla wszystkich kont na tym serwerze." } }, "carrier": "Operator", @@ -6588,18 +6607,6 @@ "label": "Raport dla lekarza", "description": "Eksport PDF i FHIR dla Twojej przychodni." }, - "weight": { - "label": "Waga", - "description": "Waga i skład ciała." - }, - "bloodPressure": { - "label": "Ciśnienie krwi", - "description": "Skurczowe i rozkurczowe." - }, - "pulse": { - "label": "Tętno", - "description": "Tętno spoczynkowe i rytm serca." - }, "medications": { "label": "Leki", "description": "Schematy, przyjęcia i zgodność." diff --git a/src/app/admin/[section]/renderer.tsx b/src/app/admin/[section]/renderer.tsx index aeea25435..4adc26c77 100644 --- a/src/app/admin/[section]/renderer.tsx +++ b/src/app/admin/[section]/renderer.tsx @@ -94,18 +94,32 @@ export function AdminSectionRenderer({ // v1.18.1 — the former ai-quality, assistant, and coach-feedback // sections fold into one Coach area: surface toggles + operator // config first, then the two feedback-quality tables. + // v1.18.6 (W9) — server-wide module availability moved out to its own + // `module-availability` section: it gates EVERY module, not just the + // coach, so it no longer belongs stacked under Coach. return ( - ); + case "module-availability": + // v1.18.6 (W9) — operator-side server-wide module on/off, its own + // admin section (was stacked under Coach). Reconciled name: the + // user-facing "Module" settings pick from what is available here. + return ( + + + + ); case "feedback": return ( ); case "about": - // v1.4.36 W4e — About section reused as-is from the settings - // surface. The component owns its own heading + cards layout - // so no SectionFrame wrapper. - // v1.18.6 — the About surface no longer carries the tour-replay card - // (consolidated onto Settings → Advanced), so there's nothing to hide. - return ; + // v1.4.36 W4e — About section reused as-is from the settings surface. + // v1.18.6 (W9) — route it through `SectionFrame` like every other admin + // section so it gets the same visible heading + subtitle (the one admin + // page that was bypassing the frame and showing no heading). + return ( + + + + ); default: slug satisfies never; return null; diff --git a/src/app/settings/[section]/page.tsx b/src/app/settings/[section]/page.tsx index 78115b334..b5295c215 100644 --- a/src/app/settings/[section]/page.tsx +++ b/src/app/settings/[section]/page.tsx @@ -1,5 +1,5 @@ import { notFound } from "next/navigation"; -import type { JSX } from "react"; +import type { JSX, ReactNode } from "react"; import { AccountSection } from "@/components/settings/account-section"; import { AboutSection } from "@/components/settings/about-section"; @@ -29,6 +29,9 @@ import { type SettingsSectionSlug, } from "@/components/settings/section-slugs"; import { SettingsShell } from "@/components/settings/settings-shell"; +import { SettingsSectionFrame } from "@/components/settings/settings-section-frame"; +import { SettingsHubBackLink } from "@/components/settings/settings-hub-back-link"; +import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger"; /** * Dynamic settings section route. Each of the `SETTINGS_SECTION_SLUGS` @@ -71,6 +74,50 @@ const SECTION_COMPONENTS: Record< advanced: AdvancedSection, }; +// v1.18.6 (W9) — the two Layout-hub child editors render a "← back to hub" +// link above their heading. The shared `` exposes a +// `topSlot` for exactly this; the link lives here so the section bodies stay +// pure card content. +const HUB_CHILD_SLUGS: ReadonlySet = new Set([ + "dashboard", + "insights", +]); + +/** Optional content rendered above the canonical heading, per slug. */ +function frameTopSlot(slug: SettingsSectionSlug): ReactNode { + if (HUB_CHILD_SLUGS.has(slug)) { + return ( + + ); + } + return undefined; +} + +/** Optional inline content rendered at the top-right of the heading row. */ +function frameHeadingAccessory(slug: SettingsSectionSlug): ReactNode { + // v1.18.6 (W9) — the module guided-tour re-entry triggers used to live in + // a `justify-end` header inside the section body; they move to the frame's + // heading row so the heading itself is the spotlight anchor. + if (slug === "gesundheitsakte") { + return ( + + + + ); + } + if (slug === "integrations") { + return ( + + + + ); + } + return undefined; +} + interface PageProps { // Next.js 16 made route `params` an async Promise. We `await` it before use. params: Promise<{ section: string }>; @@ -91,8 +138,17 @@ export default async function SettingsSectionPage({ params }: PageProps) { // guards the slug, but a future slug added to `SETTINGS_SECTION_SLUGS` // without a wired component would otherwise crash silently — placeholder // surfaces the gap visually instead. + // v1.18.6 (W9) — every section now renders inside the shared + // ``, which emits the one visible heading + subtitle + // each page must show (the section bodies no longer carry their own). const body = SectionComponent ? ( - + + + ) : ( ); diff --git a/src/components/admin/admin-shell.tsx b/src/components/admin/admin-shell.tsx index 70924d726..4b44785cf 100644 --- a/src/components/admin/admin-shell.tsx +++ b/src/components/admin/admin-shell.tsx @@ -18,6 +18,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { Bell, + Blocks, Database, FileText, Info, @@ -39,6 +40,7 @@ import { cn } from "@/lib/utils"; import { scrollBehaviorForUser } from "@/lib/motion"; import { useAuth } from "@/hooks/use-auth"; import { useTranslations } from "@/lib/i18n/context"; +import { SectionNavHeadingSpacer } from "@/components/settings/section-nav-heading-spacer"; import { isAdminSectionSlug, type AdminSectionSlug } from "./section-slugs"; interface AdminSection { @@ -82,6 +84,13 @@ export const ADMIN_SECTIONS: readonly AdminSection[] = [ titleKey: "admin.section.coach.title", icon: Sparkles, }, + // v1.18.6 (W9) — server-wide module availability split out of Coach into + // its own entry, sitting next to it. + { + slug: "module-availability", + titleKey: "admin.section.module-availability.title", + icon: Blocks, + }, { slug: "feedback", titleKey: "admin.section.feedback.title", @@ -276,6 +285,9 @@ export function AdminShell({ active, children }: AdminShellProps) { className="hidden md:block" >
+ {/* v1.18.6 (W9) — align the first nav item with the top of the + first card, matching the settings shell. */} +
  • ({ })); import { I18nProvider } from "@/lib/i18n/context"; +import { SettingsSectionFrame } from "../settings-section-frame"; import { CoachSection } from "../coach-section"; function render(locale: "en" | "de" = "en") { + // v1.18.6 (W9) — the visible heading lives in the shared frame the route + // wraps the section in; render through it so the heading id is present. return renderToStaticMarkup( - + + + , ); } @@ -51,6 +56,9 @@ describe(" — SSR smoke", () => { // v1.18.1 (D7) — polarity flipped to activate/default-on. expect(html).toContain("Activate Coach"); expect(html).toContain("Show the Coach button and drawer. On by default"); + // v1.18.6 (W9) — the proactive Coach nudge card moved here from + // Benachrichtigungen; its anchor renders under the enabled-coach gate. + expect(html).toContain('id="coach-nudge"'); // Raw key never leaks past i18n. expect(html).not.toContain("settings.sections.coach."); expect(html).not.toContain("settings.coach.activate."); diff --git a/src/components/settings/__tests__/export-section.test.tsx b/src/components/settings/__tests__/export-section.test.tsx index 264277863..bc648c7d2 100644 --- a/src/components/settings/__tests__/export-section.test.tsx +++ b/src/components/settings/__tests__/export-section.test.tsx @@ -44,6 +44,7 @@ vi.mock("@/hooks/use-auth", () => ({ })); import { I18nProvider } from "@/lib/i18n/context"; +import { SettingsSectionFrame } from "../settings-section-frame"; import { ExportSection } from "../export-section"; function render(node: React.ReactElement, locale: "en" | "de" = "en") { @@ -54,7 +55,13 @@ function render(node: React.ReactElement, locale: "en" | "de" = "en") { describe(" — SSR smoke", () => { it("renders the section heading + description", () => { - const html = render(); + // v1.18.6 (W9) — the page heading comes from the shared frame the route + // wraps the section in. + const html = render( + + + , + ); // v1.15.7 — the section was relabelled "Export & Import" (issue #281). expect(html).toContain("Export & Import"); // Raw key never leaks past i18n. diff --git a/src/components/settings/__tests__/gesundheitsakte-section.test.tsx b/src/components/settings/__tests__/gesundheitsakte-section.test.tsx index 57ab31493..baee6ffba 100644 --- a/src/components/settings/__tests__/gesundheitsakte-section.test.tsx +++ b/src/components/settings/__tests__/gesundheitsakte-section.test.tsx @@ -29,6 +29,7 @@ vi.mock("@/hooks/use-auth", () => ({ })); import { I18nProvider } from "@/lib/i18n/context"; +import { SettingsSectionFrame } from "../settings-section-frame"; import { GesundheitsakteSection } from "../gesundheitsakte-section"; function render(node: React.ReactElement, locale: "en" | "de" = "en") { @@ -38,8 +39,14 @@ function render(node: React.ReactElement, locale: "en" | "de" = "en") { } describe(" — SSR smoke", () => { - it("renders the section description and the health-record panel", () => { - const html = render(); + it("renders the section heading and the health-record panel", () => { + // v1.18.6 (W9) — the visible heading (with the historic id) comes from + // the shared frame the route wraps the section in. + const html = render( + + + , + ); expect(html).toContain('id="settings-section-gesundheitsakte-title"'); expect(html).toContain('data-testid="health-record-export-panel"'); // Raw key never leaks past i18n. diff --git a/src/components/settings/__tests__/medications-section.test.tsx b/src/components/settings/__tests__/medications-section.test.tsx index 7fc9b3d70..7bc97a732 100644 --- a/src/components/settings/__tests__/medications-section.test.tsx +++ b/src/components/settings/__tests__/medications-section.test.tsx @@ -65,12 +65,17 @@ vi.mock("@/hooks/use-auth", () => ({ })); import { I18nProvider } from "@/lib/i18n/context"; +import { SettingsSectionFrame } from "../settings-section-frame"; import { MedicationsSection } from "../medications-section"; function render(locale: "en" | "de" = "en"): string { + // v1.18.6 (W9) — the visible heading (with the historic id) comes from the + // shared frame the route wraps the section in. return renderToStaticMarkup( - + + + , ); } diff --git a/src/components/settings/__tests__/modules-section.test.tsx b/src/components/settings/__tests__/modules-section.test.tsx index 32d23c180..cb97fa27b 100644 --- a/src/components/settings/__tests__/modules-section.test.tsx +++ b/src/components/settings/__tests__/modules-section.test.tsx @@ -20,11 +20,7 @@ import { renderToStaticMarkup } from "react-dom/server"; import { I18nProvider } from "@/lib/i18n/context"; import type { AuthUser } from "@/hooks/use-auth"; import { queryKeys } from "@/lib/query-keys"; -import { - MODULE_KEYS, - CORE_DOMAIN_KEYS, - moduleDelegatesTo, -} from "@/lib/modules/registry"; +import { MODULE_KEYS, moduleDelegatesTo } from "@/lib/modules/registry"; // Capture the latest useMutation config + a shared mutate spy so we can // invoke `mutationFn` / `onSuccess` by hand (the SSR pass can't fire a @@ -46,9 +42,9 @@ vi.mock("@tanstack/react-query", () => ({ }, })); -const authSpy = vi.fn<() => { user: AuthUser | null; isAuthenticated: boolean }>( - () => ({ user: buildUser({}), isAuthenticated: true }), -); +const authSpy = vi.fn< + () => { user: AuthUser | null; isAuthenticated: boolean } +>(() => ({ user: buildUser({}), isAuthenticated: true })); vi.mock("@/hooks/use-auth", async () => { const actual = await vi.importActual( @@ -109,7 +105,10 @@ afterEach(() => { }); describe("", () => { - it("renders a Switch for every non-delegated toggleable module, a managed deep-link for delegated ones, and locked switches for core", () => { + it("renders a Switch for every non-delegated toggleable module and a managed deep-link for delegated ones", () => { + // v1.18.6 (W9) — the "Always on" core-domains card was removed (a domain + // that can't be turned off doesn't need listing), so this section now + // only renders the toggleable modules. const html = render(); for (const key of MODULE_KEYS) { if (moduleDelegatesTo(key) !== undefined) { @@ -126,8 +125,16 @@ describe("", () => { ); } } - for (const key of CORE_DOMAIN_KEYS) { - expect(html, `core switch ${key}`).toContain(`id="module-toggle-${key}"`); + }); + + it("no longer renders the 'Always on' core-domains card", () => { + // v1.18.6 (W9) — the read-only core card (weight / BP / pulse) was + // removed; the core domains are always active regardless. + const html = render(); + for (const key of ["weight", "bloodPressure", "pulse"]) { + expect(html, `core ${key} not rendered`).not.toContain( + `id="module-toggle-${key}"`, + ); } }); @@ -152,18 +159,6 @@ describe("", () => { expect(moodTag).toMatch(/data-state="checked"/); }); - it("renders the core domains locked (checked + disabled)", () => { - const html = render(); - for (const key of CORE_DOMAIN_KEYS) { - const tag = html.match( - new RegExp(`]*id="module-toggle-${key}"[^>]*>`), - )?.[0]; - expect(tag, `core ${key}`).toBeDefined(); - expect(tag).toMatch(/data-state="checked"/); - expect(tag).toMatch(/disabled/); - } - }); - it("PATCHes only the flipped key as a disabled-allowlist entry", async () => { render(); expect(lastMutation?.mutationFn).toBeTypeOf("function"); diff --git a/src/components/settings/__tests__/mood-section.test.tsx b/src/components/settings/__tests__/mood-section.test.tsx index 8f3975704..5f949344b 100644 --- a/src/components/settings/__tests__/mood-section.test.tsx +++ b/src/components/settings/__tests__/mood-section.test.tsx @@ -131,12 +131,17 @@ vi.mock("@/hooks/use-auth", () => ({ })); import { I18nProvider } from "@/lib/i18n/context"; +import { SettingsSectionFrame } from "../settings-section-frame"; import { MoodSection } from "../mood-section"; function render(locale: "en" | "de" = "en"): string { + // v1.18.6 (W9) — the visible heading (with the historic id) comes from the + // shared frame the route wraps the section in. return renderToStaticMarkup( - + + + , ); } diff --git a/src/components/settings/__tests__/notifications-section-module-gating.test.tsx b/src/components/settings/__tests__/notifications-section-module-gating.test.tsx index 4cc5cd46b..7b7831108 100644 --- a/src/components/settings/__tests__/notifications-section-module-gating.test.tsx +++ b/src/components/settings/__tests__/notifications-section-module-gating.test.tsx @@ -2,10 +2,13 @@ * v1.18.0 (S4) — module-gated per-type visibility on the consolidated * "Benachrichtigungen" reminder-types home. * - * The mood reminder shows only when the `mood` module is enabled; the Coach - * nudge only when `coach` is enabled. Low-stock maps to the medications CORE - * domain (always shown). The gate fails OPEN, so a `/me` payload without a - * module map keeps every card visible. + * The mood reminder shows only when the `mood` module is enabled. Low-stock + * maps to the medications domain (shown unless explicitly disabled). The gate + * fails OPEN, so a `/me` payload without a module map keeps every card + * visible. + * + * v1.18.6 (W9) — the proactive Coach nudge card moved to Settings → Coach, so + * it no longer appears on this screen. */ import { describe, expect, it, vi } from "vitest"; import { renderToStaticMarkup } from "react-dom/server"; @@ -47,30 +50,23 @@ function render(modules?: Record) { } describe(" module-gated reminder types", () => { - it("shows mood + coach cards when no module map (fails open)", () => { + it("shows mood + low-stock cards when no module map (fails open); no coach nudge", () => { const html = render(undefined); expect(html).toContain('id="mood-reminder"'); - expect(html).toContain('id="coach-nudge"'); expect(html).toContain('id="low-stock"'); + // v1.18.6 (W9) — the coach nudge moved to Settings → Coach. + expect(html).not.toContain('id="coach-nudge"'); }); it("hides the mood card when the mood module is disabled", () => { const html = render({ mood: false }); expect(html).not.toContain('id="mood-reminder"'); - // Coach + low-stock are unaffected. - expect(html).toContain('id="coach-nudge"'); + // Low-stock is unaffected. expect(html).toContain('id="low-stock"'); }); - it("hides the coach card when the coach module is disabled", () => { - const html = render({ coach: false }); - expect(html).not.toContain('id="coach-nudge"'); - expect(html).toContain('id="mood-reminder"'); - expect(html).toContain('id="low-stock"'); - }); - - it("always shows low-stock (medications is a CORE domain)", () => { - const html = render({ mood: false, coach: false }); + it("always shows low-stock unless medications is disabled", () => { + const html = render({ mood: false }); expect(html).toContain('id="low-stock"'); expect(html).not.toContain('id="mood-reminder"'); expect(html).not.toContain('id="coach-nudge"'); @@ -82,9 +78,9 @@ describe(" module-gated reminder types", () => { const html = render({}); expect(html).not.toContain('data-slot="notifications-channels-cross-link"'); expect(html).not.toContain('data-slot="notifications-inbox-cross-link"'); - // The three reminder-type cards remain. + // The remaining reminder-type cards (no coach nudge here anymore). expect(html).toContain('id="mood-reminder"'); expect(html).toContain('id="low-stock"'); - expect(html).toContain('id="coach-nudge"'); + expect(html).not.toContain('id="coach-nudge"'); }); }); diff --git a/src/components/settings/__tests__/sections-i18n-parity.test.ts b/src/components/settings/__tests__/sections-i18n-parity.test.ts index f85254a4d..d51d75cfc 100644 --- a/src/components/settings/__tests__/sections-i18n-parity.test.ts +++ b/src/components/settings/__tests__/sections-i18n-parity.test.ts @@ -2,11 +2,11 @@ * v1.4.16 phase B6 — settings i18n parity. * * Every slug declared in `SETTINGS_SECTION_SLUGS` MUST have a - * `settings.sections..title` AND a `settings.sections..description` - * resolved in both English and German. The audit doc - * (`docs/audit/v1416-settings-audit.md`) calls this out as the canonical - * shape — the sidebar, the page `

    `, and the page subtitle all read - * from these two keys, so a missing key paints a raw key in production. + * `settings.sections..title` AND a `settings.sections..subtitle` + * resolved in both English and German. The sidebar reads `.title`; the shared + * `` reads `.title` + `.subtitle` for the visible page + * heading + subheading (v1.18.6 W9), so a missing key paints a raw key in + * production. */ import { describe, expect, it } from "vitest"; @@ -46,43 +46,39 @@ describe("settings sections — i18n parity (B6)", () => { expect(deValue?.length ?? 0).toBeGreaterThan(0); }); - it(`every locale resolves settings.sections.${slug}.description`, () => { + it(`every locale resolves settings.sections.${slug}.subtitle`, () => { const enValue = lookup(en as Bag, [ "settings", "sections", slug, - "description", + "subtitle", ]); const deValue = lookup(de as Bag, [ "settings", "sections", slug, - "description", + "subtitle", ]); - expect(enValue, `EN missing description for ${slug}`).toBeTypeOf( - "string", - ); + expect(enValue, `EN missing subtitle for ${slug}`).toBeTypeOf("string"); expect(enValue?.length ?? 0).toBeGreaterThan(0); - expect(deValue, `DE missing description for ${slug}`).toBeTypeOf( - "string", - ); + expect(deValue, `DE missing subtitle for ${slug}`).toBeTypeOf("string"); expect(deValue?.length ?? 0).toBeGreaterThan(0); }); } - it("EN and DE description for every slug are different (catches accidental copy-paste)", () => { + it("EN and DE subtitle for every slug are different (catches accidental copy-paste)", () => { for (const slug of SETTINGS_SECTION_SLUGS) { const enValue = lookup(en as Bag, [ "settings", "sections", slug, - "description", + "subtitle", ]); const deValue = lookup(de as Bag, [ "settings", "sections", slug, - "description", + "subtitle", ]); // It's legitimate for some titles to coincide between locales when // the proper noun is the same (e.g. "Dashboard", "API & Tokens"). @@ -90,7 +86,7 @@ describe("settings sections — i18n parity (B6)", () => { // EN value into the DE bag is a bug. expect( deValue, - `DE description for ${slug} is identical to EN — needs translation`, + `DE subtitle for ${slug} is identical to EN — needs translation`, ).not.toBe(enValue); } }); diff --git a/src/components/settings/__tests__/sections.test.tsx b/src/components/settings/__tests__/sections.test.tsx index 50704d6e9..34ac19d7e 100644 --- a/src/components/settings/__tests__/sections.test.tsx +++ b/src/components/settings/__tests__/sections.test.tsx @@ -87,6 +87,8 @@ vi.mock("@/components/settings/thresholds-editor-section", () => ({ ThresholdsEditorSection: () =>
    , })); import { I18nProvider } from "@/lib/i18n/context"; +import { SettingsSectionFrame } from "../settings-section-frame"; +import type { SettingsSectionSlug } from "../section-slugs"; import { AboutSection } from "../about-section"; import { AccountSection } from "../account-section"; import { AdvancedSection } from "../advanced-section"; @@ -108,10 +110,25 @@ function render(node: React.ReactElement, locale: "en" | "de" = "en") { ); } +// v1.18.6 (W9) — the visible page heading + subtitle moved out of the section +// bodies into the shared `` that the route wraps every +// section in. The heading-level smoke checks render the body THROUGH the frame +// (mirroring production) so they assert what the user actually sees. +function renderFramed( + slug: SettingsSectionSlug, + node: React.ReactElement, + locale: "en" | "de" = "en", +) { + return render( + {node}, + locale, + ); +} + describe("settings sections — SSR smoke", () => { it(" renders the Account heading and Profile card", () => { - const html = render(); - // Section title resolves via the i18n provider. + const html = renderFramed("account", ); + // Section title resolves via the frame + i18n provider. expect(html).toContain("Account"); // Profile card heading is also painted. expect(html).toContain("Profile"); @@ -136,7 +153,7 @@ describe("settings sections — SSR smoke", () => { }); it(" renders version + license + sources/docs cards (no Updates panel)", () => { - const html = render(); + const html = renderFramed("about", ); expect(html).toContain("About"); // v1.4.36 W4f — the dedicated "Updates" card with its manual // "Check now" button is gone. The 24 h auto-check still runs and @@ -158,34 +175,33 @@ describe("settings sections — SSR smoke", () => { expect(html).not.toContain("Replay the tour"); }); - it(" renders the AI Insights heading", () => { - const html = render(); - // v1.8.7.1 — section renamed to "AI Insights" so the AI nature of the - // surface is explicit (was the bare "Insights"). The section H1 carries - // the only heading. - expect(html).toContain("AI Insights"); + it(" renders the AI provider heading", () => { + const html = renderFramed("ai", ); + // v1.18.6 (W9) — section renamed to "AI provider"; the visible heading + // now comes from the shared frame, which keeps the historic id. + expect(html).toContain("AI provider"); expect(html).toContain("settings-section-ai-title"); }); it(" renders Withings card title", () => { - const html = render(); + const html = renderFramed("integrations", ); expect(html).toContain("Integrations"); expect(html).toContain("Withings"); }); it(" renders the single reminder-types home", () => { - const html = render(); - // v1.18.1 (D0/D5) — the screen is lean: sr-only h1 only, no section blurb, - // no channels/inbox cross-links, and no embedded Vorsorge editor. + const html = renderFramed("notifications", ); + // v1.18.6 (W9) — the visible heading comes from the frame; the proactive + // Coach nudge moved to Settings → Coach, so it no longer renders here. expect(html).toContain("settings-section-notifications-title"); expect(html).not.toContain("Notification channels"); expect(html).not.toContain('data-slot="notifications-channels-cross-link"'); expect(html).not.toContain('data-slot="notifications-inbox-cross-link"'); - // The mood + coach cards fail OPEN (mock user carries no module map), - // so all three reminder-type cards render. + // The mood card fails OPEN (mock user carries no module map), so both + // remaining reminder-type cards render. The coach-nudge card is gone. expect(html).toContain('id="mood-reminder"'); - expect(html).toContain('id="coach-nudge"'); expect(html).toContain('id="low-stock"'); + expect(html).not.toContain('id="coach-nudge"'); // No raw i18n key leaks past the provider. expect(html).not.toContain("settings.sections.notifications."); }); @@ -194,32 +210,31 @@ describe("settings sections — SSR smoke", () => { // v1.4.3 split: the per-metric thresholds (Persönliche Zielwerte) moved // out into their own settings section under `/settings/thresholds`, // so the dashboard panel is now layout-only. Verifying the heading - // says "Dashboard" (not "Übersicht" — the v1.4.2 placeholder) and the - // layout slot still mounts. - const html = render(); + // (supplied by the frame) says "Dashboard" and the layout slot mounts. + const html = renderFramed("dashboard", ); expect(html).toContain("Dashboard"); expect(html).toContain('data-testid="dashboard-layout"'); expect(html).not.toContain('data-testid="thresholds"'); }); it(" renders endpoints + tokens cards", () => { - const html = render(); + const html = renderFramed("api", ); expect(html).toContain("API & Tokens"); }); it(" renders the danger-zone card only (export moved to /settings/export)", () => { - const html = render(); + const html = renderFramed("advanced", ); // v1.4.16 phase B7: every export path moved out into the dedicated // `` so what remains is the irreversible delete-all - // path. The section heading + dangerZone surface are the only - // primary surfaces here now. + // path. The heading (supplied by the frame) + dangerZone surface are the + // only primary surfaces here now. expect(html).toContain("Advanced"); expect(html).not.toContain("settings.export"); expect(html).not.toContain("doctorReport"); }); it(" resolves about.* keys in German", () => { - const html = render(, "de"); + const html = renderFramed("about", , "de"); // v1.4.36 W4f — "Updates" panel + "Jetzt prüfen" button removed; // the surviving German copy is the section title + the Sources // & docs heading + the version line. @@ -235,7 +250,7 @@ describe("settings sections — SSR smoke", () => { // `` (was ``). // v1.8.7.1 — Sources split back onto its own page, so this wrapper // renders the target-range editor only. - const html = render(); + const html = renderFramed("thresholds", ); // v1.8.7.1: section renamed to "Targets". expect(html).toContain("Targets"); expect(html).toContain('data-testid="thresholds-editor"'); @@ -244,7 +259,7 @@ describe("settings sections — SSR smoke", () => { }); it(" resolves the German title", () => { - const html = render(, "de"); + const html = renderFramed("thresholds", , "de"); // v1.8.7.1 — German title is "Zielwerte" (Targets only). expect(html).toContain("Zielwerte"); expect(html).toContain('data-testid="thresholds-editor"'); diff --git a/src/components/settings/__tests__/settings-shell.test.tsx b/src/components/settings/__tests__/settings-shell.test.tsx index ce27d54d2..275a4ec9f 100644 --- a/src/components/settings/__tests__/settings-shell.test.tsx +++ b/src/components/settings/__tests__/settings-shell.test.tsx @@ -319,9 +319,9 @@ describe("", () => { expect(html).toContain('href="/settings/mood"'); // Targets keeps its German nav entry ("Zielwerte"). expect(html).toContain("Zielwerte"); - // v1.8.7.1 — the AI Insights section is named "KI-Auswertungen" in - // German (the "KI" prefix makes the AI nature explicit). - expect(html).toContain("KI-Auswertungen"); + // v1.18.6 (W9) — the AI section is named "KI-Anbieter" in German: the + // page is about the provider / BYOK, not an "Auswertung". + expect(html).toContain("KI-Anbieter"); // API & Tokens is identical in both locales (proper noun + ampersand) expect(html).toContain("API & Tokens"); expect(html).toContain("Erweitert"); diff --git a/src/components/settings/about-section.tsx b/src/components/settings/about-section.tsx index 847c43357..787c5b46a 100644 --- a/src/components/settings/about-section.tsx +++ b/src/components/settings/about-section.tsx @@ -181,17 +181,12 @@ export function AboutSection() { }, [version?.version]); /* eslint-enable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */ + // v1.18.6 (W9) — the visible heading + subtitle now come from the section + // frame that wraps this component: `` on the settings + // route and the admin renderer's `SectionFrame` on `/admin/about`. The body + // is the version / license / sources cards. return ( -
    -
    -

    - {t("settings.sections.about.title")} -

    -
    - +
    {/* Identity card — version + license inline. The v1.4.2 layout stacked the license under the version inside its own boxed Badge, which read as a separate field even though the two values belong @@ -321,6 +316,6 @@ export function AboutSection() { button were collectively never the point of the surface — the only signal users ever cared about was "is a newer release out?", and the badge answers that with no clicks. */} -
    +
    ); } diff --git a/src/components/settings/account-section.tsx b/src/components/settings/account-section.tsx index bcd976592..bddb12312 100644 --- a/src/components/settings/account-section.tsx +++ b/src/components/settings/account-section.tsx @@ -367,20 +367,10 @@ export function AccountSection() { if (!user) return null; + // v1.18.6 (W9) — the visible heading + subtitle now come from the shared + // `` in the route; this body is the profile cards. return ( -
    - {/* v1.18.1 (D0) — the explanatory section blurb was dropped so every - settings section starts at the same vertical height; the sr-only - heading stays for the accessibility tree. */} -
    -

    - {t("settings.sections.account.title")} -

    -
    - +
    {/* Profile card */} {/* Profile photo card */} @@ -760,7 +750,7 @@ export function AccountSection() { -
    +

); } diff --git a/src/components/settings/advanced-section.tsx b/src/components/settings/advanced-section.tsx index 98462ca15..b8710d71f 100644 --- a/src/components/settings/advanced-section.tsx +++ b/src/components/settings/advanced-section.tsx @@ -51,26 +51,16 @@ import { apiFetchRaw } from "@/lib/api/api-fetch"; * dialog. */ export function AdvancedSection() { - const { t } = useTranslations(); - + // v1.18.6 (W9) — the visible heading + subtitle now come from the shared + // `` in the route; this body is the advanced cards. return ( -
- {/* v1.18.1 (D0) — section blurb dropped for consistent top alignment. */} -
-

- {t("settings.sections.advanced.title")} -

-
- +
{/* v1.18.1 (D1) — "Tour neu starten" relocated here from Account. */} -
+ ); } diff --git a/src/components/settings/ai-section.tsx b/src/components/settings/ai-section.tsx index 2ee988267..8cd89e9a7 100644 --- a/src/components/settings/ai-section.tsx +++ b/src/components/settings/ai-section.tsx @@ -21,10 +21,8 @@ import { AiInsightsCard } from "@/components/settings/ai/ai-insights-card"; import { useAuth } from "@/hooks/use-auth"; import { useMounted } from "@/hooks/use-mounted"; -import { useTranslations } from "@/lib/i18n/context"; export function AiSection() { - const { t } = useTranslations(); const { isAuthenticated } = useAuth(); // v1.16.6 — the auth query can resolve before this boundary // hydrates; every `disabled={!isAuthenticated}` binding in the child @@ -35,19 +33,8 @@ export function AiSection() { const mounted = useMounted(); const authed = mounted && isAuthenticated; - return ( -
- {/* v1.18.1 (D0) — section blurb dropped for consistent top alignment. */} -
-

- {t("settings.sections.ai.title")} -

-
- - - - {/* v1.18.1 (D8) — the "About me" context moved to Settings → Account - (under Profil, before Zyklus-Tracking). */} -
- ); + // v1.18.6 (W9) — the visible heading + subtitle now come from the shared + // `` in the route; this body is the single provider + // card. (The "About me" context moved to Settings → Account in v1.18.1 D8.) + return ; } diff --git a/src/components/settings/api-section.tsx b/src/components/settings/api-section.tsx index 8580b14cc..417b081de 100644 --- a/src/components/settings/api-section.tsx +++ b/src/components/settings/api-section.tsx @@ -37,19 +37,13 @@ interface ApiTokenInfo { } export function ApiSection() { - const { t } = useTranslations(); - + // v1.18.6 (W9) — the visible heading + subtitle now come from the shared + // `` in the route; this body is the API cards. return ( -
-
-

- {t("settings.sections.api.title")} -

-
- +
-
+ ); } diff --git a/src/components/settings/channels-section.tsx b/src/components/settings/channels-section.tsx index 65da12c95..ebceea87e 100644 --- a/src/components/settings/channels-section.tsx +++ b/src/components/settings/channels-section.tsx @@ -8,30 +8,13 @@ * delivery provider — the same family as a connected device — and now reads as * a first-class settings home rather than a tab hidden behind Integrationen. * - * The content is the existing ``; this wrapper only - * owns the section frame (sr-only heading scaffold matching every other - * settings section). + * The content is the existing ``. v1.18.6 (W9) — + * the visible heading + subtitle now come from the shared + * `` in the route; this wrapper is the panel only. */ import { NotificationChannelsPanel } from "@/components/settings/integrations/notification-channels-panel"; -import { useTranslations } from "@/lib/i18n/context"; export function ChannelsSection() { - const { t } = useTranslations(); - - return ( -
- {/* v1.18.1 (D0) — section blurb dropped for consistent top alignment. */} -
-

- {t("settings.sections.channels.title")} -

-
- - -
- ); + return ; } diff --git a/src/components/settings/coach-section.tsx b/src/components/settings/coach-section.tsx index 2d732bad2..7bdc78e9e 100644 --- a/src/components/settings/coach-section.tsx +++ b/src/components/settings/coach-section.tsx @@ -10,25 +10,31 @@ * 1. Disable Coach — the per-user opt-out (kept first so a user who * wants the surface gone finds it immediately). * 2. Coach preferences — tone, verbosity, data scope (`useCoachPrefs`). - * 3. Coach memory — durable-fact review + forget controls. + * 3. Proactive nudge — the daily-nudge opt-out + per-group frequency + * (moved here from Settings → Benachrichtigungen in + * v1.18.6: it is a Coach setting, not a generic + * notification). + * 4. Coach memory — durable-fact review + forget controls. * - * The nav entry is module-gated on `coach`. The preference + memory cards - * keep their own content gate on `!user.disableCoach` (mirrored from the + * The nav entry is module-gated on `coach`. The preference + nudge + memory + * cards keep their own content gate on `!user.disableCoach` (mirrored from the * old AI section): hiding the Coach hides its tuning + memory controls too. * * The "about me" context lives in the AI section, not here — the daily * briefing reads it too, so it is not a Coach-only setting. + * + * v1.18.6 (W9) — the visible heading + subtitle now come from the shared + * `` in the route; this body is the Coach cards only. */ import { DisableCoachCard } from "@/components/settings/ai/disable-coach-card"; import { CoachMemorySection } from "@/components/settings/coach-memory-section"; +import { CoachNudgeCard } from "@/components/settings/coach-nudge-card"; import { CoachPrefsSection } from "@/components/settings/coach-prefs-section"; import { useAuth } from "@/hooks/use-auth"; import { useMounted } from "@/hooks/use-mounted"; -import { useTranslations } from "@/lib/i18n/context"; export function CoachSection() { - const { t } = useTranslations(); const { isAuthenticated, user } = useAuth(); // Mirror the AI section's hydration-safe gating: the auth query can // resolve before this boundary hydrates, so gate the child props on @@ -42,22 +48,20 @@ export function CoachSection() { const coachEnabled = !mounted || !user?.disableCoach; return ( -
- {/* v1.18.1 (D0) — section blurb dropped for consistent top alignment. */} -
-

- {t("settings.sections.coach.title")} -

-
- +
{coachEnabled && } + {/* Proactive Coach nudge — moved here from Benachrichtigungen (v1.18.6). + Keeps the `#coach-nudge` anchor so existing deep links resolve. */} + {coachEnabled && ( +
+ +
+ )} + {coachEnabled && } -
+ ); } diff --git a/src/components/settings/dashboard-section.tsx b/src/components/settings/dashboard-section.tsx index 70fa97477..8342fbb7c 100644 --- a/src/components/settings/dashboard-section.tsx +++ b/src/components/settings/dashboard-section.tsx @@ -1,29 +1,12 @@ "use client"; import { DashboardLayoutSection } from "@/components/settings/dashboard-layout-section"; -import { SettingsHubBackLink } from "@/components/settings/settings-hub-back-link"; -import { useTranslations } from "@/lib/i18n/context"; +/** + * v1.18.6 (W9) — the visible heading + subtitle now come from the shared + * ``, and the "← back to hub" link rides the frame's + * `topSlot`. The body is the layout customizer only. + */ export function DashboardSection() { - const { t } = useTranslations(); - - return ( -
-
- -

- {t("settings.sections.dashboard.title")} -

- {/* v1.18.1 (D0) — section blurb dropped for consistent top alignment. */} -
- - -
- ); + return ; } diff --git a/src/components/settings/export-section.tsx b/src/components/settings/export-section.tsx index f6cf3d354..4116dd584 100644 --- a/src/components/settings/export-section.tsx +++ b/src/components/settings/export-section.tsx @@ -70,17 +70,11 @@ async function downloadFromUrl(url: string, filename: string): Promise { export function ExportSection() { const { t } = useTranslations(); + // v1.18.6 (W9) — the visible page heading + subtitle now come from the + // shared `` in the route; the inner `

` keeps its + // own "other options" subsection label. return ( -
-
-

- {t("settings.sections.export.title")} -

-
- +
{/* v1.18.0 (S5) — the full health-record export moved to its own top-level "Gesundheitsakte" section. This page keeps the generic CSV/JSON data-out paths and the import surface. */} @@ -111,7 +105,7 @@ export function ExportSection() { `export.zip` and the generic JSON paths. The export routes had backends with no UI until now. */} -
+ ); } diff --git a/src/components/settings/gesundheitsakte-section.tsx b/src/components/settings/gesundheitsakte-section.tsx index 7bf511e54..72ee9adac 100644 --- a/src/components/settings/gesundheitsakte-section.tsx +++ b/src/components/settings/gesundheitsakte-section.tsx @@ -18,32 +18,10 @@ */ import { HealthRecordExportPanel } from "@/components/settings/health-record-export-panel"; -import { useTranslations } from "@/lib/i18n/context"; -import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger"; export function GesundheitsakteSection() { - const { t } = useTranslations(); - return ( -
- {/* v1.18.1 (D0) — section blurb dropped for consistent top alignment. */} -
-

- {t("settings.sections.gesundheitsakte.title")} -

- {/* v1.18.6 — guided-tour re-entry for the doctor-report module + a - real spotlight anchor (the section h1 is sr-only). */} - - - -
- - -
- ); + // v1.18.6 (W9) — the visible heading + subtitle and the module tour-replay + // trigger now live in the shared `` in the route; the + // body is the health-record export panel only. + return ; } diff --git a/src/components/settings/insights-section.tsx b/src/components/settings/insights-section.tsx index ffc434e4b..2fa49fa9d 100644 --- a/src/components/settings/insights-section.tsx +++ b/src/components/settings/insights-section.tsx @@ -2,8 +2,6 @@ import { InsightsOverviewArrangeSection } from "@/components/settings/insights-overview-arrange-section"; import { InsightsPillOrderSection } from "@/components/settings/insights-pill-order-section"; -import { SettingsHubBackLink } from "@/components/settings/settings-hub-back-link"; -import { useTranslations } from "@/lib/i18n/context"; /** * v1.15.18 — the "Insights" settings section (analogous to the Dashboard @@ -15,28 +13,16 @@ import { useTranslations } from "@/lib/i18n/context"; * * Both write through the same `/api/insights/layout` contract the tab strip * and the overview share, so a save here repaints both surfaces in lockstep. + * + * v1.18.6 (W9) — the visible "Insights" heading + subtitle now come from the + * shared ``, and the "← back to hub" link rides the + * frame's `topSlot`. The body is the two arrange cards only. */ export function InsightsSection() { - const { t } = useTranslations(); - return ( -
-
- -

- {t("settings.sections.insights.title")} -

- {/* v1.18.1 (D0) — section blurb dropped for consistent top alignment. */} -
- +
-
+ ); } diff --git a/src/components/settings/integrations-section.tsx b/src/components/settings/integrations-section.tsx index 2bd9fc16c..ef21a48bf 100644 --- a/src/components/settings/integrations-section.tsx +++ b/src/components/settings/integrations-section.tsx @@ -16,8 +16,6 @@ */ import { ConnectionsPanel } from "@/components/settings/integrations/connections-panel"; -import { useTranslations } from "@/lib/i18n/context"; -import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger"; export { parseOAuthOutcome, @@ -25,27 +23,8 @@ export { } from "@/components/settings/integrations/connections-panel"; export function IntegrationsSection() { - const { t } = useTranslations(); - - return ( -
- {/* v1.18.1 (D0) — section blurb dropped for consistent top alignment. */} -
-

- {t("settings.sections.integrations.title")} -

- {/* v1.18.6 — guided-tour re-entry for the integrations module. The - visible affordance also gives the spotlight a real anchor (the - section h1 is sr-only). */} - - - -
- - -
- ); + // v1.18.6 (W9) — the visible heading + subtitle and the module tour-replay + // trigger now live in the shared `` in the route; the + // body is the connections panel only. + return ; } diff --git a/src/components/settings/layout-section.tsx b/src/components/settings/layout-section.tsx index a636c39af..e5ad4528c 100644 --- a/src/components/settings/layout-section.tsx +++ b/src/components/settings/layout-section.tsx @@ -53,47 +53,38 @@ const LAYOUT_LINKS: ReadonlyArray = [ export function LayoutSection() { const { t } = useTranslations(); + // v1.18.6 (W9) — the visible heading + subtitle now come from the shared + // `` in the route; this body is the hub link list. return ( -
-
-

- {t("settings.sections.layout.title")} -

-
- -
    - {LAYOUT_LINKS.map((link) => { - const Icon = link.icon; - return ( -
  • - -
  • - ); - })} -
-
+
    + {LAYOUT_LINKS.map((link) => { + const Icon = link.icon; + return ( +
  • + +
  • + ); + })} +
); } diff --git a/src/components/settings/medications-section.tsx b/src/components/settings/medications-section.tsx index c19d5c261..a1421aba9 100644 --- a/src/components/settings/medications-section.tsx +++ b/src/components/settings/medications-section.tsx @@ -73,17 +73,10 @@ export function MedicationsSection() { ), ].map((m) => ({ id: m.id, name: m.name, dose: m.dose, active: m.active })); + // v1.18.6 (W9) — the visible heading + subtitle now come from the shared + // `` in the route; this body is the medication cards. return ( -
-
-

- {t("settings.sections.medications.title")} -

-
- +
{/* View preference — cards vs table. The shared header toggle writes optimistically, so there is no Save button here. */}
-
+ ); } diff --git a/src/components/settings/modules-section.tsx b/src/components/settings/modules-section.tsx index f90fff8f7..d52ed2507 100644 --- a/src/components/settings/modules-section.tsx +++ b/src/components/settings/modules-section.tsx @@ -32,12 +32,9 @@ import { Droplet, Dumbbell, FileText, - HeartPulse, - Lock, MessageCircleHeart, Moon, Pill, - Scale, Smile, Sparkles, TestTube, @@ -52,10 +49,8 @@ import { useTranslations } from "@/lib/i18n/context"; import { apiPatch } from "@/lib/api/api-fetch"; import { queryKeys } from "@/lib/query-keys"; import { - CORE_DOMAIN_KEYS, MODULE_REGISTRY, moduleDelegatesTo, - type CoreDomainKey, type ModuleKey, } from "@/lib/modules/registry"; @@ -77,13 +72,6 @@ const MODULE_ICONS: Record = { doctorReport: FileText, }; -/** Neutral Lucide glyph per core (always-on) domain. */ -const CORE_ICONS: Record = { - weight: Scale, - bloodPressure: HeartPulse, - pulse: Activity, -}; - export function ModulesSection() { const { t } = useTranslations(); const { user } = useAuth(); @@ -113,103 +101,65 @@ export function ModulesSection() { }, }); + // v1.18.6 (W9) — the visible heading + subtitle now come from the shared + // `` (the former muted-`

` intro folded into the + // frame's standard subtitle). The "Module immer aktiv" core-domains card + // was removed: a domain that can't be turned off doesn't need to be listed. return ( -

- {/* v1.18.6 — restore a short intro line so a new user understands - that this page drives the nav, tiles and assessments they see. - (Dropped in v1.18.1 D0 for top alignment; the orientation cost - outweighed the alignment.) */} -
-

- {t("settings.sections.modules.title")} -

-

- {t("settings.sections.modules.description")} -

-
- -
- - {/* v1.18.6 — restore the one-line "what this section does" hint. */} -

- {t("settings.sections.modules.toggleable.description")} -

-
- {(Object.keys(MODULE_REGISTRY) as ModuleKey[]).map((key) => { - const def = MODULE_REGISTRY[key]; - // Default-on: a module is enabled unless explicitly `false`. - const enabled = modules[key] !== false; - // Delegated modules (cycle/coach) are owned elsewhere — never a - // live toggle here; deep-link to their real control instead. - const delegated = moduleDelegatesTo(key) !== undefined; - // Operator turned this module off server-wide: no per-user - // toggle can re-enable it, so show a read-only note. Delegated - // rows already deep-link, so this only matters for owned keys. - const operatorDisabled = - !delegated && moduleAvailability[key] === false; - return ( - toggle.mutate({ key, enabled: next })} - /> - ); - })} -
-
- - {/* Core domains — always on, read-only */} -
- -
- {CORE_DOMAIN_KEYS.map((key) => ( +
+ + {/* v1.18.6 — restore the one-line "what this section does" hint. */} +

+ {t("settings.sections.modules.toggleable.description")} +

+
+ {(Object.keys(MODULE_REGISTRY) as ModuleKey[]).map((key) => { + const def = MODULE_REGISTRY[key]; + // Default-on: a module is enabled unless explicitly `false`. + const enabled = modules[key] !== false; + // Delegated modules (cycle/coach) are owned elsewhere — never a + // live toggle here; deep-link to their real control instead. + const delegated = moduleDelegatesTo(key) !== undefined; + // Operator turned this module off server-wide: no per-user + // toggle can re-enable it, so show a read-only note. Delegated + // rows already deep-link, so this only matters for owned keys. + const operatorDisabled = + !delegated && moduleAvailability[key] === false; + return ( toggle.mutate({ key, enabled: next })} /> - ))} -
+ ); + })}
-
+ ); } diff --git a/src/components/settings/mood-section.tsx b/src/components/settings/mood-section.tsx index 5f31a7b58..dff7c7019 100644 --- a/src/components/settings/mood-section.tsx +++ b/src/components/settings/mood-section.tsx @@ -40,17 +40,10 @@ export function MoodSection() { ); + // v1.18.6 (W9) — the visible heading + subtitle now come from the shared + // `` in the route; this body is the mood-tag cards. return ( -
-
-

- {t("settings.sections.mood.title")} -

-
- +
{/* Groups — picker group order + own groups. */}
)}
-
+ ); } diff --git a/src/components/settings/notifications-section.tsx b/src/components/settings/notifications-section.tsx index ff979d0c5..edeeb8df3 100644 --- a/src/components/settings/notifications-section.tsx +++ b/src/components/settings/notifications-section.tsx @@ -1,8 +1,6 @@ "use client"; import { useAuth } from "@/hooks/use-auth"; -import { useTranslations } from "@/lib/i18n/context"; -import { CoachNudgeCard } from "@/components/settings/coach-nudge-card"; import { LowStockCard } from "@/components/settings/low-stock-card"; import { MoodReminderCard } from "@/components/settings/mood-reminder-card"; @@ -17,44 +15,31 @@ import { MoodReminderCard } from "@/components/settings/mood-reminder-card"; * * Each reminder-type card is shown ONLY when its module is enabled, read from * the resolved `useAuth().user.modules` map (the same `/auth/me` map nav and - * Insights pills gate off). Mood → `mood`, Coach nudge → `coach`, low-stock - * runway → `medications` (a toggleable fail-open module since D3, so a missing - * key reads as enabled and a stale `/me` payload never blanks a card). + * Insights pills gate off). Mood → `mood`, low-stock runway → `medications` + * (a toggleable fail-open module since D3, so a missing key reads as enabled + * and a stale `/me` payload never blanks a card). * * v1.18.1 (D5) — the page is intentionally lean: the section blurb, the * channels / inbox cross-links, and the embedded Vorsorge (preventive-care) * editor were all removed. Vorsorge has its own `/vorsorge` page, so embedding * it here was a duplicate. Delivery CHANNELS (Telegram / ntfy / Web Push / * Webhook / Email) live under Settings → Integrations → Channels. + * + * v1.18.6 (W9) — the visible heading + subtitle now come from the shared + * ``. The proactive-Coach nudge card moved to Settings → + * Coach (it is a Coach setting, not a generic notification), so this screen + * carries the mood + low-stock reminder cards only. */ export function NotificationsSection() { - const { t } = useTranslations(); const { isAuthenticated, user } = useAuth(); // v1.18.0 (S4) — module-gated per-type visibility. Fail OPEN (`!== false`) // so a stale `/me` payload without the module map keeps every card visible. const moodEnabled = user?.modules?.mood !== false; - const coachEnabled = user?.modules?.coach !== false; const medsEnabled = user?.modules?.medications !== false; return ( -
- {/* v1.18.1 (D0/D5) — the section blurb and the channels / inbox cross- - links were dropped: the header now starts at the same height as every - other section and the page carries only the reminder-type cards. */} -
-

- {t("settings.sections.notifications.title")} -

-
- - {/* v1.18.1 (D5) — the Vorsorge (preventive-care) block was removed; it - has its own /vorsorge page, so embedding the editor here was a - duplicate. This screen keeps the three reminder-type cards below. */} - +
{/* Mood check-in reminder — only when the mood module is enabled. */} {moodEnabled ? (
@@ -69,13 +54,6 @@ export function NotificationsSection() {
) : null} - - {/* Proactive Coach nudge — only when the coach module is enabled. */} - {coachEnabled ? ( -
- -
- ) : null} -
+ ); } diff --git a/src/components/settings/section-nav-heading-spacer.tsx b/src/components/settings/section-nav-heading-spacer.tsx new file mode 100644 index 000000000..5a5bcf962 --- /dev/null +++ b/src/components/settings/section-nav-heading-spacer.tsx @@ -0,0 +1,25 @@ +/** + * `` — invisible spacer that reserves exactly the + * height of a section's heading block (h1 + subtitle) plus the `space-y-6` + * gap that separates the heading from the first card. + * + * v1.18.6 (W9) — Marc's symmetry ask: the left settings / admin menu's first + * item must line up with the TOP OF THE FIRST CARD, not with the page + * heading. The page heading lives in the content column above the first card, + * so the sidebar needs to start one heading-block lower. Replicating the + * exact heading markup (same `text-2xl` h1 + `text-sm` subtitle) as a hidden, + * `aria-hidden` block keeps the offset pixel-accurate across locales and + * line-wrapping — a hard-coded `pt-[…]` would drift the moment a title or + * subtitle wraps to a second line. + * + * It is only rendered on `md+` (the desktop sticky sidebar); the mobile chip + * strip has no heading-to-align-with. + */ +export function SectionNavHeadingSpacer() { + return ( + + ); +} diff --git a/src/components/settings/settings-section-frame.tsx b/src/components/settings/settings-section-frame.tsx new file mode 100644 index 000000000..1191d1cb0 --- /dev/null +++ b/src/components/settings/settings-section-frame.tsx @@ -0,0 +1,74 @@ +"use client"; + +/** + * `` — the canonical page-frame for every Settings + * section. The settings twin of the admin renderer's `SectionFrame` + * (`src/app/admin/[section]/renderer.tsx`): it emits the one visible + * heading + subheading every page must show, in the reference style used by + * the Admin Konsole → Übersicht landing. + * + * Before this primitive every settings section hand-rolled its own + * `sr-only` `

`, so no settings page showed a visible heading and the + * left-menu-to-first-card alignment drifted page to page. Routing all 19 + * sections through one frame fixes both: a constant-height heading block + * above the first card, and a real visible `h1 + subtitle`. + * + * + * {cards} + * + * + * Title comes from `settings.sections..title`; subtitle from + * `settings.sections..subtitle`. The `

` keeps the historic + * `settings-section--title` id so the section's own `aria-labelledby` + * linkage — and the spotlight-tour anchors that reference it — stay intact. + * + * `topSlot` renders ABOVE the heading (used by the Layout-hub child + * editors for their "← back to hub" link, which must sit above the title). + * `headingAccessory` renders inline at the top-right of the heading row + * (used by Gesundheitsakte for its guided-tour re-entry trigger). + */ + +import * as React from "react"; + +import { useTranslations } from "@/lib/i18n/context"; +import type { SettingsSectionSlug } from "./section-slugs"; + +export interface SettingsSectionFrameProps { + slug: SettingsSectionSlug; + /** Optional content rendered above the heading (e.g. a back-to-hub link). */ + topSlot?: React.ReactNode; + /** Optional content rendered inline at the top-right of the heading row + * (e.g. a guided-tour re-entry trigger). */ + headingAccessory?: React.ReactNode; + children: React.ReactNode; +} + +export function SettingsSectionFrame({ + slug, + topSlot, + headingAccessory, + children, +}: SettingsSectionFrameProps) { + const { t } = useTranslations(); + const titleId = `settings-section-${slug}-title`; + + return ( +
+ {topSlot ?
{topSlot}
: null} +
+
+

+ {t(`settings.sections.${slug}.title`)} +

+

+ {t(`settings.sections.${slug}.subtitle`)} +

+
+ {headingAccessory ? ( +
{headingAccessory}
+ ) : null} +
+ {children} +
+ ); +} diff --git a/src/components/settings/settings-shell.tsx b/src/components/settings/settings-shell.tsx index e47b640a4..f99b74e2f 100644 --- a/src/components/settings/settings-shell.tsx +++ b/src/components/settings/settings-shell.tsx @@ -48,6 +48,7 @@ import { isSettingsSectionSlug, type SettingsSectionSlug, } from "./section-slugs"; +import { SectionNavHeadingSpacer } from "./section-nav-heading-spacer"; // Re-export so existing call-sites importing from this module keep working. // New server-side call-sites (e.g. `generateStaticParams()`) should import @@ -259,8 +260,7 @@ export function SettingsShell({ active, children }: SettingsShellProps) { // no `moduleGate` (global / CORE) are always shown. const modules = user?.modules; const visibleSections = SETTINGS_SECTIONS.filter( - (section) => - !section.moduleGate || modules?.[section.moduleGate] !== false, + (section) => !section.moduleGate || modules?.[section.moduleGate] !== false, ); // v1.4.33 IW4 — keep the active chip in view inside the horizontal @@ -358,6 +358,10 @@ export function SettingsShell({ active, children }: SettingsShellProps) { className="hidden md:block" >
+ {/* v1.18.6 (W9) — push the first nav item down to the top of the + first card (level with the page heading's following gap), so + the menu reads symmetric with the content column. */} +
    {visibleSections.map((section) => { const isActive = section.slug === activeSlug; diff --git a/src/components/settings/sources-section.tsx b/src/components/settings/sources-section.tsx index 077b6de04..9a714b156 100644 --- a/src/components/settings/sources-section.tsx +++ b/src/components/settings/sources-section.tsx @@ -278,31 +278,13 @@ export function SourcesSection() { return Object.keys(priority.deviceTypePriority).length; }, [priority]); + // v1.18.6 (W9) — the visible heading + subtitle now come from the shared + // `` (the off-style muted intro folded into the + // standard subtitle). The quiet cross-link back to Integrationen — where + // the sources this ladder ranks are added / removed — stays inside the + // card, below the ladder. return ( -
    - {/* v1.18.1 (D0/D4) — standalone left-side entry again. The section - blurb is dropped for consistent top alignment; the cross-link back - to Integrationen (where the sources this ladder ranks are added / - removed) stays as a quiet pointer. */} -
    -

    - {t("settings.sections.sources.title")} -

    -

    - {t("settings.sections.sources.integrationsHint")}{" "} - - {t("settings.sections.sources.integrationsHintLink")} - -

    -
    - +
    )} + + {/* v1.18.6 (W9) — quiet pointer back to Integrationen, where the + sources this ladder ranks are connected / removed. Folded into + the card so the page top stays the standard heading + subtitle. */} +

    + {t("settings.sections.sources.integrationsHint")}{" "} + + {t("settings.sections.sources.integrationsHintLink")} + +

    -
    +
); } diff --git a/src/components/settings/thresholds-section.tsx b/src/components/settings/thresholds-section.tsx index 7dd154044..2b94ab727 100644 --- a/src/components/settings/thresholds-section.tsx +++ b/src/components/settings/thresholds-section.tsx @@ -1,7 +1,6 @@ "use client"; import { ThresholdsEditorSection } from "@/components/settings/thresholds-editor-section"; -import { useTranslations } from "@/lib/i18n/context"; /** * `` — route-level wrapper for `/settings/thresholds` @@ -16,7 +15,8 @@ import { useTranslations } from "@/lib/i18n/context"; * flows, so each concern is self-contained on its own page. * * Visual structure on this page: - * 1. Section header (title + description). + * 1. Section heading + subtitle — supplied by the shared + * `` in the route (v1.18.6 W9). * 2. `` — per-metric range inputs. The editor * card keeps its own affordance row (icon, reset-all button). * @@ -31,21 +31,6 @@ import { useTranslations } from "@/lib/i18n/context"; * the names stop clashing. */ export function ThresholdsSection() { - const { t } = useTranslations(); - - return ( -
-
-

- {t("settings.sections.thresholds.title")} -

-
- - {/* Per-metric threshold ranges. */} - -
- ); + // Per-metric threshold ranges; the heading is supplied by the route frame. + return ; }