Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | 3x 76x 152x 152x 7x | // SPDX-License-Identifier: GPL-3.0-only
// Copyright (C) 2026 Max Boksem. See NOTICE for additional terms under GPLv3 section 7.
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
export interface SegmentedOption<T> {
value: T;
label: string;
icon?: ReactNode;
}
interface SegmentedControlProps<T> {
value: T;
onChange: (value: T) => void;
options: SegmentedOption<T>[];
/** Stretches the options to share the full width instead of sizing to their labels. */
fill?: boolean;
className?: string;
"aria-label"?: string;
}
/** A row of buttons where exactly one is chosen, like Off and On. */
export const SegmentedControl = <T extends string | number | boolean>({
value,
onChange,
options,
fill = false,
className,
"aria-label": ariaLabel,
}: SegmentedControlProps<T>) => (
<div
role="group"
aria-label={ariaLabel}
className={cn(
"flex bg-secondary rounded-lg",
fill ? "gap-1 p-1" : "gap-0.5 p-0.5",
className
)}
>
{options.map((option) => {
const selected = option.value === value;
return (
<button
key={String(option.value)}
type="button"
aria-pressed={selected}
onClick={() => onChange(option.value)}
className={cn(
"rounded-md text-xs font-medium transition-colors",
fill
? "flex-1 flex items-center justify-center gap-1.5 py-1.5 px-3"
: "px-3 py-1",
selected
? "bg-background text-foreground shadow-xs"
: "text-muted-foreground hover:text-foreground"
)}
>
{option.icon}
{option.label}
</button>
);
})}
</div>
);
|