Files
HealthLog/src/components/settings/_card-header.tsx
T
Marc-André Bombeck 024da9fe96 fix(ui): granular sleep sources, viewport-safe tour, one aligned settings language
A night with coarse apple-health samples next to granular watch data
showed only awake and asleep — the per-session source pick now prefers
the source that knows its stages. Tour popovers clamp into the
viewport, wrap their footer and fall back to a bottom sheet on narrow
screens. Settings and admin sections share one alignment rule — icon
column left, everything else flush on the content column — with neutral
header icons, no duplicated section titles under the shell heading, and
every select on the account-style idiom with a visible chevron. The
dashboard's customize icon matches insights, the target-settings link
leaves the home charts, and the mood heatmap adopts the damped fills
the other mood charts already use.
2026-06-10 21:19:05 +02:00

100 lines
3.9 KiB
TypeScript

"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-muted-foreground h-5 w-5`.
* Neutral by design — primary/purple stays reserved for actions and
* highlight states, never for section-header iconography. */
icon: LucideIcon;
/** Card title — rendered as `<h2 class="text-lg font-semibold">`.
* Accepts a node so a card can wrap the title in a link. */
title: React.ReactNode;
/** Optional id for the `<h2>` so an outer `aria-labelledby`
* attribute on the card itself can reference it. */
titleId?: string;
/** Optional inline accessory rendered in the title row, immediately
* after the title (e.g. a tag chip + experimental badge). The title
* row wraps so the accessories reflow below on a narrow viewport. */
titleAccessory?: React.ReactNode;
/** Optional short description rendered as muted text below the
* title row. Either a string (paragraph) or arbitrary node
* (links, badges, extra sub-notes, 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,
titleAccessory,
description,
status,
className,
}: SettingsCardHeaderProps) {
return (
<header className={cn("flex items-start gap-2", className)}>
<Icon
className="text-muted-foreground mt-0.5 h-5 w-5 shrink-0"
aria-hidden="true"
/>
{/* Title + description share one column to the RIGHT of the icon, so the
description left-aligns with the title rather than slipping back under
the icon gutter. */}
<div className="min-w-0 flex-1 space-y-1">
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<h2 id={titleId} className="text-lg font-semibold">
{title}
</h2>
{titleAccessory}
</div>
{status ? (
<div className="flex shrink-0 items-center gap-2">{status}</div>
) : null}
</div>
{description ? (
<div className="text-muted-foreground space-y-1 text-xs">
{description}
</div>
) : null}
</div>
</header>
);
}