nsfwapp/frontend/src/components/ui/ButtonGroup.tsx
2026-02-25 15:00:33 +01:00

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>
)
}