import type { ReactNode } from "react" import { PanelTabs } from "./PanelTabs.tsx" import { RoutedTabs } from "./RoutedTabs.tsx" import type { TabItem, TabLinkItem, TabsActivation, TabsOrientation, } from "./tabItems.ts" type TabsCommonProps = { className?: string /** The tab bar's accessible name. Required. */ label: string orientation?: TabsOrientation } export type TabsPanelProps = TabsCommonProps & { /** **Initial** only. Charcuterie owns it from then on. */ activeKey?: string /** * `automatic` — the ARIA Authoring Practices default — shows a * panel as soon as the arrow keys reach its tab. `manual` moves * focus only, and Enter or Space commits. * * The distinction exists because **focus is not selection**, and * it is the entire reason `RovingFocus` is its own state kind. A * tab whose panel costs a network request wants `manual`, and * the two modes differ by one line below — because two kinds are * composed rather than one kind doing both jobs badly. */ activation?: TabsActivation onChange?: (activeKey: string | null) => void tabs: readonly TabItem[] } export type TabsLinkProps = TabsCommonProps & { /** * The address the app is at — usually `location.pathname`. The tab * whose `href` matches wears `aria-current="page"`. * * **Required, and it is the discriminant.** `Nav` leaves the * equivalent optional because a top-level bar is honest about * naming nothing on a screen it does not contain. A routed tab bar * is drawn *inside* the section it divides, so "none of these" is * not a state it has; an app that wants it passes a path that * matches no tab. */ activeHref: string tabs: readonly TabLinkItem[] } /** * The union splits on `activeHref`, which only the routed member * has. `tabs` alone could not carry the split — TypeScript does not * narrow a union by the shape of an array's elements, so a mixed bar * would type-check and then render half a navigation. */ export type TabsProps = TabsLinkProps | TabsPanelProps const getIsRoutedProps = ( props: TabsProps, ): props is TabsLinkProps => "activeHref" in props /** * One choice out of a handful, all of them on screen at once — and * the choice is either a **panel** this component reveals or a * **place** the router goes to. * * ```tsx * // A section of this screen. Nothing outside knows about it. * * * // A section that has an address. * * * ``` * * ## Which one, and the question is not "how does it look" * * **Does this section have an address?** If reloading the page, or * opening it in a second tab, or pressing Back has to land the * reader back on the same section, it is routed and every tab is an * ``. If the section is a disclosure inside one screen — * Settings' groups, a card's detail — it is a panel and every tab is * a `