// 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' }) => (
);
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 (
);
};
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 (
);
};
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 (
);
};
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 (
);
};
// ---------- 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,
});