mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-16 20:02:20 +02:00
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>
101 lines
3.8 KiB
JavaScript
101 lines
3.8 KiB
JavaScript
/**
|
||
* Topbar overflow handler — keeps lightweight labels updated and hides
|
||
* only low-priority AI model controls when the editor window gets narrow.
|
||
*
|
||
* Plus the small canvas-size display label updater (since it sits in
|
||
* the topbar too).
|
||
*
|
||
* Import and Canvas stay as real topbar buttons; there is intentionally
|
||
* no "More" overflow menu here.
|
||
*
|
||
* @param {{
|
||
* container: HTMLElement,
|
||
* registerDocClickAway: (handler: (e: Event) => void) => void,
|
||
* }} deps
|
||
*/
|
||
import { state } from './state.js';
|
||
|
||
let disposeTopbar;
|
||
|
||
export function wireTopbarOverflow({ container }) {
|
||
disposeTopbar?.();
|
||
const cleanups = [];
|
||
disposeTopbar = () => cleanups.forEach(cleanup => cleanup());
|
||
// Canvas-size badge updater (kept simple — it lives in the topbar).
|
||
const sizeLabel = document.getElementById('ge-canvas-size');
|
||
function updateSizeLabel() {
|
||
if (sizeLabel) sizeLabel.textContent = `${state.imgWidth}×${state.imgHeight} px`;
|
||
}
|
||
updateSizeLabel();
|
||
|
||
const topbar = container.querySelector('.ge-topbar');
|
||
// The Gen control + its "Gen" label span — collapse as a group when
|
||
// narrow. The Inpaint model selector moved into the side panel.
|
||
const aiGroup = [
|
||
container.querySelector('#ge-ai-model'),
|
||
...container.querySelectorAll('.ge-topbar span[style*="font-size:9px"]'),
|
||
].filter(Boolean);
|
||
|
||
// Native popovers keep the existing DOM/event ownership while escaping
|
||
// the toolbar's horizontal scroll clip and the editor's stacking context.
|
||
topbar?.querySelectorAll('.dropdown[hidden]').forEach(menu => {
|
||
if (!menu.showPopover) return;
|
||
const anchor = menu.parentElement.querySelector('button');
|
||
if (!anchor) return;
|
||
menu.setAttribute('popover', 'manual');
|
||
const position = () => {
|
||
const rect = anchor.getBoundingClientRect();
|
||
menu.style.top = `${rect.bottom + 4}px`;
|
||
// Anchor the menu's LEFT edge under the button's left edge
|
||
// (Photoshop behaviour), then clamp so the whole menu stays inside
|
||
// the viewport — a right-hand menu would otherwise overflow on the
|
||
// right, and a left-hand button (e.g. SELECT at 390px) would push
|
||
// a ~224px menu past the left edge.
|
||
const margin = 8;
|
||
const width = menu.getBoundingClientRect().width || menu.offsetWidth || 0;
|
||
let left = rect.left;
|
||
if (left + width > window.innerWidth - margin) {
|
||
left = window.innerWidth - margin - width;
|
||
}
|
||
if (left < margin) left = margin;
|
||
menu.style.left = `${left}px`;
|
||
menu.style.right = 'auto';
|
||
};
|
||
const sync = () => {
|
||
if (menu.hidden) {
|
||
if (menu.matches(':popover-open')) menu.hidePopover();
|
||
} else if (menu.isConnected) {
|
||
position();
|
||
if (!menu.matches(':popover-open')) menu.showPopover();
|
||
}
|
||
};
|
||
const observer = new MutationObserver(sync);
|
||
observer.observe(menu, { attributes: true, attributeFilter: ['hidden'] });
|
||
topbar.addEventListener('scroll', position, { passive: true });
|
||
window.addEventListener('resize', position);
|
||
cleanups.push(() => {
|
||
observer.disconnect();
|
||
topbar.removeEventListener('scroll', position);
|
||
window.removeEventListener('resize', position);
|
||
if (menu.matches(':popover-open')) menu.hidePopover();
|
||
});
|
||
});
|
||
|
||
function syncOverflow() {
|
||
if (!topbar) return;
|
||
aiGroup.forEach(el => { el.style.display = ''; });
|
||
if (topbar.scrollWidth > topbar.clientWidth) {
|
||
// Hide AI group first — bulky and least essential at narrow widths.
|
||
aiGroup.forEach(el => { el.style.display = 'none'; });
|
||
}
|
||
}
|
||
|
||
if (topbar && window.ResizeObserver) {
|
||
const ro = new ResizeObserver(() => syncOverflow());
|
||
ro.observe(topbar);
|
||
cleanups.push(() => ro.disconnect());
|
||
}
|
||
// Initial pass after layout settles.
|
||
requestAnimationFrame(syncOverflow);
|
||
}
|