/* Shared UI primitives + chart components — FESPA prototype */ const { useState, useEffect, useMemo, useRef } = React; /* ---------- Brand mark ---------- */ function FespaMark({ variant = "dark", size = 1 }) { const fg = variant === "dark" ? "#0F1822" : "#FFFFFF"; const accent = "#D8232A"; // FESPA red touch return (
FESPA·FRANCE
); } /* ---------- Atoms ---------- */ function Button({ children, variant = "primary", size = "md", onClick, disabled, type = "button", icon, ...rest }) { return ( ); } function Chip({ children, tone = "neutral", size = "md" }) { return {children}; } function Card({ children, padding = "lg", className = "", style }) { return
{children}
; } function Eyebrow({ children, color }) { return
{children}
; } function StatTile({ label, value, sub, tone = "navy", icon }) { return (
{label} {icon && {icon}}
{value}
{sub &&
{sub}
}
); } /* ---------- Charts (pure SVG) ---------- */ function DonutChart({ value, total, size = 180, stroke = 18, color = "var(--byi-orange-500)", track = "var(--byi-navy-100)", label, sublabel }) { const r = (size - stroke) / 2; const c = 2 * Math.PI * r; const pct = total > 0 ? value / total : 0; const offset = c * (1 - pct); return (
{label ?? `${Math.round(pct * 100)}%`}
{sublabel &&
{sublabel}
}
); } function HBar({ data, max, color = "var(--byi-orange-500)", showCount = true, height = 24 }) { const m = max ?? Math.max(...data.map(d => d.value), 1); return (
{data.map((d, i) => (
{d.label}
{showCount &&
{d.value}
}
))}
); } function VBar({ data, height = 200, color = "var(--byi-navy-800)", accent }) { const max = Math.max(...data.map(d => d.value), 1); return (
{data.map((d, i) => (
{d.value}
{d.label}
))}
); } function Sparkline({ data, width = 320, height = 80, color = "var(--byi-orange-500)" }) { const max = Math.max(...data, 1); const step = data.length > 1 ? width / (data.length - 1) : width; const points = data.map((v, i) => `${i * step},${height - (v / max) * height * 0.85 - 4}`); const path = `M ${points.join(" L ")}`; const area = `${path} L ${width},${height} L 0,${height} Z`; return ( {points.map((p, i) => { const [x, y] = p.split(",").map(Number); return ; })} ); } function StackedBar({ segments, height = 14 }) { const total = segments.reduce((s, x) => s + x.value, 0); return (
{segments.map((seg, i) => (
))}
); } function Legend({ items }) { return (
{items.map((it, i) => (
{it.label} {it.value}
))}
); } /* ---------- Aggregations ---------- */ function aggregate(respondents) { const count = (arr, key) => { const m = new Map(); arr.forEach(r => { const v = r[key]; if (Array.isArray(v)) v.forEach(x => m.set(x, (m.get(x) || 0) + 1)); else if (v != null) m.set(v, (m.get(v) || 0) + 1); }); return [...m.entries()].map(([label, value]) => ({ label, value })).sort((a, b) => b.value - a.value); }; const skillHist = Array.from({ length: 10 }, (_, i) => ({ label: String(i + 1), value: respondents.filter(r => r.skill === i + 1).length })); const avgSkill = respondents.reduce((s, r) => s + r.skill, 0) / Math.max(respondents.length, 1); // 21 days const dayCounts = Array.from({ length: 21 }, () => 0); respondents.forEach(r => { const days = Math.floor((new Date(2026, 4, 23) - r.submittedAt) / (1000 * 60 * 60 * 24)); const idx = 20 - Math.max(0, Math.min(20, days)); dayCounts[idx] += 1; }); return { usage: count(respondents, "usage"), tools: count(respondents, "tools"), domains: count(respondents, "domains"), benefits: count(respondents, "benefits"), satisfaction: count(respondents.filter(r => r.satisfaction), "satisfaction"), policy: count(respondents, "policy"), training: count(respondents, "trainingInterests"), budget: count(respondents, "budget"), timeline: count(respondents, "timeline"), format: count(respondents, "format"), sizes: count(respondents, "sizeLabel"), regions: count(respondents, "region"), skillHist, avgSkill, dayCounts }; } /* expose */ Object.assign(window, { FespaMark, Button, Chip, Card, Eyebrow, StatTile, DonutChart, HBar, VBar, Sparkline, StackedBar, Legend, aggregate });