/* ============================================================ frame.jsx — icons, phone shell, bottom nav, shared atoms ============================================================ */ /* ---------- Icons (stroke, currentColor) ---------- */ const PATHS = { target: '', spark: '', cap: '', briefcase: '', clock: '', pin: '', chevron: '', chevDown: '', check: '', arrow: '', plus: '', mic: '', clip: '', send: '', sun: '', moon: '', globe: '', lock: '', fire: '', eye: '', trend: '', // nav n_stories: '', n_jobs: '', n_advisor: '', n_growth: '', n_profile: '', voice: '', }; function Icon({ name, size = 22, stroke = 2, fill, style, className }) { return ( ); } /* ---------- Animated number (count-up) ---------- */ function useCountUp(target, dur = 900, run = true) { const [v, setV] = useState(run ? 0 : target); useEffect(() => { if (!run) { setV(target); return; } let raf, t0; const step = (t) => { if (!t0) t0 = t; const p = Math.min(1, (t - t0) / dur); const e = 1 - Math.pow(1 - p, 3); setV(Math.round(target * e)); if (p < 1) raf = requestAnimationFrame(step); }; raf = requestAnimationFrame(step); // failsafe: if rAF is throttled/paused (page not visible), still land on target const safety = setTimeout(() => setV(target), dur + 250); return () => { cancelAnimationFrame(raf); clearTimeout(safety); }; }, [target, run]); return v; } function fmtSum(n) { return n.toLocaleString('ru-RU').replace(/,/g, ' '); } /* ---------- Progress ring ---------- */ function Ring({ value, size = 56, stroke = 6, color = 'var(--amber)', track = 'var(--surface-3)', children, run = true }) { const r = (size - stroke) / 2; const c = 2 * Math.PI * r; const v = useCountUp(value, 1000, run); return (
{children ?? `${v}%`}
); } /* ---------- Avatar ---------- */ function Avatar({ size = 44, lang }) { return (
{lang === 'uz' ? PERSONA.initialsUz : PERSONA.initials}
); } /* ---------- Status bar ---------- */ function StatusBar() { return (
9:41
); } /* ---------- responsive scale ---------- Desktop: capped at 0.87 (≈13% smaller, proportional on all sides). Mobile / small viewports: scaled down further so the whole 396×844 mockup fits both width and height, no overflow. */ const PHONE_W = 396, PHONE_H = 844; const DESKTOP_SCALE = 0.87; function useAppScale() { const [scale, setScale] = useState(DESKTOP_SCALE); useEffect(() => { const calc = () => { const vw = window.innerWidth, vh = window.innerHeight; const pad = vw < 480 ? 12 : 24; // per-side gutter, matches #root padding const fitW = (vw - pad * 2) / PHONE_W; const fitH = (vh - pad * 2) / PHONE_H; setScale(Math.max(0.3, Math.min(DESKTOP_SCALE, fitW, fitH))); }; calc(); window.addEventListener('resize', calc); window.addEventListener('orientationchange', calc); return () => { window.removeEventListener('resize', calc); window.removeEventListener('orientationchange', calc); }; }, []); return scale; } /* ---------- Phone shell ---------- */ function Phone({ children, theme, overlay }) { const scale = useAppScale(); return (
{children}
{overlay}
); } /* ---------- Gesture handle ---------- */ function GestureBar() { return (
); } /* ---------- Bottom nav ---------- */ function BottomNav({ route, go, t }) { const items = [ { key: 'stories', icon: 'n_stories', label: t.nav_stories }, { key: 'jobs', icon: 'n_jobs', label: t.nav_jobs }, { key: 'advisor', icon: 'n_advisor', label: t.nav_advisor, center: true }, { key: 'growth', icon: 'n_growth', label: t.nav_growth }, { key: 'profile', icon: 'n_profile', label: t.nav_profile }, ]; return (
{items.map(it => { const active = route === it.key; if (it.center) { return ( ); } return ( ); })}
); } /* ---------- Header (per screen) ---------- */ function Header({ title, sub, right, lang, setLang, theme, setTheme, t }) { return (
{sub &&
{sub}
}

{title}

{right}
); } /* ---------- Lang/Theme quick toggles ---------- */ function QuickToggles({ lang, setLang, theme, setTheme }) { return (
); } /* ---------- Skill bar ---------- */ function SkillBar({ skill, lang, delay = 0 }) { return (
{tr(skill, lang)}
); } /* ---------- Pill / chip ---------- */ function Chip({ children, active, onClick, accent, icon }) { return ( ); } /* ---------- employer logo tile (monogram, brand colour) ---------- */ function Logo({ brand, size = 48, radius }) { const b = BRANDS[brand] || { short: '?', bg: 'var(--brand)' }; const r = radius ?? Math.round(size * 0.28); const [err, setErr] = useState(false); // real logo file present → render image on a clean white tile, fall back // to the colour monogram if the asset fails to load. if (b.logo && !err) { return (
{b.name} setErr(true)} style={{ width: '76%', height: '76%', objectFit: 'contain', display: 'block' }} />
); } return (
{b.short}
); } function brandName(brand) { return (BRANDS[brand] || {}).name || brand; } /* ---------- app brand mark (rising path / growth) ---------- */ function BrandMark({ size = 72, animate = false, plain = false }) { const mark = ( ); if (plain) return mark; return (
{mark}
); } Object.assign(window, { Icon, useCountUp, fmtSum, Ring, Avatar, StatusBar, Phone, GestureBar, BottomNav, Header, QuickToggles, SkillBar, Chip, Logo, brandName, BrandMark });