瀏覽代碼

docs: HTML-first 幻灯片架构 + grammar 模板 + moxt 实战经验

幻灯片交付升级为 HTML-first:每页独立 HTML + index.html 聚合永远是
默认基础产物,PDF/PPTX 是从 HTML 一行命令导出的衍生物。

核心变化:
- export_deck_pptx.mjs 删掉 image 模式(168 行→85 行),只保留 editable 路径
- editable-pptx.md 末尾新增 Fallback 章节:已有视觉稿又必须出 editable PPTX
  时,AI 以视觉稿为蓝本重写合规 HTML(不是硬转截图)
- slide-decks.md 新增 3 节:
  · 批量制作前先做 2 页 grammar showcase 的流程规则(≥5 页 deck 强制)
  · 出版物 grammar 模板(masthead + kicker + h1 + italic 副 + footer,moxt 实测)
  · 常见踩坑(emoji 不渲染 / playwright 路径 / 字体加载 / 信息密度)
- SKILL.md 同步更新 checkpoint 和 Starter Components 表格

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
alchain 2 月之前
父節點
當前提交
a75835d3ef
共有 4 個文件被更改,包括 259 次插入138 次删除
  1. 9 4
      SKILL.md
  2. 61 3
      references/editable-pptx.md
  3. 156 38
      references/slide-decks.md
  4. 33 93
      scripts/export_deck_pptx.mjs

+ 9 - 4
SKILL.md

