From 9182b60dc87e5e863df0989daf0b6233f68b15eb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Fri, 19 Jun 2026 21:10:21 +0200
Subject: [PATCH 1/7] feat(settings): add SettingsCard primitive for the
standard card shape
Wrap the ui Card so every Settings section composes from one bordered
container with the shared p-4 md:p-6 padding contract, replacing the
hand-rolled bg-card border-border rounded-xl border p-4 sm:p-6 div that
bypassed the primitive and stepped padding at sm: instead of md:.
---
src/components/settings/settings-card.tsx | 39 +++++++++++++++++++++++
1 file changed, 39 insertions(+)
create mode 100644 src/components/settings/settings-card.tsx
diff --git a/src/components/settings/settings-card.tsx b/src/components/settings/settings-card.tsx
new file mode 100644
index 000000000..237ae497f
--- /dev/null
+++ b/src/components/settings/settings-card.tsx
@@ -0,0 +1,39 @@
+import * as React from "react";
+
+import { Card } from "@/components/ui/card";
+import { cn } from "@/lib/utils";
+
+/**
+ * `` — the standard Settings card container.
+ *
+ * Every Settings section paints the same bordered surface: a rounded card
+ * with the app's one card-padding contract (`p-4 md:p-6` — 16 px on phones,
+ * 24 px from `md` up). Before this primitive the surface hand-rolled
+ * `bg-card border-border rounded-xl border p-4 sm:p-6` in ~35 files, which
+ * (a) bypassed the `` primitive entirely and (b) stepped padding at
+ * `sm:` (640 px) instead of `md:` (768 px), so on a ~700 px tablet Settings
+ * cards read denser than every other surface.
+ *
+ * This wraps the ui `` so the whole surface composes from one shape.
+ * Unlike the bare `` it does NOT impose the `flex flex-col gap` layout:
+ * Settings cards manage their own internal rhythm (`SettingsCardHeader` +
+ * `mt-4`/`space-y-*` bodies), so the container only owns the border, radius,
+ * background, and padding. Pass `className` for per-card extras
+ * (`scroll-mt-28`, `space-y-4`, `flex h-full flex-col`, …).
+ */
+export function SettingsCard({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
From e31f9690e28d06c2b848b62a746567779d8713c3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Fri, 19 Jun 2026 21:14:57 +0200
Subject: [PATCH 2/7] refactor(settings): compose single-card sections from
SettingsCard
Swap the hand-rolled bg-card border-border rounded-xl border p-4 sm:p-6
container for SettingsCard across the single-card sections (integration
cards, notification channel cards, coach prefs, cycle, modules, mood
reminder, thresholds, dashboard-layout, about-me, low-stock, and the
health-record export panel). Drops the sm: padding split and the
primitive bypass; landmark sections keep their element via as="section".
---
src/components/settings/about-me-section.tsx | 8 ++--
.../settings/ai/ai-insights-card.tsx | 5 +-
.../settings/ai/disable-coach-card.tsx | 7 +--
.../settings/coach-memory-section.tsx | 8 ++--
src/components/settings/coach-nudge-card.tsx | 7 +--
.../settings/coach-prefs-section.tsx | 6 +--
.../settings/cycle-tracking-card.tsx | 5 +-
.../settings/dashboard-layout-section.tsx | 7 +--
src/components/settings/email-card.tsx | 5 +-
.../settings/health-record-export-panel.tsx | 12 ++---
.../insights-overview-arrange-section.tsx | 7 +--
.../settings/insights-pill-order-section.tsx | 8 ++--
.../settings/integrations/fitbit-card.tsx | 6 +--
.../settings/integrations/nightscout-card.tsx | 6 +--
.../integrations/oauth-provider-card.tsx | 6 +--
.../settings/integrations/whoop-card.tsx | 5 +-
.../settings/integrations/withings-card.tsx | 5 +-
src/components/settings/low-stock-card.tsx | 8 ++--
src/components/settings/modules-section.tsx | 5 +-
.../settings/mood-reminder-card.tsx | 7 +--
src/components/settings/ntfy-card.tsx | 5 +-
src/components/settings/settings-card.tsx | 46 ++++++++++++++-----
src/components/settings/sources-section.tsx | 5 +-
src/components/settings/telegram-card.tsx | 5 +-
.../settings/thresholds-editor-section.tsx | 7 +--
src/components/settings/web-push-card.tsx | 5 +-
src/components/settings/webhook-card.tsx | 5 +-
src/generated | 1 +
28 files changed, 129 insertions(+), 83 deletions(-)
create mode 120000 src/generated
diff --git a/src/components/settings/about-me-section.tsx b/src/components/settings/about-me-section.tsx
index aeb554416..b92cace67 100644
--- a/src/components/settings/about-me-section.tsx
+++ b/src/components/settings/about-me-section.tsx
@@ -33,6 +33,7 @@ import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -181,10 +182,11 @@ export function AboutMeSection({
}
return (
-
{t("settings.ai.aboutMe.hint")} {t("settings.ai.aboutMe.profileHint")}
-
+
);
}
diff --git a/src/components/settings/ai/ai-insights-card.tsx b/src/components/settings/ai/ai-insights-card.tsx
index 0bd064e26..d761bc7da 100644
--- a/src/components/settings/ai/ai-insights-card.tsx
+++ b/src/components/settings/ai/ai-insights-card.tsx
@@ -21,6 +21,7 @@ import { Sparkles } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Label } from "@/components/ui/label";
import { NativeSelect } from "@/components/ui/native-select";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { apiFetchRaw } from "@/lib/api/api-fetch";
import { formatDateTime } from "@/lib/format";
@@ -133,7 +134,7 @@ export function AiInsightsCard({
}
return (
-
+
{/* The card used to render an icon-only header row (Sparkles +
status badges, no title), which left the tile unanchored next
to its titled siblings. It now follows the shared
@@ -192,7 +193,7 @@ export function AiInsightsCard({
}
/>
-
+
);
}
diff --git a/src/components/settings/ai/disable-coach-card.tsx b/src/components/settings/ai/disable-coach-card.tsx
index 9e1339768..73852c7d8 100644
--- a/src/components/settings/ai/disable-coach-card.tsx
+++ b/src/components/settings/ai/disable-coach-card.tsx
@@ -33,6 +33,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { MessageCircleHeart } from "lucide-react";
import { Switch } from "@/components/ui/switch";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { apiPatch } from "@/lib/api/api-fetch";
import { useAuth } from "@/hooks/use-auth";
@@ -140,10 +141,10 @@ export function DisableCoachCard({
}
return (
-
)}
-
+
);
}
diff --git a/src/components/settings/coach-memory-section.tsx b/src/components/settings/coach-memory-section.tsx
index d687c8901..f2a1c3670 100644
--- a/src/components/settings/coach-memory-section.tsx
+++ b/src/components/settings/coach-memory-section.tsx
@@ -35,6 +35,7 @@ import {
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { formatDateOrRelative } from "@/lib/format";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -134,10 +135,11 @@ export function CoachMemorySection({
}, [facts]);
return (
-
{t("settings.ai.coachMemory.summaryNote")}
-
+
);
}
diff --git a/src/components/settings/coach-nudge-card.tsx b/src/components/settings/coach-nudge-card.tsx
index 132f3ed87..61e2fc8d1 100644
--- a/src/components/settings/coach-nudge-card.tsx
+++ b/src/components/settings/coach-nudge-card.tsx
@@ -21,6 +21,7 @@ import { MessageCircleHeart } from "lucide-react";
import { Switch } from "@/components/ui/switch";
import { NativeSelect } from "@/components/ui/native-select";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -170,9 +171,9 @@ export function CoachNudgeCard({
}
return (
-
)}
-
+
);
}
diff --git a/src/components/settings/coach-prefs-section.tsx b/src/components/settings/coach-prefs-section.tsx
index 22b580069..b0ab641ea 100644
--- a/src/components/settings/coach-prefs-section.tsx
+++ b/src/components/settings/coach-prefs-section.tsx
@@ -34,6 +34,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { useCoachPrefs, useSaveCoachPrefs } from "@/hooks/use-coach-prefs";
@@ -106,8 +107,7 @@ export function CoachPrefsSection({ isAuthenticated }: CoachPrefsSectionProps) {
const isLoading = isAuthenticated && !persisted;
return (
-
-
+
);
}
diff --git a/src/components/settings/cycle-tracking-card.tsx b/src/components/settings/cycle-tracking-card.tsx
index 2a7cbf6af..d5c3d4a13 100644
--- a/src/components/settings/cycle-tracking-card.tsx
+++ b/src/components/settings/cycle-tracking-card.tsx
@@ -7,6 +7,7 @@ import { CalendarHeart } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -59,7 +60,7 @@ export function CycleTrackingCard({
const enabled = data?.cycleTrackingEnabled ?? false;
return (
-
+
) : null}
-
+
);
}
diff --git a/src/components/settings/dashboard-layout-section.tsx b/src/components/settings/dashboard-layout-section.tsx
index 581050345..b302eeabb 100644
--- a/src/components/settings/dashboard-layout-section.tsx
+++ b/src/components/settings/dashboard-layout-section.tsx
@@ -51,6 +51,7 @@ import {
import { apiDelete, apiGet, apiPut } from "@/lib/api/api-fetch";
import { useAuth } from "@/hooks/use-auth";
import { WIDGET_MODULE_BY_ID } from "@/lib/dashboard/widget-modules";
+import { SettingsCard } from "@/components/settings/settings-card";
/**
* v1.4.47 W4 — pure reorder helper shared by the arrow buttons and the
@@ -316,9 +317,9 @@ export function DashboardLayoutSection({ id }: { id: string }) {
const dirty = draft !== null && layout !== null;
return (
-
{/* v1.4.19 A6 — title / action row uses the same stack-on-mobile,
right-align-on-desktop contract as Account → Password +
@@ -577,7 +578,7 @@ export function DashboardLayoutSection({ id }: { id: string }) {
: t("dashboard.layoutCustomized")}
)}
-
+
);
}
diff --git a/src/components/settings/email-card.tsx b/src/components/settings/email-card.tsx
index f0b5b5a90..0a2801501 100644
--- a/src/components/settings/email-card.tsx
+++ b/src/components/settings/email-card.tsx
@@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { TestConnectionButton } from "@/components/settings/test-connection-button";
import { useTranslations } from "@/lib/i18n/context";
@@ -74,7 +75,7 @@ export function EmailCard({ isAuthenticated }: { isAuthenticated: boolean }) {
if (settings && !settings.smtpConfigured) return null;
return (
-
+
-
+
);
}
diff --git a/src/components/settings/health-record-export-panel.tsx b/src/components/settings/health-record-export-panel.tsx
index 213b4e1e5..fb46b085c 100644
--- a/src/components/settings/health-record-export-panel.tsx
+++ b/src/components/settings/health-record-export-panel.tsx
@@ -23,6 +23,7 @@
import { useId, useState } from "react";
import { ChevronDown, Download, FileText, Loader2 } from "lucide-react";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
@@ -191,15 +192,10 @@ export function HealthRecordExportPanel() {
}
return (
-
)}
-
+
);
}
diff --git a/src/components/settings/insights-overview-arrange-section.tsx b/src/components/settings/insights-overview-arrange-section.tsx
index 35b3fd4b9..e73d5ec09 100644
--- a/src/components/settings/insights-overview-arrange-section.tsx
+++ b/src/components/settings/insights-overview-arrange-section.tsx
@@ -9,6 +9,7 @@ import { useAuth } from "@/hooks/use-auth";
import { useFeatureFlags } from "@/hooks/use-feature-flags";
import { useInsightsLayoutQuery } from "@/hooks/use-insights-layout";
import { InsightsEditMode } from "@/components/insights/insights-edit-mode";
+import { SettingsCard } from "@/components/settings/settings-card";
import { type InsightsSectionId } from "@/lib/insights-layout";
/**
@@ -67,12 +68,12 @@ export function InsightsOverviewArrangeSection({ id }: { id?: string }) {
defaults over the user's real saved layout (the same QA-L1 gate the
mother page applies to its "Anpassen" toggle). */}
{isLoading ? (
-
-
+
) : (
@@ -286,7 +288,7 @@ export function InsightsPillOrderSection({ id }: { id?: string }) {
))}
)}
-
+
);
}
diff --git a/src/components/settings/integrations/fitbit-card.tsx b/src/components/settings/integrations/fitbit-card.tsx
index 1b280ad7d..666bd4607 100644
--- a/src/components/settings/integrations/fitbit-card.tsx
+++ b/src/components/settings/integrations/fitbit-card.tsx
@@ -34,6 +34,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { PasswordInput } from "@/components/ui/password-input";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { IntegrationStatusPill } from "@/components/settings/integration-status-pill";
import type { IntegrationPillState } from "@/components/settings/integration-status-pill";
@@ -188,9 +189,8 @@ export function FitbitCard({
: null;
return (
-
)}
-
+
);
}
diff --git a/src/components/settings/integrations/nightscout-card.tsx b/src/components/settings/integrations/nightscout-card.tsx
index c0ad7785a..8d13e06b0 100644
--- a/src/components/settings/integrations/nightscout-card.tsx
+++ b/src/components/settings/integrations/nightscout-card.tsx
@@ -28,6 +28,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { PasswordInput } from "@/components/ui/password-input";
import { Switch } from "@/components/ui/switch";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import {
IntegrationStatusPill,
@@ -158,9 +159,8 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) {
: null;
return (
-
)}
-
+
);
}
diff --git a/src/components/settings/integrations/oauth-provider-card.tsx b/src/components/settings/integrations/oauth-provider-card.tsx
index f107a1772..c5564c783 100644
--- a/src/components/settings/integrations/oauth-provider-card.tsx
+++ b/src/components/settings/integrations/oauth-provider-card.tsx
@@ -28,6 +28,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { PasswordInput } from "@/components/ui/password-input";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import {
IntegrationStatusPill,
@@ -201,9 +202,8 @@ export function OAuthProviderCard({
}
return (
-
)}
-
+
);
}
diff --git a/src/components/settings/integrations/whoop-card.tsx b/src/components/settings/integrations/whoop-card.tsx
index 4d7b4797a..1669ee9df 100644
--- a/src/components/settings/integrations/whoop-card.tsx
+++ b/src/components/settings/integrations/whoop-card.tsx
@@ -26,6 +26,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { PasswordInput } from "@/components/ui/password-input";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { IntegrationStatusPill } from "@/components/settings/integration-status-pill";
import type { IntegrationPillState } from "@/components/settings/integration-status-pill";
@@ -182,7 +183,7 @@ export function WhoopCard({
: null;
return (
-
+
)}
-
+
);
}
diff --git a/src/components/settings/integrations/withings-card.tsx b/src/components/settings/integrations/withings-card.tsx
index 2560322c3..1920c17f7 100644
--- a/src/components/settings/integrations/withings-card.tsx
+++ b/src/components/settings/integrations/withings-card.tsx
@@ -19,6 +19,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { PasswordInput } from "@/components/ui/password-input";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { IntegrationStatusPill } from "@/components/settings/integration-status-pill";
import type { IntegrationPillState } from "@/components/settings/integration-status-pill";
@@ -190,7 +191,7 @@ export function WithingsCard({
: null;
return (
-
+
)}
-
+
);
}
diff --git a/src/components/settings/low-stock-card.tsx b/src/components/settings/low-stock-card.tsx
index d25019d79..763c0a16d 100644
--- a/src/components/settings/low-stock-card.tsx
+++ b/src/components/settings/low-stock-card.tsx
@@ -7,6 +7,7 @@ import { PackageOpen } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -190,12 +191,13 @@ export function LowStockCard({
}
return (
-
-
+
);
}
diff --git a/src/components/settings/modules-section.tsx b/src/components/settings/modules-section.tsx
index d52ed2507..72db906b7 100644
--- a/src/components/settings/modules-section.tsx
+++ b/src/components/settings/modules-section.tsx
@@ -42,6 +42,7 @@ import {
type LucideIcon,
} from "lucide-react";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { ModuleToggleRow } from "@/components/settings/module-toggle-row";
import { useAuth } from "@/hooks/use-auth";
@@ -106,7 +107,7 @@ export function ModulesSection() {
// 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 (
-
+
-
+
);
}
diff --git a/src/components/settings/mood-reminder-card.tsx b/src/components/settings/mood-reminder-card.tsx
index 95ee03ae5..71510fafd 100644
--- a/src/components/settings/mood-reminder-card.tsx
+++ b/src/components/settings/mood-reminder-card.tsx
@@ -6,6 +6,7 @@ import { SmilePlus } from "lucide-react";
import { NativeSelect } from "@/components/ui/native-select";
import { Switch } from "@/components/ui/switch";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -155,9 +156,9 @@ export function MoodReminderCard({
}
return (
-
)}
-
+
);
}
diff --git a/src/components/settings/ntfy-card.tsx b/src/components/settings/ntfy-card.tsx
index 28cdcaed0..7f2022d2e 100644
--- a/src/components/settings/ntfy-card.tsx
+++ b/src/components/settings/ntfy-card.tsx
@@ -9,6 +9,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { PasswordInput } from "@/components/ui/password-input";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { TestConnectionButton } from "@/components/settings/test-connection-button";
import { useTranslations } from "@/lib/i18n/context";
@@ -82,7 +83,7 @@ export function NtfyCard({ isAuthenticated }: { isAuthenticated: boolean }) {
});
return (
-
+
-
+
);
}
diff --git a/src/components/settings/settings-card.tsx b/src/components/settings/settings-card.tsx
index 237ae497f..9537ca7a1 100644
--- a/src/components/settings/settings-card.tsx
+++ b/src/components/settings/settings-card.tsx
@@ -14,26 +14,50 @@ import { cn } from "@/lib/utils";
* `sm:` (640 px) instead of `md:` (768 px), so on a ~700 px tablet Settings
* cards read denser than every other surface.
*
- * This wraps the ui `` so the whole surface composes from one shape.
+ * The default renders the ui `` so the surface composes from one shape.
* Unlike the bare `` it does NOT impose the `flex flex-col gap` layout:
- * Settings cards manage their own internal rhythm (`SettingsCardHeader` +
+ * Settings card bodies own their internal rhythm (`SettingsCardHeader` +
* `mt-4`/`space-y-*` bodies), so the container only owns the border, radius,
* background, and padding. Pass `className` for per-card extras
* (`scroll-mt-28`, `space-y-4`, `flex h-full flex-col`, …).
+ *
+ * `as="section"` keeps a card's semantic landmark element (a `` with
+ * an `aria-labelledby`) while painting the same shape and padding contract.
*/
-export function SettingsCard({
+const SETTINGS_CARD_SHELL = "block gap-0 p-4 md:p-6";
+
+type SettingsCardOwnProps = { className?: string };
+
+type SettingsCardProps = SettingsCardOwnProps & {
+ as?: E;
+} & Omit, keyof SettingsCardOwnProps | "as">;
+
+export function SettingsCard({
+ as,
className,
...props
-}: React.ComponentProps) {
+}: SettingsCardProps) {
+ if (as) {
+ const Component = as as React.ElementType;
+ return (
+ -backed one.
+ className={cn(
+ "bg-card text-card-foreground rounded-xl border p-4 shadow-sm md:p-6",
+ className,
+ )}
+ {...props}
+ />
+ );
+ }
return (
)}
/>
);
}
diff --git a/src/components/settings/sources-section.tsx b/src/components/settings/sources-section.tsx
index 9a714b156..237b019c7 100644
--- a/src/components/settings/sources-section.tsx
+++ b/src/components/settings/sources-section.tsx
@@ -42,6 +42,7 @@ import {
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -285,7 +286,7 @@ export function SourcesSection() {
// card, below the ladder.
return (
-
+
-
+
);
}
diff --git a/src/components/settings/telegram-card.tsx b/src/components/settings/telegram-card.tsx
index 40d42f681..2813f4f2c 100644
--- a/src/components/settings/telegram-card.tsx
+++ b/src/components/settings/telegram-card.tsx
@@ -9,6 +9,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { PasswordInput } from "@/components/ui/password-input";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { TestConnectionButton } from "@/components/settings/test-connection-button";
import { useTranslations } from "@/lib/i18n/context";
@@ -87,7 +88,7 @@ export function TelegramCard({
}
return (
-
+
-
+
);
}
diff --git a/src/components/settings/thresholds-editor-section.tsx b/src/components/settings/thresholds-editor-section.tsx
index 092429e2f..e1cf9aa80 100644
--- a/src/components/settings/thresholds-editor-section.tsx
+++ b/src/components/settings/thresholds-editor-section.tsx
@@ -19,6 +19,7 @@ import {
type EffectiveRange,
} from "@/lib/analytics/effective-range";
import { apiFetchRaw, apiGet } from "@/lib/api/api-fetch";
+import { SettingsCard } from "@/components/settings/settings-card";
interface ThresholdsApiResponse {
effective: Record;
@@ -115,9 +116,9 @@ export function ThresholdsEditorSection({ id }: { id: string }) {
});
return (
-
{/* v1.4.19 A8 / F-07: page header `settings.sections.thresholds.*`
already provides the title + description for this route, so the
@@ -159,7 +160,7 @@ export function ThresholdsEditorSection({ id }: { id: string }) {
))}
)}
-
+
);
}
diff --git a/src/components/settings/web-push-card.tsx b/src/components/settings/web-push-card.tsx
index f3204b4dc..fc44fe612 100644
--- a/src/components/settings/web-push-card.tsx
+++ b/src/components/settings/web-push-card.tsx
@@ -5,6 +5,7 @@ import { BellRing, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { TestConnectionButton } from "@/components/settings/test-connection-button";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { apiFetchRaw } from "@/lib/api/api-fetch";
@@ -204,7 +205,7 @@ export function WebPushCard() {
);
return (
-
+
) : null}
-
+
);
}
diff --git a/src/components/settings/webhook-card.tsx b/src/components/settings/webhook-card.tsx
index bf2a17b69..e43e0e40d 100644
--- a/src/components/settings/webhook-card.tsx
+++ b/src/components/settings/webhook-card.tsx
@@ -9,6 +9,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { PasswordInput } from "@/components/ui/password-input";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { TestConnectionButton } from "@/components/settings/test-connection-button";
import { useTranslations } from "@/lib/i18n/context";
@@ -80,7 +81,7 @@ export function WebhookCard({ isAuthenticated }: { isAuthenticated: boolean }) {
});
return (
-
+
-
+
);
}
diff --git a/src/generated b/src/generated
new file mode 120000
index 000000000..a5d735124
--- /dev/null
+++ b/src/generated
@@ -0,0 +1 @@
+/Users/marc/Projects/HealthLog/src/generated
\ No newline at end of file
From 96f2b4754567c81066bf0e49a68d1a84a81f957a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Fri, 19 Jun 2026 21:16:10 +0200
Subject: [PATCH 3/7] refactor(settings): compose multi-card sections from
SettingsCard
Migrate the remaining hand-rolled card containers in the account,
about, medications, api, mood, notification-status, and advanced
sections to SettingsCard, preserving each card's heading, body, and
test hooks. Leaves the passkey list-row (rounded-lg) for the list-row
token track.
---
src/components/settings/about-section.tsx | 9 +++++----
src/components/settings/account-section.tsx | 17 +++++++++--------
src/components/settings/advanced-section.tsx | 16 +++++++---------
src/components/settings/api-section.tsx | 9 +++++----
.../settings/medications-section.tsx | 13 +++++++------
src/components/settings/mood-section.tsx | 19 ++++++++++---------
.../settings/notification-status-card.tsx | 14 +++++++-------
7 files changed, 50 insertions(+), 47 deletions(-)
diff --git a/src/components/settings/about-section.tsx b/src/components/settings/about-section.tsx
index 787c5b46a..4f746ea83 100644
--- a/src/components/settings/about-section.tsx
+++ b/src/components/settings/about-section.tsx
@@ -13,6 +13,7 @@ import {
} from "lucide-react";
import { Button } from "@/components/ui/button";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useFormatters, useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -191,7 +192,7 @@ export function AboutSection() {
stacked the license under the version inside its own boxed Badge,
which read as a separate field even though the two values belong
on the same row. */}
-
+
{/* Sources & docs — every link card was untitled in the v1.4.2
About surface, so the buttons floated alone with no heading
telling the user what the section was for. */}
{version && (
-
+
-
+
)}
{/* v1.18.6 — the tour-replay card consolidated onto a single home in
diff --git a/src/components/settings/account-section.tsx b/src/components/settings/account-section.tsx
index bddb12312..393370666 100644
--- a/src/components/settings/account-section.tsx
+++ b/src/components/settings/account-section.tsx
@@ -48,6 +48,7 @@ import { useTranslations } from "@/lib/i18n/context";
import { describePasskeyError } from "@/lib/passkey-errors";
import { queryKeys } from "@/lib/query-keys";
import { AboutMeSection } from "@/components/settings/about-me-section";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { TimezonePicker } from "@/components/settings/timezone-picker";
import { TimeFormatSelect } from "@/components/settings/time-format-select";
@@ -375,7 +376,7 @@ export function AccountSection() {
{/* Profile photo card */}
-
+
-
+
{/* v1.18.0 (S5) — injection-site exclusions moved to the dedicated
Medikamente settings section, where every medication-specific
@@ -599,7 +600,7 @@ export function AccountSection() {
{/* Passkeys card */}
-
+
)}
-
+
{/* Password card. v1.4.19 A6: action-button placement contract —
on mobile (`
+
@@ -665,7 +666,7 @@ export function AccountSection() {
{t("settings.changePassword")}
-
+
{/* v1.18.1 (D1) — the "Tour neu starten" card moved to Settings →
Erweitert. It is a maintenance / reset action, not a profile or
@@ -1096,7 +1097,7 @@ function AvatarSection() {
const busy = upload.isPending || remove.isPending;
return (
-
+
)}
-
+
);
}
diff --git a/src/components/settings/advanced-section.tsx b/src/components/settings/advanced-section.tsx
index 7650cefee..52cb08886 100644
--- a/src/components/settings/advanced-section.tsx
+++ b/src/components/settings/advanced-section.tsx
@@ -19,6 +19,7 @@ import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { ResearchModeAcknowledgmentDialog } from "@/components/medications/ResearchModeAcknowledgmentDialog";
import { formatDateTime } from "@/lib/format";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -139,8 +140,7 @@ function ResearchModeCard() {
}
return (
-
+
);
}
diff --git a/src/components/settings/medications-section.tsx b/src/components/settings/medications-section.tsx
index a1421aba9..9d488a10d 100644
--- a/src/components/settings/medications-section.tsx
+++ b/src/components/settings/medications-section.tsx
@@ -8,6 +8,7 @@ import {
type ReorderMedication,
} from "@/components/medications/medication-order-editor";
import { MedicationViewToggle } from "@/components/medications/medication-view-toggle";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { InjectionSitesCard } from "@/components/settings/injection-sites-card";
import { useAuth } from "@/hooks/use-auth";
@@ -79,9 +80,9 @@ export function MedicationsSection() {
{/* View preference — cards vs table. The shared header toggle
writes optimistically, so there is no Save button here. */}
-
)}
-
+
{/* Manual order — applies to both list views. */}
-
)}
-
+
{/* v1.18.0 (S5) — injection-site exclusions are a medication setting;
they moved here from the account profile so all medication-specific
diff --git a/src/components/settings/mood-section.tsx b/src/components/settings/mood-section.tsx
index dff7c7019..f05539029 100644
--- a/src/components/settings/mood-section.tsx
+++ b/src/components/settings/mood-section.tsx
@@ -2,6 +2,7 @@
import { Archive, Folders, Loader2, Tags } from "lucide-react";
+import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { ArchivedTagsCard } from "@/components/mood/manage/archived-tags-card";
import { TagGroupsCard } from "@/components/mood/manage/tag-groups-card";
@@ -45,30 +46,30 @@ export function MoodSection() {
return (
{/* Groups — picker group order + own groups. */}
-