mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
Merge wave-mobile-settings: SettingsCard primitive + phone tap targets
Replace the hand-rolled Settings card divs across every section with a shared SettingsCard wrapping the Card primitive, eliminating the sm-vs-md padding split and the primitive bypass, and raise the Settings touch controls to a 44px floor that condenses at sm+.
This commit is contained in:
commit
4daa5a428f
43 files changed
+299
-144
No files matched your search
@@ -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 (
|
||||
<section
|
||||
<SettingsCard
|
||||
as="section"
|
||||
aria-labelledby="settings-ai-about-me-title"
|
||||
data-testid="settings-about-me-card"
|
||||
className="bg-card border-border space-y-5 rounded-xl border p-4 sm:p-6"
|
||||
className="space-y-5"
|
||||
>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<SettingsCardHeader
|
||||
@@ -272,6 +274,7 @@ export function AboutMeSection({
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
data-testid="settings-about-me-clear"
|
||||
disabled={!isAuthenticated || save.isPending}
|
||||
onClick={() =>
|
||||
@@ -290,6 +293,7 @@ export function AboutMeSection({
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
data-testid="settings-about-me-save"
|
||||
disabled={!isAuthenticated || save.isPending || !dirty}
|
||||
onClick={() => save.mutate(value)}
|
||||
@@ -346,6 +350,6 @@ export function AboutMeSection({
|
||||
<p className="text-muted-foreground border-border border-t pt-3 pl-7 text-xs">
|
||||
{t("settings.ai.aboutMe.hint")} {t("settings.ai.aboutMe.profileHint")}
|
||||
</p>
|
||||
</section>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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. */}
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader icon={Info} title="HealthLog" className="mb-4" />
|
||||
|
||||
{isLoading || !version ? (
|
||||
@@ -255,20 +256,25 @@ export function AboutSection() {
|
||||
)}
|
||||
</dl>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* 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 && (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={BookOpen}
|
||||
title={t("settings.about.linksHeading")}
|
||||
className="mb-4"
|
||||
/>
|
||||
<div className="flex flex-col gap-2 pl-7 sm:flex-row sm:flex-wrap">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
>
|
||||
<a
|
||||
href={version.repository}
|
||||
target="_blank"
|
||||
@@ -279,7 +285,12 @@ export function AboutSection() {
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
>
|
||||
<a
|
||||
href={version.changelog}
|
||||
target="_blank"
|
||||
@@ -290,7 +301,12 @@ export function AboutSection() {
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
>
|
||||
<a href={version.docs} target="_blank" rel="noopener noreferrer">
|
||||
<BookOpen className="h-4 w-4" />
|
||||
{t("settings.about.docs")}
|
||||
@@ -298,7 +314,7 @@ export function AboutSection() {
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
)}
|
||||
|
||||
{/* v1.18.6 — the tour-replay card consolidated onto a single home in
|
||||
|
||||
@@ -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 */}
|
||||
<AvatarSection />
|
||||
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={User}
|
||||
title={t("settings.profile")}
|
||||
@@ -570,7 +571,11 @@ export function AccountSection() {
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={saving}>
|
||||
<Button
|
||||
type="submit"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
@@ -580,7 +585,7 @@ export function AccountSection() {
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* 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() {
|
||||
<CycleTrackingCard isAuthenticated={isAuthenticated} />
|
||||
|
||||
{/* Passkeys card */}
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={Shield}
|
||||
title={t("settings.passkeys")}
|
||||
@@ -611,6 +616,7 @@ export function AccountSection() {
|
||||
<div className="mt-4 flex justify-end pl-7">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
onClick={handleAddPasskey}
|
||||
disabled={passkeyLoading}
|
||||
>
|
||||
@@ -632,7 +638,7 @@ export function AccountSection() {
|
||||
{statusText(passkeyMsg, t)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Password card. v1.4.19 A6: action-button placement contract —
|
||||
on mobile (`<sm`) the action button stacks below the title +
|
||||
@@ -643,7 +649,7 @@ export function AccountSection() {
|
||||
(the German "Passwort ändern" already pushes it; the tour
|
||||
card's "Restart onboarding tour" actually broke through the
|
||||
right border by ~48 px). */}
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<Shield className="text-muted-foreground mt-0.5 h-5 w-5 shrink-0" />
|
||||
@@ -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")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* 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() {
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleChangePassword} className="space-y-4">
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<div className="grid gap-3 lg:grid-cols-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="current-password">
|
||||
{t("settings.currentPassword")}
|
||||
@@ -739,7 +745,12 @@ export function AccountSection() {
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Button type="submit" variant="outline" disabled={passwordSaving}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
disabled={passwordSaving}
|
||||
>
|
||||
{passwordSaving ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
@@ -1096,7 +1107,7 @@ function AvatarSection() {
|
||||
const busy = upload.isPending || remove.isPending;
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={ImageUp}
|
||||
title={t("settings.avatar.title")}
|
||||
@@ -1122,6 +1133,7 @@ function AvatarSection() {
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
disabled={busy}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
@@ -1137,7 +1149,7 @@ function AvatarSection() {
|
||||
{avatarUrl && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-destructive"
|
||||
className="text-destructive min-h-11 sm:min-h-9"
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
setMsg(null);
|
||||
@@ -1168,6 +1180,6 @@ function AvatarSection() {
|
||||
{statusText(msg, t)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div
|
||||
className="bg-card border-border rounded-xl border p-4 sm:p-6"
|
||||
<SettingsCard
|
||||
data-slot="settings-research-mode-card"
|
||||
>
|
||||
<SettingsCardHeader
|
||||
@@ -164,7 +164,7 @@ function ResearchModeCard() {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="mt-2"
|
||||
className="mt-2 min-h-11 sm:min-h-9"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
data-slot="settings-research-mode-reprompt-cta"
|
||||
>
|
||||
@@ -211,7 +211,7 @@ function ResearchModeCard() {
|
||||
onOpenChange={setDialogOpen}
|
||||
currentDisclaimerVersion={status?.currentDisclaimerVersion ?? null}
|
||||
/>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -254,8 +254,7 @@ function DataResetCard() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-card border-border rounded-xl border p-4 sm:p-6"
|
||||
<SettingsCard
|
||||
data-slot="settings-data-reset-card"
|
||||
>
|
||||
{/* 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 ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
@@ -338,7 +337,7 @@ function DataResetCard() {
|
||||
{msg}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -402,8 +401,7 @@ function AccountDeleteCard() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-card border-border rounded-xl border p-4 sm:p-6"
|
||||
<SettingsCard
|
||||
data-slot="settings-account-delete-card"
|
||||
>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-2">
|
||||
@@ -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}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="bg-card border-border space-y-4 rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard className="space-y-4">
|
||||
{/* 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({
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -149,6 +149,7 @@ export function AnthropicProviderForm({
|
||||
<div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
onClick={() => saveMutation.mutate()}
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
|
||||
@@ -208,7 +208,7 @@ export function CodexProviderForm({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-destructive shrink-0"
|
||||
className="text-destructive min-h-11 shrink-0 sm:min-h-9"
|
||||
onClick={handleDisconnect}
|
||||
disabled={disconnecting}
|
||||
>
|
||||
@@ -274,7 +274,7 @@ export function CodexProviderForm({
|
||||
variant="outline"
|
||||
onClick={handleConnect}
|
||||
disabled={devicePolling}
|
||||
className="w-full sm:w-auto"
|
||||
className="min-h-11 w-full sm:min-h-9 sm:w-auto"
|
||||
>
|
||||
{devicePolling ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
|
||||
@@ -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 (
|
||||
<section
|
||||
<SettingsCard
|
||||
as="section"
|
||||
aria-labelledby="settings-coach-activate-title"
|
||||
data-testid="settings-disable-coach-card"
|
||||
className="bg-card border-border rounded-xl border p-4 sm:p-6"
|
||||
>
|
||||
<SettingsCardHeader
|
||||
icon={MessageCircleHeart}
|
||||
@@ -173,6 +174,6 @@ export function DisableCoachCard({
|
||||
{msg}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -196,6 +196,7 @@ export function FallbackChainCard({
|
||||
aria-label={t("settings.ai.providerChain.moveUp")}
|
||||
disabled={idx === 0}
|
||||
onClick={() => move(idx, -1)}
|
||||
className="min-h-11 min-w-11 sm:min-h-9 sm:min-w-9"
|
||||
>
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -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"
|
||||
>
|
||||
<ArrowDown className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -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({
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
onClick={() => saveMutation.mutate(entries)}
|
||||
disabled={saveMutation.isPending || entries.length === 0}
|
||||
data-testid="ai-fallback-chain-save"
|
||||
@@ -247,7 +250,11 @@ export function FallbackChainCard({
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button size="sm" variant="outline">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
{t("settings.ai.providerChain.resetDefaults")}
|
||||
</Button>
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -150,6 +150,7 @@ export function LocalProviderForm({
|
||||
<div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
onClick={() => saveMutation.mutate()}
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
|
||||
@@ -186,6 +186,7 @@ export function OpenAIProviderForm({
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
onClick={() => saveMutation.mutate()}
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
|
||||
@@ -198,6 +198,7 @@ export function RuntimeActionsRow({
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
onClick={runTest}
|
||||
disabled={testing}
|
||||
data-testid="ai-test-active-provider"
|
||||
@@ -213,6 +214,7 @@ export function RuntimeActionsRow({
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
onClick={regenerate}
|
||||
disabled={regen}
|
||||
>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { formatDate, formatDateTime } from "@/lib/format";
|
||||
@@ -66,7 +67,7 @@ function ApiEndpointsCard() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={Key}
|
||||
title={t("settings.apiEndpointsTitle")}
|
||||
@@ -139,7 +140,7 @@ function ApiEndpointsCard() {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -229,7 +230,7 @@ function ApiTokensCard() {
|
||||
}, null);
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={Key}
|
||||
title={t("settings.apiTokens")}
|
||||
@@ -626,6 +627,6 @@ function ApiTokensCard() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<section
|
||||
<SettingsCard
|
||||
as="section"
|
||||
aria-labelledby="settings-ai-coach-memory-title"
|
||||
data-testid="settings-coach-memory-card"
|
||||
className="bg-card border-border space-y-4 rounded-xl border p-4 sm:p-6"
|
||||
className="space-y-4"
|
||||
>
|
||||
<SettingsCardHeader
|
||||
icon={Brain}
|
||||
@@ -152,6 +154,7 @@ export function CoachMemorySection({
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
data-testid="settings-coach-memory-forget-all"
|
||||
disabled={!isAuthenticated || forgetAll.isPending}
|
||||
>
|
||||
@@ -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({
|
||||
<p className="text-muted-foreground border-border border-t pt-3 text-xs">
|
||||
{t("settings.ai.coachMemory.summaryNote")}
|
||||
</p>
|
||||
</section>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<section
|
||||
<SettingsCard
|
||||
as="section"
|
||||
aria-labelledby="settings-coach-nudge-title"
|
||||
className="bg-card border-border rounded-xl border p-4 sm:p-6"
|
||||
data-testid="settings-coach-nudge-card"
|
||||
>
|
||||
<SettingsCardHeader
|
||||
@@ -266,6 +267,6 @@ export function CoachNudgeCard({
|
||||
{msg}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div
|
||||
className="bg-card border-border rounded-xl border p-4 sm:p-6"
|
||||
<SettingsCard
|
||||
data-slot="coach-prefs-section"
|
||||
>
|
||||
<SettingsCardHeader
|
||||
@@ -338,6 +338,7 @@ export function CoachPrefsSection({ isAuthenticated }: CoachPrefsSectionProps) {
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
onClick={() => 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")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={CalendarHeart}
|
||||
title={t("settings.cycleTracking.title")}
|
||||
@@ -79,7 +80,12 @@ export function CycleTrackingCard({
|
||||
/>
|
||||
</div>
|
||||
{enabled ? (
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
asChild
|
||||
>
|
||||
<Link href="/cycle">{t("settings.cycleTracking.manage")}</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
@@ -89,6 +95,6 @@ export function CycleTrackingCard({
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div
|
||||
<SettingsCard
|
||||
id={id}
|
||||
className="bg-card border-border scroll-mt-28 space-y-4 rounded-xl border p-4 sm:p-6"
|
||||
className="scroll-mt-28 space-y-4"
|
||||
>
|
||||
{/* 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"
|
||||
>
|
||||
<RotateCcw className="h-3.5 w-3.5" />
|
||||
{t("dashboard.layoutReset")}
|
||||
@@ -550,6 +551,7 @@ export function DashboardLayoutSection({ id }: { id: string }) {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
onClick={() => setDraft(null)}
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
@@ -557,6 +559,7 @@ export function DashboardLayoutSection({ id }: { id: string }) {
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
onClick={() => layout && saveMutation.mutate(layout)}
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
@@ -577,7 +580,7 @@ export function DashboardLayoutSection({ id }: { id: string }) {
|
||||
: t("dashboard.layoutCustomized")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={Mail}
|
||||
title={t("settings.email")}
|
||||
@@ -137,6 +138,6 @@ export function EmailCard({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div
|
||||
<SettingsCard
|
||||
data-testid={testId}
|
||||
className={`bg-card border-border flex h-full flex-col rounded-xl border p-4 sm:p-6${outerClassName ? ` ${outerClassName}` : ""}`}
|
||||
className={cn("flex h-full flex-col", outerClassName)}
|
||||
>
|
||||
<SettingsCardHeader
|
||||
icon={Icon}
|
||||
@@ -151,7 +153,7 @@ function ExportCardShell({
|
||||
/>
|
||||
{children && <div className="mt-3 space-y-3">{children}</div>}
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3">{footer}</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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 (
|
||||
<section
|
||||
<SettingsCard
|
||||
as="section"
|
||||
aria-labelledby="health-record-export-title"
|
||||
data-testid="health-record-export-panel"
|
||||
className={cn(
|
||||
// v1.18.1 (D9) — the gradient/glow hero treatment dropped in favour
|
||||
// of a normal card so the Gesundheitsakte reads like every other
|
||||
// settings surface rather than shouting.
|
||||
"bg-card border-border rounded-xl border p-4 sm:p-6",
|
||||
)}
|
||||
>
|
||||
<SettingsCardHeader
|
||||
className="mb-4"
|
||||
@@ -462,7 +458,7 @@ export function HealthRecordExportPanel() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
<SettingsCard
|
||||
data-testid={testId}
|
||||
className="bg-card border-border flex h-full flex-col rounded-xl border p-4 sm:p-6"
|
||||
className="flex h-full flex-col"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon
|
||||
@@ -115,7 +116,7 @@ function ImportCardShell({
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 text-xs">{description}</p>
|
||||
<div className="mt-3 flex flex-1 flex-col gap-3">{children}</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
disabled={busy || text.trim().length === 0}
|
||||
onClick={handleImport}
|
||||
data-testid="import-action-json"
|
||||
@@ -862,6 +867,7 @@ function CsvImportCard() {
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
data-testid="import-csv-choose-file"
|
||||
>
|
||||
@@ -872,6 +878,7 @@ function CsvImportCard() {
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
onClick={downloadExample}
|
||||
data-testid="import-csv-download-example"
|
||||
>
|
||||
@@ -882,6 +889,7 @@ function CsvImportCard() {
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
disabled={busy || text.trim().length === 0}
|
||||
onClick={() => void send(true)}
|
||||
data-testid="import-csv-preview"
|
||||
@@ -894,6 +902,7 @@ function CsvImportCard() {
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
disabled={busy || text.trim().length === 0}
|
||||
onClick={() => void send(false)}
|
||||
data-testid="import-action-csv"
|
||||
|
||||
@@ -72,7 +72,7 @@ export function InjectionSitesCard({
|
||||
title={t("settings.globalExcludedInjectionSitesLabel")}
|
||||
description={t("settings.globalExcludedInjectionSitesHint")}
|
||||
/>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2 pl-7">
|
||||
<div className="mt-3 grid grid-cols-1 gap-2 pl-7 sm:grid-cols-2">
|
||||
{INJECTION_SITE_KEYS.map((site) => (
|
||||
<label key={site} className="flex items-center gap-2 text-sm">
|
||||
<Checkbox
|
||||
|
||||
@@ -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 ? (
|
||||
<div
|
||||
className="bg-card border-border text-muted-foreground flex items-center gap-2 rounded-xl border p-4 text-sm sm:p-6"
|
||||
<SettingsCard
|
||||
className="text-muted-foreground flex items-center gap-2 text-sm"
|
||||
data-slot="insights-overview-arrange-loading"
|
||||
>
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
</SettingsCard>
|
||||
) : (
|
||||
<InsightsEditMode
|
||||
layout={layout}
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
} from "@/lib/insights/sub-page-metric";
|
||||
import { SUB_PAGE_TABS } from "@/components/insights/insights-tab-strip";
|
||||
import { apiPut } from "@/lib/api/api-fetch";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
|
||||
/**
|
||||
* v1.15.18 — dedicated pill-sort control for the Insights settings section.
|
||||
@@ -202,11 +203,12 @@ export function InsightsPillOrderSection({ id }: { id?: string }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
<SettingsCard
|
||||
as="section"
|
||||
id={id}
|
||||
data-slot="insights-pill-order-section"
|
||||
aria-labelledby="insights-pill-order-title"
|
||||
className="bg-card border-border space-y-4 rounded-xl border p-4 sm:p-6"
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-3">
|
||||
<div className="space-y-1">
|
||||
@@ -222,7 +224,7 @@ export function InsightsPillOrderSection({ id }: { id?: string }) {
|
||||
onClick={() => saveMutation.mutate(draftTiles)}
|
||||
disabled={busy || !dirty}
|
||||
data-slot="insights-pill-order-save"
|
||||
className="self-end sm:self-auto"
|
||||
className="min-h-11 self-end sm:min-h-9 sm:self-auto"
|
||||
>
|
||||
{saveMutation.isPending && (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
@@ -286,7 +288,7 @@ export function InsightsPillOrderSection({ id }: { id?: string }) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
<SettingsCard
|
||||
data-testid="fitbit-card"
|
||||
className="bg-card border-border rounded-xl border p-4 sm:p-6"
|
||||
>
|
||||
<SettingsCardHeader
|
||||
icon={HeartPulse}
|
||||
@@ -478,6 +478,6 @@ export function FitbitCard({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div
|
||||
<SettingsCard
|
||||
data-testid="nightscout-card"
|
||||
className="bg-card border-border rounded-xl border p-4 sm:p-6"
|
||||
>
|
||||
<SettingsCardHeader
|
||||
icon={Droplet}
|
||||
@@ -369,6 +369,6 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div
|
||||
<SettingsCard
|
||||
data-testid={`${provider}-card`}
|
||||
className="bg-card border-border rounded-xl border p-4 sm:p-6"
|
||||
>
|
||||
<SettingsCardHeader
|
||||
icon={icon}
|
||||
@@ -429,6 +429,6 @@ export function OAuthProviderCard({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={Activity}
|
||||
title={t("settings.whoop")}
|
||||
@@ -456,6 +457,6 @@ export function WhoopCard({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={Link2}
|
||||
title={t("settings.withings")}
|
||||
@@ -501,6 +502,6 @@ export function WithingsCard({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<section
|
||||
<SettingsCard
|
||||
as="section"
|
||||
// `id` is the anchor target of the supply tab's cross-link
|
||||
// (`/settings/notifications#low-stock`) — keep it stable.
|
||||
id="low-stock"
|
||||
aria-labelledby="settings-low-stock-title"
|
||||
className="bg-card border-border scroll-mt-20 rounded-xl border p-4 sm:p-6"
|
||||
className="scroll-mt-20"
|
||||
>
|
||||
<SettingsCardHeader
|
||||
icon={PackageOpen}
|
||||
@@ -303,6 +305,6 @@ export function LowStockCard({
|
||||
{t("notifications.lowStock.medicationsLink")}
|
||||
</Link>
|
||||
</p>
|
||||
</section>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
<div className="space-y-6">
|
||||
{/* View preference — cards vs table. The shared header toggle
|
||||
writes optimistically, so there is no Save button here. */}
|
||||
<div
|
||||
<SettingsCard
|
||||
id="medications-view"
|
||||
className="bg-card border-border scroll-mt-28 space-y-4 rounded-xl border p-4 sm:p-6"
|
||||
className="scroll-mt-28 space-y-4"
|
||||
>
|
||||
<SettingsCardHeader
|
||||
icon={LayoutGrid}
|
||||
@@ -97,12 +98,12 @@ export function MedicationsSection() {
|
||||
<MedicationViewToggle view={layout.view} onChange={setView} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Manual order — applies to both list views. */}
|
||||
<div
|
||||
<SettingsCard
|
||||
id="medications-order"
|
||||
className="bg-card border-border scroll-mt-28 space-y-4 rounded-xl border p-4 sm:p-6"
|
||||
className="scroll-mt-28 space-y-4"
|
||||
>
|
||||
<SettingsCardHeader
|
||||
icon={Pill}
|
||||
@@ -117,7 +118,7 @@ export function MedicationsSection() {
|
||||
) : (
|
||||
<MedicationOrderEditor medications={ordered} />
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* v1.18.0 (S5) — injection-site exclusions are a medication setting;
|
||||
they moved here from the account profile so all medication-specific
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={Blocks}
|
||||
title={t("settings.sections.modules.toggleable.title")}
|
||||
@@ -160,6 +161,6 @@ export function ModulesSection() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<section
|
||||
<SettingsCard
|
||||
as="section"
|
||||
aria-labelledby="settings-mood-reminder-title"
|
||||
className="bg-card border-border rounded-xl border p-4 sm:p-6"
|
||||
>
|
||||
<SettingsCardHeader
|
||||
icon={SmilePlus}
|
||||
@@ -214,6 +215,6 @@ export function MoodReminderCard({
|
||||
{msg}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="space-y-6">
|
||||
{/* Groups — picker group order + own groups. */}
|
||||
<div
|
||||
<SettingsCard
|
||||
id="mood-groups"
|
||||
className="bg-card border-border scroll-mt-28 space-y-4 rounded-xl border p-4 sm:p-6"
|
||||
className="scroll-mt-28 space-y-4"
|
||||
>
|
||||
<SettingsCardHeader
|
||||
icon={Folders}
|
||||
title={t("mood.manage.groupsTitle")}
|
||||
/>
|
||||
{isLoading || !catalog ? loading : <TagGroupsCard catalog={catalog} />}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Tags — visibility, order, edit, move, create. */}
|
||||
<div
|
||||
<SettingsCard
|
||||
id="mood-tags"
|
||||
className="bg-card border-border scroll-mt-28 space-y-4 rounded-xl border p-4 sm:p-6"
|
||||
className="scroll-mt-28 space-y-4"
|
||||
>
|
||||
<SettingsCardHeader icon={Tags} title={t("mood.manage.tagsTitle")} />
|
||||
{isLoading || !catalog ? loading : <TagManagerCard catalog={catalog} />}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Archived custom tags — restore / hard delete. */}
|
||||
<div
|
||||
<SettingsCard
|
||||
id="mood-archived"
|
||||
className="bg-card border-border scroll-mt-28 space-y-4 rounded-xl border p-4 sm:p-6"
|
||||
className="scroll-mt-28 space-y-4"
|
||||
>
|
||||
<SettingsCardHeader
|
||||
icon={Archive}
|
||||
@@ -79,7 +80,7 @@ export function MoodSection() {
|
||||
) : (
|
||||
<ArchivedTagsCard catalog={catalog} />
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,8 +129,7 @@ export function NotificationStatusCard() {
|
||||
const channels = Array.isArray(data) ? data : [];
|
||||
if (channels.length === 0) {
|
||||
return (
|
||||
<div
|
||||
className="bg-card border-border rounded-xl border p-4 sm:p-6"
|
||||
<SettingsCard
|
||||
data-testid="notification-status-empty"
|
||||
>
|
||||
<SettingsCardHeader
|
||||
@@ -137,12 +137,12 @@ export function NotificationStatusCard() {
|
||||
title={t("settings.notificationStatus.title")}
|
||||
description={t("settings.notificationStatus.emptyDescription")}
|
||||
/>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={Bell}
|
||||
title={t("settings.notificationStatus.title")}
|
||||
@@ -166,7 +166,7 @@ export function NotificationStatusCard() {
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={Bell}
|
||||
title={t("settings.ntfy")}
|
||||
@@ -171,6 +172,6 @@ export function NtfyCard({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* `<SettingsCard>` — 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 `<Card>` 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 `<Card>` so the surface composes from one shape.
|
||||
* Unlike the bare `<Card>` 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 `<section>` 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<E extends React.ElementType> = SettingsCardOwnProps & {
|
||||
as?: E;
|
||||
} & Omit<React.ComponentPropsWithoutRef<E>, keyof SettingsCardOwnProps | "as">;
|
||||
|
||||
export function SettingsCard<E extends React.ElementType = typeof Card>({
|
||||
as,
|
||||
className,
|
||||
...props
|
||||
}: SettingsCardProps<E>) {
|
||||
if (as) {
|
||||
const Component = as as React.ElementType;
|
||||
return (
|
||||
<Component
|
||||
data-slot="card"
|
||||
// Mirror the ui Card shell so a semantic landmark card paints
|
||||
// identically to the default <Card>-backed one.
|
||||
className={cn(
|
||||
"bg-card text-card-foreground rounded-xl border p-4 shadow-sm md:p-6",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card
|
||||
// Reset the primitive's flex/gap layout and apply the shared
|
||||
// `p-4 md:p-6` padding; Settings bodies own their internal spacing.
|
||||
className={cn(SETTINGS_CARD_SHELL, className)}
|
||||
{...(props as React.ComponentProps<typeof Card>)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-card border-border space-y-4 rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard className="space-y-4">
|
||||
<SettingsCardHeader
|
||||
icon={Layers}
|
||||
title={t("settings.sections.sources.cardTitle")}
|
||||
@@ -574,7 +575,7 @@ export function SourcesSection() {
|
||||
{t("settings.sections.sources.integrationsHintLink")}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={MessageCircle}
|
||||
title={t("settings.telegram")}
|
||||
@@ -177,6 +178,6 @@ export function TelegramCard({
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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<ThresholdMetric, EffectiveRange>;
|
||||
@@ -115,9 +116,9 @@ export function ThresholdsEditorSection({ id }: { id: string }) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
<SettingsCard
|
||||
id={id}
|
||||
className="bg-card border-border scroll-mt-28 space-y-4 rounded-xl border p-4 sm:p-6"
|
||||
className="scroll-mt-28 space-y-4"
|
||||
>
|
||||
{/* 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 }) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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")}
|
||||
</Button>
|
||||
@@ -330,6 +332,7 @@ function MetricRow({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
onClick={onReset}
|
||||
disabled={busy}
|
||||
>
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={BellRing}
|
||||
title={t("settings.webPush")}
|
||||
@@ -234,6 +235,6 @@ export function WebPushCard() {
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={Webhook}
|
||||
title={t("settings.webhook")}
|
||||
@@ -169,6 +170,6 @@ export function WebhookCard({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user