commit 82d221bb90b40334839c57da624cad6b22241841 Author: 郭浩 Date: Wed May 27 22:00:55 2026 +0800 v0.13 baseline · git 初始化 + 仓库瘦身 把所有本地 demo / 测试 / 上传图 / 调试脚本搬入 local-tests/,写 .gitignore 忽略整个测试目录与 node_modules/。本次起 skill 仓库 只跟踪:SKILL.md / HANDOFF.md / PRODUCT.md / assets/(3 模板)/ references/(15 规则文档)/ agents/openai.yaml / validate-social-deck.mjs / package.json + lock。 历次版本(v0.1-v0.12)的全部实质改动已包含在当前文件状态中—— HANDOFF.md 的版本历史段保留作为时间轴文字记录,但代码层面以 此 commit 为唯一基线,后续逐次 commit 对应 HANDOFF.md 新增段落。 HANDOFF/PRODUCT/category-cookbook/components/style-system 中的 demo 路径引用已加 local-tests/ 前缀。SKILL.md Step 5 line 190 的 'social-card-/' 是下游用户工作区的命名约定,未改动。 Co-Authored-By: Claude Opus 4.7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ae4580 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Local test workspace — demos, screenshots, downloaded assets, debug scripts. +# Skill iterates only on SKILL.md / references/ / assets/ / agents/ / validate. +local-tests/ + +# Node + macOS +node_modules/ +.DS_Store +*.log +npm-debug.log* + +# Editor / OS +.vscode/ +.idea/ diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 0000000..7d5da15 --- /dev/null +++ b/HANDOFF.md @@ -0,0 +1,370 @@ +# Handoff — guizang-social-card-skill + +最后更新:2026-05-27 · 版本:v0.13(git 初始化 + 仓库瘦身:本地测试归集到 `local-tests/` 并 gitignore) + +这份文档只记录**事实**:当前 skill 由哪些文件组成、每个文件管什么、本轮更新改了什么、怎么验证它还能跑。想读"为什么这样做"或"未来要怎么走",去看 `PRODUCT.md`。 + +> **v0.13 路径约定**:本文件 v0.12 及更早版本历史段落里的 `demo-*` / `social-card-*` / `smoke-*` / `test-*` 目录已统一搬入 `local-tests/`。读历史时请在脑中前缀 `local-tests/`。 + +--- + +## 1. 目录结构(事实版) + +``` +guizang-social-card-skill/ +├── SKILL.md # 266 行 · 7 步工作流入口 +├── HANDOFF.md # 本文件 +├── PRODUCT.md # 产品思考 +├── assets/ +│ ├── template-editorial-card.html # 585 行 · 杂志风种子 +│ ├── template-swiss-card.html # 740 行 · 瑞士风种子(v0.6 已硬化) +│ └── magazine-bg-webgl.js # WebGL 墨流背景 +├── references/ # 12 份子规范,按需读取 +│ ├── platform-specs.md (122) 平台 × 分辨率 × 命名 +│ ├── style-system.md (182) 两种风格的硬规则与反模式 +│ ├── theme-presets.md (212) 6 套杂志 palette(含 1 套暗色 Midnight Ink)+ 4 套 Swiss accent +│ ├── layout-recipes.md (667) 27 个版式骨架(M01-M15 + S01-S12) +│ ├── components.md (226) 字体 / 卡片 / 间距 / 图标 / 标题字数表 +│ ├── background-systems.md (108) 墨流 / 网格 / 纸纹层 +│ ├── portrait-fill.md (85) 3:4 板的留白对策 +│ ├── content-planning.md (138) 钩子 / 拆页 / 文案压缩 +│ ├── production-workflow.md (172) 渲染管线与命令 +│ ├── image-overlay.md (150) 文字压图:蒙版 + 主体映射 +│ ├── category-cookbook.md (232) 小红书 11 个品类的版式路由 +│ └── qa-checklist.md (67) 交付前 checklist +├── agents/ +│ └── openai.yaml # 子代理配置(备用) +├── validate-social-deck.mjs # 6 条规则的渲染后校验器 +├── package.json / package-lock.json / node_modules # 仅 playwright +├── .gitignore # v0.13 新增 +└── local-tests/ # 本地测试与素材(git 忽略,不随 skill 分发) + ├── demo-* # 各次设计案例(kyoto / yading / wukong / kit-* / zzz-* / m16-camping / eyeliner / showcase / smoke-editorial-travel / map-component / screenshot-treatment / multi-platform) + ├── social-card-* # 历史与 Codex 外部测试产物(含 codex-remote-* / wukong-breakout / kyoto-guide / moon-eyes-lip-mud) + ├── smoke-ai-tools/ # v0.6 冒烟测试样例 + ├── test-dark-theme-elden-ring/ # v0.12 Midnight Ink 验证 + ├── Image/ # 用户上传的测试图 + ├── 全图封面参考/ # v0.10 杂志封面参考图 + ├── .codepilot-uploads/ # 历史上传缓存 + └── *.cjs # 旧的 debug / probe / render 脚本 +``` + +v0.13 起:git 仓库。`local-tests/` 整目录在 `.gitignore` 中,commit 只跟踪 skill 内容。 + +--- + +## 2. 数据规模与覆盖面 + +| 维度 | 数量 | +| --- | --- | +| 工作流步骤 | 7 步(Intake → Extract → Style → Plan → Seed → Build → Image → Deliver) | +| 风格模式 | 2 个(Editorial Magazine × E-ink / Swiss International) | +| 主题色 | Editorial 6 套(5 浅 + 1 暗 Midnight Ink)+ Swiss 4 套 = 10 套 | +| 版式骨架 | Editorial 16 个(M01-M16)+ Swiss 12 个(S01-S12)= 28 个 | +| 平台 × 比例 | 小红书 3:4 / 1:1,微信 21:9 + 1:1 配对,方封面 1:1 | +| 小红书品类覆盖 | 11 个品类中 7 个完整支持,4 个需用户提供素材,4 个明确不接 | +| 种子模板 | 2 份(Editorial / Swiss),单文件即含全部 class 与字体 | +| 已交付案例 | 7 个 demo(2 个 image-cover + 5 个 kit) | + +--- + +## 3. 版本历史 + +仅记录"什么时候新增/变更了哪些可见资产",不记录文风调整。 + +### v0.13 · 2026-05-27(本轮) + +主题:**git 初始化 + 仓库瘦身**。把所有本地测试 / demo / 上传图 / 旧脚本统一搬到 `local-tests/`,写 `.gitignore` 忽略整个测试目录与 `node_modules/`。skill 本身只保留:`SKILL.md` / `HANDOFF.md` / `PRODUCT.md` / `assets/` / `references/` / `agents/` / `validate-social-deck.mjs` / `package.json` 与锁文件。 + +**改动事实**: +- 新建 `local-tests/`,搬入 28 个测试目录(demo-* 16 个 / social-card-* 6 个 / smoke-* 1 个 / test-* 1 个 / Image / 全图封面参考 / .codepilot-uploads)以及 3 个旧脚本(`render.cjs` / `debug-bg.cjs` / `probe-webgl.cjs`) +- 新增 `.gitignore`:忽略 `local-tests/` / `node_modules/` / `.DS_Store` / `*.log` / `.vscode/` / `.idea/` +- `HANDOFF.md` 目录结构段同步更新,列入 `local-tests/` 子目录视图 +- skill 自身文件结构未变,模板与规则文档一律未改 + +**重要约定**:`local-tests/` 不进 git,不分发,不放入 skill 包。`demo-showcase/editorial.html` 和 `demo-showcase/swiss.html` 仍然是字号/字距/版式还原的 source-of-truth——它们移到了 `local-tests/demo-showcase/`,路径变了但作用未变。所有后续 demo / 冒烟测试都应在 `local-tests/` 下新建子目录。 + +### v0.12 · 2026-05-27(上一轮) + +主题:**Editorial 美学回退 + 第 6 个杂志主题 Midnight Ink + 图片裁切策略 + 交付工作流调整**。本轮起因是用户对 v0.11 冒烟测试的"质量回退"反馈——v0.10/v0.11 在把 showcase 模板化进 seed 的过程中,把 Editorial 的字重/字距/正文字族无意中改成了"信息图横幅"风(900 字重 / 负字距 / sans 正文),与 `demo-showcase/editorial.html` 的"克制磁带感"完全偏离。同时 Codex 外部测试 `social-card-wukong-breakout` 暴露了缺暗色主题、自造 `wukong-night` 不合规的问题。 + +**新增** +- `assets/template-editorial-card.html` 增加 `[data-theme="midnight-ink"]` 暗色 palette + 三段必随覆盖(`.grain` 翻成 screen-mode 暖光斑 / `.paper-wash` 改成金辉 + 阴影 vignette / `.frame-img` 加深底色 + 1px 暖边)。Editorial palette 从 5 套变 6 套。 +- `references/theme-presets.md` 增 **Midnight Ink** 段,含色值 + 必随覆盖代码 + 使用场景(游戏 key art / 夜景 / 影调封面)。明确"暗色 Editorial 只有这一套,不要自造第二套"。 +- `test-dark-theme-elden-ring/` — Midnight Ink 验证 deck(4 张 1080×1440,《艾尔登法环》新手指南)。图源 Steam 官方 8 张截图,记录在 `assets/`。验证 P1 全幅 hero-bleed + 黄金 accent、P2 M14 pipeline 4 步 + 21:9 atmosphere、P3 M07 ledger 4 行 + 战斗 atmosphere、P4 quote-card + 4:3 静态时刻。 + +**修改:Editorial 美学回退(核心)** +- `assets/template-editorial-card.html` 全部 Editorial 排版默认值恢复 showcase 美学: + - `.h-display` 124px **weight 500** letter-spacing **+.04em**(v0.10/v0.11 误改为 900 / −.01em) + - `.h-xl` 88px weight 500 +.03em(曾经 700 / −.01em) + - `.h-md` 56px weight 500 +.02em + - `.lead` 28px **font-family: var(--serif-zh)**(曾经被切到 sans-zh,是塌掉杂志感的主因) + - `.body` 24px serif-zh(同上) + - `.kicker` 21px letter-spacing **+.22em** + - `.meta` / `.label` 18px +.20em + - `.ledger-row .ledger-title` 42px weight 500,`.ledger-note` 22px serif-zh + - `.pipeline-v .step-title` 40px weight 500,`.step-desc` 24px serif-zh +- `references/components.md` Editorial 字号表完整改写为上述值,加一段 **Restored 2026-05-27** 警示,写明"v0.10/v0.11 错改值是什么,为何会塌掉,showcase 是 source-of-truth"。 +- `references/style-system.md` Mode A 加 **Typography stance — "the larger, the lighter"** 段:Editorial display 必须 500、serif body、宽字距;反模式(700-900 + sans + 负字距)= "infographic banner" 塌方。 + +**修改:图片裁切策略** +- `assets/template-editorial-card.html` `.frame-img img` 默认 `object-position` 从 `center top` 改为 `center 50%`。Editorial 之前的 `center top` 是默认值意外,导致竖图主体被切掉;showcase 实际上是逐图 inline 覆盖。 +- `references/components.md` Image Containers 段加 **主体感知裁切表**(主体位置 → object-position 取值)+ 硬规则"每张图都要 inline 写 object-position,默认值是 fallback 不是推荐"。 +- `SKILL.md` Step 6 Image and Screenshot Handling **Crop discipline** 改写为强制每张图 inline 写 `object-position`,带具体取值示例(`center 25%` 天空多 / `center 50%` 居中 / `center 62%` 半身 / `center 70%` 前景物)。 + +**修改:交付工作流("show user first, ask before validating")** +- `SKILL.md` Step 7 Deliver 重写: + - 默认 **不再** 渲完自动跑 validator。 + - 渲完先把图给用户看,问一句"先你自己看,还是我先自动核查一遍?"。 + - 用户说"自己看"→ 停下来等反馈;说"你查吧"→ 才跑 `validate-social-deck.mjs`。 +- 起因:用户反馈每次渲完自动跑 validator 让交付窗口变得很长,用户大多数时候眼睛比脚本更快。 +- 代价:自动校验的"安全网"被撤掉一格——但脚本仍然可用、用户随时可调。 + +**修改:已有 demo 同步** +- `demo-kyoto-trip/index.html` — 字号回退到 showcase 值,P4 ledger 去掉 03(岚山)解决 3:4 溢出,title 改"四条建议"。 +- `demo-smoke-editorial-travel/index.html` — 字号回退到 showcase 值。P1 hero 从 `r-16x10` 改 `r-3x2` + `object-position:center 62%`(hero-grassland 主体在画面中下)。P5 camp-tarp 加 `center 55%`。两份 demo 都 4-5 张 1080×1440 重渲过,无溢出。 + +**Codex `social-card-wukong-breakout` 诊断** +- 状态:未由本 skill 修复。Codex 在 v0.11 规则下做的测试。 +- 根因:Codex 实际**遵守**了规则——错的是规则本身。`components.md` 当时还在写 900/700/sans-body 默认值,Codex 照搬出来必然塌;Codex 自造的 `wukong-night` 是"规则没暗色"的合理外推。 +- 修复路径:v0.12 已落地 Midnight Ink 官方暗色 + 美学回退;后续 Codex 任务直接 `data-theme="midnight-ink"` 即可,无需自造。 + +**未做** +1. 截图美化资产 port(仍在短期清单) +2. 地图组件(同上) +3. 1:1 短标题生成器(同上) +4. 多平台一次出包(同上) +5. `social-card-wukong-breakout` 未重新生成——v0.12 是规则修复,不回灌历史 Codex 测试 + +### v0.11 · 2026-05-27 + +主题:**短期规划 Phase A 收口 — validator + Editorial 冒烟测试**。短期规划 6 件事完成第 1、2 件,把 v0.10 留下的 Editorial 长内容欠债(M11/M14 在 3:4 上从未单独验证)也清掉。 + +**新增** +- `validate-social-deck.mjs` — 单文件 Node 脚本,接受 task-dir 或 index.html,对每个 `
` 跑 6 条规则:R1 overflow / R2 footer 碰撞 / R3 Swiss 粗体 display / R4 字号下限 / R5 4 横带密度(pixel-row union 覆盖率)/ R6 `.h-xl` 行/字 cap。`--style=swiss|editorial` 可显式指定,默认按 `data-theme/data-accent` 或 serif 字族 fallback 自动判断。退出码 1 仅在 FAIL 时;WARN 是 advisory。 +- `package.json` `scripts.validate` — 把 `node validate-social-deck.mjs` 包到 `npm run validate `。 +- `demo-smoke-editorial-travel/` — Editorial 冒烟测试 5 张 1080×1440:M01 cover + M11 Marginalia Essay(3 段正文 + 7 行 marginalia)+ M07 Field Ledger(5 行账本)+ M14 Vertical Pipeline(5 步)+ M16 Image-Led Cover 收束。全部 PASS validator。 +- `references/components.md` § Editorial `.h-xl` Hard Caps — 实测表:xhs 92px / square 78px / wide 96px;附 M11/M14/M07 各自的 ledger / paragraph / step 数硬上限。 + +**修改** +- `SKILL.md` Step 5 Build And Render 加:每次 render 后必须跑 validator,FAIL 必修。Step 7 Deliver 加:交付前再跑一次;WARN 在交付消息里报告让用户决定。 +- `validate-social-deck.mjs` 修两轮 false positive: + - R2 footer collision:原版把 `.content` 容器 + 全幅 `.mag-bg` 误判碰底;改成只看 leaf text/media + 跳过 ≥95% 全幅 absolute 节点。 + - R4 min font:原版按 class 选择器命中所有 `.lead`,但 Swiss `.role-card .lead` 是子组件文本。改成只查 leaf text 节点的角色。 +- `references/components.md` § Image Containers 加:figcaption 类名硬规则——Editorial 用 `.img-cap`、Swiss 用 `.swiss-img-caption`、**没有共享 `.cap`**。冒烟测试中我把它写成 `.cap` 触发 R4,已固化为警示。 + +**回归测试**(在 5 个 demo 上跑过 validator) +- `demo-smoke-editorial-travel`:5 pass / 0 fail / 0 warn +- `demo-zzz-miyabi`:4 pass / 1 R5 warn(p4 H-Bar 底部 trailing whitespace,可接受) +- `demo-eyeliner`:3 pass / 1 R5 warn(xhs-03 底带 13%,可接受) +- `smoke-ai-tools`:发现一处历史 R1 overflow(xhs-03 +101px)—— 之前漏看,已暴露 +- `demo-showcase/swiss.html` + `editorial.html`:共暴露 39 + 46 条 R4/R5/R6 warn,主要是 lead/body/caption 在数据密集页里被压到 18-22px、几条 h-statement 行数超 cap。下一轮可逐张回修。 + +**未做(同短期规划清单)** +1. 截图美化资产 port(下一件) +2. 地图组件 +3. 1:1 短标题生成器 +4. 多平台一次出包 + +### v0.10 · 2026-05-26 + +主题:**内容密度硬规则 + 风格-内容类型解绑 + 全版式 showcase**。两件事并轨:(1) 解决 3:4 卡内容欠填——把"杂志留白逻辑"和"社交卡逐张刷"这两件事在文档层面拆清;(2) 把"Editorial 只接生活类 / Swiss 只接产品类"的隐含绑定明确解除,让用户用同一篇内容自由选风格。 + +**新增** +- `demo-eyeliner/` — 4 张 3:4 美妆类 Editorial 案例(kraft-paper),P1 M16 全幅产品图,P2 M03 翻车叙事,P3 M01 双头机制,P4 M07 收束。验证密度规则在"图+文+ledger"混合卡上的落地。 +- `demo-showcase-editorial/` — 16 张 1080×1440 案例,逐张覆盖 M01-M16,循环用 5 套杂志 palette。 +- `demo-showcase-swiss/` — 12 张 1080×1440 案例,逐张覆盖 S01-S12,循环用 4 套 Swiss accent。两套 showcase 演示「内容类型与风格脱钩」——美食内容也可走 Swiss、AI 工具也可走 Editorial。 + +**修改** +- `SKILL.md` Step 3 重写:"Editorial 用于 / Swiss 用于" 的硬绑定改成"两种风格是 visual stance,不是 content category;任何内容都可走任一风格;选风格按 editorial intent(feature story 还是 release note),不按 topic lookup。" +- `SKILL.md` Non-Negotiables 增加 **3:4 内容密度硬规则**:内容 ≥75% 画布高度;>15% 空白带需"留白理由";禁用 `
` 上下夹击;杂志留白逻辑不适用于社交卡。 +- `references/style-system.md` 顶部增 **"Style ↔ content type are decoupled"** 段;Mode A / Mode B 的"Use for"硬列表改成"Good fits"软建议(含明确反例:workplace essay 也可走 Editorial、travel ledger 也可走 Swiss)。 +- `references/layout-recipes.md`: + - Editorial 和 Swiss recipe 总览段都加了"这些结构适用于任何 topic"的前言。 + - **新增 Content density rule (hard)** 段,写在 Editorial recipe 列表前。 + - **M03 / M04 / M07 / M13 各加 Minimum density 段**: + - M03:title + 3 段 或 title + 2 段 + 编号 footer 列表。title 单独是 M04 不是 M03。 + - M04:唯一允许 ≤60% 内容的 recipe,但必须有 3 个 anchor(source row / kicker / hairline)。 + - M07:title + ≥4 ledger items with sub-lines + closing block。"3 短 ledger 行"是已知失败模式。 + - M13:必备 anchor(top kicker + bottom prompt + bottom metadata + 可见 WebGL bg)。 +- `references/qa-checklist.md`:新增 **4 横带密度检查**(Filled / Justified empty / Under-filled 分类,>15% 画布的 under-filled 带判失败;通过门槛:Filled+Justified ≥100%、Filled ≥75%、相邻两带不能都 justified)。 +- `PRODUCT.md` 决策 1 重写:风格不再绑定内容品类。删除"Editorial × 人文/Swiss × 工程"的硬对应;明确"两种风格是视觉立场不是内容门"。 + +**修复路径(落到代码)** +- 杂志留白逻辑→社交卡的对策:用 `flex-grow: 1` + `justify-content: space-between` 让"内容块本身撑开",禁用空 `
` 作为占位。`demo-eyeliner` P2/P4 两次重渲染后通过 4 横带检查。 +- M07 spec 从 "2-3 rules" 改成 "4-6 ledger items with sub-lines",每行 100-140px 垂直。 + +**未变动** +- Recipe 计数:仍是 M01-M16 + S01-S12 = 28 个。 +- Theme palette 计数:5 + 4 = 9 套。 +- 工作流步数仍 7 步;种子模板未动。 +- 小红书品类能力圈未变;只是不再把"风格-品类"硬绑定。 + +**对比验证** +- 旧 P2/P4(v0.9 demo-eyeliner 第一版):内容散在画布中段,底部 25%-35% 是空白带,缩略图刷过去像 PPT 漏排。 +- 新 P2/P4(v0.10):内容撑满到 ~92% 画布高度,引语+footer 紧贴底缘;FALLOUT 四条 a-d 均匀分布到引语之上。 +- showcase 两套:28 个 recipe 每个出 1 张 1080×1440,全部通过 4 横带检查、覆盖全 9 调色、内容类型故意打散(户外 / AI / 食谱 / 数据 / 节气 / 影评等都既出现在 Editorial 也出现在 Swiss)。 + +### v0.9 · 2026-05-26 + +主题:**M16 编辑杂志化重写**。v0.8 的 M16 借了"游戏 key art"那套(全幅黑色 vertical falloff + 132px 黑体感字 + 纯白文字),出来像旅游海报、不像 Kinfolk / Cereal。本轮按 8 张参考的共同语言推倒重来:**选图是主要杠杆,遮罩是兜底;字小、字细、字色偏纸**。 + +**修改** +- `references/image-overlay.md` **Rule 1 重写**:"选图先,遮罩后,禁全幅 falloff"四步法。Step 1 选图门(quiet-zone 测试 + light 测试),Step 2 默认不加 mask 直接验,Step 3 兜底用 localized + image-toned 软渐变(peak α ≤ 0.30),Step 4 缩略图测试。Banned 段把"全幅 vertical falloff"和"纯黑 mask 色"明确点名禁掉。 +- `references/image-overlay.md` Rule 2 增 **Composition discipline** 段:4 条编辑杂志构图硬规(非对称、≥60% 留白、标题只占一个 quiet zone、绝不跨越主体轮廓)。 +- `references/image-overlay.md` Checklist 重写:把"≥60% → 强制 mask"翻成"先看选图通不通过,mask 仅在 Step 4 失败时"。 +- `references/layout-recipes.md` **M16 重写**: + - 顶部加 **Photo qualification gate**——quiet-zone test + light test 都过才能走 M16,否则退回 M01。 + - Modes A/B/C/D 表里"Mask"列改成"Tint (only if Step 4 fails)",明确"先无 mask、不行再局部图调色软渐变"。 + - **字号字色字体彻底改**:标题 88-108px(v0.8 是 108-132px),Noto Serif SC 400-500(v0.8 没限制字重,default 700+),色 `#f5f1e8` 纸米(v0.8 是 var(--paper) 容易跑成 #fff),letter-spacing 0.10-0.15em。 + - 加 **Forbidden in M16 type** 段:禁 inline 700+ 字重、禁纯白 `#fff`、禁标题 > 120px、禁拉丁 sans 作主标题、禁 D 模式标题居中。 + - HTML skeleton 两段重写:默认不加 mask、用 `Noto Serif SC weight 500 + #f5f1e8`,全 inline 字号(不依赖 `.h-display`,因为种子模板 `.h-display` 强制 `color: var(--ink)` 在暗背景上失效)。 +- `demo-m16-camping/index.html` **P1 封面重做**:Mode A(顶 kicker + 底标题),无全幅 mask,仅底部 22%x84% 苔绿色 (`rgba(20,32,22,...)`) 软 radial 在标题区,标题 92px Noto Serif SC weight 500 / tracking 0.12em / 色 `#f5f1e8`。HTML 注释加 photo qualification 记录。 + +**未变动** +- Editorial recipe 计数:仍是 M01-M16。 +- Swiss recipe / theme / 工作流 / 种子模板没动。 +- demo-m16-camping P2 (ledger) / P3 (field notes) 没动——它们用的是 M05/M02 文字主导版式,没踩 M16 的坑。 + +**对比验证** +- 旧 P1(v0.8):黑色 78% 侧栏 scrim + 132px 字 + 整页压重。读起来像旅游打卡。 +- 新 P1(v0.9):林荫透出、字小克制、底部苔绿软渐渍只够支撑标题对比。读起来像 Kinfolk camping 专题。 +- 缩略图测试(360px wide):标题 / kicker / 底部 mono strip 全部可读,无糊。 + +### v0.8 · 2026-05-26 + +主题:**图先序列 + 满图封面 recipe**。给"生活化、吃图片"的内容一个明确的 P1 图占满 + P2-3 数据的版式。 + +**新增** +- `references/layout-recipes.md` 新增 **M16 Image-Led Cover (Full-Bleed Hero)**:填补 M01"图+文分块"到"图占满 + 文字压上"的空白。含 4 种文字位置(顶压底沉 / 侧栏立柱 / 角落徽章 / 下沉条带,按 subject map 选)、字号 spec 表、标题长度上限表、1:1 / 21:9 适配、两段完整 HTML 骨架(Mode A + Mode D)。Editorial 模式专用,Swiss 不用。Editorial recipe 数:15 → 16。 +- `references/content-planning.md` 新增 **Image-Led Sequence** 段:P1 M16 满图 → P2 文字数据卡(S11/M05/M10)→ P3 M02 副图 → … 硬规则"绝不连续两张 M16"。 + +**修改** +- `references/category-cookbook.md`: + - 旅行 / 家居 / 美食(食谱)路由首选改为 M16 → S11/数据卡 → M02 的图先序列。 + - 情感不强推图先序列(M16 不适合 quiet documentary 的 "窗 / 椅 / 手"——继续走 M04/M09/M11 文字主导)。 +- `SKILL.md` Seed 引用范围 `M01-M15` → `M01-M16`。 + +**未变动** +- 工作流步数与种子模板。 +- Swiss 12 个 recipe / 9 套 palette / image-overlay 规则。 +- M16 强依赖既有 image-overlay.md(mask + subject map + crop),没有放宽任何安全检查。 + +### v0.7 · 2026-05-26(上一轮) + +主题:**Editorial 图源扩展**。没有新增 layout,没有改种子。 + +**修改** +- `SKILL.md` Step 6 web-sourced 图源从 3 条扩到 5 条:新增 **Pexels**(#2,原生支持中文搜索,补 Unsplash 在国内场景的缺口)与 **Flickr CC**(#3,`license=2,3,4,5,6,9` 过滤,补"纪实感真实感"的缺口)。加了取图顺序:Pexels(中文/国内)→ Unsplash → Flickr CC(纪实)→ direct search。明确"全部免版权图库,不接 视觉中国 / Getty / 站酷海洛 等付费源"。Swiss 模式继续不走图库。 +- `references/category-cookbook.md`: + - 旅行:Pexels 优先用于国内目的地(中文关键词);Flickr CC 用于"真实旅程"的纪实质感。 + - 美食:Pexels 用于中式菜品 / 国内餐厅;Unsplash 标注为"西式 stock 感",仅做兜底。 + - 情感:Flickr CC 升为主推(窗 / 椅 / 手这类纪实质感最契合);Pexels 补国内语境;Unsplash 仍然可用但避开"moodcore" gradient-light stock。 +- `references/qa-checklist.md`:图源 checklist 改为枚举 Pexels / Unsplash / Flickr CC / Wallhaven / direct search,并提示 Flickr CC 在用户同意标注时要保留作者署名。 + +**新增** +- `SKILL.md` Step 1 Intake 加"三选一"入口:用户只给文字时问一次(A. 自己提供照片 推荐 / B. 我帮你找 / C. AI 生成),按选择走 Step 6,不二次劝导。 +- `SKILL.md` Step 6 "When the user has no images" 改为依赖 Step 1 gate,明确不再自动跳到 B/C。 + +**未变动** +- 工作流步数(仍为 7 步,三选一是 Step 1 内的子项)与种子模板。 +- 9 套 palette 与 27 个 recipe。 +- 不接付费图库(视觉中国 / Getty)是产品边界(见 `PRODUCT.md` 决策 4)。 + +### v0.6 · 2026-05-26(上一轮) + +主题:**冒烟测试 + 种子模板硬化**。没有新增 layout,没有改工作流。 + +**新增** +- `smoke-ai-tools/` — 完整冒烟测试样例。主题"程序员一周 AI 工具使用周记"。用 5 张 1080×1440 跑通 S01/S07/S10/S11/S12 五个 recipe。Klein Blue accent。 +- `HANDOFF.md` · `PRODUCT.md`(本文件 + 产品文档) + +**修改** +- `assets/template-swiss-card.html`:为 `.poster.xhs` 补全 display 字号 override。 + - `.h-hero` 168px · `.h-statement` 124px · `.h-xl` 96px · `.num-mega` 168px · `.num-xl` 120px + - `.matrix-fill` 改为 `grid-auto-rows: min-content`、`min-height: 0`、`gap: var(--sp-4)` + - `.matrix-cell .cell-title` 在 xhs 下降到 24px + - `.hero-stat-bottom` 在 xhs 下紧凑 margin / padding,`num-mega` 128px +- `references/components.md`:新增两张实测对照表。 + - "Swiss `.h-xl` Hard Caps per Board" — 每板最多几行、单行最多几字、超限会怎样 + - "Matrix + hero-stat-bottom on `.poster.xhs`" — cell 数对应的搭配建议 + +**已知遗留** +- Editorial 模板(`template-editorial-card.html`)**未做同样的冒烟测试**。M14 纵向 Pipeline 5 步在 3:4 板上是否溢出,未验证。 +- 冒烟测试只覆盖 5 个 Swiss recipe(S01/S07/S10/S11/S12)。其余 S02-S06/S08/S09 未跑过真实小例子。 + +### v0.5 · 2026-05-26 早些时候 + +主题:**image-overlay 规则上线 + 文字压图验证**。 + +**新增** +- `references/image-overlay.md` — 文字压图规则:蒙版强制 + 多模态主体映射 + 缩略图测试 +- `demo-image-02-wukong/` — 用《黑神话:悟空》主视觉验证 image-overlay 规则 + +**修改** +- `SKILL.md` Step 6 加入文字压图小节 +- `references/style-system.md` 新增 Anti-Pattern D(封面照片裸跑无蒙版) + +### v0.4 · 之前 + +主题:**模板种子 + 9 个新版式补全**(对齐已成熟的 PPT skill)。 + +**新增** +- `assets/template-editorial-card.html` · `assets/template-swiss-card.html` 两份种子 +- `references/components.md` 字体/卡片/间距/Lucide token 表 +- `layout-recipes.md` 加 M12-M15(Editorial 缺失节奏型)+ S08-S12(Swiss 5 个 port from PPT) + +**修改** +- `SKILL.md` 加 Step 4.5(拷贝种子模板) +- Required References 加 `components.md` + +### v0.3 及更早 + +`SKILL.md` + 9 份 reference 的初始体系。小红书品类能力圈(`category-cookbook.md`)。Editorial 11 个版式 + Swiss 7 个版式。 + +--- + +## 4. 如何验证 skill 还能跑 + +最快的验证路径,不要发明新需求。 + +```bash +cd /Users/guohao/Documents/code/HyperFrames-test/guizang-social-card-skill/local-tests/smoke-ai-tools +node render.mjs +ls output/ # 应该看到 xhs-01.png ... xhs-05.png +sips -g pixelWidth -g pixelHeight output/xhs-01.png # 应为 1080×1440 +``` + +5 张图都能渲染、尺寸都对 → Swiss 种子模板 + 5 个 recipe 工作正常。 + +视觉验收:用 `Read` 工具读 `output/xhs-04.png`,确认大数字 "8" 在右下角完整可见、未被截断。 + +更全的 demo:`local-tests/demo-image-02-wukong/index.html` 用 Playwright 渲染,验证 image-overlay 规则下文字与人物未冲突。 + +--- + +## 5. 工作流要点(给接手者的极简版) + +按 `SKILL.md` 第 7 步走,**不要跳步**。最易踩坑的两步: + +- **Step 3 不要混风格**:Editorial 用 serif + ledger/marginalia;Swiss 用 Inter + card-fill/matrix。两套 class 命名空间隔离,模板间不能跨用。 +- **Step 4.5 必须拷种子**:从零写 HTML 是 v0.4 之前的工作流,已废弃。 + +文字压图的页(封面 / 大图位 / 全幅图)三步:先 `Read` 图、再写 HTML 注释记录主体位置、再选 `object-position`。蒙版是强制的。 + +--- + +## 6. 已知风险与限制(事实层面) + +- **没有自动校验脚本**。所有"标题超过几个字会溢出"目前靠 components.md 的硬上限表 + 人工渲染 + 视觉判断。 +- **没有跨平台一次出包工具**。一篇文章要同时出小红书 3:4 + 微信 21:9+1:1 + 方封面 1:1,目前要手工开两个任务文件夹。 +- **品类能力圈未硬编码**。`category-cookbook.md` 写明了 4 个不接的品类,但 skill 不会主动拒绝;需要执行者在 Step 1 自己读到并主动告知用户。 +- **图片来源签名机制是"用户决定"**。`SOURCES.md` 总会写,但是否在图上加标注由用户回答。这不是技术限制是产品决策(见 `PRODUCT.md`)。 + +--- + +## 7. 不要做的事 + +- 不要改 PPT skill(在 `~/.claude/skills/guizang-ppt-skill/`)。本 skill 借了它的视觉原则但**独立维护**。 +- 不要把 demo 文件夹删掉来"清理"。它们是回归测试样本。 +- 不要在两份种子模板里塞动效系统、键盘翻页、ESC 索引——那是 PPT 的需求,社交卡是静态 PNG。 +- 不要把 components.md 的硬上限表"调宽"以接更长的标题。先压文案。 diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..b7e06a3 --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,260 @@ +# Product — guizang-social-card-skill + +这份文档讲**为什么**。事实在 `HANDOFF.md`。 + +--- + +## 1. 这个 skill 解决什么问题 + +**真正的问题不是"画图",是"把一篇文章变成一组能打的小红书图"。** + +普通用户的现实链路: + +1. 写完一篇文章(笔记 / 影评 / 攻略 / 周记 / 教程)。 +2. 想把它发小红书或公众号,需要 5-9 张图 + 1 张封面。 +3. 没有设计师,没有时间在 Canva / Figma 里逐张拉。 +4. 用模板套,又被几百个相似封面挤掉点击。 +5. 让 AI 生成,结果大部分 AI 出图就是"题目放正中 + 渐变背景 + 一个 emoji"。 + +我们要在第 2-5 步之间插一个东西,让产出不像 AI,又不需要用户懂排版。 + +### 核心矛盾 + +- **质量**:要像 Guizang 这种磁带、墨水、左对齐网格的杂志感,不能像 PPT 模板。 +- **效率**:要 30 分钟内能拿到可发布的图,不能让用户在 prompt 上反复打磨。 +- **不假**:数据、报价、版本号不能编。文字压图不能压到人脸上。 + +三者之中**质量是分水岭**。Canva 模板已经解决了效率。AI 出图已经解决了速度。但都没解决"看一眼就知道是用心做的"这件事。 + +--- + +## 2. 产品定位(一句话) + +> 把一篇文章交给 Claude,30 分钟内拿到一组可以直接发的小红书图或公众号封面,**长得像一本被偏爱的杂志**,而不是套模板。 + +这里"被偏爱的杂志"是一个具体的视觉锚点: + +- 上海译文社的封面 +- The New Yorker 的内文版式 +- Apartamento / Kinfolk 的留白节奏 +- Massimo Vignelli / Helmut Schmid 的瑞士网格 + +不是"能看"或"好看",是**能让人停下手指**。 + +--- + +## 3. 目标用户与典型场景 + +主要画像是 **"内容创作者 + 工程师 / 产品 / 设计师 / 自由职业者"**。共同点:自己有内容,没有"组里专门做封面的人"。 + +| 场景 | 用户类型 | 输入 | 期望输出 | +| --- | --- | --- | --- | +| 周报 / 工具周记 | 工程师 / 产品 | 一段 Markdown + 数字(小时数 / 价格) | 5-7 张小红书 3:4,数据图 + 总结 | +| 旅行 / outdoor | 摄影爱好者 / 旅人 | 一组照片 + 文字日志 | 一张公众号封面对(21:9 主图 + 1:1 方图,同一篇文章的两个尺寸)+ 5-9 张小红书组图 | +| AI / 产品 release | 创业者 / DevRel | 一段更新说明 + 截图 | 一组 Swiss 风的 release deck | +| 影视 / 游戏笔记 | 影评 / 游戏播客 | 一段评论 + 主视觉 | 1 张大图压字封面 + 数据榜单 + 总结页 | +| 美食 / 食谱 | 美食博主 | 文字步骤 + 成品照 | 3:4 教程卡(用户提供照片) | +| 健身 / 家居 / 穿搭精选 | 生活方式博主 | 推荐清单 + 产品图 | 推荐卡 + 单品图 | + +**反向画像**(这个 skill 不服务他们): + +- 想做 OOTD / 日常自拍:我们不生成人像照片。 +- 想做菜品大片摆盘:我们不替代专业美食摄影。 +- 想做"梦核 / Y2K / kawaii":和我们的两套视觉系统正面冲突,硬接出来一定难看。 + +这些在 `references/category-cookbook.md` 里写明了"不接",是产品决策,不是技术缺口。 + +--- + +## 4. 用户视角的核心思考 + +整个工作流是按"用户脑子里的步骤"组织的,不是按"代码模块"组织的。 + +| 用户脑子里 | Skill 对应步骤 | +| --- | --- | +| "我有这篇东西,想发小红书" | Step 1 Intake | +| "我该出几张?写什么标题?" | Step 2 Extract Story + Step 4 Plan Pages | +| "我要这种感觉的图,不要那种" | Step 3 Choose Style Mode | +| "随便先出一版我看看" | Step 4.5 Seed + Step 5 Build | +| "这张图有问题,再改改" | Step 5 重渲染 + Step 7 Deliver | + +每个步骤都有一个明确的"用户在意什么": + +- **Step 1 不要审讯式提问**。能从已有信息推断就推断,只问真的会改变结果的问题(平台、比例、必须出现的元素)。 +- **Step 2 是产品差异**。一篇 1000 字的笔记,应该是 5 张图还是 9 张?哪些段落该被丢进文案区不放进图?大部分 AI 工具不做这一步——这是为什么它们出的图像"被切碎的文字"。 +- **Step 3 不让用户选"主题色"**。让用户选"感觉":是杂志感的人文叙事,还是瑞士的工程冷感。色板是结果,不是入口。 +- **Step 4.5 是体力活的拐点**。v0.4 之前每次任务都要从零写 HTML,能写出来,但一致性差。种子模板把"杂志的形"固化下来,让 AI 把精力花在"内容如何变形"上。 +- **Step 6 文字压图**是最常翻车的位置。v0.5 之前 AI 经常把标题压在人脸上,看起来不假但很难看。`image-overlay.md` 强制三步走(读图 / 写主体映射 / 选 object-position)后才把这块兜住。 + +--- + +## 5. 关键设计决策(为什么这样做) + +### 决策 1:拆两套视觉系统,不做"一站式" + +更通用 = 更平庸。我们决定**只做两件事**: + +- **Editorial Magazine × E-ink**:serif + 墨水 + 杂志结构(ledger / marginalia / pull quote / photo well) +- **Swiss International**:Inter + 一种 accent + 工程结构(matrix / KPI tower / h-bar / 编号 statement) + +不做"卡通 IP 风"、"少女风"、"国潮"、"Y2K"。一个 skill 同时维护 5 种风格的结果一定是 5 种都做不好。 + +**注意**(v0.10 起明确):两套系统是**视觉立场**,不是**内容类型门**。一篇 AI 笔记可以走 Editorial(如果你想要它读起来慢、像专栏),一组旅行支出也可以走 Swiss(如果你想要它读起来像数据报表)。我们曾在 v0.9 之前在文档里把"Editorial = 人文 / Swiss = 工程"写得过死,结果出现"美食教程一定要塞进 Editorial 哪怕用户想要 step 数据感"这种错误路由。v0.10 解绑:选风格按"你想要这篇内容读起来什么节奏",不按"这篇是哪个品类"。代价是用户多一道选择题,收益是别再让风格绑架内容。 + +代价:用户如果想要的"梦核氛围"在我们能力圈外,要在 Intake 时直接告知,不能硬接。这是产品边界,不是 bug。 + +### 决策 2:种子模板 = 不可见的设计师 + +v0.4 之前,每次任务 AI 都从空文件开始写 CSS。结果是: + +- 字体栈每次都重新写,偶尔忘记加 fallback。 +- 间距 token 每次都重新发明,导致两张图之间间距不一致。 +- 卡片填充逻辑混乱,Swiss 的"四类卡片互斥"规则形同虚设。 + +种子模板把这些**藏在用户看不见的地方**。用户拿到的产出依然是"一份 HTML",但 AI 不用再每次重新决定"是用 `--gap-7` 还是 24px"。这是为"质量上限"和"一致性"花的成本。 + +### 决策 3:components.md 的硬上限是"产品承诺" + +`.h-xl` 在小红书 3:4 板上 96px、最多 2 行、单行最多 8 个汉字。这不是技术上限——CSS 当然允许写 120px、3 行、12 个字。这是**产品承诺**: + +> 我们承诺出的图不会让你尴尬。 +> 如果你的标题撞破这个上限,AI 会主动建议你压缩文案,而不是悄悄把图拉变形。 + +很多 AI 工具的失败模式是"用户说什么就硬塞什么"。我们选择反过来——**告知用户文案太长,比交付一张丑图更礼貌**。 + +### 决策 4:图片来源"先取后说" + +`SKILL.md` Step 6 的 web-sourced 部分写得很明确: + +> Policy: 先取,后告知,让用户决定。不要靠猜版权预先过滤。 + +理由: + +1. 大多数 AI 工具的"版权安全"逻辑实际是"假装版权安全"。在 Unsplash / Wallhaven 取图本身不构成侵权,**是否在终稿署名是用户的判断**。 +2. 用户是终稿的所有人。把决策权交给他,附上 `SOURCES.md` 留痕,是更尊重也更准确的做法。 +3. 等"完美的免版权图库 API"会饿死。先解决"小红书今天能发",慢慢解决"版权完美"。 + +### 决策 5:暗色 Editorial 只开一套——Midnight Ink + +v0.12 之前 Editorial 只有 5 套浅色 palette。Codex 在测试黑神话主题时自造了 `wukong-night`,事后看是合理外推(游戏 key art 强行套浅色 paper 会塌),但 skill 文档当时还在写"用 5 套之一不要改",造成"规则太死 vs. 视觉合理"两难。 + +v0.12 解法:**新增官方暗色 Midnight Ink,但只开一套**。 + +- 开一套的理由:游戏 / 夜景 / 影调封面这类内容确实需要暗背景,硬塞浅色 palette 是质量塌方。 +- 不开多套的理由:暗色 palette 的差异(金辉 / 冷蓝 / 紫罗兰 / 血红)很容易滑向"游戏宣传海报"的廉价感。一套金辉(`#d4a04a`)作为默认 accent,既呼应黄金树 / 神道 / 唐宋夜画这类经典暗调,也守住"杂志感不是游戏感"的底线。 +- 用户想要其他暗色 accent → 优先建议改用 Swiss 暗变体(未来可考虑)或回退选最接近的浅色 palette 重新构图。 + +这是和"决策 1:拆两套视觉系统不做一站式"同一类思路——**少而准 > 多而平庸**。 + +### 决策 6:交付前不自动跑 validator,问用户再决定 + +v0.6-v0.11 都把 validator(`validate-social-deck.mjs`)写成"渲完必须自动跑"。v0.12 改成"渲完先把图给用户看,问一句要不要核查,用户说要才跑"。 + +起因是真实工作流里的两件事: + +1. **校验耗时**。一份 5 张图的 deck 跑一遍 R1-R6 + 重渲修补,常常 5-10 分钟。多数情况用户的眼睛在 30 秒内就能扫出"哪张需要改"。 +2. **校验的盲区**。Validator 测的是溢出 / 字号下限 / 行 cap,**测不出** "这张图气质塌了 / 这个 ledger 第三条没说人话 / 这个 accent 在暗背景上太蜡黄"——这些是用户最在意的、也是 AI 最容易翻车的位置。 + +所以让用户做第一道审查、自动校验作为兜底的可选项,而不是反过来。代价是"完全自动化"的承诺降级——但这个 skill 的目标从来不是无人工,是**30 分钟内出图**。校验时间从默认路径里抠出来,能直接让平均交付时间从 8-12 分钟压到 3-5 分钟。 + +### 决策 7:所有图必须是静态 PNG,没有动效 + +社交平台导出后只显示静态图。任何 motion / animation / WebGL 交互在最终产物里**不可见**。所以: + +- 不引入 Motion One / Framer Motion +- 不写翻页 / 键盘导航 / ESC 索引(这些是 PPT skill 的需求) +- WebGL 背景只用于一次性渲染纹理,不真的"动起来" + +这条决策让 skill 比同等复杂度的"PPT 网页"轻一个数量级。 + +--- + +## 6. 反模式(我们主动拒绝的事) + +文档里散落着多处 anti-pattern。这里集中归一下: + +1. **不可读的小字**:正文低于 26px on 1080×1440 → 拒绝。 +2. **重字号 + 重字重的标题**:Swiss 80-120px 配 weight 700 → 拒绝。"越大越细"是 Swiss 的内核。 +3. **任意装饰 SVG**:blob、雨滴、kawaii 贴纸、随手画的圈 → 拒绝。 +4. **嵌套卡片 / SaaS 拼盘**:默认布局不应该是"卡片里再放卡片" → 拒绝。 +5. **裸跑图片压字**:照片占 ≥60% 画面但没加蒙版 → 拒绝。 +6. **假数据 / 假版本号 / 假报价** → 拒绝。 +7. **一图复用到所有比例**:把 21:9 直接裁成 1:1 → 拒绝。两个比例必须分别构图。 +8. **图上加教程说明**:键盘快捷键、操作步骤压在卡上 → 拒绝(教程内容写在 caption / 文案区)。 + +每一条都对应了线上观察到的"AI 出图很糟"的具体形态。这些拒绝是 skill 的**护城河**。 + +--- + +## 7. 未来想做的事 + +按 ROI 排序。下面分两层:**短期已规划** 和 **中长期会做**。最末尾只有一条是真的不做。 + +### 短期已规划(下两轮内开工) + +1. **Editorial 模板的冒烟测试**。v0.6 只在 Swiss 上跑了真实小例子,Editorial 模板尚未做同样的硬化。预计会暴露 M14 纵向 Pipeline 5 步在 3:4 上的溢出,以及 M11 Marginalia Essay 的字号上限。下一轮优先做。 + +2. **校验脚本 `validate-social-deck.mjs`**。在 Step 5 渲染后自动跑: + - 检查每张图的 `scrollHeight` 是否超过 board 高度(v0.6 用 Playwright `evaluate` 验证过这个方法) + - 检查正文字号是否低于平台最小值 + - 检查 `.poster.xhs` 上的 `.h-xl` 是否超过硬上限 + 把 "渲染前看不出来 / 渲染后才发现溢出" 这种坑用脚本兜底。 + +3. **一次出多平台的"包"**。当前一篇文章如果同时要发小红书 + 公众号,要开两个任务文件夹各做一遍。可以做一个 `deck` 概念:一份 source content → 同时产出 **小红书 5-9 张 3:4 组图** + **公众号封面对(21:9 主图 + 1:1 方图,同一封面的两个尺寸)** → 一次渲染出全部 PNG。 + 关键设计点:避免把 21:9 内容直接缩到 1:1(产品决策 7 已禁止)。需要在 plan 阶段就为 21:9 写长标题、为 1:1 写一版短标题。 + +4. **1:1 短标题生成器(公众号封面对的同源短标题)**。21:9 主图用 "一周的代码 · 我和五个 AI 工具的同居生活",1:1 方图用 "AI 同居生活"。注意这是**同一篇文章的封面对**,不是组图——21:9 有水平空间放长标题,1:1 没有所以必须压短。当前两个尺寸的标题靠 AI 自由发挥,可以加一个明确的"6-8 字短标题"约束在 Step 2 的 plan 阶段。 + +5. **地图组件**。旅行类内容经常需要"路线图 + 关键节点"。Mapbox 静态 API 出图能用,但目前要 AI 现写代码调用。可以做成一个 `.map-block` class,传 GeoJSON 进去自动出。 + **执行路径**:用 Mapbox Static Images API(免费额度够冒烟测试),出 1080×720 或 1080×1440 PNG,限制底图为 `light-v11` / `dark-v11` 两种以匹配 Editorial / Swiss 的中性基调,禁用花哨色板。节点用 Swiss 编号风(黑底白数字)或 Editorial 墨点风,避开默认蓝针。GeoJSON 仅接 LineString + Point,复杂面/标注超出范围。 + **路由**:先接入 `category-cookbook.md` 的"旅行"路由,作为 M02 / S08 的可选 inset;不强推。 + +6. **截图美化资产(从 PPT skill 借现成的)**。Tutorial / News 类目最大的痛点是"截图本身不好看"——背景太杂、有阴影、文字太多。可以加一组工具:自动加圆角、自动模糊背景、自动裁安全区。 + **执行路径**:`~/.claude/skills/guizang-ppt-skill/` 已经有现成的截图处理资产,直接 port 过来即可(与 v0.4 拷 Swiss class 的方式一致——只搬截图美化相关的部分,不带 PPT 翻页 / 动效那一坨)。 + **范围控制**:只搬"加圆角 / 加阴影 / 加 device frame / 裁安全区"四件套,不带 PPT 那边的多窗格拼贴;社交卡是单屏阅读,截图最多并排两张。 + +### 中长期会做 + +7. **品类专属配图 prompt 库**。`category-cookbook.md` 已经路由"什么品类 → 用哪些 recipe",下一步是"什么品类的封面图应该用什么 prompt 去生成"。outdoor 需要"晨雾 / 冷调 / 35mm 胶片感",AI 产品需要"keyshot 渲染 / 极少元素 / 单光源"——这些 prompt 现在散在每个任务里重写。 + +8. **小红书 1:1 图文("小绿书")的种子**。当前两份种子主打 3:4 + 21:9。1:1 是次要适配。如果"小绿书"格式起飞,需要一份独立的 1:1-first 种子。 + +9. **AI 出图后处理**。Gemini / Imagen 生成的图经常有"AI 印记"(多手指、变形字、不自然光照)。可以加一步"AI 出图 → 用 Read 工具读 → 标注问题 → 给用户决定要不要重出 / 局部重绘"。 + **执行逻辑**(重点,因为分析图片耗时): + - 默认**渲染完成后不自动分析**,避免每次任务多一次几十秒的等待。 + - 渲染完成后在交付消息里主动问一句:"这页图里 AI 生成了 `assets/hero-xxx.png`,要我帮你检查一下有没有 AI 印记吗?需要几十秒。" + - 用户说要 → 用 Read 工具读图、给出问题清单(多手指 / 文字变形 / 光照断裂)、按问题严重度建议"接受 / 局部重绘 / 整张重出"。 + - 用户说不要 → 跳过,正常交付。 + 也允许在 Intake 阶段就指定"出图后默认帮我检查",让需要严格质控的用户一次配置长期生效。 + +10. **视频 + 图组合包**。让 skill 出完 5 张图之后,调用独立的视频 skill(`seedance-prompt` / `video-wrapper`)顺手出一个 15s 短视频,作为小红书第一帧的"动态封面"。 + **执行边界**:本 skill 不自己做视频生成,只做**调度** —— 把 plan 阶段拿到的钩子、调色、文案传给视频 skill,避免重复一次内容理解。 + +11. **Web UI / 拖拽编辑**。Skill 的最终产物是 HTML+PNG,技术上可以做一个轻量的"出完图后让用户在浏览器里拖一下"的小编辑器(比如 React 包一层、改改文案/换图/调间距、再触发重渲染)。 + **执行边界**:不做"从零拼版"的 Canva 平替——那背离了"文本 → 设计"的核心链路。只做**渲染后微调**,作为最后一公里。 + +### 真的不做 + +12. **风格自定义 / 用户主题色**。诱惑很大,但一旦开放,质量会崩。当前 10 套 palette 是经过视觉验证的上限。让用户传一个 `#A3FF21` 进来,AI 配的衍生色大概率难看。除非有"色板生成器"先过一层,否则不开。 + 这是一条**产品边界**:宁可让用户从 10 套里选一个最接近的,也不让用户精确指定。和"不接 OOTD / 梦核"是同一类决策。 + +--- + +## 8. 度量方向 + +目前没有自动度量,全靠看产出。如果未来要量化,三个方向: + +- **完成率**:一个真实小例子(5-9 张图)的端到端用时。目标:30 分钟以内(v0.6 的冒烟测试在 ~25 分钟做完,含修两次 overflow)。 +- **首版可用率**:第一次渲染出来的图,用户不需要任何修改就能发的比例。目标:> 60%。 +- **品类匹配命中率**:用户说出小红书品类后,路由到的 recipe 用户接受的比例。目标:> 80%(不接受的需要进 `category-cookbook.md` 调整)。 + +这些都暂时是"心里的目标",没有埋点。 + +--- + +## 9. 给接手者的两句话 + +> 当你拿到一份内容,先问"这是不是我们能力圈内的品类"。 +> 当你出完一张图,先问"如果用户截屏发给朋友,朋友会不会愿意停下来看"。 + +前者管"不假"。后者管"能打"。两件事都做到,这个 skill 就值得继续存在。 diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..046ed49 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,300 @@ +--- +name: guizang-social-card-skill +description: Generate Guizang-style social card image sets and WeChat official account cover pairs from articles, scripts, screenshots, product notes, subtitles, or photos. Use when the user asks for 小红书图文, Rednote/Xiaohongshu images, social cards, carousel images, 3:4 covers, 微信公众号封面, WeChat 21:9 + 1:1 covers, Swiss Style, or magazine-style social images. +--- + +# Guizang Social Card Skill + +Create polished social card packages for Xiaohongshu/Rednote, WeChat Official Account, article covers, and platform thumbnails. + +This skill is self-contained. It borrows visual principles from the Guizang PPT style system, but it must not edit the original PPT skill, its templates, or its references. If the original PPT skill is available, you may read it for reference only. Put all generated work in the current project or in the user-requested output folder. + +## What To Produce + +Use this skill for: + +- Social card / carousel image sets: cover plus content pages, especially Xiaohongshu/Rednote 3:4. +- WeChat Official Account cover pairs: one `21:9` main cover plus one `1:1` square cover, composed together in the same HTML for visual checking. +- Screenshot-heavy product posts, article covers, tutorial carousels, outdoor/lifestyle notes, AI/product update explainers. +- Social images that need Guizang-style Swiss or editorial magazine layouts. + +Do not use this skill for: + +- Full slide decks or horizontal PPT websites. Use the PPT skill for that. +- Long-form video generation. Use a video skill for that. +- Pure image editing with no layout or article extraction requirement. + +### Rednote Category Capability (capability circle) + +The 11 most-common Rednote (小红书) categories fall into three buckets. See `references/category-cookbook.md` for the recipe-by-recipe routing. + +**Strong end-to-end** (text, structure, and image story all in scope): + +- 旅行 (Travel), 职场 (Workplace), 推荐 (Recommended, after specifying a subtype). + +**Strong on text & structure; image needs to come from the user or a sourced library:** + +- 游戏 (Game), 影视 (Film/TV), 美食 食谱方向 (Food — recipes only), 彩妆 教程方向 (Makeup — tutorials only), 健身 (Fitness), 家居 (Home), 穿搭 精选方向 (Outfit — capsule/essay only). + +**Outside scope — push back honestly rather than promise a result:** + +- 美食 菜品大片摆盘 (food-photography showcase). +- 穿搭 日常 OOTD 全身 (daily OOTD body shots; we cannot generate or simulate). +- 情感 梦核 / 氛围感装饰风 (dreamcore / aesthetic-light styling — clashes with both Editorial and Swiss). +- Y2K / 千禧辣妹 / 哥特萝莉 / kawaii decorated aesthetics. +- Pure photography showcase posts where the image is the entire deliverable. + +When a request falls in the third bucket, name what we cannot do at intake — do not silently retrofit a layout that misses the user's intent. + +## Core Principle + +Expression comes first. The goal is not to squeeze text into posters; it is to turn the source into a clear visual argument. + +For each page, decide: + +- What should the viewer understand in one glance? +- What evidence, screenshot, or image supports it? +- Which words must be large, and which can become captions or metadata? +- What can be removed because it belongs in the post body, not the image? + +## Required References + +Read these files as needed: + +- `references/platform-specs.md` for exact ratios, output sizes, and naming. +- `references/style-system.md` for Guizang editorial and Swiss visual rules. +- `references/theme-presets.md` when choosing electronic-magazine palettes or Swiss accent palettes. +- `references/layout-recipes.md` when selecting carousel/social-card/WeChat page structures. +- `references/components.md` for the shared component spec: font stacks, type scale, minimum readable sizes, Chinese title length bands, Swiss card-fill mutual-exclusion rule, image-container ratio classes, spacing tokens, and Lucide icon rules. +- `references/background-systems.md` when building electronic-magazine WebGL/ink/paper backgrounds. +- `references/portrait-fill.md` when adapting layouts to 3:4 and avoiding under-filled vertical space. +- `references/content-planning.md` for cover hooks, page breakdown, and copy compression. +- `references/production-workflow.md` for HTML/CSS rendering and image handling. +- `references/image-overlay.md` whenever text sits on top of a photo: contrast mask rules + face / subject avoidance via multimodal subject mapping. +- `references/screenshot-treatment.md` when the user supplies an app / web / code / dashboard screenshot — picks `.frame-shot` over `.frame-img`, sets corners/shadow/bg/inset, decides on `.device-browser` or `.device-phone` chrome. +- `references/map-component.md` when the content has spatial relationships (travel route, store locations, walking tour) — picks `.map-block` mode S (schematic SVG, default) / T (Mapbox Static, requires token) / O (OSM, fallback), places pins, never uses live JS. +- `references/title-shortener.md` when the task is a WeChat 21:9+1:1 cover pair, or any cross-platform reuse — derives the 1:1 short title from the long one (5-step extraction, 4 patterns, anti-patterns, sizing on `.poster.square`). +- `references/category-cookbook.md` to route a user-named Rednote category (旅行 / 职场 / 游戏 / 影视 / 彩妆 / 美食 / 穿搭 / 家居 / 健身 / 情感 / 推荐) to applicable recipes and to confirm scope. +- `references/qa-checklist.md` before delivering final images. + +## Workflow + +### 1. Intake + +Gather only the missing information that changes the output: + +- Target platforms and ratios. +- Source text, subtitles, article, or title. +- **Rednote category** — if the user names one of the 11 common types (旅行 / 职场 / 游戏 / 影视 / 美食 / 彩妆 / 穿搭 / 家居 / 健身 / 情感 / 推荐), route via `references/category-cookbook.md` to find the right recipes and to confirm the request is inside the capability circle (see "Rednote Category Capability" above). If a request lands in the outside-scope bucket, surface that to the user **before** designing, do not silently retrofit. +- Supplied images/screenshots and where each should appear. **For News / Tutorial / Data / Review content, actively prompt for screenshots or photos** — they are the evidence layer. A poster with no real artifact tends to read as filler. +- **If the user supplies only text (no images at all), ask once before designing:** + + ``` + 这篇我需要 1-2 张图。三种走法: + A. 你自己有照片 / 截图,传给我(推荐——最不"AI 感") + B. 我去 Pexels / Unsplash / Flickr 帮你找 + C. 用 AI 生成 + ``` + + Recommend A in one line — your own photo is what makes a poster not look AI-generated. Accept whatever the user picks (including "都行你看着办") and proceed. **Do not re-prompt later, do not keep nudging toward A across multiple turns.** This question is one-shot. +- Preferred style if specified: Swiss Style, magazine/editorial, tech, outdoor, etc. +- Hard constraints: title text, no image on 1:1 cover, must include a hardware photo, keep screenshot readable, and so on. + +If the user has already supplied enough context, proceed with reasonable assumptions. + +If the content involves current product releases, policies, prices, claims, or news, verify unstable facts with browsing and cite sources in the final response. + +### 2. Extract The Story + +Turn the source into a page plan before designing. + +For Rednote: + +- Page 1 is the cover hook. +- Pages 2-N each carry one idea only. +- Use 5-9 pages for most posts. Compress or combine pages when lower areas become empty. +- Keep the post body for nuance; images should carry hooks, comparisons, checklists, and sharp takeaways. + +For WeChat: + +- Always produce a paired system: `21:9` main cover and `1:1` square cover. +- Build both covers in the same HTML file and add a combined preview section so their visual relationship can be checked together. +- `21:9` keeps the full or near-full title, subtitle, and one strong visual relation. +- `1:1` uses a simplified short title derived from the long title: big centered type, no image by default, no cramped subtitles. + +### 3. Choose Style Mode + +Pick one mode per package. **The two systems are not bound to specific content types** — what changes is the visual stance, not which topic you can talk about. A workplace essay can be Editorial; a travel ledger can be Swiss. Pick by the feeling you want, not by category lookup. + +**Editorial Magazine x E-ink** brings: + +- Serif/Songti display + quiet sans body, paper + ink palette. +- Atmosphere layer (paper grain / ink wash / WebGL canvas) over a warm paper base. +- Ledger rows, marginalia, pull quotes, large photo wells — magazine-feature feel. +- Best when you want the page to feel slow, considered, hand-set. + +**Swiss International** brings: + +- Inter / Helvetica feel, very light display at large sizes, mono labels at small. +- Strict left-aligned grid, hairline rules, one high-saturation accent. +- Card-fill matrices, KPI towers, h-bar charts, numbered statements — system / data feel. +- Best when you want the page to feel engineered, quantified, decisive. + +If both feel viable for a piece of content, the question becomes editorial intent: "is this a feature story or a release note?" That decides the mode, not the topic itself. + +Do not mix the two visual systems inside the same image set unless the user explicitly asks for a hybrid. + +Then pick one theme: + +- Editorial Magazine x E-ink uses one of 6 magazine palettes: Ink Classic, Indigo Porcelain, Forest Ink, Kraft Paper, Dune, or Midnight Ink (the only dark variant; reserved for game key art / night photography / cinematic covers). +- Swiss International uses one of 4 accent palettes: IKB Blue, Lemon Yellow, Lemon Green, or Safety Orange. + +Read `references/theme-presets.md` for exact CSS tokens. Do not invent arbitrary colors unless the user has a strict brand requirement. + +### 4. Plan Pages + +Create a concise internal plan: + +```text +Page 01 / cover / hook / image source / layout intent +Page 02 / point / key copy / visual evidence / layout intent +... +``` + +When the user asks for approval, show this plan before rendering. Otherwise use it internally and proceed. + +Use `references/layout-recipes.md` to choose page structures. Avoid making every page a repeated title-plus-card layout. + +For 3:4 images, check `references/portrait-fill.md` before coding. A short table or ledger must be expanded into a full portrait composition with a quote column, image evidence, marginalia, larger rows, or a background hero zone. + +### 4.5. Copy The Seed Template + +Do not write HTML from scratch. Pick one seed template based on the style mode chosen in Step 3: + +- Editorial Magazine × E-ink → copy `assets/template-editorial-card.html` into the task folder as `index.html`. +- Swiss International → copy `assets/template-swiss-card.html` into the task folder as `index.html`. + +The seed already wires up: font loading, theme tokens, all three poster sizes (`.poster.xhs` / `.poster.square` / `.poster.wide`), the pair-preview frame, grain/background layers, and all class definitions referenced by the layout recipes. + +Set the theme/accent on the `` element: + +- Editorial: ``. +- Swiss: ``. + +Replace the single placeholder poster after `` with one `
` block per page, each carrying the HTML skeleton from a chosen Layout Recipe (M01-M16 for Editorial, S01-S12 for Swiss). Never load the wrong template's class system: Editorial recipes assume serif display + ledger/marginalia/pipeline-v; Swiss recipes assume Inter + card-fills + matrix/h-bar/kpi-tower. Mixing them silently breaks the layout. + +### 5. Build And Render + +Default implementation pattern: + +- Create a task folder in the current workspace, for example `social-card-/`. +- Put source images in `assets/`. +- Start from the seed template copied in Step 4.5, not a blank file. Only the `` region should change page-to-page. +- Use Playwright or a browser screenshot tool to export each `.poster` or `.cover` node. +- Save rendered images in `output/`. +- Verify dimensions and inspect the rendered PNGs. +- Run `node validate-social-deck.mjs ` after every render. It checks overflow (R1), footer collision (R2), Swiss bold display (R3), minimum font size (R4), 4-band density (R5), and `.h-xl` line caps (R6). Exit code 1 on any FAIL — fix before delivery. WARN is advisory but read it. + +Do not place visible instructions, keyboard shortcuts, or usage explanations inside the images. + +For Editorial Magazine x E-ink, use a layered background system. Prefer a subtle WebGL ink-flow canvas or a frozen procedural canvas plus paper grain. Read `references/background-systems.md`; do not rely on a flat beige background, and do not add page-wide grid/dot backgrounds. + +### 6. Image And Screenshot Handling + +When the user provides screenshots: + +- Preserve screenshot content unless the user asks for redesign. +- Prefer programmatic framing: target-ratio canvas, safe padding, clean background, readable screenshot. +- Do not stretch screenshots. +- If screenshot clarity matters, enlarge the screenshot area and reduce nearby text. + +#### Text-On-Image Composition + +Whenever a poster places text on top of a photo (full-bleed cover, large image well, generated-image overlay), follow `references/image-overlay.md`: + +- **Mask is mandatory.** A photo covering ≥60% of the canvas needs a contrast mask layer (vertical falloff / side scrim / radial vignette). Never rely on the photo's own brightness. +- **Subject mapping is mandatory.** Before placing the title, read the image with the Read tool, describe in plain language where the subject's face/focal feature sits, and record the subject map as an HTML comment next to the hero block. Place text only in the documented safe zones. +- **Crop discipline — set `object-position` inline on every photo.** The template default (`center 50%`) is a fallback, not a recommendation. For every ``, decide based on subject location and write it inline: e.g. `style="object-position:center 62%"` for mid-body subjects, `center 30%` for sky-heavy landscapes with horizon-line subjects, `center 70%` for foreground gear. See the table in `references/components.md` for ranges and `image-overlay.md` for face-photo specifics. Skipping this silently crops subjects out of frame on tall ratios (`r-3x4`, `r-21x9`). +- **Thumbnail test.** Downscale the rendered PNG to 360 px wide and confirm the title is still legible. If the title fights the photo, the mask is too thin; if the photo looks dead, the mask is too heavy. + +Editorial dark covers (e.g. game journals on key art) and Swiss covers with hero photos both require these checks. Skipping them is a known failure mode (see `style-system.md` Anti-Pattern D). + +When the user has no images: + +- This branch only runs if the Step 1 "三选一" gate landed on B (web-sourced) or C (AI-generated). Never silently fall into B or C — the user picked one. +- For C (AI-generated): use generated bitmaps only where they add real value, usually 1-2 pages. Generate images that match the page's visual role, not generic decoration. Keep generated images free of embedded titles, page numbers, logos, or fake UI labels unless explicitly needed. +- For B (web-sourced): see the Web-Sourced Images section below. + +#### Web-Sourced Images (fallback when user has none) + +When the user has no screenshots/photos and a generated bitmap would not fit the page's role (e.g. Editorial atmosphere shot, outdoor / lifestyle backdrop, game cover art, real-world product shot), fetch from the web instead of leaving the page thin. + +Policy: **grab first, disclose after, let the user decide on attribution.** Do not pre-filter sources by guessed license — the user is the rights holder of the final composition and decides what is acceptable. + +Recommended sources, in order of preference. **All five below are free-tier libraries with no required licensing fees**; we do not pull from paid stock sites (视觉中国 / Getty / 站酷海洛 etc.). + +1. **Unsplash** — `https://unsplash.com/s/photos/`. Strong for outdoor / lifestyle / atmospheric backdrops. English keywords work best. License is permissive but verify case by case. +2. **Pexels** — `https://www.pexels.com/search//` or `https://www.pexels.com/zh-cn/search//`. **Supports Chinese keyword search natively** — fills Unsplash's gap on 国内场景 (中文街景 / 国风物件 / 本地地名). Use this first when the subject is China-specific or the keyword is Chinese. Free under Pexels License. +3. **Flickr CC-licensed pool** — `https://www.flickr.com/search/?text=&license=2%2C3%2C4%2C5%2C6%2C9`. The license filter (`license=2,3,4,5,6,9`) restricts to Creative Commons photos. Fills the "documentary realness" gap: street photography, people-in-context, real interiors, non-styled scenes that Unsplash/Pexels lack. Always preserve CC attribution if the user opts in. +4. **Wallhaven** — `https://wallhaven.cc/search?q=`. Strong for game / anime / wallpaper themes. Content is user-uploaded, rights are unverified. +5. **Direct web search** — when a specific subject is needed (a product render, a game still, a historical photo). Use WebFetch / WebSearch to find a candidate URL. + +Editorial-mode picking order: **Pexels (if keyword is Chinese / China-specific) → Unsplash → Flickr CC (if you need real-life feel) → direct search**. Swiss mode rarely needs any of these — product renders, UI screenshots, and keyshot-style images should be user-supplied or AI-generated, not stock. + +How to fetch: + +- Use WebFetch or `curl` to download the image into the task folder's `assets/` directory. +- Name the file by purpose, not by hash: `assets/hero-mountain.jpg`, `assets/ui-pulse-card.png`. +- Record the source URL in a `assets/SOURCES.md` file next to the images (one line per file: `hero-mountain.jpg ← `). Always do this even if the user declines attribution in the final image — it preserves provenance for the human author. + +After fetching, surface the provenance to the user **before** finalizing the design: + +``` +我从 取了这些图: +- assets/hero-mountain.jpg — +- assets/ui-pulse-card.png — + +⚠️ 版权未经核实。请你判断是否可用。 +是否需要在图文中标注来源? +- 要:我把 "Photo · · @" 加到对应页脚 / 角标。 +- 不要:原样使用,不加注释。 +``` + +If the user picks "标注" — add a small `mono` caption (Swiss: `.t-meta` 18-20px in corner; Editorial: `.label` next to the image well). Never crowd the caption into the layout's focal area. + +If the user picks "不标注" — proceed silently. The provenance still lives in `assets/SOURCES.md` for the user's own records. + +If an image is only one element among many in a composite (e.g. one of nine photos in a matrix), the user may reasonably skip attribution. Do not force a credit label that breaks the layout. + +### 7. Deliver + +**Show user first, validate on request.** Auto-running the validator after every render takes too long and delays the user from seeing results. Default flow: + +1. After rendering completes, immediately show the user the rendered images inline (absolute paths) with a one-sentence summary of what was built. +2. Ask one question: **"先你自己看,还是我先自动核查一遍?"** (Do you want to review first, or should I run the auto-check?) +3. If the user says "我自己看" / "先给我" / "no need" — stop here, let them inspect, and respond to whatever they raise. +4. If the user says "你查吧" / "auto-check" / "yes" — only then run `node validate-social-deck.mjs `, fix any FAIL, and re-render before final delivery. Mention density/cap WARNs. + +Never silently run the validator before showing the user — it costs minutes per pass and the user often spots issues faster. + +Final response (after the user has reviewed or asked for auto-check) should include: + +- Output folder path. +- Rendered images shown inline with absolute paths when useful. +- A short note on dimensions and verification (or "not yet validated, awaiting your review"). +- For any image fetched from the web: source URL + site + the attribution decision the user made. +- Any unresolved risks, such as source images being low resolution. + +## Non-Negotiables + +- Never edit the original Guizang PPT skill or any upstream skill copied from elsewhere. +- Do not create random decorative SVG ovals, blobs, rain drops, stickers, or meaningless circles. +- Do not use nested cards or generic SaaS card layouts as the default. +- Do not let text overflow, touch the edge, or collide with the footer band. Pin `.foot` with `margin-top: auto` inside a flex column, never with `position: absolute` over growing content. +- Do not let text become too small to read on mobile. +- Do not write inline `font-size` + `font-weight` on display titles in Swiss. Use the typed classes (`.h-hero` / `.h-statement` / `.h-xl` / `.num-mega`). A 80-120px headline at weight 700-900 is not Swiss; "the larger, the lighter" is a hard rule. +- Do not deliver Editorial posters with a flat paper background, mono labels on every row, and no atmosphere layer. Run the Editorial Identity Test in `references/style-system.md` — a serif title alone does not make a poster Editorial. +- Do not fake data, release details, or percentages. +- Do not crop faces, key UI text, or hardware/product details unless the user explicitly accepts it. +- Do not reuse a 21:9 cover by blindly cropping it into 1:1. Compose each ratio separately. +- **3:4 卡必须吃满画布**。Content (text + image + data) 必须覆盖 ≥75% 画布高度。任何 >15% 画布高度的纯空白带都需要"留白理由":(a) hero image 自带呼吸、(b) 单句宣言式 hero statement、(c) 段落顶/底 leading & trailing whitespace(前后总和 ≤15%)。**禁止用 `
` 上下夹击把内容塞到中段**——杂志页留白逻辑不适用于社交卡(杂志靠对开页吸收留白,社交卡逐张独立刷,欠填看着像 PPT 漏排)。Recipe-by-recipe 最小密度见 `references/layout-recipes.md` 每条 recipe 的「Minimum density」段。Render 后必须跑 `qa-checklist.md` 的 4 横带密度检查。 diff --git a/agents/openai.yaml b/agents/openai.yaml new file mode 100644 index 0000000..1030a6d --- /dev/null +++ b/agents/openai.yaml @@ -0,0 +1,8 @@ +interface: + display_name: "Guizang Social Card Skill" + short_description: "Create social card sets and WeChat covers." + brand_color: "#002FA7" + default_prompt: "Use $guizang-social-card-skill to turn this article into a 3:4 social card image set and a paired WeChat 21:9 + 1:1 cover." + +policy: + allow_implicit_invocation: true diff --git a/assets/magazine-bg-webgl.js b/assets/magazine-bg-webgl.js new file mode 100644 index 0000000..6ab58f6 --- /dev/null +++ b/assets/magazine-bg-webgl.js @@ -0,0 +1,160 @@ +/* Reusable Guizang-style electronic magazine background. + Copy or inline into generated HTML. It renders a deterministic WebGL ink-flow + canvas and falls back to 2D if WebGL is unavailable. */ +(function () { + function rgb(input, fallback) { + return Array.isArray(input) && input.length === 3 ? input.map((v) => v / 255) : fallback; + } + + function fallback2d(canvas, opts) { + const ctx = canvas.getContext("2d"); + if (!ctx) return; + const dpr = Math.min(2, window.devicePixelRatio || 1); + const rect = canvas.getBoundingClientRect(); + canvas.width = Math.max(1, Math.floor(rect.width * dpr)); + canvas.height = Math.max(1, Math.floor(rect.height * dpr)); + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + const w = rect.width; + const h = rect.height; + const ink = opts.inkCss || "rgba(26,46,31,.22)"; + const accent = opts.accentCss || "rgba(46,107,79,.18)"; + ctx.clearRect(0, 0, w, h); + for (let i = 0; i < 5; i++) { + const x = w * (0.14 + ((i * 0.19 + 0.17) % 0.72)); + const y = h * (0.10 + ((i * 0.29 + 0.23) % 0.78)); + const r = Math.max(w, h) * (0.22 + i * 0.045); + const g = ctx.createRadialGradient(x, y, 0, x, y, r); + g.addColorStop(0, i % 2 ? accent : ink); + g.addColorStop(1, "rgba(0,0,0,0)"); + ctx.fillStyle = g; + ctx.fillRect(0, 0, w, h); + } + ctx.globalAlpha = 0.09; + ctx.strokeStyle = opts.inkCss || "rgba(26,46,31,.22)"; + for (let i = 0; i < 18; i++) { + ctx.beginPath(); + ctx.ellipse(w * (0.5 + Math.sin(i) * 0.34), h * (0.5 + Math.cos(i * 1.7) * 0.28), 120 + i * 18, 42 + i * 9, i * 0.37, 0, Math.PI * 2); + ctx.stroke(); + } + ctx.globalAlpha = 1; + } + + function mount(canvas, options) { + const opts = options || {}; + const gl = canvas.getContext("webgl", { alpha: true, antialias: true, preserveDrawingBuffer: true }); + if (!gl) { + fallback2d(canvas, opts); + return { mode: "2d" }; + } + + const vert = ` + attribute vec2 aPos; + void main(){ gl_Position = vec4(aPos, 0.0, 1.0); } + `; + const frag = ` + precision mediump float; + uniform vec2 uRes; + uniform float uTime; + uniform vec3 uInk; + uniform vec3 uPaper; + uniform vec3 uAccent; + uniform float uStrength; + + float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); } + float noise(vec2 p){ + vec2 i = floor(p); + vec2 f = fract(p); + vec2 u = f * f * (3.0 - 2.0 * f); + return mix( + mix(hash(i + vec2(0.0,0.0)), hash(i + vec2(1.0,0.0)), u.x), + mix(hash(i + vec2(0.0,1.0)), hash(i + vec2(1.0,1.0)), u.x), + u.y + ); + } + float fbm(vec2 p){ + float v = 0.0; + float a = 0.5; + mat2 m = mat2(1.6, 1.2, -1.2, 1.6); + for(int i=0; i<5; i++){ + v += a * noise(p); + p = m * p * 1.18; + a *= 0.52; + } + return v; + } + void main(){ + vec2 uv = gl_FragCoord.xy / uRes.xy; + vec2 p = (gl_FragCoord.xy - 0.5 * uRes.xy) / min(uRes.x, uRes.y); + float t = uTime * 0.045; + float n = fbm(p * 2.2 + vec2(t, -t * 0.7)); + float n2 = fbm(p * 5.4 - vec2(t * 1.7, t)); + float contour = smoothstep(0.015, 0.0, abs(fract((n + n2 * 0.35) * 7.0) - 0.5)); + float wash = smoothstep(0.22, 0.88, n); + float vignette = 1.0 - smoothstep(0.28, 0.92, length(p)); + vec3 col = mix(uPaper, uInk, wash * 0.42 + contour * 0.18); + col = mix(col, uAccent, (0.18 + 0.25 * n2) * contour); + float alpha = uStrength * (0.18 + wash * 0.42 + contour * 0.30) * (0.55 + 0.45 * vignette); + gl_FragColor = vec4(col, alpha); + } + `; + + function shader(type, source) { + const sh = gl.createShader(type); + gl.shaderSource(sh, source); + gl.compileShader(sh); + if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(sh)); + return sh; + } + + const program = gl.createProgram(); + gl.attachShader(program, shader(gl.VERTEX_SHADER, vert)); + gl.attachShader(program, shader(gl.FRAGMENT_SHADER, frag)); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program)); + gl.useProgram(program); + + const buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, -1,1, 1,-1, 1,1]), gl.STATIC_DRAW); + const loc = gl.getAttribLocation(program, "aPos"); + gl.enableVertexAttribArray(loc); + gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0); + + const uRes = gl.getUniformLocation(program, "uRes"); + const uTime = gl.getUniformLocation(program, "uTime"); + const uInk = gl.getUniformLocation(program, "uInk"); + const uPaper = gl.getUniformLocation(program, "uPaper"); + const uAccent = gl.getUniformLocation(program, "uAccent"); + const uStrength = gl.getUniformLocation(program, "uStrength"); + const ink = rgb(opts.ink, [0.10, 0.18, 0.12]); + const paper = rgb(opts.paper, [0.96, 0.94, 0.90]); + const accent = rgb(opts.accent, [0.18, 0.42, 0.31]); + + function resize() { + const dpr = Math.min(2, window.devicePixelRatio || 1); + const rect = canvas.getBoundingClientRect(); + canvas.width = Math.max(1, Math.floor(rect.width * dpr)); + canvas.height = Math.max(1, Math.floor(rect.height * dpr)); + gl.viewport(0, 0, canvas.width, canvas.height); + } + + function draw(time) { + resize(); + gl.clearColor(0, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.uniform2f(uRes, canvas.width, canvas.height); + gl.uniform1f(uTime, opts.frozenTime == null ? time * 0.001 : opts.frozenTime); + gl.uniform3f(uInk, ink[0], ink[1], ink[2]); + gl.uniform3f(uPaper, paper[0], paper[1], paper[2]); + gl.uniform3f(uAccent, accent[0], accent[1], accent[2]); + gl.uniform1f(uStrength, opts.strength == null ? 0.34 : opts.strength); + gl.drawArrays(gl.TRIANGLES, 0, 6); + } + + draw(0); + if (opts.frozenTime == null) requestAnimationFrame(draw); + return { mode: "webgl", redraw: () => draw(0) }; + } + + window.MagazineBg = { mount }; +})(); diff --git a/assets/template-editorial-card.html b/assets/template-editorial-card.html new file mode 100644 index 0000000..38c820a --- /dev/null +++ b/assets/template-editorial-card.html @@ -0,0 +1,829 @@ + + + + + [必填] 替换为社交卡组标题 · Social Card Set + + + + + + + + +
+ + + + + +
+ +
+
+
+ Vol. 012026.05 +
+
+

封面 · Cover

+

[标题占位]
替换为真实文案

+

A working subtitle

+
+
+ cover image placeholder +
+

用一段克制的引语解释为什么这一组卡片值得读完。

+
+
+ Issue · Magazine + — + by [作者] +
+
+ +
+ + + + + + diff --git a/assets/template-swiss-card.html b/assets/template-swiss-card.html new file mode 100644 index 0000000..7a82774 --- /dev/null +++ b/assets/template-swiss-card.html @@ -0,0 +1,935 @@ + + + + + [必填] 替换为社交卡组标题 · Social Card Set + + + + + + + + + +
+ + + + + +
+
+
+ Vol. 01 · Swiss + 2026.05 +
+
+

封面 · Cover

+

替换
标题

+
+
+
+

用一句克制的解释,告诉读者这一组卡片在讲什么。

+
+

Issue 01

+

/ [作者]

+
+
+
+ +
+ + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..93495e7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,57 @@ +{ + "name": "guizang-social-card-skill", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "guizang-social-card-skill", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "playwright": "^1.60.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..224c8fe --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "guizang-social-card-skill", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "validate": "node validate-social-deck.mjs" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "playwright": "^1.60.0" + } +} diff --git a/references/background-systems.md b/references/background-systems.md new file mode 100644 index 0000000..9621280 --- /dev/null +++ b/references/background-systems.md @@ -0,0 +1,108 @@ +# Background Systems + +The original Guizang PPT electronic-magazine mode uses WebGL fluid, contour, ink, and chromatic atmosphere. Static Rednote/WeChat images should preserve that feeling when the style mode is Editorial Magazine x E-ink. + +Do not reduce this mode to a flat beige page with a faint grid. Do not add visible grid, dot-matrix, or drafting-paper patterns to the background. + +## Layer Model + +Use 3-5 layers: + +1. Paper base from `theme-presets.md`. +2. Procedural paper grain. +3. Ink wash, contour field, or WebGL fluid canvas. +4. Optional hairline structure as content dividers only, not as a page-wide background grid. +5. Content layer. + +Recommended CSS order: + +```html +
+ +
+
+
...
+
+``` + +```css +.poster { position: relative; overflow: hidden; background: var(--paper); } +.mag-bg { position:absolute; inset:0; width:100%; height:100%; z-index:0; opacity:.34; } +.poster.hero .mag-bg { opacity:.62; } +.grain { + position:absolute; inset:0; z-index:1; pointer-events:none; + opacity:.12; mix-blend-mode:multiply; + background: + linear-gradient(112deg, rgba(0,0,0,.035), transparent 26%, rgba(255,255,255,.16) 52%, transparent 82%), + radial-gradient(circle at 18% 22%, rgba(var(--ink-rgb),.05), transparent 38%), + radial-gradient(circle at 78% 84%, rgba(var(--accent-rgb),.045), transparent 42%); +} +.ink-vignette { + position:absolute; inset:0; z-index:2; pointer-events:none; + background: + radial-gradient(circle at 18% 12%, rgba(var(--ink-rgb),.10), transparent 34%), + radial-gradient(circle at 82% 80%, rgba(var(--accent-rgb),.10), transparent 38%); +} +.content { position:relative; z-index:3; } +``` + +## When To Show WebGL + +Use stronger visible atmosphere for: + +- Cover. +- Chapter/divider. +- Pull quote. +- Sparse thesis page. +- Closing page. + +Use subtle atmosphere for: + +- Screenshot pages. +- Dense ledgers. +- Checklists. +- Product evidence pages. + +If screenshots are the main evidence, the background should support them rather than compete with them. + +## WebGL Helper + +The skill includes `assets/magazine-bg-webgl.js`, a small helper for a deterministic ink-flow background: + +```html + + + +``` + +For screenshot exports, prefer `frozenTime` so repeated renders look stable. + +## 2D Fallback + +If WebGL fails, create a 2D canvas with: + +- Large radial ink wash. +- Several translucent contour arcs. +- Soft paper noise or uneven ink wash. +- Theme color at very low opacity. + +The output should still feel like an editorial background, not a plain fill. + +## Do Not + +- Do not use bright gradients. +- Do not use page-wide grid, dot-matrix, graph-paper, or drafting-paper backgrounds. +- Do not use decorative blobs or circles with no relationship to the layout. +- Do not place strong background marks behind body text. +- Do not let WebGL obscure screenshots or small captions. +- Do not animate the final image sequence unless the task is video. diff --git a/references/category-cookbook.md b/references/category-cookbook.md new file mode 100644 index 0000000..fbbd6ee --- /dev/null +++ b/references/category-cookbook.md @@ -0,0 +1,232 @@ +# Category Cookbook + +Per-category cheat sheet for the 11 most common Rednote (小红书) content types. Each entry maps a category to: + +- **What we can do** — concrete poster types this skill produces well. +- **Style mode** — which side of the system (Editorial / Swiss) fits. +- **Recipes** — which layout IDs (M01-M15 / S01-S12) are first reach. +- **Text scheme** — how text relates to image (text-on-image / text-beside-image / text-only). +- **Image source** — where the photos come from. +- **Common pitfalls** — the way this category breaks. + +This is a routing document. When the user names a category, find the row here and start from the listed recipes instead of building from a blank file. + +For any category, if a poster has a full-bleed image with text on top, also follow `image-overlay.md` (mask + subject avoidance). + +--- + +## 旅行 · Travel + +**Strongest fit.** + +- **Recipes**: M01 (text-led cover, no photo dominance) **or M16 (image-led cover, when the user has 1+ great photos)**, M02 (field-note photo), M11 (marginalia essay), M07 (closing note), S11 (itinerary ledger). For image-heavy submissions use the M16 → S11/M05 → M02 sequence in `references/content-planning.md`. +- **Style mode**: Editorial × kraft-paper / dune (warm-tone destinations) or × forest-ink (mountain/wilderness). Swiss × IKB works for "trip data report" style. +- **Text scheme**: Text beside image is the default. Cover uses text-on-image with a vertical falloff mask. Body pages use photo + caption pairs (field-note style). +- **Image source**: User photos > Pexels (for China destinations — supports Chinese keyword search) > Unsplash (`/s/photos/`, best for overseas / English keywords) > Flickr CC (`license=2,3,4,5,6,9`, when you need documentary "real trip" feel rather than postcard polish). Always log to `assets/SOURCES.md`. +- **Content shape**: 5-7 pages. Cover (destination + dates) → atmosphere photo + lead → itinerary ledger → 2-3 field notes (one location each) → closing quote / next-stop teaser. +- **Pitfalls**: Generic "best places in X city" listicle voice. Cure: keep one specific date / weather / mileage detail per page to anchor it as observed, not researched. + +See: `local-tests/demo-image-01-yading/` for a reference build. + +--- + +## 职场 · Workplace + +**Strongest fit.** This is what Swiss-International was made for. + +- **Recipes**: S01 (cover), S02 (comparison), S05 (warning rows), S06 (pipeline), S07 (takeaway ledger), S09 (KPI tower), S11 (stacked ledger), S12 (matrix + hero stat). +- **Style mode**: Swiss × IKB Blue or × Safety Orange. Avoid lemon-yellow / lemon-green for serious workplace content; they read as marketing. +- **Text scheme**: Text-only or text-with-diagram. Almost never text-on-photo (workplace photos read as stock). +- **Image source**: Avoid stock business photos. If you need an image, prefer a diagram, a screenshot of a real artifact (Notion, Linear, Figma), or omit. Generated images rarely add value here. +- **Content shape**: 5-9 pages. Cover question / claim → context KPI → 3-5 numbered insights as ledger → one comparison or pipeline diagram → takeaway. +- **Pitfalls**: + 1. **Listicle voice** ("8 个让你..."). Cure: rewrite as a numbered argument, not numbered tips. Each row makes a falsifiable claim, not a platitude. + 2. **Cheap "advice" energy** — soft language ("一定要", "千万别"). Cure: replace with observed action verbs and a number. + 3. **Stock-photo seasoning** — handshake, laptop-with-coffee. Cure: omit, or use a small Lucide icon glyph instead. + +--- + +## 游戏 · Game + +**Strong fit** for journals, recap, build/strat lists. **Has image-rights risk** that the user must accept. + +- **Recipes**: M01 (cover with full-bleed art), M08 (boss tier ledger), S07 (takeaway ledger), S11 (chapter timeline), M15 (build before/after). +- **Style mode**: Editorial dark (ink-classic with paper inverted to near-black) for atmospheric games (黑神话 / Elden Ring). Swiss for esports / competitive data ("胜率 / KDA / 出装"). +- **Text scheme**: Text-on-image is standard for game covers because game art is the primary draw. Mask is mandatory (see `image-overlay.md` Rule 1A vertical falloff). +- **Image source**: Wallhaven JSON API (see SKILL.md Step 6) for keyword pulls, official screenshots for specific moments. Always disclose copyright risk and log to `SOURCES.md`. If user opts out of attribution, do not crop the credit out of the image itself. +- **Content shape**: 4-6 pages. Cover (game name + playtime) → first impression page → chapter-by-chapter ledger → memorable boss / scene page → verdict. +- **Pitfalls**: + 1. **Score-card seriousness** (8.5/10 in a giant block). We're not IGN — keep the verdict as one short clause, not a number. + 2. **Generic fan-art covers** with the game logo retraced — looks counterfeit. Prefer key art the publisher already released. + +See: `local-tests/demo-image-02-wukong/` for a reference build. + +--- + +## 影视 · Film & TV + +**Strong fit** for reviews, scene analysis, quote cards. **Image-rights risk**, same as Game. + +- **Recipes**: M04 (pull quote — for memorable lines), M10 (evidence feature — for scene analysis), M11 (marginalia essay), S02 (two signals — for comparison reviews), S12 (matrix — for film weeklies). +- **Style mode**: Editorial × ink-classic or × indigo-porcelain. Letterboxd visual vocabulary fits Editorial naturally — serif title, italic original-language subtitle, monospace metadata. +- **Text scheme**: Text-beside-image for review cards (poster on left, take on right). Text-on-image only for atmospheric "what this film made me feel" quote pages. +- **Image source**: Official posters / stills. Do not generate fake stills — they look immediately wrong. +- **Content shape**: For single film: cover (title + year + 1-line take) → 1-2 scene captures → director-quote / theme pullquote → verdict ledger. For weekly: matrix of 6-9 films + one hero recommendation. +- **Pitfalls**: + 1. **Fake film-festival typography** — adding fake awards badges. Don't. + 2. **Spoiler in title without warning.** If a cover gives away the ending, mark `剧透` in the kicker. + +--- + +## 美食 · Food + +**Split fit.** Recipes work. Food-photo showcase does not. + +### What works: recipes & food essays + +- **Recipes**: M16 (image-led cover when finished-dish photo is hero-worthy) → S11 (ingredient/price ledger) → M14 (cooking steps pipeline) → M02 (extra dish detail). Also M05 (checklist for shopping list). +- **Style mode**: Editorial × kraft-paper feels like a cookbook. Swiss × Lemon Yellow / Safety Orange works for "cost-per-serving" data posts. +- **Text scheme**: Text-with-image. The finished-dish photo is one frame; everything else is text/data. +- **Image source**: User photos of finished dish are best. For 中式菜品 / 国内餐厅 scenes, try Pexels first (Chinese keyword search hits 国内场景 better than Unsplash). Unsplash food photos read as Western stock; use sparingly. +- **Content shape**: 5-7 pages. Cover (dish name + one verdict line) → ingredients ledger → numbered steps pipeline → finished-dish photo page → tips / variation marginalia. + +### What does not work: food-photo showcase / restaurant reviews requiring drool-shot art direction + +- We don't shoot food, and stock food photos kill the "I cooked this" trust signal. +- For restaurant posts, require user-shot photos at the table. If they don't have them, push back — a stock-photo restaurant post is filler. +- For "look at this cake" pure-display posts, refer the user to a photography-focused workflow, not this skill. + +**Pitfalls**: +1. **Excited recipe voice** ("超绝!!!"). Editorial doesn't shout — let the dish do the talking. +2. **Calorie-shaming asides** in casual recipes. Drop them. + +--- + +## 彩妆 · Makeup + +**Split fit.** Tutorials and product reviews work. Selfie-driven "look" posts need user images we can't generate. + +### What works + +- **Recipes**: M14 (tutorial step pipeline), S12 (product matrix), S11 (空瓶 / 复购 ledger), S02 (色号对比). +- **Style mode**: Swiss × Lemon Yellow / Lemon Green / Safety Orange for bright product reviews. Editorial × indigo-porcelain for editorial-feel beauty essays. +- **Text scheme**: Text-with-image. Each step gets a small hand/face crop + a one-sentence instruction. +- **Image source**: User photos. Stock makeup photos have legal grey areas and look generic. +- **Content shape**: Tutorial — cover (look name + occasion) → tools/products ledger → 5-7 numbered step pipeline → finish photo → tips marginalia. + +### What does not work + +- "Get-the-look" posts that rely on a recognizable model face. We can't generate those reliably; user must supply. +- Trending Y2K / 哥特 / dolly aesthetic decoration — outside the Swiss/Editorial system. + +**Pitfalls**: +1. **Color-swatch inaccuracy** if you eyeball CSS hex from a photo. Always ask the user for the official shade name + hex from the brand. +2. **Skin-tone homogenization** when scaling photos — preserve user-supplied photos at native ratio. + +--- + +## 健身 · Fitness + +**Half fit.** Training plans and nutrition data work. Progress photos need user. + +- **Recipes**: M14 (workout pipeline), S11 (set/rep ledger), S09 (KPI tower for weekly volume), M15 (before/after — needs user photos). +- **Style mode**: Swiss × Safety Orange / Lemon Green for energetic training posts. Editorial × forest-ink for "mindful movement / 山系跑步" essays. +- **Text scheme**: Mostly text-with-data. Progress posts text-beside-image. +- **Image source**: User progress photos (mandatory for before/after). For ambient illustrations, generated gym/outdoor shots are fine. +- **Content shape**: Training plan — cover (cycle name + duration) → weekly schedule ledger → per-day pipeline of exercises → recovery / nutrition note → closing data. +- **Pitfalls**: + 1. **Fake aspirational numbers** (50 kg 卧推 8 周从 0 起步). Don't. + 2. **Body-shaming subtext** in before/after captions. Use neutral language. + +--- + +## 家居 · Home & Living + +**Half fit.** Item recommendations, before/after, and floorplan analysis work. Pure interior-design photo posts need user photos. + +- **Recipes**: M16 (image-led cover when user has a strong room photo) → S11 (item ledger with price) → M02 (detail crops). Also M15 (before/after — needs paired user photos), M11 (marginalia for floorplan annotation), M03 (essay-style decor manifesto). +- **Style mode**: Editorial × kraft-paper / dune for warm rustic homes. Editorial × indigo-porcelain for cool modern. Swiss × IKB for "rental hack data" posts. +- **Text scheme**: Item recommendations are text-beside-image (small product crop + name + price + reason). Before/after is two photos with mid-page divider. +- **Image source**: User interior photos. Web-sourced interior images have heavy copyright exposure (Pinterest creators are vigilant) — avoid unless user explicitly authorizes. +- **Content shape**: Item rec — cover (room + budget) → 4-6 item rows ledger → 1 hero detail crop → closing tip. +- **Pitfalls**: + 1. **Pinterest-aesthetic stock** that the user can't actually reproduce. Stick to what they live in. + 2. **Price drift** — products move quickly. Don't quote a price more than 90 days old without re-checking. + +--- + +## 穿搭 · Outfit / Fashion + +**Weak fit.** Mood-board essays and product reviews work; OOTD full-body shots are outside our scope. + +### What works + +- **Recipes**: M11 (marginalia essay — "this week I wore"), S12 (item matrix), M04 (style manifesto pull-quote), S11 (capsule wardrobe ledger). +- **Style mode**: Editorial × ink-classic / indigo-porcelain for editorial-feel essays. Swiss × Lemon Yellow / Safety Orange for capsule-wardrobe data posts. +- **Text scheme**: Text-beside-image — small item crops as evidence, one full-body hero only if user provides. +- **Image source**: User OOTD photos (we don't generate body shots). For mood-board context, web-sourced flat-lay or product photos are okay with disclosure. +- **Content shape**: Capsule guide — cover (season + count) → wardrobe ledger → 1-2 outfit demonstrations → care/styling marginalia. + +### What does not work + +- Daily OOTD posts. The skill cannot generate or solicit body photography. +- High-styling editorial fashion shoots — requires real photography production. + +**Pitfalls**: +1. **Brand-and-price-tag overload** turning every poster into an ad. Limit to 6 named items max. +2. **Generic "minimalist 25 件夏季基础款" listicle**. Cure: anchor to a specific climate / lifestyle constraint. + +--- + +## 情感 · Emotion / Personal Essay + +**Weak fit.** Essay-with-pullquote works. Dreamcore / 氛围感 aesthetic does not fit our system. + +### What works + +- **Recipes**: M04 (pull quote / thesis), M09 (atmospheric thesis), M11 (marginalia essay), M13 (hero question — for rhetorical-question covers), M07 (closing note). +- **Style mode**: Editorial × ink-classic or × dune. Magazine pace with restraint. +- **Text scheme**: Mostly text-only with one quiet supporting image. The text is the content. +- **Image source**: Quiet documentary-style photo (a window, a chair, a hand) — user-supplied is best. If sourcing: **Flickr CC** (`license=2,3,4,5,6,9`) is the strongest match — real interiors, real hands, real moments without stock polish. Pexels for China-context scenes (Chinese keyword search). Unsplash works but skews styled; avoid the "moodcore" gradient-light stock entirely. +- **Content shape**: 3-5 pages. Cover question → essay split (M03) or atmospheric thesis → pull quote → closing note. Resist over-stretching to 9 pages. + +### What does not work + +- "梦核 / 氛围感" with soft fluorescent gradients, glowing ovals, hazy lights. Our anti-pattern list explicitly bans those decorations. +- Y2K / 早期网络回忆 / 千禧辣妹 aesthetic — outside Swiss and Editorial both. +- Heavy emoji / kaomoji captions. We use no emoji. + +**Pitfalls**: +1. **Inspirational generic copy** ("愿你...愿我..."). Editorial demands specifics — name the day, the weather, the person. +2. **Pretty empty space** — page after page of one pullquote on paper. Risk: reads as filler. Mix with one M02 field photo or M11 marginalia to ground it. + +--- + +## 推荐 · Recommended / Mixed + +**Default channel.** This category is the catch-all on the platform; the actual content underneath is always one of the above 10. + +When the user says 推荐 without specifying, ask which underlying type before designing. If they can't say, default to: + +- Swiss × IKB for "I tested 5 products" → S12 matrix + S11 ledger. +- Editorial × kraft-paper for "this week's reading / watching" → M02 field notes. + +--- + +## Capability Circle Summary + +Categories where this skill produces strong work end-to-end (text + structure + image story all from us): + +- 旅行 · 职场 · 推荐 (with subtype) + +Categories where the skill is strong on text/structure but needs user photos for image story: + +- 游戏 · 影视 · 美食(食谱) · 彩妆(教程) · 健身 · 家居 · 穿搭(精选) + +Categories that are outside scope (skill cannot reliably produce): + +- 美食(菜品大片摆盘) · 穿搭(日常 OOTD 全身) · 情感(梦核 / 氛围感装饰风) +- Any post requiring high-saturation Y2K / 千禧辣妹 / dolly / kawaii decoration vocabulary +- Pure photography showcase posts where the image is the entire product + +Be explicit with the user when their request lands in the "outside scope" bucket. Do not promise a result the system was not designed to make. diff --git a/references/components.md b/references/components.md new file mode 100644 index 0000000..02b253b --- /dev/null +++ b/references/components.md @@ -0,0 +1,285 @@ +# Components + +Shared component spec for both seed templates. Read this when you need to look up a class, a font size, or a hard rule that recurs across recipes. Per-recipe details live in `layout-recipes.md`; per-theme color tokens live in `theme-presets.md`. + +## Font Stacks + +Editorial Magazine x E-ink (`template-editorial-card.html`): + +- `--serif-zh`: Noto Serif SC, Songti SC, STSong — display titles. +- `--serif-en`: Playfair Display, italic for subtitles and pull quotes. +- `--sans-zh`: Noto Sans SC, PingFang SC — body and lead. +- `--sans-en`: Inter — Latin body in mixed content. +- `--mono`: IBM Plex Mono — labels, page metadata, issue strip, captions. + +Swiss International (`template-swiss-card.html`): + +- `--sans`: Inter, Helvetica Neue, Helvetica. +- `--sans-zh`: Noto Sans SC, PingFang SC — Chinese body. +- `--mono`: IBM Plex Mono — labels, captions, t-meta. + +Never load a serif font in the Swiss template. Never let the Editorial template lose its serif display family. + +## Type Scale + Weight Mapping + +Both templates follow the rule **"the larger, the lighter."** Small text always uses heavier weight than large text. On 1080x1440 boards, the minimum readable size is 22-28px after factoring in mobile downsample. + +Editorial scale (default for 3:4): + +| Role | Class | Size | Weight | Tracking | Family | +| ----------- | ----------- | ----- | ------ | -------- | --------- | +| Display | `.h-display`| 124px | 500 | +.04em | serif-zh | +| Section ttl | `.h-xl` | 88px | 500 | +.03em | serif-zh | +| Mid title | `.h-md` | 56px | 500 | +.02em | serif-zh | +| Subtitle | `.h-sub` | 36px | 400 it | normal | serif-en | +| Pull quote | `.pullquote`| 64px | 500 it | normal | serif-zh | +| Lead | `.lead` | 28px | 400 | normal | **serif-zh** | +| Body | `.body` | 24px | 400 | normal | **serif-zh** | +| Kicker | `.kicker` | 21px | 500 | +.22em | mono | +| Meta | `.meta` | 18px | 500 | +.20em | mono | +| Label | `.label` | 18px | 500 | +.20em | mono | + +> **Restored 2026-05-27.** The previous defaults (900 / 700 / 700 / sans body / `−.01em`) made Editorial cards read as heavy infographic banners. They are NOT the Guizang showcase aesthetic — see `local-tests/demo-showcase/editorial.html` for the source-of-truth. The rule is restrained typography: light weights, **serif body italic-tolerant**, **wide tracking** on display + mono. Heaviness in Editorial = visual demotion to "generic landing-page", same trap as Swiss. + +Swiss scale (default for 3:4): + +| Role | Class | Size | Weight | Family | +| ----------- | ------------- | ----- | ------ | --------- | +| Hero | `.h-hero` | 240px | 200 | sans | +| Statement | `.h-statement`| 180px | 200 | sans | +| Section ttl | `.h-xl` | 120px | 300 | sans | +| Mid title | `.h-md` | 56px | 400 | sans | +| Mega number | `.num-mega` | 200px | 200 | sans | +| XL number | `.num-xl` | 144px | 200 | sans | +| Lead | `.lead` | 30px | 400 | sans-zh | +| Body | `.body` | 26px | 400 | sans-zh | +| Category | `.t-cat` | 22px | 600 | sans | +| Meta | `.t-meta` | 20px | 500 | mono | +| Caption | `.swiss-img-caption` | 18px | 500 | mono | + +Both templates auto-shrink display sizes inside `.poster.square` and `.poster.wide`. You usually do not override them. Override only when a single title cannot be shortened further. + +### Chinese Title Length Bands + +Chinese characters are visually denser than Latin. Pick a band before sizing: + +| Title shape | Editorial display | Swiss h-hero | +| ------------------------------------------ | ----------------- | ------------ | +| 1 line, <= 6 Chinese chars | 132px (default) | 240px (default) | +| 1 line, 7-10 chars | 108px | 200px | +| 2 lines, each <= 8 chars | 96px | 180px | +| 2 lines, any line 9-12 chars | 84px | 152px | +| 3 lines (rare) | 72px | 132px | + +If the title still does not fit, **shorten the copy first**. Never solve overflow by shrinking body text below the minimum readable size. + +### Swiss `.h-xl` — Hard Caps per Board (validated) + +The seed template ships these caps. Anything more violates the vertical budget — verified empirically in `local-tests/smoke-ai-tools/`: + +| Board | Default `.h-xl` | Max lines | Max chars / line | What happens past the cap | +| -------------------- | --------------- | --------- | ---------------- | ------------------------------------------------------------------ | +| `.poster.xhs` (1080×1440) | 96px | 2 | 8 | 3-line title pushes ledger/matrix past 1440 — push content off-canvas | +| `.poster.square` (1080×1080) | 88px | 2 | 7 | Bottom card or metadata strip clips | +| `.poster.wide` (2100×900) | 104px | 1 | 14 | Wraps to 2 lines and crowds the right column | + +**If your title needs 3 Chinese lines on `.poster.xhs`:** switch the recipe from S03/S10/S12 (data-heavy) to S01/S05 (cover-style) where the title can dominate. Don't shrink `.h-xl` below 80px — small + heavy reads as Web1.0. + +### Editorial `.h-xl` — Hard Caps per Board (validated) + +The Editorial seed default for `.h-xl` is 92px serif weight 700. Verified in `local-tests/demo-smoke-editorial-travel/` on M11/M14/M07 with 3-paragraph body + ledger + 5-step pipeline: + +| Board | Default `.h-xl` | Max lines | Max chars / line | What happens past the cap | +| -------------------- | --------------- | --------- | ---------------- | ------------------------------------------------------------------------- | +| `.poster.xhs` (1080×1440) | 92px | 2 | 9 | 3 lines push the marginalia / ledger / pipeline rows past the issue strip | +| `.poster.square` (1080×1080) | 78px | 2 | 8 | Sub-paragraph or pull-quote clips | +| `.poster.wide` (2100×900) | 96px | 1 | 16 | Wraps and crowds the marginalia column | + +The smoke deck's p2 title `第三次进山,装备比上一次轻 3.4kg` is the 2-line × 9-char limit and worked. The `
` was load-bearing — let CJK auto-wrap freely on `.h-xl` and you get awkward breaks. Hard-break manually at semantic seams. + +**Per-recipe nuance:** +- M11 (Marginalia Essay): 2-line title leaves room for **3 paragraphs of 3-4 sentences each** in a 700px main column, and 5-7 marginalia rows in a 220px aside. +- M14 (Vertical Pipeline): 2-line title + **5 steps** (each step-title 42px serif + step-desc 26px sans, 28px gap) hits the 1440 floor exactly — adding a 6th step requires shrinking title to 1 line. +- M07 (Field Ledger): 2-line title + **5 ledger rows** with both `.ledger-title` and `.ledger-note` ≈ 100-120px row height — exactly fills. 6 rows requires 1-line title. + +### Matrix + hero-stat-bottom on `.poster.xhs` + +`.matrix-fill` with `grid-auto-rows: min-content` is the safe default on `xhs`. Hard limits: + +| Cells | Rows | Pair with hero-stat-bottom? | Notes | +| ----- | ---- | --------------------------- | ------------------------------------------------- | +| 4 | 2 | Yes — comfortable | Cells get 200+ px each | +| 6 | 3 | Yes — recommended | The sweet spot | +| 8 | 4 | Only with shortened title + `num-mega` 110-128px | Compressed; do not also use 3-line title | +| 10+ | 5+ | **No** | Use S10 H-Bar Chart instead — that pattern scales | + +## Minimum Readable Sizes (mobile-safe) + +A 1080x1440 PNG is usually viewed on a phone at 360-420 logical pixels wide. Anything below the listed size becomes unreadable: + +| Role | Minimum | Notes | +| ------------------- | ------- | ---------------------------------------- | +| Body / paragraph | 28px | Editorial, Swiss alike | +| Lead | 30px | The "1.5x body" guard | +| Caption / kicker | 20px | Do not drop below 18px | +| Label / meta strip | 20px | Mono only | +| Cell title in grids | 24px | Matrix / brief cards | +| Number annotation | 22px | Stat-card .lbl, ledger .sub | + +If you cannot fit the copy, cut the copy. Do not shrink type. + +## Card Fills — Swiss Only, Mutually Exclusive + +Four card classes exist; **never combine them on the same node:** + +- `.card-ink` — solid black ink, paper text. Use for one statement card. +- `.card-accent` — accent fill, accent-on text. Use sparingly; max one accent card per poster. +- `.card-fill` — grey-1 fill, ink text. The workhorse for matrix / brief / takeaway grids. +- `.card-outlined` — transparent + 1px grey-2 border, ink text. Use when you need a card without weight. + +Multi-card grids must use **the same** card class for every cell, except a single accent highlight is allowed when one item is meant to stand out. Mixing `card-fill` with `card-outlined` in the same grid looks like sloppy templating. + +The Editorial template intentionally does not provide card classes. Editorial layouts express hierarchy through type, rules, ledger rows, and column structure — not card backgrounds. If you find yourself wanting a card on an Editorial poster, you probably picked the wrong style mode. + +## Image Containers + +Both templates share the same `.frame-img` system. Always pick a standard ratio class. Never write `aspect-ratio: 2592/1798` style ad-hoc ratios. + +| Class | Ratio | Use | +| ----------- | ------ | -------------------------------------------------- | +| `.r-3x4` | 3 : 4 | Default for portrait covers and field-note photos. | +| `.r-1x1` | 1 : 1 | Square portraits, product objects, balanced grids. | +| `.r-4x3` | 4 : 3 | Classic editorial photo, full-bleed top zone. | +| `.r-3x2` | 3 : 2 | Magazine inline figure. | +| `.r-16x9` | 16 : 9 | Landscape photo, infographic. | +| `.r-16x10` | 16 : 10| Default for left-text + right-image splits. | +| `.r-21x9` | 21 : 9 | WeChat 21:9 hero image, S08 Image Hero top zone. | + +Default `object-fit: cover` keeps `object-position: center 50%` (both templates). Use `.fit-contain` for UI screenshots, dense text, code, and infographics. Do not crop faces or product key features. + +**Subject-aware cropping — always set `object-position` inline per photo.** The template default is a fallback, not a recommendation. Before each ``, look at the source and ask: where is the subject? Then set `style="object-position:center N%"` on the `` directly: + +| Subject location in source | Inline value | +| ------------------------------------- | ------------------------ | +| Subject near top (sky-heavy landscape, face at top) | `center 25-35%` | +| Subject centered (default) | `center 50%` (omit, it's the default) | +| Subject mid-body (hiker, hands, mid-frame) | `center 55-65%` | +| Subject low (foreground gear, ledger, foot of frame) | `center 70-80%` | + +The default `center 50%` will silently crop subjects out of frame on tall ratios (`r-3x4`, `r-21x9`). Every photo in a delivered package needs a deliberate `object-position` — even if the conclusion is "50% is fine here." Catch this in the render-check pass: if the subject is more than 1/3 outside the visible crop, fix it before delivering. + +**Caption class name (don't get this wrong):** +- Editorial template: `.img-cap` — 18px mono, applied to `
` under `.frame-img`. Writing `.cap` falls back to browser-default 16px and trips R4. +- Swiss template: `.swiss-img-caption` — same role, 18px mono. + +There is no shared `.cap` class. If you see `.cap` in a draft, fix it before render. + +## Screenshot Containers (.frame-shot) + +For UI screenshots / web captures / code shots, use `.frame-shot` (both seeds carry it). It defaults to `object-fit: contain` so the source pixels stay pristine — the opposite of `.frame-img`. See `references/screenshot-treatment.md` for the full parameter matrix. + +Quick summary of style-locked defaults (don't second-guess these): + +| Style | Corners | Shadow | Default bg | Default inset | +| --------- | ------------ | ------------ | ------------ | ------------- | +| Swiss | `corners-sq` | `shadow-none`| `bg-grey-1` | `inset-sub` | +| Editorial | `corners-sm` | `shadow-soft`| `bg-paper-2` | `inset-sub` | + +Two device wrappers (`.device-browser`, `.device-phone`) ship in both templates and provide CSS-only browser chrome / phone bezel — no SVG, no image deps. Wrap one `.frame-shot` per chrome. + +## Map Block (.map-block) + +For content with spatial relationships (routes, neighborhoods, store locations). Both seeds ship `.map-block` + `.map-pin` + `.map-legend`. Three modes — see `references/map-component.md` for the full playbook: + +- **Mode S** (default) — schematic SVG drawn in `viewBox="0 0 100 100"`, no token, hand-illustrated feel. +- **Mode T** — Mapbox Static Images API URL as `` source; needs `MAPBOX_ACCESS_TOKEN`. Apply `tone-paper` (Editorial) or `tone-paper` saturation-0 (Swiss). +- **Mode O** — OSM tile composite, fallback when no token. + +Hard limits: ≤6 pins per board, ≤1 accent pin, no SVG text labels (names go in `.map-pin .card`). + +## Spacing Tokens (Swiss only) + +Swiss template exposes a Carbon-style 2x scale. Stick to these tokens — arbitrary `px` margins drift fast across posters. + +| Token | Value | Typical use | +| ------- | ------ | ---------------------------------------------------- | +| `--sp-3`| 8px | Tight chip gap, inline meta spacing. | +| `--sp-4`| 12px | Card inner gap, dense list rows. | +| `--sp-5`| 16px | Body block bottom margin, lead-to-paragraph. | +| `--sp-6`| 24px | Card padding (compact), grid gap (tight). | +| `--sp-7`| 32px | Default grid gap. | +| `--sp-8`| 40px | Card padding (default), section gap (compact). | +| `--sp-9`| 48px | Section gap (default), pair-preview outer padding. | +| `--sp-10`| 64px | Major break between content blocks on portrait. | +| `--sp-11`| 80px | Poster outer padding (square / wide). | +| `--sp-12`| 96px | Poster outer padding (portrait). | +| `--sp-13`| 160px | Wide poster horizontal padding only. | + +Editorial template uses bare `px` for now because magazine layouts often want bespoke spacing for atmosphere. If you find a pattern that recurs, add an editorial token here. + +## Icons + +Default icon library is **Lucide** (loaded only by the Swiss template). Editorial layouts rarely need icons; when they do, use the same Lucide set, but limit to one or two per poster. + +Rules: + +- No emoji anywhere. Emojis ruin both styles. +- Use angular Lucide icons (`arrow-right`, `check`, `triangle-alert`, `dot`, `plus`, `equal`, `slash`). Avoid pillow-rounded icons (`smile`, `heart-filled`) — they fight Swiss geometry. +- Icon stroke weight: 1.5 (Lucide default). Do not thicken. +- Size: 56px for ledger row indicators, 32px inline with body, 24px in chips. Never below 20px. +- Color: `var(--accent)` for highlight, `var(--grey-3)` for neutral, `var(--ink)` for primary. Never gradient. + +To add an icon: + +```html + +``` + +The Swiss template runs `lucide.createIcons()` on load. If you inject icons after load, call it again manually. + +## Issue Label and Corner Metadata + +Social cards do not have PPT's chrome/foot dual-row metadata. They use a single, quiet element instead. + +Editorial issue elements: + +- `.issue-row` — top: "Vol. 01 · 2026.05" with a small accent dot between segments. +- `.issue-strip` — bottom band: 3-5 short labels separated by em-dashes, above a hairline. + +Swiss issue elements: + +- `.chrome-min` — top single row with category on left, date/issue on right, bottom hairline. +- `.t-meta` — inline mono uppercase metadata anywhere on the poster. + +One issue element per poster. Never both top issue strip and bottom issue strip on the same poster — it looks like decoration. + +## Layout Primitives + +Both templates share the same primitives (different default gaps). + +| Class | Editorial gap | Swiss gap | Use | +| ---------------- | --------------- | --------------- | ------------------------------------ | +| `.stack` | flex column | flex column | Default vertical flow. | +| `.row` | flex row | flex row | Horizontal flow. | +| `.gap-1`...`-5` | 12-64px | 8-16px | Editorial gaps (px) vs Swiss (token).| +| `.gap-6`...`-10` | n/a | 24-64px | Swiss only — Carbon scale. | +| `.col-2-7-5` | 7:5 split | n/a | Editorial only — quote + image. | +| `.col-2-8-4` | 8:4 split | n/a | Editorial only — text-heavy + image. | +| `.grid-12` | n/a | 12-col grid | Swiss only. | +| `.span-N` | n/a | grid column span| Swiss only. | +| `.grid-3` | 3 columns | 3 columns | Both. | + +## Hard Rules (Shared) + +These rules guard the visual identity. Violations are almost always wrong. + +1. Pick exactly one style mode per package. Never mix Editorial and Swiss. +2. Pick exactly one theme/accent per package. Never combine two palettes. +3. No emojis, ever. Use Lucide or restrained type. +4. No fake data, no fake percentages, no `Lorem` text in delivered output. +5. No `border-radius`, `box-shadow`, or `linear-gradient` in Swiss. +6. No flat one-color background in Editorial — paper grain + optional WebGL atmosphere is required on hero / quote / sparse pages. +7. Every `.poster` must have stable export dimensions. Never use `vw` / `vh` in poster content. +8. Every image must be wrapped in `.frame-img` with a standard ratio class. +9. Multi-card grids use one card class. One accent card maximum. +10. Honor the type scale and minimum sizes. Cut copy before shrinking type. diff --git a/references/content-planning.md b/references/content-planning.md new file mode 100644 index 0000000..d0cb641 --- /dev/null +++ b/references/content-planning.md @@ -0,0 +1,162 @@ +# Content Planning + +## Compression Ladder + +Start from the user's original text and compress in layers: + +1. Core claim: one sentence. +2. Viewer promise: what the reader gets from swiping. +3. Section map: 4-8 ideas. +4. Page hooks: short, concrete, not abstract. +5. Body fragments: only the words needed to explain the visual. + +Do not put the full article on images. The image set is a visual outline that makes the user want to read the caption/body. + +## Rednote Page Plan Template + +```text +Title: +Audience: +Style: +Image assets: + +01 Cover +- Hook: +- Visual: +- Bottom points: + +02 Page +- Point: +- Copy: +- Visual: +- Layout: + +... +``` + +## Good Page Roles + +Use varied roles instead of repeating the same card page: + +- Hook cover. +- Problem scene. +- Misconception vs reality. +- Checklist. +- Comparison. +- Product screenshot evidence. +- One big quote or conclusion. +- Step-by-step flow. +- Gear list or buying guide. +- Summary page with "remember these three". + +## Copy Rules + +- Titles should be plain and sharp. +- Avoid abstract labels like "背景介绍" unless the page is actually context. +- Use verbs and consequences: "高帮鞋会让脚更闷" is better than "鞋子选择". +- For product updates, lead with the user-facing change, then add the feature name. +- For outdoor posts, lead with the embodied problem: hot, wet, slippery, heavy, exposed, lost, cold. +- For AI/tool posts, lead with workflow change: faster capture, less manual work, cross-app automation, editable output. + +## Cover Hook Patterns + +Use one of these: + +- "终于有 Mac 版了" +- "新手别急着买" +- "这个功能真的改变用法" +- "我替你踩过坑了" +- "先看这 5 个点" +- "不要被装备带着走" +- "从此不用手动..." + +Avoid: + +- Overlong complete article titles. +- Vague hype without a concrete object. +- Multiple unrelated claims on one cover. + +## WeChat Square Title Compression + +When producing a WeChat `1:1` cover, derive a short title from the long `21:9` title. + +Rules: + +- Preserve the core object and the strongest action. +- Use 4-10 Chinese characters when possible. +- Use 2 short lines if one line becomes too cramped. +- Remove explanations, clauses, and long modifiers. +- Do not use the full long title in the square. + +Examples: + +```text +开源了一个 Skill,让 AI 接管你屏幕边那张便签纸 +=> AI 接管便签纸 +``` + +```text +Codex 史诗级更新,体验爆炸提升 +=> Codex 大更新 +``` + +## Image-Led Sequence (lifestyle, image-heavy content) + +When the user has multiple good lifestyle photos (旅行 / 户外 / 自家成菜 / 家居 / 真实情感场景), front-load the strongest photo on page 1 and let pages 2-3 carry the text/data. Do not bury great photos. + +Pattern: + +``` +P1 M16 Image-Led Cover ← single best photo, full bleed, restrained title +P2 S11 / M05 / M10 ← text-heavy: ledger / checklist / mini-data — eye rests +P3 M02 Field-Note Photo ← second-best photo, in a frame, with caption +P4-N M02 / M11 ← more field notes or marginalia essays +P_last M07 / M04 ← closing note or pull quote +``` + +Hard rule: **never two M16 in a row**. After a full-bleed cover the eye needs to settle on text before another big image. Two image-led covers back-to-back read as "we couldn't decide which photo to use." + +When this pattern does NOT apply: + +- News / tutorial / data / review posts — the cover is a hook line, not a photo. Use M01 or S01. +- Posts with one good photo only — use M16 for P1, then M02 once on P3-4 with that same photo or a different angle. Do not pad. +- Swiss-mode product/release posts — photos in Swiss are product renders or UI screenshots, not lifestyle. Use S08 Image Hero, not M16. + +For the cover title placement and typography on a great photo, see `references/layout-recipes.md` M16 (4 modes: 顶压底沉 / 侧栏立柱 / 角落徽章 / 下沉条带) and `references/image-overlay.md` (subject map + mask). + +## Page Count Guidance + +- 600-1000 Chinese characters: 5-7 images. +- 1000-1800 Chinese characters: 7-9 images. +- Very long source: compress to 8-10 images and put the rest in caption/body. +- If several pages have large empty lower areas, merge adjacent ideas. + +## Matching Visuals To Content + +Use visuals only when they support the point: + +- Screenshot: proof of a product feature or UI. +- Photo: scene, object, person, outdoor atmosphere. +- Generated image: missing scene, conceptual hero, stylized product/lifestyle illustration. +- Diagram: flow, cross-app chain, checklist, system relation. + +Generated image prompts should be short and role-specific: + +```text +Editorial documentary photo for a Rednote hiking gear guide, summer mountain trail, lightweight long-sleeve outfit, natural sunlight, clean composition, no text, no logo, 3:4. +``` + +```text +Swiss style conceptual product image, Android home screen widgets generated by natural language, clean off-white background, one IKB blue accent, no text, no logo, 3:4. +``` + +## Captions And Metadata + +Small recurring elements help the set feel designed: + +- Category row: `AI TOOL / UPDATE / 2026` +- Issue row: `SUMMER HIKING / BEGINNER GUIDE` +- Page number: `03 / 08` +- Compact labels: `DO`, `DON'T`, `WHY`, `NOTE` + +Keep these consistent. They should support orientation, not become decoration. diff --git a/references/image-overlay.md b/references/image-overlay.md new file mode 100644 index 0000000..ef4f003 --- /dev/null +++ b/references/image-overlay.md @@ -0,0 +1,152 @@ +# Image Overlay & Subject Safety + +Two rules that govern any poster where text sits on top of an image — full-bleed background, large photo well with title bar, or generated/AI image with text overlay. + +A composition can pass HTML linting and still be unreadable. Both rules below must clear before you ship. + +## Rule 1 — Selection first, mask only if selection fails + +The default editorial-magazine answer is **no mask**. Real magazine covers (Kinfolk, Cereal, Apartamento, Monocle) almost never lay a uniform black gradient over a hero photo. They rely on **photo selection + composition** so text lands in a quiet zone naturally. If you cannot find such a photo, the answer is usually to swap the photo, not to caulk over a bad one with a 70%-black scrim. + +Apply the steps in order. Stop at the first one that passes. + +### Step 1 — Photo selection (the main lever) + +Before picking a mask, verify the photo qualifies for full-bleed treatment. Both tests must pass: + +**Quiet-zone test**: at least one band of ≥30% canvas (full width × ≥30% height, or full height × ≥30% width) is **low-detail / low-contrast / uniform**. Examples: out-of-focus background, deep shade, fog, calm water, plain sky, blurred grass. This band is where the title will land. + +**Light test**: the photo carries **atmospheric / restrained light** — overcast, dawn fog, golden hour, forest understory, film softness, dusk silhouette. Reject high-saturation noon shots, on-camera flash, "tourist trap" selfies, generic stock cheerfulness. + +If either test fails, **the photo is wrong for M16**. Fall back to M01 (split-layout cover with photo in a frame), or shoot/source again. Do not "fix it with a mask." + +### Step 2 — Compose without a mask first + +Place the title inside the qualified quiet zone. Run the thumbnail contrast check (Step 4). If it reads clean, ship as-is. Most well-selected editorial photos pass at this step. + +A no-mask cover signals craft. A heavily masked cover signals "we couldn't find the right photo." Try Step 2 every time before reaching for Step 3. + +### Step 3 — Localized, image-toned tint (fallback only) + +Only when Step 2 fails the contrast check. Three rules: + +1. **Localized, not full-canvas.** Tint only the title region. Use `radial-gradient` centered on the title block, or a one-sided linear gradient that fades to fully transparent past the title. Never `inset: 0` over the whole image with a uniform alpha curve. +2. **Image-toned, not black.** Sample a dark tone already in the photo and use it as the tint color. Forest → deep moss `#1a2818`. Dusk → tea-rose dusk `#4a2638`. Snow → cool grey-blue `#2a3438`. Pure black `#000` reads as "annotation layer," not "atmospheric depth." +3. **Soft, not opaque.** Peak alpha 0.15-0.30 in the title region, falling to 0 outside. If you need >0.40 to read, the photo failed Step 1 — go back. + +```css +/* Example: title in lower-left, image-toned soft tint */ +.hero-bleed::after { + content: ""; + position: absolute; inset: 0; + background: radial-gradient( + 60% 45% at 25% 80%, /* centered on title block */ + rgba(26, 40, 24, 0.28) 0%, /* forest moss tone, soft peak */ + rgba(26, 40, 24, 0.0) 100%); + pointer-events: none; +} +``` + +### Step 4 — Thumbnail contrast check (always) + +Whether or not you applied a tint: + +- Render the PNG, downscale to 360 px wide in Preview / a browser tab, and look. +- Title strokes must be legible without zoom. +- If title looks like it's "fighting" the photo → swap photo or shift title to a different quiet zone, **not** strengthen the mask. +- If photo looks dead grey under uniform dark → mask is way too heavy or covers wrong area; restart from Step 1. + +### Banned + +- **Uniform full-canvas vertical falloff** (the `rgba(0,0,0,.55) → .10 → .10 → .80` pattern from earlier versions of this skill). That is game-key-art treatment, not editorial. +- **Pure black mask color**. Always image-toned (forest, dusk, snow, ink-blue, sepia). +- Flat black/white rectangle behind text — reads like screenshot annotation. +- `mix-blend-mode: difference` for readability — wrecks skin tones, breaks on print. +- `img { opacity: .6 }` — kills the photo's depth, the whole point of full-bleed. +- Reaching for Step 3 before honestly attempting Step 1 with a better photo. + +--- + +## Rule 2 — Place text away from subject / face zones + +Posters in 旅行 / 游戏 / 影视 / 穿搭 / 美食 frequently use a real photo as the hero. The photo has a subject — a face, a hand, a product, a peak. Text that overlaps the subject reads as graffiti, not editorial. + +### Subject zone discovery — multimodal first + +Before designing the title position on a hero photo, look at the image. Open it with the Read tool and observe in plain language: + +1. Where is the **primary subject's face / focal feature**? (e.g. "the climber's face is in the upper-right third around 70% x 30% y") +2. Where is the **edge of the subject's silhouette**? (e.g. "left edge ends at ~40% x; below that is sky") +3. Where is the **largest open / low-detail area**? (e.g. "the lower-left quadrant is uniform fog") + +Record the answers as a comment in the HTML next to the hero block: + +```html + +``` + +This is the input the layout uses. It's deterministic and reviewable. + +### Safe-zone placement rules + +Given the subject map, place text in this order of preference: + +1. **Above + below** the subject (kicker top, title bottom). 90% of full-bleed covers should do this. +2. **One side** — if subject occupies one vertical column (e.g. portrait shot on right side), text fills the opposite column. Combine with side tint from Rule 1 Step 3 only if needed. +3. **Diagonal corner** — only when the subject is in one corner and the opposite corner is genuinely empty. Rare. + +Never place display titles **across** the face. A 90 px Chinese title cutting through a person's eyes is destructive even with a mask. + +### Composition discipline (editorial look) + +These four rules are what separates an editorial poster from a generic image-with-text: + +- **Asymmetric placement.** Titles offset to one side / one corner read more confident than dead-centered ones. The Kinfolk / Cereal habit. +- **Generous negative space.** Title should occupy ≤40% of the canvas. The photo and the empty quiet zone are doing the design work. +- **Title in one quiet zone, only one.** Splitting the title into two zones (top + bottom) is fine; splitting it into three or wrapping around the subject is busy. +- **Title never overlaps the subject silhouette.** If the only safe placement requires crossing the subject's edge, switch modes (move to a frame, diptych, or off-image title) — do not enlarge the mask. + +### Crop guards + +The face-avoidance rule should also drive `object-position`. If the subject is in the upper third, do not use `object-position: center top` (that pushes the face into the title band). + +| Subject location in raw image | Recommended `object-position` for 3:4 crop | +| ----------------------------- | ------------------------------------------ | +| Face / focus in upper third | `center 25%` (lifts face higher, gives bottom room for title) | +| Face / focus in middle third | `center center` (default; text goes top + bottom) | +| Face / focus in lower third | `center 70%` (drops face down, top room for title) | +| Wide landscape, no single subject | `center 35%` (slight upward bias to retain horizon line) | +| Vertical portrait, full body | `center top` | + +### When the photo cannot accommodate text + +If running the subject map shows there is no safe zone wider than ~30% of the poster (subject fills the frame, e.g. tight portrait), do not retrofit. Options: + +- Move the photo into a **smaller frame** (use `.frame-img.r-3x4` or `.r-1x1` inside the layout — give it a defined slot, surround it with whitespace and text). +- **Two-image diptych** with one tight subject + one negative-space image, run titles over the negative-space one. +- **Move text off the image entirely** — title above, photo below. The Yading cover already does this. + +A photo that demands the full canvas is not a sign that text should fight it. It is a sign that text should sit elsewhere. + +--- + +## Checklist before delivery + +Run this for every poster that has text touching an image: + +- [ ] Photo passes quiet-zone test (≥30% canvas low-detail band) AND light test (atmospheric, not high-saturation noon). +- [ ] Tried no-mask composition first. Tint only added if Step 4 contrast check failed. +- [ ] If tinted: localized (`radial-gradient` or one-sided), image-toned color (not pure black), peak alpha ≤ 0.30. +- [ ] No full-canvas vertical falloff present (`linear-gradient(180deg, rgba(0,0,0,.X) → ...)` over `inset:0`). +- [ ] Subject map documented as a comment near the hero block. +- [ ] No display title (≥ 72 px) overlaps a face / hand / key product feature / subject silhouette. +- [ ] Title occupies ≤40% of canvas. Asymmetric placement preferred over centered. +- [ ] `object-position` chosen from the table above, not left at default `center top` for face shots. +- [ ] Thumbnail test passed (downscale to 360 px wide; title still legible without strain). +- [ ] No flat black/white text plate behind the title. + +If any item fails, fix it before exporting the final PNG. Do not deliver and "we can adjust later" — the QA window for social cards is the moment of upload. diff --git a/references/layout-recipes.md b/references/layout-recipes.md new file mode 100644 index 0000000..613d749 --- /dev/null +++ b/references/layout-recipes.md @@ -0,0 +1,831 @@ +# Layout Recipes + +These are static social-image recipes adapted from the Guizang PPT style language. They are not copied PPT templates. + +## Editorial Magazine x E-ink Recipes + +These structures (ledger / marginalia / pull-quote / photo-well / pipeline-vertical) work for **any topic** that wants a magazine-feature pace — outdoor, AI, finance, food, workplace, gaming, all welcome. The mode is a visual stance, not a content filter. See `style-system.md` "Style ↔ content type are decoupled" for the full reasoning. + +Portrait rule: + +- Every 3:4 page should occupy the vertical canvas intentionally. If the content only creates a thin table, switch to M08/M09/M10 below or add a large pull quote, evidence image, marginal column, or full-height ledger. + +**Content density rule (hard)**: on 1080×1440 cards, content must cover ≥75% of canvas height. Any pure-whitespace band >15% canvas height (>216px) needs a stated reason — hero-image breathing, single-sentence statement, or leading/trailing margins (combined ≤15%). Do NOT use `
` to push content to the vertical centre — Editorial magazines absorb whitespace across opposing pages; social cards are scrolled one at a time and under-filled cards read as "PowerPoint with a missing element." Each recipe below carries a `Minimum density:` line indicating the smallest content set that fills a 3:4 canvas. If your copy doesn't reach that floor, **shorten the canvas (switch to 1:1) or pick a different recipe** — never just publish under-filled. + +### M01 Cover: Magazine Issue Cover + +Best for Rednote page 1, portrait social cards, or article-card covers. + +Structure: + +- Top issue row: category, date, ratio, or account label. +- Large serif/Songti-like title, usually 2-4 lines. +- One large image or photo crop occupying 35%-55% of the page. +- Bottom issue strip with 3-5 points. + +Style: + +- Paper background, deep ink title. +- Photo can bleed inside a large rectangular well. +- Accent appears as one vertical rule, page number, or small label. + +### M02 Field Note Photo + +Best for outdoor, object, hardware, or real-world observation. + +Structure: + +- Large documentary photo. +- Narrow caption column or bottom caption band. +- One short takeaway in large type. + +Use when the photo is evidence, not decoration. + +### M03 Editorial Essay Split + +Best for explaining one idea with nuance. + +Structure: + +- Left: large title or pull quote. +- Right: 2-3 short paragraphs or numbered fragments. +- Thin rule between columns. + +Keep paragraphs short. If it becomes dense, split the page. + +**Minimum density (3:4)**: title + 3 short paragraphs OR title + 2 paragraphs + numbered footer list. Title alone is M04, not M03. If you have only a title + 1 paragraph, switch to M04 Pull Quote or fold a marginal column in for M11 Marginalia Essay. + +### M04 Pull Quote / Thesis + +Best for a core sentence or conclusion. + +Structure: + +- Large quote across the page. +- Small source/context row. +- Optional tiny note or issue marker. + +Use to create rhythm between dense pages. + +**Minimum density (3:4)**: this is the **one recipe** where ≤60% canvas content is allowed (hero statement = intentional whitespace). BUT you must add (a) source/context row 18-20px mono ≤15% from bottom, (b) date-stamp or 章号 kicker at top, (c) hairline rule above the source row. Without these three "anchor points," whitespace reads as missing content. Don't use M04 if you can't supply at least one anchor. + +### M05 Checklist / Buying Guide + +Best for Rednote practical content. + +Structure: + +- Header title. +- 4-6 rows, each with a number, item, and consequence. +- Optional small photo crop or material swatch. + +Avoid generic rounded cards; use rows, rules, columns, and issue labels. + +### M06 Evidence Wall + +Best for multiple screenshots, references, or small images. + +Structure: + +- 2x2 or 3-column image grid. +- Each image has a short caption. +- One larger headline anchors the interpretation. + +Use only when supplied images are readable at the final size. + +### M07 Closing Note + +Best for final page. + +Structure: + +- Big takeaway title (≤2 lines). +- **4-6 ledger items**, each 26-30px serif title + 16-18px serif sub-line ("consequence" / "reason" / "example"). Each item row should consume 100-140px vertical (border + padding + content). +- Closing block: pull-quote OR signature line OR price/CTA OR small marginalia (one of these is required). +- Small footer label. + +Mood should feel like the end of a magazine feature, not a sales banner. + +**Minimum density (3:4)**: title + **≥4 ledger items with sub-lines** + closing block. The previous "2-3 rules" version under-fills 3:4 by ~40%. If you genuinely only have 3 reasons, expand each to "title + 2-line consequence + example/quote" OR pair with a pull-quote closing block. **3 short ledger lines on a 3:4 canvas is a failure mode**; either expand or switch to M04 Pull Quote. + +### M08 Tall Ledger + +Best for lists, roles, pros/cons, gear items, product capabilities, and agent responsibilities. + +Structure: + +- Header title. +- 4-6 full-width rows. +- Each row minimum 118-170px on a 1080x1440 canvas. +- Left index/marginalia column, right title + consequence. +- Optional vertical accent strip or bottom issue note. + +Use when a normal table would become too short. Do not let the ledger occupy only the middle third of the page. + +### M09 Atmospheric Thesis + +Best for sparse but important points. + +Structure: + +- WebGL/ink-flow background visible across the page. +- One very large thesis or quote. +- 1-2 supporting notes. +- Small issue metadata and a bottom rule. + +Use this instead of forcing a small table onto a page with little content. + +### M10 Evidence Feature + +Best for supplied screenshots/photos. + +Structure: + +- Large screenshot/photo occupying 45%-65% of the vertical canvas. +- Headline and lead above or beside it. +- Bottom caption band with 2-3 takeaways. + +The image is not an afterthought. If the screenshot is important, it must be big enough to inspect. + +### M11 Marginalia Essay + +Best for nuanced explanation with moderate text. + +Structure: + +- Wide editorial title. +- Main column with 2-3 paragraphs. +- Narrow marginal column containing keywords, quote fragments, or small evidence crops. +- Hairline vertical rule between columns. + +Use this when M03 feels too empty but a ledger feels too mechanical. + +### M12 Section Divider + +Best for a mid-carousel breath between dense pages. Drop one between act 1 and act 2 of a 7-9 page Rednote set so the reader gets a beat of silence. + +Structure: + +- WebGL ink-flow background visible across most of the canvas (Editorial template mounts this by default). +- One mono `kicker` like `Act II` or `Part 2 of 3`. +- One large serif `h-display` naming the section (3-6 Chinese characters). +- One short subtitle in serif italic. +- Optional bottom `issue-strip` with the section's promise. + +HTML skeleton (3:4): + +```html +
+ +
+
+

