// Shared icons and small UI primitives const Icon = ({ name, size = 16, stroke = 1.75 }) => { const s = size; const props = { width: s, height: s, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: stroke, strokeLinecap: "round", strokeLinejoin: "round" }; switch (name) { case 'home': return ; case 'cart': return ; case 'return': return ; case 'refresh-cw': return ; case 'users': return ; case 'book': return ; case 'alert': return ; case 'search': return ; case 'plus': return ; case 'check': return ; case 'x': return ; case 'arrow-right': return ; case 'arrow-left': return ; case 'chevron-right': return ; case 'chevron-down': return ; case 'invoice': return ; case 'printer': return ; case 'trash': return ; case 'edit': return ; case 'filter': return ; case 'download': return ; case 'mail': return ; case 'log-out': return ; case 'euro': return ; case 'calendar': return ; case 'circle': return ; case 'list': return ; case 'settings': return ; case 'sparkle': return ; case 'archive': return ; case 'package': return ; default: return null; } }; const Avatar = ({ name, size = 32 }) => { const initials = name.split(' ').map(n => n[0]).join('').slice(0,2).toUpperCase(); // Stable color hash let h = 0; for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0; const hue = h % 360; return (
{initials}
); }; const Badge = ({ tone = 'slate', children, dot = false }) => { const tones = { slate: { bg: '#f1f5f9', fg: '#475569', dot: '#94a3b8' }, blue: { bg: '#eff6ff', fg: '#1d4ed8', dot: '#3b82f6' }, green: { bg: '#ecfdf5', fg: '#047857', dot: '#10b981' }, amber: { bg: '#fffbeb', fg: '#b45309', dot: '#f59e0b' }, red: { bg: '#fef2f2', fg: '#b91c1c', dot: '#ef4444' }, violet: { bg: '#f5f3ff', fg: '#6d28d9', dot: '#8b5cf6' }, }; const t = tones[tone] || tones.slate; return ( {dot && } {children} ); }; const Btn = ({ kind = 'secondary', children, icon, onClick, full, density = 'regular', accent = '#2563eb', disabled }) => { const padY = density === 'compact' ? 5 : (density === 'comfy' ? 9 : 7); const styles = { primary: { background: accent, color: '#fff', border: `1px solid ${accent}` }, secondary: { background: '#fff', color: '#0f172a', border: '1px solid #e2e8f0' }, ghost: { background: 'transparent', color: '#475569', border: '1px solid transparent' }, danger: { background: '#fff', color: '#b91c1c', border: '1px solid #fecaca' }, }; return ( ); }; const Card = ({ children, padding = 16, style = {} }) => (
{children}
); const SearchInput = ({ value, onChange, placeholder = 'Suchen…', autoFocus, kbd }) => (
onChange(e.target.value)} placeholder={placeholder} style={{ width: '100%', padding: '8px 12px 8px 34px', border: '1px solid #e2e8f0', borderRadius: 8, background: '#fff', fontSize: 13.5, fontFamily: 'inherit', outline: 'none', color: '#0f172a', }} /> {kbd && {kbd}}
); const Money = ({ value, mono = true }) => { const formatted = (value || 0).toFixed(2).replace('.', ',') + ' €'; return {formatted}; }; const _toastListeners = []; window.showToast = function(type, message, duration = 3500) { const id = Date.now() + Math.random(); _toastListeners.forEach(fn => fn({ id, type, message, duration })); }; function ToastItem({ toast, onRemove }) { const [visible, setVisible] = React.useState(false); React.useEffect(() => { requestAnimationFrame(() => setVisible(true)); const t = setTimeout(() => { setVisible(false); setTimeout(onRemove, 250); }, toast.duration); return () => clearTimeout(t); }, []); const colors = { success: { bg: '#f0fdf4', border: '#bbf7d0', icon: '#16a34a', text: '#15803d' }, error: { bg: '#fef2f2', border: '#fecaca', icon: '#dc2626', text: '#b91c1c' }, info: { bg: '#eff6ff', border: '#bfdbfe', icon: '#2563eb', text: '#1d4ed8' }, }; const c = colors[toast.type] || colors.info; const iconName = toast.type === 'success' ? 'check' : toast.type === 'error' ? 'alert' : 'circle'; return (
{toast.message}
); } function ToastContainer() { const [toasts, setToasts] = React.useState([]); React.useEffect(() => { const handler = (toast) => setToasts(prev => [...prev, toast]); _toastListeners.push(handler); return () => { const idx = _toastListeners.indexOf(handler); if (idx > -1) _toastListeners.splice(idx, 1); }; }, []); return (
{toasts.map(toast => ( setToasts(prev => prev.filter(t => t.id !== toast.id))} /> ))}
); } const _confirmListeners = []; window.showConfirm = function({ message, detail, confirmLabel = 'Bestätigen', cancelLabel = 'Abbrechen', danger = false, onConfirm, onCancel }) { _confirmListeners.forEach(fn => fn({ message, detail, confirmLabel, cancelLabel, danger, onConfirm, onCancel })); }; function GlobalConfirmDialog() { const [state, setState] = React.useState(null); React.useEffect(() => { const handler = (opts) => setState(opts); _confirmListeners.push(handler); return () => { const idx = _confirmListeners.indexOf(handler); if (idx > -1) _confirmListeners.splice(idx, 1); }; }, []); if (!state) return null; const handleConfirm = () => { const cb = state.onConfirm; setState(null); cb?.(); }; const handleCancel = () => { const cb = state.onCancel; setState(null); cb?.(); }; return (
e.stopPropagation()}>
{state.message}
{state.detail &&
{state.detail}
}
); } Object.assign(window, { Icon, Avatar, Badge, Btn, Card, SearchInput, Money, ToastContainer, GlobalConfirmDialog });