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