Skip to content

fix(desktop): 优化用量历史图表配色、交互反馈与文字层级 - #4076

Merged
kirozeng merged 7 commits into
makecindy:mainfrom
kirozeng:fix/usage-chart-shapes-and-date-selection
Sep 8, 2026
Merged

fix(desktop): 优化用量历史图表配色、交互反馈与文字层级#4076
kirozeng merged 7 commits into
makecindy:mainfrom
kirozeng:fix/usage-chart-shapes-and-date-selection

Conversation

@kirozeng

@kirozeng kirozeng commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

这次改了什么

摘要

优化用量历史的图表配色、交互反馈和文字层级。本地最后一轮试用修改已同步至本 PR,保留此前颜色分类登记的 CI 修复。

暂不合并: 新增日期入口按所有者要求取消后,密集目标的点击尺寸方案仍未解决。本次同步供评审,不代表已满足 Equivalent 条件或获准 Essential 豁免;同时仍依赖 #4072 的圆角登记规范。此前对旧提交的批准与预览不能代替本轮验证。

变更类型

  • feat 新功能
  • fix 缺陷修复
  • refactor / perf 重构或性能优化
  • docs / test / chore 文档、测试或工程维护
  • 其他:

范围

  • 关联 Issue / 需求:用量历史界面优化;圆角登记依赖 docs(design): 按可见层与登记分配圆角并明确审查边界 #4072,不占用 DS 编号。
  • 本 PR 包含:图表形状与自适应宽度、蓝色热力图、模型配色、悬停微弹、选中与范围强调、字号字重、规范及回归测试。
  • 明确不包含:统计口径、数据库、IPC、全局字体设置或其他页面配色调整。
  • 用户可见变化:详见下表。原有时间范围预设、图表按日筛选、固定图表窗口及明细统计保留。
  • 是否存在 breaking change:无。

UI 变化

  • 引用的设计规范:docs/design-rules/DESIGN.md §2/§10 语义配色、§3 字号字重、§5 登记圆角、§14.4 局部反馈;docs/design-rules/usage-history-charts.mddesign-governance.md §6/§7。
  • 设计风险类别:有意可见变化
优化项目 当前实现
图表形状 热力格和柱体四角 2px;热力格 12px、间距 3px;柱图自适应宽度,无 807px 下限
配色 蓝色强度热力图;五种模型分类色与明细表同步,“其它”保持中性
放大反馈 热力格扩大至 14px;柱体仅宽度增加 2px,柱高和基线不变;减少动态效果时立即切换
热力格选中 保持放大,增加 Light 深色 / Dark 浅色的 1px 描边与 1px 间隙
柱图强调 最近 7 天强调对应七根柱;单日优先;图外日期无柱选中;其他柱保留 60% 不透明度
文字层级 页面标题和概览 16px/500、分区标题 14px/500、正文与表格 13px、辅助信息 12px、图表注释 11px
日期入口 取消本 PR 中间版本新增的日期输入及“查看当天”,保留原有范围下拉和图表点击
  • 默认无单日选中;不增加点击空白或重复点击取消功能。独立键盘焦点保留。
  • 平台:macOS arm64 / Electron 41.10.3 / Global 隔离开发版,Light/Dark 均实测。
  • 当前证据索引:docs/design-evidence/2026-09-08/usage-chart-shapes.md 的“当前同步版本”。真实截图及实测 JSON 以 sync- 开头,保存在本地忽略目录;公开附件待上传。评论中的旧 HTML 近似预览不代表本轮 UI。
  • 当前代码提交:fea5c7b7890a2fb99bc3338060a2e8acef63abec

怎么验证的

自动验证

pnpm test:unit:related
通过:test runner、Desktop 与 design-tokens。

pnpm --filter desktop run --if-present typecheck
pnpm --filter @cindy/design-tokens run --if-present typecheck
均通过。

pnpm check:dco
提交前、提交后通过;当前 5 个提交均带 Signed-off-by。

pnpm design:inventory
pnpm exec prettier --check <本轮修改的 TS/TSX/CSS>
git diff --check
通过。

pnpm check:i18n
pnpm check:i18n-glossary
通过;保留既有警告,无新增违规。

按所有者取消新增日期入口的要求,将旧表单存在性断言更新为保留两张图表的日期选择路径,并验证页面不再显示日期输入。仍覆盖相同日期筛选结果、默认状态、重复与空白点击、零值/历史日、固定图表数据,以及七日范围跨月、单日优先和图外日期。行为测试通过不代表点击尺寸合规。

手工验证

真实隔离 Electron 的 Light/Dark 均验证:默认无选中、七个日历日强调、单日联动、图外日期无柱选中、60% 不透明度、热力格描边与间隙、2px 键盘焦点,以及选择前后柱高不变。

两种模式均确认概览 16px/500;临时按 UI 字号 14→18 放大本页 Token,概览计算为 21px且无水平溢出,测试后恢复。数据为六组模型、150 天合成用量,未修改正式数据。GPT-6 已自审最终差异,按所有者授权未运行双审。

未执行的验证

  • 点击尺寸方案的最终合规验收:尚未解决,合入前必须收口。
  • 最终公开实机附件与设计师视觉验收记录:待完成;本地截图不是公开附件。
  • Windows/Linux 实机、屏幕阅读器、发布构建:本轮本地环境为 macOS;远端 CI 在新提交上重跑。
  • 任务明细表本轮未作实机目检:合成环境没有任务数据,源码和类型已检查。
  • pr-autopilot 双审与自动跟踪回执:本轮未执行,不作完成声明。

风险

风险分类

  • 无已知风险
  • SQLite / migration
  • system prompt
  • 协议兼容
  • 权限 / 安全 / 用户数据
  • 存量插件兼容(批准状态 / 指纹 / manifest 校验 / 安装布局 / 包格式)
  • 原生层 / fingerprint / OTA
  • 跨平台差异
  • 其他:有意可见变化;点击尺寸和最终证据未收口,暂不合并。

影响与回滚

  • 影响范围:Desktop 用量历史、共享热力图的基础圆角与新增图表颜色登记。配色、排版和微弹限定在用量页;无 Mobile 原生或插件改动。
  • 回滚 / 降级方式:整体回退本 PR 的界面优化;密集目标与其等价操作方案必须一起处理。不得仅凭测试通过将当前命中尺寸缺口记为已解决。

提交前检查

  • 已 review 完整 diff,并明确记录未解决的点击尺寸问题
  • 每个 commit 都带 DCO 签名
  • UI 改动已在「UI 变化」注明引用的设计规范章节
  • 未提交凭证、令牌或授权文件
  • 已补充必要文档
  • 已确认测试结果或说明未执行原因

Signed-off-by: kirozeng <kirozeng@gmail.com>
Signed-off-by: kirozeng <kirozeng@gmail.com>
@kirozeng kirozeng changed the title fix(desktop): 按裁决修复用量图表形状与日期选择 fix(desktop): 按裁决修复用量图表形状、配色与日期交互 Sep 7, 2026
Signed-off-by: kirozeng <kirozeng@gmail.com>
@kirozeng kirozeng changed the title fix(desktop): 按裁决修复用量图表形状、配色与日期交互 fix(desktop): 优化用量历史图表配色、交互反馈与文字层级 Sep 7, 2026
@kirozeng
kirozeng marked this pull request as ready for review September 7, 2026 19:50
@kirozeng
kirozeng requested a review from a team as a code owner September 7, 2026 19:50
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@greptile-apps

