/* 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': , 'arrow-up-right': , 'arrow-up': , 'arrow-down': , 'sparkles': <>, 'zap': , 'play': , 'pause': <>, 'send': , 'mic': <>, 'x': , 'check': , 'plus': , 'minus': , 'search': <>, 'filter': , 'calendar': <>, 'map-pin': <>, 'users': <>, 'home': , 'layout-dashboard': <>, 'newspaper': <>, 'bell': <>, 'trophy': <>, 'flame': , 'target': <>, 'chart-line': , 'chart-bar': <>, 'chart-area': , 'eye': <>, 'eye-off': <>, 'download': <>, 'share': <>, 'refresh': <>, 'menu': , 'chevron-down': , 'chevron-right': , 'chevron-left': , 'star': , 'shield': , 'shield-check': <>, 'sticky-note': <>, 'briefcase': <>, 'package': <>, 'compass': <>, 'message-square': , 'volume-2': <>, 'volume-x': <>, 'lightbulb': <>, 'globe': <>, 'rocket': <>, 'check-circle': <>, 'alert-circle': <>, 'qr-code': <>, 'mail': <>, 'linkedin': <>, 'youtube': <>, 'twitter': , 'instagram': <>, 'meta': , 'lock': <>, 'sliders': <>, 'arrow-up-down': , 'circle-dot': <>, 'gift': <>, 'crown': , 'medal': <>, 'flag': <>, 'lightning': , 'note': , 'cube': , 'heart': , 'message': , 'bookmark': , 'clock': <>, 'rotate-ccw': <>, 'message-circle': , 'phone': , }; const p = paths[name] || ; return ( {p} ); }; // ===== Brand ===== const Brand = ({ onClick }) => ( { e.preventDefault(); onClick && onClick(); }}> Marcianus 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 (
{items.map(t => (
{t.title}
{t.desc &&
{t.desc}
}
setItems(prev => prev.filter(p => p.id !== t.id))}>
))}
); }; // ===== Animated number ===== const AnimNum = ({ value, prefix = '', suffix = '', duration = 800, decimals = 0 }) => { const [v, setV] = useState(value); const ref = useRef(value); useEffect(() => { const start = ref.current; const end = value; const t0 = performance.now(); let raf; const step = (t) => { const k = Math.min(1, (t - t0) / duration); const e = 1 - Math.pow(1 - k, 3); const cur = start + (end - start) * e; setV(cur); if (k < 1) raf = requestAnimationFrame(step); else ref.current = end; }; raf = requestAnimationFrame(step); return () => cancelAnimationFrame(raf); }, [value, duration]); const display = decimals ? v.toFixed(decimals) : Math.round(v).toLocaleString('pt-PT'); return <>{prefix}{display}{suffix}; }; // ===== Sparkline SVG ===== const Sparkline = ({ data, color = '#A78BFA', height = 24, fill = true }) => { const max = Math.max(...data); const min = Math.min(...data); const range = max - min || 1; const w = 100; const h = height; const pts = data.map((d, i) => { const x = (i / (data.length - 1)) * w; const y = h - ((d - min) / range) * (h - 2) - 1; return `${x},${y}`; }); const path = `M ${pts.join(' L ')}`; const area = `${path} L ${w},${h} L 0,${h} Z`; return ( {fill && } ); }; // ===== expose ===== Object.assign(window, { Icon, Brand, SoundFX, fireConfetti, ToastBus, ToastHost, AnimNum, Sparkline });