/* Shared utility components and helpers — exposed on window */
const { useState, useEffect, useRef, useMemo, useCallback } = React;
// ===== Lucide icons (mini SVG set, 1.5px stroke) =====
const Icon = ({ name, size = 18, className = "", color }) => {
const paths = {
'arrow-right':
Marcianus Experience.
);
// ===== Sound effects (synth via WebAudio) =====
const SoundFX = (() => {
let ctx = null;
let muted = (typeof localStorage !== 'undefined' && localStorage.getItem('mds-muted') === '1');
const ensure = () => { if (!ctx && typeof AudioContext !== 'undefined') ctx = new (window.AudioContext || window.webkitAudioContext)(); return ctx; };
const blip = (freq = 880, dur = 0.07, type = 'sine', vol = 0.04) => {
if (muted) return;
const c = ensure(); if (!c) return;
const o = c.createOscillator(); const g = c.createGain();
o.type = type; o.frequency.value = freq;
g.gain.value = 0;
g.gain.linearRampToValueAtTime(vol, c.currentTime + 0.01);
g.gain.linearRampToValueAtTime(0, c.currentTime + dur);
o.connect(g); g.connect(c.destination);
o.start(); o.stop(c.currentTime + dur);
};
const chime = (notes = [880, 1320]) => { notes.forEach((f, i) => setTimeout(() => blip(f, 0.12, 'sine', 0.05), i * 80)); };
return {
tap: () => blip(660, 0.05, 'sine', 0.03),
success: () => chime([880, 1175, 1567]),
notify: () => chime([1320, 880]),
levelUp: () => chime([523, 659, 783, 1046]),
error: () => blip(220, 0.12, 'sawtooth', 0.04),
setMuted: (v) => { muted = v; try { localStorage.setItem('mds-muted', v ? '1' : '0'); } catch (e) {} },
isMuted: () => muted,
};
})();
// ===== Confetti =====
const fireConfetti = (origin = { x: window.innerWidth / 2, y: window.innerHeight / 2 }, colors = ['#FFD700', '#7B61FF', '#00D1FF', '#00C48C']) => {
const host = document.createElement('div');
host.className = 'confetti-host';
host.style.left = origin.x + 'px';
host.style.top = origin.y + 'px';
document.body.appendChild(host);
for (let i = 0; i < 24; i++) {
const p = document.createElement('div');
p.className = 'confetti-piece';
const angle = (Math.PI * 2 * i) / 24 + Math.random() * 0.4;
const dist = 60 + Math.random() * 80;
p.style.background = colors[i % colors.length];
p.style.transform = `translate(${Math.cos(angle) * dist}px, ${Math.sin(angle) * dist}px)`;
p.style.left = '0';
p.style.top = '0';
p.style.animationDelay = (Math.random() * 0.1) + 's';
host.appendChild(p);
}
setTimeout(() => host.remove(), 1600);
};
// ===== Toast manager =====
const ToastBus = (() => {
const subs = new Set();
let id = 0;
return {
push(t) { id++; const item = { id, ...t }; subs.forEach(fn => fn({ type: 'add', item })); setTimeout(() => subs.forEach(fn => fn({ type: 'remove', id: item.id })), t.duration || 4500); },
subscribe(fn) { subs.add(fn); return () => subs.delete(fn); },
};
})();
const ToastHost = () => {
const [items, setItems] = useState([]);
useEffect(() => ToastBus.subscribe((ev) => {
if (ev.type === 'add') setItems(prev => [...prev, ev.item]);
if (ev.type === 'remove') setItems(prev => prev.filter(p => p.id !== ev.id));
}), []);
return (