mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
feat(settings): extract <SettingsCardHeader> primitive
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 `<sm`, description sits muted below the title row. Call sites keep ownership of their body forms and action footers. First three call sites switched: `telegram-card.tsx`, `ntfy-card.tsx`, `web-push-card.tsx`. The remaining cards (`account-section.tsx`, `integrations-section.tsx`, `api-section.tsx`, `advanced-section.tsx`, `notification-status-card.tsx`) are wired in a follow-up — the primitive is now available without forcing every consumer to migrate in one commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8131fcb369
commit
981a3d55e1
4 files changed
+96
-34
No files matched your search
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* `<SettingsCardHeader>` — 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 `<sm` it falls below the
|
||||
* title block so the action surface keeps its 44 px tap target.
|
||||
* - Description is muted text, sits below the title row.
|
||||
*
|
||||
* Call sites can still wire their own action surfaces inside the body
|
||||
* of the card — the primitive only owns the *header* slice, not the
|
||||
* footer or the form rows.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface SettingsCardHeaderProps {
|
||||
/** Lucide icon component; rendered as `text-primary h-5 w-5`. */
|
||||
icon: LucideIcon;
|
||||
/** Card title — rendered as `<h2 class="text-lg font-semibold">`. */
|
||||
title: string;
|
||||
/** Optional id for the `<h2>` 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
|
||||
* `<IntegrationStatusPill>` 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 (
|
||||
<header className={cn("space-y-1", className)}>
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="text-primary h-5 w-5 shrink-0" aria-hidden="true" />
|
||||
<h2 id={titleId} className="text-lg font-semibold">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
{status ? <div className="flex shrink-0 items-center gap-2">{status}</div> : null}
|
||||
</div>
|
||||
{description ? (
|
||||
<p className="text-muted-foreground text-xs">{description}</p>
|
||||
) : null}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
{/* v1.4.33 — drop the per-card status badge pair; the channel
|
||||
pill in `<NotificationStatusCard>` above already shows
|
||||
live ntfy state. See `telegram-card.tsx` for the same fix. */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Bell className="text-primary h-5 w-5" />
|
||||
<h2 className="text-lg font-semibold">{t("settings.ntfy")}</h2>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
{t("settings.ntfyDescription")}
|
||||
</p>
|
||||
<SettingsCardHeader
|
||||
icon={Bell}
|
||||
title={t("settings.ntfy")}
|
||||
description={t("settings.ntfyDescription")}
|
||||
/>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
{/* v1.4.33 — drop the per-card status badge pair. The
|
||||
`<NotificationStatusCard>` 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. */}
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageCircle className="text-primary h-5 w-5" />
|
||||
<h2 className="text-lg font-semibold">{t("settings.telegram")}</h2>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
{t("settings.telegramDescription")}
|
||||
</p>
|
||||
<SettingsCardHeader
|
||||
icon={MessageCircle}
|
||||
title={t("settings.telegram")}
|
||||
description={t("settings.telegramDescription")}
|
||||
/>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
<form onSubmit={handleSave} className="space-y-4">
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
{/* v1.4.33 — drop the per-card status badge pair; the channel
|
||||
pill in `<NotificationStatusCard>` above already shows
|
||||
live Web Push state. See `telegram-card.tsx` for the same
|
||||
fix. */}
|
||||
<div className="flex items-center gap-2">
|
||||
<BellRing className="text-primary h-5 w-5" />
|
||||
<h2 className="text-lg font-semibold">{t("settings.webPush")}</h2>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
{t("settings.webPushDescription")}
|
||||
</p>
|
||||
<SettingsCardHeader
|
||||
icon={BellRing}
|
||||
title={t("settings.webPush")}
|
||||
description={t("settings.webPushDescription")}
|
||||
/>
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
{loading ? (
|
||||
|
||||
Reference in new issue
Block a user