Act II · Part 2

+

实测结果

+

What actually happened.

+
+
+ Section · Findings + — + 3 pages +
+
+``` + +Adapt for 1:1: drop the subtitle, center the `h-display`, keep the ink-flow background strong. Adapt for 21:9: shift title to the left half, let the WebGL atmosphere occupy the right half. + +### M13 Hero Question + +Best for the last page of a Rednote carousel or for a sharp pivot mid-set. The question is the entire page. + +Structure: + +- WebGL ink-flow background. +- Quiet kicker like `The Question` or `留给你的`. +- Big serif question, broken into 2-3 lines on `
` at semantic breakpoints (not random wraps). +- One short single-sentence prompt in `lead`, often a hint or invitation to comment. +- Minimal metadata at bottom. + +**Minimum density (3:4)**: like M04, this is intentionally airy. Required anchors: top kicker (0-12% y) + bottom prompt + bottom metadata strip. The ink-flow WebGL background **must be visible** (don't let `.content` cover it). If the WebGL background is missing or the kicker is empty, the page reads as under-filled rather than meditative. + +HTML skeleton (3:4): + +```html +
+ +
+
+

The Question · 留给你的

+

+ 下一次
+ 你会先
+ 问 AI 还是问朋友? +

+

在评论区告诉我你的答案。

