1/** 2 * Build the right-hand panel (controls + layers) â DOM creation, 3 * controls innerHTML population, mobile bottom-sheet swipe behavior, 4 * controls-panel re-parenting on mobile, slider value-chip layout 5 * normalization, layer-panel header + mobile peek/expand swipe, and 6 * the panel-width drag-resize handle. 7 * 8 * Owns its own event listeners (touch swipe gestures, mouse resize 9 * drag). Returns the `rightPanel` element + the `panelResize` handle 10 * + the inner `controls` element so the caller can wire any post- 11 * mount tweaks. Reads state.container (for mobile re-parenting) and 12 * state.color / state.brushSize / state.wandTolerance (initial slider 13 * values). 14 * 15 * @param {{ 16 * controlsHTML: (ctx: {color, brushSize, wandTolerance}) => string, 17 * layerPanelHTML: () => string, 18 * }} build 19 * 20 * @returns {{ 21 * rightPanel: HTMLDivElement, 22 * controls: HTMLDivElement, 23 * layerPanel: HTMLDivElement, 24 * panelResize: HTMLDivElement, 25 * }} 26 */ 27import { state } from '../state.js'; 28 29export function buildRightPanel({ controlsHTML, layerPanelHTML }) { 30 const rightPanel = document.createElement('div'); 31 rightPanel.className = 'ge-right-panel'; 32 33 // Controls section. 34 const controls = document.createElement('div'); 35 controls.className = 'ge-controls'; 36 // Swipe-down to dismiss on mobile. Tap the same tool again to bring 37 // the sheet back. Only the top ~40 px (grab handle area) initiates 38 // the gesture so taps on inputs/sliders inside the panel still work. 39 { 40 let sy = 0, dragging = false; 41 controls.addEventListener('touchstart', (e) => { 42 if (window.innerWidth > 700) return; 43 const rect = controls.getBoundingClientRect(); 44 const t = e.touches[0]; 45 // Only engage if touch starts in the top grab zone. 46 if (t.clientY - rect.top > 40) return; 47 sy = t.clientY; 48 dragging = true; 49 controls.style.transition = 'none'; 50 }, { passive: true }); 51 controls.addEventListener('touchmove', (e) => { 52 if (!dragging) return; 53 const dy = e.touches[0].clientY - sy; 54 if (dy > 0) controls.style.transform = `translateY(${dy}px)`; 55 }, { passive: true }); 56 controls.addEventListener('touchend', (e) => { 57 if (!dragging) return; 58 dragging = false; 59 const dy = e.changedTouches[0].clientY - sy; 60 controls.style.transition = ''; 61 controls.style.transform = ''; 62 if (dy > 60) controls.classList.add('dismissed'); 63 }); 64 } 65 controls.innerHTML = controlsHTML({ 66 color: state.color, 67 brushSize: state.brushSize, 68 wandTolerance: state.wandTolerance, 69 }); 70 rightPanel.appendChild(controls); 71 // Mobile only (⤠700 px â matches the .ge-editor-body column-stack 72 // breakpoint): the right panel becomes a transformed bottom-sheet, 73 // so any position:fixed descendant gets trapped by the transform 74 // and rides along with the panel. Re-parent the controls panel to 75 // the editor root so it can truly fix to the viewport bottom 76 // regardless of the layers-sheet state. On desktop, controls stay 77 // docked inside the right panel above the layers list. 78 if (window.innerWidth <= 700 && state.container) { 79 state.container.appendChild(controls); 80 } 81 82 // Move every slider-row's value chip out of its <label> and place 83 // it AFTER the slider, so the value sits on the right edge of the 84 // row instead of being smashed against the slider track on the left. 85 controls.querySelectorAll('.ge-eraser-row').forEach(row => { 86 const valueSpan = row.querySelector('label > span[id$="-label"]'); 87 const slider = row.querySelector('input[type="range"]'); 88 if (valueSpan && slider) { 89 valueSpan.classList.add('ge-slider-value'); 90 slider.after(valueSpan); 91 } 92 }); 93 94 // Layer panel. 95 const layerPanel = document.createElement('div'); 96 layerPanel.className = 'ge-layers'; 97 layerPanel.innerHTML = layerPanelHTML(); 98 rightPanel.appendChild(layerPanel); 99 // Mobile: tap the header grab handle or swipe up/down to toggle 100 // the layers sheet between peek and expanded. The peek state 101 // always shows the active layer so users never lose access to it. 102 { 103 const header = layerPanel.querySelector('.ge-layers-header'); 104 if (header) { 105 let sy = 0, sx = 0, dragging = false, didSwipe = false;
106 header.addEventListener('touchstart', (e) => { 107 if (window.innerWidth > 700) return; 108 if (e.target.closest('button')) return; 109 sy = e.touches[0].clientY; 110 sx = e.touches[0].clientX; 111 dragging = true; 112 didSwipe = false; 113 }, { passive: true }); 114 header.addEventListener('touchend', (e) => { 115 if (!dragging) return; 116 dragging = false; 117 const dy = e.changedTouches[0].clientY - sy; 118 const dx = Math.abs(e.changedTouches[0].clientX - sx); 119 // Real swipe â three states cycle by direction: 120 // minimized â peek â expanded (swipe up) 121 // expanded â peek â minimized (swipe down) 122 if (Math.abs(dy) > 20 && Math.abs(dy) > dx) { 123 didSwipe = true; 124 const isExpanded = rightPanel.classList.contains('expanded'); 125 const isMinimized = rightPanel.classList.contains('minimized'); 126 if (dy < 0) { 127 if (isMinimized) { 128 rightPanel.classList.remove('minimized'); 129 } else if (!isExpanded) { 130 rightPanel.classList.add('expanded'); 131 } 132 } else { 133 if (isExpanded) { 134 rightPanel.classList.remove('expanded'); 135 } else if (!isMinimized) { 136 rightPanel.classList.add('minimized'); 137 } 138 } 139 e.preventDefault(); 140 } 141 }); 142 header.addEventListener('click', (e) => { 143 if (window.innerWidth > 700) return; 144 if (e.target.closest('button')) return; 145 if (didSwipe) { didSwipe = false; return; } 146 // Click cycles between peek and expanded; minimized comes 147 // back to peek (so a tap on the handle always reveals at 148 // least the active layer row). 149 if (rightPanel.classList.contains('minimized')) { 150 rightPanel.classList.remove('minimized'); 151 } else { 152 rightPanel.classList.toggle('expanded'); 153 } 154 }); 155 } 156 } 157 158 // Horizontal drag handle on the LEFT edge of the right panel â drag 159 // left to widen, right to narrow. Persists chosen width in 160 // localStorage so it survives reopens. (Earlier version was a 161 // vertical-drag for height; horizontal feels more natural since 162 // cramped LAYER ROWS are about width, not height.) 163 const panelResize = document.createElement('div'); 164 panelResize.className = 'ge-panel-resize'; 165 panelResize.title = 'Drag to resize panel'; 166 rightPanel.appendChild(panelResize); 167 try { 168 const savedW = parseInt(localStorage.getItem('ge-right-panel-width') || '', 10); 169 if (savedW && savedW > 160 && savedW < 800) rightPanel.style.flex = `0 0 ${savedW}px`; 170 } catch {} 171 let panelResizing = false; 172 let panelStartX = 0; 173 let panelStartW = 0; 174 panelResize.addEventListener('mousedown', (e) => { 175 panelResizing = true; 176 panelStartX = e.clientX; 177 panelStartW = rightPanel.getBoundingClientRect().width; 178 e.preventDefault(); 179 document.body.style.cursor = 'ew-resize'; 180 }); 181 document.addEventListener('mousemove', (e) => { 182 if (!panelResizing) return; 183 // Dragging left â wider panel (the panel sits on the right of 184 // the editor, so a leftward drag pulls its left edge left). 185 const delta = panelStartX - e.clientX; 186 const next = Math.max(160, Math.min(window.innerWidth - 200, panelStartW + delta)); 187 rightPanel.style.flex = `0 0 ${next}px`; 188 }); 189 document.addEventListener('mouseup', () => { 190 if (!panelResizing) return; 191 panelResizing = false; 192 document.body.style.cursor = ''; 193 try { 194 const w = Math.round(rightPanel.getBoundingClientRect().width); 195 localStorage.setItem('ge-right-panel-width', String(w)); 196 } catch {} 197 }); 198 199 return { rightPanel, controls, layerPanel, panelResize }; 200}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.