mcpField
V
visant-motion

visant-motion

github.com/pedrojaques99设计与 UI8115 次安装1 天前更新
代码仓库

关于 visant-motion

面向界面开发者的动效设计 Skill,强调复用仓库现有的动效配置源,并提供关于时长、缓动、减少动态效果、浮层和等待状态的规则。

主要功能

  1. 01复用仓库动效配置
  2. 02按场景选择缓动和时长
  3. 03优先使用 transform 与 opacity 动画
  4. 04兼顾减少动态效果偏好
  5. 05设计加载与等待状态

适用场景

  1. 01审查界面动效行为
  2. 02基于共享配置实现过渡
  3. 03让浮层动效与触发器关联
  4. 04规划加载和等待反馈

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/pedrojaques99/visant-skills-public --skill visant-motion

安装后在 Claude Code 中输入/visant-motion确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

assets/motion-ssot-starter/
use-delayed-flag.ts
motion.ts
globals-tokens.css
references/
cases.md
motion-rules.md
scripts/
measure-motion.mjs
codemod-transition-all.mjs
codemod-duration.mjs
motion-lint.mjs
scripts/__fixtures__/
bad.tsx
bad.css
leak.css
suppressed.css
suppressed.tsx
scripts/__fixtures__/good/
good.css
good.tsx
guard.test.tsx

详细介绍

Visant Motion

构建动效自然得恰到好处的界面:每个过渡都有明确目的、响应迅速,感知上不会显得“慢”。总原则:以 SSoT 为准,绝不在组件中硬编码三次贝塞尔曲线或时长。

§1 和 §2 就是实际使用中所需的全部内容。 后续内容仅供特定场景参考;SSoT 初始化位于 §10,因为每个仓库只需处理一次(原因:references/cases.md)。

原则来源: Emil Kowalski 的 skills,https://github.com/emilkowalski/skills(`emil-design-eng`、`apple-design`、`improve-animations`、`review-animations`),提炼自 animations.dev 和 Apple 的 Designing Fluid Interfaces(WWDC 2018)。请查阅该仓库,了解上游的规范指南;本技能是 Visant 对其进行应用并统一到 SSoT 后的版本。

0. 先找到目标仓库的动效 SSoT

编写或审查任何动效之前,先找到仓库中动效的存放位置。查找以下内容:

  • 导出缓动函数和时长的 JS/TS 模块:搜索 EASE_OUT、cubic-bezier、DUR、transitions(常见名称:lib/motion.ts、src/lib/motion.ts、utils/animation.ts);
  • CSS 自定义属性:在全局样式表中搜索 --ease- 和 --dur-;
  • 已包含动效的共享原语:Button(按压反馈)、覆盖层封装(弹出层/下拉菜单/对话框)、Toast 库(sonner)、延迟加载器钩子。

找到了 → 使用它,并将下方的符号名称调整为对应名称,绝不另建一套系统。没找到 → §10。下文中的每个 lib/motion.ts / @/lib/motion 都表示“仓库的动效 SSoT”,无论其实际路径是什么。

1. 决策规则(简明版)

按以下顺序决定。完整数值目录和审查严重程度 → references/motion-rules.md。

  1. 是否应该添加动效? 每天发生 100 次以上(快捷键、切换、⌘K)→ 不要添加。偶尔发生(模态框、Toast)→ 使用标准动效。罕见场景(新手引导)→ 可以增添趣味。
  2. 缓动: 进入/退出 → EASE_OUT。屏幕内移动 → EASE_IN_OUT。悬停/颜色 → ease。匀速(跑马灯/闪烁/进度)→ linear。界面中绝不使用 ease-in,唯一例外见 §5 规则 2。
  3. 时长: 界面动效 < 300ms。按压 100-160 · 下拉菜单 150-250 · 模态框 200-300。路由过渡(§5)、营销、滚动和 3D 动效可以更长、更从容;时长上限主要针对响应点击的控件。
  4. 绝不使用 scale(0):从 scale(.95) + opacity 开始。
  5. 只使用 transform + opacity(GPU)。绝不对 width/height/margin/padding 添加动效。
  6. 仅在惯性/手势场景(拖动、快速滑动)使用 Spring。菜单淡入使用时长,不使用 Spring。
  7. 可中断 = CSS transition / Spring,而非 @keyframes(后者会从头开始)。

