/* Shared UI pieces. Icons are inline SVG paths so the app never fetches an icon font (and works with no network on the server). */ const ICONS = { board: 'M4 4h6v16H4zM14 4h6v9h-6z', list: 'M4 6h16M4 12h16M4 18h16', users: 'M16 20v-1a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v1M9.5 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7M21 20v-1a4 4 0 0 0-3-3.87M16 4.13a4 4 0 0 1 0 7.75', building: 'M4 21V5a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v16M15 10h4a1 1 0 0 1 1 1v10M3 21h18M8 8h3M8 12h3M8 16h3', video: 'M15 10l6-3v10l-6-3zM3 7a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z', image: 'M3 5h18v14H3zM3 15l5-5 4 4 3-3 6 6', plus: 'M12 5v14M5 12h14', bell: 'M18 8a6 6 0 1 0-12 0c0 7-3 8-3 8h18s-3-1-3-8M13.7 21a2 2 0 0 1-3.4 0', back: 'M19 12H5M12 19l-7-7 7-7', check: 'M20 6L9 17l-5-5', x: 'M18 6L6 18M6 6l12 12', upload: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12', external: 'M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3', clock: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 6v6l4 2', settings: 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z', comment: 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z', menu: 'M3 12h18M3 6h18M3 18h18', trash: 'M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2M19 6l-1 14a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1L5 6', drive: 'M8 3h8l6 10-4 7H6l-4-7zM8 3l-6 10M16 3l-6 10-4 7', pen: 'M12 19l7-7 3 3-7 7-3-3zM18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5zM2 2l7.586 7.586', calendar: 'M8 2v4M16 2v4M3 10h18M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z', target: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 18a6 6 0 1 0 0-12 6 6 0 0 0 0 12zM12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4z', }; function Icon({ name, size = 17, color = 'currentColor', strokeWidth = 1.8 }) { const d = ICONS[name]; if (!d) return null; return ( ); } function Avatar({ user, size }) { if (!user) return
; return (
{initials(user.name || user.email)}
); } function StatusPill({ status }) { const color = STATUS_COLORS[status] || 'var(--text-dim)'; return ( {STATUS_SHORT[status] || status} ); } function EmptyState({ title, hint, action }) { return (

{title}

{hint &&

{hint}

} {action}
); } function Modal({ title, onClose, children, footer, width }) { React.useEffect(() => { const onKey = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onClose]); return (
{ if (e.target === e.currentTarget) onClose(); }}>
{title}
{children}
{footer &&
{footer}
}
); } /* Toasts: a module-level queue so any screen can call toast() without props. */ let _pushToast = () => {}; function toast(message, kind) { _pushToast({ id: Math.random(), message, kind }); } function ToastHost() { const [items, setItems] = React.useState([]); React.useEffect(() => { _pushToast = (t) => { setItems((list) => [...list, t]); setTimeout(() => setItems((list) => list.filter((i) => i.id !== t.id)), 4200); }; }, []); return (
{items.map((t) =>
{t.message}
)}
); } /* Loads the reference data every screen needs (team + clients) once. */ function useDirectory() { const [users, setUsers] = React.useState([]); const [clients, setClients] = React.useState([]); const reload = React.useCallback(() => { Promise.all([api.get('/api/users'), api.get('/api/clients')]) .then(([u, c]) => { setUsers(u); setClients(c); }) .catch((e) => toast(e.message, 'error')); }, []); React.useEffect(reload, [reload]); const byId = React.useMemo(() => { const map = {}; users.forEach((u) => { map[u.id] = u; }); clients.forEach((c) => { map[c.id] = c; }); return map; }, [users, clients]); return { users, clients, byId, reload }; }