+
+
+ End · Page 9 + — + 欢迎留言 +
+
+``` + +Title length cap: 3 lines, each <= 6 Chinese characters at default size. If the question is longer, shorten it; do not shrink the `h-display`. + +### M14 Vertical Pipeline + +Best for explaining a 3-5 step workflow, decision tree, or recipe. PPT's pipeline is horizontal; for portrait social cards rotate it vertical. + +Structure: + +- `kicker` + `h-xl` page title. +- 3-5 `.pipeline-v .step` rows. Each row has step number (mono), step title (serif), one-line description (sans). +- Hairline between steps, gap 28-36px. +- Optional small evidence image to the right of the title, or a single bottom caption. + +HTML skeleton (3:4): + +```html +
+ +
+
+

Workflow · 4 步

+

我的写作流水线

+
+
+
01
+
+

Draft

+

用语音备忘录把想法说出来

+
+
+
+
02
+
+

Reorder

+

用 AI 把段落顺序重新排

+
+
+
+
03
+
+

Polish

+

人工逐句去 AI 味

+
+
+
+
04
+
+

Ship

+

同时发小红书、推特、公众号

+
+
+
+
+
+``` + +Hard limit: 3-5 steps. If you have 6+, split into two pages or switch to M05 Checklist. + +### M15 Before / After + +Best for "old way vs new way", "before AI vs after AI", "rookie vs pro" comparisons. PPT's Before/After is left-right; on 3:4 portrait, stack top/bottom to avoid pinched columns. + +Structure: + +- `kicker` + `h-xl` page title. +- `.beforeafter` container with two `.ba-block` rows: one with class `before` (dimmed at .68 opacity), one without. +- Each block has its own kicker, mid title, and 3-4 short bullets. + +HTML skeleton (3:4): + +```html +
+ +
+
+

