mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
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.
100 lines
3.9 KiB
TypeScript
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>
|
|
);
|
|
}
|