// Shared UI components and utilities
const { useState, useEffect, useMemo, useRef, useCallback, createContext, useContext, Fragment } = React;
// ---------- Icon helper (Lucide via font) ----------
const Icon = ({ name, size = 16, style, className = '' }) => (
);
// ---------- Button ----------
const Btn = ({ kind = 'primary', size = 'md', icon, iconRight, children, onClick, disabled, type = 'button', style, full }) => {
const base = {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 8,
fontWeight: 500, letterSpacing: '-0.005em',
borderRadius: 999, transition: 'all .15s ease',
cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1,
whiteSpace: 'nowrap', border: '1px solid transparent',
width: full ? '100%' : 'auto',
};
const sizes = {
sm: { padding: '6px 12px', fontSize: 13 },
md: { padding: '10px 18px', fontSize: 14 },
lg: { padding: '14px 24px', fontSize: 15 },
};
const kinds = {
primary: { background: 'var(--accent)', color: 'white' },
dark: { background: 'var(--green-deep)', color: '#F5EEDE' },
ghost: { background: 'transparent', color: 'var(--ink)', border: '1px solid #dcd5c5' },
ghostDark: { background: 'transparent', color: '#F5EEDE', border: '1px solid rgba(245,238,222,0.3)' },
soft: { background: 'rgba(232,93,47,0.12)', color: 'var(--accent)' },
link: { background: 'transparent', color: 'var(--accent)', padding: 0 },
};
return (
);
};
// ---------- Badge / pill ----------
const Pill = ({ tone = 'neutral', children, icon, style }) => {
const tones = {
neutral: { bg: '#EFEADA', fg: '#5a5544' },
green: { bg: 'rgba(46,92,66,0.12)', fg: 'var(--green)' },
orange: { bg: 'rgba(232,93,47,0.14)', fg: '#B8431F' },
blue: { bg: 'rgba(27,73,101,0.1)', fg: '#1B4965' },
warn: { bg: '#F8E7C8', fg: '#8A5A1C' },
red: { bg: '#F7D9D1', fg: '#9A3318' },
white: { bg: 'rgba(255,255,255,0.9)', fg: 'var(--ink)' },
dark: { bg: 'rgba(26,31,28,0.88)', fg: '#fff' },
};
const t = tones[tone] || tones.neutral;
return (
{icon && }
{children}
);
};
// ---------- Text input ----------
const Input = ({ label, value, onChange, placeholder, type = 'text', hint, required, suffix, prefix, style }) => (
);
const Textarea = ({ label, value, onChange, placeholder, rows = 4, hint, required, style }) => (
);
const Select = ({ label, value, onChange, options, required, style }) => (
);
const Toggle = ({ value, onChange, label, hint }) => (
);
// ---------- Card (placeholder image with ken-burns-feel) ----------
const Img = ({ src, alt = '', style, ratio, className }) => {
const [loaded, setLoaded] = useState(false);
return (
{!loaded &&
}

setLoaded(true)}
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', opacity: loaded ? 1 : 0, transition: 'opacity .4s' }} />
);
};
// ---------- Modal ----------
const Modal = ({ open, onClose, children, width = 600, title, subtitle }) => {
useEffect(() => {
if (!open) return;
const onKey = e => e.key === 'Escape' && onClose && onClose();
window.addEventListener('keydown', onKey);
document.body.style.overflow = 'hidden';
return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = ''; };
}, [open, onClose]);
if (!open) return null;
return (
e.stopPropagation()} style={{ background: 'white', borderRadius: 16, width: '100%', maxWidth: width, maxHeight: '90vh', overflow: 'auto', animation: 'slideUp .2s ease', boxShadow: '0 20px 60px rgba(0,0,0,0.3)' }}>
{(title || subtitle) && (
{title &&
{title}
}
{subtitle &&
{subtitle}
}
)}
{children}
);
};
// ---------- Empty / toast ----------
const useToast = () => {
const [toast, setToast] = useState(null);
const show = (message, tone = 'success') => {
setToast({ message, tone, id: Date.now() });
setTimeout(() => setToast(null), 3000);
};
const node = toast && (
{toast.message}
);
return { show, node };
};
// ---------- Slug helper ----------
const slugify = s => (s || '').toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
// ---------- Format ----------
const fmtMYR = n => 'RM ' + n.toLocaleString('en-MY', { maximumFractionDigits: 0 });
const fmtDate = d => {
try {
return new Date(d).toLocaleDateString('en-MY', { day: '2-digit', month: 'short', year: 'numeric' });
} catch { return d; }
};
const relTime = d => {
const diff = (Date.now() - new Date(d).getTime()) / 1000;
if (diff < 60) return 'just now';
if (diff < 3600) return Math.floor(diff/60) + ' min ago';
if (diff < 86400) return Math.floor(diff/3600) + 'h ago';
if (diff < 86400*7) return Math.floor(diff/86400) + 'd ago';
return fmtDate(d);
};
Object.assign(window, {
Icon, Btn, Pill, Input, Textarea, Select, Toggle, Img, Modal, useToast,
slugify, fmtMYR, fmtDate, relTime,
useState, useEffect, useMemo, useRef, useCallback, createContext, useContext, Fragment,
});