2. 易踩坑点(不太显眼,但都经过实战验证;写之前请先阅读)

  1. duration-[var(--dur-x)] 会触发构建“ambiguous”警告(Tailwind 无法判断这是 transition-duration 还是 animation-duration)。请使用明确指定属性的写法:[transition-duration:var(--dur-base)]。
  2. framer-motion 会覆盖 CSS transforms。 如果 motion.div 为 y/scale 设置了动画,Tailwind 的 hover:-translate-y-* 会被内联 transform 悄悄覆盖。解决方法:让 framer 负责 transform(使用 whileHover={{y:-2}} + whileTap),CSS 只处理阴影/边框。
  3. whileTap 需要单独设置 transition,否则会继承(较慢的)进入动画 transition:whileTap={{ scale:.985, transition:{ duration: DUR.press, ease: EASE_OUT } }}。
  4. 绝不要使用 transition-all:它会为意料之外的属性添加动画,导致卡顿。请限定范围:transition-colors、transition-[color,background-color,border-color,box-shadow,opacity] 或 transition-transform。使用 codemod(§7)扫描整个代码仓库。
  5. 让覆盖层根据原点变换: Radix 会暴露 --radix-<primitive>-content-transform-origin。添加 origin-[var(--radix-popover-content-transform-origin)](以及 dropdown-menu/select/context-menu/tooltip/hover-card),使其从触发元素的位置缩放。例外:模态框始终使用 transform-origin: center(不锚定到触发元素)。
  6. 减少动态效果是真正的信号,不代表“关闭”。 保留透明度/颜色变化,移除位移/弹簧效果。全局的 @media (prefers-reduced-motion) { *{…0.001ms} } 防护是低成本的安全措施。对于列表行的退出动画,这意味着该行仍会离开:将动画时长归零,而不是取消最终结果。
  7. 在打开的模态框后播放退出动画纯属浪费,还会拖慢关闭过程。 如果在对话框中完成某个条目,主要反馈是该行从列表中折叠消失,那么天真的顺序会先等待约 300ms 的退出动画(在该行设置 completing 标记),然后关闭模态框,导致折叠动画被模态框遮住,而模态框则空等约 300ms。应当先关闭界面:模态框的关闭本身就是有分量的退出动画,随后列表行会在视野中折叠消失。看板卡片切换列时也应跳过仪式流程(列切换本身就是退出动画)。每个界面只显示一个可见的退出动画,绝不同时播放两个。模式 → visant-frontend › 在退出仪式前关闭界面。
  8. 显得业余的是幅度,不是时长。 一个带大幅旋转、过冲和粒子云的删除动画被认为很业余;修正方案保留了时长,却缩小了幅度(旋转约 12°,行缩至 40%,邻近元素移动不超过 4px,消失元素不产生过冲)。原则:先把幅度减半,再减半;好的动态效果几乎不会被察觉。完整案例:cases.md。
  9. 每个界面只设置一个动态焦点。 队列在跳动、图片不断加载、导出百分比在增长、错误提示在抖动、Toast 在滑入——这些同时发生,会让人觉得信息过载。请按顺序呈现状态(已发送 → 生成中 → 就绪 → 导出中),确保同一时间只有一个元素在动。
  10. 初始状态就应是最佳状态。 只在悬停时出现的倾斜/光泽/透视效果,在触屏设备、网格缩略图或截图中都不存在。应从主视觉状态开始;悬停只会增强效果,而不是揭示效果。
  11. 缩放后的容器会让动态效果代码产生错误。 在 transform: scale 容器内(例如组件展示页、缩放预览)时,指针位移必须除以缩放比例,position: fixed 会以变换后的盒子为参考进行测量(请通过 portal 挂载到 body),R3F 画布则需要 resize={{ offsetSize: true }}。请在 scale(0.5) 下测试任何拖动操作。

