关于 cursor-animated-sites
用于制作自包含 HTML 交互演示的技能,通过动画光标逐步浏览文件夹树,讲解生命周期、顺序或数据流。它基于 `/docs-site-macos`,提供手动步进、自动播放、说明文字和按角色着色的节点。
主要功能
- 01单文件 HTML 演示
- 02动画光标与文件夹树
- 03手动步进和自动播放模式
- 04按说明文字节奏显示内容
- 05按角色着色节点并标注顺序
- 06支持减少动态效果偏好
适用场景
- 01讲解从安装到运行的生命周期
- 02展示操作读取或写入哪些文件
- 03逐步呈现一个处理过程
- 04制作保留完整最终画面的演示
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rheinmir/ setup --skill cursor-animated-sites安装后在 Claude Code 中输入/cursor-animated-sites确认已加载。
详细介绍
技能:cursor-animated-sites
构建一个单文件、自包含的 HTML演示页面,逐步展示光标在文件夹树上移动的动画。这是叠加在 /docs-site-macos 之上的一层——复用该技能的玻璃设计系统(背景平面、玻璃层级、滚动条、字体、波纹、输出路径
llmwiki/html/DDMMYY-<slug>.html、自动托管)。此技能新增交互式演示。
同样继承 docs-site-macos 的无障碍与文档头部部分,包括
<meta viewport>+<title>/description、内联 favicon、⏮/⏭/▶ 控件上的:focus-visible焦点环,以及全局prefers-reduced-motion防护(规则 11 已跳过演示动画;该防护还会让光标和打字机动画静止)。请勿在缺少 viewport meta 的情况下发布页面,否则响应式网格会在移动设备上错位。
内容
目的与背景
- 目的: 构建一个单文件、自包含的 HTML 演示页面(叠加在
/docs-site-macos之上),展示边栏光标逐步从控制器移动到文件;创建文件时,文件内容会逐字显示;节点按角色着色,并提供手动步进和自动播放模式,以及逐帧说明。 - 触发条件(何时使用):
- 解释一个生命周期(安装 → 配置 → 运行)、一个序列或一条数据流。
- 任何“X 是如何逐步运作的”场景,只要观看者需要看清每个操作会读取/写入哪个文件以及操作顺序。
- 触发词:“hoạt họa”、“con trỏ chạy”、“life cycle”、“từng bước”、“transcript”、“minh hoạ cây thư mục”、“/cursor-animated-sites”。
- 非目标: 不重新定义玻璃设计系统(背景平面、玻璃层级、滚动条、字体、波纹、输出路径、自动托管)——复用
/docs-site-macos;此技能只新增交互式演示。
心智模型
STEPS[] (ph · sp · h · d · flow · chain · role · marks · leak · verdict) + 树行(data-born)→ prep(每步强调色、按 born 控制可见性)→ animate(说明文本记录 = 主时钟、born 行逐字显示、光标按顺序访问 chain、角色颜色 + 序号徽标)→ rest = 完整最终帧。
输入与输出约定
| 字段 | 必填? | 含义 | |
|---|---|---|---|
| 输入 | 要解释的生命周期/序列 | 是 | 步骤的真实顺序,以及每步读取/写入哪些文件 |
| 输入 | 文件夹树(文件 + 参与者) | 是 | 每个文件都有一个创建步骤 |
| 输出 | llmwiki/html/DDMMYY-<slug>.html | 是 | 单文件,基于 docs-site-macos,自动托管 |
| 输出 | 草稿输出报告 + 索引 + 日志 | 是 | OKF YAML frontmatter |
规则和能力
来之不易的规则(提炼版——不要重新摸索)。这些是从实际使用中总结出的修正。违反它们会让页面难以理解:
- RULE-01(必须):光标位于左侧边栏中,绝不能覆盖内容。 它是一个向右的小箭头,位于
x = rowLeft − ~22px,主要沿垂直方向移动。将鼠标指针图标放在行上会遮住文字,看起来杂乱。→ 在边栏中使用▶样式的箭头。平滑移动:left和top必须使用相同的持续时间和缓动(例如.5s cubic-bezier(.45,.05,.2,1))——时间不匹配(例如 left 为 .32s、top 为 .5s)会导致对角移动卡顿,因为 x 会先于 y 到位。添加will-change:left,top。 - RULE-02(必须):为每个访问过的节点编号(内嵌序号标记)。 暂停时,观看者仍必须能看出顺序 ①②③。仅靠移动光标,在画面冻结时无法辨认。
- RULE-03(必须):默认静止状态 = 当前步骤的完整最终画面,而不是空白的首帧。 加载时显示完整画面(所有节点均已命中,说明文字完整)。绝不要显示什么都没有的空白“第 0 帧”。
- RULE-04(必须):文件创建之前并不存在。 在文件“诞生”步骤之前,它应为
display:none(从树中消失——不是淡化或半透明显示)。它应当恰好在创建它的步骤中出现(逐字输入)。树应随时间逐渐填满。明确保留一个空白的“安装前”步骤,以便确实呈现空状态。 - RULE-05(必须):呈现方式要像文字记录,而不是复制粘贴式弹出。 新文件行应以裁剪式打字机效果显示;说明文字应逐字呈现。文字记录是主时钟——画面应以文字记录的速度推进,而不是按固定计时器推进。
- RULE-06(必须):放慢速度。 阅读速度比动画效果更重要。约每字符 30ms,画面结束时停留较长时间。速度太快会让人放弃观看。
- RULE-07(必须):按步骤内的角色着色,不要所有内容都用同一种颜色。 最容易造成混淆的是:复制来源和复制目标染成相同颜色 → 无法分辨方向。来源 = 蓝色“读取”,目标 = 青绿色“写入/创建”,传递 = 灰色,拒绝 = 红色,成功 = 绿色,泄漏 = 琥珀色。行上也要添加文字标签。
- RULE-08(必须):每个步骤的强调色只用于左侧列表和说明文字标签(以便区分列表中的步骤)——右侧节点使用角色颜色。两套配色,各司其职;不要混用在同一个元素上。
- RULE-09(必须):画面下方的说明文字不得重复左侧栏内容。 左侧 = 精简导航(标签 + 标题)。右侧说明文字 = 当前画面的详细描述和流程。完整重复会让人觉得两者都是多余内容。
- RULE-10(必须):左栏要窄,把空间留给动画。
grid-template-columns: minmax(190px,.5fr) 1.5fr。 - RULE-11(必须):两种模式:“逐步播放”(⏭ 播放一帧的文字记录后即停止)和“自动播放”(自动推进,速度较慢)。默认 = 暂停/手动。遵循
prefers-reduced-motion(跳过动画,显示完整画面)。 - RULE-12(必须):说明文字应使用通俗语言,绝不使用行话或过于简略的表达。 每帧的说明(
d)都必须写成完整句子,让项目之外的人也能看懂。每个术语首次出现时都要展开说明:“hook” → “运行时在每次操作前执行的步骤”;“R1/R2” → 用实际措辞说明规则(“禁止写入 raw/ 的规则”);“exit 2 / exit 0” → “代码 2 = 阻止 / 代码 0 = 通过”;“断言” → “要求结果必须为 X 的检查”;“layer=repo” → “在提交/PR 时针对已更改文件运行的检查”。说明读取了什么、写入了什么,以及原因。像“PreToolUse → LÕI → R1 → exit 2”这样简短的符号式说明晦涩难懂,观看者会直接跳过——请写成完整句子;简短的flow行可以保留简写。(d较长时,应降低每字符的打字速度,约 18ms,以免播放太拖沓。) - 能力:阅读
/docs-site-macosskill;编写一个 HTML 文件以及 draft/index/log;运行本地 HTML 解析检查,并通过 auto-host server 打开页面。
失败边界
- 页面缺少 viewport meta → 移动端响应式网格会错乱:失败,不可发布。
- HTML 解析检查报错 → 失败,托管前修复。
- 暂停时画面无法读懂(没有 seq 徽标、帧 0 为空、说明文字重复左侧栏内容)→ 不符合 RULE-02/03/09,判定为 失败;修复后重新逐帧检查。
HOW
主工作流
| 步骤 | 类型 | 输入 | 操作 | 输出/退出 | 失败/下一步 |
|---|---|---|---|---|---|
| W01 | effect | docs-site-macos 约定 | 生成基础页面 | glass 基础页面,位于 llmwiki/html/DDMMYY-<slug>.html | — |
| W02 | effect | 基础页面 | 将 body 替换为 hero + controls + lifecycle + legend | 结构 | — |
| W03 | judgment | 实际文件列表 | 编写带有 data-born + seqslot 的树行和 actor 芯片 | 树 | — |
| W04 | judgment | 生命周期 | 编写 STEPS[],先添加空的 "before" 步骤,再添加角色、verdict/leak/marks | 数据 | — |
| W05 | effect | 参考引擎 | 加入引擎 JS + CSS 增量;默认暂停/静态 | 可运行页面 | — |
| W06 | deterministic | 页面 | 验证:HTML 解析 + 暂停时点击 ⏭ 遍历所有步骤 | 每一帧都清晰易读 | 无法读懂 → 修复,重复执行 W06 |
各步骤的详细说明(W01–W06 的权威依据):
- 按照
/docs-site-macos约定生成基础页面(glass CSS、字体、滚动条、背景平面,输出到llmwiki/html/DDMMYY-<slug>.html,并自动托管)。 - 将 body 替换为上述
hero + controls + .lifecycle(left steps / right tree-pane) + legend结构。 - 编写文件夹树:每个文件对应一个
.trow id="f-*" data-born="…",并以<span class="seqslot"></span>开头;.controllers中也要添加 actor 芯片。 - 编写
STEPS[]:按真实生命周期排序,明确先添加一个空的 "before" 步骤;为每个节点标注role(src/dst);在适用时添加verdict/leak/marks。 - 加入引擎 JS + CSS 增量。默认暂停/静态;⏭ 前进一步,▶ 缓慢自动播放。
- 验证:
python3 -c "import html.parser; html.parser.HTMLParser().feed(open(F).read())";通过自动托管服务器打开页面;点击 ⏭ 遍历每个步骤,并检查暂停时每一帧是否清晰易读。
分支
| ID | 类型 | 条件 | 行为 | 跳过/失败 | 汇合 |
|---|---|---|---|---|---|
| B01 | conditional_required | prefers-reduced-motion: reduce | 跳过动画,显示完整帧(RULE-11) | — | W06 |
| B02 | user_optional | 观看者按下 ▶ "自动播放" | 缓慢自动推进;⏭ 播放一帧后停止 | 默认 = 暂停/手动 | W06 |
验证与停止
确定性检查:HTML 解析。需要人工检查:点击 ⏭ 遍历每个步骤,并确认暂停时每一帧都清晰易读。每一帧都通过后停止;然后撰写输出报告。
示例
- 正例:“制作安装 harness 的生命周期动画页面” → STEPS 以空的 "before install" 开始;步骤 1 的光标依次访问
c-install → f-policy → f-cli,f-policy以青绿色显示“写入/创建”;暂停帧显示 ①②③ 徽标和完整说明文字。 - **边界/失败:**源行和目标行都染成步骤强调色 → 观看者无法分辨复制方向 → 违反 RULE-07/08;将右侧面板节点改为 ROLE 颜色(源为蓝色“读取”,目标为青绿色“写入/创建”),并添加文字标签。
参考 — 页面结构
hero (title + 1-line how-to-read)
controls: ⏮ ⏭ Bước tiếp ▶ Tự chạy <hint> <phase chip> <n / N>
.lifecycle (grid: narrow left | wide right)
├─ .steps (LEFT) — compact cards: <span class=sp>label</span><h3>title</h3> (NO paragraph)
└─ .tree-pane (RIGHT, sticky glass)
├─ chrome dots + title
├─ .controllers — actor chips (installer / agent / git / CI), each with a .seqslot
├─ .tree — rows with id=f-*, data-born="<step|−1 pre-exist|99 never>", leading .seqslot
├─ .frame-cap — fcNum / fcSp / fcH / fcD(streams) / fcFlow (the per-frame caption)
├─ .cursor — gutter arrow (fill=currentColor)
└─ .verdict + .leak — floating tags on the RIGHT side near the active row
legend: đọc(blue) · ghi(teal) · cho qua(green) · chặn(red) · lọt(amber)
参考 — STEP 数据模型
const STEPS = [
{ ph:'install'|'run', // phase (left chip text)
sp:'CÀI · B0', // short label
h:'Đặt lõi vào repo', // title (left + caption)
d:'... mô tả đầy đủ ...', // detailed narration → ONLY in right caption, streams
flow:'installer → policy.yaml', // 1-line data-flow summary
chain:['c-install','f-policy','f-cli'], // node ids the cursor visits IN ORDER
role:{'f-policy':'dst','f-cli':'dst'}, // per-node role: 'src'|'dst' (pass/actor inferred)
marks:{'f-x':'warn','f-y':'deny'}, // optional: override role on specific nodes
leak:'f-wiki', // optional: node that "leaked" (amber tag)
verdict:{kind:'deny'|'ok'|'neutral', text:'exit 2 · CHẶN'} }, // optional: end badge
];
每个文件行上的 data-born:文件创建时所在的步骤索引(-1 = 已存在的供应商文件,99 = 从未创建,例如被阻止的写入尝试)。born === cur 的文件在此步骤中逐字显示;born < cur 的文件已存在;其他文件隐藏。
参考 — 核心 JS(引擎 — 请忠实复现)
const PAL=[{c:'#0a84ff',bg:'rgba(10,132,255,.12)'},{c:'#30b0c7',bg:'rgba(48,176,199,.13)'},
{c:'#5856d6',bg:'rgba(88,86,214,.12)'},{c:'#34c759',bg:'rgba(52,199,89,.14)'},
{c:'#ff9500',bg:'rgba(255,149,0,.15)'},{c:'#ff2d55',bg:'rgba(255,45,85,.12)'}]; // màu mỗi step
const ROLEINFO={src:{c:'#0a84ff',t:'đọc',cls:'r-src'}, dst:{c:'#30b0c7',t:'ghi/tạo',cls:'r-dst'},
pass:{c:'#9aa3b2',t:'',cls:'r-pass'}, actor:{c:'#9aa3b2',t:'',cls:'r-pass'},
neutral:{c:'#0a84ff',t:'',cls:'r-pass'}, deny:{c:'#e0264b',t:'chặn',cls:'deny'},
ok:{c:'#34c759',t:'cho qua',cls:'ok'}, warn:{c:'#f0902a',t:'lọt',cls:'warn'},
attempt:{c:'#e0264b',t:'',cls:'deny'}};
let cur=0, auto=false, timers=[];
const clear=()=>{timers.forEach(clearTimeout);timers=[]};
// con trỏ ở GUTTER trái (x = rowLeft − 22), y = giữa dòng
const at=id=>{const el=document.getElementById(id),pr=pane.getBoundingClientRect(),r=el.getBoundingClientRect();
return {gx:Math.max(2,r.left-pr.left-22), y:r.top-pr.top+r.height/2, el};};
// hiện nếu pre-existing(-1) hoặc đã tạo ở bước TRƯỚC (b<i); born===i sẽ "gõ" ra trong bước này
function applyVis(i){document.querySelectorAll('.trow[data-born]').forEach(e=>{
const b=+e.dataset.born; e.classList.toggle('notyet', !((b===-1)||(b!==99&&b<i)));});}
function prep(){ const s=STEPS[cur];
/* left active card, prog, phase, caption (fcNum/fcSp/fcH/fcD=full/fcFlow) */
const a=PAL[cur%PAL.length], rs=document.documentElement.style;
rs.setProperty('--ac',a.c); rs.setProperty('--acbg',a.bg); // accent PER-STEP (trái + chip)
document.querySelectorAll('.trow,.chip').forEach(e=>e.classList.remove('hit','deny','ok','warn','attempt','born-now','r-src','r-dst','r-pass'));
document.querySelectorAll('.rtag').forEach(t=>t.remove());
document.querySelectorAll('.seqslot').forEach(s2=>{s2.textContent='';s2.className='seqslot';});
applyVis(cur); return s; }
function paint(idx,s){ const id=s.chain[idx], last=idx===s.chain.length-1, vk=s.verdict?.kind||'';
const el=document.getElementById(id); el.classList.remove('notyet'); const p=at(id);
let role = id==='f-raw' ? 'attempt'
: (s.marks&&s.marks[id]) || (last&&vk) || (s.role&&s.role[id]) || (id.indexOf('c-')===0?'actor':'pass');
const ri=ROLEINFO[role]||ROLEINFO.pass;
el.classList.add('hit'); if(ri.cls)el.classList.add(ri.cls); if(id==='f-raw')el.classList.add('attempt');
const slot=el.querySelector('.seqslot'); if(slot){slot.textContent=idx+1; slot.className='seqslot on '+(ri.cls||'');}
if(ri.t && id.indexOf('c-')!==0){const tag=document.createElement('span');tag.className='rtag '+ri.cls;tag.textContent=ri.t;el.appendChild(tag);}
cursor.style.left=p.gx+'px'; cursor.style.top=(p.y-9)+'px'; cursor.style.color=ri.c; // cursor đổi màu theo vai trò
/* leak tag + verdict tag positioned on the RIGHT side near p.y */ }
function revealBorn(i){document.querySelectorAll('.trow[data-born]').forEach(e=>{if(+e.dataset.born===i)e.classList.remove('notyet');});}
function typeRow(e,dur){e.classList.remove('notyet'); if(dur<=0)return; e.style.setProperty('--td',dur+'ms');
e.classList.add('typing'); timers.push(setTimeout(()=>e.classList.remove('typing'),dur+40));}
function typeText(el,txt,per){el.textContent='';el.classList.add('streaming');let i=0;
const tick=()=>{el.textContent=txt.slice(0,i); i++<txt.length?timers.push(setTimeout(tick,per)):el.classList.remove('streaming');};tick();}
function full(){const s=prep(); revealBorn(cur); s.chain.forEach((id,idx)=>paint(idx,s));} // TĨNH = frame đầy đủ
function animate(){const s=prep();
if(reduce){revealBorn(cur); s.chain.forEach((id,idx)=>paint(idx,s)); return;}
const per=30, capDur=Math.max(1600, s.d.length*per); // transcript = đồng hồ; CHẬM
typeText(document.getElementById('fcD'), s.d, per);
const born=[...document.querySelectorAll('.trow[data-born]')].filter(e=>+e.dataset.born===cur);
let bt=400; const slot=born.length?(capDur*0.55)/born.length:0;
born.forEach(e=>{timers.push(setTimeout(()=>typeRow(e,Math.min(slot*0.8,700)),bt)); bt+=slot;});
const n=Math.max(1,s.chain.length);
s.chain.forEach((id,idx)=>timers.push(setTimeout(()=>paint(idx,s), 300+Math.round(idx*(capDur-300)/n))));
if(auto) timers.push(setTimeout(()=>step(cur+1), capDur+2200)); // CHỈ tự sang khi auto
}
function step(i){clear(); cur=(i+STEPS.length)%STEPS.length; animate();} // Tự bước: gõ 1 frame rồi dừng
// prev/next: auto=false; step(±1). play: auto=!auto; auto?step(cur):full().
// LOAD: requestAnimationFrame(()=>setTimeout(()=>{cur=0;full();},200)); // mặc định TĨNH frame đầy đủ
参考 — 必要的 CSS 增量(除 docs-site-macos 外)
.lifecycle{display:grid;grid-template-columns:minmax(190px,.5fr) 1.5fr;gap:22px;align-items:start}
.steps{max-height:74vh;overflow-y:auto} .step{opacity:.62} .step.active{opacity:1;border-left:3px solid var(--ac)}
.tree-pane{position:sticky;top:18px} .trow{display:flex;align-items:center;white-space:pre;border-radius:8px;padding:5px 8px}
.seqslot{display:inline-flex;align-items:center;justify-content:center;width:17px;height:17px;margin-right:8px;border-radius:50%;font-size:10px;font-weight:800;background:transparent;color:transparent;border:1px solid rgba(30,90,170,.1)}
.seqslot.r-src{background:#0a84ff;color:#fff;border-color:transparent}
.seqslot.r-dst{background:#30b0c7;color:#fff;border-color:transparent}
.seqslot.r-pass{background:#9aa3b2;color:#fff;border-color:transparent}
.trow.hit.r-src{background:rgba(10,132,255,.13);box-shadow:inset 0 0 0 1px #0a84ff}
.trow.hit.r-dst{background:rgba(48,176,199,.17);box-shadow:inset 0 0 0 1px #30b0c7}
.trow.hit.r-pass{background:rgba(140,150,165,.12);box-shadow:inset 0 0 0 1px rgba(120,130,145,.5)}
.rtag{margin-left:9px;font-size:10px;font-weight:700;padding:1px 8px;border-radius:11px}
.rtag.r-src{background:rgba(10,132,255,.14);color:#0a84ff} .rtag.r-dst{background:rgba(48,176,199,.2);color:#1d7d8c}
.trow.notyet{display:none} /* CHƯA tạo → ẩn hẳn (không mờ) */
.trow.typing{overflow:hidden;animation:typeline var(--td,500ms) steps(30) both}
@keyframes typeline{from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0 0 0)}}
.cursor{position:absolute;z-index:7;will-change:left,top;color:var(--ac);
transition:left .5s cubic-bezier(.45,.05,.2,1),top .5s cubic-bezier(.45,.05,.2,1),color .3s ease} /* MƯỢT: left+top CÙNG duration+easing, đừng lệch */
.cursor svg path{fill:currentColor} /* arrow color = currentColor (set per role) */
.fc-sp{background:var(--acbg);color:var(--ac)} /* caption chip = per-step accent */
交付 — 输出报告
将 propose 草稿写入 llmwiki/wiki/sources/draft/DDMMYY-<ten>.md,并追加到 wiki/index.md + wiki/log.md。
使用 YAML frontmatter(符合 OKF v0.1 标准 — 不要使用粗体 **Type:**,否则会导致 OKF 检查失败):
---
type: draft
title: DDMMYY-<ten>
status: proposed
tags: [cursor-animated-sites, output-report]
timestamp: YYYY-MM-DD
---
# DDMMYY-<ten>
## What
<một câu — trang này dựng/quyết định cái gì>
## Files
| File | Action |
|------|--------|
| `llmwiki/html/DDMMYY-<ten>.html` | created |
## Origin
- **Draft:** `wiki/sources/draft/DDMMYY-<ten>.md`
- **Commit:** _(verify-before-commit điền)_
来源
- 由
/orca-eval从构建llmwiki/html/250626-harness-lifecycle.html的会话中提炼而来。 - 基础主题:
/docs-site-macos(~/.claude/skills/docs-site-macos/SKILL.md)。