From 99833eba564ea609c3f6c023e5c401bdc56d80be Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 19 Aug 2026 18:36:36 +0800 Subject: [PATCH] fix(sidebar): restyle session history as a quiet overlay scrollbar The native always-on track and white chrome were competing with the parchment sidebar. Co-authored-by: Cursor --- frontend/DESIGN.md | 1 + frontend/src/app/globals.css | 16 +++++++++++++++- frontend/tests/sidebar-contract.test.ts | 20 ++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 281cd950..f2b5ffb8 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -154,6 +154,7 @@ The base unit is 4px. Tokens are `--space-1: 4px`, `--space-2: 8px`, `--space-3: - **Mobile:** no icon rail; an off-canvas drawer uses `min(86vw, 320px)` and closes through its scrim or Escape. - **Collapsed content:** logo, new-chat action, one history expansion action, and account avatar. Individual sessions do not become indistinguishable repeated icons. - **Scroll ownership:** header and footer remain fixed; `SidebarContent` is the sole sidebar scroll owner. +- **Scrollbar:** `SidebarContent` uses a quiet overlay scrollbar: transparent track, no `scrollbar-gutter`, and a 4px warm thumb mixed from `--color-ink`. The thumb stays transparent until hover or keyboard focus inside the history, then uses `color-mix(in srgb, var(--color-ink) 26%, transparent)`; thumb hover uses 40%. Increased contrast keeps the thumb visible; forced colors restore the system scrollbar. - **Motion:** Sidebar state changes are immediate on desktop, tablet, and mobile. The 44px trigger keeps one stable 18px sidebar glyph and never enters an intermediate scale or opacity state. - **Accessibility:** Command/Control+B shortcut outside editable controls, contextual trigger labels, 44px targets, focus return, collapsed-only tooltips, reduced-motion, reduced-transparency, and increased-contrast support. - **State:** session-local; reload uses breakpoint defaults rather than cookie or local-storage persistence. diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 86cc605a..c285fcc1 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -388,7 +388,21 @@ button:disabled { cursor: default; opacity: .45; } .chat-app { width: 100%; height: 100%; min-height: 0; overflow: hidden; display: grid; background: var(--color-canvas); grid-template-columns: minmax(0, 1fr); } .sidebar { position: relative; width: 100%; height: 100%; min-width: 0; min-height: 0; overflow: hidden; display: flex; flex-direction: column; border-right: 1px solid var(--sidebar-border); color: var(--sidebar-foreground); padding: var(--space-5) var(--space-3) 0; background: var(--sidebar-background); backdrop-filter: saturate(130%) blur(20px); } [data-sidebar="header"] { flex: 0 0 auto; } -[data-sidebar="content"] { min-height: 0; flex: 1 1 auto; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; } +[data-sidebar="content"] { min-height: 0; flex: 1 1 auto; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: transparent transparent; } +[data-sidebar="content"]:hover, [data-sidebar="content"]:focus-within { scrollbar-color: color-mix(in srgb, var(--color-ink) 26%, transparent) transparent; } +[data-sidebar="content"]::-webkit-scrollbar { width: 10px; background: transparent; } +[data-sidebar="content"]::-webkit-scrollbar-button { display: none; width: 0; height: 0; } +[data-sidebar="content"]::-webkit-scrollbar-track, [data-sidebar="content"]::-webkit-scrollbar-corner { background: transparent; } +[data-sidebar="content"]::-webkit-scrollbar-thumb { background-color: transparent; border: 3px solid transparent; border-radius: 999px; background-clip: content-box; } +[data-sidebar="content"]:hover::-webkit-scrollbar-thumb, [data-sidebar="content"]:focus-within::-webkit-scrollbar-thumb { background-color: color-mix(in srgb, var(--color-ink) 26%, transparent); } +[data-sidebar="content"]::-webkit-scrollbar-thumb:hover { background-color: color-mix(in srgb, var(--color-ink) 40%, transparent); } +@media (prefers-contrast: more) { + [data-sidebar="content"] { scrollbar-color: var(--color-ink-secondary) transparent; } + [data-sidebar="content"]::-webkit-scrollbar-thumb, [data-sidebar="content"]:hover::-webkit-scrollbar-thumb, [data-sidebar="content"]:focus-within::-webkit-scrollbar-thumb { background-color: var(--color-ink-secondary); } +} +@media (forced-colors: active) { + [data-sidebar="content"] { scrollbar-width: auto; scrollbar-color: auto; } +} [data-sidebar="footer"] { flex: 0 0 auto; } [data-sidebar="sidebar"]:focus { outline: none; } .brand-row { display: flex; align-items: center; min-height: 48px; gap: var(--space-3); padding: 0 var(--space-2); font-family: var(--font-display); font-size: var(--type-title-lg); letter-spacing: -.3px; } diff --git a/frontend/tests/sidebar-contract.test.ts b/frontend/tests/sidebar-contract.test.ts index f00a2ea7..50e0fb91 100644 --- a/frontend/tests/sidebar-contract.test.ts +++ b/frontend/tests/sidebar-contract.test.ts @@ -275,6 +275,26 @@ test("makes SidebarContent the only sidebar scroll owner", () => { assert.match(globalStyles, /\[data-active="true"\][^{]*\{[^}]*background:\s*var\(--sidebar-accent\)/); }); +test("styles the session history scrollbar as a quiet overlay", () => { + const content = cssBlock('[data-sidebar="content"]'); + assert.match(content, /scrollbar-width:\s*thin/); + assert.match(content, /scrollbar-color:\s*transparent\s+transparent/); + assert.doesNotMatch(content, /scrollbar-gutter/); + assert.match(cssBlock('[data-sidebar="content"]:hover, [data-sidebar="content"]:focus-within'), /scrollbar-color:\s*color-mix\(in srgb,\s*var\(--color-ink\)\s+26%,\s*transparent\)\s+transparent/); + assert.match(cssBlock('[data-sidebar="content"]::-webkit-scrollbar'), /width:\s*10px/); + assert.match(cssBlock('[data-sidebar="content"]::-webkit-scrollbar'), /background:\s*transparent/); + assert.match(cssBlock('[data-sidebar="content"]::-webkit-scrollbar-button'), /display:\s*none/); + assert.match(cssBlock('[data-sidebar="content"]::-webkit-scrollbar-track, [data-sidebar="content"]::-webkit-scrollbar-corner'), /background:\s*transparent/); + assert.match(cssBlock('[data-sidebar="content"]::-webkit-scrollbar-thumb'), /background-color:\s*transparent/); + assert.match(cssBlock('[data-sidebar="content"]::-webkit-scrollbar-thumb'), /border:\s*3px\s+solid\s+transparent/); + assert.match(cssBlock('[data-sidebar="content"]::-webkit-scrollbar-thumb'), /background-clip:\s*content-box/); + assert.match(cssBlock('[data-sidebar="content"]:hover::-webkit-scrollbar-thumb, [data-sidebar="content"]:focus-within::-webkit-scrollbar-thumb'), /background-color:\s*color-mix\(in srgb,\s*var\(--color-ink\)\s+26%,\s*transparent\)/); + assert.match(cssBlock('[data-sidebar="content"]::-webkit-scrollbar-thumb:hover'), /background-color:\s*color-mix\(in srgb,\s*var\(--color-ink\)\s+40%,\s*transparent\)/); + assert.match(globalStyles, /@media\s*\(prefers-contrast:\s*more\)[\s\S]*\[data-sidebar="content"\][^{]*\{[^}]*scrollbar-color:\s*var\(--color-ink-secondary\)\s+transparent/); + assert.match(globalStyles, /@media\s*\(forced-colors:\s*active\)[\s\S]*\[data-sidebar="content"\][^{]*\{[^}]*scrollbar-color:\s*auto/); + assert.match(readProjectFile("DESIGN.md"), /quiet overlay scrollbar/); +}); + test("renders each session title and menu as one unified row surface", () => { assert.match(cssBlock(".session-row"), /grid-template-columns:\s*minmax\(0,\s*1fr\)\s+44px/); assert.match(globalStyles, /\.session-row:has\(\.session-main\[data-active="true"\]\)[^{]*\{[^}]*background:\s*var\(--sidebar-accent\)/);