/* UI primitives — exported to window for cross-file use */
// ---- Icon (lucide) ----
function Icon({ name, size = 20, sw = 2, className = '', style }) {
const ref = React.useRef(null);
React.useEffect(() => {
const host = ref.current;
if (!host || !window.lucide) return;
const node = window.lucide.icons?.[name];
let svg;
if (node && window.lucide.createElement) {
svg = window.lucide.createElement(node);
}
if (!svg) { // fallback
host.innerHTML = ``;
window.lucide.createIcons({ nameAttr: 'data-lucide' });
svg = host.querySelector('svg');
if (!svg) return;
} else {
host.replaceChildren(svg);
}
svg.setAttribute('width', size);
svg.setAttribute('height', size);
svg.setAttribute('stroke-width', sw);
}, [name, size, sw]);
return ;
}
// ---- Status badge ----
function StatusBadge({ status, size = 'sm' }) {
const s = CK.STATUS[status]; if (!s) return null;
const pad = size === 'sm' ? 'text-[11px] px-2 py-0.5' : 'text-xs px-2.5 py-1';
return (
{s.th}
);
}
function DayStatusBadge({ status, size='sm' }) {
const s = CK.DAYSTATUS[status]; if (!s) return null;
const pad = size === 'sm' ? 'text-[11px] px-2 py-0.5' : 'text-xs px-2.5 py-1';
return {s.th};
}
// ---- Buttons ----
function Btn({ children, variant = 'primary', size = 'md', icon, iconRight, className = '', ...rest }) {
const sizes = { sm:'text-xs px-3 py-1.5 gap-1.5', md:'text-sm px-4 py-2.5 gap-2', lg:'text-base px-5 py-3 gap-2', xl:'text-base px-6 py-4 gap-2.5' };
const variants = {
primary: 'bg-brand-600 hover:bg-brand-700 text-white shadow-sm',
dark: 'bg-slate-900 hover:bg-slate-800 text-white',
outline: 'bg-white hover:bg-slate-50 text-slate-700 ring-1 ring-slate-300',
ghost: 'hover:bg-slate-100 text-slate-600',
softred: 'bg-brand-50 hover:bg-brand-100 text-brand-700 ring-1 ring-brand-200',
danger: 'bg-white hover:bg-brand-50 text-brand-700 ring-1 ring-brand-200',
};
return (
);
}
// ---- Card ----
function Card({ children, className = '', as: As = 'div', ...rest }) {
return {children};
}
// ---- Section title ----
function SectionTitle({ icon, children, hint }) {
return (
{icon && }
{children}
{hint && {hint}}
);
}
// ---- Form fields ----
function Field({ label, required, hint, children }) {
return (
);
}
const inputCls = 'w-full rounded-xl ring-1 ring-slate-300 bg-white px-3.5 py-2.5 text-[15px] font-light text-slate-800 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-brand-500';
function TextInput(props){ return ; }
function TextArea(props){ return ; }
function Select({ children, ...props }){
return (
);
}
// ---- Chip selector (single/multi) ----
function ChipGroup({ options, value, onChange, multi, getKey = o=>o.id, getLabel = o=>o.th, getIcon }) {
const isActive = (k) => multi ? (value||[]).includes(k) : value === k;
function toggle(k){
if (multi){ const set = new Set(value||[]); set.has(k)?set.delete(k):set.add(k); onChange([...set]); }
else onChange(k);
}
return (
{options.map(o => { const k = getKey(o); const a = isActive(k);
return (
);
})}
);
}
// ---- Stat card ----
function StatCard({ icon, label, value, sub, tone='slate', onClick }) {
const tones = {
slate:'text-slate-500 bg-slate-100', red:'text-brand-600 bg-brand-50', amber:'text-amber-600 bg-amber-50',
green:'text-green-600 bg-green-50', blue:'text-blue-600 bg-blue-50', violet:'text-violet-600 bg-violet-50',
};
return (
{sub && {sub}}
);
}
// ---- Avatar ----
function Avatar({ name, color='#dc2626', size=32 }) {
const initials = (name||'?').trim().split(' ').slice(0,1)[0].slice(0,2);
return {initials};
}
// ---- Modal / Sheet ----
function Modal({ open, onClose, children, title, maxW='max-w-lg' }) {
if (!open) return null;
return (
{title && (
{title}
)}
{children}
);
}
// ---- Empty state ----
function Empty({ icon='Inbox', title, sub }) {
return (
);
}
// ---- Image placeholder slot ----
function PhotoSlot({ label, filled, onClick, h=96 }) {
return (
);
}
Object.assign(window, {
Icon, StatusBadge, DayStatusBadge, Btn, Card, SectionTitle, Field, TextInput, TextArea,
Select, ChipGroup, StatCard, Avatar, Modal, Empty, PhotoSlot, inputCls,
});