diff --git a/src/frontend/web/von_interface/static/js/chatTab.js b/src/frontend/web/von_interface/static/js/chatTab.js index c87feca6..0f98fe17 100644 --- a/src/frontend/web/von_interface/static/js/chatTab.js +++ b/src/frontend/web/von_interface/static/js/chatTab.js @@ -1361,24 +1361,15 @@ function isChatSessionTabsNarrowViewport() { } function getEffectiveChatSessionTabsLayout(preferredLayout = chatSessionTabsLayout) { - const preferred = normaliseChatSessionTabsLayout(preferredLayout); - if (preferred !== CHAT_SESSION_TABS_LAYOUT_VERTICAL) { - return CHAT_SESSION_TABS_LAYOUT_HORIZONTAL; - } - if (isChatSessionTabsNarrowViewport()) { - return CHAT_SESSION_TABS_LAYOUT_HORIZONTAL; - } - return CHAT_SESSION_TABS_LAYOUT_VERTICAL; + // Desktop layout is a preference; narrow/touch navigation always uses the tray. + return isChatSessionTabsNarrowViewport() + ? CHAT_SESSION_TABS_LAYOUT_VERTICAL + : normaliseChatSessionTabsLayout(preferredLayout); } function getChatSessionTabsLayoutMenuLabel() { - if (chatSessionTabsLayout === CHAT_SESSION_TABS_LAYOUT_VERTICAL) { - return isChatSessionTabsNarrowViewport() - ? 'Use conversation tabs across top on wider screens' - : 'Move conversation tabs to top'; - } - return isChatSessionTabsNarrowViewport() - ? 'Use conversation tabs on left on wider screens' + return chatSessionTabsLayout === CHAT_SESSION_TABS_LAYOUT_VERTICAL + ? 'Move conversation tabs to top' : 'Move conversation tabs to left'; } @@ -1443,9 +1434,8 @@ function setChatSessionTabsLayout(layout, options = {}) { } if (options.announce === true) { let message; - if (result.preferred === CHAT_SESSION_TABS_LAYOUT_VERTICAL - && result.effective === CHAT_SESSION_TABS_LAYOUT_HORIZONTAL) { - message = 'Left conversation tabs saved for wider screens.'; + if (isChatSessionTabsNarrowViewport()) { + message = 'Desktop conversation layout saved.'; } else if (result.preferred === CHAT_SESSION_TABS_LAYOUT_VERTICAL) { message = 'Conversation tabs moved to the left.'; } else { @@ -27756,12 +27746,12 @@ function buildNewChatContextMenuItems() { }); } }, - { + ...(!isChatSessionTabsNarrowViewport() ? [{ label: getChatSessionTabsLayoutMenuLabel(), onClick: () => { toggleChatSessionTabsLayout({ announce: true }); } - } + }] : []) ]; const agentTotalCount = agentCreatedSessionVisibilityState.totalCount || 0; diff --git a/src/frontend/web/von_interface/static/js/components/conversationTray.js b/src/frontend/web/von_interface/static/js/components/conversationTray.js index a0fcc43e..bd247592 100644 --- a/src/frontend/web/von_interface/static/js/components/conversationTray.js +++ b/src/frontend/web/von_interface/static/js/components/conversationTray.js @@ -30,7 +30,7 @@ export function createConversationTray(workspace) { cleanup.push(() => target.removeEventListener(event, callback)); }; const isVertical = () => workspace.dataset.effectiveTabsLayout === 'vertical'; - const isMobile = () => !isVertical() && !!narrow?.matches; + const isMobile = () => !!narrow?.matches; const isCollapsed = () => isMobile() ? !!preferences.mobileCollapsed : isVertical() && preferences.collapsed; const isMenuOpen = () => !!document.querySelector('.chat-session-menu.open'); const maxWidth = () => Math.max(220, Math.min(400, (workspace.clientWidth || window.innerWidth) - 420)); @@ -46,16 +46,16 @@ export function createConversationTray(workspace) { // Scroll only the list: scrollIntoView could displace the transcript. const bounds = tabs.getBoundingClientRect(); const row = active.getBoundingClientRect(); - if (row.left < bounds.left) tabs.scrollLeft += row.left - bounds.left; - else if (row.right > bounds.right) tabs.scrollLeft += Math.min(row.left - bounds.left, row.right - bounds.right); + if (row.top < bounds.top) tabs.scrollTop += row.top - bounds.top; + else if (row.bottom > bounds.bottom) tabs.scrollTop += Math.min(row.top - bounds.top, row.bottom - bounds.bottom); } } function render() { const collapsed = isCollapsed(); workspace.dataset.trayMobile = String(isMobile()); - if (!collapsed || !isVertical()) peek = false; - const nextPresentation = isMobile() && collapsed ? 'mobile-closed' : !isVertical() ? 'horizontal' : collapsed && !peek ? 'rail' : 'expanded'; + if (!collapsed || !isVertical() || isMobile()) peek = false; + const nextPresentation = isMobile() ? (collapsed ? 'mobile-closed' : 'mobile-open') : !isVertical() ? 'horizontal' : collapsed && !peek ? 'rail' : 'expanded'; const presentationChanged = presentation !== nextPresentation; if (tabs && presentationChanged && presentation) { scrollPositions.set(presentation, { top: tabs.scrollTop, left: tabs.scrollLeft }); @@ -104,7 +104,7 @@ export function createConversationTray(workspace) { listen(toggle, 'click', () => setCollapsed(!isCollapsed())); listen(navigation, 'pointerenter', (event) => { clearTimers(); - if (event.pointerType === 'touch' || !preferences.expandOnHover || !preferences.collapsed || !isVertical()) return; + if (isMobile() || event.pointerType === 'touch' || !preferences.expandOnHover || !preferences.collapsed || !isVertical()) return; enterTimer = setTimeout(() => { peek = true; render(); }, 200); }); listen(navigation, 'pointerleave', () => { @@ -114,7 +114,7 @@ export function createConversationTray(workspace) { listen(navigation, 'focusin', (event) => { clearTimers(); // Focusing the collapse button itself must not immediately reopen it. - if (event.target !== toggle && preferences.collapsed && isVertical()) { + if (!isMobile() && event.target !== toggle && preferences.collapsed && isVertical()) { peek = true; render(); } @@ -122,7 +122,15 @@ export function createConversationTray(workspace) { listen(navigation, 'focusout', (event) => { if (!navigation.contains(event.relatedTarget)) leaveTimer = setTimeout(closePeek, 0); }); + listen(document, 'focusin', (event) => { + if (isMobile() && !isCollapsed() && !navigation?.contains(event.target) && !event.target.closest?.('.chat-session-menu')) { + setCollapsed(true); + } + }); listen(document, 'pointerdown', (event) => { + if (isMobile() && !isCollapsed() && !navigation?.contains(event.target) && !event.target.closest?.('.chat-session-menu')) { + setCollapsed(true); + } if (peek && !navigation?.contains(event.target) && !event.target.closest?.('.chat-session-menu')) { clearTimers(); peek = false; @@ -144,6 +152,10 @@ export function createConversationTray(workspace) { } }); + listen(tabs, 'click', (event) => { + if (isMobile() && event.target.closest?.('[role="tab"]')) setCollapsed(true); + }); + // Roving focus follows the actual layout. Enter/Space retain native activation. listen(tabs, 'keydown', (event) => { if (!event.target.matches?.('[role="tab"]')) return; @@ -171,7 +183,7 @@ export function createConversationTray(workspace) { } listen(resize, 'pointerdown', (event) => { - if (event.button !== 0 || !isVertical()) return; + if (event.button !== 0 || !isVertical() || isMobile()) return; event.preventDefault(); drag = { x: event.clientX, width: effectiveWidth(), savedWidth: preferences.width, pointerId: event.pointerId }; resize.setPointerCapture?.(event.pointerId); @@ -186,6 +198,7 @@ export function createConversationTray(workspace) { listen(resize, 'pointercancel', () => finishDrag(true)); listen(resize, 'lostpointercapture', () => finishDrag(true)); listen(resize, 'keydown', (event) => { + if (isMobile()) return; if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return; event.preventDefault(); preferences.width = event.key === 'Home' ? 220 : event.key === 'End' ? maxWidth() diff --git a/src/frontend/web/von_interface/static/styles.css b/src/frontend/web/von_interface/static/styles.css index cab4dcaf..8eddd9cc 100644 --- a/src/frontend/web/von_interface/static/styles.css +++ b/src/frontend/web/von_interface/static/styles.css @@ -9021,6 +9021,34 @@ body[data-active-tab="settingsTab"] .tab-content-area { .conversation-workspace[data-effective-tabs-layout="vertical"], .conversation-tray-toggle span { transition: none; } } +/* Mobile keeps chat full-width and opens the shared left tray over it. */ +.conversation-workspace[data-effective-tabs-layout="vertical"][data-tray-mobile="true"][data-tray-collapsed] { + grid-template-columns: minmax(0, 1fr); + gap: 8px; +} +.conversation-workspace[data-tray-mobile="true"] .chat-conversation-nav { + height: 58px; + max-height: none; + margin-bottom: 0; +} +.conversation-workspace[data-tray-mobile="true"] .chat-session-tabs-row { + position: absolute; + top: 0; + left: 0; + width: min(320px, 100%); + height: max(120px, calc(100dvh - var(--von-fixed-shell-height, 104px) - var(--von-fixed-footer-clearance, 64px) - 24px)); + box-shadow: 0 8px 18px rgba(15, 23, 42, 0.16); +} +.conversation-workspace[data-tray-mobile="true"][data-tray-collapsed="true"] .chat-session-tabs-row { + width: 100%; + height: 58px; + box-shadow: none; +} +.conversation-workspace[data-effective-tabs-layout="vertical"][data-tray-mobile="true"] .conversation-tray-resize { display: none; } +@media (max-width: 800px), (hover: none) and (pointer: coarse) { + .conversation-desktop-setting { display: none; } +} + /* Keep current context above the mobile list without moving any conversation. */ .conversation-workspace[data-tray-mobile="true"] .chat-session-tabs-row { flex-direction: column; @@ -9038,7 +9066,9 @@ body[data-active-tab="settingsTab"] .tab-content-area { .conversation-workspace[data-tray-mobile="true"] .conversation-tray-context { display: block; min-width: 0; - overflow-wrap: anywhere; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; font-size: 13px; font-weight: 600; } @@ -9049,8 +9079,9 @@ body[data-active-tab="settingsTab"] .tab-content-area { } .conversation-workspace[data-tray-mobile="true"] .chat-session-tabs { width: 100%; - flex: 0 1 auto; + flex: 1 1 auto; } +.conversation-workspace[data-tray-mobile="true"] .chat-session-tab { min-height: 44px; } .conversation-workspace[data-tray-mobile="true"][data-tray-collapsed="true"] .chat-session-tabs, .conversation-workspace[data-tray-mobile="true"][data-tray-collapsed="true"] .chat-session-history-summary, .conversation-workspace[data-tray-mobile="true"][data-tray-collapsed="true"] .catalogue-filters { display: none; } diff --git a/src/frontend/web/von_interface/templates/settings_tab.html b/src/frontend/web/von_interface/templates/settings_tab.html index 1b373bb9..da9cd73f 100644 --- a/src/frontend/web/von_interface/templates/settings_tab.html +++ b/src/frontend/web/von_interface/templates/settings_tab.html @@ -147,20 +147,20 @@