/* @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, });