// 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 (
e.currentTarget.style.background = 'hsl(var(--surface-2))'}
onMouseLeave={e => e.currentTarget.style.background = 'hsl(var(--surface))'}>
{dark ? : }
);
}
// ——— 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 (
setHover(true)} onMouseLeave={()=>{setHover(false);setPress(false)}}
onMouseDown={()=>setPress(true)} onMouseUp={()=>setPress(false)}
style={{
display:'inline-flex', alignItems:'center', justifyContent:'center', gap:8,
height:sizes.h, padding:`0 ${sizes.px}px`, fontSize:sizes.fs, fontWeight:600,
borderRadius: 999, cursor: disabled?'not-allowed':'pointer', fontFamily:'inherit',
background: hover && !disabled ? variants.hoverBg : variants.bg,
color: variants.color, border: variants.border,
boxShadow: press? 'var(--shadow-xs)' : variants.shadow,
transform: press?'scale(0.98)':'scale(1)',
transition:'transform 120ms, background 200ms, box-shadow 200ms, color 200ms',
opacity: disabled?0.5:1, width: full?'100%':'auto', letterSpacing:'-0.01em',
...(style||{})
}}>
{icon}{children}{iconRight}
);
};
const Input = ({label, icon, hint, error, value, onChange, type='text', placeholder, right}) => (
{label && {label}
}
{e.currentTarget.style.boxShadow='0 0 0 3px hsl(var(--ring) / 0.2)'; e.currentTarget.style.borderColor='hsl(var(--ring))';}}
onBlur={e=>{e.currentTarget.style.boxShadow='none'; e.currentTarget.style.borderColor=error?'hsl(var(--danger))':'hsl(var(--border))';}}>
{icon && {icon} }
{right && {right} }
{hint && {hint}
}
);
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
});