/* ===== tweaks-panel.jsx ===== */
// @ds-adherence-ignore -- omelette starter scaffold (raw elements/hex/px by design)
/* BEGIN USAGE */
// tweaks-panel.jsx
// Reusable Tweaks shell + form-control helpers.
// Exports (to window): useTweaks, TweaksPanel, TweakSection, TweakRow, TweakSlider,
// TweakToggle, TweakRadio, TweakSelect, TweakText, TweakNumber, TweakColor, TweakButton.
//
// Owns the host protocol (listens for __activate_edit_mode / __deactivate_edit_mode,
// posts __edit_mode_available / __edit_mode_set_keys / __edit_mode_dismissed) so
// individual prototypes don't re-roll it. Ships a consistent set of controls so you
// don't hand-draw , segmented radios, steppers, etc.
//
// Usage (in an HTML file that loads React + Babel):
//
// const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
// "primaryColor": "#D97757",
// "palette": ["#D97757", "#29261b", "#f6f4ef"],
// "fontSize": 16,
// "density": "regular",
// "dark": false
// }/*EDITMODE-END*/;
//
// function App() {
// const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
// return (
//
// Hello
//
//
// setTweak('fontSize', v)} />
// setTweak('density', v)} />
//
// setTweak('primaryColor', v)} />
// setTweak('palette', v)} />
// setTweak('dark', v)} />
//
//
// );
// }
//
// TweakRadio is the segmented control for 2–3 short options (auto-falls-back to
// TweakSelect past ~16/~10 chars per label); reach for TweakSelect directly when
// options are many or long. For color tweaks always curate 3-4 options rather than
// a free picker; an option can also be a whole 2–5 color palette (the stored value
// is the array). The Tweak* controls are a floor, not a ceiling — build custom
// controls inside the panel if a tweak calls for UI they don't cover.
/* END USAGE */
// ─────────────────────────────────────────────────────────────────────────────
const __TWEAKS_STYLE = `
.twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px;
max-height:calc(100vh - 32px);display:flex;flex-direction:column;
transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom right;
background:rgba(250,249,247,.78);color:#29261b;
-webkit-backdrop-filter:blur(24px) saturate(160%);backdrop-filter:blur(24px) saturate(160%);
border:.5px solid rgba(255,255,255,.6);border-radius:14px;
box-shadow:0 1px 0 rgba(255,255,255,.5) inset,0 12px 40px rgba(0,0,0,.18);
font:11.5px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden}
.twk-hd{display:flex;align-items:center;justify-content:space-between;
padding:10px 8px 10px 14px;cursor:move;user-select:none}
.twk-hd b{font-size:12px;font-weight:600;letter-spacing:.01em}
.twk-x{appearance:none;border:0;background:transparent;color:rgba(41,38,27,.55);
width:22px;height:22px;border-radius:6px;cursor:default;font-size:13px;line-height:1}
.twk-x:hover{background:rgba(0,0,0,.06);color:#29261b}
.twk-body{padding:2px 14px 14px;display:flex;flex-direction:column;gap:10px;
overflow-y:auto;overflow-x:hidden;min-height:0;
scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.15) transparent}
.twk-body::-webkit-scrollbar{width:8px}
.twk-body::-webkit-scrollbar-track{background:transparent;margin:2px}
.twk-body::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15);border-radius:4px;
border:2px solid transparent;background-clip:content-box}
.twk-body::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.25);
border:2px solid transparent;background-clip:content-box}
.twk-row{display:flex;flex-direction:column;gap:5px}
.twk-row-h{flex-direction:row;align-items:center;justify-content:space-between;gap:10px}
.twk-lbl{display:flex;justify-content:space-between;align-items:baseline;
color:rgba(41,38,27,.72)}
.twk-lbl>span:first-child{font-weight:500}
.twk-val{color:rgba(41,38,27,.5);font-variant-numeric:tabular-nums}
.twk-sect{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
color:rgba(41,38,27,.45);padding:10px 0 0}
.twk-sect:first-child{padding-top:0}
.twk-field{appearance:none;box-sizing:border-box;width:100%;min-width:0;height:26px;padding:0 8px;
border:.5px solid rgba(0,0,0,.1);border-radius:7px;
background:rgba(255,255,255,.6);color:inherit;font:inherit;outline:none}
.twk-field:focus{border-color:rgba(0,0,0,.25);background:rgba(255,255,255,.85)}
select.twk-field{padding-right:22px;
background-image:url("data:image/svg+xml;utf8, ");
background-repeat:no-repeat;background-position:right 8px center}
.twk-slider{appearance:none;-webkit-appearance:none;width:100%;height:4px;margin:6px 0;
border-radius:999px;background:rgba(0,0,0,.12);outline:none}
.twk-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
width:14px;height:14px;border-radius:50%;background:#fff;
border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
.twk-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%;
background:#fff;border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
.twk-seg{position:relative;display:flex;padding:2px;border-radius:8px;
background:rgba(0,0,0,.06);user-select:none}
.twk-seg-thumb{position:absolute;top:2px;bottom:2px;border-radius:6px;
background:rgba(255,255,255,.9);box-shadow:0 1px 2px rgba(0,0,0,.12);
transition:left .15s cubic-bezier(.3,.7,.4,1),width .15s}
.twk-seg.dragging .twk-seg-thumb{transition:none}
.twk-seg button{appearance:none;position:relative;z-index:1;flex:1;border:0;
background:transparent;color:inherit;font:inherit;font-weight:500;min-height:22px;
border-radius:6px;cursor:default;padding:4px 6px;line-height:1.2;
overflow-wrap:anywhere}
.twk-toggle{position:relative;width:32px;height:18px;border:0;border-radius:999px;
background:rgba(0,0,0,.15);transition:background .15s;cursor:default;padding:0}
.twk-toggle[data-on="1"]{background:#34c759}
.twk-toggle i{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;
background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}
.twk-toggle[data-on="1"] i{transform:translateX(14px)}
.twk-num{display:flex;align-items:center;box-sizing:border-box;min-width:0;height:26px;padding:0 0 0 8px;
border:.5px solid rgba(0,0,0,.1);border-radius:7px;background:rgba(255,255,255,.6)}
.twk-num-lbl{font-weight:500;color:rgba(41,38,27,.6);cursor:ew-resize;
user-select:none;padding-right:8px}
.twk-num input{flex:1;min-width:0;height:100%;border:0;background:transparent;
font:inherit;font-variant-numeric:tabular-nums;text-align:right;padding:0 8px 0 0;
outline:none;color:inherit;-moz-appearance:textfield}
.twk-num input::-webkit-inner-spin-button,.twk-num input::-webkit-outer-spin-button{
-webkit-appearance:none;margin:0}
.twk-num-unit{padding-right:8px;color:rgba(41,38,27,.45)}
.twk-btn{appearance:none;height:26px;padding:0 12px;border:0;border-radius:7px;
background:rgba(0,0,0,.78);color:#fff;font:inherit;font-weight:500;cursor:default}
.twk-btn:hover{background:rgba(0,0,0,.88)}
.twk-btn.secondary{background:rgba(0,0,0,.06);color:inherit}
.twk-btn.secondary:hover{background:rgba(0,0,0,.1)}
.twk-swatch{appearance:none;-webkit-appearance:none;width:56px;height:22px;
border:.5px solid rgba(0,0,0,.1);border-radius:6px;padding:0;cursor:default;
background:transparent;flex-shrink:0}
.twk-swatch::-webkit-color-swatch-wrapper{padding:0}
.twk-swatch::-webkit-color-swatch{border:0;border-radius:5.5px}
.twk-swatch::-moz-color-swatch{border:0;border-radius:5.5px}
.twk-chips{display:flex;gap:6px}
.twk-chip{position:relative;appearance:none;flex:1;min-width:0;height:46px;
padding:0;border:0;border-radius:6px;overflow:hidden;cursor:default;
box-shadow:0 0 0 .5px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.06);
transition:transform .12s cubic-bezier(.3,.7,.4,1),box-shadow .12s}
.twk-chip:hover{transform:translateY(-1px);
box-shadow:0 0 0 .5px rgba(0,0,0,.18),0 4px 10px rgba(0,0,0,.12)}
.twk-chip[data-on="1"]{box-shadow:0 0 0 1.5px rgba(0,0,0,.85),
0 2px 6px rgba(0,0,0,.15)}
.twk-chip>span{position:absolute;top:0;bottom:0;right:0;width:34%;
display:flex;flex-direction:column;box-shadow:-1px 0 0 rgba(0,0,0,.1)}
.twk-chip>span>i{flex:1;box-shadow:0 -1px 0 rgba(0,0,0,.1)}
.twk-chip>span>i:first-child{box-shadow:none}
.twk-chip svg{position:absolute;top:6px;left:6px;width:13px;height:13px;
filter:drop-shadow(0 1px 1px rgba(0,0,0,.3))}
`;
// ── useTweaks ───────────────────────────────────────────────────────────────
// Single source of truth for tweak values. setTweak persists via the host
// (__edit_mode_set_keys → host rewrites the EDITMODE block on disk).
function useTweaks(defaults) {
const [values, setValues] = React.useState(defaults);
// Accepts either setTweak('key', value) or setTweak({ key: value, ... }) so a
// useState-style call doesn't write a "[object Object]" key into the persisted
// JSON block.
const setTweak = React.useCallback((keyOrEdits, val) => {
const edits = typeof keyOrEdits === 'object' && keyOrEdits !== null
? keyOrEdits : { [keyOrEdits]: val };
setValues((prev) => ({ ...prev, ...edits }));
window.parent.postMessage({ type: '__edit_mode_set_keys', edits }, '*');
// Same-window signal so in-page listeners (deck-stage rail thumbnails)
// can react — the parent message only reaches the host, not peers.
window.dispatchEvent(new CustomEvent('tweakchange', { detail: edits }));
}, []);
return [values, setTweak];
}
// ── TweaksPanel ─────────────────────────────────────────────────────────────
// Floating shell. Registers the protocol listener BEFORE announcing
// availability — if the announce ran first, the host's activate could land
// before our handler exists and the toolbar toggle would silently no-op.
// The close button posts __edit_mode_dismissed so the host's toolbar toggle
// flips off in lockstep; the host echoes __deactivate_edit_mode back which
// is what actually hides the panel.
function TweaksPanel({ title = 'Tweaks', children }) {
const [open, setOpen] = React.useState(false);
const dragRef = React.useRef(null);
const offsetRef = React.useRef({ x: 16, y: 16 });
const PAD = 16;
const clampToViewport = React.useCallback(() => {
const panel = dragRef.current;
if (!panel) return;
const w = panel.offsetWidth, h = panel.offsetHeight;
const maxRight = Math.max(PAD, window.innerWidth - w - PAD);
const maxBottom = Math.max(PAD, window.innerHeight - h - PAD);
offsetRef.current = {
x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)),
y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y)),
};
panel.style.right = offsetRef.current.x + 'px';
panel.style.bottom = offsetRef.current.y + 'px';
}, []);
React.useEffect(() => {
if (!open) return;
clampToViewport();
if (typeof ResizeObserver === 'undefined') {
window.addEventListener('resize', clampToViewport);
return () => window.removeEventListener('resize', clampToViewport);
}
const ro = new ResizeObserver(clampToViewport);
ro.observe(document.documentElement);
return () => ro.disconnect();
}, [open, clampToViewport]);
React.useEffect(() => {
const onMsg = (e) => {
const t = e?.data?.type;
if (t === '__activate_edit_mode') setOpen(true);
else if (t === '__deactivate_edit_mode') setOpen(false);
};
window.addEventListener('message', onMsg);
window.parent.postMessage({ type: '__edit_mode_available' }, '*');
return () => window.removeEventListener('message', onMsg);
}, []);
const dismiss = () => {
setOpen(false);
window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*');
};
const onDragStart = (e) => {
const panel = dragRef.current;
if (!panel) return;
const r = panel.getBoundingClientRect();
const sx = e.clientX, sy = e.clientY;
const startRight = window.innerWidth - r.right;
const startBottom = window.innerHeight - r.bottom;
const move = (ev) => {
offsetRef.current = {
x: startRight - (ev.clientX - sx),
y: startBottom - (ev.clientY - sy),
};
clampToViewport();
};
const up = () => {
window.removeEventListener('mousemove', move);
window.removeEventListener('mouseup', up);
};
window.addEventListener('mousemove', move);
window.addEventListener('mouseup', up);
};
if (!open) return null;
return (
<>
{title}
e.stopPropagation()}
onClick={dismiss}>✕
{children}
>
);
}
// ── Layout helpers ──────────────────────────────────────────────────────────
function TweakSection({ label, children }) {
return (
<>
{label}
{children}
>
);
}
function TweakRow({ label, value, children, inline = false }) {
return (
{label}
{value != null && {value} }
{children}
);
}
// ── Controls ────────────────────────────────────────────────────────────────
function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = '', onChange }) {
return (
onChange(Number(e.target.value))} />
);
}
function TweakToggle({ label, value, onChange }) {
return (
{label}
onChange(!value)}>
);
}
function TweakRadio({ label, value, options, onChange }) {
const trackRef = React.useRef(null);
const [dragging, setDragging] = React.useState(false);
// The active value is read by pointer-move handlers attached for the lifetime
// of a drag — ref it so a stale closure doesn't fire onChange for every move.
const valueRef = React.useRef(value);
valueRef.current = value;
// Segments wrap mid-word once per-segment width runs out. The track is
// ~248px (280 panel − 28 body pad − 4 seg pad), each button loses 12px
// to its own padding, and 11.5px system-ui averages ~6.3px/char — so 2
// options fit ~16 chars each, 3 fit ~10. Past that (or >3 options), fall
// back to a dropdown rather than wrap.
const labelLen = (o) => String(typeof o === 'object' ? o.label : o).length;
const maxLen = options.reduce((m, o) => Math.max(m, labelLen(o)), 0);
const fitsAsSegments = maxLen <= ({ 2: 16, 3: 10 }[options.length] ?? 0);
if (!fitsAsSegments) {
// emits strings — map back to the original option value so the
// fallback stays type-preserving (numbers, booleans) like the segment path.
const resolve = (s) => {
const m = options.find((o) => String(typeof o === 'object' ? o.value : o) === s);
return m === undefined ? s : typeof m === 'object' ? m.value : m;
};
return onChange(resolve(s))} />;
}
const opts = options.map((o) => (typeof o === 'object' ? o : { value: o, label: o }));
const idx = Math.max(0, opts.findIndex((o) => o.value === value));
const n = opts.length;
const segAt = (clientX) => {
const r = trackRef.current.getBoundingClientRect();
const inner = r.width - 4;
const i = Math.floor(((clientX - r.left - 2) / inner) * n);
return opts[Math.max(0, Math.min(n - 1, i))].value;
};
const onPointerDown = (e) => {
setDragging(true);
const v0 = segAt(e.clientX);
if (v0 !== valueRef.current) onChange(v0);
const move = (ev) => {
if (!trackRef.current) return;
const v = segAt(ev.clientX);
if (v !== valueRef.current) onChange(v);
};
const up = () => {
setDragging(false);
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerup', up);
};
window.addEventListener('pointermove', move);
window.addEventListener('pointerup', up);
};
return (
{opts.map((o) => (
{o.label}
))}
);
}
function TweakSelect({ label, value, options, onChange }) {
return (
onChange(e.target.value)}>
{options.map((o) => {
const v = typeof o === 'object' ? o.value : o;
const l = typeof o === 'object' ? o.label : o;
return {l} ;
})}
);
}
function TweakText({ label, value, placeholder, onChange }) {
return (
onChange(e.target.value)} />
);
}
function TweakNumber({ label, value, min, max, step = 1, unit = '', onChange }) {
const clamp = (n) => {
if (min != null && n < min) return min;
if (max != null && n > max) return max;
return n;
};
const startRef = React.useRef({ x: 0, val: 0 });
const onScrubStart = (e) => {
e.preventDefault();
startRef.current = { x: e.clientX, val: value };
const decimals = (String(step).split('.')[1] || '').length;
const move = (ev) => {
const dx = ev.clientX - startRef.current.x;
const raw = startRef.current.val + dx * step;
const snapped = Math.round(raw / step) * step;
onChange(clamp(Number(snapped.toFixed(decimals))));
};
const up = () => {
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerup', up);
};
window.addEventListener('pointermove', move);
window.addEventListener('pointerup', up);
};
return (
{label}
onChange(clamp(Number(e.target.value)))} />
{unit && {unit} }
);
}
// Relative-luminance contrast pick — checkmarks drawn over a swatch need to
// read on both #111 and #fafafa without per-option configuration. Hex input
// only (#rgb / #rrggbb); named or rgb()/hsl() colors fall through to "light".
function __twkIsLight(hex) {
const h = String(hex).replace('#', '');
const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, '0');
const n = parseInt(x.slice(0, 6), 16);
if (Number.isNaN(n)) return true;
const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
return r * 299 + g * 587 + b * 114 > 148000;
}
const __TwkCheck = ({ light }) => (
);
// TweakColor — curated color/palette picker. Each option is either a single
// hex string or an array of 1-5 hex strings; the card adapts — a lone color
// renders solid, a palette renders colors[0] as the hero (left ~2/3) with the
// rest stacked in a sharp column on the right. onChange emits the
// option in the shape it was passed (string stays string, array stays array).
// Without options it falls back to the native color input for back-compat.
function TweakColor({ label, value, options, onChange }) {
if (!options || !options.length) {
return (
);
}
// Native emits lowercase hex per the HTML spec, so
// compare case-insensitively. String() guards JSON.stringify(undefined),
// which returns the primitive undefined (no .toLowerCase).
const key = (o) => String(JSON.stringify(o)).toLowerCase();
const cur = key(value);
return (
{options.map((o, i) => {
const colors = Array.isArray(o) ? o : [o];
const [hero, ...rest] = colors;
const sup = rest.slice(0, 4);
const on = key(o) === cur;
return (
onChange(o)}>
{sup.length > 0 && (
{sup.map((c, j) => )}
)}
{on && <__TwkCheck light={__twkIsLight(hero)} />}
);
})}
);
}
function TweakButton({ label, onClick, secondary = false }) {
return (
{label}
);
}
Object.assign(window, {
useTweaks, TweaksPanel, TweakSection, TweakRow,
TweakSlider, TweakToggle, TweakRadio, TweakSelect,
TweakText, TweakNumber, TweakColor, TweakButton,
});
/* ===== hooks.jsx ===== */
// Shared hooks + small primitives
const { useState, useEffect, useRef, useCallback } = React;
// Reveal-on-scroll: returns a ref to attach. Adds .in when visible.
function useReveal(opts = {}) {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const targets = [];
if (el.matches?.(".reveal")) targets.push(el);
el.querySelectorAll?.(".reveal").forEach((n) => targets.push(n));
const show = (t) => t.classList.add("in");
// Immediately reveal anything already within (or near) the viewport — robust to
// capture/offscreen contexts where IntersectionObserver may not fire.
const revealInView = () => {
const vh = window.innerHeight || 800;
targets.forEach((t) => {
const r = t.getBoundingClientRect();
if (r.top < vh * 0.96 && r.bottom > 0) show(t);
});
};
revealInView();
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) { show(e.target); io.unobserve(e.target); }
});
}, { threshold: opts.threshold ?? 0.12, rootMargin: opts.rootMargin ?? "0px 0px -6% 0px" });
targets.forEach((t) => { if (!t.classList.contains("in")) io.observe(t); });
// Safety net: if observers never fire (headless/offscreen), reveal everything.
const fallback = setTimeout(() => targets.forEach(show), 1600);
return () => { io.disconnect(); clearTimeout(fallback); };
}, []);
return ref;
}
// Count-up number when it scrolls into view
function CountUp({ value, decimals = 0, suffix = "", duration = 1500 }) {
const ref = useRef(null);
const [disp, setDisp] = useState(0);
useEffect(() => {
const el = ref.current;
if (!el) return;
let raf, started = false;
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting && !started) {
started = true;
const t0 = performance.now();
const tick = (now) => {
const p = Math.min(1, (now - t0) / duration);
const eased = 1 - Math.pow(1 - p, 3);
setDisp(value * eased);
if (p < 1) raf = requestAnimationFrame(tick);
else setDisp(value);
};
raf = requestAnimationFrame(tick);
io.unobserve(e.target);
}
});
}, { threshold: 0.5 });
io.observe(el);
return () => { io.disconnect(); cancelAnimationFrame(raf); };
}, [value]);
return {disp.toFixed(decimals)}{suffix} ;
}
// Magnetic wrapper — element drifts slightly toward cursor
function Magnetic({ strength = 0.32, children, className, style }) {
const ref = useRef(null);
const onMove = useCallback((e) => {
const el = ref.current; if (!el) return;
const r = el.getBoundingClientRect();
const x = (e.clientX - (r.left + r.width / 2)) * strength;
const y = (e.clientY - (r.top + r.height / 2)) * strength;
el.style.transform = `translate(${x}px, ${y}px)`;
}, [strength]);
const reset = useCallback(() => {
const el = ref.current; if (el) el.style.transform = "translate(0,0)";
}, []);
return (
{children}
);
}
// Section scaffold with eyebrow/number + title
function SectionHead({ num, eyebrow, title }) {
return (
);
}
Object.assign(window, { useReveal, CountUp, Magnetic, SectionHead });
/* ===== sections-top.jsx ===== */
// Nav + Hero + About
const { useState: useStateT, useEffect: useEffectT, useRef: useRefT } = React;
const P = window.PORTFOLIO;
const NAV_LINKS = [
{ id: "research", label: "Research" },
{ id: "publications", label: "Publications" },
{ id: "experience", label: "Experience" },
{ id: "achievements", label: "Recognition" },
{ id: "contact", label: "Contact" },
];
function Nav() {
const [solid, setSolid] = useStateT(false);
const [open, setOpen] = useStateT(false);
useEffectT(() => {
const onScroll = () => setSolid(window.scrollY > 80);
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
const go = (id) => {
setOpen(false);
document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" });
};
return (
{NAV_LINKS.map((l) => (
{ e.preventDefault(); go(l.id); }}
className="navlink" style={{ fontFamily: "var(--mono)", fontSize: 12, letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--muted)" }}>
{l.label}
))}
Download CV ↓
setOpen(!open)} aria-label="Menu" style={{
display: "none", background: "none", border: "1px solid var(--line)", borderRadius: 999,
color: "var(--ink)", padding: "8px 14px", fontFamily: "var(--mono)", fontSize: 12, cursor: "pointer",
}}>{open ? "Close" : "Menu"}
{open && (
)}
);
}
function Hero() {
const ref = useReveal();
const imgWrap = useRefT(null);
// parallax drift on the portrait
useEffectT(() => {
const onScroll = () => {
const y = window.scrollY;
if (imgWrap.current) imgWrap.current.style.transform = `translateY(${y * 0.06}px)`;
};
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
const keywords = ["Fuzzy MCDM", "Graph Theory", "Machine Learning", "Decision Science", "Topological Indices", "Computational Math"];
return (
{/* faint backdrop glyph */}
∑
PhD Mathematics · Researcher & Educator
Dr.
Muhammad
Umar Mirza
{P.tagline}
{/* Portrait */}
Bahawalnagar · Punjab
Open to collaboration
{/* keyword marquee */}
{[...keywords, ...keywords, ...keywords].map((k, i) => (
{k}✦
))}
);
}
function About() {
const ref = useReveal();
return (
{P.bio.map((para, i) => (
{para}
))}
{P.stats.map((s, i) => (
))}
);
}
Object.assign(window, { Nav, Hero, About });
/* ===== sections-mid.jsx ===== */
// Research expertise + Publications
const { useState: useStateM } = React;
const PM = window.PORTFOLIO;
function Research() {
const ref = useReveal();
return (
{PM.expertise.map((e, i) => (
0{i + 1}
{e.title}
{e.desc}
))}
);
}
function Publications() {
const ref = useReveal();
const [filter, setFilter] = useStateM("All");
const list = PM.publications.map((p, i) => ({ ...p, n: i + 1 }));
const shown = filter === "All" ? list : list.filter((p) => p.tags.includes(filter));
return (
peer-reviewed publications
{/* filters */}
{PM.pubFilters.map((f) => {
const active = f === filter;
const count = f === "All" ? list.length : list.filter((p) => p.tags.includes(f)).length;
return (
setFilter(f)} className="filter-chip" style={{
fontFamily: "var(--mono)", fontSize: 12, letterSpacing: "0.04em", cursor: "pointer",
padding: "8px 15px", borderRadius: 999, transition: "all .3s var(--ease)",
border: `1px solid ${active ? "var(--ink)" : "var(--line)"}`,
background: active ? "var(--ink)" : "transparent",
color: active ? "var(--bg)" : "var(--muted)",
}}>
{f} {count}
);
})}
);
}
Object.assign(window, { Research, Publications });
/* ===== sections-bottom.jsx ===== */
// Experience, Education, Skills, Recognition, Speaking, Contact, Footer
const { useState: useStateB, useEffect: useEffectB, useRef: useRefB } = React;
const PB = window.PORTFOLIO;
function Experience() {
const ref = useReveal();
return (
{/* timeline */}
{PB.experience.map((x, i) => (
{x.period}
{x.role}
{x.org}
{x.points.map((pt, j) => (
—
{pt}
))}
))}
{/* education + skills */}
Education
{PB.education.map((e, i) => (
{e.period}
{e.degree}
{e.school}
{e.detail}
))}
Tools & Methods
{PB.tools.map((t, i) => (
))}
);
}
function SkillBar({ name, level, delay }) {
const ref = useRefB(null);
const [w, setW] = useStateB(0);
useEffectB(() => {
const el = ref.current; if (!el) return;
const io = new IntersectionObserver((ents) => {
ents.forEach((e) => { if (e.isIntersecting) { setTimeout(() => setW(level), delay); io.unobserve(e.target); } });
}, { threshold: 0.6 });
io.observe(el);
return () => io.disconnect();
}, [level, delay]);
return (
{name}
{w}
);
}
function Recognition() {
const ref = useReveal();
return (
{PB.achievements.map((a, i) => (
{String(i + 1).padStart(2, "0")}
{a}
))}
Speaking
{PB.speaking.map((s, i) => (
{s.title}
{s.venue}
{s.note}
))}
Languages
{PB.languages.map((l) => (
{l}
))}
);
}
function Contact() {
const ref = useReveal();
return (
);
}
function Footer() {
return (
);
}
Object.assign(window, { Experience, Recognition, Contact, Footer });
/* ===== app.jsx ===== */
// Mount
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"displayFont": "Newsreader",
"accent": "#7fd18a",
"showMarquee": true,
"showGrid": true,
"showCollab": true,
"animations": true
}/*EDITMODE-END*/;
function App() {
const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
useEffect(() => {
const root = document.documentElement;
root.style.setProperty("--serif", `"${t.displayFont}", Georgia, "Times New Roman", serif`);
root.style.setProperty("--dot", t.accent);
root.classList.toggle("no-marquee", !t.showMarquee);
root.classList.toggle("no-grid", !t.showGrid);
root.classList.toggle("no-collab", !t.showCollab);
root.classList.toggle("tweak-no-anim", !t.animations);
}, [t]);
return (
setTweak("displayFont", v)} />
setTweak("accent", v)} />
setTweak("showMarquee", v)} />
setTweak("showGrid", v)} />
setTweak("showCollab", v)} />
setTweak("animations", v)} />
);
}
ReactDOM.createRoot(document.getElementById("root")).render( );