From 3ce0b69802bdffcd46f89b6f4801f5cb709bc2c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 5 Jun 2026 03:44:02 +0200 Subject: [PATCH] refactor(settings): unify card headers, integration verbs, test buttons, and tap targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Holistic consistency pass over the settings and integration surfaces so the same concept reads and behaves the same way everywhere. - Card headers: migrate the four integration cards (Withings, WHOOP, Fitbit/Google Health, Mood Log) plus the profile, passkeys, avatar, about, api-endpoints, api-tokens, and sharing cards onto the shared SettingsCardHeader primitive. Extend the primitive with an inline title accessory slot (Fitbit tag + experimental badge) and a multi-line description slot (overlap / experimental / deprecation sub-notes), keeping the rendered result identical. - Integration verbs: normalise Mood Log onto the canonical "Sync now" / "Sync all data" / "Synchronize" set used by the other integrations, across all six locales. Swap the full-sync icon from Download to RefreshCw and align its action-row icon metrics with the sibling cards. - Test buttons: migrate Telegram and ntfy off their hand-rolled "Test message" buttons onto the shared TestConnectionButton, so every channel and integration test control shares one icon, label, latency readout, and error taxonomy. Retire the now-unused testMessage / testSent strings. - ntfy auth-token placeholder no longer reuses the "Saved" status string; it now reads "Saved — enter new to replace" like the other secret fields. - Save-success copy: collapse the moodLog and Telegram bespoke strings onto the shared settings.saved string. - Tap targets: floor every size="sm" action-row button (sync / full-sync / test / disconnect / save / connect-secret copy) at min-h-11, fixed at the TestConnectionButton source and across the cards, so they clear the 44px touch minimum. - Contrast: the onboarding source-card badge moves from muted-on-muted to text-foreground; web-push card body rhythm aligns on space-y-4; Mood Log's deprecation note drops its bespoke 11px-italic treatment for the shared sub-note style. (cherry picked from commit 11931d14b7875b7e2184a3b373d7a34b842f8eae) --- messages/de.json | 10 +- messages/en.json | 10 +- messages/es.json | 10 +- messages/fr.json | 10 +- messages/it.json | 10 +- messages/pl.json | 10 +- src/components/onboarding/SourceCardGrid.tsx | 2 +- src/components/settings/_card-header.tsx | 19 +- src/components/settings/about-section.tsx | 17 +- src/components/settings/account-section.tsx | 28 ++- src/components/settings/api-section.tsx | 57 ++--- .../settings/integrations-section.tsx | 232 ++++++++++-------- src/components/settings/ntfy-card.tsx | 44 +--- src/components/settings/sharing-section.tsx | 15 +- src/components/settings/telegram-card.tsx | 48 +--- .../settings/test-connection-button.tsx | 1 + src/components/settings/web-push-card.tsx | 4 +- 17 files changed, 248 insertions(+), 279 deletions(-) diff --git a/messages/de.json b/messages/de.json index 01f86b281..fc2c4d664 100644 --- a/messages/de.json +++ b/messages/de.json @@ -3327,12 +3327,9 @@ "backedUp": "gesichert", "telegram": "Telegram", "telegramDescription": "Medikamenten-Erinnerungen per Bot.", - "telegramSaved": "Telegram-Einstellungen gespeichert", "botToken": "Bot-Token", "chatId": "Chat-ID", "enableNotifications": "Benachrichtigungen aktivieren", - "testMessage": "Test-Nachricht", - "testSent": "Test-Nachricht gesendet!", "telegramStep1": "1. Erstelle einen Bot über @BotFather in Telegram und kopiere den Token.", "telegramStep2": "2. Sende /start an deinen Bot, um den Chat zu aktivieren.", "telegramStep3": "3. Finde deine Chat-ID über @userinfobot oder die Bot-API.", @@ -3663,9 +3660,9 @@ "moodLogApiKeyPlaceholder": "ml_...", "moodLogWebhookSecret": "Webhook-Secret", "moodLogWebhookSecretHelp": "Dieses Secret in moodLog als Webhook-Secret eintragen", - "moodLogSync": "Sync starten", - "moodLogFullSync": "Voll-Sync", - "moodLogFullSyncConfirm": "Vollständig synchronisieren", + "moodLogSync": "Jetzt synchronisieren", + "moodLogFullSync": "Alle Daten synchronisieren", + "moodLogFullSyncConfirm": "Synchronisieren", "moodLogFullSyncTitle": "Vollsynchronisierung?", "moodLogFullSyncDescription": "Alle verfügbaren Stimmungsdaten werden vollständig synchronisiert.", "moodLogDisconnect": "Trennen", @@ -3674,7 +3671,6 @@ "moodLogEntries": "Einträge", "moodLogSyncResult": "{count} Einträge synchronisiert", "moodLogSyncFailed": "Sync fehlgeschlagen", - "moodLogSaved": "moodLog-Verbindung gespeichert", "moodLogDisconnected": "moodLog getrennt", "about": { "version": "App-Version", diff --git a/messages/en.json b/messages/en.json index d9a6e01b2..8aebe5038 100644 --- a/messages/en.json +++ b/messages/en.json @@ -3327,12 +3327,9 @@ "backedUp": "backed up", "telegram": "Telegram", "telegramDescription": "Medication reminders via bot.", - "telegramSaved": "Telegram settings saved", "botToken": "Bot Token", "chatId": "Chat ID", "enableNotifications": "Enable notifications", - "testMessage": "Test message", - "testSent": "Test message sent!", "telegramStep1": "1. Create a bot via @BotFather in Telegram and copy the token.", "telegramStep2": "2. Send /start to your bot to activate the chat.", "telegramStep3": "3. Find your Chat ID via @userinfobot or the Bot API.", @@ -3663,9 +3660,9 @@ "moodLogApiKeyPlaceholder": "ml_...", "moodLogWebhookSecret": "Webhook Secret", "moodLogWebhookSecretHelp": "Enter this secret in moodLog as webhook secret", - "moodLogSync": "Start sync", - "moodLogFullSync": "Full sync", - "moodLogFullSyncConfirm": "Synchronize fully", + "moodLogSync": "Sync now", + "moodLogFullSync": "Sync all data", + "moodLogFullSyncConfirm": "Synchronize", "moodLogFullSyncTitle": "Full sync?", "moodLogFullSyncDescription": "All available mood data will be fully synchronized.", "moodLogDisconnect": "Disconnect", @@ -3674,7 +3671,6 @@ "moodLogEntries": "Entries", "moodLogSyncResult": "{count} entries synchronized", "moodLogSyncFailed": "Sync failed", - "moodLogSaved": "moodLog connection saved", "moodLogDisconnected": "moodLog disconnected", "about": { "version": "App version", diff --git a/messages/es.json b/messages/es.json index 2dc15688b..2e4324c1e 100644 --- a/messages/es.json +++ b/messages/es.json @@ -3327,12 +3327,9 @@ "backedUp": "con copia de seguridad", "telegram": "Telegram Notifications", "telegramDescription": "Get reminders for missed medication intake via Telegram.", - "telegramSaved": "Ajustes de Telegram guardados", "botToken": "Token del bot", "chatId": "ID del chat", "enableNotifications": "Activar notificaciones", - "testMessage": "Mensaje de prueba", - "testSent": "¡Mensaje de prueba enviado!", "telegramStep1": "1. Crea un bot con @BotFather en Telegram y copia el token.", "telegramStep2": "2. Envía /start a tu bot para activar el chat.", "telegramStep3": "3. Obtén tu ID del chat con @userinfobot o la API del bot.", @@ -3663,9 +3660,9 @@ "moodLogApiKeyPlaceholder": "ml_...", "moodLogWebhookSecret": "Secreto del webhook", "moodLogWebhookSecretHelp": "Introduce este secreto en moodLog como secreto del webhook", - "moodLogSync": "Iniciar sincronización", - "moodLogFullSync": "Sincronización completa", - "moodLogFullSyncConfirm": "Sincronizar por completo", + "moodLogSync": "Sincronizar ahora", + "moodLogFullSync": "Sincronizar todos los datos", + "moodLogFullSyncConfirm": "Sincronizar", "moodLogFullSyncTitle": "¿Sincronización completa?", "moodLogFullSyncDescription": "Se sincronizarán por completo todos los datos de estado de ánimo disponibles.", "moodLogDisconnect": "Desconectar", @@ -3674,7 +3671,6 @@ "moodLogEntries": "Entradas", "moodLogSyncResult": "{count} entradas sincronizadas", "moodLogSyncFailed": "Falló la sincronización", - "moodLogSaved": "Conexión con moodLog guardada", "moodLogDisconnected": "moodLog desconectado", "about": { "version": "Versión de la app", diff --git a/messages/fr.json b/messages/fr.json index b85affb7c..10360185a 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -3327,12 +3327,9 @@ "backedUp": "sauvegardée", "telegram": "Telegram Notifications", "telegramDescription": "Get reminders for missed medication intake via Telegram.", - "telegramSaved": "Réglages Telegram enregistrés", "botToken": "Jeton du bot", "chatId": "ID du chat", "enableNotifications": "Activer les notifications", - "testMessage": "Message de test", - "testSent": "Message de test envoyé !", "telegramStep1": "1. Créez un bot via @BotFather dans Telegram et copiez le jeton.", "telegramStep2": "2. Envoyez /start à votre bot pour activer le chat.", "telegramStep3": "3. Trouvez votre ID de chat via @userinfobot ou l’API du bot.", @@ -3663,9 +3660,9 @@ "moodLogApiKeyPlaceholder": "ml_...", "moodLogWebhookSecret": "Secret du webhook", "moodLogWebhookSecretHelp": "Saisissez ce secret dans moodLog comme secret de webhook", - "moodLogSync": "Démarrer la synchronisation", - "moodLogFullSync": "Synchronisation complète", - "moodLogFullSyncConfirm": "Synchroniser entièrement", + "moodLogSync": "Synchroniser maintenant", + "moodLogFullSync": "Synchroniser toutes les données", + "moodLogFullSyncConfirm": "Synchroniser", "moodLogFullSyncTitle": "Synchronisation complète ?", "moodLogFullSyncDescription": "Toutes les données d’humeur disponibles seront entièrement synchronisées.", "moodLogDisconnect": "Déconnecter", @@ -3674,7 +3671,6 @@ "moodLogEntries": "Entrées", "moodLogSyncResult": "{count} entrées synchronisées", "moodLogSyncFailed": "Échec de la synchronisation", - "moodLogSaved": "Connexion moodLog enregistrée", "moodLogDisconnected": "moodLog déconnecté", "about": { "version": "Version de l’app", diff --git a/messages/it.json b/messages/it.json index f20349f8f..82b6af4e4 100644 --- a/messages/it.json +++ b/messages/it.json @@ -3327,12 +3327,9 @@ "backedUp": "con backup", "telegram": "Telegram Notifications", "telegramDescription": "Get reminders for missed medication intake via Telegram.", - "telegramSaved": "Impostazioni Telegram salvate", "botToken": "Token del bot", "chatId": "ID chat", "enableNotifications": "Attiva le notifiche", - "testMessage": "Messaggio di prova", - "testSent": "Messaggio di prova inviato!", "telegramStep1": "1. Crea un bot tramite @BotFather su Telegram e copia il token.", "telegramStep2": "2. Invia /start al tuo bot per attivare la chat.", "telegramStep3": "3. Trova il tuo ID chat tramite @userinfobot o l’API del bot.", @@ -3663,9 +3660,9 @@ "moodLogApiKeyPlaceholder": "ml_...", "moodLogWebhookSecret": "Secret del webhook", "moodLogWebhookSecretHelp": "Inserisci questo secret in moodLog come secret del webhook", - "moodLogSync": "Avvia sincronizzazione", - "moodLogFullSync": "Sincronizzazione completa", - "moodLogFullSyncConfirm": "Sincronizza completamente", + "moodLogSync": "Sincronizza ora", + "moodLogFullSync": "Sincronizza tutti i dati", + "moodLogFullSyncConfirm": "Sincronizza", "moodLogFullSyncTitle": "Sincronizzazione completa?", "moodLogFullSyncDescription": "Tutti i dati sull’umore disponibili verranno sincronizzati completamente.", "moodLogDisconnect": "Disconnetti", @@ -3674,7 +3671,6 @@ "moodLogEntries": "Voci", "moodLogSyncResult": "{count} voci sincronizzate", "moodLogSyncFailed": "Sincronizzazione non riuscita", - "moodLogSaved": "Connessione moodLog salvata", "moodLogDisconnected": "moodLog disconnesso", "about": { "version": "Versione dell’app", diff --git a/messages/pl.json b/messages/pl.json index 7a5ee5662..4562d276a 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -3327,12 +3327,9 @@ "backedUp": "z kopią zapasową", "telegram": "Telegram Notifications", "telegramDescription": "Get reminders for missed medication intake via Telegram.", - "telegramSaved": "Ustawienia Telegrama zapisane", "botToken": "Token bota", "chatId": "ID czatu", "enableNotifications": "Włącz powiadomienia", - "testMessage": "Wiadomość testowa", - "testSent": "Wiadomość testowa wysłana!", "telegramStep1": "1. Utwórz bota przez @BotFather w Telegramie i skopiuj token.", "telegramStep2": "2. Wyślij /start do swojego bota, aby aktywować czat.", "telegramStep3": "3. Znajdź swoje ID czatu przez @userinfobot lub API bota.", @@ -3663,9 +3660,9 @@ "moodLogApiKeyPlaceholder": "ml_...", "moodLogWebhookSecret": "Sekret webhooka", "moodLogWebhookSecretHelp": "Wprowadź ten sekret w moodLog jako sekret webhooka", - "moodLogSync": "Rozpocznij synchronizację", - "moodLogFullSync": "Pełna synchronizacja", - "moodLogFullSyncConfirm": "Synchronizuj w pełni", + "moodLogSync": "Synchronizuj teraz", + "moodLogFullSync": "Synchronizuj wszystkie dane", + "moodLogFullSyncConfirm": "Synchronizuj", "moodLogFullSyncTitle": "Pełna synchronizacja?", "moodLogFullSyncDescription": "Wszystkie dostępne dane o nastroju zostaną w pełni zsynchronizowane.", "moodLogDisconnect": "Rozłącz", @@ -3674,7 +3671,6 @@ "moodLogEntries": "Wpisy", "moodLogSyncResult": "{count} wpisów zsynchronizowano", "moodLogSyncFailed": "Synchronizacja nie powiodła się", - "moodLogSaved": "Połączenie moodLog zapisane", "moodLogDisconnected": "moodLog rozłączony", "about": { "version": "Wersja aplikacji", diff --git a/src/components/onboarding/SourceCardGrid.tsx b/src/components/onboarding/SourceCardGrid.tsx index 3067336f9..f4ea15554 100644 --- a/src/components/onboarding/SourceCardGrid.tsx +++ b/src/components/onboarding/SourceCardGrid.tsx @@ -227,7 +227,7 @@ function SourceCardItem({

