Files
HealthLog/src/components/settings/settings-card.tsx
T
Marc-André Bombeck 13b4a0be0b refactor(settings): thin the Account tree down to the profile
Two cards on Settings → Account were asking questions that already have a
home somewhere else.

- cycle tracking is a module, and the Modules hub carries the switch that
  drives the same `cycleTrackingEnabled` column. The Account card was a
  second door to one decision, so it goes, and the "manage in Account"
  deep-link beside the module switch goes with it. That link pointed at a
  `#cycle-tracking` anchor no element ever rendered, so it never landed
  anywhere in the first place
- changing a password belongs with the second factors and the passkeys.
  The card moves to Settings → Security and leads that page. It renders
  before the factor status resolves: it is now the only way to change a
  password and must not depend on a read it does not need

Account keeps the profile, the photo, the identity fields and the AI
context. The `settings.cycleTracking.*` strings leave all six bundles.
2026-08-08 15:17:05 +02:00

88 lines
3.6 KiB
TypeScript

import * as React from "react";
import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
/**
* `<SettingsCard>` — the standard Settings card container.
*
* Every Settings section paints the same bordered surface: a rounded card
* with the app's one card-padding contract (`p-4 md:p-6` — 16 px on phones,
* 24 px from `md` up). Before this primitive the surface hand-rolled
* `bg-card border-border rounded-xl border p-4 sm:p-6` in ~35 files, which
* (a) bypassed the `<Card>` primitive entirely and (b) stepped padding at
* `sm:` (640 px) instead of `md:` (768 px), so on a ~700 px tablet Settings
* cards read denser than every other surface.
*
* The default renders the ui `<Card>` so the surface composes from one shape,
* and like `<Card>` the card owns its internal rhythm: `flex flex-col gap-4`.
* The header→body distance therefore comes from the container's gap, never
* from a margin a call site remembers to add. It used to come from three
* competing techniques at once — `mt-4` on the first body child (20 files),
* `space-y-4` on the card (9), `mb-3`/`mb-4` on the header (2) — which is
* exactly the drift that made neighbouring cards read as different systems.
* A denser card overrides the gap on the card itself (`className="gap-2"`),
* as `<Card>` documents; nothing else may re-declare the header→body step.
*
* Pass `className` for per-card extras (`scroll-mt-28`, `flex h-full`, …).
*
* `as="section"` keeps a card's semantic landmark element (a `<section>` with
* an `aria-labelledby`) while painting the same shape, rhythm, and padding.
*
* `flush` is for the other body shape: an edge-to-edge `divide-y` ledger whose
* rows carry their own inset. Such a card drops BOTH the padding and the gap.
* Dropping only the padding is the trap — the gap survives as dead,
* unclickable space between the rows that the first row does not have, so the
* list reads a step off itself. `<InsightSectionCard flush>` names the same
* shape on the Insights side.
*/
const SETTINGS_CARD_SHELL = "flex flex-col gap-4 p-4 md:p-6";
// Both breakpoints are named explicitly: the `<Card>` primitive carries
// `md:gap-6` / `md:py-6`, and a bare `gap-0` / `p-0` loses to a `md:` variant
// from `md` up — which is exactly the width the dead space showed at.
const SETTINGS_CARD_FLUSH_SHELL =
"flex flex-col gap-0 overflow-hidden p-0 md:gap-0 md:p-0";
type SettingsCardOwnProps = { className?: string; flush?: boolean };
type SettingsCardProps<E extends React.ElementType> = SettingsCardOwnProps & {
as?: E;
} & Omit<React.ComponentPropsWithoutRef<E>, keyof SettingsCardOwnProps | "as">;
export function SettingsCard<E extends React.ElementType = typeof Card>({
as,
className,
flush = false,
...props
}: SettingsCardProps<E>) {
if (as) {
const Component = as as React.ElementType;
return (
<Component
data-slot="card"
// Mirror the ui Card shell so a semantic landmark card paints
// identically to the default <Card>-backed one.
className={cn(
"bg-card text-card-foreground rounded-xl border shadow-sm",
flush
? SETTINGS_CARD_FLUSH_SHELL
: "flex flex-col gap-4 p-4 md:gap-6 md:p-6",
className,
)}
{...props}
/>
);
}
return (
<Card
// Reset the primitive's flex/gap layout and apply the shared
// `p-4 md:p-6` padding; Settings bodies own their internal spacing.
className={cn(
flush ? SETTINGS_CARD_FLUSH_SHELL : SETTINGS_CARD_SHELL,
className,
)}
{...(props as React.ComponentProps<typeof Card>)}
/>
);
}