feat(photo-editor): Ajax Image Editor client (Photoshop-parity tools)

Photoshop-parity editor client: layers, selection/masking, brushes,
smart filters, LUT import, Quick Edit, mobile gestures, workspaces,
batch operations, and the service-worker versioning (v978->v1006).

Source: vendor/photocraft (ad1cff44) photo-editor client files, merged
onto upstream/dev (a8c147b2). Conflict resolution documented in
pr_work/A_conflicts.md; editor files taken from vendor/photocraft
(upstream's editor is an earlier snapshot of the same feature).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pewdiepie-archdaemon
2026-10-15 11:57:35 +00:00
co-authored by Claude Opus 5.5
parent c74f475679
commit 5fac693c1c
290 changed files with 34358 additions and 766 deletions
+85 -5
View File
@@ -13,7 +13,7 @@
* values).
*
* @param {{
* controlsHTML: (ctx: {color, brushSize, wandTolerance}) => string,
* controlsHTML: (ctx: {color, brushSize, wandTolerance, bucketTolerance, cropAspectPreset}) => string,
* layerPanelHTML: () => string,
* }} build
*
@@ -22,13 +22,19 @@
* controls: HTMLDivElement,
* layerPanel: HTMLDivElement,
* panelResize: HTMLDivElement,
* dock: {dock: HTMLDivElement, show: (id: string) => void}|null,
* }}
*/
import { state } from '../state.js';
import { buildDock } from './dock.js';
export function buildRightPanel({ controlsHTML, layerPanelHTML }) {
export function buildRightPanel({ controlsHTML, layerPanelHTML, extraPanels = [] }) {
const rightPanel = document.createElement('div');
rightPanel.className = 'ge-right-panel';
// Desktop: present Tool Options + Layers as tabs in a dock. Mobile
// (≤ 700 px) keeps the existing bottom-sheet model, where the two
// panels are independent sheets, so the dock is bypassed entirely.
const useDock = window.innerWidth > 700;
// Controls section.
const controls = document.createElement('div');
@@ -66,8 +72,11 @@ export function buildRightPanel({ controlsHTML, layerPanelHTML }) {
color: state.color,
brushSize: state.brushSize,
wandTolerance: state.wandTolerance,
bucketTolerance: state.bucketTolerance,
cropAspectPreset: state.cropAspectPreset,
eyedropperSampleSize: state.eyedropperSampleSize,
});
rightPanel.appendChild(controls);
if (!useDock) 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
@@ -99,7 +108,7 @@ export function buildRightPanel({ controlsHTML, layerPanelHTML }) {
const layerPanel = document.createElement('div');
layerPanel.className = 'ge-layers';
layerPanel.innerHTML = layerPanelHTML();
rightPanel.appendChild(layerPanel);
if (!useDock) 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.
@@ -164,6 +173,77 @@ export function buildRightPanel({ controlsHTML, layerPanelHTML }) {
}
}
// Mobile panel access (≤ 700 px). The desktop dock is bypassed on
// phones, which left the whole studio panel set (Swatches, Gradients,
// Patterns, Properties, Adjustments, Histogram, Info, Channels, Paths,
// Character, Navigator, History, Actions) unreachable — there was no way
// to add an adjustment or pick a colour from the sheet. Mount the same
// entries in a compact `<select>` overlay reachable from a "Panels"
// button in the layers header, without touching the peek/expand model.
let mobilePanels = null;
if (!useDock && Array.isArray(extraPanels) && extraPanels.length) {
const mDock = buildDock({
entries: extraPanels,
persistKey: 'ge-dock-active',
defaultId: null,
compact: true,
});
const overlay = document.createElement('div');
overlay.className = 'ge-mobile-panels';
overlay.hidden = true;
const head = document.createElement('div');
head.className = 'ge-mobile-panels-head';
const title = document.createElement('span');
title.className = 'ge-mobile-panels-title';
title.textContent = 'Panels';
const closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'ge-mobile-panels-close';
closeBtn.setAttribute('aria-label', 'Close panels');
closeBtn.textContent = '✕';
head.append(title, closeBtn);
overlay.append(head, mDock.dock);
rightPanel.appendChild(overlay);
closeBtn.addEventListener('click', () => { overlay.hidden = true; });
const hdr = layerPanel.querySelector('.ge-layers-header');
if (hdr) {
const panelsBtn = document.createElement('button');
panelsBtn.type = 'button';
panelsBtn.className = 'ge-layers-panels-btn';
panelsBtn.textContent = 'Panels';
panelsBtn.title = 'Show panels';
panelsBtn.addEventListener('click', (e) => {
e.stopPropagation();
overlay.hidden = !overlay.hidden;
});
hdr.appendChild(panelsBtn);
}
mobilePanels = overlay;
}
// Desktop dock: Tool Options + Layers as tabs (PhotoCraft-parity).
// The panels are only re-parented, so every existing selector and
// listener keeps working; switching tabs just toggles `hidden`.
let dock = null;
if (useDock) { rightPanel.classList.add('ge-right-panel--dock');
dock = buildDock({
entries: [
...(Array.isArray(extraPanels) ? extraPanels : []),
],
defaultId: (Array.isArray(extraPanels) && extraPanels[0]?.id) || null,
persistKey: 'ge-dock-active',
// Narrow desktop columns get the compact selector fallback; the
// multi-group stack needs a bit more room to be usable.
compact: window.innerWidth <= 820,
});
rightPanel.appendChild(dock.dock);
// Layers stays pinned below the dock so the layer list is always
// visible while editing (Photoshop-style), regardless of the active
// dock tab. Only the tool options + studio panels live in tabs.
rightPanel.appendChild(layerPanel);
}
// 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
@@ -205,5 +285,5 @@ export function buildRightPanel({ controlsHTML, layerPanelHTML }) {
} catch {}
});
return { rightPanel, controls, layerPanel, panelResize };
return { rightPanel, controls, layerPanel, panelResize, dock };
}