Files
HealthLog/src/components/ui/sheet-section.tsx
T

115 lines
4.1 KiB
TypeScript

"use client";
import * as React from "react";
import { ChevronDown } from "lucide-react";
import { Collapsible as CollapsiblePrimitive } from "radix-ui";
import { cn } from "@/lib/utils";
/**
* v1.17.0 — `SheetSection` is the collapsible disclosure primitive that
* powers the sectioned single-sheet add flows (mood, cycle). Each section
* carries a title, an optional leading icon, and a `summary` slot that
* communicates what is selected inside it while collapsed (e.g. a count
* badge) — so a closed section never hides its contents silently.
*
* Built on Radix Collapsible. The chevron rotates and the content height
* animates open/closed via the `tw-animate-css` `collapsible-down/up`
* keyframes; both motions are suppressed under `prefers-reduced-motion`.
* Matches the shadcn new-york + zinc token language used across the app.
*/
interface SheetSectionProps {
/** Section heading shown on the always-visible trigger row. */
title: string;
/**
* A node rendered on the trigger row's trailing edge — typically a count
* badge that reports what is selected inside while the section is
* collapsed. Stays visible whether the section is open or closed.
*/
summary?: React.ReactNode;
/** Optional leading icon (a Lucide component or any node). */
icon?: React.ReactNode;
/** Whether the section starts expanded (uncontrolled). Defaults to closed. */
defaultOpen?: boolean;
/** Controlled open state. When set, pair with `onOpenChange`. */
open?: boolean;
/** Controlled open-state change handler. */
onOpenChange?: (open: boolean) => void;
/** Optional className merged onto the section wrapper. */
className?: string;
children: React.ReactNode;
}
export function SheetSection({
title,
summary,
icon,
defaultOpen = false,
open,
onOpenChange,
className,
children,
}: SheetSectionProps) {
return (
<CollapsiblePrimitive.Root
// Controlled when `open` is provided, otherwise uncontrolled.
{...(open === undefined ? { defaultOpen } : { open, onOpenChange })}
data-slot="sheet-section"
className={cn("border-border/60 border-t pt-3", className)}
>
<CollapsiblePrimitive.Trigger
data-slot="sheet-section-trigger"
className="group focus-visible:ring-ring/50 -mx-1 flex w-[calc(100%+0.5rem)] items-center gap-2 rounded-md px-1 py-1.5 text-left transition-colors focus-visible:ring-2 focus-visible:outline-none"
>
{icon ? (
<span
className="text-muted-foreground flex h-4 w-4 shrink-0 items-center justify-center [&>svg]:h-4 [&>svg]:w-4"
aria-hidden="true"
>
{icon}
</span>
) : null}
<span className="text-foreground flex-1 text-sm font-medium">
{title}
</span>
{summary ? (
<span
data-slot="sheet-section-summary"
className="flex items-center"
>
{summary}
</span>
) : null}
<ChevronDown
aria-hidden="true"
className="text-muted-foreground h-4 w-4 shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-180 motion-reduce:transition-none"
/>
</CollapsiblePrimitive.Trigger>
<CollapsiblePrimitive.Content
data-slot="sheet-section-content"
className="overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down motion-reduce:animate-none"
>
<div className="space-y-3 pt-3 pb-1">{children}</div>
</CollapsiblePrimitive.Content>
</CollapsiblePrimitive.Root>
);
}
/**
* A compact "(N)" count badge for a `SheetSection` summary slot. Renders
* nothing when `count <= 0`, so a section with no selections shows a bare
* chevron rather than a "(0)".
*/
export function SheetSectionCount({ count }: { count: number }) {
if (count <= 0) return null;
return (
<span
data-slot="sheet-section-count"
className="bg-primary/15 text-primary inline-flex min-w-5 items-center justify-center rounded-full px-1.5 py-0.5 text-xs font-medium tabular-nums"
>
{count}
</span>
);
}