From b9038eafccb5d9b8c5003211213479657cda17c5 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 10 Sep 2026 02:56:40 +0200 Subject: [PATCH] fix: the assistant loads again, the switch fits, and a hidden menu can be brought back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three faults from moving the switch into the app sidebar: 1. Clicking Assistant did nothing. activateTab() bailed unless a matching .tab-btn existed, and removing the "AI Assistant" nav entry removed exactly that — so every /assistant visit fell through to the fallback tab. A tab reached from the mode switch legitimately has a section without a button, so activateTab now keys off the section and only touches the button if present. 2. The switch overflowed its box. The sidebar is 210px; without min-width:0 the two buttons refused to shrink below their content. They now shrink, with a slightly tighter font and gap so both labels fit. 3. "Hide menu" had no way back. The toggle sits inside the sidebar it collapses, so it disappeared with it. It now leaves the collapsing box and pins itself beside the content, in both the app sidebar and the assistant rail, and the label already flips to "Show menu". Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WjVc5oaAaYFNbZGLeJp6TX --- public/css/styles.css | 20 ++++++++++++++------ public/js/app.js | 9 ++++++--- test/assistant-workspace-layout.test.js | 9 +++++++-- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/public/css/styles.css b/public/css/styles.css index 5c86b63c..206b7861 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -1217,9 +1217,11 @@ button, a, .btn-sm, .btn-generate, .btn-send, .tab-btn, input, textarea, select, /* ── Assistant / Workspace switch ──────────────────────────────────────────── Rendered identically in the app sidebar and the assistant rail, so moving between them changes which pill is highlighted and nothing else. */ -.assistant-mode-switch { display:grid; grid-template-columns:1fr 1fr; gap:4px; margin:8px 12px 10px; padding:4px; background:var(--g100); border:1px solid var(--g200); border-radius:10px; } -.assistant-mode-switch button { display:flex; align-items:center; justify-content:center; gap:6px; border:0; border-radius:7px; padding:7px 6px; font-family:inherit; font-size:12px; font-weight:600; color:var(--g600); background:transparent; cursor:pointer; } -.assistant-mode-switch button i { font-size:11px; } +/* The sidebar is 210px wide. Without min-width:0 the buttons refuse to shrink + below their content and the switch overflows its own box. */ +.assistant-mode-switch { display:grid; grid-template-columns:1fr 1fr; gap:4px; margin:8px 12px 10px; padding:4px; background:var(--g100); border:1px solid var(--g200); border-radius:10px; box-sizing:border-box; max-width:calc(100% - 24px); } +.assistant-mode-switch button { display:flex; align-items:center; justify-content:center; gap:5px; min-width:0; border:0; border-radius:7px; padding:7px 4px; font-family:inherit; font-size:11.5px; font-weight:600; color:var(--g600); background:transparent; cursor:pointer; white-space:nowrap; overflow:hidden; } +.assistant-mode-switch button i { font-size:11px; flex:0 0 auto; } .assistant-mode-switch button:hover { color:var(--g900); } .assistant-mode-switch button.active { background:white; color:var(--blue); box-shadow:var(--shadow); } /* Inside the assistant rail the gutters already exist, so it only needs the gap. */ @@ -1232,11 +1234,17 @@ button, a, .btn-sm, .btn-generate, .btn-send, .tab-btn, input, textarea, select, /* Hidden menu: the rail collapses in both places, and the layout takes the space back rather than leaving a gutter. */ -body.menu-hidden .sidebar { width:0; border:none; overflow:hidden; } +/* Collapsing must not take the control away with it, or there is no way back. + The toggle leaves the collapsing box and pins itself beside the content. */ +body.menu-hidden .sidebar { width:0; border:none; overflow:visible; } +body.menu-hidden .sidebar > *:not(.sidebar-nav) { display:none; } +body.menu-hidden .sidebar-nav > *:not(.assistant-menu-toggle) { display:none; } +body.menu-hidden .assistant-menu-toggle, +body.assistant-workspace.menu-hidden .assistant-history .assistant-menu-toggle { + position:fixed; top:76px; left:10px; z-index:120; margin:0; background:white; box-shadow:var(--shadow); } +body.assistant-workspace.menu-hidden .assistant-menu-toggle { top:10px; } body.assistant-workspace.menu-hidden .assistant-layout { grid-template-columns:0 minmax(0,1fr) 330px; } body.assistant-workspace.menu-hidden.assistant-no-citations .assistant-layout { grid-template-columns:0 minmax(0,1fr); } body.assistant-workspace.menu-hidden .assistant-history > *:not(.assistant-menu-toggle) { display:none; } body.assistant-workspace.menu-hidden .assistant-history { width:0; min-width:0; overflow:visible; padding:0; } -/* The toggle must survive its own collapse, or there is no way back. */ -body.assistant-workspace.menu-hidden .assistant-history .assistant-menu-toggle { position:absolute; left:8px; top:8px; z-index:5; } @media (max-width:640px) { .assistant-menu-toggle { display:none; } body.menu-hidden .sidebar { width:auto; } } diff --git a/public/js/app.js b/public/js/app.js index 7e442e53..bf8d3013 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -100,12 +100,15 @@ document.addEventListener('DOMContentLoaded', function() { // --- TAB NAVIGATION --- function activateTab(tabName) { var btn = document.querySelector('.tab-btn[data-tab="' + tabName + '"]'); - if (!btn || btn.classList.contains('hidden')) return false; + var tabEl = document.getElementById(tabName + '-tab'); + // The assistant is reached from the mode switch, not a sidebar button, so a + // tab can legitimately have a section without one. Requiring a button here + // silently sent every /assistant visit to the fallback tab instead. + if (btn ? btn.classList.contains('hidden') : !tabEl) return false; var activation = ++_tabActivation; document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); }); document.querySelectorAll('.tab-content').forEach(function(c) { c.classList.remove('active'); }); - btn.classList.add('active'); - var tabEl = document.getElementById(tabName + '-tab'); + if (btn) btn.classList.add('active'); if (tabEl) { tabEl.classList.add('active'); // Lazy-load component HTML, then fire tabChanged after DOM is ready diff --git a/test/assistant-workspace-layout.test.js b/test/assistant-workspace-layout.test.js index dd5dfe73..ce623433 100644 --- a/test/assistant-workspace-layout.test.js +++ b/test/assistant-workspace-layout.test.js @@ -115,8 +115,13 @@ test('one menu toggle serves both the app sidebar and the assistant rail', () => // Shared chrome lives in styles.css: the app page never loads assistant.css. const css = read('public/css/styles.css'); assert.match(css, /body\.menu-hidden \.sidebar \{ width:0/); - assert.match(css, /body\.assistant-workspace\.menu-hidden \.assistant-history \.assistant-menu-toggle \{ position:absolute/, - 'the toggle survives its own collapse, or there is no way back'); + // The toggle lives inside the box it collapses, so it must leave that box or + // there is no way to bring the menu back. + assert.match(css, /body\.menu-hidden \.assistant-menu-toggle,\n[^\n]*assistant-menu-toggle \{\n\s*position:fixed/, + 'the toggle pins itself outside the collapsed menu'); + assert.match(css, /body\.menu-hidden \.sidebar-nav > \*:not\(\.assistant-menu-toggle\) \{ display:none; \}/, + 'everything else in the collapsed sidebar is hidden'); + assert.match(read('public/js/app.js'), /'Show menu' : 'Hide menu'/, 'and it says how to get back'); }); test('mobile drawer rows carry no chat icons and keep the options menu', () => {