mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-16 17:55:26 -04:00
Odysseus v1.0
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* 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 <label> and place
|
||||
// it AFTER the slider, so the value sits on the right edge of the
|
||||
// row instead of being smashed against the slider track on the left.
|
||||
controls.querySelectorAll('.ge-eraser-row').forEach(row => {
|
||||
const valueSpan = row.querySelector('label > span[id$="-label"]');
|
||||
const slider = row.querySelector('input[type="range"]');
|
||||
if (valueSpan && slider) {
|
||||
valueSpan.classList.add('ge-slider-value');
|
||||
slider.after(valueSpan);
|
||||
}
|
||||
});
|
||||
|
||||
// Layer panel.
|
||||
const layerPanel = document.createElement('div');
|
||||
layerPanel.className = 'ge-layers';
|
||||
layerPanel.innerHTML = layerPanelHTML();
|
||||
rightPanel.appendChild(layerPanel);
|
||||
// Mobile: tap the header grab handle or swipe up/down to toggle
|
||||
// the layers sheet between peek and expanded. The peek state
|
||||
// always shows the active layer so users never lose access to it.
|
||||
{
|
||||
const header = layerPanel.querySelector('.ge-layers-header');
|
||||
if (header) {
|
||||
let sy = 0, sx = 0, dragging = false, didSwipe = false;
|
||||
header.addEventListener('touchstart', (e) => {
|
||||
if (window.innerWidth > 700) return;
|
||||
if (e.target.closest('button')) return;
|
||||
sy = e.touches[0].clientY;
|
||||
sx = e.touches[0].clientX;
|
||||
dragging = true;
|
||||
didSwipe = false;
|
||||
}, { passive: true });
|
||||
header.addEventListener('touchend', (e) => {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
const dy = e.changedTouches[0].clientY - sy;
|
||||
const dx = Math.abs(e.changedTouches[0].clientX - sx);
|
||||
// Real swipe — three states cycle by direction:
|
||||
// minimized → peek → expanded (swipe up)
|
||||
// expanded → peek → minimized (swipe down)
|
||||
if (Math.abs(dy) > 20 && Math.abs(dy) > dx) {
|
||||
didSwipe = true;
|
||||
const isExpanded = rightPanel.classList.contains('expanded');
|
||||
const isMinimized = rightPanel.classList.contains('minimized');
|
||||
if (dy < 0) {
|
||||
if (isMinimized) {
|
||||
rightPanel.classList.remove('minimized');
|
||||
} else if (!isExpanded) {
|
||||
rightPanel.classList.add('expanded');
|
||||
}
|
||||
} else {
|
||||
if (isExpanded) {
|
||||
rightPanel.classList.remove('expanded');
|
||||
} else if (!isMinimized) {
|
||||
rightPanel.classList.add('minimized');
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
header.addEventListener('click', (e) => {
|
||||
if (window.innerWidth > 700) return;
|
||||
if (e.target.closest('button')) return;
|
||||
if (didSwipe) { didSwipe = false; return; }
|
||||
// Click cycles between peek and expanded; minimized comes
|
||||
// back to peek (so a tap on the handle always reveals at
|
||||
// least the active layer row).
|
||||
if (rightPanel.classList.contains('minimized')) {
|
||||
rightPanel.classList.remove('minimized');
|
||||
} else {
|
||||
rightPanel.classList.toggle('expanded');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal drag handle on the LEFT edge of the right panel — drag
|
||||
// left to widen, right to narrow. Persists chosen width in
|
||||
// localStorage so it survives reopens. (Earlier version was a
|
||||
// vertical-drag for height; horizontal feels more natural since
|
||||
// cramped LAYER ROWS are about width, not height.)
|
||||
const panelResize = document.createElement('div');
|
||||
panelResize.className = 'ge-panel-resize';
|
||||
panelResize.title = 'Drag to resize panel';
|
||||
rightPanel.appendChild(panelResize);
|
||||
try {
|
||||
const savedW = parseInt(localStorage.getItem('ge-right-panel-width') || '', 10);
|
||||
if (savedW && savedW > 160 && savedW < 800) rightPanel.style.flex = `0 0 ${savedW}px`;
|
||||
} catch {}
|
||||
let panelResizing = false;
|
||||
let panelStartX = 0;
|
||||
let panelStartW = 0;
|
||||
panelResize.addEventListener('mousedown', (e) => {
|
||||
panelResizing = true;
|
||||
panelStartX = e.clientX;
|
||||
panelStartW = rightPanel.getBoundingClientRect().width;
|
||||
e.preventDefault();
|
||||
document.body.style.cursor = 'ew-resize';
|
||||
});
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!panelResizing) return;
|
||||
// Dragging left → wider panel (the panel sits on the right of
|
||||
// the editor, so a leftward drag pulls its left edge left).
|
||||
const delta = panelStartX - e.clientX;
|
||||
const next = Math.max(160, Math.min(window.innerWidth - 200, panelStartW + delta));
|
||||
rightPanel.style.flex = `0 0 ${next}px`;
|
||||
});
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (!panelResizing) return;
|
||||
panelResizing = false;
|
||||
document.body.style.cursor = '';
|
||||
try {
|
||||
const w = Math.round(rightPanel.getBoundingClientRect().width);
|
||||
localStorage.setItem('ge-right-panel-width', String(w));
|
||||
} catch {}
|
||||
});
|
||||
|
||||
return { rightPanel, controls, layerPanel, panelResize };
|
||||
}
|
||||
Reference in New Issue
Block a user