import type { ReactNode } from 'react'
import { IconChevronRight, IconDotsVertical } from '@tabler/icons-react'
import { useRender } from '@base-ui/react/use-render'
import { cn } from './cn'
import { COUNT_TEXT_CLASSES, SIDEBAR_ROW_CLASSES, SIDEBAR_ROW_TEXT_CLASSES } from './looks'
import { IconButton } from './IconButton'
import { Menu } from './Menu'
import { SectionLabel } from './SectionLabel'
/**
* The 32px row a section starts with (Peek's SectionHeader, 2026-09-01):
* a SectionLabel, an optional collapse chevron that makes the title the
* toggle, and actions that appear while the row is hovered or keyboard-focused,
* as IconButtons with tooltips.
*
* Three things changed on 2026-09-09, for `CollapsibleSection`:
*
* - **The title is a button when it toggles.** It was a `div` with an
* `onClick`, so the keyboard could not open or close a section at all.
* The button fills the row up to the actions, so the whole row is still
* the hit target; the actions sit beside it rather than inside it,
* because a button inside a button is invalid HTML — and was why every
* action had to stop its click from also toggling.
* - **The hover is CSS.** The fill and the actions' reveal were React
* state (`isHovered`), which the README forbids — a mount cannot stay in
* step with a transition. `group-hover` now; the actions are in the row
* at `opacity-0` until hovered or focused, so a keyboard user reaches
* them too.
* - **`render`** lets `CollapsibleSection` hand in Base UI's
* `Collapsible.Trigger` as the title button — one header for the
* hand-held section (`isExpanded` / `onToggle`) and the Base UI one,
* with no second click handler.
*
* One API change from Peek's original, safe because no call site used the
* old shape yet: the two hard-wired action slots (add, sort) became
* `actions` — the caller brings the icon, the tooltip and the handler.
*/
export interface SectionAction {
/** 16px, stroke 1.5. */
icon: ReactNode
tooltip: string
onClick: () => void
}
export interface SectionHeaderProps {
title: string
/** Collapsible: draws the chevron and makes the title a button that toggles. */
chevron?: boolean
/**
* 16px, stroke 1.5, before the title — a folder's (Katerina, 5 October:
* Peek's Folders, the standard heading with a folder icon). With `chevron`
* the icon is what shows at rest and the arrow takes its place under the
* pointer or the keyboard, as a NavItem's icon turns into its arrow.
*/
icon?: ReactNode
isExpanded?: boolean
onToggle?: () => void
/**
* Beside the title and always visible, before the actions — a count. An
* `UnreadDot` instead sits in the last action's place (the ⋮'s, with a
* `menu`) and steps aside while the actions show, as a NavItem's dot does.
*
* The actions come and go with the hover; this does not, because a count is
* information rather than an affordance. Peek's Screener header carries its
* number as a `Chip` here, and was the last hand-drawn folding header in the
* app for want of the slot (ADOPTION B23).
*/
trailing?: ReactNode
/**
* How many there are, 8px after the title — not at the row's end, where
* `trailing` goes (Katerina, 7 October: a heading over a list says its
* count beside its name, as a tab does).
*/
count?: number
/** Right-aligned, in the order given. */
actions?: SectionAction[]
/**
* The heading's "More options" menu: `MenuItem`s — what acts on the whole
* section, a folder's Rename or Archive (Katerina, 5 October: Peek's Folders
* page goes, and its folder actions move onto the folder's heading). The ⋮
* is NavItem's, after the actions and revealed with them; it stays while its
* menu is open.
*/
menu?: ReactNode
/** The ⋮'s accessible name. Default "More options for" the title. */
menuLabel?: string
/** `hover` reveals the actions while the row is hovered or keyboard-focused (always on a screen with no hover); `always` keeps them. */
showActions?: 'hover' | 'always'
/**
* `fill` lights the row under the pointer when it does something (a toggle,
* actions); `none` keeps it still — a heading whose actions are always shown,
* where the buttons light up on their own (UIG-14, Katerina, 19 September).
*/
hover?: 'fill' | 'none'
/**
* `heading` (default): a section's title, the `SectionLabel`. `quiet`: a
* small grey label over a list (12px, muted) — "Folders" over the folders,
* which never reads as one of them. `row`: a row among rows — the chevron
* and the title at a row's weight and colour, as a sidebar row is (Katerina,
* 30 September: Leaf's folders, in Ship).
*/
look?: 'heading' | 'quiet' | 'row'
/**
* What the title renders as, in Base UI's manner. A plain button with
* `onToggle` by default; `CollapsibleSection` hands in `Collapsible.Trigger`.
*/
render?: useRender.RenderProp
className?: string
}
export function SectionHeader({ title, chevron = false, icon, isExpanded = true, onToggle, trailing, count, actions, menu, menuLabel, showActions = 'hover', hover = 'fill', look = 'heading', render, className }: SectionHeaderProps) {
const row = look === 'row'
/*
One arrow for everything that folds, a NavItem's 16px, and the title 8px
after it as a row's label is (Katerina, 5 October: a folder heading and
the rows under it fold with one arrow; then every folding heading, Peek's
Starred too). It was 12px with a 4px gap; with an icon, the arrow now
fills the icon's place exactly.
*/
const acts = (actions && actions.length > 0) || menu != null
/*
A dot sits in the place of the last button and steps aside while the
buttons show — the rows' dot and ⋮ share a place the same way (Katerina,
5 October). A count stays beside them: it is information (09-09).
"A dot" is whatever holds an `UnreadDot`, found by the `data-unread` it
draws, so the CSS decides (`has-[[data-unread]]`) rather than a check of
the element's type: that missed a dot in a tooltip or in an app's own
wrapper, which then sat beside the buttons as a count does. With no
buttons it sits on their axis all the same (`-mr-1`, as they are pulled).
In the last button's place it is the 24px slot at `right-1`, where the
buttons' box ends, gone while they show — on a screen with no hover, then,
always.
*/
const trailingClasses = !acts
? 'has-[[data-unread]]:-mr-1'
: showActions === 'hover' &&
'has-[[data-unread]]:pointer-events-none has-[[data-unread]]:absolute has-[[data-unread]]:inset-y-0 has-[[data-unread]]:right-1 has-[[data-unread]]:w-6 has-[[data-unread]]:justify-center has-[[data-unread]]:transition-opacity group-hover:has-[[data-unread]]:opacity-0 group-has-[:focus-visible]:has-[[data-unread]]:opacity-0 group-has-[[data-section-menu]_[aria-expanded=true]]:has-[[data-unread]]:opacity-0 [@media(hover:none)]:has-[[data-unread]]:opacity-0'
/*
The buttons show under the pointer, while the keyboard is anywhere in the
row — the title's toggle included, as a NavItem's ⋮ shows once its link
has keyboard focus; it was only one of the buttons themselves — and while
the menu is open. `aria-expanded` is asked of the buttons' own box: the
toggle says it too, and an open section would keep them for good.
A screen with no hover (a phone, a tablet) shows them always: nothing
there can point at the row, so hidden they could never be reached.
*/
const reveal = showActions === 'hover' && 'opacity-0 transition-opacity group-hover:opacity-100 group-has-[:focus-visible]:opacity-100 has-[[aria-expanded=true]]:opacity-100 [@media(hover:none)]:opacity-100'
const titleElement = useRender({
render: render ?? (chevron ? : ),
props: {
// `text-left`: a button centres its text. `h-full` and `flex-1`: the
// whole row up to the actions is the hit target, as it was when the
// row itself carried the click.
// An icon is a row's: NavItem's 8px after it.
className: cn('flex h-full min-w-0 flex-1 items-center text-left', row || chevron || icon != null ? 'gap-2' : 'gap-1'),
children: (
<>
{icon != null ? (
{icon}
{chevron && (