@@ -606,7 +606,12 @@ Screen 组件接 callback props(`onEnter`、`onClose`、`onTabChange`、`onOpe
    - 🔍 **0. 事实验证(涉及具体产品/技术时必做,优先级最高)**:任务涉及具体产品/技术/事件(DJI Pocket 4、Gemini 3 Pro、Nano Banana Pro、某新 SDK 等)时,**第一个动作**是 `WebSearch` 验证其存在性、发布状态、最新版本、关键规格。把事实写入 `product-facts.md`。详见「核心原则 #0」。**这步做在问 clarifying questions 之前**——事实错了问什么都歪。
    - 新任务或模糊任务必须问clarifying questions,详见 `references/workflow.md`。一次focused一轮问题通常够,小修小补跳过。
    - 🛑 **检查点1:问题清单一次性发给用户,等用户批量答完再往下走**。不要边问边做。
-   - 🛑 **幻灯片/PPT 任务额外必问「最终交付格式」**(浏览器演讲 / PDF / 可编辑 PPTX)——**要可编辑 PPTX 就必须从第一行 HTML 开始按 `references/editable-pptx.md` 的 4 条硬约束写**,事后补救会导致 2-3 小时返工。详见 `references/slide-decks.md` 开头「开工前先确认交付格式」一节。
+   - 🛑 **幻灯片/PPT 任务:HTML 聚合演示版永远是默认基础产物**(不管用户最终要什么格式):
+     - **必做**:每页独立 HTML + `assets/deck_index.html` 聚合(重命名为 `index.html`,编辑 MANIFEST 列所有页),浏览器里键盘翻页、全屏演讲——这是幻灯片作品的"源"
+     - **可选导出**:额外询问是否需要 PDF(`export_deck_pdf.mjs`)或可编辑 PPTX(`export_deck_pptx.mjs`)作为衍生物
+     - **只有要可编辑 PPTX 时**,HTML 必须从第一行就按 4 条硬约束写(见 `references/editable-pptx.md`);事后补救会 2-3 小时返工
+     - **≥ 5 页 deck 必须先做 2 页 showcase 定 grammar 再批量推**(见 `references/slide-decks.md` 的「批量制作前先做 showcase」章节)——跳过这步 = 方向错返工 N 次而非 2 次
+     - 详见 `references/slide-decks.md` 开头「HTML 优先架构 + 交付格式决策树」
    - ⚡ **如果用户需求严重模糊(没参考、没明确风格、"做个好看的"类)→ 走「设计方向顾问(Fallback 模式)」大节,完成 Phase 1-4 选定方向后,再回到这里 Step 2**。
 2. **探索资源 + 抽核心资产**(不只是抽色值):读 design system、linked files、上传的截图/代码。**涉及具体品牌时必走 §1.a「核心资产协议」五步**(问→按类型搜→按类型下载 logo/产品图/UI→验证+提取→写 `brand-spec.md` 含所有资产路径)。
    - 🛑 **检查点2·资产自检**:开工前确认核心资产到位——实体产品要有产品图(不是 CSS 剪影)、数字产品要有 logo+UI 截图、色值从真实 HTML/SVG 抽取。缺了就停下补,不硬做。
@@ -701,12 +706,12 @@ Screen 组件接 callback props(`onEnter`、`onClose`、`onTabChange`、`onOpe
 
 | 文件 | 何时用 | 提供 |
 |------|--------|------|
-| `deck_index.html` | **做幻灯片(默认,多文件架构)** | iframe拼接 + 键盘导航 + scale + 计数器 + 打印合并,每页独立HTML免CSS串扰 |
+| `deck_index.html` | **幻灯片的默认基础产物**(不管最终出 PDF 还是 PPTX,HTML 聚合版永远先做) | iframe拼接 + 键盘导航 + scale + 计数器 + 打印合并,每页独立HTML免CSS串扰。用法:复制为 `index.html`、编辑 MANIFEST 列出所有页、浏览器打开即成演示版 |
 | `deck_stage.js` | 做幻灯片(单文件架构,≤10页) | web component:auto-scale + 键盘导航 + slide counter + localStorage + speaker notes ⚠️ **script 必须放在 `</deck-stage>` 之后,section 的 `display: flex` 必须写到 `.active` 上**,详见 `references/slide-decks.md` 的两个硬约束 |
 | `scripts/export_deck_pdf.mjs` | **HTML→PDF 导出(多文件架构)** · 每页独立 HTML 文件,playwright 逐个 `page.pdf()` → pdf-lib 合并。文字保留矢量可搜。依赖 `playwright pdf-lib` |
 | `scripts/export_deck_stage_pdf.mjs` | **HTML→PDF 导出(单文件 deck-stage 架构专用)** · 2026-04-20 新增。处理 shadow DOM slot 导致的「只出 1 页」、absolute 子元素溢出等坑。详见 `references/slide-decks.md` 末节。依赖 `playwright` |
-| `scripts/export_deck_pptx.mjs` | **HTML→PPTX 导出(双模式)** · `--mode image` 图片铺底视觉 100% 保真但文字不可编辑;`--mode editable` 调 `html2pptx.js` 导出原生可编辑文本框,但 HTML 必须符合 4 条硬约束(见 `references/editable-pptx.md`)。依赖 `playwright pptxgenjs`(editable 模式还需 `sharp`) |
-| `scripts/html2pptx.js` | **HTML→PPTX 元素级翻译器** · 读 computedStyle 把 DOM 逐元素翻译成 PowerPoint 对象(text frame / shape / picture)。`export_deck_pptx.mjs --mode editable` 内部调用。要求 HTML 严格满足 4 条硬约束 |
+| `scripts/export_deck_pptx.mjs` | **HTML→可编辑 PPTX 导出** · 调 `html2pptx.js` 导出原生可编辑文本框,文字在 PPT 里双击可直接编辑。**HTML 必须符合 4 条硬约束**(见 `references/editable-pptx.md`),视觉自由度优先的场景请改走 PDF 路径。依赖 `playwright pptxgenjs sharp` |
+| `scripts/html2pptx.js` | **HTML→PPTX 元素级翻译器** · 读 computedStyle 把 DOM 逐元素翻译成 PowerPoint 对象(text frame / shape / picture)。`export_deck_pptx.mjs` 内部调用。要求 HTML 严格满足 4 条硬约束 |
 | `design_canvas.jsx` | 并排展示≥2个静态variations | 带label的网格布局 |
 | `animations.jsx` | 任何动画HTML | Stage + Sprite + useTime + Easing + interpolate |
 | `ios_frame.jsx` | iOS App mockup | iPhone bezel + 状态栏 + 圆角 |

+ 61 - 3
references/editable-pptx.md

@@ -1,8 +1,10 @@
 # 可编辑 PPTX 导出:HTML 硬约束 + 尺寸决策 + 常见错误
 
-本文档讲的是**用 `scripts/html2pptx.js` + `pptxgenjs` 把 HTML 逐元素翻译成真·可编辑 PowerPoint 文本框**的路径。和 `export_deck_pptx.mjs --mode image`(截图铺底、文字变图片、不可编辑)是两回事
+本文档讲的是**用 `scripts/html2pptx.js` + `pptxgenjs` 把 HTML 逐元素翻译成真·可编辑 PowerPoint 文本框**的路径,也是 `export_deck_pptx.mjs` 唯一支持的路径
 
 > **核心前提**:要走这条路,HTML 必须从第一行就按下面 4 条约束写。**不是写完再转**——事后补救会触发 2-3 小时返工(2026-04-20 期权私董会项目实测踩坑)。
+>
+> 视觉自由度优先的场景(动画 / web component / CSS 渐变 / 复杂 SVG)请改走 PDF 路径(`export_deck_pdf.mjs` / `export_deck_stage_pdf.mjs`),**不要**指望 pptx 导出能兼得视觉保真和可编辑——这是 PPTX 文件格式本身的物理约束(见文末「为什么 4 条约束不是 Bug 而是物理约束」)。
 
 ---
 
@@ -225,9 +227,65 @@ const html2pptx = require('../scripts/html2pptx.js');  // 本 skill 脚本
 |------|------|
 | 同事会改 PPTX 里的文字 / 发给非技术人员继续编辑 | **本文路径**(editable,需从头按 4 条约束写 HTML) |
 | 只是演讲用 / 发存档,不再改 | `export_deck_pdf.mjs`(多文件)或 `export_deck_stage_pdf.mjs`(单文件 deck-stage),出矢量 PDF |
-| 视觉自由度优先(动画、web component、CSS 渐变、复杂 SVG),接受不可编辑 | `export_deck_pptx.mjs --mode image`(图片铺底 PPTX) |
+| 视觉自由度优先(动画、web component、CSS 渐变、复杂 SVG),接受不可编辑 | **PDF**(同上)——PDF 既保真又跨平台,比「图片 PPTX」更合适 |
 
-**绝不要在视觉自由写好的 HTML 上硬跑 html2pptx**——实测视觉驱动的 HTML pass 率 < 30%,剩下的逐页改造比重写还慢。
+**绝不要在视觉自由写好的 HTML 上硬跑 html2pptx**——实测视觉驱动的 HTML pass 率 < 30%,剩下的逐页改造比重写还慢。这种场景应该出 PDF,不是硬挤 PPTX。
+
+---
+
+## Fallback:已有视觉稿但用户坚持要 editable PPTX
+
+偶尔会遇到这个场景:你/用户已经写好一份视觉驱动的 HTML(渐变、web component、复杂 SVG 都用上了),本来出 PDF 最合适,但用户明确说「不行,必须是可编辑的 PPTX」。
+
+**不要硬跑 `html2pptx` 期待它 pass**——实测视觉驱动 HTML 在 html2pptx 上 pass 率 <30%,剩下 70% 会报错或走样。正确的 fallback 是:
+
+### Step 1 · 先告知局限性(透明沟通)
+
+一句话跟用户说清三件事:
+
+> 「你现在的 HTML 用了 [具体列出:渐变 / web component / 复杂 SVG / ...],直接转 editable PPTX 会 fail。我有两个方案:
+> - A. **出 PDF**(推荐)——视觉 100% 保留,接收方能看能印但不能改文字
+> - B. **以视觉稿为蓝本,重写一版 editable HTML**(保留色彩/布局/文案的设计决策,但按 4 条硬约束重新组织 HTML 结构,**牺牲**渐变、web component、复杂 SVG 等视觉能力)→ 再导出 editable PPTX
+>
+> 你选哪个?」
+
+不要把 B 方案说得云淡风轻——明确告知**会丢失什么**。让用户做取舍。
+
+### Step 2 · 如果用户选 B:AI 主动改写,不要求用户自己写
+
+这里的 doctrine 是:**用户给的是设计意图,你负责翻译成合规实现**。不是让用户去学 4 条硬约束然后自己重写。
+
+改写时的遵循原则:
+- **保留**:色彩系统(主色/辅色/中性色)、信息层级(标题/副标题/正文/注解)、核心文案、layout 骨架(上中下 / 左右分栏 / 网格)、页面节奏
+- **降级**:CSS 渐变 → 纯色或 flex 分段、web component → 段落级 HTML、复杂 SVG → 简化的 `<img>` 或纯色几何、阴影 → 删除或降为极弱、自定义字体 → 向系统字体靠齐
+- **重写**:裸文字 → 包进 `<p>` / `<h*>`、`background-image` → `<img>` 标签、`<p>` 上的背景边框 → 外层 div 承载
+
+### Step 3 · 产出对照清单(透明交付)
+
+改写完成后给用户一份 before/after 对照,让他知道哪些视觉细节被简化了:
+
+```
+原设计 → editable 版调整
+- 标题区紫色渐变 → 主色 #5B3DE8 纯色背景
+- 数据卡片阴影 → 删除(改为 2pt 描边区分)
+- 复杂 SVG 折线图 → 简化为 <img> PNG(从 HTML 截图生成)
+- Hero 区 web component 动效 → 静态首帧(web component 无法翻译)
+```
+
+### Step 4 · 导出 & 双格式交付
+
+- `editable` 版 HTML → 跑 `scripts/export_deck_pptx.mjs` 出可编辑 PPTX
+- **建议同时保留**原视觉稿 → 跑 `scripts/export_deck_pdf.mjs` 出高保真 PDF
+- 双格式交付给用户:视觉稿的 PDF + 可编辑的 PPTX,各司其职
+
+### 什么情况下直接拒绝 B 方案
+
+个别场景下改写代价过高,应该劝用户放弃 editable PPTX:
+- HTML 核心价值是动画或交互(改写后只剩静态首帧,信息量损失 50%+)
+- 页数 > 30,改写成本超过 2 小时
+- 视觉设计深度依赖精确 SVG / 自定义 filter(改写后和原图几乎无关)
+
+此时告诉用户:「这个 deck 改写代价过高,建议出 PDF 而不是 PPTX。如果接收方确实要 pptx 格式,就接受视觉会大幅朴素化——要不要换成 PDF?」
 
 ---
 

+ 156 - 38
references/slide-decks.md

@@ -3,9 +3,19 @@
 做幻灯片是设计工作的高频场景。这份文档说明怎么做好HTML幻灯片——从架构选型、单页设计,到 PDF/PPTX 导出的完整路径。
 
 **本 skill 的能力覆盖**:
-- HTML 播放/PDF 导出 → 本文档 + `scripts/export_deck_pdf.mjs` / `scripts/export_deck_stage_pdf.mjs`
-- 可编辑 PPTX 导出 → `references/editable-pptx.md` + `scripts/html2pptx.js` + `scripts/export_deck_pptx.mjs --mode editable`
-- 图片铺底 PPTX(不可编辑但视觉保真)→ `scripts/export_deck_pptx.mjs --mode image`
+- **HTML 演示版(基础产物,永远默认必做)** → 每页独立 HTML + `assets/deck_index.html` 聚合,浏览器里键盘翻页、全屏演讲
+- HTML → PDF 导出 → `scripts/export_deck_pdf.mjs` / `scripts/export_deck_stage_pdf.mjs`
+- HTML → 可编辑 PPTX 导出 → `references/editable-pptx.md` + `scripts/html2pptx.js` + `scripts/export_deck_pptx.mjs`(要求 HTML 按 4 条硬约束写)
+
+> **⚠️ HTML 是基础,PDF/PPTX 是衍生物。** 不管最终交付什么格式,都**必须**先做 HTML 聚合演示版(`index.html` + `slides/*.html`),它是幻灯片作品的「源」。PDF/PPTX 是从 HTML 一行命令导出的快照。
+>
+> **为什么 HTML 优先**:
+> - 演讲/演示现场最好用(投影仪 / 共享屏幕直接全屏,键盘翻页,不依赖 Keynote/PPT 软件)
+> - 开发过程中每页可单独双击打开验证,不用每次重新跑导出
+> - 是 PDF/PPTX 导出的唯一上游(避免「导出后才发现要改 HTML 又要重出」的死循环)
+> - 交付物可以是「HTML + PDF」或「HTML + PPTX」双份,接收方爱用哪个用哪个
+>
+> 2026-04-22 moxt brochure 实测:做完 13 页 HTML + index.html 聚合后,`export_deck_pdf.mjs` 一行导出 PDF,零改动。HTML 版本身就是可直接浏览器演讲的交付物。
 
 ---
 
@@ -13,20 +23,37 @@
 
 **这个决策比「单文件还是多文件」更先。** 2026-04-20 期权私董会项目实测:**不在动手前确认交付格式 = 2-3 小时返工。**
 
-### 决策树
+### 决策树(HTML-first 架构)
+
+所有交付都从同一套 HTML 聚合页(`index.html` + `slides/*.html`)开始。交付格式只决定 **HTML 的写法约束** 和 **导出命令**:
 
 ```
-│ 问:最终要交付什么?
-├── 只要浏览器全屏演讲 / 本地 HTML    → 视觉最自由,随便做
-├── 要 PDF(打印 / 发群 / 存档)      → 视觉最自由,任何架构都能导出
-└── 要可编辑 PPTX(同事会改文字)    → 🛑 从第一行 HTML 开始就按 `references/editable-pptx.md` 的 4 条硬约束写
+【永远默认 · 必做】 HTML 聚合演示版(index.html + slides/*.html)
+   │
+   ├── 只要浏览器演讲 / 本地 HTML 存档   → 到这里已经完成,HTML 视觉自由度最大
+   │
+   ├── 还要 PDF(打印 / 发群 / 存档)     → 跑 export_deck_pdf.mjs 一键出
+   │                                          HTML 写法自由,视觉无约束
+   │
+   └── 还要可编辑 PPTX(同事要改文字)    → 从第一行 HTML 就按 4 条硬约束写
+                                              跑 export_deck_pptx.mjs 一键出
+                                              牺牲渐变 / web component / 复杂 SVG
 ```
 
-### 为什么「要 PPTX 就得从头走 Path A」
+### 开工话术(抄走即用)
+
+> 不管最后交付是 HTML、PDF 还是 PPTX,我都会先做一个可在浏览器里切换和演讲的 HTML 聚合版(`index.html` 加键盘翻页)——这是永远的默认基础产物。在此之上再问你要不要额外出 PDF / PPTX 的快照。
+>
+> 你需要哪个导出格式?
+> - **只要 HTML**(演讲/存档)→ 视觉完全自由
+> - **还要 PDF** → 同上,加一条导出命令
+> - **还要可编辑 PPTX**(同事会在 PPT 里改文字)→ 我必须从第一行 HTML 就按 4 条硬约束写,会牺牲一些视觉能力(无渐变、无 web component、无复杂 SVG)。
+
+### 为什么「要 PPTX 就得从头走 4 条硬约束」
 
 PPTX 可编辑的前提是 `html2pptx.js` 能把 DOM 逐元素翻译为 PowerPoint 对象。它需要 **4 条硬约束**:
 
-1. body 固定 720pt × 405pt(不是 1920×1080px)
+1. body 固定 960pt × 540pt(匹配 `LAYOUT_WIDE`,13.333″ × 7.5″,不是 1920×1080px)
 2. 所有文字包在 `<p>`/`<h1>`-`<h6>` 里(禁止 div 直接放文字,禁止用 `<span>` 承载主文字)
 3. `<p>`/`<h*>` 自身不能有 background/border/shadow(放外层 div)
 4. `<div>` 不能用 `background-image`(用 `<img>` 标签)
@@ -39,15 +66,7 @@ PPTX 可编辑的前提是 `html2pptx.js` 能把 DOM 逐元素翻译为 PowerPoi
 | 路径 | 做法 | 结果 | 代价 |
 |------|------|------|------|
 | ❌ **先自由写 HTML,事后补救 PPTX** | 单文件 deck-stage + 大量 SVG/span 装饰 | 要可编辑 PPTX 只剩两条路:<br>A. 手写 pptxgenjs 几百行 hardcode 坐标<br>B. 重写 17 页 HTML 成 Path A 格式 | 2-3 小时返工,且手写版**维护成本永续**(HTML 改一个字,PPTX 要再人肉同步) |
-| ✅ **从第一步按 Path A 约束写** | 每页独立 HTML + 4 条硬约束 + 720×405pt | 一条命令导出 100% 可编辑 PPTX,同时也能浏览器全屏演讲(Path A HTML 就是浏览器可播放的标准 HTML) | 写 HTML 时多花 5 分钟想「文字怎么包进 `<p>`」,零返工 |
-
-### 开工话术(抄走即用)
-
-> 动手之前先确认交付格式:
-> - **浏览器演讲 / PDF** → 我按设计自由度最大的方式做(可以用动画、web component、复杂 SVG、CSS 渐变)
-> - **需要可编辑 PPTX**(同事会改文字) → 我必须从一开始就按 `references/editable-pptx.md` 的 4 条硬约束写 HTML。视觉能力会少一些(无渐变、无 web component、无复杂 SVG),但导出就是一条命令的事
->
-> 你选哪条?
+| ✅ **从第一步按 Path A 约束写** | 每页独立 HTML + 4 条硬约束 + 960×540pt | 一条命令导出 100% 可编辑 PPTX,同时也能浏览器全屏演讲(Path A HTML 就是浏览器可播放的标准 HTML) | 写 HTML 时多花 5 分钟想「文字怎么包进 `<p>`」,零返工 |
 
 ### 混合交付怎么办
 
@@ -57,12 +76,115 @@ PPTX 可编辑的前提是 `html2pptx.js` 能把 DOM 逐元素翻译为 PowerPoi
 
 ### 事后才知道要 PPTX 怎么办(紧急补救)
 
-极个别情况:HTML 已经写好了才发现要 PPTX。此时两个选项都不完美
+极个别情况:HTML 已经写好了才发现要 PPTX。推荐走 **fallback 流程**(完整说明见 `references/editable-pptx.md` 末尾「Fallback:已有视觉稿但用户坚持要 editable PPTX」)
 
-1. **图片铺底 PPTX**(`scripts/export_deck_pptx.mjs` image 模式)——视觉 100% 保真但文字不可编辑。适合「演讲用 PPT 播放、不改内容」场景
-2. **手写 pptxgenjs 重建**(为每页手写 addText/addShape + 图形 PNG 嵌入)——文字可编辑,但位置、字体、对齐都要手调,维护成本高。**只有用户明确接受「HTML 源要改就得重新手调 PPTX」才走这条**
+1. **首选:改出 PDF**(视觉 100% 保留,跨平台,接收方能看能印)—— 如果接收方实际需求是「演讲/存档」,PDF 就是最佳交付物
+2. **次选:AI 以视觉稿为蓝本,重写一版 editable HTML** → 导出 editable PPTX —— 保留色彩/布局/文案的设计决策,牺牲渐变、web component、复杂 SVG 等视觉能力
+3. **不推荐:手写 pptxgenjs 重建**——位置、字体、对齐都要手调,维护成本高,且后续 HTML 改一个字都得再人肉同步一次
 
-永远优先把选择告诉用户,让他决定。**永远不要第一反应就开始手写 pptxgenjs**——那是最后的兜底手段。
+永远把选择告诉用户,让他决定。**永远不要第一反应就开始手写 pptxgenjs**——那是最后的兜底手段。
+
+---
+
+## 🛑 批量制作前:先做 2 页 showcase 定 grammar
+
+**只要 deck ≥ 5 页,绝对不能从第 1 页直接写到最后一页。** 2026-04-22 moxt brochure 实战验证的正确顺序:
+
+1. 选 **2 个视觉差异最大的页面类型**先做 showcase(如「封面」+「情绪/引用页」,或「封面」+「产品展示页」)
+2. 截图让用户确认 grammar(masthead / 字体 / 色 / 间距 / 结构 / 中英双语比例)
+3. 方向通过了再批量推剩下 N-2 页,每页复用已建立的 grammar
+4. 全部完成后一起合成 HTML 聚合 + PDF / PPTX 衍生物
+
+**为什么**:直接写 13 页到底 → 用户说「方向不对」= 返工 13 次。先做 2 页 showcase → 方向错 = 返工 2 次。视觉 grammar 一旦确立,后续 N 页的决策空间大幅收窄,只剩「内容怎么放进去」。
+
+**showcase 页选择原则**:选视觉结构最不一样的两页。这两页过了 = 其他中间态都能过。
+
+| Deck 类型 | 推荐 showcase 页组合 |
+|-----------|---------------------|
+| B2B brochure / 产品宣发 | 封面 + 内容页(理念/情感页) |
+| 品牌发布 | 封面 + 产品特色页 |
+| 数据报告 | 数据大图页 + 分析结论页 |
+| 教程课件 | 章节封页 + 具体知识点页 |
+
+---
+
+## 📐 出版物 grammar 模板(moxt 实测可复用)
+
+适合 B2B brochure / 产品宣发 / 长报告类 deck。每页复用这套结构 = 13 页视觉完全一致、0 返工。
+
+### 每页骨架
+
+```
+┌─ masthead(顶部 strip + 横线)────────────┐
+│  [logo 22-28px] · A Product Brochure                Issue · Date · URL │
+├──────────────────────────────────────────┤
+│                                          │
+│  ── kicker(绿色短横 + uppercase 标签)   │
+│  CHAPTER XX · SECTION NAME                 │
+│                                          │
+│  H1(中文 Noto Serif SC 900)             │
+│  重点词单独上品牌主色                      │
+│                                          │
+│  English subtitle (Lora italic,副标题)   │
+│  ─────────── 分隔线 ──────────            │
+│                                          │
+│  [具体内容:双栏 60/40 / 2x2 grid / 列表] │
+│                                          │
+├──────────────────────────────────────────┤
+│ section name                     XX / total │
+└──────────────────────────────────────────┘
+```
+
+### 样式约定(直接抄走)
+
+- **H1**:中文 Noto Serif SC 900,字号 80-140px 看信息量,重点词单独上品牌主色(不要全文堆色)
+- **英文副**:Lora italic 26-46px,品牌签名词(如 "AI team")粗体 + 主色斜体
+- **正文**:Noto Serif SC 17-21px,line-height 1.75-1.85
+- **accent 高亮**:正文里用主色加粗标注关键词,每页不超过 3 处(过多就失去锚点作用)
+- **背景**:暖米底 #FAFAFA + 极淡 radial-gradient noise(`rgba(33,33,33,0.015)`)增加纸感
+
+### 视觉主角必须差异化
+
+13 页如果全是「文字 + 一张截图」就太单调。**每页的视觉主角类型轮换**:
+
+| 视觉类型 | 适合的 section |
+|---------|---------------|
+| 封面排版(大字 + masthead + pillar) | 首页 / 篇章封 |
+| 单角色 portrait(超大单只 momo 等) | 介绍单个概念/角色 |
+| 多角色合影 / 头像卡并排 | 团队 / 用户案例 |
+| 时间轴卡片递进 | 展示「长期关系」「演进」 |
+| 知识图谱 / 连接节点图 | 展示「协作」「流动」 |
+| Before/After 对比卡 + 中间箭头 | 展示「改变」「差异」 |
+| 产品 UI 截图 + 描边设备框 | 具体功能展示 |
+| 大引号 big-quote(半页大字) | 情绪页 / 问题页 / 引文页 |
+| 真人头像 + 引言卡(2×2 或 1×4) | 用户见证 / 使用场景 |
+| 大字封底 + URL 椭圆按钮 | CTA / 结尾 |
+
+---
+
+## ⚠️ 常见踩坑(moxt 实战总结)
+
+### 1. Emoji 在 Chromium / Playwright 导出时不渲染
+
+Chromium 默认不带彩色 emoji 字体,`page.pdf()` 或 `page.screenshot()` 时 emoji 显示为空方框。
+
+**对策**:用 Unicode 文字符号(`✦` `✓` `✕` `→` `·` `—`)替代,或直接改纯文字(「Email · 23」而不是「📧 23 emails」)。
+
+### 2. `export_deck_pdf.mjs` 报错 `Cannot find package 'playwright'`
+
+原因:ESM 模块解析从脚本所在位置向上找 `node_modules`。脚本在 `~/.claude/skills/huashu-design/scripts/`,那里没依赖。
+
+**对策**:把脚本复制到 deck 项目目录(例如 `brochure/build-pdf.mjs`),在项目根跑 `npm install playwright pdf-lib`,然后 `node build-pdf.mjs --slides slides --out output/deck.pdf`。
+
+### 3. Google Fonts 没加载完就截图 → 中文显示为系统默认黑体
+
+Playwright 截图/PDF 前至少 `wait-for-timeout=3500` 让 webfont 下载并 paint。或者把字体 self-host 到 `shared/fonts/` 减少网络依赖。
+
+### 4. 信息密度失衡:内容页塞太多
+
+moxt philosophy 页第一版用 2×2 = 4 段 + 底部 3 信条 = 7 块内容,挤压且重复。改成 1×3 = 3 段后呼吸感立刻回来。
+
+**对策**:每页控制在「1 个核心信息 + 3-4 个辅助点 + 1 个视觉主角」,超过就拆到新页。**少即是多**——观众一页看 10 秒,给他 1 个记忆点比 4 个记忆点更容易记住。
 
 ---
 
@@ -511,22 +633,16 @@ await page.pdf({ width: '1920px', height: '1080px', printBackground: true, prefe
 
 ---
 
-### `export_deck_pptx.mjs` — 导出 PPTX(两种模式)
+### `export_deck_pptx.mjs` — 导出可编辑 PPTX
 
 ```bash
-# 图片铺底(视觉 100% 保真,不可编辑文字)
-node scripts/export_deck_pptx.mjs --slides <dir> --out deck.pptx --mode image
-
-# 每个文本独立文本框(可编辑,但字体会回落)
-node scripts/export_deck_pptx.mjs --slides <dir> --out deck.pptx --mode editable
+# 唯一模式:文本框原生可编辑(字体会回落到系统字体)
+node scripts/export_deck_pptx.mjs --slides <dir> --out deck.pptx
 ```
 
-| 模式 | 视觉保真 | 文字可编辑 | 工作原理 | 限制 |
-|------|---------|----------|---------|------|
-| `image` | ✅ 100% | ❌ | Playwright 截图 → pptxgenjs addImage | 文字变图片 |
-| `editable` | 🟡 ~70% | ✅ | html2pptx 提取每个文本框 | 见下方约束 |
+工作原理:`html2pptx` 逐元素读 computedStyle 把 DOM 翻译成 PowerPoint 对象(text frame / shape / picture)。文字变成真文本框,PPT 里双击即可编辑。
 
-**editable 模式的硬性约束**(用户 HTML 必须满足,否则该页 skip):
+**硬性约束**(HTML 必须满足,否则该页 skip,详细说明见 `references/editable-pptx.md`):
 - 所有文字必须在 `<p>`/`<h1>`-`<h6>`/`<ul>`/`<ol>` 里(禁止裸文本 div)
 - `<p>`/`<h*>` 标签自身不能有 background/border/shadow(放外层 div)
 - 不用 `::before`/`::after` 插入装饰文字(伪元素提不出来)
@@ -536,14 +652,16 @@ node scripts/export_deck_pptx.mjs --slides <dir> --out deck.pptx --mode editable
 
 脚本已内置**自动预处理器**——把 "叶子 div 里的裸文本" 自动包成 `<p>`(保留 class)。这解决了最常见的违规(裸文本)。但其他违规(p 上有 border、span 上有 margin 等)仍需 HTML 源头合规。
 
-**editable 模式的另一个 caveat——字体回落**:
+**字体回落 caveat**:
 - Playwright 用 webfont 测量 text-box 尺寸;PowerPoint/Keynote 用本机字体渲染
 - 两者不同时会有**溢出或错位**——每页都要肉眼过
 - 建议目标机器装好 HTML 里用的字体,或 fallback 到 `system-ui`
 
+**视觉优先场景不要走这条路径** → 改用 `export_deck_pdf.mjs` 出 PDF。PDF 视觉 100% 保真、矢量、跨平台、文字可搜——是视觉优先 deck 的真正归宿,不是什么「不可编辑的妥协」。
+
 ### 从一开始就让 HTML 对导出友好
 
-对性能最稳的 deck:**从写 HTML 时就按 editable 模式的约束写**。这样 `--mode editable` 可以直接全部 pass。额外成本不大:
+对性能最稳的 deck:**从写 HTML 时就按 editable 的 4 条硬约束写**。这样 `export_deck_pptx.mjs` 可以直接全部 pass。额外成本不大:
 
 ```html
 <!-- ❌ 不好 -->
@@ -567,12 +685,12 @@ node scripts/export_deck_pptx.mjs --slides <dir> --out deck.pptx --mode editable
 |------|------|
 | 给主办方/档案存档 | **PDF**(通用、高保真、文字可搜) |
 | 发给协作者让他们微调文字 | **PPTX editable**(接受字体回落) |
-| 要现场演讲、不改内容 | **PDF** 或 **PPTX image** |
+| 要现场演讲、不改内容 | **PDF**(矢量保真,跨平台) |
 | HTML 是首选呈现媒介 | 直接浏览器播放,导出只是备份 |
 
 ## 导出为可编辑 PPTX 的深度路径(仅长期项目)
 
-如果你的 deck 会长期维护、反复修改、团队协作——建议**一开始就按 html2pptx 约束写 HTML**,让 `--mode editable` 稳定通过。详见 `references/editable-pptx.md`(4 条硬约束 + HTML 模板 + 常见错误速查)。
+如果你的 deck 会长期维护、反复修改、团队协作——建议**一开始就按 html2pptx 约束写 HTML**,这样 `export_deck_pptx.mjs` 可以直接全部 pass。详见 `references/editable-pptx.md`(4 条硬约束 + HTML 模板 + 常见错误速查 + 已有视觉稿的 fallback 流程)。
 
 ---
 

+ 33 - 93
scripts/export_deck_pptx.mjs

@@ -1,109 +1,69 @@
 #!/usr/bin/env node
 /**
- * export_deck_pptx.mjs — 把多文件 slide deck 导出为 PPTX
- *
- * 两种模式:
- *   --mode image     图片铺底,视觉 100% 保真,⚠️ 文字不可编辑
- *   --mode editable  文本框原生,文字可编辑,要求 HTML 符合 4 条硬约束(见 references/editable-pptx.md)
+ * export_deck_pptx.mjs — 把多文件 slide deck 导出为可编辑 PPTX
  *
  * 用法:
- *   # 图片模式(默认)
  *   node export_deck_pptx.mjs --slides <dir> --out <file.pptx>
- *   # 可编辑模式
- *   node export_deck_pptx.mjs --slides <dir> --out <file.pptx> --mode editable
- *
- * --mode image 特点:
- *   - 每张 slide 截图成 PNG,满铺一张 PPTX 页面
- *   - 视觉 100% 保真(因为就是图片)
- *   - 文字不可编辑
- *   - HTML 随便写,不挑格式
  *
- * --mode editable 特点
- *   - 调用 scripts/html2pptx.js 把 HTML DOM 逐元素翻译成 PowerPoint 对象
+ * 行为:
+ *   - 调用 scripts/html2pptx.js 把 HTML DOM 逐元素翻译成 PowerPoint 原生对象
  *   - 文字是真文本框,PPT 里直接双击能编辑
- *   - ⚠️ HTML 必须符合 4 条硬约束(见 references/editable-pptx.md):
- *     1. 文字包在 <p>/<h1>-<h6> 里(div 不能直接放文字)
- *     2. 不用 CSS 渐变
- *     3. <p>/<h*> 不能有 background/border/shadow(放外层 div
- *     4. div 不能 background-image(用 <img>)
- *   - body 尺寸默认 960pt × 540pt(LAYOUT_WIDE,13.333″ × 7.5″
- *   - 视觉驱动的 HTML 几乎无法 pass —— 必须从写 HTML 的第一行就按约束写
+ *   - body 尺寸 960pt × 540pt(LAYOUT_WIDE,13.333″ × 7.5″)
+ *
+ * ⚠️ HTML 必须符合 4 条硬约束(见 references/editable-pptx.md):
+ *   1. 文字包在 <p>/<h1>-<h6> 里(div 不能直接放文字)
+ *   2. 不用 CSS 渐变
+ *   3. <p>/<h*> 不能有 background/border/shadow(放外层 div
+ *   4. div 不能 background-image(用 <img>)
  *
- * 依赖:
- *   --mode image:    npm install playwright pptxgenjs
- *   --mode editable: npm install playwright pptxgenjs sharp
+ * 视觉驱动的 HTML 几乎无法 pass —— 必须从写 HTML 的第一行就按约束写。
+ * 视觉自由度优先的场景(动画、web component、CSS 渐变、复杂 SVG)
+ * 应改用 export_deck_pdf.mjs / export_deck_stage_pdf.mjs 导出 PDF。
+ *
+ * 依赖:npm install playwright pptxgenjs sharp
  *
  * 按文件名排序(01-xxx.html → 02-xxx.html → ...)。
  */
 
-import { chromium } from 'playwright';
 import pptxgen from 'pptxgenjs';
 import fs from 'fs/promises';
 import path from 'path';
-import os from 'os';
 import { fileURLToPath } from 'url';
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
 function parseArgs() {
-  const args = { width: 1920, height: 1080, mode: 'image' };
+  const args = {};
   const a = process.argv.slice(2);
   for (let i = 0; i < a.length; i += 2) {
     const k = a[i].replace(/^--/, '');
     args[k] = a[i + 1];
   }
   if (!args.slides || !args.out) {
-    console.error('用法: node export_deck_pptx.mjs --slides <dir> --out <file.pptx> [--mode image|editable] [--width 1920] [--height 1080]');
-    process.exit(1);
-  }
-  args.width = parseInt(args.width);
-  args.height = parseInt(args.height);
-  if (!['image', 'editable'].includes(args.mode)) {
-    console.error(`未知 --mode: ${args.mode}。支持: image, editable`);
+    console.error('用法: node export_deck_pptx.mjs --slides <dir> --out <file.pptx>');
+    console.error('');
+    console.error('⚠️ HTML 必须符合 4 条硬约束(见 references/editable-pptx.md)。');
+    console.error('   视觉自由度优先的场景请改用 export_deck_pdf.mjs 导出 PDF。');
     process.exit(1);
   }
   return args;
 }
 
-async function exportImage({ slidesDir, outFile, files, width, height }) {
-  console.log(`[image mode] Rendering ${files.length} slides as PNG...`);
-
-  const browser = await chromium.launch();
-  const ctx = await browser.newContext({ viewport: { width, height } });
-  const page = await ctx.newPage();
-
-  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'deck-pptx-'));
-  const pngs = [];
-  for (const f of files) {
-    const url = 'file://' + path.join(slidesDir, f);
-    await page.goto(url, { waitUntil: 'networkidle' }).catch(() => page.goto(url));
-    await page.waitForTimeout(1200);
-    const out = path.join(tmpDir, f.replace(/\.html$/, '.png'));
-    await page.screenshot({ path: out, fullPage: false });
-    pngs.push(out);
-    console.log(`  [${pngs.length}/${files.length}] ${f}`);
-  }
-  await browser.close();
+async function main() {
+  const { slides, out } = parseArgs();
+  const slidesDir = path.resolve(slides);
+  const outFile = path.resolve(out);
 
-  const pres = new pptxgen();
-  pres.defineLayout({ name: 'DECK', width: width / 96, height: height / 96 });
-  pres.layout = 'DECK';
-  for (const png of pngs) {
-    const s = pres.addSlide();
-    s.addImage({ path: png, x: 0, y: 0, w: pres.width, h: pres.height });
+  const files = (await fs.readdir(slidesDir))
+    .filter(f => f.endsWith('.html'))
+    .sort();
+  if (!files.length) {
+    console.error(`No .html files found in ${slidesDir}`);
+    process.exit(1);
   }
-  await pres.writeFile({ fileName: outFile });
-
-  for (const p of pngs) await fs.unlink(p).catch(() => {});
-  await fs.rmdir(tmpDir).catch(() => {});
 
-  console.log(`\n✓ Wrote ${outFile}  (${files.length} slides, image mode, 文字不可编辑)`);
-}
+  console.log(`Converting ${files.length} slides via html2pptx...`);
 
-async function exportEditable({ slidesDir, outFile, files }) {
-  console.log(`[editable mode] Converting ${files.length} slides via html2pptx...`);
-
-  // 动态 require html2pptx.js(CommonJS 模块)
   const { createRequire } = await import('module');
   const require = createRequire(import.meta.url);
   let html2pptx;
@@ -111,7 +71,7 @@ async function exportEditable({ slidesDir, outFile, files }) {
     html2pptx = require(path.join(__dirname, 'html2pptx.js'));
   } catch (e) {
     console.error(`✗ 加载 html2pptx.js 失败:${e.message}`);
-    console.error(`  该模块依赖 sharp —— 请跑 npm install sharp 后重试。`);
+    console.error(`  依赖缺失时请跑:npm install playwright pptxgenjs sharp`);
     process.exit(1);
   }
 
@@ -141,27 +101,7 @@ async function exportEditable({ slidesDir, outFile, files }) {
   }
 
   await pres.writeFile({ fileName: outFile });
-  console.log(`\n✓ Wrote ${outFile}  (${files.length - errors.length}/${files.length} slides, editable mode, 文字可在 PPT 中直接编辑)`);
-}
-
-async function main() {
-  const { slides, out, width, height, mode } = parseArgs();
-  const slidesDir = path.resolve(slides);
-  const outFile = path.resolve(out);
-
-  const files = (await fs.readdir(slidesDir))
-    .filter(f => f.endsWith('.html'))
-    .sort();
-  if (!files.length) {
-    console.error(`No .html files found in ${slidesDir}`);
-    process.exit(1);
-  }
-
-  if (mode === 'image') {
-    await exportImage({ slidesDir, outFile, files, width, height });
-  } else {
-    await exportEditable({ slidesDir, outFile, files });
-  }
+  console.log(`\n✓ Wrote ${outFile}  (${files.length - errors.length}/${files.length} slides, 可编辑 PPTX)`);
 }
 
 main().catch(e => { console.error(e); process.exit(1); });