Before · After

+

写作流的演变

+
+
+

Before · 旧

+

在 Notion 里反复修

+
    +
  • 开头改了 20 遍
  • +
  • 每个段落都想精雕
  • +
  • 一周才发一条
  • +
+
+
+

After · 新

+

先发版,再迭代

+
    +
  • 20 分钟出初稿
  • +
  • 评论区做精修
  • +
  • 一天发三条
  • +
+
+
+
+
+``` + +Adapt for 21:9: switch to left/right split (use `.col-2` grid). Adapt for 1:1: usually too cramped; consider M04 Pull Quote instead. + +### M16 Image-Led Cover (Full-Bleed Hero) + +Best for **lifestyle, image-heavy** content where the user has 1+ excellent photos: 旅行目的地 / 户外场景 / 自家成菜 / 家居一角 / 情感场景照. Photo fills the canvas; title rides on top with restraint. This is what M01 is **not** — M01 splits the canvas into photo + text blocks. M16 lets the photo own the canvas. + +**Reference look**: Kinfolk / Cereal / Apartamento / Monocle covers. Quiet photo, serif title, paper-cream text, generous negative space, asymmetric placement. **NOT** game key-art with heavy black gradient. **NOT** travel poster with chunky bold caption. + +**Required**: `references/image-overlay.md` Rule 1 Steps 1-4 must all pass. The first gate (photo selection) is the hardest — most photos fail it. + +### Photo qualification (gate before choosing M16) + +Run both tests on the user's photo. Both must pass, or **switch to M01** (split-layout) instead of forcing M16. + +1. **Quiet-zone test**: photo has a band of ≥30% canvas (full width × ≥30% height, or full height × ≥30% width) that is low-detail / out-of-focus / uniform. This is where the title will land. +2. **Light test**: photo carries atmospheric/restrained light — overcast, dawn fog, golden hour, forest understory, dusk silhouette, film softness. Reject high-saturation noon shots, on-camera flash, generic tourist snaps. + +If only one passes: try M01. If neither: ask user for a different photo, or use M01 with a smaller `.frame-img` slot. **Do not "fix it with a mask."** + +**Page sequence convention** (this is the "image-led" pattern): + +``` +P1 (M16) ← full-bleed photo + short title — the hook +P2 (S11 / M05 / M10) ← ledger / checklist / mini-data — text-heavy rest stop +P3 (M02 or M16-small) ← second photo as field-note evidence, not full-bleed +P4-N (M02 / M11) ← more field notes or marginalia essays +P_last (M07 / M04) ← closing note or pull quote +``` + +Never run two M16 in a row. After a full-bleed photo the eye needs text. P2 must be data/text. + +**Four title-placement modes** — pick by subject map (from image-overlay.md Rule 2): + +| Mode | Subject location | Title position | Tint (only if Step 4 contrast fails) | +| ---- | ---------------- | -------------- | ------------------------------------ | +| **A · 顶压底沉** (top kicker + bottom title) | Subject in middle third (face, peak, hero object), top + bottom open | Top: kicker 0-12% y. Bottom: 1-2 line title 72-92% y. | Try no tint. If needed: bottom-only radial, image-toned, peak α ≤ 0.30 over title block. | +| **B · 侧栏立柱** (vertical column) | Subject occupies a **clean** vertical column with ≥40% width safe opposite side. Skip B if subject edge is irregular (e.g. tree canopy spilling sideways) — use A or D instead. | Opposite column (~36-40% of width), stacked: kicker → title → subtitle | Try no tint. If needed: one-sided horizontal falloff `radial-gradient(45% 60% at 18% 50%, …)`, image-toned. | +| **C · 角落徽章** (corner badge) | Subject fills most of the frame; one corner is genuinely empty | Small block in the empty corner (≤35% w × ≤25% h): kicker / short title / meta | Light vignette only in that corner — never darken the whole image | +| **D · 下沉条带** (bottom ribbon) | Wide landscape / atmospheric scene / no single subject; abundant negative space at bottom | Bottom band 78-92% y: title + meta strip; align flush-left or flush-right (not centered) | Try no tint. If needed: bottom-only radial, image-toned, peak α 0.20-0.30 | + +**Typography spec on 1080×1440 (Editorial × E-ink) — restrained, NOT chunky**: + +| Element | Mode A | Mode B | Mode C | Mode D | +| ------- | ------ | ------ | ------ | ------ | +| Kicker | mono 20-22px, uppercase, tracking 0.18-0.22em | mono 20-22px | mono 18-20px | mono 22-24px | +| Title font | Noto Serif SC **400-500** (NOT 700-900) | same | same | same | +| Title size | **88-108px**. Default 96px for 2-line zh title. Drop to 88px if 2 lines × ≥7 chars. | **84-100px**, wrap on phrase boundary | 56-64px (NOT display) | **96-112px**, single line preferred | +| Title tracking (zh) | 0.10-0.15em | 0.10-0.15em | 0.05-0.10em | 0.12-0.18em | +| Title line-height | 1.10-1.18 | 1.10-1.18 | 1.20 | 1.05-1.10 | +| Subtitle | optional `h-sub` 28-32px italic Playfair, tracking 0 | same | omit | same | +| Issue strip | mono 18-20px bottom, uppercase, tracking 0.20-0.25em, hairline above | omit | omit | mono 20-22px, tracking 0.20-0.25em | +| Text color | `#f5f1e8` (paper-cream, NOT pure white #fff) | same | same | same | +| Hairline color | `rgba(245, 241, 232, 0.35)` (paper at 35% alpha) | same | same | same | + +**Forbidden in M16 type**: + +- Title weight 700+ inline (e.g. `font-weight: 800` to "make it pop"). M16 wants **regular-medium serif**, not bold. +- Pure white `#fff` text — too clinical, fights the photo's warmth. Always paper-cream. +- Title size > 120px on 1080-wide canvas. We are not making travel posters. +- Latin sans-serif as primary title (Helvetica / Inter for the headline) — M16 belongs to serif. Sans only as kicker / meta / mono caption. +- Centered title on D mode. Always flush-left or flush-right for asymmetric Kinfolk feel. + +**Title-length budget** (cut copy if you bust this — never shrink type below 84px): + +| Mode | Max Chinese chars | Max lines | +| ---- | ----------------- | --------- | +| A | 12 (2 × 6) or 14 (2 × 7 at 88px) | 2 | +| B | 14 (2 × 7) | 2 | +| C | 8 | 1 | +| D | 10 | 1 | + +**Square (1:1, 1080×1080) and Wide (21:9, 2100×900) adaptations**: + +- **1:1**: Use Mode A or Mode D only. Mode B's column squeeze, Mode C's corner are too tight on square. Shorten title to 6-10 chars. +- **21:9**: Use Mode B (left text column + right photo bleed) or Mode D (full-width bottom band). Mode A works but the bottom title competes with the WeChat caption strip — prefer D. + +**HTML skeleton (Mode A — 顶压底沉 on 3:4, no mask)**: + +```html +
+ +
+ +
+

