Files
HealthLog/src/components/settings/_info-tile.tsx
T
Marc-André Bombeck b2e1aa0cde 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.
2026-08-07 19:39:11 +02:00

92 lines
2.7 KiB
TypeScript

"use client";
/**
* `<SettingsInfoTile>` — the one callout shape for Settings.
*
* Before this primitive, every section hand-rolled its own notice box:
* `border-l-2`/`border-l-4`, `rounded-md`/`rounded-lg`, mismatched
* border/background tone intensities, and raw palette colours used as
* text (which fail AA in light mode). The tile fixes the surface to a
* single contract — icon left, short text right, a tone-coloured left
* border — and routes every tone through the semantic tokens so the
* same notice reads identically in light and dark.
*
* Tones map to the project's semantic colour tokens:
* - `info` → `--info` (neutral guidance)
* - `warning` → `--warning` (caution / override / reconnect)
* - `success` → `--success` (confirmation)
* - `primary` → `--primary` (brand-accented instruction)
* - `neutral` → muted surface (low-emphasis aside)
*/
import * as React from "react";
import type { LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
export type SettingsInfoTileTone =
"info" | "warning" | "success" | "primary" | "neutral";
const TONE_SURFACE: Record<SettingsInfoTileTone, string> = {
info: "border-info/40 bg-info/10",
warning: "border-warning/40 bg-warning/10",
success: "border-success/40 bg-success/10",
primary: "border-primary/40 bg-primary/10",
neutral: "border-border bg-muted/30",
};
const TONE_ICON: Record<SettingsInfoTileTone, string> = {
info: "text-info",
warning: "text-warning",
success: "text-success",
primary: "text-primary",
neutral: "text-muted-foreground",
};
export interface SettingsInfoTileProps extends Omit<
React.ComponentProps<"div">,
"title"
> {
/** Lucide icon component; rendered in the tone colour. */
icon: LucideIcon;
/** Visual tone — routes through the semantic colour tokens. */
tone?: SettingsInfoTileTone;
/** Optional emphasised first line. */
title?: React.ReactNode;
/** Body content — muted text. */
children?: React.ReactNode;
/** Optional extra classes on the outer wrapper. */
className?: string;
}
export function SettingsInfoTile({
icon: Icon,
tone = "info",
title,
children,
className,
...props
}: SettingsInfoTileProps) {
return (
<div
className={cn(
"flex items-start gap-2 rounded-md border-l-4 p-3 text-sm",
TONE_SURFACE[tone],
className,
)}
{...props}
>
<Icon
className={cn("mt-0.5 h-4 w-4 shrink-0", TONE_ICON[tone])}
aria-hidden="true"
/>
<div className="min-w-0 space-y-1">
{title ? <p className="text-foreground font-medium">{title}</p> : null}
{children ? (
<div className="text-muted-foreground">{children}</div>
) : null}
</div>
</div>
);
}