geraete/components/ui/Button.tsx
2025-11-14 11:28:24 +01:00

139 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// /components/ui/Button.tsx
// src/components/Button.tsx
import * as React from 'react';
import clsx from 'clsx';
export type ButtonVariant = 'primary' | 'secondary' | 'soft';
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
export type ButtonShape = 'default' | 'pill' | 'circle';
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
shape?: ButtonShape; // "default" (normal), "pill" (rounded-full), "circle" (nur Icon)
icon?: React.ReactNode;
iconPosition?: 'leading' | 'trailing';
fullWidth?: boolean;
}
const baseClasses =
'inline-flex items-center justify-center font-semibold shadow-xs ' +
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:focus-visible:outline-indigo-500 ' +
'disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-150';
// Farben / Styles wie in deinen Beispielen
const variantClasses: Record<ButtonVariant, string> = {
primary:
'bg-indigo-600 text-white hover:bg-indigo-500 ' +
'dark:bg-indigo-500 dark:text-white dark:hover:bg-indigo-400 dark:shadow-none',
secondary:
'bg-white text-gray-900 inset-ring inset-ring-gray-300 hover:bg-gray-50 ' +
'dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20',
soft:
'bg-indigo-50 text-indigo-600 hover:bg-indigo-100 ' +
'dark:bg-indigo-500/20 dark:text-indigo-400 dark:shadow-none dark:hover:bg-indigo-500/30',
};
// Größen wie in deinen Snippets (rectangular)
const sizeClasses: Record<ButtonSize, string> = {
xs: 'rounded-sm px-2 py-1 text-xs',
sm: 'rounded-sm px-2 py-1 text-sm',
md: 'rounded-md px-2.5 py-1.5 text-sm',
lg: 'rounded-md px-3 py-2 text-sm',
xl: 'rounded-md px-3.5 py-2.5 text-sm',
};
// Pill / "rounded primary/secondary buttons" Größen
const pillSizeClasses: Record<ButtonSize, string> = {
xs: 'rounded-full px-2.5 py-1 text-xs',
sm: 'rounded-full px-2.5 py-1 text-sm',
md: 'rounded-full px-3 py-1.5 text-sm',
lg: 'rounded-full px-3.5 py-2 text-sm',
xl: 'rounded-full px-4 py-2.5 text-sm',
};
// Circular Buttons nur Icon
const circleSizeClasses: Record<ButtonSize, string> = {
xs: 'rounded-full p-1',
sm: 'rounded-full p-1.5',
md: 'rounded-full p-2',
lg: 'rounded-full p-2',
xl: 'rounded-full p-2.5',
};
function getSizeClasses(size: ButtonSize, shape: ButtonShape): string {
switch (shape) {
case 'pill':
return pillSizeClasses[size];
case 'circle':
return circleSizeClasses[size];
default:
return sizeClasses[size];
}
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = 'primary',
size = 'md',
shape = 'default',
icon,
iconPosition = 'leading',
fullWidth,
className,
children,
type = 'button',
...props
},
ref
) => {
const hasIcon = !!icon;
const hasLabel = React.Children.count(children) > 0;
const gapClasses =
hasIcon && hasLabel
? size === 'xl'
? 'gap-x-2'
: 'gap-x-1.5'
: '';
return (
<button
ref={ref}
type={type}
className={clsx(
baseClasses,
variantClasses[variant],
getSizeClasses(size, shape),
gapClasses,
fullWidth && 'w-full',
className
)}
{...props}
>
{hasIcon && iconPosition === 'leading' && (
<span aria-hidden="true">{icon}</span>
)}
{hasLabel ? (
children
) : hasIcon ? (
// Für reine Icon-Buttons: Text nur für Screenreader, per aria-label steuerbar
<span className="sr-only">{props['aria-label'] ?? 'Button'}</span>
) : null}
{hasIcon && iconPosition === 'trailing' && (
<span aria-hidden="true">{icon}</span>
)}
</button>
);
}
);
Button.displayName = 'Button';
export default Button;