+ Vol. 04 — 2026 — 户外 +

+
+

+ 在稻城
看见冬天 +

+
+ DAY 1-4 · 4,200 M · −18°C +
+
+
+``` + +**HTML skeleton (Mode D — 下沉条带 on 21:9, for WeChat main cover)**: + +```html +
+ +
+
+

+ 在稻城看见冬天 +

+
+ VOL. 04 — DAY 1-4 — 4,200 M — 35MM +
+
+
+``` + +**Pitfalls**: + +- Choosing M16 when the photo fails quiet-zone or light test. The cover will look bad no matter what mask you layer on. Fall back to M01. +- Putting Mode A's bottom title across someone's torso when the subject is in the lower third — re-run subject map and switch to Mode C, or change `object-position: center 70%`. +- Mode C with a heavy backdrop plate behind the corner title — defeats the point of having a great photo. Use vignette only. +- Using Mode B on a photo where the subject's silhouette is **irregular** (trees with sideways branches, scattered objects) — the "side column" won't be wide enough to be a true safe zone. Switch to Mode A or D. +- Adding a full-canvas vertical falloff "just in case." Mask-by-default kills the editorial look. The fix is photo selection, not opacity. +- Setting title weight 700+ or pure-white text — both are anti-pattern in Editorial M16. +- Running M16 on every cover. M16 is for **image-heavy lifestyle**. Product/tech/AI release covers still belong to S01 / S03 / M01. + +## Swiss International Recipes + +These structures (accent cover / matrix / KPI tower / h-bar / numbered statement) work for **any topic** that wants an engineered, quantified pace — software releases yes, but also travel budgets, fitness logs, recipe portions, reading stats, anything where you'd rather count it than narrate it. The mode is a visual stance, not a content filter. + +### S01 Accent Cover + +Best for Rednote cover. + +Structure: + +- Full accent or off-white background. +- Big light-weight title. +- Simple abstract system block, comparison, or two-node diagram. +- Bottom metadata strip. + +Use one clear concept. No decorative blobs. + +### S02 Two Signals / Comparison + +Best for explaining two sources, two options, or two product directions. + +Structure: + +- Page title. +- Two large rectangular modules. +- One module may be ink-filled; the other paper/outlined. +- Short notes below each. + +### S03 Data Layer / File Card + +Best for Markdown, memory, source-of-truth, database, or state. + +Structure: + +- Large file-type or object block. +- List of 3-4 properties. +- Strong mono labels. + +### S04 Interface / Browser Mock + +Best for HTML, UI, presentation, interaction, or output layer. + +Structure: + +- Browser-window style frame with straight edges. +- Inside: one hero content block plus 2-3 functional modules. +- Bottom action or interaction strip. + +### S05 Trap / Warning Rows + +Best for problems, anti-patterns, and "do not do this" pages. + +Structure: + +- Big warning title. +- Three horizontal rows. +- Left mono label, right consequence. + +Safety Orange works well here if the whole package uses it. + +### S06 Pipeline / Architecture + +Best for workflows and layered systems. + +Structure: + +- Three rows or columns: source, render, share. +- Each has number, label, action, and consequence. +- Use hairline boxes and neutral grey fills. + +### S07 Takeaway Ledger + +Best for final page. + +Structure: + +- Big thesis title. +- Three ledger rows with number, phrase, and compressed explanation. +- Dark or ink background can create closure. + +### S08 Image Hero + +Best for the WeChat 21:9 main cover and the Rednote cover when you have one strong photo or product render. The image fills the page; the title sits inside an overlay block; 3 quantified stats anchor the bottom. + +Structure: + +- `.chrome-min` top single row (category + date). +- `.image-hero` grid: `.hero-img-wrap` with one `.frame-img.r-21x9` photo (or `.r-3x2` on 3:4). +- `.hero-overlay-block` sitting over the upper-left of the image with `t-cat` kicker + `h-statement` title. +- `.hero-stats` row of 3 `.stat-block`s, each with a big `num` (e.g. `132K`) and a mono `lbl`. + +HTML skeleton (21:9, the natural format for this recipe): + +```html +
+
+
+ Release · 产品 + 2026.05 · v3.0 +
+
+
+ hero +
+

