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', () => {