Skip to content

修复首页统计图表在小高度下裁切#838

Merged
ding113 merged 26 commits intoding113:devfrom
tesgth032:fix/statistics-chart-clipping
Feb 27, 2026
Merged

修复首页统计图表在小高度下裁切#838
ding113 merged 26 commits intoding113:devfrom
tesgth032:fix/statistics-chart-clipping

Conversation

@tesgth032
Copy link
Contributor

@tesgth032 tesgth032 commented Feb 26, 2026

目的

修复首页「使用统计」卡片(StatisticsChartCard)在浏览器视口高度较小时,底部 Legend / 操作按钮被裁切不可见的问题。

根因

首页 Bento 布局为保持紧凑,对该卡片设置了 max-h;但 Admin 多用户模式会额外渲染 Legend 与操作按钮。如果图表仍使用固定高度,总高度可能超过卡片高度上限,从而发生裁切。

方案

在不引入卡片整体滚动条的前提下:

  • 外框高度策略:min-h: 300px + max-h: min(60dvh, 720px)(补充 --cch-viewport-height-60 变量,支持 dvh 时优先使用)。
    • 720px cap:在常见 1080p 屏下 60vh≈648px,不会触发 cap;在更大屏幕上避免卡片过高影响首页布局。
  • 图表高度自适应:DOM 实测 Header / Metric Tabs / Legend / padding / border 的像素高度,计算可用高度并更新图表容器高度。
    • 监听:ResizeObserver(仅观察 header/tabs/legend)+ window.resize + visualViewport.resize;并用 rounding + safety gap 避免缩放/子像素导致的 1–2px 级裁切。
    • 兜底:极端视口高度下可用空间不足时,优先保证 Legend/按钮不被裁切(图表区域可退化收缩)。
  • Tooltip:tooltip 允许浮出卡片(用于 tooltip,卡片本身仍不滚动);tooltip 列表高度上限 min(80dvh, 720px);鼠标悬停图表区域时滚轮优先滚动 tooltip 列表(无需把光标移入 tooltip 才能滚动)。

UI 细节

  • 留白收紧:p-0 md:p-1;Legend 操作按钮改为右上角悬浮,减少额外占行。
  • i18n/RTL:操作按钮 whitespace-nowrap + 预留宽度,并补 rtl: 适配;Legend 用户名截断补 title

验证

  • 本地:bun run lint / bun run lint:fix / bun run typecheck / bun run test / bun run build
  • GitHub Actions:全部通过

手动验证建议

  • bun run dev,访问 /dashboard
  • 将浏览器高度缩到约 500–600px,切到 Admin 多用户模式
  • 确认:卡片无整体滚动条;图表高度会收缩;底部 Legend/按钮始终可见;tooltip 在顶部显示且列表可滚动

Greptile Summary

Implements dynamic chart height calculation to prevent clipping of legend and controls on small viewports. The approach uses ResizeObserver to measure actual DOM element heights (header, tabs, legend, padding, borders) and calculates available space within card constraints (min-h-[300px], max-h-[min(60vh, 720px)]).

Key Changes:

  • Dynamic chart height: Uses getBoundingClientRect() measurements and parsePx utility to calculate remaining space after accounting for all UI elements
  • Viewport-aware constraints: New CSS variable --cch-viewport-height-60 with dvh fallback for better mobile support
  • Legend controls repositioned: Changed from inline to absolute floating (top-right), reducing vertical space consumption
  • Tooltip enhancements: Scrollable with wheel event hijacking, max-height capped at min(80vh, 720px)
  • Padding adjustments: Card padding reduced to p-0 md:p-1, metric tabs compressed from py-3 to py-2

Major Concerns from Previous Review (36 existing comments):

  • Critical: Chart can become 0px at extreme viewport sizes (~400px viewport) with no minimum threshold or fallback UI
  • Critical: overflow-visible contradicts stated goal of preventing card scrolling - allows content to overflow boundaries
  • Logic: Missing data.users dependency risk if users array changes
  • Performance: ResizeObserver watching 3+ elements, window + visualViewport listeners may fire compute() excessively
  • RTL: Uses right-4/pr-24 workarounds instead of logical properties (end-4, pe-24)
  • Accessibility: 4px safety gap may be excessive, magic numbers (w-24, max-h-[72px]) lack documentation
  • UX: "Deselect All" button actually selects first user with usage, not true deselect

Note: PR title indicates this is [未完成] (incomplete/work-in-progress). Many of the flagged issues should be addressed before marking as ready for merge.

Confidence Score: 2/5

  • Not safe to merge - PR marked as incomplete, has 36+ unresolved review comments including critical issues (0px chart height, overflow contradictions, logic gaps)
  • Score reflects WIP status and substantial existing feedback. The dynamic height calculation is architecturally sound but has edge cases (extreme viewport sizes allowing 0px chart, no minimum threshold). The 36 previous review comments cover performance concerns (redundant listeners), accessibility issues (magic numbers, excessive safety gaps), RTL support gaps (missing logical properties), and logic risks (missing dependencies). While the core approach is promising, the volume of unresolved feedback and [未完成] tag indicate this needs another iteration
  • Focus on statistics-chart-card.tsx - address the 0px chart height edge case, reconsider overflow-visible vs stated goals, review all 36 flagged comments for validity

Important Files Changed

Filename Overview
src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx Major refactor implementing dynamic chart height calculation with ResizeObserver, adds viewport-aware constraints, repositions legend controls, but has multiple flagged issues including potential 0px chart height, overflow-visible contradicting goals, and ~36 existing review comments needing resolution

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Component Mount] --> B[Initialize chartHeightPx State]
    B --> C[useLayoutEffect: Setup Height Calculation]
    C --> D{Check Required Refs}
    D -->|Missing| E[Return Early]
    D -->|All Present| F[Create compute Function]
    F --> G[Get viewport height<br/>visualViewport or innerHeight]
    G --> H[Calculate fallback: min 60% vh, 720px]
    H --> I[Parse CSS maxHeight from card]
    I --> J[Get effectiveMaxHeight<br/>max minHeight, maxHeight]
    J --> K[Measure Elements<br/>header, tabs, legend, padding, border]
    K --> L[Calculate Reserved Height<br/>sum + round + safetyGap 4px]
    L --> M[Calculate Available Height<br/>effectiveMax - reserved - border]
    M --> N[Clamp to Chart Max<br/>240px or 280px based on legend]
    N --> O{Height Changed?}
    O -->|No| P[Skip Update]
    O -->|Yes| Q[Set chartHeightPx State]
    Q --> R[Chart Re-renders with New Height]
    C --> S[Setup ResizeObserver<br/>or fallback to window resize]
    S --> T[Observe: header, tabs, legend<br/>Listen: window, visualViewport]
    T --> U[On Resize: Trigger compute]
    U --> F
    V[useEffect: Wheel Handler] --> W[Listen to Chart Wheel Events]
    W --> X{Tooltip Visible<br/>& Scrollable?}
    X -->|Yes| Y[Scroll Tooltip<br/>preventDefault]
    X -->|No| Z[Allow Default Scroll]
