feat: workspace mode keeps both the rail menu and the cards, merged into one look
Both surfaces render from the same pass over the app's real .tab-btn list, so neither can drift from the menu it mirrors. - The card view now shares the empty state's graph-paper ground, card language and spacing, and is centred with a max width, so switching modes reads as the same app instead of a page that looks nothing like it. - Cards carry the clinical work tabs only: Settings, Admin, Docs and FAQ stay in the rail list, where someone looking for them will go, rather than sitting alongside Encounter HPI and Calculators. - On phones the card grid is hidden entirely and the chat stays on screen — the drawer already is the menu there, so a second copy only pushed it away. - The Sources column and its 330px grid track both go in workspace mode. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
This commit is contained in:
parent
5940ebede7
commit
1971436c26
4 changed files with 54 additions and 10 deletions
|
|
@ -22,6 +22,11 @@
|
|||
<button id="btn-assistant-goback-rail" class="btn-sm btn-ghost assistant-goback-rail" type="button" title="Back to the main menu"><i class="fas fa-arrow-left"></i> Go back</button>
|
||||
<button id="btn-assistant-create-image" class="assistant-create-image" type="button"><i class="fas fa-wand-magic-sparkles"></i> Create image</button>
|
||||
<button id="btn-assistant-clear" class="assistant-new-chat" type="button"><i class="fas fa-plus"></i> <span>New chat</span><kbd class="assistant-kbd">Ctrl+Shift+O</kbd></button>
|
||||
<!-- Rail menu and the card grid are both rendered from the app's real
|
||||
.tab-btn list, so neither can drift from the menu it mirrors. -->
|
||||
<nav class="assistant-rail-workspace" id="assistant-rail-workspace" aria-label="Workspace" hidden>
|
||||
<div id="assistant-workspace-links" class="assistant-rail-links"></div>
|
||||
</nav>
|
||||
<div class="card">
|
||||
<button type="button" class="card-header assistant-chats-header" id="btn-assistant-chats-toggle" aria-expanded="true" aria-controls="assistant-saved-chats">
|
||||
<h3><i class="fas fa-bookmark"></i> Saved Chats</h3>
|
||||
|
|
@ -31,14 +36,12 @@
|
|||
<p class="assistant-muted">Chats save automatically as you go.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Populated from the app's real .tab-btn list so this never drifts
|
||||
out of sync with the menu it mirrors. Shown in Workspace mode. -->
|
||||
<nav class="assistant-rail-workspace" id="assistant-rail-workspace" aria-label="Workspace" hidden>
|
||||
<div id="assistant-workspace-links" class="assistant-rail-links"></div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<section class="assistant-main card">
|
||||
<div id="assistant-workspace-view" class="assistant-workspace-view" hidden>
|
||||
<div id="assistant-workspace-cards" class="assistant-workspace-cards"></div>
|
||||
</div>
|
||||
<div id="assistant-chat-view">
|
||||
<div class="assistant-toolbar">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1
|
|||
.assistant-main { display:flex; flex-direction:column; min-height:calc(100vh - 200px); min-width:0; }
|
||||
#assistant-chat-view { display:grid; grid-template-rows:auto minmax(420px,1fr) auto; min-height:calc(100vh - 200px); min-width:0; }
|
||||
#assistant-chat-view[hidden] { display:none; }
|
||||
/* Phones keep the chat visible: the drawer carries the workspace menu there. */
|
||||
@media (max-width:640px) { body.assistant-mode-workspace #assistant-chat-view[hidden] { display:flex; } }
|
||||
.assistant-toolbar { display:flex; justify-content:space-between; align-items:center; gap:10px; padding:10px 14px; border-bottom:1px solid var(--g200); background:var(--g50); }
|
||||
.assistant-toolbar-actions { display:flex; gap:6px; flex-wrap:wrap; }
|
||||
.assistant-messages { padding:16px; max-height:65vh; overflow-y:auto; overflow-x:hidden; background:linear-gradient(180deg,#fff,var(--g50)); min-width:0; }
|
||||
|
|
@ -161,6 +163,21 @@ body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1
|
|||
.assistant-mode-switch button i { font-size:11px; }
|
||||
.assistant-mode-switch button.active { background:white; color:var(--purple); box-shadow:var(--shadow); }
|
||||
|
||||
/* Workspace view: same ground, same card language and same spacing as the empty
|
||||
state, so switching modes reads as the same app rather than a separate page. */
|
||||
.assistant-workspace-view { padding:24px 18px; overflow-y:auto; min-height:0;
|
||||
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; }
|
||||
.assistant-workspace-view[hidden] { display:none; }
|
||||
.assistant-workspace-cards { display:grid; grid-template-columns:repeat(auto-fill,minmax(190px,1fr)); gap:10px; max-width:860px; margin:0 auto; }
|
||||
.assistant-workspace-card { display:flex; align-items:center; gap:10px; border:1px solid var(--g200); background:white; border-radius:10px; padding:12px 14px; font-size:12.5px; color:var(--g700); text-align:left; cursor:pointer; box-shadow:var(--shadow); }
|
||||
.assistant-workspace-card:hover { border-color:var(--purple-light); background:#faf5ff; color:var(--purple); }
|
||||
.assistant-workspace-card i { width:16px; text-align:center; color:var(--g400); font-size:13px; }
|
||||
.assistant-workspace-card:hover i { color:var(--purple); }
|
||||
/* On a phone the rail IS the drawer menu; a second copy as cards is redundant
|
||||
and pushes the real list off the screen. */
|
||||
@media (max-width:640px) { .assistant-workspace-view { display:none !important; } }
|
||||
|
||||
/* Workspace mode: the sources column goes with it, and the track goes too —
|
||||
hiding the panel alone left an empty 330px gutter. */
|
||||
body.assistant-mode-workspace .assistant-layout { grid-template-columns:260px minmax(0,1fr); }
|
||||
|
|
|
|||
|
|
@ -163,10 +163,17 @@ import {
|
|||
|
||||
// The rail mirrors the app's real tab list rather than restating it, so a tab
|
||||
// added, renamed or hidden in index.html shows up here with no extra work.
|
||||
// Cards are the clinical work surface: account and administration tabs stay
|
||||
// in the rail list, where someone looking for them will go, but they do not
|
||||
// sit alongside Encounter HPI and Calculators.
|
||||
var NON_WORK_TABS = ['settings', 'admin', 'docs', 'faq'];
|
||||
|
||||
var renderWorkspaceLinks = function() {
|
||||
var host = document.getElementById('assistant-workspace-links');
|
||||
var cards = document.getElementById('assistant-workspace-cards');
|
||||
if (!host) return;
|
||||
host.innerHTML = '';
|
||||
if (cards) cards.innerHTML = '';
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.tab-btn'), function(tab) {
|
||||
var name = tab.getAttribute('data-tab');
|
||||
if (!name || name === 'assistant' || tab.classList.contains('hidden')) return;
|
||||
|
|
@ -179,6 +186,15 @@ import {
|
|||
link.innerHTML = '<i class="' + escapeAttr(icon ? icon.className : 'fas fa-circle') + '"></i>' +
|
||||
'<span>' + escapeHtml(label ? label.textContent : name) + '</span>';
|
||||
host.appendChild(link);
|
||||
if (cards && NON_WORK_TABS.indexOf(name) === -1) {
|
||||
var card = document.createElement('button');
|
||||
card.type = 'button';
|
||||
card.className = 'assistant-workspace-card';
|
||||
card.setAttribute('data-assistant-workspace-tab', name);
|
||||
card.innerHTML = '<i class="' + escapeAttr(icon ? icon.className : 'fas fa-circle') + '"></i>' +
|
||||
'<span>' + escapeHtml(label ? label.textContent : name) + '</span>';
|
||||
cards.appendChild(card);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -192,12 +208,14 @@ import {
|
|||
var newChat = document.getElementById('btn-assistant-clear');
|
||||
var createImage = document.getElementById('btn-assistant-create-image');
|
||||
if (workspace) renderWorkspaceLinks();
|
||||
var chatView = document.getElementById('assistant-chat-view');
|
||||
var workspaceView = document.getElementById('assistant-workspace-view');
|
||||
if (chatView) chatView.hidden = workspace;
|
||||
if (workspaceView) workspaceView.hidden = !workspace;
|
||||
if (rail) rail.hidden = !workspace;
|
||||
if (chats) chats.hidden = workspace;
|
||||
if (newChat) newChat.hidden = workspace;
|
||||
if (createImage) createImage.hidden = workspace;
|
||||
// The chat column stays put. Only the rail swaps, so switching modes is a
|
||||
// change of menu rather than a jump to a page that looks nothing like it.
|
||||
var side = document.querySelector('.assistant-side');
|
||||
if (side) side.hidden = workspace || !citationsOn;
|
||||
document.body.classList.toggle('assistant-mode-workspace', workspace);
|
||||
|
|
|
|||
|
|
@ -188,18 +188,24 @@ test('the rail switches between Assistant and Workspace like Home and Code', ()
|
|||
assert.match(html, /assistant-mode-switch/, 'a two-pill switch sits at the top of the rail');
|
||||
assert.match(html, /data-assistant-mode="assistant"/);
|
||||
assert.match(html, /data-assistant-mode="workspace"/);
|
||||
// ONE menu, not two: switching modes swaps the rail, it does not jump to a
|
||||
// page that looks nothing like the assistant.
|
||||
assert.doesNotMatch(html, /assistant-workspace-cards/, 'no second copy of the menu in the main column');
|
||||
assert.match(html, /id="assistant-workspace-cards"/, 'cards carry the work tabs');
|
||||
assert.match(html, /id="assistant-workspace-links"/, 'and the rail carries the full menu');
|
||||
|
||||
const js = fs.readFileSync(path.join(root, 'public/js/clinicalAssistant.js'), 'utf8');
|
||||
assert.match(js, /if \(side\) side\.hidden = workspace \|\| !citationsOn;/, 'sources are hidden in workspace mode');
|
||||
assert.match(js, /if \(chats\) chats\.hidden = workspace;/, 'and the saved-chat list');
|
||||
// Account and administration are not clinical work surfaces.
|
||||
assert.match(js, /NON_WORK_TABS = \['settings', 'admin', 'docs', 'faq'\]/);
|
||||
assert.match(js, /cards && NON_WORK_TABS\.indexOf\(name\) === -1/, 'excluded from the cards only');
|
||||
|
||||
const css = fs.readFileSync(path.join(root, 'public/css/assistant.css'), 'utf8');
|
||||
// Hiding the panel without dropping its track leaves an empty 330px gutter.
|
||||
assert.match(css, /body\.assistant-mode-workspace \.assistant-layout \{ grid-template-columns:260px minmax\(0,1fr\); \}/);
|
||||
assert.match(css, /body\.assistant-mode-workspace \.assistant-layout\.history-collapsed \{ grid-template-columns:0 minmax\(0,1fr\); \}/);
|
||||
// On a phone the drawer already is the menu; a card grid would push it away.
|
||||
assert.match(css, /@media \(max-width:640px\) \{ \.assistant-workspace-view \{ display:none !important; \} \}/);
|
||||
assert.match(css, /body\.assistant-mode-workspace #assistant-chat-view\[hidden\] \{ display:flex; \}/,
|
||||
'so the chat stays on screen there');
|
||||
});
|
||||
|
||||
test('Learning Hub sits with the content tools, not above the clinical ones', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue