refactor(settings): one header, one action row, one integration card

The sweep that follows from the primitives. Nothing here is new behaviour;
every change removes a second way of saying something the surface already
said one way somewhere else.

Headers. The two collapsible security cards stop rebuilding
`SettingsCardHeader` by hand to carry a chevron — the chevron rides inside
the title button, and the description moves out of the collapsed region, so
the first paint is no longer a bare title. The import card shell, the export
and import section headings, and the four free-standing group headers stop
inventing their own type scale: a heading above a GROUP of cards is
`SectionHeading` (`text-base`), a card title is `SettingsCardHeader`
(`text-lg`), and the step between them is what says which is which. The
targets card gets a title instead of a naked icon.

Actions. Every card-level row is `SettingsCardActions`. The primary action
comes out of the header's status slot in six cards (password, web push,
pillar order, coach memory, and the two danger-zone triggers) and out of the
middle of the card in three more; the reset that hung in the dashboard-layout
header joins save as a secondary in the same row. Result and status lines
move ABOVE the row — sources' cross-link, the invite form's two status lines
— because nothing follows a card's actions. Revoking a share link is
`variant="destructive"` rather than an `outline` hand-tinted red in two
different ways. The import row had four variants for four buttons; it has
two. Export downloads are the card's primary, not an outline link.

Integrations. One description sentence with the setup-guide link inline: the
second paragraph was byte-identical across six of eight cards, which is the
tell that it said nothing about the provider it sat under. The `<hr>` under
the header — which existed only in this folder — is gone. So is the hint that
pointed at a guide 40 px above it, and the second format for "not
configured". Connect is one button everywhere: primary, `sm`, in the action
row, instead of primary-and-small in three cards and outline-and-large in
four.

Plus the standards leftovers the audit listed: two `pl-5` list gutters, one
`scroll-mt-20` anchor, a purple header icon, `text-[10px]` badges, four
hand-rolled callout shells that `SettingsInfoTile` already paints, and the
two inline buttons that never had the mobile tap floor at all.
This commit is contained in:
Marc-André Bombeck committed 2026-08-07 19:39:11 +02:00
1 parent ce9b2814e6
commit b2e1aa0cde
63 files changed
+720 -760

No files matched your search