Loading

Last reviewed commit: 9aad41f

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @tesgth032, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此 PR 旨在解决首页统计图表在视口高度较小时内容被裁切的 UI 问题,尤其是在启用多用户图例时。通过为图表卡片引入纵向滚动和动态高度调整机制,确保了所有图表内容(包括图例和控制按钮)在不同屏幕尺寸下都能完整显示。此外,为了便于开发和验证这些 UI 改进,新增了一个专门的内部预览页面,并相应地调整了开发环境下的访问权限。

Highlights

  • UI 修复: 修复了首页统计图表在小高度下内容被裁切的问题。
  • 图表卡片改进: 统计图表卡片现在支持纵向滚动,并根据可用高度自适应调整图表尺寸。
  • 开发工具: 新增了一个内部 UI 预览页面,用于在开发环境中方便地测试统计图表。
  • 访问控制: 更新了代理配置,允许开发环境免登录访问内部 UI 预览页面。
Changelog
  • src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
    • 调整了 BentoCard 的样式,使其支持纵向滚动 (overflow-y-auto) 并隐藏横向溢出 (overflow-x-hidden)。
    • 为图表容器引入了动态高度计算,使用 clamp() 函数根据视口高度和是否启用用户筛选器来限制图表高度。
    • 统一了图表和图例部分的内边距。
  • src/app/[locale]/internal/ui-preview/statistics-chart/_components/statistics-chart-preview.tsx
    • 新增了一个 React 组件,用于生成模拟的用户统计数据和图表数据。
    • 包含了构建模拟用户、模拟图表数据(使用高斯峰值模拟活动)以及模拟统计数据的功能。
    • 利用 useState 和 useMemo 管理时间范围和数据生成,以展示 StatisticsChartCard。
  • src/app/[locale]/internal/ui-preview/statistics-chart/page.tsx
    • 新增了一个 Next.js 页面,用于渲染 StatisticsChartPreview 组件。
    • 增加了环境检查,确保该预览页面仅在开发环境下可用,生产环境会返回 404。
  • src/proxy.ts
    • 定义了 DEV_PUBLIC_PATH_PATTERNS 数组,将 /internal/ui-preview 路径添加到开发环境的公共路径中。
    • 修改了 proxyHandler 函数,使其在开发模式下允许对 DEV_PUBLIC_PATH_PATTERNS 中的路径进行免认证访问。
Activity
  • PR 标题标记为 "[未完成]",表明此工作仍在进行中。
  • PR 描述中包含了详细的验证步骤,包括 bun install、bun run dev、调整浏览器视口高度等,以及通过 bun run build、bun run lint、bun run typecheck、bun run test 的检查项,显示作者已进行了初步的测试和验证。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions bot added bug Something isn't working area:UI area:statistics labels Feb 26, 2026
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 files reviewed, 38 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +171 to +172
? "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)-248px),240px)] w-full"
: "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)-138px),280px)] w-full";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

calc() operator requires spaces in Tailwind arbitrary values - use underscores

Suggested change
? "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)-248px),240px)] w-full"
: "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)-138px),280px)] w-full";
? "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)_-_248px),240px)] w-full"
: "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)_-_138px),280px)] w-full";
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 171-172

Comment:
calc() operator requires spaces in Tailwind arbitrary values - use underscores

```suggestion
    ? "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)_-_248px),240px)] w-full"
    : "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)_-_138px),280px)] w-full";
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 177 to 179
"p-0 md:p-0 overflow-x-hidden overflow-y-auto max-h-[var(--cch-viewport-height-50)]",
"scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent",
className
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed flex flex-col - verify header/tabs/chart/legend still stack correctly

Also, md:p-0 is redundant when p-0 is already set

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 177-179

Comment:
removed `flex flex-col` - verify header/tabs/chart/legend still stack correctly

Also, `md:p-0` is redundant when `p-0` is already set

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

本次 PR 修复了首页统计图表在小高度屏幕上内容被裁切的问题。主要改动包括:将图表容器改为纵向滚动,并使用 clamp() 使图表高度自适应。此外,为方便调试,新增了一个 UI 预览页面,并更新了中间件以在开发环境下免登录访问。

整体改动方向正确。我只发现一个可维护性方面的小问题:在计算图表动态高度时使用了“魔术数字”,这会使代码未来难以维护。我已在具体代码行提出了改进建议。

Comment on lines +170 to +172
const chartContainerClassName = enableUserFilter
? "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)-248px),240px)] w-full"
: "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)-138px),280px)] w-full";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这里的 248px138px 是“魔术数字”(Magic Numbers)。它们的含义不明确,导致代码难以理解和维护。如果卡片内其他元素的高度发生变化,就需要手动重新计算和更新这些值,这很容易出错。

建议添加注释来解释这些值的来源,以提高代码的可读性和可维护性。

  const chartContainerClassName = enableUserFilter
    // 248px is the approximate height of non-chart elements when the legend is visible.
    ? "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)-248px),240px)] w-full"
    // 138px is the approximate height of non-chart elements when the legend is hidden.
    : "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)-138px),280px)] w-full";

@github-actions github-actions bot added the size/S Small PR (< 200 lines) label Feb 26, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

新增统计图表预览组件与预览页,修改仪表盘统计图表卡片的高度与滚动布局;在开发模式下将 /internal/ui-preview 路径视为公开以绕过鉴权,并新增针对该行为的单元测试。

Changes

Cohort / File(s) Summary
统计图表卡片样式调整
src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
添加图表高度常量并基于 enableUserFilter 计算动态高度(使用 clamp);替换外层包裹以支持 overflow-x-hiddenoverflow-y-auto,调整内边距(p-4px-4 py-3)、移除/简化自定义滚动条类,替换固定高度为样式化高度。
统计图表预览组件
src/app/[locale]/internal/ui-preview/statistics-chart/_components/statistics-chart-preview.tsx
新增客户端组件 StatisticsChartPreview:生成模拟用户与按时间范围的图表数据(含高斯峰模型),使用 useMemo 管理数据并渲染 StatisticsChartCardcurrencyCode="USD"),导出组件。
预览页面路由
src/app/[locale]/internal/ui-preview/statistics-chart/page.tsx
新增 Next.js 页面,导出 dynamic = "force-dynamic";生产环境调用 notFound(),开发/预览环境渲染 StatisticsChartPreview
代理:开发公开路径处理
src/proxy.ts
新增 DEV_PUBLIC_PATH_PATTERNS = ["/internal/ui-preview"]isDevPublicPath(),在开发模式下匹配该路径时将其视为公开并提前返回 localeResponse(绕过鉴权流程)。
单元测试(代理)
tests/unit/proxy/proxy-dev-public-path.test.ts
新增测试文件:Hoist 并 mock 依赖,验证开发模式下 /internal/ui-preview/* 可在无 cookie 时访问且不匹配相似前缀(如 /internal/ui-preview-xxx)。

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • fix(ui): admin panel UI improvements #538 — 修改了 ChartContainer/ResponsiveContainer 的尺寸与布局控制,与本次对 statistics-chart-card 中 ChartContainer 类名和高度控制的修改存在代码层面关联。
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed PR 描述清晰完整,详细说明了问题、根因、解决方案和验证步骤,与变更内容高度相关。
Title check ✅ Passed 标题用中文简洁地总结了主要改动:修复首页统计图表在小高度下的裁切问题,准确反映了PR的核心目的。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@tesgth032
Copy link
Contributor Author

已根据 review 修复 calc() 运算符需要空格的问题:Tailwind 任意值里用 _ 代表空格,确保 height: clamp(..., calc(var(--cch-viewport-height-50) - xxxpx), ...) 在浏览器中能正确解析。

对应提交:f639b139

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`[locale]/internal/ui-preview/statistics-chart/page.tsx:
- Line 2: 当前在 page.tsx 中通过相对路径导入 StatisticsChartPreview(import {
StatisticsChartPreview } from
"./_components/statistics-chart-preview";),请把相对路径改为仓库约定的别名导入,使用 '@/...'
别名来引用该组件(即将开头的 "./" 换为 "@/..."),确保导入符号仍为 StatisticsChartPreview 并保存原有导出路径末段
"_components/statistics-chart-preview"。

