Files
odysseus/static/js/editor/wire-topbar-menus.js
T
pewdiepie-archdaemonandClaude Opus 5.5 5fac693c1c 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>
2026-10-15 11:57:35 +00:00

467 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Topbar dropdown menus — Image, Filter, and Resize.
*
* Image menu (#ge-image-menu-btn → #ge-image-menu):
* resize, selection (edge feather/delete), fill, rotate 90/180,
* flip horizontal/vertical.
*
* Filter menu (#ge-filter-menu-btn → #ge-filter-menu):
* Blur sub-menu — Gaussian, Zoom.
*
* Resize menu (#ge-resize-menu-btn → #ge-resize-menu):
* preset W×H items (data-resize-w/-h) apply immediately;
* [data-resize-custom] opens a themed prompt for arbitrary sizes.
*
* Returns the resize helpers so the keyboard-shortcuts module can
* call them too (Ctrl+Shift+T opens the custom prompt).
*
* @param {{
* closeOtherTopbarMenus: (keepId: string) => void,
* registerDocClickAway: (handler: (e: Event) => void) => void,
* saveState: (label?: string) => void,
* composite: () => void,
* fitZoom: () => void,
* renderLayerPanel: () => void,
* promptCanvasSize: (opts: object) => Promise<{w, h} | null>,
* doFillSelection: () => void,
* rotateAllLayers: (deg: number) => void,
* flipAllLayers: (axis: 'h' | 'v') => void,
* rotateActiveLayer: (deg: number) => void,
* flipActiveLayer: (axis: 'h' | 'v') => void,
* applyGaussianBlur: () => void,
* applyZoomBlur: () => void,
* addRetainedGaussianBlur: () => void,
* addRetainedEffect: (type: string) => void,
* uiModule: object,
* }} deps
*
* @returns {{
* applyCanvasResize: (newW: number, newH: number) => void,
* applyImageResize: (newW: number, newH: number) => void,
* canvasSizePrompt: () => Promise<void>,
* imageSizePrompt: () => Promise<void>,
* }}
*/
import { state } from './state.js';
import { resizeCanvasDocument, resizeImageDocument } from './document-geometry.js';
import { wireSubmenuToggles } from './submenu-toggles.js';
// Ask for a single number (Photoshop-style modify radius/width). Mirrors the
// canvas context menu's helper so Select > Modify matches the right-click flow.
function promptNumber(message, fallback, run) {
const raw = window.prompt(message, String(fallback));
if (raw == null) return;
const value = Number(raw);
if (Number.isFinite(value)) run(value);
}
export function wireTopbarMenus({
closeOtherTopbarMenus, registerDocClickAway,
saveState, composite, fitZoom, renderLayerPanel,
promptCanvasSize, doFillSelection,
rotateAllLayers, rotateAllLayersArbitrary, openPerspectiveCrop, flipAllLayers,
rotateActiveLayer, flipActiveLayer,
applyGaussianBlur, applyZoomBlur, applyMotionBlur, applyNoise, applyDespeckle, applyDehaze,
applyPixelate, applyCrystallize, applyEmboss, applyFindEdges, applyGlowingEdges, applySolarize,
applyOffset, applyBoxBlur, applySurfaceBlur, applyCustom,
applyMaximum, applyMinimum, applyMedian, applyPolarCoordinates, applyDifferenceClouds,
applyLensFlare, applyFibers, applyOilPaint, applyLensCorrection, applyReplaceColor,
applyDiffuse, applyOceanRipple, applyGlass, applyMosaicTiles, applyWind,
applyTraceContour, applyHalftonePattern, applyReticulation, applyShear, applyNotePaper,
applyCharcoal, applyChalkCharcoal, applyGraphicPen, applyPhotocopy, applyWaterPaper,
applySharpenMore, applyPosterEdges, applyCutout, applyInkOutlines, applyStainedGlass,
applyGrain, applyTexturizer, applyPatchwork, applyCraquelure, applyCrosshatch,
applyTwirl, applyPinch, applySpherize, applyRipple, applyClouds,
applySharpen, applySharpenEdges, applyUnsharpMask,
addRetainedGaussianBlur, addRetainedEffect,
selectAll, deselectSelection, reselectSelection, invertSelection,
transformSelection, refineSelection, colorRange, selectAndMask,
saveNamedSelection, loadNamedSelection, deleteNamedSelection,
setColorMode, uiModule,
}) {
function finishGeometryChange(newW, newH, message) {
const sizeLabel = document.getElementById('ge-canvas-size');
if (sizeLabel) sizeLabel.textContent = `${newW}×${newH}`;
fitZoom();
composite();
renderLayerPanel?.();
uiModule.showToast(message);
}
// Photoshop-style nested flyouts are wired by the shared submenu-toggles
// helper (also used by the Window > Workspace submenu).
// Canvas Size changes document bounds without resampling layer pixels.
function applyCanvasResize(newW, newH, options = {}) {
if (!newW || !newH || newW < 1 || newH < 1) {
uiModule.showToast('Invalid size');
return;
}
saveState('Resize canvas');
const result = resizeCanvasDocument(state, newW, newH, options);
finishGeometryChange(result.width, result.height, `Canvas resized to ${result.width}×${result.height}`);
}
function applyImageResize(newW, newH, options = {}) {
if (!newW || !newH || newW < 1 || newH < 1) {
uiModule.showToast('Invalid size');
return;
}
saveState('Resize image');
const result = resizeImageDocument(state, newW, newH, {
smoothingQuality: options.interpolation || 'high',
});
finishGeometryChange(result.width, result.height, `Image resized to ${result.width}×${result.height}`);
}
async function canvasSizePrompt() {
const result = await promptCanvasSize({
title: 'Canvas size',
okLabel: 'Apply',
initialW: state.imgWidth,
initialH: state.imgHeight,
showUnits: true,
showInterpolation: true,
keepProportions: true,
showAnchor: true,
});
if (!result) return;
applyCanvasResize(result.w, result.h, result);
}
async function imageSizePrompt() {
const result = await promptCanvasSize({
title: 'Image size',
okLabel: 'Resample',
initialW: state.imgWidth,
initialH: state.imgHeight,
showUnits: true,
showInterpolation: true,
keepProportions: true,
showPresets: false,
showPreview: true,
showResolution: true,
});
if (!result) return;
applyImageResize(result.w, result.h, result);
}
// ── Image menu ──
{
const btn = document.getElementById('ge-image-menu-btn');
const menu = document.getElementById('ge-image-menu');
if (btn && menu) {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const willOpen = menu.hidden;
if (willOpen) closeOtherTopbarMenus('ge-image-menu');
menu.hidden = !menu.hidden;
});
wireSubmenuToggles(menu);
menu.addEventListener('click', (e) => {
const item = e.target.closest('[data-image-action]');
if (!item || item.disabled) return;
menu.hidden = true;
const action = item.dataset.imageAction;
if (action === 'canvas-size') canvasSizePrompt();
else if (action === 'image-size') imageSizePrompt();
else if (action === 'selection') document.getElementById('ge-edge-menu-btn')?.click();
else if (action === 'fill') doFillSelection();
else if (action === 'rotate-90') rotateAllLayers(90);
else if (action === 'rotate-180') rotateAllLayers(180);
else if (action === 'flip-h') flipAllLayers('h');
else if (action === 'flip-v') flipAllLayers('v');
else if (action === 'mode-rgb') setColorMode?.('rgb');
else if (action === 'mode-grayscale') setColorMode?.('grayscale');
else if (action === 'desaturate') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'hue-saturation', params: { saturation: 0 } });
else if (action === 'auto-tone') window.geEditorControl?.invoke?.('photo.autoTone', {});
else if (action === 'auto-contrast') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'levels', params: { inBlack: 12, inWhite: 243 } });
else if (action === 'auto-color') window.geEditorControl?.invoke?.('photo.autoTone', {});
else if (action === 'equalize') window.geEditorControl?.invoke?.('adjustments.equalize', {});
else if (action === 'trim') window.geEditorControl?.invoke?.('document.trim', {});
else if (action === 'replace-color') applyReplaceColor?.();
else if (action === 'rotate-ccw') rotateAllLayers(270);
else if (action === 'rotate-arbitrary') promptNumber('Rotation angle (degrees, clockwise; negative for counter-clockwise)', 15, (v) => rotateAllLayersArbitrary?.(v));
else if (action === 'perspective-crop') openPerspectiveCrop?.();
else if (action === 'adj-levels') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'levels' });
else if (action === 'adj-curves') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'curves' });
else if (action === 'adj-exposure') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'exposure' });
else if (action === 'adj-develop') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'develop' });
else if (action === 'adj-vibrance') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'vibrance' });
else if (action === 'adj-color-balance') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'color-balance' });
else if (action === 'adj-black-white') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'black-white' });
else if (action === 'adj-photo-filter') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'photo-filter' });
else if (action === 'adj-channel-mixer') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'channel-mixer' });
else if (action === 'adj-invert') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'invert' });
else if (action === 'adj-posterize') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'posterize' });
else if (action === 'adj-threshold') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'threshold' });
else if (action === 'adj-gradient-map') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'gradient-map' });
else if (action === 'adj-shadows-highlights') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'shadows-highlights' });
else if (action === 'adj-brightness-contrast') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'brightness-contrast' });
else if (action === 'adj-hue-saturation') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'hue-saturation' });
else if (action === 'adj-selective-color') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'selective-color' });
else if (action === 'adj-levels') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'levels' });
else if (action === 'adj-curves') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'curves' });
else if (action === 'adj-exposure') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'exposure' });
else if (action === 'adj-develop') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'develop' });
else if (action === 'adj-vibrance') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'vibrance' });
else if (action === 'adj-color-balance') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'color-balance' });
else if (action === 'adj-black-white') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'black-white' });
else if (action === 'adj-photo-filter') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'photo-filter' });
else if (action === 'adj-channel-mixer') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'channel-mixer' });
else if (action === 'adj-invert') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'invert' });
else if (action === 'adj-posterize') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'posterize' });
else if (action === 'adj-threshold') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'threshold' });
else if (action === 'adj-gradient-map') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'gradient-map' });
else if (action === 'adj-shadows-highlights') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'shadows-highlights' });
else if (action === 'adj-brightness-contrast') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'brightness-contrast' });
else if (action === 'adj-hue-saturation') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'hue-saturation' });
else if (action === 'adj-selective-color') window.geEditorControl?.invoke?.('adjustments.apply', { type: 'selective-color' });
});
registerDocClickAway((e) => {
if (!menu.hidden && !menu.contains(e.target) && e.target !== btn) menu.hidden = true;
});
}
}
// ── Select menu ──
const renderSelectionMenu = () => {
const hasSelection = !!state.wandMask || (state.lassoPoints?.length >= 3 && !state.lassoActive);
const menu = document.getElementById('ge-selection-menu');
if (!menu) return;
menu.querySelector('[data-selection-action="deselect"]')?.toggleAttribute('disabled', !hasSelection);
menu.querySelector('[data-selection-action="invert"]')?.toggleAttribute('disabled', !hasSelection);
menu.querySelector('[data-selection-action="transform"]')?.toggleAttribute('disabled', !hasSelection);
menu.querySelector('[data-selection-action="refine"]')?.toggleAttribute('disabled', !hasSelection);
menu.querySelector('[data-selection-action="save"]')?.toggleAttribute('disabled', !hasSelection);
for (const id of ['modifyBorder', 'modifySmooth', 'modifyExpand', 'modifyContract', 'modifyFeather']) {
menu.querySelector(`[data-selection-action="${id}"]`)?.toggleAttribute('disabled', !hasSelection);
}
menu.querySelector('[data-selection-action="reselect"]')?.toggleAttribute('disabled', !state.lastSelection?.canvas);
const list = document.getElementById('ge-saved-selection-list');
if (!list) return;
list.innerHTML = '';
for (const saved of state.savedSelections || []) {
const row = document.createElement('div');
row.className = 'ge-saved-selection-row';
const load = document.createElement('button');
load.type = 'button';
load.className = 'ge-saved-selection-load';
load.dataset.selectionAction = 'load';
load.dataset.selectionId = saved.id;
load.textContent = saved.name;
load.title = `Load ${saved.name}`;
const remove = document.createElement('button');
remove.type = 'button';
remove.className = 'ge-saved-selection-delete';
remove.dataset.selectionAction = 'delete';
remove.dataset.selectionId = saved.id;
remove.textContent = '×';
remove.title = `Delete ${saved.name}`;
remove.setAttribute('aria-label', `Delete ${saved.name}`);
row.append(load, remove);
list.appendChild(row);
}
if (!list.children.length) {
const empty = document.createElement('div');
empty.className = 'ge-saved-selection-empty';
empty.textContent = 'None saved';
list.appendChild(empty);
}
};
{
const btn = document.getElementById('ge-selection-menu-btn');
const menu = document.getElementById('ge-selection-menu');
const nameInput = document.getElementById('ge-selection-name');
if (btn && menu) {
btn.addEventListener('click', event => {
event.stopPropagation();
const willOpen = menu.hidden;
if (willOpen) {
closeOtherTopbarMenus('ge-selection-menu');
renderSelectionMenu();
}
menu.hidden = !menu.hidden;
});
wireSubmenuToggles(menu);
menu.addEventListener('click', event => {
const item = event.target.closest('[data-selection-action]');
if (!item || item.disabled) return;
const action = item.dataset.selectionAction;
let handled = false;
if (action === 'all') handled = selectAll?.();
else if (action === 'deselect') handled = deselectSelection?.();
else if (action === 'reselect') handled = reselectSelection?.();
else if (action === 'invert') handled = invertSelection?.();
else if (action === 'grow') handled = !!window.geEditorControl?.invoke?.('selection.grow', { amount: 1 });
else if (action === 'modifyFeather') { promptNumber('Feather radius (px)', 8, v => window.geEditorControl?.invoke?.('selection.feather', { radius: v })); handled = true; }
else if (action === 'modifyExpand') { promptNumber('Expand by (px)', 4, v => window.geEditorControl?.invoke?.('selection.expand', { amount: v })); handled = true; }
else if (action === 'modifyContract') { promptNumber('Contract by (px)', 4, v => window.geEditorControl?.invoke?.('selection.contract', { amount: v })); handled = true; }
else if (action === 'modifyBorder') { promptNumber('Border width (px)', 4, v => window.geEditorControl?.invoke?.('selection.border', { width: v })); handled = true; }
else if (action === 'modifySmooth') { promptNumber('Smooth radius (px)', 4, v => window.geEditorControl?.invoke?.('selection.smooth', { radius: v })); handled = true; }
else if (action === 'similar') handled = !!window.geEditorControl?.invoke?.('selection.similar', {});
else if (action === 'subject') { window.geEditorControl?.invoke?.('selection.subject', {}); handled = true; }
else if (action === 'transform') handled = transformSelection?.();
else if (action === 'refine') handled = refineSelection?.();
else if (action === 'colorRange') handled = !!colorRange?.();
else if (action === 'selectAndMask') handled = !!selectAndMask?.();
else if (action === 'save') {
handled = saveNamedSelection?.(nameInput?.value);
if (handled && nameInput) nameInput.value = '';
} else if (action === 'load') handled = loadNamedSelection?.(item.dataset.selectionId);
else if (action === 'delete') handled = deleteNamedSelection?.(item.dataset.selectionId);
if (handled) renderSelectionMenu();
if (handled && !['save', 'delete'].includes(action)) menu.hidden = true;
});
nameInput?.addEventListener('keydown', event => {
if (event.key !== 'Enter') return;
event.preventDefault();
if (saveNamedSelection?.(nameInput.value)) {
nameInput.value = '';
renderSelectionMenu();
}
});
registerDocClickAway(event => {
if (!menu.hidden && !menu.contains(event.target) && event.target !== btn) menu.hidden = true;
});
}
}
// ── Filter menu (Blur sub-menu — Gaussian / Zoom) ──
{
const btn = document.getElementById('ge-filter-menu-btn');
const menu = document.getElementById('ge-filter-menu');
if (btn && menu) {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const willOpen = menu.hidden;
if (willOpen) closeOtherTopbarMenus('ge-filter-menu');
menu.hidden = !menu.hidden;
});
wireSubmenuToggles(menu);
menu.addEventListener('click', (e) => {
const item = e.target.closest('[data-filter-action]');
if (!item) return;
menu.hidden = true;
const action = item.dataset.filterAction;
if (action === 'effect-blur-gaussian') addRetainedGaussianBlur?.();
else if (action === 'effect-sharpen') addRetainedEffect?.('sharpen');
else if (action === 'effect-color-overlay') addRetainedEffect?.('color-overlay');
else if (action === 'effect-drop-shadow') addRetainedEffect?.('drop-shadow');
else if (action === 'effect-stroke') addRetainedEffect?.('stroke');
else if (action === 'effect-motion-blur') addRetainedEffect?.('motion-blur');
else if (action === 'effect-zoom-blur') addRetainedEffect?.('zoom-blur');
else if (action === 'effect-pixelate') addRetainedEffect?.('pixelate');
else if (action === 'effect-preset-soft-blur') addRetainedGaussianBlur?.('soft-blur');
else if (action === 'effect-preset-crisp-detail') addRetainedEffect?.('sharpen', 'crisp-detail');
else if (action === 'effect-preset-soft-shadow') addRetainedEffect?.('drop-shadow', 'soft-shadow');
else if (action === 'effect-preset-white-outline') addRetainedEffect?.('stroke', 'white-outline');
else if (action === 'blur-gaussian') applyGaussianBlur?.();
else if (action === 'dehaze') applyDehaze?.();
else if (action === 'blur-zoom') applyZoomBlur?.();
else if (action === 'blur-motion') applyMotionBlur?.();
else if (action === 'radial-blur') applyZoomBlur?.();
else if (action === 'blur-box') applyBoxBlur?.();
else if (action === 'blur-surface') applySurfaceBlur?.();
else if (action === 'sharpen') applySharpen?.();
else if (action === 'sharpen-edges') applySharpenEdges?.();
else if (action === 'unsharp-mask') applyUnsharpMask?.();
else if (action === 'smart-sharpen') applyUnsharpMask?.();
else if (action === 'smart-sharpen') applyUnsharpMask?.();
else if (action === 'noise-add') applyNoise?.();
else if (action === 'noise-despeckle') applyDespeckle?.();
else if (action === 'reduce-noise') applyDespeckle?.();
else if (action === 'dust-scratches') applyDespeckle?.();
else if (action === 'pixelate-mosaic') applyPixelate?.();
else if (action === 'pixelate-crystallize') applyCrystallize?.();
else if (action === 'stylize-emboss') applyEmboss?.();
else if (action === 'stylize-find-edges') applyFindEdges?.();
else if (action === 'stylize-glowing-edges') applyGlowingEdges?.();
else if (action === 'stylize-solarize') applySolarize?.();
else if (action === 'distort-twirl') applyTwirl?.();
else if (action === 'distort-pinch') applyPinch?.();
else if (action === 'distort-spherize') applySpherize?.();
else if (action === 'distort-ripple') applyRipple?.();
else if (action === 'render-clouds') applyClouds?.();
else if (action === 'other-offset') applyOffset?.();
else if (action === 'other-custom') applyCustom?.();
else if (action === 'other-maximum') applyMaximum?.();
else if (action === 'other-minimum') applyMinimum?.();
else if (action === 'noise-median') applyMedian?.();
else if (action === 'distort-polar') applyPolarCoordinates?.();
else if (action === 'render-difference-clouds') applyDifferenceClouds?.();
else if (action === 'render-lens-flare') applyLensFlare?.();
else if (action === 'render-fibers') applyFibers?.();
else if (action === 'stylize-oil-paint') applyOilPaint?.();
else if (action === 'distort-lens-correction') applyLensCorrection?.();
else if (action === 'stylize-diffuse') applyDiffuse?.();
else if (action === 'distort-ocean-ripple') applyOceanRipple?.();
else if (action === 'distort-glass') applyGlass?.();
else if (action === 'pixelate-mosaic-tiles') applyMosaicTiles?.();
else if (action === 'stylize-wind') applyWind?.();
else if (action === 'stylize-trace-contour') applyTraceContour?.();
else if (action === 'sketch-halftone-pattern') applyHalftonePattern?.();
else if (action === 'sketch-reticulation') applyReticulation?.();
else if (action === 'distort-shear') applyShear?.();
else if (action === 'sketch-note-paper') applyNotePaper?.();
else if (action === 'sketch-charcoal') applyCharcoal?.();
else if (action === 'sketch-chalk-charcoal') applyChalkCharcoal?.();
else if (action === 'sketch-graphic-pen') applyGraphicPen?.();
else if (action === 'sketch-photocopy') applyPhotocopy?.();
else if (action === 'sketch-water-paper') applyWaterPaper?.();
else if (action === 'sharpen-more') applySharpenMore?.();
else if (action === 'artistic-poster-edges') applyPosterEdges?.();
else if (action === 'artistic-cutout') applyCutout?.();
else if (action === 'brush-ink-outlines') applyInkOutlines?.();
else if (action === 'texture-stained-glass') applyStainedGlass?.();
else if (action === 'texture-grain') applyGrain?.();
else if (action === 'texture-texturizer') applyTexturizer?.();
else if (action === 'texture-patchwork') applyPatchwork?.();
else if (action === 'texture-craquelure') applyCraquelure?.();
else if (action === 'brush-crosshatch') applyCrosshatch?.();
});
registerDocClickAway((e) => {
if (!menu.hidden && !menu.contains(e.target) && e.target !== btn) menu.hidden = true;
});
}
}
// ── Resize popup (preset items + Custom… → resizeCustomPrompt) ──
{
const btn = document.getElementById('ge-resize-menu-btn');
const menu = document.getElementById('ge-resize-menu');
if (btn && menu) {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const willOpen = menu.hidden;
if (willOpen) closeOtherTopbarMenus('ge-resize-menu');
menu.hidden = !menu.hidden;
});
menu.querySelectorAll('[data-resize-w]').forEach(item => {
item.addEventListener('click', () => {
menu.hidden = true;
applyCanvasResize(parseInt(item.dataset.resizeW, 10), parseInt(item.dataset.resizeH, 10));
});
});
menu.querySelector('[data-resize-custom]')?.addEventListener('click', () => {
menu.hidden = true;
canvasSizePrompt();
});
registerDocClickAway((e) => {
if (!menu.hidden && !menu.contains(e.target) && e.target !== btn) menu.hidden = true;
});
}
}
return {
applyCanvasResize,
applyImageResize,
canvasSizePrompt,
imageSizePrompt,
renderSelectionMenu,
// Compatibility alias consumed by the existing Ctrl+Shift+T shortcut.
resizeCustomPrompt: canvasSizePrompt,
};
}