Cover · 主封面

+

下一代
写作工作流

+
+
+
+
+

132K

+

Weekly Writers

+
+
+

3.4×

+

Output Speed

+
+
+

98%

+

Ship Rate

+
+
+
+
+
+``` + +Adapt for 3:4: keep the same `.image-hero` structure but swap `.r-21x9` to `.r-3x2`, drop overlay block down to 35% height. Adapt for 1:1: drop the overlay block entirely, push title above the image, keep at most 2 stats in `hero-stats`. + +### S09 KPI Tower + +Best for product-update pages, release notes, traffic dashboards, growth posts — anywhere 3-4 numbers need to be compared at a glance. + +Structure: + +- `kicker` + `h-xl` page title. +- `.kpi-tower-row` with 4 `.tower-col`s, each containing a `num`, `lbl`, and a `.bar-tower` whose height encodes the value via `style="--h:Npx"`. +- One column may carry the `.muted` modifier to drop the bar to neutral grey (use for a comparison baseline). + +HTML skeleton (3:4 — template auto-collapses to 2 columns): + +```html +
+
+

Data · 半年增长

+

六个月,四组数字

+
+
+

132K

+

Subscribers

+
+
+
+

3.4M

+

Reads

+
+
+
+

68%

+

Repeat Visits

+
+
+
+

8.2%

+

Conversion

+
+
+
+
+
+``` + +`--h` is a real height in px — pick values that proportionally encode the data. Do not invent numbers; if you only have 2 numbers, drop to 2 columns and use S03 File Card instead. Adapt for 21:9: keep 4 columns at the original width (it's where the layout shines). Adapt for 1:1: 2 columns max, taller bars. + +### S10 H-Bar Chart + +Best for rankings, comparisons of 5-10 items, "top N" lists, before/after pairs at scale. The bar fill encodes magnitude via `style="--w:NN%"`. + +Structure: + +- `kicker` + `h-xl` page title. +- `.h-bar-chart` grid with 5-10 `.bar-row`s. +- Each row: `.row-lbl` (Chinese item name), `.row-track` containing `.row-fill`, `.row-val` (mono number). + +HTML skeleton (3:4): + +```html +
+
+

Ranking · TOP 6

+

本月最受欢迎的工具

+
+
+
Claude Code
+
+
94%
+
+
+
Cursor
+
+
78%
+
+
+
Linear
+
+
62%
+
+
+
Raycast
+
+
48%
+
+
+
Notion
+
+
36%
+
+
+
Obsidian
+
+
28%
+
+
+
+
+``` + +Hard limits: max 6 rows on 1:1, max 10 rows on 3:4, max 8 rows on 21:9. On 3:4 the template stacks `row-lbl` above the track automatically — leave it alone. Always use real percentages or normalize the largest value to 100%; never fabricate. + +### S11 Stacked Ledger + +Best for shopping lists, expense rollups, agent capability inventories, or any "big number + label + icon" row stack where each item is quantified. + +Structure: + +- `kicker` + `h-xl` page title. +- `.stacked-ledger` containing 4-6 `.ledger-row`s. +- Each row: `.ledger-num` (large mono-feel sans number), `.ledger-lbl` (Chinese label + optional `.sub` secondary line), `.ledger-icn` (Lucide icon). + +HTML skeleton (3:4): + +```html +
+
+

Spend · 五月账单

+

这个月我把钱花在哪

+
+
+

¥1,280

+
订阅 · Subscriptions + Claude · Cursor · Linear +
+ +
+
+

¥860

+
书 · Books + 6 本中文 · 2 本英文 +
+ +
+
+

¥540

+
咖啡 · Coffee + 主要在 Manner 和 Seesaw +
+ +
+
+

¥320

+
硬件 · Hardware + 键盘配件 · 一根 USB-C +
+ +
+
+
+
+``` + +Hard limits: max 6 rows on 3:4, max 4 rows on 1:1, max 8 rows on 21:9. Pick angular Lucide icons (`book-open`, `coffee`, `bolt`, `square-stack`, `keyboard`) — never `heart-filled` or `smile`. If you cannot find numerical evidence, this is the wrong recipe; use S05 Trap Rows or M08 Tall Ledger. + +### S12 Matrix + Hero Stat + +Best for capability matrices, agent inventories, "this set covers X domains" pages where 8-12 small cells back a single bottom-line number. + +Structure: + +- `kicker` + `h-xl` page title. +- `.matrix-fill` grid of 8-12 `.matrix-cell`s. Each cell: `.cell-nb` (numbered mono label like `01`) and `.cell-title` (one short Chinese phrase). +- Exactly one cell may carry the `.is-accent` modifier to single out the most important capability. +- `.hero-stat-bottom` below the grid: left column with a kicker + short sentence, right column with one `num-mega` summary stat. + +HTML skeleton (3:4 — matrix collapses to 2 columns): + +```html +
+
+

Capabilities · 能力清单

+

一个 Agent,十二件事

+
+

01

读取项目结构

+

02

运行测试

+

03

起草 PR 描述

+

04

回滚错误改动

+

05

迁移 lockfile

+

06

检查类型

+

07

追踪 issue

+

08

阅读日志

+
+
+
+

In total · 累计

+

在 6 个仓库上,持续 4 周。

+
+

12