2b. 声音(SFX):模拟设备点击声,仅在用户操作时播放

声音是耳朵感知的动效,也遵循相同的上限。项目的声音引擎是 sfx-bus(使用 Web Audio 合成,无文件、无请求;从 Visant 库 ui.visantlabs.com 安装:npx shadcn@latest add @visant/sfx-bus,该库还提供现成的动效组件:motion-layer、fold-motion、line-reveal、text-morph、count-up、toast-stack)。先检查目标仓库是否已有声音层。

  • 让每种音色都模拟真实的模拟设备,而非合成器音符。 使用高 Q 值带通滤波器过滤噪声脉冲(模态合成),用几个不同时间点的音效模拟机械装置。音色库:click(相机按钮)、detent(旋钮/选择器的定位齿)、toggle(带实体感的拨杆)、focus(镜头调焦环棘轮,用于连续拖动)、complete(快门上弦并释放,用于确认操作)、drop(指针落下/轻柔气流,用于强操作)。不要音高,不要尾音:带混响的有音高声音会让人厌烦。
  • 映射手势 → 音色: 离散步骤(旋钮、标签页、经过的 Dock 图标)→ detent;连续拖动(滑块)→ 每移动 1/20–1/40 行程播放一次 focus;开关 → toggle;按住/滑动至末端以确认 → complete。拖动时不能每移动一个像素就播放一次声音;用户花几分钟调整的参数曲线应保持静音。
  • 默认不在悬停时播放,仅在用户操作时播放。可行的例外:指针经过 Dock 图标时播放轻微的 detent,节流间隔约为 45ms。
  • 仅在触发后启用,不自动播放: 仅在真实手势触发后(navigator.userActivation)才创建 AudioContext;在 prefers-reduced-motion 下静音;用户保存的偏好设置优先;每种音色设置最小间隔;加入随机偏移/±6% 音高变化,避免连续十二次播放听起来像循环音效。仅在 (pointer: fine) 时启用;触屏设备上隐藏开关。
  • 音量: 每种音色的音量都应低于典型的 UI 点击声(约 -44 dBFS RMS);用 OfflineAudioContext 渲染并测量,在源码注释中用表格记录数值。

3. API(入门项目的 lib/motion.ts;映射为仓库自己的名称)

import { EASE_OUT, EASE_IN_OUT, EASE_DRAWER, DUR, transitions,
         fadeInUp, fade, itemEnter, pressable } from "@/lib/motion";
符号值用途
EASE_OUT[0.23,1,0.32,1]进入/退出,响应输入:默认值
EASE_IN_OUT[0.77,0,0.175,1]屏幕上的移动/形变(不用于进入/退出)
EASE_DRAWER[0.32,0.72,0,1]面板/抽屉(类似 iOS)
DUR{press:.12, fast:.16, base:.2, slow:.3}时长比例(秒)
transitions.*fast/base/slow/spring/springSoft现成的过渡对象
fadeInUp、fade变体(hidden/show)项目默认的入场动效
itemEnter(i, step=.04, cap=.4)(函数)列表项入场,短促且延迟有上限
pressable(对象)为可点击卡片/图块提供轻微的 whileTap 效果

