Improve edge-docked window behavior (#2779)

* Make edge-docked windows resizable

Add draggable resize seams for left and right docked windows.

Keep the main chat area from getting too narrow and remember each window's dock width.

* Show emoji shortcodes as icons by default

Keep text-only emoji mode opt-in so model output like 😊 goes through the normal emoji renderer.

* Fix dock resize seams and left dock layout

Hide the resize seam when another floating modal is open, and keep the left-docked window from covering the chat area.

* Keep narrow modal tabs usable

* Fix split layout with both edge docks

* Fix left snap after right dock

* Enable left edge snap for all windows

* Tighten dock resize handle observers

* Use edge docking for settings window
This commit is contained in:
Enes Öz
2026-06-05 18:07:08 +03:00
committed by GitHub
parent 8ce945d338
commit 977daf0643
5 changed files with 434 additions and 43 deletions
+4 -6
View File
@@ -2497,7 +2497,7 @@ function initializeEventListeners() {
};
// Keys hidden by default on first run (no localStorage yet)
const UI_VIS_DEFAULT_OFF = new Set(['models-section', 'rag-toggle-btn']);
const UI_VIS_DEFAULT_OFF = new Set(['models-section', 'rag-toggle-btn', 'text-emojis']);
// Keys that need admin to toggle off (reserved for future use)
const UI_VIS_ADMIN_ONLY = new Set([]);
@@ -2525,11 +2525,9 @@ function initializeEventListeners() {
document.querySelectorAll('.section[draggable]').forEach(el => {
el.setAttribute('draggable', dragEnabled ? 'true' : 'false');
});
// Text-only emojis toggle. Default is ON (the checkbox defaults to
// checked because text-emojis isn't in UI_VIS_DEFAULT_OFF), so treat
// an absent value as enabled — otherwise the toggle looked on at
// startup but the effect only activated after the user flipped it.
applyTextEmojis(state['text-emojis'] !== false);
// Text-only emojis toggle. Default is OFF so model-emitted shortcodes
// like `:blush:` render through the normal monochrome emoji path.
applyTextEmojis(state['text-emojis'] === true);
// Hide thinking sections toggle (show-thinking: checked=show, unchecked=hide)
document.body.classList.toggle('hide-thinking', state['show-thinking'] === false);
}
+314 -21
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,14 +288,17 @@ 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');
}
}
@@ -316,19 +402,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 +494,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 +507,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
@@ -498,7 +588,9 @@ function _onDockedModalGone(modal, dockClass) {
}
delete _c._preDockSnapshot;
delete _c._dockSide;
delete _c._dockOwner;
}
_positionEdgeDockResizeHandles();
}
function _expandSidebarFromRail() {
@@ -526,6 +618,7 @@ export function clearRightDock(modal, cx, cy, dockClass) {
_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
@@ -571,6 +664,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
@@ -604,6 +698,7 @@ export function suspendDock(modal) {
modal.classList.remove('email-snap-left');
_clearEmailDocSplitGeometry();
delete content._dockSide;
delete content._dockOwner;
delete content._dockSuspended;
return null;
}
@@ -614,6 +709,7 @@ export function suspendDock(modal) {
_expandSidebarFromRail();
}
content._dockSuspended = side;
_positionEdgeDockResizeHandles();
return side;
}
@@ -641,15 +737,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
@@ -692,6 +784,207 @@ 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';
handle.style.background = 'linear-gradient(to right, transparent 0 3px, color-mix(in srgb, var(--accent, var(--red)) 35%, transparent) 3px 7px, transparent 7px 10px)';
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');
+1 -1
View File
@@ -53,7 +53,7 @@ function initDrag() {
content,
header,
skipSelector: 'button, input, select, .theme-opacity-wrap',
enableDock: false,
enableDock: true,
});
}
+5 -5
View File
@@ -93,11 +93,11 @@ export function makeWindowDraggable(modal, options = {}) {
}
const rightDock = enableDock ? makeEdgeDockController(modal, 'right') : null;
// Left dock is opt-in (enableLeftDock). For most windows it's off — the
// sidebar lives on the left, so a left dock collides with it. The email
// window enables it so you can park the message on the left and read it
// while replying in the document on the right.
const leftDock = (enableDock && options.enableLeftDock) ? makeEdgeDockController(modal, 'left') : null;
// Left dock is enabled by default too. modalSnap collapses the wide sidebar
// and anchors the panel beside the icon rail, so it no longer collides with
// the navigation. Callers can still pass enableLeftDock:false for a special
// modal that should only dock right.
const leftDock = (enableDock && options.enableLeftDock !== false) ? makeEdgeDockController(modal, 'left') : null;
// Per-drag state, reset on mousedown.
let dragging = false;
+110 -10
View File
@@ -97,9 +97,9 @@ html, body { overflow-x: hidden; height: 100%; margin: 0; overscroll-behavior: n
body {
background-color: var(--bg);
color: var(--fg);
/* Animate the dock push BOTH ways. Keeping the transition on the base body
(not on .right/left-dock-active) means removing the class on undock also
animates padding back to 0 otherwise the chat snapped back instantly. */
/* Keep the base padding transition for older layout paths that still adjust
the body directly. Edge docks reserve workspace room on the flex panes
below so left + right docks can coexist without skewing the whole body. */
transition: padding-left 160ms cubic-bezier(0.22, 0.61, 0.36, 1),
padding-right 160ms cubic-bezier(0.22, 0.61, 0.36, 1);
font-family: var(--font-family, 'Fira Code', monospace);
@@ -1773,6 +1773,8 @@ body.bg-pattern-sparkles {
min-width:0;
margin-top:8px;
margin-bottom: 0;
transition: margin-left 160ms cubic-bezier(0.22, 0.61, 0.36, 1),
margin-right 160ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.chat-meta { font-size:12px; color:color-mix(in srgb, var(--fg) 60%, transparent); margin-bottom:6px; }
.chat-history {
@@ -4939,6 +4941,15 @@ body.bg-pattern-sparkles {
pointer-events:auto;
animation: modal-enter 0.25s ease-out both;
}
.memory-modal-content,
.tasks-modal-content,
.preset-modal-content,
#cookbook-modal .modal-content,
#theme-popup,
.doclib-modal-content,
.gallery-modal-content {
container-type: inline-size;
}
.modal-header {
display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;
cursor:grab; user-select:none;
@@ -14843,7 +14854,7 @@ body:has(.doc-version-panel:not(.hidden)) .hamburger-btn {
body.email-doc-split-active.doc-view .doc-editor-pane {
position: fixed !important;
left: var(--email-doc-split-right-x, 420px) !important;
right: 0 !important;
right: var(--right-dock-w, 0px) !important;
top: 0 !important;
bottom: 0 !important;
width: auto !important;
@@ -14864,15 +14875,21 @@ body [data-act="from-sender"] {
display: none !important;
}
/* Snap-to-right docking. A modal dragged to the right edge becomes a
docked side panel (mirrors Notes/Doc panels). Body reserves space via
padding-right so the chat / notes / doc panel underneath shrinks to
fit instead of being hidden behind the panel. */
/* Edge docking. Docked panels are fixed to the viewport edge; the workspace
panes reserve room with margins so left + right docks can be active at the
same time without skewing the entire body box. */
body.right-dock-active {
padding-right: var(--right-dock-w, 0px);
padding-right: 0;
}
body.left-dock-active {
padding-left: var(--left-dock-w, 0px);
padding-left: 0;
}
body.left-dock-active:not(.email-doc-split-active) .chat-container {
margin-left: var(--left-dock-w, 0px);
}
body.right-dock-active .chat-container,
body.right-dock-active:not(.email-doc-split-active) .doc-editor-pane {
margin-right: var(--right-dock-w, 0px);
}
.modal.modal-right-docked {
align-items: stretch;
@@ -23192,6 +23209,89 @@ input.settings-select::placeholder { color: color-mix(in srgb, var(--fg) 35%, tr
opacity: 1;
border-bottom-color: var(--red);
}
/* Narrow modal tab strips should stay on one row. Resized docked windows can
be much narrower than the viewport, so this cannot live only in mobile media
queries. */
.cookbook-tabs,
.memory-tabs,
.admin-tabs,
.lib-tabs,
.gallery-tabs,
.preset-tabs {
flex-wrap: nowrap !important;
overflow-x: auto !important;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
overscroll-behavior-x: contain;
scrollbar-width: none;
}
.cookbook-tabs::-webkit-scrollbar,
.memory-tabs::-webkit-scrollbar,
.admin-tabs::-webkit-scrollbar,
.lib-tabs::-webkit-scrollbar,
.gallery-tabs::-webkit-scrollbar,
.preset-tabs::-webkit-scrollbar {
display: none;
}
.cookbook-tabs > *,
.memory-tabs > *,
.admin-tabs > *,
.lib-tabs > *,
.gallery-tabs > *,
.preset-tabs > * {
flex: 0 0 auto;
}
.cookbook-tab,
.memory-tab,
.admin-tab,
.lib-tab,
.gallery-tab,
.preset-tab {
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
line-height: 1;
}
.gallery-tab {
gap: 6px;
}
@container (max-width: 360px) {
.cookbook-tab:has(svg),
.memory-tab:has(svg),
.admin-tab:has(svg),
.lib-tab:has(svg),
.gallery-tab:has(svg),
.preset-tab:has(svg) {
width: 34px;
min-width: 34px;
padding-left: 0;
padding-right: 0;
font-size: 0;
}
.cookbook-tab:has(svg) svg,
.memory-tab:has(svg) svg,
.admin-tab:has(svg) svg,
.lib-tab:has(svg) svg,
.gallery-tab:has(svg) svg,
.preset-tab:has(svg) svg {
width: 14px;
height: 14px;
margin-right: 0 !important;
vertical-align: middle !important;
}
.memory-tab:has(svg) .memory-count,
.gallery-tab:has(svg) .gallery-tab-label,
.gallery-tab:has(svg) .gallery-tab-close,
.cookbook-tab:has(svg) .cookbook-tab-count,
.preset-tab:has(svg) .preset-count {
display: none !important;
}
}
/* Icon + label layout inside each tab. */
.gallery-tab {
display: inline-flex;