+
+
+
+``` + +Hard limits: 8 cells on 3:4 (2×4), 9 cells on 1:1 (3×3), 12 cells on 21:9 (4×3). Max one `is-accent` cell — two breaks the rhythm. The `num-mega` at bottom must agree with the cell count (`12` if 12 cells, `8` if 8); never display a number unsupported by the grid. + +## WeChat Adaptation + +For 21:9: + +- Use M01/S01 as a wide composition, not a crop. +- Put title in the left or center-left safe area. +- Use the right half for photo/object/system visual. +- Enlarge title and subtitle spacing if the center feels empty. + +For 1:1: + +- Distill the long title into a simple centered title. +- If the user asks for no image, use type only. +- Avoid small subtitles and avoid photos by default. +- Keep the square readable as a thumbnail: one idea, big type, generous margins. + +For pair preview: + +- Put the `21:9` and `1:1` compositions in one HTML preview frame. +- The preview frame is for review only; do not treat it as a third required WeChat deliverable unless requested. diff --git a/references/map-component.md b/references/map-component.md new file mode 100644 index 0000000..7519519 --- /dev/null +++ b/references/map-component.md @@ -0,0 +1,142 @@ +# Map Component (.map-block) + +Use when the content has **spatial relationships**: a travel route, store locations, walking tour, neighborhood overview, store-vs-store comparison by district, before/after relocation. Single coordinates ("我在北京") don't need a map. + +Both seeds ship `.map-block` + `.map-pin` + `.map-legend` classes. The same markup works in either; only the style-locked tones differ. + +## Three rendering modes + +Pick mode **before** writing the markup. They are not stackable. + +### Mode S · Schematic SVG (default, no token) + +Hand-drawn-style vector map drawn into a `viewBox="0 0 100 100"` SVG. Best for editorial decks where the map should feel like an illustrated guide, not a satellite capture. No external service. + +```html +
+ + + + + + + + + + + + +
+
+
大昭寺
DAY 1
+
+
+
+
布达拉宫
DAY 2 · 18:40 SUNSET
+
+ +
LHASA · 拉萨城关 · 1:8K
+
+``` + +The SVG paths are stylized, not GPS-accurate. **Don't** try to trace OpenStreetMap by hand — this map is a graphic device, not a wayfinding tool. Sketch the major axes that frame the pins. + +Layer order in the SVG (back to front): +1. `.map-water` polygons (rivers, lakes, parks) +2. `.map-coast` (boundary) +3. `.map-grid` (optional reference grid lines) +4. `.map-road` (transport lines) + +Pins sit *outside* the SVG, as HTML siblings — they get the `.frame-shot` treatment for free. + +### Mode T · Mapbox Static Image (signed token) + +Real raster tiles, desaturated and tinted to match the deck. Use when the content is travel / hiking and you need to show actual terrain. Requires `MAPBOX_ACCESS_TOKEN` in the build env. + +``` +https://api.mapbox.com/styles/v1/mapbox/light-v11/static/ + pin-s-1+555555(116.404,39.915), + pin-l-2+1936b3(116.421,39.918) + /116.412,39.916,14,0/1200x675@2x + ?access_token=YOUR_TOKEN +``` + +```html +
+ Lhasa walking route + + +
...
+
...
+ +
LHASA · MAPBOX LIGHT
+
+``` + +Editorial uses `tone-paper` (multiply-blend, saturation 36%). Swiss uses `tone-paper` with saturation 0 (pure greyscale). Don't show the raw Mapbox colour-saturated style — it fights the deck. + +**Style allowlist** for the URL — keep it monochrome: +- `mapbox/light-v11` +- `mapbox/dark-v11` +- `mapbox/streets-v12` (only with `tone-paper`, never raw) + +Never use `outdoors-v12` or `satellite-v9` — too colourful, will clash with both styles. + +### Mode O · OpenStreetMap tile composite (fallback) + +Free, no token. Reach for this only if you don't have a Mapbox token and the schematic doesn't carry the message. Render server-side via `staticmaps` npm package or similar — not covered here. Quality is lower than Mapbox. + +## Hard rules + +- **No pin labels on the SVG**. Names live in `.map-pin .card`, never as SVG ``. Keeps font rendering consistent and avoids vector-text scaling issues. +- **Max 6 pins per board**. More than that, the cards collide. Use a relations / index card to list secondary points. +- **One accent pin maximum**. Multiple accents kill the visual hierarchy. +- **Pins must not overlap cards**. Test in browser first — if two cards are within 80 px on the long axis, alternate `.left` placement. +- **No live JavaScript map**. Social cards are static PNGs, MapLibre is wasted weight. The PPT skill has an interactive variant — don't port it here. + +## Pin placement playbook + +The `.map-pin` element uses `left:X%; top:Y%;` to anchor on its centre. The card grows to the right by default; add `.left` to flip: + +```html +
+
+
车公庄
START
+
+``` + +Rule of thumb: +- Pin sits in left 60% of canvas → card opens right (default). +- Pin sits in right 60% of canvas → card opens left (`.left`). +- Two pins close to each other → alternate sides so cards don't stack. + +## Recipe routing + +Map block fits into existing recipes; it doesn't get its own. + +| Recipe | How the map fits | +| --------------- | --------------------------------------------------------------- | +| M01 split | Top half = `.map-block r-16x9`; bottom half = explanatory text. | +| M11 marginalia | Main column = `.map-block r-3x4`; aside lists relation cards. | +| M14 pipeline | One step contains a small map (r-16x10) showing that step's location. | +| S02 two signals | One signal cell holds a map; the other holds the data table. | +| S08 image hero | Replace the hero photo with `.map-block r-16x10`. | + +Don't invent a "map-only" page — the map always pairs with text that says **why** the spatial relationship matters. + +## Tone tokens + +| Token | Editorial behaviour | Swiss behaviour | +| ------------- | ------------------------------------------------ | --------------------------------------------- | +| (none) | Raw — only use for SVG schematic. | Raw — only use for SVG schematic. | +| `tone-paper` | Multiply blend on warm paper. Default for travel.| Hard greyscale. Default for everything. | +| `tone-ink` | Brightness 62%, saturation 18%. Dark deck pages. | Brightness 58%, saturation 0%. Dark posters. | + +Tone only affects `> img` children, not the SVG schematic. + +## Common mistakes + +- **Pasting Google Maps screenshots** — wrong tone, has UI chrome, will look out of place. Use Mapbox Static or schematic. +- **Tracing a real map into SVG** — schematic should *abstract*, not replicate. If a viewer can guess the city from your SVG alone, you over-traced. +- **Tiny pin cards** — `.card .name` is 15-16 px minimum. Cut the name if it overflows; never shrink. +- **Accent on every pin** — defeats the highlight system. Pick one most-important point. diff --git a/references/platform-specs.md b/references/platform-specs.md new file mode 100644 index 0000000..419504c --- /dev/null +++ b/references/platform-specs.md @@ -0,0 +1,122 @@ +# Platform Specs + +Use high-resolution exports. The user can downsample later. + +## Xiaohongshu / Rednote + +Default size: + +- `1080 x 1440` +- Ratio: `3:4` +- Export: PNG for text-heavy images, JPEG only when photo-only and file size matters. + +Safe area: + +- Side margin: 72-96px. +- Top margin: 72-112px. +- Bottom margin: 80-120px. +- Keep all text and key objects inside the safe area. + +Recommended set: + +- 1 cover. +- 4-8 content pages. +- 1 summary or checklist page if the article benefits from it. + +Naming: + +```text +output/xhs-01-cover.png +output/xhs-02-.png +output/xhs-03-.png +... +``` + +Cover structure: + +- Big title with a sharp hook. +- One strong visual: supplied image, screenshot, generated photo, or product object. +- Bottom line with 3-5 key points or a compact issue strip. +- Avoid long subtitles. Put nuance in later pages. + +Content page structure: + +- One idea per page. +- 12-30 Chinese characters for the title when possible. +- 2-4 short bullet fragments or one concise paragraph. +- If a page has a screenshot, reduce the text and give the screenshot enough area. +- If lower space is empty across pages, combine pages or add a functional element such as checklist, comparison row, quote, or image evidence. + +## WeChat Official Account Cover Pair + +Generate a pair: one `21:9` main cover and one `1:1` square cover. Build both in the same HTML file, and include a combined preview section that places them together for visual checking. + +### 21:9 Main Cover + +Default size: + +- `2100 x 900` +- Ratio: `21:9` + +Use: + +- Main article cover. +- Full or near-full headline, subtitle, and visual evidence. +- Keep title readable near center-left or within a clear safe band. +- Avoid an empty middle. Increase title scale, image scale, or spacing if the center feels hollow. + +### 1:1 Cover + +Default size: + +- `1080 x 1080` +- Ratio: `1:1` + +Default style: + +- Short title only, simplified from the long title. +- Big type, center aligned. +- No image unless the user explicitly asks for one. +- No subtitle by default. +- Strong contrast and enough breathing room. + +Short-title rule: + +- Extract the core object and action from the long title. +- Keep it to roughly 4-10 Chinese characters, or 2 short lines when needed. +- Do not squeeze the full `21:9` title into the square. + +Example: + +```text +Long: 开源了一个 Skill,让 AI 接管你屏幕边那张便签纸 +Square: AI 接管便签纸 +``` + +Naming: + +```text +output/wechat-21x9-cover.png +output/wechat-1x1-cover.png +output/wechat-cover-pair-preview.png +``` + +## Screenshot And Photo Placement + +For supplied screenshots/photos: + +- Put originals in `assets/`. +- Use CSS `object-fit:cover` only when cropping is safe. +- Use CSS `object-fit:contain` for UI screenshots, dense text, code, tables, or app windows. +- Set `object-position` deliberately: top for long UI when top matters, center for objects, 35%-45% vertical for portraits. +- Do not pin images to the canvas edge unless the design intentionally uses bleed. + +## Final Response Media + +In Codex desktop, local media can be shown with absolute paths: + +```markdown +![cover](/absolute/path/to/output/xhs-01-cover.png) +``` + +Do this for the most important images, especially the cover and any pages the user asked to inspect. diff --git a/references/portrait-fill.md b/references/portrait-fill.md new file mode 100644 index 0000000..8f8d88b --- /dev/null +++ b/references/portrait-fill.md @@ -0,0 +1,85 @@ +# Portrait Fill Rules + +3:4 social images have much more vertical space than a horizontal PPT slide. A layout that works in 16:9 often becomes a thin strip in 3:4. Solve that at the planning stage. + +## Vertical Zones + +Think of a 1080x1440 Rednote image as five zones: + +| Zone | Typical Height | Role | +| --- | --- | --- | +| Header/meta | 56-90px | issue label, category, page | +| Title/lead | 240-420px | hook and claim | +| Evidence/body | 520-760px | image, ledger, diagram, comparison | +| Bottom strip | 90-180px | captions, checklist, summary | +| Footer | 52-86px | issue/page metadata | + +Not every page needs all five, but every page needs an intentional vertical composition. + +## Underfilled Page Smells + +Revise if: + +- A table or ledger occupies less than 45% of the canvas height. +- The lower 25% of the page is empty without being an atmospheric hero page. +- Four list items are rendered as short rows with large blank space below. +- A screenshot is small even though it is the evidence. +- Body copy is centered in the middle with no top/bottom rhythm. + +## Fixes For Thin Tables + +When a table is too short: + +1. Increase row height to 118-170px. +2. Add a left marginal column with large numbers, keywords, or category labels. +3. Add a pull quote column beside the table. +4. Add a top or bottom issue strip with 2-3 consequences. +5. Convert the table to M08 Tall Ledger. +6. If there are only 2-3 points, use M04 Pull Quote or M09 Atmospheric Thesis instead of a table. + +## Fixes For Sparse Text + +If a page has only one core sentence: + +- Use M09 Atmospheric Thesis with visible WebGL/ink background. +- Make the sentence the hero, not a small paragraph. +- Add one small source/context row. +- Use dark/ink page if the package needs rhythm. + +## Fixes For Screenshot Pages + +If screenshot content matters: + +- Give it 45%-65% of the page height. +- Put only one key sentence above it. +- Use a bottom caption band instead of a side paragraph. +- Crop carefully; preserve readable UI labels. + +## Page Rhythm + +For a 7-page Rednote set, use at least 5 distinct shapes: + +- Cover / feature image. +- Essay split. +- Tall ledger. +- Evidence screenshot. +- Pull quote or atmospheric thesis. +- Checklist/comparison. +- Closing note. + +Avoid repeating `title + lead + 3 rows` more than twice in one set. + +## Minimum Occupancy + +As a rough rule, the active composition should occupy at least 78% of the page height. + +This can include: + +- Large type. +- Images/screenshots. +- Ledger rows. +- Background atmosphere on hero pages. +- Caption bands. +- Marginalia columns. + +Whitespace is good only when it is designed. Empty leftover space is not. diff --git a/references/production-workflow.md b/references/production-workflow.md new file mode 100644 index 0000000..dbc5466 --- /dev/null +++ b/references/production-workflow.md @@ -0,0 +1,172 @@ +# Production Workflow + +## Recommended Folder Shape + +Create a task folder under the current workspace: + +```text +social-card-/ + index.html + render.cjs + assets/ + output/ +``` + +Use descriptive slugs: + +- `social-card-doubao-input` +- `social-card-hiking-outfit` +- `wechat-ai-card-skill-cover` + +## HTML/CSS Rendering Pattern + +Build one HTML file containing all frames: + +```html +
+
...
+
...
+
...
+
...
+
+
...
+
...
+
+
+``` + +Each frame must have stable dimensions: + +```css +.xhs { + width: 1080px; + height: 1440px; +} + +.wechat.wide { + width: 2100px; + height: 900px; +} + +.wechat.square { + width: 1080px; + height: 1080px; +} + +.wechat-pair-preview { + width: 2400px; + min-height: 1180px; +} +``` + +Use `box-sizing:border-box`, fixed safe margins, and `overflow:hidden`. + +For WeChat covers: + +- Compose `wechat-21x9` and `wechat-1x1` as separate source frames. +- Also include `wechat-pair-preview` in the same HTML so the pair can be inspected together. +- Export the two real deliverables separately. +- Export the pair preview only when helpful for review. +- The `1:1` cover should use a simplified title derived from the long title, not a crop or text squeeze from the `21:9` frame. + +For electronic-magazine pages, place background canvases inside each `.poster` instead of relying only on CSS color: + +```html +
+ +
+
...
+
+``` + +The skill includes a reusable helper at `assets/magazine-bg-webgl.js`. Copy or inline it into the generated project when a WebGL background is useful. + +## Rendering + +Use Playwright or an equivalent browser screenshot workflow: + +1. Open `index.html`. +2. Wait for fonts and images. +3. Screenshot each frame node by id. +4. Save to `output/`. +5. Verify dimensions. + +If using WebGL or procedural canvas backgrounds: + +- Wait at least 500-900ms before screenshots so the canvas has rendered. +- Use deterministic seeds or a fixed time when repeatability matters. +- On normal content pages, keep background opacity subtle. +- On cover, divider, pull-quote, and sparse pages, let the atmosphere show more strongly. + +Example render logic: + +```js +const targets = [ + ["#xhs-01", "xhs-01-cover.png"], + ["#xhs-02", "xhs-02-point.png"], + ["#wechat-21x9", "wechat-21x9-cover.png"], + ["#wechat-1x1", "wechat-1x1-cover.png"], + ["#wechat-pair-preview", "wechat-cover-pair-preview.png"], +]; +``` + +If a local dev server is needed for assets or font loading, start it and tell the user the URL. If `file://` rendering works, no server is required. + +## Verification Commands + +Useful checks on macOS: + +```bash +sips -g pixelWidth -g pixelHeight output/*.png +``` + +For visual inspection in Codex: + +- Use image viewing tools for local PNGs. +- Show final PNGs inline with absolute paths. + +## Screenshot Treatment + +Programmatic framing is preferred for user-provided screenshots: + +- Create a clean target-ratio frame. +- Add plain white, refined grey, or paper background. Do not add page-wide grid/dot backgrounds unless the user explicitly asks for a technical blueprint look. +- Place screenshot inside with safe padding. +- Preserve readable text. +- Do not redraw the screenshot unless the user asked for redesign. + +For Swiss: + +- Straight corners. +- No shadow by default. +- Hairline only if the screenshot edge disappears. + +For editorial magazine: + +- Small radius or subtle shadow is allowed, but avoid SaaS marketing-card styling. + +## Generated Images + +When generating missing visuals: + +- Generate only the raw visual asset. +- Keep text out of generated images. +- Match the page role and style mode. +- Save generated assets into `assets/` and place them in the HTML. +- Generate only the pages that need it, usually 1-2 images for a set. + +## Accessibility And Readability + +- Use strong contrast for all text. +- Do not place long text over busy photos. +- If text must sit over photo, use a solid ink/paper block or a high-contrast strip, not a blur blob. +- Avoid negative letter spacing in Chinese body text. +- Use line-height that lets Chinese breathe: roughly 1.08-1.22 for big titles, 1.35-1.55 for body. + +## Common Fixes + +- Cover feels empty: enlarge title, enlarge image, or add a functional bottom strip. +- Screenshot too small: reduce side text, give screenshot 55%-70% of the canvas. +- Lower area empty: read `portrait-fill.md`; merge pages, add a full-height ledger, use a larger evidence image, add a marginal quote column, or switch to an atmospheric thesis page. +- Style feels generic: add issue metadata, better type hierarchy, a stronger evidence image, richer WebGL/ink atmosphere, or more intentional content dividers. +- Text overflows: shorten copy before shrinking type. diff --git a/references/qa-checklist.md b/references/qa-checklist.md new file mode 100644 index 0000000..3a796c0 --- /dev/null +++ b/references/qa-checklist.md @@ -0,0 +1,83 @@ +# QA Checklist + +Run this before final delivery. + +## Dimensions + +- Rednote images are `1080 x 1440`. +- WeChat 21:9 cover is `2100 x 900`. +- WeChat 1:1 cover is `1080 x 1080`. +- WeChat pair preview exists in the same HTML when WeChat covers are requested. +- File names are stable and ordered. + +## Text + +- No text overflows or touches the edge. +- No content collides with `.foot` / `.issue-strip`. If body copy or a closing line is overlapping the page footer, the footer is probably `position: absolute` while content above grew. Switch to flex `margin-top: auto` (see Anti-pattern C in `style-system.md`). +- Cover title is readable on a phone at thumbnail size. +- Body text is not smaller than it needs to be. +- Chinese line breaks are intentional. +- Important product names, English terms, and code snippets are spelled correctly. +- Captions match the source text; no invented facts. + +## Layout + +- Each page has one clear focal point. +- Pages do not all use the same structure. +- Lower empty space is either intentional or fixed. +- Images and screenshots align to the grid. +- Screenshot pages give the screenshot enough area. +- 1:1 WeChat cover uses a simplified short title and is composed separately, not blindly cropped. + +### 4-band Density Check (3:4 only — run after render) + +Open the rendered PNG. Mentally divide it into 4 horizontal bands of 360px each (0-25%, 25-50%, 50-75%, 75-100%). For each band, classify as: + +- **Filled** — contains text, image, data, or rule. +- **Justified empty** — empty for a stated reason: hero-image breathing (M01/M16), single-sentence statement (M04/M13), leading/trailing margin (top 8% / bottom 8%). +- **Under-filled** — empty with no reason. **Any single under-filled band > 15% canvas height (>216px) is a fail.** + +A poster passes when: + +1. Total Filled + Justified empty ≥ 100%. +2. Filled bands cover ≥ 75% canvas height (≥ 1080px of 1440px). +3. No two adjacent bands are both "justified empty" — that creates a >25% void mid-poster. + +If failing: don't shrink the canvas, don't add decorative blobs. Either expand copy (more ledger items, longer paragraphs, supporting evidence row, marginalia column) or switch recipe (M07 → M04 for genuinely-short content; M03 → M11 to add a marginal column; thin ledger → M08 Tall Ledger with bigger rows). + +## Style + +- Package uses one visual system. +- Swiss identity test passes for every Swiss poster (see `style-system.md` "Style Identity Test"). In particular: every display title >=72px uses a typed class with weight <=300; no serif font is loaded; only one accent color is used. A bold 90px headline is not Swiss. +- Editorial identity test passes for every Editorial poster: at least one atmosphere layer beyond a flat fill (paper grain / ink wash / WebGL); serif display family on the title; at least one of large photo well, serif pull quote, marginalia column, or true ledger structure. A flat paper with serif title + mono pills everywhere is Swiss-in-disguise. +- Swiss uses one accent color only. +- Magazine style uses restrained paper/ink tones. +- No random SVG blobs, ovals, drops, circles, stickers, or decorative gradients. +- No nested cards. +- No excessive rounded corners. + +## Images + +- Supplied images are not distorted. +- Faces, hardware, product UI, and key text are not accidentally cropped. +- Screenshots remain readable. +- Generated images do not contain unwanted text, logos, page numbers, or poster borders. +- Photo crops feel intentional. +- For any image fetched from the web (Pexels / Unsplash / Flickr CC / Wallhaven / direct search): the source URL is recorded in `assets/SOURCES.md`, and the user has been asked whether to add an in-image attribution caption. The user's answer is honored. Flickr CC attribution preserves the author name when the user opts in. + +## Text-On-Image (when applicable) + +Run these only for posters where text touches a photo (full-bleed cover, large image well, generated overlay). See `references/image-overlay.md`. + +- Image area ≥60% of canvas → A, B, or C contrast mask is present (not a flat black rectangle behind text). +- Subject map is documented as an HTML comment next to the hero block (face / focal feature location + safe zones). +- No display title (≥72 px) overlaps a face, hand, or key product feature. +- `object-position` was chosen from the subject-location table, not left at default for face shots. +- Thumbnail test: downscale the rendered PNG to 360 px wide and confirm the title is still legible without strain. + +## Final Response + +- Include the output path. +- Show the main cover inline when useful. +- Mention verification performed. +- Mention any limitations, such as low-resolution source assets. diff --git a/references/screenshot-treatment.md b/references/screenshot-treatment.md new file mode 100644 index 0000000..4bf13b6 --- /dev/null +++ b/references/screenshot-treatment.md @@ -0,0 +1,148 @@ +# Screenshot Treatment + +When the user supplies an app / web / code / dashboard screenshot, **do not** drop it raw into a `.frame-img`. The aspect ratio mismatch crops UI and the harsh edge looks SaaS-y. Use `.frame-shot` instead — a sibling class added to both seed templates. + +## When to reach for it + +- App / Web UI capture — anything with a status bar, tab bar, toolbar, or window chrome. +- Code / terminal screenshots. +- Dashboard / chart screenshots that need to keep every label readable. +- IDE captures where text density matters more than composition. + +For **photographic** content (people, scenery, products) — keep using `.frame-img`. The treatments below assume a pixel-perfect UI source where contain-fit is non-negotiable. + +## Anatomy + +``` +.frame-shot.r-{ratio}.corners-{sq|sm|md}.shadow-{none|soft|ed}.bg-{paper|grid|dot|grey-1|ink}.inset-{none|sub|bal} + └─ (object-fit: contain by default) +``` + +Optional wrapping: + +``` +.device-browser + └─ .frame-shot.r-16x10.bg-paper + └─ + +.device-phone + └─ .frame-shot.r-3x4.bg-paper + └─ +``` + +## Parameters + +Pick six values before writing the markup. Treat this like the M16 cover decision tree — pick once, don't fiddle mid-build. + +### 1. `r-*` ratio (required, matches slot) + +| Class | Use | +| ---------- | --------------------------------------------------------- | +| `r-16x10` | Default for app / web shots, looks like a real window | +| `r-16x9` | Landscape video / dashboard / wide chart | +| `r-4x3` | Classic desktop window, legacy app | +| `r-3x2` | DSLR-style — only if the source is photographic UI mockup | +| `r-1x1` | App icon / square widget | +| `r-3x4` | Mobile portrait shot (pair with `.device-phone`) | +| `r-21x9` | Multi-monitor / ultra-wide / WeChat hero | + +### 2. `corners-*` (style-locked default) + +- **Swiss** default: `corners-sq`. Use `corners-sm` only if the slot has surrounding shadow or chrome. +- **Editorial** default: `corners-sm` (6 px). Bump to `corners-md` (14 px) for "cutout" feel on paper. + +Never go above 14 px — anything bigger reads as iOS marketing. + +### 3. `shadow-*` (style-locked default) + +- Swiss → `shadow-none` 90% of the time. `shadow-soft` only on screenshots that float on `bg-grid` / `bg-dot`. `shadow-ed` adds a `1px` outline as part of the shadow — reserve for hero shots. +- Editorial → `shadow-soft` on `bg-paper-2` is the warm default. `shadow-ed` for hero shots. + +### 4. `bg-*` (the screenshot "stage") + +| Token | Swiss role | Editorial role | +| ------------ | ------------------- | ---------------------- | +| `bg-paper` | Default plain stage | Same | +| `bg-paper-2` | n/a | Default warm stage | +| `bg-grey-1` | Default plain stage | n/a | +| `bg-grid` | Engineering / data | Field-notes engineering | +| `bg-dot` | Subtle structure | Subtle structure | +| `bg-ink` | Dark-mode UI shot | Dark-mode UI shot | + +Backgrounds are **never** accent-coloured. If the screenshot needs an accent emphasis, add a `.t-cat` chip or `.kicker` next to it — don't tint the stage. + +### 5. `inset-*` (padding between shot and stage) + +- `inset-none` — image fills the frame. Use when the screenshot itself already has window chrome. +- `inset-sub` (Swiss 20 px / Editorial 24 px) — default. Lets the stage breathe. +- `inset-bal` (Swiss 48 px / Editorial 56 px) — when the shot is busy and needs to feel calm. + +### 6. `fit-cover` (override) + +Default is `object-fit: contain` — this is the whole point of `.frame-shot`. **Only** add `.fit-cover` when: +- The slot is a hero where exact pixels of the source don't matter (e.g. a code shot used as a background pattern). +- The user explicitly says they want the shot cropped. + +## Device chrome + +Two wrapper classes ship with the seeds. They wrap a `.frame-shot`: + +### `.device-browser` + +Adds a 32–36 px chrome bar with traffic-light dots. Use for web / desktop app captures. + +```html +
+
+ Linear inbox +
+
+``` + +Pair with `shadow-soft` on the screenshot itself for a desk-photo feel. + +### `.device-phone` + +Wraps a 3:4 or 16:10 shot in an ink-coloured bezel with 18-24 px rounded inner corners. Use for mobile captures. + +```html +
+
+ WeChat detail +
+
+``` + +Don't stack `corners-md` on top of `.device-phone` — the bezel already rounds the inner shot. + +## Safe-area cropping + +When the user delivers a full-screen capture (1290×2796 iOS / 1080×2400 Android / 1920×1080 desktop), do **not** show the status bar, dock, or browser tab strip unless that chrome is the subject. Crop before importing: + +1. Trim the iOS / Android status bar (top 47-59 px on retina). +2. Trim the home indicator / nav gesture bar (bottom 34 px on iOS). +3. For desktop: trim everything above the page content unless wrapped in `.device-browser`. + +If you can't crop ahead of time, use `object-position: center 6%` to bias the visible region downward — but this is a workaround, not the preferred path. + +## Style cheat-sheet + +Two recipes that cover 80% of cases. + +**Swiss product demo** — pure stage, no shadow: +``` +.frame-shot.r-16x10.corners-sq.shadow-none.bg-grey-1.inset-bal +``` + +**Editorial deep-dive** — desk-photo warmth: +``` +.frame-shot.r-16x10.corners-sm.shadow-soft.bg-paper-2.inset-sub +``` + +For comparison shots (before / after), use **the same parameters** on both — different treatment between cells reads as inconsistency, not contrast. + +## Validator + +`validate-social-deck.mjs` doesn't have a screenshot-specific rule (yet). The existing R1 / R2 / R5 still apply: a `.frame-shot` that overflows or pushes the footer is still a fail. + +If a deck mixes `.frame-img` and `.frame-shot` on the same poster, that's usually a smell — pick one approach per poster. diff --git a/references/style-system.md b/references/style-system.md new file mode 100644 index 0000000..3b4664c --- /dev/null +++ b/references/style-system.md @@ -0,0 +1,188 @@ +# Style System + +This skill extracts Guizang PPT visual principles for static social images. It does not depend on the original PPT templates. + +## Shared Rules + +- Content shape decides layout. Do not pick a pretty layout first and invent content to fit it. +- Use strong hierarchy: title, hook, evidence, caption, metadata. +- Use real images as evidence or atmosphere, not decoration. +- Avoid visible clutter: no random SVG circles, oval drops, blobs, bokeh, ornamental stickers, fake diagram decorations, or decorative gradients. +- Keep all pages in a package visually related through grid, typography, palette, and recurring metadata. +- Every page should have a clear focal point. + +## Style ↔ content type are decoupled + +The two modes below are **visual stances, not content categories**. Any topic can be rendered in either mode — what changes is the page's feel and which structural devices are available (ledger / marginalia / pull quote vs matrix / KPI tower / h-bar). Pick by editorial intent ("feature story" vs "release note" / "system explainer"), not by topic lookup. The lists below are *good fits*, not exhaustive. + +## Mode A: Editorial Magazine x E-ink + +Good fits: outdoor, lifestyle, reflective, humanistic, cultural, narrative — but also workplace essays, AI think-pieces, product retrospectives, anything that wants a slow magazine-feature pace. + +Visual anchors: + +- Serif or Songti-like display title plus quiet sans-serif body text. +- Warm paper, deep ink, refined greys, restrained color. +- WebGL or procedural ink/contour atmosphere, especially on covers, dividers, pull quotes, and sparse pages. +- Documentary photography, field-note captions, issue labels, page numbers. +- Large but purposeful whitespace. +- Fine rules, magazine columns, image captions, pull quotes, and editorial contrast. +- Subtle paper texture is allowed; it must not lower text readability. + +Use one of the 6 magazine palettes in `theme-presets.md`. Do not improvise arbitrary warm paper colors. Five palettes are light (paper-and-ink); one — **Midnight Ink** — is the only sanctioned dark variant, reserved for content where the source imagery is already dark (game art, night photography, cinematic covers). Do not invent a second dark palette. + +**Typography stance — "the larger, the lighter."** This rule is non-negotiable for Editorial. Display titles run at **weight 500** with **wide tracking** (`+.03em` to `+.04em`). Small text (kicker, meta, label) is the *only* place where weight 500 is paired with mono uppercase and wide letter-spacing (`+.20em` to `+.22em`). Body uses **serif-zh**, not sans. The anti-pattern — 700 to 900 weight display, sans body, negative tracking — collapses Editorial into a generic "infographic banner" look. If a page feels heavy or shouty, the title is too bold or the body switched to sans. See `local-tests/demo-showcase/editorial.html` for source-of-truth. + +Layout patterns: + +- Cover: big title top or left, large photo/image evidence, bottom checklist or issue-strip. +- Feature page: one large image plus a narrow column of compressed explanation. +- Checklist page: editorial header, numbered items, one supporting image crop or icon-free diagram. +- Quote/takeaway page: large pull quote with small source/context row. +- Comparison page: two-column tension, simple labels, one visual anchor. + +Background rule: + +- A flat paper color is not enough for this mode. Use a layered background from `background-systems.md`: paper base, subtle grain, ink wash or contour field, and optional WebGL canvas. The background should create atmosphere while keeping text readable. + +## Mode B: Swiss International + +Good fits: AI tools, product updates, engineering workflows, software explainers, release notes, comparison posts — but also travel ledgers, expense logs, fitness data, recipe step counts, anything that wants a engineered/quantified pace. Topic is not the gate; visual stance is. + +Visual anchors: + +- Inter, Helvetica Neue, Noto Sans SC feeling. +- Very light display weights for huge type; stronger weights for small labels. +- Strict 12/16-column grid, left alignment, asymmetric whitespace. +- White/off-white, black, refined greys, exactly one high-saturation accent. +- Straight modules, hairline rules, no rounded cards, no shadows, no glassmorphism. + +Use one of the 4 Swiss accent palettes in `theme-presets.md`. One package gets exactly one accent. + +Rules: + +- Use one accent only. Do not mix blue, yellow, green, and orange. +- Avoid gradients. Use pure color blocks, hairline rules, and grid modules. +- Headings usually sit on the top-left content axis, not center, except special square covers or explicit poster compositions. +- Huge text should be lighter; small text should be heavier. +- Images are proof blocks. Use straight edges and stable aspect-ratio containers. + +Typography guidance: + +| Role | Size Guidance | Weight | +| --- | --- | --- | +| Main cover title | 84-128px on 1080x1440 | 200-400 | +| Page title | 52-82px | 300-500 | +| Body copy | 28-42px | 400-500 | +| Captions/meta | 20-28px | 500-650 | + +Chinese titles are visually dense. Shorten first, then reduce size. Do not solve overflow by shrinking body text below readability. + +## Image Rules + +- Photos: crop with intent. Keep faces, hands, products, and main objects in safe areas. +- Product screenshots: preserve text; use `object-fit:contain` when detail matters. +- Hardware photos: make the object a first-viewport signal. The photo should be large enough to inspect. +- Generated images: generate only the image content, not the final poster with text. +- Do not make generated images include page chrome, titles, borders, captions, or fake UI unless required by the concept. + +## Bad Smells + +Revise if you see: + +- A page that looks like a generic template with the article pasted in. +- Large empty lower area across multiple pages. +- Decorative shapes that do not explain anything. +- Screenshots too small to read. +- Inconsistent margins between pages. +- Blue text on black or dark photo with poor contrast. +- A 1:1 cover that is a cramped crop of a 21:9 cover. + +## Style Identity Test + +A poster compiles cleanly long before its style identity is right. Run this test on every page before delivering. + +### Swiss identity test + +A poster is Swiss only if **all four** hold: + +1. Every large display title (>=72px on 1080x1440) uses a typed class from `components.md` (`.h-hero` / `.h-statement` / `.h-xl` / `.num-mega`). Its computed `font-weight` is `<=300`. +2. No serif family is loaded in the document head. No element uses `font-family: serif` or `Noto Serif SC` / `Playfair Display`. +3. Section separators are hairline rules (1-2px) or grid gutters, not card borders + drop shadows. +4. Exactly one accent palette is in use. No mixed accents across pages. + +If any one of these fails, the poster is not Swiss — it is generic editorial or "Swiss-flavored card". Fix it or relabel the package. + +### Editorial identity test + +A poster is Editorial only if **all three** hold: + +1. The background has at least one atmosphere layer beyond a flat fill: paper grain, ink wash, frozen WebGL canvas, or contour field (see `background-systems.md`). A page-wide solid `#f3f0e8` with nothing on top is not Editorial. +2. The display title uses a serif-zh / serif-en family (`Noto Serif SC` / `Songti SC` / `Playfair Display`). +3. The poster contains at least one of: a large photo well, a serif pull-quote with em-italic English, a left/right marginalia column, or a ledger with real magazine row hierarchy. + +If a poster has a serif title, mono labels everywhere, structured pill grids, and a flat paper background — it is **Swiss-with-a-serif**, not Editorial. Either add atmosphere + magazine structure, or switch the package to Swiss honestly. + +## Anti-Patterns From Real Demos + +These all rendered without errors and looked plausible in isolation, but failed once compared against a clean reference. + +### Anti-pattern A: thick big title in Swiss + +```html + +

如果只能留 5 个,我留这些。

+ + +

如果只能
留 5 个,
我留这些。

