/* ============================================================
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 (
);
}
/* ---------- Avatar ---------- */
function Avatar({ size = 44, lang }) {
return (
{lang === 'uz' ? PERSONA.initialsUz : PERSONA.initials}
);
}
/* ---------- Status bar ---------- */
function StatusBar() {
return (
);
}
/* ---------- 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 (
);
}
/* ---------- 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 (
);
}
/* ---------- Lang/Theme quick toggles ---------- */
function QuickToggles({ lang, setLang, theme, setTheme }) {
return (
);
}
/* ---------- Skill bar ---------- */
function SkillBar({ skill, lang, delay = 0 }) {
return (
);
}
/* ---------- 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 (

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 });