From d02fa8bbf3ee93395fd8650f6e6f9206d6c12887 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Sat, 29 Aug 2026 11:22:39 +0000 Subject: [PATCH] fix(frontend): left-align the sidebar nav items on the mobile drawer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nav buttons defaulted to centered for the desktop icon rail and were flipped back by `[data-state="expanded"]`. That attribute tracks the desktop open state, while the mobile drawer runs off `openMobile`, so on a phone the labels rendered but the override never matched: 新建对话 and 我的报告 floated mid-drawer while every other row sat flush left. Left alignment is the base layer now, correct for both the drawer and the expanded desktop sidebar, and centering moved into the >=768px collapsed block beside the rest of the rail rules. That was the only top-level `[data-state=...]` selector left in the stylesheet. personal-report-entry asserted `justify-content: center` on the report button, which is the defect itself; the value is updated and the rest of that assertion stands. BUG-438. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu --- docs/BUG_HISTORY.md | 17 +++++++++++++ frontend/src/app/globals.css | 11 ++++++--- frontend/tests/personal-report-entry.test.ts | 5 +++- frontend/tests/sidebar-contract.test.ts | 26 ++++++++++++++++++++ 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 195f3edb..6075b600 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -6702,3 +6702,20 @@ - 相关记录:BUG-410、BUG-411、BUG-432 - 复发自:无 - 修复版本:待发布 +## BUG-438 | 移动端侧边抽屉里「新建对话」「我的报告」居中悬空,与其余左对齐项不齐 + +- 状态:resolved +- 首次发现:2026-08-29 +- 最近更新:2026-08-29 +- 影响面:`/` 移动端(<768px)侧边抽屉顶部两个导航项 +- 用户现象:品牌行、星盘列表、收藏对话、历史对话全部左对齐,只有「新建对话」和「我的报告」的图标+文字组居中飘在抽屉正中,视觉上完全不成列。 +- 触发条件:在移动端打开侧边抽屉即出现。 +- 根因:`.new-chat` / `.report-nav-button` 的基础样式是 `justify-content: center`(为桌面折叠图标栏准备),左对齐靠顶层的 `[data-state="expanded"] … { justify-content: flex-start }` 翻回来。但 `data-state` 取的是 `open ? "expanded" : "collapsed"`,`open` 是**桌面**折叠状态;移动抽屉由独立的 `openMobile` 控制。组件侧 `isCollapsedDesktop = state === "collapsed" && !isMobile`,移动端恒为 false,所以文字标签照常渲染;CSS 侧却因 `data-state="collapsed"` 匹配不上覆盖规则,按钮保持居中。JS 与 CSS 对"展开"的定义不一致。品牌行没被带歪,是因为 `[data-state="collapsed"] .brand-row` 那整套图标栏规则写在 `@media (min-width: 768px)` 里,移动端本就不生效。 +- 修复:把默认翻过来 —— 基础样式改为 `justify-content: flex-start`(移动抽屉与桌面展开态都正确),删掉依赖 `data-state` 的顶层覆盖,居中改为 `@media (min-width: 768px)` 内的 `[data-state="collapsed"]` 规则,与既有的 `.brand-row`、`[data-sidebar="menu-button"]`、`.profile-trigger` 等图标栏规则并列。全仓仅此一处顶层 `[data-state=…]` 选择器,其余均已按视口收口。 +- 验证:`frontend/tests/sidebar-contract.test.ts` 新增一条 —— 锁基础层左对齐、禁止再出现 `[data-state="expanded"]` 版覆盖、并要求居中规则位于 768px media 内。反向验证三次(基础样式改回 center / 覆盖加回来 / 居中规则移出 media),每次都变红。 +- 既有断言改动(唯一一处):`frontend/tests/personal-report-entry.test.ts` 原先断言 `.report-nav-button` 必须 `justify-content: center`,那正是本条缺陷本身,改为 `flex-start` 并注明原因。该断言其余部分(`display: flex`、`align-items: center`)未动。 +- 防复发:`data-state` 只描述桌面折叠态,不得用它决定移动抽屉的布局;折叠图标栏的样式一律写进 `@media (min-width: 768px)`,不得作为基础层默认值再由覆盖翻回。 +- 相关记录:BUG-434、BUG-436 +- 复发自:无 +- 修复版本:待发布 + diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index f93f9310..53a1d6a6 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -537,12 +537,10 @@ button:disabled { cursor: default; opacity: .45; } .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 oklch(0 0 0 / .1); } .auth-story-brand img { width: 32px; height: 32px; border-radius: 50%; object-fit: contain; box-shadow: 0 0 0 1px oklch(0 0 0 / .1); } -.new-chat { width: 100%; min-height: 44px; display: flex; align-items: center; justify-content: center; gap: var(--space-2); padding: 0 var(--space-3); border: 0; background: transparent; color: var(--sidebar-foreground); cursor: pointer; font-size: var(--type-body-sm); line-height: 1.35; transition: background-color 120ms ease-out, transform 120ms ease-out; margin: 0; border-radius: var(--radius-md); font-weight: 500; } -.report-nav-button { width: 100%; min-height: 44px; display: flex; align-items: center; justify-content: center; gap: var(--space-2); padding: 0 var(--space-3); margin: 0; border: 0; border-radius: var(--radius-md); background: transparent; color: var(--sidebar-foreground); cursor: pointer; font-size: var(--type-body-sm); font-weight: 500; line-height: 1.35; transition: background-color 120ms ease-out, transform 120ms ease-out; } +.new-chat { width: 100%; min-height: 44px; display: flex; align-items: center; justify-content: flex-start; gap: var(--space-2); padding: 0 var(--space-3); border: 0; background: transparent; color: var(--sidebar-foreground); cursor: pointer; font-size: var(--type-body-sm); line-height: 1.35; transition: background-color 120ms ease-out, transform 120ms ease-out; margin: 0; border-radius: var(--radius-md); font-weight: 500; } +.report-nav-button { width: 100%; min-height: 44px; display: flex; align-items: center; justify-content: flex-start; gap: var(--space-2); padding: 0 var(--space-3); margin: 0; border: 0; border-radius: var(--radius-md); background: transparent; color: var(--sidebar-foreground); cursor: pointer; font-size: var(--type-body-sm); font-weight: 500; line-height: 1.35; transition: background-color 120ms ease-out, transform 120ms ease-out; } .report-nav-button:hover, .new-chat:hover { background: var(--sidebar-accent); color: var(--sidebar-accent-foreground); } -[data-state="expanded"] .new-chat, -[data-state="expanded"] .report-nav-button { justify-content: flex-start; } .new-chat svg, .report-nav-button svg, .sidebar-label > svg, @@ -641,6 +639,11 @@ button:disabled { cursor: default; opacity: .45; } [data-state="collapsed"] .brand-row { justify-content: center; gap: 0; padding: 0; } [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; } + /* Centering belongs to the icon rail, which only exists at this breakpoint. + data-state tracks the desktop open state, so it must not decide layout on + the mobile drawer — that drawer is always expanded. */ + [data-state="collapsed"] .new-chat, + [data-state="collapsed"] .report-nav-button { justify-content: center; } [data-state="collapsed"] .new-chat { margin-block: var(--space-2); } [data-state="collapsed"] .profile-trigger { width: 44px; height: 44px; min-height: 44px; margin-inline: auto; padding: 6px; display: grid; grid-template-columns: 32px; place-items: center; } [data-state="collapsed"] .sidebar-footer { padding-top: var(--space-3); } diff --git a/frontend/tests/personal-report-entry.test.ts b/frontend/tests/personal-report-entry.test.ts index 459a9943..571742d1 100644 --- a/frontend/tests/personal-report-entry.test.ts +++ b/frontend/tests/personal-report-entry.test.ts @@ -211,9 +211,12 @@ test("entry is global in the sidebar and absent from the active session header", assert.match(pageSource, /onOpenReports=\{\(\) => router\.push\("\/reports"\)\}/); assert.doesNotMatch(pageSource, /window\.location\.assign\("\/reports"\)/); assert.doesNotMatch(pageSource, /GeneratePersonalReportButton|reportEntryVisible|reportEvidenceState/); + // Was `justify-content: center`, which is what put 我的报告 in the middle of + // the mobile drawer (BUG-438). The base layer is left-aligned now; centering + // moved into the >=768px collapsed rail, locked by sidebar-contract. assert.match( globalStyles, - /\.report-nav-button \{[^}]*display: flex;[^}]*align-items: center;[^}]*justify-content: center;/, + /\.report-nav-button \{[^}]*display: flex;[^}]*align-items: center;[^}]*justify-content: flex-start;/, ); }); diff --git a/frontend/tests/sidebar-contract.test.ts b/frontend/tests/sidebar-contract.test.ts index 710cfe1e..82b14f45 100644 --- a/frontend/tests/sidebar-contract.test.ts +++ b/frontend/tests/sidebar-contract.test.ts @@ -10,6 +10,32 @@ const globalStyles = readProjectFile("src/app/globals.css"); const cssBlock = (selector: string) => cssDeclarations(selector, globalStyles); +/** The base-layer rule only — media-query copies of a selector are indented. */ +const topLevelRule = (selector: string) => { + const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const hit = globalStyles.match(new RegExp(`^${escaped}\\s*\\{([^}]*)\\}`, "m")); + assert.ok(hit, `no base-layer rule for ${selector}`); + return hit[1]; +}; + +test("sidebar nav items read left-aligned everywhere; centering is the desktop rail only", () => { + // `data-state` tracks the DESKTOP open state, so it cannot decide layout on + // the mobile drawer — that drawer is always expanded no matter what the + // desktop rail is doing. Centering as the base layer, flipped back by a + // `[data-state="expanded"]` override, left 新建对话 / 我的报告 floating in + // the middle of the drawer on phones while every other row sat flush left. + for (const selector of [".new-chat", ".report-nav-button"]) { + assert.match(topLevelRule(selector), /justify-content:\s*flex-start/); + assert.doesNotMatch(topLevelRule(selector), /justify-content:\s*center/); + } + assert.doesNotMatch(globalStyles, /\[data-state="expanded"\][^{]*\.(?:new-chat|report-nav-button)/); + // The rail lives at >=768px, beside the other collapsed-state rules. + assert.match( + globalStyles, + /@media\s*\(min-width:\s*768px\)[\s\S]*\[data-state="collapsed"\]\s+\.new-chat,\s*\[data-state="collapsed"\]\s+\.report-nav-button\s*\{[^}]*justify-content:\s*center/, + ); +}); + test("provides the generic composable sidebar primitive", () => { assert.equal(existsSync(projectFile("src/components/ui/sidebar.tsx")), true); });