@@ -146,12 +146,12 @@ describe("<AppleHealthCard>", () => {
expect(html).toContain("quiet");
});
it("carries the shared card divider like every sibling", () => {
it("carries no card divider — the card gap does that job", () => {
statusPayload = {
lastSyncedAt: null,
syncHealth: { verdict: "pending_first_sync", since: null },
};
expect(render()).toContain('data-testid="integration-card-divider"');
expect(render()).not.toContain('data-testid="integration-card-divider"');
});
it("links to the existing one-shot Apple Health import fallback", () => {
@@ -80,13 +80,13 @@ describe("NightscoutCard — parked + test + data-link + invalidation", () => {
expect(html).toContain('href="/insights/blood-glucose"');
});
it("carries the shared card divider like every sibling", () => {
it("carries no card divider — the card gap does that job", () => {
const html = render({
connected: true,
configured: true,
syncHealth: { verdict: "fresh", since: null },
});
expect(html).toContain('data-testid="integration-card-divider"');
expect(html).not.toContain('data-testid="integration-card-divider"');
});
it("renders the per-metric freshness disclosure with a stale row", () => {
@@ -160,8 +160,6 @@ export function AppleHealthCard({ enabled }: { enabled: boolean }) {
}
/>
<hr data-testid="integration-card-divider" className="border-border/60" />
<div className="space-y-4">
{isError && (
<IntegrationErrorMessage
@@ -32,6 +32,7 @@ import {
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Badge } from "@/components/ui/badge";
import { SettingsCardActions } from "@/components/settings/_card-actions";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -243,8 +244,6 @@ export function FitbitCard({
}
/>
<hr data-testid="integration-card-divider" className="border-border/60" />
<div className="space-y-4">
{errorMessage && <IntegrationErrorMessage message={errorMessage} />}
{/* Parked-integration resume CTA. Surfaces only when the row
@@ -360,10 +359,7 @@ export function FitbitCard({
/>
</div>
</div>
<p className="text-muted-foreground text-xs">
{t("settings.integrationCredentialsHint")}
</p>
<div className="flex justify-end">
<SettingsCardActions>
<Button
type="submit"
variant="outline"
@@ -380,7 +376,7 @@ export function FitbitCard({
)}
{t("settings.fitbitSaveCredentials")}
</Button>
</div>
</SettingsCardActions>
{credsMsg && (
<p
role="alert"
@@ -503,19 +499,26 @@ export function FitbitCard({
</Link>
</>
) : status?.configured ? (
<Button
variant="outline"
onClick={() => {
window.location.href = "/api/fitbit/connect";
}}
>
<Link2 className="h-4 w-4" />
{t("settings.fitbitConnect")}
</Button>
<SettingsCardActions>
<Button
size="sm"
className="min-h-11 w-full sm:w-auto"
data-testid="fitbit-connect"
onClick={() => {
window.location.href = "/api/fitbit/connect";
}}
>
<Link2 className="h-3.5 w-3.5" />
{t("settings.fitbitConnect")}
</Button>
</SettingsCardActions>
) : (
<div className="bg-muted/50 text-muted-foreground rounded-lg p-3 text-sm">
<p
className="text-muted-foreground text-xs"
data-testid="integration-unavailable"
>
{t("settings.fitbitNoCredentials")}
</div>
</p>
)}
</div>
</SettingsCard>
@@ -40,6 +40,7 @@ import {
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Badge } from "@/components/ui/badge";
import { SettingsCardActions } from "@/components/settings/_card-actions";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -415,8 +416,6 @@ export function GoogleHealthCard({
}
/>
<hr data-testid="integration-card-divider" className="border-border/60" />
<div className="space-y-4">
{errorMessage && <IntegrationErrorMessage message={errorMessage} />}
@@ -553,10 +552,7 @@ export function GoogleHealthCard({
/>
</div>
</div>
<p className="text-muted-foreground text-xs">
{t("settings.integrationCredentialsHint")}
</p>
<div className="flex justify-end">
<SettingsCardActions>
<Button
type="submit"
variant="outline"
@@ -573,7 +569,7 @@ export function GoogleHealthCard({
)}
{t("settings.googleHealthSaveCredentials")}
</Button>
</div>
</SettingsCardActions>
{credsMsg && (
<p
role="alert"
@@ -740,19 +736,26 @@ export function GoogleHealthCard({
</Link>
</>
) : status?.configured ? (
<Button
variant="outline"
onClick={() => {
window.location.href = "/api/google-health/connect";
}}
>
<Link2 className="h-4 w-4" />
{t("settings.googleHealthConnect")}
</Button>
<SettingsCardActions>
<Button
size="sm"
className="min-h-11 w-full sm:w-auto"
data-testid="googlehealth-connect"
onClick={() => {
window.location.href = "/api/google-health/connect";
}}
>
<Link2 className="h-3.5 w-3.5" />
{t("settings.googleHealthConnect")}
</Button>
</SettingsCardActions>
) : (
<div className="bg-muted/50 text-muted-foreground rounded-lg p-3 text-sm">
<p
className="text-muted-foreground text-xs"
data-testid="integration-unavailable"
>
{t("settings.googleHealthNoCredentials")}
</div>
</p>
)}
</div>
</SettingsCard>
@@ -34,6 +34,7 @@ import {
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { SettingsCardActions } from "@/components/settings/_card-actions";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { PasswordInput } from "@/components/ui/password-input";
@@ -216,8 +217,6 @@ export function NightscoutCard({
}
/>
<hr data-testid="integration-card-divider" className="border-border/60" />
<div className="space-y-4">
{errorMessage && <IntegrationErrorMessage message={errorMessage} />}
{operatorApprovalRequired && (
@@ -323,7 +322,7 @@ export function NightscoutCard({
</p>
</div>
<div className="flex justify-end">
<SettingsCardActions>
<Button
type="submit"
variant="outline"
@@ -342,7 +341,7 @@ export function NightscoutCard({
? t("settings.nightscoutUpdate")
: t("settings.nightscoutConnect")}
</Button>
</div>
</SettingsCardActions>
{msg && (
<p
@@ -33,6 +33,7 @@ import {
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { SettingsCardActions } from "@/components/settings/_card-actions";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { PasswordInput } from "@/components/ui/password-input";
@@ -285,8 +286,6 @@ export function OAuthProviderCard({
}
/>
<hr data-testid="integration-card-divider" className="border-border/60" />
<div className="space-y-4">
{errorMessage && (
<div data-testid={`${provider}-error`}>
@@ -387,10 +386,7 @@ export function OAuthProviderCard({
/>
</div>
</div>
<p className="text-muted-foreground text-xs">
{t("settings.integrationCredentialsHint")}
</p>
<div className="flex justify-end">
<SettingsCardActions>
<Button
type="submit"
variant="outline"
@@ -407,7 +403,7 @@ export function OAuthProviderCard({
)}
{t(`${i18nPrefix}SaveCredentials`)}
</Button>
</div>
</SettingsCardActions>
{credsMsg && (
<p
role="alert"
@@ -505,17 +501,19 @@ export function OAuthProviderCard({
</Link>
</>
) : (
<Button
type="button"
size="sm"
className="min-h-11 w-full sm:w-auto"
disabled={serverUnavailable}
onClick={handleConnect}
data-testid={`${provider}-connect`}
>
<Link2 className="h-3.5 w-3.5" />
{t(`${i18nPrefix}Connect`)}
</Button>
<SettingsCardActions>
<Button
type="button"
size="sm"
className="min-h-11 w-full sm:w-auto"
disabled={serverUnavailable}
onClick={handleConnect}
data-testid={`${provider}-connect`}
>
<Link2 className="h-3.5 w-3.5" />
{t(`${i18nPrefix}Connect`)}
</Button>
</SettingsCardActions>
)}
{msg && (
@@ -102,11 +102,14 @@ export function IntegrationRedirectGuide({
}
/**
* v1.18.7 (Wave F) — the one description skeleton every integration card paints:
* a WHITE primary sentence (`${i18nPrefix}Description`), then a GREY secondary
* sentence (`${i18nPrefix}DescriptionSecondary`) that ends with the setup-guide
* link rendered INLINE at the very end of the grey copy — not a standalone
* bottom-of-card element. Single-sourced so all six cards read as one family.
* The one description every integration card paints: ONE sentence, then the
* setup-guide link inline at the end of it.
*
* It used to be two paragraphs stacked in the description slot — a white
* primary sentence and a grey secondary one whose only job was to carry the
* link. The secondary sentence was byte-identical across six of the eight
* cards, which is the tell: it said nothing about the provider it sat under.
* The link is the part that carried information, so the link is what stayed.
*/
export function IntegrationCardDescription({
i18nPrefix,
@@ -118,21 +121,18 @@ export function IntegrationCardDescription({
const { t } = useTranslations();
return (
<>
<p className="text-foreground">{t(`${i18nPrefix}Description`)}</p>
<p className="text-muted-foreground">
{t(`${i18nPrefix}DescriptionSecondary`)}{" "}
<a
href={integrationDocsHref(provider)}
target="_blank"
rel="noopener noreferrer"
data-testid={`${provider}-setup-guide`}
data-slot="integration-setup-guide"
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 underline underline-offset-2"
>
{t("settings.integrationSetupGuide")}
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</a>
</p>
{t(`${i18nPrefix}Description`)}{" "}
<a
href={integrationDocsHref(provider)}
target="_blank"
rel="noopener noreferrer"
data-testid={`${provider}-setup-guide`}
data-slot="integration-setup-guide"
className="hover:text-foreground inline-flex items-center gap-1 underline underline-offset-2"
>
{t("settings.integrationSetupGuide")}
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</a>
</>
);
}
@@ -25,6 +25,7 @@ import {
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { SettingsCardActions } from "@/components/settings/_card-actions";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { PasswordInput } from "@/components/ui/password-input";
@@ -224,8 +225,6 @@ export function WhoopCard({
}
/>
<hr data-testid="integration-card-divider" className="border-border/60" />
<div className="space-y-4">
{errorMessage && <IntegrationErrorMessage message={errorMessage} />}
{/* Parked-integration resume CTA. Surfaces only when the row
@@ -342,10 +341,7 @@ export function WhoopCard({
/>
</div>
</div>
<p className="text-muted-foreground text-xs">
{t("settings.integrationCredentialsHint")}
</p>
<div className="flex justify-end">
<SettingsCardActions>
<Button
type="submit"
variant="outline"
@@ -362,7 +358,7 @@ export function WhoopCard({
)}
{t("settings.whoopSaveCredentials")}
</Button>
</div>
</SettingsCardActions>
{credsMsg && (
<p
role="alert"
@@ -485,19 +481,26 @@ export function WhoopCard({
</Link>
</>
) : status?.configured ? (
<Button
variant="outline"
onClick={() => {
window.location.href = "/api/whoop/connect";
}}
>
<Link2 className="h-4 w-4" />
{t("settings.whoopConnect")}
</Button>
<SettingsCardActions>
<Button
size="sm"
className="min-h-11 w-full sm:w-auto"
data-testid="whoop-connect"
onClick={() => {
window.location.href = "/api/whoop/connect";
}}
>
<Link2 className="h-3.5 w-3.5" />
{t("settings.whoopConnect")}
</Button>
</SettingsCardActions>
) : (
<div className="bg-muted/50 text-muted-foreground rounded-lg p-3 text-sm">
<p
className="text-muted-foreground text-xs"
data-testid="integration-unavailable"
>
{t("settings.whoopNoCredentials")}
</div>
</p>
)}
</div>
</SettingsCard>
@@ -24,6 +24,7 @@ import {
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { SettingsCardActions } from "@/components/settings/_card-actions";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { PasswordInput } from "@/components/ui/password-input";
@@ -236,8 +237,6 @@ export function WithingsCard({
}
/>
<hr data-testid="integration-card-divider" className="border-border/60" />
<div className="space-y-4">
{errorMessage && <IntegrationErrorMessage message={errorMessage} />}
{/* v1.4.25 W5d — Withings activity-scope reconnect banner.
@@ -390,10 +389,7 @@ export function WithingsCard({
/>
</div>
</div>
<p className="text-muted-foreground text-xs">
{t("settings.integrationCredentialsHint")}
</p>
<div className="flex justify-end">
<SettingsCardActions>
<Button
type="submit"
variant="outline"
@@ -410,7 +406,7 @@ export function WithingsCard({
)}
{t("settings.withingsSaveCredentials")}
</Button>
</div>
</SettingsCardActions>
{credsMsg && (
<p
role="alert"
@@ -534,19 +530,26 @@ export function WithingsCard({
</Link>
</>
) : status?.configured ? (
<Button
variant="outline"
onClick={() => {
window.location.href = "/api/withings/connect";
}}
>
<Link2 className="h-4 w-4" />
{t("settings.withingsConnect")}
</Button>
<SettingsCardActions>
<Button
size="sm"
className="min-h-11 w-full sm:w-auto"
data-testid="withings-connect"
onClick={() => {
window.location.href = "/api/withings/connect";
}}
>
<Link2 className="h-3.5 w-3.5" />
{t("settings.withingsConnect")}
</Button>
</SettingsCardActions>
) : (
<div className="bg-muted/50 text-muted-foreground rounded-lg p-3 text-sm">
<p
className="text-muted-foreground text-xs"
data-testid="integration-unavailable"
>
{t("settings.withingsNoCredentials")}
</div>
</p>
)}
</div>
</SettingsCard>