diff --git a/src/components/settings/about-me-section.tsx b/src/components/settings/about-me-section.tsx index aeb554416..6683d45a4 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 ( -
@@ -290,6 +293,7 @@ export function AboutMeSection({
+ ); } diff --git a/src/components/settings/about-section.tsx b/src/components/settings/about-section.tsx index 787c5b46a..de0bae81d 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. */} -
+ {isLoading || !version ? ( @@ -255,20 +256,25 @@ export function AboutSection() { )} )} -
+ {/* 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..4329df081 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 +604,7 @@ export function AccountSection() { {/* Passkeys card */} -
+
+ {/* Password card. v1.4.19 A6: action-button placement contract — on mobile (` +
@@ -660,12 +666,12 @@ export function AccountSection() { type="button" variant="outline" onClick={() => setPasswordDialogOpen(true)} - className="w-full shrink-0 sm:w-auto" + className="min-h-11 w-full shrink-0 sm:min-h-9 sm:w-auto" > {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 @@ -693,7 +699,7 @@ export function AccountSection() {
-
+
+ ); } diff --git a/src/components/settings/advanced-section.tsx b/src/components/settings/advanced-section.tsx index 7650cefee..9005f5ef3 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 ( -
setDialogOpen(true)} data-slot="settings-research-mode-reprompt-cta" > @@ -211,7 +211,7 @@ function ResearchModeCard() { onOpenChange={setDialogOpen} currentDisclaimerVersion={status?.currentDisclaimerVersion ?? null} /> -
+ ); } @@ -254,8 +254,7 @@ function DataResetCard() { } return ( -
{/* v1.4.43 QoL (L5) — dropped the `AlertTriangle` icon and @@ -287,7 +286,7 @@ function DataResetCard() { variant="destructive" size="sm" disabled={deleting} - className="w-full shrink-0 sm:w-auto" + className="min-h-11 w-full shrink-0 sm:min-h-9 sm:w-auto" > {deleting ? ( @@ -338,7 +337,7 @@ function DataResetCard() { {msg}

)} -
+ ); } @@ -402,8 +401,7 @@ function AccountDeleteCard() { } return ( -
@@ -430,7 +428,7 @@ function AccountDeleteCard() { variant="destructive" size="sm" disabled={deleting} - className="w-full shrink-0 sm:w-auto" + className="min-h-11 w-full shrink-0 sm:min-h-9 sm:w-auto" data-slot="settings-account-delete-trigger" > {deleting ? ( @@ -482,6 +480,6 @@ function AccountDeleteCard() { {msg}

)} -
+ ); } 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/anthropic-provider-form.tsx b/src/components/settings/ai/anthropic-provider-form.tsx index c7e8b810f..6f9c395f3 100644 --- a/src/components/settings/ai/anthropic-provider-form.tsx +++ b/src/components/settings/ai/anthropic-provider-form.tsx @@ -149,6 +149,7 @@ export function AnthropicProviderForm({
@@ -206,6 +207,7 @@ export function FallbackChainCard({ aria-label={t("settings.ai.providerChain.moveDown")} disabled={idx === entries.length - 1} onClick={() => move(idx, 1)} + className="min-h-11 min-w-11 sm:min-h-9 sm:min-w-9" > @@ -214,7 +216,7 @@ export function FallbackChainCard({ size="sm" variant="outline" aria-label={t("settings.ai.providerChain.removeFromChain")} - className="text-destructive" + className="text-destructive min-h-11 min-w-11 sm:min-h-9 sm:min-w-9" onClick={() => remove(idx)} disabled={entries.length <= 1} > @@ -234,6 +236,7 @@ export function FallbackChainCard({ )} - @@ -308,6 +315,7 @@ function AddProviderControl({ type="button" size="sm" variant="outline" + className="min-h-11 sm:min-h-9" onClick={() => { if (isProviderType(picked)) onAdd(picked); }} diff --git a/src/components/settings/ai/local-provider-form.tsx b/src/components/settings/ai/local-provider-form.tsx index 7c18a92f7..a482df7e4 100644 --- a/src/components/settings/ai/local-provider-form.tsx +++ b/src/components/settings/ai/local-provider-form.tsx @@ -150,6 +150,7 @@ export function LocalProviderForm({
+ ); } diff --git a/src/components/settings/coach-memory-section.tsx b/src/components/settings/coach-memory-section.tsx index d687c8901..80f079d01 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 ( -
@@ -243,6 +246,7 @@ export function CoachMemorySection({ type="button" variant="ghost" size="sm" + className="min-h-11 sm:min-h-9" data-testid="settings-coach-memory-forget" aria-label={t("settings.ai.coachMemory.forgetAria")} // v1.11.2 — per-id pending: only the row being @@ -278,6 +282,6 @@ export function CoachMemorySection({

{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..a502d519f 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 ( -
save.mutate(draft)} disabled={save.isPending || isLoading} data-slot="coach-prefs-save" @@ -351,6 +352,6 @@ export function CoachPrefsSection({ isAuthenticated }: CoachPrefsSectionProps) { {t("insights.coach.settingsSave")}
-
+ ); } diff --git a/src/components/settings/cycle-tracking-card.tsx b/src/components/settings/cycle-tracking-card.tsx index 2a7cbf6af..c4469b068 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 ( -
+
{enabled ? ( - ) : null} @@ -89,6 +95,6 @@ export function CycleTrackingCard({

) : null}
-
+ ); } diff --git a/src/components/settings/dashboard-layout-section.tsx b/src/components/settings/dashboard-layout-section.tsx index 581050345..028e35e8b 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 + @@ -336,7 +337,7 @@ export function DashboardLayoutSection({ id }: { id: string }) { size="sm" onClick={() => resetMutation.mutate()} disabled={resetMutation.isPending} - className="self-end sm:self-auto" + className="min-h-11 self-end sm:min-h-9 sm:self-auto" > {t("dashboard.layoutReset")} @@ -550,6 +551,7 @@ export function DashboardLayoutSection({ id }: { id: string }) {
+ ); } 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/export-section.tsx b/src/components/settings/export-section.tsx index 4116dd584..8b84ce5f6 100644 --- a/src/components/settings/export-section.tsx +++ b/src/components/settings/export-section.tsx @@ -39,7 +39,9 @@ import { Button } from "@/components/ui/button"; import { DateInput } from "@/components/ui/date-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 { cn } from "@/lib/utils"; import { ImportPanel } from "@/components/settings/import-panel"; import { queryKeys } from "@/lib/query-keys"; import { useTranslations } from "@/lib/i18n/context"; @@ -135,9 +137,9 @@ function ExportCardShell({ outerClassName, }: ExportCardShellProps) { return ( -
{children &&
{children}
}
{footer}
-
+ ); } @@ -265,6 +267,7 @@ function CsvCard({ data-testid={actionTestId} variant="outline" size="sm" + className="min-h-11 sm:min-h-9" onClick={handleDownload} disabled={busy} > @@ -352,6 +355,7 @@ function MedicationsCsvCard() { data-testid="export-action-medications-csv" variant="outline" size="sm" + className="min-h-11 sm:min-h-9" onClick={handleDownload} disabled={busy} > @@ -441,6 +445,7 @@ function FullBackupCard() { data-testid="export-action-full-backup" variant="outline" size="sm" + className="min-h-11 sm:min-h-9" onClick={handleDownload} disabled={busy} > @@ -550,6 +555,7 @@ function CycleExportCard() { data-testid="export-action-cycle" variant="outline" size="sm" + className="min-h-11 sm:min-h-9" onClick={handleGenerate} disabled={busy} > 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/import-panel.tsx b/src/components/settings/import-panel.tsx index 879237ceb..039d4e59a 100644 --- a/src/components/settings/import-panel.tsx +++ b/src/components/settings/import-panel.tsx @@ -41,6 +41,7 @@ import { import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Progress } from "@/components/ui/progress"; +import { SettingsCard } from "@/components/settings/settings-card"; import { Textarea } from "@/components/ui/textarea"; import { queryKeys } from "@/lib/query-keys"; import { useTranslations } from "@/lib/i18n/context"; @@ -102,9 +103,9 @@ function ImportCardShell({ children, }: ImportCardShellProps) { return ( -

{description}

{children}
-
+ ); } @@ -365,6 +366,7 @@ function AppleHealthImportCard() { type="button" variant="outline" size="sm" + className="min-h-11 sm:min-h-9" disabled={busy} onClick={() => fileInputRef.current?.click()} data-testid="import-action-apple-health" @@ -614,6 +616,7 @@ function JsonImportCard() { type="button" variant="outline" size="sm" + className="min-h-11 sm:min-h-9" onClick={() => fileInputRef.current?.click()} data-testid="import-json-choose-file" > @@ -624,6 +627,7 @@ function JsonImportCard() { type="button" variant="ghost" size="sm" + className="min-h-11 sm:min-h-9" onClick={downloadExample} data-testid="import-json-download-example" > @@ -633,6 +637,7 @@ function JsonImportCard() {
+ ); } 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/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/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/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. */} -
{isLoading || !catalog ? loading : } -
+ {/* Tags — visibility, order, edit, move, create. */} -
{isLoading || !catalog ? loading : } -
+ {/* Archived custom tags — restore / hard delete. */} -
)} -
+
); } diff --git a/src/components/settings/notification-status-card.tsx b/src/components/settings/notification-status-card.tsx index eea9c5763..f13ac3c96 100644 --- a/src/components/settings/notification-status-card.tsx +++ b/src/components/settings/notification-status-card.tsx @@ -32,6 +32,7 @@ import { import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { useAuth } from "@/hooks/use-auth"; +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"; @@ -114,9 +115,9 @@ export function NotificationStatusCard() { if (isLoading) { return ( -
+ -
+ ); } @@ -128,8 +129,7 @@ export function NotificationStatusCard() { const channels = Array.isArray(data) ? data : []; if (channels.length === 0) { return ( -
-
+ ); } 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 new file mode 100644 index 000000000..9537ca7a1 --- /dev/null +++ b/src/components/settings/settings-card.tsx @@ -0,0 +1,63 @@ +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. + * + * 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 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. + */ +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 +}: 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..c4035b2e2 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 }) { ))}
)} - + ); } @@ -323,6 +324,7 @@ function MetricRow({ onClick={() => valid && onSave({ min: minNum, max: maxNum })} disabled={busy || !valid} size="sm" + className="min-h-11 sm:min-h-9" > {t("common.save")} @@ -330,6 +332,7 @@ function MetricRow({