PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/editor/build/right-panel.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 17:01:14 UTC 8,052 bytes, 200 lines download raw bytes

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.