diff --git a/src/components/settings/_card-header.tsx b/src/components/settings/_card-header.tsx new file mode 100644 index 000000000..9a5b7f2ca --- /dev/null +++ b/src/components/settings/_card-header.tsx @@ -0,0 +1,78 @@ +"use client"; + +/** + * `` — v1.4.33 IW4 polish primitive. + * + * Every Settings card paints the same shape: a primary icon, a title, + * an optional status surface (badge / pill / inline switch), and an + * optional short description below the title. Before this primitive + * the codebase carried five different `flex flex-wrap items-start + * justify-between` permutations across `account-section.tsx`, + * `integrations-section.tsx`, `notification-status-card.tsx`, + * `telegram-card.tsx`, `ntfy-card.tsx`, `web-push-card.tsx`, + * `api-section.tsx`, and `advanced-section.tsx`. The audit's mechanical + * drift report (`.planning/round-v1433-audit-settings.md` §2.5) listed + * one card with `mb-4 flex items-center gap-2`, the next with + * `flex flex-col gap-3 sm:flex-row sm:justify-between`, another with + * `flex flex-wrap items-start justify-between`, etc. + * + * The primitive consolidates the contract: + * - Icon left, title right of it (`gap-2` between them). + * - Optional status slot lives top-right; on ``. */ + title: string; + /** Optional id for the `

` so an outer `aria-labelledby` + * attribute on the card itself can reference it. */ + titleId?: string; + /** Optional short description rendered as muted text below the + * title row. Either a string (paragraph) or arbitrary node + * (links, badges, etc.). */ + description?: React.ReactNode; + /** Optional right-aligned status surface — typically an + * `` or a wrapper around badges. */ + status?: React.ReactNode; + /** Optional extra classes applied to the outer wrapper. */ + className?: string; +} + +export function SettingsCardHeader({ + icon: Icon, + title, + titleId, + description, + status, + className, +}: SettingsCardHeaderProps) { + return ( +
+
+
+
+ {status ?
{status}
: null} +
+ {description ? ( +

{description}

+ ) : null} +
+ ); +} diff --git a/src/components/settings/ntfy-card.tsx b/src/components/settings/ntfy-card.tsx index 68f3abbc2..d7508e576 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 { SettingsCardHeader } from "@/components/settings/_card-header"; import { useTranslations } from "@/lib/i18n/context"; interface NtfySettings { @@ -100,16 +101,11 @@ export function NtfyCard({ isAuthenticated }: { isAuthenticated: boolean }) { return (
- {/* v1.4.33 — drop the per-card status badge pair; the channel - pill in `` above already shows - live ntfy state. See `telegram-card.tsx` for the same fix. */} -
- -

{t("settings.ntfy")}

-
-

- {t("settings.ntfyDescription")} -

+
diff --git a/src/components/settings/telegram-card.tsx b/src/components/settings/telegram-card.tsx index 0501c8aa7..a943b03c2 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 { SettingsCardHeader } from "@/components/settings/_card-header"; import { useTranslations } from "@/lib/i18n/context"; interface TelegramSettings { @@ -105,19 +106,11 @@ export function TelegramCard({ return (
- {/* v1.4.33 — drop the per-card status badge pair. The - `` above already paints the live - channel pill for Telegram; surfacing the same fact a - second time inside this card produced overlapping status - surfaces (page header + card pill + dual Badge + the - enable Switch) for one bit of state. */} -
- -

{t("settings.telegram")}

-
-

- {t("settings.telegramDescription")} -

+
diff --git a/src/components/settings/web-push-card.tsx b/src/components/settings/web-push-card.tsx index 11de4a57a..6eb552af5 100644 --- a/src/components/settings/web-push-card.tsx +++ b/src/components/settings/web-push-card.tsx @@ -5,6 +5,7 @@ import { BellRing, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { TestConnectionButton } from "@/components/settings/test-connection-button"; +import { SettingsCardHeader } from "@/components/settings/_card-header"; import { useTranslations } from "@/lib/i18n/context"; /** Convert a URL-safe base64 string to an ArrayBuffer (for VAPID key). */ @@ -156,17 +157,11 @@ export function WebPushCard() { return (
- {/* v1.4.33 — drop the per-card status badge pair; the channel - pill in `` above already shows - live Web Push state. See `telegram-card.tsx` for the same - fix. */} -
- -

{t("settings.webPush")}

-
-

- {t("settings.webPushDescription")} -

+
{loading ? (