From 981a3d55e1c08d43d2493ccb1b295d11405b7655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 16 May 2026 17:37:16 +0200 Subject: [PATCH] feat(settings): extract `` primitive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every Settings card paints the same shape — icon + title + optional status surface + optional muted description — but the codebase carried five different `flex-*` permutations across `account-section.tsx`, `integrations-section.tsx`, `notification-status-card.tsx`, the three notification channel cards, `api-section.tsx`, and `advanced-section.tsx`. The mechanical drift table in `.planning/round-v1433-audit-settings.md` §2.5 itemised the spread. Consolidate the contract into `_card-header.tsx`: icon left, title right of it, status slot lives top-right and falls below the title on ` --- src/components/settings/_card-header.tsx | 78 +++++++++++++++++++++++ src/components/settings/ntfy-card.tsx | 16 ++--- src/components/settings/telegram-card.tsx | 19 ++---- src/components/settings/web-push-card.tsx | 17 ++--- 4 files changed, 96 insertions(+), 34 deletions(-) create mode 100644 src/components/settings/_card-header.tsx 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 ? (