fix: collapsed strip, workspace-mode search, image dialog chat list

- Collapsed menu: its own plain white 52px column against the tiled ground,
  no border line, in both views. Flat, it had no edge.
- Collapsed + workspace launcher: assistant.css set the rail track to 0 and
  beat the 52px rule, so the collapsed menu (name card included) vanished.
- Search follows the view, not the URL: the workspace launcher keeps
  /assistant, so Workspace searched chats.
- Create image, "Base it on": a native <select> list is as wide as its
  longest option, so full titles pushed it past the dialog. Labels now keep
  the whole words that fit the select's width; the full title is on hover.

Verified in Chromium: account menu items on top in all 8 states; collapsed
strip 52px white with the name card at the same place in all 3 views;
search placeholder and results per mode; dropdown widest label 500px in a
506px box (279 in 280 on a phone).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4rptBNvn6RYieQw54GXNS
This commit is contained in:
Daniel 2026-09-10 13:17:50 +02:00
parent 8756c9a140
commit f93994ad69
5 changed files with 89 additions and 3 deletions

View file

@ -472,7 +472,10 @@ body.assistant-mode-workspace .assistant-history .card,
body.assistant-mode-workspace .assistant-rail-actions,
body.assistant-mode-workspace .assistant-side { display:none; }
body.assistant-mode-workspace .assistant-layout { grid-template-columns:210px minmax(0,1fr); }
body.assistant-mode-workspace.menu-hidden .assistant-layout { grid-template-columns:0 minmax(0,1fr); }
/* Collapsed is the 52px strip in workspace mode too. This said 0, and loading
after styles.css it beat the 52px rule there, so the collapsed menu (name
card included) vanished whenever the workspace launcher was showing. */
body.assistant-mode-workspace.menu-hidden .assistant-layout { grid-template-columns:52px minmax(0,1fr); }
/* Workspace mode works on a phone too: the launcher is the same grid of cards,
one per column. Suppressing it here is what made the Workspace pill do
nothing at all on mobile. */

View file

@ -1311,6 +1311,12 @@ button, a, .btn-sm, .btn-generate, .btn-send, .tab-btn, input, textarea, select,
The phone sheet has nothing to collapse the same control closes it
so none of this may leak past the breakpoint. */
body.menu-hidden .sidebar { width:52px; }
/* Collapsed, the strip is a column of its own: plain white against the tiled
ground, no drawn border the white is the edge. Flat on the tiles, the 52px
column had no edge and read as icons pressed against the page card. The
full-width menu stays flat. Same strip in both views. */
body.menu-hidden .sidebar,
body.assistant-workspace.menu-hidden .assistant-history { background:white; }
body.menu-hidden .sidebar-tabs,
body.menu-hidden .assistant-mode-switch,
body.menu-hidden .sidebar-section-label,

View file

