diff --git a/config/external-sources.lock.json b/config/external-sources.lock.json index 6cc056f1..858a9559 100644 --- a/config/external-sources.lock.json +++ b/config/external-sources.lock.json @@ -60,8 +60,8 @@ "repo": "https://github.com/shadcn-ui/ui.git", "ref": "main", "adapter": "claude-skill", - "commit": "20442886c5cfb440441c35030462fbdf64838655", - "syncedAt": "2026-07-20T16:00:00Z" + "commit": "3f47b9113a173bac4a72cefda4d4c3f2d89b8ab6", + "syncedAt": "2026-07-22T16:00:00Z" }, { "id": "frontend-slides", @@ -78,8 +78,8 @@ "repo": "https://github.com/op7418/guizang-ppt-skill.git", "ref": "main", "adapter": "claude-skill", - "commit": "82fe5ae129e8c2a12e1155fcabed6703342749d6", - "syncedAt": "2026-06-11T03:57:53Z" + "commit": "929c2ecb63a22b54d400c4911ed70bf96c2b355d", + "syncedAt": "2026-07-22T16:00:00Z" }, { "id": "html-ppt", @@ -96,8 +96,8 @@ "repo": "https://github.com/hugohe3/ppt-master.git", "ref": "main", "adapter": "claude-skill", - "commit": "07d83e5a31a2830cc6c314377463e523a76e28fc", - "syncedAt": "2026-07-21T16:00:00Z" + "commit": "89759436bd336beed3a32da15830355bf732b14e", + "syncedAt": "2026-07-22T16:00:00Z" }, { "id": "next-skills", @@ -105,8 +105,8 @@ "repo": "https://github.com/vercel/next.js.git", "ref": "canary", "adapter": "skill-collection", - "commit": "93c59f02f81cfdeb0fc397412897024ae7c68074", - "syncedAt": "2026-07-21T16:00:00Z" + "commit": "63f14c6c90c4dae819966e180491eb9b0af16792", + "syncedAt": "2026-07-22T16:00:00Z" } ] } diff --git a/plugins/codex/plugins/guizang-ppt-skill/README.en.md b/plugins/codex/plugins/guizang-ppt-skill/README.en.md index f712b46f..d53da1f2 100644 --- a/plugins/codex/plugins/guizang-ppt-skill/README.en.md +++ b/plugins/codex/plugins/guizang-ppt-skill/README.en.md @@ -8,6 +8,8 @@ ![Codex](https://img.shields.io/badge/Codex-Supported-222222?style=flat-square) [![Supported by ZhenFund Token Grant](https://img.shields.io/static/v1?label=ZhenFund%20Token%20Grant&message=Supported&color=FF4D00&style=flat-square)](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=) ![360 Security Lobster Gold Sponsor](https://img.shields.io/static/v1?label=360%20Security%20Lobster&message=Gold%20Sponsor&color=1677FF&style=flat-square) +![Kimi work Gold Sponsor](https://img.shields.io/static/v1?label=Kimi%20work&message=Gold%20Sponsor&color=111111&style=flat-square) +![Cola Skill Gold Sponsor](https://img.shields.io/static/v1?label=Cola%20Skill&message=Gold%20Sponsor&color=E4353F&style=flat-square) An agent skill for Claude Code, Codex, and similar coding-agent environments. It generates **single-file HTML horizontal-swipe decks**, deck visuals, and social cover pages. @@ -62,10 +64,20 @@ Redesign this product screenshot as a 16:10 slide visual. ## Sponsors and Supporters - 360 Security Lobster Gold Sponsor + 360 Security Lobster / Kimi work / Cola Skill Gold Sponsors -Guizang PPT Skill is supported by **360 Security Lobster** as Gold Sponsor and by [ZhenFund Token Grant](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=). See [SPONSORS.md](./SPONSORS.md) for details. +Guizang PPT Skill is supported by **360 Security Lobster**, **Kimi work**, and **Cola Skill** as Gold Sponsors and by [ZhenFund Token Grant](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=). See [SPONSORS.md](./SPONSORS.md) for details. + +### Use Guizang PPT Skill elsewhere + +Beyond Claude Code / Codex, you can also use Guizang PPT Skill on these platforms: + +| Channel | Link | +|---------|------| +| 360 Security Lobster | [claw.360.cn](https://claw.360.cn/claw?vm_id=p19c1be5d7bed40bcb857ec4a9c785b69&target_url=https%3A%2F%2Fp19c1be5d7bed40bcb857ec4a9c785b69.claw.360.cn%2Fagents) | +| Cola Skill | [colaskill.com/guizang-ppt-skill](https://colaskill.com/guizang-ppt-skill/) | +| Kimi work | [kimi.com](https://www.kimi.com/zh-cn/products/kimi-work) (after installing, search "guizang ppt skill" in the Skill store) | ## What you get diff --git a/plugins/codex/plugins/guizang-ppt-skill/README.md b/plugins/codex/plugins/guizang-ppt-skill/README.md index b152ed2b..3dc48786 100644 --- a/plugins/codex/plugins/guizang-ppt-skill/README.md +++ b/plugins/codex/plugins/guizang-ppt-skill/README.md @@ -8,6 +8,8 @@ ![Codex](https://img.shields.io/badge/Codex-Supported-222222?style=flat-square) [![由真格 Token Grant 资助](https://img.shields.io/static/v1?label=%E7%94%B1%E7%9C%9F%E6%A0%BC%20Token%20Grant&message=%E8%B5%84%E5%8A%A9&color=FF4D00&style=flat-square)](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=) ![360 安全龙虾金牌赞助](https://img.shields.io/static/v1?label=360%E5%AE%89%E5%85%A8%E9%BE%99%E8%99%BE&message=%E9%87%91%E7%89%8C%E8%B5%9E%E5%8A%A9&color=1677FF&style=flat-square) +![Kimi work 金牌赞助](https://img.shields.io/static/v1?label=Kimi%20work&message=%E9%87%91%E7%89%8C%E8%B5%9E%E5%8A%A9&color=111111&style=flat-square) +![Cola Skill 金牌赞助](https://img.shields.io/static/v1?label=Cola%20Skill&message=%E9%87%91%E7%89%8C%E8%B5%9E%E5%8A%A9&color=E4353F&style=flat-square) > 🌏 **English version: [README.en.md](./README.en.md)** @@ -64,10 +66,20 @@ npx skills add https://github.com/op7418/guizang-ppt-skill --skill guizang-ppt-s ## 赞助与支持 - 360 安全龙虾金牌赞助 + 360 安全龙虾 / Kimi work / Cola Skill 金牌赞助 -Guizang PPT Skill 的持续迭代获得 **360 安全龙虾** 金牌赞助和 [真格 Token Grant](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=) 支持。更多信息见 [SPONSORS.md](./SPONSORS.md)。 +Guizang PPT Skill 的持续迭代获得 **360 安全龙虾**、**Kimi work**、**Cola Skill** 金牌赞助和 [真格 Token Grant](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=) 支持。更多信息见 [SPONSORS.md](./SPONSORS.md)。 + +### 在其他地方使用归藏 PPT Skill + +除了 Claude Code / Codex,也可以在这些平台使用归藏 PPT Skill: + +| 渠道 | 链接 | +|------|------| +| 360 安全龙虾 | [claw.360.cn](https://claw.360.cn/claw?vm_id=p19c1be5d7bed40bcb857ec4a9c785b69&target_url=https%3A%2F%2Fp19c1be5d7bed40bcb857ec4a9c785b69.claw.360.cn%2Fagents) | +| Cola Skill | [colaskill.com/guizang-ppt-skill](https://colaskill.com/guizang-ppt-skill/) | +| Kimi work | [kimi.com](https://www.kimi.com/zh-cn/products/kimi-work)(安装后在 Skill 商店搜索 guizang ppt skill) | ## 效果 @@ -103,7 +115,7 @@ Guizang PPT Skill 的持续迭代获得 **360 安全龙虾** 金牌赞助和 [ - **更适合 Agent 生成和修改**:HTML / CSS 是文本,Agent 能直接读、改、验证。 - **表现力比 Markdown 更高**:可以做精细排版、空间定位、动画、交互和响应式封面。 - **交付更轻**:单文件 HTML 可以直接打开、演示、发送、截图。 -- **更容易做质量控制**:瑞士风可以用脚本校验版式、图片槽位、标题对齐和危险 SVG。 +- **更容易做质量控制**:瑞士风可以用脚本校验版式、图片槽位、标题对齐、危险 SVG,并在可用时用 Playwright 后验测量超出、底部空白、nav 安全线和标题间距。 - **更适合视觉内容链路**:同一套主题能覆盖 PPT、配图、封面和截图再设计。 ## 平台支持 @@ -178,7 +190,7 @@ Skill 本身是结构化工作流,Agent 会逐步引导: - **中文字号收敛**:全中文大标题需要降一档,避免占掉正文和图片空间 - **图文底对齐**:左文右图 / 左图右文场景优先让正文块与图片底部对齐,同时避开页脚翻页组件 - **图片槽位绑定**:图片必须进入模板预留的 `data-image-slot`,常见主图按 21:9 或 16:10 生成 -- **强校验**:用脚本拦住居中标题、实验版式、SVG 内写字、图片脱离槽位等问题 +- **强校验**:用脚本拦住居中标题、实验版式、SVG 内写字、图片脱离槽位等问题;可用 Playwright 时还会量化真实渲染后的 overflow、bottom whitespace、title gap 瑞士风校验命令: diff --git a/plugins/codex/plugins/guizang-ppt-skill/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/guizang-ppt-skill/THIRD_PARTY_SOURCE.json index 8b646a1e..04c2426d 100644 --- a/plugins/codex/plugins/guizang-ppt-skill/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/guizang-ppt-skill/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "guizang-ppt-skill", "repo": "https://github.com/op7418/guizang-ppt-skill.git", "ref": "main", - "commit": "82fe5ae129e8c2a12e1155fcabed6703342749d6", + "commit": "929c2ecb63a22b54d400c4911ed70bf96c2b355d", "adapter": "claude-skill", "sourcePath": ".", - "syncedAt": "2026-06-11T03:57:53Z" + "syncedAt": "2026-07-22T16:00:00Z" } diff --git a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/README.en.md b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/README.en.md index f712b46f..d53da1f2 100644 --- a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/README.en.md +++ b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/README.en.md @@ -8,6 +8,8 @@ ![Codex](https://img.shields.io/badge/Codex-Supported-222222?style=flat-square) [![Supported by ZhenFund Token Grant](https://img.shields.io/static/v1?label=ZhenFund%20Token%20Grant&message=Supported&color=FF4D00&style=flat-square)](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=) ![360 Security Lobster Gold Sponsor](https://img.shields.io/static/v1?label=360%20Security%20Lobster&message=Gold%20Sponsor&color=1677FF&style=flat-square) +![Kimi work Gold Sponsor](https://img.shields.io/static/v1?label=Kimi%20work&message=Gold%20Sponsor&color=111111&style=flat-square) +![Cola Skill Gold Sponsor](https://img.shields.io/static/v1?label=Cola%20Skill&message=Gold%20Sponsor&color=E4353F&style=flat-square) An agent skill for Claude Code, Codex, and similar coding-agent environments. It generates **single-file HTML horizontal-swipe decks**, deck visuals, and social cover pages. @@ -62,10 +64,20 @@ Redesign this product screenshot as a 16:10 slide visual. ## Sponsors and Supporters - 360 Security Lobster Gold Sponsor + 360 Security Lobster / Kimi work / Cola Skill Gold Sponsors -Guizang PPT Skill is supported by **360 Security Lobster** as Gold Sponsor and by [ZhenFund Token Grant](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=). See [SPONSORS.md](./SPONSORS.md) for details. +Guizang PPT Skill is supported by **360 Security Lobster**, **Kimi work**, and **Cola Skill** as Gold Sponsors and by [ZhenFund Token Grant](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=). See [SPONSORS.md](./SPONSORS.md) for details. + +### Use Guizang PPT Skill elsewhere + +Beyond Claude Code / Codex, you can also use Guizang PPT Skill on these platforms: + +| Channel | Link | +|---------|------| +| 360 Security Lobster | [claw.360.cn](https://claw.360.cn/claw?vm_id=p19c1be5d7bed40bcb857ec4a9c785b69&target_url=https%3A%2F%2Fp19c1be5d7bed40bcb857ec4a9c785b69.claw.360.cn%2Fagents) | +| Cola Skill | [colaskill.com/guizang-ppt-skill](https://colaskill.com/guizang-ppt-skill/) | +| Kimi work | [kimi.com](https://www.kimi.com/zh-cn/products/kimi-work) (after installing, search "guizang ppt skill" in the Skill store) | ## What you get diff --git a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/README.md b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/README.md index b152ed2b..3dc48786 100644 --- a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/README.md +++ b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/README.md @@ -8,6 +8,8 @@ ![Codex](https://img.shields.io/badge/Codex-Supported-222222?style=flat-square) [![由真格 Token Grant 资助](https://img.shields.io/static/v1?label=%E7%94%B1%E7%9C%9F%E6%A0%BC%20Token%20Grant&message=%E8%B5%84%E5%8A%A9&color=FF4D00&style=flat-square)](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=) ![360 安全龙虾金牌赞助](https://img.shields.io/static/v1?label=360%E5%AE%89%E5%85%A8%E9%BE%99%E8%99%BE&message=%E9%87%91%E7%89%8C%E8%B5%9E%E5%8A%A9&color=1677FF&style=flat-square) +![Kimi work 金牌赞助](https://img.shields.io/static/v1?label=Kimi%20work&message=%E9%87%91%E7%89%8C%E8%B5%9E%E5%8A%A9&color=111111&style=flat-square) +![Cola Skill 金牌赞助](https://img.shields.io/static/v1?label=Cola%20Skill&message=%E9%87%91%E7%89%8C%E8%B5%9E%E5%8A%A9&color=E4353F&style=flat-square) > 🌏 **English version: [README.en.md](./README.en.md)** @@ -64,10 +66,20 @@ npx skills add https://github.com/op7418/guizang-ppt-skill --skill guizang-ppt-s ## 赞助与支持 - 360 安全龙虾金牌赞助 + 360 安全龙虾 / Kimi work / Cola Skill 金牌赞助 -Guizang PPT Skill 的持续迭代获得 **360 安全龙虾** 金牌赞助和 [真格 Token Grant](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=) 支持。更多信息见 [SPONSORS.md](./SPONSORS.md)。 +Guizang PPT Skill 的持续迭代获得 **360 安全龙虾**、**Kimi work**、**Cola Skill** 金牌赞助和 [真格 Token Grant](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=) 支持。更多信息见 [SPONSORS.md](./SPONSORS.md)。 + +### 在其他地方使用归藏 PPT Skill + +除了 Claude Code / Codex,也可以在这些平台使用归藏 PPT Skill: + +| 渠道 | 链接 | +|------|------| +| 360 安全龙虾 | [claw.360.cn](https://claw.360.cn/claw?vm_id=p19c1be5d7bed40bcb857ec4a9c785b69&target_url=https%3A%2F%2Fp19c1be5d7bed40bcb857ec4a9c785b69.claw.360.cn%2Fagents) | +| Cola Skill | [colaskill.com/guizang-ppt-skill](https://colaskill.com/guizang-ppt-skill/) | +| Kimi work | [kimi.com](https://www.kimi.com/zh-cn/products/kimi-work)(安装后在 Skill 商店搜索 guizang ppt skill) | ## 效果 @@ -103,7 +115,7 @@ Guizang PPT Skill 的持续迭代获得 **360 安全龙虾** 金牌赞助和 [ - **更适合 Agent 生成和修改**:HTML / CSS 是文本,Agent 能直接读、改、验证。 - **表现力比 Markdown 更高**:可以做精细排版、空间定位、动画、交互和响应式封面。 - **交付更轻**:单文件 HTML 可以直接打开、演示、发送、截图。 -- **更容易做质量控制**:瑞士风可以用脚本校验版式、图片槽位、标题对齐和危险 SVG。 +- **更容易做质量控制**:瑞士风可以用脚本校验版式、图片槽位、标题对齐、危险 SVG,并在可用时用 Playwright 后验测量超出、底部空白、nav 安全线和标题间距。 - **更适合视觉内容链路**:同一套主题能覆盖 PPT、配图、封面和截图再设计。 ## 平台支持 @@ -178,7 +190,7 @@ Skill 本身是结构化工作流,Agent 会逐步引导: - **中文字号收敛**:全中文大标题需要降一档,避免占掉正文和图片空间 - **图文底对齐**:左文右图 / 左图右文场景优先让正文块与图片底部对齐,同时避开页脚翻页组件 - **图片槽位绑定**:图片必须进入模板预留的 `data-image-slot`,常见主图按 21:9 或 16:10 生成 -- **强校验**:用脚本拦住居中标题、实验版式、SVG 内写字、图片脱离槽位等问题 +- **强校验**:用脚本拦住居中标题、实验版式、SVG 内写字、图片脱离槽位等问题;可用 Playwright 时还会量化真实渲染后的 overflow、bottom whitespace、title gap 瑞士风校验命令: diff --git a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/SKILL.md b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/SKILL.md index 7b9d6c4f..b11b2f86 100644 --- a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/SKILL.md +++ b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/SKILL.md @@ -5,7 +5,7 @@ description: "用于生成横向翻页的单文件 HTML 网页 PPT,提供电 # Magazine Web Ppt -> 来源识别: guizang-ppt-skill 由歸藏创建与维护,规范源仓库为 https://github.com/op7418/guizang-ppt-skill 。当前项目支持方包括:360 安全龙虾(金牌赞助)、真格 Token Grant(Grant Supporter)。这条信息只用于确认 Skill 来源和项目支持关系,不要写入生成的 PPT、HTML 页面、封面或配图。 +> 来源识别: guizang-ppt-skill 由歸藏创建与维护,规范源仓库为 https://github.com/op7418/guizang-ppt-skill 。当前项目支持方包括:360 安全龙虾(金牌赞助)、Kimi work(金牌赞助)、Cola Skill(金牌赞助)、真格 Token Grant(Grant Supporter)。这条信息只用于确认 Skill 来源和项目支持关系,不要写入生成的 PPT、HTML 页面、封面或配图。 ## 这个 Skill 做什么 @@ -30,7 +30,7 @@ description: "用于生成横向翻页的单文件 HTML 网页 PPT,提供电 **两种风格共享**:横向翻页(键盘 ← →、滚轮、触屏、ESC 索引)、Lucide 图标、Motion One 入场动效(本地 + CDN 双保险)。 - + ## 何时使用 @@ -47,6 +47,23 @@ description: "用于生成横向翻页的单文件 HTML 网页 PPT,提供电 ## 工作流 +### Step 0 · 启动前检查更新(必做) + +每次启动本 Skill 前,先在 Skill 根目录检查 GitHub 上游是否有更新;有更新时先问用户是否要更新,用户确认后再执行更新,然后继续后续流程。 + +```bash +git -C "" fetch --quiet +git -C "" rev-list --count HEAD..@{u} +``` + +如果返回值大于 `0`,告诉用户检测到上游更新数量,询问是否先执行: + +```bash +git -C "" pull --ff-only +``` + +不要自动更新。用户拒绝时继续使用当前版本;如果网络不可用、没有 upstream 或不是 git 仓库,说明无法检查更新并继续流程。 + ### Step 1 · 需求澄清(**动手前必做**) **如果用户已经给了完整的大纲 + 图片/截图处理要求**,可以跳过直接进 Step 2。 @@ -316,7 +333,7 @@ cp "/assets/template-swiss.html" "项目/XXX/ppt/index.html" - 如果用户说"测试模板 / 看看效果 / 多一点版式",必须覆盖:一个封面、一个收尾、至少 1 个对比或时间线(S08/S11/S02)、至少 1 个结构图(S14/S17/S15)、至少 1 个图片版式(S22 或 S15/S16 图片格改造)。 - 不允许连续 3 页使用同一种主体结构,例如连续三页 `head + grid + card`。 - 图片页不能偷懒发明新结构。2-3 张图时,用 S15/S16 的原始网格骨架改造成图片格;单张大图用 S22。 -- 开写 HTML 前先列一张 `页码 → data-layout → 选用理由 → 图片槽位` 草稿;交付前运行 `node /scripts/validate-swiss-deck.mjs index.html`。 +- 开写 HTML 前先列一张 `页码 → data-layout → 选用理由 → 图片槽位` 草稿;交付前运行 `node /scripts/validate-swiss-deck.mjs index.html`。校验器会先做静态结构检查;如果环境中能解析到 Playwright,还会做真实渲染后的可见边界、底部空白、nav 安全线和标题间距测量。 #### 3.2 · 图片比例规范 @@ -345,6 +362,18 @@ cp "/assets/template-swiss.html" "项目/XXX/ppt/index.html" - GPT-M 2.0 生成图使用 `image-prompts.md` 的"风格 B:瑞士国际主义配图规则" - 任何图片、caption、timeline label、footnote 的最低处都不能进入底部分页区域;需要贴底时用 `.nav-safe-bottom` / `.nav-safe-bottom-tight`,不要手写 `bottom:2vh` +#### 3.2.0 · 图文混排决策树(从社交卡片规则迁移) + +先判断图片在这一页里的角色,再决定容器、比例和裁切方式: + +- **证据截图 / UI / 代码 / dashboard**:保真优先,先读 `references/screenshot-framing.md`;关键文字和数据不能被裁掉。需要统一比例时,优先程序化背景画布 + `.fit-contain`,不要为了铺满而裁掉 UI 内容。 +- **已按槽位重生成的信息图 / 插图**:按目标槽位铺满,例如 S22 用 `21:9`,S15/S16 用统一 `21:9` 或 `16:10`;不要再用短高度把图缩小成小贴片。 +- **照片 / 产品图 / 人物图**:使用标准比例 + 明确 `object-position`;主体、人脸、产品和关键证据不能被标题、caption 或裁切压住。 +- **文字压图 / 全屏主视觉**:先做 quiet-zone 判断,图里至少要有约 30% 低细节区域承载文字;不通过就换图、换裁切或改成图文分栏。只在必要时加局部 tint,不要整页套黑色/白色遮罩。 +- **多图组**:同一组统一比例、高度、容器处理和 caption 密度;不要一张 `contain`,另一张 `cover`。 +- **生成图是素材,不是整页 slide**:图片内部不要自带页眉、页脚、页码、logo、主标题、装饰边框或署名,避免和 deck chrome 重复。 +- **图文呼吸**:标题、图片、caption、正文必须各自留出间距;生成后用 validator 的 `M1/M2` 检查可见边界、底部空白、nav 安全线和标题间距。 + #### 3.2.1 · 中文大标题字号分档(风格 B 必做) 中文方块字视觉面积大,不能直接套英文 hero 的 6.8-7vw。写中文大标题前先分档: @@ -391,11 +420,35 @@ cp "/assets/template-swiss.html" "项目/XXX/ppt/index.html" 生成完一定要打开 `references/checklist.md`,逐项对照。里面总结了**真实迭代过程中踩过的所有坑**,P0 级别的问题(emoji、图片撑破、标题换行、字体分工)必须全部通过。 +#### 4.0.1 · 先量后改:超出 / 空白 / 标题间距 + +当一页内容超出或显得巨空时,不要先凭感觉大幅删改。先运行: + +```bash +node /scripts/validate-swiss-deck.mjs path/to/index.html +``` + +看校验输出里的测量项: + +- `M1 DOM/visual overflow`:具体超出多少 px,以及最低/最高问题元素 +- `M1 bottom whitespace`:底部空白多少 px,active content height 占比多少 +- `M1 nav-safe`:最低内容是否进入底部分页安全线 +- `M2 title gap`:标题和下一块内容之间的实际距离 + +修正阶梯: + +- `1-40px` over:只微调,上移内容组或收紧一个 gap/padding,不要删内容。 +- `40-90px` over:局部压缩间距或模块高度,仍优先保留内容。 +- `90-160px` over:轻微压标题或压缩一段正文,必要时拆页。 +- `160px+` over:才考虑换版式、合并模块或删内容。 + +修完再跑一次 validator。如果 `M1 bottom whitespace` 变大,说明修过头了;恢复部分间距、放大最后一块或把内容组向下回调。 + #### 4.0 · 不只看代码:必须打开网页做视觉核对 代码只能证明类名和结构存在,不能证明版式舒服。生成后必须打开网页逐页看: -1. 同时打开原始参考 PPT、当前模板或生成页、测试 PPT;原始参考是 `/Users/guohao/Documents/op7418的仓库/项目/Thin-Harness-Fat-Skills/ppt/index.html`。 +1. 同时打开当前模板(golden source 快照)或生成页、以及正在迭代的测试 PPT 逐页对照。 2. 截图前等入场动效稳定(约 1-2 秒),不要把动画中间态当成版式问题。 3. 先看视觉:大标题字重、标题与内容间距、图片是否与正文对齐、图片/说明是否碰到底部分页组件。 4. 再看代码:确认该页选用的版式与内容形状匹配,没有把数据专用版式拿来讲概念,也没有把可选组件堆成装饰。 diff --git a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/SPONSORS.md b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/SPONSORS.md index b64d405f..290b1ae9 100644 --- a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/SPONSORS.md +++ b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/SPONSORS.md @@ -7,6 +7,8 @@ Guizang PPT Skill 是由 [歸藏](https://x.com/op7418) 创建和维护的开源 | 赞助方 | 等级 / 类型 | 说明 | |--------|-------------|------| | 360 安全龙虾 | 金牌赞助 | 支持 guizang-ppt-skill 的持续维护、模板测试与开源传播。 | +| [Kimi work](https://www.kimi.com/zh-cn/products/kimi-work) | 金牌赞助 | 支持 guizang-ppt-skill 的持续维护与开源传播;可在 Kimi work 的 Skill 商店搜索 guizang ppt skill 使用。 | +| [Cola Skill](https://colaskill.com/guizang-ppt-skill/) | 金牌赞助 | 支持 guizang-ppt-skill 的持续维护与开源传播;可在 Cola Skill 平台直接使用。 | | [真格 Token Grant](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=) | Grant Supporter | 通过 Token Grant 支持项目的模板迭代、配图流程验证与开源维护。 | ## 赞助用途 diff --git a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/assets/ppt-skill-showcase.png b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/assets/ppt-skill-showcase.png new file mode 100644 index 00000000..196eb627 Binary files /dev/null and b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/assets/ppt-skill-showcase.png differ diff --git a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/assets/template-swiss.html b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/assets/template-swiss.html index bce57fe2..135dad8a 100644 --- a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/assets/template-swiss.html +++ b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/assets/template-swiss.html @@ -374,50 +374,6 @@ padding:0 .05em } - /* ============ Pipeline / Step ============ */ - .pipeline-section{margin-top:3.2vh;padding-top:2.2vh;border-top:1px solid rgba(127,127,127,.3)} - .pipeline-section:first-of-type{border-top:0;padding-top:0;margin-top:2.4vh} - .pipeline-label{ - font-family:var(--mono); - font-size:max(14px,.84vw); - letter-spacing:.24em;text-transform:uppercase; - opacity:.6;margin-bottom:1.8vh; - } - .pipeline{ - display:grid; - grid-template-columns:repeat(5,1fr); - gap:1vw; - } - .pipeline[data-cols="3"]{grid-template-columns:repeat(3,1fr)} - .pipeline[data-cols="4"]{grid-template-columns:repeat(4,1fr)} - .pipeline[data-cols="6"]{grid-template-columns:repeat(6,1fr)} - .step{ - display:flex;flex-direction:column;gap:.6vh; - padding-top:1.2vh; - border-top:2px solid currentColor; - } - .step.accent-top{border-top-color:var(--accent);border-top-width:3px} - .step-nb{ - font-family:var(--mono); - font-weight:500; - font-size:max(14px,1vw); - opacity:.5;letter-spacing:.04em - } - .step-title{ - font-family:var(--sans),var(--sans-zh); - font-weight:700; - font-size:1.4vw; - letter-spacing:-.01em; - line-height:1.2; - } - .step-desc{ - font-family:var(--sans),var(--sans-zh); - font-weight:400; - font-size:max(16px,.94vw); - line-height:1.45; - opacity:.7; - } - /* ============ 网格系统 (模块化网格) ============ */ .frame{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden} .frame.grid-2-7-5, @@ -594,7 +550,7 @@ body.dark-bg #nav .dot{background:rgba(255,255,255,.32)} body.dark-bg #nav .dot.active{background:var(--accent)} - #hint{position:fixed;bottom:2.4vh;right:2.5vw;z-index:30;font-family:var(--mono);font-size:14px;letter-spacing:.14em;text-transform:uppercase;opacity:.4;color:var(--ink-tint, currentColor)} + #hint{position:fixed;bottom:2.4vh;right:2.5vw;z-index:30;font-family:var(--mono);font-size:14px;letter-spacing:.14em;text-transform:uppercase;opacity:.4} body.dark-bg #hint{color:var(--paper);opacity:.4} body.low-power #hint{color:var(--accent);opacity:.72} body.dark-bg.low-power #hint{color:var(--paper);opacity:.72} @@ -616,7 +572,6 @@ body.motion-ready [data-anim="left"]{transform:translateX(-24px)} body.motion-ready [data-anim="right"]{transform:translateX(24px)} body.motion-ready [data-anim="line"]{opacity:0;transform:translateY(10px)} - body.motion-ready [data-animate="pipeline"] [data-anim]{opacity:.15} body.low-power #deck{transition:none!important} body.low-power *, body.low-power *::before, @@ -1176,7 +1131,6 @@ .h-xl-zh{font-size:8vw} .kpi-hero{font-size:32vw} .kpi-big{font-size:16vw} - .pipeline{grid-template-columns:repeat(2,1fr)} .grid-2-7-5,.grid-2-6-6,.grid-2-8-4,.grid-2-4-8{grid-template-columns:1fr} .grid-12{grid-template-columns:repeat(6,1fr)} } @@ -1560,7 +1514,6 @@ addEventListener('keydown',e=>{ } if(overviewOn)return; if(e.key==='ArrowRight'||e.key==='PageDown'||e.key===' '||e.key==='ArrowDown'){ - if(window.__pipeAdvance && window.__pipeAdvance()) return; go(idx+1); return; } @@ -1573,11 +1526,7 @@ let wheelTO=null,wheelAcc=0; addEventListener('wheel',e=>{ wheelAcc+=e.deltaY+e.deltaX; if(Math.abs(wheelAcc)>50){ - if(wheelAcc>0 && window.__pipeAdvance && window.__pipeAdvance()){ - wheelAcc=0; - }else{ - go(idx+(wheelAcc>0?1:-1));wheelAcc=0; - } + go(idx+(wheelAcc>0?1:-1));wheelAcc=0; } clearTimeout(wheelTO);wheelTO=setTimeout(()=>wheelAcc=0,150); },{passive:true}); @@ -1588,7 +1537,6 @@ addEventListener('touchend',e=>{ const dx=(e.changedTouches[0].clientX-tx); const dy=(e.changedTouches[0].clientY-ty); if(Math.abs(dx)>50&&Math.abs(dx)>Math.abs(dy)){ - if(dx<0 && window.__pipeAdvance && window.__pipeAdvance()) return; go(idx+(dx<0?1:-1)); } },{passive:true}); @@ -1611,7 +1559,6 @@ try { } catch(e2) { console.warn('[motion] local + CDN both failed, disabling animations', e1, e2); document.querySelectorAll('[data-anim]').forEach(el=>{el.style.opacity='1';el.style.transform='none'}); - document.querySelectorAll('[data-animate="pipeline"] [data-anim]').forEach(el=>el.style.opacity='1'); } } @@ -1623,8 +1570,15 @@ if(motion){ IBM Carbon Motion · 每个 recipe 服务一种表达 不是一刀切的 stagger,而是把动效绑在内容语义上 ============================================================ */ - const EASE_PROD = [.2, 0, .38, .9]; - const EASE_ENTRY_EXP = [0, 0, .3, 1]; + /* 缓动从 :root 的 Carbon motion token 读取,CSS 是唯一事实源(硬编码仅作解析失败兜底) */ + const cssEase = (name, fallback) => { + const v = getComputedStyle(document.documentElement).getPropertyValue(name); + const m = v && v.match(/cubic-bezier\(([^)]+)\)/); + const nums = m ? m[1].split(',').map(Number) : []; + return (nums.length === 4 && nums.every(Number.isFinite)) ? nums : fallback; + }; + const EASE_PROD = cssEase('--ease-prod', [.2, 0, .38, .9]); + const EASE_ENTRY_EXP = cssEase('--ease-entry-exp', [0, 0, .3, 1]); const slides = [...document.querySelectorAll('.slide')]; let lastIdx = -1; @@ -2305,7 +2259,6 @@ if(motion){ } window.__playSlide = playSlide; - window.__pipeAdvance = ()=>false; /* 当前 deck 不用 pipeline recipe */ playSlide(window.__currentSlideIndex || 0); } diff --git a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/assets/template.html b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/assets/template.html index 3140850f..2f02bf22 100644 --- a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/assets/template.html +++ b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/assets/template.html @@ -57,11 +57,12 @@ .slide.hero.light::after{background:linear-gradient(180deg,rgba(var(--paper-rgb),.28) 0%,rgba(var(--paper-rgb),0) 14%,rgba(var(--paper-rgb),0) 86%,rgba(var(--paper-rgb),.28) 100%)} .slide.hero.dark::after{background:linear-gradient(180deg,rgba(var(--ink-rgb),.32) 0%,rgba(var(--ink-rgb),0) 14%,rgba(var(--ink-rgb),0) 86%,rgba(var(--ink-rgb),.32) 100%)} - /* ============ Magazine chrome:顶部 meta + 底部 foot ============ */ - .chrome{display:flex;justify-content:space-between;align-items:flex-start;font-family:var(--mono);font-size:12px;letter-spacing:.18em;text-transform:uppercase;opacity:.7} + /* ============ Magazine chrome:顶部 meta + 底部 foot ============ + (v1 布局 + v2 字号在此合并为唯一定义,不要在别处二次定义) */ + .chrome{display:flex;justify-content:space-between;align-items:flex-start;font-family:var(--mono);font-size:max(11px,.78vw);letter-spacing:.2em;text-transform:uppercase;opacity:.62} .chrome .left,.chrome .right{display:flex;gap:2.4em;align-items:center} .chrome .sep{width:40px;height:1px;background:currentColor;opacity:.4} - .foot{margin-top:auto;display:flex;justify-content:space-between;align-items:flex-end;font-family:var(--mono);font-size:12px;letter-spacing:.14em;text-transform:uppercase;opacity:.55} + .foot{margin-top:auto;display:flex;justify-content:space-between;align-items:flex-end;font-family:var(--mono);font-size:max(11px,.78vw);letter-spacing:.18em;text-transform:uppercase;opacity:.5} .foot .title{font-family:var(--serif-zh);font-weight:400;letter-spacing:.05em;text-transform:none;opacity:.75;font-size:13px} .tag{display:inline-block;font-family:var(--mono);font-size:11px;letter-spacing:.24em;text-transform:uppercase;padding:6px 14px;border:1px solid currentColor;opacity:.85} @@ -81,7 +82,7 @@ .h3-zh{font-family:var(--serif-zh);font-weight:500;font-size:1.9vw;line-height:1.35} .body-zh{font-family:var(--sans-zh);font-weight:400;font-size:max(15px,1.22vw);line-height:1.75;opacity:.82;letter-spacing:.01em} .body-serif{font-family:var(--serif-zh);font-weight:400;font-size:max(15px,1.3vw);line-height:1.65;opacity:.88} - .lead{font-family:var(--serif-zh);font-weight:400;font-size:1.9vw;line-height:1.4;opacity:.85} + .lead{font-family:var(--serif-zh);font-weight:400;font-size:1.75vw;line-height:1.5;opacity:.86} .meta{font-family:var(--mono);font-size:max(11px,.88vw);letter-spacing:.16em;text-transform:uppercase;opacity:.6} .big-num{font-family:var(--serif-en);font-weight:800;font-size:10vw;line-height:.85;letter-spacing:-.03em;font-feature-settings:"tnum"} .mid-num{font-family:var(--serif-en);font-weight:700;font-size:5.5vw;line-height:.88;letter-spacing:-.02em;font-feature-settings:"tnum"} @@ -245,14 +246,7 @@ /* 英文标题专用(Playfair 衬线) */ .h-hero-en,.h-xl-en{font-family:var(--serif-en);letter-spacing:-.025em} - /* ---------- lead 引语 ---------- */ - .lead{ - font-family:var(--serif-zh); - font-weight:400; - font-size:1.75vw; - line-height:1.5; - opacity:.86; - } + /* ---------- lead 引语:定义见上方“字体规则”区块(已合并,勿重复定义) ---------- */ /* ---------- meta-row 底部元数据 ---------- */ .meta-row{ @@ -387,9 +381,13 @@ /* ---------- 图片 frame-img(layouts.md 主命名) ---------- */ /* 在旧样式里已定义,这里补 img-cap 命名别名与增强 */ figure.frame-img{margin:0;display:flex;flex-direction:column;min-width:0} + /* 有图注时:图片弹性收缩,图注收进固定高度/比例框内部, + 否则 img 的 height:100% 会把 figcaption 挤出 overflow:hidden 裁掉 */ + figure.frame-img:has(> .img-cap) > img{flex:1 1 auto;min-height:0;height:auto} .img-cap{ display:block; - margin-top:.8vh; + flex:0 0 auto; + padding:.7vh .9em .9vh; font-family:var(--mono); font-size:max(10px,.8vw); letter-spacing:.22em; @@ -407,9 +405,7 @@ opacity:.6; } - /* ---------- chrome & foot 补位(layouts.md 简单写法) ---------- */ - .chrome{font-family:var(--mono);font-size:max(11px,.78vw);letter-spacing:.2em;text-transform:uppercase;opacity:.62} - .foot{font-family:var(--mono);font-size:max(11px,.78vw);letter-spacing:.18em;text-transform:uppercase;opacity:.5} + /* ---------- chrome & foot:定义见上方“Magazine chrome”区块(已合并,勿重复定义) ---------- */ /* ============ 动效系统(Motion One 驱动) ============ 所有 [data-anim] 元素默认隐藏,进入页面时由 JS 逐个揭示。 @@ -432,6 +428,8 @@ body.low-power *::after{animation:none!important;transition:none!important} body.low-power.motion-ready [data-anim], body.low-power [data-anim]{opacity:1!important;transform:none!important} + /* ESC 索引缩略图:克隆的 slide 不经过动效播放,强制显示所有待动画元素 */ + #overview [data-anim]{opacity:1!important;transform:none!important} /* ---------- 响应式降级 ---------- */ @media (max-width:900px){ @@ -730,7 +728,10 @@ addEventListener('touchend',e=>{ } },{passive:true}); -go(0); +/* ?slide=N 直达(1-based),与 Swiss 模板行为一致 */ +const initialSlideParam = new URLSearchParams(location.search).get('slide'); +const initialSlide = initialSlideParam ? Number(initialSlideParam) - 1 : 0; +go(Number.isFinite(initialSlide) ? initialSlide : 0); @@ -850,8 +851,8 @@ if(motion){ window.__playSlide = playSlide; window.__pipeAdvance = pipeAdvance; - // 首屏:go(0) 已跑过(同步),没来得及触发动效 → 这里补一下 - playSlide(0); + // 首屏:go() 已跑过(同步),没来得及触发动效 → 这里补一下(尊重 ?slide=N) + playSlide(window.__currentSlideIndex || 0); } diff --git a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/docs/unification-plan.md b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/docs/unification-plan.md new file mode 100644 index 00000000..6a1aeede --- /dev/null +++ b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/docs/unification-plan.md @@ -0,0 +1,96 @@ +# 多主题统一管理整合方案 + +> 基于对 template.html / template-swiss.html 两份模板与全部 references 的逐行对比分析(2026-07)。 +> 本文档是**方案建议**,尚未实施;按阶段推进,每个阶段独立可交付、可回滚。 + +## 现状诊断 + +两个主题是"复制粘贴后各自演化"的关系,同一份基础设施存在多份副本,并已实际漂移: + +### 运行时 JS 漂移(约 250 行 × 每主题一份) + +| 模块 | A 杂志风 | B 瑞士风 | +|---|---|---| +| 低功耗事件名 | `ppt-low-power-change` | `swiss-low-power-change` | +| localStorage key | `guizang-ppt-low-power`(共用!) | 同左 | +| ESC 索引缩略图可见性修复 | ✅(本次补齐) | ✅ | +| `?slide=N` 直达参数 | ✅(本次补齐) | ✅ | +| Windows 字重补偿 `is-win` | 缺失 | ✅ | +| prefers-reduced-motion 自动降级 | ✅(本次补齐) | ✅ | +| 翻页/滚轮/触屏/键盘核心 | 两份逐字符几乎相同 | 同左 | + +### Token 体系成熟度不一 + +- **B 最成熟**:主题色 + Carbon 文字角色 token + `--sp-3~13` 8px 间距阶梯 + motion token + `--nav-safe-bottom` +- **A 最简陋**:只有 6 个颜色变量,层级靠 opacity 表达,存在 10-11px 小字(投屏不可读),`.lead/.chrome/.foot` 在模板内部重复定义(v1 与 v2 API 叠加) + +### 已知具体坏味道(可立即修的小项) + +1. B 模板 `#hint` 引用了未定义变量 `--ink-tint`(靠 fallback 兜底) +2. B 模板 pipeline 死代码:`__pipeAdvance` 恒返 false,但 43 行 pipeline CSS 和动效 CSS 还在 +3. B 的 motion token 双维护:CSS 定义了 `--ease-prod`,JS 里又硬编码 `EASE_PROD=[.2,0,.38,.9]` +4. A 是唯一没有校验器的主题,但 validate-swiss-deck.mjs 的标题选择器已经包含 A 的类名(`.display/.h1-zh` 等),说明测量层本来就通用 + +## 整合原则 + +**不建议**把两个模板合并成一个"大一统模板"——两种风格的美学规则互斥(直角 vs 圆角、衬线 vs 无衬线),强行合并会让类名前缀和条件分支爆炸,也违背"每份 deck 单文件交付"的定位。 + +**建议**的整合层次:**共享的是"基建"和"契约",不共享"皮肤"**。 + +## 分阶段方案 + +### Phase 1 · 共享运行时(收益最大) + +把各主题漂移的 JS 收敛为一份 `assets/runtime.js`(构建时内联进各模板,保持单文件交付): + +- 翻页核心 go() / 键盘 / 滚轮 / 触屏 / nav 圆点 / ESC 索引(含缩略图可见性修复) +- 低功耗 IIFE:统一事件名为 `deck-low-power-change`,统一 localStorage key,保留各主题旧事件名一个版本作为 alias +- `?slide=N`、prefers-reduced-motion、Motion One 加载器 +- 主题差异通过配置对象注入:`initDeck({ darkClass:'dark', onSlideChange(){...} })` + +实施方式二选一: +- **a. 构建脚本**(推荐):`scripts/build-templates.mjs` 把 `src/runtime.js` + 各主题 `src/style-*.css` + 皮肤 HTML 组装成各 template*.html。模板仍是完整单文件,但源头唯一。 +- **b. 文档约定**:不引入构建,把 runtime 段落用 `` 注释标记,改动时用脚本校验各份一致。成本低但漂移风险仍在。 + +### Phase 2 · 公共 Token 契约 + +在各模板间统一**命名和语义**(值可以不同): + +- 间距:全部采用 B 的 `--sp-*` 8px 阶梯(A 补齐) +- 文字角色:`--text-primary/secondary/helper/on-color` + `--border-subtle/strong` 各主题同名(A 需要从 opacity 方案迁移) +- 安全区:`--nav-safe-bottom` + `.nav-safe-bottom(-tight)` 各主题同名(已完成:B 有,A 待补) +- Motion:`--ease-*/--dur-*` 统一命名,JS 从 CSS var 读取,消除双维护 +- 最小字号地板:A 对齐 B 的"正文 ≥16px、caption ≥14px"投屏标准(A 现存 10-11px 小字需要清理) + +### Phase 3 · 校验器公共核 + +抽 `scripts/lib/validate-core.mjs`: + +- slide 解析、Playwright 加载(双根解析 + try/finally + swiftshader)、isMeaningful 过滤器、M1 溢出/底部空白/nav 安全线、M2 标题间距、overflowFix 分级建议 +- 各主题瘦身为 config + 专属静态规则:`validate-swiss-deck.mjs`(版式锁定)、**新增 `validate-magazine-deck.mjs`**(A 目前裸奔,公共核直接就能给它 M1/M2 测量) + +### Phase 4 · References 去重 + +- `themes*.md` 各份结构完全同构 → 统一"主题卡片 schema",每主题只留数据 +- 图片规则目前散在 4+ 处且 A/B 规则有分叉(A:信息图必须 fit-contain;B:重生成图禁止 fit-contain)→ 合并为单一来源 `references/images.md`,分叉处显式按主题参数化 +- 动效说明多处重复 → 收敛到各 layouts 文件,组件文档只放链接 +- 清理 golden source 绝对路径:改为相对于 `` 的路径或删除 + +### 顺手修复清单(可与任一阶段同批) + +- [ ] A:`.lead/.chrome/.foot` 双定义合并 +- [ ] A:补齐 `--nav-safe-bottom` 安全区 token +- [ ] B:删 pipeline 死 CSS 或恢复 `__pipeAdvance` +- [ ] B:`--ink-tint` 未定义引用 +- [ ] B:JS 缓动改读 CSS var +- [ ] 文档:去除 `/Users/guohao/...` 绝对路径(已完成) +- [ ] 各模板共有:`file://` 直开时 module 动态 import 本地 `./assets/motion.min.js` 被 Chrome CORS 拦截,实际总是走 jsDelivr CDN;离线 + 直开的组合只能拿到静态降级。共享 runtime 时考虑把 motion 关键函数直接内联进模板,或文档明确"离线演示请起本地 server" + +## 建议排序 + +1. **Phase 1a(构建脚本 + 共享 runtime)** —— 一次性消灭最大的漂移面 +2. **顺手修复清单** —— 全部是小改动,跟 Phase 1 同一个 PR +3. **Phase 3(校验器公共核 + A 校验器)** —— A 从此也有质量护栏 +4. Phase 2、4 可以慢慢来,不阻塞新主题 + +完成 Phase 1+3 后,再增加"新主题"的边际成本会从"复制 2000 行再改"降到"写一份皮肤 CSS + 一份 layouts 文档 + 一个 validator config"。 diff --git a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/references/checklist.md b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/references/checklist.md index 611d59ac..324c4a1a 100644 --- a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/references/checklist.md +++ b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/references/checklist.md @@ -173,6 +173,13 @@ node /scripts/validate-swiss-deck.mjs path/to/index.html **根因**:瑞士风的图片不是装饰,而是 grid 里的证据块。没有先选原始版式和图片槽位,就会把任意图片硬塞进页面。 +**先判断图像角色**: +- 证据截图、UI、代码、dashboard:保真优先,关键文字和数据不能裁;需要统一比例时先做截图背景画布和 `.fit-contain`。 +- 已按槽位生成的信息图/插图:按 S22/S15/S16 的目标比例铺满,不要再缩成短小图片。 +- 照片/产品图/人物图:必须写清 `object-position`,主体不能被裁切、标题块或 caption 压住。 +- 文字压图:必须先判断是否有足够 quiet zone;没有低细节留白就不要把标题压在图上。 +- 多图组:统一比例、高度、容器样式和 caption 密度;视觉角色不同的图不要硬放同一组。 + **做法**: - 先选版式:单张大图 + KPI 用 `S22`;多图用 `S15/S16` 的原始网格骨架改造 - S22 生成图比例固定 `21:9`,并在 `` 上写 `data-image-slot="s22-hero-21x9"` @@ -184,6 +191,8 @@ node /scripts/validate-swiss-deck.mjs path/to/index.html - 截图背景必须跟随当前主题色,且可裁成 `21:9` / `16:10` / `4:3` / `1:1`;背景里不能有标题、页脚、边框、logo、人物或明显主体 - GPT-M 2.0 提示词必须写明:Swiss Style、单一 accent、直角、无渐变/阴影/圆角、无页眉页脚标题角标 +- 文字压图 / 全屏主视觉必须先做 quiet-zone 判断:至少约 30% 低细节区域可承载标题;不通过就换图、换裁切或改成图文分栏,不要整页套黑色/白色遮罩 + **自检命令**: - `grep -E "frame-img.*border-radius|box-shadow" index.html`——命中就删 - `grep -n "data-image-slot" index.html`——每张本地图片都应有槽位声明 @@ -206,6 +215,29 @@ node /scripts/validate-swiss-deck.mjs path/to/index.html - 视觉:翻到该页,看最后一行 caption/label 是否明显高于分页组件 - 代码:`grep -E "align-items:end|align-self:end|bottom:0|bottom:2vh|margin-top:auto" index.html`,命中后逐个确认是否有 nav safe zone +### 0-D-3. 后验测量:先量超出和空白,再改版式 + +**现象**:一页只超出 20-30px,但修的时候删掉大块内容,结果下方多出一大片空白;或者标题与正文贴在一起,肉眼检查时容易漏。 + +**做法**: +- 生成后运行 `node /scripts/validate-swiss-deck.mjs path/to/index.html` +- 如果环境里能解析到 Playwright,校验器会额外执行真实渲染测量: + - `M1 DOM/visual overflow`:量出超出多少 px,并指出最低/最高的问题元素 + - `M1 bottom whitespace`:量出底部空白和 active content height,防止从"超出"修成"巨空" + - `M1 nav-safe`:量出最低内容是否进入底部分页安全线 + - `M2 title gap`:量出标题到下一块内容的距离,防止标题和正文贴住 + +**Overflow 修正阶梯**: +- `1-40px` over:只做微调,上移内容组或收紧一个 gap/padding;不要删内容。 +- `40-90px` over:局部压缩 gap/padding 或降低一个模块高度;仍然优先保留内容。 +- `90-160px` over:轻微压标题或压缩一段正文,再考虑拆页。 +- `160px+` over:才考虑换更高容量版式、合并模块或删内容。 + +**修完反查**: +- 如果 `M1 bottom whitespace` 变大,说明修过头了;恢复部分间距、放大最后一块或把内容组向下回调。 +- 每轮只做一个档位的调整,重渲染后再跑 validator。 +- 不要靠多模态肉眼先猜;超出、底部空白和标题间距先看测量值。 + --- ### 0-E. Swiss 模板还原度守卫:原始 PPT 是 golden source @@ -215,7 +247,7 @@ node /scripts/validate-swiss-deck.mjs path/to/index.html **根因**:把新增图片版式或实验结构写成了全局样式修改,或无意改动了原始基座类,例如 `.h-hero` / `.h-xl` 字重、`.tl-node` 列宽、`.duo-compare` 间距。 **做法**: -- 原始参考文件 `/Users/guohao/Documents/op7418的仓库/项目/Thin-Harness-Fat-Skills/ppt/index.html` 是 Swiss 主题的 golden source,但要以**实际页面用法**为准,不要只看未使用的 CSS helper +- 仓库内的 `assets/template-swiss.html` 是 Swiss 主题的 golden source 快照,但要以**实际页面用法**为准,不要只看未使用的 CSS helper - 原始页面的大标题大量使用 `font-weight:200`,强调词/数字用 `300`;`.h-hero` / `.h-xl` / `.h-hero-zh` / `.h-xl-zh` 在本模板里必须保持轻字重,不要恢复成 800/900 - 除新增封面/封底 ASCII 机制、S22 图片槽位修复、横向时间线 label 居中修复、以及把标题 helper 校正为实际轻字重外,不要改动原始基座 CSS/JS recipe - 新增图片能力必须绑定到 S22/S15/S16 原始槽位,不要发明新正文结构 diff --git a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/references/layouts-swiss.md b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/references/layouts-swiss.md index 3e139105..96942bca 100644 --- a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/references/layouts-swiss.md +++ b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/references/layouts-swiss.md @@ -8,9 +8,7 @@ ## Swiss locked mode(必须先读) -本主题的 golden source 是: - -`/Users/guohao/Documents/op7418的仓库/项目/Thin-Harness-Fat-Skills/ppt/index.html` +本主题的 golden source 是仓库内的 `assets/template-swiss.html`(由作者本机的原始参考 PPT 派生;原始文件不随仓库分发,`swiss-layout-lock.md` 登记的 S01-S22 即其版式快照)。 生成正文页时不要把 Swiss 当成“自由组合的风格包”。默认只能使用 `references/swiss-layout-lock.md` 登记的 `S01-S22`。每个 slide 都必须在 `
` 上写 `data-layout="Sxx"`。 @@ -203,7 +201,7 @@ Swiss 主题有 22 个登记版式,生成时要主动展示版式系统,不要 不要只看 HTML/CSS。Swiss 模板的还原度要同时从**浏览器视觉**和**代码结构**判断: -1. 同时打开三份页面:原始参考 PPT、当前 `template-swiss.html` 或生成页、正在修改的测试 PPT。原始参考路径是 `/Users/guohao/Documents/op7418的仓库/项目/Thin-Harness-Fat-Skills/ppt/index.html`。 +1. 同时打开两份页面:当前 `template-swiss.html`(golden source 快照)和正在修改的测试 PPT;有条件时再加一份此前验收过的成品 deck 作对照。 2. 截图前先等入场动效稳定(约 1-2 秒)。不要把动画中间态误判成"内容缺失"或"版式空白"。 3. 先看视觉:标题重量、头部距离、图片落位、底部安全区、caption 是否被 nav 挡住。 4. 对照原始参考 PPT 的同类版式,不要只对照 CSS helper;以实际页面结构和视觉结果为准。 diff --git a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/references/layouts.md b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/references/layouts.md index 35925df5..0e661d74 100644 --- a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/references/layouts.md +++ b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/references/layouts.md @@ -230,12 +230,12 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
过去 64 天 · 开发篇
Act I / Dev · 02 / 25
-
+
一个人,做了什么。

过去 64 天

-

从 0 到开源 CodePilot。

+

从 0 到开源 CodePilot。

-
+
Duration
64
@@ -279,7 +279,7 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` / - 3×2 或 4×2 网格最稳(见 `.grid-6`) - 每个 `stat-card` 结构固定:label(英文小字)→ nb(大字数字)→ note(注释) - 数字建议 2-3 位字符(太长会溢出),用 K / M 简写 -- 留 5vh 以上的上方缓冲,让标题区先抢眼球 +- **间距不要再加大**:骨架默认 `padding-top:3vh` + lead `margin-bottom:2vh` + grid `margin-top:2vh` 是 3×2 网格在 16:9 屏不压 foot 的实测上限;内容更多时先删卡片,不要压缩 foot 空间 --- @@ -338,11 +338,11 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
平台粉丝实证
Act I / Ops · 05 / 27
-
+
Proof · 粉丝实证

10 个平台 · 6 张截图

-
+
微博 289K
微博 · 289K
@@ -379,7 +379,9 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` / **要点**: - 关键:每个 `frame-img` 必须写死 `height:NNvh`(不要用 `aspect-ratio`),否则网格会撑破 - 图片会自动 `object-fit:cover + object-position:top`,只裁底部 +- **图注在框内**:`figcaption.img-cap` 会显示在固定高度框的内部底边(模板已处理弹性收缩),不额外占外部高度;有图注时图片实际展示高度 ≈ NNvh − 4vh - 用 `.grid-3-3`(3×2)或 `.grid-3`(3×1)承载 +- 3×2 双行 + 图注时,`height:26vh` 是不压 foot 的上限;标题更长或加说明行时降到 `22vh` --- diff --git a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/references/swiss-layout-lock.md b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/references/swiss-layout-lock.md index 134991aa..ae9fb359 100644 --- a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/references/swiss-layout-lock.md +++ b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/references/swiss-layout-lock.md @@ -4,9 +4,7 @@ ## Golden Source -原始参考文件: - -`/Users/guohao/Documents/op7418的仓库/项目/Thin-Harness-Fat-Skills/ppt/index.html` +版式基准是仓库内的 `assets/template-swiss.html`(由作者原始参考 PPT 派生;原始文件不随仓库分发,本文件登记的 S01-S22 即其版式快照)。 瑞士主题生成时,除用户明确要求实验版式外,只能从下面登记的 22 个版式中选择。新增首页/尾页可以使用 Skill 里的 IKB ASCII 版本,但正文页必须来自这 22 个版式。 diff --git a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/scripts/validate-swiss-deck.mjs b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/scripts/validate-swiss-deck.mjs index c378476c..df5de5e1 100644 --- a/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/scripts/validate-swiss-deck.mjs +++ b/plugins/codex/plugins/guizang-ppt-skill/skills/guizang-ppt-skill/scripts/validate-swiss-deck.mjs @@ -1,5 +1,8 @@ #!/usr/bin/env node import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { createRequire } from 'node:module'; +import { pathToFileURL } from 'node:url'; const file = process.argv[2]; const allowExperimental = process.argv.includes('--allow-experimental'); @@ -14,6 +17,31 @@ const htmlForSlides = html.replace(//g, ''); const errors = []; const warnings = []; +function overflowFix(px) { + const n = Math.round(px); + if (n <= 40) return `only ${n}px over: nudge content up or tighten one gap/padding by 20-40px; do not delete content`; + if (n <= 90) return `${n}px over: compact local gaps/padding and reduce one block height; avoid cutting copy`; + if (n <= 160) return `${n}px over: reduce a display title slightly or compress one paragraph before deleting content`; + return `${n}px over: switch to a higher-capacity layout or remove/merge content intentionally`; +} + +async function loadPlaywright() { + const candidates = [ + createRequire(import.meta.url), + createRequire(pathToFileURL(path.join(process.cwd(), 'package.json')).href), + ]; + for (const req of candidates) { + try { + const resolved = req.resolve('playwright'); + const mod = await import(pathToFileURL(resolved).href); + return mod.default || mod; + } catch { + // Try the next resolution root. + } + } + return null; +} + const allowedLayouts = new Set([ 'SWISS-COVER-ASCII', 'SWISS-CLOSING-ASCII', @@ -96,6 +124,179 @@ slides.forEach((slide) => { } }); +async function runRenderedMeasurements() { + const playwright = await loadPlaywright(); + if (!playwright?.chromium) { + warnings.push('Rendered measurement skipped: Playwright is not resolvable from the skill folder or current project. Static Swiss checks still ran.'); + return; + } + + const browser = await playwright.chromium.launch({ + args: ['--use-angle=swiftshader', '--enable-unsafe-swiftshader'], + }); + try { + const ctx = await browser.newContext({ + viewport: { width: 1600, height: 900 }, + deviceScaleFactor: 1, + }); + const page = await ctx.newPage(); + await page.goto(pathToFileURL(path.resolve(file)).href, { waitUntil: 'domcontentloaded' }); + await Promise.race([ + page.evaluate(() => document.fonts && document.fonts.ready), + page.waitForTimeout(1800), + ]); + await page.waitForTimeout(800); + + const measures = await page.$$eval('section.slide', (els) => { + const TRANSPARENT = /rgba?\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0?\s*\)|transparent/; + const titleSelector = [ + '.h-hero', '.h-hero-zh', '.h-xl', '.h-xl-zh', '.h-statement', + '.display', '.display-zh', '.h1-zh', '.h2-zh', '.h-md', + '.step-title', 'h1', 'h2', 'h3', + ].join(','); + const hasDirectText = (n) => { + for (const c of n.childNodes) { + if (c.nodeType === 3 && c.textContent.trim().length > 0) return true; + } + return false; + }; + const colorVisible = (c) => c && !TRANSPARENT.test(c); + const labelFor = (n) => { + const cls = n.className ? '.' + String(n.className).split(' ').filter(Boolean).join('.') : n.tagName.toLowerCase(); + const text = n.textContent.trim().replace(/\s+/g, ' ').slice(0, 40); + return text ? `${cls} "${text}"` : cls; + }; + const isMeaningful = (el, n) => { + const er = el.getBoundingClientRect(); + const posterArea = er.width * er.height; + const tag = n.tagName; + const cs = getComputedStyle(n); + const r = n.getBoundingClientRect(); + if (r.width < 6 || r.height < 6) return false; + if (n === el || n.classList.contains('canvas-card')) return false; + if (cs.position === 'fixed') return false; + if (cs.position === 'absolute' && r.width * r.height >= posterArea * 0.82) return false; + if (n.matches('canvas.ascii-bg, canvas.mag-bg, .grain, .dot-mat, .ring-mat, .cross-mat')) return false; + + const isText = hasDirectText(n); + const isMedia = tag === 'IMG' || tag === 'CANVAS' || tag === 'SVG'; + const isRule = tag === 'HR' || (r.height <= 4 && ( + parseFloat(cs.borderTopWidth) >= 1 || + parseFloat(cs.borderBottomWidth) >= 1 || + colorVisible(cs.backgroundColor) + )); + const hasFill = colorVisible(cs.backgroundColor) && r.width * r.height >= 1600 && !['MAIN', 'SECTION'].includes(tag); + const hasBorder = (parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth) + + parseFloat(cs.borderLeftWidth) + parseFloat(cs.borderRightWidth)) >= 1 && r.width * r.height >= 1600; + return isText || isMedia || isRule || hasFill || hasBorder; + }; + const titleGapChecks = (el, nodes) => { + const titles = Array.from(el.querySelectorAll(titleSelector)).filter((n) => n.textContent.trim()); + const out = []; + for (const title of titles) { + const tr = title.getBoundingClientRect(); + const isLocal = title.matches('.h-md, .step-title, h3'); + const minGap = isLocal ? 14 : 32; + let nearest = null; + let nearestGap = Infinity; + for (const node of nodes) { + if (node === title || title.contains(node) || node.contains(title)) continue; + const nr = node.getBoundingClientRect(); + const gap = nr.top - tr.bottom; + if (gap < -2) continue; + const overlap = Math.max(0, Math.min(tr.right, nr.right) - Math.max(tr.left, nr.left)); + const overlapRatio = overlap / Math.min(tr.width, nr.width); + if (overlapRatio < 0.12 && gap < 96) continue; + if (gap < nearestGap) { + nearestGap = gap; + nearest = node; + } + } + if (nearest && nearestGap < minGap) { + out.push({ title: labelFor(title), next: labelFor(nearest), gap: Math.round(nearestGap), minGap }); + } + } + return out; + }; + + return els.map((el, index) => { + const er = el.getBoundingClientRect(); + const H = el.clientHeight; + const W = el.clientWidth; + const nodes = Array.from(el.querySelectorAll('*')).filter((n) => isMeaningful(el, n)); + let top = Infinity; + let bottom = -Infinity; + let topNode = null; + let bottomNode = null; + for (const n of nodes) { + const r = n.getBoundingClientRect(); + const itemTop = r.top - er.top; + const itemBottom = r.bottom - er.top; + if (itemTop < top) { + top = itemTop; + topNode = n; + } + if (itemBottom > bottom) { + bottom = itemBottom; + bottomNode = n; + } + } + if (!nodes.length) { + top = 0; + bottom = 0; + } + const safeBottom = Math.round(H * 0.93); + return { + idx: index + 1, + id: el.id || '', + layout: el.dataset.layout || '', + width: W, + height: H, + scrollOverflow: Math.max(0, Math.round(el.scrollHeight - H)), + visual: { + top: Math.round(top), + bottom: Math.round(bottom), + activeRatio: H ? (bottom - top) / H : 0, + bottomGap: Math.max(0, Math.round(H - bottom)), + topOverflow: Math.max(0, Math.round(-top)), + bottomOverflow: Math.max(0, Math.round(bottom - H)), + bottomNode: bottomNode ? labelFor(bottomNode) : '', + topNode: topNode ? labelFor(topNode) : '', + safeBottom, + }, + titleGaps: titleGapChecks(el, nodes), + }; + }); + }); + + for (const m of measures) { + const prefix = `Slide ${m.idx}${m.layout ? ` (${m.layout})` : ''}`; + if (m.scrollOverflow > 4) { + errors.push(`${prefix}: M1 DOM overflow ${m.scrollOverflow}px. ${overflowFix(m.scrollOverflow)}.`); + } + if (m.visual.bottomOverflow > 4) { + errors.push(`${prefix}: M1 visual bottom overflow ${m.visual.bottomOverflow}px; lowest element is ${m.visual.bottomNode}. ${overflowFix(m.visual.bottomOverflow)}.`); + } + if (m.visual.topOverflow > 4) { + errors.push(`${prefix}: M1 visual top overflow ${m.visual.topOverflow}px; highest element is ${m.visual.topNode}. Move content down by the measured overflow plus 16-24px.`); + } + if (m.visual.bottom > m.visual.safeBottom) { + warnings.push(`${prefix}: M1 content reaches ${Math.round(m.visual.bottom)}px; nav-safe line is ${m.visual.safeBottom}px. Lift the lowest block or add .nav-safe-bottom.`); + } + if (m.visual.bottomGap > 170 && m.visual.activeRatio < 0.74) { + warnings.push(`${prefix}: M1 bottom whitespace ${m.visual.bottomGap}px; active content height ${Math.round(m.visual.activeRatio * 100)}%. Restore spacing/content instead of over-correcting overflow.`); + } + for (const gap of m.titleGaps) { + warnings.push(`${prefix}: M2 ${gap.title} has ${gap.gap}px gap before ${gap.next} (min ${gap.minGap}px).`); + } + } + } finally { + await browser.close(); + } +} + +await runRenderedMeasurements(); + if (warnings.length) { console.warn('Warnings:'); for (const warning of warnings) console.warn(`- ${warning}`); diff --git a/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json b/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json index 05bd12ea..67cbbe25 100644 --- a/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json +++ b/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json @@ -3,5 +3,5 @@ "name": "playwright浏览器自动化操作", "version": "20260605", "keySource": "none", - "syncedAt": "2026-07-21T16:01:43Z" + "syncedAt": "2026-07-22T16:02:51Z" } diff --git a/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json index 8bea2def..ad1214d0 100644 --- a/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "next-skills", "repo": "https://github.com/vercel/next.js.git", "ref": "canary", - "commit": "93c59f02f81cfdeb0fc397412897024ae7c68074", + "commit": "63f14c6c90c4dae819966e180491eb9b0af16792", "adapter": "skill-collection", "sourcePath": "skills", - "syncedAt": "2026-07-21T16:00:00Z" + "syncedAt": "2026-07-22T16:00:00Z" } diff --git a/plugins/codex/plugins/next-skills/skills/next-cache-components-adoption/SKILL.md b/plugins/codex/plugins/next-skills/skills/next-cache-components-adoption/SKILL.md index 0c2e5fee..9a5d45d7 100644 --- a/plugins/codex/plugins/next-skills/skills/next-cache-components-adoption/SKILL.md +++ b/plugins/codex/plugins/next-skills/skills/next-cache-components-adoption/SKILL.md @@ -11,11 +11,13 @@ Enable Cache Components on an app and walk it to a passing build. This skill seq - **App Router project.** Cache Components is an App Router feature; `cacheComponents: true` does nothing for `pages/` routes. If the project has a `pages/` or `src/pages/` tree but no `app/` or `src/app/` tree, stop and tell the user — Pages → App migration is its own project, not part of this skill. A hybrid app (both `pages/` and `app/`) is fine: the flag affects the `app/` routes; `pages/` routes are unaffected and don't need opt-outs. +- **A runnable app.** The whole loop verifies against `next dev` and a browser, so the app has to boot. If it reads a database or required env at import (e.g. an `env.ts` that throws on a missing `DATABASE_URL`), confirm it actually starts — with the real environment, or local data you stand up — before step 1. Adoption can't be verified against an app that won't run. + - **Next.js 16.3 or later.** That release is where the pieces this skill relies on land: top-level `cacheComponents`, `export const instant`, the dev-overlay instant-navigation validation warnings, and the `cache-components-instant-false` codemod. If `next --version` reports below 16.3, upgrade first: - `npx @next/codemod@latest upgrade latest` to apply the version-to-version codemods. - Read the relevant [version upgrade guide](https://nextjs.org/docs/app/guides/upgrading) (e.g. [Version 16](https://nextjs.org/docs/app/guides/upgrading/version-16)) for what the codemod doesn't cover. -- **No incompatible config keys.** `cacheComponents: true` errors on any file that still exports `dynamic`, `revalidate`, or `fetchCache`. **Translate, don't delete.** Each export encodes behavior the route needs to keep doing; migrate each one to its Cache Components equivalent via the [migration guide's per-key sections](https://nextjs.org/docs/app/guides/migrating-to-cache-components#enable-cache-components). If a value can't be cleanly translated yet, leave a `// TODO: Cache Components adoption — restore revalidate = 3600` comment so the loop picks it up. The `cache-components-instant-false` codemod does not touch these. +- **No incompatible config keys.** `cacheComponents: true` errors on any file that still exports `dynamic`, `revalidate`, or `fetchCache`. **Translate, don't delete.** Each export encodes behavior the route needs to keep doing; migrate each one to its Cache Components equivalent via the [migration guide's per-key sections](https://nextjs.org/docs/app/guides/migrating-to-cache-components#enable-cache-components). The exception is `dynamic = 'force-dynamic'`: under Cache Components every route is already dynamic by default, so the migration guide removes it outright rather than translating it — don't overthink a batch of identical `force-dynamic` deletions. `revalidate` and `fetchCache` still need real translation. If a value can't be cleanly translated yet, leave a `// TODO: Cache Components adoption — restore revalidate = 3600` comment so the loop picks it up. The `cache-components-instant-false` codemod does not touch these. - **`experimental.dynamicIO` is fatal.** It was renamed to top-level `cacheComponents` and the old key now aborts before any build can run — remove it (or replace with `cacheComponents: true`) first. `experimental.useCache` is still accepted as a deprecated alias; redundant once `cacheComponents: true` is set, so remove it for clarity. @@ -36,7 +38,7 @@ The choice in step 1 is whether to opt every route out of validation first or fi - **With a quiet pre-step (Incremental).** Run the codemod to opt every page and layout out of validation. Once you've also fixed what the codemod can't (sync-IO calls, leftover `revalidate`/`dynamic`/`fetchCache` exports), the build passes; you ship that as its own PR and then start the loop — removing one opt-out at a time and adopting that route. This splits the work into small, reviewable PRs. - **Without (Direct).** Enable `cacheComponents` and start the loop on whatever the build flags first. Same loop, but every fix sits on one branch until adoption is complete. -In both, the per-route success bar is the same: **dev loop reports no errors AND `next build` passes**. Check in with the user after every feature. Expect to spend most of the time in the loop, not in the pre-step. +In both, the per-route success bar is the same: **dev loop reports no errors AND `next build` passes**. Check in with the user after every feature, and suggest a commit but never make one without their confirmation. Expect to spend most of the time in the loop, not in the pre-step. ## background @@ -73,7 +75,7 @@ In preference order: npx skills add https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop ``` - The skill requires `agent-browser >= 0.27.0` and walks you through it. + The skill states its required `agent-browser` version and walks you through it. **Requires Turbopack.** If `package.json`'s `dev` script passes `--webpack`, flag it to the user and ask whether there's a reason to stay on webpack. If not, switch to Turbopack (the Next.js 16.3+ default). If they want to keep webpack, skip this install and use the [build-only loop](#the-loop-build-only-fallback) instead. @@ -83,7 +85,7 @@ In preference order: 3. **Build-only.** If you can't run a dev server at all, the build is your only signal. `○ (Static)` routes with no `` are fully verified by the build (nothing streamed to test). `◐ (Partial Prerender)` routes are only shell-verified — flag them when you report back. -4. **No tooling at all.** Ask the user to run the dev server (or build) and report what they see, or commit the milestone you've reached and hand off. +4. **No tooling at all.** Ask the user to run the dev server (or build) and report what they see, or hand off the milestone you've reached. ## step 1: choose a strategy @@ -134,7 +136,7 @@ Confirm the pre-step with `next build`. The build is the proof, not the codemod After the build passes, confirm the root layout got an opt-out (`grep -n "export const instant" app/layout.*`). The root layout renders every route, including framework routes like `/_not-found`, so if it was missed, add `export const instant = false` to it by hand. -Synthetic routes like `/_not-found` have no user file — when they block, fix the root layout's opt-out, not the synthetic route. Client Components (`"use client"`) get no opt-out (it's a build error — `E1344` — to export `instant` from them) and rarely block on their own; when a client route blocks, fix the server-side data in its ancestor layout. +Synthetic routes like `/_not-found` have no user file — when they block, fix the root layout's opt-out, not the synthetic route. Client Components (`"use client"`) get no opt-out (it's a build error — `E1344` — to export `instant` from them), but they are not a rare blocker. The high-frequency case is a client component in the root layout's nav or header calling `usePathname()`/`useSearchParams()`: it blocks _every_ dynamic route with `blocking-prerender-client-hook`, and static routes pass (the pathname is known at prerender), which masks it until you reach a dynamic segment. It's not an ancestor-data fix — follow the [error's docs page](https://nextjs.org/docs/messages/blocking-prerender-client-hook) for the `` recipe. Only when a client route blocks on _server_ data do you fix that data in its ancestor. ### end of the pre-step: check in @@ -221,8 +223,7 @@ When the loop has run on every feature — every remaining `instant = false` sit The work below is optional and lives in the docs — link the user to them and let them decide which to take on next. Don't walk these through inside this skill. -- [Instant navigation](https://nextjs.org/docs/app/guides/instant-navigation) — dev-only validation warnings the overlay raises on client navigation. Same shape as the blocking-prerender errors you cleared in step 2; the guide covers the per-warning details. Recommend it next if the user wants navigations to actually be instant (a passing build doesn't guarantee that — a `` above the shared layout caught the page-load case but doesn't cover client navigation). -- [`next-partial-prefetching-adoption`](https://github.com/vercel/next.js/tree/canary/skills/next-partial-prefetching-adoption) — the follow-up skill that adopts Partial Prefetching: it audits `` calls (driven by the dev overlay's `link-prefetch-partial` warning) with the flag off first, then flips the `partialPrefetching` config. It sequences this the same way this skill sequences Cache Components, but the insights are dev-only, so it's a browser click-through, not a build loop. Recommended after instant navigation, since those fixes feed directly into how much of each route the shell can prefetch. Concepts live in the [Adopting Partial Prefetching guide](https://nextjs.org/docs/app/guides/adopting-partial-prefetching). -- [Prefetching](https://nextjs.org/docs/app/guides/prefetching) and [Runtime prefetching](https://nextjs.org/docs/app/guides/runtime-prefetching) — broader prefetching reference. Runtime prefetching extends the static shell with per-session content; reach for it when a route's shell is too thin to be useful and Partial Prefetching alone doesn't cover the gap. +- [Sweep for more instant navigations](./references/dev-only-validations.md) — an optional follow-up once adoption is done, never required. A passing build is not the last word, because dev validates every route on each page load (simulating both page loads and client navigations) and catches what the build's first-error exit and descendant shadowing skipped. Offer it as the smaller path to instant navigation for a user who doesn't want to adopt Partial Prefetching. Adopting Partial Prefetching (below) runs the same kind of loop and meets these insights anyway, so recommend both and let the user pick which, or whether. The reference is the loop to execute. +- [`next-partial-prefetching-adoption`](https://github.com/vercel/next.js/tree/canary/skills/next-partial-prefetching-adoption) — the follow-up skill that adopts Partial Prefetching: it enables `partialPrefetching` and audits every `` against a decision table (or adopts incrementally with the flag off, driven by the `link-prefetch-partial` insight). It sequences this the same way this skill sequences Cache Components, but the insights are dev-only, so it's a browser click-through, not a build loop. Recommended after instant navigation, since those fixes feed directly into how much of each route the shell can prefetch. Concepts live in the [Adopting Partial Prefetching guide](https://nextjs.org/docs/app/guides/adopting-partial-prefetching). - [Prevent regressions with e2e tests](https://nextjs.org/docs/app/guides/instant-navigation#prevent-regressions-with-e2e-tests) — the `@next/playwright` [`instant()`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/instant#testing-instant-navigation) helper asserts on the UI that's available immediately on navigation, so regressions surface in CI. Recommend it once a route is instant: `next-dev-loop` confirms it _now_; an `instant()` test keeps it that way. - [`next-cache-components-optimizer`](https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-optimizer) — a separate skill that grows each route's static shell so more of the page prerenders and less streams in. Pure optimization, not part of adoption. diff --git a/plugins/codex/plugins/next-skills/skills/next-cache-components-adoption/references/dev-only-validations.md b/plugins/codex/plugins/next-skills/skills/next-cache-components-adoption/references/dev-only-validations.md new file mode 100644 index 00000000..f65c4a19 --- /dev/null +++ b/plugins/codex/plugins/next-skills/skills/next-cache-components-adoption/references/dev-only-validations.md @@ -0,0 +1,29 @@ +# dev-only validation sweep + +How to surface and fix the instant-navigation insights that a clean `next build` does not show. The [Instant navigation guide](https://nextjs.org/docs/app/guides/instant-navigation) is the canonical reference for the validation model this exercises. + +## what the build misses + +By default (`validationLevel: 'warning'`) Cache Components validates every Page and Default segment in `next dev`, and the insights land in the dev overlay's Insights tab, not the build. Validation runs on every page load using the real request, and for each route it independently checks the initial page load and client navigations at different points in the hierarchy. So a `` boundary that covers the page load can still leave a client navigation blocking, and a layout stays clean at build time while a descendant keeps its `instant = false`. The build stops at the first blocking route and does not raise this family by default. Loading each route in dev is what surfaces it. + +## when to run it + +After the Cache Components build is clean, not before. While the app is mid-adoption the build redboxes mask this, so a full clean build (every route `◐`, no errors) is the precondition. A quiet sweep is the expected result of a clean adoption, not a missing signal. + +## the loop + +Reuse the [`next-dev-loop`](https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop) preflight (Turbopack), then add one job. On a webpack app, drive a browser directly with `agent-browser` or Playwright instead. You lose the `/_next/mcp` cross-checks, not the insights, which still show in the overlay and the dev log. + +1. Build a route queue from the last build's route table or the `app/` tree. +2. Load each route in `next dev` with a browser. A refresh or a link click both work, and validation simulates both the page-load and client-navigation cases on that load, so you do not need to click through every link by hand. Dynamic params are checked against the real values you visit, so hit a concrete `[slug]`, not the pattern. +3. Watch the dev log and the Insights tab. The dev log is the greppable record, one `Error: Route "...": Next.js encountered ...` line per insight with its `docs/messages/` link, and it reads the same on Turbopack and webpack. The Insights tab is amber and appears only once an insight fires, so a route with no tab is clean. Through `next-dev-loop`'s `/_next/mcp`, these come from `get_errors` and the overlay, not `get_request_insights`, which is the performance recorder and reports nothing here. +4. Open the linked page for each distinct insight and apply its fix (usually pull a `` boundary down to the read). Reload to confirm it clears. + +## gotchas + +- The overlay renders inside a shadow root (`nextjs-portal`), so accessibility-tree snapshots miss it. Read it through `shadowRoot`. +- No browser, no sweep. There is no build-only fallback for this family. Apply the static fix you can from the docs page (gate on type-check) and hand off the live confirmation. + +## when this shrinks + +Build-time instant validation is opt-in today (`experimental.instantInsights.validationLevel: 'experimental-error'`); the default `'warning'` surfaces in the overlay only. Once the build raises these reliably, the sweep collapses into reading `next build` output and this reference can shrink to that. diff --git a/plugins/codex/plugins/next-skills/skills/next-cache-components-adoption/references/per-page-decisions.md b/plugins/codex/plugins/next-skills/skills/next-cache-components-adoption/references/per-page-decisions.md index d43a20e9..4e57a733 100644 --- a/plugins/codex/plugins/next-skills/skills/next-cache-components-adoption/references/per-page-decisions.md +++ b/plugins/codex/plugins/next-skills/skills/next-cache-components-adoption/references/per-page-decisions.md @@ -10,7 +10,9 @@ If you're unsure which fix fits, the right call usually depends on what this par ## security gates and other code you can't infer -If the blocking code looks like it's there for a _reason you can't infer_ — a security gate at the page top (`await verifyAccess()`, an auth redirect, a feature-flag check) where moving it inside `` would change what the code guarantees — stop and ask the user before refactoring. The build error wants ``, but wrapping a gate in `` defeats the gate. Only the person who wrote it knows whether to keep the route blocking (`instant = false` as a documented Block), restructure the page so the gate runs differently, or move the check to [Proxy](https://nextjs.org/docs/app/api-reference/file-conventions/proxy). +If the blocking code looks like it's there for a _reason you can't infer_ — a security gate at the page top (`await verifyAccess()`, an auth redirect, a feature-flag check) where moving it inside `` would change what the code guarantees — stop and ask the user before refactoring. The build error wants ``, but wrapping a gate in `` defeats the gate. Only the person who wrote it knows whether to keep the route blocking (`instant = false` as a documented Block), restructure the page so the gate runs differently, move the check to [Proxy](https://nextjs.org/docs/app/api-reference/file-conventions/proxy), or — if the gate duplicates protection the app already relies on elsewhere (platform auth such as Vercel Deployment Protection, a Proxy check, a Data Access Layer) — remove it. + +Relocating a read (`await connection()`, Proxy) only changes _where_ it runs, never _whether it should run at all_ — so a gate that's redundant or broken looks identical to a correctly-placed one through the fix card's lens. If a gate looks strange, redundant, or out of place, say so plainly — "this looks like it might be unnecessary here — are you sure it belongs?" — instead of quietly relocating it. Surfacing the doubt is the agent's job; deciding is the user's. If _every_ route under a layout is gated this way, a documented Block on the layout is the correct end state. Moving the gate to [Proxy](https://nextjs.org/docs/app/api-reference/file-conventions/proxy) is the architectural fix, not a Cache Components one, and that's a follow-up rather than something to hold the migration on. diff --git a/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/SKILL.md b/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/SKILL.md index 7cd18b5a..cd4e2958 100644 --- a/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/SKILL.md +++ b/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/SKILL.md @@ -5,124 +5,439 @@ description: "优化 Next.js Cache Components 页面静态壳和导航体验。" # next-cache-components-optimizer -Two loops, shared levers and primitives, different diagnostics: +Set up an agentic optimization loop that drives a Next.js route from "not +instant" to "instant" and keeps it there. The loop is test-driven: encode the +goal as a failing `@next/playwright` `instant()` test, work it to green, and +ship the test as the regression guard. Run it once per target route. Work the +phases P → G in order; each ends in a gate. Fix recipes live in two lazily-read +references — `reference/patterns.md` (before→after for each blocker type) and +`reference/real-app-patterns.md` (parallel routes, auth gates, the empty-shell +and responsive-skeleton failure modes). Read one only when its phase points +there. -- **Page-render loop** ([ppr-loop.md](./ppr-loop.md)) — grow the static shell of a single page. Rank Suspense fallback areas on a shell-only render. -- **Nav loop** ([instant-nav-loop.md](./instant-nav-loop.md)) — when the user clicks a link from A to B, show B's static layout immediately (chrome, structure, content-shaped fallbacks) instead of holding A's UI until B's data resolves. Capture B's suspended boundaries post-`pushstate`, classify each by `suspended_by[].name`, drop SSR-only client hooks. +## What is invariant, and what is yours -Pick one and run it end-to-end. +One thing here is fixed. The rest is yours. Read this before treating any +command, platform, or env var below as a requirement. -## requires +- **Invariant: the verification loop.** Maximizing the shell is worthless + unless you can prove it. The proof is an automated check: under a lock that + gates dynamic data, the static shell still commits. RED shows the gap, GREEN + shows it closed, the test ships as the regression guard. It must run on a + production-like build and must not be able to pass vacuously. Stand the loop + up once; every later optimization is then verifiable by construction. The + loop is the deliverable, not any one route. +- **The mechanism: `@next/playwright` `instant()`.** This skill locks with + [`instant()`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/instant#testing-instant-navigation): + a ruler, not a stopwatch (phase A). It comes from + `@next/playwright` (installed alongside `@playwright/test`, on the same + release line as `next`), so it isn't tied to any host. Keep it. Timing a + navigation by hand is too flaky to trust, and is the failure mode this skill + exists to prevent. +- **Yours: the rig.** How you build, deploy, authenticate, configure + Playwright, and loop belongs to your stack, not to this skill. A local + `next build && next start`, a CI/staging container, and a per-push preview + deploy are equally valid rigs; the verdict comes from the build, never the + platform. Phase 0 maps the invariant onto your repo. Read every platform + name, env-var spelling, and command below as an example to translate, not a + requirement. -- `next-dev-loop` initiated for this session — it opens the headed browser, exposes the `agent-browser` CLI, and wires the dev MCP server that provides `mcp get_logs`. -- `cacheComponents: true` in `next.config.ts`. Refuse otherwise. +## Two navigations, two loading states -## preflight (shared) +A route reaches the user two ways, and both must be instant: -1. Confirm `cacheComponents: true`. -2. **The user must already be at the page each loop needs** in the headed browser (from `next-dev-loop`) — logged in, with any state set up. This skill can't drive auth, SSO, or MFA; it takes the manual setup as the starting point. (Each sub-loop names which page it expects.) -3. `agent-browser get url` to anchor the current route. +- **Initial load (hard navigation)** commits the route's prerendered static + shell; deferred parts stream in behind their loading skeletons (Suspense + fallbacks, `loading.tsx`). +- **Client-side navigation (soft navigation)** commits the destination's + prefetched App Shell — the `` default under Partial Prefetching — + re-rendering only the segments that change. -Each loop sets the instant cookie as needed (see the shared `instant cookie` section below). +The fix patterns are identical for both; the test differs only in how the +navigation is driven ("Driving the navigation in tests" below). The two shells +can differ; guard the one you ship, both when both matter +(`reference/real-app-patterns.md`). -## instant cookie (shared) +## Goal -Both loops use the `next-instant-navigation-testing` cookie to freeze the framework's dynamic-data writes. Once set, visible content on the page is the static shell + Suspense fallbacks — that's what we capture to assess the optimization. +Maximizing the static shell is the optimization objective: the most meaningful +prerendered content commits immediately, and only genuinely per-request data +streams in afterward. The shipped test deterministically encodes **present ∧ +instant**; **non-blank** is the additional bar the workflow enforces by +judgment (D1/D2/E), because an `instant()` pass alone is satisfied by a blank +`fallback={null}` shell (the empty-shell failure mode, +`reference/real-app-patterns.md`). -Set it with a pending-lock tuple `[0, ""]`. The id is any unique string; the convention is a `p`-prefixed random stamp so concurrent scopes don't collide: +`instant()` is a ruler, not a stopwatch: assert that the shell appears under +the lock; do not time it. A trustworthy verdict requires a production build +(phase A). + +The GREEN under the lock is the deterministic verdict; each gate keeps it +trustworthy. + +## Reporting to the user + +This loop is meant to run unattended — ideally across many navigations in one +pass — so it doesn't stop to ask after each route. What matters is how you word +and present the results, not how often you interrupt. The mechanics below — the +rig, RED, GREEN, the gates — are your scaffolding; the user never needs to hear +those words. + +- **Speak their language.** Describe the gap and the result in terms of what the + user sees: "navigating to the dashboard waited on the charts query before + anything painted; now the layout and skeletons paint instantly and the charts + stream in" — not RED/GREEN, the lock, or the phase letters. +- **Show, don't tell.** When you report a route, drive the browser (or attach + before/after screenshots) so the user watches the shell commit immediately and + the data stream in, rather than reading a claim. Identical before and after + means the fix did nothing — roll it back. +- **Present a run as a list of results,** one line per navigation — which route, + what's now instant, what streams — not a transcript of the loop. +- **Only surface a question for a genuine fork:** a fix that would change + behavior, a security-sensitive read, or a route that's dynamic by design (a + runtime-prefetch candidate, not a shell to grow). A clean instant fix is not a + fork — keep going. With no one to ask (an unattended run), don't block: take + the safe default and note the assumption — for a cache-freshness choice, + defer the read behind `` (always fresh, still instant) rather than + guess a `cacheLife`. + +## The workflow ``` -agent-browser cookies set next-instant-navigation-testing '[0,"p"]' \ - --url +- [ ] P PREREQS Next.js 16.3+ with cacheComponents: true; upgrade first → below +- [ ] 0 SETUP once per repo: discover + write instant-nav.rig.md → rig-template.md +- [ ] A RIG production build with the testing API exposed → below +- [ ] B BASELINE unlocked: the marker renders for the test user → test-template.md +- [ ] C RED locked instant(): the shell does not commit → test-template.md +- [ ] C-gate VERIFY-RED: stop until the RED is trustworthy → reference/red-test-robustness.md +- [ ] D FIX push each Suspense boundary down to the data it guards → reference/patterns.md +- [ ] D1 reuse the route's existing loading UI; do not hand-build skeletons +- [ ] D2 the shell matches the real render at every breakpoint → reference/real-app-patterns.md +- [ ] E PARITY the refactor changed only whether the route is instant +- [ ] F DIFFERENTIAL revert only the fix → RED; re-apply → GREEN → reference/red-test-robustness.md +- [ ] G REVIEW PR checklist (below) ``` -Each loop's preflight specifies when to set it within the flow. Clear it at the end (see `teardown` below). - -## decide which loop - -- **Page-render** when the complaint is about one route's initial load. Read [ppr-loop.md](./ppr-loop.md). -- **Nav** when it's about navigating between two routes. Read [instant-nav-loop.md](./instant-nav-loop.md). - -Ambiguous → ask. - -## shared refactor levers - -- **Push down** — extract I/O into a Suspense-wrapped child so the parent stays static and static siblings lift into the shell. - - **Recurse, don't blind-wrap.** If a Suspense boundary already wraps a component containing both static content and the I/O, read inside, extract the I/O-dependent JSX into a new leaf, and lift the static siblings up. -- **Cache** — `'use cache'` + `cacheLife()`. Always ask the user for freshness; map to a preset (`seconds` / `minutes` / `hours` / `days` / `weeks` / `max` / `default`). - -Push-down and cache compose: push-down lifts static structure, cache eliminates the remaining data gap. - -## propose via plan mode (shared) - -Each refactor goes through plan mode before applying. Treat this as a signal: the application work is non-trivial agentic engineering, not a templated edit. This skill provides the framework — which lever to reach for, which candidate to fix, what the expected visible delta is — but the real work (which file to edit, how to cleanly extract the I/O, where to place the new Suspense boundary, which `cacheLife` profile to ask the user for) is a judgment call you have to think through. Plan mode forces a coherent proposal before touching code, and gives the user a chance to redirect on any of those decisions. - -## no-shell bailout (shared) - -The levers presume a shell exists to grow or cache toward. If the route is fully blocking — HTTP 500 with `blocking-route` or `NEXT_STATIC_GEN_BAILOUT` in `mcp get_logs`, or zero Suspense boundaries on a visibly-rendered page — there's no shell. Surface the structural blocker and stop; the user has to wrap the offending dynamic access in `` before either loop can help. - -## verify requires a visible delta (shared) - -Each loop captures a baseline screenshot of the shell before applying any change, then re-screenshots after. Report both paths in the final summary so the user can see what changed. The two captures must visibly differ — fallback area shrunk, content promoted to the static surface, target fallback gone or content-shaped. Identical-looking captures mean the refactor didn't land; undo. "Compiles cleanly" is not the bar. - -**Hide the dev overlay before each screenshot.** The Next.js dev overlay (`` at the document root) renders instant-nav guidance, build errors, and other dev chrome that pollute the before/after comparison. Hide it, screenshot, restore: - -``` -agent-browser eval "document.querySelector('nextjs-portal').style.display='none'" -agent-browser screenshot -agent-browser eval "document.querySelector('nextjs-portal').style.display=''" -``` - -## anti-patterns (shared) - -**Don't replace granular Suspense boundaries with a top-level loading skeleton.** A `loading.tsx` for the whole segment, or a root-level `}>` (or worse, `fallback={null}` that blanks the UI), defeats this skill's optimization — which is to extract real static chrome above each granular boundary and use content-shaped fallbacks per region. A coarse "the page is loading" stand-in bypasses the work entirely. - -## gotchas (shared) - -- Dev doesn't prefetch the way production does, and routes compile on first hit — so after a navigation or reload, the DOM keeps updating for noticeably longer than the eventual production experience. Wait patiently for the DOM to stabilize before capturing the React tree or taking a screenshot — e.g., poll `document.documentElement.innerHTML.length` until it's unchanged across two consecutive reads. A fixed short delay risks sampling mid-render. -- Don't try to verify nav prefetch by inspecting dev network traffic — dev doesn't fire prefetch requests at all, so the network tab, manual `router.prefetch()` calls, and `` will all look broken regardless of whether your code is correct. The cookie-locked SPA-nav recipe in [instant-nav-loop.md](./instant-nav-loop.md) under `verify` is already the canonical recipe for this — it simulates what production would prerender into the prefetched RSC without requiring prefetch to actually fire. Use it; don't invent a network-tab alternative. -- The diagnose pipeline can be flaky — DevTools attachment timing, DOM-settle races, and dev compilation effects can each produce inconsistent captures from one run to the next. When a result feels off (a candidate appears that you don't expect, or one you expect doesn't), re-run the diagnose 2–3 times and cross-check; boundaries that appear consistently are real, one-off appearances are noise. - -## reference (shared primitives) - -``` -agent-browser react suspense add --only-dynamic to filter ---json server-side to actually- - suspended boundaries. Each - entry has jsx_source + - suspended_by[] with raw blocker - names (usePathname, cookies, - fetch, cache, ...); classify by - name for per-loop rules - -POST /__nextjs_original-stack-frames body { frames: StackFrame[], - isServer, isEdgeServer, - isAppDirectory }; returns one - result per frame with - file:line:column - -mcp get_logs dev MCP tool from - next-dev-loop; surfaces - blocking-route / - NEXT_STATIC_GEN_BAILOUT 500s - -cacheLife('') default | seconds | minutes - | hours | days | weeks | max -``` - -Per-loop primitives in [instant-nav-loop.md](./instant-nav-loop.md). - -## teardown (shared) - -Delete the cookie by name — overwrite with an expired stamp: - -``` -agent-browser cookies set next-instant-navigation-testing x \ - --url --expires 1 -``` - -Never `agent-browser cookies clear` (no args) — wipes auth. +Phases B and C build the test; only the locked test from C ships. --- -Sibling of `next-dev-loop` — initiate that first. +## P. PREREQUISITES: current Next.js with Cache Components + +The workflow depends on framework capabilities that ship with current Next.js: + +- **Next.js 16.3+ with `cacheComponents: true`** in `next.config.ts`. Without + Cache Components there is no static shell to optimize. +- **`@next/playwright`** on the same release line as the project's `next`; it + provides `instant()`. Verify with `npm ls next @next/playwright` (or the + project's package manager) and align them if they differ. The matching + testing API is in the `next` runtime, gated by the + `experimental.exposeTestingApiInProductionBuild` config flag (phase A). + +If the project does not meet these, upgrade first (`npx @next/codemod upgrade` +automates most of it), then enable Cache Components in `next.config.ts`: + +```ts +export default { cacheComponents: true } +``` + +Enabling the flag surfaces the blocking routes to resolve first; the +[`next-cache-components-adoption`](https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-adoption) +skill drives that adoption. Reach for this optimizer once the app builds under +Cache Components. + +This gate is deliberate: the skill targets current Next.js, and none of the +verdicts below are meaningful on older versions. + +## 0. SETUP: discover this project's rig, once per repo + +The principles in this skill are fixed; the infrastructure they run on is +yours. On first use in a repository, discover how the project builds, deploys, +authenticates, and tests (inspect the repository first, and ask the user only +what it cannot answer), then write the answers to a committed +`instant-nav.rig.md`. Every later run reads that file instead of +rediscovering. The six questions (BUILD / EXPOSE / RUN / TEST USER / DRIFT / +LOOP), the file template, and filled examples (local-only, generic CI + +container, preview deploy) are in **`rig-template.md`**. + +If the repo has no Playwright e2e harness yet, standing up a minimal one +(`@next/playwright`, a config with `baseURL`, one authenticated path) is part +of this step; the loop does not assume a pre-existing suite. + +## A. RIG: a production build with the testing API exposed + +Stand up the rig described by `instant-nav.rig.md`. Two invariants hold on +every platform: + +1. **Never measure on `next dev`.** It does not prefetch, and its lock is + unreliable for blocking routes, so a dev `instant()` result is not a valid + RED or GREEN. +2. **The measured build must expose the testing API.** Otherwise `instant()` + silently no-ops and the test passes vacuously (see + `reference/red-test-robustness.md`). The lock-engagement proof is the phase-C + RED itself: the unfixed target route is the known-blocking route, and its + RED under the lock shows the lock engages on this build (C-gate); the + self-validating variant in `test-template.md` is the in-band guarantee. Wire + `experimental.exposeTestingApiInProductionBuild` to a condition that is + true for every build you measure and never true in production: + + ```ts + experimental: { + // Use the condition your platform provides, and record it in the rig file: + // local: an explicit opt-in, as below + // generic CI: process.env.DEPLOY_ENV === 'staging' + // Vercel: process.env.VERCEL_ENV === 'preview' + exposeTestingApiInProductionBuild: + process.env.EXPOSE_TESTING_API === '1', + } + ``` + +The rig is any production-like build that exposes the testing API: a local +`next build && next start`, a CI/staging container, and a preview deploy are +all equally valid; the verdict comes from the build, not the platform. See +`rig-template.md` for filled examples. + +For any deployed or remote build, poll the rig's LIVENESS probe to confirm the +artifact contains `HEAD` before trusting a verdict (a stale deploy reads as a +false RED or GREEN); a local `next build && next start` needs none. The probe +mechanism is in `rig-template.md` (question 6). + +## B. BASELINE (unlocked): development scaffold, do not ship + +Drive the real navigation with no `instant()` lock and assert that the +destination's `SHELL_MARKER` renders **as the test user**: the account the +e2e suite authenticates as (in CI, the CI account; locally, your e2e login +fixture), with its flags, plan, role, and data. This establishes that the +marker is real and reachable: not flag-gated, not redirected away, not a +guessed selector. The suite runs as the test account, not the author's session; +that environment drift (the rig DRIFT list) is a common source of +untrustworthy REDs. Scaffold and run command: **`test-template.md`**. +**Delete this baseline before the PR.** + +## C. RED (locked) + the VERIFY-RED gate + +Wrap the same navigation in `instant()`; assert the shell commits under the +lock. A RED here is the gap. **This is the test that ships** +(`test-template.md`). + +> **C-gate: do not start optimizing until the RED is verified trustworthy.** A +> RED that is red for the wrong reason sends you optimizing a route that was +> never broken. + +The question that settles it: **does `SHELL_MARKER` render without the lock, +as the test user?** Answer it by re-running phase B as the test user, not by +adding assertions to the shipped test. The two-branch resolution (No → marker +or environment bug; Yes → genuine gap, proceed to D), the full taxonomy of +untrustworthy REDs, the checklist, and worked cases are in +**`reference/red-test-robustness.md`**. Read it now. + +--- + +## D. FIX: push each boundary down to the data it guards + +**The anti-pattern: one coarse boundary.** A single `` high in the +tree with a page-level fallback has three costs: + +- The layout UI stays out of the static shell: only a throwaway copy of it is + prerendered. +- The entire subtree is replaced when the boundary resolves, which discards + client state and shifts layout. +- The hand-built fallback drifts out of sync as the UI changes, because it + duplicates structure that also exists in the resolved tree. + +**The fix: hoist the static, push the Suspense down.** Render the layout UI +once, synchronously, in the shell, and wrap each await in a boundary scoped to +the single read it guards. Only that leaf streams; the stable ancestors are +reused as-is. + +**Rule:** if an element renders in both the fallback and the resolved tree, +hoist it above the boundary. + +### The most common blocker: a top-level `await` in a layout on a fallback route + +``` +app/[locale]/(app)/[tenant]/dashboard/... + │ generateStaticParams ✅ │ no generateStaticParams → fallback route +``` + +When any dynamic segment in the route lacks `generateStaticParams`, the route +is a fallback route, and **all** params defer to request time, including the +enumerated ones. A top-level `await` in a layout (`await params`, a +request-time session read, an auth gate) then blocks the whole subtree out of +the static shell, even when it reads a statically known param. Minimal shape: a +dynamic-segment route with one segment lacking `generateStaticParams`, plus a +top-level `await` in the layout above it. + +### The fix: defer the gate, render children + +Render `children` unconditionally; move the top-level `await` into a +``-wrapped child. Mechanism and before→after: +`reference/real-app-patterns.md`, "Deferring an auth gate". + +**Fix the page below the shell too, not only the layout.** A page-level +top-level `await` (commonly `await params`) blocks the same way the layout's +does, so make the page sync and push its dynamic reads into a +``-wrapped leaf as well. `fallback={null}` is correct only when a gate renders nothing on +success; for data, the fallback must be a real loading skeleton (see D1). + +Every other blocker shape — `cookies()`/`headers()`, uncached fetch or database +reads, `searchParams`, metadata, viewport, non-deterministic values (`Date.now()`, +`Math.random()`, `crypto.randomUUID()`) — surfaces its own insight when you hit +it: the build prints a `https://nextjs.org/docs/messages/` link. The +default build output is often abbreviated and may carry no usable stack trace; +add `--debug-prerender` for the full failing frame and to report every blocker +past the first. Scope the build to the route you're on with +`next build --debug-build-paths "app//**"` rather than rebuilding the app. +Open that page and apply its recipe; don't improvise from the inline message. + +The before→after recipe for each shape is in `reference/patterns.md`, which maps it to the insight +that explains it. + +A few things those per-error pages don't stress for the instant-navigation goal: + +- **A boundary in the root layout isn't enough for client navigations.** It + passes a page-load check but leaves sibling client navigations blocking; put + the boundary below the lowest layout the source and destination routes share. +- **Keep the LCP element** (usually the main heading) out of any boundary, so it + paints in the shell instead of waiting on a stream. +- **A green check isn't always instant.** `export const instant = false` opts + the segment out of validation while the navigation still blocks, and a + `` above the document `` prerenders an empty shell — neither + makes the route instant. + +### D1: reuse the route's existing loading UI; do not hand-build skeletons + +Before writing any skeleton, search the repository for the loading UI that +already exists for this route, in order: + +1. the route's `loading.tsx`; +2. an exported `*Skeleton` colocated with the component; +3. the fallback already inside the component's own ``. + +The **divergence point** is the lowest layout shared by the source and +destination routes: a soft navigation re-renders only the segments below it, +while an initial load re-runs every layout from the root. (Also called the +shared boundary.) A `loading.tsx` above the divergence point fills only +the initial-load shell; it sits above the soft-nav re-render scope. A +`loading.tsx` at the destination segment is itself the in-tree boundary for a +soft navigation into that segment and serves both. Reuse whichever boundary +actually covers the navigation you are shipping; below the divergence point, +`loading.tsx` and colocated skeletons are interchangeable for that purpose. + +If a component has no skeleton, extract its loading markup into a colocated +skeleton beside it. Do not author a fresh skeleton that mirrors the page +layout: it duplicates structure, drifts as the page changes, and pulls the +design back toward a single coarse boundary. Reusing the component's own +skeleton also keeps the prefetched shell consistent with the loaded UI. + +Exception: if the deferred component renders `null` for some users (for +example, a flag-gated control), `fallback={null}` is correct, since a skeleton +would flash and then collapse. + +### D2: the shell must match the real render at every breakpoint + +A skeleton frozen to one breakpoint misaligns on the others. Fix it the same +way: one responsive component renders both the live UI and the shell (D1 +skeleton in its data slots), so the breakpoint switch happens once. Verify by +re-asserting the shell marker at two widths +(`await page.setViewportSize({ width: 1280, height: 800 })`, then +`{ width: 390, height: 844 }`), or by adding a mobile Playwright project, so +this gate is as machine-checkable as the others. Detail: +`reference/real-app-patterns.md`. + +> **D-gate: phase D is complete when the locked test from phase C passes GREEN +> under the lock on the production-build rig**, not when the code compiles. That +> GREEN is the deterministic stop for the fix loop; proceed to E. + +**When the read can't be pushed down** (an ID minted per request, an +all-dynamic page, a per-request auth/scope read the whole subtree needs), there +is no shell to grow. Don't force one: opt the route into **runtime prefetching** +so the prefetch runs the dynamic render ahead of the click and the soft nav +commits the real content. See [Runtime Prefetching](https://nextjs.org/docs/app/guides/runtime-prefetching) +for the mechanism (`prefetch = 'allow-runtime'` on the route plus a full +``) and the [dynamic-data-during-prefetching insight](https://nextjs.org/docs/messages/instant-link-prefetch-partial) +for adoption. The `instant()`-specific gotchas the docs don't cover: + +- **The full prefetch is mandatory.** An auto/PPR prefetch bails before the + runtime spawn (`subtreeHasSpeculativePrefetch`); only `prefetch={true}` / + `kind: 'full'` reaches it. If you set `prefetch = 'allow-runtime'` and it's + still RED, the link is doing an auto prefetch. +- **All leaf slots must agree.** `allow-runtime` on the content segment but + nothing on a sibling `@header`/`@sidebar` leaf leaves the route's runtime entry + incomplete, so the lock falls back to the shell. Flip every leaf together. +- **Prefetch the canonical URL.** A link whose href 307-redirects can't be + prefetched — the prefetch receives the redirect, not the tree. Point the link + and the prefetch at the final URL. +- **Don't blanket the full prefetch.** It fetches _all_ the target's dynamic + data; issuing it on hover for every link is wasteful. Scope `kind: 'full'` to + the runtime-prefetch targets only. +- **Marker must be a committed node, not RSC bytes.** The content is often a + client component, so its text isn't in the prefetch response. Assert a + `data-testid` that renders when the client subtree commits. + +## E. PARITY: the refactor changed only whether the route is instant + +The push-down is a mechanical transform, not a redesign. Afterward the route +must render the same tree, data, ordering, empty and error states, redirects, +and interactions as before; the only observable difference is that the shell +now commits instantly. Verify: + +- **Same render output.** The moved `await`s compute and return the same + values; after the stream, the route shows the same content as the base + branch for the test user. +- **Side effects still fire.** A deferred `redirect()` or `notFound()` still + happens, at request time rather than during prerender. Confirm an + unauthorized user is still redirected and a missing record still returns 404. +- **Both viewports reach the real UI** after the stream (D2). +- **Client state survives.** Because the layout UI is hoisted into the stable + shell rather than swapped on resolve, open menus, scroll position, focus, + and input state persist across the stream. + +If anything other than whether the route is instant changed, reduce the refactor. + +## F. DIFFERENTIAL + +Revert only the fix → RED; re-apply → GREEN; link both runs +(`reference/red-test-robustness.md`). On a deployed rig, confirm each run is live +(LIVENESS, phase A) before trusting its color. + +## G. REVIEW (PR checklist) + +A green final state means nothing if the RED was never trustworthy. The +test-trustworthiness items are the robustness checklist +(`reference/red-test-robustness.md`); confirm them, then require these +PR-specific items: + +- [ ] **Differential shown**: RED without the fix, GREEN with it, runs linked. +- [ ] **Parity confirmed (E)**: same content, redirects, and state. +- [ ] **Existing loading UI reused (D1)**: no new page-mirroring skeleton. +- [ ] **Shell matches the real render at desktop and mobile widths (D2)**. + +**Stop condition for the whole workflow:** the locked test from C is GREEN on +the rig, the differential (F) holds, and every item above is checked. Until all +three hold, you are not done. + +## Driving the navigation in tests + +- **Soft navigation** → drive a real `` click. **Initial load** → use + `page.goto()` inside `instant()` with the `baseURL` option. Do not substitute + `goto` for a soft-nav verdict; the two shells can differ + (`test-template.md`, `reference/real-app-patterns.md`). +- With parallel routes, only the slots that change re-render on a soft + navigation; client-rendered navigation UI does not re-render at all. Do not + chase a slot the navigation never touches + (`reference/real-app-patterns.md`). + +## Files + +- `rig-template.md`: phase 0, the six-question rig discovery, the + `instant-nav.rig.md` template, and filled examples (local-only, generic CI, + preview deploy). +- `test-template.md`: the shipped `instant()` specs for both navigation + types (phase C), and the delete-before-PR baseline scaffold (phase B). +- `reference/red-test-robustness.md`: the C-gate and phase F. The taxonomy of + untrustworthy REDs, the checklist, the differential recipe, the vacuous-pass + failure mode, and worked cases. +- `reference/real-app-patterns.md`: parallel routes, deferring an auth gate, + initial-load vs soft-navigation shells, the empty-shell failure mode, the + responsive-skeleton mismatch, edge cases. diff --git a/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/instant-nav-loop.md b/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/instant-nav-loop.md deleted file mode 100644 index 474ed750..00000000 --- a/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/instant-nav-loop.md +++ /dev/null @@ -1,89 +0,0 @@ -# instant-nav-loop (sub-reference of next-cache-components-optimizer) - -In-app navigation optimization: when the user clicks a link from A to B, show B's static layout immediately — chrome, structure, content-shaped fallbacks — instead of holding A's UI until B's data resolves. - -Strictly smaller than page-render — only segments newly mounted on B's path need a shell. The LCA layout stays mounted with its already-resolved data, so even a dynamic-root app can have instant in-app nav. - -The hard part is **identifying real blockers**. After `pushstate`, capture B's suspended boundaries with `agent-browser react suspense --only-dynamic --json`. Each boundary carries `suspended_by[].name` — `usePathname` / `useSearchParams` / `useRouter` (client-hook), `cookies` / `headers` / `connection` (request-api), names containing `fetch` or `cache` (server-fetch / cache), etc. - -**Client-hook blockers don't block instant nav.** They suspend only during SSR prerender; on a client nav (`pushstate`) they resolve instantly from the router store. Diagnose drops them as candidates (see step 4 for the operational set), so the loop doesn't recommend SSR-only refactors that have no effect on click-to-paint. - -When the user navigates from A to B, the layouts they share stay mounted; only the segments past the point where the two routes diverge actually load. So B's suspended-boundary capture is naturally focused on the new work — you don't have to filter out the shared parts. With multiple real candidates, fix the one highest in the new-segments tree (closest to where A's and B's paths diverge) first. - -Complementary to Next.js's Instant Insights, which checks Suspense _existence_ (structural). This loop checks fallback _quality_ (visual) — closing the `` loophole. - -## preflight (in addition to shared) - -The starting page is **A** — the route the user is navigating _from_. The shared preflight has already anchored A (the user is on it, logged in, with state set up). Now anchor B: - -1. Ask the user to perform the navigation to B in the headed browser — click the link, button, or whatever leads there. Read `agent-browser get url` to confirm B's URL. -2. `agent-browser pushstate ` to return the browser to A. The diagnose loop starts from A. - -Set the instant cookie (per shared `instant cookie` section) any time after the browser is on A but before calling `pushstate`. There's no race — the cookie only needs to be present at the moment of navigation. The cookie doesn't block the navigation; it gates the framework's dynamic-data writes, so B's React tree mounts normally but its dynamic Suspense boundaries stay in fallback until the cookie is cleared. (Setting the cookie before a direct load of A or B would freeze that page at its static shell — that's why we wait until the user is on A first.) - -## loop - -### diagnose - -1. **Set the instant cookie and navigate via `pushstate `.** Wait for the DOM to settle. B's tree is now mounted; its dynamic boundaries stay in fallback while the cookie holds. - -2. **Check B for the no-shell bailout** per SKILL.md. - -3. **Capture B's suspended set.** `agent-browser react suspense --only-dynamic --json` → boundaries with `suspended_by[]`. - -4. **Filter to real nav candidates.** Drop boundaries whose `suspended_by` entries are **all** client-hook names (`usePathname`, `useSearchParams`, `useRouter`, `useSelectedLayoutSegment(s)`, `useParams`) — SSR-only, won't help nav. Keep boundaries with at least one request-api, server-fetch, or cache blocker. - -5. **Gauge the gap.** - - No candidates → ask the user if the nav still feels slow. No → stop and offer to audit other A → B pairs. Yes → step 8 (unwrapped async). - - Candidates present → screenshot the locked B state (hide `nextjs-portal` per the shared rule), check rendered area. All sub-viewport → already in good shape; stop. - -6. **Resolve sources** for each remaining candidate via `POST /__nextjs_original-stack-frames` on `suspended_by[].owner_stack` (or `jsx_source` if the stack is empty). - -7. **Pick the highest candidate.** With multiple candidates, fix the one highest in B's new-segments tree — closest to where A's and B's paths diverge. If that candidate wraps the others, recurse: read inside the wrapper to find the I/O that's actually blocking. - -8. **Fallback: unwrapped async.** Reached from step 5 when there are no `react suspense` candidates but the nav still feels slow. The blocker has no `` so it doesn't appear in the capture. Direct-load B (full page navigation, not pushstate; cookie still set); `mcp get_logs` surfaces a `blocking-route` 500 naming the unwrapped I/O. Filter to sources past the point where A's and B's paths diverge; bailouts in shared layouts above are PPR concerns, not nav. - -### decide / apply - -Apply the shared lever rules from SKILL.md; push-down recipes work at layouts too. - -**Nav-only third lever: private cache + runtime prefetch.** For I/O that reads `cookies()` / `headers()` / `searchParams`, shared `'use cache'` won't help — those reads bail to dynamic. Use `'use cache: private'` + `cacheLife({ stale: N })` on **the scope that encloses the request-API read** (see scope rule below), plus `prefetch = 'allow-runtime'` as a route segment config (page or layout export) on the segment that owns the private content. Private-cache results live only in the browser — **never stored on the server** — so allowing runtime prefetching lets Next.js resolve them at link-visibility time with the user's session; the click commits with cookie-derived data already in place. - -**Scope rule when cookie-read and data-fetch live in different frames.** The directive's semantics are about the cache scope enclosing the request-API call, not "the I/O function" as a label. If a page reads `cookies()` and passes the value into a separate fetch helper, putting `'use cache: private'` on the helper alone leaves the cookie read outside any cache scope and the segment stays dynamic. Either move the directive up to the frame that reads cookies, or move the cookie read down into the helper. Compiles and typechecks either way — only runtime behavior tells you which is correct. - -The `prefetch` flag applies to the segment plus every descendant — put it on the most ancestral segment with runtime-cacheable content (layout-level covers all child pages; page-level covers only that page). Server Component only (not allowed with `"use client"`); requires `cacheComponents: true`. Valid values: `'auto'` (default) / `'force-disabled'` / `'force-static'` / `'allow-runtime'`. It doesn't make any segment cacheable on its own (each still needs `'use cache: private'`), doesn't help cold loads (no `` to prefetch from), and doesn't override `await connection()`. - -### verify - -**Cookie-locked SPA-nav screenshot** is the canonical visible delta — what production would have prefetched. Recipe (run once before applying, once after): - -1. Be on A. Set the instant cookie. -2. `agent-browser pushstate ` — a real client navigation. -3. Wait for the DOM to settle. With the cookie set, the framework gates dynamic-data writes (see preflight), so the captured state is the static shell + Suspense fallbacks. -4. Hide `nextjs-portal`, screenshot, restore (per the shared rule). - -Compare the two screenshots per the shared visible-delta rule. The after-shot must visibly differ — more static content promoted, content-shaped fallbacks, or (for the private + runtime-prefetch lever) cookie-derived data resolved. - -**Identical before/after is two distinct signals, not one.** Either the lever didn't apply (code is wrong) or no I/O resolved either time (environment is broken — DB unreachable, stale TLS sockets, etc.). Before iterating on the code shape, check `mcp get_logs` for socket timeouts or fetch failures in either capture window. If the data path didn't complete in either run, the comparison is inconclusive — fix the environment and re-capture. - -Also: - -1. **Re-run the diagnose capture.** Repeat steps 1–4 on the new code. The target candidate should be absent from B's suspended set (the blocker is gone), or its fallback should now be content-shaped. -2. Re-check B for the no-shell bailout. - -## reference (loop-specific) - -``` -agent-browser pushstate client-side navigation (no - HTTP request, no full reload) - -'use cache: private' per-session cache; cookies / - headers / searchParams reads ok - -prefetch = 'allow-runtime' route-level: permits runtime - prefetching of private-cached - content when is visible - -instant = false layout-level opt-out; escape - hatch, anti-pattern -``` diff --git a/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/ppr-loop.md b/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/ppr-loop.md deleted file mode 100644 index 75fa9d3a..00000000 --- a/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/ppr-loop.md +++ /dev/null @@ -1,31 +0,0 @@ -# ppr-loop (sub-reference of next-cache-components-optimizer) - -Page-render optimization: grow the static shell of a single `cacheComponents` page. - -Rank candidates by visible pixel area; largest gap first. - -## preflight (in addition to shared) - -The page to optimize is whatever route the user is currently on (per shared preflight). Set the instant cookie (per shared `instant cookie` section), then reload. - -## loop - -### diagnose - -1. **Check for the no-shell bailout** per SKILL.md. - -2. **List candidates.** `agent-browser react suspense --only-dynamic --json` → each boundary has `jsx_source` (file:line:col) and `suspended_by[].name`. Resolve `jsx_source` (or `suspended_by[].owner_stack`) via `POST /__nextjs_original-stack-frames`. - -3. **Rank by rendered area.** Per candidate, take max(fallback rect on shell-only, rendered subtree rect on full). Fallback rect alone misleads when developers used an undersized spinner. - -4. **Gauge the gap.** Same capture as verify — the shell-only render. If the top-ranked candidate is sub-viewport (thin fallback bar, sidebar widget), the shell is already in good shape; surface that and offer to audit other routes for better targets, rather than forcing a marginal refactor. - -5. **One boundary dominates →** that wrapper is the shell. Read inside, enumerate the awaits, recurse with those. - -### decide / apply - -Apply the shared lever rules from SKILL.md. - -### verify - -Re-take the shell-only render and compare against the baseline screenshot. The targeted gap must shrink or vanish; identical captures fail per the shared visible-delta rule. Re-check the no-shell bailout; a botched extract can break the shell. diff --git a/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/reference/patterns.md b/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/reference/patterns.md new file mode 100644 index 00000000..1593f5b9 --- /dev/null +++ b/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/reference/patterns.md @@ -0,0 +1,354 @@ +# Refactor patterns — push dynamic down into the shell + +Each pattern is **before → after**: keep as much as possible in the prerendered shell, and wrap only genuinely per-request work in a tight `` (or hoist it into `use cache`). Production shapes — parallel-route slots, deferring an auth gate, client slot-routers — are in `real-app-patterns.md`. + +--- + +## 1. Awaiting at the top → move the await into a Suspense child + +The most common blocking shape. Awaiting request-time data at the top of a page/layout makes **everything below it** dynamic. + +```tsx +// ❌ before — top-level await of a non-static param + uncached data +export default async function Page(props: PageProps<'/store/[slug]'>) { + const { slug } = await props.params + const product = await db.products.findBySlug(slug) + return ( +
+

{product.name}

+
+ ) +} +``` + +```tsx +// ✅ after — pass the params promise down; await inside a Suspense-wrapped child +import { Suspense } from 'react' + +export default function Page(props: PageProps<'/store/[slug]'>) { + return ( + Loading product…

}> + +
+ ) +} + +async function Product({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params + const product = await db.products.findBySlug(slug) + return ( +
+

{product.name}

+
+ ) +} +``` + +Inline variant when you don't want a separate component — unwrap the promise without awaiting at the top: + +```tsx +export default function Page(props: PageProps<'/store/[category]'>) { + return ( + }> + {props.params.then(({ category }) => ( + + ))} + + ) +} +``` + +**Insight:** [runtime data during prerendering](https://nextjs.org/docs/messages/blocking-prerender-runtime). + +--- + +## 2. `cookies()` / `headers()` in a layout → start, don't await; pass down + +A layout that awaits request data blocks the layout **and every page under it**. + +```tsx +// ❌ before — whole layout (and all children) becomes dynamic +export default async function Layout({ children }) { + const cookieStore = await cookies() + const theme = cookieStore.get('theme')?.value + return {children} +} +``` + +```tsx +// ✅ after — start the read without awaiting, pass the promise to a Suspense child +import { Suspense } from 'react' +import { cookies } from 'next/headers' + +export default function Layout({ children }: { children: React.ReactNode }) { + const cookieStore = cookies() // not awaited → does not block the shell + return ( + + + {children} + + ) +} + +async function UserMenu({ + cookiePromise, +}: { + cookiePromise: ReturnType +}) { + const theme = (await cookiePromise).get('theme')?.value + return
+} +``` + +`{children}` and `