162 lines
4.8 KiB
TypeScript
162 lines
4.8 KiB
TypeScript
// components/ui/Badge.tsx
|
|
'use client';
|
|
|
|
import clsx from 'clsx';
|
|
import * as React from 'react';
|
|
|
|
type BadgeTone =
|
|
| 'gray'
|
|
| 'red'
|
|
| 'yellow'
|
|
| 'green'
|
|
| 'blue'
|
|
| 'indigo'
|
|
| 'purple'
|
|
| 'pink';
|
|
|
|
type BadgeVariant = 'border' | 'flat';
|
|
type BadgeShape = 'rounded' | 'pill';
|
|
type BadgeSize = 'sm' | 'md';
|
|
|
|
export type BadgeProps = {
|
|
children: React.ReactNode;
|
|
tone?: BadgeTone;
|
|
variant?: BadgeVariant;
|
|
shape?: BadgeShape;
|
|
size?: BadgeSize;
|
|
/** Zeigt vorne einen farbigen Punkt an */
|
|
dot?: boolean;
|
|
/** Wenn gesetzt, wird rechts ein Remove-Button angezeigt */
|
|
onRemove?: () => void;
|
|
className?: string;
|
|
};
|
|
|
|
/* ───────── Style-Mappings ───────── */
|
|
|
|
const baseClasses =
|
|
'inline-flex items-center text-xs font-medium';
|
|
|
|
const sizeClasses: Record<BadgeSize, string> = {
|
|
md: 'px-2 py-1',
|
|
sm: 'px-1.5 py-0.5',
|
|
};
|
|
|
|
const shapeClasses: Record<BadgeShape, string> = {
|
|
rounded: 'rounded-md',
|
|
pill: 'rounded-full',
|
|
};
|
|
|
|
const borderToneClasses: Record<BadgeTone, string> = {
|
|
gray:
|
|
'bg-gray-50 text-gray-600 inset-ring inset-ring-gray-500/10 dark:bg-gray-400/10 dark:text-gray-400 dark:inset-ring-gray-400/20',
|
|
red:
|
|
'bg-red-50 text-red-700 inset-ring inset-ring-red-600/10 dark:bg-red-400/10 dark:text-red-400 dark:inset-ring-red-400/20',
|
|
yellow:
|
|
'bg-yellow-50 text-yellow-800 inset-ring inset-ring-yellow-600/20 dark:bg-yellow-400/10 dark:text-yellow-500 dark:inset-ring-yellow-400/20',
|
|
green:
|
|
'bg-green-50 text-green-700 inset-ring inset-ring-green-600/20 dark:bg-green-400/10 dark:text-green-400 dark:inset-ring-green-500/20',
|
|
blue:
|
|
'bg-blue-50 text-blue-700 inset-ring inset-ring-blue-700/10 dark:bg-blue-400/10 dark:text-blue-400 dark:inset-ring-blue-400/30',
|
|
indigo:
|
|
'bg-indigo-50 text-indigo-700 inset-ring inset-ring-indigo-700/10 dark:bg-indigo-400/10 dark:text-indigo-400 dark:inset-ring-indigo-400/30',
|
|
purple:
|
|
'bg-purple-50 text-purple-700 inset-ring inset-ring-purple-700/10 dark:bg-purple-400/10 dark:text-purple-400 dark:inset-ring-purple-400/30',
|
|
pink:
|
|
'bg-pink-50 text-pink-700 inset-ring inset-ring-pink-700/10 dark:bg-pink-400/10 dark:text-pink-400 dark:inset-ring-pink-400/20',
|
|
};
|
|
|
|
const flatToneClasses: Record<BadgeTone, string> = {
|
|
gray:
|
|
'bg-gray-100 text-gray-600 dark:bg-gray-400/10 dark:text-gray-400',
|
|
red:
|
|
'bg-red-100 text-red-700 dark:bg-red-400/10 dark:text-red-400',
|
|
yellow:
|
|
'bg-yellow-100 text-yellow-800 dark:bg-yellow-400/10 dark:text-yellow-500',
|
|
green:
|
|
'bg-green-100 text-green-700 dark:bg-green-400/10 dark:text-green-400',
|
|
blue:
|
|
'bg-blue-100 text-blue-700 dark:bg-blue-400/10 dark:text-blue-400',
|
|
indigo:
|
|
'bg-indigo-100 text-indigo-700 dark:bg-indigo-400/10 dark:text-indigo-400',
|
|
purple:
|
|
'bg-purple-100 text-purple-700 dark:bg-purple-400/10 dark:text-purple-400',
|
|
pink:
|
|
'bg-pink-100 text-pink-700 dark:bg-pink-400/10 dark:text-pink-400',
|
|
};
|
|
|
|
const dotToneClasses: Record<BadgeTone, string> = {
|
|
gray: 'fill-gray-400',
|
|
red: 'fill-red-500 dark:fill-red-400',
|
|
yellow: 'fill-yellow-500 dark:fill-yellow-400',
|
|
green: 'fill-green-500 dark:fill-green-400',
|
|
blue: 'fill-blue-500 dark:fill-blue-400',
|
|
indigo: 'fill-indigo-500 dark:fill-indigo-400',
|
|
purple: 'fill-purple-500 dark:fill-purple-400',
|
|
pink: 'fill-pink-500 dark:fill-pink-400',
|
|
};
|
|
|
|
/* ───────── Component ───────── */
|
|
|
|
export default function Badge({
|
|
children,
|
|
tone = 'gray',
|
|
variant = 'border',
|
|
shape = 'rounded',
|
|
size = 'md',
|
|
dot = false,
|
|
onRemove,
|
|
className,
|
|
}: BadgeProps) {
|
|
const toneClasses =
|
|
variant === 'border'
|
|
? borderToneClasses[tone]
|
|
: flatToneClasses[tone];
|
|
|
|
const hasRemove = typeof onRemove === 'function';
|
|
|
|
return (
|
|
<span
|
|
className={clsx(
|
|
baseClasses,
|
|
sizeClasses[size],
|
|
shapeClasses[shape],
|
|
toneClasses,
|
|
dot && 'gap-x-1.5',
|
|
hasRemove && 'gap-x-0.5',
|
|
className,
|
|
)}
|
|
>
|
|
{/* Dot (optional) */}
|
|
{dot && (
|
|
<svg
|
|
viewBox="0 0 6 6"
|
|
aria-hidden="true"
|
|
className={clsx('size-1.5', dotToneClasses[tone])}
|
|
>
|
|
<circle r={3} cx={3} cy={3} />
|
|
</svg>
|
|
)}
|
|
|
|
<span>{children}</span>
|
|
|
|
{/* Remove-Button (optional) */}
|
|
{hasRemove && (
|
|
<button
|
|
type="button"
|
|
onClick={onRemove}
|
|
className="group relative -mr-1 size-3.5 rounded-xs hover:bg-black/5 dark:hover:bg-white/10"
|
|
>
|
|
<span className="sr-only">Entfernen</span>
|
|
<svg
|
|
viewBox="0 0 14 14"
|
|
className="size-3.5 stroke-current/60 group-hover:stroke-current/90"
|
|
>
|
|
<path d="M4 4l6 6m0-6l-6 6" />
|
|
</svg>
|
|
<span className="absolute -inset-1" />
|
|
</button>
|
|
)}
|
|
</span>
|
|
);
|
|
}
|