mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
feat(desktop): align sidebar app rail
This commit is contained in:
parent
3bf70ebb57
commit
18d5552cc3
6 changed files with 450 additions and 102 deletions
|
|
@ -101,6 +101,7 @@ async function main() {
|
|||
await setFieldByAriaLabel('Message', '');
|
||||
await assertConversationChangesSummary('conversation-changes-summary.json');
|
||||
await waitForSelector('[data-testid="thread-list"]');
|
||||
await assertSidebarAppRail('sidebar-app-rail.json');
|
||||
|
||||
await clickButton('Review Changes');
|
||||
await waitForText('README.md');
|
||||
|
|
@ -419,6 +420,8 @@ async function assertWorkbenchLandmarks() {
|
|||
return [
|
||||
'desktop-workspace',
|
||||
'project-sidebar',
|
||||
'sidebar-app-actions',
|
||||
'sidebar-footer-settings',
|
||||
'workspace-topbar',
|
||||
'workspace-grid',
|
||||
'chat-thread',
|
||||
|
|
@ -621,6 +624,172 @@ async function assertRalphWorkspaceLayout(fileName) {
|
|||
}
|
||||
}
|
||||
|
||||
async function assertSidebarAppRail(fileName) {
|
||||
const metrics = await evaluate(`(() => {
|
||||
const rectFor = (element) => {
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {
|
||||
top: rect.top,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
};
|
||||
};
|
||||
const overflows = (element) =>
|
||||
Boolean(element && element.scrollWidth > element.clientWidth + 4);
|
||||
const sidebar = document.querySelector('[data-testid="project-sidebar"]');
|
||||
const appActions = document.querySelector('[data-testid="sidebar-app-actions"]');
|
||||
const footerSettings = document.querySelector(
|
||||
'[data-testid="sidebar-footer-settings"]'
|
||||
);
|
||||
const projectList = document.querySelector('[data-testid="project-list"]');
|
||||
const threadList = document.querySelector('[data-testid="thread-list"]');
|
||||
const rowSelector =
|
||||
'.sidebar-action-row, .project-row, .session-row';
|
||||
const rows = [...document.querySelectorAll(rowSelector)].map((row) => {
|
||||
const label =
|
||||
row.getAttribute('aria-label') ||
|
||||
row.getAttribute('title') ||
|
||||
row.textContent.trim();
|
||||
return {
|
||||
label,
|
||||
text: row.textContent.trim(),
|
||||
rect: rectFor(row),
|
||||
scrollWidth: row.scrollWidth,
|
||||
clientWidth: row.clientWidth,
|
||||
overflows: overflows(row)
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
viewport: {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
},
|
||||
sidebar: rectFor(sidebar),
|
||||
appActions: rectFor(appActions),
|
||||
footerSettings: rectFor(footerSettings),
|
||||
projectList: rectFor(projectList),
|
||||
threadList: rectFor(threadList),
|
||||
hasLegacyToolbar: document.querySelector('.sidebar-toolbar') !== null,
|
||||
appActionLabels: appActions
|
||||
? [...appActions.querySelectorAll('button')].map(
|
||||
(button) => button.getAttribute('aria-label') || ''
|
||||
)
|
||||
: [],
|
||||
footerLabel:
|
||||
footerSettings?.getAttribute('aria-label') ||
|
||||
footerSettings?.textContent.trim() ||
|
||||
'',
|
||||
rows,
|
||||
sidebarText: sidebar?.innerText ?? '',
|
||||
overflows: {
|
||||
sidebar: overflows(sidebar),
|
||||
appActions: overflows(appActions),
|
||||
projectList: overflows(projectList),
|
||||
threadList: overflows(threadList),
|
||||
footerSettings: overflows(footerSettings)
|
||||
}
|
||||
};
|
||||
})()`);
|
||||
|
||||
await writeFile(
|
||||
join(artifactDir, fileName),
|
||||
`${JSON.stringify(metrics, null, 2)}\n`,
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const missing = [
|
||||
'sidebar',
|
||||
'appActions',
|
||||
'footerSettings',
|
||||
'projectList',
|
||||
'threadList',
|
||||
].filter((key) => metrics[key] === null);
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`Missing sidebar app rail rects: ${missing.join(', ')}`);
|
||||
}
|
||||
|
||||
if (metrics.hasLegacyToolbar) {
|
||||
throw new Error('Sidebar should not render the old project toolbar.');
|
||||
}
|
||||
|
||||
for (const expectedLabel of ['New Thread', 'Open Project', 'Models']) {
|
||||
if (!metrics.appActionLabels.includes(expectedLabel)) {
|
||||
throw new Error(
|
||||
`Sidebar app actions missing ${expectedLabel}: ${metrics.appActionLabels.join(
|
||||
', ',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (metrics.footerLabel !== 'Settings') {
|
||||
throw new Error(
|
||||
`Sidebar footer label should be Settings: ${metrics.footerLabel}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (metrics.sidebar.width < 236 || metrics.sidebar.width > 320) {
|
||||
throw new Error(
|
||||
`Sidebar width is no longer compact: ${metrics.sidebar.width}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (metrics.appActions.top > metrics.sidebar.top + 24) {
|
||||
throw new Error('Sidebar app actions are not pinned near the top.');
|
||||
}
|
||||
|
||||
if (metrics.footerSettings.bottom > metrics.sidebar.bottom + 1) {
|
||||
throw new Error('Sidebar Settings footer overflows the sidebar.');
|
||||
}
|
||||
|
||||
if (metrics.footerSettings.top < metrics.threadList.bottom - 1) {
|
||||
throw new Error(
|
||||
'Sidebar Settings should stay below the project/thread browser.',
|
||||
);
|
||||
}
|
||||
|
||||
const tallRows = metrics.rows.filter((row) => row.rect.height > 44);
|
||||
if (tallRows.length > 0) {
|
||||
throw new Error(
|
||||
`Sidebar rows are too tall for the compact rail: ${JSON.stringify(
|
||||
tallRows,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const overflowingRows = metrics.rows.filter((row) => row.overflows);
|
||||
if (overflowingRows.length > 0) {
|
||||
throw new Error(
|
||||
`Sidebar rows overflow horizontally: ${JSON.stringify(overflowingRows)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (Object.values(metrics.overflows).some(Boolean)) {
|
||||
throw new Error(
|
||||
`Sidebar rail regions overflow horizontally: ${JSON.stringify(
|
||||
metrics.overflows,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
metrics.sidebarText.includes('session-e2e') ||
|
||||
metrics.sidebarText.includes('/tmp/') ||
|
||||
metrics.sidebarText.includes('Connected to')
|
||||
) {
|
||||
throw new Error(
|
||||
`Sidebar leaked protocol or path noise: ${metrics.sidebarText}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function assertConversationChangesSummary(fileName) {
|
||||
await waitForSelector('[data-testid="conversation-changes-summary"]');
|
||||
const snapshot = await evaluate(`(() => {
|
||||
|
|
|
|||
|
|
@ -50,44 +50,49 @@ export function ProjectSidebar({
|
|||
aria-label="Projects and threads"
|
||||
data-testid="project-sidebar"
|
||||
>
|
||||
<div className="sidebar-toolbar">
|
||||
<h1>Projects</h1>
|
||||
<div className="sidebar-toolbar-actions">
|
||||
<button
|
||||
aria-label="Settings"
|
||||
className="sidebar-icon-button"
|
||||
title="Settings"
|
||||
type="button"
|
||||
onClick={onOpenSettings}
|
||||
>
|
||||
<SlidersIcon />
|
||||
<span className="sr-only">Settings</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="New Thread"
|
||||
className="sidebar-icon-button"
|
||||
disabled={loadState.state !== 'ready' || !activeProject}
|
||||
title="New Thread"
|
||||
type="button"
|
||||
onClick={onCreateSession}
|
||||
>
|
||||
<NewThreadIcon />
|
||||
<span className="sr-only">New Thread</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Open Project"
|
||||
className="sidebar-icon-button"
|
||||
title="Open Project"
|
||||
type="button"
|
||||
onClick={onChooseWorkspace}
|
||||
>
|
||||
<FolderPlusIcon />
|
||||
<span className="sr-only">Open Project</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<nav
|
||||
className="sidebar-app-actions"
|
||||
aria-label="Workspace actions"
|
||||
data-testid="sidebar-app-actions"
|
||||
>
|
||||
<button
|
||||
aria-label="New Thread"
|
||||
className="sidebar-action-row"
|
||||
disabled={loadState.state !== 'ready' || !activeProject}
|
||||
title="New Thread"
|
||||
type="button"
|
||||
onClick={onCreateSession}
|
||||
>
|
||||
<NewThreadIcon />
|
||||
<span>New Thread</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Open Project"
|
||||
className="sidebar-action-row"
|
||||
title="Open Project"
|
||||
type="button"
|
||||
onClick={onChooseWorkspace}
|
||||
>
|
||||
<FolderPlusIcon />
|
||||
<span>Open Project</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Models"
|
||||
className="sidebar-action-row"
|
||||
title="Models"
|
||||
type="button"
|
||||
onClick={onOpenSettings}
|
||||
>
|
||||
<SlidersIcon />
|
||||
<span>Models</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<section className="sidebar-section project-navigator">
|
||||
<div className="sidebar-section-heading">
|
||||
<h2>Projects</h2>
|
||||
<span>{projects.length}</span>
|
||||
</div>
|
||||
<ProjectList
|
||||
activeProjectId={activeProjectId}
|
||||
projects={projects}
|
||||
|
|
@ -107,6 +112,19 @@ export function ProjectSidebar({
|
|||
onSelect={onSelectSession}
|
||||
/>
|
||||
</section>
|
||||
<div className="sidebar-footer">
|
||||
<button
|
||||
aria-label="Settings"
|
||||
className="sidebar-action-row sidebar-footer-action"
|
||||
data-testid="sidebar-footer-settings"
|
||||
title="Settings"
|
||||
type="button"
|
||||
onClick={onOpenSettings}
|
||||
>
|
||||
<SlidersIcon />
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ describe('WorkspacePage', () => {
|
|||
for (const testId of [
|
||||
'desktop-workspace',
|
||||
'project-sidebar',
|
||||
'sidebar-app-actions',
|
||||
'workspace-topbar',
|
||||
'workspace-grid',
|
||||
'chat-thread',
|
||||
|
|
@ -64,9 +65,18 @@ describe('WorkspacePage', () => {
|
|||
expect(
|
||||
renderedContainer.querySelector('[data-testid="settings-page"]'),
|
||||
).toBeNull();
|
||||
expect(renderedContainer.querySelector('.sidebar-toolbar')).toBeNull();
|
||||
expect(
|
||||
renderedContainer.querySelector(
|
||||
'[data-testid="sidebar-footer-settings"]',
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
expect(renderedContainer.textContent).toContain('example-workspace');
|
||||
expect(renderedContainer.textContent).toContain('main');
|
||||
expect(renderedContainer.textContent).toContain('New Thread');
|
||||
expect(renderedContainer.textContent).toContain('Open Project');
|
||||
expect(renderedContainer.textContent).toContain('Models');
|
||||
expect(
|
||||
renderedContainer.querySelector('[data-testid="project-sidebar"]')
|
||||
?.textContent,
|
||||
|
|
|
|||
|
|
@ -96,44 +96,21 @@ summary:focus-visible {
|
|||
height: 100vh;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 9px;
|
||||
overflow: hidden;
|
||||
padding: 16px 12px 14px;
|
||||
padding: 12px 10px;
|
||||
border-right: 1px solid var(--line);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(37, 58, 61, 0.95), rgba(22, 28, 29, 0.98)),
|
||||
radial-gradient(
|
||||
circle at 32px 0,
|
||||
rgba(242, 199, 112, 0.08),
|
||||
transparent 34%
|
||||
),
|
||||
#151b1d;
|
||||
linear-gradient(180deg, rgba(20, 26, 51, 0.98), rgba(13, 17, 32, 0.98)),
|
||||
#101322;
|
||||
}
|
||||
|
||||
.sidebar-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 30px;
|
||||
gap: 12px;
|
||||
padding: 0 4px 4px;
|
||||
}
|
||||
|
||||
.sidebar-toolbar h1,
|
||||
.topbar h2,
|
||||
.topbar p,
|
||||
.panel h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-toolbar h1 {
|
||||
color: rgba(225, 232, 228, 0.62);
|
||||
font-size: 16px;
|
||||
font-weight: 720;
|
||||
letter-spacing: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.eyebrow,
|
||||
.message-role {
|
||||
color: var(--muted);
|
||||
|
|
@ -143,35 +120,48 @@ summary:focus-visible {
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sidebar-toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
.sidebar-app-actions {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.sidebar-icon-button {
|
||||
position: relative;
|
||||
.sidebar-action-row {
|
||||
display: grid;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
place-items: center;
|
||||
grid-template-columns: 22px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
gap: 8px;
|
||||
padding: 0 8px;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: rgba(225, 232, 228, 0.58);
|
||||
color: rgba(225, 232, 242, 0.72);
|
||||
font-size: 13px;
|
||||
font-weight: 680;
|
||||
letter-spacing: 0;
|
||||
text-align: left;
|
||||
transition:
|
||||
background 140ms ease,
|
||||
color 140ms ease,
|
||||
transform 140ms ease;
|
||||
color 140ms ease;
|
||||
}
|
||||
|
||||
.sidebar-icon-button:not(:disabled):hover {
|
||||
background: rgba(238, 244, 239, 0.08);
|
||||
color: rgba(245, 248, 244, 0.9);
|
||||
transform: translateY(-1px);
|
||||
.sidebar-action-row:not(:disabled):hover {
|
||||
background: rgba(213, 224, 255, 0.07);
|
||||
color: rgba(247, 249, 255, 0.96);
|
||||
}
|
||||
|
||||
.sidebar-icon-button svg {
|
||||
.sidebar-action-row svg {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.sidebar-action-row span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
|
|
@ -188,7 +178,7 @@ summary:focus-visible {
|
|||
display: flex;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.sidebar-section-fill {
|
||||
|
|
@ -213,8 +203,8 @@ summary:focus-visible {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 24px;
|
||||
padding: 4px 8px 0 36px;
|
||||
min-height: 22px;
|
||||
padding: 4px 8px 0;
|
||||
}
|
||||
|
||||
.sidebar-section-heading span {
|
||||
|
|
@ -223,10 +213,10 @@ summary:focus-visible {
|
|||
|
||||
.empty-row,
|
||||
.workspace-path {
|
||||
min-height: 40px;
|
||||
padding: 10px 2px;
|
||||
min-height: 34px;
|
||||
padding: 8px 2px;
|
||||
color: rgba(225, 232, 228, 0.52);
|
||||
font-size: 15px;
|
||||
font-size: 13px;
|
||||
font-weight: 680;
|
||||
}
|
||||
|
||||
|
|
@ -286,7 +276,7 @@ summary:focus-visible {
|
|||
}
|
||||
|
||||
.project-list {
|
||||
max-height: 132px;
|
||||
max-height: 120px;
|
||||
}
|
||||
|
||||
.session-list {
|
||||
|
|
@ -299,7 +289,7 @@ summary:focus-visible {
|
|||
display: grid;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
min-height: 36px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
|
|
@ -311,44 +301,44 @@ summary:focus-visible {
|
|||
}
|
||||
|
||||
.project-row {
|
||||
grid-template-columns: 28px minmax(0, 1fr);
|
||||
padding: 6px 8px 6px 6px;
|
||||
grid-template-columns: 24px minmax(0, 1fr);
|
||||
padding: 5px 8px 5px 6px;
|
||||
}
|
||||
|
||||
.session-row {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
column-gap: 8px;
|
||||
padding: 6px 8px 6px 36px;
|
||||
column-gap: 7px;
|
||||
padding: 5px 8px 5px 28px;
|
||||
}
|
||||
|
||||
.project-row-active,
|
||||
.session-row-active {
|
||||
background: rgba(238, 244, 239, 0.052);
|
||||
background: rgba(213, 224, 255, 0.062);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.project-row-active::before,
|
||||
.session-row-active::before {
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
bottom: 9px;
|
||||
top: 7px;
|
||||
bottom: 7px;
|
||||
left: 0;
|
||||
width: 2px;
|
||||
border-radius: 999px;
|
||||
background: rgba(242, 199, 112, 0.74);
|
||||
background: rgba(117, 131, 255, 0.78);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.project-row:not(.project-row-active):hover,
|
||||
.session-row:not(.session-row-active):hover {
|
||||
background: rgba(238, 244, 239, 0.035);
|
||||
background: rgba(213, 224, 255, 0.045);
|
||||
color: rgba(248, 250, 247, 0.95);
|
||||
}
|
||||
|
||||
.project-row-icon {
|
||||
justify-self: start;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 0;
|
||||
color: rgba(225, 232, 228, 0.82);
|
||||
}
|
||||
|
|
@ -376,8 +366,8 @@ summary:focus-visible {
|
|||
|
||||
.project-row-copy span,
|
||||
.session-row-title {
|
||||
font-size: 14px;
|
||||
font-weight: 680;
|
||||
font-size: 13px;
|
||||
font-weight: 660;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
|
|
@ -419,11 +409,22 @@ summary:focus-visible {
|
|||
.session-row-meta {
|
||||
max-width: 48px;
|
||||
color: rgba(225, 232, 228, 0.54);
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
font-weight: 720;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
flex: 0 0 auto;
|
||||
margin-top: auto;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid rgba(213, 224, 255, 0.08);
|
||||
}
|
||||
|
||||
.sidebar-footer-action {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.session-row-draft {
|
||||
cursor: default;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue