fix: style the shared chrome from the stylesheet the app page actually loads

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjVc5oaAaYFNbZGLeJp6TX
This commit is contained in:
Daniel 2026-09-10 02:49:53 +02:00
parent 18cee14549
commit 6c2bafbe02
3 changed files with 54 additions and 36 deletions

View file

@ -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;

View file

@ -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; } }

View file

@ -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');
});