/* global React */ // Shared atoms used across marketing + product const { useState, useEffect, useRef, useMemo } = React; /* ========== Wordmark ========== */ function Wordmark({ size = 22, variant = "kanji", color }) { const style = { fontSize: size, color: color || "var(--ink)", letterSpacing: "-0.02em", fontFamily: "var(--f-display)", fontStyle: "normal", fontWeight: "var(--display-weight)", display: "inline-flex", alignItems: "baseline", gap: "0.16em", lineHeight: 1, }; if (variant === "kanji") { return ( kaizen 改 ); } if (variant === "slash") { return ( ka i zen ); } return kaizen; } /* ========== Animated number counter ========== */ function useCount(target, opts = {}) { const { duration = 800, decimals = 0 } = opts; const [v, setV] = useState(target); const startRef = useRef({ from: target, to: target, t: performance.now() }); useEffect(() => { const from = v; const to = target; const t0 = performance.now(); let raf; const step = (now) => { const p = Math.min(1, (now - t0) / duration); const eased = 1 - Math.pow(1 - p, 3); setV(from + (to - from) * eased); if (p < 1) raf = requestAnimationFrame(step); }; raf = requestAnimationFrame(step); return () => cancelAnimationFrame(raf); // eslint-disable-next-line }, [target]); return Number(v).toFixed(decimals); } /* ========== Money ========== */ function Money({ value, large = false, sign = false, decimals = 2, prefix = "$" }) { const negative = value < 0; const v = Math.abs(value); const counted = useCount(v, { decimals, duration: 600 }); const [whole, frac] = String(counted).split("."); const wholeFmt = Number(whole).toLocaleString(); return ( {sign && (negative ? "−" : "+")} {prefix} {wholeFmt} {decimals > 0 && ( .{frac || "00".slice(0, decimals)} )} ); } /* ========== Sparkline ========== */ function Sparkline({ data, w = 120, h = 32, stroke = "currentColor", fill, smooth = true }) { const path = useMemo(() => { if (!data || !data.length) return ""; const min = Math.min(...data); const max = Math.max(...data); const range = max - min || 1; const step = w / (data.length - 1); const points = data.map((v, i) => [i * step, h - ((v - min) / range) * h * 0.85 - h * 0.075]); if (!smooth) return "M " + points.map(p => p.join(",")).join(" L "); let d = `M ${points[0][0]},${points[0][1]}`; for (let i = 1; i < points.length; i++) { const [px, py] = points[i - 1]; const [x, y] = points[i]; const cx = (px + x) / 2; d += ` Q ${cx},${py} ${cx},${(py + y) / 2} T ${x},${y}`; } return d; }, [data, w, h, smooth]); const fillPath = path && `${path} L ${w},${h} L 0,${h} Z`; return ( ); } /* ========== Donut ========== */ function Donut({ segments, size = 160, thickness = 18, label, sublabel }) { const total = segments.reduce((s, x) => s + x.value, 0) || 1; const r = (size - thickness) / 2; const c = 2 * Math.PI * r; let acc = 0; return (