// Shared UI primitives for CredNX dashboard. // All colors source from CSS custom properties in colors_and_type.css // so the entire UI re-themes automatically when [data-theme] flips. // ——— Icons (Lucide-style, 1.75px stroke) ——— const I = { logout: (p={}) => , upload: (p={}) => , fileText: (p={}) => , users: (p={}) => , layers: (p={}) => , settings: (p={}) => , download: (p={}) => , check: (p={}) => , checkCircle: (p={}) => , x: (p={}) => , chevronDown: (p={}) => , chevronRight: (p={}) => , plus: (p={}) => , search: (p={}) => , arrowRight: (p={}) => , arrowLeft: (p={}) => , mail: (p={}) => , lock: (p={}) => , building: (p={}) => , trash: (p={}) => , refresh: (p={}) => , eye: (p={}) => , moreH: (p={}) => , bank: (p={}) => , creditCard: (p={}) => , receipt: (p={}) => , trendUp: (p={}) => , user: (p={}) => , help: (p={}) => , alert: (p={}) => , sun: (p={}) => , moon: (p={}) => , home: (p={}) => , bell: (p={}) => , calendar: (p={}) => , clock: (p={}) => , sparkles: (p={}) => , activity: (p={}) => , command: (p={}) => , filter: (p={}) => , zap: (p={}) => , chart: (p={}) => , key: (p={}) => , webhook: (p={}) => , }; // ——— Google G logo (kept verbatim for Google sign-in) ——— const GoogleG = (p={}) => ( ); // ——— Brand Logo (matches the website's SVG mark) ——— // `tone="dark"` forces lime/cream rendering for dark surfaces (sidebar, footer-style panels). const BrandLogo = ({ height = 28, variant = 'full', tone = 'auto', style = {} }) => { const uid = React.useId ? React.useId() : `lg-${Math.random().toString(36).slice(2, 8)}`; const isFull = variant === 'full'; const aspect = isFull ? 405 / 84 : 83 / 83; const viewBox = isFull ? '0 0 405 84' : '0 0 83 83'; const width = Math.round(height * aspect); // For "dark" tone (always-dark surfaces) wrap output in [data-theme="dark"] // so CSS variables resolve to dark-theme values regardless of global theme. const wrapStyle = tone === 'dark' ? { display: 'inline-flex', alignItems: 'center', ...style } : { display: 'inline-flex', alignItems: 'center', ...style }; const dataTheme = tone === 'dark' ? 'dark' : undefined; return ( {isFull && ( )} ); }; // Compact wordmark used inline; inherits theme from current data-theme scope. const Wordmark = ({ size = 20, style = {} }) => ( ); // ——— Theme toggle (drop into TopBar) ——— function ThemeToggle({ size = 36 }) { const [theme, setTheme] = React.useState( () => (typeof document !== 'undefined' ? document.documentElement.getAttribute('data-theme') || 'light' : 'light') ); const flip = () => { const next = theme === 'dark' ? 'light' : 'dark'; document.documentElement.classList.add('theme-transition'); document.documentElement.setAttribute('data-theme', next); try { localStorage.setItem('crednx_theme', next); } catch {} setTheme(next); setTimeout(() => document.documentElement.classList.remove('theme-transition'), 360); }; const dark = theme === 'dark'; return ( ); } // ——— Atoms ——— const Button = ({children, variant='primary', size='md', icon, iconRight, onClick, disabled, type='button', style, full}) => { const sizes = { sm: {h:32, px:14, fs:13}, md: {h:40, px:18, fs:14}, lg: {h:48, px:22, fs:15}, }[size]; const variants = { primary: { bg:'hsl(var(--primary))', color:'hsl(var(--primary-fg))', border:'1px solid transparent', shadow:'var(--shadow-brand)', hoverBg:'hsl(var(--primary-glow))' }, secondary: { bg:'hsl(var(--surface))', color:'hsl(var(--fg))', border:'1px solid hsl(var(--border))', shadow:'var(--shadow-xs)', hoverBg:'hsl(var(--surface-2))' }, ghost: { bg:'transparent', color:'hsl(var(--fg))', border:'1px solid transparent', shadow:'none', hoverBg:'hsl(var(--surface-2))' }, danger: { bg:'hsl(var(--surface))', color:'hsl(var(--danger))', border:'1px solid hsl(var(--danger) / 0.35)', shadow:'var(--shadow-xs)', hoverBg:'hsl(var(--danger-bg))' }, dark: { bg:'hsl(var(--surface-deep))', color:'hsl(var(--surface-deep-fg))', border:'1px solid transparent', shadow:'var(--shadow-md)', hoverBg:'hsl(var(--primary-glow))' }, accent: { bg:'hsl(var(--accent))', color:'hsl(var(--accent-fg))', border:'1px solid transparent', shadow:'var(--shadow-glow-lime)', hoverBg:'hsl(var(--accent-glow))' }, }[variant]; const [hover, setHover] = React.useState(false); const [press, setPress] = React.useState(false); return ( ); }; const Input = ({label, icon, hint, error, value, onChange, type='text', placeholder, right}) => ( ); const Badge = ({children, tone='neutral', size='md', style}) => { const tones = { neutral: { bg:'hsl(var(--surface-2))', color:'hsl(var(--fg))', border:'hsl(var(--border))' }, brand: { bg:'hsl(var(--accent-soft))', color:'hsl(var(--primary))', border:'hsl(var(--accent) / 0.35)' }, success: { bg:'hsl(var(--success-bg))', color:'hsl(var(--success-fg))', border:'hsl(var(--success) / 0.3)' }, warning: { bg:'hsl(var(--warning-bg))', color:'hsl(var(--warning-fg))', border:'hsl(var(--warning) / 0.3)' }, danger: { bg:'hsl(var(--danger-bg))', color:'hsl(var(--danger-fg))', border:'hsl(var(--danger) / 0.3)' }, accent: { bg:'hsl(var(--accent))', color:'hsl(var(--accent-fg))', border:'transparent' }, gold: { bg:'hsl(var(--gold-soft))', color:'hsl(var(--gold))', border:'hsl(var(--gold) / 0.3)' }, }[tone] || { bg:'hsl(var(--surface-2))', color:'hsl(var(--fg))', border:'hsl(var(--border))' }; const sz = size==='sm'?{fs:11, h:20, px:8, r:6}:{fs:12, h:24, px:10, r:999}; return {children}; }; const Card = ({children, padding=24, style, className}) => (
{children}
); // Avatar — accepts either a CSS color string OR a CSS var token. Defaults to deep forest. const Avatar = ({name, size=32, color}) => { const initials = (name||'?').split(' ').map(p=>p[0]).slice(0,2).join('').toUpperCase(); const bg = color || 'hsl(var(--primary))'; // Pick contrasting fg — if user passed a hex/hsl we can't introspect, so default to inverse fg const fg = 'hsl(var(--primary-fg))'; return
{initials}
; }; const Divider = ({v}) => v ?
:
; const Spinner = ({size=16, color}) => ( ); // ——— Currency context (unchanged — used by reports for IDR formatting) ——— const CurrencyCtx = React.createContext({ code: 'IDR', symbol: 'Rp', locale: 'id-ID' }); const useCurrency = () => React.useContext(CurrencyCtx); const fmtAmt = (value, cur) => { const { symbol, locale } = cur || { symbol: 'Rp', locale: 'id-ID' }; return `${symbol} ${Math.abs(Number(value) || 0).toLocaleString(locale)}`; }; Object.assign(window, { I, GoogleG, BrandLogo, Wordmark, ThemeToggle, Button, Input, Badge, Card, Avatar, Divider, Spinner, CurrencyCtx, useCurrency, fmtAmt });