// v4-components.jsx — Awesomic-skinned design system for 9mith Advisory
// Cosmica type (weight-driven hierarchy), Mist/Snow/Aubergine surfaces,
// maximum roundness (36px cards, pill buttons), flat surfaces — no drop shadows.
const { useState, useEffect, useRef } = React;
// Palette (Awesomic tokens)
const C = {
aubergine: '#421d24', aubergineDeep: '#4e242c',
graphite: '#3f3f46', slate: '#52525b', steel: '#71717a', ash: '#a1a1aa',
pebble: '#d4d4d8', fog: '#ececee', mist: '#f4f4f5', snow: '#ffffff',
gold: '#c2a05a', // subtle gold — logo "g" only
onDark: 'rgba(255,255,255,0.92)', onDarkMuted: 'rgba(255,255,255,0.66)',
};
// ── Scroll visibility hook ────────────────────────────────────────────────────
function useVisible() {
const ref = useRef(null);
const [vis, setVis] = useState(false);
useEffect(() => {
const el = ref.current; if (!el) return;
const timer = setTimeout(() => setVis(true), 800);
const io = new IntersectionObserver(([e]) => {
if (e.isIntersecting) { setVis(true); clearTimeout(timer); io.disconnect(); }
}, { threshold: 0, rootMargin: '0px 0px 80px 0px' });
io.observe(el);
return () => { clearTimeout(timer); io.disconnect(); };
}, []);
return [ref, vis];
}
// ── Fade-in wrapper (Awesomic overshoot spring) ───────────────────────────────
function FadeIn({ children, delay = 0, style: s, className }) {
const [ref, vis] = useVisible();
return (
{children}
);
}
// ── CountUp ───────────────────────────────────────────────────────────────────
function CountUp({ to, startFrom = 0, suffix = '', prefix = '', duration = 1000, style: s, className }) {
const [val, setVal] = useState(startFrom);
const [triggered, setTriggered] = useState(false);
const ref = useRef(null);
useEffect(() => {
const el = ref.current; if (!el) return;
const io = new IntersectionObserver(([e]) => {
if (e.isIntersecting) { setTriggered(true); io.disconnect(); }
}, { threshold: 0.5 });
io.observe(el); return () => io.disconnect();
}, []);
useEffect(() => {
if (!triggered) return;
const t0 = Date.now();
const tick = () => {
const p = Math.min((Date.now() - t0) / duration, 1);
const ease = 1 - Math.pow(1 - p, 3);
setVal(Math.round(startFrom + (to - startFrom) * ease));
if (p < 1) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}, [triggered]);
return {prefix}{val.toLocaleString()}{suffix};
}
// ── Logo ──────────────────────────────────────────────────────────────────────
// "g" set in Giaza — subtle gold; "mith" in aubergine (light bg) / snow (dark bg).
function Logo({ dark = false, compact = false, onClick }) {
const gs = compact ? 30 : 46;
const ms = compact ? 17 : 26;
const mithColor = dark ? C.mist : C.aubergine;
return (
);
}
// ── Eyebrow label (quiet, sentence case, with short rule) ─────────────────────
function Eyebrow({ label, dark = false }) {
return (
{label}
);
}
// ── Typography ────────────────────────────────────────────────────────────────
// Display headline / pull quote. Use inside children for weight contrast.
function PullQuote({ children, size = 36, dark = false, center = false, maxWidth = 780, muted = false, italic = false }) {
const color = muted ? (dark ? C.onDarkMuted : C.steel) : (dark ? C.snow : C.aubergine);
return (
30 ? 1.1 : 1.28, maxWidth, margin: center ? '0 auto' : 0, textAlign: center ? 'center' : 'left', textWrap: 'pretty', letterSpacing: '-0.01em' }}>{children}
);
}
function Body({ children, size = 16, color, dark = false, maxWidth, style: s, italic = false }) {
const c = color || (dark ? C.onDarkMuted : C.slate);
return {children}
;
}
function Caption({ children, dark = false }) {
return {children};
}
function Hairline({ style: s, dark = false }) {
return ;
}
// ── Stat (Awesomic bare-numeral style) ────────────────────────────────────────
function StatAnnotation({ number, suffix = '', prefix = '', label, dark = false, countFrom }) {
const borderC = dark ? 'rgba(255,255,255,0.14)' : C.pebble;
return (
{countFrom !== undefined
?
: <>{prefix}{number}{suffix}>}
{label}
);
}
// ── Primary CTA — aubergine pill, pressed-glass shadow ────────────────────────
function CTAButton({ children, onClick, variant = 'primary' }) {
const [press, setPress] = useState(false);
const primary = variant === 'primary';
return (
);
}
// ── Text link with arrow (strong affordance via weight + arrow) ──────────────
function GoldLink({ children, onClick, dark = false, style: s }) {
const [hov, setHov] = useState(false);
return (
);
}
// ── Page section hero ─────────────────────────────────────────────────────────
function SectionHero({ label, headline, sub, dark = true, size = 46 }) {
return (
{dark && g
}
{label &&
}
{headline}
{sub &&
{sub}
}
);
}
// ── Sector card — Snow card, 36px radius, color dot marker ────────────────────
function SectorCard({ name, color, metric, count }) {
const [hov, setHov] = useState(false);
return (
setHov(true)} onMouseLeave={() => setHov(false)}>
{name}
{metric}
{count}
);
}
// ── Issue tree — rounded fog boxes, pebble connectors ─────────────────────────
function IssueTree({ root, branches }) {
return (
{branches.map((b, i) => (
))}
);
}
window.AdvC = C;
Object.assign(window, {
useVisible, FadeIn, CountUp, Logo, Eyebrow, PullQuote, Body, Caption, Hairline,
StatAnnotation, CTAButton, GoldLink, SectionHero, SectorCard, IssueTree
});