/* @jsx React.createElement */
/* ============================================================
Yappari — shared kit: icons, motif, charts, primitives
============================================================ */
const { useState, useEffect, useRef, useMemo, useCallback } = React;
/* ---------------- ICONS (lucide-style, currentColor) ---------------- */
const ICONS = {
overview: '',
sales: '',
stock: '',
planogram: '',
control: '',
products: '',
events: '',
settings: '',
refresh: '',
chevronDown: '',
chevronUp: '',
chevronRight: '',
chevronLeft: '',
sun: '',
moon: '',
menu: '',
search: '',
plus: '',
pencil: '',
trash: '',
x: '',
check: '',
alert: '',
thermometer: '',
door: '',
wifi: '',
wifiOff: '',
zap: '',
power: '',
bulb: '',
volume: '',
flame: '',
download: '',
upload: '',
mapPin: '',
clock: '',
arrowUp: '',
arrowDown: '',
packagePlus: '',
packageX: '',
wallet: '',
cart: '',
cube: '',
snow: '',
reset: '',
play: '',
users: '',
cpu: '',
check2: '',
filter: '',
calendar: '',
dollar: '',
layers: '',
screen: '',
};
function Icon({ name, size = 20, sw = 1.75, style, className }) {
return React.createElement('svg', {
width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
stroke: 'currentColor', strokeWidth: sw, strokeLinecap: 'round', strokeLinejoin: 'round',
style, className, dangerouslySetInnerHTML: { __html: ICONS[name] || '' },
});
}
/* ---------------- PLUM BLOSSOM motif (Yappari crest) ---------------- */
function PlumBlossom({ size = 24, color = 'currentColor', style }) {
// 5 petals around center — matches the logo crest
const petals = [];
for (let i = 0; i < 5; i++) {
const a = (i * 72 - 90) * Math.PI / 180;
const cx = 12 + Math.cos(a) * 5.6;
const cy = 12 + Math.sin(a) * 5.6;
petals.push(React.createElement('circle', { key: i, cx, cy, r: 4.1 }));
}
return React.createElement('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: color, strokeWidth: 1.4, style },
petals,
React.createElement('circle', { cx: 12, cy: 12, r: 2.3, fill: color, stroke: 'none' }),
);
}
/* ---------------- PRODUCT GLYPH (drawn placeholder thumbnails) ---------------- */
function ProductGlyph({ glyph, color, size = 40, radius = 8 }) {
const W = 100, H = 100;
let inner;
const c = color;
if (glyph === 'bottle') {
inner = React.createElement('g', null,
React.createElement('path', { d: 'M42 14h16v8l4 8v50a8 8 0 0 1-8 8H46a8 8 0 0 1-8-8V30l4-8z', fill: c }),
React.createElement('rect', { x: 44, y: 40, width: 12, height: 30, rx: 2, fill: 'rgba(255,255,255,.55)' }),
React.createElement('rect', { x: 43, y: 8, width: 14, height: 8, rx: 2, fill: '#fff', opacity: .85 }),
);
} else if (glyph === 'can') {
inner = React.createElement('g', null,
React.createElement('rect', { x: 36, y: 16, width: 28, height: 68, rx: 6, fill: c }),
React.createElement('rect', { x: 36, y: 30, width: 28, height: 18, fill: 'rgba(255,255,255,.85)' }),
React.createElement('rect', { x: 38, y: 12, width: 24, height: 6, rx: 2, fill: '#b9b9b9' }),
);
} else if (glyph === 'carton') {
inner = React.createElement('g', null,
React.createElement('path', { d: 'M36 26h28v54a4 4 0 0 1-4 4H40a4 4 0 0 1-4-4z', fill: c }),
React.createElement('path', { d: 'M36 26l14-12 14 12z', fill: c, opacity: .8 }),
React.createElement('rect', { x: 42, y: 44, width: 16, height: 22, rx: 2, fill: 'rgba(255,255,255,.6)' }),
);
} else if (glyph === 'cupnoodle') {
inner = React.createElement('g', null,
React.createElement('path', { d: 'M30 30h40l-5 50a6 6 0 0 1-6 5H41a6 6 0 0 1-6-5z', fill: c }),
React.createElement('rect', { x: 26, y: 24, width: 48, height: 9, rx: 4, fill: '#fff', opacity: .92 }),
React.createElement('rect', { x: 40, y: 46, width: 20, height: 22, rx: 2, fill: 'rgba(255,255,255,.55)' }),
);
} else if (glyph === 'bar') {
inner = React.createElement('g', null,
React.createElement('rect', { x: 26, y: 34, width: 48, height: 32, rx: 5, fill: c, transform: 'rotate(-8 50 50)' }),
React.createElement('rect', { x: 33, y: 44, width: 34, height: 6, rx: 3, fill: 'rgba(255,255,255,.7)', transform: 'rotate(-8 50 50)' }),
);
} else { // bag
inner = React.createElement('g', null,
React.createElement('path', { d: 'M32 20h36v60a4 4 0 0 1-4 4H36a4 4 0 0 1-4-4z', fill: c }),
React.createElement('path', { d: 'M32 20l5-6h26l5 6z', fill: c, opacity: .75 }),
React.createElement('circle', { cx: 50, cy: 52, r: 12, fill: 'rgba(255,255,255,.6)' }),
);
}
return React.createElement('div', { style: { width: size, height: size, borderRadius: radius, background: 'var(--bg-sunken)', display: 'grid', placeItems: 'center', flexShrink: 0, overflow: 'hidden', border: '1px solid var(--border-subtle)' } },
React.createElement('svg', { width: size * 0.86, height: size * 0.86, viewBox: '0 0 100 100' }, inner),
);
}
/* ---------------- hooks / helpers ---------------- */
function useCountUp(target, dur = 900) {
const [val, setVal] = useState(0);
const ref = useRef(0);
useEffect(() => {
let raf, start;
const from = ref.current;
const step = (t) => {
if (!start) start = t;
const p = Math.min(1, (t - start) / dur);
const e = 1 - Math.pow(1 - p, 3);
const v = from + (target - from) * e;
setVal(v);
if (p < 1) raf = requestAnimationFrame(step);
else ref.current = target;
};
raf = requestAnimationFrame(step);
return () => cancelAnimationFrame(raf);
}, [target]);
return val;
}
const fmtRp = (n) => 'Rp ' + Math.round(n).toLocaleString('id-ID');
const fmtNum = (n) => Math.round(n).toLocaleString('id-ID');
const fmtRpShort = (n) => {
if (n >= 1e6) return 'Rp ' + (n / 1e6).toFixed(n >= 1e7 ? 0 : 1) + ' jt';
if (n >= 1e3) return 'Rp ' + Math.round(n / 1e3) + 'rb';
return 'Rp ' + n;
};
function relMins(m) {
if (m == null) return '—';
if (m < 1) return 'baru saja';
if (m < 60) return m + ' menit lalu';
const h = Math.floor(m / 60);
if (h < 24) return h + ' jam lalu';
return Math.floor(h / 24) + ' hari lalu';
}
/* ---------------- STATUS ---------------- */
const STATUS = {
online: { label: 'Online', cls: 'badge-online' },
offline: { label: 'Offline', cls: 'badge-offline' },
error: { label: 'Error', cls: 'badge-error' },
maintenance: { label: 'Maintenance', cls: 'badge-maintenance' },
};
function StatusBadge({ status }) {
const s = STATUS[status] || STATUS.offline;
return React.createElement('span', { className: 'badge ' + s.cls },
React.createElement('span', { className: 'b-dot' }), s.label);
}
/* ---------------- CARD ---------------- */
function Card({ children, className = '', style, pad }) {
return React.createElement('div', { className: 'card ' + (pad ? 'card-pad ' : '') + className, style }, children);
}
function SectionCard({ title, sub, right, children, bodyStyle, motif }) {
return React.createElement('div', { className: 'card' },
React.createElement('div', { className: 'card-head' },
React.createElement('div', { className: 'row gap-2' },
motif && React.createElement(PlumBlossom, { size: 18, color: 'var(--st-maint)' }),
React.createElement('div', null,
React.createElement('h3', null, title),
sub && React.createElement('div', { className: 'ch-sub' }, sub),
),
),
right,
),
React.createElement('div', { style: bodyStyle }, children),
);
}
/* ---------------- METRIC CARD ---------------- */
function MetricCard({ label, value, prefix, suffix, sub, subIcon, icon, bg, trend, animate = true }) {
const n = useCountUp(animate ? value : value);
const display = (prefix || '') + fmtNum(n) + (suffix || '');
return React.createElement('div', { className: 'metric', style: { background: bg } },
React.createElement('div', { className: 'm-blossom' }, React.createElement(PlumBlossom, { size: 96, color: '#fff' })),
React.createElement('div', { className: 'm-top' },
React.createElement('div', { className: 'm-label' }, label),
React.createElement('div', { className: 'm-icon' }, React.createElement(Icon, { name: icon, size: 20 })),
),
React.createElement('div', null,
React.createElement('div', { className: 'm-value tnum' }, display),
sub && React.createElement('div', { className: 'm-sub' },
trend != null && React.createElement('span', { className: 'm-trend' },
React.createElement(Icon, { name: trend >= 0 ? 'arrowUp' : 'arrowDown', size: 11, style: { verticalAlign: '-1px' } }),
' ' + Math.abs(trend) + '%'),
subIcon && React.createElement(Icon, { name: subIcon, size: 13 }),
sub),
),
);
}
/* ---------------- PROGRESS BAR ---------------- */
function ProgressBar({ value, max = 100, color, className = '', height }) {
const pct = Math.max(0, Math.min(100, (value / max) * 100));
return React.createElement('div', { className: 'bar ' + className, style: height ? { height } : null },
React.createElement('span', { style: { width: pct + '%', background: color } }));
}
/* ---------------- BAR CHART (HTML, solid bold) ---------------- */
function BarChart({ data, height = 200, color = 'var(--st-maint)', fmt = fmtRpShort, highlightLast }) {
const max = Math.max(...data.map(d => d.value), 1);
const [hover, setHover] = useState(null);
return React.createElement('div', { style: { paddingTop: 26 } },
React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 8, height, position: 'relative' } },
data.map((d, i) => {
const h = (d.value / max) * 100;
const isLast = highlightLast && i === data.length - 1;
return React.createElement('div', {
key: i, style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', height: '100%', justifyContent: 'flex-end', cursor: 'default' },
onMouseEnter: () => setHover(i), onMouseLeave: () => setHover(null),
},
hover === i && React.createElement('div', { style: { position: 'absolute', top: -22, fontSize: 12, fontWeight: 700, color: 'var(--fg-1)', background: 'var(--bg-elev)', padding: '2px 8px', borderRadius: 6, boxShadow: 'var(--shadow-2)', whiteSpace: 'nowrap', zIndex: 5 } }, fmt(d.value)),
React.createElement('div', {
style: {
width: '100%', maxWidth: 46, height: h + '%', minHeight: 4,
background: isLast ? 'var(--accent)' : color,
borderRadius: '5px 5px 0 0',
opacity: hover === null || hover === i ? 1 : 0.45,
transition: 'opacity var(--dur-fast), height var(--dur-slow) var(--ease-out)',
},
}),
React.createElement('div', { style: { fontSize: 11, color: 'var(--fg-3)', marginTop: 7, fontWeight: 600 } }, d.label),
);
}),
),
);
}
/* ---------------- LINE CHART (SVG area) ---------------- */
function LineChart({ data, height = 240, color = 'var(--st-maint)', fmt = fmtRpShort }) {
const W = 760, H = height;
const padL = 56, padR = 16, padT = 16, padB = 30;
const max = Math.max(...data.map(d => d.value), 1) * 1.1;
const min = 0;
const ix = (i) => padL + (i / (data.length - 1)) * (W - padL - padR);
const iy = (v) => padT + (1 - (v - min) / (max - min)) * (H - padT - padB);
const pts = data.map((d, i) => [ix(i), iy(d.value)]);
const line = pts.map((p, i) => (i ? 'L' : 'M') + p[0] + ' ' + p[1]).join(' ');
const area = line + ` L${ix(data.length - 1)} ${H - padB} L${padL} ${H - padB} Z`;
const gid = 'lg' + Math.round(max);
const [hover, setHover] = useState(null);
const ticks = 4;
return React.createElement('div', { style: { position: 'relative' } },
React.createElement('svg', { viewBox: `0 0 ${W} ${H}`, width: '100%', height: H, style: { display: 'block', overflow: 'visible' }, preserveAspectRatio: 'none' },
React.createElement('defs', null,
React.createElement('linearGradient', { id: gid, x1: 0, y1: 0, x2: 0, y2: 1 },
React.createElement('stop', { offset: '0%', stopColor: color, stopOpacity: 0.32 }),
React.createElement('stop', { offset: '100%', stopColor: color, stopOpacity: 0.02 }),
)),
// gridlines + y labels
Array.from({ length: ticks + 1 }).map((_, t) => {
const v = (max / ticks) * t;
const y = iy(v);
return React.createElement('g', { key: t },
React.createElement('line', { x1: padL, x2: W - padR, y1: y, y2: y, stroke: 'var(--border-subtle)', strokeWidth: 1 }),
React.createElement('text', { x: padL - 8, y: y + 4, textAnchor: 'end', fontSize: 11, fill: 'var(--fg-3)', fontFamily: 'var(--font-mono)' }, fmtRpShort(v).replace('Rp ', '')),
);
}),
React.createElement('path', { d: area, fill: `url(#${gid})` }),
React.createElement('path', { d: line, fill: 'none', stroke: color, strokeWidth: 2.5, strokeLinejoin: 'round', strokeLinecap: 'round', vectorEffect: 'non-scaling-stroke' }),
pts.map((p, i) => React.createElement('g', { key: i },
React.createElement('rect', { x: ix(i) - 14, y: 0, width: 28, height: H, fill: 'transparent', onMouseEnter: () => setHover(i), onMouseLeave: () => setHover(null) }),
React.createElement('circle', { cx: p[0], cy: p[1], r: hover === i ? 5 : 3.5, fill: 'var(--bg-elev)', stroke: color, strokeWidth: 2.5 }),
)),
data.map((d, i) => i % Math.ceil(data.length / 7) === 0 || i === data.length - 1 ?
React.createElement('text', { key: 'x' + i, x: ix(i), y: H - 10, textAnchor: 'middle', fontSize: 11, fill: 'var(--fg-3)', fontWeight: 600 }, d.label) : null),
),
hover != null && React.createElement('div', { style: { position: 'absolute', left: `${(ix(hover) / W) * 100}%`, top: 0, transform: 'translateX(-50%)', background: 'var(--bg-inverse)', color: 'var(--bg)', padding: '4px 10px', borderRadius: 6, fontSize: 12, fontWeight: 700, whiteSpace: 'nowrap', pointerEvents: 'none', boxShadow: 'var(--shadow-2)' } }, fmt(data[hover].value)),
);
}
/* ---------------- DONUT ---------------- */
function Donut({ segments, size = 180, thickness = 26, centerLabel, centerSub }) {
const total = segments.reduce((a, s) => a + s.value, 0) || 1;
const r = (size - thickness) / 2;
const C = 2 * Math.PI * r;
let acc = 0;
return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 24, flexWrap: 'wrap' } },
React.createElement('div', { style: { position: 'relative', width: size, height: size, flexShrink: 0 } },
React.createElement('svg', { width: size, height: size, style: { transform: 'rotate(-90deg)' } },
React.createElement('circle', { cx: size / 2, cy: size / 2, r, fill: 'none', stroke: 'var(--bg-sunken)', strokeWidth: thickness }),
segments.map((s, i) => {
const frac = s.value / total;
const dash = frac * C;
const el = React.createElement('circle', {
key: i, cx: size / 2, cy: size / 2, r, fill: 'none', stroke: s.color, strokeWidth: thickness,
strokeDasharray: `${dash} ${C - dash}`, strokeDashoffset: -acc, strokeLinecap: 'butt',
style: { transition: 'stroke-dasharray var(--dur-slow) var(--ease-out)' },
});
acc += dash;
return el;
}),
),
React.createElement('div', { style: { position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', textAlign: 'center' } },
React.createElement('div', null,
React.createElement('div', { style: { fontFamily: 'var(--font-display)', fontSize: 30, lineHeight: 1, color: 'var(--fg-1)' } }, centerLabel),
centerSub && React.createElement('div', { style: { fontSize: 11, color: 'var(--fg-3)', textTransform: 'uppercase', letterSpacing: '.08em', fontWeight: 700, marginTop: 4 } }, centerSub),
)),
),
React.createElement('div', { className: 'col gap-3', style: { flex: 1, minWidth: 140 } },
segments.map((s, i) => React.createElement('div', { key: i, className: 'row gap-2', style: { justifyContent: 'space-between' } },
React.createElement('div', { className: 'row gap-2' },
React.createElement('span', { style: { width: 12, height: 12, borderRadius: 3, background: s.color } }),
React.createElement('span', { style: { fontSize: 14, fontWeight: 600, color: 'var(--fg-2)' } }, s.label),
),
React.createElement('div', { style: { textAlign: 'right' } },
React.createElement('span', { style: { fontWeight: 700, color: 'var(--fg-1)' } }, Math.round((s.value / total) * 100) + '%'),
React.createElement('span', { style: { fontSize: 12, color: 'var(--fg-3)', marginLeft: 6 } }, s.note || ''),
),
)),
),
);
}
/* ---------------- stock level color ---------------- */
function stockColor(cur, cap) {
const p = cap ? cur / cap : 0;
if (cur === 0) return 'var(--st-error)';
if (p <= 0.34) return 'var(--st-warn)';
return 'var(--st-online)';
}
/* ---------------- MODAL ---------------- */
function Modal({ title, eyebrow, danger, children, onClose, footer, lg }) {
useEffect(() => {
const h = (e) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', h);
return () => window.removeEventListener('keydown', h);
}, []);
return React.createElement('div', { className: 'modal-scrim', onClick: onClose },
React.createElement('div', { className: 'modal ' + (lg ? 'modal-lg' : ''), onClick: (e) => e.stopPropagation() },
danger && React.createElement('div', { className: 'modal-danger-strip' }),
React.createElement('div', { className: 'modal-head' },
React.createElement('div', { className: 'row', style: { justifyContent: 'space-between', alignItems: 'flex-start' } },
React.createElement('div', null,
eyebrow && React.createElement('div', { className: 'ph-eyebrow', style: { color: danger ? 'var(--st-error)' : 'var(--fg-3)' } }, eyebrow),
React.createElement('h3', { style: { fontSize: 'var(--fs-lg)', fontWeight: 800, margin: '4px 0 0' } }, title),
),
React.createElement('button', { className: 'icon-btn', onClick: onClose, style: { width: 34, height: 34 } }, React.createElement(Icon, { name: 'x', size: 18 })),
)),
React.createElement('div', { className: 'modal-body' }, children),
footer && React.createElement('div', { className: 'modal-foot' }, footer),
),
);
}
Object.assign(window, {
Icon, ICONS, PlumBlossom, ProductGlyph, useCountUp, fmtRp, fmtNum, fmtRpShort,
relMins, STATUS, StatusBadge, Card, SectionCard, MetricCard, ProgressBar,
BarChart, LineChart, Donut, stockColor, Modal,
});