import { useUniqueId } from "@charcuterie/logic" import type { IntentName } from "@charcuterie/tokens" import type { ReactNode } from "react" import { toClassName } from "../toClassName.ts" import { GroupedListRow } from "./GroupedListRow.tsx" import { STICKY_HEADING_CLASS } from "./groupedListStickyHeading.ts" export type GroupedListHeadingLevel = 2 | 3 | 4 | 5 | 6 export type GroupedListSize = "md" | "sm" export type GroupedListItem = { /** * Trailing controls — usually one `Button size="sm"`. They sit at * the row's inline end and wrap under the text when the list is * too narrow to hold both. * * **Name a repeated action after its row.** Ten rows of "Open" are * ten buttons an agent cannot tell apart; `aria-label="Open Lobby"` * on each is one `getByRole` away. * * In a row with `onSelect` these stay their own targets: they sit * above the whole-row button, so pressing one never selects the * row. */ actions?: ReactNode /** * A figure that belongs beside the text rather than under it — a * payout, a total, a schedule. It sits between the text and the * actions in a wide list and drops under the text in a narrow one, * the same content-decided wrap the actions use: it asks for at * least 11rem beside the text column's 12rem. Never interactive: * in a row with `onSelect` the whole-row button sits over it. */ aside?: ReactNode /** `Badge`s, drawn after the label and wrapping onto their own line when they run out of room. */ badges?: ReactNode /** * Anything richer than one line of help — a payout, a rule list. * Drawn under the description, inside the text column, and never * truncated. Never interactive in a row with `onSelect`; a control * belongs in `actions`. */ content?: ReactNode /** One muted line under the label: a path, a key, a count. Truncated like the label. */ description?: string /** * The intent `isHighlighted` rings the row in. `accent` by default; * an app whose highlight means "this just paid out" passes * `success`. */ highlightIntent?: IntentName /** * `true` rings the row and pulses the ring twice — "this one just * changed", for a row a scan, a save or an arrival has touched. * The ring stays while the flag does, so the app decides how long * it lasts, usually by clearing it on a timer. * * **To pulse a row that is already highlighted again, change its * `key`.** A new key is a new row, and a new row starts the pulse * from the beginning; `` `${id}-${sequence}` `` is the usual shape. * * Under `prefers-reduced-motion` and on ePaper the ring does not * pulse; it is simply there. */ isHighlighted?: boolean /** * `true` draws the row receded — a sunken surface and a quieter * label — for an entry that exists but is switched off. It stays * fully operable and every colour on it still clears AA, which is * why it is a surface and a text role rather than an opacity: a * faded row is not a disabled one, and only a disabled control may * drop below the contrast floor. Say WHY in a badge too ("off"). */ isMuted?: boolean key: string /** * Plain text, because it is what the row is called — the thing a * search matched and the text a clipped row's tooltip repeats. */ label: string /** * Makes the whole row one native `