// Bellia — tokens, icons, primitives, mini charts // Palette orange crème + blanc crème, coins arrondis, Inter. const C = { bg: '#FBF6EC', card: '#FFFFFF', cardSoft: '#FBF3E5', cardWash: '#FDF7EC', ink: '#241B12', ink2: '#3D2F22', mute: '#8A7B69', mute2: '#A89A87', line: '#EADDC8', lineSoft: '#F1E7D5', orange: '#E89B6C', orangeDp: '#C97849', orangeMid: '#DE8657', orangeSft: '#F7D7BD', orangeWh: '#FCEDDC', ok: '#7FB069', okSft: '#E7F2DD', warn: '#D9923B', warnSft: '#FBE9D5', err: '#C46A5F', errSft: '#FBE0DC', white: '#FFFDF8', cream: '#FFF8EE', }; // ---------- Lucide-style icons (inline SVG) ---------- const ICONS = { search: '', bell: '', chevDown:'', chevRight:'', chevLeft:'', settings:'', plus: '', download:'', filter: '', dashboard:'', bag: '', receipt: '', users: '', package: '', book: '', chart: '', shield: '', zap: '', calc: '', percent: '', euro: '', cal: '', file: '', activity:'', trendUp: '', trendDn: '', card: '', check: '', checkC: '', alertT: '', alertC: '', mail: '', lock: '', eye: '', eyeOff: '', logout: '', arrowUp: '', arrowR: '', arrowDn: '', x: '', more: '', store: '', list: '', refresh: '', coffee: '', moon: '', power: '', play: '', sparkles:'', bot: '', hash: '', layers: '', truck: '', }; const Icon = ({ name, size = 18, color = 'currentColor', stroke = 1.75, style, className }) => { const inner = ICONS[name]; if (!inner) return ; return ( ); }; // ---------- format helpers ---------- const fmtEUR = (n, decimals = 0) => { const s = Math.abs(n).toLocaleString('fr-FR', { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); return `${n < 0 ? '-' : ''}${s.replace(/\u202F/g, ' ').replace(/,/g, decimals === 0 ? ' ' : ',').replace(/(\d)\u00a0(\d{3})/g, '$1 $2')} €`; }; // simpler: use built-in const eur = (n, dec = 0) => n.toLocaleString('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: dec, maximumFractionDigits: dec }).replace(/\u202F/g, ' ').replace(/\u00a0/g, ' '); const pct = (n, dec = 1) => `${n.toLocaleString('fr-FR', { minimumFractionDigits: dec, maximumFractionDigits: dec })} %`; const signedPct = (n, dec = 1) => `${n > 0 ? '+' : ''}${n.toLocaleString('fr-FR', { minimumFractionDigits: dec, maximumFractionDigits: dec })} %`; const num = (n) => n.toLocaleString('fr-FR'); // ---------- primitives ---------- const Pill = ({ children, kind = 'neutral', size = 'md', style, icon }) => { const map = { neutral: { bg: C.cardSoft, fg: C.ink2 }, accent: { bg: C.orangeWh, fg: C.orangeDp }, ok: { bg: C.okSft, fg: C.ok }, warn: { bg: C.warnSft, fg: C.warn }, err: { bg: C.errSft, fg: C.err }, invert: { bg: 'rgba(255,248,238,0.18)', fg: C.cream }, }; const s = map[kind]; const pad = size === 'sm' ? '3px 8px' : '5px 11px'; const fs = size === 'sm' ? 10 : 11; return ( {icon}{children} ); }; const Btn = ({ children, kind = 'primary', size = 'md', icon, iconAfter, full, onClick, style, type }) => { const map = { primary: { bg: C.orange, fg: '#fff', bd: 'transparent' }, primaryDark: { bg: C.ink, fg: C.cream, bd: 'transparent' }, ghost: { bg: 'transparent', fg: C.ink, bd: C.line }, soft: { bg: C.cardSoft, fg: C.ink, bd: 'transparent' }, accent: { bg: C.orangeWh, fg: C.orangeDp, bd: 'transparent' }, }; const m = map[kind]; const sz = size === 'sm' ? { h: 32, fs: 12, px: 14 } : size === 'lg' ? { h: 48, fs: 14, px: 22 } : { h: 38, fs: 13, px: 18 }; return ( ); }; const Avatar = ({ initials = 'YB', size = 32, c = C.orange, fg = '#fff', style }) => (
{initials}
); const Logo = ({ size = 30, tone = 'light' }) => (
b
Bellia
); const Card = ({ children, style, hoverable, onClick }) => (
hoverable && (e.currentTarget.style.boxShadow = '0 8px 24px -16px rgba(36,27,18,0.18)')} onMouseLeave={e => hoverable && (e.currentTarget.style.boxShadow = 'none')} >{children}
); // ---------- charts ---------- // Pretty area line chart with N-1 dashed overlay const AreaLineChart = ({ data, prevData, height = 240, accent = C.orange, accentSoft = C.orangeWh, showAxis = true, padding = { l: 36, r: 12, t: 18, b: 28 } }) => { const w = 800, h = height; const all = [...data, ...(prevData || [])]; const max = Math.max(...all) * 1.1; const min = 0; const pw = w - padding.l - padding.r; const ph = h - padding.t - padding.b; const pt = (i, v, n) => [padding.l + (i / (n - 1)) * pw, padding.t + ph - ((v - min) / (max - min)) * ph]; const pts = data.map((v, i) => pt(i, v, data.length)); const prevPts = (prevData || []).map((v, i) => pt(i, v, prevData.length)); const linePath = pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' '); const areaPath = `${linePath} L${pts[pts.length-1][0]},${padding.t + ph} L${padding.l},${padding.t + ph} Z`; const prevPath = prevPts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' '); const yTicks = 4; const ticks = Array.from({ length: yTicks + 1 }, (_, i) => max * (i / yTicks)); return ( {showAxis && ticks.map((t, i) => { const y = padding.t + ph - ((t - min) / (max - min)) * ph; return ( {Math.round(t/1000)}k ); })} {prevPath && } {pts.map((p, i) => i % Math.ceil(data.length / 6) === 0 || i === pts.length - 1 ? ( ) : null)} ); }; const BarChartGrouped = ({ data, prevData, height = 200, accent = C.orange, accentSoft = C.orangeSft }) => { const w = 800, h = height; const pad = { l: 36, r: 12, t: 14, b: 26 }; const pw = w - pad.l - pad.r, ph = h - pad.t - pad.b; const max = Math.max(...data, ...(prevData || [0])) * 1.15; const bw = pw / data.length * 0.62; const sw = pw / data.length; return ( {[0, 0.25, 0.5, 0.75, 1].map((p, i) => { const y = pad.t + ph - p * ph; return ; })} {data.map((v, i) => { const x = pad.l + i * sw + sw/2 - bw/2; const hh = (v / max) * ph; const y = pad.t + ph - hh; const prev = prevData?.[i]; return ( {prev != null && (() => { const hp = (prev / max) * ph; const yp = pad.t + ph - hp; return ; })()} {i === data.length - 1 && } ); })} ); }; const Sparkline = ({ data, height = 36, accent = C.orange, fill = true }) => { const w = 120, h = height; const max = Math.max(...data), min = Math.min(...data); const range = max - min || 1; const pts = data.map((v, i) => [(i / (data.length - 1)) * (w - 4) + 2, h - 4 - ((v - min) / range) * (h - 8)]); const path = pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' '); const area = fill ? `${path} L${pts[pts.length-1][0]},${h} L${pts[0][0]},${h} Z` : ''; return ( {fill && } ); }; const DonutChart = ({ slices, size = 140, thickness = 22 }) => { const r = (size - thickness) / 2; const c = 2 * Math.PI * r; const total = slices.reduce((a, s) => a + s.value, 0); let offset = 0; return ( {slices.map((s, i) => { const frac = s.value / total; const len = c * frac; const dash = `${len} ${c - len}`; const el = ( ); offset += len; return el; })} ); }; // ---------- demo data ---------- // 30 jours réalistes: lundis bas, week-ends hauts const seedDay = (i, base) => { const dow = (i + 1) % 7; // 0 lun, ..., 6 dim const weekend = dow === 5 || dow === 6 ? 1.35 : dow === 4 ? 1.15 : dow === 0 ? 0.75 : 1; const noise = 0.92 + ((i * 37) % 17) / 100; return Math.round(base * weekend * noise); }; const SALES_30 = Array.from({ length: 30 }, (_, i) => seedDay(i, 2850)); const SALES_30_PREV = Array.from({ length: 30 }, (_, i) => seedDay(i, 2640) - 80); const MONTH_TOTAL = SALES_30.reduce((a, b) => a + b, 0); const BREADCRUMB = { restaurant: 'Cantine Corner — République', period: 'Mai 2026' }; Object.assign(window, { C, ICONS, Icon, eur, pct, signedPct, num, Pill, Btn, Avatar, Logo, Card, AreaLineChart, BarChartGrouped, Sparkline, DonutChart, SALES_30, SALES_30_PREV, MONTH_TOTAL, BREADCRUMB, });