greptile-apps Bot commented Sep 7, 2026

Copy link
Copy Markdown

Greptile Summary

本 PR 优化 Desktop 用量历史图表的形状、自适应布局、模型分类配色和交互反馈,并新增等价的单日日期筛选入口。

  • 热力图和每日 Token 柱采用已登记的 2px 圆角及悬停、焦点、选中反馈。
  • 五个主要模型使用稳定的分类色,图表柱段与模型明细表保持一致,其余模型使用中性灰。
  • 新增原生日期输入,使用户能够通过图表、柱图或表单进入同一单日筛选路径。
  • 补充 Light/Dark 主题 Token、国际化文本、设计规范、证据记录和交互测试。

Confidence Score: 5/5

该 PR 看起来可以安全合并,未发现由当前提交引入的具体功能、安全或规则问题。

日期筛选、图表窗口、模型颜色映射、响应式布局及交互状态之间的契约均保持一致,新增测试覆盖了主要选择路径和视觉状态。

Important Files Changed

Filename Overview
apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx 汇总日期选择、稳定模型颜色顺序和两类图表的共享筛选路径,未发现行为缺陷。
apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx 恢复 12px 密集网格并加入独立图元放大与焦点指示,保留非交互分支和横向滚动。
apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx 将 30 根柱自适应到可用宽度,并统一模型配色、选中状态和键盘语义。
apps/desktop/src/renderer/components/settings/usage/UsageDateFilter.tsx 新增受 todayKey 约束、显式提交且与图表筛选同步的原生日期入口。
apps/desktop/src/renderer/components/settings/usage/usageCharts.css 集中实现悬停、焦点、选中淡化及减少动态效果规则,状态范围与设计规范一致。
apps/desktop/src/renderer/themes/colors.ts 注册用量热力图和五个模型类别的 Light/Dark 语义 Token,现有主题注册机制可正确解析。

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  H[热力图日期格] --> S[handleDayClick]
  B[每日 Token 柱] --> S
  D[日期输入与提交] --> S
  S --> R["day:YYYY-MM-DD 范围"]
  R --> O[概览与明细重聚合]
  R --> A[图表 aria-pressed 与视觉反馈]
  F[固定图表历史窗口] --> H
  F --> B
Loading

Reviews (1): Last reviewed commit: "fix(desktop): 按参考图微调用量配色并取消选中外框" | Re-trigger Greptile

本 PR 给 colors.ts 与冻结快照新增了 usage-heatmap-high 和 usage-model-1..5
共 6 个 token,但没跑 design-tokens 生成器,classification.json 仍停在
535 条,「磁盘上的影子层与内存生成字节一致」守卫在 Linux/Windows unit
tests 的同一断言上红灯。

执行 pnpm --filter @cindy/design-tokens generate 再生成影子层:

- snapshotCount 535 → 541,literal +5(usage-model-1..5)、alias +1
  (usage-heatmap-high)
- 不改任何颜色取值;reference/semantic/component 三层与现状字节一致

Signed-off-by: Kirozeng <kirozeng@gmail.com>
@kirozeng

kirozeng commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

本轮修复:CI 红灯是 design-tokens 影子层没跟着新 token 再生成,已补上

失败在哪:Linux unit tests (1/2) 和 Windows unit tests (1/2) 挂在同一个断言——packages/design-tokens 的 classification.test.ts「磁盘上的影子层与内存生成字节一致」。verify 和那个 4 秒的 Windows unit tests 只是分片的聚合门,不是独立故障。两个平台红在同一处,不是 Windows 特有问题。

为什么挂:本 PR 给 colors.ts 和冻结快照 desktop-color-defaults.json 新增了 usage-heatmap-high 和 usage-model-1..5 共 6 个 token,但没有跑 design-tokens 生成器,classification.json 还停在 535 条,跟不上快照的 541 条。

修复(779f9f47b):跑 pnpm --filter @cindy/design-tokens generate 再生成影子层。snapshotCount 535→541,literal +5(usage-model-1..5)、alias +1(usage-heatmap-high)。不改任何颜色取值;reference/semantic/component 三层与之前字节一致。

证据

  • packages/design-tokens 单测 40/40 全绿,包含之前红灯的 classification.test.ts
  • 仓库根 pnpm test:unit:related 通过(test:runner / apps/desktop / packages/design-tokens 全 PASS)
  • pnpm --filter @cindy/design-tokens run typecheck 通过

CI 正在新 head 上重跑,等结果。

另外两条评论无需处理:Codex 那条是账号用量限额通知;Greptile 摘要结论 5/5、没有待办项。

dispatch:5259cbde02e1fa05

@MagicLizi MagicLizi added status:awaiting-bot-review 等外部审查机器人表态(review-pr 自动维护,仅展示) touches:core 改动碰到架构核心路径(review-pr 自动维护,仅展示) touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示) touches:rules 改动碰到规则 / 规范文档(review-pr 自动维护,仅展示) labels Sep 7, 2026
@MagicLizi MagicLizi removed the status:awaiting-bot-review 等外部审查机器人表态(review-pr 自动维护,仅展示) label Sep 7, 2026
@MagicLizi

Copy link
Copy Markdown
Contributor

