refactor(settings): unify the integration card skeleton

Every Settings → Integrations block now paints one consistent shape:
heading (icon + name + status pill) → a white primary description → a
grey secondary description that ends with the setup-guide link rendered
inline, instead of a standalone bottom-of-card element.

OAuth-only (WHOOP/Fitbit), BYO-credential (Withings/Oura/Polar) and
manual-key (Nightscout) cards share the same skeleton via the new
IntegrationCardDescription helper; the credential form stays as an extra
block where it applies. The "where to find these credentials" hint moves
above the Save button instead of sitting below it.

Rewrite the per-provider description strings across all six locales into a
parallel two-part shape with even length, and drop the now-orphaned
overlap/experimental sub-notes and the standalone setup-guide link.
This commit is contained in:
Marc-André Bombeck committed 2026-06-19 19:58:48 +02:00
1 parent d35c7660ea
commit aa092df9f0
12 files changed
+154 -104

No files matched your search

@@ -51,7 +51,7 @@ import {
pillStateFor,
type IntegrationStatusViewModel,
} from "./shared";
import { IntegrationSetupGuideLink } from "./setup-guide-link";
import { IntegrationCardDescription } from "./setup-guide-link";
export function FitbitCard({
viewModel,
@@ -210,18 +210,10 @@ export function FitbitCard({
</>
}
description={
<>
<p>{t("settings.fitbitDescription")}</p>
<p
data-testid="fitbit-experimental-note"
className="text-muted-foreground/80"
>
{t("settings.fitbitExperimentalNote")}
</p>
<p className="text-muted-foreground/80">
{t("settings.fitbitOverlapNote")}
</p>
</>
<IntegrationCardDescription
i18nPrefix="settings.fitbit"
provider="fitbit"
/>
}
status={
<IntegrationStatusPill
@@ -293,9 +285,6 @@ export function FitbitCard({
<h3 className="text-sm font-semibold">
{t("settings.fitbitCredentials")}
</h3>
<p className="text-muted-foreground text-xs">
{t("settings.fitbitCredentialsHelp")}
</p>
<form onSubmit={handleSaveCredentials} className="space-y-3">
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
@@ -341,6 +330,9 @@ export function FitbitCard({
/>
</div>
</div>
<p className="text-muted-foreground/80 text-xs">
{t("settings.integrationCredentialsHint")}
</p>
<div className="flex justify-end">
<Button
type="submit"
@@ -485,8 +477,6 @@ export function FitbitCard({
{t("settings.fitbitNoCredentials")}
</div>
)}
<IntegrationSetupGuideLink provider="fitbit" />
</div>
</div>
);
@@ -39,7 +39,7 @@ import { useTranslations } from "@/lib/i18n/context";
import { invalidateKeys, measurementDependentKeys, queryKeys } from "@/lib/query-keys";
import { IntegrationErrorMessage } from "./shared";
import { IntegrationSetupGuideLink } from "./setup-guide-link";
import { IntegrationCardDescription } from "./setup-guide-link";
interface NightscoutStatus {
connected: boolean;
@@ -170,7 +170,12 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) {
{t("settings.nightscoutTag")}
</span>
}
description={<p>{t("settings.nightscoutDescription")}</p>}
description={
<IntegrationCardDescription
i18nPrefix="settings.nightscout"
provider="nightscout"
/>
}
status={
<IntegrationStatusPill
state={pillState}
@@ -219,10 +224,6 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) {
</div>
)}
<p className="text-muted-foreground text-xs">
{t("settings.nightscoutHelp")}
</p>
<form ref={formRef} onSubmit={handleConnect} className="space-y-3">
<div className="space-y-1.5">
<Label htmlFor="nightscout-url">{t("settings.nightscoutUrl")}</Label>
@@ -367,8 +368,6 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) {
</Link>
</>
)}
<IntegrationSetupGuideLink provider="nightscout" />
</div>
</div>
);
@@ -41,7 +41,10 @@ import {
measurementDependentKeys,
queryKeys,
} from "@/lib/query-keys";
import { IntegrationSetupGuideLink } from "./setup-guide-link";
import {
IntegrationCardDescription,
type IntegrationDocsProvider,
} from "./setup-guide-link";
export interface OAuthProviderStatus {
connected: boolean;
@@ -205,7 +208,12 @@ export function OAuthProviderCard({
<SettingsCardHeader
icon={icon}
title={t(`${i18nPrefix}`)}
description={<p>{t(`${i18nPrefix}Description`)}</p>}
description={
<IntegrationCardDescription
i18nPrefix={i18nPrefix}
provider={provider as IntegrationDocsProvider}
/>
}
status={
<IntegrationStatusPill
state={pillState}
@@ -257,18 +265,11 @@ export function OAuthProviderCard({
</div>
)}
<p className="text-muted-foreground text-xs">
{t(`${i18nPrefix}Help`)}
</p>
{credentials && (
<div className="space-y-3" data-testid={`${provider}-credentials`}>
<h3 className="text-sm font-semibold">
{t(`${i18nPrefix}Credentials`)}
</h3>
<p className="text-muted-foreground text-xs">
{t(`${i18nPrefix}CredentialsHelp`)}
</p>
<form onSubmit={handleSaveCredentials} className="space-y-3">
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
@@ -314,6 +315,9 @@ export function OAuthProviderCard({
/>
</div>
</div>
<p className="text-muted-foreground/80 text-xs">
{t("settings.integrationCredentialsHint")}
</p>
<div className="flex justify-end">
<Button
type="submit"
@@ -424,8 +428,6 @@ export function OAuthProviderCard({
{msg}
</p>
)}
<IntegrationSetupGuideLink provider={provider} />
</div>
</div>
);
@@ -35,23 +35,38 @@ export function integrationDocsHref(provider: IntegrationDocsProvider): string {
return `${INTEGRATION_DOCS_BASE}/${provider}`;
}
export function IntegrationSetupGuideLink({
/**
* 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.
*/
export function IntegrationCardDescription({
i18nPrefix,
provider,
}: {
i18nPrefix: string;
provider: IntegrationDocsProvider;
}) {
const { t } = useTranslations();
return (
<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 text-xs underline-offset-2 hover:underline"
>
{t("settings.integrationSetupGuide")}
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</a>
<>
<p className="text-foreground">{t(`${i18nPrefix}Description`)}</p>
<p className="text-muted-foreground/80">
{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>
</>
);
}
@@ -43,7 +43,7 @@ import {
pillStateFor,
type IntegrationStatusViewModel,
} from "./shared";
import { IntegrationSetupGuideLink } from "./setup-guide-link";
import { IntegrationCardDescription } from "./setup-guide-link";
export function WhoopCard({
viewModel,
@@ -187,12 +187,10 @@ export function WhoopCard({
icon={Activity}
title={t("settings.whoop")}
description={
<>
<p>{t("settings.whoopDescription")}</p>
<p className="text-muted-foreground/80">
{t("settings.whoopOverlapNote")}
</p>
</>
<IntegrationCardDescription
i18nPrefix="settings.whoop"
provider="whoop"
/>
}
status={
<IntegrationStatusPill
@@ -265,9 +263,6 @@ export function WhoopCard({
<h3 className="text-sm font-semibold">
{t("settings.whoopCredentials")}
</h3>
<p className="text-muted-foreground text-xs">
{t("settings.whoopCredentialsHelp")}
</p>
<form onSubmit={handleSaveCredentials} className="space-y-3">
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
@@ -313,6 +308,9 @@ export function WhoopCard({
/>
</div>
</div>
<p className="text-muted-foreground/80 text-xs">
{t("settings.integrationCredentialsHint")}
</p>
<div className="flex justify-end">
<Button
type="submit"
@@ -457,8 +455,6 @@ export function WhoopCard({
{t("settings.whoopNoCredentials")}
</div>
)}
<IntegrationSetupGuideLink provider="whoop" />
</div>
</div>
);
@@ -36,7 +36,7 @@ import {
pillStateFor,
type IntegrationStatusViewModel,
} from "./shared";
import { IntegrationSetupGuideLink } from "./setup-guide-link";
import { IntegrationCardDescription } from "./setup-guide-link";
export function WithingsCard({
viewModel,
@@ -194,7 +194,12 @@ export function WithingsCard({
<SettingsCardHeader
icon={Link2}
title={t("settings.withings")}
description={t("settings.withingsDescription")}
description={
<IntegrationCardDescription
i18nPrefix="settings.withings"
provider="withings"
/>
}
status={
<IntegrationStatusPill
state={pillState}
@@ -347,6 +352,9 @@ export function WithingsCard({
/>
</div>
</div>
<p className="text-muted-foreground/80 text-xs">
{t("settings.integrationCredentialsHint")}
</p>
<div className="flex justify-end">
<Button
type="submit"
@@ -492,8 +500,6 @@ export function WithingsCard({
{t("settings.withingsNoCredentials")}
</div>
)}
<IntegrationSetupGuideLink provider="withings" />
</div>
</div>
);