From 00d8762c131fa548776b9386b7da1e224040b3a1 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Sat, 18 Jul 2026 21:47:55 +0800 Subject: [PATCH] fix: left align chat session title --- frontend/DESIGN.md | 7 +++++-- frontend/src/app/globals.css | 18 ++++++++++-------- frontend/tests/sidebar-contract.test.ts | 14 ++++++++++++++ 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 3448cdd5..cd5f047a 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -137,11 +137,13 @@ The base unit is 4px. Tokens are `--space-1: 4px`, `--space-2: 8px`, `--space-3: ### Sidebar shell - **Composition:** provider, fixed header, one scroll-owning content region, fixed footer, trigger, rail, and flexible chat inset. +- **Trigger placement:** the single visible collapse/expand trigger sits beside the active session title in the chat header. The sidebar brand row has no duplicate trigger. - **Desktop:** 288px expanded by default at 1024px and above; 64px collapsed icon rail. - **Tablet:** 64px collapsed by default from 768px through 1023px; 240px when expanded. -- **Mobile:** no icon rail; an off-canvas drawer uses `min(86vw, 320px)` and closes through its scrim, trigger, or Escape. +- **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. +- **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. @@ -202,7 +204,8 @@ The base unit is 4px. Tokens are `--space-1: 4px`, `--space-2: 8px`, `--space-3: | Type | Duration | Easing | Usage | |---|---:|---|---| | Micro | 120ms | ease-out | Button and row feedback | -| Standard | 180ms | `cubic-bezier(.22, 1, .36, 1)` | Sheet, sidebar, message entry | +| Standard | 180ms | `cubic-bezier(.22, 1, .36, 1)` | Sheet and message entry | +| Spatial | Instant | None | Sidebar state changes and mobile drawer | | Emphasis | 360ms | `cubic-bezier(.16, 1, .3, 1)` | Loading mark only | Only `transform`, `opacity`, and color/filter transitions animate. Reduced-motion disables non-essential animation. Motion communicates state; decorative looping is limited to an active loading state. diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index f55ffa44..1f5e02a9 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -262,6 +262,7 @@ button:disabled { cursor: default; opacity: .45; } [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="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; } .brand-row strong { font-weight: 400; } .brand-mark, .auth-brand span { width: 32px; height: 32px; border-radius: 50%; background: var(--color-canvas) url("/jyotish-logo.png") center / contain no-repeat; box-shadow: 0 0 0 1px var(--color-border); } @@ -292,7 +293,9 @@ button:disabled { cursor: default; opacity: .45; } .profile-initial { width: 32px; height: 32px; display: grid; place-items: center; border-radius: 50%; font-size: 12px; text-transform: uppercase; border: 0; background: var(--color-action); color: var(--color-on-dark); font-weight: 500; } .profile-trigger b { display: block; overflow: hidden; line-height: 1.4; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 500; } [data-sidebar="trigger"] { width: 44px; height: 44px; display: grid; flex: 0 0 auto; place-items: center; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--sidebar-foreground); cursor: pointer; transition: background-color 120ms ease-out, color 120ms ease-out, transform 120ms ease-out; } -.brand-row [data-sidebar="trigger"] { margin-left: auto; } +[data-sidebar="trigger"] svg { width: 18px; height: 18px; } +[data-sidebar="trigger"]:hover { background: var(--sidebar-accent); color: var(--sidebar-accent-foreground); } +[data-sidebar="trigger"]:active { transform: translateY(1px); } [data-sidebar="rail"] { position: absolute; z-index: 2; top: 0; right: calc(var(--space-1) / -1); width: var(--space-2); min-width: var(--space-2); height: 100%; min-height: 0; padding: 0; border: 0; background: transparent; cursor: col-resize; transition: background-color 120ms ease-out, color 120ms ease-out; } [data-sidebar="rail"]:focus-visible { outline: 3px solid color-mix(in srgb, var(--sidebar-ring) 56%, transparent); outline-offset: -2px; } @@ -302,7 +305,6 @@ button:disabled { cursor: default; opacity: .45; } [data-viewport="desktop"][data-state="collapsed"] .chat-app, [data-viewport="tablet"][data-state="collapsed"] .chat-app { grid-template-columns: var(--sidebar-width-icon) minmax(0, 1fr); } [data-state="collapsed"] .sidebar { padding-inline: var(--space-2); } [data-state="collapsed"] .brand-row { justify-content: center; gap: 0; padding: 0; } - [data-state="collapsed"] .brand-row [data-sidebar="trigger"] { display: none; } [data-state="collapsed"] [data-sidebar="menu"] { justify-items: center; } [data-state="collapsed"] [data-sidebar="menu-button"] { width: 44px; height: 44px; min-height: 44px; padding: 0; place-items: center; } [data-state="collapsed"] .new-chat { margin-block: var(--space-4) var(--space-6); } @@ -311,7 +313,8 @@ button:disabled { cursor: default; opacity: .45; } } .chat-panel { height: 100%; min-width: 0; min-height: 0; overflow: hidden; display: grid; background: var(--color-canvas); grid-template-rows: 68px minmax(0, 1fr) auto; } -.chat-header { z-index: 2; min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 20px; border-bottom: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent); padding: 0 var(--space-8); background: var(--color-frosted); backdrop-filter: saturate(130%) blur(20px); } +.chat-header { z-index: 2; min-width: 0; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 20px; border-bottom: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent); padding: 0 var(--space-8); background: var(--color-frosted); backdrop-filter: saturate(130%) blur(20px); text-align: left; } +.chat-header > div { min-width: 0; } .chat-header strong { max-width: min(560px, 62vw); overflow: hidden; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; font-family: var(--font-display); font-size: var(--type-title-md); font-weight: 400; letter-spacing: -.3px; } .chat-header span { color: var(--color-ink-secondary); margin-top: var(--space-1); font-size: 12px; } .status { display: inline-block; margin: 0 6px 1px 0; border-radius: 50%; background: var(--color-success); width: 7px; height: 7px; margin-right: var(--space-2); } @@ -625,14 +628,13 @@ input:disabled, select:disabled { color: var(--color-ink-tertiary); background: @media (max-width: 767px) { .chat-app { grid-template-columns: 1fr; } - [data-sidebar="sidebar"] { position: fixed; z-index: 12; inset: 0 auto 0 0; width: var(--sidebar-width-mobile); height: 100dvh; padding: max(var(--space-3), env(safe-area-inset-top)) var(--space-3) max(var(--space-3), env(safe-area-inset-bottom)); border-right: 1px solid var(--sidebar-border); transition: transform 180ms var(--ease-out), visibility 0s linear 180ms; } + [data-sidebar="sidebar"] { position: fixed; z-index: 12; inset: 0 auto 0 0; width: var(--sidebar-width-mobile); height: 100dvh; padding: max(var(--space-3), env(safe-area-inset-top)) var(--space-3) max(var(--space-3), env(safe-area-inset-bottom)); border-right: 1px solid var(--sidebar-border); } [data-sidebar="sidebar"][data-mobile-open="false"] { visibility: hidden; transform: translateX(-100%); } - [data-sidebar="sidebar"][data-mobile-open="true"] { visibility: visible; transform: translateX(0); transition-delay: 0s; } - .sidebar-scrim { position: fixed; z-index: 11; inset: 0; display: block; border: 0; background: var(--color-scrim); opacity: 1; visibility: visible; transition: opacity 180ms ease-out, visibility 0s linear; } - @starting-style { .sidebar-scrim { opacity: 0; visibility: hidden; } } + [data-sidebar="sidebar"][data-mobile-open="true"] { visibility: visible; transform: translateX(0); } + .sidebar-scrim { position: fixed; z-index: 11; inset: 0; display: block; border: 0; background: var(--color-scrim); opacity: 1; visibility: visible; } [data-sidebar="rail"] { display: none; } .chat-panel { grid-template-rows: calc(64px + env(safe-area-inset-top)) minmax(0, 1fr) auto; } - .chat-header { justify-content: initial; gap: 10px; padding: max(0px, env(safe-area-inset-top)) var(--space-3) 0; } + .chat-header { gap: 10px; padding: max(0px, env(safe-area-inset-top)) var(--space-3) 0; } .conversation.is-empty { padding: var(--space-5) var(--space-4); } .welcome { padding: var(--space-6) 0 var(--space-10); } .welcome > .onboarding-message:first-child .message-bubble p { font-size: var(--type-display-sm); } diff --git a/frontend/tests/sidebar-contract.test.ts b/frontend/tests/sidebar-contract.test.ts index d3a444de..14923b05 100644 --- a/frontend/tests/sidebar-contract.test.ts +++ b/frontend/tests/sidebar-contract.test.ts @@ -97,6 +97,8 @@ test("documents the sidebar shell design contract", () => { assert.match(design, /### Sidebar shell/); assert.match(design, /Scroll ownership/); assert.match(design, /session-local/); + assert.match(design, /single visible collapse\/expand trigger/); + assert.match(design, /Sidebar state changes are immediate/); }); test("composes the Jyotisha app sidebar from the generic shell", () => { @@ -218,6 +220,17 @@ test("selects desktop and tablet shell widths from provider data", () => { assert.doesNotMatch(globalStyles, /transition:[^;}]*(?:width|grid-template-columns)/); }); +test("changes sidebar state without transition frames", () => { + assert.match(globalStyles, /\[data-sidebar="trigger"\]\s+svg\s*\{[^}]*width:\s*18px[^}]*height:\s*18px/); + assert.doesNotMatch(cssBlock('[data-sidebar="sidebar"]'), /transition:/); +}); + +test("keeps the chat title in the flexible left-aligned header column", () => { + assert.match(cssBlock(".chat-header"), /grid-template-columns:\s*auto\s+minmax\(0,\s*1fr\)\s+auto/); + assert.match(cssBlock(".chat-header"), /text-align:\s*left/); + assert.doesNotMatch(cssBlock(".chat-header"), /justify-content:\s*space-between/); +}); + test("makes SidebarContent the only sidebar scroll owner", () => { assert.match(cssBlock('[data-sidebar="header"]'), /flex:\s*0\s+0\s+auto/); assert.match(cssBlock('[data-sidebar="content"]'), /min-height:\s*0/); @@ -242,6 +255,7 @@ test("uses provider attributes for the mobile drawer and scrim", () => { assert.match(globalStyles, /\[data-sidebar="sidebar"\]\[data-mobile-open="false"\][^{]*\{[^}]*transform:\s*translateX\(-100%\)/); assert.match(globalStyles, /\[data-sidebar="sidebar"\]\[data-mobile-open="true"\][^{]*\{[^}]*visibility:\s*visible[^}]*transform:\s*translateX\(0\)/); assert.match(globalStyles, /\.sidebar-scrim\s*\{[^}]*position:\s*fixed[^}]*background:\s*var\(--color-scrim\)/); + assert.match(globalStyles, /\[data-sidebar="sidebar"\]:focus\s*\{[^}]*outline:\s*none/); assert.match(globalStyles, /\[data-sidebar="rail"\]\s*\{[^}]*display:\s*none/); });