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 (
);
}
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 ? (