mirror of
https://github.com/Caho1/open-claude-ui.git
synced 2026-11-07 10:26:02 -10:00
Merge pull request #23 from Caho1/fix/cds-token-closure-and-client-spring-audit
fix(cds): close undefined token references and realign motion presets with client 2.42304.2
This commit is contained in:
6 files changed
+307
-130
No files matched your search
@@ -83,18 +83,71 @@ import {
|
||||
```
|
||||
|
||||
### The 4 Canonical Spring Presets
|
||||
1. **`SPRING_SNAPPY`** (`stiffness: 600, damping: 30, mass: 0.6`, $\zeta = 0.791$):
|
||||
- **Overshoot**: `+1.8%`, settle time: `~0.22s`.
|
||||
- **Where to use**: Primary button clicks, segmented control pill switching (`layoutId`), tabs, micro-interactions.
|
||||
2. **`SPRING_BOUNCY`** (`stiffness: 400, damping: 25, mass: 0.8`, $\zeta = 0.612$):
|
||||
- **Overshoot**: `+8.8%`, settle time: `~0.38s`.
|
||||
- **Where to use**: Toggle switches, badge growth entrances, success confirmation pop-ins.
|
||||
3. **`SPRING_GENTLE`** (`stiffness: 220, damping: 24, mass: 1.0`, $\zeta = 0.809$):
|
||||
- **Overshoot**: `+1.5%`, settle time: `~0.48s`.
|
||||
- **Where to use**: Modals, Thinking Accordion height expansion, message bubble entry, split-pane 50/50 reset.
|
||||
4. **`SPRING_HEAVY_SNAP`** (`stiffness: 1200, damping: 80, mass: 0.1`, $\zeta = 1.826$, overdamped):
|
||||
|
||||
Presets 1, 2 and 4 are observed values extracted from `Claude.app` `ion-dist`.
|
||||
Preset 3 (`SPRING_GENTLE`) is a project-local addition, not a CDS observation —
|
||||
no client release checked here contains `stiffness:380` or `damping:18`. Values
|
||||
below are recomputed from the exported constants, not copied from prose.
|
||||
`Observed use` lines record call sites found in the shipped bundle, not inferred intent.
|
||||
|
||||
> **Provenance and version scope.** `Observed use` and the client inventory in
|
||||
> this section were verified against **`/Applications/Claude.app` 2.42304.2**.
|
||||
> The `docs/v1` chapters describe **v1.28** and are deliberately left as the
|
||||
> historical record of that release; the `docs/v2` chapters describe
|
||||
> **v2.37337.1** but do not carry a spring table. Constant values are stable
|
||||
> across these releases; call sites are not.
|
||||
|
||||
1. **`SPRING_BOUNCY`** (`stiffness: 600, damping: 30, mass: 1.0`, $\zeta = 0.612$):
|
||||
- **Overshoot**: `+8.8%`.
|
||||
- **Observed use**: Microphone button mount/unmount `scale`+`opacity`, voice-bar
|
||||
`width: 0 → 32` expansion, button padding morphs, and `popLayout` icon swaps.
|
||||
- **Source**: `ion-dist/assets/v1/shared-8-PHXy9B6B.js`.
|
||||
2. **`SPRING_SNAPPY`** (`stiffness: 320, damping: 28, mass: 1.0`, $\zeta = 0.783$):
|
||||
- **Overshoot**: `+1.9%`.
|
||||
- **Observed use**: Drag-release rebound — paired `animate(dx|dy, target, {...preset, velocity})`
|
||||
calls that settle a dragged element onto its slot using the release velocity.
|
||||
Not an entrance animation. Guarded by `prefers-reduced-motion`, which snaps instead.
|
||||
- **Source**: `ion-dist/assets/v1/c427176b2-DuM09fKk.js`.
|
||||
3. **`SPRING_GENTLE`** (`stiffness: 380, damping: 18, mass: 1.0`, $\zeta = 0.462$):
|
||||
- **Overshoot**: `+19.5%`.
|
||||
- **Observed use**: none — this preset has no client call site.
|
||||
- **Source**: none. Project-local preset filling the gap between the
|
||||
near-critically-damped pair and the high-frequency pop; do not present it as a Claude reading.
|
||||
- **Do not equate it with `duration .32 / bounce .15`.** That real client config
|
||||
(`shared-9-Co0ph6Xn.js`, dictation↔rest state swap) resolves through framer-motion
|
||||
`tn()` to `stiffness ≈ 244, damping ≈ 26.6, ζ ≈ 0.85` — `+0.6%` overshoot, not `+19.5%`.
|
||||
4. **`SPRING_HEAVY_SNAP`** (`stiffness: 1200, damping: 80, mass: 0.1`, $\zeta = 3.652$, overdamped):
|
||||
- **Overshoot**: `0%` (Zero overshoot).
|
||||
- **Where to use**: Session item drag-and-drop placeholder collision, magnetic drawer snapping.
|
||||
- **Observed use**: `file_viewer` panel width animation — `animate(width, 384, preset)`
|
||||
on open and back to the computed initial width on close. In the same module
|
||||
`kt = 320` is the minimum width and `At = 480` the default width.
|
||||
- **Source**: `ion-dist/assets/v1/c8ad300b7-07P9B-v4.js`.
|
||||
|
||||
### Other spring presets observed in the client
|
||||
|
||||
The four names above are a project convention, not a complete client inventory.
|
||||
The shipped bundle contains 14 distinct spring configs; these three are missing
|
||||
from the preset file and are worth knowing about:
|
||||
|
||||
| Config | ζ | Overshoot | Observed use | Source |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| `420 / 28` | 0.683 | `+5.3%` | Draggable capture popover, `scale .25 → 1` entrance with `dragElastic: .05` | `c0447db9a-KKfDTbDr.js` |
|
||||
| `260 / 16` | 0.496 | `+16.6%` | Capture-selection handle rotation, `-90°` arrow marker; exit `scale .6` | `c0447db9a-KKfDTbDr.js` |
|
||||
| `300 / 35 / mass 1` | 1.01 | `0%` | Staggered list entrance, `translateY 32 → 0` with `staggerChildren: .15` and a separate `opacity` ease-out | `ccd647019-DrnyijvI.js` |
|
||||
|
||||
Two more appear as timing variants rather than reusable presets: `320 / 30`
|
||||
(`delay: 1`, credits controls row) and `500 / 30 / mass .7`.
|
||||
|
||||
### Version caveat
|
||||
|
||||
`docs/v1` records the matrix as it appeared in client **v1.28**, labelling
|
||||
`600 / 30` as "buttons" and `1200 / 80 / .1` as "sidebar drag damping". That
|
||||
mapping was correct for that release. In **2.42304.2** both constants survive
|
||||
unchanged but their call sites moved: `600 / 30` now drives the microphone and
|
||||
voice bar, and `1200 / 80 / .1` drives the `file_viewer` panel width. Neither
|
||||
`docs/v1` nor `docs/v2` covers 2.42304.2, so the table above is the only
|
||||
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:
|
||||
|
||||
@@ -55,8 +55,11 @@ export function calcDampedHarmonicOscillator(
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 预设 1: 按钮点击、开关切换、图标按压 (Button & Small Controls)
|
||||
* 特征:高频微弹,利落归位无粘滞。超调量 +8.8% (ζ = 0.612),0.22s 极速收敛。
|
||||
* 预设 1: 麦克风按钮 / 语音状态条 (Mic & Voice Bar)
|
||||
* 特征:高频微弹,超调量 +8.8% (ζ = 0.612)。
|
||||
* 来源:`ion-dist/assets/v1/shared-8-PHXy9B6B.js` — `{type:"spring",stiffness:600,damping:30}`
|
||||
* 实测用途:麦克风按钮 mount/unmount 的 `scale/opacity` 出入场、语音状态
|
||||
* `width:0→32` 展开、按钮 padding 形变、图形切换的 `popLayout`。
|
||||
*/
|
||||
export const SPRING_BOUNCY: Transition = {
|
||||
type: "spring",
|
||||
@@ -66,8 +69,12 @@ export const SPRING_BOUNCY: Transition = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 预设 2: 气泡上浮、卡片展开、浮层 Popover 唤起 (Card / Popover Reveal)
|
||||
* 特征:沉稳顺滑,微冲后平稳着陆。超调量 +1.8%,0.35s 收敛。
|
||||
* 预设 2: 手势拖拽回弹 (Drag Release) —— 不是入场动画
|
||||
* 特征:沉稳顺滑,超调量 +1.9% (ζ = 0.783)。
|
||||
* 来源:`ion-dist/assets/v1/c427176b2-DuM09fKk.js` — `{type:"spring",stiffness:320,damping:28}`
|
||||
* 实测用途:`animate(current.dx, target.dx, {...preset, velocity})` 与 `dy` 成对,
|
||||
* 拖动结束后按松手速度回弹到落位点;带 `prefers-reduced-motion`
|
||||
* 分支直接跳位(客户端 65 个文件均做此降级)。
|
||||
*/
|
||||
export const SPRING_SNAPPY: Transition = {
|
||||
type: "spring",
|
||||
@@ -77,8 +84,18 @@ export const SPRING_SNAPPY: Transition = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 预设 3: 点赞动画、复制成功勾选、新消息微提示 (Bouncy Light Feedback)
|
||||
* 特征:两周期明显跳跃感,激发积极情绪反馈。超调量 +19.4%,0.58s 收敛。
|
||||
* 预设 3: **非客户端提取,本项目自建**
|
||||
* 特征:明显两周期跳跃感,超调量 +19.5% (ζ = 0.462)。
|
||||
*
|
||||
* 客户端全量搜索无 `stiffness:380` / `damping:18`,故 `380/18` 不是 CDS 观测值。
|
||||
* 文档曾把客户端的 `{type:"spring",duration:.32,bounce:.15}`
|
||||
* (`shared-9-Co0ph6Xn.js`,听写/静息状态切换)标注为「或 `dur 0.32`」的等价写法,
|
||||
* 但按 framer-motion `tn()` 反解,该配置实际等价于
|
||||
* `stiffness≈244, damping≈26.6, ζ≈0.85`(超调仅 +0.6%),与 380/18 相差极大。
|
||||
* 如需还原该切换手感,请直接用 duration/bounce 形式,不要用本预设近似。
|
||||
*
|
||||
* 本预设仅用于补足「零过冲」与「高频微弹」之间的中段强弹性档位,
|
||||
* 借用前请确认符合你的设计意图,不要当作 Claude 观测值引用。
|
||||
*/
|
||||
export const SPRING_GENTLE: Transition = {
|
||||
type: "spring",
|
||||
@@ -88,8 +105,12 @@ export const SPRING_GENTLE: Transition = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 预设 4: 抽屉与侧边栏手势吸附、面板分割拖拽 (High Damping Panel Drag)
|
||||
* 特征:绝对零过冲 (过阻尼 ζ = 3.65),松手瞬间如磁吸般强力咬合。
|
||||
* 预设 4: 文件查看器面板宽度 (File Viewer Panel Width)
|
||||
* 特征:绝对零过冲(过阻尼 ζ = 3.652)。
|
||||
* 来源:`ion-dist/assets/v1/c8ad300b7-07P9B-v4.js` — `{type:"spring",stiffness:1200,damping:80,mass:.1}`
|
||||
* 实测用途:`file_viewer` 面板展开时 `animate(width, 384, preset)` 将宽度从
|
||||
* `initial` 弹到 `384`(常量 `jt=384`),关闭时弹回 `Pt().initial`;
|
||||
* 同一文件中 `kt=320` 是最小宽度、`At=480` 是默认宽度。
|
||||
*/
|
||||
export const SPRING_HEAVY_SNAP: Transition = {
|
||||
type: "spring",
|
||||
|
||||
@@ -90,7 +90,7 @@ export function SegmentedControl({
|
||||
{/* 1. 原生纯几何定位滑动指示器 */}
|
||||
{!useFramerMotion ? (
|
||||
<div
|
||||
className="df-pill-indicator absolute top-[1px] bottom-[1px] rounded-full bg-white dark:bg-[#2c2c2a] pointer-events-none transition-all duration-120 ease-[cubic-bezier(0.32,0.72,0,1)] will-change-transform"
|
||||
className="df-pill-indicator absolute top-[1px] bottom-[1px] rounded-full bg-white dark:bg-[#2c2c2a] pointer-events-none transition-all duration-[var(--cds-dur-snap)] ease-[var(--cds-ease-snap)] will-change-transform"
|
||||
style={{
|
||||
left: `calc(1px + ${activeIndex} * (100% - 2px) / ${normalizedTabs.length})`,
|
||||
width: `calc((100% - 2px) / ${normalizedTabs.length})`,
|
||||
|
||||
+163
-110
@@ -158,6 +158,18 @@
|
||||
--radius-card: calc(var(--cds-radius, 8px) + calc(.25rem * var(--cds-rem-scale, 1)));
|
||||
|
||||
/* 会话气泡与版心计量核心令牌 (REA v6.5.0 取证自 frame-shell-chrome-CAw-tYzh.js) */
|
||||
/* 中性档别名 (REA 取证自 .cds-root 作用域, Claude 2.42304.2)
|
||||
用户气泡背景经 --cds-bg-user-message 间接依赖这两个别名;缺失会使
|
||||
var() 链在解析期失效、气泡背景回退为透明。 */
|
||||
--cds-bg-neutral: var(--cds-alpha-1);
|
||||
--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-blue-450: #2a78d6;
|
||||
--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);
|
||||
--cds-bg-user-message: var(--cds-bg-neutral);
|
||||
--cds-bg-user-message-personal: var(--cds-bg-tint-blue);
|
||||
--cds-text-user-message-personal: var(--cds-text-tint-blue, #193c5f);
|
||||
@@ -199,6 +211,15 @@
|
||||
--cds-ease-pulse: cubic-bezier(.4, 0, .2, 1); /* 思考三点跳动呼吸脉冲 */
|
||||
--cds-ease-rise: cubic-bezier(.3, .7, .4, 1); /* 状态条平滑抬升 */
|
||||
--cds-ease-fall: cubic-bezier(0, 0, .2, 1); /* 状态条平滑下落 */
|
||||
|
||||
/* 12b. 时间尺度令牌 (REA 取证自 c6a992d55-sXiDGMFQ.css, Claude 2.42304.2)
|
||||
缺失时下列 var() 引用会让整条 transition 声明被丢弃、计算值退化为 `all 0s`。
|
||||
--cds-dur-slow 被 .cds-btn-squish、卡片按压与 clip-path 过渡共同依赖。 */
|
||||
--cds-dur-fast: 60ms; /* 瞬态响应:按压塌陷、Focus 环显现、Checkbox 勾选 */
|
||||
--cds-dur-snap: .12s; /* 瞬时吸附:分段控件滑块、Tab 切换 */
|
||||
--cds-dur-base: .2s; /* 标准组件过渡:菜单弹出、折叠展开、卡片入场 */
|
||||
--cds-dur-sheet: .3s; /* 大面板与抽屉:侧边栏、Artifact 面板滑入 */
|
||||
--cds-dur-slow: .45s; /* 环境转场与按压回弹:.cds-btn-squish 松手复位的弹簧时长 */
|
||||
--cds-btn-spring: linear(0, .2459, .6526, .9468, 1.0764, 1.0915, 1.0585, 1.0219, .9993, .9914, .9921, .9957, .9988, 1.0004, 1); /* 15阶物理弹簧回弹 */
|
||||
|
||||
/* 13. 字体栈与 CJK 真实回退链 */
|
||||
@@ -271,12 +292,29 @@
|
||||
--cds-clay: #d97757;
|
||||
--cds-clay-hover: #e08264;
|
||||
|
||||
/* 强调色链在暗色下不变 (REA 核实: --cds-blue-* 全库仅单值,无 dark 变体;
|
||||
与 --cds-clay 同属品牌色,此处显式重复以对齐既有约定)。 */
|
||||
--cds-blue-450: #2a78d6;
|
||||
--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);
|
||||
|
||||
--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);
|
||||
|
||||
|
||||
/* 会话气泡背景在暗色下的取值。
|
||||
--cds-bg-user-message 在 :root 处已绑定 var(--cds-bg-neutral),该绑定在
|
||||
:root 层完成替换;仅改 --cds-alpha-* 不会回溯更新气泡颜色,故此处显式重声明
|
||||
整条链。客户端对应实现为 hsl(from --cds-neutral-900 ... ) 形式,
|
||||
其中 --cds-neutral-900 在暗色下取 gray-900(#0b0b0b) → 白 5%。 */
|
||||
--cds-bg-neutral: var(--cds-alpha-1);
|
||||
--cds-bg-neutral-hover: var(--cds-alpha-2);
|
||||
--cds-bg-user-message: var(--cds-bg-neutral);
|
||||
|
||||
--cds-border: var(--cds-alpha-2);
|
||||
--cds-border-strong: var(--cds-alpha-3);
|
||||
|
||||
@@ -631,21 +669,6 @@
|
||||
/* 1. CSS Houdini 自定义变量注册 (CSS Properties & Values API) */
|
||||
@property --cds-anim { syntax: "<number>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-anim2 { syntax: "<number>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-size { syntax: "<length>"; inherits: false; initial-value: 28px; }
|
||||
@property --cds-scroll-fade-top { syntax: "<length>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-bottom { syntax: "<length>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-left { syntax: "<length>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-right { syntax: "<length>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-top-raw { syntax: "<length>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-bottom-raw { syntax: "<length>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-left-raw { syntax: "<length>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-right-raw { syntax: "<length>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-y-active { syntax: "<number>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-x-active { syntax: "<number>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-top-on { syntax: "<number>"; inherits: false; initial-value: 1; }
|
||||
@property --cds-scroll-fade-bottom-on { syntax: "<number>"; inherits: false; initial-value: 1; }
|
||||
@property --cds-scroll-fade-left-on { syntax: "<number>"; inherits: false; initial-value: 1; }
|
||||
@property --cds-scroll-fade-right-on { syntax: "<number>"; inherits: false; initial-value: 1; }
|
||||
@property --sheet-card-sweep { syntax: "<angle>"; inherits: false; initial-value: 90deg; }
|
||||
@property --sheet-card-lit { syntax: "<number>"; inherits: false; initial-value: 0.55; }
|
||||
@property --sheet-card-light-angle { syntax: "<angle>"; inherits: false; initial-value: 165deg; }
|
||||
@@ -814,118 +837,148 @@
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
7. Chromium 115+ / Electron 44+ 纯 CSS @property 滚动插值引擎 (Zero-JS Scroll-Fade Engine)
|
||||
基于 CSS @property 与 animation-timeline: scroll(self block / inline)
|
||||
7. 滚动边缘渐隐引擎 (Scroll-Edge Fade)
|
||||
---------------------------------------------------------------------------
|
||||
REA 取证自 Claude 2.42304.2 `assets/v1/c6a992d55-sXiDGMFQ.css`。
|
||||
|
||||
客户端设计要点(与简化版的关键差异):
|
||||
1. 不使用 `animation-range` 硬编码 48px;由 `animation-timeline: scroll(self)`
|
||||
驱动 0→100% 全程插值,实际可见宽度由 `--cds-scroll-fade-size` 决定。
|
||||
2. `*-raw` 只承载「几何进度」(0 → size),不承载「是否启用」;
|
||||
是否启用由 `*-on`(0/1)与 `*-active`(0/1)相乘控制。三者分离,
|
||||
因此可以在触顶/触底时把 `-on` 归零而不打断 raw 的插值连续性。
|
||||
3. `--cds-scroll-fade-{top,bottom,left,right}` 是派生量:
|
||||
raw × active × on,供 mask 引用。
|
||||
4. 全部 `@property` 均声明 `inherits: false`,初始值即兜底
|
||||
——这是它们不出现在 :root 的原因,而非遗漏。
|
||||
========================================================================== */
|
||||
@property --cds-scroll-fade-top {
|
||||
syntax: '<length>';
|
||||
inherits: false;
|
||||
initial-value: 0px;
|
||||
|
||||
@property --cds-scroll-fade-size { syntax: "<length>"; inherits: false; initial-value: 28px; }
|
||||
@property --cds-scroll-fade-top-raw { syntax: "<length>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-bottom-raw { syntax: "<length>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-left-raw { syntax: "<length>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-right-raw { syntax: "<length>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-top-on { syntax: "<number>"; inherits: false; initial-value: 1; }
|
||||
@property --cds-scroll-fade-bottom-on { syntax: "<number>"; inherits: false; initial-value: 1; }
|
||||
@property --cds-scroll-fade-left-on { syntax: "<number>"; inherits: false; initial-value: 1; }
|
||||
@property --cds-scroll-fade-right-on { syntax: "<number>"; inherits: false; initial-value: 1; }
|
||||
@property --cds-scroll-fade-x-active { syntax: "<number>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-y-active { syntax: "<number>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-top { syntax: "<length>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-bottom { syntax: "<length>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-left { syntax: "<length>"; inherits: false; initial-value: 0; }
|
||||
@property --cds-scroll-fade-right { syntax: "<length>"; inherits: false; initial-value: 0; }
|
||||
@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; }
|
||||
|
||||
/* 只推进几何进度,不决定可见性 */
|
||||
@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; } }
|
||||
@keyframes cds-scroll-fade-left { 0% { --cds-scroll-fade-left-raw: 0px; } to { --cds-scroll-fade-left-raw: var(--cds-scroll-fade-size); } }
|
||||
@keyframes cds-scroll-fade-right { 0% { --cds-scroll-fade-right-raw: var(--cds-scroll-fade-size); } to { --cds-scroll-fade-right-raw: 0px; } }
|
||||
/* 仅用于把 *-active 抬到 1,本身不产生视觉变化 */
|
||||
@keyframes cds-scroll-fade-y-active { 0%, to { --cds-scroll-fade-y-active: 1; } }
|
||||
@keyframes cds-scroll-fade-x-active { 0%, to { --cds-scroll-fade-x-active: 1; } }
|
||||
|
||||
/* 无滚动时间线支持时的静态兜底:保留 scroll-padding,不做遮罩 */
|
||||
.scroll-fade-y {
|
||||
scroll-padding-block:
|
||||
calc(var(--cds-scroll-fade-size) * var(--cds-scroll-fade-top-on))
|
||||
calc(var(--cds-scroll-fade-size) * var(--cds-scroll-fade-bottom-on));
|
||||
}
|
||||
@property --cds-scroll-fade-bottom {
|
||||
syntax: '<length>';
|
||||
inherits: false;
|
||||
initial-value: 0px;
|
||||
}
|
||||
@property --cds-scroll-fade-left {
|
||||
syntax: '<length>';
|
||||
inherits: false;
|
||||
initial-value: 0px;
|
||||
}
|
||||
@property --cds-scroll-fade-right {
|
||||
syntax: '<length>';
|
||||
inherits: false;
|
||||
initial-value: 0px;
|
||||
.scroll-fade-x {
|
||||
scroll-padding-inline:
|
||||
calc(var(--cds-scroll-fade-size) * var(--cds-scroll-fade-left-on))
|
||||
calc(var(--cds-scroll-fade-size) * var(--cds-scroll-fade-right-on));
|
||||
}
|
||||
|
||||
@keyframes cds-scroll-fade-top-in {
|
||||
from { --cds-scroll-fade-top: 0px; }
|
||||
to { --cds-scroll-fade-top: var(--cds-scroll-fade-size, 28px); }
|
||||
}
|
||||
@keyframes cds-scroll-fade-bottom-out {
|
||||
from { --cds-scroll-fade-bottom: var(--cds-scroll-fade-size, 28px); }
|
||||
to { --cds-scroll-fade-bottom: 0px; }
|
||||
}
|
||||
@keyframes cds-scroll-fade-left-in {
|
||||
from { --cds-scroll-fade-left: 0px; }
|
||||
to { --cds-scroll-fade-left: var(--cds-scroll-fade-size, 28px); }
|
||||
}
|
||||
@keyframes cds-scroll-fade-right-out {
|
||||
from { --cds-scroll-fade-right: var(--cds-scroll-fade-size, 28px); }
|
||||
to { --cds-scroll-fade-right: 0px; }
|
||||
}
|
||||
|
||||
[data-cds-scroll-fade],
|
||||
.cds-scroll-fade {
|
||||
--cds-scroll-fade-size: 28px;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@supports (animation-timeline: scroll(self block)) {
|
||||
[data-cds-scroll-fade="vertical"],
|
||||
[data-cds-scroll-fade="both"],
|
||||
.cds-scroll-fade,
|
||||
.cds-scroll-fade-vertical,
|
||||
.cds-scroll-fade-v {
|
||||
animation-timeline: scroll(self block), scroll(self block);
|
||||
animation-name: cds-scroll-fade-top-in, cds-scroll-fade-bottom-out;
|
||||
animation-range: 0 48px, calc(100% - 48px) 100%;
|
||||
animation-fill-mode: both;
|
||||
@supports (animation-timeline: scroll()) {
|
||||
.scroll-fade-y {
|
||||
--cds-scroll-fade-top: calc(var(--cds-scroll-fade-top-raw) * var(--cds-scroll-fade-y-active) * var(--cds-scroll-fade-top-on));
|
||||
--cds-scroll-fade-bottom: calc(var(--cds-scroll-fade-bottom-raw) * var(--cds-scroll-fade-y-active) * var(--cds-scroll-fade-bottom-on));
|
||||
animation-name: cds-scroll-fade-top, cds-scroll-fade-bottom, cds-scroll-fade-y-active;
|
||||
animation-duration: auto;
|
||||
animation-timing-function: linear;
|
||||
mask-image: linear-gradient(to bottom,
|
||||
transparent,
|
||||
rgba(0, 0, 0, 0.05) calc(var(--cds-scroll-fade-top) * 0.15),
|
||||
rgba(0, 0, 0, 0.15) calc(var(--cds-scroll-fade-top) * 0.3),
|
||||
rgba(0, 0, 0, 0.30) calc(var(--cds-scroll-fade-top) * 0.45),
|
||||
rgba(0, 0, 0, 0.50) calc(var(--cds-scroll-fade-top) * 0.6),
|
||||
rgba(0, 0, 0, 0.70) calc(var(--cds-scroll-fade-top) * 0.75),
|
||||
rgba(0, 0, 0, 0.85) calc(var(--cds-scroll-fade-top) * 0.87),
|
||||
#000 var(--cds-scroll-fade-top),
|
||||
#000 calc(100% - var(--cds-scroll-fade-bottom)),
|
||||
rgba(0, 0, 0, 0.85) calc(100% - var(--cds-scroll-fade-bottom) * 0.87),
|
||||
rgba(0, 0, 0, 0.70) calc(100% - var(--cds-scroll-fade-bottom) * 0.75),
|
||||
rgba(0, 0, 0, 0.50) calc(100% - var(--cds-scroll-fade-bottom) * 0.6),
|
||||
rgba(0, 0, 0, 0.30) calc(100% - var(--cds-scroll-fade-bottom) * 0.45),
|
||||
rgba(0, 0, 0, 0.15) calc(100% - var(--cds-scroll-fade-bottom) * 0.3),
|
||||
rgba(0, 0, 0, 0.05) calc(100% - var(--cds-scroll-fade-bottom) * 0.15),
|
||||
transparent);
|
||||
animation-fill-mode: both, both, none;
|
||||
animation-timeline: scroll(self);
|
||||
/* 15 段遮罩:顶部按 0.15/0.30/0.45/0.60/0.75/0.87 递减,底部对称 */
|
||||
-webkit-mask-image: linear-gradient(to bottom,
|
||||
transparent,
|
||||
rgba(0, 0, 0, 0.05) calc(var(--cds-scroll-fade-top) * 0.15),
|
||||
rgba(0, 0, 0, 0.15) calc(var(--cds-scroll-fade-top) * 0.3),
|
||||
rgba(0, 0, 0, 0.30) calc(var(--cds-scroll-fade-top) * 0.45),
|
||||
rgba(0, 0, 0, 0.50) calc(var(--cds-scroll-fade-top) * 0.6),
|
||||
rgba(0, 0, 0, 0.70) calc(var(--cds-scroll-fade-top) * 0.75),
|
||||
rgba(0, 0, 0, 0.85) calc(var(--cds-scroll-fade-top) * 0.87),
|
||||
#0000000d calc(var(--cds-scroll-fade-top) * .15),
|
||||
#00000026 calc(var(--cds-scroll-fade-top) * .3),
|
||||
#0000004d calc(var(--cds-scroll-fade-top) * .45),
|
||||
#00000080 calc(var(--cds-scroll-fade-top) * .6),
|
||||
#000000b3 calc(var(--cds-scroll-fade-top) * .75),
|
||||
#000000d9 calc(var(--cds-scroll-fade-top) * .87),
|
||||
#000 var(--cds-scroll-fade-top),
|
||||
#000 calc(100% - var(--cds-scroll-fade-bottom)),
|
||||
rgba(0, 0, 0, 0.85) calc(100% - var(--cds-scroll-fade-bottom) * 0.87),
|
||||
rgba(0, 0, 0, 0.70) calc(100% - var(--cds-scroll-fade-bottom) * 0.75),
|
||||
rgba(0, 0, 0, 0.50) calc(100% - var(--cds-scroll-fade-bottom) * 0.6),
|
||||
rgba(0, 0, 0, 0.30) calc(100% - var(--cds-scroll-fade-bottom) * 0.45),
|
||||
rgba(0, 0, 0, 0.15) calc(100% - var(--cds-scroll-fade-bottom) * 0.3),
|
||||
rgba(0, 0, 0, 0.05) calc(100% - var(--cds-scroll-fade-bottom) * 0.15),
|
||||
#000000d9 calc(100% - var(--cds-scroll-fade-bottom) * .87),
|
||||
#000000b3 calc(100% - var(--cds-scroll-fade-bottom) * .75),
|
||||
#00000080 calc(100% - var(--cds-scroll-fade-bottom) * .6),
|
||||
#0000004d calc(100% - var(--cds-scroll-fade-bottom) * .45),
|
||||
#00000026 calc(100% - var(--cds-scroll-fade-bottom) * .3),
|
||||
#0000000d calc(100% - var(--cds-scroll-fade-bottom) * .15),
|
||||
transparent);
|
||||
mask-image: linear-gradient(to bottom,
|
||||
transparent,
|
||||
#0000000d calc(var(--cds-scroll-fade-top) * .15),
|
||||
#00000026 calc(var(--cds-scroll-fade-top) * .3),
|
||||
#0000004d calc(var(--cds-scroll-fade-top) * .45),
|
||||
#00000080 calc(var(--cds-scroll-fade-top) * .6),
|
||||
#000000b3 calc(var(--cds-scroll-fade-top) * .75),
|
||||
#000000d9 calc(var(--cds-scroll-fade-top) * .87),
|
||||
#000 var(--cds-scroll-fade-top),
|
||||
#000 calc(100% - var(--cds-scroll-fade-bottom)),
|
||||
#000000d9 calc(100% - var(--cds-scroll-fade-bottom) * .87),
|
||||
#000000b3 calc(100% - var(--cds-scroll-fade-bottom) * .75),
|
||||
#00000080 calc(100% - var(--cds-scroll-fade-bottom) * .6),
|
||||
#0000004d calc(100% - var(--cds-scroll-fade-bottom) * .45),
|
||||
#00000026 calc(100% - var(--cds-scroll-fade-bottom) * .3),
|
||||
#0000000d calc(100% - var(--cds-scroll-fade-bottom) * .15),
|
||||
transparent);
|
||||
}
|
||||
|
||||
[data-cds-scroll-fade="horizontal"],
|
||||
.cds-scroll-fade-horizontal,
|
||||
.cds-scroll-fade-h {
|
||||
animation-timeline: scroll(self inline), scroll(self inline);
|
||||
animation-name: cds-scroll-fade-left-in, cds-scroll-fade-right-out;
|
||||
animation-range: 0 48px, calc(100% - 48px) 100%;
|
||||
animation-fill-mode: both;
|
||||
.scroll-fade-x {
|
||||
--cds-scroll-fade-left: calc(var(--cds-scroll-fade-left-raw) * var(--cds-scroll-fade-x-active) * var(--cds-scroll-fade-left-on));
|
||||
--cds-scroll-fade-right: calc(var(--cds-scroll-fade-right-raw) * var(--cds-scroll-fade-x-active) * var(--cds-scroll-fade-right-on));
|
||||
animation-name: cds-scroll-fade-left, cds-scroll-fade-right, cds-scroll-fade-x-active;
|
||||
animation-duration: auto;
|
||||
animation-timing-function: linear;
|
||||
mask-image: linear-gradient(to right,
|
||||
transparent,
|
||||
#000 var(--cds-scroll-fade-left),
|
||||
#000 calc(100% - var(--cds-scroll-fade-right)),
|
||||
transparent);
|
||||
animation-fill-mode: both, both, none;
|
||||
animation-timeline: scroll(self inline);
|
||||
-webkit-mask-image: linear-gradient(to right,
|
||||
transparent,
|
||||
#0000000d calc(var(--cds-scroll-fade-left) * .15),
|
||||
#00000026 calc(var(--cds-scroll-fade-left) * .3),
|
||||
#0000004d calc(var(--cds-scroll-fade-left) * .45),
|
||||
#00000080 calc(var(--cds-scroll-fade-left) * .6),
|
||||
#000000b3 calc(var(--cds-scroll-fade-left) * .75),
|
||||
#000000d9 calc(var(--cds-scroll-fade-left) * .87),
|
||||
#000 var(--cds-scroll-fade-left),
|
||||
#000 calc(100% - var(--cds-scroll-fade-right)),
|
||||
#000000d9 calc(100% - var(--cds-scroll-fade-right) * .87),
|
||||
#000000b3 calc(100% - var(--cds-scroll-fade-right) * .75),
|
||||
#00000080 calc(100% - var(--cds-scroll-fade-right) * .6),
|
||||
#0000004d calc(100% - var(--cds-scroll-fade-right) * .45),
|
||||
#00000026 calc(100% - var(--cds-scroll-fade-right) * .3),
|
||||
#0000000d calc(100% - var(--cds-scroll-fade-right) * .15),
|
||||
transparent);
|
||||
mask-image: linear-gradient(to right,
|
||||
transparent,
|
||||
#0000000d calc(var(--cds-scroll-fade-left) * .15),
|
||||
#00000026 calc(var(--cds-scroll-fade-left) * .3),
|
||||
#0000004d calc(var(--cds-scroll-fade-left) * .45),
|
||||
#00000080 calc(var(--cds-scroll-fade-left) * .6),
|
||||
#000000b3 calc(var(--cds-scroll-fade-left) * .75),
|
||||
#000000d9 calc(var(--cds-scroll-fade-left) * .87),
|
||||
#000 var(--cds-scroll-fade-left),
|
||||
#000 calc(100% - var(--cds-scroll-fade-right)),
|
||||
#000000d9 calc(100% - var(--cds-scroll-fade-right) * .87),
|
||||
#000000b3 calc(100% - var(--cds-scroll-fade-right) * .75),
|
||||
#00000080 calc(100% - var(--cds-scroll-fade-right) * .6),
|
||||
#0000004d calc(100% - var(--cds-scroll-fade-right) * .45),
|
||||
#00000026 calc(100% - var(--cds-scroll-fade-right) * .3),
|
||||
#0000000d calc(100% - var(--cds-scroll-fade-right) * .15),
|
||||
transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,3 +74,52 @@ author: "REA 逆向工程实验室"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Framer Motion 物理弹簧参数矩阵
|
||||
|
||||
上一节的时间 Token 描述的是**缓动时长**;本节记录的是**二阶阻尼振子**参数。两者不可互换:前者是固定的时间曲线,后者由 `stiffness` / `damping` / `mass` 共同决定超调量与收敛过程。
|
||||
|
||||
> **取证边界**:本表由 `ion-dist/assets/v1/` 打包产物静态提取,验证目标为 `/Applications/Claude.app` **2.42304.2**。`react` 侧 Framer Motion 的默认值是 `{stiffness:100, damping:10, mass:1}`,`mass` 省略即按 `1` 计算。阻尼比与超调量按 `ζ = c / (2√(mk))`、`Mp = exp(−πζ/√(1−ζ²))` 由常量复算得出,不是照抄描述文字。
|
||||
|
||||
#### 1. 四个命名预设之外的完整清单
|
||||
|
||||
客户端打包产物中共有 **14 组** distinct spring 配置。下表列出其中的可复用项及其真实调用点:
|
||||
|
||||
| stiffness / damping / mass | ζ | 超调量 | 客户端真实调用点 | 源文件 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| `600` / `30` / `1` | `0.612` | `+8.8%` | 麦克风按钮 `scale`+`opacity` 出入场、语音条 `width 0→32` 展开、按钮 padding 形变、图形 `popLayout` 切换 | `shared-8-PHXy9B6B.js` |
|
||||
| `320` / `28` / `1` | `0.783` | `+1.9%` | 拖拽松手回弹:成对 `animate(dx|dy, target, {...preset, velocity})` 按松手速度落位。**不是入场动画**;带 `prefers-reduced-motion` 直接跳位分支 | `c427176b2-DuM09fKk.js` |
|
||||
| `1200` / `80` / `0.1` | `3.652` | `0%` | `file_viewer` 面板宽度动画:展开 `animate(width, 384)`,关闭回到计算初值。同模块 `kt=320` 最小宽、`At=480` 默认宽 | `c8ad300b7-07P9B-v4.js` |
|
||||
| `420` / `28` / `1` | `0.683` | `+5.3%` | 可拖拽截图浮层,`scale .25→1` 入场,配 `dragElastic:.05` | `c0447db9a-KKfDTbDr.js` |
|
||||
| `260` / `16` / `1` | `0.496` | `+16.6%` | 截图选区手柄旋转 `rotate→-90°`,退出 `scale .6` | `c0447db9a-KKfDTbDr.js` |
|
||||
| `300` / `35` / `1` | `1.01` | `0%` | 列表逐项入场 `translateY 32→0`,`staggerChildren:.15`,透明度另用 `easeOut` | `ccd647019-DrnyijvI.js` |
|
||||
|
||||
另有 `320` / `30`(`delay:1`,credits 控制行)与 `500` / `30` / `0.7` 属于内联时序变体,未提炼为命名预设。
|
||||
|
||||
#### 2. 六阶 `linear()` 弹簧曲线与 `--cds-dur-slow` 的耦合
|
||||
|
||||
除 Framer Motion 之外,按钮按压回弹走的是纯 CSS 路径,由 `.cds-btn-squish` 承担:
|
||||
|
||||
```css
|
||||
/* 客户端 c6a992d55-sXiDGMFQ.css 真实写法 */
|
||||
.cds-btn-squish {
|
||||
--cds-btn-press-sx: .975;
|
||||
--cds-btn-press-sy: .975;
|
||||
transform-origin: 50%;
|
||||
transition: transform var(--cds-dur-slow) var(--cds-btn-spring);
|
||||
}
|
||||
.cds-btn-squish:active {
|
||||
transform: scale(var(--cds-btn-press-sx), var(--cds-btn-press-sy));
|
||||
transition: transform var(--cds-dur-fast) ease-out; /* 按下:60ms 快速塌陷 */
|
||||
}
|
||||
```
|
||||
|
||||
`--cds-btn-spring` 是一条 15 阶采样(14 段)的 `linear()` 曲线,在第 35.7% 时间点冲出至 `1.0915`(**+9.15% 物理过冲**),随后回落至 `0.9914`,并在 `1.0004` 处收敛。
|
||||
|
||||
**关键耦合关系**:这条 `transition` 依赖 `--cds-dur-slow` 与 `--cds-dur-fast` 两个时长 Token。二者任一缺失时,整条 `transition` 简写会被浏览器判定为无效并丢弃,计算值退化为 `transition-property: all; transition-duration: 0s`——**按钮按下会跳变,松手完全没有回弹**,而 `--cds-btn-spring` 曲线也随之失效。
|
||||
|
||||
这一失效已在本地复现确认:补齐 `--cds-dur-*` 五个 Token 后,`.cds-btn-squish` 的计算值从 `all 0s` 恢复为 `transform 0.45s` 加完整 `linear()` 函数。
|
||||
|
||||
> **实现要求**:使用 `.cds-btn-squish` 或任何引用 `var(--cds-dur-*)` 的过渡时,必须同时定义 §4.3 的五个时间 Token。仅复制 `--cds-btn-spring` 而遗漏时长 Token,会得到一条静默失效的过渡。
|
||||
|
||||
---
|
||||
@@ -51,6 +51,7 @@
|
||||
- 4.1 纯 CSS 动效参数 (47 项 @keyframes cds-* 完整清单)
|
||||
- 4.2 原生组件命名空间 (40 大 data-cds 原生组件命名空间契约)
|
||||
- 4.3 响应时间与贝塞尔曲线 (5 阶时间 Token 60ms~450ms 与 3 大精确贝塞尔控制点)
|
||||
- 4.4 物理弹簧参数矩阵 (14 组 Framer Motion spring 实测调用点、`linear()` 弹簧曲线与时长 Token 的耦合失效)
|
||||
- [第五章:生产级参考实现代码](05-生产级参考实现代码.md)
|
||||
- 5.1 模型选择与算力调节器 (`ModelSelectorEffort.tsx`)
|
||||
- 5.2 输入框底部工具栏 (`ChatComposerChin.tsx`)
|
||||
|
||||
Reference in new issue
Block a user