Sync third-party and MCP marketplace plugins

Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources.
Confidence: high
Scope-risk: narrow
Directive: Keep private/internal skills out of the public marketplace and preserve normal incremental market Git history.
Tested: Marketplace validation passed.
This commit is contained in:
KeyInfo Bot committed 2026-10-31 00:02:59 +08:00
1 parent b397f552f0
commit bd2c873b8a
30 files changed
+3086 -915

No files matched your search

+10 -10
View File
@@ -6,8 +6,8 @@
"repo": "https://github.com/Nutlope/hallmark.git", "repo": "https://github.com/Nutlope/hallmark.git",
"ref": "main", "ref": "main",
"adapter": "claude-skill", "adapter": "claude-skill",
"commit": "8f6969f8922caabde9de1784959eae11ddf68073", "commit": "4f6a46c6dc325d40ab268c93e25c7b96676ebccd",
"syncedAt": "2026-10-29T15:25:46Z" "syncedAt": "2026-10-30T16:00:00Z"
}, },
{ {
"id": "docx", "id": "docx",
@@ -150,8 +150,8 @@
"repo": "https://github.com/shadcn-ui/ui.git", "repo": "https://github.com/shadcn-ui/ui.git",
"ref": "main", "ref": "main",
"adapter": "claude-skill", "adapter": "claude-skill",
"commit": "96da9cc7f1aa10580a3741664e2a77c721206c00", "commit": "b8d7baa15b1dca5716f13153e63795fc3d741f6b",
"syncedAt": "2026-10-28T01:57:49Z" "syncedAt": "2026-10-30T16:00:00Z"
}, },
{ {
"id": "frontend-slides", "id": "frontend-slides",
@@ -168,8 +168,8 @@
"repo": "https://github.com/op7418/guizang-ppt-skill.git", "repo": "https://github.com/op7418/guizang-ppt-skill.git",
"ref": "main", "ref": "main",
"adapter": "claude-skill", "adapter": "claude-skill",
"commit": "c91369c449d34755d320a8b81d0734000d99d1ab", "commit": "93d21bf4a73a851907a6eb00acbf30618236667a",
"syncedAt": "2026-08-08T16:00:01Z" "syncedAt": "2026-10-30T16:00:00Z"
}, },
{ {
"id": "html-ppt", "id": "html-ppt",
@@ -186,8 +186,8 @@
"repo": "https://github.com/hugohe3/ppt-master.git", "repo": "https://github.com/hugohe3/ppt-master.git",
"ref": "main", "ref": "main",
"adapter": "claude-skill", "adapter": "claude-skill",
"commit": "15baa7c570f12c78cd9ac6ead897724571e32e87", "commit": "5e8856be0da8e6bc85c4aa28acc614478f7b7137",
"syncedAt": "2026-10-29T16:00:00Z" "syncedAt": "2026-10-30T16:00:00Z"
}, },
{ {
"id": "grill-me", "id": "grill-me",
@@ -267,8 +267,8 @@
"repo": "https://github.com/vercel/next.js.git", "repo": "https://github.com/vercel/next.js.git",
"ref": "canary", "ref": "canary",
"adapter": "skill-collection", "adapter": "skill-collection",
"commit": "79107ebd94e53c69865c1a49ac734ba8004491ba", "commit": "7571895249a22086367a12ddec90fae97bac067d",
"syncedAt": "2026-10-29T16:00:00Z" "syncedAt": "2026-10-30T16:00:00Z"
}, },
{ {
"id": "shuorenhua", "id": "shuorenhua",
@@ -86,11 +86,11 @@ Beyond Claude Code / Codex, you can also use Guizang PPT Skill on these platform
- 📐 **Horizontal swipe navigation**: ← → arrows / scroll wheel / touch swipe / bottom dots / ESC for index - 📐 **Horizontal swipe navigation**: ← → arrows / scroll wheel / touch swipe / bottom dots / ESC for index
- 🧩 **Style A 10 layouts**: cover, divider, big numbers, image/text, image grid, pipeline, comparison, and more - 🧩 **Style A 10 layouts**: cover, divider, big numbers, image/text, image grid, pipeline, comparison, and more
- 🧱 **Style B 22 locked layouts**: Cover, Statement, KPI Tower, Loop Diagram, Duo Compare, Image Hero, Closing Manifesto, and more - 🧱 **Style B 22 locked layouts**: Cover, Statement, KPI Tower, Loop Diagram, Duo Compare, Image Hero, Closing Manifesto, and more
- 🎨 **Curated theme presets**: 5 electronic-ink themes for Style A, 4 Swiss anchor-color themes for Style B - 🎨 **Curated theme presets**: 5 electronic-ink themes for Style A, 4 Swiss anchor-color themes for Style B; a brand color becomes a constrained brand theme generated with color science
- 🖼 **Optional Codex image flow**: generate documentary photos, infographics, flow diagrams, system maps, and UI scenes with GPT-Image 2.0 / GPT-M 2.0, then insert them at template-safe ratios - 🖼 **Optional Codex image flow**: generate documentary photos, infographics, flow diagrams, system maps, and UI scenes with GPT-Image 2.0 / GPT-M 2.0, then insert them at template-safe ratios
- 📰 **Social covers**: generate 21:9 WeChat cover images, 1:1 share cards, 3:4 Xiaohongshu covers, video thumbnails, and related variants - 📰 **Social covers**: generate 21:9 WeChat cover images, 1:1 share cards, 3:4 Xiaohongshu covers, video thumbnails, and related variants
- 🎤 **Presenter mode**: dual-window audience output, 16:9 current/next previews, speaker notes, timing and rehearsal, auto advance, laser pointer, circles, and live recovery controls - 🎤 **Presenter mode**: dual-window audience output, 16:9 current/next previews, speaker notes, timing and rehearsal, auto advance, laser pointer, circles, and live recovery controls
- 📴 **Low-power static mode**: press `B` to turn WebGL / canvas animation into static visuals - 📴 **Low-power static mode**: press `B` to turn WebGL / canvas animation into static visuals; on slow machines the background lowers its resolution automatically and freezes to a still frame if needed
- 📄 **Single HTML file** — no build, no server, open directly in the browser - 📄 **Single HTML file** — no build, no server, open directly in the browser
## Fits / Doesn't fit ## Fits / Doesn't fit
@@ -327,7 +327,7 @@ guizang-ppt-skill/
## Theme presets ## Theme presets
Pick from `references/themes.md`. **Custom hex values are not allowed** — protecting the aesthetic matters more than freedom of choice. Pick from `references/themes.md`. **Don't write hex values by hand** — protecting the aesthetic matters more than freedom of choice. Every theme's text, rule and small accent-text colors are derived with the same contrast rules (WCAG + APCA).
### Style A Editorial Themes ### Style A Editorial Themes
@@ -339,11 +339,13 @@ Pick from `references/themes.md`. **Custom hex values are not allowed** — prot
| <img src="https://github.com/user-attachments/assets/dfea080e-e916-417e-93cd-0a3628de84ca" width="260" alt="Kraft Paper theme preview"> | 🍂 **Kraft Paper** | `#2a1e13` / `#eedfc7`. Nostalgic, humanist, literary, indie zines. | | <img src="https://github.com/user-attachments/assets/dfea080e-e916-417e-93cd-0a3628de84ca" width="260" alt="Kraft Paper theme preview"> | 🍂 **Kraft Paper** | `#2a1e13` / `#eedfc7`. Nostalgic, humanist, literary, indie zines. |
| <img src="https://github.com/user-attachments/assets/f3705592-9a72-4dbc-9818-df3aea61bc75" width="260" alt="Dune theme preview"> | 🌙 **Dune** | `#1f1a14` / `#f0e6d2`. Art, design, creative, fashion, gallery-like decks. | | <img src="https://github.com/user-attachments/assets/f3705592-9a72-4dbc-9818-df3aea61bc75" width="260" alt="Dune theme preview"> | 🌙 **Dune** | `#1f1a14` / `#f0e6d2`. Art, design, creative, fashion, gallery-like decks. |
Switching themes only requires replacing the 6 variables at the top of `template.html`'s `:root{}` block — all other CSS flows through `var(--...)`. To switch themes, set the theme id on the template's first line, e.g. `<html data-theme="indigo-porcelain">`. All themes are built into the template.
When the user gives a brand or logo color, run `node scripts/gz-theme.mjs brand "#4D6BFE" --family magazine` (`--family swiss` for Style B) and paste the printed theme into the deck. The script pulls the color into the lightness and chroma range the style allows and refuses it if it cannot meet the readability floors — fall back to a preset then.
### Style B Swiss Themes ### Style B Swiss Themes
Pick from `references/themes-swiss.md`. **Custom hex values are not allowed** here either. Pick from `references/themes-swiss.md`. **Don't write hex values by hand** here either; brand colors go through the same script.
| Preview | Theme | Anchor color and best for | | Preview | Theme | Anchor color and best for |
|---------|-------|---------------------------| |---------|-------|---------------------------|
@@ -379,7 +381,7 @@ If the user asks for a Swiss-style deck without specifying color, default to Int
- Add more Swiss layout validation rules - Add more Swiss layout validation rules
- Improve screenshot redesign and infographic generation workflows - Improve screenshot redesign and infographic generation workflows
- Prepare marketplace-specific variants such as WorkBuddy - Prepare marketplace-specific variants such as WorkBuddy
- Add more curated theme packs while keeping custom colors restricted - Add more curated theme packs; brand colors stay constrained
## FAQ ## FAQ
@@ -387,7 +389,7 @@ If the user asks for a Swiss-style deck without specifying color, default to Int
The main output is HTML. You can present it in a browser, screenshot it, or record it. PPTX conversion can be done as a separate workflow, but it is not the core path today. The main output is HTML. You can present it in a browser, screenshot it, or record it. PPTX conversion can be done as a separate workflow, but it is not the core path today.
**Why are custom colors not allowed?** **Why are custom colors not allowed?**
The skill is designed for stable visual output. Arbitrary colors often break the system, so decks must use curated presets. The skill is designed for stable visual output. Arbitrary colors often break the system, so decks must use curated presets. For a brand color, generate a theme with `scripts/gz-theme.mjs`: it constrains lightness and chroma per style and checks contrast for every text role.
**Can I add my own layout?** **Can I add my own layout?**
Yes. Style A layouts can be extended in `references/layouts.md`. Style B is stricter: update `template-swiss.html`, `layouts-swiss.md`, `swiss-layout-lock.md`, and the validator together. Yes. Style A layouts can be extended in `references/layouts.md`. Style B is stricter: update `template-swiss.html`, `layouts-swiss.md`, `swiss-layout-lock.md`, and the validator together.
@@ -414,7 +416,7 @@ Bugs, layout issues, new layout requests — Issues and PRs welcome. Prioritize:
- Presenter runtime changes must land in both templates and pass `scripts/check-presenter-runtime-sync.mjs`; CI rejects CSS / JavaScript drift - Presenter runtime changes must land in both templates and pass `scripts/check-presenter-runtime-sync.mjs`; CI rejects CSS / JavaScript drift
- Presenter-note or live-behavior changes must also update `references/presenter-mode.md`, `references/checklist.md`, and `scripts/validate-presenter-mode.mjs` - Presenter-note or live-behavior changes must also update `references/presenter-mode.md`, `references/checklist.md`, and `scripts/validate-presenter-mode.mjs`
- Log pitfalls into `checklist.md` at the matching P0 / P1 / P2 / P3 tier - Log pitfalls into `checklist.md` at the matching P0 / P1 / P2 / P3 tier
- New theme colors go into `themes.md` with a recommended use case - Themes, type sizes, fonts and background effects come from a shared design system: the templates' `GZ:FONTS` / `GZ:TOKENS` / `GZ:EFFECTS` blocks and the `GZ:THEMES` blocks in `themes*.md` are generated — don't edit them by hand; open an issue for new theme colors or type changes
## License ## License
@@ -88,11 +88,11 @@ Guizang PPT Skill 的持续迭代获得 **360 安全龙虾**、**Kimi work**、*
- 📐 **横向左右翻页**:键盘 ← → / 滚轮 / 触屏滑动 / 底部圆点 / ESC 索引 - 📐 **横向左右翻页**:键盘 ← → / 滚轮 / 触屏滑动 / 底部圆点 / ESC 索引
- 🧩 **Style A 10 种布局**:封面、章节、数据大字报、图文、图片网格、Pipeline、对比等 - 🧩 **Style A 10 种布局**:封面、章节、数据大字报、图文、图片网格、Pipeline、对比等
- 🧱 **Style B 22 种锁定版式**:Cover、Statement、KPI Tower、Loop Diagram、Duo Compare、Image Hero、Closing Manifesto 等 - 🧱 **Style B 22 种锁定版式**:Cover、Statement、KPI Tower、Loop Diagram、Duo Compare、Image Hero、Closing Manifesto 等
- 🎨 **主题色预设**:Style A 5 套电子墨水主题,Style B 4 套瑞士高饱和锚点色 - 🎨 **主题色预设**:Style A 5 套电子墨水主题,Style B 4 套瑞士高饱和锚点色;给品牌色时按色彩科学生成受约束的品牌主题
- 🖼 **Codex 可选配图流程**:可用 GPT-Image 2.0 / GPT-M 2.0 生成纪实照片、信息图、流程图、系统关系图、UI 情景图,并按模板比例插入 - 🖼 **Codex 可选配图流程**:可用 GPT-Image 2.0 / GPT-M 2.0 生成纪实照片、信息图、流程图、系统关系图、UI 情景图,并按模板比例插入
- 📰 **多平台封面**:可用同一套视觉规则生成公众号 21:9、公众号分享卡 1:1、小红书 3:4、视频号横版等封面 - 📰 **多平台封面**:可用同一套视觉规则生成公众号 21:9、公众号分享卡 1:1、小红书 3:4、视频号横版等封面
- 🎤 **演讲者模式**:双窗口观众屏、当前页/下一页 16:9 预览、演讲备注、计时排练、自动翻页、激光笔、圈选和现场故障恢复 - 🎤 **演讲者模式**:双窗口观众屏、当前页/下一页 16:9 预览、演讲备注、计时排练、自动翻页、激光笔、圈选和现场故障恢复
- 📴 **低性能静态模式**:按 `B` 可关闭 WebGL / canvas 动画,让动态内容退回静态背景 - 📴 **低性能静态模式**:按 `B` 可关闭 WebGL / canvas 动画,让动态内容退回静态背景;电脑带不动时背景会自动降分辨率,再不行就定格成静态
- 📄 **单文件 HTML**:不需要构建、不需要服务器,浏览器直接打开 - 📄 **单文件 HTML**:不需要构建、不需要服务器,浏览器直接打开
## 适合 / 不适合 ## 适合 / 不适合
@@ -328,7 +328,7 @@ guizang-ppt-skill/
## 主题色预设 ## 主题色预设
从 `references/themes.md` 里选一套——**不允许自定义 hex 值**,保护美学比给自由更重要。 从 `references/themes.md` 里选一套——**不允许随手写 hex 值**,保护美学比给自由更重要。所有主题的文字色、线条色和强调色小字都按同一套对比度规则(WCAG + APCA)算出来。
### Style A 电子杂志主题 ### Style A 电子杂志主题
@@ -340,11 +340,13 @@ guizang-ppt-skill/
| <img src="https://github.com/user-attachments/assets/dfea080e-e916-417e-93cd-0a3628de84ca" width="260" alt="牛皮纸主题预览"> | 🍂 **牛皮纸** | `#2a1e13` / `#eedfc7`。怀旧、人文、阅读、历史、文学分享。 | | <img src="https://github.com/user-attachments/assets/dfea080e-e916-417e-93cd-0a3628de84ca" width="260" alt="牛皮纸主题预览"> | 🍂 **牛皮纸** | `#2a1e13` / `#eedfc7`。怀旧、人文、阅读、历史、文学分享。 |
| <img src="https://github.com/user-attachments/assets/f3705592-9a72-4dbc-9818-df3aea61bc75" width="260" alt="沙丘主题预览"> | 🌙 **沙丘** | `#1f1a14` / `#f0e6d2`。艺术、设计、创意、时尚和画廊感内容。 | | <img src="https://github.com/user-attachments/assets/f3705592-9a72-4dbc-9818-df3aea61bc75" width="260" alt="沙丘主题预览"> | 🌙 **沙丘** | `#1f1a14` / `#f0e6d2`。艺术、设计、创意、时尚和画廊感内容。 |
切换主题只需替换 `template.html` 开头 `:root{}` 里的 6 行变量,其他 CSS 全走 `var(--...)`。 切换主题只需把模板第一行 `<html data-theme="…">` 改成主题 id,例如 `data-theme="indigo-porcelain"`,全部主题都已内置在模板里。
用户给了品牌色或 logo 主色时,运行 `node scripts/gz-theme.mjs brand "#4D6BFE" --family magazine`(瑞士风用 `--family swiss`),把输出的主题贴进 deck。脚本会把品牌色收进这套风格允许的明度和彩度范围,达不到可读性要求就拒绝,这时回到预设。
### Style B 瑞士主题 ### Style B 瑞士主题
瑞士风从 `references/themes-swiss.md` 里选一套,同样**不允许自定义 hex 值**。 瑞士风从 `references/themes-swiss.md` 里选一套,同样**不允许随手写 hex 值**,品牌色走同一个脚本。
| 预览 | 主题 | 锚点色与适合场景 | | 预览 | 主题 | 锚点色与适合场景 |
|------|------|------------------| |------|------|------------------|
@@ -380,7 +382,7 @@ guizang-ppt-skill/
- 增加更多瑞士风版式校验规则 - 增加更多瑞士风版式校验规则
- 优化截图再设计和信息图生成工作流 - 优化截图再设计和信息图生成工作流
- 整理 WorkBuddy 等平台上架版本 - 整理 WorkBuddy 等平台上架版本
- 增加更多主题包,但继续限制自定义颜色 - 增加更多主题包,品牌色继续走受约束生成
## FAQ ## FAQ
@@ -388,7 +390,7 @@ guizang-ppt-skill/
当前核心交付是 HTML。你可以用浏览器演示、截图或录屏。如果需要 PPTX,建议把 HTML 页面作为视觉稿再转换,但这不是当前主流程。 当前核心交付是 HTML。你可以用浏览器演示、截图或录屏。如果需要 PPTX,建议把 HTML 页面作为视觉稿再转换,但这不是当前主流程。
**为什么不允许自定义颜色?** **为什么不允许自定义颜色?**
这个 Skill 的重点是稳定产出。自由选色很容易破坏整体风格,所以只允许从预设主题里选。 这个 Skill 的重点是稳定产出。自由选色很容易破坏整体风格,所以只允许从预设主题里选。需要品牌色时用 `scripts/gz-theme.mjs` 生成,它会按风格约束明度和彩度,并检查每个文字角色的对比度。
**我能加自己的版式吗?** **我能加自己的版式吗?**
可以。Style A 可以在 `references/layouts.md` 里扩展;Style B 更严格,需要同步更新 `template-swiss.html`、`layouts-swiss.md`、`swiss-layout-lock.md` 和校验器。 可以。Style A 可以在 `references/layouts.md` 里扩展;Style B 更严格,需要同步更新 `template-swiss.html`、`layouts-swiss.md`、`swiss-layout-lock.md` 和校验器。
@@ -415,7 +417,7 @@ Bug、排版问题、新布局需求——欢迎开 Issue 或 PR。改动请优
- 演讲者运行时必须同时更新两套模板,并运行 `scripts/check-presenter-runtime-sync.mjs`;CI 会拦截两份模板的 CSS / JavaScript 漂移 - 演讲者运行时必须同时更新两套模板,并运行 `scripts/check-presenter-runtime-sync.mjs`;CI 会拦截两份模板的 CSS / JavaScript 漂移
- 演讲备注或现场行为变化时,同步更新 `references/presenter-mode.md`、`references/checklist.md` 和 `scripts/validate-presenter-mode.mjs` - 演讲备注或现场行为变化时,同步更新 `references/presenter-mode.md`、`references/checklist.md` 和 `scripts/validate-presenter-mode.mjs`
- 把踩过的坑写到 `checklist.md` 对应的 P0 / P1 / P2 / P3 级别 - 把踩过的坑写到 `checklist.md` 对应的 P0 / P1 / P2 / P3 级别
- 新主题色进 `themes.md` 并给出适合的场景 - 主题、字号、字体和背景特效来自共享设计系统,模板里 `GZ:FONTS` / `GZ:TOKENS` / `GZ:EFFECTS` 区块和 `themes*.md` 的 `GZ:THEMES` 区块是生成的,不要手改;新主题色或字号调整请提 Issue
## License ## License
@@ -2,8 +2,8 @@
"sourceId": "guizang-ppt-skill", "sourceId": "guizang-ppt-skill",
"repo": "https://github.com/op7418/guizang-ppt-skill.git", "repo": "https://github.com/op7418/guizang-ppt-skill.git",
"ref": "main", "ref": "main",
"commit": "c91369c449d34755d320a8b81d0734000d99d1ab", "commit": "93d21bf4a73a851907a6eb00acbf30618236667a",
"adapter": "claude-skill", "adapter": "claude-skill",
"sourcePath": ".", "sourcePath": ".",
"syncedAt": "2026-08-08T16:00:01Z" "syncedAt": "2026-10-30T16:00:00Z"
} }
@@ -1,2 +1,5 @@
.DS_Store .DS_Store
*.backup *.backup
.gz-visual/
.gz-smoke/
.gz-dbg/
@@ -86,11 +86,11 @@ Beyond Claude Code / Codex, you can also use Guizang PPT Skill on these platform
- 📐 **Horizontal swipe navigation**: ← → arrows / scroll wheel / touch swipe / bottom dots / ESC for index - 📐 **Horizontal swipe navigation**: ← → arrows / scroll wheel / touch swipe / bottom dots / ESC for index
- 🧩 **Style A 10 layouts**: cover, divider, big numbers, image/text, image grid, pipeline, comparison, and more - 🧩 **Style A 10 layouts**: cover, divider, big numbers, image/text, image grid, pipeline, comparison, and more
- 🧱 **Style B 22 locked layouts**: Cover, Statement, KPI Tower, Loop Diagram, Duo Compare, Image Hero, Closing Manifesto, and more - 🧱 **Style B 22 locked layouts**: Cover, Statement, KPI Tower, Loop Diagram, Duo Compare, Image Hero, Closing Manifesto, and more
- 🎨 **Curated theme presets**: 5 electronic-ink themes for Style A, 4 Swiss anchor-color themes for Style B - 🎨 **Curated theme presets**: 5 electronic-ink themes for Style A, 4 Swiss anchor-color themes for Style B; a brand color becomes a constrained brand theme generated with color science
- 🖼 **Optional Codex image flow**: generate documentary photos, infographics, flow diagrams, system maps, and UI scenes with GPT-Image 2.0 / GPT-M 2.0, then insert them at template-safe ratios - 🖼 **Optional Codex image flow**: generate documentary photos, infographics, flow diagrams, system maps, and UI scenes with GPT-Image 2.0 / GPT-M 2.0, then insert them at template-safe ratios
- 📰 **Social covers**: generate 21:9 WeChat cover images, 1:1 share cards, 3:4 Xiaohongshu covers, video thumbnails, and related variants - 📰 **Social covers**: generate 21:9 WeChat cover images, 1:1 share cards, 3:4 Xiaohongshu covers, video thumbnails, and related variants
- 🎤 **Presenter mode**: dual-window audience output, 16:9 current/next previews, speaker notes, timing and rehearsal, auto advance, laser pointer, circles, and live recovery controls - 🎤 **Presenter mode**: dual-window audience output, 16:9 current/next previews, speaker notes, timing and rehearsal, auto advance, laser pointer, circles, and live recovery controls
- 📴 **Low-power static mode**: press `B` to turn WebGL / canvas animation into static visuals - 📴 **Low-power static mode**: press `B` to turn WebGL / canvas animation into static visuals; on slow machines the background lowers its resolution automatically and freezes to a still frame if needed
- 📄 **Single HTML file** — no build, no server, open directly in the browser - 📄 **Single HTML file** — no build, no server, open directly in the browser
## Fits / Doesn't fit ## Fits / Doesn't fit
@@ -327,7 +327,7 @@ guizang-ppt-skill/
## Theme presets ## Theme presets
Pick from `references/themes.md`. **Custom hex values are not allowed** — protecting the aesthetic matters more than freedom of choice. Pick from `references/themes.md`. **Don't write hex values by hand** — protecting the aesthetic matters more than freedom of choice. Every theme's text, rule and small accent-text colors are derived with the same contrast rules (WCAG + APCA).
### Style A Editorial Themes ### Style A Editorial Themes
@@ -339,11 +339,13 @@ Pick from `references/themes.md`. **Custom hex values are not allowed** — prot
| <img src="https://github.com/user-attachments/assets/dfea080e-e916-417e-93cd-0a3628de84ca" width="260" alt="Kraft Paper theme preview"> | 🍂 **Kraft Paper** | `#2a1e13` / `#eedfc7`. Nostalgic, humanist, literary, indie zines. | | <img src="https://github.com/user-attachments/assets/dfea080e-e916-417e-93cd-0a3628de84ca" width="260" alt="Kraft Paper theme preview"> | 🍂 **Kraft Paper** | `#2a1e13` / `#eedfc7`. Nostalgic, humanist, literary, indie zines. |
| <img src="https://github.com/user-attachments/assets/f3705592-9a72-4dbc-9818-df3aea61bc75" width="260" alt="Dune theme preview"> | 🌙 **Dune** | `#1f1a14` / `#f0e6d2`. Art, design, creative, fashion, gallery-like decks. | | <img src="https://github.com/user-attachments/assets/f3705592-9a72-4dbc-9818-df3aea61bc75" width="260" alt="Dune theme preview"> | 🌙 **Dune** | `#1f1a14` / `#f0e6d2`. Art, design, creative, fashion, gallery-like decks. |
Switching themes only requires replacing the 6 variables at the top of `template.html`'s `:root{}` block — all other CSS flows through `var(--...)`. To switch themes, set the theme id on the template's first line, e.g. `<html data-theme="indigo-porcelain">`. All themes are built into the template.
When the user gives a brand or logo color, run `node scripts/gz-theme.mjs brand "#4D6BFE" --family magazine` (`--family swiss` for Style B) and paste the printed theme into the deck. The script pulls the color into the lightness and chroma range the style allows and refuses it if it cannot meet the readability floors — fall back to a preset then.
### Style B Swiss Themes ### Style B Swiss Themes
Pick from `references/themes-swiss.md`. **Custom hex values are not allowed** here either. Pick from `references/themes-swiss.md`. **Don't write hex values by hand** here either; brand colors go through the same script.
| Preview | Theme | Anchor color and best for | | Preview | Theme | Anchor color and best for |
|---------|-------|---------------------------| |---------|-------|---------------------------|
@@ -379,7 +381,7 @@ If the user asks for a Swiss-style deck without specifying color, default to Int
- Add more Swiss layout validation rules - Add more Swiss layout validation rules
- Improve screenshot redesign and infographic generation workflows - Improve screenshot redesign and infographic generation workflows
- Prepare marketplace-specific variants such as WorkBuddy - Prepare marketplace-specific variants such as WorkBuddy
- Add more curated theme packs while keeping custom colors restricted - Add more curated theme packs; brand colors stay constrained
## FAQ ## FAQ
@@ -387,7 +389,7 @@ If the user asks for a Swiss-style deck without specifying color, default to Int
The main output is HTML. You can present it in a browser, screenshot it, or record it. PPTX conversion can be done as a separate workflow, but it is not the core path today. The main output is HTML. You can present it in a browser, screenshot it, or record it. PPTX conversion can be done as a separate workflow, but it is not the core path today.
**Why are custom colors not allowed?** **Why are custom colors not allowed?**
The skill is designed for stable visual output. Arbitrary colors often break the system, so decks must use curated presets. The skill is designed for stable visual output. Arbitrary colors often break the system, so decks must use curated presets. For a brand color, generate a theme with `scripts/gz-theme.mjs`: it constrains lightness and chroma per style and checks contrast for every text role.
**Can I add my own layout?** **Can I add my own layout?**
Yes. Style A layouts can be extended in `references/layouts.md`. Style B is stricter: update `template-swiss.html`, `layouts-swiss.md`, `swiss-layout-lock.md`, and the validator together. Yes. Style A layouts can be extended in `references/layouts.md`. Style B is stricter: update `template-swiss.html`, `layouts-swiss.md`, `swiss-layout-lock.md`, and the validator together.
@@ -414,7 +416,7 @@ Bugs, layout issues, new layout requests — Issues and PRs welcome. Prioritize:
- Presenter runtime changes must land in both templates and pass `scripts/check-presenter-runtime-sync.mjs`; CI rejects CSS / JavaScript drift - Presenter runtime changes must land in both templates and pass `scripts/check-presenter-runtime-sync.mjs`; CI rejects CSS / JavaScript drift
- Presenter-note or live-behavior changes must also update `references/presenter-mode.md`, `references/checklist.md`, and `scripts/validate-presenter-mode.mjs` - Presenter-note or live-behavior changes must also update `references/presenter-mode.md`, `references/checklist.md`, and `scripts/validate-presenter-mode.mjs`
- Log pitfalls into `checklist.md` at the matching P0 / P1 / P2 / P3 tier - Log pitfalls into `checklist.md` at the matching P0 / P1 / P2 / P3 tier
- New theme colors go into `themes.md` with a recommended use case - Themes, type sizes, fonts and background effects come from a shared design system: the templates' `GZ:FONTS` / `GZ:TOKENS` / `GZ:EFFECTS` blocks and the `GZ:THEMES` blocks in `themes*.md` are generated — don't edit them by hand; open an issue for new theme colors or type changes
## License ## License
@@ -88,11 +88,11 @@ Guizang PPT Skill 的持续迭代获得 **360 安全龙虾**、**Kimi work**、*
- 📐 **横向左右翻页**:键盘 ← → / 滚轮 / 触屏滑动 / 底部圆点 / ESC 索引 - 📐 **横向左右翻页**:键盘 ← → / 滚轮 / 触屏滑动 / 底部圆点 / ESC 索引
- 🧩 **Style A 10 种布局**:封面、章节、数据大字报、图文、图片网格、Pipeline、对比等 - 🧩 **Style A 10 种布局**:封面、章节、数据大字报、图文、图片网格、Pipeline、对比等
- 🧱 **Style B 22 种锁定版式**:Cover、Statement、KPI Tower、Loop Diagram、Duo Compare、Image Hero、Closing Manifesto 等 - 🧱 **Style B 22 种锁定版式**:Cover、Statement、KPI Tower、Loop Diagram、Duo Compare、Image Hero、Closing Manifesto 等
- 🎨 **主题色预设**:Style A 5 套电子墨水主题,Style B 4 套瑞士高饱和锚点色 - 🎨 **主题色预设**:Style A 5 套电子墨水主题,Style B 4 套瑞士高饱和锚点色;给品牌色时按色彩科学生成受约束的品牌主题
- 🖼 **Codex 可选配图流程**:可用 GPT-Image 2.0 / GPT-M 2.0 生成纪实照片、信息图、流程图、系统关系图、UI 情景图,并按模板比例插入 - 🖼 **Codex 可选配图流程**:可用 GPT-Image 2.0 / GPT-M 2.0 生成纪实照片、信息图、流程图、系统关系图、UI 情景图,并按模板比例插入
- 📰 **多平台封面**:可用同一套视觉规则生成公众号 21:9、公众号分享卡 1:1、小红书 3:4、视频号横版等封面 - 📰 **多平台封面**:可用同一套视觉规则生成公众号 21:9、公众号分享卡 1:1、小红书 3:4、视频号横版等封面
- 🎤 **演讲者模式**:双窗口观众屏、当前页/下一页 16:9 预览、演讲备注、计时排练、自动翻页、激光笔、圈选和现场故障恢复 - 🎤 **演讲者模式**:双窗口观众屏、当前页/下一页 16:9 预览、演讲备注、计时排练、自动翻页、激光笔、圈选和现场故障恢复
- 📴 **低性能静态模式**:按 `B` 可关闭 WebGL / canvas 动画,让动态内容退回静态背景 - 📴 **低性能静态模式**:按 `B` 可关闭 WebGL / canvas 动画,让动态内容退回静态背景;电脑带不动时背景会自动降分辨率,再不行就定格成静态
- 📄 **单文件 HTML**:不需要构建、不需要服务器,浏览器直接打开 - 📄 **单文件 HTML**:不需要构建、不需要服务器,浏览器直接打开
## 适合 / 不适合 ## 适合 / 不适合
@@ -328,7 +328,7 @@ guizang-ppt-skill/
## 主题色预设 ## 主题色预设
从 `references/themes.md` 里选一套——**不允许自定义 hex 值**,保护美学比给自由更重要。 从 `references/themes.md` 里选一套——**不允许随手写 hex 值**,保护美学比给自由更重要。所有主题的文字色、线条色和强调色小字都按同一套对比度规则(WCAG + APCA)算出来。
### Style A 电子杂志主题 ### Style A 电子杂志主题
@@ -340,11 +340,13 @@ guizang-ppt-skill/
| <img src="https://github.com/user-attachments/assets/dfea080e-e916-417e-93cd-0a3628de84ca" width="260" alt="牛皮纸主题预览"> | 🍂 **牛皮纸** | `#2a1e13` / `#eedfc7`。怀旧、人文、阅读、历史、文学分享。 | | <img src="https://github.com/user-attachments/assets/dfea080e-e916-417e-93cd-0a3628de84ca" width="260" alt="牛皮纸主题预览"> | 🍂 **牛皮纸** | `#2a1e13` / `#eedfc7`。怀旧、人文、阅读、历史、文学分享。 |
| <img src="https://github.com/user-attachments/assets/f3705592-9a72-4dbc-9818-df3aea61bc75" width="260" alt="沙丘主题预览"> | 🌙 **沙丘** | `#1f1a14` / `#f0e6d2`。艺术、设计、创意、时尚和画廊感内容。 | | <img src="https://github.com/user-attachments/assets/f3705592-9a72-4dbc-9818-df3aea61bc75" width="260" alt="沙丘主题预览"> | 🌙 **沙丘** | `#1f1a14` / `#f0e6d2`。艺术、设计、创意、时尚和画廊感内容。 |
切换主题只需替换 `template.html` 开头 `:root{}` 里的 6 行变量,其他 CSS 全走 `var(--...)`。 切换主题只需把模板第一行 `<html data-theme="…">` 改成主题 id,例如 `data-theme="indigo-porcelain"`,全部主题都已内置在模板里。
用户给了品牌色或 logo 主色时,运行 `node scripts/gz-theme.mjs brand "#4D6BFE" --family magazine`(瑞士风用 `--family swiss`),把输出的主题贴进 deck。脚本会把品牌色收进这套风格允许的明度和彩度范围,达不到可读性要求就拒绝,这时回到预设。
### Style B 瑞士主题 ### Style B 瑞士主题
瑞士风从 `references/themes-swiss.md` 里选一套,同样**不允许自定义 hex 值**。 瑞士风从 `references/themes-swiss.md` 里选一套,同样**不允许随手写 hex 值**,品牌色走同一个脚本。
| 预览 | 主题 | 锚点色与适合场景 | | 预览 | 主题 | 锚点色与适合场景 |
|------|------|------------------| |------|------|------------------|
@@ -380,7 +382,7 @@ guizang-ppt-skill/
- 增加更多瑞士风版式校验规则 - 增加更多瑞士风版式校验规则
- 优化截图再设计和信息图生成工作流 - 优化截图再设计和信息图生成工作流
- 整理 WorkBuddy 等平台上架版本 - 整理 WorkBuddy 等平台上架版本
- 增加更多主题包,但继续限制自定义颜色 - 增加更多主题包,品牌色继续走受约束生成
## FAQ ## FAQ
@@ -388,7 +390,7 @@ guizang-ppt-skill/
当前核心交付是 HTML。你可以用浏览器演示、截图或录屏。如果需要 PPTX,建议把 HTML 页面作为视觉稿再转换,但这不是当前主流程。 当前核心交付是 HTML。你可以用浏览器演示、截图或录屏。如果需要 PPTX,建议把 HTML 页面作为视觉稿再转换,但这不是当前主流程。
**为什么不允许自定义颜色?** **为什么不允许自定义颜色?**
这个 Skill 的重点是稳定产出。自由选色很容易破坏整体风格,所以只允许从预设主题里选。 这个 Skill 的重点是稳定产出。自由选色很容易破坏整体风格,所以只允许从预设主题里选。需要品牌色时用 `scripts/gz-theme.mjs` 生成,它会按风格约束明度和彩度,并检查每个文字角色的对比度。
**我能加自己的版式吗?** **我能加自己的版式吗?**
可以。Style A 可以在 `references/layouts.md` 里扩展;Style B 更严格,需要同步更新 `template-swiss.html`、`layouts-swiss.md`、`swiss-layout-lock.md` 和校验器。 可以。Style A 可以在 `references/layouts.md` 里扩展;Style B 更严格,需要同步更新 `template-swiss.html`、`layouts-swiss.md`、`swiss-layout-lock.md` 和校验器。
@@ -415,7 +417,7 @@ Bug、排版问题、新布局需求——欢迎开 Issue 或 PR。改动请优
- 演讲者运行时必须同时更新两套模板,并运行 `scripts/check-presenter-runtime-sync.mjs`;CI 会拦截两份模板的 CSS / JavaScript 漂移 - 演讲者运行时必须同时更新两套模板,并运行 `scripts/check-presenter-runtime-sync.mjs`;CI 会拦截两份模板的 CSS / JavaScript 漂移
- 演讲备注或现场行为变化时,同步更新 `references/presenter-mode.md`、`references/checklist.md` 和 `scripts/validate-presenter-mode.mjs` - 演讲备注或现场行为变化时,同步更新 `references/presenter-mode.md`、`references/checklist.md` 和 `scripts/validate-presenter-mode.mjs`
- 把踩过的坑写到 `checklist.md` 对应的 P0 / P1 / P2 / P3 级别 - 把踩过的坑写到 `checklist.md` 对应的 P0 / P1 / P2 / P3 级别
- 新主题色进 `themes.md` 并给出适合的场景 - 主题、字号、字体和背景特效来自共享设计系统,模板里 `GZ:FONTS` / `GZ:TOKENS` / `GZ:EFFECTS` 区块和 `themes*.md` 的 `GZ:THEMES` 区块是生成的,不要手改;新主题色或字号调整请提 Issue
## License ## License
@@ -202,27 +202,30 @@ cp "<SKILL_ROOT>/assets/template-swiss.html" "项目/XXX/ppt/index.html"
每次拷贝完 template.html 第一件事:grep 一下"[必填]" 确认全部替换完。 每次拷贝完 template.html 第一件事:grep 一下"[必填]" 确认全部替换完。
#### 2.2 · 选定主题色(5 套预设 · 不允许自定义) #### 2.2 · 选定主题色(预设优先 · 品牌色受约束生成)
本 skill **只允许从 5 套精心调配的预设里选一套**,不接受用户自定义 hex 值——颜色搭配错了画面瞬间变丑,保护美学比给自由更重要。 颜色搭配错了画面瞬间变丑,所以**不直接使用用户给的任何颜色值**。默认从精调预设里选一套;用户明确有品牌色或 logo 主色时,用脚本生成一套受约束的品牌主题。
| # | 主题 | 适合 | | # | 杂志风主题 (`themes.md`) | 适合 |
|---|------|------| |---|------|------|
| 1 | 🖋 墨水经典 | 通用 / 商业发布 / 不知道选啥的默认 | | 1 | 🖋 墨水经典 `ink-classic` | 通用 / 商业发布 / 不知道选啥的默认 |
| 2 | 🌊 靛蓝瓷 | 科技 / 研究 / 数据 / 技术发布会 | | 2 | 🌊 靛蓝瓷 `indigo-porcelain` | 科技 / 研究 / 数据 / 技术发布会 |
| 3 | 🌿 森林墨 | 自然 / 可持续 / 文化 / 非虚构 | | 3 | 🌿 森林墨 `forest-ink` | 自然 / 可持续 / 文化 / 非虚构 |
| 4 | 🍂 牛皮纸 | 怀旧 / 人文 / 文学 / 独立杂志 | | 4 | 🍂 牛皮纸 `kraft-paper` | 怀旧 / 人文 / 文学 / 独立杂志 |
| 5 | 🌙 沙丘 | 艺术 / 设计 / 创意 / 画廊 | | 5 | 🌙 沙丘 `dune` | 艺术 / 设计 / 创意 / 画廊 |
瑞士风 4 套(`ikb` / `lemon` / `lemon-green` / `safety-orange`)见 `themes-swiss.md`。
**操作**: **操作**:
1. 基于内容主题推荐一套,或直接问用户选哪一套 1. 基于内容主题推荐一套,或直接问用户选哪一套
2. 打开 `references/themes.md`,找到对应主题的 `:root` 块 2. 把拷贝好的模板第一行 `<html>` 的 `data-theme` 改成主题 id,例如 `data-theme="indigo-porcelain"`。全部主题都已内置在模板的 `GZ:TOKENS` 区块里,**不要改那个区块**
3. **整体替换** `assets/template.html`(已拷贝版本)开头 `:root{` 块里标有"主题色"注释的那几行(`--ink` / `--ink-rgb` / `--paper` / `--paper-rgb` / `--paper-tint` / `--ink-tint`) 3. 用户给了品牌色:运行 `node <SKILL_ROOT>/scripts/gz-theme.mjs brand "#4D6BFE" --family magazine`(瑞士风用 `--family swiss`),把输出的 `[data-theme="brand"]{…}` 贴到 deck `<style>` 末尾,再把 `data-theme` 改成 `brand`。脚本判定不达标时会说明原因,这时回到预设
4. 其他 CSS 都走 `var(--...)`,无需任何其他改动
**颜色变量**(页面里只用这些,不写 hex):文字 `--text-primary` / `--text-body` / `--text-secondary` / `--text-helper`;线条 `--line` / `--line-strong`;强调色色块 `--accent`;强调色当小字 `--accent-ink`、当大字 `--accent-display`;强调色块上的文字 `--accent-on`。
**硬规则**: **硬规则**:
- 一份 deck 只用一套主题,不要中途换色 - 一份 deck 只用一套主题,不要中途换色
- 不要接受用户给的任意 hex 值——委婉拒绝并展示 5 套让选 - 不要把用户给的 hex 值直接写进页面——走品牌色脚本
- 不要混搭(例如 ink 取墨水经典、paper 取沙丘)——会彻底违和 - 不要混搭(例如 ink 取墨水经典、paper 取沙丘)——会彻底违和
### Step 3 · 填充内容 ### Step 3 · 填充内容
@@ -435,7 +438,7 @@ cp "<SKILL_ROOT>/assets/template-swiss.html" "项目/XXX/ppt/index.html"
**硬规则:** **硬规则:**
- 同一页内,字号越小的元素字重必须 ≥ 字号越大的元素(不允许 16px 正文用 300 而 1.8vw 标题用 500) - 同一页内,字号越小的元素字重必须 ≥ 字号越大的元素(不允许 16px 正文用 300 而 1.8vw 标题用 500)
- 16px 左右的小字拒绝使用 weight 300(太细不可读),最低 400,推荐 500 - 16px 左右的小字拒绝使用 weight 300(太细不可读),最低 400,推荐 500
- 封面/IkB 反白大标题内强调字用 `italic + weight 300`,不要用 accent 色(蓝压蓝看不见) - 封面/IKB 反白大标题内强调字:英文用 `italic + weight 300`,中文用 `font-weight:400`(中文不用斜体),不要用 accent 色(蓝压蓝看不见)
组件细节(字体、颜色、网格、图标、callout、stat-card 等)在 `references/components.md`。 组件细节(字体、颜色、网格、图标、callout、stat-card 等)在 `references/components.md`。
@@ -549,11 +552,12 @@ open "项目/XXX/ppt/index.html"
guizang-ppt-skill/ guizang-ppt-skill/
├── SKILL.md ← 你正在读 ├── SKILL.md ← 你正在读
├── assets/ ├── assets/
│ ├── template.html ← 风格 A · 电子杂志风模板(种子文件) │ ├── template.html ← 风格 A · 电子杂志风模板(种子文件;GZ:* 标记区由 guizang-design 生成,不要手改)
│ ├── template-swiss.html ← 风格 B · 瑞士国际主义风模板(种子文件) │ ├── template-swiss.html ← 风格 B · 瑞士国际主义风模板(种子文件)
│ ├── screenshot-backgrounds/ ← 截图美化内置背景(WebP):style-a 5 套 / style-b 4 套 │ ├── screenshot-backgrounds/ ← 截图美化内置背景(WebP):style-a 5 套 / style-b 4 套
│ └── motion.min.js ← Motion One 本地副本(离线兜底,约 64KB,共用) │ └── motion.min.js ← Motion One 本地副本(离线兜底,约 64KB,共用)
├── scripts/ ├── scripts/
│ ├── gz-theme.mjs ← 品牌色 → 受约束主题(由 guizang-design 生成,零依赖)
│ ├── validate-swiss-deck.mjs ← 风格 B 静态校验:登记版式、图片槽位、SVG 文本、标题对齐 │ ├── validate-swiss-deck.mjs ← 风格 B 静态校验:登记版式、图片槽位、SVG 文本、标题对齐
│ └── validate-presenter-mode.mjs ← 两种风格共用:页面 ID、演讲备注、时长和演讲者运行时校验 │ └── validate-presenter-mode.mjs ← 两种风格共用:页面 ID、演讲备注、时长和演讲者运行时校验
└── references/ └── references/
@@ -562,8 +566,8 @@ guizang-ppt-skill/
├── swiss-layout-lock.md ← 风格 B · 原始 22P 版式锁,正文页必须按这里登记 ├── swiss-layout-lock.md ← 风格 B · 原始 22P 版式锁,正文页必须按这里登记
├── layouts-swiss.md ← 风格 B · 原始 22P 骨架说明 + 少量明确标注的实验区 ├── layouts-swiss.md ← 风格 B · 原始 22P 骨架说明 + 少量明确标注的实验区
├── swiss-map-component.md ← 风格 B · S08 地图扩展组件(MapLibre 点位/连线/卡片/控制) ├── swiss-map-component.md ← 风格 B · S08 地图扩展组件(MapLibre 点位/连线/卡片/控制)
├── themes.md ← 风格 A · 5 套主题色预设(只能选不能自定义) ├── themes.md ← 风格 A · 5 套主题色预设 + 品牌色流程(主题部分由 guizang-design 生成)
├── themes-swiss.md ← 风格 B · 4 套瑞士风主题色预设(IKB / 柠檬黄 / 柠檬绿 / 安全橙) ├── themes-swiss.md ← 风格 B · 4 套瑞士风主题色预设 + 品牌色流程(主题部分由 guizang-design 生成)
├── image-prompts.md ← GPT-M 2.0 配图类型、比例和基础提示词 ├── image-prompts.md ← GPT-M 2.0 配图类型、比例和基础提示词
├── screenshot-framing.md ← CleanShot X 式截图适配语义 + 内置背景资产映射 ├── screenshot-framing.md ← CleanShot X 式截图适配语义 + 内置背景资产映射
├── presenter-mode.md ← 演讲者 UI、AI 备注结构、观众屏同步与恢复契约 ├── presenter-mode.md ← 演讲者 UI、AI 备注结构、观众屏同步与恢复契约
File diff suppressed because it is too large. Load diff
@@ -109,7 +109,7 @@ node <SKILL_ROOT>/scripts/validate-swiss-deck.mjs path/to/index.html
- 13-15px → weight **500-600** - 13-15px → weight **500-600**
- 同一页内,字号小的元素字重必须 ≥ 字号大的元素。 - 同一页内,字号小的元素字重必须 ≥ 字号大的元素。
- **16px 左右小字禁止使用 weight 300**(太细不可读),最低 400,推荐 500。 - **16px 左右小字禁止使用 weight 300**(太细不可读),最低 400,推荐 500。
- 封面/IKB 反白大标题内强调字用 `italic + weight 300`,不要用 accent 色。 - 封面/IKB 反白大标题内强调字:英文 `italic + weight 300`,中文 `font-weight:400`(中文不用斜体),不要用 accent 色。
**检查**: **检查**:
- `rg -n "font-size:(10px|11px|12px|13px)|max\\((9|10|11|12|13)px" index.html` - `rg -n "font-size:(10px|11px|12px|13px)|max\\((9|10|11|12|13)px" index.html`
@@ -173,7 +173,7 @@ node <SKILL_ROOT>/scripts/validate-swiss-deck.mjs path/to/index.html
**做法**(瑞士风必守): **做法**(瑞士风必守):
- **封面强制 `<section class="slide accent">`**(满屏 IKB),不要 `slide.light`,也不要 `slide.dark`;在 `.canvas-card` 内**第一个子元素**插入 `<canvas class="ascii-bg">`(ASCII 字符呼吸场,模板自带 IIFE 自动激活) - **封面强制 `<section class="slide accent">`**(满屏 IKB),不要 `slide.light`,也不要 `slide.dark`;在 `.canvas-card` 内**第一个子元素**插入 `<canvas class="ascii-bg">`(ASCII 字符呼吸场,模板自带 IIFE 自动激活)
- **不要再写"01"等编号大字**:`.chrome-min` 已经显示 `01 / N`,封面再放一个巨大的"01"=同义重复,直接删掉 - **不要再写"01"等编号大字**:`.chrome-min` 已经显示 `01 / N`,封面再放一个巨大的"01"=同义重复,直接删掉
- **强调字必须用斜体**:`font-style:italic;font-weight:300`,**禁止**用 `color:var(--accent)`——IKB 蓝压 IKB 蓝,人眼看不见任何强调 - **强调字**:英文用斜体 `font-style:italic;font-weight:300`,中文用 `font-weight:400`(中文没有斜体字形,不要用 italic),**禁止**用 `color:var(--accent)`——IKB 蓝压 IKB 蓝,人眼看不见任何强调
- **封底强制 `slide.split`** 双半屏,左半 `.half.b-accent` + ASCII canvas(与封面色彩闭环),右半 paper 白底放 3 条 takeaway;**第 03 条**用 `var(--accent)` 上色,完成"开场全 IKB ↔ 收尾半 IKB"的色彩闭环 - **封底强制 `slide.split`** 双半屏,左半 `.half.b-accent` + ASCII canvas(与封面色彩闭环),右半 paper 白底放 3 条 takeaway;**第 03 条**用 `var(--accent)` 上色,完成"开场全 IKB ↔ 收尾半 IKB"的色彩闭环
- ASCII canvas 在模板的 `<style>` 里已经预设 `mix-blend-mode:screen;opacity:.92`,不要去动这个值 - ASCII canvas 在模板的 `<style>` 里已经预设 `mix-blend-mode:screen;opacity:.92`,不要去动这个值
- 封面/封底主标题字号双约束:`min(11.6vw,19vh)` ~ `min(8vw,14vh)`(遵守 Y ≥ X × 1.6 规则) - 封面/封底主标题字号双约束:`min(11.6vw,19vh)` ~ `min(8vw,14vh)`(遵守 Y ≥ X × 1.6 规则)
@@ -181,7 +181,7 @@ node <SKILL_ROOT>/scripts/validate-swiss-deck.mjs path/to/index.html
**自检命令**: **自检命令**:
- `grep -c "ascii-bg" index.html`——封面 + 封底应至少命中 ≥ 2(各一个 canvas) - `grep -c "ascii-bg" index.html`——封面 + 封底应至少命中 ≥ 2(各一个 canvas)
- `grep -E '"slide accent"' index.html | head -1`——封面应是 `slide accent` 而非 `slide light` - `grep -E '"slide accent"' index.html | head -1`——封面应是 `slide accent` 而非 `slide light`
- `grep "color:var(--accent)" index.html`——若命中行同时含 `font-style:italic` 即危险信号(蓝压蓝),改为只 italic 不 accent;只有封底"03 takeaway"那一处用 `var(--accent)` 是合法的(此时背景是白色) - `grep "color:var(--accent)" index.html`——若命中行同时含 `font-style:italic` 即危险信号(蓝压蓝),改为只 italic 不 accent;封底"03 takeaway"在白底上做强调,用 `var(--accent-ink)`(柠檬黄 / 柠檬绿主题下会自动加深到可读)
- 目视:打开页面看封面有没有"01"等大编号——有就删 - 目视:打开页面看封面有没有"01"等大编号——有就删
### 0-C. 瑞士风大字号双约束:`min(Xvw, Yvh)` 中 Y ≥ X × 1.6 ### 0-C. 瑞士风大字号双约束:`min(Xvw, Yvh)` 中 Y ≥ X × 1.6
@@ -593,7 +593,8 @@ JS 会动态算总页数并扩展底部翻页圆点,但 `.chrome` 里的 `XX /
□ 瑞士风:封底是 `slide split` + 左 `b-accent` + ASCII canvas / 右 paper 3 条 takeaway,第 03 条用 var(--accent) □ 瑞士风:封底是 `slide split` + 左 `b-accent` + ASCII canvas / 右 paper 3 条 takeaway,第 03 条用 var(--accent)
□ 瑞士风:`grep -c "ascii-bg" index.html` ≥ 2(封面 + 封底各一) □ 瑞士风:`grep -c "ascii-bg" index.html` ≥ 2(封面 + 封底各一)
□ 瑞士风:封面没有"01"等大编号(chrome 已显示 01/N,不要重复) □ 瑞士风:封面没有"01"等大编号(chrome 已显示 01/N,不要重复)
□ 瑞士风:IKB 背景上的强调字用 `font-style:italic`,禁止用 `color:var(--accent)`(蓝压蓝) □ 瑞士风:IKB 背景上的强调字英文用 `font-style:italic`、中文用 `font-weight:400`,禁止用 `color:var(--accent)`(蓝压蓝)
□ 强调色当文字色:小字用 `var(--accent-ink)`、大字用 `var(--accent-display)`,不要直接写 `var(--accent)`(柠檬黄 / 柠檬绿主题下看不见)
内容 内容
□ 每一幕的页数比例合理(不会头重脚轻) □ 每一幕的页数比例合理(不会头重脚轻)
@@ -2,6 +2,10 @@
这是 `guizang-ppt-skill` skill 的组件手册。template.html 已经定义好了所有样式,这里只写"这个组件长什么样、怎么用"。 这是 `guizang-ppt-skill` skill 的组件手册。template.html 已经定义好了所有样式,这里只写"这个组件长什么样、怎么用"。
## 设计系统共享组件(可选)
除了下面这些模板类,模板里还内置了三个 skill 共用的 `gz-*` 组件(封面、大数字、引言、流程、对比、图框、图片网格、条目清单、页眉页脚),结构和字段见 `references/gz-components.md`(自动生成)。模板类覆盖不到时再用;字号用文档里的 `gz-t-<角色>` 类。
## 目录 ## 目录
- [基础 Slide 外壳](#基础-slide-外壳) - [基础 Slide 外壳](#基础-slide-外壳)
@@ -23,13 +27,13 @@
## 基础 Slide 外壳 ## 基础 Slide 外壳
每一页都是一个 `<section class="slide ...">`。必须包含 `data-theme` 属性(`light` 或 `dark`),JS 翻页时会根据这个属性切换背景。 每一页都是一个 `<section class="slide ...">`,用 class `light` 或 `dark` 标明深浅,JS 翻页时据此切换背景。**不要在 section 上写配色主题**:`data-theme="ink-classic"` 这类配色主题只写在 `<html>` 上(见 themes.md)。深色页里文字、线条、强调色的语义变量(`--text-*`、`--line-*`、`--accent-ink`)会自动切到深底版本。
```html ```html
<section class="slide light" data-theme="light"> <!-- 浅色页 --> <section class="slide light"> <!-- 浅色页 -->
<section class="slide dark" data-theme="dark"> <!-- 深色页 --> <section class="slide dark"> <!-- 深色页 -->
<section class="slide light hero" data-theme="light"> <!-- Hero 页:浅色 + 薄遮罩透出 WebGL --> <section class="slide light hero"> <!-- Hero 页:浅色 + 薄遮罩 -->
<section class="slide dark hero" data-theme="dark"> <!-- Hero 页:深色 + 薄遮罩 --> <section class="slide dark hero"> <!-- Hero 页:深色 + 薄遮罩 -->
``` ```
**light vs dark 的使用:交替使用**,每 2-3 页切换一次主题,避免连续超过 3 页同色。翻页时 WebGL 背景会自动在两个 shader 之间渐变过渡。 **light vs dark 的使用:交替使用**,每 2-3 页切换一次主题,避免连续超过 3 页同色。翻页时 WebGL 背景会自动在两个 shader 之间渐变过渡。
@@ -0,0 +1,289 @@
# 组件参考
由 `scripts/docs.mjs` 从 `components/render.mjs` 生成,不要手改。
用法:页面根节点写 `<html data-family="magazine|swiss" data-medium="slide|card|page" data-theme="…">`,
引入 `gz-<family>.css`、`gz-<family>-<medium>.css`、`gz-components.css`。组件只管布局和颜色,
字号来自 `gz-t-<角色>` 类,所以同一段 HTML 在 PPT、卡片、网页里会自动换成各自的字号和排法。
反色块加 `data-surface="ink|accent|paper-2"`,块内文字颜色自动切换;整张强调色或墨色的卡片 / 页面,把它加在 `<section class="poster">` 或 `<section class="slide">` 上。
强调色当文字色时不要直接写 `var(--accent)`:小字用 `.gz-c-accent`(`--accent-ink`),大字和大数字用 `.gz-c-accent-display`(`--accent-display`)。
条目清单的编号用 `gz-t-label`,大数字组件把数字角色类(如 `gz-t-num-l`)加在 `.gz-stat__value` 上,单位按它的 0.4 倍显示。
## 页眉页脚 · `chrome`
| 字段 | 说明 |
|---|---|
| `left` | 左侧文字(数组) |
| `right` | 右侧文字(数组) |
| `pos` | top \| bottom |
| `accentFirst` | 左侧第一项用强调色 |
```html
<div class="gz-chrome gz-t-meta" data-pos="top">
<div class="gz-chrome__group">
<span class="gz-chrome__accent">Issue 07</span>
<span>Numbers</span>
</div>
<div class="gz-chrome__group">
<span>02 / 08</span>
</div>
</div>
```
## 封面 · `cover`
| 字段 | 说明 |
|---|---|
| `kicker` | 眉题 |
| `title` | 标题,可用 `<br>` 断行 |
| `sub` | 副标题 |
| `meta` | 底部信息(数组) |
| `surface` | ink \| accent(可选) |
文案上限:
- `title`:card 每行 ≤ 8 字,≤ 2 行;slide 每行 ≤ 12 字,≤ 2 行;page ≤ 30 字
```html
<header class="gz-cover">
<div class="gz-cover__kicker gz-t-label">Issue 07 · AI Weekly</div>
<div class="gz-cover__body">
<h1 class="gz-cover__title gz-t-display-zh" style="--chars:7.10">模型越来越便宜<br>Agent 才刚开始</h1>
<div class="gz-cover__rule">
</div>
<p class="gz-cover__sub gz-t-lead">推理成本一年降了一个数量级,真正改变工作方式的是能连续工作的系统。</p>
</div>
<div class="gz-cover__meta gz-chrome gz-t-meta">
<div class="gz-chrome__group">
<span>歸藏</span>
<span>2026.10.30</span>
<span>内容生产 · 第 7 期</span>
</div>
</div>
</header>
```
## 大数字 · `stats`
| 字段 | 说明 |
|---|---|
| `items` | [{ label, value, unit, note, accent }],1–4 个 |
文案上限:
- `value`:数字 ≤ 4 位,单位单独写在 unit 里
```html
<div class="gz-stats" data-count="3">
<div class="gz-stat">
<div class="gz-stat__label gz-t-label">推理成本</div>
<div class="gz-stat__value gz-t-num-l">
<span class="gz-stat__num">−92</span>
<span class="gz-stat__unit">%</span>
</div>
<p class="gz-stat__note gz-t-small">同等能力的模型,每百万 token 价格</p>
</div>
<div class="gz-stat is-accent">
<div class="gz-stat__label gz-t-label">Agent 起草</div>
<div class="gz-stat__value gz-t-num-l">
<span class="gz-stat__num">87</span>
<span class="gz-stat__unit">%</span>
</div>
<p class="gz-stat__note gz-t-small">本栏目选题由 Agent 起草初稿</p>
</div>
<div class="gz-stat">
<div class="gz-stat__label gz-t-label">单次任务</div>
<div class="gz-stat__value gz-t-num-l">
<span class="gz-stat__num">3.5</span>
<span class="gz-stat__unit">h</span>
</div>
<p class="gz-stat__note gz-t-small">Agent 不间断工作的中位时长</p>
</div>
</div>
```
## 引言 · `quote`
| 字段 | 说明 |
|---|---|
| `text` | 引文,可用 `<br>` 断行 |
| `source` | 出处 |
文案上限:
- `text`:card ≤ 3 行;slide ≤ 3 行;page ≤ 80 字
```html
<figure class="gz-quote">
<blockquote class="gz-quote__text gz-t-quote">
<p>好的工具不替你做决定,<br>而是让你更快看清<br>该做什么决定。</p>
</blockquote>
<figcaption class="gz-quote__source gz-t-meta">歸藏 · Next Token 第 31 期</figcaption>
</figure>
```
## 流程 · `process`
| 字段 | 说明 |
|---|---|
| `steps` | [{ title, desc }],3–5 步 |
```html
<ol class="gz-process" style="--n:4">
<li class="gz-process__step">
<span class="gz-process__nb gz-t-label">01</span>
<h3 class="gz-process__title gz-t-heading">去 AI 味</h3>
<p class="gz-process__desc gz-t-small">先过一遍 Humanizer,统一语气。</p>
</li>
<li class="gz-process__step">
<span class="gz-process__nb gz-t-label">02</span>
<h3 class="gz-process__title gz-t-heading">锁定主题</h3>
<p class="gz-process__desc gz-t-small">读取工作区 theme.json,所有产出同一套色板。</p>
</li>
<li class="gz-process__step">
<span class="gz-process__nb gz-t-label">03</span>
<h3 class="gz-process__title gz-t-heading">并行生成</h3>
<p class="gz-process__desc gz-t-small">长文网页、小红书图文、公众号封面同时出。</p>
</li>
<li class="gz-process__step">
<span class="gz-process__nb gz-t-label">04</span>
<h3 class="gz-process__title gz-t-heading">入库归档</h3>
<p class="gz-process__desc gz-t-small">素材按栏目进展示库,可以一键做同款。</p>
</li>
</ol>
```
## 对比 · `compare`
| 字段 | 说明 |
|---|---|
| `before` | { tag, title, desc, list } |
| `after` | 同 before |
| `afterSurface` | 瑞士风可给 after 加 ink / accent 底 |
```html
<div class="gz-compare">
<section class="gz-compare__side" data-side="before">
<div class="gz-compare__tag gz-t-label">Before</div>
<h3 class="gz-compare__title gz-t-heading">三个 skill 各管各的</h3>
<ul class="gz-compare__list gz-t-small">
<li>每次重新选风格</li>
<li>配色对不上</li>
<li>字号文档和模板不一致</li>
</ul>
</section>
<div class="gz-compare__rule">
</div>
<section class="gz-compare__side" data-side="after">
<div class="gz-compare__tag gz-t-label">After</div>
<h3 class="gz-compare__title gz-t-heading">一套 token 驱动三种产出</h3>
<ul class="gz-compare__list gz-t-small">
<li>主题一次锁定</li>
<li>对比度构建时校验</li>
<li>字体按需内置</li>
</ul>
</section>
</div>
```
## 图框 · `figure`
| 字段 | 说明 |
|---|---|
| `src` | 图片地址 |
| `alt` | 替代文字 |
| `kind` | photo \| screenshot |
| `ratio` | 16x9 \| 16x10 \| 4x3 \| 3x2 \| 1x1 \| 3x4 \| 21x9 |
| `idx` | 编号,如 Fig.1 |
| `caption` | 说明 |
```html
<figure class="gz-figure" data-kind="screenshot" data-ratio="16x10">
<div class="gz-figure__well">
<img src="assets/screenshot.png" alt="">
</div>
<figcaption class="gz-figure__cap gz-t-caption">
<span class="gz-figure__idx">Fig.1</span>
<span>瑞士风 4 个主题的小红书卡片样张</span>
</figcaption>
</figure>
```
## 图片网格 · `gallery`
| 字段 | 说明 |
|---|---|
| `items` | [figure 的数据],2–6 张 |
| `cols` | 列数(幻灯片 / 网页) |
```html
<div class="gz-gallery" data-count="3">
<figure class="gz-figure" data-kind="photo" data-ratio="4x3">
<div class="gz-figure__well">
<img src="assets/forest-ink.webp" alt="">
</div>
<figcaption class="gz-figure__cap gz-t-caption">
<span class="gz-figure__idx">01</span>
<span>森林墨</span>
</figcaption>
</figure>
<figure class="gz-figure" data-kind="photo" data-ratio="4x3">
<div class="gz-figure__well">
<img src="assets/kraft-paper.webp" alt="">
</div>
<figcaption class="gz-figure__cap gz-t-caption">
<span class="gz-figure__idx">02</span>
<span>牛皮纸</span>
</figcaption>
</figure>
<figure class="gz-figure" data-kind="photo" data-ratio="4x3">
<div class="gz-figure__well">
<img src="assets/indigo-porcelain.webp" alt="">
</div>
<figcaption class="gz-figure__cap gz-t-caption">
<span class="gz-figure__idx">03</span>
<span>靛蓝瓷</span>
</figcaption>
</figure>
</div>
```
## 条目清单 · `ledger`
| 字段 | 说明 |
|---|---|
| `rows` | [{ title, note, value }],3–6 行 |
```html
<ol class="gz-ledger">
<li class="gz-ledger__row">
<span class="gz-ledger__nb gz-t-label">01</span>
<div>
<div class="gz-ledger__title gz-t-heading">共享组件库</div>
<p class="gz-ledger__note gz-t-small">九个组件覆盖三个 skill 的重叠部分</p>
</div>
<span class="gz-ledger__value gz-t-meta">进行中</span>
</li>
<li class="gz-ledger__row">
<span class="gz-ledger__nb gz-t-label">02</span>
<div>
<div class="gz-ledger__title gz-t-heading">特效库</div>
<p class="gz-ledger__note gz-t-small">WebGL 背景、ASCII 呼吸场、点阵</p>
</div>
<span class="gz-ledger__value gz-t-meta">11 月</span>
</li>
<li class="gz-ledger__row">
<span class="gz-ledger__nb gz-t-label">03</span>
<div>
<div class="gz-ledger__title gz-t-heading">迁移 PPT</div>
<p class="gz-ledger__note gz-t-small">逐页截图对比,视觉不回退</p>
</div>
<span class="gz-ledger__value gz-t-meta">11 月</span>
</li>
<li class="gz-ledger__row">
<span class="gz-ledger__nb gz-t-label">04</span>
<div>
<div class="gz-ledger__title gz-t-heading">插件与展示库</div>
<p class="gz-ledger__note gz-t-small">DeepSeek Harness</p>
</div>
<span class="gz-ledger__value gz-t-meta">12 月</span>
</li>
</ol>
```
@@ -75,7 +75,7 @@
**硬规则:** **硬规则:**
- 同一页内,字号越小的元素字重必须 ≥ 字号越大的元素(不允许 16px 正文用 300 而 1.8vw 标题用 500) - 同一页内,字号越小的元素字重必须 ≥ 字号越大的元素(不允许 16px 正文用 300 而 1.8vw 标题用 500)
- 16px 左右的小字拒绝使用 weight 300(太细不可读),最低 400,推荐 500 - 16px 左右的小字拒绝使用 weight 300(太细不可读),最低 400,推荐 500
- 封面/IKB 反白大标题内强调字用 `italic + weight 300`,不要用 accent 色(蓝压蓝看不见) - 封面/IKB 反白大标题内强调字:英文用 `italic + weight 300`;中文不用斜体(中文没有斜体字形,浏览器会硬掰变形),改用 `font-weight:400`。都不要用 accent 色(蓝压蓝看不见)
**网格**(IBM Carbon 2x Grid 改造) **网格**(IBM Carbon 2x Grid 改造)
- 16 列 grid:`grid-template-columns:repeat(16,1fr)` + `gap:16px` - 16 列 grid:`grid-template-columns:repeat(16,1fr)` + `gap:16px`
@@ -255,7 +255,7 @@ Swiss 主题有 22 个登记版式,生成时要主动展示版式系统,不要
**默认推荐:IKB 满屏 + ASCII 呼吸场** ⭐ **默认推荐:IKB 满屏 + ASCII 呼吸场** ⭐
- `<section class="slide accent">` 满屏 IKB,**不是** light 白底 - `<section class="slide accent">` 满屏 IKB,**不是** light 白底
- `.canvas-card` 内首位插入 `<canvas class="ascii-bg" aria-hidden="true">`,模板底部 IIFE 自动驱动 sin/cos 二维噪声呼吸场 - `.canvas-card` 内首位插入 `<canvas class="ascii-bg" aria-hidden="true">`,模板底部 IIFE 自动驱动 sin/cos 二维噪声呼吸场
- 主标题反白 weight 200,微强调字用斜体(`font-style:italic;font-weight:300`)而非 IKB 蓝(底已是蓝、蓝压蓝看不见) - 主标题反白 weight 200,微强调:英文用斜体(`font-style:italic;font-weight:300`),中文用 `font-weight:400`,不要用 IKB 蓝(底已是蓝、蓝压蓝看不见)
- **不要**再放编号大字"01"——chrome-min 已经标 01/NN - **不要**再放编号大字"01"——chrome-min 已经标 01/NN
- 与 P9 Closing 的 IKB 半屏配合形成"开场全 IKB ↔ 收尾半 IKB"色彩闭环 - 与 P9 Closing 的 IKB 半屏配合形成"开场全 IKB ↔ 收尾半 IKB"色彩闭环
@@ -272,13 +272,13 @@ Swiss 主题有 22 个登记版式,生成时要主动展示版式系统,不要
<div class="r">SS · 26.05.10 · 01 / NN</div> <div class="r">SS · 26.05.10 · 01 / NN</div>
</div> </div>
<div style="flex:1;padding:0;display:grid;grid-template-rows:auto 1fr auto;gap:2.6vh"> <div style="flex:1;padding:0;display:grid;grid-template-rows:auto 1fr auto;gap:2.6vh">
<div data-anim="kicker" class="t-meta" style="color:rgba(255,255,255,.78);letter-spacing:.22em">[必填] 章节英文 / Section En</div> <div data-anim="kicker" class="t-meta" style="color:rgba(var(--accent-on-rgb),.78);letter-spacing:.22em">[必填] 章节英文 / Section En</div>
<h1 data-anim="title" style="align-self:center;font-family:var(--sans),var(--sans-zh);font-weight:200;font-size:min(11.6vw,19vh);line-height:.94;letter-spacing:-.025em;color:#fff">[必填] 中文主标题<br/>(可在某字加 <span style="font-style:italic;font-weight:300">italic</span> 微强调)</h1> <h1 data-anim="title" style="align-self:center;font-family:var(--sans),var(--sans-zh);font-weight:200;font-size:min(11.6vw,19vh);line-height:.94;letter-spacing:-.025em;color:var(--accent-on)">[必填] 中文主标题<br/>(可在某字加 <span style="font-style:italic;font-weight:300">italic</span> 微强调)</h1>
<div data-anim="bottom" style="display:grid;grid-template-rows:auto auto;gap:1.6vh;border-top:1px solid rgba(255,255,255,.22);padding-top:2vh"> <div data-anim="bottom" style="display:grid;grid-template-rows:auto auto;gap:1.6vh;border-top:1px solid rgba(var(--accent-on-rgb),.22);padding-top:2vh">
<div data-anim="lead" class="lead" style="max-width:52ch;color:rgba(255,255,255,.86);font-weight:300">[必填] 一段 1-2 行的副标 / 引子,定调全场.</div> <div data-anim="lead" class="lead" style="max-width:52ch;color:rgba(var(--accent-on-rgb),.86);font-weight:300">[必填] 一段 1-2 行的副标 / 引子,定调全场.</div>
<div style="display:flex;justify-content:space-between;align-items:end"> <div style="display:flex;justify-content:space-between;align-items:end">
<div class="t-meta" style="color:rgba(255,255,255,.6)">[选填] 作者 · 日期 · 出处</div> <div class="t-meta" style="color:rgba(var(--accent-on-rgb),.6)">[选填] 作者 · 日期 · 出处</div>
<div class="t-meta" style="color:rgba(255,255,255,.6)">→ swipe / arrow keys</div> <div class="t-meta" style="color:rgba(var(--accent-on-rgb),.6)">→ swipe / arrow keys</div>
</div> </div>
</div> </div>
</div> </div>
@@ -486,7 +486,7 @@ Swiss 主题有 22 个登记版式,生成时要主动展示版式系统,不要
- 用 `<section class="slide split">` + 左半 `.half.b-accent` + ASCII canvas + 右半白底 takeaway - 用 `<section class="slide split">` + 左半 `.half.b-accent` + ASCII canvas + 右半白底 takeaway
- 与 P1 封面的全 IKB 形成"开场全 IKB ↔ 收尾半 IKB"色彩闭环 - 与 P1 封面的全 IKB 形成"开场全 IKB ↔ 收尾半 IKB"色彩闭环
- 右侧第 03 条 takeaway 用 `var(--accent)` 强调,把 IKB 蓝从左半穿到右半,完成色彩缝合 - 右侧第 03 条 takeaway 用 `var(--accent)` 强调,把 IKB 蓝从左半穿到右半,完成色彩缝合
- 大标题反白 weight 200,强调字用斜体(底已是蓝、不要再用 `var(--accent)` 标蓝) - 大标题反白 weight 200,强调字:英文斜体、中文加字重到 400(底已是蓝、不要再用 `var(--accent)` 标蓝)
**关键类**:`.slide.split` `.half.b-accent` `.ascii-bg`(IIFE 自动启动) **关键类**:`.slide.split` `.half.b-accent` `.ascii-bg`(IIFE 自动启动)
**动效 recipe**:`split-statement` — 左 ink/IKB 标题字符序列升起 → 右白半 takeaway 三条尾随 **动效 recipe**:`split-statement` — 左 ink/IKB 标题字符序列升起 → 右白半 takeaway 三条尾随
@@ -504,13 +504,13 @@ Swiss 主题有 22 个登记版式,生成时要主动展示版式系统,不要
<div class="r">CLOSING</div> <div class="r">CLOSING</div>
</div> </div>
<div data-anim="manifesto" style="display:flex;flex-direction:column;gap:2vh;position:relative;z-index:1"> <div data-anim="manifesto" style="display:flex;flex-direction:column;gap:2vh;position:relative;z-index:1">
<div class="t-meta" style="color:rgba(255,255,255,.78);letter-spacing:.22em;margin-bottom:1.6vh">MANIFESTO</div> <div class="t-meta" style="color:rgba(var(--accent-on-rgb),.78);letter-spacing:.22em;margin-bottom:1.6vh">MANIFESTO</div>
<h2 style="font-family:var(--sans),var(--sans-zh);font-size:min(8vw,14vh);line-height:.94;letter-spacing:-.025em;font-weight:200;color:#fff">[必填] Build a model.<br/>Run <span style="font-style:italic;font-weight:300">forever</span>.</h2> <h2 style="font-family:var(--sans),var(--sans-zh);font-size:min(8vw,14vh);line-height:.94;letter-spacing:-.025em;font-weight:200;color:var(--accent-on)">[必填] Build a model.<br/>Run <span style="font-style:italic;font-weight:300">forever</span>.</h2>
<div style="font-family:var(--sans),var(--sans-zh);font-size:max(13px,1vw);line-height:1.6;color:rgba(255,255,255,.82);font-weight:300;max-width:36ch;margin-top:1.4vh">[必填] 一句中英文落地注脚.</div> <div style="font-family:var(--sans),var(--sans-zh);font-size:max(13px,1vw);line-height:1.6;color:rgba(var(--accent-on-rgb),.82);font-weight:300;max-width:36ch;margin-top:1.4vh">[必填] 一句中英文落地注脚.</div>
</div> </div>
<div data-anim="signature" style="display:flex;justify-content:space-between;align-items:end;border-top:1px solid rgba(255,255,255,.22);padding-top:2vh;position:relative;z-index:1"> <div data-anim="signature" style="display:flex;justify-content:space-between;align-items:end;border-top:1px solid rgba(var(--accent-on-rgb),.22);padding-top:2vh;position:relative;z-index:1">
<div class="t-meta" style="color:rgba(255,255,255,.62)">[选填] 作者 · 头衔</div> <div class="t-meta" style="color:rgba(var(--accent-on-rgb),.62)">[选填] 作者 · 头衔</div>
<div class="t-meta" style="color:rgba(255,255,255,.62)">YY.MM.DD</div> <div class="t-meta" style="color:rgba(var(--accent-on-rgb),.62)">YY.MM.DD</div>
</div> </div>
</div> </div>
<!-- 右半 · 白底 takeaway,第 03 条用 IKB 蓝强调,首尾色彩闭环 --> <!-- 右半 · 白底 takeaway,第 03 条用 IKB 蓝强调,首尾色彩闭环 -->
@@ -729,7 +729,7 @@ Swiss 主题有 22 个登记版式,生成时要主动展示版式系统,不要
<div class="image-hero-stats" style="gap:4vw"> <div class="image-hero-stats" style="gap:4vw">
<div style="display:flex;flex-direction:column;gap:.6vh"><div style="height:1px;background:var(--ink)"></div><div class="t-meta">Metric 01</div><div style="font-family:var(--sans);font-weight:200;font-size:min(4.6vw,7.6vh);line-height:.95;letter-spacing:-.04em">12×</div><div style="height:1px;background:var(--border-subtle);margin-top:auto"></div><p class="body-sm">[必填] 指标解释</p></div> <div style="display:flex;flex-direction:column;gap:.6vh"><div style="height:1px;background:var(--ink)"></div><div class="t-meta">Metric 01</div><div style="font-family:var(--sans);font-weight:200;font-size:min(4.6vw,7.6vh);line-height:.95;letter-spacing:-.04em">12×</div><div style="height:1px;background:var(--border-subtle);margin-top:auto"></div><p class="body-sm">[必填] 指标解释</p></div>
<div style="display:flex;flex-direction:column;gap:.6vh"><div style="height:1px;background:var(--ink)"></div><div class="t-meta">Metric 02</div><div style="font-family:var(--sans);font-weight:200;font-size:min(4.6vw,7.6vh);line-height:.95;letter-spacing:-.04em">3.4h</div><div style="height:1px;background:var(--border-subtle);margin-top:auto"></div><p class="body-sm">[必填] 指标解释</p></div> <div style="display:flex;flex-direction:column;gap:.6vh"><div style="height:1px;background:var(--ink)"></div><div class="t-meta">Metric 02</div><div style="font-family:var(--sans);font-weight:200;font-size:min(4.6vw,7.6vh);line-height:.95;letter-spacing:-.04em">3.4h</div><div style="height:1px;background:var(--border-subtle);margin-top:auto"></div><p class="body-sm">[必填] 指标解释</p></div>
<div style="display:flex;flex-direction:column;gap:.6vh"><div style="height:1px;background:var(--ink)"></div><div class="t-meta">Metric 03</div><div style="font-family:var(--sans);font-weight:200;font-size:min(4.6vw,7.6vh);line-height:.95;letter-spacing:-.04em;color:var(--accent)">100%</div><div style="height:1px;background:var(--border-subtle);margin-top:auto"></div><p class="body-sm">[必填] 指标解释</p></div> <div style="display:flex;flex-direction:column;gap:.6vh"><div style="height:1px;background:var(--ink)"></div><div class="t-meta">Metric 03</div><div style="font-family:var(--sans);font-weight:200;font-size:min(4.6vw,7.6vh);line-height:.95;letter-spacing:-.04em;color:var(--accent-display)">100%</div><div style="height:1px;background:var(--border-subtle);margin-top:auto"></div><p class="body-sm">[必填] 指标解释</p></div>
</div> </div>
</div> </div>
</div> </div>
@@ -771,7 +771,7 @@ Swiss 主题有 22 个登记版式,生成时要主动展示版式系统,不要
</div> </div>
<div class="swiss-img-split align-image-bottom" data-anim="up"> <div class="swiss-img-split align-image-bottom" data-anim="up">
<div class="swiss-img-copy"> <div class="swiss-img-copy">
<div class="t-cat" style="color:var(--accent)">Why it matters</div> <div class="t-cat" style="color:var(--accent-ink)">Why it matters</div>
<p class="lead" style="font-weight:300;max-width:36ch">[必填] 2-3 行解释图片与论点的关系.</p> <p class="lead" style="font-weight:300;max-width:36ch">[必填] 2-3 行解释图片与论点的关系.</p>
<div class="body" style="font-weight:300;color:var(--text-secondary)">[必填] 可以放 2-3 条短 bullet 或一段说明,保持左对齐和充足留白.</div> <div class="body" style="font-weight:300;color:var(--text-secondary)">[必填] 可以放 2-3 条短 bullet 或一段说明,保持左对齐和充足留白.</div>
</div> </div>
@@ -58,7 +58,7 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
### D. 主题色与主题节奏 ### D. 主题色与主题节奏
- 主题色从 `references/themes.md` 的 5 套预设里选一套,不允许自定义 hex 值 - 主题色从 `references/themes.md` 的 5 套预设里选一套(改 `<html data-theme>`);用户有品牌色时按 themes.md 的品牌色流程生成,不直接写 hex 值
- 主题节奏(每页用 light / dark / hero light / hero dark 哪一个)在下文"主题节奏规划"一节有硬规则,生成前必读 - 主题节奏(每页用 light / dark / hero light / hero dark 哪一个)在下文"主题节奏规划"一节有硬规则,生成前必读
- 两件事都要在挑布局之前决定,避免返工 - 两件事都要在挑布局之前决定,避免返工
@@ -4,115 +4,86 @@
--- ---
<!-- GZ:THEMES:START · guizang-design 生成,不要手改;改主题请改 guizang-design/tokens/palettes.json 后运行 scripts/sync-skill.mjs -->
## 使用方法 ## 使用方法
1. 问用户选哪套(或基于内容推荐一套) 全部主题已内置在模板的 `GZ:TOKENS` 区块里。选定主题后,只改拷贝好的模板第一行 `<html>` 上的 `data-theme`:
2. 打开 `assets/template-swiss.html` 的 `<style>` 块
3. 找到开头的 `:root{` 块
4. **整体替换**标有"主题色"注释的所有变量:`--paper` / `--paper-rgb` / `--ink` / `--ink-rgb` / `--grey-1` / `--grey-2` / `--grey-3` / `--accent` / `--accent-rgb` / `--accent-on`
5. 其他 CSS 都走 `var(--...)`,无需任何其他改动
--- ```html
<html lang="zh-CN" data-family="swiss" data-medium="slide" data-theme="lemon">
## 🔵 克莱因蓝 (IKB · International Klein Blue)
**适合**:通用场合、商业发布、AI/科技产品、设计领域分享。最经典的瑞士风配色,绝不出错。
**调性**:纯白底 + IKB 克莱因蓝,极致冷静、理性、有学术感,像 Helvetica Forever 或 Massimo Vignelli 的作品集。
```css
--paper:#fafaf8;
--paper-rgb:250,250,248;
--ink:#0a0a0a;
--ink-rgb:10,10,10;
--grey-1:#f0f0ee;
--grey-2:#d4d4d2;
--grey-3:#737373;
--accent:#002FA7;
--accent-rgb:0,47,167;
--accent-on:#ffffff;
``` ```
**使用要点**: 不要改 `GZ:TOKENS` 区块,也不要在页面里写颜色值。文字、线条、强调色一律用语义变量:
- IKB 是高饱和深蓝,在大色块(如 `.accent-block`)上极有视觉冲击
- KPI 数字加 `.accent` 类用蓝色,但不要满屏蓝——IKB 一旦泛滥就掉档 | 变量 | 用途 |
- 推荐配合 `dark` 主题页交替使用,黑底高亮 IKB 同样高级 |---|---|
| `--paper` / `--ink` | 底色 / 主文字 |
| `--text-body` / `--text-secondary` / `--text-helper` | 正文 / 次要 / 辅助文字,对比度已校验,代替 opacity |
| `--line` / `--line-strong` / `--line-subtle` | 分割线 |
| `--accent` | 强调色:色块、细线、圆点、高亮条 |
| `--accent-ink` | 强调色用作**小字**颜色时用它(浅色强调色会自动加深到可读) |
| `--accent-display` | 强调色用作**大字**(标题、KPI 数字)颜色时用它 |
| `--accent-on` / `--accent-on-rgb` | 强调色色块上的文字颜色(黄、绿上是墨色,蓝、橙上是白色) |
### 用户给了品牌色或 logo 主色
不要直接使用这个颜色。运行:
```bash
node <SKILL_ROOT>/scripts/gz-theme.mjs brand "#4D6BFE" --family swiss
```
把输出的 `[data-theme="brand"]{…}` 贴到 deck `<style>` 的末尾,再把 `data-theme` 改成 `brand`。
脚本会把颜色收进本风格精调色板的明度 / 彩度区间,并检查 WCAG 与 APCA 对比度;不达标时会说明原因,这时从下面的预设里选。杂志风会把彩度压到 0.12 以内,高饱和的品牌色会变得更克制(例如 #FF2442 → #b75a59,第一行注释会写明调整);需要保留鲜艳度时用瑞士风。
--- ---
## 🟡 柠檬黄 (Lemon · Cadmium Yellow) ## 🔵 克莱因蓝 · `ikb`(默认)
**适合**:通用场合、商业发布、AI/科技产品、设计领域分享。最经典的瑞士风配色,绝不出错。
**调性**:纯白底 + IKB 克莱因蓝,极致冷静、理性、有学术感,像 Helvetica Forever 或 Massimo Vignelli 的作品集。
| paper | ink | accent | accent-ink | accent-on | 正文对比度 |
|---|---|---|---|---|---|
| `#fafaf8` | `#0a0a0a` | `#002FA7` | `#002FA7` | `#ffffff` | 14.7 / 99 |
---
## 🟡 柠檬黄 · `lemon`
**适合**:年轻、运动、零售、消费品、活力主题、Y2K 复古设计。 **适合**:年轻、运动、零售、消费品、活力主题、Y2K 复古设计。
**调性**:浅米白底 + 柠檬黄,鲜亮、有活力、警示感强,像 IKEA 或 Beck Design 的视觉语言。 **调性**:浅米白底 + 柠檬黄,鲜亮、有活力、警示感强,像 IKEA 或 Beck Design 的视觉语言。
```css | paper | ink | accent | accent-ink | accent-on | 正文对比度 |
--paper:#fafaf8; |---|---|---|---|---|---|
--paper-rgb:250,250,248; | `#fafaf8` | `#0a0a0a` | `#FFD500` | `#7a5f00` | `#0a0a0a` | 14.7 / 99 |
--ink:#0a0a0a;
--ink-rgb:10,10,10;
--grey-1:#f0f0ee;
--grey-2:#d4d4d2;
--grey-3:#737373;
--accent:#FFD500;
--accent-rgb:255,213,0;
--accent-on:#0a0a0a;
```
**使用要点**:
- 柠檬黄属于浅色高饱和,**`.accent-on` 必须用纯黑**(不是白)才能保证可读性
- 黄色色块上不要放白字——会糊掉
- 柠檬黄做单字符高亮(`.mark` / `.underline-accent`)效果最强
--- ---
## 🟢 柠檬绿 (Lemon Green · Highlighter Green) ## 🟢 柠檬绿 · `lemon-green`
**适合**:生态、可持续、健康、新兴科技、Z 世代品牌、AI 创业项目。 **适合**:生态、可持续、健康、新兴科技、Z 世代品牌、AI 创业项目。
**调性**:浅米白底 + 荧光柠檬绿,有未来感、年轻、当代,像 Acne Studios 或 Off-White 的视觉。 **调性**:浅米白底 + 荧光柠檬绿,有未来感、年轻、当代,像 Acne Studios 或 Off-White 的视觉。
```css | paper | ink | accent | accent-ink | accent-on | 正文对比度 |
--paper:#fafaf8; |---|---|---|---|---|---|
--paper-rgb:250,250,248; | `#fafaf8` | `#0a0a0a` | `#C5E803` | `#4a6400` | `#0a0a0a` | 14.7 / 99 |
--ink:#0a0a0a;
--ink-rgb:10,10,10;
--grey-1:#f0f0ee;
--grey-2:#d4d4d2;
--grey-3:#737373;
--accent:#C5E803;
--accent-rgb:197,232,3;
--accent-on:#0a0a0a;
```
**使用要点**:
- 荧光绿和黄色一样属于浅色,**`.accent-on` 必须用纯黑**
- 屏幕显色比印刷漂亮,适合演讲投影场景
- 推荐用于"新兴技术"、"未来"主题
--- ---
## 🟠 安全橙 (Safety Orange) ## 🟠 安全橙 · `safety-orange`
**适合**:工业、警示、运动、施工、汽车工业、技术发布会的"警告/重点"页。 **适合**:工业、警示、运动、施工、汽车工业、技术发布会的「警告/重点」页。
**调性**:浅米白底 + 安全橙,工业感、紧迫感、视觉锚点感,像 Saul Bass 海报或 Highway Gothic 标识系统。 **调性**:浅米白底 + 安全橙,工业感、紧迫感、视觉锚点感,像 Saul Bass 海报或 Highway Gothic 标识系统。
```css | paper | ink | accent | accent-ink | accent-on | 正文对比度 |
--paper:#fafaf8; |---|---|---|---|---|---|
--paper-rgb:250,250,248; | `#fafaf8` | `#0a0a0a` | `#FF6B35` | `#c2410c` | `#ffffff` | 14.7 / 99 |
--ink:#0a0a0a;
--ink-rgb:10,10,10;
--grey-1:#f0f0ee;
--grey-2:#d4d4d2;
--grey-3:#737373;
--accent:#FF6B35;
--accent-rgb:255,107,53;
--accent-on:#ffffff;
```
**使用要点**: > 强调色色块上的文字只用大号粗体(APCA 59)。
- 橙色介于浅色和深色之间,**白字勉强能读但建议加粗**(`font-weight:600` 以上)
- 工业感强,适合涉及"警告"、"决策"、"转折点"的内容
- 不建议整页 `.accent` 模式,橙色满屏会过于刺眼,做局部高亮即可
--- ---
<!-- GZ:THEMES:END -->
## 推荐选择参考 ## 推荐选择参考
@@ -137,7 +108,7 @@
## ❌ 不要做的事 ## ❌ 不要做的事
- ❌ **不允许混搭**(例如 IKB 蓝 + 柠檬黄同时出现作高亮)——彻底违反瑞士风"单一锚点色"原则 - ❌ **不允许混搭**(例如 IKB 蓝 + 柠檬黄同时出现作高亮)——彻底违反瑞士风"单一锚点色"原则
- ❌ **不允许用户自定义任意 hex 值**——委婉拒绝,展示 4 套预设让选 - ❌ **不要把用户给的 hex 值直接写进页面**——用 `scripts/gz-theme.mjs --family swiss` 生成受约束的品牌主题;灰色或彩度太低的颜色会被拒绝,这时展示 4 套预设让选
- ❌ **不要改灰阶变量**——`--paper` / `--grey-1/2/3` / `--ink` 跨主题统一,只换 accent - ❌ **不要改灰阶变量**——`--paper` / `--grey-1/2/3` / `--ink` 跨主题统一,只换 accent
- ❌ **不要用渐变**——瑞士风拒绝任何渐变,所有色块必须纯色 - ❌ **不要用渐变**——瑞士风拒绝任何渐变,所有色块必须纯色
- ❌ **不要给 accent 加阴影 / 圆角 / 透明度**——直角、纯色、不透明,这是瑞士风的硬规则 - ❌ **不要给 accent 加阴影 / 圆角 / 透明度**——直角、纯色、不透明,这是瑞士风的硬规则
@@ -1,99 +1,101 @@
# 主题色预设(Themes) # 主题色预设(Themes)
5 套精心调配的主题色板,保证"电子杂志 × 电子墨水"的美学不垮。**不允许用户自定义颜色——色彩搭配错了画面瞬间变丑**,只从以下预设中挑选。 5 套精心调配的主题色板,保证"电子杂志 × 电子墨水"的美学不垮。**不直接使用用户给的颜色值——色彩搭配错了画面瞬间变丑**。用户有品牌色或 logo 主色时,走下方「用户给了品牌色」流程生成受约束的主题;否则只从以下预设中挑选。
--- ---
<!-- GZ:THEMES:START · guizang-design 生成,不要手改;改主题请改 guizang-design/tokens/palettes.json 后运行 scripts/sync-skill.mjs -->
## 使用方法 ## 使用方法
1. 问用户选哪套(或基于内容推荐一套) 全部主题已内置在模板的 `GZ:TOKENS` 区块里。选定主题后,只改拷贝好的模板第一行 `<html>` 上的 `data-theme`:
2. 打开 `assets/template.html` 的 `<style>` 块
3. 找到开头的 `:root{` 块
4. **整体替换**标有"主题色"注释的那几行 `--ink` / `--ink-rgb` / `--paper` / `--paper-rgb` / `--paper-tint` / `--ink-tint`
5. 其他 CSS 都走 `var(--...)`,无需任何其他改动
--- ```html
<html lang="zh-CN" data-family="magazine" data-medium="slide" data-theme="indigo-porcelain">
## 🖋 墨水经典 (Monocle 默认)
**适合**:通用分享、商业发布、科技产品、任何场景都安全的默认选择。
**调性**:纯墨黑 + 暖米白,杂志感最强,Monocle / Apricot / A Book Apart 风。
```css
--ink:#0a0a0b;
--ink-rgb:10,10,11;
--paper:#f1efea;
--paper-rgb:241,239,234;
--paper-tint:#e8e5de;
--ink-tint:#18181a;
``` ```
--- 不要改 `GZ:TOKENS` 区块,也不要在页面里写颜色值。文字、线条、强调色一律用语义变量:
## 🌊 靛蓝瓷 (Indigo Porcelain) | 变量 | 用途 |
|---|---|
| `--paper` / `--ink` | 底色 / 主文字 |
| `--text-body` / `--text-secondary` / `--text-helper` | 正文 / 次要 / 辅助文字,对比度已校验,代替 opacity |
| `--line` / `--line-strong` / `--line-subtle` | 分割线 |
| `--accent` | 强调色:色块、细线、圆点、高亮条 |
| `--accent-ink` | 强调色用作**小字**颜色时用它(浅色强调色会自动加深到可读) |
| `--accent-display` | 强调色用作**大字**(标题、KPI 数字)颜色时用它 |
| `--accent-on` / `--accent-on-rgb` | 强调色色块上的文字颜色(黄、绿上是墨色,蓝、橙上是白色) |
**适合**:科技/研究/数据分享、工程师文化、深度内容、技术发布会。 ### 用户给了品牌色或 logo 主色
**调性**:深靛蓝 + 瓷白,冷静、理性、有深度,像学术期刊或蓝印花瓷器。
```css 不要直接使用这个颜色。运行:
--ink:#0a1f3d;
--ink-rgb:10,31,61; ```bash
--paper:#f1f3f5; node <SKILL_ROOT>/scripts/gz-theme.mjs brand "#4D6BFE" --family magazine
--paper-rgb:241,243,245;
--paper-tint:#e4e8ec;
--ink-tint:#152a4a;
``` ```
--- 把输出的 `[data-theme="brand"]{…}` 贴到 deck `<style>` 的末尾,再把 `data-theme` 改成 `brand`。
脚本会把颜色收进本风格精调色板的明度 / 彩度区间,并检查 WCAG 与 APCA 对比度;不达标时会说明原因,这时从下面的预设里选。杂志风会把彩度压到 0.12 以内,高饱和的品牌色会变得更克制(例如 #FF2442 → #b75a59,第一行注释会写明调整);需要保留鲜艳度时用瑞士风。
## 🌿 森林墨 (Forest Ink)
**适合**:自然/可持续/文化/非虚构内容、户外品牌、环保主题。
**调性**:深森林绿 + 象牙,沉稳、有呼吸感,像旧版《国家地理》。
```css
--ink:#1a2e1f;
--ink-rgb:26,46,31;
--paper:#f5f1e8;
--paper-rgb:245,241,232;
--paper-tint:#ece7da;
--ink-tint:#253d2c;
```
--- ---
## 🍂 牛皮纸 (Kraft Paper) ## 🖋 墨水经典 · `ink-classic`(默认)
**适合**:怀旧/人文/阅读/历史/文学分享、独立杂志、手作品牌。 **适合**:通用分享、商业发布、科技产品、任何场景都安全的默认选择。
**调性**:深棕 + 暖米,像牛皮信封或老笔记本,温暖、有年代感。 **调性**:纯墨黑 + 暖米白,杂志感最强,Monocle / Apricot / A Book Apart 风。
```css | paper | ink | accent | accent-ink | accent-on | 正文对比度 |
--ink:#2a1e13; |---|---|---|---|---|---|
--ink-rgb:42,30,19; | `#f1efea` | `#0a0a0b` | `#111111` | `#111111` | `#f1efea` | 12.3 / 91 |
--paper:#eedfc7;
--paper-rgb:238,223,199;
--paper-tint:#e0d0b6;
--ink-tint:#3a2a1d;
```
--- ---
## 🌙 沙丘 (Dune) ## 🌊 靛蓝瓷 · `indigo-porcelain`
**适合**:艺术/设计/创意/时尚分享、画廊手册、审美优先的私享会。 **适合**:科技/研究/数据分享、工程师文化、深度内容、技术发布会。
**调性**:炭灰 + 沙色,克制、高级、中性,像沙漠黄昏或建筑设计图册。 **调性**:深靛蓝 + 瓷白,冷静、理性、有深度,像学术期刊或蓝印花瓷器。
```css | paper | ink | accent | accent-ink | accent-on | 正文对比度 |
--ink:#1f1a14; |---|---|---|---|---|---|
--ink-rgb:31,26,20; | `#f1f3f5` | `#0a1f3d` | `#315d93` | `#315d93` | `#f1f3f5` | 9.3 / 87 |
--paper:#f0e6d2;
--paper-rgb:240,230,210;
--paper-tint:#e3d7bf;
--ink-tint:#2d2620;
```
--- ---
## 🌿 森林墨 · `forest-ink`
**适合**:自然/可持续/文化/非虚构内容、户外品牌、环保主题。
**调性**:深森林绿 + 象牙,沉稳、有呼吸感,像旧版《国家地理》。
| paper | ink | accent | accent-ink | accent-on | 正文对比度 |
|---|---|---|---|---|---|
| `#f5f1e8` | `#1a2e1f` | `#2e6b4f` | `#2e6b4f` | `#f5f1e8` | 8.0 / 83 |
---
## 🍂 牛皮纸 · `kraft-paper`
**适合**:怀旧/人文/阅读/历史/文学分享、独立杂志、手作品牌。
**调性**:深棕 + 暖米,像牛皮信封或老笔记本,温暖、有年代感。
| paper | ink | accent | accent-ink | accent-on | 正文对比度 |
|---|---|---|---|---|---|
| `#eedfc7` | `#2a1e13` | `#9b5a2e` | `#804212` | `#eedfc7` | 8.1 / 77 |
---
## 🌙 沙丘 · `dune`
**适合**:艺术/设计/创意/时尚分享、画廊手册、审美优先的私享会。
**调性**:炭灰 + 沙色,克制、高级、中性,像沙漠黄昏或建筑设计图册。
| paper | ink | accent | accent-ink | accent-on | 正文对比度 |
|---|---|---|---|---|---|
| `#f0e6d2` | `#1f1a14` | `#8f7650` | `#705833` | `#f0e6d2` | 9.2 / 82 |
> 强调色色块上的文字只用大号粗体(APCA 60)。
---
<!-- GZ:THEMES:END -->
## 推荐选择参考 ## 推荐选择参考
| 如果是... | 推荐主题 | | 如果是... | 推荐主题 |
@@ -116,7 +118,7 @@
## ❌ 不要做的事 ## ❌ 不要做的事
- ❌ **不允许混搭**(例如 ink 取墨水经典的,paper 取沙丘的)——会彻底违和 - ❌ **不允许混搭**(例如 ink 取墨水经典的,paper 取沙丘的)——会彻底违和
- ❌ **不允许用户随便给一个 hex 值**——需委婉拒绝并展示 5 套预设让选 - ❌ **不要把用户给的 hex 值直接写进页面**——用 `scripts/gz-theme.mjs` 生成受约束的品牌主题;脚本判定不达标时,展示 5 套预设让选
- ❌ **不要直接修改 template.html 其他地方的颜色**——所有散落 rgba 都走 var,改 :root 一处即可 - ❌ **不要直接修改 template.html 里的颜色**——所有颜色都走语义变量,换主题只改 `<html>` 的 `data-theme` 一处
选定主题后在 skill 对话中告诉用户:"用 🖋 墨水经典 / 🌊 靛蓝瓷 ..."并在 deck 项目记录里备注,方便后续迭代时保持一致。 选定主题后在 skill 对话中告诉用户:"用 🖋 墨水经典 / 🌊 靛蓝瓷 ..."并在 deck 项目记录里备注,方便后续迭代时保持一致。
@@ -0,0 +1,434 @@
#!/usr/bin/env node
// 歸藏设计系统 · 品牌主题生成(guizang-design v0.1.0 生成,不要手改)
//
// node scripts/gz-theme.mjs brand "#4D6BFE" [--family magazine|swiss] [--id brand]
//
// 用户给品牌色或 logo 主色时用:颜色被收进现有精调色板的明度 / 彩度区间,并通过 WCAG + APCA 对比度检查。
// 输出一段 [data-theme="<id>"]{…} CSS,贴进 deck 的 <style> 末尾,再把 <html> 的 data-theme 改成 <id>。
// 不达标时不输出主题,只给出原因。
// 色彩基础:sRGB ↔ OKLab / OKLCH、sRGB 色域映射、WCAG 2 对比度、APCA 对比度。
// 设计系统里所有「科学配色」都建立在这几个函数上:
// 用 OKLCH 描述颜色(明度 L、彩度 C、色相 H 互相独立,调一个不会把另外两个带偏),
// 用 WCAG + APCA 两套对比度卡住可读性。纯函数、无依赖。
const clamp = (x, lo, hi) => Math.min(hi, Math.max(lo, x))
// ---------- hex ----------
function parseHex(hex) {
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(String(hex).trim())
if (!m) throw new TypeError(`不是合法的 hex 颜色:${hex}`)
let h = m[1]
if (h.length === 3) h = h.replace(/./g, (c) => c + c)
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255)
}
function toHex(rgb) {
return '#' + rgb.map((c) => Math.round(clamp(c, 0, 1) * 255).toString(16).padStart(2, '0')).join('')
}
// ---------- sRGB 传递函数 ----------
const toLinear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)
const fromLinear = (c) => (c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055)
// ---------- OKLab(Björn Ottosson, 2020) ----------
function rgbToOklab([r, g, b]) {
const R = toLinear(r), G = toLinear(g), B = toLinear(b)
const l = Math.cbrt(0.4122214708 * R + 0.5363325363 * G + 0.0514459929 * B)
const m = Math.cbrt(0.2119034982 * R + 0.6806995451 * G + 0.1073969566 * B)
const s = Math.cbrt(0.0883024619 * R + 0.2817188376 * G + 0.6299787005 * B)
return [
0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s,
]
}
// 返回未裁剪的 sRGB,可能超出 [0,1],由调用方判断是否在色域内
function oklabToRgb([L, a, b]) {
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3
const s = (L - 0.0894841775 * a - 1.2914855480 * b) ** 3
return [
fromLinear(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
fromLinear(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
fromLinear(-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s),
]
}
// ---------- OKLCH ----------
function oklabToOklch([L, a, b]) {
const C = Math.hypot(a, b)
let H = (Math.atan2(b, a) * 180) / Math.PI
if (H < 0) H += 360
return { l: L, c: C, h: C < 1e-4 ? 0 : H }
}
function oklchToOklab({ l, c, h }) {
const r = (h * Math.PI) / 180
return [l, c * Math.cos(r), c * Math.sin(r)]
}
const hexToOklch = (hex) => oklabToOklch(rgbToOklab(parseHex(hex)))
const inGamut = (rgb, eps = 1e-6) => rgb.every((c) => c >= -eps && c <= 1 + eps)
// 保持明度和色相,二分降低彩度直到落进 sRGB。和 CSS Color 4 的思路一致,足够用于生成色板
function oklchToHex({ l, c, h }) {
l = clamp(l, 0, 1)
let rgb = oklabToRgb(oklchToOklab({ l, c, h }))
if (!inGamut(rgb)) {
let lo = 0, hi = c
while (hi - lo > 1e-5) {
const mid = (lo + hi) / 2
if (inGamut(oklabToRgb(oklchToOklab({ l, c: mid, h })))) lo = mid
else hi = mid
}
rgb = oklabToRgb(oklchToOklab({ l, c: lo, h }))
}
return toHex(rgb)
}
function maxChroma(l, h) {
let lo = 0, hi = 0.5
while (hi - lo > 1e-5) {
const mid = (lo + hi) / 2
if (inGamut(oklabToRgb(oklchToOklab({ l, c: mid, h })))) lo = mid
else hi = mid
}
return lo
}
// ---------- 对比度 ----------
function luminance(hex) {
const [r, g, b] = parseHex(hex).map(toLinear)
return 0.2126 * r + 0.7152 * g + 0.0722 * b
}
// WCAG 2.x:1–21。正文 ≥ 4.5,大字 ≥ 3
function wcag(fg, bg) {
const a = luminance(fg), b = luminance(bg)
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05)
}
// APCA (SAPC 0.0.98G-4g):返回 Lc,深字浅底为正、浅字深底为负。
// 参考阈值:正文 |Lc| ≥ 75,大号正文 ≥ 60,标题 ≥ 45,不承担阅读的装饰 ≥ 30。
// WCAG 2 对深色背景和中等亮度颜色偏差很大,APCA 更接近人眼,两者都看。
function apca(fg, bg) {
const y = (hex) => {
const [r, g, b] = parseHex(hex).map((c) => c ** 2.4)
let Y = 0.2126729 * r + 0.7151522 * g + 0.072175 * b
if (Y < 0.022) Y += (0.022 - Y) ** 1.414
return Y
}
const Yt = y(fg), Yb = y(bg)
if (Math.abs(Yb - Yt) < 0.0005) return 0
if (Yb > Yt) {
const s = (Yb ** 0.56 - Yt ** 0.57) * 1.14
return s < 0.1 ? 0 : (s - 0.027) * 100
}
const s = (Yb ** 0.65 - Yt ** 0.62) * 1.14
return s > -0.1 ? 0 : (s + 0.027) * 100
}
// 两个颜色在 OKLab 里的距离(ΔEok,×100 便于阅读,≈2 以下肉眼难分)
function deltaE(a, b) {
const [L1, a1, b1] = rgbToOklab(parseHex(a))
const [L2, a2, b2] = rgbToOklab(parseHex(b))
return Math.hypot(L1 - L2, a1 - a2, b1 - b2) * 100
}
const formatOklch = ({ l, c, h }) => `oklch(${(l * 100).toFixed(1)}% ${c.toFixed(4)} ${h.toFixed(1)})`
// 从色板派生完整主题:文字角色、线条、accent-on、accent-ink、-rgb 三元组。
// 派生规则对所有主题一样,自定义品牌主题也走这里,所以「每套主题都可读」是由规则保证的,不靠逐个手调。
//
// 文字角色沿用现有模板的层级比例(PPT / 卡片里正文 .82、次要 .72 这类透明度),
// 但输出实色而不是透明度:导出 PDF 和截图时透明叠加会出问题,实色也能直接算对比度。
// 比例混出来的颜色对比度不够时,沿 OKLab 继续往墨色方向推,直到达到该角色的下限。
// APCA |Lc| 下限。参考 APCA 的使用建议:正文 75、大号正文 / 次要文字 60、标题 45、非阅读元素 15–30
const ROLE_FLOORS = {
textBody: { apca: 75, wcag: 7 },
textSecondary: { apca: 60, wcag: 4.5 },
textHelper: { apca: 60, wcag: 4.5 },
accentInk: { apca: 60, wcag: 4.5 },
// 色块上的正文级文字(≥ 16px):WCAG 4.5 + APCA 45。饱和中等明度的底色(安全橙)上 APCA 会低估深色字,
// WCAG 会高估白字,两个都要过
onText: { apca: 45, wcag: 4.5 },
// 大字(标题、KPI 大数字)只看 APCA 标题线 45:WCAG 对明亮饱和色(安全橙)严重低估可读性
accentDisplay: { apca: 45, wcag: 0 },
}
// 大字用的强调色:原色够大字标准就用原色(保持设计),不够就退到 accent-ink(柠檬黄这类浅色强调色)
const accentDisplay = (accent, accentInk, paper) => (passes(accent, paper, ROLE_FLOORS.accentDisplay) ? accent : accentInk)
// 在 OKLab 里线性混合:t=0 得到 a,t=1 得到 b
function mix(a, b, t) {
const A = rgbToOklab(parseHex(a))
const B = rgbToOklab(parseHex(b))
return toHex(oklabToRgb(A.map((v, i) => v + (B[i] - v) * t)))
}
const rgbTriplet = (hex) => parseHex(hex).map((c) => Math.round(c * 255)).join(',')
const passes = (fg, bg, floor) => Math.abs(apca(fg, bg)) >= floor.apca && wcag(fg, bg) >= floor.wcag
// 从 paper 往 ink 混,比例 t 起步;不达标就加大 t
function inkRole(ink, paper, t, floor) {
for (let k = t; k <= 1.0001; k += 0.01) {
const c = mix(paper, ink, Math.min(k, 1))
if (!floor || passes(c, paper, floor)) return { hex: c, ratio: +Math.min(k, 1).toFixed(2), adjusted: k > t + 1e-9 }
}
return { hex: ink, ratio: 1, adjusted: true }
}
// 保持色相,调整明度直到能当文字用:浅底往暗调,深底往亮调
function textSafeAccent(accent, paper, floor = ROLE_FLOORS.accentInk) {
if (passes(accent, paper, floor)) return { hex: accent, adjusted: false }
const { l, c, h } = hexToOklch(accent)
const dark = hexToOklch(paper).l < 0.5
for (let step = 1; step <= 200; step++) {
const L = dark ? l + step * 0.005 : l - step * 0.005
if (L <= 0 || L >= 1) break
const hex = oklchToHex({ l: L, c, h })
if (passes(hex, paper, floor)) return { hex, adjusted: true }
}
return { hex: dark ? '#ffffff' : '#000000', adjusted: true }
}
// 在 ink 和 paper 里选对比更强的那个当 accent 上的文字色
function onColor(bg, candidates) {
return candidates.reduce((best, c) => (Math.abs(apca(c, bg)) > Math.abs(apca(best, bg)) ? c : best))
}
// 色块上的正文级文字(高亮块正文、标签):设计指定的 accent-on 达到 ROLE_FLOORS.onText 就用它;
// 不达标(安全橙、中等明度的品牌绿上的白字)换成达标的候选;都不达标取 WCAG 最高的
function onText(bg, preferred, candidates) {
const floor = ROLE_FLOORS.onText
if (passes(preferred, bg, floor)) return preferred
return candidates.find((c) => passes(c, bg, floor)) || candidates.reduce((best, c) => (wcag(c, bg) > wcag(best, bg) ? c : best))
}
// 强调色小字既出现在纸色上,也出现在 paper-2 / 灰底卡片上:按两者里对比更低的那个底色来调
const harderBg = (fg, bgs) => bgs.reduce((a, b) => (Math.abs(apca(fg, b)) < Math.abs(apca(fg, a)) ? b : a))
function deriveMagazine(t) {
const roles = {
textBody: inkRole(t.ink, t.paper, 0.82, ROLE_FLOORS.textBody),
textSecondary: inkRole(t.ink, t.paper, 0.72, ROLE_FLOORS.textSecondary),
// 手调的 muted 达标就用它,否则从 paper→ink 派生一个达标的
textHelper: passes(t.muted, t.paper, ROLE_FLOORS.textHelper)
? { hex: t.muted, adjusted: false }
: inkRole(t.ink, t.paper, 0.55, ROLE_FLOORS.textHelper),
lineStrong: inkRole(t.ink, t.paper, 0.45),
line: inkRole(t.ink, t.paper, 0.22),
lineSubtle: inkRole(t.ink, t.paper, 0.12),
accentInk: textSafeAccent(t.accent, harderBg(t.accent, [t.paper, t.paper2])),
}
return {
vars: {
'--paper': t.paper, '--paper-2': t.paper2, '--ink': t.ink, '--muted': t.muted,
'--accent': t.accent, '--accent-soft': t.accentSoft,
'--accent-on': onColor(t.accent, [t.ink, t.paper]),
'--accent-ink': roles.accentInk.hex,
'--accent-display': accentDisplay(t.accent, roles.accentInk.hex, t.paper),
// 反色块(以墨色为底)上的强调色文字:同色相提亮到可读,墨水经典的 #111 会变成浅灰
'--accent-ink-inverse': textSafeAccent(t.accent, t.ink).hex,
'--text-primary': t.ink,
'--text-body': roles.textBody.hex,
'--text-secondary': roles.textSecondary.hex,
'--text-helper': roles.textHelper.hex,
'--line-strong': roles.lineStrong.hex,
'--line': roles.line.hex,
'--line-subtle': roles.lineSubtle.hex,
'--ink-rgb': rgbTriplet(t.ink), '--paper-rgb': rgbTriplet(t.paper), '--accent-rgb': rgbTriplet(t.accent),
'--accent-on-rgb': rgbTriplet(onColor(t.accent, [t.ink, t.paper])),
'--accent-on-text': onText(t.accent, onColor(t.accent, [t.ink, t.paper]), [t.ink, t.paper, '#ffffff']),
'--accent-on-text-rgb': rgbTriplet(onText(t.accent, onColor(t.accent, [t.ink, t.paper]), [t.ink, t.paper, '#ffffff'])),
},
roles,
}
}
function deriveSwiss(n, t) {
const textBody = inkRole(n.ink, n.paper, 0.86, ROLE_FLOORS.textBody)
return {
vars: {
'--paper': n.paper, '--paper-2': n.grey1, '--ink': n.ink, '--muted': n.grey3,
'--grey-1': n.grey1, '--grey-2': n.grey2, '--grey-3': n.grey3,
'--accent': t.accent, '--accent-on': t.accentOn, '--accent-ink': t.accentInk,
'--accent-bright': t.accentBright, '--accent-deep': t.accentDeep,
'--accent-display': accentDisplay(t.accent, t.accentInk, n.paper),
// 墨色块上的强调色文字:优先用手调的 accent-bright(IKB 的亮蓝),不够再按规则提亮
'--accent-ink-inverse': textSafeAccent(t.accentBright, n.ink).hex,
'--accent-soft': mix(n.paper, t.accent, 0.14),
'--text-primary': n.ink,
'--text-body': textBody.hex,
'--text-secondary': n.textSecondary,
'--text-helper': n.grey3,
'--text-placeholder': n.textPlaceholder,
'--text-on-color': t.accentOn,
'--line-strong': n.textPlaceholder,
'--line': n.grey2,
'--line-subtle': n.borderSubtle,
'--ink-rgb': rgbTriplet(n.ink), '--paper-rgb': rgbTriplet(n.paper), '--accent-rgb': rgbTriplet(t.accent),
'--accent-on-rgb': rgbTriplet(t.accentOn),
'--accent-on-text': onText(t.accent, t.accentOn, [n.ink, '#ffffff']),
'--accent-on-text-rgb': rgbTriplet(onText(t.accent, t.accentOn, [n.ink, '#ffffff'])),
},
roles: { textBody },
}
}
// 对一套已派生的主题做可读性体检,返回不达标项(不抛错,由调用方决定是失败还是警告)
function auditTheme(vars) {
const bg = vars['--paper']
const checks = [
['--text-body', ROLE_FLOORS.textBody],
['--text-secondary', ROLE_FLOORS.textSecondary],
['--text-helper', ROLE_FLOORS.textHelper],
['--accent-ink', ROLE_FLOORS.accentInk],
]
const out = []
for (const [k, floor] of checks) {
const fg = vars[k]
const r = { token: k, fg, bg, wcag: +wcag(fg, bg).toFixed(2), apca: +apca(fg, bg).toFixed(1) }
if (!passes(fg, bg, floor)) out.push({ ...r, floor })
}
// 强调色小字也会放在 paper-2 / 灰底卡片上
const bg2 = vars['--paper-2']
if (bg2 && !passes(vars['--accent-ink'], bg2, ROLE_FLOORS.accentInk)) {
out.push({ token: '--accent-ink', fg: vars['--accent-ink'], bg: bg2, wcag: +wcag(vars['--accent-ink'], bg2).toFixed(2), apca: +apca(vars['--accent-ink'], bg2).toFixed(1), floor: ROLE_FLOORS.accentInk })
}
// 色块上的正文级文字
const onT = vars['--accent-on-text']
if (onT && !passes(onT, vars['--accent'], ROLE_FLOORS.onText)) {
out.push({ token: '--accent-on-text', fg: onT, bg: vars['--accent'], apca: +apca(onT, vars['--accent']).toFixed(1), wcag: +wcag(onT, vars['--accent']).toFixed(2), note: '色块上只放大号粗体' })
}
// accent 上的文字:大号粗体允许到 Lc 45(APCA 标题线),小字要 60
const onLc = Math.abs(apca(vars['--accent-on'], vars['--accent']))
if (onLc < 60) out.push({ token: '--accent-on', fg: vars['--accent-on'], bg: vars['--accent'], apca: +onLc.toFixed(1), wcag: +wcag(vars['--accent-on'], vars['--accent']).toFixed(2), note: onLc >= 45 ? '只能用于大号粗体' : '不可用于文字' })
return out
}
// 受约束的品牌主题:用户给任意品牌色,生成一套符合家族审美的主题。
//
// 不直接用原色。每个颜色槽位都有一个明度 / 彩度区间,区间来自现有精调色板的 OKLCH 实测
// (见 docs/decisions.md D9),品牌色只贡献色相和「有多鲜艳」的倾向,最终落在区间里。
// 生成后走和内置主题同一套派生与体检(lib/theme.mjs),不达标就不返回主题。
const r3 = (x) => Math.round(x * 1000) / 1000
// 由 tokens/palettes.json 的浅色杂志主题实测(node 脚本见 docs/decisions.md D9)
const MAGAZINE_BANDS = {
paper: { l: [0.909, 0.963], c: [0.003, 0.036] },
paper2: { l: [0.864, 0.929], c: [0.007, 0.039] },
ink: { l: [0.145, 0.279], c: [0.002, 0.063] },
muted: { l: [0.499, 0.527], c: [0.015, 0.044] },
accent: { l: [0.4, 0.58], c: [0.04, 0.12] },
accentSoft: { l: [0.792, 0.905], c: [0.018, 0.063] },
}
// 瑞士风四个强调色实测彩度 0.181–0.207,下限放宽到 0.15;明度不限,但要留出能派生 bright / deep 的空间
const SWISS_BANDS = { accent: { l: [0.35, 0.9], c: [0.15, 0.32] } }
function place(slot, l, c, h, band, notes) {
const L = clamp(l, ...band.l)
const C = Math.min(clamp(c, ...band.c), maxChroma(L, h))
if (Math.abs(L - l) > 0.02) notes.push(`${slot} 明度 ${r3(l)} → ${r3(L)}`)
if (Math.abs(C - c) > 0.01) notes.push(`${slot} 彩度 ${r3(c)} → ${r3(C)}`)
return oklchToHex({ l: L, c: C, h })
}
// 纸张是「有一点颜色的米白」:现有 5 套里 4 套是暖米色(色相 80–90°),靛蓝瓷是几乎无彩的冷白。
// 所以暖色系品牌把纸张色相往 85° 拉,其余色相只留极低彩度,避免红色品牌生成粉色纸
function paperTone(b, neutral) {
if (neutral) return { h: 85, c: 0.007 }
const warm = b.h >= 20 && b.h <= 115
return warm ? { h: 85 + (b.h - 85) * 0.3, c: clamp(b.c * 0.05, 0.006, 0.014) } : { h: b.h, c: clamp(b.c * 0.02, 0.003, 0.006) }
}
function magazineFromBrand(brandHex, { name = '品牌主题' } = {}) {
const b = hexToOklch(brandHex)
const notes = []
const B = MAGAZINE_BANDS
// 品牌色很灰(c<0.02)时按墨水经典处理:色相没有意义,强调色就用墨色
const neutral = b.c < 0.02
const h = neutral ? 85 : b.h
const paper = paperTone(b, neutral)
const ink = place('ink', 0.22, neutral ? 0.004 : clamp(b.c * 0.35, 0.01, 0.06), h, B.ink, [])
if (neutral) notes.push('品牌色接近中性灰,强调色改用墨色(同墨水经典)')
const accent = neutral ? ink : place('accent', b.l, b.c, b.h, B.accent, notes)
const t = {
name, scheme: 'light',
paper: place('paper', 0.95, paper.c, paper.h, B.paper, []),
paper2: place('paper2', 0.91, paper.c + 0.006, paper.h, B.paper2, []),
ink,
muted: place('muted', 0.51, neutral ? 0.015 : clamp(b.c * 0.3, 0.015, 0.044), h, B.muted, []),
accent,
}
t.accentSoft = neutral ? place('accentSoft', 0.86, 0.018, paper.h, B.accentSoft, []) : place('accentSoft', 0.85, clamp(hexToOklch(accent).c * 0.35, 0.018, 0.063), b.h, B.accentSoft, [])
return finish('magazine', t, deriveMagazine(t), notes)
}
function swissFromBrand(brandHex, neutrals, { name = '品牌主题' } = {}) {
const b = hexToOklch(brandHex)
const notes = []
if (b.c < 0.04) {
return { ok: false, reason: `品牌色 ${brandHex} 几乎没有彩度,瑞士风需要一个高饱和强调色。可以改用杂志风,或从 logo 里另选一个颜色。` }
}
const accent = place('accent', b.l, b.c, b.h, SWISS_BANDS.accent, notes)
const a = hexToOklch(accent)
const t = {
name, accent,
accentBright: oklchToHex({ l: Math.min(a.l + 0.18, 0.92), c: a.c, h: a.h }),
accentDeep: oklchToHex({ l: Math.max(a.l - 0.15, 0.2), c: a.c, h: a.h }),
}
// accent-on / accent-ink 交给派生规则算,和内置主题一致
const probe = deriveSwiss(neutrals, { ...t, accentOn: neutrals.ink, accentInk: accent })
t.accentOn = pickOn(accent, neutrals)
// 强调色小字也放在灰底卡片(grey-1)上,按更暗的灰底来调
t.accentInk = textSafe(accent, neutrals.grey1)
return finish('swiss', t, deriveSwiss(neutrals, t), notes)
}
const pickOn = (accent, n) => (Math.abs(apca('#ffffff', accent)) > Math.abs(apca(n.ink, accent)) ? '#ffffff' : n.ink)
const textSafe = (accent, paper) => textSafeAccent(accent, paper).hex
function finish(family, theme, derived, notes) {
const problems = auditTheme(derived.vars)
const hard = problems.filter((p) => !p.note)
if (hard.length) return { ok: false, family, theme, reason: hard.map((p) => `${p.token} 对比度不足(APCA ${p.apca})`).join(';') }
return { ok: true, family, theme, vars: derived.vars, notes, limits: problems.filter((p) => p.note).map((p) => `${p.token}:${p.note}`) }
}
// 预览用:把品牌色和生成结果并排给出
const describe = (r) => (r.ok ? `${r.family} ✓ accent ${r.theme.accent} paper ${r.theme.paper || ''} ink ${r.theme.ink || ''}${r.notes.length ? '|调整:' + r.notes.join(',') : ''}` : `✗ ${r.reason}`)
const SWISS_NEUTRALS = {"paper":"#fafaf8","ink":"#0a0a0a","grey1":"#f0f0ee","grey2":"#d4d4d2","grey3":"#737373","textSecondary":"#525252","textPlaceholder":"#a3a3a3","borderSubtle":"#e0e0e0"}
const args = process.argv.slice(2)
const opt = (k, d) => (args.includes(k) ? args[args.indexOf(k) + 1] : d)
if (args[0] !== 'brand' || !args[1]) {
console.error('用法:node scripts/gz-theme.mjs brand "#4D6BFE" [--family magazine|swiss] [--id brand]')
process.exit(1)
}
const family = opt('--family', 'magazine'), id = opt('--id', 'brand')
const r = family === 'swiss' ? swissFromBrand(args[1], SWISS_NEUTRALS) : magazineFromBrand(args[1])
if (!r.ok) { console.error('✗ ' + r.reason); process.exit(1) }
const v = r.vars
// 一行说清楚:强调色调了什么、小字和色块上的文字各用什么颜色、有什么限制
const parts = [
r.notes.length ? r.notes.join(';') : `强调色保持 ${v['--accent']}`,
`小字强调色 ${v['--accent-ink']}`,
`色块上的文字 ${v['--accent-on-text']}${v['--accent-on-text'] !== v['--accent-on'] ? `(${v['--accent-on']} 只够大号粗体)` : ''}`,
...r.limits.filter((l) => !l.startsWith('--accent-on:')),
]
console.log(`/* 品牌主题 ${args[1]} · ${family}:${parts.join(';')} */`)
console.log(`[data-theme="${id}"]{${Object.entries(r.vars).map(([k, v]) => `${k}:${v}`).join(';')};color-scheme:light}`)
@@ -2,8 +2,8 @@
"sourceId": "hallmark", "sourceId": "hallmark",
"repo": "https://github.com/Nutlope/hallmark.git", "repo": "https://github.com/Nutlope/hallmark.git",
"ref": "main", "ref": "main",
"commit": "8f6969f8922caabde9de1784959eae11ddf68073", "commit": "4f6a46c6dc325d40ab268c93e25c7b96676ebccd",
"adapter": "claude-skill", "adapter": "claude-skill",
"sourcePath": "skills/hallmark", "sourcePath": "skills/hallmark",
"syncedAt": "2026-10-29T15:25:46Z" "syncedAt": "2026-10-30T16:00:00Z"
} }
@@ -1,5 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
import { readFileSync, readdirSync, statSync, existsSync, mkdtempSync, copyFileSync, rmSync } from 'node:fs'; import { readFileSync, readdirSync, statSync, existsSync, mkdtempSync, copyFileSync, rmSync, mkdirSync, writeFileSync, symlinkSync } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join, dirname, relative, basename, resolve } from 'node:path'; import { join, dirname, relative, basename, resolve } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -284,6 +284,23 @@ for (const target of clean) {
} }
const FIX = join(ROOT, 'scripts', 'fixtures'); const FIX = join(ROOT, 'scripts', 'fixtures');
{
const tmp = mkdtempSync(join(tmpdir(), 'hallmark-links-'));
try {
const project = join(tmp, 'project');
const shared = join(tmp, 'shared');
mkdirSync(project);
mkdirSync(shared);
writeFileSync(join(shared, 'style.css'), 'p { color: var(--color-text); }');
symlinkSync(shared, join(project, 'a-shared'), process.platform === 'win32' ? 'junction' : 'dir');
symlinkSync(shared, join(project, 'b-shared'), process.platform === 'win32' ? 'junction' : 'dir');
symlinkSync(project, join(project, 'parent'), process.platform === 'win32' ? 'junction' : 'dir');
const run = spawnSync(process.execPath, [join(ROOT, 'scripts', 'sloplint.mjs'), project, '--scope', 'component', '--json'], { encoding: 'utf8', timeout: 5000 });
let report = null;
try { report = JSON.parse(run.stdout); } catch {}
if (!report || report.summary.files !== 1) add('error', SKILL, 1, 'fixture', 'directory symlink cycles must terminate and each real file must be linted once');
} finally { rmSync(tmp, { recursive: true, force: true }); }
}
const lintJson = (t) => { const r = spawnSync(process.execPath, [join(ROOT, 'scripts', 'sloplint.mjs'), t, '--json'], { encoding: 'utf8' }); try { return JSON.parse(r.stdout); } catch { return null; } }; const lintJson = (t) => { const r = spawnSync(process.execPath, [join(ROOT, 'scripts', 'sloplint.mjs'), t, '--json'], { encoding: 'utf8' }); try { return JSON.parse(r.stdout); } catch { return null; } };
const gatesOf = (j) => new Set(j.findings.map((x) => String(x.gate))); const gatesOf = (j) => new Set(j.findings.map((x) => String(x.gate)));
const expectations = [ const expectations = [
@@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
/* sloplint: Hallmark's mechanical gate checker. Zero dependencies, Node 18+. Gate ids and tiers: references/gates.md. */ /* sloplint: Hallmark's mechanical gate checker. Zero dependencies, Node 18+. Gate ids and tiers: references/gates.md. */
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import { readFileSync, readdirSync, statSync, existsSync, writeFileSync, mkdirSync } from 'node:fs'; import { readFileSync, readdirSync, statSync, realpathSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
import { join, extname, dirname, basename, resolve } from 'node:path'; import { join, extname, dirname, basename, resolve } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -46,16 +46,18 @@ function collectFiles(paths) {
const seen = new Set(); const seen = new Set();
const visit = (p, top) => { const visit = (p, top) => {
const abs = resolve(p); const abs = resolve(p);
if (seen.has(abs)) return;
let st; let st;
try { st = statSync(abs); } catch { console.error(`sloplint: cannot read ${p}`); if (top) process.exit(2); return; } let real;
try { st = statSync(abs); real = realpathSync(abs); } catch { console.error(`sloplint: cannot read ${p}`); if (top) process.exit(2); return; }
if (seen.has(real)) return;
seen.add(real);
if (st.isDirectory()) { if (st.isDirectory()) {
const base = basename(abs); const base = basename(abs);
if (base === 'node_modules' || base.startsWith('.') || (!top && BUILD_DIRS.has(base))) return; if (base === 'node_modules' || base.startsWith('.') || (!top && BUILD_DIRS.has(base))) return;
for (const e of readdirSync(abs)) visit(join(abs, e), false); for (const e of readdirSync(abs)) visit(join(abs, e), false);
} else { } else {
const ext = extname(abs).toLowerCase(); const ext = extname(abs).toLowerCase();
if (ext === '.css' || DOC_EXT.has(ext)) { seen.add(abs); files.push(abs); } if (ext === '.css' || DOC_EXT.has(ext)) files.push(abs);
} }
}; };
for (const p of paths) visit(p, true); for (const p of paths) visit(p, true);
@@ -36,7 +36,7 @@ function usage() {
} }
function parseHeader(src, file) { function parseHeader(src, file) {
const m = /^---\n([\s\S]*?)\n---/.exec(src); const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(src);
if (!m) throw new Error(`${file}: no header block`); if (!m) throw new Error(`${file}: no header block`);
const h = {}; const h = {};
for (const line of m[1].split('\n')) { for (const line of m[1].split('\n')) {
@@ -0,0 +1,25 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
test('validate accepts a theme with either LF or CRLF line endings', () => {
const source = readFileSync(new URL('../references/themes/hum.md', import.meta.url), 'utf8').replace(/\r\n/g, '\n');
const cli = fileURLToPath(new URL('./theme.mjs', import.meta.url));
const dir = mkdtempSync(join(tmpdir(), 'hallmark-theme-'));
try {
for (const [name, text] of [['lf', source], ['crlf', source.replace(/\n/g, '\r\n')]]) {
assert.ok(Buffer.byteLength(text) < 8000, 'fixture must stay below the independent size limit');
const file = join(dir, `${name}.md`);
writeFileSync(file, text);
const result = spawnSync(process.execPath, [cli, 'validate', file], { encoding: 'utf8' });
assert.equal(result.status, 0, `${name}: ${result.stdout}${result.stderr}`);
assert.match(result.stdout, /: ok \(Hum,/);
}
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
@@ -3,5 +3,5 @@
"name": "playwright浏览器自动化操作", "name": "playwright浏览器自动化操作",
"version": "20260605", "version": "20260605",
"keySource": "none", "keySource": "none",
"syncedAt": "2026-10-29T16:03:34Z" "syncedAt": "2026-10-30T16:02:58Z"
} }
@@ -2,8 +2,8 @@
"sourceId": "next-skills", "sourceId": "next-skills",
"repo": "https://github.com/vercel/next.js.git", "repo": "https://github.com/vercel/next.js.git",
"ref": "canary", "ref": "canary",
"commit": "79107ebd94e53c69865c1a49ac734ba8004491ba", "commit": "7571895249a22086367a12ddec90fae97bac067d",
"adapter": "skill-collection", "adapter": "skill-collection",
"sourcePath": "skills", "sourcePath": "skills",
"syncedAt": "2026-10-29T16:00:00Z" "syncedAt": "2026-10-30T16:00:00Z"
} }
@@ -2,8 +2,8 @@
"sourceId": "ppt-master", "sourceId": "ppt-master",
"repo": "https://github.com/hugohe3/ppt-master.git", "repo": "https://github.com/hugohe3/ppt-master.git",
"ref": "main", "ref": "main",
"commit": "15baa7c570f12c78cd9ac6ead897724571e32e87", "commit": "5e8856be0da8e6bc85c4aa28acc614478f7b7137",
"adapter": "claude-skill", "adapter": "claude-skill",
"sourcePath": "skills/ppt-master", "sourcePath": "skills/ppt-master",
"syncedAt": "2026-10-29T16:00:00Z" "syncedAt": "2026-10-30T16:00:00Z"
} }
@@ -2,7 +2,7 @@
name: ppt-master name: ppt-master
description: "多格式源文档到高质量 SVG 页面再导出 PPTX 的多阶段演示文稿生成工作流。" description: "多格式源文档到高质量 SVG 页面再导出 PPTX 的多阶段演示文稿生成工作流。"
metadata: metadata:
version: "6.7.3" version: "6.8.0"
copyright: "Copyright (c) 2025-2026 Hugo He" copyright: "Copyright (c) 2025-2026 Hugo He"
license: "MIT" license: "MIT"
official_repository: "https://github.com/hugohe3/ppt-master" official_repository: "https://github.com/hugohe3/ppt-master"
@@ -8,6 +8,7 @@ reusable by other tools.
import argparse import argparse
import re import re
import xml.etree.ElementTree as ET
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
@@ -516,12 +517,14 @@ def _outline_hyperlink_lines(design_text: str) -> Dict[int, str | None]:
def _svg_hyperlink_targets(svg_file: Path) -> Tuple[set, set] | None: def _svg_hyperlink_targets(svg_file: Path) -> Tuple[set, set] | None:
"""Return (same-deck slide numbers, external hrefs) of an SVG's anchors, or None if unreadable.""" """Return (same-deck slide numbers, external hrefs) of an SVG's anchors, or None if unreadable."""
try: try:
text = svg_file.read_text(encoding='utf-8') root = ET.parse(svg_file).getroot()
except OSError: except (OSError, ET.ParseError):
return None return None
slides, externals = set(), set() slides, externals = set(), set()
for match in re.finditer(r'<a\b[^>]*?\s(?:xlink:)?href=["\']([^"\']*)["\']', text): for anchor in root.iter():
href = match.group(1).strip() if anchor.tag not in ('a', '{http://www.w3.org/2000/svg}a'):
continue
href = anchor.get('href', anchor.get('{http://www.w3.org/1999/xlink}href', '')).strip()
jump = _SLIDE_JUMP_RE.fullmatch(href) jump = _SLIDE_JUMP_RE.fullmatch(href)
if jump: if jump:
slides.add(int(jump.group(1))) slides.add(int(jump.group(1)))
@@ -0,0 +1,30 @@
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from project_utils import validate_outline_hyperlinks
class OutlineHyperlinksTests(unittest.TestCase):
def test_xml_attributes_are_decoded_exactly_once(self):
with tempfile.TemporaryDirectory() as directory:
project = Path(directory)
(project / 'svg_output').mkdir()
(project / 'design_spec.md').write_text(
'## IX. Content Outline\n### Slide 07\n'
'- Hyperlinks: https://example.com/?a=1&b=2; '
'https://example.com/?literal=&amp;value; #slide-2\n',
encoding='utf-8',
)
(project / 'svg_output' / '07_links.svg').write_text(
'<svg xmlns="http://www.w3.org/2000/svg" '
'xmlns:link="http://www.w3.org/1999/xlink">'
'<a href="https://example.com/?a=1&amp;b=2"><text>One</text></a>'
'<a link:href="https://example.com/?literal=&amp;amp;value"/>'
'<a href="&#35;slide-2"/></svg>',
encoding='utf-8',
)
self.assertEqual(validate_outline_hyperlinks(project), [])
@@ -2,8 +2,8 @@
"sourceId": "shadcn", "sourceId": "shadcn",
"repo": "https://github.com/shadcn-ui/ui.git", "repo": "https://github.com/shadcn-ui/ui.git",
"ref": "main", "ref": "main",
"commit": "96da9cc7f1aa10580a3741664e2a77c721206c00", "commit": "b8d7baa15b1dca5716f13153e63795fc3d741f6b",
"adapter": "claude-skill", "adapter": "claude-skill",
"sourcePath": "skills/shadcn", "sourcePath": "skills/shadcn",
"syncedAt": "2026-10-28T01:57:49Z" "syncedAt": "2026-10-30T16:00:00Z"
} }