{titleLabel}

{badgeLabel ? ( {badgeLabel} diff --git a/src/components/settings/_card-header.tsx b/src/components/settings/_card-header.tsx index 9a5b7f2ca..1198dec7d 100644 --- a/src/components/settings/_card-header.tsx +++ b/src/components/settings/_card-header.tsx @@ -35,14 +35,19 @@ import { cn } from "@/lib/utils"; export interface SettingsCardHeaderProps { /** Lucide icon component; rendered as `text-primary h-5 w-5`. */ icon: LucideIcon; - /** Card title — rendered as `

`. */ - title: string; + /** Card title — rendered as `

`. + * Accepts a node so a card can wrap the title in a link. */ + title: React.ReactNode; /** Optional id for the `

` so an outer `aria-labelledby` * attribute on the card itself can reference it. */ titleId?: string; + /** Optional inline accessory rendered in the title row, immediately + * after the title (e.g. a tag chip + experimental badge). The title + * row wraps so the accessories reflow below on a narrow viewport. */ + titleAccessory?: React.ReactNode; /** Optional short description rendered as muted text below the * title row. Either a string (paragraph) or arbitrary node - * (links, badges, etc.). */ + * (links, badges, extra sub-notes, etc.). */ description?: React.ReactNode; /** Optional right-aligned status surface — typically an * `` or a wrapper around badges. */ @@ -55,6 +60,7 @@ export function SettingsCardHeader({ icon: Icon, title, titleId, + titleAccessory, description, status, className, @@ -62,16 +68,19 @@ export function SettingsCardHeader({ return (
-
+
{status ?
{status}
: null}
{description ? ( -

{description}

+
+ {description} +
) : null}
); diff --git a/src/components/settings/about-section.tsx b/src/components/settings/about-section.tsx index 2cfdadc4c..c8ff258be 100644 --- a/src/components/settings/about-section.tsx +++ b/src/components/settings/about-section.tsx @@ -14,6 +14,7 @@ import { } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { SettingsCardHeader } from "@/components/settings/_card-header"; import { useAuth } from "@/hooks/use-auth"; import { useFormatters, useTranslations } from "@/lib/i18n/context"; import { restartOnboardingTour } from "@/lib/onboarding/tour-restart"; @@ -251,10 +252,7 @@ export function AboutSection() { which read as a separate field even though the two values belong on the same row. */}
-
- -

HealthLog

-
+ {isLoading || !version ? (
@@ -324,12 +322,11 @@ export function AboutSection() { telling the user what the section was for. */} {version && (
-
- -

- {t("settings.about.linksHeading")} -

-
+
- - -
@@ -1740,7 +1767,7 @@ function MoodLogCard({ setMsg(t("common.copied")); setMsgType("success"); }} - className="w-full sm:w-auto" + className="min-h-11 w-full sm:w-auto" > {/* v1.4.22 D / D-DSGN-M-04 — use the proper common.copy key instead of stripping the @@ -1778,18 +1805,27 @@ function MoodLogCard({ - @@ -1821,9 +1857,9 @@ function MoodLogCard({ diff --git a/src/components/settings/ntfy-card.tsx b/src/components/settings/ntfy-card.tsx index a4deb5803..ee23d3dc2 100644 --- a/src/components/settings/ntfy-card.tsx +++ b/src/components/settings/ntfy-card.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { Bell, Loader2, Send } from "lucide-react"; +import { Bell, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -10,6 +10,7 @@ import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { PasswordInput } from "@/components/ui/password-input"; import { SettingsCardHeader } from "@/components/settings/_card-header"; +import { TestConnectionButton } from "@/components/settings/test-connection-button"; import { useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; @@ -82,24 +83,6 @@ export function NtfyCard({ isAuthenticated }: { isAuthenticated: boolean }) { }, }); - const test = useMutation({ - mutationFn: async () => { - const res = await fetch("/api/settings/ntfy/test", { method: "POST" }); - if (!res.ok) { - const json = await res.json(); - throw new Error(json.error || t("common.error")); - } - }, - onSuccess: () => { - setSaveMsg(t("settings.testSent")); - setSaveMsgType("success"); - }, - onError: (err: Error) => { - setSaveMsg(err.message); - setSaveMsgType("error"); - }, - }); - return (
)} -
+
+ - - diff --git a/src/components/settings/test-connection-button.tsx b/src/components/settings/test-connection-button.tsx index 6d0b737d0..05a405609 100644 --- a/src/components/settings/test-connection-button.tsx +++ b/src/components/settings/test-connection-button.tsx @@ -83,6 +83,7 @@ export function TestConnectionButton({ type="button" variant="outline" size="sm" + className="min-h-11" onClick={handleClick} disabled={disabled || testing} > diff --git a/src/components/settings/web-push-card.tsx b/src/components/settings/web-push-card.tsx index 6eb552af5..7adc85ec3 100644 --- a/src/components/settings/web-push-card.tsx +++ b/src/components/settings/web-push-card.tsx @@ -163,7 +163,7 @@ export function WebPushCard() { description={t("settings.webPushDescription")} /> -
+
{loading ? (
@@ -182,6 +182,7 @@ export function WebPushCard() {