107 lines
3.3 KiB
TypeScript
107 lines
3.3 KiB
TypeScript
// components/ui/ButtonGroup.tsx
|
|
'use client'
|
|
|
|
import * as React from 'react'
|
|
|
|
type Size = 'sm' | 'md' | 'lg'
|
|
|
|
export type ButtonGroupItem = {
|
|
id: string
|
|
label?: React.ReactNode // optional (für icon-only)
|
|
icon?: React.ReactNode
|
|
srLabel?: string // für icon-only (Screenreader)
|
|
disabled?: boolean
|
|
}
|
|
|
|
export type ButtonGroupProps = {
|
|
items: ButtonGroupItem[]
|
|
value: string
|
|
onChange: (id: string) => void
|
|
size?: Size
|
|
className?: string
|
|
ariaLabel?: string
|
|
}
|
|
|
|
function cn(...parts: Array<string | false | null | undefined>) {
|
|
return parts.filter(Boolean).join(' ')
|
|
}
|
|
|
|
const sizeMap: Record<Size, { btn: string; icon: string; iconOnly: string }> = {
|
|
sm: { btn: 'px-2.5 py-1.5 text-sm', icon: 'size-5', iconOnly: 'h-9 w-9' },
|
|
md: { btn: 'px-3 py-2 text-sm', icon: 'size-5', iconOnly: 'h-10 w-10' },
|
|
lg: { btn: 'px-3.5 py-2.5 text-sm', icon: 'size-5', iconOnly: 'h-11 w-11' },
|
|
}
|
|
|
|
export default function ButtonGroup({
|
|
items,
|
|
value,
|
|
onChange,
|
|
size = 'md',
|
|
className,
|
|
ariaLabel = 'Optionen',
|
|
}: ButtonGroupProps) {
|
|
const s = sizeMap[size]
|
|
|
|
return (
|
|
<span
|
|
className={cn(
|
|
'isolate inline-flex rounded-md shadow-xs dark:shadow-none ring-1 ring-gray-300 dark:ring-gray-700 overflow-hidden',
|
|
className
|
|
)}
|
|
role="group"
|
|
aria-label={ariaLabel}
|
|
>
|
|
{items.map((it, idx) => {
|
|
const active = it.id === value
|
|
const isFirst = idx === 0
|
|
const isLast = idx === items.length - 1
|
|
const iconOnly = !it.label && !!it.icon
|
|
|
|
return (
|
|
<button
|
|
key={it.id}
|
|
type="button"
|
|
disabled={it.disabled}
|
|
onClick={() => onChange(it.id)}
|
|
aria-pressed={active}
|
|
className={cn(
|
|
'relative inline-flex items-center justify-center font-semibold leading-none focus:z-10 transition-colors',
|
|
!isFirst && 'before:absolute before:left-0 before:top-0 before:bottom-0 before:w-px before:bg-gray-300 dark:before:bg-gray-700',
|
|
isFirst && 'rounded-l-md',
|
|
isLast && 'rounded-r-md',
|
|
|
|
// Base vs Active: gegenseitig ausschließen (wichtig, sonst gewinnt oft bg-white)
|
|
active
|
|
? 'bg-indigo-100 text-indigo-800 hover:bg-indigo-200 dark:bg-indigo-500/40 dark:text-indigo-100 dark:hover:bg-indigo-500/50'
|
|
: 'bg-white text-gray-900 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:hover:bg-white/20',
|
|
|
|
// Disabled
|
|
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
|
|
// Padding / Größe
|
|
iconOnly ? `p-0 ${s.iconOnly}` : s.btn
|
|
)}
|
|
title={typeof it.label === 'string' ? it.label : it.srLabel}
|
|
>
|
|
{iconOnly && it.srLabel ? <span className="sr-only">{it.srLabel}</span> : null}
|
|
|
|
{it.icon ? (
|
|
<span
|
|
className={cn(
|
|
'shrink-0',
|
|
iconOnly ? '' : '-ml-0.5',
|
|
active ? 'text-indigo-600 dark:text-indigo-200' : 'text-gray-400 dark:text-gray-500'
|
|
)}
|
|
>
|
|
{it.icon}
|
|
</span>
|
|
) : null}
|
|
|
|
{it.label ? <span className={it.icon ? 'ml-1.5' : ''}>{it.label}</span> : null}
|
|
</button>
|
|
)
|
|
})}
|
|
</span>
|
|
)
|
|
}
|