import { useContext, useId, type ComponentProps, type ReactNode } from "react"; import { cn } from "../lib/cn"; /** * Groups related rows on a page, a sheet or a dialog. Title 16/24 semibold, * one step above the 14/20 row labels so a section never reads as a setting * without a control; optional 12/18 description 4px below, content 12px below * that, and an optional action vertically centred on the title line. * * Variants (how the rows are held): * - `open` (default outside settings): rows sit on the page, no box. Sections * are separated by one full-width hairline (SectionStack). * - `group` (default inside settings, via `SectionVariantProvider` in the * settings shell): the heading sits above, the rows share one bordered * card (radius 14, surface, 20px inner gutter), split by hairlines. A * Notice among the rows loses its own box so it never nests a card in a * card, and a Section whose rows render nothing shows no empty card. * - `tiles`: every row gets its own bordered card. * * Rows are the direct children of a Section (SettingRow, ListRow, a Notice...). * `group` and `tiles` add the horizontal padding their boxes need, so the same * rows work in every variant. */ /** The section title type. Shared by every heading that sits above setting rows. */ export const SECTION_TITLE_CLASS = "text-base leading-6 font-semibold tracking-[-0.2px] text-fg"; import { SectionCardContext, SectionVariantContext, type SectionVariant } from "./section-variant"; export { SectionVariantProvider, useSectionListFrame, type SectionVariant, } from "./section-variant"; export interface SectionStackProps extends ComponentProps<"div"> { /** * Applies to every Section inside unless a Section sets its own. Defaults to * the enclosing provider's variant, then `open`. */ variant?: SectionVariant | undefined; /** * A full-width hairline between sections with 24px above and below. * Defaults to true for `open` and false for boxed variants (32px gap). */ divided?: boolean | undefined; } /** A column of sections with the page rhythm between them. */ export function SectionStack({ variant: variantProp, divided, className, children, ...props }: SectionStackProps) { const inherited = useContext(SectionVariantContext); const variant = variantProp ?? inherited ?? "open"; const hairlines = divided ?? variant === "open"; return (
*]:py-6 [&>*:first-child]:pt-0 [&>*:last-child]:pb-0" : "gap-8", className, )} {...props} > {children}
); } export interface SectionProps extends Omit, "title"> { /** * Omit only for the first rows of a page whose page title already names them * (Settings > General: Name, ID). Pass `aria-label` then if the rows need a name. */ title?: ReactNode | undefined; /** One or two short sentences, 12px muted. */ description?: ReactNode | undefined; /** Right of the title, centred on the title line. One quiet action. */ action?: ReactNode | undefined; /** Defaults to the enclosing SectionStack's variant, then `open`. */ variant?: SectionVariant | undefined; /** Hairlines between rows. `group` always has them. */ divided?: boolean | undefined; /** Heading level. Default 2 (under the page's h1). */ headingLevel?: 2 | 3 | undefined; /** Classes for the rows container. */ contentClassName?: string | undefined; children?: ReactNode | undefined; } const CONTENT_CLASS: Record = { open: "flex flex-col", group: cn( // The 20px gutter is on the card, so the hairlines between rows are inset // like the text they separate. "flex flex-col divide-y divide-border overflow-hidden rounded-lg border border-border bg-surface px-5 py-1 empty:hidden", // A Notice among the rows becomes a row: no second border, fill or radius. "[&>[data-slot=notice]]:rounded-none [&>[data-slot=notice]]:border-0 [&>[data-slot=notice]]:bg-transparent [&>[data-slot=notice]]:px-0 [&>[data-slot=notice]]:py-4", ), tiles: "flex flex-col gap-2 [&>*]:rounded-[14px] [&>*]:border [&>*]:border-border [&>*]:bg-surface [&>*]:px-4 [&>*]:py-1", }; export function Section({ title, description, action, variant: variantProp, divided, headingLevel = 2, contentClassName, className, children, ...props }: SectionProps) { const inherited = useContext(SectionVariantContext); const variant = variantProp ?? inherited ?? "open"; const headingId = useId(); const Heading = headingLevel === 3 ? "h3" : "h2"; const hasContent = children !== undefined && children !== null && children !== false; const hasHeader = title !== undefined && title !== null && title !== false; return (
{/* Title and action share the first line; on narrow widths the description runs under both, so a wide action never squeezes it into a column. */} {hasHeader ? (
{title} {action ? (
{action}
) : null} {description ? (

{description}

) : null}
) : null} {hasContent ? (
{children}
) : null}
); }