+``` + +"The larger, the lighter" is a hard rule. A 90px h1 at weight 700+ instantly downgrades the design from Swiss International to generic landing-page editorial. Always reach for `.h-hero` / `.h-statement` / `.h-xl` / `.num-mega`. Do not bypass them with inline `font-size` + `font-weight`. + +### Anti-pattern B: Editorial without atmosphere + +A page with all of the following looks Swiss-in-disguise even if you intended Editorial: + +- Single flat paper color, no grain, no ink wash, no WebGL, +- Mono labels (`IBM Plex Mono uppercase`) on every kicker, foot, pill, and row label, +- A serif headline floating alone with no large image, pull quote, or marginalia column. + +Fix by **one** of: + +- Add the paper grain + ink-wash background per `background-systems.md`. +- Replace mono labels with serif / italic micro copy. +- Introduce a large photo or serif pull quote with column structure. +- Switch the package to Swiss honestly — the content might want Swiss. + +### Anti-pattern C: footer overlap (closing essay collides with `.foot`) + +When `.foot` or `.issue-strip` is positioned with `position: absolute; bottom: ...`, any content above that grows past its expected height crashes through the footer. Use one of these safe patterns instead: + +```css +/* Pattern A — flex column. Foot is the last child, pushed by margin-top: auto. */ +.poster .pad { display: flex; flex-direction: column; height: 100%; } +.poster .foot { margin-top: auto; } + +/* Pattern B — grid with fixed footer row. */ +.poster .pad { display: grid; grid-template-rows: auto 1fr auto; height: 100%; } +``` + +Both seed templates already use Pattern A. Preserve it. If a recipe truly needs an absolutely-positioned foot, reserve `padding-bottom: ` on the content container. + +### Anti-pattern D: full-bleed photo with title crossing the subject + +The poster looks dramatic at 100% zoom and unreadable at thumbnail size. Two common subfailures: + +- A hero photo covers the entire poster, the title sits directly on whatever pixel happens to be behind it. The image's own brightness varies across the canvas, so the title is legible in some bands and invisible in others. +- A mask layer is present but only on one band (bottom), while the subject's face was pushed to the top by `object-position: center top`. The display title lands across the face. + +Both pass HTML linting; both fail the thumbnail readability test. + +Fix by following `image-overlay.md`: + +1. **Add a mask layer** appropriate to where text lands — vertical falloff (top+bottom), side scrim (one column), or radial vignette (text fills the canvas). +2. **Map the subject before placing the title.** Read the image, describe in plain language where the face/focal feature sits, record the subject map as an HTML comment next to the hero block. Place text only in documented safe zones. +3. **Match `object-position` to subject location** — see the table in `image-overlay.md`. Never leave it at default for face / hero shots. +4. **Thumbnail test** — render the PNG, downscale to 360 px wide, and look at the title. If it fights the photo, the mask is too thin; if the photo looks dead, the mask is too heavy. + +This anti-pattern survives almost every other check because the HTML is valid, the image loads, and the title renders. The only way to catch it is to look at the rendered output at the size readers will actually see it. diff --git a/references/theme-presets.md b/references/theme-presets.md new file mode 100644 index 0000000..4e31c88 --- /dev/null +++ b/references/theme-presets.md @@ -0,0 +1,208 @@ +# Theme Presets + +Use one theme for one image package. Do not mix palettes across pages unless the user explicitly asks for a deliberate multi-chapter system. + +## Editorial Magazine x E-ink Palettes + +These are adapted from the Guizang PPT electronic-magazine mode for static Rednote and WeChat images. + +### Ink Classic + +Use for business commentary, AI essays, product thinking, and neutral editorial posts. + +```css +:root { + --paper: #f3f0e8; + --paper-2: #ebe6da; + --ink: #0a0a0b; + --muted: #68625a; + --line: rgba(10,10,11,.22); + --accent: #111111; + --accent-soft: #d8d2c6; +} +``` + +### Indigo Porcelain + +Use for technology, research, data, AI infrastructure, and calm analytical writing. + +```css +:root { + --paper: #f2f4f5; + --paper-2: #e5ebef; + --ink: #0a1f3d; + --muted: #5f6d78; + --line: rgba(10,31,61,.20); + --accent: #315d93; + --accent-soft: #d7e1ec; +} +``` + +### Forest Ink + +Use for hiking, outdoor, nature, sustainability, personal field notes, and grounded lifestyle posts. + +```css +:root { + --paper: #f5f1e8; + --paper-2: #e8dfcf; + --ink: #16251b; + --muted: #5d665d; + --line: rgba(22,37,27,.22); + --accent: #2e6b4f; + --accent-soft: #d4dfd2; +} +``` + +### Kraft Paper + +Use for memory, craft, personal essays, old objects, creator notes, and warm low-tech topics. + +```css +:root { + --paper: #eedfc7; + --paper-2: #dfc9a8; + --ink: #2a1e13; + --muted: #755f49; + --line: rgba(42,30,19,.24); + --accent: #9b5a2e; + --accent-soft: #d5b58f; +} +``` + +### Dune + +Use for design, object studies, portfolio-like covers, gallery tone, and restrained aesthetic posts. + +```css +:root { + --paper: #f0e6d2; + --paper-2: #ded0b7; + --ink: #1f1a14; + --muted: #6f6557; + --line: rgba(31,26,20,.22); + --accent: #8f7650; + --accent-soft: #d4c2a4; +} +``` + +### Midnight Ink + +The **only** official dark Editorial palette. Use for game key art, night photography, cinematic covers, dark-themed cultural pieces — content whose source imagery is already dark and would be diminished by paper backgrounds. Do not improvise other dark palettes; if Midnight Ink does not fit, pick a different mode (Editorial dark is not a universal switch). + +```css +:root { + --paper: #0e0d0c; + --paper-2: #1a1714; + --ink: #ece2cf; + --muted: #9a8c75; + --line: rgba(236,226,207,.22); + --accent: #d4a04a; + --accent-soft: #3a2a14; +} +``` + +Midnight Ink **must** also override two background layers — light-paper math does not carry over: + +```css +[data-theme="midnight-ink"] .grain { + opacity: .26; + mix-blend-mode: screen; + background-image: radial-gradient(rgba(255,244,214,.10) 1px, transparent 1px); +} +[data-theme="midnight-ink"] .paper-wash { + background: + radial-gradient(80% 50% at 28% 16%, rgba(212,160,74,.12), transparent 64%), + radial-gradient(70% 60% at 80% 86%, rgba(60,40,20,.20), transparent 72%), + linear-gradient(180deg, rgba(236,226,207,.02), rgba(0,0,0,.32)); +} +[data-theme="midnight-ink"] .frame-img { + background: #18120f; + box-shadow: 0 0 0 1px rgba(236,226,207,.10); +} +``` + +The seed `template-editorial-card.html` ships these overrides — just switch `data-theme` and they apply automatically. + +Magazine palette rules: + +- Use `--paper` as the main background and `--ink` as primary type. +- Use `--accent` sparingly: section marker, page number, pull quote rule, or one highlighted phrase. +- `--paper-2` can support photo wells, issue strips, or checklist bands. +- Light palettes (the first five): do not turn into beige-on-beige. Maintain real contrast. +- Midnight Ink: do not stack opaque cards or fills on the page. Dark Editorial relies on photo bleeds + warm gilt accent for hierarchy, not background blocks. + +## Swiss International Palettes + +These are adapted from the Guizang PPT Swiss mode. + +### IKB Blue + +Default for AI, technology, product updates, design, and engineering topics. + +```css +:root { + --paper: #fafaf8; + --ink: #0a0a0a; + --grey-1: #f0f0ee; + --grey-2: #d4d4d2; + --grey-3: #737373; + --accent: #002FA7; + --accent-on: #ffffff; +} +``` + +### Lemon Yellow + +Use for young, consumer, active, retail, sporty, or playful information. + +```css +:root { + --paper: #fafaf8; + --ink: #0a0a0a; + --grey-1: #f0f0ee; + --grey-2: #d4d4d2; + --grey-3: #737373; + --accent: #FFD500; + --accent-on: #0a0a0a; +} +``` + +### Lemon Green + +Use for ecology, future, emerging tech, health, and highlighter-like contemporary topics. + +```css +:root { + --paper: #fafaf8; + --ink: #0a0a0a; + --grey-1: #f0f0ee; + --grey-2: #d4d4d2; + --grey-3: #737373; + --accent: #C5E803; + --accent-on: #0a0a0a; +} +``` + +### Safety Orange + +Use for industrial, warning, urgency, risk, decision points, and sharp corrections. + +```css +:root { + --paper: #fafaf8; + --ink: #0a0a0a; + --grey-1: #f0f0ee; + --grey-2: #d4d4d2; + --grey-3: #737373; + --accent: #FF6B35; + --accent-on: #ffffff; +} +``` + +Swiss palette rules: + +- Use exactly one `--accent`. +- Do not use gradients, shadows, glass, or mixed accent colors. +- If the accent is yellow or green, text on accent must use `--accent-on: #0a0a0a`. +- Prefer pure blocks, hairline rules, and grid rhythm. diff --git a/references/title-shortener.md b/references/title-shortener.md new file mode 100644 index 0000000..ee3be33 --- /dev/null +++ b/references/title-shortener.md @@ -0,0 +1,128 @@ +# Title Shortener (1:1 covers + cross-platform reuse) + +A long title that works in a 3:4 XHS card or a 21:9 WeChat cover **does not** work in a 1:1 square. Square covers need a different sentence — shorter, blunter, with one idea hitting the viewer in the first glance. This file is the playbook for generating that second title. + +It also applies whenever you reuse one piece of content across multiple platforms: XHS carousel + WeChat cover pair + Lvshu single — each surface has a different attention budget. + +## Why a separate title is needed + +Tested empirically across the smoke decks: + +| Source title (3:4 / 21:9) | Squeezed into 1:1 verbatim | Authored short title | +| -------------------------------------------------------- | ---------------------------------- | ----------------------- | +| 第三次进山,装备比上一次轻 3.4kg | overflows at 88px, fights bleed | 装备减重 3.4kg | +| 开源了一个 Skill,让 AI 接管你屏幕边那张便签纸 | 3-line wrap, looks like a paragraph | AI 接管便签纸 | +| 当代年轻人,不为了变美,只为了不丑 | 4-line wrap on `.poster.square` | 不为变美,只为不丑 | +| 我把家从北京搬到大理后,第一次安静地写完了一本书 | overflows even at 80px | 北京到大理,写完一本书 | + +The squeezed version reads as a wall of text. The authored short title reads as a poster headline. + +## The 5-step extraction + +When the user gives you a long title (or you wrote one for the 3:4 cover), derive the 1:1 like this: + +**1. Identify the core verb.** Every good poster headline has one. `接管` / `减重` / `搬到` / `写完`. Drop modifiers, drop preambles, drop adjectival noise. + +**2. Identify the core object.** What is the verb acting on? `便签纸` / `装备` / `家` / `书`. Skip qualifiers (`你屏幕边那张` → `便签纸`). + +**3. Compress to 4-10 Chinese characters.** That is the readable band for 88-120px type on a 1080×1080 canvas. Outside that band, you either need 2 lines (still OK), 3+ lines (almost certainly wrong), or a typographic stunt (one giant character + meta — see Big Word recipe below). + +**4. Drop English unless it carries the verb.** Mixed CJK + Latin at large size is a Swiss/Editorial trick that takes a lot of practice. For 1:1 short titles, prefer pure Chinese. Exceptions: brand names (`AI`, `Mac`, `iOS`), domain terms with no Chinese equivalent (`PPT`, `MCP`). + +**5. Add a small subtitle only if needed for disambiguation.** Most 1:1 covers don't need one. If you must add it, use `.kicker` or `.t-meta` size (20-22px mono), never a second `.h-xl`. + +## Patterns that work for 1:1 covers + +### A · Verb + Object (single line, 4-8 chars) + +``` +装备减重 3.4kg +AI 接管便签纸 +家搬到大理 +眼线一笔到位 +``` + +Use when the verb is concrete and the object is short. The headline does the work alone — no subtitle. + +### B · Two-clause cut (comma between, 2 lines) + +``` +不为变美, +只为不丑 +``` + +``` +六个停靠点, +两天走完 +``` + +Use when the title needs a contrast or a setup-payoff. Hard-break at the comma; don't let CJK auto-wrap pick the line. Each line should be ≤ 6 characters. + +### C · Big Word (one giant character + meta) + +```html +

家

+

从北京搬到大理

+``` + +A single character can carry a cover if it's the conceptual anchor of the piece. Pair it with a 1-line meta in mono. Don't try this with 2 characters — it always looks worse than either 1 or 4+. + +### D · Number-led headline + +``` +↓ 3.4kg +装备减重 +``` + +``` +1+1=3 +共创笔记 +``` + +When the numerical fact is the news, lead with it. Editorial uses `.num-mega` (200px) over an 56px serif subtitle; Swiss uses `.num-mega` (200px) over a 22px mono cat. + +## Anti-patterns + +- **Squeezing the 21:9 title.** Cover the urge to use the same exact sentence. The square needs a separate sentence. +- **Adding "如何"/"怎样"/"为什么" preambles.** Question-form titles are too long for 1:1. Use a declarative form: `怎样减重装备` → `装备减重 3.4kg`. +- **Three-line titles.** If your draft wraps to 3 lines on `.poster.square`, shorten the copy — never shrink `.h-hero` below 152px. Three lines collapses into a paragraph. +- **Adding a long subtitle.** A 1:1 cover with a 2-line subtitle reads as a flyer. If the subtitle is essential, you probably picked the wrong recipe — switch to a `.poster.xhs` 3:4 instead. +- **English-only titles when the content is Chinese.** Looks like stock template. Mix only when the English carries the actual headline (e.g. brand launch). + +## Sizing on `.poster.square` (1080×1080) + +Type scale defaults on square boards: + +| Style | Class | Default size | Use | +| --------- | ----------- | ------------ | ---------------------------------- | +| Editorial | `.h-display`| 110px | Pattern A or B, 1-2 lines | +| Editorial | `.h-xl` | 78px | Subtitle or secondary headline | +| Swiss | `.h-hero` | 200px | Pattern A or B, 1-2 lines | +| Swiss | `.h-statement` | 160px | Same role as `.h-hero`, slightly tamer | +| Both | `.num-mega` | 200px | Pattern D leading number | + +If a single-line short title is ≤ 4 Chinese chars on Swiss square, you can push `.h-hero` to 240px without overflow. + +## Cross-platform pairing + +When the same content runs as XHS carousel + WeChat cover pair + Lvshu single: + +| Surface | Title source | Length band | +| -------------------- | ------------------------------------------- | ------------------------ | +| XHS p1 cover (3:4) | Authored long title (12-30 chars) | 1-2 lines, ≤ 9 chars/line| +| WeChat 21:9 main | Authored long title or 80% of it | 1 line, ≤ 14 chars | +| WeChat 1:1 square | Shortened per this file (4-10 chars) | 1-2 lines, ≤ 6 chars/line| +| Lvshu single (3:4) | Same as XHS p1 or shortened to 8-12 chars | 1-2 lines | + +The 1:1 short title is the **shortest** of the family. If you cannot shorten further without losing meaning, the content probably needs a different framing — talk to the user. + +## Workflow integration + +When a user task includes "公众号封面" or "WeChat cover", you should: + +1. Write the long title for 21:9 first. +2. Apply this file's 5-step extraction to derive the 1:1 short title. +3. Print both in the planning notes before rendering. +4. Render both in the same HTML file, plus a `.pair-preview` section showing them side-by-side. + +If the user supplies only one title and asks for the pair, derive the short one yourself and call it out in the response (don't silently swap text — the user needs to confirm the shortening reads right). diff --git a/validate-social-deck.mjs b/validate-social-deck.mjs new file mode 100644 index 0000000..f0e7de7 --- /dev/null +++ b/validate-social-deck.mjs @@ -0,0 +1,336 @@ +#!/usr/bin/env node +/* + * validate-social-deck.mjs + * + * node validate-social-deck.mjs + * + * Checks each
in the target HTML against the rules + * codified in SKILL.md / qa-checklist.md / components.md. Exits 1 if any FAIL. + * + * Rules implemented: + * R1 overflow scrollHeight > clientHeight on the poster + * R2 footer collision .foot is position:absolute AND content above reaches into its band + * R3 swiss bold display .h-xl / .h-hero / .h-statement / .num-mega with computed weight >= 600 + * R4 min readable font body/lead/caption/label/meta below the mobile-safe floor + * R5 4-band density on 3:4 boards: <75% filled OR any under-filled band > 216px + * R6 h-xl hard cap display title lines/chars exceed the per-board cap + */ +import { chromium } from "playwright"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; +import fs from "node:fs"; + +const args = process.argv.slice(2); +let target = null; +let styleOverride = null; +for (const a of args) { + if (a.startsWith("--style=")) styleOverride = a.slice(8); + else if (!target) target = a; +} +if (!target) { + console.error("usage: node validate-social-deck.mjs [--style=swiss|editorial]"); + process.exit(2); +} +const abs = path.resolve(target); +let htmlPath = abs; +if (fs.statSync(abs).isDirectory()) { + htmlPath = path.join(abs, "index.html"); +} +if (!fs.existsSync(htmlPath)) { + console.error(`not found: ${htmlPath}`); + process.exit(2); +} + +const url = "file://" + htmlPath; +const browser = await chromium.launch({ + args: ["--use-angle=swiftshader", "--enable-unsafe-swiftshader"], +}); +const ctx = await browser.newContext({ + viewport: { width: 1400, height: 1700 }, + deviceScaleFactor: 1, +}); +const page = await ctx.newPage(); +await page.goto(url, { waitUntil: "networkidle" }); +await page.waitForTimeout(1200); + +const style = styleOverride || await page.evaluate(() => { + const html = document.documentElement; + if (html.dataset.theme) return "editorial"; + if (html.dataset.accent) return "swiss"; + // Fallback: any display class using a serif family = editorial; otherwise swiss. + // Note: "sans-serif" generic name also contains the substring "serif", so we + // match on serif-only families by token, not substring. + const SERIF_TOKENS = ["noto serif", "playfair", "source serif", "songti", "stsong", "simsun", "serif sc", "kinfolk", "merriweather"]; + for (const n of document.querySelectorAll(".h-display, .h-xl, .h-hero, .pullquote, .h-statement")) { + const ff = getComputedStyle(n).fontFamily.toLowerCase(); + if (SERIF_TOKENS.some(t => ff.includes(t))) return "editorial"; + } + return "swiss"; +}); + +const sections = await page.$$("section.poster"); +const report = []; + +// "Obviously too small" floors — below these, text becomes unreadable at phone size. +// Seeds use 26-28px body, 30-32px lead — those are fine. Anything 4+px below is suspect. +const MIN_FONT = { + body: 22, + lead: 26, + caption: 18, + meta: 18, + cellTitle: 20, + numAnnotation: 20, +}; + +const HXL_CAPS = { + xhs: { maxLines: 2, maxChars: 8 }, + square: { maxLines: 2, maxChars: 7 }, + wide: { maxLines: 1, maxChars: 14 }, +}; + +const DISPLAY_CLASSES = ["h-xl", "h-hero", "h-statement", "h-display", "num-mega", "num-xl"]; + +for (const s of sections) { + const meta = await s.evaluate(el => { + const w = el.clientWidth, h = el.clientHeight; + let board = el.classList.contains("xhs") ? "xhs" + : el.classList.contains("square") ? "square" + : el.classList.contains("wide") ? "wide" : null; + if (!board) { + const ratio = w / h; + if (Math.abs(ratio - 0.75) < 0.02) board = "xhs"; // 3:4 + else if (Math.abs(ratio - 1.0) < 0.02) board = "square"; // 1:1 + else if (Math.abs(ratio - 7 / 3) < 0.05) board = "wide"; // 21:9 + else board = "unknown"; + } + return { id: el.id || "(no-id)", dataId: el.dataset.id || "", board, clientH: h, scrollH: el.scrollHeight, clientW: w }; + }); + const fails = []; + const warns = []; + + // R1 overflow + const overflow = meta.scrollH - meta.clientH; + if (overflow > 4) { + fails.push({ rule: "R1", msg: `overflow ${overflow}px (scrollH ${meta.scrollH} > clientH ${meta.clientH})`, fix: "tighten content or switch to a higher-capacity recipe" }); + } + + // R2 footer collision — only flag leaf text or media nodes, not containers + // whose bbox merely shares y-space with an absolute-positioned strip. + const footIssue = await s.evaluate(el => { + const foot = el.querySelector(".foot, .issue-strip, .magazine-foot"); + if (!foot) return null; + const cs = getComputedStyle(foot); + if (cs.position !== "absolute") return null; + const footTop = foot.offsetTop; + const er = el.getBoundingClientRect(); + const hasOwnText = n => { + for (const c of n.childNodes) { + if (c.nodeType === 3 && c.textContent.trim().length > 0) return true; + } + return false; + }; + const isMedia = n => n.tagName === "IMG" || n.tagName === "CANVAS" || n.tagName === "SVG" || n.tagName === "FIGURE"; + let worstOverlap = 0; + let worstSel = ""; + const posterArea = el.clientWidth * el.clientHeight; + for (const node of el.querySelectorAll("*")) { + if (node === foot || foot.contains(node)) continue; + if (!hasOwnText(node) && !isMedia(node)) continue; + const r = node.getBoundingClientRect(); + // Skip full-bleed background layers (>=95% of poster area + position:absolute). + const ncs = getComputedStyle(node); + if (ncs.position === "absolute" && r.width * r.height >= posterArea * 0.95) continue; + const bottom = r.bottom - er.top; + if (bottom > footTop + 2) { + const overlap = bottom - footTop; + if (overlap > worstOverlap) { + worstOverlap = overlap; + worstSel = node.className ? "." + node.className.split(" ").filter(Boolean).join(".") : node.tagName.toLowerCase(); + } + } + } + return worstOverlap > 6 ? { overlap: Math.round(worstOverlap), sel: worstSel, footTop } : null; + }); + if (footIssue) { + fails.push({ rule: "R2", msg: `.foot is position:absolute, ${footIssue.sel} extends ${footIssue.overlap}px past foot top`, fix: "switch .foot to flex margin-top:auto (see style-system.md Anti-pattern C)" }); + } + + // R3 swiss bold display + if (style === "swiss") { + const bolds = await s.evaluate((el, cls) => { + const out = []; + for (const c of cls) { + for (const n of el.querySelectorAll("." + c)) { + const cs = getComputedStyle(n); + const w = parseInt(cs.fontWeight, 10); + const size = parseFloat(cs.fontSize); + if (size >= 72 && w >= 600) { + out.push({ cls: c, weight: w, size: Math.round(size), text: n.textContent.trim().slice(0, 20) }); + } + } + } + return out; + }, DISPLAY_CLASSES); + for (const b of bolds) { + fails.push({ rule: "R3", msg: `.${b.cls} "${b.text}" is ${b.size}px @ weight ${b.weight} — Swiss "larger = lighter" hard rule`, fix: "remove inline font-weight; use the typed class default (200-300)" }); + } + } + + // R4 min readable font + const textChecks = await s.evaluate((el, MIN) => { + const out = []; + const seen = new Set(); + const test = (selector, role, min, requireLeaf = true) => { + for (const n of el.querySelectorAll(selector)) { + if (seen.has(n)) continue; + seen.add(n); + // Skip containers — typography roles only apply to leaf text nodes. + // A real `.lead` is a

; `.role-card.lead` is a container with children. + if (requireLeaf && n.children.length > 0) continue; + // Skip decorative chips inside map pins — labels are sized to fit on the map by design. + if (n.closest(".map-pin .card")) continue; + const cs = getComputedStyle(n); + const size = parseFloat(cs.fontSize); + const text = n.textContent.trim(); + if (!text) continue; + if (size > 0 && size < min) { + out.push({ role, selector, min, size: Math.round(size), text: text.slice(0, 30) }); + } + } + }; + test(".body, p.body", "body", MIN.body); + test(".lead", "lead", MIN.lead); + test(".kicker, .cap, .caption, .swiss-img-caption, .h-sub", "caption", MIN.caption); + test(".meta, .label, .mono", "meta", MIN.meta); + test(".matrix-fill .cell-title, .brief-card .title, .char-grid .name", "cellTitle", MIN.cellTitle); + test(".stat-card .lbl, .ledger .sub, .num .sub", "numAnnotation", MIN.numAnnotation); + return out; + }, MIN_FONT); + for (const t of textChecks) { + warns.push({ rule: "R4", msg: `${t.role} "${t.text}" at ${t.size}px < ${t.min}px floor`, fix: "cut copy instead of shrinking type (components.md Minimum Readable Sizes)" }); + } + + // R5 4-band density (3:4 only) + if (meta.board === "xhs") { + const bands = await s.evaluate(el => { + const er = el.getBoundingClientRect(); + const H = el.clientHeight; + // Pixel-row occupancy bitmap. Mark any row covered by a content element. + const rows = new Uint8Array(H); + const hasDirectText = n => { + for (const c of n.childNodes) { + if (c.nodeType === 3 && c.textContent.trim().length > 0) return true; + } + return false; + }; + for (const n of el.querySelectorAll("*")) { + const r = n.getBoundingClientRect(); + if (r.width < 8 || r.height < 8) continue; + const tag = n.tagName; + const cs = getComputedStyle(n); + const isText = hasDirectText(n); + const isImg = tag === "IMG" || tag === "CANVAS" || tag === "SVG" + || (cs.backgroundImage && cs.backgroundImage !== "none"); + const isRule = tag === "HR" || (parseFloat(cs.borderTopWidth) >= 1 && r.height < 4); + const hasFill = cs.backgroundColor && !cs.backgroundColor.match(/rgba?\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0?\s*\)/) && cs.backgroundColor !== "transparent"; + if (!isText && !isImg && !isRule && !hasFill) continue; + const top = Math.max(0, Math.floor(r.top - er.top)); + const bot = Math.min(H, Math.ceil(r.bottom - er.top)); + for (let y = top; y < bot; y++) rows[y] = 1; + } + const BAND = H / 4; + const occ = [0, 0, 0, 0]; + for (let i = 0; i < 4; i++) { + let count = 0; + const bTop = Math.floor(i * BAND), bBot = Math.floor((i + 1) * BAND); + for (let y = bTop; y < bBot; y++) count += rows[y]; + occ[i] = count / (bBot - bTop); + } + return { H, BAND, occ }; + }); + const total = bands.occ.reduce((a, b) => a + b, 0) / 4; + const pct = (o) => Math.round(o * 100) + "%"; + if (total < 0.745) { + warns.push({ rule: "R5", msg: `density ${pct(total)} (bands ${bands.occ.map(pct).join(" / ")})`, fix: "expand copy or switch recipe — see qa-checklist.md 4-band density" }); + } + for (let i = 0; i < 3; i++) { + if (bands.occ[i] < 0.15 && bands.occ[i + 1] < 0.15) { + warns.push({ rule: "R5", msg: `bands ${i + 1}+${i + 2} both under-filled (${pct(bands.occ[i])} / ${pct(bands.occ[i + 1])}) — >25% void mid-poster`, fix: "expand body content or insert a marginalia column" }); + break; + } + } + } + + // R6 h-xl hard cap + const cap = HXL_CAPS[meta.board]; + if (cap) { + const titles = await s.evaluate(el => { + const out = []; + for (const n of el.querySelectorAll(".h-xl, .h-hero, .h-display, .h-statement")) { + const cs = getComputedStyle(n); + const size = parseFloat(cs.fontSize); + const lineH = parseFloat(cs.lineHeight) || size * 1.2; + const lines = Math.round(n.getBoundingClientRect().height / lineH); + out.push({ cls: n.className.split(" ")[0], text: n.textContent.trim(), lines, size: Math.round(size) }); + } + return out; + }); + for (const t of titles) { + const longestLine = t.text.split(/\s+/).reduce((m, w) => Math.max(m, w.length), t.text.length); + if (t.lines > cap.maxLines) { + warns.push({ rule: "R6", msg: `.${t.cls} "${t.text}" renders ${t.lines} lines (cap ${cap.maxLines} on ${meta.board})`, fix: "switch to S01/S05 cover recipes that allow taller titles, or trim copy" }); + } else if (longestLine > cap.maxChars + 2) { + // soft warn only — wraps naturally + } + } + } + + report.push({ meta, fails, warns }); +} + +await browser.close(); + +let totalFail = 0, totalWarn = 0; +const ruleCounts = new Map(); +for (const { fails, warns } of report) { + totalFail += fails.length; + totalWarn += warns.length; + for (const v of [...fails, ...warns]) ruleCounts.set(v.rule, (ruleCounts.get(v.rule) || 0) + 1); +} +const cleanCount = report.filter(r => r.fails.length === 0 && r.warns.length === 0).length; + +const lines = []; +lines.push(`==== validate-social-deck ====`); +lines.push(`target: ${path.relative(process.cwd(), htmlPath)}`); +lines.push(`style: ${style}`); +lines.push(`sections: ${report.length} · ${cleanCount} clean · ${totalFail} fails · ${totalWarn} warns`); +if (ruleCounts.size > 0) { + const ruleSummary = [...ruleCounts.entries()].sort().map(([r, c]) => `${r}=${c}`).join(" "); + lines.push(`rules: ${ruleSummary}`); +} +lines.push(""); + +const fixCache = new Map(); +for (const { meta, fails, warns } of report) { + if (fails.length === 0 && warns.length === 0) { + lines.push(`[PASS] ${meta.id} ${meta.dataId ? ` · ${meta.dataId}` : ""} · ${meta.board}`); + continue; + } + const tag = fails.length ? "[FAIL]" : "[WARN]"; + lines.push(`${tag} ${meta.id} ${meta.dataId ? ` · ${meta.dataId}` : ""} · ${meta.board}`); + for (const v of [...fails, ...warns]) { + const sev = fails.includes(v) ? "FAIL" : "WARN"; + lines.push(` ${sev} · ${v.rule} ${v.msg}`); + if (!fixCache.has(v.rule)) { + lines.push(` fix: ${v.fix}`); + fixCache.set(v.rule, true); + } + } +} + +lines.push(""); +lines.push(`Legend: R1 overflow · R2 footer collision · R3 swiss bold display · R4 min font · R5 4-band density · R6 h-xl cap`); +lines.push(`Exit code 1 only on FAIL. Warnings are advisory.`); + +console.log(lines.join("\n")); +process.exit(totalFail > 0 ? 1 : 0);