/* 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 ( {fill && } ); } /* ========== 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 (
{segments.map((s, i) => { const len = (s.value / total) * c; const off = c - acc; acc += len; return ( ); })}
{label}
{sublabel &&
{sublabel}
}
); } /* ========== Bar chart (months) ========== */ function BarChart({ data, height = 140, accent }) { const max = Math.max(...data.map(d => d.v)); return (
{data.map((d, i) => (
{d.l}
))}
); } /* ========== Stacked area chart ========== */ function AreaChart({ series, w = 600, h = 220, showAxis = true }) { // series: [{ data:[..], color, name }] const data = series[0].data; const min = 0; const max = Math.max(...series.flatMap(s => s.data)) * 1.15; const step = w / (data.length - 1); const buildPath = (arr) => { let d = `M 0,${h - ((arr[0] - min) / (max - min)) * h}`; for (let i = 1; i < arr.length; i++) { const x = i * step; const y = h - ((arr[i] - min) / (max - min)) * h; const px = (i - 1) * step; const py = h - ((arr[i - 1] - min) / (max - min)) * h; const cx = (px + x) / 2; d += ` Q ${cx},${py} ${cx},${(py + y) / 2} T ${x},${y}`; } return d; }; return ( {series.map((s, i) => ( ))} {/* baseline grid */} {[0.25, 0.5, 0.75].map((g, i) => ( ))} {series.map((s, i) => { const path = buildPath(s.data); const area = `${path} L ${w},${h} L 0,${h} Z`; return ( ); })} {showAxis && ( {["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"].slice(0, data.length).map((m, i) => ( {m.toUpperCase()} ))} )} ); } /* ========== Progress bar ========== */ function Progress({ value, max = 100, color, height = 6, animate = true }) { const pct = Math.max(0, Math.min(100, (value / max) * 100)); return (
); } /* ========== Avatar ========== */ function Avatar({ name = "User", size = 32, color }) { const initials = name.split(" ").map(n => n[0]).slice(0, 2).join("").toUpperCase(); const hue = name.split("").reduce((s, c) => s + c.charCodeAt(0), 0) % 360; return (
{initials}
); } /* ========== Icon (minimal stroke set) ========== */ function Icon({ name, size = 18, color = "currentColor", strokeWidth = 1.6 }) { const paths = { arrow: , arrowUp: , arrowDown: , plus: , check: , close: , home: , pie: <>, target: <>, seed: , list: <>, settings: <>, bell: <>, search: <>, sparkle: , bank: <>, card: <>, bolt: , leaf: , lock: <>, shield: , chevron: , calendar: <>, upload: , user: <>, car: <>, plane: , house: , grad: , coffee: <>, cart: <>, grid: <>, }; return ( {paths[name] || paths.sparkle} ); } /* Export to window */ Object.assign(window, { Wordmark, Money, Sparkline, Donut, BarChart, AreaChart, Progress, Avatar, Icon, useCount, });