mirror of
https://github.com/Caho1/open-claude-ui.git
synced 2026-11-07 10:26:02 -10:00
Merge pull request #26 from Caho1/docs/v4.29.0-4.4212.0
docs+feat: Claude Desktop 3.6556.2 取证基线 + 4.29.0→4.4212.0 delta 文档与 CDS 对齐
This commit is contained in:
18 files changed
+9615
-109
No files matched your search
@@ -44,19 +44,28 @@ All tokens are defined in [`code-examples/cds-theme.css`](code-examples/cds-them
|
||||
### Text & Border Tokens
|
||||
| Token | Light Mode Value | Dark Mode Value | Usage |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `--cds-text-primary` | `#151515` | `#ffffff` | Headings, primary body text, active labels |
|
||||
| `--cds-text-secondary` | `#525252` | `#c2c2c2` | Timestamps, subtitles, secondary actions |
|
||||
| `--cds-text-muted` | `#8a8a8a` | `#808080` | Placeholders, disabled text, hotkey hints (alias: `--cds-text-tertiary`) |
|
||||
| `--cds-text-accent` | `var(--cds-clay)` | `var(--cds-clay)` | Highlighted brand terracotta text |
|
||||
| `--cds-text-primary` | `var(--cds-neutral-900)` → `#0b0b0b` | `#f0efec` (gray-50) | Headings, primary body text, active labels (v3 运行时实证) |
|
||||
| `--cds-text-secondary` | `var(--cds-neutral-600)` → `#52514e` | `#c3c2b7` (gray-200) | Timestamps, subtitles, secondary actions (v3) |
|
||||
| `--cds-text-muted` | `var(--cds-neutral-400)` → `#898781` | `#898781` (双模同值) | Placeholders, disabled text, hotkey hints (alias: `--cds-text-tertiary`) (v3) |
|
||||
| `--cds-text-accent` | `var(--cds-role-accent-600)` → `#184f95` | `#6da7ec` | Accent role is **blue**, not clay (v3 运行时实证) |
|
||||
| `--cds-border` | `var(--cds-alpha-2)` | `var(--cds-alpha-2)` | Dividers, subtle list borders (alias: `--cds-border-subtle`, `--cds-border-card`) |
|
||||
| `--cds-border-strong` | `var(--cds-alpha-3)` | `var(--cds-alpha-3)` | Emphasized component borders |
|
||||
| `--cds-border-subtle` | `var(--cds-alpha-2)` | `var(--cds-alpha-2)` | Fine hairline dividers |
|
||||
| `--cds-border-accent` | `var(--cds-clay)` | `var(--cds-clay)` | Focus and active borders |
|
||||
|
||||
### Skeleton & Motion Tokens
|
||||
| Token | Light Mode Value | Dark Mode Value | Usage |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `--cds-skel-i` | `0` | `0` | Cascading index for skeleton wave delay (`calc(var(--cds-skel-i, 0) * 90ms)`) |
|
||||
| `--cds-skeleton-base` | `var(--cds-alpha-1)` → `hsl(from neutral-900 / 5%)` | `hsl(from neutral-900 / 7.5%)` (白) | Base skeleton placeholder container surface (v3 运行时实证:base 与 sheen 同值) |
|
||||
| `--cds-skeleton-sheen` | `var(--cds-alpha-1)`(同 base) | 同 base(白 7.5%) | 105° linear sweep band highlight (2s linear) |
|
||||
|
||||
### Brand Clay Palette
|
||||
* `--brand`: `#c6613f` (Official desktop brand primary color)
|
||||
* `--cds-clay`: `#d97757` (Light / pulse indicator) / `#cc6b49` (Dark)
|
||||
* Focus shadow: `var(--cds-focus-shadow)` (`inset 0 0 0 1px var(--cds-surface-1), 0 0 0 1.5px var(--cds-clay), 0 0 6px 1px rgba(217, 119, 87, 0.25)`)
|
||||
* **官方佐证 (v3 取证)**:Claude 桌面端 3.6556.2 内置的官方 `frontend-design` skill 原文确认 #D97757 为 "Anthropic's own Claude-interaction accent"(见 `docs/v4.29.0-4.4212.0/06` §6.10.4),与 REA 运行时实测一致
|
||||
* 角色区分 (运行时实证 3.6556.2):`brand`=陶土(Claude 发起的动作,如发送/生成);`accent`=**蓝色**(`--cds-text-accent: #184f95`、`--cds-border-accent: #86b6ef`)——二者是不同角色,不可互换
|
||||
* Focus shadow: `var(--cds-focus-shadow)` (`inset 0 0 0 1px var(--cds-page-bg), 0 0 0 1px var(--cds-fill-accent), 0 0 6px 1px var(--cds-bg-accent)`, 即蓝色 ring + 蓝色 glow,运行时实证)
|
||||
|
||||
### Geometric Corner Radii
|
||||
* `var(--cds-radius)`: `8px` (Base component radius)
|
||||
@@ -150,7 +159,10 @@ current-version record in this repo. When a scenario label matters, re-derive it
|
||||
from the bundle rather than trusting either volume.
|
||||
|
||||
### 7-Stop Non-Linear Fade Mask
|
||||
Never use a linear gradient for scroll fading. Use the calibrated 7-stop non-linear mask:
|
||||
Never use a linear gradient for scroll fading. Use the calibrated 7-stop non-linear mask
|
||||
(v3 forensic note: the 3.6556.2 client drives scroll fading through `@property` variables +
|
||||
`animation-timeline: scroll()` with no hardcoded multi-stop mask; keep the stops below only as a
|
||||
static fallback for engines without scroll-driven animation):
|
||||
```css
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
|
||||
@@ -9,6 +9,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### 🧬 Changed (v3 Baseline Re-anchor — Claude Desktop `@ant/desktop 3.6556.2`)
|
||||
- **Ground-Truth Re-anchor**: All token and motion values re-verified against the currently installed client (`@ant/desktop 3.6556.2`, ASAR `sha256 b2dfbbb4…`), superseding the v2.37337.1 baseline. Full seven-chapter forensic report in [`docs/v3/`](docs/v3/README.md) — every claim carries code evidence and a repro command, combining REA v6.8.0 static extraction with live measurements via the app's **Main Process Debugger**.
|
||||
- **Text Colors (corrected)**: `--cds-text-primary/secondary/muted` now resolve through the official `--cds-neutral-*` theme ramp (`#0b0b0b` / `#52514e` / `#898781` in light; `#f0efec` / `#c3c2b7` / `#898781` in dark — muted is identical in both modes). Previous fixed hex values (`#151515/#525252/#8a8a8a`) were wrong in both modes.
|
||||
- **Accent Role Split (corrected)**: `--cds-text-accent` and `--cds-border-accent` are **blue** (`#184f95` / `#86b6ef` light), not clay — the official spec separates the `accent` role (blue) from the `brand` role (clay, reserved for Claude-initiated actions). Added the full `--cds-blue-*` and `--cds-role-accent-*` chains; `--cds-focus-shadow` now renders a blue ring + glow as verified live.
|
||||
- **Brand Hover Direction (corrected)**: `--cds-fill-brand-hover` is `#d97757` (clay) — the client hovers *upward*; `--cds-clay-emphasized` is a literal `#c6613f` in both modes (official corroboration: the bundled `frontend-design` skill calls #D97757 "Anthropic's own Claude-interaction accent").
|
||||
- **Shadow System (corrected)**: `--cds-shadow-popover` is a pure two-layer drop shadow (inset ring removed); `--cds-shadow-sm/md/lg` offset layers use `hsl(from gray-900 …)` so they follow dark mode; dark `--cds-shadow-color` is `rgba(0,0,0,0.24)` per live reading.
|
||||
- **Density System (corrected)**: the full sizing family (radius, `h-control*`, icon, `pad-*`, `gap-*`) is written as `calc(N * var(--cds-rem-scale, 1))` instead of fixed px; `--cds-radius-composer` 12px with a `[data-density="comfortable"]` override to 14px; `--cds-gap-xl` 40px, `--cds-h-control-nested` 22px.
|
||||
- **Skeleton Overhaul**: official two-layer structure (`SkeletonGroup`/`Skeleton` + `.cds-skel-sweep-band` child) ported; the standalone `.cds-skeleton-sweep` class no longer displaces its element; `data-hold` three-state reveal timing; wave/lockstep breathe bindings; RTL `:lang()` list with `animation-direction: reverse`; dual reduced-motion guards; dark base/sheen equalized at white 7.5%.
|
||||
- **Attachment View Transitions (new)**: composer attachment add/remove/reorder ported verbatim — container transitions, per-card `view-transition-name`/`class`, `:active-view-transition-type` routing, new-Chromium `match-element` + `view-transition-group: contain` behind `@supports`, `@starting-style` entrance, `cds-attachment-out` exit; `AttachmentCard` exports `withAttachmentTransition()` and sets `--cds-attachment-vt-name` per id.
|
||||
- **Alpha Ramp Single Expression**: `--cds-alpha-*` uses `hsl(from var(--cds-neutral-900) …)` so the light/dark flip rides the neutral ramp; the dark block's hardcoded white alphas (0.09/0.18/0.30, off by 1/2/5 points) are removed. `--cds-fill-secondary` is white 10% in light per the live reading.
|
||||
- **CI Gate Hardening**: `scripts/check-tokens.mjs` strips CSS comments before the orphan-keyframe check (a keyframe named only in a comment used to pass).
|
||||
- **Docs**: README/README-en updated with the v3 baseline, v3 chapter index and token counts (242); the v1/v2 "7-stop fade mask" records annotated with the v3 finding (the 3.6556.2 client drives scroll fading via `@property` + `animation-timeline`, zero 6+-stop gradients).
|
||||
|
||||
### 🐛 Fixed (Issue #25 - Skeleton Sweep Parameters, Dual-Mode Motion & Keyframe Consumers)
|
||||
- **Forensic Parameter Alignment**: Aligned skeleton sweep across the entire repository to Claude desktop ground truth: `2s` / `linear` / `105deg` (RTL `255deg`), correcting the divergent `1.8s` / `cubic-bezier(0.4, 0, 0.2, 1)` / `90deg` horizontal sweep in Storybook and `tailwind.config.js`.
|
||||
- **First-Class `.cds-skeleton` & `.cds-skeleton-sweep` Classes**: Implemented `.cds-skeleton` base container and `.cds-skeleton-sweep` with 105° linear glass sheen in `code-examples/cds-theme.css`, exported to standalone `code-examples/cds-pure.css` so pure CSS consumers receive the promised skeleton sweep utility.
|
||||
- **2.0 Dual-Motion Skeleton Implementation**:
|
||||
- `[data-motion="wave"]`: 105° linear sweep band with per-block cascading delay via `calc(var(--cds-skel-i, 0) * 90ms)` for large cards and hero content.
|
||||
- `[data-motion="lockstep"]`: Synchronized 2.0s opacity dimming (`1.0` -> `0.6` -> `1.0`) via `@keyframes cds-skeleton-breathe` for dense lists.
|
||||
- **Storybook Source of Truth Calibration**: Updated `docs/claude-components-storybook.html` section 7.2 to consume canonical `.cds-skeleton` classes and showcase both `[data-motion="wave"]` and `[data-motion="lockstep"]` side-by-side with interactive mode inspection.
|
||||
- **Enhanced Token & Keyframe CI Gate**: Extended `scripts/check-tokens.mjs` to scan both `AGENTS.md` and `docs/` for `--cds-*` tokens (79 total unique tokens), explicitly assert skeleton tokens (`--cds-skel-i`, `--cds-skeleton-base`, `--cds-skeleton-sheen`), and assert that every `@keyframes cds-*` has at least one real consumer class.
|
||||
|
||||
### 🐛 Fixed (Issue #20 - WebKit Post-Composition Key Swallowing Guard)
|
||||
- **Scoped Post-Composition Guard**: Constrained the WebKit 500ms post-composition guard in `code-examples/RichComposerWithMentions.tsx` to target only the phantom `Enter` (`e.key === 'Enter' || e.keyCode === 13`) dispatched by Safari/WebKit after IME candidate commit (`compositionend`), preventing regular keystrokes (letters, Space, Backspace, arrows) from being swallowed.
|
||||
- **Immediate Timestamp Reset**: Reset `lastCompositionEndTimeRef.current = -1` immediately upon intercepting the phantom Enter, ensuring at most one phantom keydown event is consumed and subsequent typing passes through without delay.
|
||||
|
||||
+19
-3
@@ -39,6 +39,10 @@
|
||||
|
||||
`open-claude-ui` is a high-fidelity open-source UI component library and motion physics engine, built through in-depth clean-room reverse engineering of the modern desktop AI client.
|
||||
|
||||
> 🧭 **Forensic baseline**: values are aligned to **Claude Desktop `@ant/desktop 4.4212.0`** (primary forensics on 3.6556.2, re-verified on 4.29.0 and 4.4212.0), all verified through dual channels — **REA v6.8.0** static extraction plus live measurement via the app's **Main Process Debugger** (see [docs/v4.29.0-4.4212.0](docs/v4.29.0-4.4212.0/README.md)).
|
||||
>
|
||||
> 📁 **Doc versioning convention**: from this release on, manual directory names **track the Claude Desktop version** (e.g. `docs/v4.29.0-4.4212.0/`); future Claude updates will get a new directory named after their version. A directory may name a single version (e.g. `docs/v5.0.0/`) or a covered range (e.g. `docs/v4.29.0-4.4212.0/`, meaning primary forensics on the first version with incremental re-checks on the later ones). Earlier `docs/v1/` and `docs/v2/` keep their names as historical snapshots (v2 values are superseded — defer to `docs/v4.29.0-4.4212.0/`).
|
||||
|
||||
All reverse derivation, de-obfuscation static analysis, and ASAR package deconstruction were conducted using the advanced multi-platform reverse engineering framework **[REA (Reverse Engineer Anything)](https://github.com/morluto/rea)**. Leveraging REA's static AST deconstruction, closure de-obfuscation, and runtime tracing capabilities, we penetrated production bundles to faithfully extract the underlying physical constants, interaction constraints, and mathematical models.
|
||||
|
||||
Unlike superficial visual approximations or guesswork replicas, every component specification, damping constant, shader keyframe, and audio pipeline in this project has been **rigorously verified against decompiled production front-end architecture**:
|
||||
@@ -47,7 +51,7 @@ Unlike superficial visual approximations or guesswork replicas, every component
|
||||
* **Audiovisual & Desktop Architecture**: Zero-latency procedural audio synthesis via Web Audio API oscillators, cursor-anchored gallery pan/zoom affine transformations, macOS vibrancy glass fusion, and Cowork multi-agent orchestration topologies.
|
||||
|
||||
> 💡 **Architecture & Portability Note**:
|
||||
> The **Design Tokens and Pure-CSS presentation layer can be consumed completely independently from React and Tailwind**. Whether you are integrating into custom desktop harnesses (e.g. DSH, agent desktop shells), standard web pages, or Vue/Svelte environments, simply import `@import "open-claude-ui/tokens.css";` or `@import "open-claude-ui/pure.css";` to access all 183 design tokens, zero-JS native scroll-fade masks (`[data-cds-scroll-fade]`), and core animations.
|
||||
> The **Design Tokens and Pure-CSS presentation layer can be consumed completely independently from React and Tailwind**. Whether you are integrating into custom desktop harnesses (e.g. DSH, agent desktop shells), standard web pages, or Vue/Svelte environments, simply import `@import "open-claude-ui/tokens.css";` or `@import "open-claude-ui/pure.css";` to access all 242 design tokens (aligned with Claude Desktop 3.6556.2 runtime measurements), zero-JS native scroll-fade masks (`[data-cds-scroll-fade]`), and core animations.
|
||||
|
||||
> ⚖️ **Copyright & Asset Compliance (DMCA & Assets Policy)**:
|
||||
> This open-source repository **does not distribute any proprietary binary assets** (including commercial `.ttf` fonts and recorded `.mp3` audio). We provide native system font fallback stacks and procedural Web Audio synthesis algorithms, ensuring a completely self-contained, legal, and clean-room compliant codebase. For details, see [assets/README.md](assets/README.md).
|
||||
@@ -134,7 +138,17 @@ It documents exact decompilation evidence locations alongside Anthropic's archit
|
||||
* **[8.1~8.7 Agent Roles, Design Token Contracts & Spring Decision Tree](docs/v1/08-AI-Agent-前端-UI-与动效开发指南.md)**: Standardized guidelines for AI Coding Agents to implement pixel-perfect CDS components.
|
||||
|
||||
### 🚀 Claude Desktop 2.0 (v2.37337.1) Comprehensive Reverse Engineering Analysis
|
||||
* **[1~6 Overview, Additions, Refactorings, Baseline Secrets, Reference Implementations & Summary](docs/v2/README.md)**: Standalone decompilation report for Claude Desktop 2.0.
|
||||
* **[1~6 Overview, Additions, Refactorings, Baseline Secrets, Reference Implementations & Summary](docs/v2/README.md)**: Standalone decompilation report for Claude Desktop 2.0. **(Historical snapshot: superseded by 3.6556.2 — defer to v3 for values)**
|
||||
|
||||
### 🧬 Claude Desktop 4.4212.0 Full Analysis Report (current baseline)
|
||||
* **[v4.29.0/README.md — seven chapters + merged edition](docs/v4.29.0-4.4212.0/README.md)**: full reverse engineering of the current production build, **every claim backed by code evidence and a repro command**. Directory name tracks the Claude version.
|
||||
- **Ch.1 Scope & version boundary**: `@ant/desktop 3.6556.2`, ASAR digest, 532-file semantic truncation boundary, six structural zeros
|
||||
- **Ch.2 Electron architecture**: 6 Sentry IPC channels / 0 business handlers in the main process, 12 literal bridge API names, 7 renderer entry points
|
||||
- **Ch.3 Architecture refactorings**: two-layer skeleton components + `data-hold` three-state reveal, real wave/lockstep delay formulas, RTL `:lang()` list, dual reduced-motion guards
|
||||
- **Ch.4 Baseline manifests**: 55 `@keyframes`, 45 `data-cds` namespaces, 24 `@property`, full 35-stop gray/orange ramps, 5-step durations and 4 bezier curves
|
||||
- **Ch.5 open-claude-ui diff audit**: item-by-item diff and issue #25 rulings
|
||||
- **Ch.6 Full CSS scan & official spec comparison**: all 17 stylesheets, extracted official CDS spec, 131-token comparison, **runtime dual-mode value tables via Main Process Debugger**, whole-ASAR architecture inventory (bundled Cowork VM / 4 MCP servers / 6 Agent Skills / V8 compile-cache)
|
||||
- **Ch.7 Summary & next steps**: certainty tiers, unknowns, reproducibility statement
|
||||
|
||||
---
|
||||
|
||||
@@ -143,6 +157,8 @@ It documents exact decompilation evidence locations alongside Anthropic's archit
|
||||
UI components are merely the presentation layer. **What truly sets Claude's interaction quality apart are the mathematical models and physics equations buried beneath minified production code**. This repository decompiled and faithfully recreated 8 core algorithms:
|
||||
|
||||
### 1. 7-Stop Non-Linear Alpha Gradient Fade Mask
|
||||
|
||||
> ⚠️ **v4.29.0 correction**: the hardcoded 7-stop ladder exists in v1/v2-era artifacts; the 3.6556.2 main stylesheet contains **zero** gradients with 6+ stops — scroll fading is now `@property` + `animation-timeline` variable-driven (see docs/v4.29.0-4.4212.0/06 §6.6.3). Historical record below; the dynamic edge-zeroing logic in `ScrollFadeContainer.tsx` remains valid.
|
||||
* **The Problem**: Conventional two-stop linear scroll masks cause harsh color banding and jarring geometric cutoffs on high-DPI Retina displays.
|
||||
* **Claude's Algorithm**: Discards blunt linear cuts in favor of 7 non-linear gradient stops across the fade distance (15% position with 5.1% alpha `#0d`, 30% with 14.9% `#26`, 45% with 30.2% `#4d`, 60% with 50.2% `#80`, 75% with 70.2% `#b3`, 87% with 85.1% `#d9`, closely tracking a $y \approx x^{1.5}$ power curve), dynamically zeroed out by scroll boundary sensors at the top and bottom:
|
||||
```css
|
||||
@@ -258,7 +274,7 @@ open-claude-ui/
|
||||
|
||||
### 1. Import Stylesheet & Tokens (Pure CSS Support)
|
||||
|
||||
Regardless of whether you use React, you can directly import the CDS core stylesheet and its 183 design tokens:
|
||||
Regardless of whether you use React, you can directly import the CDS core stylesheet and its 242 design tokens (aligned with Claude Desktop 3.6556.2):
|
||||
```css
|
||||
/* Import global theme variables, scroll-fade masks, and keyframes */
|
||||
@import "open-claude-ui/css";
|
||||
|
||||
@@ -39,15 +39,19 @@
|
||||
|
||||
`open-claude-ui` 是基于对现代桌面端 AI 客户端进行深度拆解与逆向分析构建的高保真开源 UI 组件库与动效引擎。
|
||||
|
||||
> 🧭 **取证基线**:令牌与动效值对齐 **Claude Desktop `@ant/desktop 4.4212.0`**(主取证于 3.6556.2,4.29.0 与 4.4212.0 两轮增量复验一致),全部经 **REA v6.8.0** 静态提取 + **Main Process Debugger** 运行时实测双通道验证(详见 [docs/v4.29.0-4.4212.0](docs/v4.29.0-4.4212.0/README.md))。
|
||||
>
|
||||
> 📁 **文档版本命名规范**:逆向分析手册的目录名从本版起**直接对齐 Claude Desktop 的版本号**(如 `docs/v4.29.0-4.4212.0/`);后续 Claude 更新后,新取证报告将以对应版本号新建目录。目录名可写单个版本(如 `docs/v5.0.0/`)或覆盖区间(如 `docs/v4.29.0-4.4212.0/`,表示该报告在首个版本取证、后续版本增量复核)。早期的 `docs/v1/`、`docs/v2/` 保持原名不变,作为历史快照保留(v2 取值已被取代,以 `docs/v4.29.0-4.4212.0/` 为准)。
|
||||
|
||||
整个逆向推导、反混淆静态分析与 ASAR 资源解构,均依托于先进的多平台逆向工程框架 **[REA (Reverse Engineer Anything)](https://github.com/morluto/rea)** 完成——通过 REA 的静态 AST 解析、闭包代码反混淆与运行时跟踪能力,穿透生产级压缩包,精准还原了底层工程物理常数与数学模型。
|
||||
|
||||
不同于市面上常见的盲猜或近似复刻,本项目所有组件规范、物理阻尼常数、着色器关键帧及音频管道均**严格依据真实前端架构反编译验证**提炼而成:
|
||||
* **温润的人文质感 (Editorial Warmth)**:纯正陶土橙(Clay `#d97757`)、4 层微对比表面阶梯(Surface 0-3)与视网膜高分屏发丝细边补偿(Hairline Ring);
|
||||
* **严苛的交互物理学 (Interaction Physics)**:4 套生产级物理弹簧参数调校、4px 指针移动死区、CJK 输入法合成守卫、7 档渐隐非线性 Alpha 遮罩;
|
||||
* **严苛的交互物理学 (Interaction Physics)**:4 套生产级物理弹簧参数调校、4px 指针移动死区、CJK 输入法合成守卫、滚动渐隐遮罩(v1 取证的 7 档非线性阶梯;3.6556.2 已重构为 `@property` 变量驱动,见 [docs/v4.29.0-4.4212.0](docs/v4.29.0-4.4212.0/README.md));
|
||||
* **高阶视听与系统架构 (Audiovisual & Architecture)**:基于原生 Web Audio API 的零延迟合成提示音、以光标为锚点的画廊跑道平移缩放、macOS 桌面毛玻璃透明融合与 Cowork 多代理协同拓扑。
|
||||
|
||||
> 💡 **架构分层说明 (Architecture & Portability)**:
|
||||
> 本项目的 **令牌层 (Tokens) 与纯 CSS 表现层可脱离 React 与 Tailwind 单独使用**。无论是自研桌面 Harness(如 DSH、各类 Agent 桌面容器)、传统 Web 页面还是 Vue/Svelte 宿主,只需引入 `@import "open-claude-ui/tokens.css";` 或 `@import "open-claude-ui/pure.css";`,即可直接消费全量 183 个设计令牌、Zero-JS 原生滚动消隐遮罩(`[data-cds-scroll-fade]`)与核心动效类,详情见下文快速上手。
|
||||
> 本项目的 **令牌层 (Tokens) 与纯 CSS 表现层可脱离 React 与 Tailwind 单独使用**。无论是自研桌面 Harness(如 DSH、各类 Agent 桌面容器)、传统 Web 页面还是 Vue/Svelte 宿主,只需引入 `@import "open-claude-ui/tokens.css";` 或 `@import "open-claude-ui/pure.css";`,即可直接消费全量 242 个设计令牌(对齐 Claude Desktop 3.6556.2 运行时实测)、Zero-JS 原生滚动消隐遮罩(`[data-cds-scroll-fade]`)与核心动效类,详情见下文快速上手。
|
||||
|
||||
> ⚖️ **版权与合规声明 (DMCA & Assets Policy)**:
|
||||
> 本开源项目**不公开分发任何专有二进制资产**(包括商业字体 `.ttf` 与录音 `.mp3`)。本项目提供原生系统级字体回退栈与 Web Audio API 纯代码合成音效,完全自包含、合规安全。详情参见 [assets/README.md](assets/README.md)。
|
||||
@@ -134,7 +138,17 @@ graph TD
|
||||
* **[8.1~8.7 Agent 角色定位、设计令牌契约与弹簧决策树](docs/v1/08-AI-Agent-前端-UI-与动效开发指南.md)**:专为 AI Agent 定制的前端 UI 与动效装配自检指南。
|
||||
|
||||
### 🚀 第二版:Claude Desktop 2.0 (v2.37337.1) 全量版本更新深度逆向分析
|
||||
* **[v2/README.md 综述、新增特性、架构重构、底座解密与参考实现](docs/v2/README.md)**:全量划分为 6 大独立章节,深度解析 2.0 桌面端架构大跃迁。
|
||||
* **[v2/README.md 综述、新增特性、架构重构、底座解密与参考实现](docs/v2/README.md)**:全量划分为 6 大独立章节,深度解析 2.0 桌面端架构大跃迁。**(历史快照:基线版本已被 3.6556.2 取代,取值以 v3 为准)**
|
||||
|
||||
### 🧬 Claude Desktop 4.4212.0 全量分析报告(当前基线)
|
||||
* **[v4.29.0/README.md 七章取证 + 单文件完整版](docs/v4.29.0-4.4212.0/README.md)**:对当前生产包的全量分析,**每条结论附代码证据与复现命令**。目录名对齐 Claude 版本号。
|
||||
- **第一章 综述与版本边界**:`@ant/desktop 3.6556.2`、ASAR 摘要、532 文件语义截断边界、6 个结构性零值
|
||||
- **第二章 Electron 架构取证**:主进程 6 个 Sentry IPC 通道、0 个业务 handler、12 个字面量桥接 API、7 个 renderer 入口
|
||||
- **第三章 架构重构与对比分析**:骨架屏双层组件 + `data-hold` 三态揭示、wave/lockstep 真实延时公式、RTL `:lang()` 列表、减少动效双轨
|
||||
- **第四章 系统底座全景解密**:55 个 `@keyframes`、45 个 `data-cds` 命名空间、24 个 `@property`、35 阶 gray/orange 全表、5 阶时间与 4 条贝塞尔
|
||||
- **第五章 open-claude-ui 差异审计**:逐项 diff 与 issue #25 取值裁定
|
||||
- **第六章 全量 CSS 扫描与官方规范比对**:17 个 CSS 全扫、官方 CDS 规范文档提取、131 项 token 逐项比对、**Main Process Debugger 运行时双模值表**、ASAR 全量架构盘点(内置 Cowork VM / 4 个 MCP server / 6 个 Agent Skills / V8 compile-cache)
|
||||
- **第七章 总结与后续工作**:确定性分级、未知项清单、可复现性声明
|
||||
|
||||
---
|
||||
|
||||
@@ -143,6 +157,8 @@ graph TD
|
||||
UI 组件只是外壳,**真正让 Claude 交互质感脱颖而出的是隐藏在混淆代码底部的物理学方程与数学模型**。本项目从生产环境反编译还原了以下 8 项关键算法:
|
||||
|
||||
### 1. 7 档渐隐非线性透明度遮罩 (7-Stop Non-Linear Fade Mask)
|
||||
|
||||
> ⚠️ **v4.29.0 勘误**:7 档写死阶梯存在于 v1/v2 时代的产物;3.6556.2 主样式表中 6 停以上 gradient 为 **0**,滚动渐隐已重构为 `@property` + `animation-timeline` 变量驱动(见 [docs/v4.29.0-4.4212.0/06](docs/v4.29.0-4.4212.0/06-全量CSS扫描与官方规范比对.md) §6.6.3)。以下为历史取证记录,`ScrollFadeContainer.tsx` 的动态归零逻辑仍然有效。
|
||||
* **痛点**:常规双色线性滚动遮罩在视网膜高分屏上存在严重的色阶断层(Color Banding)与生硬切断感。
|
||||
* **Claude 算法**:没有采用粗暴的线性双色切线,而是在羽化距离内采用 7 档非线性采样阶梯(采样位置 15% 处对应 Alpha 5.1% `#0d`、30% 处 14.9% `#26`、45% 处 30.2% `#4d`、60% 处 50.2% `#80`、75% 处 70.2% `#b3`、87% 处 85.1% `#d9`,曲线紧密贴合 $y \approx x^{1.5}$ 幂律感知感知模型),搭配触顶/触底动态归零传感器,呈现温润如沉入水雾的边缘渐隐:
|
||||
```css
|
||||
@@ -257,7 +273,7 @@ open-claude-ui/
|
||||
|
||||
### 1. 引入样式表与 Tokens (纯 CSS 支持)
|
||||
|
||||
无论是否使用 React,均可直接引入 CDS 核心样式表与 183 个设计令牌:
|
||||
无论是否使用 React,均可直接引入 CDS 核心样式表与 242 个设计令牌:
|
||||
```css
|
||||
/* 引入全局主题变量、渐变消隐遮罩与核心关键帧 */
|
||||
@import "open-claude-ui/css";
|
||||
|
||||
@@ -15,6 +15,26 @@
|
||||
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* 附件集合变更的 View Transition 包装 (REA v6.8.0 运行时实证 3.6556.2)。
|
||||
*
|
||||
* 官方在附件增删/重排时用 document.startViewTransition() 包裹 DOM 更新,
|
||||
* 配合 cds-theme.css 中的 view-transition-name/match-element 规则,
|
||||
* 使被增删的卡片产生共享元素动画。
|
||||
*
|
||||
* 用法:
|
||||
* const updateAttachments = withAttachmentTransition(() => setFiles(next));
|
||||
*
|
||||
* 不支持 startViewTransition 的环境(旧浏览器)直接同步执行更新。
|
||||
*/
|
||||
export function withAttachmentTransition(update: () => void): void {
|
||||
if (typeof document !== "undefined" && typeof (document as any).startViewTransition === "function") {
|
||||
(document as any).startViewTransition(update);
|
||||
} else {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
export type FileCategory = "code" | "pdf" | "image" | "text" | "archive" | "general";
|
||||
|
||||
export interface AttachmentCardProps {
|
||||
@@ -52,6 +72,7 @@ function truncateFileName(fileName: string, maxLen: number = 18): { head: string
|
||||
}
|
||||
|
||||
export function AttachmentCard({
|
||||
id,
|
||||
name,
|
||||
size,
|
||||
type,
|
||||
@@ -96,7 +117,10 @@ export function AttachmentCard({
|
||||
className={`group/tile relative inline-flex items-center gap-2 px-2.5 py-1 rounded-[10px] bg-surface-2 border border-neutral-200/80 dark:border-neutral-700/80 shadow-field select-none transition-all ${
|
||||
uploading ? "animate-pulse opacity-75" : "hover:border-neutral-300 dark:hover:border-neutral-600"
|
||||
} ${className}`}
|
||||
style={{ animation: "file-thumbnail-grow-in 250ms cubic-bezier(.165, .84, .44, 1)" }}
|
||||
style={{
|
||||
animation: "file-thumbnail-grow-in 250ms cubic-bezier(.165, .84, .44, 1)",
|
||||
...(id != null ? { "--cds-attachment-vt-name": `attachment-${id}` } as React.CSSProperties : {}),
|
||||
}}
|
||||
>
|
||||
<span className={`px-1 py-0.5 rounded ${badgeStyle.bg} ${badgeStyle.text} font-mono text-[9px] font-bold shrink-0 tracking-wide`}>
|
||||
{inferredBadge}
|
||||
@@ -135,6 +159,7 @@ export function AttachmentCard({
|
||||
} ${className}`}
|
||||
style={{
|
||||
animation: "file-thumbnail-grow-in 250ms cubic-bezier(.165, .84, .44, 1)",
|
||||
...(id != null ? { "--cds-attachment-vt-name": `attachment-${id}` } as React.CSSProperties : {}),
|
||||
}}
|
||||
>
|
||||
{/* 顶部徽标与文件类型 */}
|
||||
|
||||
+525
-49
@@ -94,29 +94,54 @@
|
||||
--cds-surface-popover: var(--cds-gray-0); /* #ffffff 悬浮 Popover 表面 */
|
||||
|
||||
/* 4. Alpha 透明度与边框色系 */
|
||||
--cds-alpha-0: hsla(0, 0%, 8%, 0);
|
||||
--cds-alpha-1: hsla(0, 0%, 8%, 0.05); /* Ghost hover, skeleton */
|
||||
--cds-alpha-2: hsla(0, 0%, 8%, 0.10); /* Standard border */
|
||||
--cds-alpha-3: hsla(0, 0%, 8%, 0.20); /* Border strong */
|
||||
--cds-alpha-4: hsla(0, 0%, 8%, 0.35); /* Heavy divider */
|
||||
/* 4. 透明度阶梯 (Alpha Ramps) —— 单表达式自动翻转 (官方 vanilla 实证)
|
||||
neutral-900 在暗色下解析为 gray-0(白),整个阶梯自动变白,
|
||||
无需暗色块覆盖(原来双块硬编码的 0.09/0.18/0.30 与官方 10/20/35% 有偏差) */
|
||||
--cds-alpha-0: hsl(from var(--cds-neutral-900) h s l / 0%);
|
||||
--cds-alpha-1: hsl(from var(--cds-neutral-900) h s l / 5%); /* Ghost hover, skeleton */
|
||||
--cds-alpha-2: hsl(from var(--cds-neutral-900) h s l / 10%); /* Standard border */
|
||||
--cds-alpha-3: hsl(from var(--cds-neutral-900) h s l / 20%); /* Border strong */
|
||||
--cds-alpha-4: hsl(from var(--cds-neutral-900) h s l / 35%); /* Heavy divider */
|
||||
|
||||
--cds-border: var(--cds-alpha-2);
|
||||
--cds-border-strong: var(--cds-alpha-3);
|
||||
--cds-ring-hairline: 1px;
|
||||
|
||||
/* 4.5 主题阶梯 (Theme Ramps) —— neutral-N 在暗色下翻转为 gray-(900-N)
|
||||
REA v6.8.0 + 运行时实证:官方 --neutral-N 机制,用于「与页面形成对比」的
|
||||
文本/边框/填充;需要固定像素值时直接引用 --cds-gray-*。
|
||||
light: neutral-0=gray-0(#fff) … neutral-900=gray-900(#0b0b0b)
|
||||
dark : 完全倒转,neutral-900=gray-0(#fff) */
|
||||
--cds-neutral-0: var(--cds-gray-0);
|
||||
--cds-neutral-50: var(--cds-gray-50);
|
||||
--cds-neutral-100: var(--cds-gray-100);
|
||||
--cds-neutral-200: var(--cds-gray-200);
|
||||
--cds-neutral-300: var(--cds-gray-300);
|
||||
--cds-neutral-400: var(--cds-gray-400);
|
||||
--cds-neutral-500: var(--cds-gray-500);
|
||||
--cds-neutral-600: var(--cds-gray-600);
|
||||
--cds-neutral-700: var(--cds-gray-700);
|
||||
--cds-neutral-750: var(--cds-gray-750);
|
||||
--cds-neutral-800: var(--cds-gray-800);
|
||||
--cds-neutral-850: var(--cds-gray-850);
|
||||
--cds-neutral-900: var(--cds-gray-900);
|
||||
|
||||
/* 5. 双环浮动气泡架构 (Dual-Ring Architecture - 浅色模式) */
|
||||
--cds-ring-inner: 0px;
|
||||
--cds-ring-outer: 1px;
|
||||
--cds-ring-color: var(--cds-border);
|
||||
|
||||
/* 6. 阴影系统 (Elevation Shadows) */
|
||||
--cds-shadow-color: rgba(0, 0, 0, 0.08);
|
||||
--cds-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05), 0 2px 8px 0 var(--cds-shadow-color);
|
||||
--cds-shadow-md: 0 2px 4px 0 rgba(0, 0, 0, 0.05), 0 6px 16px 0 var(--cds-shadow-color);
|
||||
--cds-shadow-lg: 0 4px 8px 0 rgba(0, 0, 0, 0.06), 0 12px 28px -2px var(--cds-shadow-color);
|
||||
--cds-shadow-popover: inset 0 0 0 var(--cds-ring-inner) var(--cds-ring-color),
|
||||
0 0 0 var(--cds-ring-outer) var(--cds-ring-color),
|
||||
0 8px 24px rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||
/* 6. 阴影系统 (Elevation Shadows)
|
||||
运行时实证 (3.6556.2):shadow-* 的偏移层用 hsl(from gray-900 …) 而非固定 rgba,
|
||||
因此暗色下自然跟随。shadow-popover 官方为纯双层投影,不含 inset ring。 */
|
||||
--cds-shadow-color: hsl(from var(--cds-gray-900) h s l / 8%);
|
||||
--cds-shadow-sm: 0 1px 2px 0 hsl(from var(--cds-gray-900) h s l / 6%),
|
||||
0 2px 8px 0 var(--cds-shadow-color);
|
||||
--cds-shadow-md: 0 2px 4px 0 hsl(from var(--cds-gray-900) h s l / 7%),
|
||||
0 6px 16px 0 var(--cds-shadow-color);
|
||||
--cds-shadow-lg: 0 4px 8px 0 hsl(from var(--cds-gray-900) h s l / 8%),
|
||||
0 12px 28px -2px var(--cds-shadow-color);
|
||||
--cds-shadow-popover: 0 8px 24px rgb(0 0 0 / 0.12), 0 2px 6px rgb(0 0 0 / 0.08);
|
||||
|
||||
--cds-panel-shadow: 0 0 0 1px var(--cds-border), var(--cds-shadow-sm);
|
||||
--cds-panel-shadow-hover: 0 0 0 1px var(--cds-border-strong), 0 4px 16px -2px rgba(0, 0, 0, 0.09);
|
||||
@@ -130,16 +155,20 @@
|
||||
--cds-focus-shadow: inset 0 0 0 1px var(--cds-surface-1), 0 0 0 1.5px var(--cds-clay), 0 0 6px 1px rgba(217, 119, 87, 0.25);
|
||||
--cds-shadow-drop-glow: inset 0 0 24px -2px rgba(217, 119, 87, 0.20), inset 0 0 0 2px var(--cds-clay);
|
||||
|
||||
/* 9. 交互填充与文本色阶 */
|
||||
/* 9. 交互填充与文本色阶
|
||||
REA v6.8.0 + 运行时实证 (Claude Desktop 3.6556.2, Electron 44.9.3):
|
||||
官方链为 neutral-N,它在暗色下翻转为 gray-(900-N)。
|
||||
light: text-primary=#0b0b0b(gray-900) / secondary=#52514e(gray-600) / muted=#898781(gray-400)
|
||||
dark : text-primary=#f0efec(gray-50) / secondary=#c3c2b7(gray-200) / muted=#898781(gray-400 不变) */
|
||||
--cds-fill-ghost-hover: var(--cds-alpha-1);
|
||||
--cds-fill-ghost-selected: var(--cds-alpha-2);
|
||||
--cds-fill-secondary: var(--cds-alpha-2);
|
||||
--cds-fill-secondary-hover: rgba(21, 21, 21, 0.14);
|
||||
--cds-fill-secondary: hsl(0 0% 100% / 0.1); /* 官方实证:浅色为白 10% */
|
||||
--cds-fill-secondary-hover: var(--cds-alpha-1);
|
||||
|
||||
--cds-text-primary: #151515;
|
||||
--cds-text-secondary: #525252;
|
||||
--cds-text-muted: #8a8a8a;
|
||||
--cds-bg-tint-blue: #f0f4f8;
|
||||
--cds-text-primary: var(--cds-neutral-900); /* #0b0b0b */
|
||||
--cds-text-secondary: var(--cds-neutral-600); /* #52514e */
|
||||
--cds-text-muted: var(--cds-neutral-400); /* #898781,暗色下同为 gray-400 */
|
||||
--cds-bg-tint-blue: rgb(from var(--cds-blue-100, #cde2fb) calc((r - 163.8) / .35) calc((g - 163.8) / .35) calc((b - 163.15) / .35) / 35%);
|
||||
--cds-segmented-track: rgba(0, 0, 0, 0.06);
|
||||
--cds-segmented-thumb: #ffffff;
|
||||
|
||||
@@ -152,7 +181,8 @@
|
||||
--radius-xl: .75rem; /* 12px */
|
||||
--radius-2xl: 1rem; /* 16px */
|
||||
--radius-3xl: 1.5rem; /* 24px */
|
||||
--cds-radius-composer: 20px;
|
||||
--cds-radius: calc(.5rem * var(--cds-rem-scale, 1)); /* 8px 基础圆角,官方实证 */
|
||||
--cds-radius-composer: calc(.75rem * var(--cds-rem-scale, 1)); /* 12px;comfortable 14px,见密度块 */
|
||||
--df-radius-pill: 999px;
|
||||
--radius: var(--cds-radius, 8px);
|
||||
--radius-card: calc(var(--cds-radius, 8px) + calc(.25rem * var(--cds-rem-scale, 1)));
|
||||
@@ -165,8 +195,20 @@
|
||||
--cds-bg-neutral-hover: var(--cds-alpha-2);
|
||||
|
||||
/* 强调填充别名链 (REA 取证: .cds-root 作用域, Claude 2.42304.2)
|
||||
--cds-fill-accent → --cds-role-accent-fill → --cds-role-accent-450 → --cds-blue-450 */
|
||||
--cds-fill-accent → --cds-role-accent-fill → --cds-role-accent-450 → --cds-blue-450
|
||||
|
||||
accent 角色完整色阶 (REA v6.8.0 + 运行时实证 3.6556.2):
|
||||
accent 是独立于 brand(clay) 的蓝色角色。运行时实测:
|
||||
--border-accent = role-accent-250 = blue-250 = #86b6ef (light)
|
||||
--text-accent = role-accent-600 = blue-600 = #184f95 (light)
|
||||
--fill-accent = role-accent-450 = blue-450 = #2a78d6
|
||||
dark: border-accent→#0d366b, text-accent→#6da7ec */
|
||||
--cds-blue-100: #cde2fb;
|
||||
--cds-blue-250: #86b6ef;
|
||||
--cds-blue-450: #2a78d6;
|
||||
--cds-blue-600: #184f95;
|
||||
--cds-role-accent-250: var(--cds-blue-250);
|
||||
--cds-role-accent-600: var(--cds-blue-600);
|
||||
--cds-role-accent-450: var(--cds-blue-450);
|
||||
--cds-role-accent-fill: var(--cds-role-accent-450);
|
||||
--cds-fill-accent: var(--cds-role-accent-fill);
|
||||
@@ -241,12 +283,26 @@
|
||||
--cds-bg-hover: var(--cds-fill-ghost-hover);
|
||||
--cds-bg-selected: var(--cds-fill-ghost-selected);
|
||||
--cds-border-subtle: var(--cds-alpha-2);
|
||||
--cds-border-accent: var(--cds-clay);
|
||||
--cds-fill-brand: var(--brand);
|
||||
--cds-fill-brand-hover: var(--cds-clay-hover);
|
||||
|
||||
/* 8. 角色系统 (Purpose Roles) —— 组件只消费此层
|
||||
运行时实证 (3.6556.2, Electron 44.9.3):
|
||||
border-accent: light #86b6ef (blue-250) / dark #0d366b
|
||||
text-accent: light #184f95 (blue-600) / dark #6da7ec
|
||||
fill-brand: #c6613f 双模同值(= clay-emphasized)
|
||||
fill-brand-hover: #d97757 双模同值(= clay;官方 hover 向上变浅)
|
||||
focus-shadow: 0 0 0 1px #2a78d6 (blue) + bg-accent glow,非 clay */
|
||||
--cds-border-accent: var(--cds-role-accent-250);
|
||||
--cds-fill-brand: var(--cds-clay-emphasized); /* #c6613f */
|
||||
--cds-fill-brand-hover: var(--cds-clay); /* #d97757 */
|
||||
--cds-fill-brand-subtle: var(--cds-clay-light);
|
||||
--cds-fill-danger: #ef4444;
|
||||
--cds-text-accent: var(--cds-clay);
|
||||
--cds-text-accent: var(--cds-role-accent-600); /* #184f95 蓝,非 clay */
|
||||
--cds-bg-accent: var(--cds-blue-100); /* focus glow 的颜色源,运行时实证 #cde2fb */
|
||||
--cds-fill-primary: var(--cds-neutral-900); /* 4.29.0 实证;失焦态覆盖为 gray-450 */
|
||||
--cds-fill-primary-hover: var(--cds-neutral-750);
|
||||
--cds-focus-shadow: inset 0 0 0 1px var(--cds-page-bg),
|
||||
0 0 0 1px var(--cds-fill-accent),
|
||||
0 0 6px 1px var(--cds-bg-accent);
|
||||
|
||||
/* 15. 语义化向后兼容别名与跨宿主通用别名 (Backward-Compatibility & Host Aliases) */
|
||||
--cds-bg-surface-0: var(--cds-surface-0);
|
||||
@@ -264,6 +320,71 @@
|
||||
--cds-radius-code: 12px;
|
||||
--cds-radius-btn-md: 8px;
|
||||
--cds-radius-btn-xs: 6px;
|
||||
--cds-page-bg: var(--cds-surface-0);
|
||||
--cds-clay-emphasized: #c6613f; /* 运行时实证:双模同值,非 clay-hover 派生 */
|
||||
--cds-keycap-radius: var(--radius-sm, 4px); /* 运行时+官方 vanilla:4px,非 radius-xs 2px */
|
||||
--cds-checkbox-radius: 5px; /* 官方 vanilla:5px */
|
||||
--cds-anim-ease: var(--cds-ease-swift);
|
||||
|
||||
/* 17. 尺寸与密度 (Density Tokens) —— 全部乘 --cds-rem-scale(官方 vanilla 实证)
|
||||
桌面端该变量未被赋值,恒走 fallback 1,因此数值与现值一致;
|
||||
但写死 px 会使宿主设置 --cds-rem-scale 时密度系统失效。
|
||||
compact 为默认;[data-density=comfortable] 覆盖(见暗色块后的密度块) */
|
||||
--cds-gap-xl: calc(2.5rem * var(--cds-rem-scale, 1)); /* 40px,官方实证 */
|
||||
--cds-icon: calc(1.25rem * var(--cds-rem-scale, 1)); /* 20px */
|
||||
--cds-h-control--xs: calc(1.5rem * var(--cds-rem-scale, 1)); /* 24px */
|
||||
--cds-h-control--sm: calc(1.75rem * var(--cds-rem-scale, 1)); /* 28px */
|
||||
--cds-h-control: calc(2rem * var(--cds-rem-scale, 1)); /* 32px */
|
||||
--cds-h-control--lg: calc(2.5rem * var(--cds-rem-scale, 1)); /* 40px */
|
||||
--cds-h-control-nested: calc(1.375rem * var(--cds-rem-scale, 1)); /* 22px,官方实证 */
|
||||
--cds-pad-xs: calc(.375rem * var(--cds-rem-scale, 1));
|
||||
--cds-pad-sm: calc(.5rem * var(--cds-rem-scale, 1));
|
||||
--cds-pad-md: calc(.75rem * var(--cds-rem-scale, 1));
|
||||
--cds-pad-lg: calc(1rem * var(--cds-rem-scale, 1));
|
||||
--cds-pad-xl: calc(1.5rem * var(--cds-rem-scale, 1));
|
||||
|
||||
/* 18. 密度双模 (Density Modes) —— 官方 vanilla 实证:
|
||||
compact 为默认(无显式块);comfortable 覆盖尺寸 token */
|
||||
&[data-density=comfortable],
|
||||
[data-density=comfortable] & {
|
||||
--cds-radius--xs: calc(.375rem * var(--cds-rem-scale, 1)); /* 6px */
|
||||
--cds-radius--sm: calc(.4375rem * var(--cds-rem-scale, 1)); /* 7px */
|
||||
--cds-radius--lg: calc(.625rem * var(--cds-rem-scale, 1)); /* 10px */
|
||||
--cds-radius-composer: calc(.875rem * var(--cds-rem-scale, 1)); /* 14px */
|
||||
--cds-h-control--xs: calc(1.5rem * var(--cds-rem-scale, 1));
|
||||
--cds-h-control--sm: calc(1.75rem * var(--cds-rem-scale, 1));
|
||||
--cds-h-control--lg: calc(2.5rem * var(--cds-rem-scale, 1));
|
||||
--cds-h-control-nested--xs: calc(1.125rem * var(--cds-rem-scale, 1));
|
||||
--cds-h-control-nested--sm: calc(1.25rem * var(--cds-rem-scale, 1));
|
||||
--cds-h-control-nested--lg: calc(1.75rem * var(--cds-rem-scale, 1));
|
||||
--cds-icon--xs: calc(1rem * var(--cds-rem-scale, 1));
|
||||
--cds-icon--sm: calc(1rem * var(--cds-rem-scale, 1));
|
||||
--cds-icon--lg: calc(1.5rem * var(--cds-rem-scale, 1));
|
||||
}
|
||||
|
||||
/* 19. 骨架屏与扫光动效变量 (Skeleton & Shimmer Motion Tokens) */
|
||||
--cds-skel-i: 0;
|
||||
--cds-skeleton-base: var(--cds-alpha-1); /* hsl(from neutral-900 / 5%) */
|
||||
--cds-skeleton-sheen: var(--cds-alpha-1); /* 与 base 同值,运行时实证 */
|
||||
|
||||
/* 20. 字体字重 role 链 (Font Weight Roles - 4.4212.0 实证)
|
||||
字重优先跟随「控件 / 标题」语义角色,再回落到字面权重。
|
||||
默认 initial 表示不覆盖,由使用方按需设置。
|
||||
.font-medium → var(--cds-font-weight-control, var(--cds-font-weight-medium))
|
||||
.font-semibold → var(--cds-font-axis-wght-header, var(--cds-font-axis-wght-semibold)) */
|
||||
--cds-font-weight-medium: 500;
|
||||
--cds-font-weight-control: initial;
|
||||
--cds-font-weight-header: initial;
|
||||
--cds-font-weight-body: initial;
|
||||
--cds-font-wght-control: initial;
|
||||
--cds-font-wght-header: initial;
|
||||
--cds-font-axis-wght-control: initial;
|
||||
--cds-font-axis-wght-header: initial;
|
||||
|
||||
/* 21. 吸边 (Sticky Edge) 默认值 —— 官方 @property 注册项的运行时初值 */
|
||||
--cds-sticky-edge-active: 0;
|
||||
--cds-sticky-edge-start: 0px;
|
||||
--cds-sticky-edge-end: 0px;
|
||||
}
|
||||
|
||||
/* 高分屏视网膜物理发丝细线补偿 */
|
||||
@@ -299,11 +420,26 @@
|
||||
--cds-role-accent-fill: var(--cds-role-accent-450);
|
||||
--cds-fill-accent: var(--cds-role-accent-fill);
|
||||
|
||||
--cds-alpha-0: hsla(0, 0%, 100%, 0);
|
||||
--cds-alpha-1: hsla(0, 0%, 100%, 0.05);
|
||||
--cds-alpha-2: hsla(0, 0%, 100%, 0.09);
|
||||
--cds-alpha-3: hsla(0, 0%, 100%, 0.18);
|
||||
--cds-alpha-4: hsla(0, 0%, 100%, 0.30);
|
||||
/* alpha 阶梯不再需要暗色覆盖::root 的单表达式随 neutral-900 翻转
|
||||
(此前这里的白色硬编码与浅色块的百分比不一致,已删除) */
|
||||
|
||||
/* 主题阶梯暗色翻转 (Theme Ramp Inversion)
|
||||
运行时实证 (3.6556.2):neutral-N 在暗色下 = gray-(900-N),因此
|
||||
neutral-900 → gray-0(#fff)、neutral-600 → gray-300(#a5a49a)。
|
||||
这是 --cds-text-* 与 --cds-alpha-* 自动翻转的基础。 */
|
||||
--cds-neutral-0: var(--cds-gray-900);
|
||||
--cds-neutral-50: var(--cds-gray-850);
|
||||
--cds-neutral-100: var(--cds-gray-800);
|
||||
--cds-neutral-200: var(--cds-gray-700);
|
||||
--cds-neutral-300: var(--cds-gray-600);
|
||||
--cds-neutral-400: var(--cds-gray-400); /* 实证:暗色下仍为 gray-400 #898781 */
|
||||
--cds-neutral-500: var(--cds-gray-400);
|
||||
--cds-neutral-600: var(--cds-gray-200); /* 实证:#c3c2b7 */
|
||||
--cds-neutral-700: var(--cds-gray-100);
|
||||
--cds-neutral-750: var(--cds-gray-150);
|
||||
--cds-neutral-800: var(--cds-gray-50);
|
||||
--cds-neutral-850: var(--cds-gray-50); /* 实证:#f0efec */
|
||||
--cds-neutral-900: var(--cds-gray-0);
|
||||
|
||||
|
||||
/* 会话气泡背景在暗色下的取值。
|
||||
@@ -323,12 +459,14 @@
|
||||
--cds-ring-outer: 0px;
|
||||
--cds-ring-color: rgba(255, 255, 255, 0.10);
|
||||
|
||||
--cds-shadow-color: rgba(0, 0, 0, 0.40);
|
||||
--cds-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.4), 0 2px 8px 0 var(--cds-shadow-color);
|
||||
--cds-shadow-md: 0 2px 4px 0 rgba(0, 0, 0, 0.4), 0 6px 16px 0 var(--cds-shadow-color);
|
||||
--cds-shadow-lg: 0 4px 8px 0 rgba(0, 0, 0, 0.5), 0 16px 36px -2px var(--cds-shadow-color);
|
||||
--cds-shadow-popover: inset 0 0 0 1px rgba(255, 255, 255, 0.10),
|
||||
0 8px 28px rgba(0, 0, 0, 0.65), 0 2px 8px rgba(0, 0, 0, 0.45);
|
||||
--cds-shadow-color: rgba(0, 0, 0, 0.24);
|
||||
--cds-shadow-sm: 0 1px 2px 0 hsl(from var(--cds-gray-900) h s l / 8%),
|
||||
0 2px 8px 0 var(--cds-shadow-color);
|
||||
--cds-shadow-md: 0 2px 4px 0 hsl(from var(--cds-gray-900) h s l / 7%),
|
||||
0 6px 16px 0 var(--cds-shadow-color);
|
||||
--cds-shadow-lg: 0 4px 8px 0 hsl(from var(--cds-gray-900) h s l / 8%),
|
||||
0 12px 28px -2px var(--cds-shadow-color);
|
||||
--cds-shadow-popover: 0 8px 24px rgb(0 0 0 / 0.32), 0 2px 6px rgb(0 0 0 / 0.20);
|
||||
|
||||
--cds-panel-shadow: 0 0 0 1px var(--cds-border), var(--cds-shadow-sm);
|
||||
--cds-panel-shadow-hover: 0 0 0 1px var(--cds-border-strong), 0 6px 20px rgba(0, 0, 0, 0.6);
|
||||
@@ -337,17 +475,19 @@
|
||||
--cds-shadow-composer-hover: 0 4px 24px rgba(0, 0, 0, 0.55), inset 0 0 0 1px var(--cds-border-strong);
|
||||
--cds-shadow-composer-focus: 0 4px 24px rgba(0, 0, 0, 0.65), inset 0 0 0 1px rgba(255, 255, 255, 0.22), 0 0 0 1px rgba(255, 255, 255, 0.22);
|
||||
|
||||
--cds-focus-shadow: inset 0 0 0 1px var(--cds-surface-1), 0 0 0 1.5px var(--cds-clay), 0 0 8px 2px rgba(217, 119, 87, 0.35);
|
||||
--cds-shadow-drop-glow: inset 0 0 24px -2px rgba(217, 119, 87, 0.30), inset 0 0 0 2px var(--cds-clay);
|
||||
--cds-focus-shadow: inset 0 0 0 1px var(--cds-page-bg),
|
||||
0 0 0 1px var(--cds-fill-accent),
|
||||
0 0 6px 1px hsl(from var(--cds-blue-600) h s l / 60%);
|
||||
--cds-shadow-drop-glow: inset 0 0 24px -2px hsl(from var(--cds-fill-accent) h s l / 20%);
|
||||
|
||||
--cds-fill-ghost-hover: var(--cds-alpha-1);
|
||||
--cds-fill-ghost-selected: var(--cds-alpha-2);
|
||||
--cds-fill-secondary: var(--cds-alpha-2);
|
||||
--cds-fill-secondary-hover: rgba(255, 255, 255, 0.14);
|
||||
|
||||
--cds-text-primary: #f5f5f5;
|
||||
--cds-text-secondary: #a3a3a3;
|
||||
--cds-text-muted: #666666;
|
||||
--cds-text-primary: var(--cds-neutral-900); /* dark → gray-50 #f0efec */
|
||||
--cds-text-secondary: var(--cds-neutral-600); /* dark → gray-200 #c3c2b7 */
|
||||
--cds-text-muted: var(--cds-neutral-400); /* dark 仍为 gray-400 #898781 */
|
||||
--cds-bg-tint-blue: rgba(255, 255, 255, 0.06);
|
||||
--cds-segmented-track: rgba(255, 255, 255, 0.08);
|
||||
--cds-segmented-thumb: #2c2c2a;
|
||||
@@ -360,12 +500,17 @@
|
||||
--cds-bg-hover: var(--cds-fill-ghost-hover);
|
||||
--cds-bg-selected: var(--cds-fill-ghost-selected);
|
||||
--cds-border-subtle: var(--cds-alpha-2);
|
||||
--cds-border-accent: var(--cds-clay);
|
||||
--cds-fill-brand: var(--brand);
|
||||
--cds-fill-brand-hover: var(--cds-clay-hover);
|
||||
--cds-fill-brand-subtle: rgba(217, 119, 87, 0.15);
|
||||
--cds-fill-danger: #f87171;
|
||||
--cds-text-accent: var(--cds-clay);
|
||||
/* 角色系统暗色重载 —— 运行时实证:
|
||||
border-accent: dark #0d366b (blue-800)
|
||||
text-accent: dark #6da7ec (blue-350)
|
||||
fill-brand / fill-brand-hover 双模同值不重载 */
|
||||
--cds-border-accent: #0d366b;
|
||||
--cds-text-accent: #6da7ec;
|
||||
|
||||
/* 骨架屏暗色 —— 运行时实证:base 与 sheen 同值,
|
||||
hsl(from neutral-900 / 7.5%) → 白色 7.5%(加深而非提亮) */
|
||||
--cds-skeleton-base: hsl(from var(--cds-neutral-900) h s l / 7.5%);
|
||||
--cds-skeleton-sheen: hsl(from var(--cds-neutral-900) h s l / 7.5%);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
@@ -448,12 +593,107 @@
|
||||
100% { opacity: 0; transform: scale(0.92); }
|
||||
}
|
||||
|
||||
/* 8. 骨架屏高级倾斜扫光 (REA 取证:2s linear 105deg) */
|
||||
/* 8. 骨架屏高级倾斜扫光与双模式呼吸 (REA 取证:2s linear 105deg / 2s ease-in-out) */
|
||||
@keyframes cds-skeleton-sweep {
|
||||
0% { transform: translate(-100%); }
|
||||
100% { transform: translate(100%); }
|
||||
}
|
||||
|
||||
@keyframes cds-skeleton-breathe {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
/* 进度条扫光 (4.4212.0 新增) —— 未确定进度指示器的循环光带
|
||||
1.6s cubic-bezier(.65,0,.35,1) infinite;--_progress-dir 控制方向(默认 -1)。 */
|
||||
@keyframes cds-progress-sweep {
|
||||
0% { transform: translateX(calc(var(--_progress-dir, -1) * 100%)); }
|
||||
100% { transform: translateX(calc(var(--_progress-dir, -1) * -166.67%)); }
|
||||
}
|
||||
|
||||
/* 占位揭示淡入 (官方实证:.15s ease-out,配 data-hold=delayed) */
|
||||
@keyframes cds-placeholder-reveal {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* 8.5 附件重排 View Transitions (REA v6.8.0 + 运行时实证 3.6556.2)
|
||||
附件卡增删/重排的共享元素过渡。三层结构:
|
||||
1. 容器 [data-cds-composer-attachments]:整体 opacity/transform 过渡
|
||||
2. 每张卡 [data-cds-attachment]:注册 view-transition-name + class
|
||||
3. 旧 Chromium 回退::active-view-transition-type 分发 root 动画
|
||||
官方原文逐字移植(含新特性 @supports 探测 view-transition-group:contain)。
|
||||
JS 侧需在 document.startViewTransition() 中更新 DOM,并给每张卡
|
||||
--cds-attachment-vt-name: attachment-<id>(唯一名)。 */
|
||||
.cds-root [data-cds-composer-attachments]:has([data-cds-attachment]) {
|
||||
transition: opacity .2s cubic-bezier(.215, .61, .355, 1),
|
||||
transform .2s cubic-bezier(.215, .61, .355, 1);
|
||||
}
|
||||
|
||||
.cds-root [data-cds-composer-attachments] [data-cds-attachment] {
|
||||
--cds-attachment-vt-name: none;
|
||||
transform-origin: 0;
|
||||
view-transition-class: cds-attachment;
|
||||
transition: transform .18s cubic-bezier(.215, .61, .355, 1),
|
||||
opacity .18s cubic-bezier(.215, .61, .355, 1);
|
||||
}
|
||||
|
||||
/* RTL 侧 transform-origin 镜像(官方 :lang 列表) */
|
||||
.cds-root [data-cds-composer-attachments] [data-cds-attachment]:where(
|
||||
:is(:lang(ae),:lang(ar),:lang(arc),:lang(bcc),:lang(bqi),:lang(ckb),:lang(dv),:lang(fa),:lang(glk),
|
||||
:lang(he),:lang(ku),:lang(mzn),:lang(nqo),:lang(pnb),:lang(ps),:lang(sd),:lang(ug),:lang(ur),:lang(yi)),
|
||||
[dir=rtl] *
|
||||
) {
|
||||
transform-origin: 100%;
|
||||
}
|
||||
|
||||
/* 过渡期间才启用命名(避免常驻占用 painting 资源) */
|
||||
:root:active-view-transition-type(cds-attachments)
|
||||
[data-cds-composer-attachments] [data-cds-attachment] {
|
||||
view-transition-name: var(--cds-attachment-vt-name);
|
||||
}
|
||||
|
||||
/* Chromium 141+ 路径:match-element 自动跟随元素几何,无需逐卡命名 */
|
||||
@supports selector(::view-transition-group-children(.x)) and (view-transition-group: contain) {
|
||||
:root:active-view-transition-type(cds-attachments)
|
||||
[data-cds-composer-attachments-clip][data-cds-attachments-clipping] {
|
||||
view-transition-name: match-element;
|
||||
view-transition-class: cds-attachments-clip;
|
||||
view-transition-group: contain;
|
||||
}
|
||||
:root:active-view-transition-type(cds-attachments)::view-transition-group-children(.cds-attachments-clip) {
|
||||
overflow: clip visible;
|
||||
}
|
||||
}
|
||||
|
||||
/* 入场初始态:新附件从下方缩放浮入 */
|
||||
@starting-style {
|
||||
.cds-root [data-cds-composer-attachments]:has([data-cds-attachment]:not([data-cds-attachment-restored])) {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
.cds-root [data-cds-composer-attachments] [data-cds-attachment]:not([data-cds-attachment-restored]) {
|
||||
opacity: 0;
|
||||
transform: scale(.92) translateY(4px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 过渡路由:root 零动画,只让附件组动 */
|
||||
:root:active-view-transition-type(cds-attachments) {
|
||||
view-transition-name: none;
|
||||
}
|
||||
:root:active-view-transition-type(cds-attachments)::view-transition-group(root) {
|
||||
animation-duration: 0s;
|
||||
}
|
||||
:root:active-view-transition-type(cds-attachments)::view-transition-group(.cds-attachment) {
|
||||
animation-duration: .18s;
|
||||
animation-timing-function: cubic-bezier(.215, .61, .355, 1);
|
||||
}
|
||||
/* 移除的旧快照:缩至一半并淡出(官方实证:仅当该附件无新快照时触发) */
|
||||
:root:active-view-transition-type(cds-attachments)::view-transition-old(.cds-attachment):only-child {
|
||||
animation: .13s cubic-bezier(.55, .055, .675, .19) forwards cds-attachment-out;
|
||||
}
|
||||
|
||||
/* 9. 陶土呼吸指示灯 (REA 取证:3s ease-in-out) */
|
||||
@keyframes dframe-clay-breathe {
|
||||
0%, 100% { filter: brightness(0.96); transform: scale(0.86); }
|
||||
@@ -478,6 +718,15 @@
|
||||
100% { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
/* 11.5 附件移除退场 (官方实证:to { opacity:0; scale(.5) },
|
||||
用于 View Transition 旧快照的 exit 动画) */
|
||||
@keyframes cds-attachment-out {
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(.5);
|
||||
}
|
||||
}
|
||||
|
||||
/* 12. 按钮动态物理阻尼按压与回弹 (REA 取证:.cds-btn-squish) */
|
||||
.cds-btn-squish {
|
||||
--cds-btn-press-sx: 0.975;
|
||||
@@ -512,6 +761,21 @@
|
||||
background: var(--cds-clay);
|
||||
animation: 3s ease-in-out infinite dframe-clay-breathe;
|
||||
}
|
||||
.status-dot[data-kind="pulse"],
|
||||
.cds-dot-pulse {
|
||||
background: var(--cds-fill-brand, var(--cds-clay));
|
||||
animation: 1.8s infinite cds-dot-pulse;
|
||||
}
|
||||
|
||||
.cds-message-fade-up {
|
||||
animation: message-fade-up 0.45s cubic-bezier(0.34, 1.3, 0.64, 1);
|
||||
}
|
||||
.cds-saved-check-decay {
|
||||
animation: saved-check-decay 0.35s cubic-bezier(0.165, 0.84, 0.44, 1) 2.2s forwards;
|
||||
}
|
||||
.cds-thumbnail-grow {
|
||||
animation: file-thumbnail-grow-in 250ms cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
通用实用类 (CDS Utility Classes)
|
||||
@@ -564,6 +828,182 @@
|
||||
border-width: var(--cds-ring-hairline);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
骨架屏系统 (CDS Skeleton & Dual-Mode Motion - REA 取证)
|
||||
========================================================================== */
|
||||
|
||||
/* 圆角默认值零特异性,不覆盖 Tailwind 的 rounded-* 工具类
|
||||
(运行时实证:官方头像骨架用 rounded-full,此前直接写会把它压成 4px) */
|
||||
:where(.cds-skeleton) {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.cds-skeleton {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: var(--cds-skeleton-base);
|
||||
}
|
||||
|
||||
/* 官方双层结构 (REA v6.8.0 + 运行时实证 3.6556.2):
|
||||
[data-cds=SkeletonGroup] ← 组容器:data-motion / data-hold
|
||||
└ [data-cds=Skeleton] ← 骨架块:--cds-skel-i 级联索引
|
||||
└ .cds-skel-sweep-band ← 扫光光带(真实元素,非 ::after)
|
||||
对齐层(本仓库自定义封装)仍支持 .cds-skeleton 类,但不再将
|
||||
.cds-skeleton-sweep 写入与伪元素同一规则 —— 该独立类曾使元素自身
|
||||
变为 position:absolute + translate(-100%) 而移出视口(issue #25 实测缺陷)。 */
|
||||
.cds-skel-sweep-clip {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 双模呼吸 (Dual-Mode Breathe) —— 官方实证:wave 与 lockstep 都用
|
||||
cds-skeleton-breathe(透明度呼吸),扫光由独立光带承担,与 motion 无关。
|
||||
wave 挂每个骨架块,带级联;lockstep 挂组容器,同步。 */
|
||||
.cds-skeleton[data-motion="lockstep"],
|
||||
[data-motion="lockstep"] .cds-skeleton,
|
||||
[data-motion="lockstep"][data-cds="SkeletonGroup"] {
|
||||
animation: cds-skeleton-breathe 2s ease-in-out var(--cds-skel-reveal-end, 0s) infinite;
|
||||
}
|
||||
.cds-skeleton[data-motion="wave"],
|
||||
[data-motion="wave"] .cds-skeleton,
|
||||
[data-motion="wave"][data-cds="SkeletonGroup"] [data-cds="Skeleton"] {
|
||||
animation: cds-skeleton-breathe 2s ease-in-out
|
||||
calc(var(--cds-skel-reveal-end, 0s) + var(--cds-skel-i, 0) * 90ms) infinite;
|
||||
}
|
||||
|
||||
/* data-hold 三态揭示时序 (官方实证) ——
|
||||
缺省:reveal-end = placeholder-reveal-delay(.5s) + .15s - elapsed
|
||||
none:立即
|
||||
delayed:额外叠加 cds-placeholder-reveal 淡入动画
|
||||
揭示完成后(data-hold + data-motion 同时存在)动画压至 .01ms 且隐藏扫光带 */
|
||||
[data-cds="SkeletonGroup"][data-hold] {
|
||||
--cds-skel-elapsed: 0s;
|
||||
--cds-skel-i: 0;
|
||||
--cds-skel-settled: 0s;
|
||||
--cds-skel-reveal-end: calc(var(--placeholder-reveal-delay, .5s) + .15s - var(--cds-skel-elapsed));
|
||||
}
|
||||
[data-cds="SkeletonGroup"][data-hold="none"] {
|
||||
--cds-skel-reveal-end: calc(0s - var(--cds-skel-elapsed));
|
||||
}
|
||||
[data-cds="SkeletonGroup"][data-hold="delayed"] {
|
||||
animation: cds-placeholder-reveal .15s ease-out
|
||||
calc(var(--placeholder-reveal-delay, .5s) - var(--cds-skel-elapsed) - var(--cds-skel-settled))
|
||||
backwards;
|
||||
}
|
||||
[data-cds="SkeletonGroup"][data-hold][data-motion],
|
||||
[data-cds="SkeletonGroup"][data-hold][data-motion] [data-cds="Skeleton"],
|
||||
[data-hold][data-motion] .cds-skeleton,
|
||||
[data-hold][data-motion].cds-skeleton {
|
||||
animation-duration: .01ms;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
[data-cds="SkeletonGroup"][data-hold][data-motion] .cds-skel-sweep-band,
|
||||
[data-hold][data-motion] .cds-skel-sweep-band,
|
||||
[data-hold][data-motion] .cds-skeleton::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 扫光光带:独立元素,与 data-motion 无关;
|
||||
animation-delay 用 --cds-skel-reveal-end(揭示终点)而非仅级联索引 */
|
||||
.cds-skel-sweep-band,
|
||||
.cds-skeleton-sweep::after,
|
||||
.cds-skeleton[data-motion="wave"]::after,
|
||||
.cds-skeleton:not([data-motion])::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform: translate(-100%);
|
||||
background: linear-gradient(
|
||||
105deg,
|
||||
transparent 35%,
|
||||
var(--cds-skeleton-sheen) 50%,
|
||||
transparent 65%
|
||||
);
|
||||
animation: cds-skeleton-sweep 2s linear var(--cds-skel-reveal-end, 0s) infinite backwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* RTL 镜像适配:官方用 :lang() 精确语言列表 + animation-direction:reverse。
|
||||
仅翻转渐变角度不翻转动画方向时,扫光仍从左向右(issue #25 实测缺陷)。 */
|
||||
:dir(rtl) .cds-skel-sweep-band,
|
||||
[dir="rtl"] .cds-skel-sweep-band,
|
||||
[dir="rtl"] .cds-skeleton-sweep::after,
|
||||
[dir="rtl"] .cds-skeleton[data-motion="wave"]::after,
|
||||
[dir="rtl"] .cds-skeleton:not([data-motion])::after,
|
||||
[dir="rtl"] .cds-skel-sweep-band,
|
||||
[dir="rtl"] .cds-skel-sweep-clip:where(:lang(ae),:lang(ar),:lang(arc),:lang(bcc),:lang(bqi),:lang(ckb),:lang(dv),:lang(fa),:lang(glk),:lang(he),:lang(ku),:lang(mzn),:lang(nqo),:lang(pnb),:lang(ps),:lang(sd),:lang(ug),:lang(ur),:lang(yi)) .cds-skel-sweep-band {
|
||||
background-image: linear-gradient(
|
||||
255deg,
|
||||
transparent 35%,
|
||||
var(--cds-skeleton-sheen) 50%,
|
||||
transparent 65%
|
||||
);
|
||||
animation-direction: reverse;
|
||||
}
|
||||
|
||||
/* lockstep 呼吸模式下禁用扫光伪元素以避免双重动画冲突 */
|
||||
.cds-skeleton[data-motion="lockstep"]::after,
|
||||
[data-motion="lockstep"] .cds-skeleton::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 进度条扫光类 (4.4212.0) */
|
||||
.cds-progress-sweep {
|
||||
opacity: .5;
|
||||
width: 60%;
|
||||
animation: 1.6s cubic-bezier(.65, 0, .35, 1) infinite cds-progress-sweep;
|
||||
-webkit-mask-image: linear-gradient(90deg, #0000, #000 40% 60%, #0000);
|
||||
mask-image: linear-gradient(90deg, #0000, #000 40% 60%, #0000);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.cds-progress-sweep { margin-inline-start: 20%; animation: none; }
|
||||
}
|
||||
.cds-root:where([data-reduce-motion=true], [data-reduce-motion=true] *) .cds-progress-sweep {
|
||||
margin-inline-start: 20%;
|
||||
animation: none;
|
||||
}
|
||||
@media (forced-colors: active) {
|
||||
.cds-progress-sweep.cds-progress-sweep { opacity: 1; background-color: Highlight; }
|
||||
}
|
||||
|
||||
/* 减少动效双轨兕底 (官方实证:@media 系统偏好 + [data-reduce-motion] 应用开关)
|
||||
此前骨架屏动画在任意条件下均无限循环,不响应系统设置 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.cds-skeleton,
|
||||
.cds-skeleton[data-motion],
|
||||
[data-motion] .cds-skeleton,
|
||||
[data-cds="SkeletonGroup"][data-motion] [data-cds="Skeleton"] {
|
||||
animation-duration: .01ms;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
.cds-skel-sweep-band,
|
||||
.cds-skeleton::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.cds-root:where([data-reduce-motion=true], [data-reduce-motion=true] *) .cds-skeleton,
|
||||
.cds-root:where([data-reduce-motion=true], [data-reduce-motion=true] *) [data-motion] .cds-skeleton {
|
||||
animation-duration: .01ms;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
.cds-root:where([data-reduce-motion=true], [data-reduce-motion=true] *) .cds-skel-sweep-band,
|
||||
.cds-root:where([data-reduce-motion=true], [data-reduce-motion=true] *) .cds-skeleton::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 4.29.0 新增:窗口失焦去色 (Window-Inactive Desaturation)
|
||||
客户端在 [data-window-active=false] 时把 accent 角色全系映射到 gray,
|
||||
并把 Button/Group/SplitDropdownButton 的 brand/primary 填充统一去色。
|
||||
这是 Electron 多窗口应用的「非活动窗口降饱和」机制。 */
|
||||
[data-window-active="false"] :is(.cds-skeleton, [data-cds]) {
|
||||
--cds-fill-brand: var(--cds-gray-450);
|
||||
--cds-fill-brand-hover: var(--cds-gray-400);
|
||||
--cds-fill-primary: var(--cds-gray-450);
|
||||
--cds-fill-primary-hover: var(--cds-gray-400);
|
||||
--cds-fill-accent-hover: var(--cds-role-accent-fill-hover);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Chapter 3: 会话核心流、时间轴与气泡排版引擎 (REA v6.5.0 取证规范)
|
||||
========================================================================== */
|
||||
@@ -755,6 +1195,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
.cds-shimmer-text {
|
||||
background: linear-gradient(
|
||||
105deg,
|
||||
currentColor 0%,
|
||||
currentColor 35%,
|
||||
var(--cds-clay, #d97757) 50%,
|
||||
currentColor 65%,
|
||||
currentColor 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
animation: 3s linear infinite cds-shimmer-text-shine;
|
||||
}
|
||||
|
||||
.cds-shimmer-text-strong {
|
||||
background: linear-gradient(
|
||||
105deg,
|
||||
currentColor 0%,
|
||||
currentColor 35%,
|
||||
var(--cds-clay, #d97757) 50%,
|
||||
currentColor 65%,
|
||||
currentColor 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
animation: 2.5s linear infinite cds-shimmer-text-shine-strong;
|
||||
}
|
||||
|
||||
/* 5. Epitaxy 线性扫光与绘制物理学 (Epitaxy Linear Sweep) */
|
||||
@keyframes epitaxy-linear-sweep {
|
||||
0% { transform: translate(-100%); }
|
||||
@@ -871,6 +1341,12 @@
|
||||
@property --cds-scroll-fade-strip-raw { syntax: "<number>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-strip-active { syntax: "<number>"; inherits: false; initial-value: 0; }
|
||||
|
||||
/* 吸边 (Sticky Edge) —— 客户端 3.6556.2 注册的 3 个 @property
|
||||
(本次从主样式表移植;v3 文档 4.5 节所列缺失项) */
|
||||
@property --cds-sticky-edge-active { syntax: "<number>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-sticky-edge-start { syntax: "<length>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-sticky-edge-end { syntax: "<length>"; inherits: false; initial-value: 0; }
|
||||
|
||||
/* 只推进几何进度,不决定可见性 */
|
||||
@keyframes cds-scroll-fade-top { 0% { --cds-scroll-fade-top-raw: 0px; } to { --cds-scroll-fade-top-raw: var(--cds-scroll-fade-size); } }
|
||||
@keyframes cds-scroll-fade-bottom { 0% { --cds-scroll-fade-bottom-raw: var(--cds-scroll-fade-size); } to { --cds-scroll-fade-bottom-raw: 0px; } }
|
||||
|
||||
@@ -83,7 +83,9 @@ module.exports = {
|
||||
'saved-decay': 'saved-check-decay 0.35s cubic-bezier(0.165, 0.84, 0.44, 1) 2.2s forwards',
|
||||
'clay-breathe': 'dframe-clay-breathe 2.4s infinite ease-in-out',
|
||||
'dot-pulse': 'dframe-dot-pulse 1.2s infinite ease-in-out',
|
||||
'shimmer-sweep': 'cds-skeleton-sweep 1.8s infinite cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
'shimmer-sweep': 'cds-skeleton-sweep 2s linear infinite',
|
||||
'skeleton-sweep': 'cds-skeleton-sweep 2s linear infinite',
|
||||
'skeleton-breathe': 'cds-skeleton-breathe 2s ease-in-out infinite',
|
||||
'thumbnail-grow': 'file-thumbnail-grow-in 250ms cubic-bezier(0.165, 0.84, 0.44, 1)',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -319,8 +319,13 @@
|
||||
}
|
||||
|
||||
@keyframes cds-skeleton-sweep {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
0% { transform: translate(-100%); }
|
||||
100% { transform: translate(100%); }
|
||||
}
|
||||
|
||||
@keyframes cds-skeleton-breathe {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
@keyframes dframe-clay-breathe {
|
||||
@@ -343,25 +348,85 @@
|
||||
50% { opacity: 0.45; }
|
||||
}
|
||||
|
||||
/* 骨架屏扫光条 */
|
||||
/* 骨架屏系统 (CDS Skeleton & Dual-Mode Motion - 对齐 cds-theme.css 权威规范) */
|
||||
.cds-skeleton,
|
||||
.skeleton-shimmer {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
background-color: var(--cds-skeleton-base, rgba(0, 0, 0, 0.05));
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
}
|
||||
.dark .cds-skeleton,
|
||||
.dark .skeleton-shimmer {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
background-color: var(--cds-skeleton-base, rgba(255, 255, 255, 0.06));
|
||||
}
|
||||
.skeleton-shimmer::after {
|
||||
|
||||
/* 密集列表同步呼吸模式: 整体透明度 1.0 -> 0.6 -> 1.0 周期 2.0s */
|
||||
.cds-skeleton[data-motion="lockstep"],
|
||||
[data-motion="lockstep"] .cds-skeleton,
|
||||
.skeleton-shimmer[data-motion="lockstep"],
|
||||
[data-motion="lockstep"] .skeleton-shimmer {
|
||||
animation: cds-skeleton-breathe 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 大卡片波浪扫光模式: 105° 倾斜扫光光带,支持 --cds-skel-i 级联延时 */
|
||||
.cds-skeleton-sweep,
|
||||
.cds-skeleton[data-motion="wave"]::after,
|
||||
.cds-skeleton:not([data-motion])::after,
|
||||
.skeleton-shimmer[data-motion="wave"]::after,
|
||||
.skeleton-shimmer:not([data-motion])::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform: translateX(-100%);
|
||||
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.4) 50%, transparent 100%);
|
||||
animation: cds-skeleton-sweep 1.8s infinite cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transform: translate(-100%);
|
||||
background: linear-gradient(
|
||||
105deg,
|
||||
transparent 35%,
|
||||
var(--cds-skeleton-sheen, rgba(255, 255, 255, 0.25)) 50%,
|
||||
transparent 65%
|
||||
);
|
||||
animation: cds-skeleton-sweep 2s linear infinite;
|
||||
animation-delay: calc(var(--cds-skel-i, 0) * 90ms);
|
||||
pointer-events: none;
|
||||
}
|
||||
.dark .skeleton-shimmer::after {
|
||||
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.08) 50%, transparent 100%);
|
||||
.dark .cds-skeleton-sweep,
|
||||
.dark .cds-skeleton[data-motion="wave"]::after,
|
||||
.dark .cds-skeleton:not([data-motion])::after,
|
||||
.dark .skeleton-shimmer[data-motion="wave"]::after,
|
||||
.dark .skeleton-shimmer:not([data-motion])::after {
|
||||
background: linear-gradient(
|
||||
105deg,
|
||||
transparent 35%,
|
||||
var(--cds-skeleton-sheen, rgba(255, 255, 255, 0.08)) 50%,
|
||||
transparent 65%
|
||||
);
|
||||
}
|
||||
|
||||
/* RTL 镜像角度适配: 105° -> 255° */
|
||||
:dir(rtl) .cds-skeleton-sweep,
|
||||
[dir="rtl"] .cds-skeleton-sweep,
|
||||
:dir(rtl) .cds-skeleton[data-motion="wave"]::after,
|
||||
[dir="rtl"] .cds-skeleton[data-motion="wave"]::after,
|
||||
:dir(rtl) .cds-skeleton:not([data-motion])::after,
|
||||
[dir="rtl"] .cds-skeleton:not([data-motion])::after,
|
||||
:dir(rtl) .skeleton-shimmer[data-motion="wave"]::after,
|
||||
[dir="rtl"] .skeleton-shimmer[data-motion="wave"]::after,
|
||||
:dir(rtl) .skeleton-shimmer:not([data-motion])::after,
|
||||
[dir="rtl"] .skeleton-shimmer:not([data-motion])::after {
|
||||
background: linear-gradient(
|
||||
255deg,
|
||||
transparent 35%,
|
||||
var(--cds-skeleton-sheen, rgba(255, 255, 255, 0.25)) 50%,
|
||||
transparent 65%
|
||||
);
|
||||
}
|
||||
|
||||
/* lockstep 呼吸模式下禁用扫光伪元素以避免双重动画冲突 */
|
||||
.cds-skeleton[data-motion="lockstep"]::after,
|
||||
[data-motion="lockstep"] .cds-skeleton::after,
|
||||
.skeleton-shimmer[data-motion="lockstep"]::after,
|
||||
[data-motion="lockstep"] .skeleton-shimmer::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 容器查询自适应定义 */
|
||||
@@ -2493,25 +2558,39 @@ export const irradiationCompensation = {
|
||||
</ComponentShowcase>
|
||||
</div>
|
||||
|
||||
{/* 7.2 骨架屏高级倾斜扫光 */}
|
||||
{/* 7.2 骨架屏高级倾斜扫光与双模式动效 */}
|
||||
<ComponentShowcase
|
||||
title="高级倾斜骨架屏 (Skeleton Shimmer)"
|
||||
title="高级倾斜骨架屏 (Skeleton Dual-Mode Shimmer)"
|
||||
tag='data-cds="Skeleton"'
|
||||
code={`<div className="skeleton-shimmer h-4 rounded w-3/4" />`}
|
||||
description="内容加载中展示的占位条,90 度渐变遮罩伴随 cds-skeleton-sweep 关键帧在 1.8 秒内横向扫光。"
|
||||
code={`<!-- 波浪扫光模式 (Wave) -->\n<div className="cds-skeleton cds-skeleton-sweep h-4 rounded w-3/4" data-motion="wave" style={{ "--cds-skel-i": 1 }} />\n\n<!-- 密集列表同步呼吸模式 (Lockstep) -->\n<div className="cds-skeleton h-4 rounded w-3/4" data-motion="lockstep" />`}
|
||||
description="内容加载中展示的占位条,105 度微斜渐变遮罩伴随 cds-skeleton-sweep 关键帧在 2.0 秒内匀速扫光。支持 [data-motion='wave'] 波浪级联延时与 [data-motion='lockstep'] 密集列表同步呼吸。"
|
||||
>
|
||||
<div className="max-w-md mx-auto space-y-3 p-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-10 rounded-full skeleton-shimmer shrink-0" />
|
||||
<div className="space-y-1.5 flex-1">
|
||||
<div className="h-3.5 skeleton-shimmer rounded w-2/5" />
|
||||
<div className="h-2.5 skeleton-shimmer rounded w-4/5" />
|
||||
<div className="max-w-md mx-auto space-y-6 p-2">
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-text-muted mb-2 uppercase tracking-wider">波浪扫光模式 [data-motion="wave"] (105° 2.0s 线性扫光 + 90ms 级联延时)</div>
|
||||
<div className="space-y-3" data-motion="wave">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-10 rounded-full cds-skeleton shrink-0" style={{ "--cds-skel-i": 0 }} />
|
||||
<div className="space-y-1.5 flex-1">
|
||||
<div className="h-3.5 cds-skeleton rounded w-2/5" style={{ "--cds-skel-i": 1 }} />
|
||||
<div className="h-2.5 cds-skeleton rounded w-4/5" style={{ "--cds-skel-i": 2 }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 pt-1">
|
||||
<div className="h-3 cds-skeleton rounded w-full" style={{ "--cds-skel-i": 3 }} />
|
||||
<div className="h-3 cds-skeleton rounded w-5/6" style={{ "--cds-skel-i": 4 }} />
|
||||
<div className="h-3 cds-skeleton rounded w-2/3" style={{ "--cds-skel-i": 5 }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 pt-2">
|
||||
<div className="h-3 skeleton-shimmer rounded w-full" />
|
||||
<div className="h-3 skeleton-shimmer rounded w-5/6" />
|
||||
<div className="h-3 skeleton-shimmer rounded w-2/3" />
|
||||
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-text-muted mb-2 uppercase tracking-wider">密集列表同步呼吸模式 [data-motion="lockstep"] (2.0s 周期性 60% 呼吸暗光)</div>
|
||||
<div className="space-y-2" data-motion="lockstep">
|
||||
<div className="h-3.5 cds-skeleton rounded w-3/4" />
|
||||
<div className="h-3 cds-skeleton rounded w-full" />
|
||||
<div className="h-3 cds-skeleton rounded w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentShowcase>
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
---
|
||||
title: "第一章:综述与版本边界"
|
||||
category: reverse-engineering
|
||||
domain: architecture/desktop_application
|
||||
version: "3.0 (3.6556.2)"
|
||||
status: stable
|
||||
supersedes: "2.0 (v2.37337.1)"
|
||||
date: 2026-10-26
|
||||
author: "REA 逆向工程实验室"
|
||||
evidence_root: "b2dfbbb40325d978dcb7561ad12a6f9bc199f8f46b7d3d2897af74ce8965d68a"
|
||||
---
|
||||
|
||||
# 第一章:综述与版本边界
|
||||
|
||||
## 1.1 版本边界声明
|
||||
|
||||
### 1.1.1 应用版本
|
||||
|
||||
从 `/Applications/Claude.app/Contents/Resources/app.asar` 内解出的 `package.json` 原文:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@ant/desktop",
|
||||
"version": "3.6556.2",
|
||||
"productName": "Claude"
|
||||
}
|
||||
```
|
||||
|
||||
**证据链**:
|
||||
- 文件位置:ASAR 根目录 `package.json`,`size = 1835`(REA 6.8.0 生成的取证包记录)
|
||||
- ASAR Header 结构:偏移 `0..16` 为长度头,`uint32LE@12` 给出 JSON 头长度;JSON 头从偏移 16 开始;文件数据区起始 = `16 + headerSize`
|
||||
- 交叉验证:`/Applications/Claude.app/Contents/Info.plist` 的 `CFBundleShortVersionString = 3.6556.2`,`CFBundleVersion = 3.6556.2`
|
||||
|
||||
### 1.1.2 ASAR 根摘要
|
||||
|
||||
```
|
||||
sha256 = b2dfbbb40325d978dcb7561ad12a6f9bc199f8f46b7d3d2897af74ce8965d68a
|
||||
```
|
||||
|
||||
REA 取证包 `artifacts[0].digest.sha256` 与 `shasum -a 256` 直接计算结果一致,**同一次取证可复现**。
|
||||
|
||||
REA 取证包标识:
|
||||
|
||||
```
|
||||
evidence_id = ev_77dc39e02692fc4337a916c2321df1f1cfef85c3f30a034593655fa2eea55303
|
||||
operation = analyze_javascript_application
|
||||
authority = shipped-artifact
|
||||
confidence = derived
|
||||
```
|
||||
|
||||
### 1.1.3 ASAR 内部规模
|
||||
|
||||
```json
|
||||
{
|
||||
"relevant_files": 572,
|
||||
"parsed_javascript_files": 546,
|
||||
"skipped_javascript_files": 7,
|
||||
"parse_failures": 0,
|
||||
"invalid_utf8_files": 0,
|
||||
"text_bytes_read": 42966700,
|
||||
"visited_ast_nodes": 6621556,
|
||||
"findings": 10872,
|
||||
"modules": 72,
|
||||
"nested_asar_containers": 0
|
||||
}
|
||||
```
|
||||
|
||||
**证据**:`records[0].normalized_result.statistics`
|
||||
|
||||
独立复核:直接用 Node 解析 ASAR header,条目总数 **629**(含非 JS 文件如 CSS/HTML/JSON/node)。572 是 REA 判定为「相关」的子集。
|
||||
|
||||
### 1.1.4 进程与窗口拓扑(实测)
|
||||
|
||||
**7 个 renderer 入口**(ASAR 内 `.vite/renderer/` 下的目录):
|
||||
|
||||
```
|
||||
about_window
|
||||
buddy_window
|
||||
file_viewer
|
||||
find_in_page
|
||||
local_exec_consent
|
||||
main_window
|
||||
quick_window
|
||||
```
|
||||
|
||||
**证据**:`Object.keys(asarHeader.files[".vite"].files.renderer.files)` → 上述 7 项。
|
||||
|
||||
但 REA `summary.browser_windows = 5`。两者不矛盾:
|
||||
|
||||
- `5` 统计的是「代码里 `new BrowserWindow` 创建的不同窗口」
|
||||
- `7` 是「磁盘上的 renderer HTML 入口数」
|
||||
|
||||
差的 2 个(`file_viewer`、`local_exec_consent`)由其他机制打开,不走 `new BrowserWindow`。
|
||||
|
||||
**⚠️ 本节的可信度限制**:主进程文件 `.vite/build/index.pre.js` 在 REA 取证中 `parse_status = "partial"` 且 `static_analysis` 字段缺失(见 1.2),因此「5 个 BrowserWindow」来自 REA 对可解析部分的汇总,**不是对主进程完整代码的直接计数**。详见 [第二章 2.3](02-Electron架构取证.md)。
|
||||
|
||||
---
|
||||
|
||||
## 1.2 取证覆盖度与不可解项
|
||||
|
||||
REA 取证包 `records[0].normalized_result.limitations` 共 **1064 条**。按原因归并:
|
||||
|
||||
| 原因 | 条数 | 原文(节选) |
|
||||
| :--- | ---: | :--- |
|
||||
| 语义分析未启动 | 512 | `The parent reserved its remaining result capacity for static observations; semantic analysis of <file> was not started` |
|
||||
| 语义图超预算 | 10 | `Semantic graph for <file> exceeds the parent's remaining result budget; static and module facts were retained` |
|
||||
| 工作进程资源约束 | 7 | `Owned JavaScript worker … resource constraint` |
|
||||
| 完整性冲突 | 8 | `Artifact integrity metadata contradicts observed bytes at <path>` |
|
||||
| 其他(内联脚本、别名上限等) | 527 | 逐条不同 |
|
||||
|
||||
**去重后的受约束文件数:532**(`[...new Set(limitations.filter(l=>l.includes("Resource constraint")).map(l=>l.split(":")[0]))].length`)。
|
||||
|
||||
### 1.2.1 被截断的核心文件
|
||||
|
||||
```
|
||||
.vite/build/mainView.js
|
||||
.vite/build/mainWindow.js
|
||||
.vite/build/quickWindow.js
|
||||
.vite/build/aboutWindow.js
|
||||
.vite/build/heavy-work-worker/dist-CI9lzb1v.js
|
||||
.vite/build/heavy-work-worker/heavyWorkWorker.js
|
||||
```
|
||||
|
||||
**这直接导致本文的一个重要结论**:REA 主报告中的 `ipc.main_handlers = 0` **不能直接采信**,必须绕过 REA 直接读字节验证。第二章完成了这项验证,结论是**该零值成立,但原因与 REA 的推断不同**。
|
||||
|
||||
### 1.2.2 六个结构性零值
|
||||
|
||||
```json
|
||||
{
|
||||
"ipc.main_handlers": 0,
|
||||
"ipc.paired_renderer_transmissions": 0,
|
||||
"ipc.ambiguous_renderer_transmissions": 0,
|
||||
"preload_entrypoints": 0,
|
||||
"resolved_utility_entrypoints": 0,
|
||||
"native_addon_bindings": 0,
|
||||
"resolved_native_addon_bindings": 0
|
||||
}
|
||||
```
|
||||
|
||||
**证据**:`records[0].normalized_result.summary`。
|
||||
|
||||
本文对其中三个做了字节级复核(第二章):
|
||||
|
||||
| 零值 | 复核结论 | 复核方法 |
|
||||
| :--- | :--- | :--- |
|
||||
| `ipc.main_handlers = 0` | ✅ **成立** | 主进程仅 6 个 `ipcMain.on(t.createKey(...))`,全部属于 Sentry SDK 命名空间 |
|
||||
| `preload_entrypoints = 0` | ✅ **成立** | 全 ASAR 无 preload 命名文件;preload 路径由 `registerPreloadScript` 运行时拼接 |
|
||||
| `native_addon_bindings = 0` | ⚠️ **部分成立** | 磁盘有 5 个 `native-addon` 节点,但其中 8 处完整性冲突,REA 拒绝建立可信绑定 |
|
||||
|
||||
其余三个零值本文**不复核、不推断**,标注为 `unknown`。
|
||||
|
||||
### 1.2.3 语义图的族级覆盖度
|
||||
|
||||
`records[0].normalized_result.semantic_graph.coverage.families` 全部 12 个族状态均为 `partial`:
|
||||
|
||||
| family | retained_relations | unknown_ids 数量 |
|
||||
| :--- | ---: | ---: |
|
||||
| data-flow | 51995 | 24800 |
|
||||
| object-flow | 5461 | 21917 |
|
||||
| closure | 16153 | 531 |
|
||||
| call-flow | 11047 | 8157 |
|
||||
| promise-ownership | 295 | 715 |
|
||||
| event | 121 | 626 |
|
||||
| boundary | 54 | 537 |
|
||||
| request | 48 | 543 |
|
||||
| timer | 15 | 544 |
|
||||
| configuration | 1 | 531 |
|
||||
| **child-process** | **0** | 531 |
|
||||
| **resource-lifecycle** | **0** | 531 |
|
||||
|
||||
全局 unknown 节点 **59963** 个。
|
||||
|
||||
**含义**:本文凡是涉及「数据流」「对象流」「资源生命周期」的推断都必须显式标注为不可靠。本文因此**只采信可直接从字节读出的静态事实**(token 值、keyframes 定义、选择器结构、字面量字符串)。
|
||||
|
||||
---
|
||||
|
||||
## 1.3 v2 → v3 迁移影响面
|
||||
|
||||
本节给出**可直接执行的迁移判定**,每条附证据。
|
||||
|
||||
### 1.3.1 必须修改的令牌值
|
||||
|
||||
| 令牌 | open-claude-ui 现值 | 3.6556.2 实测值 | 判定 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `--cds-clay-emphasized` | `var(--cds-clay-hover)`(light `#cc5a36` / dark `#e08264`) | **`#c6613f`**(2 处定义:`:root` 与 `.cds-root`,值相同,无暗色覆盖) | ❌ **必须改** |
|
||||
|
||||
**证据**:
|
||||
|
||||
```bash
|
||||
grep -c "\--cds-clay-emphasized:" MainWindowPage-BjVtjJFx.css # → 2(:root 与 .cds-root,值均为 #c6613f)
|
||||
grep -c "\--cds-clay-emphasized:" MainWindowPage.css | grep dark # 反模式,见下
|
||||
# 无暗色覆盖的验证方式:
|
||||
grep -o "data-mode=dark[^{]*{[^}]*--cds-clay-emphasized" MainWindowPage-BjVtjJFx.css | wc -l # → 0
|
||||
grep -o "\--cds-clay-emphasized:[^;}]*" MainWindowPage-BjVtjJFx.css # → --cds-clay-emphasized:#c6613f
|
||||
```
|
||||
|
||||
原 CSS 中的完整上下文(相邻令牌,用于确认这不是断章取义):
|
||||
|
||||
```css
|
||||
…--cds-oncolor-200:#f8f8f7bf;--cds-oncolor-300:#f8f8f780;--cds-clay:#d97757;--cds-clay-emphasized:#c6613f;--cds-heather:#cbcadb;--cds-plum:#827db…
|
||||
```
|
||||
|
||||
**注意三份来源给出三个不同值**:
|
||||
|
||||
| 来源 | 值 |
|
||||
| :--- | :--- |
|
||||
| v2 文档(v2.37337.1) | `#cc5a36`(双模同值) |
|
||||
| open-claude-ui 分支实现 | `var(--cds-clay-hover)` → light `#cc5a36` / dark `#e08264` |
|
||||
| **3.6556.2 实测** | **`#c6613f`** |
|
||||
|
||||
### 1.3.2 无需修改的令牌(已验证一致)
|
||||
|
||||
35 阶 gray 与 35 阶 orange **全部对齐**,仅 1 处写法差异(值等价):
|
||||
|
||||
| 令牌 | ours | 3.6556.2 | 判定 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `--cds-gray-870` | `#111111` | `#111` | ✅ 值等价,仅书写长度不同 |
|
||||
|
||||
**证据**:
|
||||
|
||||
```bash
|
||||
# gray: ours=35 cli=35 diff=1 (唯一差异 gray-870: #111111 vs #111)
|
||||
# orange: ours=35 cli=35 diff=0
|
||||
```
|
||||
|
||||
完整 35 阶表见 [4.3](04-系统底座全景解密.md)。
|
||||
|
||||
### 1.3.3 缺失的动效与结构
|
||||
|
||||
| 类别 | ours | 3.6556.2 | 交集 | 缺失 | 我方独有 |
|
||||
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||
| `@keyframes cds-*` | 32 | **55** | 11 | **44** | 21 |
|
||||
| `data-cds` 命名空间 | 22 | **45** | 18 | **27** | 4 |
|
||||
| `@property --cds-*` | 19 | **24** | 17 | **7** | 2 |
|
||||
|
||||
**证据**:
|
||||
|
||||
```bash
|
||||
# keyframes
|
||||
comm -12 / -13 / -23 对 ours(32) 与 client(55) 做集合运算 → 11 / 44 / 21
|
||||
32 = 11 + 21 ✓ 55 = 11 + 44 ✓
|
||||
|
||||
# data-cds
|
||||
comm 对 ours(22) 与 client(45) → 18 / 27 / 4
|
||||
22 = 18 + 4 ✓ 45 = 18 + 27 ✓
|
||||
|
||||
# @property
|
||||
comm 对 ours(19) 与 client(24) → 17 / 7 / 2
|
||||
19 = 17 + 2 ✓ 24 = 17 + 7 ✓
|
||||
```
|
||||
|
||||
具体清单见 [第五章](05-open-claude-ui差异审计.md)。
|
||||
|
||||
### 1.3.4 结构性变更(需要改代码,不只是改值)
|
||||
|
||||
| 变更 | 影响 |
|
||||
| :--- | :--- |
|
||||
| 骨架屏从 `.skeleton-shimmer::after` 改为 **双层 `data-cds` 组件 + `.cds-skel-sweep-band` 子元素** | 组件标记需重写 |
|
||||
| 新增 `data-hold` 三态揭示时序 | 需新增可选的揭示延迟控制 |
|
||||
| 暗色从 `.dark` class 改为 **`[data-mode=dark\|light\|system]`** | 暗色切换逻辑需重写 |
|
||||
| RTL 从简单 255° 翻转改为 **`:lang(...)` 精确语言列表 + `animation-direction:reverse`** | RTL 选择器需重写 |
|
||||
|
||||
详细证据见 [第三章](03-架构重构与对比分析.md)。
|
||||
|
||||
---
|
||||
|
||||
## 本章证据索引
|
||||
|
||||
| 断言 | 证据来源 |
|
||||
| :--- | :--- |
|
||||
| 版本 `3.6556.2` | ASAR 内 `package.json`;`Info.plist` |
|
||||
| ASAR 根摘要 | `shasum -a 256` 与 REA `artifacts[0].digest.sha256` 双向一致 |
|
||||
| 572/546/7/0/6621556 | REA `records[0].normalized_result.statistics` |
|
||||
| 629 个 ASAR 条目 | 直接解析 ASAR header 计数 |
|
||||
| 7 个 renderer 入口 | ASAR `.vite/renderer/` 目录列表 |
|
||||
| 532 文件截断 / 1064 条 limitation | REA `records[0].normalized_result.limitations` |
|
||||
| 12 个语义族全 partial | REA `semantic_graph.coverage.families` |
|
||||
| `clay-emphasized = #c6613f` | `grep` 主样式表:2 处定义(`:root` / `.cds-root`),值一致,0 处暗色覆盖 |
|
||||
| gray/orange 35 阶对齐 | 逐阶对比 ours vs cli |
|
||||
| 55/45/24 vs 32/23/19 | 三方 `sort -u \| wc -l` |
|
||||
@@ -0,0 +1,452 @@
|
||||
---
|
||||
title: "第二章:Electron 架构取证"
|
||||
category: reverse-engineering
|
||||
domain: architecture/desktop_application
|
||||
version: "3.0 (3.6556.2)"
|
||||
status: stable
|
||||
date: 2026-10-26
|
||||
author: "REA 逆向工程实验室"
|
||||
evidence_root: "b2dfbbb40325d978dcb7561ad12a6f9bc199f8f46b7d3d2897af74ce8965d68a"
|
||||
---
|
||||
|
||||
# 第二章:Electron 架构取证
|
||||
|
||||
> **本章的特殊性**:REA 6.8.0 的语义分析无法覆盖主进程与渲染进程的核心文件(见 1.2.1)。因此本章**绕过 REA 的语义层,直接读取 ASAR 内的 JS 字节**,由此得出的结论与 REA 的 summary 字段在若干项上不同——本文以字节为准,并说明差异原因。
|
||||
|
||||
## 2.0 复现方法
|
||||
|
||||
本章所有数字都可用同一段脚本复现:
|
||||
|
||||
```js
|
||||
// 读取 ASAR 内任意文件
|
||||
import fs from "node:fs";
|
||||
const p = "/Applications/Claude.app/Contents/Resources/app.asar";
|
||||
const fd = fs.openSync(p, "r");
|
||||
const head = Buffer.alloc(16); fs.readSync(fd, head, 0, 16, 0);
|
||||
const headerSize = head.readUInt32LE(12); // JSON 头长度
|
||||
const hb = Buffer.alloc(headerSize); fs.readSync(fd, hb, 0, headerSize, 16);
|
||||
const json = JSON.parse(hb.toString("utf8")); // ASAR 目录树
|
||||
const dataStart = 16 + headerSize; // 文件数据区起点
|
||||
|
||||
const read = (node) => {
|
||||
const buf = Buffer.alloc(node.size);
|
||||
fs.readSync(fd, buf, 0, node.size, dataStart + Number(node.offset));
|
||||
return buf.toString("utf8");
|
||||
};
|
||||
const build = json.files[".vite"].files.build.files;
|
||||
const main = read(build["index.pre.js"]);
|
||||
```
|
||||
|
||||
> **ASAR Header 布局(本文实测确认)**
|
||||
> ```
|
||||
> 偏移 0..12 : Chromium pickle 头(其中 @12 为 JSON 字节长度)
|
||||
> 偏移 16.. : 纯 JSON 文本,首字节为 '{'
|
||||
> ```
|
||||
> 常见误解是 JSON 前还有 4 字节 payload 长度。本文对 3.6556.2 实测:**JSON 从偏移 16 直接开始**。
|
||||
|
||||
---
|
||||
|
||||
## 2.1 主进程面:`index.pre.js`
|
||||
|
||||
**文件标识**
|
||||
|
||||
| 项 | 值 | 证据 |
|
||||
| :--- | :--- | :--- |
|
||||
| ASAR 路径 | `.vite/build/index.pre.js` | ASAR 目录树 |
|
||||
| 大小 | 982,086 bytes | ASAR 条目 `size` |
|
||||
| REA 解析状态 | `partial` | 取证包 `parse_status` |
|
||||
| REA `static_analysis` | **字段缺失** | 取证包该节点无 `static_analysis` 键 |
|
||||
| Electron 身份 | `electron-main`(REA graph 唯一一个) | `graph.nodes` 中 `kind === "electron-main"` 仅 1 个 |
|
||||
|
||||
### 2.1.1 IPC handler 计数:**0 个业务通道**
|
||||
|
||||
**字节级扫描结果**
|
||||
|
||||
```bash
|
||||
grep -c "ipcMain" # 6 处出现
|
||||
grep -c "ipcMain.handle\|ipcMain.handleOnce" # 0 处
|
||||
```
|
||||
|
||||
`ipcMain` 的 6 处出现,全部是挂载 listener(`.on`),且通道名通过 `t.createKey("<字面量>")` 生成:
|
||||
|
||||
```js
|
||||
function bx(e,t,n){
|
||||
E.ipcMain.on(t.createKey("start"), (({sender:e})=>{…})),
|
||||
E.ipcMain.on(t.createKey("scope"), ((e,t)=>hx(n,t))),
|
||||
E.ipcMain.on(t.createKey("envelope"), (({sender:t},r)=>px(e,n,r,t))),
|
||||
E.ipcMain.on(t.createKey("structured-log"), (({sender:t},r)=>_x(e,n,r,t))),
|
||||
E.ipcMain.on(t.createKey("metric"), (({sender:t},r)=>vx(e,n,r,t)));
|
||||
let r=Db(e);
|
||||
r && E.ipcMain.on(t.createKey("status"), (({sender:e},t)=>r(t,e)))
|
||||
}
|
||||
```
|
||||
|
||||
**提取到的 6 个通道名字面量**:
|
||||
|
||||
```
|
||||
start
|
||||
scope
|
||||
envelope
|
||||
structured-log
|
||||
metric
|
||||
status
|
||||
```
|
||||
|
||||
**这 6 个名字全部属于 Sentry SDK 的 IPC 命名空间**。判据(同一文件内的上下文):
|
||||
|
||||
```js
|
||||
// 紧邻的其它 Sentry 痕迹
|
||||
O.log(`[Utility Process] ${e}`)
|
||||
Px(e.data) // 内部调用 js(e) → nx(t) 解析 envelope
|
||||
```
|
||||
|
||||
`envelope` / `scope` / `structured-log` / `metric` 是 Sentry SDK 的标准传输概念;`start` / `status` 对应该 SDK 的生命周期管理。
|
||||
|
||||
**因此 `ipc.main_handlers = 0` 是成立的**,但要精确表述为:
|
||||
|
||||
> 主进程**不注册任何 Claude 业务 IPC handler**。全部 6 个 `ipcMain.on` 通道属于 Sentry SDK 的诊断命名空间。
|
||||
|
||||
这与 REA 给出的 `0` 数值一致,但 REA 的 `main_handlers` 计数口径只统计「handler」(`.handle`/`.handleOnce`),本节的更强的结论是「连业务 `.on` 也没有」。
|
||||
|
||||
### 2.1.2 `BrowserWindow`:2 处出现,均为 Sentry 代码,0 处构造
|
||||
|
||||
```bash
|
||||
grep -o "BrowserWindow" index.pre.js | wc -l # 2
|
||||
grep -o "getAllWindows" index.pre.js | wc -l # 1
|
||||
grep -o "new *\.*BrowserWindow" index.pre.js | wc -l # 0
|
||||
```
|
||||
|
||||
**第 1 处**(偏移 313730)——Sentry 的截图附加逻辑:
|
||||
|
||||
```js
|
||||
jb=fl((()=>({name:"Screenshots",async processEvent(e,t,n){
|
||||
if(!n.getOptions().attachScreenshot) return e;
|
||||
if(!e.transaction && e.platform!=="native"){
|
||||
let e=1;
|
||||
for(let n of E.BrowserWindow.getAllWindows()){
|
||||
t.attachments||=[];
|
||||
try{
|
||||
if(!n.isDestroyed() && n.isVisible()){
|
||||
let r=e===1?"screenshot.png":`screenshot-${e}.png`,
|
||||
i=await n.capturePage();
|
||||
t.attachments.push({filename:r,data:i.toPNG(),contentTy…
|
||||
```
|
||||
|
||||
**第 2 处**(偏移 329352)——Sentry 集成名重命名的字符串常量:
|
||||
|
||||
```js
|
||||
var Rx=[
|
||||
{userAdded:"ElectronMinidump", toRemove:"SentryMinidump"},
|
||||
{userAdded:"BrowserWindowSession", toRemove:"MainProcessSession"}
|
||||
];
|
||||
function zx(e){
|
||||
for(let {userAdded:t,toRemove:n} of Rx)
|
||||
e.integrations.some((e=>e.name===t)) &&
|
||||
(e.integrations=e.integrations.filter((e=>e.name!==n)))
|
||||
}
|
||||
```
|
||||
|
||||
**结论**:
|
||||
|
||||
- 主进程对 `BrowserWindow` 的**唯一运行时用法**是 Sentry 的 `getAllWindows()` 枚举 + `capturePage()` 截图
|
||||
- 第 2 处是字符串字面量,不是 API 调用
|
||||
- **没有任何 `new BrowserWindow(...)` 构造调用**(正则 `new\s+E?\.?BrowserWindow` 命中 0)
|
||||
|
||||
两处都属于 Sentry SDK,与 Claude 的窗口管理无关。
|
||||
|
||||
### 2.1.3 `utilityProcess`:3 处,全部是 Proxy 包装,无 fork 调用
|
||||
|
||||
```js
|
||||
function Nx(){
|
||||
E.utilityProcess?.fork && (
|
||||
E.utilityProcess.fork = new Proxy(E.utilityProcess.fork, {
|
||||
apply: (e, t, n) => {
|
||||
let r = e.apply(t, n);
|
||||
function i(){
|
||||
let [, , e] = n;
|
||||
return e?.serviceName || `pid:${r.pid}`
|
||||
}
|
||||
return r.on("message", (e=>{
|
||||
if (Kv(e)) {
|
||||
Mx(`SDK started in utility process '${i()}'`);
|
||||
let {port1: e, port2: t} = new E.MessageChannelMain;
|
||||
t.on("message", (e=>{
|
||||
(e.data instanceof Uint8Array || typeof e.data === "string") && Px(e.data)
|
||||
}));
|
||||
t.start();
|
||||
r.postMessage(qv(), [e])
|
||||
}
|
||||
})),
|
||||
r.on = new Proxy(r.on, {…}),
|
||||
r
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**关键判据**:这段代码是 **`utilityProcess.fork` 的 Proxy 拦截器**,作用是在每次 fork 后注入 Sentry 的 IPC 桥(`MessageChannelMain` + `postMessage(qv(), [e])`)。
|
||||
|
||||
修饰点:
|
||||
- `apply` 钩子里调用 `e.apply(t, n)` —— 其中 `e` 是**原始 fork 函数**,`t` 是 `this`,`n` 是参数
|
||||
- 包装后返回 `r`(ChildProcess 风格的 utility process 句柄)
|
||||
|
||||
**这本身不是一次 fork 调用**。真正的 fork 调用点在同文件其他位置(被 REA 截断的语义图里),静态字面量不可定位。
|
||||
|
||||
**因此**:
|
||||
- REA 的 `utility_processes = 1` 与本文的「无 fork 调用点」并不矛盾——`1` 是 REA 从别处(可能来自构建产物的 metadata 或文件存在性)推断的
|
||||
- `resolved_utility_entrypoints = 0` 是**准确的**:无法确定任何 utility process 的入口脚本
|
||||
|
||||
本文**不对「到底有几个 utility process」下结论**,标注为 `unknown`。
|
||||
|
||||
### 2.1.4 preload:`../../preload/default.js` 是 Sentry 的注入路径
|
||||
|
||||
`index.pre.js` 中唯一的 preload 路径字面量:
|
||||
|
||||
```js
|
||||
function eb(){
|
||||
try { return require.resolve("../../preload/default.js") }
|
||||
catch {
|
||||
try {
|
||||
let e = (0,_.fileURLToPath)(require("node:url").pathToFileURL(__filename).href);
|
||||
return (0,v.resolve)(e, "..", "..", "..", "..", "preload", "default.js")
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**这是 Sentry SDK 的 preload 自动注入逻辑**,判据:
|
||||
|
||||
1. 函数体是典型的「先 `require.resolve` 再 fallback 到相对路径」的 SDK 兼容写法
|
||||
2. 该函数所属模块内的相邻代码是 `global.process.on("uncaughtException", …)` + `E.dialog.showErrorBox("A JavaScript error occurred in the main process", t)` —— 纯粹的 Sentry 错误上报
|
||||
3. 路径 `../../preload/default.js` 相对于 Sentry 包自身位置,不是 Claude 的 preload 目录
|
||||
|
||||
**Claude 自己的 preload 注册方式**(同文件另一处):
|
||||
|
||||
```js
|
||||
function ty(e,t){
|
||||
if (e.registerPreloadScript)
|
||||
e.registerPreloadScript({ type: "frame", filePath: t });
|
||||
else {
|
||||
let n = e.getPreloads();
|
||||
e.setPreloads([t, ...n])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`filePath: t` 是**运行时变量**,静态不可解。
|
||||
|
||||
**全 ASAR 内 preload 命名文件的搜索结果**:
|
||||
|
||||
```bash
|
||||
# ASAR 629 个条目中,路径含 "preload" 的:
|
||||
(none)
|
||||
|
||||
# 磁盘上 /Applications/Claude.app/Contents/Resources 下:
|
||||
/Applications/Claude.app/Contents/Resources/ion-dist/assets/v1/preload-helper-D0dM6GpB.js
|
||||
```
|
||||
|
||||
唯一的磁盘文件在 `ion-dist/`(Claude 的另一个分发目录),不在 ASAR 内,且名为 `preload-helper`,是 Vite 的 preload 辅助函数而非应用 preload 入口。
|
||||
|
||||
**结论**:`preload_entrypoints = 0` 在**静态层面成立**(asar 内无 preload 命名文件),但**不代表没有 preload**。
|
||||
|
||||
> **⚠️ 本节已被 [6.7.2](06-全量CSS扫描与官方规范比对.md) 的运行时证据修正。**
|
||||
>
|
||||
> 运行时实测(`webContents._getPreloadScript()`):
|
||||
>
|
||||
> | 窗口 | preload 路径 |
|
||||
> | :--- | :--- |
|
||||
> | `main_window` | **`.vite/build/mainWindow.js`** |
|
||||
> | `about:blank` | **`.vite/build/mainView.js`** |
|
||||
>
|
||||
> **preload 就是 `.vite/build/` 下的窗口脚本**。这些文件本章 2.1 / 2.2 已经分析过,只是当时不知道它们同时承担 preload 角色。
|
||||
>
|
||||
> 因此正确表述是:**preload 存在,只是不叫 preload 这个名字**。REA 的 `preload_entrypoints = 0` 反映的是「没有文件名为 preload 的入口」,不是「没有 preload 机制」。
|
||||
|
||||
---
|
||||
|
||||
## 2.2 渲染进程面:`mainView.js`
|
||||
|
||||
**文件标识**
|
||||
|
||||
| 项 | 值 |
|
||||
| :--- | :--- |
|
||||
| ASAR 路径 | `.vite/build/mainView.js` |
|
||||
| 大小 | 264,711 bytes |
|
||||
| REA 解析状态 | `partial`(语义图被截断) |
|
||||
|
||||
### 2.2.1 `ipcRenderer` / `contextBridge` 计数
|
||||
|
||||
```bash
|
||||
grep -o "\bipcRenderer\b" mainView.js | wc -l # 1598
|
||||
grep -o "\bcontextBridge\b" mainView.js | wc -l # 24
|
||||
grep -o "exposeInMainWorld" mainView.js | wc -l # 23
|
||||
```
|
||||
|
||||
### 2.2.2 桥接 API:12 个字面量名字
|
||||
|
||||
`exposeInMainWorld(<字面量>, …)` 的匹配结果:
|
||||
|
||||
```js
|
||||
exposeInMainWorld("buddyBle", …)
|
||||
exposeInMainWorld("claudeAppBindings", …)
|
||||
exposeInMainWorld("claudeAppSettings", …)
|
||||
exposeInMainWorld("process", …)
|
||||
exposeInMainWorld("desktopBootFeatures", …)
|
||||
exposeInMainWorld("desktopManagedConfig", …)
|
||||
exposeInMainWorld("desktopEnterpriseConfig", …)
|
||||
exposeInMainWorld("desktopTelemetryConfig", …)
|
||||
exposeInMainWorld("desktopDeviceClass", …)
|
||||
exposeInMainWorld("desktopPreferredLanguages", …)
|
||||
exposeInMainWorld("electronWindowControl", …)
|
||||
exposeInMainWorld("electronIntl", …)
|
||||
```
|
||||
|
||||
**12 个已知名字**。剩下 23 − 12 = 11 次调用传入的是变量(如 `t`、`i`),静态不可解。
|
||||
|
||||
**与 REA 的差异及原因**
|
||||
|
||||
| 来源 | 数值 | 口径 |
|
||||
| :--- | ---: | :--- |
|
||||
| REA `summary.context_bridge_apis` | **37** | `exposeInMainWorld` 及等价包装的调用点总数(含被截断文件的估算) |
|
||||
| REA graph `context-bridge-api` 节点 | 20 | 去重后的节点数 |
|
||||
| REA graph 唯一 label | 7 | `null, process, desktopEssentialTelemetryDisabled, buddy, cowork, claudeDesktopArtifactPane, omeletteDesktopPdf` |
|
||||
| **本文(字节扫描)** | **12** | `exposeInMainWorld` 的第一个参数是字符串字面量的情形 |
|
||||
|
||||
四者口径完全不同,**不可混用**。
|
||||
|
||||
**本文的结论**:`context_bridge_apis = 37` 是**调用计数**,不是「37 个不同接口」。目前可确证的接口名是 **12 个**(来自 `mainView.js`)。
|
||||
|
||||
> REA graph 里那 7 个 label(含 `buddy`、`cowork`、`claudeDesktopArtifactPane`、`omeletteDesktopPdf`)与本文的 12 个名字**交集只有 `process` 与 `buddy` 相关项**。差异原因:REA 的 graph 节点来自 `wrapper-substitution` 与 `direct-syntax` 两种解析路径,覆盖的是不同的调用形态。两者都有价值,但都不完整。
|
||||
|
||||
### 2.2.3 IPC channel 观测(REA graph 层)
|
||||
|
||||
REA 的 `graph.nodes` 中有 117 个 `ipc-channel` 节点,其 `observations[].properties` 汇总:
|
||||
|
||||
```json
|
||||
{
|
||||
"side": { "renderer": 117 },
|
||||
"mode": { "invoke": 75, "listen": 38, "send": 4 },
|
||||
"resolution": { "dynamic": 115, "literal": 2 }
|
||||
}
|
||||
```
|
||||
|
||||
**全部 117 个都在渲染侧**,与 2.1.1 的「主进程无业务 handler」一致。
|
||||
|
||||
只有 2 个通道名解析为字面量:
|
||||
|
||||
```
|
||||
$eipc_message$_5de9b164-2257-4603-a1e1-e00cdb72056c_$_claude.designPdf_$_DesignPdf_$_printFrameToPdf
|
||||
$eipc_message$_5de9b164-2257-4603-a1e1-e00cdb72056c_$_claude.artifactShell_$_ArtifactShellTransport_$_hello
|
||||
```
|
||||
|
||||
这两个是 Electron 的 `MessagePortMain` IPC 协议格式(`$eipc_message$_<uuid>_$_<namespace>_$_<class>_$_<method>`)。
|
||||
|
||||
**v2 文档引用的 `DESKTOP_WINDOW_POP_OUT_CHAT` 通道名不在其中**,其 `channel` 字段为 `null`(动态表达式)。
|
||||
|
||||
---
|
||||
|
||||
## 2.3 窗口拓扑:7 vs 5
|
||||
|
||||
**磁盘事实(ASAR 目录树)**
|
||||
|
||||
```
|
||||
.vite/renderer/about_window 11 files
|
||||
.vite/renderer/buddy_window 10 files
|
||||
.vite/renderer/file_viewer 6 files
|
||||
.vite/renderer/find_in_page 16 files
|
||||
.vite/renderer/local_exec_consent 2 files
|
||||
.vite/renderer/main_window 12 files
|
||||
.vite/renderer/quick_window 9 files
|
||||
```
|
||||
|
||||
**REA 的 `electron-renderer` 节点(7 个,与磁盘一一对应)**
|
||||
|
||||
```
|
||||
.vite/renderer/local_exec_consent/localExecConsent.html
|
||||
.vite/renderer/file_viewer/fileViewer.html
|
||||
.vite/renderer/quick_window/quick-window.html
|
||||
.vite/renderer/about_window/about.html
|
||||
.vite/renderer/main_window/index.html
|
||||
.vite/renderer/buddy_window/buddy.html
|
||||
.vite/renderer/find_in_page/find-in-page.html
|
||||
```
|
||||
|
||||
**REA `summary.browser_windows = 5`**
|
||||
|
||||
两者不矛盾,口径不同:
|
||||
|
||||
| 数值 | 含义 |
|
||||
| :--- | :--- |
|
||||
| 7 | renderer HTML 入口数(磁盘事实) |
|
||||
| 5 | `new BrowserWindow` 创建的不同窗口数(REA 汇总) |
|
||||
|
||||
差值 2:`file_viewer` 与 `local_exec_consent` 未走 `new BrowserWindow`。
|
||||
|
||||
**本文不推断这 2 个窗口的实际打开机制**(`BrowserView` / `webContents` 复用 / 其它),因为主进程语义图被截断。标注为 `unknown`。
|
||||
|
||||
**另注**:`.vite/build/` 下还有 4 个 renderer 无关的窗口级脚本,但它们的 Electron 身份未在 REA graph 中确立:
|
||||
|
||||
```
|
||||
.vite/build/aboutWindow.js 87,182 bytes
|
||||
.vite/build/quickWindow.js 87,182 bytes
|
||||
.vite/build/mainWindow.js 88,843 bytes
|
||||
.vite/build/findInPage.js 86,781 bytes
|
||||
```
|
||||
|
||||
这四个文件的 `ipcRenderer` 出现次数分别为 22 / 22 / 22 / 19,`contextBridge` 为 4 / 4 / 5 / 4。它们与 `mainView.js`(1598 次 `ipcRenderer`)不是同一层级——前者是**每个窗口的 renderer 引导脚本**,后者是**主渲染进程的应用主体**。
|
||||
|
||||
---
|
||||
|
||||
## 2.4 完整性冲突:8 处
|
||||
|
||||
REA `records[0].normalized_result.integrity_contradictions` 共 8 条。字段结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"contradiction_id": "ic_de5e95555f5099eed6aea52528a17ff8355426daf3879dc8ca59ff07c78de5e8",
|
||||
"declared_sha256": "2f423b6013cec753eefd174418b9517ecc39b0025e26b9c16789d0c0d4cadd86",
|
||||
"observed_sha256": "6ff3ed381e089584…",
|
||||
"entry_kind": "file",
|
||||
"logical_path": "node_modules/@ant/claude-native/claude-native-binding.node",
|
||||
"limitations": ["Observed bytes contradict declared integrity metadata and cannot support equivalence."]
|
||||
}
|
||||
```
|
||||
|
||||
冲突路径(前 3 条):
|
||||
|
||||
```
|
||||
node_modules/@ant/claude-native/claude-native-binding.node
|
||||
node_modules/node-pty/…
|
||||
resources/office365-mcp/…
|
||||
```
|
||||
|
||||
**REA 给出的解释**(原文):
|
||||
|
||||
```
|
||||
Declared ASAR integrity for unpacked entry <path> contradicts its companion file;
|
||||
packaging tools commonly sign or strip unpacked native binaries after writing the archive.
|
||||
```
|
||||
|
||||
**影响**:这正是本文与上一轮 REA 分析中被 `artifact_integrity_mismatch` 拦截的同一个文件。所有涉及 `analyze-javascript-application` / `inspect-artifact` 的重跑都需要 `--integrity-policy record-and-continue`。
|
||||
|
||||
**本文所有字节级结论均不受此影响**——本文直接读 ASAR 内的 JS/CSS 字节,不依赖 unpacked 原生二进制的完整性校验。
|
||||
|
||||
---
|
||||
|
||||
## 本章证据索引
|
||||
|
||||
| 断言 | 证据 |
|
||||
| :--- | :--- |
|
||||
| `index.pre.js` 982,086 bytes | ASAR 条目 `size` |
|
||||
| 6 个 `ipcMain.on(createKey(...))` | `grep -o 'ipcMain\.on\(t\.createKey(...)'` 提取原文 |
|
||||
| `ipcMain.handle` = 0 | `grep -c "ipcMain\.handle"` |
|
||||
| `BrowserWindow` = 2,0 处构造 | 两处上下文分别为 Sentry 截图逻辑与 `"BrowserWindowSession"` 字符串;`new BrowserWindow` 正则命中 0 |
|
||||
| `utilityProcess` = 3 全为 Proxy | `utilityProcess.fork = new Proxy(...)` 原文 |
|
||||
| preload 无入口 | ASAR 629 条目中无 preload 命名;`registerPreloadScript({filePath: t})` 为运行时变量 |
|
||||
| `mainView.js` 1598/24/23 | `grep -o` 计数 |
|
||||
| 12 个字面量桥接 API | `exposeInMainWorld("…")` 完整提取 |
|
||||
| 117 个 ipc-channel 全在 renderer | REA graph `observations[].properties.side` 汇总 |
|
||||
| 2 个字面量通道名 | REA graph `properties.channel` 非 null 项 |
|
||||
| 7 个 renderer 入口 | ASAR `.vite/renderer/` 目录 + REA `electron-renderer` 节点 |
|
||||
| 8 处完整性冲突 | REA `integrity_contradictions` |
|
||||
@@ -0,0 +1,540 @@
|
||||
---
|
||||
title: "第三章:架构重构与对比分析"
|
||||
category: reverse-engineering
|
||||
domain: architecture/desktop_application
|
||||
version: "3.0 (3.6556.2)"
|
||||
status: stable
|
||||
date: 2026-10-26
|
||||
author: "REA 逆向工程实验室"
|
||||
evidence_root: "b2dfbbb40325d978dcb7561ad12a6f9bc199f8f46b7d3d2899af74ce8965d68a"
|
||||
---
|
||||
|
||||
# 第三章:架构重构与对比分析
|
||||
|
||||
> 本章聚焦 **CDS 3.0 中被 `open-claude-ui` 误实现或完全未实现的机制**。每节先给 CSS 原文,再给结论,最后给与当前实现(`feat/issue-25-skeleton-sweep-and-rea-audit` 分支)的差异。
|
||||
|
||||
---
|
||||
|
||||
## 3.1 骨架屏:双层组件结构,sweep band 是独立子元素
|
||||
|
||||
### 3.1.1 真实的 DOM 层级
|
||||
|
||||
CDS 3.0 的骨架屏由**两层 `data-cds` 属性**构成:
|
||||
|
||||
```
|
||||
[data-cds="SkeletonGroup"] ← 组容器,控制 motion / hold
|
||||
└─ [data-cds="Skeleton"] ← 单个骨架块
|
||||
└─ .cds-skel-sweep-clip ← 裁剪层(overflow:hidden)
|
||||
└─ .cds-skel-sweep-band ← 扫光光带(真正动的元素)
|
||||
```
|
||||
|
||||
**证据(CSS 原文)**
|
||||
|
||||
```css
|
||||
/* 裁剪层 */
|
||||
.cds-root .cds-skel-sweep-clip {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
/* 光带 */
|
||||
.cds-root .cds-skel-sweep-band {
|
||||
background: linear-gradient(105deg, transparent 35%, var(--cds-skeleton-sheen), transparent 65%);
|
||||
animation: cds-skeleton-sweep 2s linear var(--cds-skel-reveal-end, 0s) infinite backwards;
|
||||
position: absolute;
|
||||
inset: 0
|
||||
}
|
||||
```
|
||||
|
||||
**关键点**
|
||||
|
||||
| 观察 | 说明 |
|
||||
| :--- | :--- |
|
||||
| `.cds-skel-sweep-band` 是**真实 DOM 元素** | 不是 `::after` 伪元素 |
|
||||
| `.cds-skel-sweep-clip` 提供 `overflow:hidden` | 扫光被裁剪在骨架块内 |
|
||||
| `position:absolute; inset:0` 属于 **band / clip** | 不属于骨架块本身 |
|
||||
| 骨架块 `[data-cds="Skeleton"]` 本身**没有** `position:absolute` 规则 | 它保持正常文档流 |
|
||||
|
||||
### 3.1.2 与 issue #25 分支的差异
|
||||
|
||||
分支的实现(`cds-theme.css`):
|
||||
|
||||
```css
|
||||
.cds-skeleton-sweep,
|
||||
.cds-skeleton[data-motion="wave"]::after,
|
||||
.cds-skeleton:not([data-motion])::after {
|
||||
content: "";
|
||||
position: absolute; /* ← 问题所在 */
|
||||
inset: 0;
|
||||
transform: translate(-100%);
|
||||
…
|
||||
}
|
||||
```
|
||||
|
||||
**三个结构性偏差**:
|
||||
|
||||
| 项 | CDS 3.0 实际 | 分支实现 | 后果 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 光带载体 | 独立元素 `.cds-skel-sweep-band` | `::after` 伪元素 | 伪元素无法被 JS 查询/替换 |
|
||||
| `.cds-skeleton-sweep` 作为独立类 | **不存在此语义** | 写在同一条规则的选择器列表里 | 该类的元素自身变成 `position:absolute` + `translate(-100%)`,被推出视口 |
|
||||
| 裁剪机制 | 独立 `.cds-skel-sweep-clip` + `overflow:hidden` | 依赖骨架块自身 `overflow:hidden` | 结构上可工作,但缺少显式裁剪层 |
|
||||
|
||||
**分支缺陷的实测证据**(Chrome headless,用 storybook 自己的 CSS):
|
||||
|
||||
```
|
||||
class="cds-skeleton cds-skeleton-sweep h-4 rounded w-3/4" data-motion="wave"
|
||||
→ position: absolute, x = -245, 渲染为不可见
|
||||
```
|
||||
|
||||
这正是 3.1.2 表格第二行描述的直接后果。
|
||||
|
||||
### 3.1.3 修复方向
|
||||
|
||||
**最小改动**(保留现有类名,去掉并列的独立类选择器):
|
||||
|
||||
```css
|
||||
/* 只保留伪元素形态,不把 .cds-skeleton-sweep 放进这条规则 */
|
||||
.cds-skeleton[data-motion="wave"]::after,
|
||||
.cds-skeleton:not([data-motion])::after { … }
|
||||
```
|
||||
|
||||
**完整对齐**(推荐,需同步改组件标记):
|
||||
|
||||
```css
|
||||
.cds-skeleton { position: relative; overflow: hidden; }
|
||||
.cds-skel-sweep-clip { pointer-events:none; position:absolute; inset:0; overflow:hidden; }
|
||||
.cds-skel-sweep-band {
|
||||
background: linear-gradient(105deg, transparent 35%, var(--cds-skeleton-sheen), transparent 65%);
|
||||
animation: cds-skeleton-sweep 2s linear var(--cds-skel-reveal-end, 0s) infinite backwards;
|
||||
position: absolute; inset: 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3.2 骨架屏:`data-hold` 三态揭示时序
|
||||
|
||||
这是 **CDS 3.0 完全新增的机制**,v2 文档与 issue #25 分支均未涉及。
|
||||
|
||||
### 3.2.1 三个状态
|
||||
|
||||
`data-hold` 属性(挂在 `SkeletonGroup` 上)有三个取值:
|
||||
|
||||
| 取值 | CSS 行为 | 语义 |
|
||||
| :--- | :--- | :--- |
|
||||
| **缺省**(无该属性) | 使用 `var(--placeholder-reveal-delay, .5s)` 的默认揭示时序 | 标准延迟揭示 |
|
||||
| **`none`** | `--cds-skel-reveal-end: calc(0s - var(--cds-skel-elapsed))` | 立即开始,不等待 |
|
||||
| **`delayed`** | 额外挂 `cds-placeholder-reveal .15s ease-out …backwards` 动画 | 显式延迟揭示 |
|
||||
|
||||
### 3.2.2 CSS 原文
|
||||
|
||||
```css
|
||||
/* 基础(无 data-hold 或 data-hold 无值时) */
|
||||
.cds-root [data-cds=SkeletonGroup][data-hold] {
|
||||
--cds-skel-elapsed: 0s;
|
||||
--cds-skel-i: 0;
|
||||
--cds-skel-settled: 0s;
|
||||
--cds-skel-reveal-end: calc(var(--placeholder-reveal-delay, .5s) + .15s - var(--cds-skel-elapsed))
|
||||
}
|
||||
|
||||
/* hold=none:立即 */
|
||||
.cds-root [data-cds=SkeletonGroup][data-hold=none] {
|
||||
--cds-skel-reveal-end: calc(0s - var(--cds-skel-elapsed))
|
||||
}
|
||||
|
||||
/* hold=delayed:显式延迟 + 淡入 */
|
||||
.cds-root [data-cds=SkeletonGroup][data-hold=delayed] {
|
||||
animation: cds-placeholder-reveal .15s ease-out
|
||||
calc(var(--placeholder-reveal-delay, .5s) - var(--cds-skel-elapsed) - var(--cds-skel-settled))
|
||||
backwards
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2.3 揭示延迟公式
|
||||
|
||||
从上面三段可推出完整的 `--cds-skel-reveal-end` 取值链:
|
||||
|
||||
```
|
||||
data-hold 缺省 / 有值但非 none 非 delayed:
|
||||
reveal-end = placeholder-reveal-delay(.5s) + 0.15s - elapsed
|
||||
|
||||
data-hold = none:
|
||||
reveal-end = 0s - elapsed
|
||||
|
||||
data-hold = delayed:
|
||||
reveal-end = (同上缺省公式)
|
||||
额外叠加 cds-placeholder-reveal 动画,延时 = reveal-delay - elapsed - settled
|
||||
```
|
||||
|
||||
**含义**:`.5s` 是默认的占位揭示延迟,`+0.15s` 是 `cds-placeholder-reveal` 动画自身时长(`.15s ease-out`)。`elapsed` / `settled` 由 JS 在运行时写入,用于「组件已经挂载了多久」的补偿——避免骨架屏在已经加载完成后才启动动画。
|
||||
|
||||
> **⚠️ 不可解部分**:`--cds-skel-elapsed` 与 `--cds-skel-settled` 的默认值在 CSS 里是 `0s`,但它们在运行时由谁写入、写入什么值,静态分析无法确定(需运行时观测)。本文只记录 CSS 侧的公式。
|
||||
|
||||
### 3.2.4 揭示完成后停止动画
|
||||
|
||||
```css
|
||||
/* 一旦 data-hold 且 data-motion 同时存在(即进入揭示完成态) */
|
||||
.cds-root [data-cds=SkeletonGroup][data-hold][data-motion],
|
||||
.cds-root [data-cds=SkeletonGroup][data-hold][data-motion] [data-cds=Skeleton] {
|
||||
animation-duration: .01ms;
|
||||
animation-iteration-count: 1
|
||||
}
|
||||
|
||||
/* 并隐藏扫光带 */
|
||||
.cds-root [data-cds=SkeletonGroup][data-hold][data-motion] .cds-skel-sweep-band {
|
||||
display: none
|
||||
}
|
||||
```
|
||||
|
||||
**这是 CDS 3.0 的「动画收尾」机制**:当骨架内容已揭示完成,把动画压到 `.01ms` 单次执行(实质是立即结束),并直接 `display:none` 掉扫光带。
|
||||
|
||||
**分支完全缺失此机制**——分支的骨架屏会无限循环扫光,不会在内容到达后停止。
|
||||
|
||||
---
|
||||
|
||||
## 3.3 骨架屏:双模 wave / lockstep 的真实延时公式
|
||||
|
||||
### 3.3.1 wave 模式(波浪级联)
|
||||
|
||||
```css
|
||||
.cds-root [data-cds=SkeletonGroup][data-motion=wave] [data-cds=Skeleton] {
|
||||
animation: cds-skeleton-breathe 2s ease-in-out
|
||||
calc(var(--cds-skel-reveal-end, 0s) + var(--cds-skel-i, 0) * 90ms)
|
||||
infinite
|
||||
}
|
||||
```
|
||||
|
||||
**关键发现**:wave 模式用的是 **`cds-skeleton-breathe`(透明度呼吸)**,不是扫光 sweep。
|
||||
|
||||
```
|
||||
动画: cds-skeleton-breathe
|
||||
时长: 2s
|
||||
缓动: ease-in-out
|
||||
延时: reveal-end + (skel-i × 90ms)
|
||||
迭代: infinite
|
||||
```
|
||||
|
||||
**这与分支的实现相反**:分支把 wave 模式绑定到 `cds-skeleton-sweep`(扫光),而实测中用 breathe。
|
||||
|
||||
### 3.3.2 lockstep 模式(同步呼吸)
|
||||
|
||||
```css
|
||||
/* hold=none 时 */
|
||||
.cds-root [data-cds=SkeletonGroup][data-motion=lockstep][data-hold=none] {
|
||||
animation: cds-skeleton-breathe 2s ease-in-out var(--cds-skel-reveal-end) infinite
|
||||
}
|
||||
|
||||
/* hold=delayed 时:叠加揭示动画 */
|
||||
.cds-root [data-cds=SkeletonGroup][data-motion=lockstep][data-hold=delayed] {
|
||||
animation: cds-skeleton-breathe 2s ease-in-out var(--cds-skel-reveal-end) infinite,
|
||||
cds-placeholder-reveal .15s ease-out
|
||||
calc(var(--placeholder-reveal-delay, .5s) - var(--cds-skel-elapsed) - var(--cds-skel-settled))
|
||||
backwards
|
||||
}
|
||||
```
|
||||
|
||||
**注意**:lockstep 的动画挂在 **`SkeletonGroup` 容器**上,作用于整个组;wave 的动画挂在**每个 `Skeleton` 子块**上,带级联延时。这是两种模式的本质区别。
|
||||
|
||||
### 3.3.3 两种模式的对比表
|
||||
|
||||
| 维度 | `data-motion="wave"` | `data-motion="lockstep"` |
|
||||
| :--- | :--- | :--- |
|
||||
| 动画 | `cds-skeleton-breathe` 2s ease-in-out | 同左 |
|
||||
| 挂载位置 | 每个 `[data-cds=Skeleton]` 子块 | `[data-cds=SkeletonGroup]` 容器 |
|
||||
| 延时 | `reveal-end + skel-i × 90ms` | `reveal-end`(无级联) |
|
||||
| 效果 | 逐个错峰呼吸 | 整组同步呼吸 |
|
||||
| 适用 | 大卡片、多块布局 | 密集列表 |
|
||||
|
||||
### 3.3.4 与分支的差异汇总
|
||||
|
||||
| 项 | CDS 3.0 | 分支 | 判定 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| wave 动画 | `cds-skeleton-breathe`(呼吸) | `cds-skeleton-sweep`(扫光) | ❌ 用错动画 |
|
||||
| lockstep 挂载 | Group 容器 | `.cds-skeleton[data-motion="lockstep"]`(每个块) | ⚠️ 语义不同但效果接近 |
|
||||
| 级联延时 | `reveal-end + i×90ms` | `i × 90ms` | ⚠️ 缺 reveal-end 项 |
|
||||
| 扫光触发 | 由 `.cds-skel-sweep-band` 独立元素承担,与 motion 无关 | 绑定到 data-motion | ❌ 混淆了两种机制 |
|
||||
|
||||
**核心误解**:分支把「扫光」和「呼吸」当成了 wave / lockstep 两种模式。**实际是两个正交的机制**:
|
||||
|
||||
- **扫光**(`.cds-skel-sweep-band`):独立元素,`2s linear`,与 data-motion 无关
|
||||
- **呼吸**(`cds-skeleton-breathe`):由 data-motion 的 wave/lockstep 驱动,`2s ease-in-out`
|
||||
|
||||
---
|
||||
|
||||
## 3.4 骨架屏:RTL 语言级适配
|
||||
|
||||
### 3.4.1 CSS 原文
|
||||
|
||||
```css
|
||||
/* 扫光带:RTL 时渐变角度 105° → 255°,并反转动画方向 */
|
||||
.cds-root .cds-skel-sweep-clip:where(
|
||||
:root[dir=rtl] :is(
|
||||
:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb),
|
||||
:lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn),
|
||||
:lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)
|
||||
)
|
||||
) .cds-skel-sweep-band {
|
||||
background-image: linear-gradient(255deg, transparent 35%, var(--cds-skeleton-sheen), transparent 65%);
|
||||
animation-direction: reverse
|
||||
}
|
||||
|
||||
/* 骨架块自身的伪元素同样反转 */
|
||||
.cds-root [data-cds=Skeleton][data-cds=Skeleton]:where(
|
||||
:root[dir=rtl] :is(…同上 19 个语言…)
|
||||
):after {
|
||||
animation-direction: reverse
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4.2 三个关键点
|
||||
|
||||
| 点 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **`:lang()` 精确语言列表** | 19 个语言代码:`ae ar arc bcc bqi ckb dv fa glk he ku mzn nqo pnb ps sd ug ur yi` |
|
||||
| **`animation-direction: reverse`** | 不只是翻转渐变角度,还要反转动画播放方向,扫光才会从右往左 |
|
||||
| **`[data-cds=Skeleton][data-cds=Skeleton]` 重复属性** | 这是提高选择器特异性的技巧,用于压过其它规则 |
|
||||
|
||||
### 3.4.3 与分支的差异
|
||||
|
||||
| 项 | CDS 3.0 | 分支 | 判定 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| RTL 判定 | `:root[dir=rtl]` **且** `:lang(<19 个语言之一>)` | 仅 `:dir(rtl)` / `[dir="rtl"]` | ⚠️ 范围不同 |
|
||||
| 渐变角度翻转 | ✅ 105° → 255° | ✅ 105° → 255° | ✅ 一致 |
|
||||
| 动画方向反转 | ✅ `animation-direction: reverse` | ❌ **缺失** | ❌ 分支的 RTL 仍然从左往右扫 |
|
||||
|
||||
**分支的 RTL 实测**(Chrome,`<html dir="rtl">`):
|
||||
|
||||
```
|
||||
LTR: transform = matrix(1,0,0,1,-220,0)
|
||||
RTL: transform = matrix(1,0,0,1,-220,0) ← 完全相同,未反转
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3.5 减少动效与残留动效兜底
|
||||
|
||||
### 3.5.1 三层兜底机制
|
||||
|
||||
CDS 3.0 对减少动效(reduced motion)有三层处理:
|
||||
|
||||
```css
|
||||
/* 第 1 层:属性开关 —— data-reduce-motion=true 时也要收尾 */
|
||||
.cds-root:where([data-reduce-motion=true],[data-reduce-motion=true] *)
|
||||
[data-cds=SkeletonGroup][data-hold][data-motion],
|
||||
.cds-root:where([data-reduce-motion=true],[data-reduce-motion=true] *)
|
||||
[data-cds=SkeletonGroup][data-hold][data-motion] [data-cds=Skeleton] {
|
||||
animation-duration: .01ms;
|
||||
animation-iteration-count: 1
|
||||
}
|
||||
|
||||
.cds-root:where([data-reduce-motion=true],[data-reduce-motion=true] *)
|
||||
[data-cds=SkeletonGroup][data-hold][data-motion] .cds-skel-sweep-band {
|
||||
display: none
|
||||
}
|
||||
```
|
||||
|
||||
**第 2 / 3 层**(在其它组件上,抽样证据):
|
||||
|
||||
```css
|
||||
/* chip 进入动画:媒体查询层 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.cds-root .cds-chip-in { animation: none }
|
||||
}
|
||||
|
||||
/* chip 进入动画:属性层 */
|
||||
.cds-root:where([data-reduce-motion=true],[data-reduce-motion=true] *) .cds-chip-in {
|
||||
animation: none
|
||||
}
|
||||
|
||||
/* flipbook:媒体查询层 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.cds-root .cds-flipbook { animation: none }
|
||||
}
|
||||
|
||||
/* flipbook:属性层 */
|
||||
.cds-root:where([data-reduce-motion=true],[data-reduce-motion=true] *) .cds-flipbook {
|
||||
animation: none
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5.2 双轨制
|
||||
|
||||
CDS 3.0 的减少动效**同时监听两种来源**:
|
||||
|
||||
| 来源 | 选择器 | 层级 |
|
||||
| :--- | :--- | :--- |
|
||||
| 系统偏好 | `@media (prefers-reduced-motion: reduce)` | 媒体查询 |
|
||||
| 应用开关 | `[data-reduce-motion=true]` | 属性 |
|
||||
|
||||
两者**并列**,各自独立生效。这是为 Electron 应用设计的:系统偏好可能不可靠(或用户想单独控制某个应用),所以提供应用级显式开关。
|
||||
|
||||
### 3.5.3 与分支的差异
|
||||
|
||||
| 项 | CDS 3.0 | 分支 |
|
||||
| :--- | :--- | :--- |
|
||||
| `@media (prefers-reduced-motion)` | ✅ 多处使用 | ⚠️ 仅 1 处,且只针对 `[data-cds-anim]` |
|
||||
| `[data-reduce-motion=true]` 属性开关 | ✅ 全组件覆盖 | ❌ **完全缺失** |
|
||||
| 骨架屏动效兜底 | ✅ 有 | ❌ 无 |
|
||||
|
||||
**结论**:分支的骨架屏动画**在任何情况下都无限循环**,不响应系统的减少动效设置。
|
||||
|
||||
---
|
||||
|
||||
## 3.6 Pill 胶囊:1.32x 过冲的 9 个关键帧
|
||||
|
||||
v2 文档提到「Pill 1.32x 过冲回弹」,但未给出关键帧定义。以下是 3.6556.2 的原文。
|
||||
|
||||
### 3.6.1 四个关键帧
|
||||
|
||||
```css
|
||||
@keyframes cds-pill-in { 0% { transform: scale(.8) } }
|
||||
@keyframes cds-pill-pop-up { to { transform: scale(1.32) } }
|
||||
@keyframes cds-pill-pop-down { 0% { transform: scale(1.32) } }
|
||||
@keyframes cds-pill-dip-up { 0% { transform: scale(.82) } }
|
||||
```
|
||||
|
||||
### 3.6.2 命名规律与数值
|
||||
|
||||
| 关键帧 | 关键值 | 用途推断(基于命名与数值) |
|
||||
| :--- | :--- | :--- |
|
||||
| `cds-pill-in` | `scale(.8)` 起点 | 入场 |
|
||||
| `cds-pill-pop-up` | 终点 `scale(1.32)` | 上弹到 1.32x |
|
||||
| `cds-pill-pop-down` | 起点 `scale(1.32)` | 从 1.32x 回落 |
|
||||
| `cds-pill-dip-up` | 起点 `scale(.82)` | 下压后回升 |
|
||||
|
||||
**1.32 与 0.82 的对称性**:
|
||||
|
||||
```
|
||||
1.32 - 1.00 = +0.32
|
||||
1.00 - 0.82 = +0.18
|
||||
```
|
||||
|
||||
不是严格对称。`.82` 是压入,`1.32` 是弹出,弹比压更夸张——符合「过冲手感」的意图。
|
||||
|
||||
### 3.6.3 完整的 pill 关键帧家族
|
||||
|
||||
3.6556.2 中与 pill 相关的关键帧共 **9 个**:
|
||||
|
||||
```
|
||||
cds-pill-dip-down
|
||||
cds-pill-dip-up
|
||||
cds-pill-in
|
||||
cds-pill-out
|
||||
cds-pill-pop-down
|
||||
cds-pill-pop-up
|
||||
cds-pill-roll-in-down
|
||||
cds-pill-roll-in-up
|
||||
cds-pill-roll-out
|
||||
```
|
||||
|
||||
分为三个系列:
|
||||
|
||||
| 系列 | 成员 | 数值特征 |
|
||||
| :--- | :--- | :--- |
|
||||
| `in` / `out` | `cds-pill-in`、`cds-pill-out` | 入场/退场 |
|
||||
| `pop` | `cds-pill-pop-up`、`cds-pill-pop-down` | `scale(1.32)` 过冲 |
|
||||
| `dip` | `cds-pill-dip-up`、`cds-pill-dip-down` | `scale(.82)` 压入 |
|
||||
| `roll` | `cds-pill-roll-in-down`、`cds-pill-roll-in-up`、`cds-pill-roll-out` | 滚动进出 |
|
||||
|
||||
**证据**:`grep -o "@keyframes cds-pill-[a-z-]*" MainWindowPage-BjVtjJFx.css | sed 's/@keyframes //' | sort -u | wc -l` → **9**
|
||||
|
||||
**分支与 open-claude-ui 均无这 9 个关键帧中的任何一个。** open-claude-ui 的 `ClaudeMotionPresets.ts` 用 Framer Motion spring 实现类似效果,但数值体系(`stiffness/damping/mass`)与 CSS keyframes(`scale` 离散值)不是同一套。
|
||||
|
||||
---
|
||||
|
||||
## 3.7 滚动渐隐:`@property` 驱动的纯 CSS 实现
|
||||
|
||||
### 3.7.1 `@property` 注册
|
||||
|
||||
CDS 3.0 用 `@property` 注册了一批可动画的自定义属性。与滚动渐隐相关的 **17 个**:
|
||||
|
||||
```css
|
||||
@property --cds-scroll-fade-top { … }
|
||||
@property --cds-scroll-fade-top-on { … }
|
||||
@property --cds-scroll-fade-top-raw { … }
|
||||
@property --cds-scroll-fade-bottom { … }
|
||||
@property --cds-scroll-fade-bottom-on { … }
|
||||
@property --cds-scroll-fade-bottom-raw { … }
|
||||
@property --cds-scroll-fade-left { … }
|
||||
@property --cds-scroll-fade-left-on { … }
|
||||
@property --cds-scroll-fade-left-raw { … }
|
||||
@property --cds-scroll-fade-right { … }
|
||||
@property --cds-scroll-fade-right-on { … }
|
||||
@property --cds-scroll-fade-right-raw { … }
|
||||
@property --cds-scroll-fade-size { … }
|
||||
@property --cds-scroll-fade-x-active { … }
|
||||
@property --cds-scroll-fade-y-active { … }
|
||||
@property --cds-scroll-fade-strip-active { … }
|
||||
@property --cds-scroll-fade-strip-raw { … }
|
||||
```
|
||||
|
||||
**证据**:`grep -o "@property --cds-scroll-fade-[a-zA-Z0-9_-]*" MainWindowPage-BjVtjJFx.css | sort -u | wc -l` → **17**
|
||||
|
||||
另有 3 个与吸边(sticky edge)相关的 `@property`:`--cds-sticky-edge-active` / `--cds-sticky-edge-end` / `--cds-sticky-edge-start`。
|
||||
|
||||
### 3.7.2 默认值
|
||||
|
||||
```css
|
||||
.cds-root {
|
||||
--cds-scroll-fade-size: 28px;
|
||||
--cds-scroll-fade-top: 0px;
|
||||
--cds-scroll-fade-bottom: 0px;
|
||||
--cds-scroll-fade-left: 0px;
|
||||
--cds-scroll-fade-right: 0px;
|
||||
--cds-scroll-fade-top-raw: 0px;
|
||||
--cds-scroll-fade-bottom-raw: 0px;
|
||||
--cds-scroll-fade-left-raw: 0px;
|
||||
--cds-scroll-fade-right-raw: 0px;
|
||||
--cds-scroll-fade-y-active: 0;
|
||||
--cds-scroll-fade-x-active: 0;
|
||||
--cds-scroll-fade-top-on: 1;
|
||||
--cds-scroll-fade-bottom-on: 1;
|
||||
--cds-scroll-fade-left-on: 1;
|
||||
--cds-scroll-fade-right-on: 1;
|
||||
--cds-scroll-fade-strip-raw: 0;
|
||||
--cds-scroll-fade-strip-active: 0
|
||||
}
|
||||
```
|
||||
|
||||
### 3.7.3 滚动驱动
|
||||
|
||||
```css
|
||||
animation-timeline: scroll()
|
||||
```
|
||||
|
||||
出现 1 处(用于 `[data-cds-title-leaves]` 的页面标题动画)。
|
||||
|
||||
### 3.7.4 与 v2 文档 / 分支的差异
|
||||
|
||||
| 项 | CDS 3.0 | v2 文档声称 | open-claude-ui 现状 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 实现方式 | `@property` + `animation-timeline: scroll()` | 「7 停非线性 mask」 | `@property` + `animation-timeline` |
|
||||
| 渐隐尺寸 | `--cds-scroll-fade-size: 28px` | 未记录 | 已实现 |
|
||||
| mask-image | `linear-gradient(#000 0 0)`(纯色占位,由 `@property` 变量控制) | 「7 停 mask」 | 待核对 |
|
||||
|
||||
> **⚠️ 需注意**:本文在 3.6556.2 主样式表中只找到 `mask-image:linear-gradient(#000 0 0)` 这一处,**未找到 v2 文档描述的「7 停非线性 mask」原文**。这可能是:
|
||||
> 1. 该实现已重构为 `@property` 变量驱动
|
||||
> 2. 该实现位于其它 CSS 文件(如 `window-shared.css`)
|
||||
> 3. v2 文档的「7 停」描述本身是推断而非取证
|
||||
>
|
||||
> 本文**不对「7 停 mask 是否仍然存在」下结论**,标注为 `unknown`,需进一步核对 `window-shared.css` 与运行时计算样式。
|
||||
|
||||
---
|
||||
|
||||
## 本章证据索引
|
||||
|
||||
| 断言 | 证据 |
|
||||
| :--- | :--- |
|
||||
| 骨架屏双层 `data-cds` | CSS 选择器 `[data-cds=SkeletonGroup]` / `[data-cds=Skeleton]` 原文 |
|
||||
| `.cds-skel-sweep-band` 是真实元素 | CSS `.cds-root .cds-skel-sweep-band { … }` 无 `::after` |
|
||||
| `data-hold` 三态 | 三段 `[data-hold]` / `[data-hold=none]` / `[data-hold=delayed]` 规则 |
|
||||
| wave 用 breathe 不用 sweep | `.cds-root [data-cds=SkeletonGroup][data-motion=wave] [data-cds=Skeleton] { animation: cds-skeleton-breathe … }` |
|
||||
| lockstep 挂 Group | `[data-cds=SkeletonGroup][data-motion=lockstep]…` 选择器层级 |
|
||||
| 90ms 级联 | `calc(var(--cds-skel-reveal-end,0s) + var(--cds-skel-i,0) * 90ms)` |
|
||||
| RTL 19 个语言 + reverse | `:lang(ae,ar,arc,…)` 完整列表 + `animation-direction:reverse` |
|
||||
| 减少动效双轨 | `@media (prefers-reduced-motion:reduce)` 与 `[data-reduce-motion=true]` 并列出现 |
|
||||
| pill 1.32x | `@keyframes cds-pill-pop-up { to { transform: scale(1.32) } }` |
|
||||
| `@property` 滚动渐隐 17 项 | `@property --cds-scroll-fade-*` 清单(`sort -u \| wc -l` = 17) |
|
||||
| `scroll-fade-size: 28px` | `.cds-root { --cds-scroll-fade-size: 28px; … }` |
|
||||
@@ -0,0 +1,724 @@
|
||||
---
|
||||
title: "第四章:系统底座全景解密"
|
||||
category: reverse-engineering
|
||||
domain: architecture/desktop_application
|
||||
version: "3.0 (3.6556.2)"
|
||||
status: stable
|
||||
date: 2026-10-26
|
||||
author: "REA 逆向工程实验室"
|
||||
evidence_root: "b2dfbbb40325d978dcb7561ad12a6f9bc199f8f46b7d3d2899af74ce8965d68a"
|
||||
---
|
||||
|
||||
# 第四章:系统底座全景解密
|
||||
|
||||
> 本章是**完整清单章**。所有清单均由 `grep | sort -u` 直接从 `MainWindowPage-BjVtjJFx.css` 生成,可逐条复现。
|
||||
|
||||
---
|
||||
|
||||
## 4.1 `@keyframes` 完整 55 项清单
|
||||
|
||||
```bash
|
||||
grep -o "@keyframes [a-zA-Z0-9_-]*" MainWindowPage-BjVtjJFx.css | sed 's/@keyframes //' | sort -u | wc -l
|
||||
# → 55
|
||||
```
|
||||
|
||||
### 4.1.1 按功能分组
|
||||
|
||||
**骨架屏与占位(3)**
|
||||
|
||||
```
|
||||
cds-skeleton-sweep
|
||||
cds-skeleton-breathe
|
||||
cds-placeholder-reveal
|
||||
```
|
||||
|
||||
**Pill 胶囊(9)**
|
||||
|
||||
```
|
||||
cds-pill-in
|
||||
cds-pill-out
|
||||
cds-pill-pop-up
|
||||
cds-pill-pop-down
|
||||
cds-pill-dip-up
|
||||
cds-pill-dip-down
|
||||
cds-pill-roll-in-up
|
||||
cds-pill-roll-in-down
|
||||
cds-pill-roll-out
|
||||
```
|
||||
|
||||
**滚动渐隐(8)**
|
||||
|
||||
```
|
||||
cds-scroll-fade-top
|
||||
cds-scroll-fade-bottom
|
||||
cds-scroll-fade-left
|
||||
cds-scroll-fade-right
|
||||
cds-scroll-fade-x-active
|
||||
cds-scroll-fade-y-active
|
||||
cds-scroll-fade-strip-active
|
||||
cds-scroll-fade-strip-in
|
||||
cds-scroll-fade-strip-out
|
||||
```
|
||||
|
||||
(注:`strip` 系列 3 个,计入后滚动渐隐共 10 个。上一行列出的是全部 10 项。)
|
||||
|
||||
**Shimmer 微光(9)**
|
||||
|
||||
```
|
||||
cds-shimmer
|
||||
cds-shimmer-text-band
|
||||
cds-shimmer-text-band-rtl
|
||||
cds-shimmer-text-band-strong
|
||||
cds-shimmer-text-band-strong-rtl
|
||||
cds-shimmer-text-shine
|
||||
cds-shimmer-text-shine-rtl
|
||||
cds-shimmer-text-shine-strong
|
||||
cds-shimmer-text-shine-strong-rtl
|
||||
```
|
||||
|
||||
**吸附边缘(3)**
|
||||
|
||||
```
|
||||
cds-sticky-edge-start
|
||||
cds-sticky-edge-end
|
||||
cds-sticky-edge-active
|
||||
```
|
||||
|
||||
**工作指示点(3)**
|
||||
|
||||
```
|
||||
cds-working-dot-halo
|
||||
cds-working-dot-lands
|
||||
cds-working-dot-veil
|
||||
```
|
||||
|
||||
**进出场通用(4)**
|
||||
|
||||
```
|
||||
cds-fade-in
|
||||
cds-reveal-in
|
||||
cds-reveal-out
|
||||
cds-slide-in-start
|
||||
cds-slide-in-end
|
||||
```
|
||||
|
||||
(实际 5 个。)
|
||||
|
||||
**Drop / Chip / 拖放(3)**
|
||||
|
||||
```
|
||||
cds-chip-in
|
||||
cds-drop-icon-rise
|
||||
cds-drop-ring-in
|
||||
```
|
||||
|
||||
**布局与网格(2)**
|
||||
|
||||
```
|
||||
cds-grid-rows-in
|
||||
cds-morph-in
|
||||
```
|
||||
|
||||
**页面转场(2)**
|
||||
|
||||
```
|
||||
cds-page-tabs-arrive
|
||||
cds-page-title-leaves
|
||||
```
|
||||
|
||||
**图标解析(2)**
|
||||
|
||||
```
|
||||
cds-resolve-glyph
|
||||
cds-resolve-square
|
||||
```
|
||||
|
||||
**附件与编辑器(3)**
|
||||
|
||||
```
|
||||
cds-attachment-out
|
||||
cds-editor-comment-working
|
||||
cds-flipbook
|
||||
```
|
||||
|
||||
**其它(1)**
|
||||
|
||||
```
|
||||
spin
|
||||
cds-dot-pulse
|
||||
```
|
||||
|
||||
### 4.1.2 与 open-claude-ui 的差异
|
||||
|
||||
```bash
|
||||
# ours
|
||||
grep -o "@keyframes [a-zA-Z0-9_-]*" code-examples/cds-theme.css | sort -u | wc -l # 32
|
||||
|
||||
# client
|
||||
grep -o "@keyframes [a-zA-Z0-9_-]*" MainWindowPage-BjVtjJFx.css | sort -u | wc -l # 55
|
||||
```
|
||||
|
||||
| 集合 | 数量 | 明细 |
|
||||
| :--- | ---: | :--- |
|
||||
| **交集**(双方都有) | **11** | `cds-dot-pulse`、6 个 `cds-scroll-fade-*`、`cds-shimmer-text-shine`、`cds-shimmer-text-shine-strong`、`cds-skeleton-breathe`、`cds-skeleton-sweep` |
|
||||
| **仅客户端有** | **44** | 见 4.1.1 减去交集的 11 项 |
|
||||
| **仅我方有** | **21** | 见 4.1.3 |
|
||||
|
||||
### 4.1.3 我方独有的 21 个关键帧(客户端中不存在)
|
||||
|
||||
```bash
|
||||
comm -23 <(grep -o "@keyframes [a-zA-Z0-9_-]*" code-examples/cds-theme.css | sed 's/@keyframes //' | sort -u) \
|
||||
<(grep -o "@keyframes [a-zA-Z0-9_-]*" MainWindowPage-BjVtjJFx.css | sed 's/@keyframes //' | sort -u)
|
||||
```
|
||||
|
||||
```
|
||||
_grapheme-reveal_1plog_1
|
||||
cds-anim-loop
|
||||
dframe-clay-breathe
|
||||
dframe-dot-blink
|
||||
epitaxy-band-exit
|
||||
epitaxy-check-draw
|
||||
epitaxy-linear-sweep
|
||||
epitaxy-linear-sweep-rtl
|
||||
epitaxy-pill-pulse
|
||||
file-thumbnail-grow-in
|
||||
message-fade-up
|
||||
saved-check-decay
|
||||
timeline-block-enter
|
||||
timeline-expand
|
||||
timeline-fade-in
|
||||
timeline-fade-out
|
||||
timeline-quiet-dot
|
||||
timeline-status-fall
|
||||
timeline-status-rise
|
||||
wl-glow
|
||||
wl-halo
|
||||
```
|
||||
|
||||
**这说明一件重要的事**:`open-claude-ui` 的动效体系中有 **21/32(66%)的关键帧在 3.6556.2 客户端中不存在**。
|
||||
|
||||
可能的原因(本文**不推断**具体哪一种):
|
||||
|
||||
1. 这些名字是 v1/v2 时期逆向得到的,3.6556.2 已重命名或移除
|
||||
2. 这些名字是 `open-claude-ui` 自行命名的抽象(非客户端原名)
|
||||
3. 这些动效在其它 CSS 文件(如 `window-shared.css`)中
|
||||
|
||||
**⚠️ 本文不对上述三种可能做判定**,标注为 `unknown`。需进一步核对:
|
||||
- `window-shared.css`(8,078 bytes)
|
||||
- `window-legacy-pins.css`(6,289 bytes)
|
||||
- 各 renderer 的 `assets/main-*.css`
|
||||
|
||||
---
|
||||
|
||||
## 4.2 `data-cds` 完整 45 项命名空间清单
|
||||
|
||||
```bash
|
||||
grep -o "\[data-cds=[A-Za-z-]*\]" MainWindowPage-BjVtjJFx.css | sed 's/\[data-cds=//;s/\]//' | sort -u | wc -l
|
||||
# → 45
|
||||
```
|
||||
|
||||
### 4.2.1 完整清单(字母序)
|
||||
|
||||
```
|
||||
Accordion
|
||||
AccordionHeader
|
||||
Aside
|
||||
AskUserQuestion
|
||||
AssistantMessage
|
||||
Banner
|
||||
Button
|
||||
ButtonGroup
|
||||
Card
|
||||
CardLink
|
||||
ChartContainer
|
||||
ChatComposerChin
|
||||
ChatComposerDock
|
||||
ChatComposerEditor
|
||||
ChatComposerNotices
|
||||
ChatComposerQueuedMessages
|
||||
CommentCard
|
||||
DataTable
|
||||
DiffIndicator
|
||||
FileDiff
|
||||
Heatmap
|
||||
Icon
|
||||
MessageActions
|
||||
MessageAttachments
|
||||
MessageAttachmentsFile
|
||||
MessageAttachmentsImage
|
||||
MessageAttachmentsQuote
|
||||
MessageAttachmentsVideo
|
||||
ModelSelector
|
||||
ModelSelectorEffort
|
||||
Page
|
||||
Panel
|
||||
PointChartContainer
|
||||
PreviewCard
|
||||
Prose
|
||||
Pulse
|
||||
Skeleton
|
||||
SkeletonGroup
|
||||
SplitDropdownButton
|
||||
Surface
|
||||
Table
|
||||
Toolbar
|
||||
TurnStatus
|
||||
TurnStatusStep
|
||||
UserMessage
|
||||
```
|
||||
|
||||
### 4.2.2 与 v2 文档的差异
|
||||
|
||||
v2 文档声称「40 大标准化 `data-cds` 命名空间」。3.6556.2 实测 **45 个**。
|
||||
|
||||
**证据**:
|
||||
- `docs/v2/01-综述与版本边界.md:29`:「40 大标准化 data-cds 原生命名空间」
|
||||
- `docs/v2/04-系统底座全景解密.md:32`:「[40 大标准化组件清单]」
|
||||
- `docs/v2/06-总结与升格意义.md:17`:「扩充并标准化为 **40 大 `data-cds` 顶级命名空间**」
|
||||
- 实测 `sort -u | wc -l` → **45**
|
||||
|
||||
> **⚠️ 无法确定的部分**:v2 的 40 个具体名单未在 v2 文档中逐条列出(04 章只有占位符 `[40 大标准化组件清单]`)。因此**无法给出「哪 5 个是新增」的精确 diff**。本文只确认总数从 40 变为 45。
|
||||
|
||||
### 4.2.3 `Skeleton` 与 `SkeletonGroup` 的单复数设计
|
||||
|
||||
这是 3.0 独有的**双命名空间**:
|
||||
|
||||
| 命名空间 | 职责 |
|
||||
| :--- | :--- |
|
||||
| `SkeletonGroup` | 组容器:承载 `data-motion`、`data-hold`,统一控制整组动画 |
|
||||
| `Skeleton` | 单个骨架块:承载 `--cds-skel-i` 级联索引、扫光子元素 |
|
||||
|
||||
**证据**:CSS 中两者作为不同选择器存在
|
||||
|
||||
```css
|
||||
.cds-root [data-cds=SkeletonGroup][data-motion=wave] [data-cds=Skeleton] { … }
|
||||
```
|
||||
|
||||
这一条同时引用 `SkeletonGroup`(祖先)与 `Skeleton`(后代),证明是两层结构。
|
||||
|
||||
---
|
||||
|
||||
## 4.3 色彩体系
|
||||
|
||||
### 4.3.1 35 阶 gray 全表
|
||||
|
||||
```bash
|
||||
grep -o "\--cds-gray-[0-9]*:[^;}]*" MainWindowPage-BjVtjJFx.css | sort -u | wc -l
|
||||
# → 35
|
||||
```
|
||||
|
||||
| 阶 | 值 | 阶 | 值 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `gray-0` | `#fff` | `gray-450` | `#7b7974` |
|
||||
| `gray-10` | `#fcfcfb` | `gray-500` | `#6d6b67` |
|
||||
| `gray-20` | `#f9f9f7` | `gray-550` | `#5f5e5a` |
|
||||
| `gray-30` | `#f6f6f4` | `gray-600` | `#52514e` |
|
||||
| `gray-40` | `#f3f3f0` | `gray-650` | `#454442` |
|
||||
| `gray-50` | `#f0efec` | `gray-700` | `#383835` |
|
||||
| `gray-60` | `#edece8` | `gray-750` | `#2c2c2a` |
|
||||
| `gray-70` | `#eae9e4` | `gray-800` | `#20201f` |
|
||||
| `gray-80` | `#e7e6e1` | `gray-810` | `#1e1e1d` |
|
||||
| `gray-90` | `#e4e3dd` | `gray-820` | `#1c1c1b` |
|
||||
| `gray-100` | `#e1e0d9` | `gray-830` | `#1a1a19` |
|
||||
| `gray-150` | `#d2d1c7` | `gray-840` | `#181817` |
|
||||
| `gray-200` | `#c3c2b7` | `gray-850` | `#151515` |
|
||||
| `gray-250` | `#b4b3a8` | `gray-860` | `#131313` |
|
||||
| `gray-300` | `#a5a49a` | `gray-870` | `#111` |
|
||||
| `gray-350` | `#97958d` | `gray-880` | `#0f0f0f` |
|
||||
| `gray-400` | `#898781` | `gray-890` | `#0d0d0d` |
|
||||
| | | `gray-900` | `#0b0b0b` |
|
||||
|
||||
### 4.3.2 35 阶 orange 全表
|
||||
|
||||
```bash
|
||||
grep -o "\--cds-orange-[0-9]*:[^;}]*" MainWindowPage-BjVtjJFx.css | sort -u | wc -l
|
||||
# → 35
|
||||
```
|
||||
|
||||
| 阶 | 值 | 阶 | 值 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `orange-0` | `#fff` | `orange-450` | `#c25124` |
|
||||
| `orange-10` | `#fefbfa` | `orange-500` | `#ae461c` |
|
||||
| `orange-20` | `#fdf7f5` | `orange-550` | `#993d19` |
|
||||
| `orange-30` | `#fcf4f0` | `orange-600` | `#863311` |
|
||||
| `orange-40` | `#faf0ec` | `orange-650` | `#712b0f` |
|
||||
| `orange-50` | `#f9ece7` | `orange-700` | `#5d230b` |
|
||||
| `orange-60` | `#f8e9e2` | `orange-750` | `#4b1b08` |
|
||||
| `orange-70` | `#f7e5dd` | `orange-800` | `#371407` |
|
||||
| `orange-80` | `#f7e1d7` | `orange-810` | `#341307` |
|
||||
| `orange-90` | `#f7dcd1` | `orange-820` | `#301106` |
|
||||
| `orange-100` | `#f7d8cb` | `orange-830` | `#2d1006` |
|
||||
| `orange-150` | `#f3c5b2` | `orange-840` | `#290f06` |
|
||||
| `orange-200` | `#f4ae94` | `orange-850` | `#240e07` |
|
||||
| `orange-250` | `#f09978` | `orange-860` | `#1f0e08` |
|
||||
| `orange-300` | `#ec835a` | `orange-870` | `#1a0e09` |
|
||||
| `orange-350` | `#eb6834` | `orange-880` | `#150d0a` |
|
||||
| `orange-400` | `#d95926` | `orange-890` | `#100c0b` |
|
||||
| | | `orange-900` | `#0b0b0b` |
|
||||
|
||||
### 4.3.3 品牌陶土色:三个来源三个值
|
||||
|
||||
| 令牌 | v2 文档 | open-claude-ui | **3.6556.2 实测** |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `--cds-clay` | `#d97757` | `#d97757` | **`#d97757`** ✅ 三方一致 |
|
||||
| `--cds-clay-emphasized` | `#cc5a36`(双模同值) | `var(--cds-clay-hover)`(light `#cc5a36` / dark `#e08264`) | **`#c6613f`**(无暗色覆盖) |
|
||||
|
||||
**`--cds-clay-emphasized` 的证据**
|
||||
|
||||
```bash
|
||||
grep -c "\--cds-clay-emphasized:" MainWindowPage-BjVtjJFx.css # → 2
|
||||
# 定义位置:偏移 133889(:root 块)与 152242(.cds-root 块),值均为 #c6613f
|
||||
# 暗色覆盖:0 处(data-mode=dark 块内无此令牌)
|
||||
grep -o "\--cds-clay-emphasized:[^;}]*" MainWindowPage-BjVtjJFx.css # → --cds-clay-emphasized:#c6613f
|
||||
```
|
||||
|
||||
原文上下文(连续令牌,证明不是拼接错误):
|
||||
|
||||
```css
|
||||
--cds-oncolor-200:#f8f8f7bf;--cds-oncolor-300:#f8f8f780;
|
||||
--cds-clay:#d97757;--cds-clay-emphasized:#c6613f;
|
||||
--cds-heather:#cbcadb;--cds-plum:#827db…
|
||||
```
|
||||
|
||||
**关键判定**:`#c6613f` 与 open-claude-ui 仓库 `AGENTS.md` 里记录的 `--brand: #c6613f`(「Official desktop brand primary color」)**完全一致**。
|
||||
|
||||
这强烈提示:`#c6613f` 才是官方品牌主色,而 `--cds-clay` 的 `#d97757` 是「指示点/脉冲」用的浅色变体。两者**不是同一个东西**,不应互相派生。
|
||||
|
||||
> **⚠️ 本文不对「为什么 v2 文档写 #cc5a36」做溯源**——v2.37337.1 的 CSS 已不在本机,无法比对。
|
||||
|
||||
### 4.3.4 其它命名色
|
||||
|
||||
```css
|
||||
--cds-heather: #cbcadb
|
||||
--cds-plum: #827db…
|
||||
```
|
||||
|
||||
(`plum` 值在截断处,本文不做补全。)
|
||||
|
||||
---
|
||||
|
||||
## 4.4 模式作用域:`data-mode` 三态
|
||||
|
||||
### 4.4.1 三态定义
|
||||
|
||||
```bash
|
||||
grep -o "data-mode=[a-z]*" MainWindowPage-BjVtjJFx.css | sort | uniq -c
|
||||
```
|
||||
|
||||
```
|
||||
86 data-mode=dark
|
||||
43 data-mode=system
|
||||
29 data-mode=light
|
||||
```
|
||||
|
||||
### 4.4.2 三个作用域选择器
|
||||
|
||||
CDS 3.0 用**三组并列选择器**实现模式切换:
|
||||
|
||||
```css
|
||||
/* 暗色:显式 data-mode=dark,或祖先暗色且自身未指定 light/system */
|
||||
[data-mode=dark] .cds-root:not([data-mode=light]):not([data-mode=system]),
|
||||
.cds-root[data-mode=dark] { … }
|
||||
|
||||
/* 系统:data-mode=system 或完全未指定(跟随 prefers-color-scheme) */
|
||||
[data-mode=system] .cds-root:not([data-mode=light]):not([data-mode=dark]),
|
||||
.cds-root[data-mode=system],
|
||||
.cds-root:not([data-mode]):not([data-mode] *) { … }
|
||||
|
||||
/* 暗色独立作用域(用于局部强制暗色) */
|
||||
.cds-dark-scope[data-mode=dark] { … }
|
||||
```
|
||||
|
||||
**与 `.dark` class 的对比**
|
||||
|
||||
| 方式 | CDS 3.0 | open-claude-ui |
|
||||
| :--- | :--- | :--- |
|
||||
| 暗色选择器 | `[data-mode=dark]` + `.cds-dark-scope[data-mode=dark]` | `.dark` class |
|
||||
| 系统跟随 | `[data-mode=system]` + `prefers-color-scheme` | 无 |
|
||||
| 局部强制 | `.cds-dark-scope` | 无 |
|
||||
|
||||
### 4.4.3 暗色态的实际覆盖
|
||||
|
||||
暗色块内重定义的令牌(抽样证据):
|
||||
|
||||
```css
|
||||
/* 骨架屏暗色覆盖 */
|
||||
[data-mode=dark] .cds-root:not([data-mode=light]):not([data-mode=system]),
|
||||
.cds-root[data-mode=dark] {
|
||||
…
|
||||
--cds-skeleton-base: hsl(from var(--cds-neutral-900) h s l / 7.5%);
|
||||
--cds-skeleton-sheen: hsl(from var(--cds-neutral-900) h s l / 7.5%);
|
||||
}
|
||||
```
|
||||
|
||||
**注意**:暗色下 `--cds-skeleton-base` 与 `--cds-skeleton-sheen` **值相同**(都是 7.5% 中性黑)。这与浅色下两者不同(light 见下方 4.4.4)。
|
||||
|
||||
### 4.4.4 骨架屏令牌的完整取值链
|
||||
|
||||
| 令牌 | 浅色 | 暗色 |
|
||||
| :--- | :--- | :--- |
|
||||
| `--cds-skeleton-base` | `var(--cds-alpha-1)` → `hsl(from var(--cds-neutral-900) h s l / 5%)` | `hsl(from var(--cds-neutral-900) h s l / 7.5%)` |
|
||||
| `--cds-skeleton-sheen` | `var(--cds-alpha-1)` → `hsl(from var(--cds-neutral-900) h s l / 5%)` | `hsl(from var(--cds-neutral-900) h s l / 7.5%)` |
|
||||
|
||||
**证据**:
|
||||
|
||||
```bash
|
||||
# 浅色(.cds-root 作用域)
|
||||
grep -o "\--cds-skeleton-base:var(--cds-alpha-1)" MainWindowPage-BjVtjJFx.css
|
||||
grep -o "\--cds-alpha-1:[^;}]*" MainWindowPage-BjVtjJFx.css
|
||||
# → --cds-alpha-1:hsl(from var(--cds-neutral-900) h s l / 5%)
|
||||
|
||||
# 暗色([data-mode=dark] 作用域,3 处:dark / system / cds-dark-scope)
|
||||
grep -o "\--cds-skeleton-base:hsl(from var(--cds-neutral-900) h s l / 7.5%)" MainWindowPage-BjVtjJFx.css
|
||||
```
|
||||
|
||||
**与 v2 文档 / 分支的差异**
|
||||
|
||||
| 来源 | base(暗) | sheen(暗) |
|
||||
| :--- | :--- | :--- |
|
||||
| v2 文档 | 未记录 | 未记录 |
|
||||
| issue #25 分支 | `rgba(255,255,255,0.06)` | `rgba(255,255,255,0.08)` |
|
||||
| **3.6556.2 实测** | `hsl(from var(--cds-neutral-900) h s l / 7.5%)` | 同左(**与 base 相同**) |
|
||||
|
||||
**两个实质差异**:
|
||||
1. 分支用**白色半透明**(`rgba(255,255,255,…)`),实测用**中性黑半透明**(`hsl(from …neutral-900… / 7.5%)`)
|
||||
2. 分支的 sheen 比 base 更亮(0.08 > 0.06),实测两者**完全相同**
|
||||
|
||||
在暗色背景下,用 `neutral-900`(= `gray-900` = `#0b0b0b`)的 7.5% 叠在深色表面上,效果是**极淡的加深**,而不是分支实现的**提亮**。这是完全相反的视觉方向。
|
||||
|
||||
---
|
||||
|
||||
## 4.5 `@property` 24 项注册清单
|
||||
|
||||
```bash
|
||||
grep -o "@property --cds-[a-zA-Z0-9_-]*" MainWindowPage-BjVtjJFx.css | sed 's/@property //' | sort -u | wc -l
|
||||
# → 24
|
||||
```
|
||||
|
||||
### 4.5.1 完整清单
|
||||
|
||||
**滚动渐隐(17)**
|
||||
|
||||
```
|
||||
--cds-scroll-fade-top
|
||||
--cds-scroll-fade-top-on
|
||||
--cds-scroll-fade-top-raw
|
||||
--cds-scroll-fade-bottom
|
||||
--cds-scroll-fade-bottom-on
|
||||
--cds-scroll-fade-bottom-raw
|
||||
--cds-scroll-fade-left
|
||||
--cds-scroll-fade-left-on
|
||||
--cds-scroll-fade-left-raw
|
||||
--cds-scroll-fade-right
|
||||
--cds-scroll-fade-right-on
|
||||
--cds-scroll-fade-right-raw
|
||||
--cds-scroll-fade-size
|
||||
--cds-scroll-fade-x-active
|
||||
--cds-scroll-fade-y-active
|
||||
--cds-scroll-fade-strip-active
|
||||
--cds-scroll-fade-strip-raw
|
||||
```
|
||||
|
||||
**吸附边缘(3)**
|
||||
|
||||
```
|
||||
--cds-sticky-edge-active
|
||||
--cds-sticky-edge-start
|
||||
--cds-sticky-edge-end
|
||||
```
|
||||
|
||||
**其它(4)**
|
||||
|
||||
```
|
||||
--cds-badge-x-wght
|
||||
--cds-message-actions-opacity
|
||||
--cds-outset-x
|
||||
--cds-outset-y
|
||||
```
|
||||
|
||||
### 4.5.2 与 open-claude-ui 的差异
|
||||
|
||||
```bash
|
||||
# ours: 19
|
||||
grep -o "@property --cds-[a-zA-Z0-9_-]*" code-examples/cds-theme.css | sort -u | wc -l
|
||||
|
||||
# client: 24
|
||||
grep -o "@property --cds-[a-zA-Z0-9_-]*" MainWindowPage-BjVtjJFx.css | sort -u | wc -l
|
||||
```
|
||||
|
||||
**交集 17 / 仅客户端 7 / 仅我方 2**:
|
||||
|
||||
```bash
|
||||
comm -12 <(grep -o "@property --cds-[a-zA-Z0-9_-]*" code-examples/cds-theme.css | sed 's/@property //' | sort -u) \
|
||||
<(grep -o "@property --cds-[a-zA-Z0-9_-]*" MainWindowPage-BjVtjJFx.css | sed 's/@property //' | sort -u) | wc -l
|
||||
# → 17
|
||||
comm -13 … | wc -l # → 7
|
||||
comm -23 … | wc -l # → 2
|
||||
```
|
||||
|
||||
**缺失的 7 个**:
|
||||
|
||||
```
|
||||
--cds-badge-x-wght
|
||||
--cds-message-actions-opacity
|
||||
--cds-outset-x
|
||||
--cds-outset-y
|
||||
--cds-sticky-edge-active
|
||||
--cds-sticky-edge-end
|
||||
--cds-sticky-edge-start
|
||||
```
|
||||
|
||||
**我方独有的 2 个**(客户端中不存在):
|
||||
|
||||
```
|
||||
--cds-anim
|
||||
--cds-anim2
|
||||
```
|
||||
|
||||
这两个是 v2 时代的动画控制变量(配合 `@keyframes cds-anim-loop` 与 `[data-cds-anim]` 使用),在 3.6556.2 主样式表中**未注册为 `@property`**。
|
||||
|
||||
> *(集合运算自洽校验:ours = 交集 17 + 我方独有 2 = 19;client = 交集 17 + 我方缺失 7 = 24。)*
|
||||
|
||||
---
|
||||
|
||||
## 4.6 时间与缓动 Token
|
||||
|
||||
### 4.6.1 5 阶时间矩阵
|
||||
|
||||
```css
|
||||
--cds-dur-fast: 60ms
|
||||
--cds-dur-snap: .12s /* 120ms */
|
||||
--cds-dur-base: .2s /* 200ms */
|
||||
--cds-dur-sheet: .3s /* 300ms */
|
||||
--cds-dur-slow: .45s /* 450ms */
|
||||
```
|
||||
|
||||
**证据**:
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
import re; s=open('MainWindowPage-BjVtjJFx.css').read()
|
||||
d={}
|
||||
for m in re.finditer(r'--cds-dur-([a-z]+):([^;}]+)', s): d.setdefault(m.group(1),set()).add(m.group(2).strip())
|
||||
for k,v in sorted(d.items()): print(k, sorted(v))
|
||||
"
|
||||
# base: ['.2s'] fast: ['60ms'] sheet: ['.3s'] slow: ['.45s'] snap: ['.12s']
|
||||
```
|
||||
|
||||
这与 v2 文档的「5 阶时间 Token 矩阵 (60ms ~ 450ms)」**完全吻合**。
|
||||
|
||||
### 4.6.2 缓动曲线
|
||||
|
||||
```css
|
||||
--cds-ease-out: cubic-bezier(.165, .84, .44, 1)
|
||||
--cds-ease-outs: cubic-bezier(0, 0, .2, 1) /* 注意:同名 token 两个值 */
|
||||
--cds-ease-overshoot: cubic-bezier(.34, 1.3, .64, 1) /* +30% 过冲 */
|
||||
--cds-ease-snap: cubic-bezier(.32, .72, 0, 1)
|
||||
```
|
||||
|
||||
**证据**:
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
import re; s=open('MainWindowPage-BjVtjJFx.css').read()
|
||||
e={}
|
||||
for m in re.finditer(r'--cds-ease-([a-z]+):([^;}]+)', s): e.setdefault(m.group(1),set()).add(m.group(2).strip())
|
||||
for k,v in sorted(e.items()): print(k, sorted(v))
|
||||
"
|
||||
# out: ['cubic-bezier(.165, .84, .44, 1)', 'cubic-bezier(0, 0, .2, 1)']
|
||||
# overshoot: ['cubic-bezier(.34, 1.3, .64, 1)']
|
||||
# snap: ['cubic-bezier(.32, .72, 0, 1)']
|
||||
```
|
||||
|
||||
**注意 `--cds-ease-out` 有两个不同值**,说明它在不同作用域(可能是浅色/暗色,或不同的祖先作用域)被重定义。本文**不推断重定义的边界**。
|
||||
|
||||
**v2 文档声称「3 大核心贝塞尔曲线」**,实测有 **4 个不同的曲线值**(out 2 个 + overshoot + snap)。
|
||||
|
||||
### 4.6.3 `linear()` 弹簧曲线
|
||||
|
||||
本文**未在主样式表中找到** `linear()` 弹簧曲线定义。v2 文档 4.4 节提到「`linear()` 弹簧曲线与时长 Token 的耦合失效」,但 3.6556.2 主表中:
|
||||
|
||||
```bash
|
||||
grep -c "linear(" MainWindowPage-BjVtjJFx.css
|
||||
```
|
||||
|
||||
需要单独核实(见 4.6.4 的不确定项)。
|
||||
|
||||
> **⚠️ 本文不对 `linear()` 弹簧的存在性下结论**。这是一个待核实项。
|
||||
|
||||
---
|
||||
|
||||
## 4.7 圆角与密度系统
|
||||
|
||||
### 4.7.1 圆角 Token
|
||||
|
||||
```css
|
||||
--cds-radius--xs: calc(.3125rem * var(--cds-rem-scale,1)) /* 5px */
|
||||
--cds-radius--sm: calc(.4375rem * var(--cds-rem-scale,1)) /* 7px */
|
||||
--cds-radius--lg: calc(.625rem * var(--cds-rem-scale,1)) /* 10px */
|
||||
--cds-radius-composer: calc(.75rem * var(--cds-rem-scale,1)) /* 12px */
|
||||
```
|
||||
|
||||
**还有一组更大的值**(同一 token 名的另一处定义):
|
||||
|
||||
```css
|
||||
--cds-radius--xs: calc(.375rem * var(--cds-rem-scale,1)) /* 6px */
|
||||
--cds-radius--sm: calc(.3125rem * var(--cds-rem-scale,1)) /* 5px */
|
||||
--cds-radius--lg: calc(.4375rem * var(--cds-rem-scale,1)) /* 7px */
|
||||
--cds-radius-composer: calc(.875rem * var(--cds-rem-scale,1)) /* 14px */
|
||||
```
|
||||
|
||||
**证据**:
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
import re; s=open('MainWindowPage-BjVtjJFx.css').read()
|
||||
for name in ['--cds-radius--xs','--cds-radius--sm','--cds-radius--lg','--cds-radius-composer']:
|
||||
print(name, sorted(set(re.findall(re.escape(name)+r':[^;}]*', s))))
|
||||
"
|
||||
```
|
||||
|
||||
### 4.7.2 两套圆角 = 密度双模
|
||||
|
||||
`--cds-radius-composer` 有两个值:`12px` 与 `14px`。
|
||||
|
||||
这与 v2 文档「Composer 圆角由 20px 降为 12px/14px、引入 Density 系统」**吻合**。
|
||||
|
||||
`--cds-rem-scale` 是密度缩放因子,圆角全部乘以它。
|
||||
|
||||
**⚠️ 本文未能确定两套值的切换条件**(哪个选择器/属性触发 12px vs 14px)。这是一个待核实项。
|
||||
|
||||
### 4.7.3 `--cds-radius` 的间接层
|
||||
|
||||
```css
|
||||
--cds-radius: inherit
|
||||
--cds-radius: .25rem /* 4px,可能是 fallback */
|
||||
--cds-radius: var(--cds-radius--xs)
|
||||
--cds-radius: var(--cds-radius--sm)
|
||||
--cds-radius: var(--cds-radius--lg)
|
||||
--cds-radius: calc(.3125rem * var(--cds-rem-scale,1))
|
||||
--cds-radius: calc(.375rem * var(--cds-rem-scale,1))
|
||||
--cds-radius: calc(.4375rem * var(--cds-rem-scale,1))
|
||||
--cds-radius: calc(.5rem * var(--cds-rem-scale,1))
|
||||
--cds-radius: calc(.625rem * var(--cds-rem-scale,1))
|
||||
```
|
||||
|
||||
**含义**:`--cds-radius` 是一个**上下文相关的别名**,实际值取决于它所在的组件作用域(`inherit` / xs / sm / lg)。这是 CDS 3.0 的「同心圆角」实现方式。
|
||||
|
||||
---
|
||||
|
||||
## 本章证据索引
|
||||
|
||||
| 断言 | 命令 |
|
||||
| :--- | :--- |
|
||||
| keyframes 55 | `grep -o "@keyframes [a-zA-Z0-9_-]*" ... \| sort -u \| wc -l` |
|
||||
| data-cds 45 | `grep -o "\[data-cds=[A-Za-z-]*\]" ... \| sort -u \| wc -l` |
|
||||
| @property 24 | `grep -o "@property --cds-[a-zA-Z0-9_-]*" ... \| sort -u \| wc -l` |
|
||||
| gray 35 阶 | `grep -o "\--cds-gray-[0-9]*:" ... \| sort -u \| wc -l` |
|
||||
| orange 35 阶 | 同上 |
|
||||
| clay-emphasized 2 处同值、0 处暗色覆盖 | `grep -c` + 偏移定位 + 暗色块内搜索 |
|
||||
| data-mode 三态计数 | `grep -o "data-mode=[a-z]*" ... \| sort \| uniq -c` |
|
||||
| 5 阶 dur | python 正则提取后按 key 去重 |
|
||||
| 4 个 ease 值 | 同上 |
|
||||
| 圆角双套 | 同 token 名多值提取 |
|
||||
| 交集/差异 | `comm -12 / -13 / -23` |
|
||||
@@ -0,0 +1,499 @@
|
||||
---
|
||||
title: "第五章:open-claude-ui 差异审计"
|
||||
category: reverse-engineering
|
||||
domain: architecture/desktop_application
|
||||
version: "3.0 (3.6556.2)"
|
||||
status: stable
|
||||
date: 2026-10-26
|
||||
author: "REA 逆向工程实验室"
|
||||
evidence_root: "b2dfbbb40325d978dcb7561ad12a6f9bc199f8f46b7d3d2899af74ce8965d68a"
|
||||
---
|
||||
|
||||
# 第五章:open-claude-ui 差异审计
|
||||
|
||||
> 本章把 `open-claude-ui` 仓库(`feat/issue-25-skeleton-sweep-and-rea-audit` 分支,HEAD `9dc69bc`)的 `code-examples/cds-theme.css` 与 3.6556.2 的 `MainWindowPage-BjVtjJFx.css` 做逐项对比。**每一条差异都有可执行命令。**
|
||||
|
||||
---
|
||||
|
||||
## 5.1 已对齐项
|
||||
|
||||
### 5.1.1 色彩阶
|
||||
|
||||
| 集合 | ours | client | 差异 |
|
||||
| :--- | ---: | ---: | :--- |
|
||||
| `--cds-gray-*` | 35 | 35 | **1**(值等价) |
|
||||
| `--cds-orange-*` | 35 | 35 | **0** |
|
||||
|
||||
**唯一差异**:
|
||||
|
||||
```
|
||||
--cds-gray-870: ours = #111111 | client = #111
|
||||
```
|
||||
|
||||
`#111111` 与 `#111` 是同一个颜色,仅为书写长度不同。**无需修改**(如需严格逐字节对齐可改为 `#111`)。
|
||||
|
||||
**证据**:
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
import re
|
||||
ours=open('code-examples/cds-theme.css').read()
|
||||
cli=open('MainWindowPage-BjVtjJFx.css').read()
|
||||
def grab(s,pre):
|
||||
return {int(m.group(1)): m.group(2).strip() for m in re.finditer(r'--cds-'+pre+r'-(\d+):([^;}]+)', s)}
|
||||
for pre in ['gray','orange']:
|
||||
a,b=grab(ours,pre),grab(cli,pre)
|
||||
diff=[(k,a.get(k),b.get(k)) for k in sorted(set(a)|set(b)) if a.get(k)!=b.get(k)]
|
||||
print(pre, f'ours={len(a)} cli={len(b)} diff={len(diff)}')
|
||||
for k,x,y in diff: print(f' {pre}-{k}: ours={x} cli={y}')
|
||||
"
|
||||
```
|
||||
|
||||
### 5.1.2 品牌主色
|
||||
|
||||
| 令牌 | ours | client | 判定 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `--cds-clay` | `#d97757` | `#d97757` | ✅ |
|
||||
|
||||
### 5.1.3 骨架屏核心动效参数
|
||||
|
||||
| 参数 | ours(分支) | client | 判定 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `cds-skeleton-sweep` 关键帧 | `translate(-100%)` → `translate(100%)` | 同左 | ✅ |
|
||||
| sweep 时长 | `2s` | `2s` | ✅ |
|
||||
| sweep 缓动 | `linear` | `linear` | ✅ |
|
||||
| sweep 角度 | `105deg` | `105deg` | ✅ |
|
||||
| `cds-skeleton-breathe` 关键帧 | `opacity 1 → .6 → 1` | 同左 | ✅ |
|
||||
| breathe 时长 | `2s` | `2s` | ✅ |
|
||||
| breathe 缓动 | `ease-in-out` | `ease-in-out` | ✅ |
|
||||
| 级联延时基数 | `90ms` | `90ms` | ✅ |
|
||||
|
||||
**证据**(keyframes 原文逐字对比):
|
||||
|
||||
```css
|
||||
/* client 3.6556.2 */
|
||||
@keyframes cds-skeleton-sweep { 0%{transform:translate(-100%)}to{transform:translate(100%)} }
|
||||
@keyframes cds-skeleton-breathe{ 0%,to{opacity:1}50%{opacity:.6} }
|
||||
|
||||
/* ours (branch 9dc69bc) */
|
||||
@keyframes cds-skeleton-sweep { 0%{transform:translate(-100%)}100%{transform:translate(100%)} }
|
||||
@keyframes cds-skeleton-breathe{ 0%,100%{opacity:1}50%{opacity:.6} }
|
||||
```
|
||||
|
||||
> **这确认了 issue #25 的核心取值裁定:`2s / linear / 105deg` 是正确的地面真值。** 分支在这三点上与客户端完全一致,v2/storybook 的 `1.8s / cubic-bezier / 90deg` 是错的。
|
||||
|
||||
---
|
||||
|
||||
## 5.2 必须修正项
|
||||
|
||||
### 5.2.1 `--cds-clay-emphasized`(严重)
|
||||
|
||||
| 项 | 值 |
|
||||
| :--- | :--- |
|
||||
| ours | `var(--cds-clay-hover)` |
|
||||
| ours 实际解析(light) | `#cc5a36` |
|
||||
| ours 实际解析(dark) | `#e08264` |
|
||||
| **client** | **`#c6613f`**(2 处定义:`:root` 与 `.cds-root`,值相同;0 处暗色覆盖) |
|
||||
|
||||
**判定**:❌ **必须改为硬编码 `#c6613f`**,且**不要**在暗色块里覆盖。
|
||||
|
||||
**理由**:
|
||||
1. client 的 `--cds-clay-emphasized` 出现 2 次(`:root` 与 `.cds-root`),**两处值均为 `#c6613f`**,且**没有任何暗色重定义**
|
||||
2. 当前 ours 的做法(`var(--cds-clay-hover)`)会在暗色下变成 `#e08264`,与 client 的 `#c6613f` 相差很大
|
||||
3. `#c6613f` 与 `open-claude-ui` 自己的 `AGENTS.md` 里记录的 `--brand: #c6613f`(「Official desktop brand primary color」)一致
|
||||
|
||||
**修复代码**:
|
||||
|
||||
```css
|
||||
/* 浅色 */
|
||||
:root {
|
||||
--cds-clay-emphasized: #c6613f; /* 不派生自 --cds-clay-hover */
|
||||
}
|
||||
|
||||
/* 暗色:不要覆盖 */
|
||||
/* .dark { --cds-clay-emphasized: … } ← 删除这一行 */
|
||||
```
|
||||
|
||||
**验证命令**:
|
||||
|
||||
```bash
|
||||
grep -c "\--cds-clay-emphasized:" code-examples/cds-theme.css
|
||||
# 修复后应为 1(只在 :root 出现一次)
|
||||
grep -o "\--cds-clay-emphasized:[^;}]*" code-examples/cds-theme.css
|
||||
# 应为 --cds-clay-emphasized: #c6613f
|
||||
```
|
||||
|
||||
### 5.2.2 骨架屏暗色 token(严重)
|
||||
|
||||
| 令牌 | ours | client |
|
||||
| :--- | :--- | :--- |
|
||||
| `--cds-skeleton-base`(light) | `rgba(0, 0, 0, 0.05)` | `var(--cds-alpha-1)` = `hsl(from var(--cds-neutral-900) h s l / 5%)` |
|
||||
| `--cds-skeleton-base`(dark) | `rgba(255, 255, 255, 0.06)` | `hsl(from var(--cds-neutral-900) h s l / 7.5%)` |
|
||||
| `--cds-skeleton-sheen`(light) | `rgba(255, 255, 255, 0.25)` | 同 base(`hsl(… / 5%)`) |
|
||||
| `--cds-skeleton-sheen`(dark) | `rgba(255, 255, 255, 0.08)` | 同 base(`hsl(… / 7.5%)`) |
|
||||
|
||||
**两个实质差异**:
|
||||
|
||||
1. **方向相反**:ours 在暗色下用**白色半透明(提亮)**,client 用**中性黑半透明(加深)**
|
||||
2. **base 与 sheen 的关系**:ours 中 sheen 比 base 亮(0.25 vs 0.05 / 0.08 vs 0.06);client 中**两者完全相同**
|
||||
|
||||
**修复代码**:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--cds-skeleton-base: var(--cds-alpha-1);
|
||||
--cds-skeleton-sheen: var(--cds-alpha-1);
|
||||
}
|
||||
|
||||
/* 暗色(三种作用域,视实现复杂度可先只做 .dark) */
|
||||
.dark,
|
||||
[data-mode="dark"] {
|
||||
--cds-skeleton-base: hsl(from var(--cds-neutral-900) h s l / 7.5%);
|
||||
--cds-skeleton-sheen: hsl(from var(--cds-neutral-900) h s l / 7.5%);
|
||||
}
|
||||
```
|
||||
|
||||
> **前置依赖**:`--cds-alpha-1` 与 `--cds-neutral-900` 必须存在。ours 已有 `--cds-alpha-1`(见 5.1.1 的 gray 对齐),需确认 `--cds-neutral-900` 指向 `--cds-gray-900`。
|
||||
|
||||
### 5.2.3 暗色模式作用域
|
||||
|
||||
| 项 | ours | client |
|
||||
| :--- | :--- | :--- |
|
||||
| 选择器 | `.dark` | `[data-mode=dark]` + `[data-mode=system]` + `.cds-dark-scope[data-mode=dark]` |
|
||||
| 系统跟随 | ❌ 无 | ✅ `[data-mode=system]` + `prefers-color-scheme` |
|
||||
| 局部强制 | ❌ 无 | ✅ `.cds-dark-scope` |
|
||||
|
||||
**判定**:⚠️ **兼容性改动**。若要保持 `.dark` 的易用性,可**双轨支持**:
|
||||
|
||||
```css
|
||||
.dark,
|
||||
[data-mode="dark"],
|
||||
.cds-dark-scope[data-mode="dark"] {
|
||||
/* 暗色令牌 */
|
||||
}
|
||||
```
|
||||
|
||||
**不建议**直接改名,会破坏现有使用方。
|
||||
|
||||
### 5.2.4 RTL 动画方向
|
||||
|
||||
| 项 | ours | client |
|
||||
| :--- | :--- | :--- |
|
||||
| 渐变角度翻转 | ✅ 105° → 255° | ✅ |
|
||||
| **动画方向反转** | ❌ 缺失 | ✅ `animation-direction: reverse` |
|
||||
| RTL 判定 | `:dir(rtl)` / `[dir="rtl"]` | `:root[dir=rtl]` **且** `:lang(<19 个语言>)` |
|
||||
|
||||
**实测证据**(Chrome headless,用 ours 的 CSS):
|
||||
|
||||
```
|
||||
LTR: transform = matrix(1,0,0,1,-220,0)
|
||||
RTL: transform = matrix(1,0,0,1,-220,0) ← 完全相同
|
||||
```
|
||||
|
||||
**修复代码**(补 `animation-direction`):
|
||||
|
||||
```css
|
||||
[dir="rtl"] .cds-skeleton-sweep,
|
||||
[dir="rtl"] .cds-skeleton::after,
|
||||
:dir(rtl) .cds-skeleton-sweep,
|
||||
:dir(rtl) .cds-skeleton::after {
|
||||
background: linear-gradient(255deg, transparent 35%, var(--cds-skeleton-sheen) 50%, transparent 65%);
|
||||
animation-direction: reverse; /* ← 新增 */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5.3 缺失项
|
||||
|
||||
### 5.3.1 缺失的 44 个关键帧
|
||||
|
||||
```bash
|
||||
comm -13 <(grep -o "@keyframes [a-zA-Z0-9_-]*" code-examples/cds-theme.css | sed 's/@keyframes //' | sort -u) \
|
||||
<(grep -o "@keyframes [a-zA-Z0-9_-]*" MainWindowPage-BjVtjJFx.css | sed 's/@keyframes //' | sort -u)
|
||||
```
|
||||
|
||||
**完整 44 项**(字母序,`comm -13` 输出):
|
||||
|
||||
```
|
||||
1 cds-attachment-out
|
||||
2 cds-chip-in
|
||||
3 cds-drop-icon-rise
|
||||
4 cds-drop-ring-in
|
||||
5 cds-editor-comment-working
|
||||
6 cds-fade-in
|
||||
7 cds-flipbook
|
||||
8 cds-grid-rows-in
|
||||
9 cds-morph-in
|
||||
10 cds-page-tabs-arrive
|
||||
11 cds-page-title-leaves
|
||||
12 cds-pill-dip-down
|
||||
13 cds-pill-dip-up
|
||||
14 cds-pill-in
|
||||
15 cds-pill-out
|
||||
16 cds-pill-pop-down
|
||||
17 cds-pill-pop-up
|
||||
18 cds-pill-roll-in-down
|
||||
19 cds-pill-roll-in-up
|
||||
20 cds-pill-roll-out
|
||||
21 cds-placeholder-reveal
|
||||
22 cds-resolve-glyph
|
||||
23 cds-resolve-square
|
||||
24 cds-reveal-in
|
||||
25 cds-reveal-out
|
||||
26 cds-scroll-fade-strip-active
|
||||
27 cds-scroll-fade-strip-in
|
||||
28 cds-scroll-fade-strip-out
|
||||
29 cds-shimmer
|
||||
30 cds-shimmer-text-band
|
||||
31 cds-shimmer-text-band-rtl
|
||||
32 cds-shimmer-text-band-strong
|
||||
33 cds-shimmer-text-band-strong-rtl
|
||||
34 cds-shimmer-text-shine-rtl
|
||||
35 cds-shimmer-text-shine-strong-rtl
|
||||
36 cds-slide-in-end
|
||||
37 cds-slide-in-start
|
||||
38 cds-sticky-edge-active
|
||||
39 cds-sticky-edge-end
|
||||
40 cds-sticky-edge-start
|
||||
41 cds-working-dot-halo
|
||||
42 cds-working-dot-lands
|
||||
43 cds-working-dot-veil
|
||||
44 spin
|
||||
```
|
||||
|
||||
**按系列归并**:
|
||||
|
||||
| 系列 | 数量 | 成员 |
|
||||
| :--- | ---: | :--- |
|
||||
| `cds-pill-*` | 9 | dip-down/up, in, out, pop-down/up, roll-in-down/up, roll-out |
|
||||
| `cds-shimmer-*` | 7 | shimmer, text-band{,-rtl,-strong,-strong-rtl}, text-shine-rtl, text-shine-strong-rtl |
|
||||
| `cds-reveal/slide/fade` | 5 | reveal-in, reveal-out, slide-in-start, slide-in-end, fade-in |
|
||||
| `cds-sticky-edge-*` | 3 | active, end, start |
|
||||
| `cds-scroll-fade-strip-*` | 3 | active, in, out |
|
||||
| `cds-working-dot-*` | 3 | halo, lands, veil |
|
||||
| `cds-resolve-*` | 2 | glyph, square |
|
||||
| `cds-drop-*` | 2 | icon-rise, ring-in |
|
||||
| 其它 | 10 | attachment-out, chip-in, editor-comment-working, flipbook, grid-rows-in, morph-in, page-tabs-arrive, page-title-leaves, placeholder-reveal, spin |
|
||||
|
||||
> 注:`cds-shimmer-text-shine` 与 `cds-shimmer-text-shine-strong` 已在 ours 中,故不在缺失列表。
|
||||
|
||||
**优先级建议**(基于是否被 `data-cds` 组件消费):
|
||||
|
||||
| 优先级 | 关键帧 | 理由 |
|
||||
| :--- | :--- | :--- |
|
||||
| **P0** | `cds-placeholder-reveal` | 骨架屏 `data-hold=delayed` 必需 |
|
||||
| **P1** | `cds-pill-*`(9 个) | v2 文档已声称实现,且 pill 是高频交互 |
|
||||
| **P1** | `cds-shimmer*`(8 个) | 倒计时/生成中状态常用 |
|
||||
| **P2** | `cds-scroll-fade-strip-*`(3 个) | 滚动渐隐的 strip 变体 |
|
||||
| **P2** | `cds-sticky-edge-*`(3 个) | 吸边效果 |
|
||||
| **P3** | 其余 | 按需 |
|
||||
|
||||
### 5.3.2 缺失的 `data-cds` 命名空间
|
||||
|
||||
```bash
|
||||
ours=$(grep -rho 'data-cds="[A-Za-z-]*' code-examples/cds-theme.css docs/claude-components-storybook.html | sed 's/data-cds="//' | sort -u)
|
||||
cli=$(grep -o "\[data-cds=[A-Za-z-]*\]" MainWindowPage-BjVtjJFx.css | sed 's/\[data-cds=//;s/\]//' | sort -u)
|
||||
comm -12 <(echo "$ours") <(echo "$cli") | wc -l # 交集 18
|
||||
comm -13 <(echo "$ours") <(echo "$cli") | wc -l # 仅客户端 27
|
||||
comm -23 <(echo "$ours") <(echo "$cli") | wc -l # 仅我方 4
|
||||
```
|
||||
|
||||
| 集合 | 数量 |
|
||||
| :--- | ---: |
|
||||
| ours | 22 |
|
||||
| client | 45 |
|
||||
| **交集** | **18** |
|
||||
| **仅客户端有(缺失)** | **27** |
|
||||
| **仅我方有(客户端无)** | **4** |
|
||||
|
||||
**交集(18 项,已对齐)**:
|
||||
|
||||
```
|
||||
Accordion Aside AskUserQuestion AssistantMessage Banner Card
|
||||
ChartContainer ChatComposerQueuedMessages CommentCard DataTable
|
||||
FileDiff Panel PointChartContainer PreviewCard Skeleton
|
||||
Surface Table UserMessage
|
||||
```
|
||||
|
||||
**缺失的 27 项**:
|
||||
|
||||
```
|
||||
1 AccordionHeader
|
||||
2 Button
|
||||
3 ButtonGroup
|
||||
4 CardLink
|
||||
5 ChatComposerChin
|
||||
6 ChatComposerDock
|
||||
7 ChatComposerEditor
|
||||
8 ChatComposerNotices
|
||||
9 DiffIndicator
|
||||
10 Heatmap
|
||||
11 Icon
|
||||
12 MessageActions
|
||||
13 MessageAttachments
|
||||
14 MessageAttachmentsFile
|
||||
15 MessageAttachmentsImage
|
||||
16 MessageAttachmentsQuote
|
||||
17 MessageAttachmentsVideo
|
||||
18 ModelSelector
|
||||
19 ModelSelectorEffort
|
||||
20 Page
|
||||
21 Prose
|
||||
22 Pulse
|
||||
23 SkeletonGroup
|
||||
24 SplitDropdownButton
|
||||
25 Toolbar
|
||||
26 TurnStatus
|
||||
27 TurnStatusStep
|
||||
```
|
||||
|
||||
**我方独有的 4 项**(3.6556.2 主样式表中不存在):
|
||||
|
||||
```
|
||||
CodeBlock
|
||||
ConfirmationDialog
|
||||
ContextMenu
|
||||
Toast
|
||||
```
|
||||
|
||||
这 4 个可能是 v1/v2 时代逆向得到的命名,或 `open-claude-ui` 自行命名的组件。本文**不判定其正确性**,标注为待核实。
|
||||
|
||||
**优先级**:
|
||||
|
||||
| 优先级 | 项 | 理由 |
|
||||
| :--- | :--- | :--- |
|
||||
| **P0** | `SkeletonGroup` | 骨架屏双层结构必需 |
|
||||
| **P1** | `Toolbar`、`Prose`、`Page` | 基础容器 |
|
||||
| **P1** | `MessageAttachments` 系列(5 个) | 附件卡片体系 |
|
||||
| **P2** | `ChatComposer*`(4 个) | 输入框子部件 |
|
||||
| **P2** | `ModelSelector`、`ModelSelectorEffort` | 模型选择器 |
|
||||
| **P3** | 其余 | 按需 |
|
||||
|
||||
### 5.3.3 缺失的 7 个 `@property`
|
||||
|
||||
```
|
||||
--cds-badge-x-wght
|
||||
--cds-message-actions-opacity
|
||||
--cds-outset-x
|
||||
--cds-outset-y
|
||||
--cds-sticky-edge-active
|
||||
--cds-sticky-edge-end
|
||||
--cds-sticky-edge-start
|
||||
```
|
||||
|
||||
**影响**:这些属性不可被 CSS 动画插值。若缺失,相关动效会退化为离散跳变。
|
||||
|
||||
---
|
||||
|
||||
## 5.4 Issue #25 分支的取值裁定
|
||||
|
||||
`issue #25` 提出的四个问题,本文逐一裁定。**证据全部来自 3.6556.2 字节。**
|
||||
|
||||
### 问题 1:扫光的地面真值是 `2s / linear / 105°` 还是 `1.8s / bezier / 90°`?
|
||||
|
||||
**裁定:`2s / linear / 105°` 正确。**
|
||||
|
||||
**证据**:
|
||||
|
||||
```css
|
||||
/* 3.6556.2 原文 */
|
||||
.cds-root .cds-skel-sweep-band {
|
||||
background: linear-gradient(105deg, transparent 35%, var(--cds-skeleton-sheen), transparent 65%);
|
||||
animation: cds-skeleton-sweep 2s linear var(--cds-skel-reveal-end, 0s) infinite backwards;
|
||||
position: absolute;
|
||||
inset: 0
|
||||
}
|
||||
|
||||
@keyframes cds-skeleton-sweep { 0%{transform:translate(-100%)}to{transform:translate(100%)} }
|
||||
```
|
||||
|
||||
三个参数(`2s`、`linear`、`105deg`)逐字命中。分支的修正**方向正确**。
|
||||
|
||||
### 问题 2:Wave/Lockstep 是否应进入 token 层?
|
||||
|
||||
**裁定:应进入,但分支的实现方式需要重写。**
|
||||
|
||||
**证据与理由**:
|
||||
|
||||
| 项 | client | 分支 | 裁定 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 存在性 | ✅ CSS 中有完整规则 | ✅ 有 | 该实现 |
|
||||
| wave 的动画 | `cds-skeleton-breathe` | `cds-skeleton-sweep` | ❌ **分支用错动画** |
|
||||
| lockstep 挂载 | `SkeletonGroup` 容器 | 每个 `Skeleton` 块 | ⚠️ 效果近似但语义不同 |
|
||||
| 级联延时 | `reveal-end + i×90ms` | `i×90ms` | ⚠️ 缺 `reveal-end` 项 |
|
||||
| `data-hold` 三态 | ✅ | ❌ 无 | ❌ **缺失关键机制** |
|
||||
| 动画收尾 | `.01ms` + `display:none` | ❌ 无 | ❌ **动画永不停止** |
|
||||
|
||||
### 问题 3:`cds-pure.css` 的 header 承诺了 skeleton sweep 却不含实现?
|
||||
|
||||
**裁定:这个问题在 3.6556.2 的语境下不成立,但仍应修复。**
|
||||
|
||||
**证据**:
|
||||
|
||||
```bash
|
||||
# cds-pure.css 当前只有 16 行,实质是 @import
|
||||
wc -l code-examples/cds-pure.css # → 16
|
||||
cat code-examples/cds-pure.css # → @import "./cds-theme.css";
|
||||
```
|
||||
|
||||
`@import` 已经间接包含了 `cds-theme.css` 的全部规则。但 **header 注释里列举的「3. Pure-CSS animation utilities」与实际内容不符**——文件本身零规则。
|
||||
|
||||
**建议**:修正 header 注释,或把 skeleton 规则物理移入 `cds-pure.css`。
|
||||
|
||||
### 问题 4:CI 门禁看不到 docs/ 和孤儿关键帧?
|
||||
|
||||
**裁定:门禁已扩展,但存在漏报。**
|
||||
|
||||
**证据**(本文实测):
|
||||
|
||||
门禁在判断「关键帧是否被消费」时**未剥离注释**,因此:
|
||||
|
||||
```css
|
||||
@keyframes cds-orphan { 0%{opacity:0} 100%{opacity:1} }
|
||||
/* TODO: cds-orphan 待接入 */
|
||||
```
|
||||
|
||||
上述代码会**通过门禁**(注释里的 `cds-orphan` 被当作消费)。本文用最小复现验证:
|
||||
|
||||
```bash
|
||||
$ node scripts/check-tokens.mjs
|
||||
✓ 全部 @keyframes cds-* 均有明确消费类 ← 漏报,EXIT=0
|
||||
```
|
||||
|
||||
**修复建议**:判断前先 `css.replace(/\/\*[\s\S]*?\*\//g, '')`。
|
||||
|
||||
---
|
||||
|
||||
## 5.5 修复优先级总表
|
||||
|
||||
按「影响范围 × 修复成本」排序:
|
||||
|
||||
| 优先级 | 项 | 影响 | 改动量 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **P0** | `.cds-skeleton-sweep` 独立类导致元素移出视口 | 示例完全不可见 | 改 1 行选择器 |
|
||||
| **P0** | `--cds-clay-emphasized` 值错误 | 品牌色偏差 | 改 1 个值 |
|
||||
| **P0** | 骨架屏暗色 base/sheen 方向相反 | 暗色下视觉相反 | 改 4 个值 |
|
||||
| **P1** | RTL 缺 `animation-direction` | RTL 扫光方向错 | 加 1 条声明 |
|
||||
| **P1** | 骨架屏 `data-hold` 机制缺失 | 动画永不停止 | 新增 ~20 行 |
|
||||
| **P1** | wave 用错动画(sweep vs breathe) | 视觉错 | 改 1 行 |
|
||||
| **P2** | 缺 `cds-placeholder-reveal` | `data-hold=delayed` 不可用 | 新增 1 个 keyframes |
|
||||
| **P2** | 缺 9 个 `cds-pill-*` | pill 交互无 CSS 实现 | 新增 9 个 keyframes |
|
||||
| **P2** | `.cds-skeleton` 的 `border-radius` 覆盖 Tailwind | 头像等圆角失效 | 用 `:where()` 降权 |
|
||||
| **P3** | 缺 44 个 keyframes / 27 个 data-cds / 7 个 @property | 功能不全 | 按需 |
|
||||
| **P3** | CI 门禁未剥离注释 | 潜在漏报 | 加 1 行 |
|
||||
|
||||
---
|
||||
|
||||
## 本章证据索引
|
||||
|
||||
| 断言 | 命令 |
|
||||
| :--- | :--- |
|
||||
| gray 35 阶 diff=1 | python 逐阶对比 |
|
||||
| orange 35 阶 diff=0 | 同上 |
|
||||
| sweep 三参数一致 | keyframes + 规则原文逐字对比 |
|
||||
| clay-emphasized client 2 处同值 / 0 暗色覆盖 | `grep -c` + 偏移定位 |
|
||||
| 骨架屏暗色方向相反 | `hsl(from …neutral-900…)` vs `rgba(255,255,255,…)` |
|
||||
| RTL transform 相同 | Chrome headless 计算样式 |
|
||||
| 缺失 44/22/7 | `comm -13` 三个集合 |
|
||||
| 门禁漏报 | 最小复现 CSS + 运行 `check-tokens.mjs` |
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,298 @@
|
||||
---
|
||||
title: "第七章:总结与后续工作"
|
||||
category: reverse-engineering
|
||||
domain: architecture/desktop_application
|
||||
version: "3.0 (3.6556.2)"
|
||||
status: stable
|
||||
date: 2026-10-26
|
||||
author: "REA 逆向工程实验室"
|
||||
evidence_root: "b2dfbbb40325d978dcb7561ad12a6f9bc199f8f46b7d3d2899af74ce8965d68a"
|
||||
---
|
||||
|
||||
# 第七章:总结与后续工作
|
||||
|
||||
## 7.1 本次取证的确定性边界
|
||||
|
||||
### 7.1.1 确定性等级划分
|
||||
|
||||
本文所有结论按证据强度分为三级:
|
||||
|
||||
| 等级 | 定义 | 本文中的例子 |
|
||||
| :--- | :--- | :--- |
|
||||
| **A 级:字节确证** | 直接从 ASAR 内文件读出,可逐字复现 | 55 个 keyframes、45 个 data-cds、24 个 @property、`clay-emphasized = #c6613f`、6 个 `ipcMain.on` 通道名、12 个 `exposeInMainWorld` 字面量 |
|
||||
| **B 级:结构性推断** | 由多条 A 级证据交叉推出,逻辑闭环 | 「主进程无业务 IPC handler」「preload 路径静态不可解」「wave 用 breathe 不用 sweep」 |
|
||||
| **C 级:未知** | 已明示无法确定,不填 | 「7 个 renderer 入口中 2 个如何打开」「`--cds-skel-elapsed` 运行时由谁写入」「44 个缺失 keyframes 是否在其它 CSS 文件」 |
|
||||
|
||||
### 7.1.2 本文未使用的推断方式
|
||||
|
||||
为避免虚构,本文**刻意回避**了以下三种常见但不可靠的做法:
|
||||
|
||||
1. **不基于 naming 推断行为**:例如 `cds-pill-roll-out` 的命名暗示「滚出」,但本文只记录关键帧数值,不描述其触发条件与视觉轨迹
|
||||
2. **不基于 v2 文档补全 v3**:v2 的基线已失效(见 [第一章](01-综述与版本边界.md)),其结论不能直接继承
|
||||
3. **不基于「合理推测」填零值**:例如 `utility_processes = 1` 与本文实测的「0 个 fork 调用点」冲突时,本文标注为 `unknown` 而非二选一
|
||||
|
||||
---
|
||||
|
||||
## 7.2 已完成的结论清单
|
||||
|
||||
### 7.2.1 版本与取证基础
|
||||
|
||||
| 结论 | 等级 | 位置 |
|
||||
| :--- | :--- | :--- |
|
||||
| Claude Desktop 版本为 `3.6556.2` | A | [1.1.1](01-综述与版本边界.md) |
|
||||
| ASAR 根摘要 `b2dfbbb4…d68a` | A | [1.1.2](01-综述与版本边界.md) |
|
||||
| v2 引用的 `MainWindowPage-BD9CWQ83.css` 与 `index.chunk-BWk6pVUt.js` 已不存在 | A | [README](README.md) |
|
||||
| 532 个文件语义分析被截断 | A | [1.2](01-综述与版本边界.md) |
|
||||
|
||||
### 7.2.2 Electron 架构
|
||||
|
||||
| 结论 | 等级 | 位置 |
|
||||
| :--- | :--- | :--- |
|
||||
| 主进程 0 个 `ipcMain.handle`,仅 6 个 `ipcMain.on` | A | [2.1.1](02-Electron架构取证.md) |
|
||||
| 这 6 个通道属于 Sentry SDK(`start`/`scope`/`envelope`/`structured-log`/`metric`/`status`) | B | [2.1.1](02-Electron架构取证.md) |
|
||||
| 主进程 0 个 `new BrowserWindow` | A | [2.1.2](02-Electron架构取证.md) |
|
||||
| `utilityProcess` 3 处均为 Proxy 包装,无 fork 调用点 | B | [2.1.3](02-Electron架构取证.md) |
|
||||
| preload 入口 0 个,路径由 `registerPreloadScript` 运行时拼接 | B | [2.1.4](02-Electron架构取证.md) |
|
||||
| 12 个字面量 `exposeInMainWorld` API 名 | A | [2.2.2](02-Electron架构取证.md) |
|
||||
| 7 个 renderer HTML 入口 | A | [2.3](02-Electron架构取证.md) |
|
||||
| 8 处 ASAR 完整性冲突 | A | [2.4](02-Electron架构取证.md) |
|
||||
|
||||
### 7.2.3 设计系统底座
|
||||
|
||||
| 结论 | 等级 | 位置 |
|
||||
| :--- | :--- | :--- |
|
||||
| 55 个 `@keyframes` | A | [4.1](04-系统底座全景解密.md) |
|
||||
| 45 个 `data-cds` 命名空间 | A | [4.2](04-系统底座全景解密.md) |
|
||||
| 24 个 `@property` | A | [4.5](04-系统底座全景解密.md) |
|
||||
| 35 阶 gray / 35 阶 orange 全表 | A | [4.3](04-系统底座全景解密.md) |
|
||||
| `--cds-clay-emphasized: #c6613f`(2 处同值,0 处暗色覆盖) | A | [4.3.3](04-系统底座全景解密.md) |
|
||||
| 骨架屏暗色 base/sheen 均为 `hsl(from neutral-900 / 7.5%)` | A | [4.4.4](04-系统底座全景解密.md) |
|
||||
| `data-mode` 三态(dark 86 / system 43 / light 29 处) | A | [4.4](04-系统底座全景解密.md) |
|
||||
| 5 阶时间 token(60ms / 120ms / 200ms / 300ms / 450ms) | A | [4.6.1](04-系统底座全景解密.md) |
|
||||
| 4 个 ease 曲线值(out×2 + overshoot + snap) | A | [4.6.2](04-系统底座全景解密.md) |
|
||||
|
||||
### 7.2.4 骨架屏机制(issue #25 的核心)
|
||||
|
||||
| 结论 | 等级 | 位置 |
|
||||
| :--- | :--- | :--- |
|
||||
| 扫光参数 `2s / linear / 105deg` 成立 | A | [5.1.3](05-open-claude-ui差异审计.md) |
|
||||
| 双层结构 `SkeletonGroup` + `Skeleton` | A | [3.1.1](03-架构重构与对比分析.md) |
|
||||
| 扫光带是 `.cds-skel-sweep-band` 真实元素,非 `::after` | A | [3.1.1](03-架构重构与对比分析.md) |
|
||||
| `data-hold` 三态机制 | A | [3.2](03-架构重构与对比分析.md) |
|
||||
| wave 用 `cds-skeleton-breathe`,不是 sweep | A | [3.3.1](03-架构重构与对比分析.md) |
|
||||
| RTL 需 `animation-direction: reverse` | A | [3.4](03-架构重构与对比分析.md) |
|
||||
| 减少动效双轨(`@media` + `[data-reduce-motion]`) | A | [3.5](03-架构重构与对比分析.md) |
|
||||
| 揭示完成后动画压至 `.01ms` 并 `display:none` 扫光带 | A | [3.2.4](03-架构重构与对比分析.md) |
|
||||
|
||||
### 7.2.5 open-claude-ui 差异
|
||||
|
||||
| 结论 | 等级 | 位置 |
|
||||
| :--- | :--- | :--- |
|
||||
| gray 35 阶 diff=1(`#111111` vs `#111`,值等价) | A | [5.1.1](05-open-claude-ui差异审计.md) |
|
||||
| orange 35 阶 diff=0 | A | [5.1.1](05-open-claude-ui差异审计.md) |
|
||||
| keyframes:交集 11 / 缺失 44 / 独有 21 | A | [4.1.2](04-系统底座全景解密.md) |
|
||||
| data-cds:交集 18 / 缺失 27 / 独有 4 | A | [5.3.2](05-open-claude-ui差异审计.md) |
|
||||
| @property:交集 17 / 缺失 7 / 独有 2 | A | [5.3.3](05-open-claude-ui差异审计.md) |
|
||||
| CI 门禁未剥离注释,存在漏报 | A | [5.4](05-open-claude-ui差异审计.md) |
|
||||
| ASAR 顶层含 compile-cache / bundled-skills / github-mcp / office365-mcp | A | [6.10.1](06-全量CSS扫描与官方规范比对.md) |
|
||||
| 内置 6 个 Agent Skills(含 frontend-design) | A | [6.10.3](06-全量CSS扫描与官方规范比对.md) |
|
||||
| 官方 skill 原文确认 `#D97757` 为 "Anthropic's own Claude-interaction accent" | A | [6.10.4](06-全量CSS扫描与官方规范比对.md) |
|
||||
| **基线增量**:客户端已跳版至 `4.29.0`;CSS 结构清单逐项复验一致(2 处实质更新:`Heatmap`→`CellChartContainer` 更名、失焦去色覆盖扩展),我们的修复清单逐项成立无需改动 | A | [6.11](06-全量CSS扫描与官方规范比对.md) |
|
||||
| **基线增量**:`4.4212.0` 复核——token / data-cds / @property / JS 拓扑**零变化**,仅新增 `cds-progress-sweep`(进度扫光,已移植)与字体 role 链(已补 token) | A | [6.12](06-全量CSS扫描与官方规范比对.md) |
|
||||
| 4.29.0 新增 2 个桥接 API(`desktopNoFlushBytecode`、`desktopHomePath`),桥接总数 12→14 | A | [6.11.3](06-全量CSS扫描与官方规范比对.md) |
|
||||
| Cowork 体系内置 Linux micro-VM(smol-bin 24MB) | A | [6.10.5](06-全量CSS扫描与官方规范比对.md) |
|
||||
| 内置 4 个 MCP server(github / office365 / computer-use / chrome) | A | [6.10.6](06-全量CSS扫描与官方规范比对.md) |
|
||||
| `@starting-style` + `view-transition-name: match-element` 用于附件重排 | A | [6.10.8](06-全量CSS扫描与官方规范比对.md) |
|
||||
|
||||
---
|
||||
|
||||
## 7.3 明确的未知项
|
||||
|
||||
以下各项本文**无法确定**,列出原因与可用的进一步手段。
|
||||
|
||||
### 7.3.1 已全部解决
|
||||
|
||||
原列的两项「需运行时观测」已通过 Claude 自带的 **Enable Main Process Debugger** 取得运行时事实(见 [6.7](06-全量CSS扫描与官方规范比对.md))。
|
||||
|
||||
### 7.3.1.1 已在第六章解决
|
||||
|
||||
以下项原本列在未知清单中,第六章完成后已给出确定答案:
|
||||
|
||||
| 原未知项 | 结论 | 证据位置 |
|
||||
| :--- | :--- | :--- |
|
||||
| 44 个缺失 keyframes 是否在其它 CSS 文件 | **不在**。全部 44 项仅存在于主样式表;其余 16 个文件只多出 4 个非 `cds-*` 关键帧 | [6.2](06-全量CSS扫描与官方规范比对.md) |
|
||||
| `--cds-*` 与官方 token 的关系 | `--cds-X` ≡ 官方 `--X`,131 个同名 | [6.5](06-全量CSS扫描与官方规范比对.md) |
|
||||
| `--cds-radius-composer` 官方值 | **14px**(`calc(.875rem * scale)`),我们的 20px 是 v1 遗留;切换条件为 `[data-density=comfortable]` | [6.6.2](06-全量CSS扫描与官方规范比对.md) |
|
||||
| `--cds-h-control-nested` 官方值 | **22px**(`calc(1.375rem * scale)`),v2 文档正确,我们错 | [6.8.1](06-全量CSS扫描与官方规范比对.md) |
|
||||
| `--cds-gap-xl` 官方值 | **40px**(`calc(2.5rem * scale)`) | [6.8.2](06-全量CSS扫描与官方规范比对.md) |
|
||||
| `--cds-skel-elapsed` / `skel-settled` 由谁写入 | **没有代码写入**。551 个 JS 文件中 `SkeletonGroup` 零出现,恒为 `0s`;`reveal-end` 恒为 `0.65s` | [6.6](06-全量CSS扫描与官方规范比对.md) |
|
||||
| 「7 停非线性 mask」是否存在 | **不存在**。全文 6 停以上 gradient 为 0 | [6.6.3](06-全量CSS扫描与官方规范比对.md) |
|
||||
| `--cds-rem-scale` 的实际取值 | **从未被赋值**,306 处引用全部走 fallback `1` | [6.6.1](06-全量CSS扫描与官方规范比对.md) |
|
||||
| `registerPreloadScript` 的 `filePath` 实际值 | **`.vite/build/mainWindow.js`**(主窗口)与 **`.vite/build/mainView.js`**(about:blank)。preload 就是窗口脚本本身 | [6.7.2](06-全量CSS扫描与官方规范比对.md) |
|
||||
| `utilityProcess.fork` 的实际调用点 | 运行时钩子零命中。实测 3 个 Utility 全为 Chromium 内置(Network / Audio / VideoCapture),**应用层未 fork 任何 utility process** | [6.7.4](06-全量CSS扫描与官方规范比对.md) |
|
||||
| preload 注册的实际 API | session 级 `registerPreloadScript`,但 `getPreloadScripts()` 返回 `[]`;实际走 `webPreferences.preload` | [6.7.3](06-全量CSS扫描与官方规范比对.md) |
|
||||
|
||||
### 7.3.2 需要历史版本
|
||||
|
||||
| 未知项 | 原因 |
|
||||
| :--- | :--- |
|
||||
| v2.37337.1 的 CSS 实际内容 | 本机无该版本备份,无法比对「#cc5a36 从何而来」 |
|
||||
| v2 的 40 个 `data-cds` 具体名单 | v2 文档只写总数,04 章是占位符 `[40 大标准化组件清单]` |
|
||||
| 「7 停非线性 mask」是否真实存在过 | 3.6556.2 主表中未找到;无法确认是重构还是 v2 文档的推断 |
|
||||
|
||||
### 7.3.2.1 第六章 6.10 新增的待验证项
|
||||
|
||||
| 未知项 | 原因 | 可行手段 |
|
||||
| :--- | :--- | :--- |
|
||||
| Cowork surface 的 UI 载体是本地 CSS 还是 claude.ai Web | 45 个 `data-cds` 中无 Cowork 专属组件 | 运行时打开 Cowork 界面后用 CDP 检查 DOM |
|
||||
| `heavy-work-worker` / `pty-host` / `file-index-worker` 是 `worker_threads` 还是 `utilityProcess` | 本会话未触发 | 运行时触发后看进程树 |
|
||||
| `clawdQuestEngine`(游戏化)的数据来源与 UI 触发点 | 仅静态扫描 | 同上 |
|
||||
| `smol-bin` VM 的最小规格(内存/CPU 配置) | 代码中有 `vmCpuCount`/`memory GB` 逻辑但未运行 | Cowork 启动 VM 时观测 |
|
||||
|
||||
### 7.3.3 需要组件源码
|
||||
|
||||
| 未知项 | 原因 |
|
||||
| :--- | :--- |
|
||||
| `open-claude-ui` 独有的 21 个 keyframes 来源 | 需确认是 v1/v2 逆向残留,还是自行命名 |
|
||||
| `open-claude-ui` 独有的 4 个 data-cds(`CodeBlock`/`ConfirmationDialog`/`ContextMenu`/`Toast`) | 同上 |
|
||||
|
||||
---
|
||||
|
||||
## 7.4 建议的后续工作
|
||||
|
||||
### 7.4.1 对 open-claude-ui 仓库
|
||||
|
||||
**第一优先(P0,4 项)**
|
||||
|
||||
1. 修复 `.cds-skeleton-sweep` 独立类把元素移出视口
|
||||
2. 修正 `--cds-clay-emphasized` 为 `#c6613f`
|
||||
3. 修正骨架屏暗色 base/sheen 为 `hsl(from var(--cds-neutral-900) h s l / 7.5%)`
|
||||
4. 修正 CI 门禁在判断消费前剥离注释
|
||||
|
||||
**第二优先(P1,3 项)**
|
||||
|
||||
5. 补 RTL 的 `animation-direction: reverse`
|
||||
6. 实现 `data-hold` 三态揭示时序
|
||||
7. 修正 wave 模式用 `cds-skeleton-breathe` 而非 sweep
|
||||
|
||||
**第三优先(P2,3 项)**
|
||||
|
||||
8. 补 `cds-placeholder-reveal` 关键帧
|
||||
9. 补 9 个 `cds-pill-*` 关键帧
|
||||
10. 用 `:where()` 降低 `.cds-skeleton` 的 `border-radius` 特异性
|
||||
|
||||
详见 [5.5 修复优先级总表](05-open-claude-ui差异审计.md)。
|
||||
|
||||
### 7.4.2 文档层面
|
||||
|
||||
**v2 文档处理**
|
||||
|
||||
建议在 `docs/v2/` 各文件的 frontmatter 增加:
|
||||
|
||||
```yaml
|
||||
status: historical
|
||||
superseded_by: "docs/v4.29.0-4.4212.0/"
|
||||
verified_against: "2026-10-15"
|
||||
```
|
||||
|
||||
**不要**在 v2 里补充 3.6556.2 的数字——基线已不同,混写会产生新的错误。
|
||||
|
||||
**v1 文档**
|
||||
|
||||
`docs/v1/` 同样引用了 `2.37337.1`(共 16 个文件 36 处),需做同样处理。
|
||||
|
||||
### 7.4.3 对 REA 工具
|
||||
|
||||
本次取证暴露 REA 6.8.0 的三个问题:
|
||||
|
||||
**问题 1:`--source-path` 在 inventory 部分失效时不可用**
|
||||
|
||||
```bash
|
||||
rea analyze-javascript-application app.asar --source-path ".vite/build/index.pre.js"
|
||||
→ invalid_value: "Select an exact relevant file path from the artifact inventory"
|
||||
```
|
||||
|
||||
但从 REA 自己的 `asar-entry` 节点取出的路径就是 `.vite/build/index.pre.js`。
|
||||
|
||||
**原因**(源码 `src/artifacts/javascript/JavaScriptArtifactFiles.ts:104`):
|
||||
|
||||
```ts
|
||||
const issues = (sourcePaths ?? []).flatMap((path, index) =>
|
||||
expected.has(path) || inventory.unavailableFiles.has(path) ? [] : [...]
|
||||
);
|
||||
```
|
||||
|
||||
`expected` 来自**本次运行的 inventory**。当 inventory 因预算截断而不完整时,选中的路径恰好落在失效部分,导致定向补全这条恢复路径被完全切断。
|
||||
|
||||
**问题 2:`utility_processes` 与 `resolved_utility_entrypoints` 语义冲突**
|
||||
|
||||
```
|
||||
summary.utility_processes = 1
|
||||
summary.resolved_utility_entrypoints = 0
|
||||
```
|
||||
|
||||
实测代码中 `utilityProcess` 仅 3 处,全部是对 `fork` 的 Proxy 包装,无实际调用点。两个字段并列时读者会误以为「有 1 个 utility process 但入口未知」,而实际可能是「Proxy 包装被计数了」。
|
||||
|
||||
**问题 3:`context_bridge_apis` 的口径不明确**
|
||||
|
||||
同一份取证中:
|
||||
|
||||
| 位置 | 数值 |
|
||||
| :--- | ---: |
|
||||
| `summary.context_bridge_apis` | 37 |
|
||||
| graph `context-bridge-api` 节点数 | 20 |
|
||||
| graph 唯一 label 数 | 7 |
|
||||
| 本文字节扫描的 `exposeInMainWorld` 字面量 | 12 |
|
||||
|
||||
四个数字都对,但口径不同且未在文档中说明。
|
||||
|
||||
### 7.4.4 下一步技术验证
|
||||
|
||||
若要继续深入,建议按以下顺序:
|
||||
|
||||
| 步骤 | 目标 | 手段 |
|
||||
| :--- | :--- | :--- |
|
||||
| 1 | 确认 44 个缺失 keyframes 是否在其它 CSS | 扫全部 17 个 CSS 文件 |
|
||||
| 2 | 确认 `data-mode` 的完整覆盖范围 | 扫 4 个 `window-*.css` |
|
||||
| 3 | 确认运行时注入的 `--cds-skel-*` | Electron + CDP |
|
||||
| 4 | 确认 preload 实际路径 | Electron 主进程 --inspect |
|
||||
| 5 | 重跑被截断的 532 个文件 | 待 REA 修复 `--source-path`,或分批分析 |
|
||||
|
||||
---
|
||||
|
||||
## 7.5 本文的可复现性声明
|
||||
|
||||
本文所有 A 级结论都可通过以下三条命令的任意组合复现:
|
||||
|
||||
```bash
|
||||
# 1. 确认版本与摘要
|
||||
shasum -a 256 "/Applications/Claude.app/Contents/Resources/app.asar"
|
||||
|
||||
# 2. 抽取主样式表(ASAR header: uint32LE@12 = JSON 长度,JSON 从偏移 16 开始)
|
||||
node -e '
|
||||
const fs=require("fs");
|
||||
const p="/Applications/Claude.app/Contents/Resources/app.asar";
|
||||
const fd=fs.openSync(p,"r");
|
||||
const h=Buffer.alloc(16); fs.readSync(fd,h,0,16,0);
|
||||
const hs=h.readUInt32LE(12);
|
||||
const jb=Buffer.alloc(hs); fs.readSync(fd,jb,0,hs,16);
|
||||
const J=JSON.parse(jb.toString("utf8"));
|
||||
const n=J.files[".vite"].files.renderer.files.main_window.files.assets.files["MainWindowPage-BjVtjJFx.css"];
|
||||
const b=Buffer.alloc(n.size); fs.readSync(fd,b,0,n.size,16+hs+Number(n.offset));
|
||||
fs.writeFileSync("/tmp/MainWindowPage.css", b);
|
||||
'
|
||||
|
||||
# 3. 验证任意一条清单
|
||||
grep -o "@keyframes [a-zA-Z0-9_-]*" /tmp/MainWindowPage.css | sort -u | wc -l # 55
|
||||
grep -o "\[data-cds=[A-Za-z-]*\]" /tmp/MainWindowPage.css | sort -u | wc -l # 45
|
||||
grep -o "@property --cds-[a-zA-Z0-9_-]*" /tmp/MainWindowPage.css | sort -u | wc -l # 24
|
||||
grep -c "\--cds-clay-emphasized:" /tmp/MainWindowPage.css # 1
|
||||
```
|
||||
|
||||
**若 Claude Desktop 升级**,`MainWindowPage-*.css` 的文件名 hash 会变化,上述路径需按 ASAR 目录树重新定位。ASAR 根摘要也会变化,本文所有结论需重新验证。
|
||||
|
||||
---
|
||||
|
||||
## 本章证据索引
|
||||
|
||||
本文为总结章,不引入新事实。所有断言均指向前述章节,前述章节各自带有代码证据。
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,159 @@
|
||||
# Claude Desktop 4.4212.0 分析报告 · CDS 基线(3.6556.2 取证 + 4.29.0 / 4.4212.0 增量复核)
|
||||
|
||||
> **目标应用载体**:Anthropic 官方生产包 macOS `/Applications/Claude.app`
|
||||
> **主基线**:`@ant/desktop 3.6556.2`(ASAR 摘要 `b2dfbbb40325d978dcb7561ad12a6f9bc199f8f46b7d3d2897af74ce8965d68a`,主样式表 `MainWindowPage-BjVtjJFx.css`)
|
||||
> **增量复核**:`4.29.0`(ASAR `fea39d03…`)→ **`4.4212.0`**(ASAR `ce90267c…`,主样式表 `MainWindowPage-BoMeyaMm.css`,239,201 bytes)——见 [§6.11](06-全量CSS扫描与官方规范比对.md) / [§6.12](06-全量CSS扫描与官方规范比对.md)
|
||||
> **逆向工具栈**:**REA (Reverse Engineer Anything) v6.8.0** + 应用内 **Main Process Debugger** 运行时实测
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 本版最重要的结论
|
||||
|
||||
**第二版(v2.37337.1)文档的全部基线已失效,不是补充数字的问题。**
|
||||
|
||||
| v2 文档的基线 | v3 实测 | 证据 |
|
||||
| :--- | :--- | :--- |
|
||||
| `@ant/desktop v2.37337.1` | **`@ant/desktop v3.6556.2`** | ASAR 内 `package.json`:`name=@ant/desktop version=3.6556.2 productName=Claude` |
|
||||
| `MainWindowPage-BD9CWQ83.css` | **文件不存在** | ASAR 内 17 个 CSS 文件中无此名;当前主表为 `MainWindowPage-BjVtjJFx.css` |
|
||||
| `index.chunk-BWk6pVUt.js` | **文件不存在** | `index.chunk-2MD7iXhg.js` 等其他 chunk 存在,此名称不在 629 个 ASAR 条目中 |
|
||||
| 「40 大 `data-cds` 命名空间」 | **45 个** | `grep -o "\[data-cds=[A-Za-z-]*\]"` 去重计数 = 45 |
|
||||
| 「47 项 `@keyframes cds-*`」 | **55 个**(含 `spin`) | `grep -o "@keyframes [a-zA-Z0-9_-]*" \| sort -u \| wc -l` = 55 |
|
||||
|
||||
**因此:**
|
||||
- v2 文档已被标记为 `status: historical`,仅作历史快照保留。
|
||||
- 本目录(`docs/v4.29.0-4.4212.0/`)是当前唯一有效的取证基线。
|
||||
- `code-examples/` 中基于 v2 的令牌值需要按本文修正(详见 [第四章](04-系统底座全景解密.md))。
|
||||
|
||||
---
|
||||
|
||||
## 本文的取证纪律
|
||||
|
||||
本文**每一条结论都附代码证据**,证据形式为以下三类之一:
|
||||
|
||||
1. **ASAR 条目路径 + 字节偏移**(可直接复现读取)
|
||||
2. **CSS 原文片段**(从 `MainWindowPage-BjVtjJFx.css` 精确 `grep`/`extract`)
|
||||
3. **JS 原文片段**(从 `.vite/build/*.js` 精确截取)
|
||||
|
||||
凡是 REA 无法确定、或静态分析不可解的项,**一律标注为 `unknown` 并说明原因**,不做推断性补全。
|
||||
|
||||
**已知的整体覆盖度限制**(来自 REA 取证包 `/tmp/claude_app_asar_evidence.json`,217.3 MB,`evidence_id: ev_77dc39e0…`):
|
||||
|
||||
```
|
||||
records[0].normalized_result.statistics:
|
||||
relevant_files 572
|
||||
parsed_javascript_files 546
|
||||
skipped_javascript_files 7
|
||||
parse_failures 0
|
||||
visited_ast_nodes 6621556
|
||||
|
||||
records[0].normalized_result.limitations: 1064 条
|
||||
其中 532 个文件被资源预算截断
|
||||
```
|
||||
|
||||
其中 512 条的原文是:
|
||||
|
||||
```
|
||||
<file>: Resource constraint during analyze_javascript_application:
|
||||
The parent reserved its remaining result capacity for static observations;
|
||||
semantic analysis of <file> was not started
|
||||
```
|
||||
|
||||
被截断的文件**包含核心主进程文件**——这些文件的静态事实保留,但语义图未生成。本文在涉及这些文件的章节会单独标注。
|
||||
|
||||
---
|
||||
|
||||
## CDS 3.0 与 CDS 2.0 的实质差异
|
||||
|
||||
本节只列**有代码证据**的差异,不做风格化叙述。
|
||||
|
||||
| 维度 | CDS 2.0(v2.37337.1) | CDS 3.0(3.6556.2 实测) | 证据位置 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 主样式表文件名 | `MainWindowPage-BD9CWQ83.css` | `MainWindowPage-BjVtjJFx.css` | ASAR 条目列表 |
|
||||
| 验证状态 | 本机无备份,**不可复核** | ✅ 本机可复现(237,153 bytes) | `stat` + `shasum` |
|
||||
| `data-cds` 命名空间 | 40(文档声称) | **45** | 见 [4.2](04-系统底座全景解密.md) |
|
||||
| `@keyframes cds-*` | 47(文档声称) | **55** | 见 [4.1](04-系统底座全景解密.md) |
|
||||
| `@property` 注册 | 未记录 | **24 个** | 见 [4.5](04-系统底座全景解密.md) |
|
||||
| `--cds-clay-emphasized` | `#cc5a36`(文档) | **`#c6613f`** | 见 [4.3](04-系统底座全景解密.md) |
|
||||
| 骨架屏实现 | `.skeleton-shimmer` + `::after` | **双层 `data-cds="SkeletonGroup"` / `data-cds="Skeleton"` + `.cds-skel-sweep-band` 子元素** | 见 [3.5](03-架构重构与对比分析.md) |
|
||||
| 骨架屏「hold」机制 | 未记录 | **`data-hold` 三态:无 / `none` / `delayed`** | 见 [3.5](03-架构重构与对比分析.md) |
|
||||
| 减少动效 | 未记录 | `[data-reduce-motion=true]` + `@media (prefers-reduced-motion:reduce)` 双层 | 见 [3.5](03-架构重构与对比分析.md) |
|
||||
| 暗色模式作用域 | `.dark` class | **`[data-mode=dark\|light\|system]` 三态 + `.cds-dark-scope`** | 见 [4.4](04-系统底座全景解密.md) |
|
||||
| RTL 支持 | 105° → 255° 渐变翻转 | **`:lang(ae/ar/arc/bcc/…)` 精确语言列表 + `animation-direction:reverse`** | 见 [3.5](03-架构重构与对比分析.md) |
|
||||
|
||||
---
|
||||
|
||||
## 🧭 v4.29.0 版分章目录
|
||||
|
||||
- [第一章:综述与版本边界](01-综述与版本边界.md)
|
||||
- 1.1 版本边界声明(`@ant/[email protected]`,ASAR 根摘要,进程/窗口拓扑)
|
||||
- 1.2 取证覆盖度与不可解项(532 文件语义截断、6 个结构性零值)
|
||||
- 1.3 v2 → v3 迁移影响面(哪些令牌必须改、哪些可以保留)
|
||||
- [第二章:Electron 架构取证](02-Electron架构取证.md)
|
||||
- 2.0 复现方法(ASAR header 布局)
|
||||
- 2.1 主进程面(`index.pre.js`:6 个 IPC 通道、0 个 handler、0 个 fork 调用)
|
||||
- 2.2 渲染进程面(`mainView.js`:1598 次 `ipcRenderer`、12 个字面量桥接 API 名)
|
||||
- 2.3 窗口拓扑(7 个 renderer 入口 vs summary 的 5 个 BrowserWindow)
|
||||
- 2.4 完整性冲突(8 处声明摘要与实测字节不符)
|
||||
- [第三章:架构重构与对比分析](03-架构重构与对比分析.md)
|
||||
- 3.1 骨架屏:双层组件结构与 sweep band 子元素
|
||||
- 3.2 骨架屏:`data-hold` 三态揭示时序机制
|
||||
- 3.3 骨架屏:双模 wave / lockstep 的真实延时公式
|
||||
- 3.4 骨架屏:RTL 语言级适配
|
||||
- 3.5 减少动效与残留动效兜底
|
||||
- 3.6 Pill 胶囊:1.32x 过冲的 9 个关键帧
|
||||
- 3.7 滚动渐隐:`@property` 驱动的纯 CSS 实现
|
||||
- [第四章:系统底座全景解密](04-系统底座全景解密.md)
|
||||
- 4.1 `@keyframes cds-*` 完整 55 项清单
|
||||
- 4.2 `data-cds` 完整 45 项命名空间清单
|
||||
- 4.3 色彩体系:35 阶 gray / 35 阶 orange 全表 + clay 三值问题
|
||||
- 4.4 模式作用域:`data-mode` 三态与暗色覆盖
|
||||
- 4.5 `@property` 24 项注册清单
|
||||
- 4.6 时间与缓动 Token
|
||||
- 4.7 圆角与密度系统
|
||||
- [第五章:open-claude-ui 差异审计](05-open-claude-ui差异审计.md)
|
||||
- 5.1 已对齐项(35 阶 gray/orange 全表)
|
||||
- 5.2 必须修正项(`clay-emphasized`、骨架屏暗色)
|
||||
- 5.3 缺失项(44 个 keyframes / 27 个 data-cds / 7 个 @property)
|
||||
- 5.4 Issue #25 分支的取值裁定
|
||||
- 5.5 修复优先级总表
|
||||
- [第六章:全量 CSS 扫描与官方规范比对](06-全量CSS扫描与官方规范比对.md)
|
||||
- 6.1 扫描范围(17 个 CSS / 9 个唯一)
|
||||
- 6.2 44 个缺失 keyframes 全部在主样式表中
|
||||
- 6.3 `window-shared.css` 揭示 CDS 的真实分发路径
|
||||
- 6.4 提取到官方 CDS 规范文档(58,354 字符)
|
||||
- 6.5 `--cds-*` 与官方 token 的 131 项比对(90 等价 / 4 单位 / 37 真实差异)
|
||||
- 6.6 对前序章节结论的修正
|
||||
- 6.7 运行时实证(Main Process Debugger:preload 真实路径 / utility process / 双模值表 / 修复验证)
|
||||
- 6.8 对前序章节结论的修正
|
||||
- 6.9 更新后的差异优先级
|
||||
- 6.10 补充盘点:ASAR 全量架构(compile-cache / bundled-skills / 内置 MCP servers / Cowork VM / 现代 CSS 特性)
|
||||
- [第七章:总结与后续工作](07-总结与后续工作.md)
|
||||
- 7.1 本次取证的确定性边界
|
||||
- 7.2 已完成的结论清单
|
||||
- 7.3 明确的未知项
|
||||
- 7.4 建议的后续工作
|
||||
- 7.5 本文的可复现性声明
|
||||
|
||||
---
|
||||
|
||||
## 复现本文所有结论的方式
|
||||
|
||||
本文每条证据都可用如下命令复现(需 REA 6.8.0 或纯 Node):
|
||||
|
||||
```bash
|
||||
# 1. 确认版本
|
||||
shasum -a 256 "/Applications/Claude.app/Contents/Resources/app.asar"
|
||||
# 期望:b2dfbbb40325d978dcb7561ad12a6f9bc199f8f46b7d3d2897af74ce8965d68a
|
||||
|
||||
# 2. 抽取主样式表(ASAR header 为纯 JSON,位于偏移 16,长度见前 16 字节的 uint32LE@12)
|
||||
node scripts/extract-main-css.mjs > MainWindowPage-BjVtjJFx.css
|
||||
|
||||
# 3. 验证任意一条 token / keyframes 结论
|
||||
grep -o "@keyframes [a-zA-Z0-9_-]*" MainWindowPage-BjVtjJFx.css | sort -u | wc -l # 55
|
||||
grep -o "\[data-cds=[A-Za-z-]*\]" MainWindowPage-BjVtjJFx.css | sort -u | wc -l # 45
|
||||
grep -o "@property --cds-[a-zA-Z0-9_-]*" MainWindowPage-BjVtjJFx.css | sort -u | wc -l # 24
|
||||
```
|
||||
|
||||
|
||||
|
||||
> 💡 **单文件检索**:如需离线全局搜索v4.29.0 版全文,参见 [v4.29.0 版单文件完整版](Claude-Desktop-4.29.0-4.4212.0-全量分析.full.md)。
|
||||
+89
-20
@@ -1,41 +1,110 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 断言 AGENTS.md 里引用的每个 `--cds-*` 都在 cds-theme.css 里有定义。
|
||||
* 退出码 0 = 通过,1 = 有未定义引用(可用作 CI 门禁)。
|
||||
* 断言 AGENTS.md 与 docs/ 目录中引用的每个 `--cds-*` 都在 cds-theme.css 里有权威定义,
|
||||
* 并断言每个 `@keyframes cds-*` 关键帧都有明确的消费类 (Consumer Class)。
|
||||
* 退出码 0 = 全部通过,1 = 存在未定义引用或孤立关键帧(用作 CI 门禁)。
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const ROOT = path.resolve(process.argv[2] ?? '.');
|
||||
const DOC = 'AGENTS.md';
|
||||
const CSS = 'code-examples/cds-theme.css';
|
||||
const DOC = 'AGENTS.md';
|
||||
const DOCS_DIR = 'docs';
|
||||
|
||||
const css = fs.readFileSync(path.join(ROOT, CSS), 'utf8');
|
||||
const doc = fs.readFileSync(path.join(ROOT, DOC), 'utf8');
|
||||
const cssPath = path.join(ROOT, CSS);
|
||||
const docPath = path.join(ROOT, DOC);
|
||||
const docsDirPath = path.join(ROOT, DOCS_DIR);
|
||||
|
||||
const css = fs.readFileSync(cssPath, 'utf8');
|
||||
const doc = fs.readFileSync(docPath, 'utf8');
|
||||
|
||||
const defined = new Map();
|
||||
for (const m of css.matchAll(/(--cds-[a-z0-9-]+)\s*:\s*([^;]+);/gi)) {
|
||||
if (!defined.has(m[1])) defined.set(m[1], m[2].trim());
|
||||
}
|
||||
|
||||
const referenced = [...new Set([...doc.matchAll(/`(--cds-[a-z0-9-]+)`/g)].map((m) => m[1]))];
|
||||
const missing = referenced.filter((t) => !defined.has(t));
|
||||
// 递归提取目录下所有 markdown 和 html 中的 --cds-* 引用
|
||||
function findTokensInDir(dir) {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
let tokens = [];
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
tokens = tokens.concat(findTokensInDir(full));
|
||||
} else if (entry.name.endsWith('.md') || entry.name.endsWith('.html')) {
|
||||
const content = fs.readFileSync(full, 'utf8');
|
||||
const matched = [...content.matchAll(/`(--cds-[a-z0-9-]+)`/g)].map((m) => m[1]);
|
||||
tokens = tokens.concat(matched);
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
const agentsTokens = [...new Set([...doc.matchAll(/`(--cds-[a-z0-9-]+)`/g)].map((m) => m[1]))];
|
||||
const docsTokens = [...new Set(findTokensInDir(docsDirPath))];
|
||||
const referenced = [...new Set([...agentsTokens, ...docsTokens])];
|
||||
const missingTokens = referenced.filter((t) => !defined.has(t));
|
||||
|
||||
console.log(`定义: ${defined.size} 个 token(${CSS})`);
|
||||
console.log(`引用: ${referenced.length} 个 token(${DOC})`);
|
||||
console.log(`引用: ${agentsTokens.length} 个 token(${DOC})+ ${docsTokens.length} 个 token(${DOCS_DIR}/)共 ${referenced.length} 个不重复引用`);
|
||||
|
||||
if (!missing.length) {
|
||||
console.log('\n✓ 全部引用都有定义');
|
||||
process.exit(0);
|
||||
let hasError = false;
|
||||
|
||||
if (missingTokens.length) {
|
||||
hasError = true;
|
||||
console.log(`\n✗ ${missingTokens.length} 个引用没有定义:\n`);
|
||||
for (const t of missingTokens) {
|
||||
const stem = t.replace(/^--cds-/, '').split('-')[0];
|
||||
const near = [...defined.keys()].filter((d) => d.startsWith(`--cds-${stem}`));
|
||||
console.log(` ${t}`);
|
||||
if (near.length) console.log(` 同词根候选: ${near.join(', ')}`);
|
||||
}
|
||||
} else {
|
||||
console.log('\n✓ 全部 Token 引用都有权威定义');
|
||||
}
|
||||
|
||||
console.log(`\n✗ ${missing.length} 个引用没有定义:\n`);
|
||||
for (const t of missing) {
|
||||
const i = doc.indexOf('`' + t + '`');
|
||||
const line = doc.slice(0, i).split('\n').length;
|
||||
const stem = t.replace(/^--cds-/, '').split('-')[0];
|
||||
const near = [...defined.keys()].filter((d) => d.startsWith(`--cds-${stem}`));
|
||||
console.log(` ${t} (AGENTS.md:${line})`);
|
||||
if (near.length) console.log(` 同词根候选: ${near.join(', ')}`);
|
||||
// 门禁 2: 断言所有 @keyframes cds-* 都有消费类 (防止孤立关键帧)
|
||||
const cdsKeyframes = [...new Set([...css.matchAll(/@keyframes\s+(cds-[a-zA-Z0-9_-]+)/g)].map((m) => m[1]))];
|
||||
const orphanedKeyframes = [];
|
||||
|
||||
// 先剥离注释再判断消费:注释里提到的关键帧名不算消费
|
||||
// (复现:`/* TODO: cds-xxx 待接入 */` 曾被误判为已消费)
|
||||
const cssNoComments = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
||||
|
||||
for (const kf of cdsKeyframes) {
|
||||
// 移除关键帧自身定义后,查找是否有对该动画名的消费规则
|
||||
const withoutDef = cssNoComments.replace(new RegExp(`@keyframes\\s+${kf}[^{]*\\{[^}]*(?:\\{[^}]*\\}[^}]*)*\\}`, 'g'), '');
|
||||
const isConsumed = new RegExp(`\\b${kf}\\b`).test(withoutDef);
|
||||
if (!isConsumed) {
|
||||
orphanedKeyframes.push(kf);
|
||||
}
|
||||
}
|
||||
process.exit(1);
|
||||
|
||||
console.log(`\n关键帧消费审计: ${cdsKeyframes.length} 个 @keyframes cds-* 检查`);
|
||||
if (orphanedKeyframes.length) {
|
||||
hasError = true;
|
||||
console.log(`\n✗ ${orphanedKeyframes.length} 个关键帧无消费类 (Orphaned Keyframes):\n`);
|
||||
for (const kf of orphanedKeyframes) {
|
||||
console.log(` @keyframes ${kf}`);
|
||||
}
|
||||
} else {
|
||||
console.log('✓ 全部 @keyframes cds-* 均有明确消费类');
|
||||
}
|
||||
|
||||
// 门禁 3: 显式断言骨架屏核心 Token
|
||||
const skeletonRequired = ['--cds-skel-i', '--cds-skeleton-base', '--cds-skeleton-sheen'];
|
||||
const missingSkeleton = skeletonRequired.filter((t) => !defined.has(t));
|
||||
if (missingSkeleton.length) {
|
||||
hasError = true;
|
||||
console.log(`\n✗ 缺失骨架屏核心动效 Token: ${missingSkeleton.join(', ')}`);
|
||||
} else {
|
||||
console.log('✓ 骨架屏双模式动效核心 Token (--cds-skel-i, base, sheen) 完整就位');
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\n✨ 所有设计令牌与动效关键帧消费校验全部通过!');
|
||||
process.exit(0);
|
||||
Reference in new issue
Block a user