// FloatingDock — a draggable "park-anywhere" glass FAB that blooms open into a // creative arc of shortcuts: Pricing · EMI · Annotate. Lives globally (mounted // in App, above every screen), remembers its position across screens + reloads, // and fans its actions toward open space whichever corner you leave it in. function ensureDockKeys() { if (typeof document === 'undefined' || document.getElementById('uni-dock-keys')) return; const s = document.createElement('style'); s.id = 'uni-dock-keys'; s.textContent = ` @keyframes dockPulse { 0%,100%{ box-shadow:0 10px 30px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.28), 0 0 0 0 rgba(201,160,94,0.45);} 50%{ box-shadow:0 10px 30px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.28), 0 0 0 14px rgba(201,160,94,0);} } `; document.head.appendChild(s); } // liquid-glass disc used by the FAB + shortcuts const dockGlass = { background:'linear-gradient(155deg, rgba(34,28,20,0.78) 0%, rgba(20,16,11,0.74) 100%)', border:'1px solid rgba(255,248,230,0.34)', backdropFilter:'blur(18px) saturate(1.5)', WebkitBackdropFilter:'blur(18px) saturate(1.5)', boxShadow:'0 12px 32px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.26), inset 0 -1px 0 rgba(0,0,0,0.3)', }; const DockGlyph = { plus: (p) => , emi: (p) => , pen: (p) => , }; // live canvas height (adaptive: 1600 on 16:10 → up to 1920 on iPad 4:3). The FAB // anchors to the REAL bottom so it never floats mid-screen on the taller canvas. const dockLiveH = () => (typeof window !== 'undefined' && window.UNIVERSE_CANVAS && window.UNIVERSE_CANVAS.H) || 1600; const dockHasCustomPos = () => { try { const s = JSON.parse(localStorage.getItem('uni-fab-pos')); return !!(s && typeof s.x === 'number'); } catch (e) { return false; } }; function FloatingDock() { ensureDockKeys(); const W = 2560, FAB = 86, MARGIN = 24; const [H, setH] = React.useState(dockLiveH); const [open, setOpen] = React.useState(false); const [annot, setAnnot] = React.useState(false); const [pos, setPos] = React.useState(() => { try { const s = JSON.parse(localStorage.getItem('uni-fab-pos')); if (s && typeof s.x === 'number') return s; } catch (e) {} return { x: W - 110, y: dockLiveH() - 120 }; }); const posRef = React.useRef(pos); posRef.current = pos; // keep the FAB pinned to the live bottom-right as the canvas height adapts // (orientation / device change). A user-dragged custom position is preserved. React.useEffect(() => { const onResize = () => { const nh = dockLiveH(); setH(nh); if (!dockHasCustomPos()) setPos({ x: W - 110, y: nh - 120 }); }; onResize(); window.addEventListener('resize', onResize); window.addEventListener('orientationchange', onResize); if (window.visualViewport) window.visualViewport.addEventListener('resize', onResize); return () => { window.removeEventListener('resize', onResize); window.removeEventListener('orientationchange', onResize); if (window.visualViewport) window.visualViewport.removeEventListener('resize', onResize); }; }, []); const drag = React.useRef({ active:false, moved:false, sx:0, sy:0, ox:0, oy:0, scale:1 }); const bezelScale = () => { const b = document.querySelector('.tablet-bezel'); if (!b) return 1; const r = b.getBoundingClientRect(); return (r.width / b.offsetWidth) || 1; }; const onMove = (e) => { const d = drag.current; if (!d.active) return; const dx = (e.clientX - d.sx) / d.scale, dy = (e.clientY - d.sy) / d.scale; if (Math.hypot(dx, dy) > 6) d.moved = true; const nx = clamp(d.ox + dx, FAB/2 + MARGIN, W - FAB/2 - MARGIN); const ny = clamp(d.oy + dy, FAB/2 + MARGIN, H - FAB/2 - MARGIN); setPos({ x: nx, y: ny }); }; const onUp = () => { const d = drag.current; window.removeEventListener('pointermove', onMove); window.removeEventListener('pointerup', onUp); if (!d.moved) { setOpen(o => !o); } else { try { localStorage.setItem('uni-fab-pos', JSON.stringify(posRef.current)); } catch (e) {} } d.active = false; }; const onDown = (e) => { e.stopPropagation(); e.preventDefault(); drag.current = { active:true, moved:false, sx:e.clientX, sy:e.clientY, ox:pos.x, oy:pos.y, scale:bezelScale() }; window.addEventListener('pointermove', onMove); window.addEventListener('pointerup', onUp); }; const items = [ { key:'price', label:'Pricing', glyph:'₹', onClick:() => { setOpen(false); window.dispatchEvent(new CustomEvent('uni-open-tool', { detail:{ tool:'price' } })); } }, { key:'emi', label:'EMI', icon:DockGlyph.emi, onClick:() => { setOpen(false); window.dispatchEvent(new CustomEvent('uni-open-tool', { detail:{ tool:'emi' } })); } }, { key:'pen', label:'Annotate', icon:DockGlyph.pen, onClick:() => { setOpen(false); setAnnot(a => !a); } }, ]; // fan toward the open quadrant (away from the nearest corner) const hx = pos.x > W/2 ? -1 : 1; const vy = pos.y > H/2 ? -1 : 1; // screen y-down; -1 = upward const base = Math.atan2(-vy, hx); // math angle (y-up) into open space const SPREAD = 42 * Math.PI / 180; return ( {annot && setAnnot(false)}/>} {/* no backdrop overlay — the dock just blooms in place, screen stays clear */}
{/* shortcut bloom */} {items.map((it, i) => { const ang = base + (i - 1) * SPREAD; const R = 138 + i * 4; const dx = Math.cos(ang) * R, dy = -Math.sin(ang) * R; return (
{it.label}
); })} {/* main FAB */}
); } // ── AnnotateOverlay — freehand gold pen over the current screen ────────────── function AnnotateOverlay({ onClose }) { const ref = React.useRef(null); const st = React.useRef({ on:false, last:null }); const W = 2560, H = 1600; const toC = (e) => { const c = ref.current, r = c.getBoundingClientRect(); return { x: (e.clientX - r.left) * (W / r.width), y: (e.clientY - r.top) * (H / r.height) }; }; const ctxOf = () => { const ctx = ref.current.getContext('2d'); ctx.strokeStyle = '#c9a05e'; ctx.lineWidth = 4; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.shadowColor = 'rgba(201,160,94,0.55)'; ctx.shadowBlur = 7; return ctx; }; const down = (e) => { e.preventDefault(); st.current.on = true; st.current.last = toC(e); }; const move = (e) => { if (!st.current.on) return; const ctx = ctxOf(), p = toC(e), l = st.current.last; ctx.beginPath(); ctx.moveTo(l.x, l.y); ctx.lineTo(p.x, p.y); ctx.stroke(); st.current.last = p; }; const end = () => { st.current.on = false; }; const clear = () => ref.current.getContext('2d').clearRect(0, 0, W, H); return (
✎ ANNOTATE
); } window.FloatingDock = FloatingDock;