CSS 写法(明确指定属性,见注意事项 #1):[transition-duration:var(--dur-base)] [transition-timing-function:var(--ease-out)]。

4. 等待也是一种界面(为每种待处理状态设计)

用户等待的每一刻都会有动效,无论你是否为它设计;光秃秃的转圈动画只是没人主动选择的默认方案。根据 §1 规则 1,等待也属于低频场景,因此真正值得投入惊喜设计的地方就在这里:插画、品牌角色、值得观看的循环动画。这是产品中唯一一个魅力不会干扰工作的地方。

按等待时长来选择,而不是按方便程度:

等待时长设计方式绝不要
< 100ms什么都不做:乐观写入 + 撤销提示转圈动画(只会闪一下)
100–300ms保持当前 UI;按下反馈已经给出响应将界面替换为加载器
0.3–1s在最终布局中显示骨架屏,再淡入内容转圈动画、内容到达时布局跳动
1–5s骨架屏 + 渐进式呈现;只有百分比真实时才使用确定进度条卡在 90% 的虚假进度
5s+用文字说明步骤 + 插画/循环动画,标明当前正在发生什么一分钟都不变的静态转圈动画
未知流式呈现部分内容:内容陆续到达就是加载器任何不确定进度指示
  1. 在服务端响应前,先响应点击。 按下反馈 + 立即改变状态,就是加载的第一帧。乐观更新 + 撤销(§6)胜过所有转圈动画;如果点击后界面就关闭,按钮内的转圈动画根本不会显示,所以只是无用的装饰:visant-frontend › 无法显示的转圈动画。
  2. 防闪烁规则: 约 300ms 前不要显示加载器;显示后至少保持约 500ms。闪一下就消失的加载器看起来像故障,而不是速度快。
  3. 骨架屏 ≈ 真实布局(相同的框大小、圆角和间距),这样内容出现时就不会发生位移。闪光效果使用 linear,无限循环,时长约 1.2–1.6s:保持持续动效(§1 规则 2)。这是应用中唯一合理的 @keyframes,因为它永远不会被打断。
  4. 绝不要直接将骨架屏替换为内容。 在 DUR.base 时长内淡入淡出;只让第一个区块稍微沿 y 轴移动,其他内容早已就位。
  5. 空状态也是一种等待,是在等用户的第一份数据。它值得同样的设计:插画加上结束等待的唯一操作。(是否值得占用空间,由 visant-frontend 决定,不属于本技能的职责。)
  6. 减少动态效果: 闪光/脉冲效果应降级为静态色调或缓慢的透明度渐变。进度读数应保留:它传达的是信息,不是装饰。
  7. 每个界面只使用一种等待动画。 骨架屏行 + 转圈动画 + 进度条,合起来是在回答同一个问题的三种方式。
  8. 不要让入场动画的耗时超过等待本身。 获取耗时 200ms,却用 400ms 呈现内容,等于自己制造了延迟。
  9. 等待会以结果结束:失败分支也要设计。 骨架屏 → 错误,这是最容易被忽略、冲击也最大的过渡:骨架屏已经让用户期待内容 3s 了。不要折叠布局;在原位置将骨架屏替换为错误提示,保留容器,并在其中放置重试操作。使用相同的淡入淡出,不要弹跳,也不要闪红。错误是信息,不是惊吓。

长时间任务的进度条、仪表盘和迷你折线图读数由 visant-refine 绘制;本节只规定它们的时机。

5. 导航与共享元素(路由之间的过渡)

<300ms 的上限适用于响应点击的控件。路由过渡则是另一回事,也是决定 Web 应用是否仍像一个 Web 应用的关键。

不要从头开始写。 Vercel 将这些模式和完整 CSS 作为官方技能维护;安装并遵循它来了解如何实现:

npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-react-view-transitions

本节说明何时使用以及项目特有的差异。四种模式,各自向用户传达不同含义:

模式传达的含义适用场景
共享元素变形(<ViewTransition name>)“同一个对象,深入一层”缩略图 → 详情、行 → 记录页面
Suspense 显现(边界上的 enter/exit)“数据已到达”§4 的骨架屏交接,应用于路由尺度
定向滑动(<Link> 上的 transitionTypes)“前进 / 后退”存在需要表达的真实层级关系
同路由交叉淡化(key={slug})“同一位置,不同内容”标签页、筛选器、时间段切换

项目规则:

  1. 时长定义在 --dur-vt-* 中,绝不内联:退出 150 · 进入 210(在退出后延迟开始)· 移动 400。这种不对称是刻意的:旧视图快速离开,因为它已经落了下风;新视图则从容进入,让人能够阅读。
  2. “绝不使用 ease-in”的唯一例外是 ::view-transition-old。 退出时没有任何内容需要等待,因此让它前段静止不会带来损失。其他地方仍一律禁用;motion-lint 正是按此例外配置的。
  3. **每一对具名元素都设置 default="none",**否则每个具名的 ViewTransition 都会在无关的过渡中播放动画。另一个容易踩的坑:设置 default="none" 后,如果没有显式指定 share,变形就会悄然失效。
  4. 固定一个参照物。 页头不能滑动。没有固定的参照物,用户会觉得“整个视口都跳了”,而不是“内容变了”。
  5. 包装器放在 page.tsx 中,绝不要放进 layout(Next App Router):layout 会在导航过程中持续存在,因此其中的进入 / 退出动画永远不会触发。手动排查时,可能要花一小时才发现这一点。
  6. ::view-transition { pointer-events: none } 已包含在起始 CSS 中。没有它,用户在 400ms 的过渡期间点击时,点击事件会悄然被吞掉。
  7. 浏览器后退不会携带过渡类型。 不要编码只对自有链接成立的含义。
  8. 减少动态效果需要单独的规则:全局 * 防护不会作用于视图过渡伪元素(它们附着在文档上)。定向滑动最容易引发晕动;这一条防护绝不能跳过。它也已包含在起始 CSS 中。
  9. 并非每种变形都是导航。 行重排、卡片换列、项目在列表间移动:这些都没有发生导航,因此 View Transitions 并不适用。这里应使用 framer-motion 的 layout / layoutId(它使用 FLIP,因此尺寸变化通过 transform 执行,也符合 §1 规则 5)。有两点需要注意:长列表上的 layout 会在每次提交时重新测量每个子元素,因此应将其限制在实际移动的元素上;而 layoutId 对必须挂载在同一个 AnimatePresence 中,否则变形会退化为普通交叉淡化。经验法则:路由变了 → View Transitions;DOM 在原地重排 → layout。

滚动驱动的动态效果: 优先使用原生 animation-timeline: scroll() / view(),而不是滚动库:它在主线程之外运行,因此不会造成卡顿。将它放在 @supports (animation-timeline: view()) 条件下,并将 JS 路径作为回退方案,而非默认方案。滚动和营销动态效果可以持续较长时间并经过精心设计;它们经过审查并予以保留,不属于缺陷。

6. 优先复用,再重建

在目标仓库中,编写任何新内容前先查找以下内容(真实应用的完整示例见 cases.md):

  • 共享 Button 的按压反馈(active:scale-[0.97] 或类似效果)。如果已有,所有按钮都已具备此效果;不要在每个按钮上重复添加。如果没有,就在共享基础组件上添加一次。
  • 能够识别来源的共享覆盖层。 新建自定义覆盖层?应用相同的 Radix 变量。
  • 反馈模式(通常是乐观 UI + sonner 之类的 toast + 撤销)。复用它;不要另行设计模态确认。
  • 任何完成流程(庆祝 → 行退出 → 乐观更新 → 撤销)。搜索负责该流程的钩子(use*Actions、useComplete*、列表背后的 mutation 钩子)。需要完成或处理某个条目的新界面,应在相同的数据键上调用该钩子;不要重新实现整个流程,否则计数会悄悄出现偏差。高频子值(例如运行中的时钟)应放在自己的 context 中,避免流程的使用者每秒都重新渲染:visant-frontend › 将每秒变化的值拆分到独立的 context 中。

7. 工具:先 lint,再用 codemod

motion-lint.mjs:严重性表,可执行

只有在有工具强制执行时,审计规则才真正有效。scripts/motion-lint.mjs 将 HIGH/MEDIUM 目录作为 12 条正则规则运行,并提供文件:行号、原因和修复建议。无需依赖项,也无需配置。

node scripts/motion-lint.mjs                     # human output, cwd
node scripts/motion-lint.mjs src app --strict    # fail on MEDIUM too
node scripts/motion-lint.mjs --json              # for CI or an agent

遇到任何 HIGH 时退出码为 1(--strict 还会将 MEDIUM 纳入其中),因此可以直接接入 pre-push 钩子或 CI。它了解真正的例外情况:::view-transition-old 上的 ease-in、超过 300ms 的 --dur-vt-*、SSoT 文件本身中的 cubic-bezier,以及导入了 useDelayedFlag 的文件中的加载器。如果某条规则确实不适用于某一行(例如较长且刻意设计的营销动画或滚动动画),请附上原因抑制该规则:

// motion-lint-disable-next-line slow-ui-duration  scroll-driven hero, deliberate

规则:ease-in-on-ui · scale-zero · transition-all · slow-ui-duration · hardcoded-curve · hardcoded-duration · layout-property-animation · background-position-shimmer · whiletap-inherits-transition · loader-without-guard · fake-progress · popover-origin。

修改任何规则后,都要运行 node scripts/motion-lint.mjs --self-test。 它会检查正则规则集随时间推移容易悄悄失效的三种情况:每条规则仍会在 scripts/__fixtures__/bad.* 中触发,干净的 fixture 仍报告零项(没有误报),并且抑制仍然有效。如果某条规则悄悄停止匹配,后果比没有规则还糟:审计结果会显示通过。每新增一条规则,都要添加一个 fixture 用例;如果某条规则没有对应用例,自测就会失败。

--fix:一条命令,完成机械式处理
node scripts/motion-lint.mjs --fix <target>   # runs both codemods below

实际审计中的大部分问题不需要判断(有一轮审计中,55 个问题里有 48 个是需要替换为令牌的内联时长)。--fix 是入口;哪些内容适合安全改写,由 codemod 决定。它会刻意保留以下内容不动:UI 上的 ease-in、scale(0)、无法识别来源的弹出层、闪烁的加载器、未设计过的等待时长。这些属于设计决策,而不是文本替换。

codemod-duration.mjs:将内联时长替换为 DUR.*
node scripts/codemod-duration.mjs                 # dry-run: the plan, in three buckets
node scripts/codemod-duration.mjs --apply         # exact + near
node scripts/codemod-duration.mjs --apply --over  # also >300ms (read the list first)

分成三类,因为这些改写并不等价。EXACT:数值本身就是某个令牌(0.2 → DUR.base);纯粹重命名。NEAR:与某个令牌相差不超过 25%,且低于 300ms 上限(0.25 → DUR.base)。OVER:超过 300ms(0.6 → DUR.slow),这会将动画时长减半:这是行为变更,而非重构,因此需要使用 --over 并检查列表。路由过渡和营销动画都是合理的例外,只有观察屏幕的人才能判断是否适用。

与所有令牌都相差较远的数值会标记为 UNMAPPED,而不是猜测:0.08 可能是刻意设计得更干脆的按压效果,悄悄将它改为 DUR.press 会抹去这项设计决策,而不是记录它。添加导入时,会沿用仓库本身已有的 SSoT 导入路径。

codemod-transition-all.mjs:处理第二大的问题

scripts/codemod-transition-all.mjs 会将 transition-all 改写为最精简且正确的目标;跳过在交互状态下会为 transform 设置动画的元素,并排除共享的 ui/** 基础组件。带保护措施、可幂等运行,并先进行试运行:

node scripts/codemod-transition-all.mjs          # dry-run: prints the plan + skips
node scripts/codemod-transition-all.mjs --apply  # writes changes

应用前,根据目标仓库的布局调整顶部的 ROOTS / isUi 常量。之后务必运行 tsc、测试和构建。

8. 体验检查:先测量 URL,再用眼睛判断

先在部署后的 URL 上进行测量。 源码扫描在卡顿页面上也可能顺利通过;只有运行中的页面才能说明真相。

node scripts/measure-motion.mjs https://app.example.com
node scripts/measure-motion.mjs <url> --click "[data-testid=row],#open"
node scripts/measure-motion.mjs <url> --reduced-motion --no-inp

它会驱动页面(悬停 → 点击 → 按 Escape → 滚动),并报告设备实际执行的情况:

  • INP 及其归因信息,通过 web-vitals 获取。查看细分结果:presentation 占主导,说明成本来自动画;processing 占主导,则说明成本来自处理程序。问题不同,修复方式也不同。
  • 长动画帧(Chromium 123+),附带逐脚本归因信息:哪一帧被阻塞,以及哪个函数造成了阻塞。这就是用户感受到却从不报告的卡顿。
  • 减少动态效果审计: 强制启用 reduce 后,仍运行超过 100ms 的内容都会列为泄漏。这项检查没人会手动做,也是起始 CSS 带有单独的视图过渡保护规则的原因。

超过预算(--budget-inp 200、--budget-blocking 100)时退出码为 1,因此可用于阻止部署。

然后用眼睛判断;数字做不到这一点。打开 DevTools → Animations → 25% speed:按下时是否立即缩小?弹出层是否从触发元素处展开?交叉淡化期间是否没有两个重叠状态?交错动画读起来是否像一段简短的级联,而非逐个播放?第二天再用全新的视角审查一次。

9. 审计代码库

先看 §0(找到 SSoT),然后运行 node scripts/motion-lint.mjs --json(§7):它会为你完成整个 grep 扫描,并附上严重程度和修复建议。接着测量部署后的 URL(§8)。只有这些都完成后才阅读代码,而且只读工具指出的部分。

有两项 lint 无法发现,因此需要手动审计:等待状态(列出每个 isLoading/isPending/Suspense/空状态分支,并询问用户在每种情况下看到的是什么,§4),以及导航,其判断依据是过渡是否表达了真实含义,而不是源码中的任何模式(§5)。然后阅读 references/motion-rules.md,了解审计类别、HIGH/MEDIUM 严重程度标准和具体修复方法。按杠杆率(影响 ÷ 工作量)报告问题;优先修复核心/共享基础组件(它们会影响每条路由)。长时间、刻意设计的营销、滚动或 3D 动画应经过审计后保留,而不是标记为问题。

10. 初始化 SSoT(仅当 §0 未找到时)

动效集中在一个地方,并在 JS 和 CSS 之间保持镜像,以确保 framer-motion 与 Tailwind/Radix 始终同步。将文件放在仓库存放共享库的位置(检查 tsconfig.json 中的 @/ 别名):

  • JS: assets/motion-ssot-starter/motion.ts → 例如 lib/motion.ts(缓动函数、DUR、变体,以及 §4 中的 WAIT 阈值)。
  • CSS: 将 assets/motion-ssot-starter/globals-tokens.css 合并到应用全局样式表的 :root 和 @layer utilities 代码块中。它提供 .skeleton 闪烁效果、路由过渡时长比例 --dur-vt-*,以及全局 * 规则无法覆盖的两项减少动态效果保护规则。
  • 等待: assets/motion-ssot-starter/use-delayed-flag.ts → 例如 lib/use-delayed-flag.ts。应用中的每个加载器都通过它实现(§4.2),这样防闪烁组合逻辑就是共享基础组件,而不是让每位开发者各自重新实现并犯错。

如果仓库已有等效方案,请使用它;不要另建一套系统。node scripts/motion-lint.mjs(§7)用于确保 SSoT 真正保持单一,而不只是口头规定。

评论

支持 Markdown