/** * Build the right-hand panel (controls + layers) — DOM creation, * controls innerHTML population, mobile bottom-sheet swipe behavior, * controls-panel re-parenting on mobile, slider value-chip layout * normalization, layer-panel header + mobile peek/expand swipe, and * the panel-width drag-resize handle. * * Owns its own event listeners (touch swipe gestures, mouse resize * drag). Returns the `rightPanel` element + the `panelResize` handle * + the inner `controls` element so the caller can wire any post- * mount tweaks. Reads state.container (for mobile re-parenting) and * state.color / state.brushSize / state.wandTolerance (initial slider * values). * * @param {{ * controlsHTML: (ctx: {color, brushSize, wandTolerance}) => string, * layerPanelHTML: () => string, * }} build * * @returns {{ * rightPanel: HTMLDivElement, * controls: HTMLDivElement, * layerPanel: HTMLDivElement, * panelResize: HTMLDivElement, * }} */ import { state } from '../state.js'; export function buildRightPanel({ controlsHTML, layerPanelHTML }) { const rightPanel = document.createElement('div'); rightPanel.className = 'ge-right-panel'; // Controls section. const controls = document.createElement('div'); controls.className = 'ge-controls'; // Swipe-down to dismiss on mobile. Tap the same tool again to bring // the sheet back. Only the top ~40 px (grab handle area) initiates // the gesture so taps on inputs/sliders inside the panel still work. { let sy = 0, dragging = false; controls.addEventListener('touchstart', (e) => { if (window.innerWidth > 700) return; const rect = controls.getBoundingClientRect(); const t = e.touches[0]; // Only engage if touch starts in the top grab zone. if (t.clientY - rect.top > 40) return; sy = t.clientY; dragging = true; controls.style.transition = 'none'; }, { passive: true }); controls.addEventListener('touchmove', (e) => { if (!dragging) return; const dy = e.touches[0].clientY - sy; if (dy > 0) controls.style.transform = `translateY(${dy}px)`; }, { passive: true }); controls.addEventListener('touchend', (e) => { if (!dragging) return; dragging = false; const dy = e.changedTouches[0].clientY - sy; controls.style.transition = ''; controls.style.transform = ''; if (dy > 60) controls.classList.add('dismissed'); }); } controls.innerHTML = controlsHTML({ color: state.color, brushSize: state.brushSize, wandTolerance: state.wandTolerance, }); rightPanel.appendChild(controls); // Mobile only (≤ 700 px — matches the .ge-editor-body column-stack // breakpoint): the right panel becomes a transformed bottom-sheet, // so any position:fixed descendant gets trapped by the transform // and rides along with the panel. Re-parent the controls panel to // the editor root so it can truly fix to the viewport bottom // regardless of the layers-sheet state. On desktop, controls stay // docked inside the right panel above the layers list. if (window.innerWidth <= 700 && state.container) { state.container.appendChild(controls); } // Move every slider-row's value chip out of its