geraete/components/ui/Alerts.tsx
2025-12-05 13:53:29 +01:00

240 lines
7.3 KiB
TypeScript

'use client';
import * as React from 'react';
import clsx from 'clsx';
import {
InformationCircleIcon,
CheckCircleIcon,
ExclamationTriangleIcon,
XCircleIcon,
XMarkIcon,
} from '@heroicons/react/20/solid';
export type AlertTone = 'info' | 'success' | 'warning' | 'error';
export interface AlertProps {
/** Farbschema / Typ des Alerts */
tone?: AlertTone;
/** Überschrift (z.B. "Attention needed") */
title?: React.ReactNode;
/** Beschreibungstext (als string oder JSX) */
description?: React.ReactNode;
/** Bullet-List-Einträge wie im "With list"-Beispiel */
listItems?: React.ReactNode[];
/** Eigene Icon-Komponente (null = Icon komplett ausblenden) */
icon?: React.ReactNode | null;
/** Bereich für Buttons / Actions unter dem Text */
actions?: React.ReactNode;
/** Inhalt rechts (z.B. ein "Details →"-Link) */
rightContent?: React.ReactNode;
/** Linker Accent-Border (statt Outlines im Dark Mode) */
accent?: boolean;
/** Wenn gesetzt, wird ein Dismiss-X angezeigt und dieser Handler aufgerufen */
onDismiss?: () => void;
className?: string;
}
const toneConfig: Record<
AlertTone,
{
bg: string;
outline: string;
accentBorder: string;
title: string;
text: string;
icon: string;
dismissBtn: string;
}
> = {
info: {
bg: 'bg-blue-50 dark:bg-blue-500/10',
outline: 'dark:outline dark:outline-blue-500/20',
accentBorder: 'border-l-4 border-blue-400 dark:border-blue-500',
title: 'text-blue-800 dark:text-blue-200',
text: 'text-blue-700 dark:text-blue-300',
icon: 'text-blue-400 dark:text-blue-300',
dismissBtn:
'inline-flex rounded-md bg-blue-50 p-1.5 text-blue-500 hover:bg-blue-100 ' +
'focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 focus-visible:ring-offset-blue-50 focus-visible:outline-none ' +
'dark:bg-transparent dark:text-blue-400 dark:hover:bg-blue-500/10 ' +
'dark:focus-visible:ring-blue-500 dark:focus-visible:ring-offset-1 dark:focus-visible:ring-offset-blue-900',
},
success: {
bg: 'bg-green-50 dark:bg-green-500/10',
outline: 'dark:outline dark:outline-green-500/20',
accentBorder: 'border-l-4 border-green-400 dark:border-green-500',
title: 'text-green-800 dark:text-green-200',
text: 'text-green-700 dark:text-green-200/85',
icon: 'text-green-400 dark:text-green-300',
dismissBtn:
'inline-flex rounded-md bg-green-50 p-1.5 text-green-500 hover:bg-green-100 ' +
'focus-visible:ring-2 focus-visible:ring-green-600 focus-visible:ring-offset-2 focus-visible:ring-offset-green-50 focus-visible:outline-none ' +
'dark:bg-transparent dark:text-green-400 dark:hover:bg-green-500/10 ' +
'dark:focus-visible:ring-green-500 dark:focus-visible:ring-offset-1 dark:focus-visible:ring-offset-green-900',
},
warning: {
bg: 'bg-yellow-50 dark:bg-yellow-500/10',
outline: 'dark:outline dark:outline-yellow-500/15',
accentBorder: 'border-l-4 border-yellow-400 dark:border-yellow-500',
title: 'text-yellow-800 dark:text-yellow-100',
text: 'text-yellow-700 dark:text-yellow-100/80',
icon: 'text-yellow-400 dark:text-yellow-300',
dismissBtn:
'inline-flex rounded-md bg-yellow-50 p-1.5 text-yellow-500 hover:bg-yellow-100 ' +
'focus-visible:ring-2 focus-visible:ring-yellow-600 focus-visible:ring-offset-2 focus-visible:ring-offset-yellow-50 focus-visible:outline-none ' +
'dark:bg-transparent dark:text-yellow-400 dark:hover:bg-yellow-500/10 ' +
'dark:focus-visible:ring-yellow-500 dark:focus-visible:ring-offset-1 dark:focus-visible:ring-offset-yellow-900',
},
error: {
bg: 'bg-red-50 dark:bg-red-500/15',
outline: 'dark:outline dark:outline-red-500/25',
accentBorder: 'border-l-4 border-red-400 dark:border-red-500',
title: 'text-red-800 dark:text-red-200',
text: 'text-red-700 dark:text-red-200/80',
icon: 'text-red-400 dark:text-red-400',
dismissBtn:
'inline-flex rounded-md bg-red-50 p-1.5 text-red-500 hover:bg-red-100 ' +
'focus-visible:ring-2 focus-visible:ring-red-600 focus-visible:ring-offset-2 focus-visible:ring-offset-red-50 focus-visible:outline-none ' +
'dark:bg-transparent dark:text-red-400 dark:hover:bg-red-500/10 ' +
'dark:focus-visible:ring-red-500 dark:focus-visible:ring-offset-1 dark:focus-visible:ring-offset-red-900',
},
};
function getDefaultIcon(tone: AlertTone) {
switch (tone) {
case 'info':
return (
<InformationCircleIcon
aria-hidden="true"
className="size-5"
/>
);
case 'success':
return (
<CheckCircleIcon
aria-hidden="true"
className="size-5"
/>
);
case 'warning':
return (
<ExclamationTriangleIcon
aria-hidden="true"
className="size-5"
/>
);
case 'error':
return (
<XCircleIcon
aria-hidden="true"
className="size-5"
/>
);
}
}
function Alerts({
tone = 'info',
title,
description,
listItems,
icon,
actions,
rightContent,
accent = false,
onDismiss,
className,
}: AlertProps) {
const cfg = toneConfig[tone];
const hasList = !!listItems && listItems.length > 0;
const showIcon = icon !== null;
return (
<div
role="alert"
className={clsx(
'p-4',
cfg.bg,
accent ? cfg.accentBorder : 'rounded-md ' + cfg.outline,
accent && 'rounded-none',
className,
)}
>
<div className="flex">
{showIcon && (
<div className="shrink-0">
<span className={cfg.icon}>
{icon ?? getDefaultIcon(tone)}
</span>
</div>
)}
<div
className={clsx(
'ml-3 flex-1',
rightContent || onDismiss
? 'md:flex md:justify-between md:items-start'
: undefined,
)}
>
<div className="text-sm">
{title && (
<h3 className={clsx('font-medium', cfg.title)}>{title}</h3>
)}
{description && (
<div className={clsx('mt-2', cfg.text)}>
{typeof description === 'string' ? (
<p>{description}</p>
) : (
description
)}
</div>
)}
{hasList && (
<div className={clsx(title || description ? 'mt-2' : undefined, cfg.text)}>
<ul role="list" className="list-disc space-y-1 pl-5">
{listItems!.map((item, idx) => (
<li key={idx}>{item}</li>
))}
</ul>
</div>
)}
{actions && <div className="mt-4">{actions}</div>}
</div>
{(rightContent || onDismiss) && (
<div className="mt-3 md:mt-0 md:ml-6 flex items-start gap-2">
{rightContent && <div className="text-sm">{rightContent}</div>}
{onDismiss && (
<button
type="button"
onClick={onDismiss}
className={cfg.dismissBtn}
>
<span className="sr-only">Dismiss</span>
<XMarkIcon aria-hidden="true" className="size-5" />
</button>
)}
</div>
)}
</div>
</div>
</div>
);
}
export default Alerts;