From 6c2bafbe020cc69519e624d20b96c02c0cd82e69 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 10 Sep 2026 02:49:53 +0200 Subject: [PATCH] fix: style the shared chrome from the stylesheet the app page actually loads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Assistant/Workspace switch and the menu toggle rendered as unstyled native buttons in the app sidebar. Their CSS was in assistant.css, which is pulled in by the assistant COMPONENT — so it is simply absent on "/", where those controls now also render. Both moved to styles.css, which every page loads. A stale duplicate of the switch rules was also still sitting in assistant.css; that is exactly how two copies drift and one silently wins, so it is gone. Adds a test that walks the classes rendered in index.html and asserts each is defined in styles.css and NOT duplicated in assistant.css, which would have caught this before it shipped. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WjVc5oaAaYFNbZGLeJp6TX --- public/css/assistant.css | 33 ------------------------- public/css/styles.css | 31 +++++++++++++++++++++++ test/assistant-workspace-layout.test.js | 26 ++++++++++++++++--- 3 files changed, 54 insertions(+), 36 deletions(-) diff --git a/public/css/assistant.css b/public/css/assistant.css index 32417de5..67641a7d 100644 --- a/public/css/assistant.css +++ b/public/css/assistant.css @@ -154,12 +154,6 @@ body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1 .assistant-chats-caret { font-size:10px; color:var(--g400); transition:transform .15s ease; } .assistant-chats-header[aria-expanded="false"] .assistant-chats-caret { transform:rotate(-90deg); } -/* Two-mode switch at the top of the rail, the way Home and Code swap the page. */ -.assistant-mode-switch { display:grid; grid-template-columns:1fr 1fr; gap:4px; background:var(--g100); border:1px solid var(--g200); border-radius:10px; padding:4px; margin-bottom:8px; } -.assistant-mode-switch button { display:flex; align-items:center; justify-content:center; gap:6px; border:0; border-radius:7px; padding:7px 6px; font-size:12px; font-weight:600; color:var(--g500); background:transparent; cursor:pointer; } -.assistant-mode-switch button i { font-size:11px; } -.assistant-mode-switch button.active { background:white; color:var(--blue); box-shadow:var(--shadow); } - background-image:linear-gradient(var(--g100) 1px, transparent 1px), linear-gradient(90deg, var(--g100) 1px, transparent 1px); background-size:28px 28px; background-position:-1px -1px; } /* On a phone the rail IS the drawer menu; a second copy as cards is redundant @@ -448,33 +442,6 @@ body.assistant-workspace #assistant-chat-view { min-height: 0; } .assistant-history .card-header { padding:2px 4px; } .assistant-history .card-header h3 { font-size:11px; font-weight:700; color:var(--g500); text-transform:uppercase; letter-spacing:.04em; } -/* ── 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; } -.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. */ -.assistant-history .assistant-mode-switch { margin:0 0 8px; } - -/* The one menu toggle, identical in the app sidebar and the assistant rail. */ -.assistant-menu-toggle { display:flex; align-items:center; justify-content:center; width:32px; height:32px; margin:10px 12px 0; border:1px solid var(--g200); border-radius:8px; background:white; color:var(--g500); font-size:12px; cursor:pointer; } -.assistant-menu-toggle:hover { color:var(--blue); border-color:var(--blue-light); background:var(--blue-light); } -.assistant-history .assistant-menu-toggle { margin:0 0 8px; } - -/* 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; } -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; } } - /* Workspace launcher: same ground, card language and spacing as the empty state, so the two views are one interface. Desktop only. */ .assistant-workspace-view { flex:1 1 auto; min-height:0; padding:24px 18px; overflow-y:auto; diff --git a/public/css/styles.css b/public/css/styles.css index 1bb90e6f..5c86b63c 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -1209,3 +1209,34 @@ body.assistant-workspace .app-body { min-height:100vh; } body.assistant-workspace #assistant-tab.active { width:100%; } /* iOS: taps on controls must not trigger double-tap zoom */ button, a, .btn-sm, .btn-generate, .btn-send, .tab-btn, input, textarea, select, label { touch-action: manipulation; } + +/* ── Shared app chrome: the Assistant/Workspace switch and the menu toggle ──── + These render in BOTH the app sidebar and the assistant rail. They used to live + in assistant.css, which only the assistant component loads — so on the main + app page they rendered as unstyled native buttons. */ +/* ── 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; } +.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. */ +.assistant-history .assistant-mode-switch { margin:0 0 8px; } + +/* The one menu toggle, identical in the app sidebar and the assistant rail. */ +.assistant-menu-toggle { display:flex; align-items:center; justify-content:center; width:32px; height:32px; margin:10px 12px 0; border:1px solid var(--g200); border-radius:8px; background:white; color:var(--g500); font-size:12px; cursor:pointer; } +.assistant-menu-toggle:hover { color:var(--blue); border-color:var(--blue-light); background:var(--blue-light); } +.assistant-history .assistant-menu-toggle { margin:0 0 8px; } + +/* 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; } +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/test/assistant-workspace-layout.test.js b/test/assistant-workspace-layout.test.js index 2a9c5f06..dd5dfe73 100644 --- a/test/assistant-workspace-layout.test.js +++ b/test/assistant-workspace-layout.test.js @@ -112,7 +112,8 @@ test('one menu toggle serves both the app sidebar and the assistant rail', () => assert.match(app, /classList\.toggle\('menu-hidden'\)/, 'one class drives both'); assert.match(app, /'Show menu' : 'Hide menu'/, 'the label flips so the collapsed state is escapable'); - const css = read('public/css/assistant.css'); + // 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'); @@ -249,12 +250,31 @@ test('the assistant uses the app accent, so neither screen looks like a differen assert.match(styles, /\.tab-btn\.active\{background:var\(--blue-light\);color:var\(--blue\)/, 'the app accent is blue'); - const css = read('public/css/assistant.css'); - assert.match(css, /\.assistant-mode-switch button\.active \{ background:white; color:var\(--blue\)/, + assert.match(read('public/css/styles.css'), /\.assistant-mode-switch button\.active \{ background:white; color:var\(--blue\)/, 'the switch highlights in the app accent'); + const css = read('public/css/assistant.css'); assert.match(css, /\.assistant-msg\.user \.assistant-bubble \{ white-space:pre-wrap; background:var\(--blue\)/); // Citations keep their own purple chip so evidence stays distinct from // interactive blue; nothing else may reintroduce a second accent. const stray = css.split('\n').filter(l => l.includes('var(--purple') && !l.includes('assistant-cite')); assert.deepEqual(stray, [], 'no second accent outside citation chips'); }); + +test('chrome rendered on the main app page is styled by the stylesheet that page loads', () => { + const index = read('public/index.html'); + const styles = read('public/css/styles.css'); + const assistantCss = read('public/css/assistant.css'); + + // assistant.css is pulled in by the assistant COMPONENT, so it is absent on + // "/". Anything the app page renders must be styled from styles.css or it + // shows up as an unstyled native control. + assert.doesNotMatch(index, /css\/assistant\.css/, 'the app page does not load assistant.css'); + + for (const cls of ['assistant-mode-switch', 'assistant-menu-toggle']) { + assert.match(index, new RegExp('class="[^"]*' + cls), cls + ' is rendered on the app page'); + assert.ok(styles.includes('.' + cls + ' {'), cls + ' must be defined in styles.css'); + // Defined in both files, the two copies drift and one silently wins. + assert.ok(!assistantCss.includes('.' + cls + ' {'), cls + ' must not be duplicated in assistant.css'); + } + assert.match(styles, /body\.menu-hidden \.sidebar \{ width:0/, 'the collapse state is styled where the app can see it'); +});