Squash Odysseus development history

This commit is contained in:
pewdiepie-archdaemon
2026-09-11 06:04:19 +00:00
parent e5c99a5eee
commit 6ee6502010
2050 changed files with 538359 additions and 57745 deletions
+366 -27
View File
@@ -5,8 +5,8 @@
// emailLibrary.js / documentLibrary.js / galleryEditor.js). While docked:
// - the modal-content lives at `right: 0; top: 0; bottom: 0` with a
// viewport-fraction width
// - body gets `right-dock-active` + `--right-dock-w` so the chat /
// doc panel / notes pane underneath reserves room via padding-right
// - body gets `right-dock-active` + `--right-dock-w` so the workspace
// underneath reserves room for the fixed side panel
// - if the remaining chat width would drop under 380px, the wide
// sidebar auto-collapses to the icon rail (mirrors notes-view UX)
//
@@ -21,6 +21,14 @@ const SNAP_PX = 60;
const UNSNAP_PX = 80;
const MIN_CHAT_WIDTH = 380;
const EMAIL_DOC_SPLIT_WIDTH_KEY = 'odysseus-email-doc-split-width';
const EDGE_DOCK_WIDTH_KEY_PREFIX = 'odysseus-edge-dock-width';
const MIN_EDGE_DOCK_WIDTH = 320;
let _edgeDockHandlePositioner = null;
function _positionEdgeDockResizeHandles() {
try { _edgeDockHandlePositioner && _edgeDockHandlePositioner(); } catch (_) {}
}
function _dockClassForSide(side) {
return side === 'left' ? 'modal-left-docked' : 'modal-right-docked';
@@ -48,6 +56,7 @@ export function clearDockSide(side, owner = null) {
if (side === 'left') {
try { window._restoreSidebarIfRouteCollapsed?.(); } catch (_) {}
}
_positionEdgeDockResizeHandles();
}
// Default dock width: ~38% of viewport, clamped to a reasonable band.
@@ -55,6 +64,78 @@ function _defaultDockWidth() {
return Math.min(640, Math.max(420, Math.round(window.innerWidth * 0.38)));
}
function _dockWidthStorageKey(modal, content, side) {
const id = modal?.id || content?.id || content?.dataset?.modalId || '';
return id ? `${EDGE_DOCK_WIDTH_KEY_PREFIX}:${side}:${id}` : null;
}
function _storedDockWidth(modal, content, side) {
const key = _dockWidthStorageKey(modal, content, side);
if (!key) return null;
try {
const n = parseFloat(localStorage.getItem(key) || '');
return Number.isFinite(n) && n > 0 ? n : null;
} catch (_) {
return null;
}
}
function _saveDockWidth(modal, content, side, width) {
const key = _dockWidthStorageKey(modal, content, side);
if (!key) return;
try { localStorage.setItem(key, String(Math.round(width))); } catch (_) {}
}
function _minEdgeDockWidth() {
return window.innerWidth < 900 ? 280 : MIN_EDGE_DOCK_WIDTH;
}
function _activeDockWidth(side) {
if (side !== 'left' && side !== 'right') return 0;
const cls = side === 'left' ? 'left-dock-active' : 'right-dock-active';
if (!document.body.classList.contains(cls)) return 0;
const prop = side === 'left' ? '--left-dock-w' : '--right-dock-w';
const raw = getComputedStyle(document.documentElement).getPropertyValue(prop);
const n = parseFloat(raw || '');
return Number.isFinite(n) && n > 0 ? n : 0;
}
function _clampDockWidthToSpace(width, min, max) {
const floor = Math.min(min, Math.max(220, Math.round(max)));
const ceiling = Math.max(floor, Math.round(max));
return Math.min(ceiling, Math.max(floor, Math.round(width)));
}
function _clampRightDockWidth(width) {
const min = _minEdgeDockWidth();
const navRight = _leftNavRight();
const leftDockW = _activeDockWidth('left');
const maxByChat = window.innerWidth - navRight - leftDockW - MIN_CHAT_WIDTH;
const max = Math.min(Math.round(window.innerWidth * 0.82), maxByChat);
return _clampDockWidthToSpace(width, min, max);
}
function _clampLeftDockWidth(width, left = _leftNavRight()) {
const min = _minEdgeDockWidth();
const rightDockW = _activeDockWidth('right');
const available = Math.max(0, window.innerWidth - left - rightDockW);
const max = Math.min(Math.round(available * 0.82), available - MIN_CHAT_WIDTH);
return _clampDockWidthToSpace(width, min, max);
}
function _resolveRightDockWidth(modal, content) {
return _clampRightDockWidth(content?._userDockWidth || _storedDockWidth(modal, content, 'right') || _defaultDockWidth());
}
function _resolveLeftDockWidth(content, left = _leftNavRight()) {
return _clampLeftDockWidth(content?._userDockWidth || _storedDockWidth(content?._dockOwner, content, 'left') || _resolveEmailDocSplitWidth(content, left), left);
}
function _isEmailDockOwner(owner) {
const id = owner?.id || '';
return id === 'email-lib-modal' || id.startsWith('email-reader-') || owner?.classList?.contains('email-window-modal');
}
function _showSnapHint(on, side = 'right') {
const cls = side === 'left' ? 'modal-snap-hint-left' : 'modal-snap-hint-right';
let hint = document.querySelector('.' + cls);
@@ -85,7 +166,7 @@ function _shouldAutoCollapseSidebar(dockW) {
const rl = (rail && window.getComputedStyle(rail).display !== 'none')
? rail.getBoundingClientRect().width
: 0;
const remaining = window.innerWidth - sb - rl - dockW;
const remaining = window.innerWidth - sb - rl - _activeDockWidth('left') - dockW;
return remaining < MIN_CHAT_WIDTH;
}
@@ -154,7 +235,7 @@ function _applyEmailDocSplitGeometry(left, emailWidth) {
if (!docPane || window.innerWidth <= 768) return;
docPane.style.setProperty('position', 'fixed', 'important');
docPane.style.setProperty('left', `${x}px`, 'important');
docPane.style.setProperty('right', '0px', 'important');
docPane.style.setProperty('right', 'var(--right-dock-w, 0px)', 'important');
docPane.style.setProperty('top', '0px', 'important');
docPane.style.setProperty('bottom', '0px', 'important');
docPane.style.setProperty('width', 'auto', 'important');
@@ -196,7 +277,9 @@ function _resolveEmailDocSplitWidth(content, left) {
function _anchorLeftDock(content) {
if (!content || content._dockSide !== 'left') return;
const left = _leftNavRight();
const w = _resolveEmailDocSplitWidth(content, left);
const w = document.body.classList.contains('doc-view')
? _resolveEmailDocSplitWidth(content, left)
: _resolveLeftDockWidth(content, left);
content.style.left = left + 'px';
content.style.width = w + 'px';
content.style.maxWidth = w + 'px';
@@ -205,17 +288,21 @@ function _anchorLeftDock(content) {
// the doc-pane becomes position:fixed starting at the email's right edge.
// No flex/max-width fighting; the doc just owns the right side from the
// email's right edge to the viewport edge — they touch flush, no gap.
const docOpen = document.body.classList.contains('doc-view');
const docOpen = document.body.classList.contains('doc-view') && _isEmailDockOwner(content._dockOwner);
if (docOpen) {
if (!document.body.classList.contains('email-doc-split-active')) {
document.body.classList.add('email-doc-split-active');
}
document.documentElement.style.setProperty('--left-dock-w', '0px');
_applyEmailDocSplitGeometry(left, w);
} else if (document.body.classList.contains('email-doc-split-active')) {
_clearEmailDocSplitGeometry();
} else {
document.documentElement.style.setProperty('--left-dock-w', w + 'px');
}
}
export function collapseSidebarToRail() { return _collapseSidebarToRail(); }
function _collapseSidebarToRail() {
const sidebar = document.getElementById('sidebar');
const rail = document.getElementById('icon-rail');
@@ -316,19 +403,21 @@ function _applyDockInternal(modal, side, dockClass) {
content.style.margin = '0';
let w;
if (side === 'left') {
// Email-style left dock: collapse the sidebar to the icon rail, then
// OVERLAY the window beside the rail, covering the chat area. We anchor
// at the rail's right edge (so it sits to the RIGHT of the rail — not
// left of the sidebar) and DON'T reserve body padding (so it covers the
// chat rather than pushing it), leaving the right side free for the doc.
// Left dock: collapse the sidebar to the icon rail, then pin the window
// beside the rail. Normal left docks reserve their width so chat shrinks;
// the email+document split keeps its existing overlay geometry.
_collapseSidebarToRail();
content._preDockSnapshot.collapsedSidebar = true;
content.style.right = 'auto';
content._dockSide = 'left';
content._dockOwner = modal;
_anchorLeftDock(content);
w = parseFloat(content.style.width) || 0;
document.body.classList.add('left-dock-active');
document.documentElement.style.setProperty('--left-dock-w', '0px'); // overlay, no push
document.documentElement.style.setProperty(
'--left-dock-w',
document.body.classList.contains('email-doc-split-active') ? '0px' : w + 'px',
);
// Re-anchor the email when the sidebar is toggled (expanded/collapsed) so
// the nav slides the window over instead of growing on top of it. Also
// re-anchor when the document editor pane appears/disappears (signaled by
@@ -406,7 +495,7 @@ function _applyDockInternal(modal, side, dockClass) {
};
}
} else {
w = _defaultDockWidth();
w = _resolveRightDockWidth(modal, content);
content.style.left = 'auto';
content.style.right = '0';
content.style.width = w + 'px';
@@ -419,6 +508,8 @@ function _applyDockInternal(modal, side, dockClass) {
}
}
content._dockSide = side;
content._dockOwner = modal;
_positionEdgeDockResizeHandles();
// Watch for the docked modal disappearing (removed from DOM or hidden
// via .hidden class) and clean up the body padding + sidebar in that
// case. Without this, closing a docked window leaves a phantom strip
@@ -426,11 +517,16 @@ function _applyDockInternal(modal, side, dockClass) {
// its padding-right.
if (!modal._dockCloseWatcher && typeof MutationObserver !== 'undefined') {
const onGone = () => _onDockedModalGone(modal, dockClass);
// Watch the modal itself for hidden-class flips and parent removal.
const obs = new MutationObserver(() => {
if (!modal.isConnected || modal.classList.contains('hidden')) onGone();
});
obs.observe(modal, { attributes: true, attributeFilter: ['class'] });
// Watch the modal for: the `.hidden` class flip, an inline
// `display:none` (how the draggable modals — calendar, plan, workspace,
// etc. — actually close), and parent removal. Without the `style` filter
// a display:none close left the body's dock padding on, so the chat
// stayed shifted after the docked modal was closed.
const _isGone = () => !modal.isConnected
|| modal.classList.contains('hidden')
|| modal.style.display === 'none';
const obs = new MutationObserver(() => { if (_isGone()) onGone(); });
obs.observe(modal, { attributes: true, attributeFilter: ['class', 'style'] });
// A second observer catches DOM removal — childList on the parent
// is the reliable signal for `.remove()` / `.removeChild()` calls.
if (modal.parentNode) {
@@ -475,6 +571,27 @@ function _onDockedModalGone(modal, dockClass) {
}
modal.classList.remove('modal-right-docked');
modal.classList.remove('modal-left-docked');
// Clear the content's docked inline geometry. Singleton modals (plan,
// workspace, calendar, …) reuse the same element across open/close, so if we
// only drop the body push the element stays positioned (position:fixed;
// right:0; fixed width) on the next open — floating over the chat with no
// push. We deliberately do NOT restore the pre-dock snapshot here: that
// snapshot is the drag position from when the user pulled the window to the
// edge (near the side), so restoring it would reopen the modal off to the
// side, still overlapping. Clearing the inline styles lets the modal reopen
// at its CSS default (centered). Drag-to-undock still uses clearRightDock,
// which DOES restore the snapshot for the peel-off feel.
if (_c) {
for (const prop of ['position', 'inset', 'left', 'top', 'right', 'bottom',
'width', 'maxWidth', 'height', 'maxHeight',
'borderRadius', 'transform', 'margin']) {
_c.style[prop] = '';
}
delete _c._preDockSnapshot;
delete _c._dockSide;
delete _c._dockOwner;
}
_positionEdgeDockResizeHandles();
}
function _expandSidebarFromRail() {
@@ -498,7 +615,11 @@ export function clearRightDock(modal, cx, cy, dockClass) {
if (!modal.classList.contains(dockClass)) return;
modal.classList.remove(dockClass);
clearDockSide(side, modal);
if (side === 'left' && !_hasOtherDockedWindow('left', modal)) {
_clearEmailDocSplitGeometry();
}
delete content._dockSide;
delete content._dockOwner;
_disconnectLeftDockObservers(content);
const snap = content._preDockSnapshot;
// Re-expand the wide sidebar if we collapsed it — but only if the
@@ -544,6 +665,7 @@ export function clearRightDock(modal, cx, cy, dockClass) {
content.style.top = (typeof targetTop === 'number') ? targetTop + 'px' : targetTop;
delete content._preDockSnapshot;
delete content._dockSuspended;
_positionEdgeDockResizeHandles();
}
// Temporarily release a docked modal's body push (chat returns to full
@@ -555,8 +677,10 @@ export function suspendDock(modal) {
const nodes = _resolveDockNodes(modal);
if (!nodes || !nodes.content) return null;
const content = nodes.content;
const hadEmailSnapLeft = modal.classList.contains('email-snap-left');
const side = content._dockSide
|| (modal.classList.contains('modal-left-docked') ? 'left'
: modal.classList.contains('email-snap-left') ? 'left'
: modal.classList.contains('modal-right-docked') ? 'right' : null);
if (!side) return null;
// Stop the close-watcher from tearing the dock fully down when `.hidden`
@@ -568,10 +692,25 @@ export function suspendDock(modal) {
}
// Release the body push + restore the sidebar so the chat fills the width.
clearDockSide(side, modal);
if (side === 'left') {
_disconnectLeftDockObservers(content);
}
if (hadEmailSnapLeft) {
modal.classList.remove('email-snap-left');
_clearEmailDocSplitGeometry();
delete content._dockSide;
delete content._dockOwner;
delete content._dockSuspended;
return null;
}
if (side === 'left' && !_hasOtherDockedWindow('left', modal)) {
_clearEmailDocSplitGeometry();
}
if (content._preDockSnapshot?.collapsedSidebar && !_hasAnyOtherDockedWindow(modal)) {
_expandSidebarFromRail();
}
content._dockSuspended = side;
_positionEdgeDockResizeHandles();
return side;
}
@@ -599,15 +738,11 @@ export function makeRightDockController(modal, dockClass = 'modal-right-docked')
return makeEdgeDockController(modal, 'right', dockClass);
}
// Read live rail+sidebar width — used as the LEFT "edge" for snap
// detection, since the visible left boundary the user can drag to is
// the nav, not x=0 (the rail covers 0..48 and the wide sidebar covers
// 0..~290 when open).
// Read the current visible left-nav edge for snap detection. Use measured
// geometry instead of CSS vars because the sidebar can auto-collapse during a
// dock operation while --sidebar-w is still settling.
function _leftNavWidth() {
const rs = getComputedStyle(document.documentElement);
const rail = parseInt(rs.getPropertyValue('--icon-rail-w') || '48', 10) || 0;
const sb = parseInt(rs.getPropertyValue('--sidebar-w') || '0', 10) || 0;
return rail + sb;
return _leftNavRight();
}
// Generic edge-snap controller. `side` is 'left' or 'right'. Same pattern
@@ -650,6 +785,210 @@ export function makeEdgeDockController(modal, side = 'right', dockClass) {
};
}
(function _initEdgeDockResizeHandles() {
if (typeof document === 'undefined') return;
if (!document.body) {
document.addEventListener('DOMContentLoaded', _initEdgeDockResizeHandles, { once: true });
return;
}
const handles = {
left: document.createElement('div'),
right: document.createElement('div'),
};
const _setStyle = (el, prop, value) => {
if (el.style[prop] !== value) el.style[prop] = value;
};
const _hideHandle = (handle) => _setStyle(handle, 'display', 'none');
for (const side of ['left', 'right']) {
const handle = handles[side];
handle.className = `edge-dock-resize-handle edge-dock-resize-handle-${side}`;
handle.style.position = 'fixed';
handle.style.top = '0';
handle.style.bottom = '0';
handle.style.width = '10px';
handle.style.cursor = 'col-resize';
// Invisible at rest, accent stripe fades in on hover (see
// .edge-dock-resize-handle CSS rule).
handle.style.background = 'transparent';
handle.style.transition = 'background 0.18s ease';
handle.style.pointerEvents = 'auto';
handle.style.touchAction = 'none';
handle.style.display = 'none';
handle.title = 'Drag to resize docked window';
document.body.appendChild(handle);
}
const _isUsableDockOwner = (owner) => {
if (!owner || !owner.isConnected) return false;
if (owner.classList?.contains('hidden')) return false;
if (owner.style?.display === 'none') return false;
const nodes = _resolveDockNodes(owner);
const content = nodes?.content;
if (!content || !content.isConnected) return false;
if (content.classList?.contains('hidden')) return false;
if (content.style?.display === 'none') return false;
const r = content.getBoundingClientRect();
return r.width > 0 && r.height > 0;
};
const _activeDockOwner = (side) => {
const cls = _dockClassForSide(side);
const all = Array.from(document.querySelectorAll(`.${cls}`));
for (const owner of all.reverse()) {
if (_isUsableDockOwner(owner)) return owner;
}
return null;
};
const _zIndexFor = (el, fallback = 250) => {
const raw = el ? window.getComputedStyle(el).zIndex : '';
const n = parseInt(raw, 10);
return Number.isFinite(n) ? n : fallback;
};
const _hasVisibleFloatingModal = (owner) => {
const all = Array.from(document.querySelectorAll('.modal:not(.hidden):not(.modal-minimized)'));
return all.some((modal) => {
if (!modal || modal === owner) return false;
if (owner?.contains?.(modal) || modal.contains?.(owner)) return false;
if (modal.classList.contains('modal-left-docked')
|| modal.classList.contains('modal-right-docked')
|| modal.classList.contains('email-snap-left')) return false;
if (modal.style.display === 'none') return false;
const content = _resolveDockNodes(modal)?.content;
const r = content?.getBoundingClientRect?.();
return !!r && r.width > 0 && r.height > 0;
});
};
const _setWidth = (owner, side, clientX) => {
const nodes = _resolveDockNodes(owner);
const content = nodes?.content;
if (!content) return 0;
let w = 0;
if (side === 'right') {
w = _clampRightDockWidth(window.innerWidth - clientX);
content._userDockWidth = w;
content.style.left = 'auto';
content.style.right = '0';
content.style.width = w + 'px';
content.style.maxWidth = w + 'px';
document.body.classList.add('right-dock-active');
document.documentElement.style.setProperty('--right-dock-w', w + 'px');
if (_shouldAutoCollapseSidebar(w)) {
_collapseSidebarToRail();
if (content._preDockSnapshot) content._preDockSnapshot.collapsedSidebar = true;
}
} else {
const left = _leftNavRight();
w = _clampLeftDockWidth(clientX - left, left);
content._userDockWidth = w;
content._emailDocSplitUserW = w;
content.style.left = left + 'px';
content.style.right = 'auto';
content.style.width = w + 'px';
content.style.maxWidth = w + 'px';
document.body.classList.add('left-dock-active');
document.documentElement.style.setProperty(
'--left-dock-w',
document.body.classList.contains('email-doc-split-active') ? '0px' : w + 'px',
);
}
_positionEdgeDockResizeHandles();
return w;
};
_edgeDockHandlePositioner = () => {
const splitOwnsLeftSeam = document.body.classList.contains('email-doc-split-active')
&& document.body.classList.contains('doc-view')
&& window.innerWidth > 768;
for (const side of ['left', 'right']) {
const handle = handles[side];
if (window.innerWidth <= 768 || (side === 'left' && splitOwnsLeftSeam)) {
_hideHandle(handle);
continue;
}
const owner = _activeDockOwner(side);
const content = owner && _resolveDockNodes(owner)?.content;
if (!content) {
_hideHandle(handle);
continue;
}
if (_hasVisibleFloatingModal(owner)) {
_hideHandle(handle);
continue;
}
const r = content.getBoundingClientRect();
const x = side === 'right' ? r.left : r.right;
if (!Number.isFinite(x) || x <= 0 || x >= window.innerWidth) {
_hideHandle(handle);
continue;
}
_setStyle(handle, 'display', 'block');
_setStyle(handle, 'left', (x - 5) + 'px');
_setStyle(handle, 'zIndex', String(_zIndexFor(owner) + 1));
}
};
for (const side of ['left', 'right']) {
const handle = handles[side];
handle.addEventListener('pointerdown', (e) => {
if (handle.style.display === 'none') return;
const owner = _activeDockOwner(side);
if (!owner) return;
e.preventDefault();
e.stopPropagation();
handle.setPointerCapture?.(e.pointerId);
const nodes = _resolveDockNodes(owner);
const content = nodes?.content;
const prevCursor = document.body.style.cursor;
const prevUserSelect = document.body.style.userSelect;
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
document.body.classList.add('edge-dock-resizing');
_setWidth(owner, side, e.clientX);
const onMove = (ev) => {
ev.preventDefault();
_setWidth(owner, side, ev.clientX);
};
const onUp = (ev) => {
try { handle.releasePointerCapture?.(e.pointerId); } catch (_) {}
document.removeEventListener('pointermove', onMove, true);
document.removeEventListener('pointerup', onUp, true);
document.removeEventListener('pointercancel', onUp, true);
document.body.classList.remove('edge-dock-resizing');
document.body.style.cursor = prevCursor;
document.body.style.userSelect = prevUserSelect;
const finalW = side === 'right'
? parseFloat(document.documentElement.style.getPropertyValue('--right-dock-w')) || content?.getBoundingClientRect?.().width || 0
: content?.getBoundingClientRect?.().width || 0;
if (finalW) _saveDockWidth(owner, content, side, finalW);
ev.preventDefault();
};
document.addEventListener('pointermove', onMove, true);
document.addEventListener('pointerup', onUp, true);
document.addEventListener('pointercancel', onUp, true);
});
}
new MutationObserver(_positionEdgeDockResizeHandles).observe(document.body, { attributes: true, attributeFilter: ['class'] });
new MutationObserver(_positionEdgeDockResizeHandles).observe(document.documentElement, { attributes: true, attributeFilter: ['style'] });
let raf = 0;
const schedulePosition = () => {
if (raf) return;
raf = requestAnimationFrame(() => {
raf = 0;
_positionEdgeDockResizeHandles();
});
};
new MutationObserver(schedulePosition).observe(document.body, { childList: true });
window.addEventListener('resize', _positionEdgeDockResizeHandles);
window.addEventListener('odysseus:modal-opened', _positionEdgeDockResizeHandles);
_positionEdgeDockResizeHandles();
})();
(function _initSplitSeamIndicator() {
if (typeof document === 'undefined') return;
const stripe = document.createElement('div');
@@ -659,7 +998,7 @@ export function makeEdgeDockController(modal, side = 'right', dockClass) {
stripe.style.bottom = '0';
stripe.style.width = '10px';
stripe.style.cursor = 'col-resize';
stripe.style.zIndex = '9999';
stripe.style.zIndex = '261';
stripe.style.background = 'linear-gradient(to right, transparent 0 3px, color-mix(in srgb, var(--accent, var(--red)) 35%, transparent) 3px 7px, transparent 7px 10px)';
stripe.style.pointerEvents = 'auto';
stripe.style.touchAction = 'none';