// script.js - shared tab/UI logic (() => { 'use strict'; document.addEventListener('DOMContentLoaded', () => { const ACTIVE_TAB_KEY = 'balloonDesigner:activeTab:v1'; // Ensure shared helpers are ready if (!window.shared) return; const { clamp, clamp01 } = window.shared; const { FLAT_COLORS } = window.shared; // Modal helpers const messageModal = document.getElementById('message-modal'); const modalText = document.getElementById('modal-text'); const modalCloseBtn = document.getElementById('modal-close-btn'); function showModal(msg) { if (!messageModal || !modalText) return; modalText.textContent = msg; messageModal.classList.remove('hidden'); } function hideModal() { messageModal?.classList.add('hidden'); } modalCloseBtn?.addEventListener('click', hideModal); // Tab elements const orgSheet = document.getElementById('controls-panel'); const claSheet = document.getElementById('classic-controls-panel'); const wallSheet = document.getElementById('wall-controls-panel'); const orgSection = document.getElementById('tab-organic'); const claSection = document.getElementById('tab-classic'); const wallSection = document.getElementById('tab-wall'); const tabBtns = Array.from(document.querySelectorAll('#mode-tabs .tab-btn')); const mobileActionBar = document.getElementById('mobile-action-bar'); // Export buttons const exportPngBtn = document.querySelector('[data-export="png"]'); const exportSvgBtn = document.querySelector('[data-export="svg"]'); const { XLINK_NS } = window.shared || {}; const MOBILE_TAB_DEFAULT = 'controls'; const MOBILE_TAB_PREFIX = 'designer:mobileTab:'; const exportModal = document.getElementById('export-modal'); const exportModalClose = document.getElementById('export-modal-close'); // Generic color picker modal (used by replace chips) (function initColorPickerModal() { const modal = document.getElementById('color-picker-modal'); const grid = document.getElementById('color-picker-grid'); const titleEl = document.getElementById('color-picker-title'); const subtitleEl = document.getElementById('color-picker-subtitle'); const closeBtn = document.getElementById('color-picker-close'); if (!modal || !grid || !titleEl || !subtitleEl) return; const close = () => modal.classList.add('hidden'); closeBtn?.addEventListener('click', close); modal.addEventListener('click', (e) => { if (e.target === modal) close(); }); const setChipStyle = (el, meta) => { if (!el || !meta) return; if (meta.image) { el.style.backgroundImage = `url("${meta.image}")`; el.style.backgroundColor = meta.hex || '#fff'; el.style.backgroundSize = 'cover'; el.style.backgroundPosition = `${(meta.imageFocus?.x ?? 0.5) * 100}% ${(meta.imageFocus?.y ?? 0.5) * 100}%`; } else { el.style.backgroundImage = 'none'; el.style.backgroundColor = meta.hex || '#fff'; } }; window.openColorPicker = ({ title = 'Choose a color', subtitle = '', items = [], onSelect }) => { titleEl.textContent = title; subtitleEl.textContent = subtitle; grid.innerHTML = ''; items.forEach(item => { const meta = item.meta || (Number.isInteger(item.idx) ? FLAT_COLORS?.[item.idx] : null) || {}; const sw = document.createElement('button'); sw.type = 'button'; sw.className = 'color-option'; sw.setAttribute('aria-label', item.label || meta.name || meta.hex || 'Color'); const swatch = document.createElement('div'); swatch.className = 'swatch'; setChipStyle(swatch, meta.hex ? meta : { hex: item.hex }); const label = document.createElement('div'); label.className = 'label'; label.textContent = item.label || meta.name || meta.hex || 'Color'; const metaLine = document.createElement('div'); metaLine.className = 'meta'; metaLine.textContent = item.metaText || meta.hex || ''; sw.appendChild(swatch); sw.appendChild(label); sw.appendChild(metaLine); sw.addEventListener('click', () => { close(); onSelect?.(item); }); grid.appendChild(sw); }); modal.classList.remove('hidden'); }; })(); function getImageHref(el) { return el.getAttribute('href') || (XLINK_NS ? el.getAttributeNS(XLINK_NS, 'href') : null); } function setImageHref(el, val) { el.setAttribute('href', val); if (XLINK_NS) el.setAttributeNS(XLINK_NS, 'xlink:href', val); } // Update export buttons visibility depending on tab/design window.updateExportButtonVisibility = () => { const tab = detectCurrentTab(); const isWall = tab === '#tab-wall'; const isClassic = tab === '#tab-classic'; const hasClassic = !!document.querySelector('#classic-display svg'); const hasWall = !!document.querySelector('#wall-display svg'); const enable = (btn, on) => { if (btn) btn.disabled = !on; }; if (isClassic) enable(exportPngBtn, hasClassic); else enable(exportPngBtn, true); if (isClassic) enable(exportSvgBtn, hasClassic); else if (isWall) enable(exportSvgBtn, hasWall); else enable(exportSvgBtn, true); }; async function buildClassicSvgExportFromDom() { const svgElement = document.querySelector('#classic-display svg'); if (!svgElement) throw new Error('Classic design not found. Please create a design first.'); const clonedSvg = svgElement.cloneNode(true); const allImages = Array.from(clonedSvg.querySelectorAll('image')); await Promise.all(allImages.map(async img => { const href = getImageHref(img); if (!href || href.startsWith('data:')) return; const dataUrl = await window.shared.imageUrlToDataUrl(href); if (dataUrl) setImageHref(img, dataUrl); })); if (!clonedSvg.getAttribute('xmlns')) clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); if (!clonedSvg.getAttribute('xmlns:xlink') && XLINK_NS) clonedSvg.setAttribute('xmlns:xlink', XLINK_NS); let width = Number(clonedSvg.getAttribute('width')) || null; let height = Number(clonedSvg.getAttribute('height')) || null; const viewBox = (clonedSvg.getAttribute('viewBox') || '').split(/\s+/).map(Number); if ((!width || !height) && viewBox.length === 4 && viewBox.every(v => Number.isFinite(v))) { width = width || viewBox[2]; height = height || viewBox[3]; } if (!width || !height) { const vbW = svgElement.clientWidth || 1000; const vbH = svgElement.clientHeight || 1000; width = width || vbW; height = height || vbH; } clonedSvg.setAttribute('width', width); clonedSvg.setAttribute('height', height); return { svgString: new XMLSerializer().serializeToString(clonedSvg), width, height }; } async function buildWallSvgExportFromDom() { const svgElement = document.querySelector('#wall-display svg'); if (!svgElement) throw new Error('Wall design not found. Please create a design first.'); const clonedSvg = svgElement.cloneNode(true); const allImages = Array.from(clonedSvg.querySelectorAll('image')); await Promise.all(allImages.map(async img => { const href = getImageHref(img); if (!href || href.startsWith('data:')) return; const dataUrl = await window.shared.imageUrlToDataUrl(href); if (dataUrl) setImageHref(img, dataUrl); })); if (!clonedSvg.getAttribute('xmlns')) clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); if (!clonedSvg.getAttribute('xmlns:xlink') && XLINK_NS) clonedSvg.setAttribute('xmlns:xlink', XLINK_NS); let width = Number(clonedSvg.getAttribute('width')) || null; let height = Number(clonedSvg.getAttribute('height')) || null; const viewBox = (clonedSvg.getAttribute('viewBox') || '').split(/\s+/).map(Number); if ((!width || !height) && viewBox.length === 4 && viewBox.every(v => Number.isFinite(v))) { width = width || viewBox[2]; height = height || viewBox[3]; } if (!width || !height) { const vbW = svgElement.clientWidth || 1000; const vbH = svgElement.clientHeight || 1000; width = width || vbW; height = height || vbH; } clonedSvg.setAttribute('width', width); clonedSvg.setAttribute('height', height); return { svgString: new XMLSerializer().serializeToString(clonedSvg), width, height }; } // Export routing async function buildWallSvgExportFromDom() { const svgElement = document.querySelector('#wall-display svg'); if (!svgElement) throw new Error('Wall design not found. Please create a design first.'); const clonedSvg = svgElement.cloneNode(true); const allImages = Array.from(clonedSvg.querySelectorAll('image')); await Promise.all(allImages.map(async img => { const href = getImageHref(img); if (!href || href.startsWith('data:')) return; const dataUrl = await window.shared.imageUrlToDataUrl(href); if (dataUrl) setImageHref(img, dataUrl); })); if (!clonedSvg.getAttribute('xmlns')) clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); if (!clonedSvg.getAttribute('xmlns:xlink') && XLINK_NS) clonedSvg.setAttribute('xmlns:xlink', XLINK_NS); let width = Number(clonedSvg.getAttribute('width')) || null; let height = Number(clonedSvg.getAttribute('height')) || null; const viewBox = (clonedSvg.getAttribute('viewBox') || '').split(/\s+/).map(Number); if ((!width || !height) && viewBox.length === 4 && viewBox.every(v => Number.isFinite(v))) { width = width || viewBox[2]; height = height || viewBox[3]; } if (!width || !height) { const vbW = svgElement.clientWidth || 1000; const vbH = svgElement.clientHeight || 1000; width = width || vbW; height = height || vbH; } clonedSvg.setAttribute('width', width); clonedSvg.setAttribute('height', height); const svgString = new XMLSerializer().serializeToString(clonedSvg); return { svgString, width, height }; } async function inlineSvgImages(svgString) { try { const parser = new DOMParser(); const doc = parser.parseFromString(svgString, 'image/svg+xml'); if (doc.querySelector('parsererror')) return svgString; const imgs = Array.from(doc.querySelectorAll('image')); await Promise.all(imgs.map(async img => { const href = img.getAttribute('href') || img.getAttribute('xlink:href'); if (!href || href.startsWith('data:')) return; const dataUrl = await window.shared.imageUrlToDataUrl(href); if (dataUrl) { img.setAttribute('href', dataUrl); img.removeAttribute('xlink:href'); } })); return new XMLSerializer().serializeToString(doc.documentElement); } catch (err) { console.warn('[Export] inlineSvgImages failed', err); return svgString; } } async function svgStringToPng(svgString, width, height) { const { PNG_EXPORT_SCALE } = window.shared; if (!/xmlns=/.test(svgString)) { svgString = svgString.replace(' new Promise((resolve, reject) => { img.onload = resolve; img.onerror = () => reject(new Error('Could not rasterize SVG.')); img.src = src; }); // Prefer base64 data URL to avoid parser issues with raw text. const svg64 = btoa(unescape(encodeURIComponent(svgString))); const dataUrl = `data:image/svg+xml;base64,${svg64}`; try { await loadSrc(dataUrl); } catch (e) { // Fallback to blob if base64 path fails. let blobUrl = null; try { const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' }); blobUrl = URL.createObjectURL(blob); await loadSrc(blobUrl); } finally { if (blobUrl) URL.revokeObjectURL(blobUrl); } } ctx2.drawImage(img, 0, 0, canvasEl.width, canvasEl.height); return canvasEl.toDataURL('image/png'); } function downloadSvg(svgString, filename) { const { download } = window.shared; const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' }); const url = URL.createObjectURL(blob); download(url, filename); setTimeout(() => URL.revokeObjectURL(url), 20000); } async function exportPng() { try { const tab = detectCurrentTab(); if (tab === '#tab-classic') { const { svgString, width, height } = window.ClassicExport?.buildClassicSvgPayload ? await window.ClassicExport.buildClassicSvgPayload() : await buildClassicSvgExportFromDom(); const pngUrl = await svgStringToPng(svgString, width, height); window.shared.download(pngUrl, 'classic_design.png'); return; } if (tab === '#tab-wall') { const { svgString, width, height } = await buildWallSvgExportFromDom(); const pngUrl = await svgStringToPng(svgString, width, height); window.shared.download(pngUrl, 'wall_design.png'); return; } const { svgString, width, height } = await window.organic.buildOrganicSvgPayload(); const pngUrl = await svgStringToPng(svgString, width, height); window.shared.download(pngUrl, 'balloon_design.png'); } catch (err) { console.error('[Export PNG] Failed:', err); showModal(err.message || 'Could not export PNG. Check console for details.'); } } async function exportSvg() { try { const tab = detectCurrentTab(); if (tab === '#tab-classic') { const { svgString, width, height } = window.ClassicExport?.buildClassicSvgPayload ? await window.ClassicExport.buildClassicSvgPayload() : await buildClassicSvgExportFromDom(); try { const pngUrl = await svgStringToPng(svgString, width, height); const cleanSvg = ` `; downloadSvg(cleanSvg, 'classic_design.svg'); } catch (pngErr) { console.warn('[Export SVG] PNG embed failed, falling back to vector-only SVG', pngErr); downloadSvg(svgString, 'classic_design.svg'); } return; } if (tab === '#tab-wall') { const { svgString, width, height } = await buildWallSvgExportFromDom(); downloadSvg(svgString, 'wall_design.svg'); return; } const { svgString } = await window.organic.buildOrganicSvgPayload(); downloadSvg(svgString, 'organic_design.svg'); } catch (err) { console.error('[Export] Failed:', err); showModal(err.message || 'Could not export. Check console for details.'); } } const openExportModal = () => { exportModal?.classList.remove('hidden'); }; const closeExportModal = () => { exportModal?.classList.add('hidden'); }; exportModalClose?.addEventListener('click', closeExportModal); exportModal?.addEventListener('click', (e) => { if (e.target === exportModal) closeExportModal(); }); document.body.addEventListener('click', e => { const choice = e.target.closest('[data-export-choice]'); if (choice) { const type = choice.dataset.exportChoice; closeExportModal(); if (type === 'png') exportPng(); else if (type === 'svg') exportSvg(); return; } const btn = e.target.closest('[data-export]'); if (!btn) return; e.preventDefault(); openExportModal(); }); // Tab logic function detectCurrentTab() { const bodyActive = document.body?.dataset?.activeTab; const activeBtn = document.querySelector('#mode-tabs .tab-btn.tab-active'); const classicVisible = !document.getElementById('tab-classic')?.classList.contains('hidden'); const organicVisible = !document.getElementById('tab-organic')?.classList.contains('hidden'); const wallVisible = !document.getElementById('tab-wall')?.classList.contains('hidden'); let id = bodyActive || activeBtn?.dataset?.target; if (!id) { if (classicVisible && !organicVisible && !wallVisible) id = '#tab-classic'; else if (organicVisible && !classicVisible && !wallVisible) id = '#tab-organic'; else if (wallVisible && !classicVisible && !organicVisible) id = '#tab-wall'; } if (!id) id = '#tab-organic'; if (document.body) document.body.dataset.activeTab = id; return id; } window.detectCurrentTab = detectCurrentTab; function updateSheets() { const tab = detectCurrentTab(); const hide = !window.matchMedia('(min-width: 1024px)').matches && document.body?.dataset?.controlsHidden === '1'; if (orgSheet) orgSheet.classList.toggle('hidden', hide || tab !== '#tab-organic'); if (claSheet) claSheet.classList.toggle('hidden', hide || tab !== '#tab-classic'); if (wallSheet) wallSheet.classList.toggle('hidden', hide || tab !== '#tab-wall'); } function updateMobileStacks(tabName) { const orgPanel = document.getElementById('controls-panel'); const claPanel = document.getElementById('classic-controls-panel'); const wallPanel = document.getElementById('wall-controls-panel'); const currentTab = detectCurrentTab(); const panel = currentTab === '#tab-classic' ? claPanel : (currentTab === '#tab-wall' ? wallPanel : orgPanel); const target = tabName || document.body?.dataset?.mobileTab || MOBILE_TAB_DEFAULT; const isHidden = document.body?.dataset?.controlsHidden === '1'; const isDesktop = window.matchMedia('(min-width: 1024px)').matches; if (!panel) return; const stacks = Array.from(panel.querySelectorAll('.control-stack')); if (!stacks.length) return; stacks.forEach(stack => { if (isHidden) { stack.style.display = 'none'; } else if (isDesktop) { stack.style.display = ''; } else { const show = stack.dataset.mobileTab === target; stack.style.display = show ? 'block' : 'none'; } }); } function setMobileTab(tab, mainTabId, skipPersist = false) { const name = tab || MOBILE_TAB_DEFAULT; const activeMainTab = mainTabId || detectCurrentTab(); if (!skipPersist && activeMainTab) { try { localStorage.setItem(`${MOBILE_TAB_PREFIX}${activeMainTab}`, name); } catch {} } const isDesktop = window.matchMedia('(min-width: 1024px)').matches; if (document.body) { document.body.dataset.mobileTab = name; delete document.body.dataset.controlsHidden; } // Ensure the current panel is not minimized/hidden when we select a tab. const panel = activeMainTab === '#tab-classic' ? document.getElementById('classic-controls-panel') : (activeMainTab === '#tab-wall' ? document.getElementById('wall-controls-panel') : document.getElementById('controls-panel')); panel?.classList.remove('minimized'); if (panel) panel.style.display = ''; updateSheets(); updateMobileStacks(name); const buttons = document.querySelectorAll('#mobile-tabbar .mobile-tab-btn'); buttons.forEach(btn => btn.setAttribute('aria-pressed', String(btn.dataset.mobileTab === name))); } window.__setMobileTab = setMobileTab; const NUDGE_POS_KEY = 'classic:nudgePos:v1'; const NUDGE_MARGIN = 12; const NUDGE_SIZE_HINT = { w: 180, h: 200 }; let floatingNudgeCollapsed = false; function clampNudgePos(pos, el) { const vw = window.innerWidth || 1024; const vh = window.innerHeight || 768; const rect = el?.getBoundingClientRect(); const w = rect?.width || NUDGE_SIZE_HINT.w; const h = rect?.height || NUDGE_SIZE_HINT.h; return { x: Math.min(Math.max(pos.x, NUDGE_MARGIN), Math.max(NUDGE_MARGIN, vw - w - NUDGE_MARGIN)), y: Math.min(Math.max(pos.y, NUDGE_MARGIN), Math.max(NUDGE_MARGIN, vh - h - NUDGE_MARGIN)) }; } let nudgePos = null; let nudgePosInitialized = false; function loadNudgePos(el) { try { const saved = JSON.parse(localStorage.getItem(NUDGE_POS_KEY)); if (saved && typeof saved.x === 'number' && typeof saved.y === 'number') return clampNudgePos(saved, el); } catch {} return clampNudgePos({ x: (window.innerWidth || 1024) - 240, y: 120 }, el); } function ensureNudgePos(el) { if (!nudgePos) nudgePos = loadNudgePos(el); return clampNudgePos(nudgePos, el); } function saveNudgePos(pos, el) { nudgePos = clampNudgePos(pos, el); try { localStorage.setItem(NUDGE_POS_KEY, JSON.stringify(nudgePos)); } catch {} return nudgePos; } function applyNudgePos(el, pos) { const p = clampNudgePos(pos || ensureNudgePos(el), el); el.style.left = `${p.x}px`; el.style.top = `${p.y}px`; el.style.right = 'auto'; el.style.bottom = 'auto'; nudgePos = p; nudgePosInitialized = true; } function updateFloatingNudge() { const el = document.getElementById('floating-topper-nudge'); if (!el) return; const classicActive = document.body?.dataset.activeTab === '#tab-classic'; const topperActive = document.body?.dataset.topperOverlay === '1'; const shouldShow = classicActive && topperActive; const shouldShowPanel = shouldShow && !floatingNudgeCollapsed; el.classList.toggle('hidden', !shouldShowPanel); el.classList.toggle('collapsed', floatingNudgeCollapsed); el.style.display = shouldShowPanel ? 'block' : 'none'; if (shouldShowPanel && !nudgePosInitialized) applyNudgePos(el, ensureNudgePos(el)); } function showFloatingNudge() { floatingNudgeCollapsed = false; updateFloatingNudge(); } function hideFloatingNudge() { floatingNudgeCollapsed = true; const nudge = document.getElementById('floating-topper-nudge'); if (nudge) { nudge.classList.add('hidden'); nudge.style.display = 'none'; } const tab = document.getElementById('floating-nudge-tab'); if (tab) tab.classList.remove('hidden'); updateFloatingNudge(); } window.__updateFloatingNudge = updateFloatingNudge; window.__showFloatingNudge = showFloatingNudge; window.__hideFloatingNudge = hideFloatingNudge; // Tab switching if (orgSection && claSection && tabBtns.length > 0) { let current = '#tab-organic'; const isMobileView = () => window.matchMedia('(max-width: 1023px)').matches; const updateMobileActionBarVisibility = () => { if (!mobileActionBar) return; const shouldShow = current === '#tab-organic' && isMobileView(); mobileActionBar.classList.toggle('hidden', !shouldShow); }; const wireMobileActionButtons = () => { const guardOrganic = () => current === '#tab-organic'; const clickBtn = (sel) => { if (!guardOrganic()) return; document.querySelector(sel)?.click(); }; const on = (id, fn) => document.getElementById(id)?.addEventListener('click', fn); on('mobile-act-undo', () => clickBtn('#tool-undo')); on('mobile-act-redo', () => clickBtn('#tool-redo')); on('mobile-act-eyedrop', () => clickBtn('#tool-eyedropper')); on('mobile-act-erase', () => { if (!guardOrganic()) return; const erase = document.getElementById('tool-erase'); const draw = document.getElementById('tool-draw'); const active = erase?.getAttribute('aria-pressed') === 'true'; (active ? draw : erase)?.click(); }); on('mobile-act-clear', () => clickBtn('#clear-canvas-btn-top')); on('mobile-act-export', () => clickBtn('[data-export="png"]')); }; wireMobileActionButtons(); window.addEventListener('resize', updateMobileActionBarVisibility); function setTab(id, isInitial = false) { if (!id || !document.querySelector(id)) id = '#tab-organic'; current = id; if (document.body) document.body.dataset.activeTab = id; orgSheet?.classList.remove('minimized'); claSheet?.classList.remove('minimized'); wallSheet?.classList.remove('minimized'); orgSection.classList.toggle('hidden', id !== '#tab-organic'); claSection.classList.toggle('hidden', id !== '#tab-classic'); wallSection?.classList.toggle('hidden', id !== '#tab-wall'); updateSheets(); updateFloatingNudge(); tabBtns.forEach(btn => { const active = btn.dataset.target === id; btn.classList.toggle('tab-active', active); btn.classList.toggle('tab-idle', !active); btn.setAttribute('aria-pressed', String(active)); }); if (!isInitial) { try { localStorage.setItem(ACTIVE_TAB_KEY, id); } catch {} } if (document.body) delete document.body.dataset.controlsHidden; const isOrganic = id === '#tab-organic'; const showHeaderColor = id !== '#tab-classic'; const clearTop = document.getElementById('clear-canvas-btn-top'); if (clearTop) { clearTop.classList.toggle('hidden', !isOrganic); clearTop.style.display = isOrganic ? '' : 'none'; } const headerActiveSwatch = document.getElementById('current-color-chip-global')?.closest('.flex'); headerActiveSwatch?.classList.toggle('hidden', !showHeaderColor); const savedMobile = (() => { try { return localStorage.getItem(`${MOBILE_TAB_PREFIX}${id}`) || MOBILE_TAB_DEFAULT; } catch {} return MOBILE_TAB_DEFAULT; })(); if (document.body) document.body.dataset.mobileTab = savedMobile; setMobileTab(savedMobile, id, true); orgSheet?.classList.toggle('hidden', id !== '#tab-organic'); claSheet?.classList.toggle('hidden', id !== '#tab-classic'); wallSheet?.classList.toggle('hidden', id !== '#tab-wall'); window.updateExportButtonVisibility(); updateMobileActionBarVisibility(); } tabBtns.forEach(btn => { btn.addEventListener('click', (e) => { const button = e.target.closest('button[data-target]'); if (button) setTab(button.dataset.target); }); }); let savedTab = null; try { savedTab = localStorage.getItem(ACTIVE_TAB_KEY); } catch {} setTab(savedTab || '#tab-organic', true); window.__whichTab = () => current; if (!document.body?.dataset?.mobileTab) { const initialMobile = (() => { try { return localStorage.getItem(`${MOBILE_TAB_PREFIX}${current}`) || MOBILE_TAB_DEFAULT; } catch {} return MOBILE_TAB_DEFAULT; })(); document.body.dataset.mobileTab = initialMobile; } setMobileTab(document.body.dataset.mobileTab, current, true); updateSheets(); updateMobileStacks(document.body.dataset.mobileTab); updateMobileActionBarVisibility(); } // Mobile tabbar (function initMobileTabs() { const buttons = Array.from(document.querySelectorAll('#mobile-tabbar .mobile-tab-btn')); if (!buttons.length) return; buttons.forEach(btn => { btn.addEventListener('click', () => { const tab = btn.dataset.mobileTab || 'controls'; const activeTabId = detectCurrentTab(); const panel = activeTabId === '#tab-classic' ? document.getElementById('classic-controls-panel') : (activeTabId === '#tab-wall' ? document.getElementById('wall-controls-panel') : document.getElementById('controls-panel')); const currentTab = document.body.dataset.mobileTab; if (tab === currentTab) { panel.classList.toggle('minimized'); } else { panel.classList.remove('minimized'); setMobileTab(tab); panel.scrollTop = 0; } }); }); const mq = window.matchMedia('(min-width: 1024px)'); const sync = () => { const current = document.body?.dataset?.mobileTab || MOBILE_TAB_DEFAULT; setMobileTab(current, detectCurrentTab(), true); updateFloatingNudge(); }; mq.addEventListener('change', sync); setMobileTab(document.body?.dataset?.mobileTab || MOBILE_TAB_DEFAULT, detectCurrentTab(), true); sync(); const nudgeToggle = document.getElementById('floating-nudge-toggle'); nudgeToggle?.addEventListener('click', () => { hideFloatingNudge(); }); const nudge = document.getElementById('floating-topper-nudge'); const nudgeHeader = document.querySelector('#floating-topper-nudge .floating-nudge-header'); if (nudge && nudgeHeader) { let dragId = null; let start = null; nudgeHeader.addEventListener('pointerdown', (e) => { if (e.target.closest('#floating-nudge-toggle')) return; dragId = e.pointerId; start = { x: e.clientX, y: e.clientY, pos: loadNudgePos(nudge) }; nudge.classList.add('dragging'); nudgeHeader.setPointerCapture(dragId); }); nudgeHeader.addEventListener('pointermove', (e) => { if (dragId === null || e.pointerId !== dragId) return; if (!start) return; const dx = e.clientX - start.x; const dy = e.clientY - start.y; const next = clampNudgePos({ x: start.pos.x + dx, y: start.pos.y + dy }); applyNudgePos(nudge, next); }); const endDrag = (e) => { if (dragId === null || e.pointerId !== dragId) return; const rect = nudge.getBoundingClientRect(); const next = clampNudgePos({ x: rect.left, y: rect.top }, nudge); saveNudgePos(next, nudge); nudge.classList.remove('dragging'); dragId = null; start = null; try { nudgeHeader.releasePointerCapture(e.pointerId); } catch {} }; nudgeHeader.addEventListener('pointerup', endDrag); nudgeHeader.addEventListener('pointercancel', endDrag); window.addEventListener('resize', () => { const pos = clampNudgePos(loadNudgePos(nudge), nudge); saveNudgePos(pos, nudge); nudgePosInitialized = false; applyNudgePos(nudge, pos); }); } })(); }); })();