'use client' import { cn } from '../../lib/cn' import type { IconComponent } from '../../lib/types' export interface ToggleGroupItem { value: T label: string icon?: IconComponent /** * Show the icon alone. The label is still required and still announced — an * icon-only toggle without one is a control nobody can name. */ iconOnly?: boolean disabled?: boolean } export type ToggleGroupSize = 'sm' | 'md' const SIZES: Record = { sm: { text: 'h-7 px-3 text-[12px]', square: 'h-7 w-7', icon: 14 }, md: { text: 'h-8 px-3.5 text-[13px]', square: 'h-8 w-8', icon: 15 }, } export interface ToggleGroupProps { /** The toggles, in order. */ items: ToggleGroupItem[] /** The values currently pressed. */ value: T[] /** Called with the full set of pressed values after each press. */ onValueChange: (value: T[]) => void /** * `multiple` lets any combination be on — bold and italic together. `single` * allows at most one, and pressing it again turns it off, which is what * separates this from SegmentedControl, where exactly one is always chosen. */ type?: 'single' | 'multiple' /** Accessible name for the set. */ label: string /** Control height: 28px or 32px. */ size?: ToggleGroupSize /** Merged last, so it wins. */ className?: string } /** * A row of on/off toggles. * * Each one is a real button with aria-pressed, so it is announced as a toggle * that is pressed or not — rather than as a radio or a checkbox dressed up as a * button. The pressed state takes the accent because it is the state the eye is * looking for; everything else stays quiet. */ export function ToggleGroup({ items, value, onValueChange, type = 'multiple', label, size = 'md', className, }: ToggleGroupProps) { const toggle = (item: T) => { const pressed = value.includes(item) if (type === 'single') onValueChange(pressed ? [] : [item]) else onValueChange(pressed ? value.filter((entry) => entry !== item) : [...value, item]) } return (
{items.map((item) => { const pressed = value.includes(item.value) const Icon = item.icon return ( ) })}
) }