In `@src/proxy.ts`:
- Around line 55-59: The current isDevPublicPath computation uses
DEV_PUBLIC_PATH_PATTERNS.some(... pattern => pathWithoutLocale === pattern ||
pathWithoutLocale.startsWith(pattern)) which is too permissive; change the
second check to require a true subpath boundary (e.g., pathWithoutLocale ===
pattern || pathWithoutLocale.startsWith(pattern + '/') or ensure the next
character after the match is '/'), so only exact matches or proper child paths
match; update the logic used by isDevPublicPath (referencing
DEV_PUBLIC_PATH_PATTERNS and pathWithoutLocale) accordingly.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between 8dc3e70 and f639b13.

📒 Files selected for processing (4)
  • src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
  • src/app/[locale]/internal/ui-preview/statistics-chart/_components/statistics-chart-preview.tsx
  • src/app/[locale]/internal/ui-preview/statistics-chart/page.tsx
  • src/proxy.ts

Comment on lines +170 to +172
const chartContainerClassName = enableUserFilter
? "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)_-_248px),240px)] w-full"
: "aspect-auto h-[clamp(140px,calc(var(--cch-viewport-height-50)_-_138px),280px)] w-full";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chart height uses calc(50vh - offset) but card has max-h-50vh not fixed h-50vh. when content is smaller than 50vh, chart might be unnecessarily small. consider flex layout with min-h on chart instead for more predictable sizing

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 170-172

Comment:
chart height uses `calc(50vh - offset)` but card has `max-h-50vh` not fixed `h-50vh`. when content is smaller than 50vh, chart might be unnecessarily small. consider flex layout with `min-h` on chart instead for more predictable sizing

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@tesgth032
Copy link
Contributor Author

补充:已按 CodeRabbit 建议收紧开发环境放行规则,仅匹配 /internal/ui-preview 本身或其子路径(/internal/ui-preview/...),避免误放行类似 /internal/ui-preview-xxx

对应提交:c586906c

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
tests/unit/proxy/proxy-dev-public-path.test.ts (1)

37-62: 建议添加 beforeEach 清理 mock 状态以确保测试隔离。

mockIntlMiddleware 在测试间共享状态,可能导致测试顺序依赖或状态泄漏。同时建议使用 vi.resetModules() 确保每个测试获得干净的模块实例。

