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