// Desktop UI primitives — modals (Sheet→centered dialog), toasts, form fields, // segmented controls, search, popover menus. Shared by all desktop screens. // ─── Modal dialog (desktop replacement for the mobile bottom Sheet) ─── function Sheet({ open, onClose, children, width = 520 }) { const t = useTheme(); const [mounted, setMounted] = useState(open); const [shown, setShown] = useState(false); useEffect(() => { if (open) { setMounted(true); requestAnimationFrame(() => setShown(true)); } else { setShown(false); const id = setTimeout(() => setMounted(false), 200); return () => clearTimeout(id); } }, [open]); useEffect(() => { const onKey = (e) => { if (e.key === 'Escape' && open) onClose && onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [open, onClose]); if (!mounted) return null; return (
e.stopPropagation()} style={{ width, maxWidth: '100%', maxHeight: '88%', overflow: 'auto', background: t.sheetBg, borderRadius: 16, boxShadow: '0 20px 70px rgba(0,0,0,0.4), 0 0 0 0.5px ' + t.sep, transform: shown ? 'scale(1) translateY(0)' : 'scale(0.96) translateY(8px)', opacity: shown ? 1 : 0, transition: 'transform 0.22s cubic-bezier(0.2,0.8,0.2,1), opacity 0.18s', paddingTop: 18, }}>{children}
); } // ─── Toast host ─── const ToastContext = createContext(() => {}); const useToast = () => useContext(ToastContext); function ToastHost({ children }) { const t = useTheme(); const [toasts, setToasts] = useState([]); const push = (msg) => { const id = Date.now() + Math.random(); setToasts((p) => [...p, { id, msg }]); setTimeout(() => setToasts((p) => p.filter((x) => x.id !== id)), 2400); }; return ( {children}
{toasts.map((x) => (
{x.msg}
))}
); } // ─── Form helpers ─── const inp = (t) => ({ width: '100%', padding: '11px 13px', borderRadius: 10, border: `1px solid ${t.inputBorder}`, background: t.inputBg, fontSize: 14, color: t.text, outline: 'none', fontFamily: 'inherit', boxSizing: 'border-box', transition: 'border 0.15s, box-shadow 0.15s', }); function Field({ label, required, children, hint, style }) { const t = useTheme(); return (
{label && (
{label}{required && *}
)} {children} {hint &&
{hint}
}
); } const FormGroup = Field; function TextInput(props) { const t = useTheme(); const [focus, setFocus] = useState(false); const { style, ...rest } = props; return { setFocus(true); props.onFocus && props.onFocus(e); }} onBlur={(e) => { setFocus(false); props.onBlur && props.onBlur(e); }} style={{ ...inp(t), ...(focus ? { borderColor: t.accent, boxShadow: `0 0 0 3px ${t.accentSoft}` } : {}), ...style }}/>; } // ─── Segmented control ─── function Segmented({ options, value, onChange, size = 'md', style }) { const t = useTheme(); const pad = size === 'sm' ? '5px 12px' : '7px 16px'; const fs = size === 'sm' ? 12 : 13; return (
{options.map(([id, label, count]) => ( onChange(id)} scale={0.97}>
{label} {count != null && count > 0 && ( {count} )}
))}
); } // ─── Toolbar (sits under titlebar in each screen) ─── function Toolbar({ title, subtitle, children }) { const t = useTheme(); return (
{title}
{subtitle &&
{subtitle}
}
{children}
); } // ─── Search field (toolbar) ─── function SearchField({ value, onChange, placeholder = 'Suchen', width = 220 }) { const t = useTheme(); const [focus, setFocus] = useState(false); return (
onChange(e.target.value)} placeholder={placeholder} onFocus={() => setFocus(true)} onBlur={() => setFocus(false)} style={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', fontSize: 13.5, color: t.text, fontFamily: 'inherit', minWidth: 0 }}/> {value && onChange('')} scale={0.8}>}
); } // ─── Icon button (round, toolbar) ─── function IconBtn({ icon, onClick, active, color, title, badge, size = 34 }) { const t = useTheme(); const [hover, setHover] = useState(false); return (
setHover(true)} onPointerLeave={() => setHover(false)} style={{ width: size, height: size, borderRadius: 9, position: 'relative', background: active ? t.accentSoft : (hover ? t.chip : 'transparent'), display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'background 0.15s', }}> {badge > 0 && (
{badge}
)}
); } // ─── Scroll area (page body) ─── function Scroll({ children, style, padding = 28 }) { return
{children}
; } // ─── Section header within a page ─── function SectionTitle({ children, action, onAction, style }) { const t = useTheme(); return (
{children}
{action &&
{action}
}
); } // ─── Detail row (label/value) ─── function DRow({ label, value, mono, color, multiline }) { const t = useTheme(); return (
{label}
{value}
); } // ─── Empty state ─── function Empty({ icon, title, sub, action, onAction }) { const t = useTheme(); return (
{title}
{sub &&
{sub}
} {action &&
}
); } // ─── Status dot+label ─── function StatusPill({ status }) { const m = statusMeta(status); return ( {m.label} ); } // ─── Confirm dialog ─── function Confirm({ open, title, message, confirmLabel = 'Löschen', danger = true, onConfirm, onClose }) { const t = useTheme(); return (
{title}
{message &&
{message}
}
Abbrechen
{ onConfirm(); onClose(); }} scale={0.97} style={{ flex: 1 }}>
{confirmLabel}
); } Object.assign(window, { Sheet, ToastContext, useToast, ToastHost, inp, Field, FormGroup, TextInput, Segmented, Toolbar, SearchField, IconBtn, Scroll, SectionTitle, DRow, Empty, StatusPill, Confirm, });