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