@ -314,8 +314,12 @@ document.addEventListener('DOMContentLoaded', function() {
}
function searchSources() {
// The assistant publishes its chats; anything else searches the menu.
if (window.location.pathname === '/assistant' && typeof window.assistantSearchableChats === 'function') {
// Search follows the view, not the address: the assistant's chat view
// searches chats, and Workspace — including the workspace launcher shown
// inside the assistant, which keeps the /assistant URL — searches the menu.
var inChats = document.body.classList.contains('assistant-workspace')
&& !document.body.classList.contains('assistant-mode-workspace');
if (inChats && typeof window.assistantSearchableChats === 'function') {
return { kind: 'chats', items: window.assistantSearchableChats() };
}
var items = [];

View file

@ -1350,6 +1350,7 @@ import {
'<div id="create-image-history" class="create-image-history"><p class="assistant-muted">Loading your images…</p></div>' +
'</div></div>';
document.body.appendChild(modal);
fitSelectOptions(modal.querySelector('#create-image-chat'));
modal.addEventListener('click', function (event) {
if (event.target === modal || event.target.closest('[data-create-image-close]')) modal.remove();
});
@ -2391,6 +2392,36 @@ import {
return truncateOnWord(String(first && first.content || 'Clinical assistant chat').replace(/\s+/g, ' ').trim(), 160);
}
// A native <select> opens a list as wide as its longest option, so full chat
// titles pushed "Base it on" far past the dialog. Each label keeps as many
// whole words as fit the select's own width; the full title stays on hover.
var OPTION_CHROME = 44; // list padding + scrollbar, which the text cannot use
function fitSelectOptions(select) {
if (!select) return;
var style = window.getComputedStyle(select);
var room = select.clientWidth - OPTION_CHROME;
if (room <= 0) return; // not laid out (or no layout engine): leave labels alone
var ctx = fitSelectOptions.ctx || (fitSelectOptions.ctx = document.createElement('canvas').getContext('2d'));
if (!ctx) return;
ctx.font = style.fontWeight + ' ' + style.fontSize + ' ' + style.fontFamily;
Array.prototype.forEach.call(select.options, function (opt) {
var full = opt.getAttribute('data-full-title') || opt.textContent;
opt.setAttribute('data-full-title', full);
opt.title = full;
if (ctx.measureText(full).width <= room) { opt.textContent = full; return; }
var words = full.split(/\s+/), text = '';
for (var i = 0; i < words.length; i++) {
var next = text ? text + ' ' + words[i] : words[i];
if (ctx.measureText(next + '…').width > room) break;
text = next;
}
// One word wider than the box: fall back to cutting characters.
if (!text) { text = full; while (text.length > 1 && ctx.measureText(text + '…').width > room) text = text.slice(0, -1); }
opt.textContent = text.replace(/[\s.,;:!?-]+$/, '') + '…';
});
}
window.addEventListener('resize', function () { fitSelectOptions(document.getElementById('create-image-chat')); });
// Cuts at a word boundary, and only adds an ellipsis when something was
// actually dropped.
function truncateOnWord(text, limit) {

View file

@ -568,3 +568,45 @@ test('desktop: the assistant rail is the app sidebar, flush and flat', () => {
assert.match(rail, /\.assistant-history \.account-card \{ padding:8px; \}/);
assert.match(styles, /body\.assistant-workspace\.menu-hidden \.assistant-history \{[^}]*padding:0; \}/, 'collapsed adds no gap of its own');
});
test('the collapsed menu is a bar of its own, in both views', () => {
const css = read('public/css/styles.css');
// Flat on the tiled ground, the 52px strip had no edge: its icons looked
// pressed against the page card. Expanded stays flat; collapsed gets a bar.
const desk = css.slice(css.indexOf('@media (min-width:769px) {\n /* Collapsing is a desktop idea'));
const block = desk.slice(0, desk.indexOf('\n}\n'));
// Its own white column, no border line: the white against the tiles is the edge.
assert.match(block, /body\.menu-hidden \.sidebar,\s*body\.assistant-workspace\.menu-hidden \.assistant-history \{ background:white; \}/);
assert.match(css, /@media\(min-width:769px\)\{ \.sidebar\{ background:transparent; border-right:none; \} \}/, 'expanded stays flat');
});
test('search follows the view: Workspace searches the workspace even inside the assistant', () => {
const app = read('public/js/app.js');
const fn = app.slice(app.indexOf('function searchSources()'), app.indexOf("return { kind: 'tabs'"));
// Keyed on the URL, the workspace launcher (still at /assistant) searched chats.
assert.doesNotMatch(fn, /location\.pathname === '\/assistant'/);
assert.match(fn, /classList\.contains\('assistant-workspace'\)\s*&& !document\.body\.classList\.contains\('assistant-mode-workspace'\)/);
assert.match(fn, /if \(inChats && typeof window\.assistantSearchableChats === 'function'\)/);
});
test('collapsed workspace mode keeps the 52px strip instead of losing the menu', () => {
const css = read('public/css/assistant.css');
// This track said 0 and, loading after styles.css, won — the collapsed menu
// and its name card disappeared whenever the workspace launcher showed.
assert.doesNotMatch(css, /body\.assistant-mode-workspace\.menu-hidden \.assistant-layout \{ grid-template-columns:0 /);
assert.match(css, /body\.assistant-mode-workspace\.menu-hidden \.assistant-layout \{ grid-template-columns:52px minmax\(0,1fr\); \}/);
});
test('the create-image chat list never grows wider than its dialog', () => {
const js = read('public/js/clinicalAssistant.js');
// A native <select> list is as wide as its longest option; full chat titles
// pushed it far past the dialog.
assert.match(js, /document\.body\.appendChild\(modal\);\s*\n\s*fitSelectOptions\(modal\.querySelector\('#create-image-chat'\)\);/,
'labels are fitted once the dialog is laid out');
const fn = js.slice(js.indexOf('function fitSelectOptions(select)'), js.indexOf("window.addEventListener('resize', function () { fitSelectOptions"));
assert.match(fn, /var room = select\.clientWidth - OPTION_CHROME;/, 'fitted to the select, not a fixed character count');
assert.match(fn, /if \(room <= 0\) return;/, 'no layout, no change');
assert.match(fn, /opt\.title = full;/, 'the full title stays on hover');
assert.match(fn, /words\[i\]/, 'cuts on whole words');
assert.match(js, /window\.addEventListener\('resize', function \(\) \{ fitSelectOptions\(document\.getElementById\('create-image-chat'\)\); \}\);/);
});