命中 UI 路径(apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx / apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx / apps/desktop/src/renderer/components/settings/usage/UsageDateFilter.tsx 等)但 description 未附界面效果证据——建议补充改动后效果:截图/录屏,或改动后界面的 HTML 页面(```html 代码块、.html 附件或在线预览链接),便于确认界面符合 DESIGN.md 设计规范

@kirozeng

kirozeng commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

本轮核实:无需代码改动,CI 已全绿

这条「Windows unit tests 缺席」的信号来自上轮推送后 CI 刚启动的瞬间——当时聚合门还没在新 head 上登记,不是故障。现在 run 34158598361 已出终局:

  • Windows unit tests (1/2) / (2/2) 与聚合门全 pass。上一轮的 design-tokens 影子层再生成(779f9f47b)生效,之前红灯的断言不再失败
  • Linux unit tests 两分片、verify、verify-checks、Desktop Git integration、DCO、check:pr-design-basis 全 pass

dispatch:24087dfa4bb0b004

@kirozeng

kirozeng commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

本轮针对 UI 证据提醒:补一份可直接打开的改动后界面预览(HTML)

为什么用 HTML 预览:真实 macOS Electron 的 Light/Dark 截图与录屏此前已采集,但一直存放在本地忽略目录、尚未上传公开附件(PR 描述与 docs/design-evidence/2026-09-08/usage-chart-shapes.md 已声明这一点)。当前检出的工作区里那份本地附件已不在(开发 worktree 已删除)。为不阻塞设计确认,先按提醒接受的另一种形式——改动后界面的 HTML 页面——把证据补上;若仍要公开实机截图,需要重新采集。

这份预览的构成(值全部来自 779f9f4 的源码,不是设计稿):

  • 颜色 token 逐值取自 apps/desktop/src/renderer/themes/colors.ts
  • 图表交互 CSS 为 usageCharts.css 逐字嵌入(悬停/选中 +2px、非选中柱 24% 不透明度、焦点环、prefers-reduced-motion)
  • 组件几何与 UsageHeatmap / UsageTokenBars / UsageDateFilter / UsageBreakdownTables 一致:热力格 12px、间隙 3px、四角 2px;柱图 96px 高、30 柱;日期输入 190×36
  • 数据是合成 fixture(六组模型、150 天、约 13% 零值日),非真实用量;输入框/按钮等页面级控件为近似还原

已验证(headless Chrome 实测计算样式,无脚本错误):

  • Light/Dark 两面板各 30 柱、23 列 × 7 行热力网格;热力格 12×12、圆角 2px
  • 五个模型类别色与 colors.ts 逐值一致:Light #14B8A6 #F43F5E #8B5CF6 #6366F1 #F59E0B,Dark #2DD4BF #FB7185 #A78BFA #818CF8 #FBBF24;「其它」为中性灰(text-tertiary),热力蓝 Light #2563EB / Dark #60A5FA,零值格为 surface-chip
  • 点击柱体:该柱宽 +2px(实测 17.89→19.91px)、非选中柱不透明度 0.24、日期输入同步为该天、两张明细表范围标签变为该日(如「8月12日 token」);热力格与日期表单是等价入口,Light/Dark 面板共享同一条选择

怎么用:把下面代码块存成 .html 用浏览器打开(无外部依赖)。悬停、点击、Tab 焦点、选日期提交都能试,Light 与 Dark 并排同数据对比。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PR #4076 · 用量历史图表改动后界面预览(Light / Dark)</title>
<style>
  /* ============================================================
     PR #4076 @ 779f9f47b 用量历史图表 · 界面效果预览
     - token 取自 apps/desktop/src/renderer/themes/colors.ts(真实值;
       usage-heatmap-high 按 colors.ts 的注册方式别名为 process-agent-task-icon)
     - 图表 CSS 为 usageCharts.css 逐字嵌入(真实规则)
     - 组件几何取自 UsageHeatmap / UsageTokenBars / UsageDateFilter /
       UsageBreakdownTables(12px 热力格、3px 间隙、96px 柱高、2px 圆角等)
     - 数据为合成 fixture(六组模型、150 天、约 13% 零值日),非真实用量
     - 页面级控件(输入框/按钮圆角、概览卡)为近似还原,非像素级复刻
     ============================================================ */

  /* motion tokens — apps/desktop/src/renderer/styles/globals.css 真实值 */
  :root { --motion-fast: 150ms; --motion-base: 200ms; --motion-ease-out: cubic-bezier(0.16, 1, 0.3, 1); }
  @media (prefers-reduced-motion: reduce) { :root { --motion-fast: 0ms; --motion-base: 0ms; } }

  /* 主题 token — colors.ts 真实值(Light / Dark) */
  .theme-light {
    color-scheme: light;
    --surface: #f8f8f6; --surface-elevated: #ffffff; --surface-chip: #e5e5e5;
    --border-default: #d7d7d4; --text-primary: #262626; --text-secondary: #737373;
    --text-tertiary: #a3a3a3; --focus-ring: #417CDD; --accent-emphasis: #262626;
    --process-agent-task-icon: #2563EB;
    --usage-heatmap-high: var(--process-agent-task-icon);
    --usage-model-1: #14B8A6; --usage-model-2: #F43F5E; --usage-model-3: #8B5CF6;
    --usage-model-4: #6366F1; --usage-model-5: #F59E0B;
  }
  .theme-dark {
    color-scheme: dark;
    --surface: #1f1f1e; --surface-elevated: #2c2c2a; --surface-chip: #3c3c3a;
    --border-default: #3c3c3a; --text-primary: #d4d4d4; --text-secondary: #a3a3a3;
    --text-tertiary: #737373; --focus-ring: #417CDD; --accent-emphasis: #d4d4d4;
    --process-agent-task-icon: #60A5FA;
    --usage-heatmap-high: var(--process-agent-task-icon);
    --usage-model-1: #2DD4BF; --usage-model-2: #FB7185; --usage-model-3: #A78BFA;
    --usage-model-4: #818CF8; --usage-model-5: #FBBF24;
  }

  /* ===== usageCharts.css @ 779f9f47b — 逐字嵌入 ===== */
  .usage-history-charts {
    --usage-heatmap-accent: var(--usage-heatmap-high);
    --usage-mark-ease: cubic-bezier(0.2, 0.8, 0.25, 1.18);
  }
  .usage-history-charts .usage-chart-target { --usage-mark-grow: 0px; }
  .usage-history-charts .usage-chart-target .usage-chart-mark { pointer-events: none; }
  .usage-history-charts .usage-chart-mark {
    transition-property: width, height, opacity;
    transition-duration: var(--motion-fast);
    transition-timing-function: var(--motion-ease-out);
  }
  .usage-history-charts .usage-chart-target:is(:focus-visible, [aria-pressed='true']) {
    --usage-mark-grow: 2px; z-index: 1;
  }
  .usage-history-charts .usage-chart-target:is(:focus-visible, [aria-pressed='true']) .usage-chart-mark {
    transition-duration: var(--motion-base);
    transition-timing-function: var(--usage-mark-ease);
  }
  @media (hover: hover) {
    .usage-history-charts .usage-chart-target:enabled:hover { --usage-mark-grow: 2px; z-index: 1; }
    .usage-history-charts .usage-chart-target:enabled:hover .usage-chart-mark {
      transition-duration: var(--motion-base);
      transition-timing-function: var(--usage-mark-ease);
    }
  }
  .usage-history-charts .usage-token-plot:has(.usage-chart-target[aria-pressed='true'])
    .usage-chart-target:not([aria-pressed='true']):not(:focus-visible) .usage-chart-mark { opacity: 0.24; }
  @media (hover: hover) {
    .usage-history-charts .usage-token-plot:has(.usage-chart-target[aria-pressed='true'])
      .usage-chart-target:enabled:hover .usage-chart-mark { opacity: 1; }
  }
  .usage-history-charts .usage-chart-target:focus-visible .usage-chart-indicator {
    outline: 2px solid var(--focus-ring);
    outline-offset: 1px;
  }
  @media (prefers-reduced-motion: reduce) {
    .usage-history-charts .usage-chart-mark { transition: none; }
  }
  /* ===== usageCharts.css 嵌入结束 ===== */

  /* ===== 页面还原样式(对应组件 Tailwind 类的几何/排版结果) ===== */
  body { margin: 0; background: #494947; font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; }
  .doc { max-width: 1340px; margin: 0 auto; padding: 20px 16px 8px; color: #f0f0ee; font-size: 12px; line-height: 1.8; }
  .doc h1 { font-size: 15px; margin: 0 0 6px; }
  .doc code { background: rgb(255 255 255 / 0.12); padding: 0 4px; border-radius: 3px; }
  .panels { display: flex; flex-wrap: wrap; gap: 16px; max-width: 1340px; margin: 0 auto; padding: 8px 16px 32px; align-items: flex-start; }
  .panel { flex: 1 1 600px; max-width: 660px; background: var(--surface); color: var(--text-primary);
           border-radius: 8px; padding: 20px 24px 24px; box-shadow: 0 4px 16px rgb(0 0 0 / 0.25); }
  .panel-tag { font-size: 11px; font-weight: 600; letter-spacing: 0.08em; color: var(--text-tertiary); margin-bottom: 10px; }
  .page-h { font-size: 15px; font-weight: 600; margin: 0 0 6px; }
  .page-desc { font-size: 12px; line-height: 1.7; color: var(--text-tertiary); margin: 0 0 16px; max-width: 560px; }

  /* 日期筛选(UsageDateFilter:label 12px medium,输入 190×36,按钮 36px) */
  .date-filter { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 16px; }
  .date-filter label { font-size: 12px; font-weight: 500; color: var(--text-secondary); }
  .date-filter input[type="date"] {
    width: 190px; height: 36px; font-size: 13px; font-family: inherit; color: var(--text-primary);
    background: var(--surface-elevated); border: 1px solid var(--border-default); border-radius: 9999px;
    padding: 0 16px; outline: none;
  }
  .date-filter input[type="date"]:focus { border-color: var(--text-tertiary); box-shadow: 0 0 0 2px var(--focus-ring); }
  .date-filter .apply {
    height: 36px; padding: 0 24px; font-size: 13px; font-weight: 500; font-family: inherit; cursor: pointer;
    color: var(--text-primary); background: var(--surface-elevated); border: 1px solid var(--border-default);
    border-radius: 9999px;
  }
  .date-filter .apply:hover { background: color-mix(in srgb, var(--surface-elevated) 92%, var(--text-primary)); }
  .date-filter .apply:disabled { opacity: 0.5; cursor: default; }
  .date-filter .preview-only { font-size: 11px; color: var(--text-tertiary); background: none; border: none;
    cursor: pointer; text-decoration: underline dotted; font-family: inherit; }

  .summary { display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 18px; padding: 10px 14px;
             background: var(--surface-elevated); border: 1px solid var(--border-default); border-radius: 8px; }
  .summary .stat { min-width: 90px; }
  .summary .k { font-size: 11px; color: var(--text-tertiary); margin-bottom: 3px; }
  .summary .v { font-size: 16px; font-weight: 500; font-variant-numeric: tabular-nums; }

  .section-h { display: flex; align-items: baseline; gap: 8px; margin: 18px 0 10px; }
  .section-h .t { font-size: 12px; font-weight: 500; color: var(--text-secondary); }
  .section-h .s { font-size: 11px; color: var(--text-tertiary); }

  /* 热力图(UsageHeatmap:12px 格、3px 间隙、月份标签行 14px 高) */
  .hm-wrap { width: 100%; overflow-x: auto; }
  .hm-inner { display: flex; flex-direction: column; gap: 6px; padding: 3px; width: max-content; }
  .hm-months { position: relative; height: 14px; }
  .hm-months span { position: absolute; top: 0; white-space: nowrap; font-size: 10px; line-height: 1.4; color: var(--text-tertiary); }
  .hm-grid { display: flex; gap: 3px; }
  .hm-col { display: flex; flex-direction: column; gap: 3px; }
  .usage-chart-target { display: flex; align-items: center; justify-content: center; background: transparent;
    border: 0; padding: 0; cursor: pointer; outline: none; position: relative; font-family: inherit; }
  .hm-cell-mark { width: calc(12px + var(--usage-mark-grow, 0px)); height: calc(12px + var(--usage-mark-grow, 0px)); border-radius: 2px; flex-shrink: 0; }
  .usage-chart-indicator { position: absolute; inset: 0; border-radius: 2px; pointer-events: none; }

  /* 每日 token 柱图(UsageTokenBars:96px 高、3px 间隙、Y 轴 30px、参考线半透明) */
  .bars-row { display: flex; gap: 6px; height: 96px; }
  .y-axis { position: relative; width: 30px; flex-shrink: 0; }
  .y-axis span { position: absolute; right: 0; transform: translateY(50%); font-size: 10px; line-height: 1;
                 color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
  .bars-plot-wrap { position: relative; min-width: 0; flex: 1; }
  .gridline { position: absolute; left: 0; right: 0; border-top: 1px solid var(--border-default); opacity: 0.5; }
  .usage-token-plot { position: absolute; inset: 0; display: flex; align-items: flex-end; gap: 3px; }
  .usage-token-plot .usage-chart-target { flex: 1 1 0; min-width: 0; align-items: flex-end; justify-content: center; }
  .bar-mark { display: flex; flex-direction: column; overflow: hidden; border-radius: 2px; flex-shrink: 0;
              width: calc(100% + var(--usage-mark-grow, 0px)); }
  .bar-mark span { display: block; }

  /* 明细表(UsageBreakdownTables:th 11px medium tertiary、td 12px tabular、边框分割) */
  table { width: 100%; border-collapse: collapse; }
  th { white-space: nowrap; border-bottom: 1px solid var(--border-default); padding: 0 0 8px 12px;
       text-align: right; font-size: 11px; font-weight: 500; color: var(--text-tertiary); }
  th:first-child, td:first-child { padding-left: 0; text-align: left; width: 100%; max-width: 0; }
  td { white-space: nowrap; border-bottom: 1px solid var(--border-default); padding: 8px 0 8px 12px;
       text-align: right; font-size: 12px; font-variant-numeric: tabular-nums; }
  td.muted, .share-cell { color: var(--text-tertiary); }
  .cell-main { display: flex; min-width: 0; align-items: center; gap: 8px; }
  .cell-main .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .swatch { width: 8px; height: 8px; flex-shrink: 0; border-radius: 2px; }
  .agent-tag { flex-shrink: 0; border: 1px solid var(--border-default); border-radius: 4px; padding: 1px 4px;
               font-size: 10px; line-height: 1.2; color: var(--text-tertiary); }
  .share-cell { display: flex; align-items: center; justify-content: flex-end; }
  .share-bar { display: inline-block; height: 4px; border-radius: 2px; margin-right: 6px; }
  .agent-sharebar { display: flex; height: 8px; overflow: hidden; border-radius: 2px; margin-bottom: 12px; }
  .hint { font-size: 11px; color: var(--text-tertiary); margin-top: 8px; }
</style>
</head>
<body>
<header class="doc">
  <h1>PR #4076 · 用量历史图表改动后界面预览(Light / Dark 同数据同布局)</h1>
  颜色 token 取自 <code>apps/desktop/src/renderer/themes/colors.ts</code>(commit <code>779f9f47b</code>);图表交互 CSS 为
  <code>usageCharts.css</code> 逐字嵌入;热力格 12px、柱图 96px 高 / 2px 圆角 / 悬停与选中 +2px、非选中柱 24% 不透明度等几何与
  <code>UsageHeatmap</code> / <code>UsageTokenBars</code> / <code>UsageDateFilter</code> / <code>UsageBreakdownTables</code> 一致。
  数据为合成 fixture(六组模型、150 天、含零值日)。悬停、点击、Tab 焦点、日期提交均可试;两个面板共享同一条日期选择。
  输入框 / 按钮等页面级控件为近似还原。
</header>
<main class="panels">
  <section class="panel theme-light usage-history-charts" data-panel="light"></section>
  <section class="panel theme-dark usage-history-charts" data-panel="dark"></section>
</main>

<script>
'use strict';
/* ---------- 与生产同源的常量/算法(拷贝自组件源码) ---------- */
var CELL_PX = 12, GAP_PX = 3, LEVEL_MIX = [0.22, 0.42, 0.68, 1], CHART_HEIGHT_PX = 96;
var RANK_MIX = [1, 0.75, 0.55, 0.4, 0.3, 0.18];
var TODAY = '2026-09-07';                       /* todayKey 锚点(renderer 不自取系统日期) */
var HEATMAP_WEEKS = 23;                          /* resolveHeatmapWeeks(windowDays=150) 的结果 */

function fmtTokens(n) {                          /* maker-shared usageFormat.ts 同口径 */
  if (n < 1000) return String(n);
  var units = [[1e3, 'k'], [1e6, 'M'], [1e9, 'B']];
  for (var i = 0; i < units.length; i++) { var v = n / units[i][0]; if (v < 999.95) return v.toFixed(1) + units[i][1]; }
  return (n / 1e9).toFixed(1) + 'B';
}
function niceTicks(max) {                        /* UsageTokenBars 同算法 */
  if (!(max > 0)) return [];
  var rawStep = max / 4, pow = Math.pow(10, Math.floor(Math.log10(rawStep)));
  var step = [1, 2, 2.5, 5, 10].map(function (m) { return m * pow; }).find(function (s) { return s >= rawStep; }) || 10 * pow;
  var ticks = [];
  for (var v = step; v <= max * 0.95 && ticks.length < 3; v += step) ticks.push(v);
  return ticks;
}
function modelColor(rank, colorCount) {          /* usageHistoryColors.ts 同规则 */
  return rank >= 0 && rank < colorCount && rank < 5 ? 'var(--usage-model-' + (rank + 1) + ')' : 'var(--text-tertiary)';
}
function rankColor(rank) {                       /* usagePalette.ts 同规则(Agent 表中性阶梯) */
  return 'color-mix(in srgb, var(--accent-emphasis) ' + RANK_MIX[Math.min(rank, 5)] * 100 + '%, var(--surface-chip))';
}
function parseDay(k) { var p = k.split('-'); return new Date(+p[0], +p[1] - 1, +p[2]); }
function toDayKey(d) { return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); }
function shiftDay(k, n) { var d = parseDay(k); d.setDate(d.getDate() + n); return toDayKey(d); }

/* ---------- 合成 fixture(确定性 PRNG,六组模型 × 150 天,含零值日) ---------- */
function mulberry32(a) { return function () { a |= 0; a = (a + 0x6D2B79F5) | 0; var t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
var rand = mulberry32(20260907);
var MODELS = [
  { key: 'claude-code claude-opus-4-5', name: 'claude-opus-4-5', agent: 'claude-code', w: 0.34 },
  { key: 'codex gpt-5.5', name: 'gpt-5.5', agent: 'codex', w: 0.25 },
  { key: 'claude-code glm-5.3', name: 'glm-5.3', agent: 'claude-code', w: 0.16 },
  { key: 'codex gemini-3-flash', name: 'gemini-3-flash', agent: 'codex', w: 0.11 },
  { key: 'pi kimi-k2.5', name: 'kimi-k2.5', agent: 'pi', w: 0.08 },
  { key: '__others__', name: '其它', agent: '', w: 0.06 }
];
var byDay = {};   /* day -> { total, per: [tokens×6] } */
(function genData() {
  for (var back = 149; back >= 0; back--) {
    var day = shiftDay(TODAY, -back), d = parseDay(day);
    var zero = rand() < 0.13;                     /* 150 天中约 130 天非零,与 fixture 口径一致 */
    var weekday = d.getDay(), weekend = weekday === 0 || weekday === 6 ? 0.5 : 1;
    var trend = 0.55 + 0.45 * (1 - back / 149);   /* 温和增长趋势 */
    var total = zero ? 0 : Math.round((18000 + rand() * 92000) * weekend * trend);
    var per = [], sum = 0, i;
    for (i = 0; i < MODELS.length; i++) { var w = MODELS[i].w * (0.55 + rand() * 0.9); per.push(w); sum += w; }
    for (i = 0; i < MODELS.length; i++) per[i] = total > 0 ? Math.round(total * per[i] / sum) : 0;
    byDay[day] = { total: total, per: per };
  }
})();
var totalsAll = MODELS.map(function (m, i) { return Object.values(byDay).reduce(function (s, d) { return s + d.per[i]; }, 0); });
var colorOrder = MODELS.map(function (m, i) { return i; }).sort(function (a, b) { return totalsAll[b] - totalsAll[a]; }).slice(0, 5);
function rankOf(idx) { var p = colorOrder.indexOf(idx); return p >= 0 ? p : 5; }

/* ---------- 全局共享的日期选择(两面板同一条状态,便于并排对比) ---------- */
var selectedDay = null;
var panelUpdaters = [];
function selectDay(day) { selectedDay = day; panelUpdaters.forEach(function (u) { u(); }); }

/* ---------- 渲染 ---------- */
function el(tag, cls, style, text) {
  var e = document.createElement(tag);
  if (cls) e.className = cls;
  if (style) for (var k in style) e.style[k] = style[k];
  if (text != null) e.textContent = text;
  return e;
}
function sectionH(title, sub) {
  var h = el('div', 'section-h');
  h.appendChild(el('span', 't', null, title));
  if (sub) h.appendChild(el('span', 's', null, sub));
  return h;
}
function renderPanel(root, mode) {
  root.appendChild(el('div', 'panel-tag', null, mode === 'light' ? 'LIGHT 模式' : 'DARK 模式'));
  root.appendChild(el('h2', 'page-h', null, '用量'));
  root.appendChild(el('p', 'page-desc', null, '这台设备上 Cindy 内部产生的 token 用量。记录从该功能上线起积累,不回填更早的历史。'));

  /* 日期筛选(UsageDateFilter) */
  var form = el('form', 'date-filter');
  var label = el('label', null, null, '选择日期');
  var input = el('input'); input.type = 'date'; input.max = TODAY; input.required = true; input.lang = 'zh-CN';
  var apply = el('button', 'apply', null, '查看当天'); apply.type = 'submit';
  var reset = el('button', 'preview-only', null, '重置选择(预览辅助,非产品 UI)'); reset.type = 'button';
  form.appendChild(label); form.appendChild(input); form.appendChild(apply); form.appendChild(reset);
  root.appendChild(form);

  /* 概览 */
  var summary = el('div', 'summary');
  root.appendChild(summary);

  /* 活跃热力图 */
  root.appendChild(sectionH('活跃热力图', '近 ' + HEATMAP_WEEKS + ' 周'));
  var hmWrap = el('div', 'hm-wrap'), hmInner = el('div', 'hm-inner'),
      months = el('div', 'hm-months'), grid = el('div', 'hm-grid');
  hmInner.appendChild(months); hmInner.appendChild(grid); hmWrap.appendChild(hmInner); root.appendChild(hmWrap);

  var today = parseDay(TODAY);
  var start = new Date(today); start.setHours(12, 0, 0, 0); start.setDate(start.getDate() - start.getDay() - (HEATMAP_WEEKS - 1) * 7);
  var keys = [], cursor = new Date(start);
  for (var i = 0; i < HEATMAP_WEEKS * 7; i++) { keys.push(toDayKey(cursor)); cursor.setDate(cursor.getDate() + 1); }
  var nonZero = keys.filter(function (k) { return k <= TODAY && byDay[k] && byDay[k].total > 0; })
                    .map(function (k) { return byDay[k].total; }).sort(function (a, b) { return a - b; });
  function q(p) { return nonZero.length ? nonZero[Math.min(nonZero.length - 1, Math.floor(p * nonZero.length))] : 0; }
  var thresholds = [q(0.25), q(0.5), q(0.75)];
  function levelFor(v) { return v <= 0 ? 0 : v <= thresholds[0] ? 1 : v <= thresholds[1] ? 2 : v <= thresholds[2] ? 3 : 4; }

  var cols = [];
  for (var ci = 0; ci < keys.length; ci += 7) cols.push(keys.slice(ci, ci + 7));
  var lastLabelCol = -9;
  cols.forEach(function (col, idx) {
    var colEl = el('div', 'hm-col');
    col.forEach(function (k) {
      if (k > TODAY) { colEl.appendChild(el('div', null, { width: CELL_PX + 'px', height: CELL_PX + 'px' })); return; }
      var day = byDay[k] || { total: 0 };
      var level = levelFor(day.total);
      var bg = level === 0 ? 'var(--surface-chip)'
        : 'color-mix(in srgb, var(--usage-heatmap-accent) ' + LEVEL_MIX[level - 1] * 100 + '%, var(--surface-chip))';
      var btn = el('button', 'usage-chart-target', { width: CELL_PX + 'px', height: CELL_PX + 'px' });
      btn.type = 'button'; btn.dataset.day = k; btn.setAttribute('aria-pressed', 'false');
      btn.title = k + ' · ' + (day.total > 0 ? fmtTokens(day.total) + ' token' : '当天没有用量记录');
      btn.setAttribute('aria-label', btn.title);
      var mark = el('div', 'usage-chart-mark hm-cell-mark', { backgroundColor: bg });
      mark.setAttribute('data-usage-mark', 'usage-heatmap-day'); mark.title = btn.title;
      btn.appendChild(mark);
      btn.appendChild(el('span', 'usage-chart-indicator'));
      colEl.appendChild(btn);
    });
    grid.appendChild(colEl);
    var firstOfMonth = col.find(function (k) { return k <= TODAY && k.endsWith('-01'); });
    if (firstOfMonth && idx - lastLabelCol >= 2) {
      lastLabelCol = idx;
      var m = parseDay(firstOfMonth);
      months.appendChild(el('span', null, { left: idx * (CELL_PX + GAP_PX) + 'px' }, (m.getMonth() + 1) + '月'));
    }
  });
  months.style.width = cols.length * (CELL_PX + GAP_PX) - GAP_PX + 'px';

  /* 近 30 天每日 token 柱图 */
  root.appendChild(sectionH('近 30 天每日 token'));
  var barsRow = el('div', 'bars-row'), yAxis = el('div', 'y-axis'), plotWrap = el('div', 'bars-plot-wrap'),
      plot = el('div', 'usage-token-plot');
  plotWrap.appendChild(plot); barsRow.appendChild(yAxis); barsRow.appendChild(plotWrap); root.appendChild(barsRow);

  var barDays = []; for (var b = 29; b >= 0; b--) barDays.push(shiftDay(TODAY, -b));
  var barMax = Math.max.apply(null, barDays.map(function (k) { return (byDay[k] || { total: 0 }).total; }));
  var ticks = niceTicks(barMax);
  ticks.forEach(function (v) {
    yAxis.appendChild(el('span', null, { bottom: (v / barMax) * CHART_HEIGHT_PX + 'px' }, fmtTokens(v)));
    plotWrap.appendChild(el('div', 'gridline', { bottom: (v / barMax) * CHART_HEIGHT_PX + 'px' }));
  });
  barDays.forEach(function (k) {
    var day = byDay[k] || { total: 0, per: [] };
    var ratio = barMax > 0 ? day.total / barMax : 0;
    var visual = day.total > 0 ? Math.max(3, Math.round(ratio * CHART_HEIGHT_PX)) : 2;
    var hit = Math.max(24, visual);
    var btn = el('button', 'usage-chart-target', { height: hit + 'px' });
    btn.type = 'button'; btn.dataset.day = k; btn.setAttribute('aria-pressed', 'false');
    btn.title = k + ' · ' + (day.total > 0 ? fmtTokens(day.total) + ' token' : '当天没有用量记录');
    btn.setAttribute('aria-label', btn.title);
    var mark = el('span', 'usage-chart-mark bar-mark', { height: visual + 'px' });
    mark.setAttribute('data-usage-mark', 'usage-token-bar');
    if (day.total === 0) mark.style.backgroundColor = 'var(--surface-chip)';
    var segs = [];
    MODELS.forEach(function (m, idx) { if (day.per[idx] > 0) segs.push({ rank: rankOf(idx), tokens: day.per[idx] }); });
    segs.sort(function (a, b) { return a.rank - b.rank; });
    segs.slice().reverse().forEach(function (s) {   /* rank 0(最大头)渲染在柱底,与组件一致 */
      mark.appendChild(el('span', null, { height: (s.tokens / day.total) * 100 + '%', backgroundColor: modelColor(s.rank, 5) }));
    });
    btn.appendChild(mark);
    btn.appendChild(el('span', 'usage-chart-indicator'));
    plot.appendChild(btn);
  });

  /* 按 Agent / harness 表 + 按模型表(宿主节点先建好,update 时原地重聚合) */
  root.appendChild(sectionH('按 Agent / harness', '近 30 天'));
  var agentHost = el('div'); agentHost.dataset.table = 'agent'; root.appendChild(agentHost);
  root.appendChild(sectionH('按模型', '近 30 天'));
  var modelHost = el('div'); modelHost.dataset.table = 'model'; root.appendChild(modelHost);

  root.appendChild(el('p', 'hint', null, '三个入口等价:点击热力格、点击柱体、填写日期后提交,都会选中同一天(DESIGN §5)。'));

  /* ---------- 交互与重聚合(handleDayClick 同语义) ---------- */
  function fmtRangeLabel() {
    if (!selectedDay) return '近 30 天';
    var d = parseDay(selectedDay);
    return (d.getMonth() + 1) + '月' + d.getDate() + '日';
  }
  function rangeDays() {
    var from = selectedDay || shiftDay(TODAY, -29);
    var to = selectedDay || TODAY;
    return Object.keys(byDay).filter(function (k) { return k >= from && k <= to; });
  }
  function renderAgentTable(rangeLabel) {
    agentHost.innerHTML = '';
    var days = rangeDays(), agents = ['claude-code', 'codex', 'pi'], rows = [], total = 0;
    agents.forEach(function (a) {
      var t = 0, mc = new Set();
      days.forEach(function (k) { byDay[k].per.forEach(function (v, i) { if (v > 0 && MODELS[i].agent === a) { t += v; mc.add(i); } }); });
      rows.push({ agent: a, tokens: t, models: mc.size }); total += t;
    });
    var bar = el('div', 'agent-sharebar');
    rows.forEach(function (r, ai) { bar.appendChild(el('div', null, { width: (total > 0 ? r.tokens / total * 100 : 0) + '%', backgroundColor: rankColor(ai) })); });
    agentHost.appendChild(bar);
    var tb = el('table'), thead = el('thead'), trh = el('tr');
    ['Agent / harness', rangeLabel + ' token', '占比', '今日', '缓存命中率', '用到的模型'].forEach(function (h) { trh.appendChild(el('th', null, null, h)); });
    thead.appendChild(trh); tb.appendChild(thead);
    var tbody = el('tbody');
    rows.forEach(function (r, ai) {
      var tr = el('tr'), main = el('div', 'cell-main');
      main.appendChild(el('span', 'swatch', { backgroundColor: rankColor(ai) }));
      main.appendChild(el('span', 'name', null, r.agent));
      var tdMain = el('td'); tdMain.appendChild(main); tr.appendChild(tdMain);
      tr.appendChild(el('td', null, null, fmtTokens(r.tokens)));
      var share = total > 0 ? r.tokens / total : 0;
      var shareTd = el('td', 'share-cell');
      shareTd.appendChild(el('span', 'share-bar', { width: Math.max(2, share * 46) + 'px', backgroundColor: rankColor(ai) }));
      shareTd.appendChild(document.createTextNode(Math.round(share * 100) + '%'));
      tr.appendChild(shareTd);
      var todayTokens = byDay[TODAY] ? byDay[TODAY].per.reduce(function (s, v, i) { return s + (MODELS[i].agent === r.agent ? v : 0); }, 0) : 0;
      tr.appendChild(el('td', 'muted', null, todayTokens > 0 ? fmtTokens(todayTokens) : '—'));
      tr.appendChild(el('td', 'muted', null, Math.round((48 + ai * 11)) + '%'));
      tr.appendChild(el('td', 'muted', null, String(r.models)));
      tbody.appendChild(tr);
    });
    tb.appendChild(tbody); agentHost.appendChild(tb);
  }
  function renderModelTable(rangeLabel) {
    modelHost.innerHTML = '';
    var days = rangeDays();
    var mTotals = MODELS.map(function (m, i) {
      var t = 0, input2 = 0, output = 0, cr = 0, cc = 0;
      days.forEach(function (k) {
        var v = byDay[k].per[i]; t += v;
        input2 += Math.round(v * 0.22); output += Math.round(v * 0.14); cr += Math.round(v * 0.58); cc += Math.round(v * 0.06);
      });
      return { m: m, tokens: t, input: input2, output: output, cr: cr, cc: cc };
    });
    var grand = mTotals.reduce(function (s, r) { return s + r.tokens; }, 0);
    mTotals.sort(function (a, b) { return b.tokens - a.tokens; });
    var tb = el('table'), thead = el('thead'), trh = el('tr');
    ['模型', rangeLabel + ' token', '占比', '输入', '输出', '缓存读取', '缓存写入', '缓存命中率'].forEach(function (h) { trh.appendChild(el('th', null, null, h)); });
    thead.appendChild(trh); tb.appendChild(thead);
    var tbody = el('tbody');
    mTotals.forEach(function (r) {
      if (r.tokens <= 0) return;
      var rank = r.m.key === '__others__' ? 5 : rankOf(MODELS.indexOf(r.m));
      var color = modelColor(rank, 5);
      var tr = el('tr'), main = el('div', 'cell-main');
      main.appendChild(el('span', 'swatch', { backgroundColor: color }));
      main.appendChild(el('span', 'name', null, r.m.name));
      if (r.m.agent) main.appendChild(el('span', 'agent-tag', null, r.m.agent));
      var tdMain = el('td'); tdMain.appendChild(main); tr.appendChild(tdMain);
      tr.appendChild(el('td', null, null, fmtTokens(r.tokens)));
      var share = grand > 0 ? r.tokens / grand : 0;
      var shareTd = el('td', 'share-cell');
      shareTd.appendChild(el('span', 'share-bar', { width: Math.max(2, share * 46) + 'px', backgroundColor: color }));
      shareTd.appendChild(document.createTextNode(Math.round(share * 100) + '%'));
      tr.appendChild(shareTd);
      [r.input, r.output, r.cr, r.cc].forEach(function (v) { tr.appendChild(el('td', 'muted', null, fmtTokens(v))); });
      tr.appendChild(el('td', 'muted', null, (38 + (5 - rank) * 9) + '%'));
      tbody.appendChild(tr);
    });
    tb.appendChild(tbody); modelHost.appendChild(tb);
  }
  function update() {
    root.querySelectorAll('.usage-chart-target').forEach(function (btn) {
      btn.setAttribute('aria-pressed', selectedDay != null && btn.dataset.day === selectedDay ? 'true' : 'false');
    });
    input.value = selectedDay || '';
    apply.disabled = !selectedDay;
    /* 概览与两张表按当前范围重聚合(选中单日 = day:YYYY-MM-DD 范围) */
    var days = rangeDays();
    var total = days.reduce(function (s, k) { return s + byDay[k].total; }, 0);
    var streak = 0; for (var k2 = TODAY; byDay[k2] && byDay[k2].total > 0; k2 = shiftDay(k2, -1)) streak++;
    var modelsUsed = new Set(); days.forEach(function (k) { byDay[k].per.forEach(function (t, i) { if (t > 0) modelsUsed.add(i); }); });
    summary.innerHTML = '';
    var rangeLabel = fmtRangeLabel();
    var stats = [
      ['今日 token', (byDay[TODAY] || { total: 0 }).total, true],
      [rangeLabel + ' token', total, true],
      ['连续活跃天数', streak, false],
      ['缓存命中率(' + rangeLabel + ')', 62, false],
      ['用到的模型', modelsUsed.size, false]
    ];
    stats.forEach(function (p) {
      var stat = el('div', 'stat');
      stat.appendChild(el('div', 'k', null, p[0]));
      stat.appendChild(el('div', 'v', null, p[2] ? fmtTokens(p[1]) : (p[0].indexOf('命中率') >= 0 ? p[1] + '%' : String(p[1]))));
      summary.appendChild(stat);
    });
    renderAgentTable(rangeLabel);
    renderModelTable(rangeLabel);
  }

  form.addEventListener('submit', function (e) {
    e.preventDefault();
    if (input.value && input.value <= TODAY) selectDay(input.value);
  });
  root.addEventListener('click', function (e) {
    var btn = e.target.closest('.usage-chart-target');
    if (btn && btn.dataset.day) selectDay(btn.dataset.day);
  });
  reset.addEventListener('click', function () { selectDay(null); });
  panelUpdaters.push(update);
  update();
}

document.querySelectorAll('[data-panel]').forEach(function (root) {
  renderPanel(root, root.dataset.panel);
});
</script>
</body>
</html>

dispatch:a55ee9dffdda23dd

Signed-off-by: kirozeng <kirozeng@gmail.com>
@MagicLizi MagicLizi added the status:awaiting-bot-review 等外部审查机器人表态(review-pr 自动维护,仅展示) label Sep 8, 2026
@kirozeng

kirozeng commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

本轮核实:无需代码改动,新 head 上 CI 已全绿

这条「Windows unit tests 缺席」的信号来自 fea5c7b 推送后 CI 刚启动的瞬间——聚合门当时还没登记,不是故障。现在 run 34178086110 已出终局:

  • Windows unit tests (1/2)(16m9s)/ (2/2)(16m48s)与聚合门全 pass
  • Linux unit tests 两分片、verify、verify-checks、Desktop Git integration、DCO、check:pr-design-basis 全 pass

即所有者同步的「交互反馈与排版优化」(移除日期表单入口、范围强调、描边与排版层级)在新 head 上一次通过全部必需检查,无需任何代码改动。

dispatch:5bc25e7f411b8ede

@MagicLizi MagicLizi removed the status:awaiting-bot-review 等外部审查机器人表态(review-pr 自动维护,仅展示) label Sep 8, 2026

@MagicLizi MagicLizi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

独立审查未通过(P1×2)。用量图改动把未落地的规则写进 DESIGN.md 正本,并且会覆盖已合入的 #4064

  • [P1] docs/design-rules/DESIGN.md:309 — §4 写用量图已有 “equivalent single-day selection control”,圆角走 §5 已登记成员 ID。本 PR 最终提交已删掉新增日期入口(没有 UsageDateFilter,热力格仍是 12×12);同 PR 的 usage-history-charts.md 也写明 Equivalent 未满足、不批准 Essential 豁免。权威正本不能给没落地的等价控件背书。请删掉该表述,或把合规日期入口做回来再写进去。

  • [P1] docs/design-rules/DESIGN.md:334 — §5 把 usage-heatmap-day / usage-token-bar 当已登记成员,并写交互放大 supersede 尺寸禁令。相对当前 main#4064),本分支会删掉 Settings segmented controls、Usage data graphics,以及 usage data marks 的 2px 例外,并把 pill 口径退回「every button is a pill — the only exemption is registered bare text buttons」。这是用未合入的 #4072 口径改写正本,给本 PR 的放大/彩色实现开脱。请先 rebase main,保留 #4064 的分段控件与 usage-mark 例外;成员 ID 要么等 #4072 合入再引用,要么在本 PR 完整登记。

按 review 5137957030 的两条 P1 同步 main(取到 makecindy#4064 为止,不含其后
的 CI 工作流改动):保留 makecindy#4064 已合入的 §4 Settings segmented controls /
Usage data graphics 与 §5 usage data marks 2px 例外及 pill 措辞,不再以
未合入的规范 PR 框架改写正本。

- §4 Usage History Charts 指针节去掉对已移除等价日期表单的背书,改为
  锚定 §5 已登记例外与 §2 颜色登记;Usage data graphics 的灰度条款让位
  §2 已登记的用量图表类别色
- §5 交互几何登记改写为锚定 usage data marks 例外、在本 PR 内完整登记
  usage-heatmap-day / usage-token-bar,去掉 supersede 框架
- usage-history-charts.md 的登记引用改为本 PR 自身,不再指向未合入 PR
- 组件与测试冲突取本分支终态(无框选中、独立指示层与 grow 结构);
  usageChartInteraction.test.tsx 两处断言对齐合并后实现
- design-inventory 由 pnpm design:inventory 从合并后代码再生成

Signed-off-by: Kirozeng <kirozeng@gmail.com>
@kirozeng

kirozeng commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

本轮修复:按 review 的两条 P1 同步 #4064,并把登记收进本 PR(a87a13fac)

P1-1(§4 给未落地的等价控件背书):已删。§4 的 Usage History Charts 指针节不再提「equivalent single-day selection control」,改为明确说明草稿日期表单不属于本条目、命中尺寸裁决仍在组件规范里单独记录;几何锚定回 §5 已登记的 usage data marks 例外,颜色锚定 §2 的图表类别色登记。

P1-2(覆盖 #4064、借用未合入规范 PR 的口径):已合并 main 到 #4064(merge 701c1bf)。#4064 的登记全部保留:

  • §4 Settings segmented controls 与 Usage data graphics 两节原样保留(后者仅灰度条款让位 §2 已登记的类别色)
  • §5 pill 措辞保留「usage data marks below follow their specific exceptions」
  • §5 usage data marks (2px) 例外保留

交互几何段从「supersedes + member IDs」改写为锚定该例外、在本 PR 内完整登记 usage-heatmap-day / usage-token-bar 两个成员(对应 data-usage-mark 值与组件文件);usage-history-charts.md 的登记引用同样改为本 PR 自身,不再指向未合入的规范 PR。

为什么 merge 而不是 rebase:本分支禁 force-push,merge 到 701c1bf 内容上等效且不重写历史;没有取 #4064 之后的 main,是因为其后进了 CI 工作流改动,避免本 PR 的 diff 触碰 CI 路径。

验证

  • 冲突解决后完整门禁通过:pnpm test:unit:related(desktop 全量 156s、maker-core 全量 153s、mobile、test:runner 及相关包全 PASS)、pnpm --filter desktop run typecheck 通过、pnpm check:dco 通过
  • 首轮门禁 maker-core 的 6 例失败是本机运行环境的 CLAUDE_CODE_* / XDT_* 变量干扰(如 CLAUDE_CODE_DISABLE_EXPLORE_INHERIT_CAP=1),清掉后同套测试 56/56 全绿——与本次改动无关,CI 环境没有这些变量
  • usageChartInteraction.test.tsxfix(settings): 统一分段选择样式并恢复紧凑用量图形 #4064 带入)两处断言按合并后实现更新:mark 圆角断言改为 contains + data-usage-mark,内联 outline 断言改为独立 usage-chart-indicator 层存在性;测试意图(紧凑几何、可点击、30 柱不溢出)不变
  • design-inventorypnpm design:inventory 从合并后代码再生成(35 surface,desktop.settingsSettingsSegmentedControl

CI 在新 head 上重跑中。

dispatch:6ef142a592dccb54

@MagicLizi
MagicLizi dismissed their stale review September 8, 2026 08:35

这些问题已在当前 head 的后续 commit 中修复(格式门/安全门已通过),自动 dismiss 旧的 CHANGES_REQUESTED 以解除合并阻塞。

同步 main(至 043eaecmakecindy#4072 已合入),解决与 makecindy#4076 的设计文档冲突:

- §5 采用 makecindy#4072 重写的「可见层 + 登记驱动」框架(Step 1 已登记形状 /
  Step 2 控件三档);makecindy#4064 的 usage data marks 例外已由框架内成员表吸收
- 本 PR 的交互几何登记改写为 Interaction constraints 内的
  「Registered interaction emphasis」条目,锚定 data mark 成员
  usage-heatmap-day / usage-token-bar;§4 指针与 cindy-design-system
  版本记录同步改引用
- §14.4 Press 行取 main 新措辞,Done 行保留对下方图表响应登记的指引
- design-inventory 人工区融合两边跟进记录,生成区由
  pnpm design:inventory 再生成并通过 check:design-inventory

Signed-off-by: Kirozeng <kirozeng@gmail.com>
@kirozeng

kirozeng commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

分支冲突已解决(96ab9ebba):合并 main 至 #4072(圆角登记框架)

#4072 合入后与本 PR 改到同一批设计文档产生冲突。本次合并对齐:

  • §5 采用 docs(design): 按可见层与登记分配圆角并明确审查边界 #4072 的「可见层 + 登记驱动」新框架;fix(settings): 统一分段选择样式并恢复紧凑用量图形 #4064 的 usage data marks 例外已由框架内成员表吸收(usage-heatmap-day / usage-token-bar 本就在成员表里)
  • 本 PR 的交互几何登记改写为 Interaction constraints 内的「Registered interaction emphasis」条目,锚定上述两个成员;§4 指针、cindy-design-system 版本记录同步改引用
  • §14.4 Press 行取 main 新措辞,Done 行保留对图表响应登记的指引
  • design-inventory 人工区融合两边跟进记录,生成区由 pnpm design:inventory 再生成并通过 check:design-inventory

验证:干净环境全仓 pnpm test:unit:related 通过(desktop 171s、maker-core 143s 等 30 个 workspace 全 PASS)、desktop typecheck 通过、DCO 通过。GitHub 现在报告 mergeable。

@kirozeng
kirozeng merged commit f3203a7 into makecindy:main Sep 8, 2026
10 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

touches:core 改动碰到架构核心路径(review-pr 自动维护,仅展示) touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示) touches:rules 改动碰到规则 / 规范文档(review-pr 自动维护,仅展示)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants