The browser now navigates from one narrow icon rail on the left: Home, Models, Gallery, Workflows, Materials, then Voices and Audio, then scan, Model Doctor, Node Assistant and the toolbox, with settings at the bottom. It replaces the header tabs, the image/audio toggle and the tool bar under the folder list, so the list column holds only a list and can close without hiding a command. - ui_shell_rail.js builds the rail; ui_shell_nav.js owns goTo(page), which page has a list and whether it is open (remembered per page; below 760 px it starts closed and overlays), the page title in the header and the page reopened next time. Clicking the current page's icon toggles its list (from a model's detail it returns to the grid). - ui_home.js: task cards and first steps; the first open lands there. - Window drag/resize moved to ui_shell_frame.js and scan polling to ui_scan_watch.js; ui_sidebar.js goes from 864 to 464 lines. - Workspaces (recipes, notes, materials) keep the rail visible; the list steps aside while one is open. - Removed what the rail replaced: sidebar_actions.js (bottom-bar hover labels and tooltip bubble), the domain toggle button, their CSS and unused text keys. - Spotlight tour: the rail step replaces the header-tabs step, cards open to the right of rail items, and the audio tour points at the rail's Audio entry. - Decision record AD-017; architecture map, frontend and update-guide docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
89 lines
3.5 KiB
JavaScript
89 lines
3.5 KiB
JavaScript
/**
|
|
* The floating window's frame: dragged by its header, resized from the corner handle,
|
|
* kept on screen, position and size remembered. Docked mode ignores all of it (CSS).
|
|
*/
|
|
|
|
const MIN_WIDTH = 600;
|
|
const MIN_HEIGHT = 400;
|
|
|
|
/** Drag by `header` (not from its buttons or fields); keeps `container` inside the window. */
|
|
export function bindShellDrag(container, header) {
|
|
let dragging = false;
|
|
let offsetX = 0;
|
|
let offsetY = 0;
|
|
|
|
const clamp = (x, y) => ({
|
|
x: Math.max(0, Math.min(x, window.innerWidth - container.offsetWidth)),
|
|
y: Math.max(0, Math.min(y, window.innerHeight - container.offsetHeight)),
|
|
});
|
|
|
|
header.addEventListener('mousedown', (e) => {
|
|
if (e.target.closest('button, input, select, textarea, #anomalous-close, .anomalous-header-close')) return;
|
|
dragging = true;
|
|
const rect = container.getBoundingClientRect();
|
|
offsetX = e.clientX - rect.left;
|
|
offsetY = e.clientY - rect.top;
|
|
e.preventDefault();
|
|
});
|
|
window.addEventListener('mousemove', (e) => {
|
|
if (!dragging) return;
|
|
const pos = clamp(e.clientX - offsetX, e.clientY - offsetY);
|
|
container.style.left = `${pos.x}px`;
|
|
container.style.top = `${pos.y}px`;
|
|
container.style.transform = 'none';
|
|
});
|
|
window.addEventListener('mouseup', () => {
|
|
if (!dragging) return;
|
|
dragging = false;
|
|
localStorage.setItem('anomalous_pos_x', container.style.left);
|
|
localStorage.setItem('anomalous_pos_y', container.style.top);
|
|
});
|
|
|
|
const savedX = localStorage.getItem('anomalous_pos_x');
|
|
const savedY = localStorage.getItem('anomalous_pos_y');
|
|
if (savedX && savedY) {
|
|
container.style.left = savedX;
|
|
container.style.top = savedY;
|
|
}
|
|
|
|
// The browser window may shrink or zoom after a drag; pull the frame back on screen.
|
|
setInterval(() => {
|
|
if (dragging || container.style.display === 'none' || container.classList.contains('anomalous-docked')) return;
|
|
const rect = container.getBoundingClientRect();
|
|
const pos = clamp(rect.left, rect.top);
|
|
if ((pos.x !== rect.left || pos.y !== rect.top) && container.style.left.endsWith('px') && container.style.top.endsWith('px')) {
|
|
container.style.left = `${pos.x}px`;
|
|
container.style.top = `${pos.y}px`;
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
/** The bottom-right resize handle; appended to `container`. */
|
|
export function bindShellResize(container) {
|
|
const handle = document.createElement('div');
|
|
handle.className = 'anomalous-resize-handle';
|
|
let resizing = false;
|
|
handle.onmousedown = (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
resizing = true;
|
|
};
|
|
window.addEventListener('mousemove', (e) => {
|
|
if (!resizing) return;
|
|
const rect = container.getBoundingClientRect();
|
|
container.style.width = `${Math.max(MIN_WIDTH, e.clientX - rect.left)}px`;
|
|
container.style.height = `${Math.max(MIN_HEIGHT, e.clientY - rect.top)}px`;
|
|
});
|
|
window.addEventListener('mouseup', () => {
|
|
if (!resizing) return;
|
|
resizing = false;
|
|
localStorage.setItem('anomalous_width', container.style.width);
|
|
localStorage.setItem('anomalous_height', container.style.height);
|
|
});
|
|
const savedW = localStorage.getItem('anomalous_width');
|
|
const savedH = localStorage.getItem('anomalous_height');
|
|
if (savedW) container.style.width = savedW;
|
|
if (savedH) container.style.height = savedH;
|
|
container.appendChild(handle);
|
|
}
|