♻️ 建议添加测试钩子
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { describe, expect, it, vi } from "vitest";

 // ... mocks ...

 describe("proxy dev public paths", () => {
+  beforeEach(() => {
+    vi.resetModules();
+    mockIntlMiddleware.mockReset();
+  });
+
+  afterEach(() => {
+    vi.restoreAllMocks();
+  });
+
   it("allows /internal/ui-preview/* without any cookie in development", async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/proxy/proxy-dev-public-path.test.ts` around lines 37 - 62, Add a
beforeEach that isolates tests by calling vi.resetModules() and
clearing/resetting the shared mockIntlMiddleware (e.g.,
mockIntlMiddleware.mockReset() or mockIntlMiddleware.mockClear()) before each
it; this ensures that when importing proxyHandler via await import("@/proxy")
and invoking makeRequest(...) each test gets a fresh module instance and no
leaked mock state.
src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx (1)

170-178: 魔法数字(248px / 138px)的可维护性

注释解释了这些值的来源,但它们与 Header、Tabs、Padding、Legend 等的实际高度耦合较紧。如果后续调整了这些区域的间距或字体大小,这里的估算值可能会失效。

建议:考虑将这些偏移量提取为命名常量或 CSS 变量,并在注释中列出各部分的具体高度构成,便于后续维护时快速定位和调整。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/dashboard/_components/bento/statistics-chart-card.tsx
around lines 170 - 178, The hard-coded offsets 248px and 138px in
chartContainerClassName are magic numbers; extract them into named constants or
CSS variables (e.g., CHART_OFFSET_WITH_LEGEND, CHART_OFFSET_NO_LEGEND or
--chart-offset-with-legend, --chart-offset-no-legend) and replace the inline
literals in the conditional class strings; also update the surrounding comment
to enumerate the constituent heights (Header, Tabs, Padding, Legend) and
reference the new constants/variables so future UI changes can be adjusted in
one place (use enableUserFilter and chartContainerClassName to locate the code).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`[locale]/dashboard/_components/bento/statistics-chart-card.tsx:
- Around line 183-185: The scrollbar utility classes used in
statistics-chart-card.tsx ("scrollbar-thin", "scrollbar-thumb-muted",
"scrollbar-track-transparent") are not defined; fix by either installing and
enabling the tailwind-scrollbar plugin (npm/yarn add tailwind-scrollbar and add
it to plugins in tailwind.config.js) so these classes are generated, or add
custom utility definitions in your global stylesheet (src/app/globals.css) using
an `@layer` utilities block that defines .scrollbar-thin, .scrollbar-thumb-muted,
and .scrollbar-track-transparent so the component’s className in
statistics-chart-card.tsx works as intended.

In `@tests/unit/proxy/proxy-dev-public-path.test.ts`:
- Around line 55-56: The test calls proxyHandler(...) without awaiting its
(likely) async result; update the test to await the proxy handler invocation so
the call to proxyHandler returned promise is resolved before assertions.
Specifically, in tests/unit/proxy/proxy-dev-public-path.test.ts change the line
that calls proxyHandler(makeRequest("/zh-CN/internal/ui-preview-xxx")) to await
the async call to proxyHandler (the symbol proxyHandler) so the response is
properly resolved before using it in assertions.

---

Nitpick comments:
In `@src/app/`[locale]/dashboard/_components/bento/statistics-chart-card.tsx:
- Around line 170-178: The hard-coded offsets 248px and 138px in
chartContainerClassName are magic numbers; extract them into named constants or
CSS variables (e.g., CHART_OFFSET_WITH_LEGEND, CHART_OFFSET_NO_LEGEND or
--chart-offset-with-legend, --chart-offset-no-legend) and replace the inline
literals in the conditional class strings; also update the surrounding comment
to enumerate the constituent heights (Header, Tabs, Padding, Legend) and
reference the new constants/variables so future UI changes can be adjusted in
one place (use enableUserFilter and chartContainerClassName to locate the code).

In `@tests/unit/proxy/proxy-dev-public-path.test.ts`:
- Around line 37-62: Add a beforeEach that isolates tests by calling
vi.resetModules() and clearing/resetting the shared mockIntlMiddleware (e.g.,
mockIntlMiddleware.mockReset() or mockIntlMiddleware.mockClear()) before each
it; this ensures that when importing proxyHandler via await import("@/proxy")
and invoking makeRequest(...) each test gets a fresh module instance and no
leaked mock state.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between c586906 and be37849.

📒 Files selected for processing (2)
  • src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
  • tests/unit/proxy/proxy-dev-public-path.test.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`[locale]/dashboard/_components/bento/statistics-chart-card.tsx:
- Around line 176-178: 将第 170-175 行过长的注释拆成多行以满足 100 字符行宽限制:在同一文件的
chartContainerClassName 定义附近(变量名 chartContainerClassName)将原来几行超过 100
字符的注释分割为多行注释,每行不超过 100 字符,保留原注释的全部信息(关于 max-h 50vh、启用多用户 Legend
导致非图表部分高度增大、使用“可用高度估算 + clamp”方式自适应高度,以及 overflow-y-auto
对裁切影响的说明),确保注释语义不变且对齐/缩进与周围注释风格一致。

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between be37849 and 2292e2b.

📒 Files selected for processing (2)
  • src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
  • tests/unit/proxy/proxy-dev-public-path.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/proxy/proxy-dev-public-path.test.ts

Comment on lines +37 to +68
describe("proxy dev public paths", () => {
beforeEach(() => {
vi.resetModules();
mockIntlMiddleware.mockReset();
});

it("allows /internal/ui-preview/* without any cookie in development", async () => {
const localeResponse = new Response(null, {
status: 200,
headers: { "x-test": "dev-public-ok" },
});
mockIntlMiddleware.mockReturnValue(localeResponse);

const { default: proxyHandler } = await import("@/proxy");
const response = await proxyHandler(makeRequest("/zh-CN/internal/ui-preview/statistics-chart"));

expect(response.headers.get("x-test")).toBe("dev-public-ok");
});

it("does not overmatch /internal/ui-preview-xxx", async () => {
const localeResponse = new Response(null, { status: 200 });
mockIntlMiddleware.mockReturnValue(localeResponse);

const { default: proxyHandler } = await import("@/proxy");
const response = await proxyHandler(makeRequest("/zh-CN/internal/ui-preview-xxx"));

expect(response.status).toBeGreaterThanOrEqual(300);
expect(response.status).toBeLessThan(400);
const location = response.headers.get("location");
expect(location).toContain("/login");
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing production-mode security test

The entire test file mocks isDevelopment: () => true, so it only verifies the development happy-path. There is no test case that confirms the security guarantee stated in the PR description — that /internal/ui-preview is blocked (redirects to login) when isDevelopment() returns false.

The existing proxy-auth-cookie-passthrough.test.ts covers isDevelopment: () => false for /dashboard and /login, but never exercises the new /internal/ui-preview path. If the isDevelopment() guard were accidentally removed from the proxy, no test in either file would catch the regression.

Consider adding a third case to this describe block:

it("blocks /internal/ui-preview/* in production (isDevelopment = false)", async () => {
  // Re-mock isDevelopment to return false for this test
  vi.doMock("@/lib/config/env.schema", () => ({
    isDevelopment: () => false,
  }));
  vi.resetModules();

  const localeResponse = new Response(null, { status: 200 });
  mockIntlMiddleware.mockReturnValue(localeResponse);

  const { default: proxyHandler } = await import("@/proxy");
  const response = await proxyHandler(
    makeRequest("/zh-CN/internal/ui-preview/statistics-chart")
  );

  expect(response.status).toBeGreaterThanOrEqual(300);
  expect(response.status).toBeLessThan(400);
  expect(response.headers.get("location")).toContain("/login");
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/unit/proxy/proxy-dev-public-path.test.ts
Line: 37-68

Comment:
**Missing production-mode security test**

The entire test file mocks `isDevelopment: () => true`, so it only verifies the development happy-path. There is no test case that confirms the security guarantee stated in the PR description — that `/internal/ui-preview` is **blocked** (redirects to login) when `isDevelopment()` returns `false`.

The existing `proxy-auth-cookie-passthrough.test.ts` covers `isDevelopment: () => false` for `/dashboard` and `/login`, but never exercises the new `/internal/ui-preview` path. If the `isDevelopment()` guard were accidentally removed from the proxy, no test in either file would catch the regression.

Consider adding a third case to this `describe` block:

```ts
it("blocks /internal/ui-preview/* in production (isDevelopment = false)", async () => {
  // Re-mock isDevelopment to return false for this test
  vi.doMock("@/lib/config/env.schema", () => ({
    isDevelopment: () => false,
  }));
  vi.resetModules();

  const localeResponse = new Response(null, { status: 200 });
  mockIntlMiddleware.mockReturnValue(localeResponse);

  const { default: proxyHandler } = await import("@/proxy");
  const response = await proxyHandler(
    makeRequest("/zh-CN/internal/ui-preview/statistics-chart")
  );

  expect(response.status).toBeGreaterThanOrEqual(300);
  expect(response.status).toBeLessThan(400);
  expect(response.headers.get("location")).toContain("/login");
});
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +7 to +9
if (process.env.NODE_ENV === "production") {
notFound();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent env guard vs. proxy

The proxy uses isDevelopment() (i.e. NODE_ENV === "development") to decide whether to bypass authentication. This page guard uses process.env.NODE_ENV === "production" to call notFound(). The two conditions are not logical inverses:

NODE_ENV Proxy allows access? Page calls notFound()?
"development" ✅ yes ❌ no → renders ✓
"production" ❌ no ✅ yes → 404 ✓
"test" ❌ no ❌ no → renders ⚠️

In a NODE_ENV=test environment the page itself would serve the preview content rather than returning 404, while the proxy simultaneously blocks all access. Defense-in-depth works best when both layers agree. Using !== "development" (mirroring the proxy) closes the gap:

Suggested change
if (process.env.NODE_ENV === "production") {
notFound();
}
if (process.env.NODE_ENV !== "development") {
notFound();
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/internal/ui-preview/statistics-chart/page.tsx
Line: 7-9

Comment:
**Inconsistent env guard vs. proxy**

The proxy uses `isDevelopment()` (i.e. `NODE_ENV === "development"`) to decide whether to bypass authentication. This page guard uses `process.env.NODE_ENV === "production"` to call `notFound()`. The two conditions are **not logical inverses**:

| `NODE_ENV` | Proxy allows access? | Page calls `notFound()`? |
|---|---|---|
| `"development"` | ✅ yes | ❌ no → renders ✓ |
| `"production"` | ❌ no | ✅ yes → 404 ✓ |
| `"test"` | ❌ no | ❌ no → **renders** ⚠️ |

In a `NODE_ENV=test` environment the page itself would serve the preview content rather than returning 404, while the proxy simultaneously blocks all access. Defense-in-depth works best when both layers agree. Using `!== "development"` (mirroring the proxy) closes the gap:

```suggestion
  if (process.env.NODE_ENV !== "development") {
    notFound();
  }
```

How can I resolve this? If you propose a fix, please make it concise.

</div>
{/* User list with max 3 rows and scroll - only show users with non-zero usage */}
<div className="max-h-[72px] overflow-y-auto scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent">
<div className="max-h-[72px] overflow-y-auto">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scrollbar styling removed from legend list

scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent were removed from this inner scrollable container. scrollbar-thin is still used elsewhere in the codebase (e.g. request-list-sidebar.tsx), so removing it here means the legend list will now show the default browser-native scrollbar, which can appear visually heavier than the rest of the UI on Windows/Linux.

Since the outer BentoCard now also scrolls without any scrollbar styling, both scroll surfaces render as native scrollbars. If thin scrollbars are desired on the legend list (consistent with the rest of the app), the original classes should be retained:

Suggested change
<div className="max-h-[72px] overflow-y-auto">
<div className="max-h-[72px] overflow-y-auto scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent">

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 453

Comment:
**Scrollbar styling removed from legend list**

`scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent` were removed from this inner scrollable container. `scrollbar-thin` is still used elsewhere in the codebase (e.g. `request-list-sidebar.tsx`), so removing it here means the legend list will now show the default browser-native scrollbar, which can appear visually heavier than the rest of the UI on Windows/Linux.

Since the outer `BentoCard` now also scrolls without any scrollbar styling, both scroll surfaces render as native scrollbars. If thin scrollbars are desired on the legend list (consistent with the rest of the app), the original classes should be retained:

```suggestion
          <div className="max-h-[72px] overflow-y-auto scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent">
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@tesgth032
Copy link
Contributor Author

已按 AI 审阅意见做收尾:

  • 统计图表:将高度计算中的偏移/上下限提取为常量,并改为通过 style.height 设置 clamp(calc(...)),降低魔术数字与 Tailwind 任意值解析带来的维护成本。
  • 单测:增加 beforeEach 执行 vi.resetModules() + mockIntlMiddleware.mockReset(),避免测试间状态泄漏。

当前 CI 已全绿。

@coderabbitai 如方便请重新 review/更新状态(此前标记的 changes requested 项已处理)。

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

@tesgth032 感谢您的细致处理!很高兴看到您:

✅ 将高度计算参数提取为常量并通过 style.height 设置,提升了可维护性
✅ 在单测中添加 beforeEach 清理逻辑,避免测试间状态污染
✅ CI 全部通过

我会立即重新审查本 PR 并更新状态。

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

- 统计卡片在 50vh 外框下改为可滚动,图表高度按可用空间自适应,避免 Legend 被裁切\n- 开发环境新增 /internal/ui-preview/statistics-chart 预览页(无需 DB)\n- 开发环境放行 /internal/ui-preview 免登录访问,方便本地 UI 检查
- 将图表高度与非图表区域偏移提取为常量,改用 style 计算 clamp() 高度

- 单测增加 beforeEach 重置模块与 mock,避免状态泄漏
@tesgth032 tesgth032 force-pushed the fix/statistics-chart-clipping branch from 85bc440 to 51d0b83 Compare February 27, 2026 05:34
// - DOM 计算高度可能带小数(浏览器缩放 / 子像素)
// - 卡片自身 border 也会占用 max-h 的可用空间
const reservedHeightRounded = Math.ceil(reservedHeight);
const safetyGapPx = 4;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

safetyGapPx = 4 seems excessive. Comment says avoiding "1px 级的裁切" (1-2px clipping), but adds 4px buffer. Consider 2px or document why 4px is necessary

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 125

Comment:
`safetyGapPx = 4` seems excessive. Comment says avoiding "1px 级的裁切" (1-2px clipping), but adds 4px buffer. Consider 2px or document why 4px is necessary

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

</div>
{/* User list with max 3 rows and scroll - only show users with non-zero usage */}
<div className="max-h-[72px] overflow-y-auto scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent">
<div className="max-h-[72px] overflow-y-auto">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

max-h-[72px] magic number. Comment says "max 3 rows" - document calculation (e.g., 3 rows × ~24px per row)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 539

Comment:
`max-h-[72px]` magic number. Comment says "max 3 rows" - document calculation (e.g., 3 rows × ~24px per row)

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

{t("totalCost")}
</span>
<span className="text-lg font-bold tabular-nums">
<span className="text-[17px] leading-tight font-bold tabular-nums">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text-[17px] breaks design system. Use theme values (text-base: 16px, text-lg: 18px) for consistency

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 345

Comment:
`text-[17px]` breaks design system. Use theme values (`text-base`: 16px, `text-lg`: 18px) for consistency

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

{t("totalCalls")}
</span>
<span className="text-lg font-bold tabular-nums">
<span className="text-[17px] leading-tight font-bold tabular-nums">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text-[17px] breaks design system. Use theme values for consistency

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 361

Comment:
`text-[17px]` breaks design system. Use theme values for consistency

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +41 to +42
const parsed = Number.parseFloat(trimmed.slice(0, -2));
return Number.isFinite(parsed) ? parsed : null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parsePx doesn't validate non-negative. Add check: return parsed >= 0 ? parsed : null

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 41-42

Comment:
`parsePx` doesn't validate non-negative. Add check: `return parsed >= 0 ? parsed : null`

How can I resolve this? If you propose a fix, please make it concise.

<div className="flex items-center justify-center gap-2 mb-2">
<div ref={legendRef} className="relative px-4 pb-2">
{/* Control buttons (floating, does not take extra vertical space) */}
<div className="absolute right-4 top-0.5 z-10 w-20 flex flex-wrap justify-end gap-x-2 gap-y-0.5">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed w-20 (80px) width may be insufficient for i18n button text

The container is w-20 with flex-wrap, and the user list has pr-20 to compensate. If translated button text is longer (e.g., German "Alle auswählen" ≈ 13 chars vs English "Select All" ≈ 10 chars), buttons could wrap to a second line and overlap with user badges below since there's no top padding on the user list

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 507

Comment:
fixed `w-20` (80px) width may be insufficient for i18n button text

The container is `w-20` with `flex-wrap`, and the user list has `pr-20` to compensate. If translated button text is longer (e.g., German "Alle auswählen" ≈ 13 chars vs English "Select All" ≈ 10 chars), buttons could wrap to a second line and overlap with user badges below since there's no top padding on the user list

How can I resolve this? If you propose a fix, please make it concise.

const cardStyle = getComputedStyle(card);
const maxHeightPx =
parsePx(cardStyle.maxHeight) ??
Math.floor((window.visualViewport?.height ?? window.innerHeight) * 0.5);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

window.visualViewport fallback behavior differs from CSS vh units

visualViewport.height accounts for virtual keyboards on mobile but on zoomed pages, innerHeight returns zoomed pixels while vh units use layout viewport, potentially causing 1-2px discrepancy. Consider using document.documentElement.clientHeight as fallback for consistency with CSS viewport units

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 100

Comment:
`window.visualViewport` fallback behavior differs from CSS `vh` units

`visualViewport.height` accounts for virtual keyboards on mobile but on zoomed pages, `innerHeight` returns zoomed pixels while `vh` units use layout viewport, potentially causing 1-2px discrepancy. Consider using `document.documentElement.clientHeight` as fallback for consistency with CSS viewport units

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link

greptile-apps bot commented Feb 27, 2026

Additional Comments (1)

src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
max-w-[80px] truncates usernames without tooltip

Add title={user.name} to the button or span so users can see full names on hover

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 565

Comment:
`max-w-[80px]` truncates usernames without tooltip

Add `title={user.name}` to the button or span so users can see full names on hover

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +420 to +424
wrapperStyle={{
transform: "translateY(-100%)",
marginTop: "-20px",
zIndex: 1000,
}}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hardcoded tooltip positioning can cause overflow on small chart heights

transform: "translateY(-100%)" moves tooltip up by its full height, plus 20px margin. when chart is ~50-100px tall (small viewport scenario this PR addresses), tooltip near top of chart would render outside card/viewport boundaries, getting clipped or hidden

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 420-424

Comment:
hardcoded tooltip positioning can cause overflow on small chart heights

`transform: "translateY(-100%)"` moves tooltip up by its full height, plus 20px margin. when chart is ~50-100px tall (small viewport scenario this PR addresses), tooltip near top of chart would render outside card/viewport boundaries, getting clipped or hidden

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 564 to 574
onClick={() => {
if (data.users.length > 0) {
setSelectedUserIds(new Set([data.users[0].id]));
const firstUserWithUsageId =
data.users.find((u) => {
const total = userTotals[u.dataKey];
return total.cost.greaterThan(0) || total.calls > 0;
})?.id ?? data.users[0]?.id;

if (firstUserWithUsageId != null) {
setSelectedUserIds(new Set([firstUserWithUsageId]));
}
}}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Deselect All" button doesn't actually deselect all users—it selects the first user with usage data. Users expecting typical "deselect all" behavior (clearing the selection entirely) may be confused. Consider a more descriptive label like "Select One" or "Reset Selection".

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 564-574

Comment:
"Deselect All" button doesn't actually deselect all users—it selects the first user with usage data. Users expecting typical "deselect all" behavior (clearing the selection entirely) may be confused. Consider a more descriptive label like "Select One" or "Reset Selection".

How can I resolve this? If you propose a fix, please make it concise.

<div className="text-xs font-medium text-center mb-2 pb-2 border-b border-border/50">
<div
className="rounded-lg border bg-background shadow-sm min-w-[180px] overflow-y-auto"
style={{ maxHeight: "min(var(--cch-viewport-height-80), 720px)" }}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says tooltip max-height is min(80dvh,560px) but code has 720px

The "更新" section states: "并将 tooltip 列表尽量展开(max-height: min(80dvh,560px))" but implementation uses 720px. Update PR description or code to match.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 472

Comment:
PR description says tooltip max-height is `min(80dvh,560px)` but code has `720px`

The "更新" section states: "并将 tooltip 列表尽量展开(`max-height: min(80dvh,560px)`)" but implementation uses 720px. Update PR description or code to match.

How can I resolve this? If you propose a fix, please make it concise.

ref={cardRef}
className={cn(
"flex flex-col p-0 overflow-hidden max-h-[var(--cch-viewport-height-50)]",
"p-0 md:p-1 min-h-[300px] max-h-[min(var(--cch-viewport-height-60),720px)] overflow-visible hover:z-20",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overflow-visible allows card content to overflow beyond max-h boundaries, contradicting PR goal "不让卡片本身滚动" (prevent card scrolling). If calculated height constraints fail, card internals (header/legend) will render outside the card.

Use overflow-hidden to enforce containment. Tooltip can still overflow using higher z-index + wrapperStyle positioning (already set on line 459).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 314

Comment:
`overflow-visible` allows card content to overflow beyond `max-h` boundaries, contradicting PR goal "不让卡片本身滚动" (prevent card scrolling). If calculated height constraints fail, card internals (header/legend) will render outside the card.

Use `overflow-hidden` to enforce containment. Tooltip can still overflow using higher `z-index` + `wrapperStyle` positioning (already set on line 459).

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +138 to +139
const nextHeight = Math.max(0, Math.min(maxChartHeight, availableHeight));
setChartHeightPx((prev) => (prev === nextHeight ? prev : nextHeight));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chart can become 0px height at extreme viewport sizes (e.g., 300px viewport with min-h, leaving ~50px for chart after subtracting header/tabs/legend/padding). Math.max(0, ...) allows this. user sees empty space with no feedback.

add minimum threshold:

Suggested change
const nextHeight = Math.max(0, Math.min(maxChartHeight, availableHeight));
setChartHeightPx((prev) => (prev === nextHeight ? prev : nextHeight));
const nextHeight = Math.max(80, Math.min(maxChartHeight, availableHeight));
setChartHeightPx((prev) => (prev === nextHeight ? prev : nextHeight));
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 138-139

Comment:
chart can become 0px height at extreme viewport sizes (e.g., 300px viewport with min-h, leaving ~50px for chart after subtracting header/tabs/legend/padding). `Math.max(0, ...)` allows this. user sees empty space with no feedback.

add minimum threshold:

```suggestion
      const nextHeight = Math.max(80, Math.min(maxChartHeight, availableHeight));
      setChartHeightPx((prev) => (prev === nextHeight ? prev : nextHeight));
```

How can I resolve this? If you propose a fix, please make it concise.

<div className="flex items-center justify-center gap-2 mb-2">
<div ref={legendRef} className="relative px-4 pb-2">
{/* Control buttons (floating, does not take extra vertical space) */}
<div className="absolute right-4 top-0.5 z-10 w-24 flex flex-wrap justify-end gap-x-2 gap-y-0.5">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buttons container is w-24 (96px) with flex-wrap. if i18n button text wraps to second line (e.g., long translations), buttons will expand vertically and overlap user badges below (no gap between button container and line 590).

user list has pr-24 to reserve space, but only accounts for single-line buttons. when buttons wrap, the reserved space becomes insufficient.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 553

Comment:
buttons container is `w-24` (96px) with `flex-wrap`. if i18n button text wraps to second line (e.g., long translations), buttons will expand vertically and overlap user badges below (no gap between button container and line 590). 

user list has `pr-24` to reserve space, but only accounts for single-line buttons. when buttons wrap, the reserved space becomes insufficient.

How can I resolve this? If you propose a fix, please make it concise.

const compute = () => {
const cardStyle = getComputedStyle(card);
const viewportHeightPx = window.visualViewport?.height ?? window.innerHeight;
const fallbackMaxHeightPx = Math.floor(Math.min(viewportHeightPx * 0.6, 720));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fallback multiplies viewport height by 0.6, but CSS uses 60dvh (dynamic viewport height) which excludes browser UI. on mobile with virtual keyboard visible, window.innerHeight includes keyboard area while dvh doesn't, causing 50-100px+ mismatch between calculated fallback and actual CSS max-height.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 100

Comment:
fallback multiplies viewport height by 0.6, but CSS uses `60dvh` (dynamic viewport height) which excludes browser UI. on mobile with virtual keyboard visible, `window.innerHeight` includes keyboard area while `dvh` doesn't, causing 50-100px+ mismatch between calculated fallback and actual CSS max-height.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +165 to +194
React.useEffect(() => {
const chartWrapper = chartWrapperRef.current;
if (!chartWrapper) return;

const handleWheel = (event: WheelEvent) => {
const scrollContainer = tooltipScrollRef.current;
if (!scrollContainer) return;

const maxScrollTop = scrollContainer.scrollHeight - scrollContainer.clientHeight;
if (maxScrollTop <= 0) return;

const deltaY =
event.deltaMode === 1
? event.deltaY * 16
: event.deltaMode === 2
? event.deltaY * 240
: event.deltaY;
if (!Number.isFinite(deltaY) || deltaY === 0) return;

const current = scrollContainer.scrollTop;
const next = Math.min(maxScrollTop, Math.max(0, current + deltaY));
if (next === current) return;

scrollContainer.scrollTop = next;
event.preventDefault();
};

chartWrapper.addEventListener("wheel", handleWheel, { passive: false });
return () => chartWrapper.removeEventListener("wheel", handleWheel);
}, []);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wheel handler registers with passive: false on mount and persists for component lifetime. if tooltip never appears (single user mode or no hover), handler still evaluates on every wheel event.

minor performance impact: ~6 checks per scroll gesture. consider adding enableUserFilter guard in effect dependency to skip handler registration entirely when legend is disabled.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 165-194

Comment:
wheel handler registers with `passive: false` on mount and persists for component lifetime. if tooltip never appears (single user mode or no hover), handler still evaluates on every wheel event.

minor performance impact: ~6 checks per scroll gesture. consider adding `enableUserFilter` guard in effect dependency to skip handler registration entirely when legend is disabled.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

<div className="flex items-center justify-center gap-2 mb-2">
<div ref={legendRef} className="relative px-4 pb-2">
{/* Control buttons (floating, does not take extra vertical space) */}
<div className="absolute right-4 top-0.5 z-10 w-24 flex flex-wrap justify-end gap-x-2 gap-y-0.5">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right-4 won't flip for RTL languages - buttons stay on wrong side

Use logical property end-4 instead:

Suggested change
<div className="absolute right-4 top-0.5 z-10 w-24 flex flex-wrap justify-end gap-x-2 gap-y-0.5">
<div className="absolute end-4 top-0.5 z-10 w-24 flex flex-wrap justify-end gap-x-2 gap-y-0.5">
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 554

Comment:
`right-4` won't flip for RTL languages - buttons stay on wrong side

Use logical property `end-4` instead:

```suggestion
          <div className="absolute end-4 top-0.5 z-10 w-24 flex flex-wrap justify-end gap-x-2 gap-y-0.5">
```

How can I resolve this? If you propose a fix, please make it concise.

{/* User list with max 3 rows and scroll - only show users with non-zero usage */}
<div className="max-h-[72px] overflow-y-auto scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent">
{/* User list with max 3 rows (3 * 24px = 72px) and scroll - only show users with non-zero usage */}
<div className="max-h-[72px] overflow-y-auto pr-24">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pr-24 reserves space on right side only, breaks in RTL

Use logical property pe-24:

Suggested change
<div className="max-h-[72px] overflow-y-auto pr-24">
<div className="max-h-[72px] overflow-y-auto pe-24">
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 591

Comment:
`pr-24` reserves space on right side only, breaks in RTL

Use logical property `pe-24`:

```suggestion
          <div className="max-h-[72px] overflow-y-auto pe-24">
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +150 to +158
const observer = new ResizeObserver(compute);
observer.observe(card);
observer.observe(header);
observer.observe(metricTabs);
observer.observe(chartWrapper);
if (enableUserFilter && legendRef.current) {
observer.observe(legendRef.current);
}
window.addEventListener("resize", compute);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redundant window resize listener when ResizeObserver is available

line 158 adds window resize listener even when ResizeObserver is defined (line 145 check). since ResizeObserver already tracks size changes of observed elements, the window listener is redundant in modern browsers. consider removing window listener when ResizeObserver is available:

Suggested change
const observer = new ResizeObserver(compute);
observer.observe(card);
observer.observe(header);
observer.observe(metricTabs);
observer.observe(chartWrapper);
if (enableUserFilter && legendRef.current) {
observer.observe(legendRef.current);
}
window.addEventListener("resize", compute);
const observer = new ResizeObserver(compute);
observer.observe(card);
observer.observe(header);
observer.observe(metricTabs);
observer.observe(chartWrapper);
if (enableUserFilter && legendRef.current) {
observer.observe(legendRef.current);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 150-158

Comment:
redundant window resize listener when ResizeObserver is available

line 158 adds window resize listener even when ResizeObserver is defined (line 145 check). since ResizeObserver already tracks size changes of observed elements, the window listener is redundant in modern browsers. consider removing window listener when ResizeObserver is available:

```suggestion
    const observer = new ResizeObserver(compute);
    observer.observe(card);
    observer.observe(header);
    observer.observe(metricTabs);
    observer.observe(chartWrapper);
    if (enableUserFilter && legendRef.current) {
      observer.observe(legendRef.current);
    }
```

How can I resolve this? If you propose a fix, please make it concise.

const compute = () => {
const cardStyle = getComputedStyle(card);
const viewportHeightPx = window.visualViewport?.height ?? window.innerHeight;
const fallbackMaxHeightPx = Math.floor(Math.min(viewportHeightPx * 0.6, 720));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Math.floor before Math.min reduces readability

flooring viewportHeightPx * 0.6 before comparing with 720 is less clear than Math.min(Math.floor(viewportHeightPx * 0.6), 720). current code: viewport 1199px → floor(719.4) = 719 → min(719, 720) = 719. suggested rewrite produces identical result but more clearly shows "60% of viewport, capped at 720px, rounded down"

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 100

Comment:
Math.floor before Math.min reduces readability

flooring `viewportHeightPx * 0.6` before comparing with 720 is less clear than `Math.min(Math.floor(viewportHeightPx * 0.6), 720)`. current code: viewport 1199px → floor(719.4) = 719 → min(719, 720) = 719. suggested rewrite produces identical result but more clearly shows "60% of viewport, capped at 720px, rounded down"

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link

greptile-apps bot commented Feb 27, 2026

Additional Comments (1)

src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
missing type="button" attributes on all interactive buttons

while not immediately problematic outside forms, explicit type="button" prevents unintended form submissions if this component is later used within a form context. affects buttons on lines 329, 337, 351, 372, 389, 555, 567, 604

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 329-343

Comment:
missing `type="button"` attributes on all interactive buttons

while not immediately problematic outside forms, explicit `type="button"` prevents unintended form submissions if this component is later used within a form context. affects buttons on lines 329, 337, 351, 372, 389, 555, 567, 604

How can I resolve this? If you propose a fix, please make it concise.

// - 但 vh/dvh 上限变大后,需要让图表高度回弹
// 同时监听 visualViewport,覆盖移动端地址栏/键盘导致的可视区域变化。
visualViewport?.addEventListener("resize", compute);
visualViewport?.addEventListener("scroll", compute);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

visualViewport scroll listener is unnecessary

visualViewport scroll events fire in specific mobile scenarios (e.g., keyboard appearance), but scrolling shouldn't require chart height recalculation. The resize event on line 155 is sufficient.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 156

Comment:
`visualViewport` scroll listener is unnecessary

`visualViewport` scroll events fire in specific mobile scenarios (e.g., keyboard appearance), but scrolling shouldn't require chart height recalculation. The resize event on line 155 is sufficient.

How can I resolve this? If you propose a fix, please make it concise.

ref={cardRef}
className={cn(
"flex flex-col p-0 overflow-hidden max-h-[var(--cch-viewport-height-50)]",
"p-0 md:p-1 min-h-[300px] max-h-[min(var(--cch-viewport-height-60),720px)] overflow-visible hover:z-20",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hover:z-20 elevates entire card above adjacent elements on hover

while intended to help tooltip overflow, this creates z-index stacking issues - the card will appear above nearby cards/content when hovered, potentially causing visual disruption. tooltip already has zIndex: 1000 (line 470), which should be sufficient.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
Line: 325

Comment:
`hover:z-20` elevates entire card above adjacent elements on hover

while intended to help tooltip overflow, this creates z-index stacking issues - the card will appear above nearby cards/content when hovered, potentially causing visual disruption. tooltip already has `zIndex: 1000` (line 470), which should be sufficient.

How can I resolve this? If you propose a fix, please make it concise.

@tesgth032
Copy link
Contributor Author

本轮根据 AI review 的性能建议做了小幅收敛:

  • ResizeObserver 不再观察 card/chartWrapper(避免 chartHeightPx 自身变化引发的重复 compute),仅观察 header/tabs/legend;保留 window.resize + visualViewport.resize 以覆盖 vh/dvh 上限变化时的回弹需求。
  • 同时把 fallbackMaxHeightPx 写法调整为 min(floor(60% viewport), 720),可读性更好。

已在本地重新跑完:bun run build / bun run lint / bun run lint:fix / bun run typecheck / bun run test,并确认 Actions 全绿。

@tesgth032 tesgth032 changed the title 修复首页统计图表在小高度下裁切 [未完成] 修复首页统计图表在小高度下裁切 Feb 27, 2026
@ding113 ding113 merged commit aa4e03d into ding113:dev Feb 27, 2026
9 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Feb 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:statistics area:UI bug Something isn't working size/S Small PR (< 200 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants