mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
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.
92 lines
2.7 KiB
TypeScript
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>
|
|
);
|
|
}
|