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-09-15 00:02:21 +08:00
1 parent fd01ce868a
commit 1091563a13
185 files changed
+6186 -1201

No files matched your search

+10 -10
View File
@@ -69,8 +69,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": "3ba91b1cc83e1bbe4ab35a422ff2a694849c5048", "commit": "2b3e6d4f8d9161fe5c19340dc383aade392012dd",
"syncedAt": "2026-09-08T16:00:01Z" "syncedAt": "2026-09-14T15:59:59Z"
}, },
{ {
"id": "frontend-slides", "id": "frontend-slides",
@@ -96,8 +96,8 @@
"repo": "https://github.com/lewislulu/html-ppt-skill.git", "repo": "https://github.com/lewislulu/html-ppt-skill.git",
"ref": "main", "ref": "main",
"adapter": "claude-skill", "adapter": "claude-skill",
"commit": "f3a8435d3901697d5ac5e64d356c933637e43107", "commit": "fd1629067909ff55b36b905476de4e5f26062a1f",
"syncedAt": "2026-06-11T03:57:53Z" "syncedAt": "2026-09-14T15:59:59Z"
}, },
{ {
"id": "ppt-master", "id": "ppt-master",
@@ -105,8 +105,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": "b3073f8fcf5d2ebeda621a424097dde9dd771cea", "commit": "c6ae4896da10497464ad7e87f5833bfc14321621",
"syncedAt": "2026-09-11T16:00:01Z" "syncedAt": "2026-09-14T15:59:59Z"
}, },
{ {
"id": "grill-me", "id": "grill-me",
@@ -141,8 +141,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": "d155ba9ebfffe4742efefda8d68c2e0e8e490924", "commit": "3dc93da27e29048d601546ed610c903705f7fd6c",
"syncedAt": "2026-09-11T16:00:01Z" "syncedAt": "2026-09-14T15:59:59Z"
}, },
{ {
"id": "shuorenhua", "id": "shuorenhua",
@@ -150,8 +150,8 @@
"repo": "https://github.com/MrGeDiao/shuorenhua.git", "repo": "https://github.com/MrGeDiao/shuorenhua.git",
"ref": "main", "ref": "main",
"adapter": "claude-skill", "adapter": "claude-skill",
"commit": "d2d0ce27da295581c3cf87a30ab65deb7d0ddfb8", "commit": "5a9eafefe03807404135f4d2ee4f42fe61d58759",
"syncedAt": "2026-09-03T02:45:43Z" "syncedAt": "2026-09-14T15:59:59Z"
} }
] ]
} }
+119 -10
View File
@@ -1,7 +1,7 @@
# html-ppt — HTML PPT Studio # html-ppt — HTML PPT Studio
> A world-class AgentSkill for producing professional HTML presentations in > A world-class AgentSkill for producing professional HTML presentations in
> **36 themes**, **15 full-deck templates**, **31 page layouts**, > **36 themes**, **15 full-deck templates**, **36 page layouts**,
> **47 animations** (27 CSS + 20 canvas FX), and a **true presenter mode** > **47 animations** (27 CSS + 20 canvas FX), and a **true presenter mode**
> with pixel-perfect previews + speaker script + timer — all pure static > with pixel-perfect previews + speaker script + timer — all pure static
> HTML/CSS/JS, no build step. > HTML/CSS/JS, no build step.
@@ -54,6 +54,65 @@ that supports AgentSkills can author presentations by asking things like:
> "turn this outline into a pitch deck" > "turn this outline into a pitch deck"
> "做一个小红书图文,9 张,白底柔和风" > "做一个小红书图文,9 张,白底柔和风"
## Offline / manual install
`npx skills add <url>` needs network on the target machine. Three alternatives,
in decreasing order of how much network they need.
**1. Install from a local copy.** Fetch the repo anywhere, move the folder over
(git, zip, USB), then point the CLI at the directory instead of the URL:
```bash
git clone https://github.com/lewislulu/html-ppt-skill
npx skills add ./html-ppt-skill
```
`npx` still downloads the `skills` package itself once. For a fully air-gapped
machine, run `npm i -g skills` on a connected one first, or use method 2.
**2. Copy it in by hand — no Node, no CLI.** A skill is just a folder with
`SKILL.md` at its root. Drop it in the directory your agent scans:
| Agent | Project scope | Global scope |
|---|---|---|
| Claude Code | `.claude/skills/html-ppt/` | `~/.claude/skills/html-ppt/` |
| Codex | `.agents/skills/html-ppt/` | `~/.codex/skills/html-ppt/` |
| Cursor | `.agents/skills/html-ppt/` | `~/.cursor/skills/html-ppt/` |
| OpenCode | `.agents/skills/html-ppt/` | `~/.config/opencode/skills/html-ppt/` |
| Gemini CLI | `.agents/skills/html-ppt/` | `~/.gemini/skills/html-ppt/` |
| Windsurf | `.windsurf/skills/html-ppt/` | `~/.codeium/windsurf/skills/html-ppt/` |
```bash
mkdir -p ~/.claude/skills
cp -R html-ppt-skill ~/.claude/skills/html-ppt
ls ~/.claude/skills/html-ppt/SKILL.md # must exist
```
Only `SKILL.md`, `assets/`, `templates/`, `references/` and `scripts/` are
needed at runtime. `docs/` is ~4.6 MB of README artwork and can be dropped from
an offline copy.
**3. No agent at all.** The templates are plain static files — usable directly:
```bash
./scripts/new-deck.sh my-talk
open examples/my-talk/index.html
```
### Does it run without a network?
Yes, with one caveat. Themes, layouts, animations, presenter mode and PNG
rendering are all local static HTML/CSS/JS with no build step and no runtime
fetches. The single remote dependency is `assets/fonts.css`, which `@import`s
Google Fonts.
Offline, those imports simply fail and the browser falls back to the system
stack already declared in `assets/base.css` (`-apple-system` / Helvetica /
Georgia / Menlo), so decks render correctly — just in different typefaces. To
pin typography offline, replace `assets/fonts.css` with `@font-face` rules
pointing at font files you ship yourself, or delete the imports and accept the
system stack.
## What's in the box ## What's in the box
| | Count | Where | | | Count | Where |
@@ -61,7 +120,7 @@ that supports AgentSkills can author presentations by asking things like:
| 🎤 **Presenter mode** | **NEW** | `S` key / `?preview=N` | | 🎤 **Presenter mode** | **NEW** | `S` key / `?preview=N` |
| 🎨 **Themes** | **36** | `assets/themes/*.css` | | 🎨 **Themes** | **36** | `assets/themes/*.css` |
| 📑 **Full-deck templates** | **15** | `templates/full-decks/<name>/` | | 📑 **Full-deck templates** | **15** | `templates/full-decks/<name>/` |
| 🧩 **Single-page layouts** | **31** | `templates/single-page/*.html` | | 🧩 **Single-page layouts** | **36** | `templates/single-page/*.html` |
| ✨ **CSS animations** | **27** | `assets/animations/animations.css` | | ✨ **CSS animations** | **27** | `assets/animations/animations.css` |
| 💥 **Canvas FX animations** | **20** | `assets/animations/fx/*.js` | | 💥 **Canvas FX animations** | **20** | `assets/animations/fx/*.js` |
| 🖼️ **Showcase decks** | 4 | `templates/*-showcase.html` | | 🖼️ **Showcase decks** | 4 | `templates/*-showcase.html` |
@@ -108,7 +167,7 @@ decks can be previewed side-by-side without collisions. Browse the full
gallery in `templates/full-decks-index.html`. gallery in `templates/full-decks-index.html`.
### 31 Single-page layouts ### 36 Single-page layouts
cover · toc · section-divider · bullets · two-column · three-column · cover · toc · section-divider · bullets · two-column · three-column ·
big-quote · stat-highlight · kpi-grid · table · code · diff · terminal · big-quote · stat-highlight · kpi-grid · table · code · diff · terminal ·
@@ -120,7 +179,7 @@ Every layout ships with realistic demo data so you can drop it into a deck
and immediately see it render. and immediately see it render.
*The big iframe is loading `templates/single-page/<name>.html` directly and cycling through all 31 layouts every 2.8 seconds.* *The big iframe is loading `templates/single-page/<name>.html` directly and cycling through all 36 layouts every 2.8 seconds.*
### 27 CSS animations + 20 Canvas FX ### 27 CSS animations + 20 Canvas FX
@@ -145,21 +204,71 @@ module auto-initialised on slide enter via `fx-runtime.js`.
# Scaffold a new deck from the base template # Scaffold a new deck from the base template
./scripts/new-deck.sh my-talk ./scripts/new-deck.sh my-talk
# ...or from a full-deck template, into any directory you like.
# Asset paths are computed for wherever the deck lands, then verified.
./scripts/new-deck.sh my-talk ~/decks -t pitch-deck
# Browse everything # Browse everything
open templates/theme-showcase.html # all 36 themes (iframe-isolated) open templates/theme-showcase.html # all 36 themes (iframe-isolated)
open templates/layout-showcase.html # all 31 layouts open templates/layout-showcase.html # all 36 layouts
open templates/animation-showcase.html # all 47 animations open templates/animation-showcase.html # all 47 animations
open templates/full-decks-index.html # all 14 full decks open templates/full-decks-index.html # all 15 full decks
# Render any template to PNG via headless Chrome # Render any template to PNG via headless Chrome
./scripts/render.sh templates/theme-showcase.html ./scripts/render.sh templates/theme-showcase.html
./scripts/render.sh examples/my-talk/index.html 12 ./scripts/render.sh examples/my-talk/index.html 12
``` ```
## Images on a slide
Five layouts take real images — pick by how many the page has to carry:
| I have… | Layout |
|---|---|
| one screenshot / diagram / chart | `image-single.html` — letterboxed, never cropped |
| one photo that should carry the page | `image-full-bleed.html` — fills the slide, scrim keeps the title readable |
| one image plus an argument | `image-text-split.html` — 50/50, `flip` to swap sides |
| 3–6 images | `image-gallery.html` — uniform grid, one caption each |
| a before and an after | `image-compare.html` — both sides identical size |
They share one primitive from `assets/base.css`:
```html
<figure class="img-frame"><img src="shot.png" alt=""></figure> <!-- crops to fill -->
<figure class="img-frame contain"><img src="diagram.svg" alt=""></figure> <!-- letterboxed -->
```
The frame owns the aspect ratio (`--img-ratio`) and the crop, so you can drop in
a portrait, square or ultrawide image without touching the layout. Placeholder
artwork in `assets/demo-images/` is hand-written SVG (~1 KB each) so every
layout renders **offline**.
## Custom logo
Brand a deck with one attribute — no per-slide `<img>` copy-paste:
```html
<body data-logo="logo.svg" data-logo-position="bottom-right" data-logo-size="40px">
```
`data-logo-position` takes `top-left` / `top-right` / `bottom-left` /
`bottom-right` (default `top-right`); `data-logo-size` sets the height.
Skip it on a single slide with `<section class="slide" data-no-logo>` — handy
for the cover. The logo also shows in the presenter preview, and on **every
page** of a PDF export (skipping the `data-no-logo` ones).
Prefer to place it yourself? `<img class="deck-logo" data-pos="top-left" src="logo.svg">`
inside `.deck` works with no JS at all.
## Keyboard cheat sheet ## Keyboard cheat sheet
On a phone or tablet, **swipe left for the next slide, right for the previous
one** — no keyboard needed. Pinch-zoom, vertical scrolling, the overview grid
and the notes drawer are left alone.
``` ```
← → Space PgUp PgDn Home End navigate ← → Space PgUp PgDn Home End navigate
swipe ← / → (touch) navigate
F fullscreen F fullscreen
S open presenter window (magnetic cards) S open presenter window (magnetic cards)
N quick notes drawer (bottom) N quick notes drawer (bottom)
@@ -181,7 +290,7 @@ html-ppt-skill/
│ ├── themes.md 36 themes with when-to-use │ ├── themes.md 36 themes with when-to-use
│ ├── layouts.md 31 layout types │ ├── layouts.md 31 layout types
│ ├── animations.md 27 CSS + 20 FX catalog │ ├── animations.md 27 CSS + 20 FX catalog
│ ├── full-decks.md 14 full-deck templates │ ├── full-decks.md 15 full-deck templates
│ └── authoring-guide.md full workflow │ └── authoring-guide.md full workflow
├── assets/ ├── assets/
│ ├── base.css shared tokens + primitives │ ├── base.css shared tokens + primitives
@@ -195,10 +304,10 @@ html-ppt-skill/
├── templates/ ├── templates/
│ ├── deck.html minimal starter │ ├── deck.html minimal starter
│ ├── theme-showcase.html iframe-isolated theme tour │ ├── theme-showcase.html iframe-isolated theme tour
│ ├── layout-showcase.html all 31 layouts │ ├── layout-showcase.html all 36 layouts
│ ├── animation-showcase.html 47 animation slides │ ├── animation-showcase.html 47 animation slides
│ ├── full-decks-index.html 14-deck gallery │ ├── full-decks-index.html 15-deck gallery
│ ├── full-decks/<name>/ 14 scoped multi-slide decks │ ├── full-decks/<name>/ 15 scoped multi-slide decks
│ └── single-page/*.html 31 layout files with demo data │ └── single-page/*.html 31 layout files with demo data
├── scripts/ ├── scripts/
│ ├── new-deck.sh scaffold │ ├── new-deck.sh scaffold
+111 -8
View File
@@ -1,7 +1,7 @@
# html-ppt · HTML PPT 工作室 # html-ppt · HTML PPT 工作室
> 一款专业级的 AgentSkill,让 AI 做出真正能打的 HTML 演示文稿。 > 一款专业级的 AgentSkill,让 AI 做出真正能打的 HTML 演示文稿。
> **36 套主题**、**15 套完整 deck 模板**、**31 种页面布局**、**47 个动效** > **36 套主题**、**15 套完整 deck 模板**、**36 种页面布局**、**47 个动效**
> (27 个 CSS + 20 个 Canvas FX),加上全新的 **演讲者模式** —— 像素级 > (27 个 CSS + 20 个 Canvas FX),加上全新的 **演讲者模式** —— 像素级
> 完美预览 + 逐字稿提词器 + 计时器。纯静态 HTML/CSS/JS,无需构建。 > 完美预览 + 逐字稿提词器 + 计时器。纯静态 HTML/CSS/JS,无需构建。
@@ -10,7 +10,7 @@
**English docs:** [README.md](README.md) **English docs:** [README.md](README.md)
> 一行命令装好 **36 主题 × 20 Canvas FX × 31 布局 × 15 完整 deck + 演讲者模式**。 > 一行命令装好 **36 主题 × 20 Canvas FX × 36 布局 × 15 完整 deck + 演讲者模式**。
## 🎤 演讲者模式(全新) ## 🎤 演讲者模式(全新)
@@ -51,6 +51,60 @@ npx skills add https://github.com/lewislulu/html-ppt-skill
> "做一个小红书图文,9 张,白底柔和风" > "做一个小红书图文,9 张,白底柔和风"
> "做一份带演讲者模式的产品分享,我想要有逐字稿" > "做一份带演讲者模式的产品分享,我想要有逐字稿"
## 离线 / 手动安装
`npx skills add <url>` 需要目标机器能联网。下面三种方式,对网络的依赖依次递减。
**1. 从本地副本安装。** 在任意能联网的机器上取到仓库,把整个目录搬过去(git、zip、U 盘都行),
然后把 CLI 指向本地目录而不是 URL:
```bash
git clone https://github.com/lewislulu/html-ppt-skill
npx skills add ./html-ppt-skill
```
`npx` 本身仍需下载一次 `skills` 包。完全隔离的机器请先在联网机器上
`npm i -g skills`,或者直接用方式 2。
**2. 纯手工拷贝 —— 不需要 Node,也不需要 CLI。** 一个 skill 就是一个根目录下放着
`SKILL.md` 的文件夹。放进你的 agent 会扫描的目录即可:
| Agent | 项目级 | 全局 |
|---|---|---|
| Claude Code | `.claude/skills/html-ppt/` | `~/.claude/skills/html-ppt/` |
| Codex | `.agents/skills/html-ppt/` | `~/.codex/skills/html-ppt/` |
| Cursor | `.agents/skills/html-ppt/` | `~/.cursor/skills/html-ppt/` |
| OpenCode | `.agents/skills/html-ppt/` | `~/.config/opencode/skills/html-ppt/` |
| Gemini CLI | `.agents/skills/html-ppt/` | `~/.gemini/skills/html-ppt/` |
| Windsurf | `.windsurf/skills/html-ppt/` | `~/.codeium/windsurf/skills/html-ppt/` |
```bash
mkdir -p ~/.claude/skills
cp -R html-ppt-skill ~/.claude/skills/html-ppt
ls ~/.claude/skills/html-ppt/SKILL.md # 必须存在
```
运行时只需要 `SKILL.md`、`assets/`、`templates/`、`references/`、`scripts/`。
`docs/` 是约 4.6 MB 的 README 配图,离线拷贝时可以删掉。
**3. 完全不用 agent。** 模板就是普通静态文件,可以直接用:
```bash
./scripts/new-deck.sh my-talk
open examples/my-talk/index.html
```
### 断网能用吗?
能,只有一个前提要说清楚。主题、布局、动效、演讲者模式、PNG 导出全部是本地静态
HTML/CSS/JS,零构建、运行时不发请求。唯一的远程依赖是 `assets/fonts.css`,
它 `@import` 了 Google Fonts。
断网时这些 import 直接失败,浏览器回落到 `assets/base.css` 里已经声明好的系统字体栈
(`-apple-system` / Helvetica / Georgia / Menlo),所以 deck 照常渲染,只是字体不同。
如果要在离线环境下锁定字体,把 `assets/fonts.css` 换成指向自带字体文件的 `@font-face`
规则,或者删掉这些 import、接受系统字体。
## Skill 内容一览 ## Skill 内容一览
| | 数量 | 位置 | | | 数量 | 位置 |
@@ -58,7 +112,7 @@ npx skills add https://github.com/lewislulu/html-ppt-skill
| 🎤 **演讲者模式** | **新增** | `S` 键 / `?preview=N` | | 🎤 **演讲者模式** | **新增** | `S` 键 / `?preview=N` |
| 🎨 **主题** | **36** | `assets/themes/*.css` | | 🎨 **主题** | **36** | `assets/themes/*.css` |
| 📑 **完整 deck 模板** | **15** | `templates/full-decks/<name>/` | | 📑 **完整 deck 模板** | **15** | `templates/full-decks/<name>/` |
| 🧩 **单页布局** | **31** | `templates/single-page/*.html` | | 🧩 **单页布局** | **36** | `templates/single-page/*.html` |
| ✨ **CSS 动画** | **27** | `assets/animations/animations.css` | | ✨ **CSS 动画** | **27** | `assets/animations/animations.css` |
| 💥 **Canvas FX 动画** | **20** | `assets/animations/fx/*.js` | | 💥 **Canvas FX 动画** | **20** | `assets/animations/fx/*.js` |
| 🖼️ **Showcase deck** | 4 | `templates/*-showcase.html` | | 🖼️ **Showcase deck** | 4 | `templates/*-showcase.html` |
@@ -108,7 +162,7 @@ npx skills add https://github.com/lewislulu/html-ppt-skill
同时加载不会互相污染。在 `templates/full-decks-index.html` 可以看全套 gallery。 同时加载不会互相污染。在 `templates/full-decks-index.html` 可以看全套 gallery。
### 31 种单页布局 ### 36 种单页布局
cover · toc · section-divider · bullets · two-column · three-column · cover · toc · section-divider · bullets · two-column · three-column ·
big-quote · stat-highlight · kpi-grid · table · code · diff · terminal · big-quote · stat-highlight · kpi-grid · table · code · diff · terminal ·
@@ -148,9 +202,13 @@ chart-pie · chart-radar · arch-diagram · process-steps · cta · thanks
# 从 base 模板新建一个 deck # 从 base 模板新建一个 deck
./scripts/new-deck.sh my-talk ./scripts/new-deck.sh my-talk
# 也可以指定完整 deck 模板和任意输出目录。
# assets 路径会按 deck 的实际位置算出来,并逐条校验能否解析。
./scripts/new-deck.sh my-talk ~/decks -t pitch-deck
# 浏览所有内容 # 浏览所有内容
open templates/theme-showcase.html # 全部 36 主题(iframe 隔离) open templates/theme-showcase.html # 全部 36 主题(iframe 隔离)
open templates/layout-showcase.html # 全部 31 布局 open templates/layout-showcase.html # 全部 36 布局
open templates/animation-showcase.html # 全部 47 动效 open templates/animation-showcase.html # 全部 47 动效
open templates/full-decks-index.html # 全部 15 个完整 deck open templates/full-decks-index.html # 全部 15 个完整 deck
@@ -159,10 +217,55 @@ open templates/full-decks-index.html # 全部 15 个完整 deck
./scripts/render.sh examples/my-talk/index.html 12 ./scripts/render.sh examples/my-talk/index.html 12
``` ```
## 放图片的版式
五个版式用的是真实 `<img>`,按「这一页要放几张图」挑:
| 我有… | 版式 |
|---|---|
| 一张截图 / 示意图 / 图表 | `image-single.html` —— 完整显示,**不裁剪** |
| 一张想撑满整页的照片 | `image-full-bleed.html` —— 整页铺满,底部压暗保证标题可读 |
| 一张图 + 一段论述 | `image-text-split.html` —— 各占一半,加 `flip` 左右互换 |
| 3~6 张图 | `image-gallery.html` —— 等大网格,每张一句话 |
| 改版前 / 改版后 | `image-compare.html` —— 两侧严格同尺寸 |
它们共用 `assets/base.css` 里的同一个原语:
```html
<figure class="img-frame"><img src="shot.png" alt=""></figure> <!-- 裁剪填满 -->
<figure class="img-frame contain"><img src="diagram.svg" alt=""></figure> <!-- 完整显示 -->
```
比例和裁剪由**框**决定(`--img-ratio`),不由图片决定 —— 竖图、方图、超宽图
直接换 `src` 就行,版式不用改。示例图放在 `assets/demo-images/`,是手写的
SVG(每个约 1KB),所以这些版式**离线也能正常渲染**。
## 自定义 LOGO
一个属性给整份 deck 加上公司 / 产品 LOGO,不用每页粘一个 `<img>`:
```html
<body data-logo="logo.svg" data-logo-position="bottom-right" data-logo-size="40px">
```
`data-logo-position` 可选 `top-left` / `top-right` / `bottom-left` /
`bottom-right`(默认 `top-right`);`data-logo-size` 设置高度,宽度按比例。
某一页不想要(通常是封面)就写 `<section class="slide" data-no-logo>`。
演讲者模式的预览里带 LOGO;导出 PDF 时**每一页都带**(写了 `data-no-logo`
的那页除外)。
想自己摆位置:在 `.deck` 里直接写
`<img class="deck-logo" data-pos="top-left" src="logo.svg">`,
样式在 `base.css` 里,**完全不依赖 JS**。
## 键盘快捷键 ## 键盘快捷键
手机 / 平板上**向左划到下一页,向右划回上一页**,不需要键盘。
双指缩放、纵向滚动、总览网格和 notes 抽屉都不受影响。
``` ```
← → Space PgUp PgDn Home End 翻页 ← → Space PgUp PgDn Home End 翻页
左划 / 右划(触摸) 翻页
F 全屏 F 全屏
S 打开演讲者窗口(磁吸卡片模式) S 打开演讲者窗口(磁吸卡片模式)
N 底部 notes 抽屉 N 底部 notes 抽屉
@@ -183,7 +286,7 @@ html-ppt-skill/
├── README.zh-CN.md 本文件 ├── README.zh-CN.md 本文件
├── references/ 详细文档 ├── references/ 详细文档
│ ├── themes.md 36 主题 + 使用场景 │ ├── themes.md 36 主题 + 使用场景
│ ├── layouts.md 31 布局 │ ├── layouts.md 36 布局
│ ├── animations.md 27 CSS + 20 FX 目录 │ ├── animations.md 27 CSS + 20 FX 目录
│ ├── full-decks.md 15 完整 deck 模板 │ ├── full-decks.md 15 完整 deck 模板
│ ├── presenter-mode.md 🎤 演讲者模式 + 逐字稿指南 │ ├── presenter-mode.md 🎤 演讲者模式 + 逐字稿指南
@@ -200,11 +303,11 @@ html-ppt-skill/
├── templates/ ├── templates/
│ ├── deck.html 最小起步模板 │ ├── deck.html 最小起步模板
│ ├── theme-showcase.html iframe 隔离的主题 tour │ ├── theme-showcase.html iframe 隔离的主题 tour
│ ├── layout-showcase.html 全部 31 布局 │ ├── layout-showcase.html 全部 36 布局
│ ├── animation-showcase.html 47 动画 slide │ ├── animation-showcase.html 47 动画 slide
│ ├── full-decks-index.html 15 deck gallery │ ├── full-decks-index.html 15 deck gallery
│ ├── full-decks/<name>/ 15 个 scoped 多页 deck 模板 │ ├── full-decks/<name>/ 15 个 scoped 多页 deck 模板
│ └── single-page/*.html 31 个布局文件(带示例数据) │ └── single-page/*.html 36 个布局文件(带示例数据)
├── scripts/ ├── scripts/
│ ├── new-deck.sh 脚手架 │ ├── new-deck.sh 脚手架
│ ├── render.sh headless Chrome → PNG │ ├── render.sh headless Chrome → PNG
@@ -2,8 +2,8 @@
"sourceId": "html-ppt", "sourceId": "html-ppt",
"repo": "https://github.com/lewislulu/html-ppt-skill.git", "repo": "https://github.com/lewislulu/html-ppt-skill.git",
"ref": "main", "ref": "main",
"commit": "f3a8435d3901697d5ac5e64d356c933637e43107", "commit": "fd1629067909ff55b36b905476de4e5f26062a1f",
"adapter": "claude-skill", "adapter": "claude-skill",
"sourcePath": ".", "sourcePath": ".",
"syncedAt": "2026-06-11T03:57:53Z" "syncedAt": "2026-09-14T15:59:59Z"
} }
@@ -0,0 +1 @@
.DS_Store
@@ -1,7 +1,7 @@
# html-ppt — HTML PPT Studio # html-ppt — HTML PPT Studio
> A world-class AgentSkill for producing professional HTML presentations in > A world-class AgentSkill for producing professional HTML presentations in
> **36 themes**, **15 full-deck templates**, **31 page layouts**, > **36 themes**, **15 full-deck templates**, **36 page layouts**,
> **47 animations** (27 CSS + 20 canvas FX), and a **true presenter mode** > **47 animations** (27 CSS + 20 canvas FX), and a **true presenter mode**
> with pixel-perfect previews + speaker script + timer — all pure static > with pixel-perfect previews + speaker script + timer — all pure static
> HTML/CSS/JS, no build step. > HTML/CSS/JS, no build step.
@@ -54,6 +54,65 @@ that supports AgentSkills can author presentations by asking things like:
> "turn this outline into a pitch deck" > "turn this outline into a pitch deck"
> "做一个小红书图文,9 张,白底柔和风" > "做一个小红书图文,9 张,白底柔和风"
## Offline / manual install
`npx skills add <url>` needs network on the target machine. Three alternatives,
in decreasing order of how much network they need.
**1. Install from a local copy.** Fetch the repo anywhere, move the folder over
(git, zip, USB), then point the CLI at the directory instead of the URL:
```bash
git clone https://github.com/lewislulu/html-ppt-skill
npx skills add ./html-ppt-skill
```
`npx` still downloads the `skills` package itself once. For a fully air-gapped
machine, run `npm i -g skills` on a connected one first, or use method 2.
**2. Copy it in by hand — no Node, no CLI.** A skill is just a folder with
`SKILL.md` at its root. Drop it in the directory your agent scans:
| Agent | Project scope | Global scope |
|---|---|---|
| Claude Code | `.claude/skills/html-ppt/` | `~/.claude/skills/html-ppt/` |
| Codex | `.agents/skills/html-ppt/` | `~/.codex/skills/html-ppt/` |
| Cursor | `.agents/skills/html-ppt/` | `~/.cursor/skills/html-ppt/` |
| OpenCode | `.agents/skills/html-ppt/` | `~/.config/opencode/skills/html-ppt/` |
| Gemini CLI | `.agents/skills/html-ppt/` | `~/.gemini/skills/html-ppt/` |
| Windsurf | `.windsurf/skills/html-ppt/` | `~/.codeium/windsurf/skills/html-ppt/` |
```bash
mkdir -p ~/.claude/skills
cp -R html-ppt-skill ~/.claude/skills/html-ppt
ls ~/.claude/skills/html-ppt/SKILL.md # must exist
```
Only `SKILL.md`, `assets/`, `templates/`, `references/` and `scripts/` are
needed at runtime. `docs/` is ~4.6 MB of README artwork and can be dropped from
an offline copy.
**3. No agent at all.** The templates are plain static files — usable directly:
```bash
./scripts/new-deck.sh my-talk
open examples/my-talk/index.html
```
### Does it run without a network?
Yes, with one caveat. Themes, layouts, animations, presenter mode and PNG
rendering are all local static HTML/CSS/JS with no build step and no runtime
fetches. The single remote dependency is `assets/fonts.css`, which `@import`s
Google Fonts.
Offline, those imports simply fail and the browser falls back to the system
stack already declared in `assets/base.css` (`-apple-system` / Helvetica /
Georgia / Menlo), so decks render correctly — just in different typefaces. To
pin typography offline, replace `assets/fonts.css` with `@font-face` rules
pointing at font files you ship yourself, or delete the imports and accept the
system stack.
## What's in the box ## What's in the box
| | Count | Where | | | Count | Where |
@@ -61,7 +120,7 @@ that supports AgentSkills can author presentations by asking things like:
| 🎤 **Presenter mode** | **NEW** | `S` key / `?preview=N` | | 🎤 **Presenter mode** | **NEW** | `S` key / `?preview=N` |
| 🎨 **Themes** | **36** | `assets/themes/*.css` | | 🎨 **Themes** | **36** | `assets/themes/*.css` |
| 📑 **Full-deck templates** | **15** | `templates/full-decks/<name>/` | | 📑 **Full-deck templates** | **15** | `templates/full-decks/<name>/` |
| 🧩 **Single-page layouts** | **31** | `templates/single-page/*.html` | | 🧩 **Single-page layouts** | **36** | `templates/single-page/*.html` |
| ✨ **CSS animations** | **27** | `assets/animations/animations.css` | | ✨ **CSS animations** | **27** | `assets/animations/animations.css` |
| 💥 **Canvas FX animations** | **20** | `assets/animations/fx/*.js` | | 💥 **Canvas FX animations** | **20** | `assets/animations/fx/*.js` |
| 🖼️ **Showcase decks** | 4 | `templates/*-showcase.html` | | 🖼️ **Showcase decks** | 4 | `templates/*-showcase.html` |
@@ -108,7 +167,7 @@ decks can be previewed side-by-side without collisions. Browse the full
gallery in `templates/full-decks-index.html`. gallery in `templates/full-decks-index.html`.
### 31 Single-page layouts ### 36 Single-page layouts
cover · toc · section-divider · bullets · two-column · three-column · cover · toc · section-divider · bullets · two-column · three-column ·
big-quote · stat-highlight · kpi-grid · table · code · diff · terminal · big-quote · stat-highlight · kpi-grid · table · code · diff · terminal ·
@@ -120,7 +179,7 @@ Every layout ships with realistic demo data so you can drop it into a deck
and immediately see it render. and immediately see it render.
*The big iframe is loading `templates/single-page/<name>.html` directly and cycling through all 31 layouts every 2.8 seconds.* *The big iframe is loading `templates/single-page/<name>.html` directly and cycling through all 36 layouts every 2.8 seconds.*
### 27 CSS animations + 20 Canvas FX ### 27 CSS animations + 20 Canvas FX
@@ -145,21 +204,71 @@ module auto-initialised on slide enter via `fx-runtime.js`.
# Scaffold a new deck from the base template # Scaffold a new deck from the base template
./scripts/new-deck.sh my-talk ./scripts/new-deck.sh my-talk
# ...or from a full-deck template, into any directory you like.
# Asset paths are computed for wherever the deck lands, then verified.
./scripts/new-deck.sh my-talk ~/decks -t pitch-deck
# Browse everything # Browse everything
open templates/theme-showcase.html # all 36 themes (iframe-isolated) open templates/theme-showcase.html # all 36 themes (iframe-isolated)
open templates/layout-showcase.html # all 31 layouts open templates/layout-showcase.html # all 36 layouts
open templates/animation-showcase.html # all 47 animations open templates/animation-showcase.html # all 47 animations
open templates/full-decks-index.html # all 14 full decks open templates/full-decks-index.html # all 15 full decks
# Render any template to PNG via headless Chrome # Render any template to PNG via headless Chrome
./scripts/render.sh templates/theme-showcase.html ./scripts/render.sh templates/theme-showcase.html
./scripts/render.sh examples/my-talk/index.html 12 ./scripts/render.sh examples/my-talk/index.html 12
``` ```
## Images on a slide
Five layouts take real images — pick by how many the page has to carry:
| I have… | Layout |
|---|---|
| one screenshot / diagram / chart | `image-single.html` — letterboxed, never cropped |
| one photo that should carry the page | `image-full-bleed.html` — fills the slide, scrim keeps the title readable |
| one image plus an argument | `image-text-split.html` — 50/50, `flip` to swap sides |
| 3–6 images | `image-gallery.html` — uniform grid, one caption each |
| a before and an after | `image-compare.html` — both sides identical size |
They share one primitive from `assets/base.css`:
```html
<figure class="img-frame"><img src="shot.png" alt=""></figure> <!-- crops to fill -->
<figure class="img-frame contain"><img src="diagram.svg" alt=""></figure> <!-- letterboxed -->
```
The frame owns the aspect ratio (`--img-ratio`) and the crop, so you can drop in
a portrait, square or ultrawide image without touching the layout. Placeholder
artwork in `assets/demo-images/` is hand-written SVG (~1 KB each) so every
layout renders **offline**.
## Custom logo
Brand a deck with one attribute — no per-slide `<img>` copy-paste:
```html
<body data-logo="logo.svg" data-logo-position="bottom-right" data-logo-size="40px">
```
`data-logo-position` takes `top-left` / `top-right` / `bottom-left` /
`bottom-right` (default `top-right`); `data-logo-size` sets the height.
Skip it on a single slide with `<section class="slide" data-no-logo>` — handy
for the cover. The logo also shows in the presenter preview, and on **every
page** of a PDF export (skipping the `data-no-logo` ones).
Prefer to place it yourself? `<img class="deck-logo" data-pos="top-left" src="logo.svg">`
inside `.deck` works with no JS at all.
## Keyboard cheat sheet ## Keyboard cheat sheet
On a phone or tablet, **swipe left for the next slide, right for the previous
one** — no keyboard needed. Pinch-zoom, vertical scrolling, the overview grid
and the notes drawer are left alone.
``` ```
← → Space PgUp PgDn Home End navigate ← → Space PgUp PgDn Home End navigate
swipe ← / → (touch) navigate
F fullscreen F fullscreen
S open presenter window (magnetic cards) S open presenter window (magnetic cards)
N quick notes drawer (bottom) N quick notes drawer (bottom)
@@ -181,7 +290,7 @@ html-ppt-skill/
│ ├── themes.md 36 themes with when-to-use │ ├── themes.md 36 themes with when-to-use
│ ├── layouts.md 31 layout types │ ├── layouts.md 31 layout types
│ ├── animations.md 27 CSS + 20 FX catalog │ ├── animations.md 27 CSS + 20 FX catalog
│ ├── full-decks.md 14 full-deck templates │ ├── full-decks.md 15 full-deck templates
│ └── authoring-guide.md full workflow │ └── authoring-guide.md full workflow
├── assets/ ├── assets/
│ ├── base.css shared tokens + primitives │ ├── base.css shared tokens + primitives
@@ -195,10 +304,10 @@ html-ppt-skill/
├── templates/ ├── templates/
│ ├── deck.html minimal starter │ ├── deck.html minimal starter
│ ├── theme-showcase.html iframe-isolated theme tour │ ├── theme-showcase.html iframe-isolated theme tour
│ ├── layout-showcase.html all 31 layouts │ ├── layout-showcase.html all 36 layouts
│ ├── animation-showcase.html 47 animation slides │ ├── animation-showcase.html 47 animation slides
│ ├── full-decks-index.html 14-deck gallery │ ├── full-decks-index.html 15-deck gallery
│ ├── full-decks/<name>/ 14 scoped multi-slide decks │ ├── full-decks/<name>/ 15 scoped multi-slide decks
│ └── single-page/*.html 31 layout files with demo data │ └── single-page/*.html 31 layout files with demo data
├── scripts/ ├── scripts/
│ ├── new-deck.sh scaffold │ ├── new-deck.sh scaffold
@@ -1,7 +1,7 @@
# html-ppt · HTML PPT 工作室 # html-ppt · HTML PPT 工作室
> 一款专业级的 AgentSkill,让 AI 做出真正能打的 HTML 演示文稿。 > 一款专业级的 AgentSkill,让 AI 做出真正能打的 HTML 演示文稿。
> **36 套主题**、**15 套完整 deck 模板**、**31 种页面布局**、**47 个动效** > **36 套主题**、**15 套完整 deck 模板**、**36 种页面布局**、**47 个动效**
> (27 个 CSS + 20 个 Canvas FX),加上全新的 **演讲者模式** —— 像素级 > (27 个 CSS + 20 个 Canvas FX),加上全新的 **演讲者模式** —— 像素级
> 完美预览 + 逐字稿提词器 + 计时器。纯静态 HTML/CSS/JS,无需构建。 > 完美预览 + 逐字稿提词器 + 计时器。纯静态 HTML/CSS/JS,无需构建。
@@ -10,7 +10,7 @@
**English docs:** [README.md](README.md) **English docs:** [README.md](README.md)
> 一行命令装好 **36 主题 × 20 Canvas FX × 31 布局 × 15 完整 deck + 演讲者模式**。 > 一行命令装好 **36 主题 × 20 Canvas FX × 36 布局 × 15 完整 deck + 演讲者模式**。
## 🎤 演讲者模式(全新) ## 🎤 演讲者模式(全新)
@@ -51,6 +51,60 @@ npx skills add https://github.com/lewislulu/html-ppt-skill
> "做一个小红书图文,9 张,白底柔和风" > "做一个小红书图文,9 张,白底柔和风"
> "做一份带演讲者模式的产品分享,我想要有逐字稿" > "做一份带演讲者模式的产品分享,我想要有逐字稿"
## 离线 / 手动安装
`npx skills add <url>` 需要目标机器能联网。下面三种方式,对网络的依赖依次递减。
**1. 从本地副本安装。** 在任意能联网的机器上取到仓库,把整个目录搬过去(git、zip、U 盘都行),
然后把 CLI 指向本地目录而不是 URL:
```bash
git clone https://github.com/lewislulu/html-ppt-skill
npx skills add ./html-ppt-skill
```
`npx` 本身仍需下载一次 `skills` 包。完全隔离的机器请先在联网机器上
`npm i -g skills`,或者直接用方式 2。
**2. 纯手工拷贝 —— 不需要 Node,也不需要 CLI。** 一个 skill 就是一个根目录下放着
`SKILL.md` 的文件夹。放进你的 agent 会扫描的目录即可:
| Agent | 项目级 | 全局 |
|---|---|---|
| Claude Code | `.claude/skills/html-ppt/` | `~/.claude/skills/html-ppt/` |
| Codex | `.agents/skills/html-ppt/` | `~/.codex/skills/html-ppt/` |
| Cursor | `.agents/skills/html-ppt/` | `~/.cursor/skills/html-ppt/` |
| OpenCode | `.agents/skills/html-ppt/` | `~/.config/opencode/skills/html-ppt/` |
| Gemini CLI | `.agents/skills/html-ppt/` | `~/.gemini/skills/html-ppt/` |
| Windsurf | `.windsurf/skills/html-ppt/` | `~/.codeium/windsurf/skills/html-ppt/` |
```bash
mkdir -p ~/.claude/skills
cp -R html-ppt-skill ~/.claude/skills/html-ppt
ls ~/.claude/skills/html-ppt/SKILL.md # 必须存在
```
运行时只需要 `SKILL.md`、`assets/`、`templates/`、`references/`、`scripts/`。
`docs/` 是约 4.6 MB 的 README 配图,离线拷贝时可以删掉。
**3. 完全不用 agent。** 模板就是普通静态文件,可以直接用:
```bash
./scripts/new-deck.sh my-talk
open examples/my-talk/index.html
```
### 断网能用吗?
能,只有一个前提要说清楚。主题、布局、动效、演讲者模式、PNG 导出全部是本地静态
HTML/CSS/JS,零构建、运行时不发请求。唯一的远程依赖是 `assets/fonts.css`,
它 `@import` 了 Google Fonts。
断网时这些 import 直接失败,浏览器回落到 `assets/base.css` 里已经声明好的系统字体栈
(`-apple-system` / Helvetica / Georgia / Menlo),所以 deck 照常渲染,只是字体不同。
如果要在离线环境下锁定字体,把 `assets/fonts.css` 换成指向自带字体文件的 `@font-face`
规则,或者删掉这些 import、接受系统字体。
## Skill 内容一览 ## Skill 内容一览
| | 数量 | 位置 | | | 数量 | 位置 |
@@ -58,7 +112,7 @@ npx skills add https://github.com/lewislulu/html-ppt-skill
| 🎤 **演讲者模式** | **新增** | `S` 键 / `?preview=N` | | 🎤 **演讲者模式** | **新增** | `S` 键 / `?preview=N` |
| 🎨 **主题** | **36** | `assets/themes/*.css` | | 🎨 **主题** | **36** | `assets/themes/*.css` |
| 📑 **完整 deck 模板** | **15** | `templates/full-decks/<name>/` | | 📑 **完整 deck 模板** | **15** | `templates/full-decks/<name>/` |
| 🧩 **单页布局** | **31** | `templates/single-page/*.html` | | 🧩 **单页布局** | **36** | `templates/single-page/*.html` |
| ✨ **CSS 动画** | **27** | `assets/animations/animations.css` | | ✨ **CSS 动画** | **27** | `assets/animations/animations.css` |
| 💥 **Canvas FX 动画** | **20** | `assets/animations/fx/*.js` | | 💥 **Canvas FX 动画** | **20** | `assets/animations/fx/*.js` |
| 🖼️ **Showcase deck** | 4 | `templates/*-showcase.html` | | 🖼️ **Showcase deck** | 4 | `templates/*-showcase.html` |
@@ -108,7 +162,7 @@ npx skills add https://github.com/lewislulu/html-ppt-skill
同时加载不会互相污染。在 `templates/full-decks-index.html` 可以看全套 gallery。 同时加载不会互相污染。在 `templates/full-decks-index.html` 可以看全套 gallery。
### 31 种单页布局 ### 36 种单页布局
cover · toc · section-divider · bullets · two-column · three-column · cover · toc · section-divider · bullets · two-column · three-column ·
big-quote · stat-highlight · kpi-grid · table · code · diff · terminal · big-quote · stat-highlight · kpi-grid · table · code · diff · terminal ·
@@ -148,9 +202,13 @@ chart-pie · chart-radar · arch-diagram · process-steps · cta · thanks
# 从 base 模板新建一个 deck # 从 base 模板新建一个 deck
./scripts/new-deck.sh my-talk ./scripts/new-deck.sh my-talk
# 也可以指定完整 deck 模板和任意输出目录。
# assets 路径会按 deck 的实际位置算出来,并逐条校验能否解析。
./scripts/new-deck.sh my-talk ~/decks -t pitch-deck
# 浏览所有内容 # 浏览所有内容
open templates/theme-showcase.html # 全部 36 主题(iframe 隔离) open templates/theme-showcase.html # 全部 36 主题(iframe 隔离)
open templates/layout-showcase.html # 全部 31 布局 open templates/layout-showcase.html # 全部 36 布局
open templates/animation-showcase.html # 全部 47 动效 open templates/animation-showcase.html # 全部 47 动效
open templates/full-decks-index.html # 全部 15 个完整 deck open templates/full-decks-index.html # 全部 15 个完整 deck
@@ -159,10 +217,55 @@ open templates/full-decks-index.html # 全部 15 个完整 deck
./scripts/render.sh examples/my-talk/index.html 12 ./scripts/render.sh examples/my-talk/index.html 12
``` ```
## 放图片的版式
五个版式用的是真实 `<img>`,按「这一页要放几张图」挑:
| 我有… | 版式 |
|---|---|
| 一张截图 / 示意图 / 图表 | `image-single.html` —— 完整显示,**不裁剪** |
| 一张想撑满整页的照片 | `image-full-bleed.html` —— 整页铺满,底部压暗保证标题可读 |
| 一张图 + 一段论述 | `image-text-split.html` —— 各占一半,加 `flip` 左右互换 |
| 3~6 张图 | `image-gallery.html` —— 等大网格,每张一句话 |
| 改版前 / 改版后 | `image-compare.html` —— 两侧严格同尺寸 |
它们共用 `assets/base.css` 里的同一个原语:
```html
<figure class="img-frame"><img src="shot.png" alt=""></figure> <!-- 裁剪填满 -->
<figure class="img-frame contain"><img src="diagram.svg" alt=""></figure> <!-- 完整显示 -->
```
比例和裁剪由**框**决定(`--img-ratio`),不由图片决定 —— 竖图、方图、超宽图
直接换 `src` 就行,版式不用改。示例图放在 `assets/demo-images/`,是手写的
SVG(每个约 1KB),所以这些版式**离线也能正常渲染**。
## 自定义 LOGO
一个属性给整份 deck 加上公司 / 产品 LOGO,不用每页粘一个 `<img>`:
```html
<body data-logo="logo.svg" data-logo-position="bottom-right" data-logo-size="40px">
```
`data-logo-position` 可选 `top-left` / `top-right` / `bottom-left` /
`bottom-right`(默认 `top-right`);`data-logo-size` 设置高度,宽度按比例。
某一页不想要(通常是封面)就写 `<section class="slide" data-no-logo>`。
演讲者模式的预览里带 LOGO;导出 PDF 时**每一页都带**(写了 `data-no-logo`
的那页除外)。
想自己摆位置:在 `.deck` 里直接写
`<img class="deck-logo" data-pos="top-left" src="logo.svg">`,
样式在 `base.css` 里,**完全不依赖 JS**。
## 键盘快捷键 ## 键盘快捷键
手机 / 平板上**向左划到下一页,向右划回上一页**,不需要键盘。
双指缩放、纵向滚动、总览网格和 notes 抽屉都不受影响。
``` ```
← → Space PgUp PgDn Home End 翻页 ← → Space PgUp PgDn Home End 翻页
左划 / 右划(触摸) 翻页
F 全屏 F 全屏
S 打开演讲者窗口(磁吸卡片模式) S 打开演讲者窗口(磁吸卡片模式)
N 底部 notes 抽屉 N 底部 notes 抽屉
@@ -183,7 +286,7 @@ html-ppt-skill/
├── README.zh-CN.md 本文件 ├── README.zh-CN.md 本文件
├── references/ 详细文档 ├── references/ 详细文档
│ ├── themes.md 36 主题 + 使用场景 │ ├── themes.md 36 主题 + 使用场景
│ ├── layouts.md 31 布局 │ ├── layouts.md 36 布局
│ ├── animations.md 27 CSS + 20 FX 目录 │ ├── animations.md 27 CSS + 20 FX 目录
│ ├── full-decks.md 15 完整 deck 模板 │ ├── full-decks.md 15 完整 deck 模板
│ ├── presenter-mode.md 🎤 演讲者模式 + 逐字稿指南 │ ├── presenter-mode.md 🎤 演讲者模式 + 逐字稿指南
@@ -200,11 +303,11 @@ html-ppt-skill/
├── templates/ ├── templates/
│ ├── deck.html 最小起步模板 │ ├── deck.html 最小起步模板
│ ├── theme-showcase.html iframe 隔离的主题 tour │ ├── theme-showcase.html iframe 隔离的主题 tour
│ ├── layout-showcase.html 全部 31 布局 │ ├── layout-showcase.html 全部 36 布局
│ ├── animation-showcase.html 47 动画 slide │ ├── animation-showcase.html 47 动画 slide
│ ├── full-decks-index.html 15 deck gallery │ ├── full-decks-index.html 15 deck gallery
│ ├── full-decks/<name>/ 15 个 scoped 多页 deck 模板 │ ├── full-decks/<name>/ 15 个 scoped 多页 deck 模板
│ └── single-page/*.html 31 个布局文件(带示例数据) │ └── single-page/*.html 36 个布局文件(带示例数据)
├── scripts/ ├── scripts/
│ ├── new-deck.sh 脚手架 │ ├── new-deck.sh 脚手架
│ ├── render.sh headless Chrome → PNG │ ├── render.sh headless Chrome → PNG
@@ -17,14 +17,21 @@ npx skills add https://github.com/lewislulu/html-ppt-skill
One command, no build. Pure static HTML/CSS/JS with only CDN webfonts. One command, no build. Pure static HTML/CSS/JS with only CDN webfonts.
No network on the target machine? Point the CLI at a local copy
(`npx skills add ./html-ppt-skill`), or just copy this folder into your agent's
skills directory — `~/.claude/skills/html-ppt/` for Claude Code. Decks render
offline; only the webfonts fall back to the system stack. See
[README.md](README.md#offline--manual-install).
## What the skill gives you ## What the skill gives you
- **36 themes** (`assets/themes/*.css`) — minimal-white, editorial-serif, soft-pastel, sharp-mono, arctic-cool, sunset-warm, catppuccin-latte/mocha, dracula, tokyo-night, nord, solarized-light, gruvbox-dark, rose-pine, neo-brutalism, glassmorphism, bauhaus, swiss-grid, terminal-green, xiaohongshu-white, rainbow-gradient, aurora, blueprint, memphis-pop, cyberpunk-neon, y2k-chrome, retro-tv, japanese-minimal, vaporwave, midcentury, corporate-clean, academic-paper, news-broadcast, pitch-deck-vc, magazine-bold, engineering-whiteprint - **36 themes** (`assets/themes/*.css`) — minimal-white, editorial-serif, soft-pastel, sharp-mono, arctic-cool, sunset-warm, catppuccin-latte/mocha, dracula, tokyo-night, nord, solarized-light, gruvbox-dark, rose-pine, neo-brutalism, glassmorphism, bauhaus, swiss-grid, terminal-green, xiaohongshu-white, rainbow-gradient, aurora, blueprint, memphis-pop, cyberpunk-neon, y2k-chrome, retro-tv, japanese-minimal, vaporwave, midcentury, corporate-clean, academic-paper, news-broadcast, pitch-deck-vc, magazine-bold, engineering-whiteprint
- **15 full-deck templates** (`templates/full-decks/<name>/`) — complete multi-slide decks with scoped `.tpl-<name>` CSS. 8 extracted from real-world decks (xhs-white-editorial, graphify-dark-graph, knowledge-arch-blueprint, hermes-cyber-terminal, obsidian-claude-gradient, testing-safety-alert, xhs-pastel-card, dir-key-nav-minimal), 7 scenario scaffolds (pitch-deck, product-launch, tech-sharing, weekly-report, xhs-post 3:4, course-module, **presenter-mode-reveal** — 演讲者模式专用) - **15 full-deck templates** (`templates/full-decks/<name>/`) — complete multi-slide decks with scoped `.tpl-<name>` CSS. 8 extracted from real-world decks (xhs-white-editorial, graphify-dark-graph, knowledge-arch-blueprint, hermes-cyber-terminal, obsidian-claude-gradient, testing-safety-alert, xhs-pastel-card, dir-key-nav-minimal), 7 scenario scaffolds (pitch-deck, product-launch, tech-sharing, weekly-report, xhs-post 3:4, course-module, **presenter-mode-reveal** — 演讲者模式专用)
- **31 layouts** (`templates/single-page/*.html`) with realistic demo data - **36 layouts** (`templates/single-page/*.html`) with realistic demo data, including **5 real-image layouts** (single / full-bleed / image+text / gallery / before-after)
- **27 CSS animations** (`assets/animations/animations.css`) via `data-anim` - **27 CSS animations** (`assets/animations/animations.css`) via `data-anim`
- **20 canvas FX animations** (`assets/animations/fx/*.js`) via `data-fx` — particle-burst, confetti-cannon, firework, starfield, matrix-rain, knowledge-graph (force-directed), neural-net (pulses), constellation, orbit-ring, galaxy-swirl, word-cascade, letter-explode, chain-react, magnetic-field, data-stream, gradient-blob, sparkle-trail, shockwave, typewriter-multi, counter-explosion - **20 canvas FX animations** (`assets/animations/fx/*.js`) via `data-fx` — particle-burst, confetti-cannon, firework, starfield, matrix-rain, knowledge-graph (force-directed), neural-net (pulses), constellation, orbit-ring, galaxy-swirl, word-cascade, letter-explode, chain-react, magnetic-field, data-stream, gradient-blob, sparkle-trail, shockwave, typewriter-multi, counter-explosion
- **Keyboard runtime** (`assets/runtime.js`) — arrows, T (theme), A (anim), F/O, **S (presenter mode: magnetic-card popup with CURRENT / NEXT / SCRIPT / TIMER cards)**, N (notes drawer), R (reset timer in presenter) - **Keyboard runtime** (`assets/runtime.js`) — arrows, T (theme), A (anim), F/O, **S (presenter mode: magnetic-card popup with CURRENT / NEXT / SCRIPT / TIMER cards)**, N (notes drawer), R (reset timer in presenter)
- **Touch navigation** — swipe left/right to change slides on phones and tablets
- **FX runtime** (`assets/animations/fx-runtime.js`) — auto-inits `[data-fx]` on slide enter, cleans up on leave - **FX runtime** (`assets/animations/fx-runtime.js`) — auto-inits `[data-fx]` on slide enter, cleans up on leave
- **Showcase decks** for themes / layouts / animations / full-decks gallery - **Showcase decks** for themes / layouts / animations / full-decks gallery
- **Headless Chrome render script** for PNG export - **Headless Chrome render script** for PNG export
@@ -78,7 +85,7 @@ tasteful default and confirm.
- Academic / report → `academic-paper`, `editorial-serif`, `minimal-white` - Academic / report → `academic-paper`, `editorial-serif`, `minimal-white`
- Edgy / cyber / launch → `cyberpunk-neon`, `vaporwave`, `y2k-chrome`, - Edgy / cyber / launch → `cyberpunk-neon`, `vaporwave`, `y2k-chrome`,
`neo-brutalism` `neo-brutalism`
3. **Starting point.** One of the 14 full-deck templates, or scratch? Point 3. **Starting point.** One of the 15 full-deck templates, or scratch? Point
to the closest `templates/full-decks/<name>/` and ask if it fits. If the to the closest `templates/full-decks/<name>/` and ask if it fits. If the
user's content suggests something obvious (e.g. "我要做产品发布会" → user's content suggests something obvious (e.g. "我要做产品发布会" →
`product-launch`), propose it confidently instead of asking blindly. `product-launch`), propose it confidently instead of asking blindly.
@@ -99,7 +106,9 @@ Only after those are clear, scaffold the deck and start writing.
./scripts/new-deck.sh my-talk ./scripts/new-deck.sh my-talk
open examples/my-talk/index.html open examples/my-talk/index.html
``` ```
2. **Pick a theme.** Open the deck and press `T` to cycle. Or hard-code it: 2. **Pick a theme.** Open the deck and press `T` to cycle. Or hard-code it
(`../assets/` here is a placeholder — use whatever prefix the rest of the
file already uses; `new-deck.sh` has set it to the right depth):
```html ```html
<link rel="stylesheet" id="theme-link" href="../assets/themes/aurora.css"> <link rel="stylesheet" id="theme-link" href="../assets/themes/aurora.css">
``` ```
@@ -112,10 +121,15 @@ Only after those are clear, scaffold the deck and start writing.
For canvas FX, use `<div data-fx="knowledge-graph">...</div>` and include For canvas FX, use `<div data-fx="knowledge-graph">...</div>` and include
`<script src="../assets/animations/fx-runtime.js"></script>`. `<script src="../assets/animations/fx-runtime.js"></script>`.
Catalog in [references/animations.md](references/animations.md). Catalog in [references/animations.md](references/animations.md).
5. **Use a full-deck template.** Copy `templates/full-decks/<name>/` into 5. **Use a full-deck template.** Scaffold from it, don't copy it by hand —
`examples/my-talk/` as a starting point. Each folder is self-contained with the template's `../../../assets/` is relative to *its own* location, so a
scoped CSS. Catalog in [references/full-decks.md](references/full-decks.md) manual copy lands the paths at the wrong depth:
and gallery at `templates/full-decks-index.html`. ```bash
./scripts/new-deck.sh my-talk -t pitch-deck
```
Each folder is self-contained with scoped CSS. Catalog in
[references/full-decks.md](references/full-decks.md) and gallery at
`templates/full-decks-index.html`.
6. **Render to PNG.** 6. **Render to PNG.**
```bash ```bash
./scripts/render.sh templates/theme-showcase.html # one shot ./scripts/render.sh templates/theme-showcase.html # one shot
@@ -129,12 +143,28 @@ Only after those are clear, scaffold the deck and start writing.
- **Use tokens, not literal colors.** Every color, radius, shadow should come - **Use tokens, not literal colors.** Every color, radius, shadow should come
from CSS variables defined in `assets/base.css` and overridden by a theme. from CSS variables defined in `assets/base.css` and overridden by a theme.
Good: `color: var(--text-1)`. Bad: `color: #111`. Good: `color: var(--text-1)`. Bad: `color: #111`.
Text on top of an `--accent` fill is the one people get wrong: it needs
`color: var(--accent-ink)`, because accents here run from `#ffffff` to
`#000000` and no literal ink is readable on all of them.
- **Don't invent new layout files.** Prefer composing existing ones. Only add - **Don't invent new layout files.** Prefer composing existing ones. Only add
a new `templates/single-page/*.html` if none of the 30 fit. a new `templates/single-page/*.html` if none of the 36 fit.
- **Putting images on a slide?** Start from one of the five `image-*` layouts and
use `.img-frame` — see *Images* below. Never drop a bare `<img>` into a slide:
an unframed image ignores the slide's height and pushes the rest off the page.
- **Respect chrome slots.** `.deck-header`, `.deck-footer`, `.slide-number` - **Respect chrome slots.** `.deck-header`, `.deck-footer`, `.slide-number`
and the progress bar are provided by `assets/base.css` + `runtime.js`. and the progress bar are provided by `assets/base.css` + `runtime.js`.
- **Keyboard-first.** Always include `<script src="../assets/runtime.js"></script>` - **Add a logo declaratively, once.** Put `data-logo` on `<body>` — don't paste
so the deck supports ← → / T / A / F / S / O / hash deep-links. an `<img>` into every slide. See *Custom logo* below.
- **Keyboard-first.** Always include the runtime, e.g.
`<script src="../assets/runtime.js"></script>`, so the deck supports
← → / T / A / F / S / O / hash deep-links.
- **Never hand-edit the `../` depth in asset paths.** Every `assets/` reference
is relative to the file that holds it: `templates/deck.html` uses
`../assets/`, `templates/single-page/*.html` use `../../assets/`, and
`templates/full-decks/*/index.html` use `../../../assets/`. Copying a file to
a new depth silently breaks all of them. Scaffold with
`./scripts/new-deck.sh <name> [parent] [-t <template>]`, which computes the
prefix for wherever the deck lands and verifies every reference resolves.
- **One `.slide` per logical page.** `runtime.js` makes `.slide.is-active` - **One `.slide` per logical page.** `runtime.js` makes `.slide.is-active`
visible; all others are hidden. visible; all others are hidden.
- **Supply notes.** Wrap speaker notes in `<div class="notes">…</div>` inside - **Supply notes.** Wrap speaker notes in `<div class="notes">…</div>` inside
@@ -146,6 +176,76 @@ Only after those are clear, scaffold the deck and start writing.
by default — it only appears in the S overlay. Slides should contain ONLY by default — it only appears in the S overlay. Slides should contain ONLY
audience-facing content (titles, bullet points, data, charts, images). audience-facing content (titles, bullet points, data, charts, images).
## Images
Five layouts in `templates/single-page/` take real images. Pick by how many
images the page has to carry:
| I have… | Use | Why |
|---|---|---|
| one screenshot / diagram / chart | `image-single.html` | `.img-frame.contain` — letterboxed, **never cropped** |
| one photo that should carry the page | `image-full-bleed.html` | fills the slide, gradient scrim keeps the title readable |
| one image plus an argument | `image-text-split.html` | 50/50; add `flip` to `.split` to move the image right |
| 3–6 images | `image-gallery.html` | uniform grid; mixed source ratios are normalised by the frame |
| a before and an after | `image-compare.html` | both sides identical size, conclusion under each |
`image-grid.html` and `image-hero.html` are **not** in this list: they are
gradient-placeholder layouts with no `<img>` at all. Reach for them when you
want the shape of a bento wall without supplying pictures.
All five are built on one primitive from `assets/base.css`:
```html
<figure class="img-frame"><img src="shot.png" alt=""></figure>
<figure class="img-frame contain" style="--img-ratio:4/3"><img src="diagram.svg" alt=""></figure>
```
- `.img-frame` owns the **aspect ratio and the crop**; the `<img>` fills it with
`object-fit: cover`. That's what lets a user swap in a photo of any shape
without the layout breaking.
- `.img-frame.contain` letterboxes instead of cropping — **always use it for
screenshots, diagrams and logos.**
- `--img-ratio` (default `16/10`) and `--img-pos` (`object-position`) tune it.
- `.img-scrim` / `.img-cap` / `.img-tag` are the scrim, caption and corner pill.
- Images referenced from a deck are resolved relative to the deck's own
`index.html` — keep them in the deck folder, e.g. `examples/my-talk/shot.png`.
- `assets/demo-images/` holds the placeholder artwork used by these layouts:
hand-written SVG, ~1 KB each, **no network needed**.
## Custom logo
To brand a deck with a company / product logo, declare it once on `<body>`:
```html
<body data-logo="logo.svg"
data-logo-position="bottom-right"
data-logo-size="40px">
```
| Attribute | Default | Notes |
|---|---|---|
| `data-logo` | — | Image URL, relative to the deck's own HTML file. Required. |
| `data-logo-position` | `top-right` | `top-left` / `top-right` / `bottom-left` / `bottom-right` |
| `data-logo-size` | `44px` | Any CSS length; sets the logo's **height**, width follows the aspect ratio |
| `data-logo-opacity` | `.9` | `1` for full strength |
| `data-logo-alt` | `""` | Alt text |
- **Skip it on one slide** with `<section class="slide" data-no-logo>` — usually
the cover and any full-bleed image slide that carries its own branding.
- **Fine-tune the inset** with `--logo-inset-x` / `--logo-inset-y` on `.deck-logo`.
- **Place it by hand** instead, if you want it inside the chrome slots or in a
spot the four presets don't cover:
```html
<div class="deck">
<img class="deck-logo" data-pos="bottom-left" src="logo.svg" alt="">
```
This path needs no JS at all — `base.css` styles both the same way.
- The logo shows in the **presenter preview**, and on **every page of a
print/PDF export** (unlike the header/footer/progress chrome, which print
hides). `data-no-logo` slides are skipped there too. Per-page printing is
painted by `runtime.js` + `@media print`; a deck that omits the runtime still
gets the logo on screen, but only on one page of a PDF.
## Writing guide ## Writing guide
See [references/authoring-guide.md](references/authoring-guide.md) for a See [references/authoring-guide.md](references/authoring-guide.md) for a
@@ -156,7 +256,7 @@ Chinese + English deck, and how to export.
## Catalogs (load when needed) ## Catalogs (load when needed)
- [references/themes.md](references/themes.md) — all 36 themes with when-to-use. - [references/themes.md](references/themes.md) — all 36 themes with when-to-use.
- [references/layouts.md](references/layouts.md) — all 31 layout types. - [references/layouts.md](references/layouts.md) — all 36 layout types.
- [references/animations.md](references/animations.md) — 27 CSS + 20 canvas FX animations. - [references/animations.md](references/animations.md) — 27 CSS + 20 canvas FX animations.
- [references/full-decks.md](references/full-decks.md) — all 15 full-deck templates. - [references/full-decks.md](references/full-decks.md) — all 15 full-deck templates.
- [references/presenter-mode.md](references/presenter-mode.md) — **演讲者模式 + 逐字稿编写指南(技术分享/演讲必看)**. - [references/presenter-mode.md](references/presenter-mode.md) — **演讲者模式 + 逐字稿编写指南(技术分享/演讲必看)**.
@@ -170,6 +270,7 @@ html-ppt/
├── references/ (detailed catalogs, load as needed) ├── references/ (detailed catalogs, load as needed)
├── assets/ ├── assets/
│ ├── base.css (tokens + primitives — do not edit per deck) │ ├── base.css (tokens + primitives — do not edit per deck)
│ ├── demo-images/*.svg (tiny offline placeholders for the image-* layouts)
│ ├── fonts.css (webfont imports) │ ├── fonts.css (webfont imports)
│ ├── runtime.js (keyboard + presenter + overview + theme cycle) │ ├── runtime.js (keyboard + presenter + overview + theme cycle)
│ ├── themes/*.css (36 token overrides, one per theme) │ ├── themes/*.css (36 token overrides, one per theme)
@@ -180,11 +281,11 @@ html-ppt/
├── templates/ ├── templates/
│ ├── deck.html (minimal 6-slide starter) │ ├── deck.html (minimal 6-slide starter)
│ ├── theme-showcase.html (36 slides, iframe-isolated per theme) │ ├── theme-showcase.html (36 slides, iframe-isolated per theme)
│ ├── layout-showcase.html (iframe tour of all 31 layouts) │ ├── layout-showcase.html (iframe tour of all 36 layouts)
│ ├── animation-showcase.html (20 FX + 27 CSS animation slides) │ ├── animation-showcase.html (20 FX + 27 CSS animation slides)
│ ├── full-decks-index.html (gallery of all 14 full-deck templates) │ ├── full-decks-index.html (gallery of all 15 full-deck templates)
│ ├── full-decks/<name>/ (14 scoped multi-slide deck templates) │ ├── full-decks/<name>/ (15 scoped multi-slide deck templates)
│ └── single-page/*.html (31 layout files with demo data) │ └── single-page/*.html (36 layout files with demo data)
├── scripts/ ├── scripts/
│ ├── new-deck.sh (scaffold a deck from deck.html) │ ├── new-deck.sh (scaffold a deck from deck.html)
│ └── render.sh (headless Chrome → PNG) │ └── render.sh (headless Chrome → PNG)
@@ -206,6 +307,7 @@ capture, runtime.js exposes `#/N` deep-links, and render.sh iterates 1..N.
``` ```
← → Space PgUp PgDn Home End navigate ← → Space PgUp PgDn Home End navigate
swipe left / right (touch) navigate — phones and tablets, no keyboard needed
F fullscreen F fullscreen
S open presenter window (magnetic cards: current/next/script/timer) S open presenter window (magnetic cards: current/next/script/timer)
N quick notes drawer (bottom overlay) N quick notes drawer (bottom overlay)
@@ -38,9 +38,17 @@
window.__hpxActive = window.__hpxActive || new Map(); window.__hpxActive = window.__hpxActive || new Map();
/* querySelectorAll never matches root itself, but decks put data-fx on the
* <section class="slide"> too (e.g. a cover starfield) — include it. */
function fxEls(root){
const els = Array.from(root.querySelectorAll('[data-fx]'));
if (root.matches && root.matches('[data-fx]')) els.unshift(root);
return els;
}
function initFxIn(root){ function initFxIn(root){
if (!window.HPX) return; if (!window.HPX) return;
const els = root.querySelectorAll('[data-fx]'); const els = fxEls(root);
els.forEach((el) => { els.forEach((el) => {
if (window.__hpxActive.has(el)) return; if (window.__hpxActive.has(el)) return;
const name = el.getAttribute('data-fx'); const name = el.getAttribute('data-fx');
@@ -54,7 +62,7 @@
} }
function stopFxIn(root){ function stopFxIn(root){
const els = root.querySelectorAll('[data-fx]'); const els = fxEls(root);
els.forEach((el) => { els.forEach((el) => {
const h = window.__hpxActive.get(el); const h = window.__hpxActive.get(el);
if (h && typeof h.stop === 'function'){ if (h && typeof h.stop === 'function'){
@@ -11,6 +11,14 @@
--text-2: #55596a; --text-2: #55596a;
--text-3: #8a8f9e; --text-3: #8a8f9e;
--accent: #3b6cff; --accent: #3b6cff;
/* Text drawn on top of an --accent fill. Every theme in assets/themes/
* and every full-deck style.css overrides this with whichever of white /
* near-black clears WCAG AA against its own accent — a hardcoded ink
* cannot, because accents here range from #ffffff to #000000.
* This default is black rather than white on purpose: against the default
* --accent #3b6cff, white measures 4.40:1 and black 4.77:1, so only black
* clears AA for normal text. Reached by any deck that links no theme. */
--accent-ink: #000000;
--accent-2: #7a5cff; --accent-2: #7a5cff;
--accent-3: #ff5c8a; --accent-3: #ff5c8a;
--good: #1aaf6c; --good: #1aaf6c;
@@ -37,13 +45,36 @@ html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
font-family:var(--font-sans);font-weight:400;line-height:1.6; font-family:var(--font-sans);font-weight:400;line-height:1.6;
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale; -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
letter-spacing:var(--letter-normal)} letter-spacing:var(--letter-normal)}
/* Viewport host: centres the fixed-size design canvas and letterboxes the rest.
Every px in a slide therefore means the same thing in the browser, in the
presenter preview, in the overview thumbnail and in a headless render. */
body{width:100vw;height:100vh;overflow:hidden;
display:flex;align-items:center;justify-content:center}
img,svg,video{max-width:100%;display:block} img,svg,video{max-width:100%;display:block}
a{color:var(--accent);text-decoration:none} a{color:var(--accent);text-decoration:none}
a:hover{text-decoration:underline} a:hover{text-decoration:underline}
code,kbd,pre,samp{font-family:var(--font-mono)} code,kbd,pre,samp{font-family:var(--font-mono)}
/* ================= SLIDE SYSTEM ================= */ /* ================= SLIDE SYSTEM ================= */
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)} /* ================= DESIGN CANVAS =================
Slides are authored against a FIXED 1920x1080 canvas and scaled to fit the
viewport by runtime.js (--deck-scale). Before this, .deck was 100vw/100vh
while type and spacing stayed in absolute px, so a 1512x850 laptop window
and a 1920x1080 render produced visibly different pages (issue #20).
Escape hatches:
<body data-fit="fluid"> -> old behaviour, canvas follows the viewport
<div class="deck" data-w=".." data-h=".."> -> custom canvas (e.g. 1080x1440 for 3:4)
*/
.deck{position:relative;flex:none;
width:var(--deck-w,1920px);height:var(--deck-h,1080px);
overflow:hidden;background:var(--bg);
transform:scale(var(--deck-scale,1));transform-origin:center center}
/* Opt out: fluid canvas, exactly what the deck did before. */
body[data-fit="fluid"]{display:block}
body[data-fit="fluid"] .deck{width:100vw;height:100vh;transform:none}
.slide{ .slide{
position:absolute;inset:0; position:absolute;inset:0;
display:flex;flex-direction:column;justify-content:center; display:flex;flex-direction:column;justify-content:center;
@@ -58,7 +89,8 @@ code,kbd,pre,samp{font-family:var(--font-mono)}
.slide.is-prev{transform:translateX(-30px)} .slide.is-prev{transform:translateX(-30px)}
/* single-page standalone (used when a layout file is opened directly) */ /* single-page standalone (used when a layout file is opened directly) */
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto} body.single .slide{position:relative;width:100%;height:100%;opacity:1;transform:none;pointer-events:auto}
body[data-fit="fluid"].single .slide{width:100vw;height:100vh}
/* ================= TYPOGRAPHY ================= */ /* ================= TYPOGRAPHY ================= */
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)} .eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
@@ -105,6 +137,30 @@ h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
.divider{height:1px;background:var(--border);width:100%} .divider{height:1px;background:var(--border);width:100%}
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px} .divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
/* ================= IMAGES (issue #17) =================
* Framing primitives shared by every image-* layout. The frame owns the
* aspect ratio and the crop; the <img> just fills it. That's what lets you
* swap in a photo of any shape without touching the layout.
* <figure class="img-frame"><img src="…" alt=""></figure>
* .img-frame.contain -> letterbox instead of crop (screenshots, diagrams,
* logos — anything where cropping loses information)
* --img-ratio -> aspect ratio of the frame (default 16/10)
* --img-pos -> object-position, e.g. "top" to protect a headline
*/
.img-frame{position:relative;margin:0;overflow:hidden;
border-radius:var(--radius);box-shadow:var(--shadow);background:var(--surface-2);
aspect-ratio:var(--img-ratio,16/10)}
.img-frame > img{width:100%;height:100%;object-fit:cover;object-position:var(--img-pos,center)}
.img-frame.contain{background:var(--bg-soft);box-shadow:none;border:1px solid var(--border)}
.img-frame.contain > img{object-fit:contain}
.img-frame.fill{height:100%;aspect-ratio:auto}
.img-scrim{position:absolute;inset:0;
background:linear-gradient(180deg,transparent 42%,rgba(8,10,20,.72))}
.img-cap{margin:10px 2px 0;font-size:14px;line-height:1.5;color:var(--text-3)}
.img-tag{position:absolute;top:14px;left:14px;padding:5px 12px;border-radius:999px;
background:rgba(10,12,20,.55);color:#fff;font-size:12px;letter-spacing:.06em;
text-transform:uppercase;backdrop-filter:blur(6px)}
/* ================= CHROME (header/footer/progress) ================= */ /* ================= CHROME (header/footer/progress) ================= */
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between; .deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none} font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
@@ -115,8 +171,27 @@ h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20} .progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)} .progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
/* ---- custom logo (issue #11) ----
* One logo per deck, drawn over the slide area. Two ways in, same CSS:
* 1. hand-author it -> <img class="deck-logo" data-pos="bottom-right" src="…">
* 2. declare it once -> <body data-logo="…" data-logo-position="bottom-right">
* (1) needs no JS at all; (2) is created by runtime.js. It lives inside .deck
* so it scales with the deck. On an exported PDF it is painted per page by the
* @media print block below, not by this element — see the note there. */
.deck-logo{position:absolute;z-index:11;pointer-events:none;
height:var(--logo-size,44px);width:auto;max-width:30%;object-fit:contain;
opacity:var(--logo-opacity,.9)}
.deck-logo[data-pos="top-left"] {top:var(--logo-inset-y,28px);left:var(--logo-inset-x,40px)}
.deck-logo[data-pos="top-right"] {top:var(--logo-inset-y,28px);right:var(--logo-inset-x,40px)}
.deck-logo[data-pos="bottom-left"] {bottom:var(--logo-inset-y,28px);left:var(--logo-inset-x,40px)}
.deck-logo[data-pos="bottom-right"] {bottom:var(--logo-inset-y,28px);right:var(--logo-inset-x,40px)}
.deck-logo:not([data-pos]) {top:28px;right:40px}
/* ================= PRESENTER / OVERVIEW ================= */ /* ================= PRESENTER / OVERVIEW ================= */
.notes{display:none!important} /* Keep this list in sync with NOTE_SEL in assets/runtime.js — the runtime
* reads speaker notes from all of these, so all of them must be hidden
* from the audience. */
.notes,.speaker-notes{display:none!important}
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4; .notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%); padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)} transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
@@ -145,6 +220,38 @@ h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
/* ================= PRINT ================= */ /* ================= PRINT ================= */
@media print{ @media print{
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh} /* Every declaration here is !important on purpose. A template stylesheet
* scopes its rules as `.tpl-name .slide`, which is specificity (0,2,0) and
* beats a bare `.slide` at (0,1,0) — so without !important a template
* silently keeps its screen layout in print. That is how dir-key-nav-minimal
* (position:absolute) printed 8 slides onto 1 page and xhs-post printed 9
* onto 2: only the opacity/transform declarations on this rule carried
* !important, and the ones that decide pagination did not. */
body{display:block!important;width:auto!important;height:auto!important;overflow:visible!important}
.deck{transform:none!important;width:100%!important;height:auto!important;
min-height:0!important;display:block!important}
.slide{position:relative!important;inset:auto!important;
opacity:1!important;transform:none!important;
page-break-after:always;break-after:page;
width:100%!important;height:100vh!important;max-height:100vh!important}
.slide:last-child{page-break-after:auto;break-after:auto}
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important} .deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
/* Logo in print. The .deck-logo element is position:absolute inside .deck,
* and print collapses .deck to height:auto with every slide stacked — so the
* element can only anchor ONCE against the whole document and lands on a
* single page (or none). Hide it and paint the logo per slide instead: in
* print one .slide is exactly one page. runtime.js mirrors the image URL and
* placement onto .deck (--logo-print / data-logo-print), so this only kicks
* in when the runtime is present; a JS-less deck keeps the element. */
.deck[data-logo-print] > .deck-logo{display:none!important}
.deck[data-logo-print] .slide:not([data-no-logo])::after{
content:"";position:absolute;z-index:11;pointer-events:none;
width:30%;height:var(--logo-size,44px);
background-image:var(--logo-print);background-repeat:no-repeat;
background-size:contain;opacity:var(--logo-opacity,.9)}
.deck[data-logo-print="top-left"] .slide:not([data-no-logo])::after{top:var(--logo-inset-y,28px);left:var(--logo-inset-x,40px);background-position:left top}
.deck[data-logo-print="top-right"] .slide:not([data-no-logo])::after{top:var(--logo-inset-y,28px);right:var(--logo-inset-x,40px);background-position:right top}
.deck[data-logo-print="bottom-left"] .slide:not([data-no-logo])::after{bottom:var(--logo-inset-y,28px);left:var(--logo-inset-x,40px);background-position:left bottom}
.deck[data-logo-print="bottom-right"] .slide:not([data-no-logo])::after{bottom:var(--logo-inset-y,28px);right:var(--logo-inset-x,40px);background-position:right bottom}
} }
@@ -0,0 +1,12 @@
# demo-images
Placeholder artwork for the `image-*` layouts in `templates/single-page/`.
They are hand-written SVG (1–2 KB each, no external references), so the
layout files render correctly **offline** and stay copy-paste-able. When you
build a real deck, drop your own `.jpg` / `.png` / `.svg` next to the deck's
`index.html` and swap the `src` — nothing else changes, because the framing is
done by `.img-frame` in `assets/base.css`, not by the image itself.
Aspect ratios are deliberately mixed (16:10, 3:4, 1:1, 21:9) so the layouts
demonstrate that `object-fit: cover` handles whatever you throw at it.
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1000" viewBox="0 0 1600 1000">
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#6a8dff"/><stop offset="0.55" stop-color="#a97bff"/><stop offset="1" stop-color="#ff8fa8"/>
</linearGradient></defs>
<rect width="1600" height="1000" fill="url(#g)"/>
<circle cx="1240" cy="230" r="120" fill="#fff" opacity=".55"/><path d="M0 720 Q 300 560 620 690 T 1600 640 L1600 1000 L0 1000Z" fill="#1b2140" opacity=".72"/><path d="M0 830 Q 420 700 820 800 T 1600 760 L1600 1000 L0 1000Z" fill="#0d1128" opacity=".85"/></svg>

After

Width:  |  Height:  |  Size: 607 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="1200" viewBox="0 0 900 1200">
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#12d6a0"/><stop offset="0.6" stop-color="#3b6cff"/><stop offset="1" stop-color="#0d1128"/>
</linearGradient></defs>
<rect width="900" height="1200" fill="url(#g)"/>
<circle cx="640" cy="300" r="150" fill="#fff" opacity=".38"/><rect x="0" y="820" width="900" height="380" fill="#0a0f22" opacity=".6"/><path d="M0 880 L300 640 L520 880 L700 720 L900 900 L900 1200 L0 1200Z" fill="#060a18" opacity=".85"/></svg>

After

Width:  |  Height:  |  Size: 586 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="1000" viewBox="0 0 1000 1000">
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#ffd27a"/><stop offset="0.5" stop-color="#ff7a90"/><stop offset="1" stop-color="#c084fc"/>
</linearGradient></defs>
<rect width="1000" height="1000" fill="url(#g)"/>
<circle cx="500" cy="500" r="330" fill="none" stroke="#fff" stroke-width="26" opacity=".45"/><circle cx="500" cy="500" r="210" fill="none" stroke="#fff" stroke-width="18" opacity=".6"/><circle cx="500" cy="500" r="95" fill="#fff" opacity=".8"/></svg>

After

Width:  |  Height:  |  Size: 596 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="2000" height="860" viewBox="0 0 2000 860">
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#0b1024"/><stop offset="0.55" stop-color="#2b3566"/><stop offset="1" stop-color="#6a8dff"/>
</linearGradient></defs>
<rect width="2000" height="860" fill="url(#g)"/>
<circle cx="1560" cy="250" r="90" fill="#ffe9b0" opacity=".9"/><path d="M0 600 L420 420 L760 600 L1120 400 L1500 620 L2000 470 L2000 860 L0 860Z" fill="#070b1a" opacity=".9"/></svg>

After

Width:  |  Height:  |  Size: 525 B

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1000" viewBox="0 0 1600 1000">
<rect width="1600" height="1000" fill="#0c1024"/>
<rect x="90" y="90" width="1420" height="820" rx="24" fill="#151a33"/>
<circle cx="140" cy="140" r="12" fill="#ff5f57"/><circle cx="180" cy="140" r="12" fill="#febc2e"/><circle cx="220" cy="140" r="12" fill="#28c840"/>
<rect x="150" y="190" width="420" height="34" rx="8" fill="#5ef2c6" opacity=".9"/>
<rect x="150" y="250" width="700" height="26" rx="13" fill="#5ef2c6" opacity="0.42"/><rect x="150" y="308" width="560" height="26" rx="13" fill="#5ef2c6" opacity="0.32"/><rect x="150" y="366" width="640" height="26" rx="13" fill="#5ef2c6" opacity="0.36"/><rect x="150" y="424" width="420" height="26" rx="13" fill="#5ef2c6" opacity="0.28"/><rect x="150" y="482" width="600" height="26" rx="13" fill="#5ef2c6" opacity="0.34"/>
<rect x="1010" y="250" width="440" height="560" rx="18" fill="#5ef2c6" opacity=".16"/>
<path d="M1050 700 L1170 560 L1270 640 L1410 460 L1410 770 L1050 770Z" fill="#5ef2c6" opacity=".55"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1000" viewBox="0 0 1600 1000">
<rect width="1600" height="1000" fill="#eceff6"/>
<rect x="90" y="90" width="1420" height="820" rx="24" fill="#ffffff"/>
<circle cx="140" cy="140" r="12" fill="#ff5f57"/><circle cx="180" cy="140" r="12" fill="#febc2e"/><circle cx="220" cy="140" r="12" fill="#28c840"/>
<rect x="150" y="190" width="300" height="34" rx="8" fill="#8a8f9e" opacity=".9"/>
<rect x="150" y="250" width="700" height="26" rx="13" fill="#8a8f9e" opacity="0.35"/><rect x="150" y="308" width="300" height="26" rx="13" fill="#8a8f9e" opacity="0.25"/><rect x="150" y="366" width="700" height="26" rx="13" fill="#8a8f9e" opacity="0.35"/><rect x="150" y="424" width="260" height="26" rx="13" fill="#8a8f9e" opacity="0.22"/><rect x="150" y="482" width="690" height="26" rx="13" fill="#8a8f9e" opacity="0.33"/>
<rect x="1010" y="250" width="440" height="560" rx="18" fill="#8a8f9e" opacity=".16"/>
<path d="M1050 700 L1170 560 L1270 640 L1410 460 L1410 770 L1050 770Z" fill="#8a8f9e" opacity=".55"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1000" viewBox="0 0 1600 1000">
<rect width="1600" height="1000" fill="#e9ecf5"/>
<rect x="90" y="90" width="1420" height="820" rx="24" fill="#ffffff"/>
<circle cx="140" cy="140" r="12" fill="#ff5f57"/><circle cx="180" cy="140" r="12" fill="#febc2e"/><circle cx="220" cy="140" r="12" fill="#28c840"/>
<rect x="150" y="190" width="380" height="34" rx="8" fill="#3b6cff" opacity=".9"/>
<rect x="150" y="250" width="700" height="26" rx="13" fill="#3b6cff" opacity="0.3"/><rect x="150" y="308" width="560" height="26" rx="13" fill="#3b6cff" opacity="0.22"/><rect x="150" y="366" width="640" height="26" rx="13" fill="#3b6cff" opacity="0.26"/><rect x="150" y="424" width="420" height="26" rx="13" fill="#3b6cff" opacity="0.18"/><rect x="150" y="482" width="600" height="26" rx="13" fill="#3b6cff" opacity="0.24"/>
<rect x="1010" y="250" width="440" height="560" rx="18" fill="#3b6cff" opacity=".16"/>
<path d="M1050 700 L1170 560 L1270 640 L1410 460 L1410 770 L1050 770Z" fill="#3b6cff" opacity=".55"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -23,6 +23,10 @@
'card-flip-3d','cube-rotate-3d','page-turn-3d','perspective-zoom', 'card-flip-3d','cube-rotate-3d','page-turn-3d','perspective-zoom',
'marquee-scroll','kenburns','confetti-burst','spotlight','morph-shape','ripple-reveal']; 'marquee-scroll','kenburns','confetti-burst','spotlight','morph-shape','ripple-reveal'];
/* Every class the runtime treats as speaker notes. base.css hides the same
* set from the audience — if you add one here, add it there too. */
const NOTE_SEL = '.notes, aside.notes, .speaker-notes';
function ready(fn){ if(document.readyState!='loading')fn(); else document.addEventListener('DOMContentLoaded',fn);} function ready(fn){ if(document.readyState!='loading')fn(); else document.addEventListener('DOMContentLoaded',fn);}
/* ========== Parse URL for preview-only mode ========== /* ========== Parse URL for preview-only mode ==========
@@ -37,12 +41,97 @@
return m ? parseInt(m[1], 10) - 1 : -1; return m ? parseInt(m[1], 10) - 1 : -1;
} }
/* ========== Design-canvas fit (issue #20) ==========
* Slides are authored against a fixed canvas (1920x1080 by default). The
* canvas is scaled — never reflowed — to fit whatever viewport it lands in,
* so the browser view, the presenter preview, the overview thumbnail and a
* headless PNG render are all the same picture.
*
* Opt out with <body data-fit="fluid">. Override the canvas per deck with
* <div class="deck" data-w="1080" data-h="1440"> (e.g. a 3:4 小红书 post).
*/
function initCanvasFit(deck) {
if (document.body.getAttribute('data-fit') === 'fluid') return;
const w = parseInt(deck.getAttribute('data-w'), 10) || 1920;
const h = parseInt(deck.getAttribute('data-h'), 10) || 1080;
deck.style.setProperty('--deck-w', w + 'px');
deck.style.setProperty('--deck-h', h + 'px');
function fit() {
const scale = Math.min(window.innerWidth / w, window.innerHeight / h);
deck.style.setProperty('--deck-scale', String(scale));
}
fit();
window.addEventListener('resize', fit);
if (window.visualViewport) window.visualViewport.addEventListener('resize', fit);
/* Webfonts can land after first paint; re-fit once they do. */
if (document.fonts && document.fonts.ready) document.fonts.ready.then(fit).catch(function(){});
}
ready(function () { ready(function () {
const deck = document.querySelector('.deck'); const deck = document.querySelector('.deck');
if (!deck) return; if (!deck) return;
const slides = Array.from(deck.querySelectorAll('.slide')); const slides = Array.from(deck.querySelectorAll('.slide'));
if (!slides.length) return; if (!slides.length) return;
initCanvasFit(deck);
/* ===== custom logo (issue #11) =====
* Deliberately initialised BEFORE the preview-mode branch below: the
* presenter's "pixel-perfect" preview is the audience view, so it has to
* carry the logo too. A hand-authored <img class="deck-logo"> is left
* exactly where it is — base.css styles both paths identically, so a deck
* can have a logo with runtime.js absent entirely.
*/
const logoEl = (function initLogo(){
const attr = (n) => document.body.getAttribute(n) || document.documentElement.getAttribute(n);
let el = deck.querySelector(':scope > .deck-logo');
if (!el) {
const src = attr('data-logo');
if (!src) return null;
el = document.createElement('img');
el.className = 'deck-logo';
el.src = src;
el.alt = attr('data-logo-alt') || '';
deck.appendChild(el);
}
if (!el.hasAttribute('data-pos')) {
el.setAttribute('data-pos', attr('data-logo-position') || 'top-right');
}
/* Custom props go on .deck, not on the element: the element inherits
them, and the print rules (which paint the logo per page on
.slide::after) can read them too. An inline --logo-* on a
hand-authored element wins for the element, so mirror it up. */
const mirror = (attrName, prop) => {
const v = (attrName && attr(attrName)) || el.style.getPropertyValue(prop);
if (v) deck.style.setProperty(prop, v.trim());
};
mirror('data-logo-size', '--logo-size');
mirror('data-logo-opacity', '--logo-opacity');
mirror(null, '--logo-inset-x');
mirror(null, '--logo-inset-y');
/* Print can't use the element itself — see the @media print note in
base.css. Hand the URL and the corner to the per-page painter.
Use el.src, not getAttribute('src'): a relative url() inside a custom
property is resolved against the stylesheet that *uses* the var()
(assets/base.css), not against the deck, so it must be absolute. */
const src = el.src;
if (src) {
deck.style.setProperty('--logo-print', 'url("' + src.replace(/["\\]/g, '\\$&') + '")');
deck.setAttribute('data-logo-print', el.getAttribute('data-pos'));
}
return el;
})();
/* Per-slide opt-out: <section class="slide" data-no-logo> — covers and
full-bleed image slides usually carry their own branding. */
function syncLogo(slide){
if (!logoEl) return;
logoEl.style.display = (slide && slide.hasAttribute('data-no-logo')) ? 'none' : '';
}
const previewOnlyIdx = getPreviewIdx(); const previewOnlyIdx = getPreviewIdx();
const isPreviewMode = previewOnlyIdx >= 0 && previewOnlyIdx < slides.length; const isPreviewMode = previewOnlyIdx >= 0 && previewOnlyIdx < slides.length;
@@ -61,8 +150,9 @@
}); });
} }
showSlide(previewOnlyIdx); showSlide(previewOnlyIdx);
syncLogo(slides[previewOnlyIdx]);
/* Hide chrome that the presenter shouldn't see in preview */ /* Hide chrome that the presenter shouldn't see in preview */
const hideSel = '.progress-bar, .notes-overlay, .overview, .notes, aside.notes, .speaker-notes'; const hideSel = '.progress-bar, .notes-overlay, .overview, ' + NOTE_SEL;
document.querySelectorAll(hideSel).forEach(el => { el.style.display = 'none'; }); document.querySelectorAll(hideSel).forEach(el => { el.style.display = 'none'; });
document.documentElement.setAttribute('data-preview', '1'); document.documentElement.setAttribute('data-preview', '1');
document.body.setAttribute('data-preview', '1'); document.body.setAttribute('data-preview', '1');
@@ -87,7 +177,7 @@
if (!e.data) return; if (!e.data) return;
if (e.data.type === 'preview-goto') { if (e.data.type === 'preview-goto') {
const n = parseInt(e.data.idx, 10); const n = parseInt(e.data.idx, 10);
if (n >= 0 && n < slides.length) showSlide(n); if (n >= 0 && n < slides.length) { showSlide(n); syncLogo(slides[n]); }
} else if (e.data.type === 'preview-theme' && e.data.name) { } else if (e.data.type === 'preview-theme' && e.data.name) {
let link = document.getElementById('theme-link'); let link = document.getElementById('theme-link');
if (!link) { if (!link) {
@@ -225,12 +315,13 @@
s.classList.toggle('is-prev', i<n); s.classList.toggle('is-prev', i<n);
}); });
idx = n; idx = n;
syncLogo(slides[n]);
barFill.style.width = ((n+1)/total*100)+'%'; barFill.style.width = ((n+1)/total*100)+'%';
const numEl = document.querySelector('.slide-number'); const numEl = document.querySelector('.slide-number');
if (numEl) { numEl.setAttribute('data-current', n+1); numEl.setAttribute('data-total', total); } if (numEl) { numEl.setAttribute('data-current', n+1); numEl.setAttribute('data-total', total); }
// notes (bottom overlay) // notes (bottom overlay)
const note = slides[n].querySelector('.notes, aside.notes, .speaker-notes'); const note = slides[n].querySelector(NOTE_SEL);
notes.innerHTML = note ? note.innerHTML : ''; notes.innerHTML = note ? note.innerHTML : '';
// hash // hash
@@ -322,7 +413,7 @@
// Collect slide titles + notes (HTML strings) // Collect slide titles + notes (HTML strings)
const slideMeta = slides.map((s, i) => { const slideMeta = slides.map((s, i) => {
const note = s.querySelector('.notes, aside.notes, .speaker-notes'); const note = s.querySelector(NOTE_SEL);
return { return {
title: s.getAttribute('data-title') || title: s.getAttribute('data-title') ||
(s.querySelector('h1,h2,h3')||{}).textContent || ('Slide '+(i+1)), (s.querySelector('h1,h2,h3')||{}).textContent || ('Slide '+(i+1)),
@@ -345,10 +436,14 @@
} }
function buildPresenterHTML(deckUrl, slideMeta, total, startIdx, channelName, currentTheme) { function buildPresenterHTML(deckUrl, slideMeta, total, startIdx, channelName, currentTheme) {
const metaJSON = JSON.stringify(slideMeta); /* Notes are authored HTML. Escaping "<" keeps a literal </script> in a
const deckUrlJSON = JSON.stringify(deckUrl); slide's notes from closing the inline script that carries this JSON —
const channelJSON = JSON.stringify(channelName); which would kill the whole presenter init. */
const themeJSON = JSON.stringify(currentTheme || ''); const embed = (v) => JSON.stringify(v).replace(/</g, '\\u003c');
const metaJSON = embed(slideMeta);
const deckUrlJSON = embed(deckUrl);
const channelJSON = embed(channelName);
const themeJSON = embed(currentTheme || '');
const storageKey = 'html-ppt-presenter:' + location.pathname; const storageKey = 'html-ppt-presenter:' + location.pathname;
// Build the document as a single template string for clarity // Build the document as a single template string for clarity
@@ -383,6 +478,22 @@
min-width: 180px; min-height: 100px; min-width: 180px; min-height: 100px;
transition: box-shadow .2s, border-color .2s; transition: box-shadow .2s, border-color .2s;
} }
/* Default geometry. The cards used to get position and size ONLY from
applyLayout(), which runs at the very end of the init — so anything that
stopped that script (a blocked inline script under CSP, a stale layout in
localStorage, any throw in the wiring above it) left four cards collapsed
on top of each other and just the hint bar visible (#14). These rules make
the presenter usable with zero JS; applyLayout() overrides them with px. */
#card-cur { left: 16px; top: 16px;
width: calc(55% - 24px); height: calc((100% - 36px) * 0.62 - 16px); }
#card-nxt { left: calc(55% + 8px); top: 16px;
width: calc(45% - 24px); height: calc((100% - 36px) * 0.42 - 16px); }
#card-notes { left: calc(55% + 8px); top: calc((100% - 36px) * 0.42 + 8px);
width: calc(45% - 24px); height: calc((100% - 36px) * 0.58 - 16px); }
#card-timer { left: 16px; top: calc((100% - 36px) * 0.62 + 8px);
width: calc(55% - 24px); height: calc((100% - 36px) * 0.38 - 16px); }
.pcard.dragging { box-shadow: 0 16px 48px rgba(0,0,0,.6), 0 0 0 2px rgba(88,166,255,.5); border-color: #58a6ff; transition: none; z-index: 9999; } .pcard.dragging { box-shadow: 0 16px 48px rgba(0,0,0,.6), 0 0 0 2px rgba(88,166,255,.5); border-color: #58a6ff; transition: none; z-index: 9999; }
.pcard.resizing { box-shadow: 0 16px 48px rgba(0,0,0,.6), 0 0 0 2px rgba(63,185,80,.5); border-color: #3fb950; transition: none; z-index: 9999; } .pcard.resizing { box-shadow: 0 16px 48px rgba(0,0,0,.6), 0 0 0 2px rgba(63,185,80,.5); border-color: #3fb950; transition: none; z-index: 9999; }
.pcard:hover { border-color: rgba(88,166,255,.3); } .pcard:hover { border-color: rgba(88,166,255,.3); }
@@ -580,7 +691,7 @@
var total = ${total}; var total = ${total};
var idx = ${startIdx}; var idx = ${startIdx};
var deckUrl = ${deckUrlJSON}; var deckUrl = ${deckUrlJSON};
var STORAGE_KEY = ${JSON.stringify(storageKey)}; var STORAGE_KEY = ${embed(storageKey)};
var bc; var bc;
try { bc = new BroadcastChannel(${channelJSON}); } catch(e) {} try { bc = new BroadcastChannel(${channelJSON}); } catch(e) {}
@@ -592,10 +703,21 @@
var timerDisplay = document.getElementById('timer-display'); var timerDisplay = document.getElementById('timer-display');
var timerCount = document.getElementById('timer-count'); var timerCount = document.getElementById('timer-count');
/* Lay the cards out before anything else. This used to be the LAST statement
of the init, so every line below it was a single point of failure for the
whole presenter's visibility (#14). Function declarations hoist, so this is
safe here. */
applyLayout(readLayout());
/* ===== Default card layout ===== */ /* ===== Default card layout ===== */
var CARD_IDS = ['card-cur','card-nxt','card-notes','card-timer'];
var MIN_W = 180, MIN_H = 100;
function defaultLayout() { function defaultLayout() {
var w = window.innerWidth; /* A popup that opened minimised or in a background tab can report 0 here;
var h = window.innerHeight - 36; /* leave room for hint bar */ the old code turned that into negative widths, which CSS discards. */
var w = Math.max(640, window.innerWidth || 0);
var h = Math.max(400, (window.innerHeight || 0) - 36); /* room for hint bar */
return { return {
'card-cur': { x: 16, y: 16, w: Math.round(w*0.55) - 24, h: Math.round(h*0.62) - 16 }, 'card-cur': { x: 16, y: 16, w: Math.round(w*0.55) - 24, h: Math.round(h*0.62) - 16 },
'card-nxt': { x: Math.round(w*0.55) + 8, y: 16, w: w - Math.round(w*0.55) - 24, h: Math.round(h*0.42) - 16 }, 'card-nxt': { x: Math.round(w*0.55) + 8, y: 16, w: w - Math.round(w*0.55) - 24, h: Math.round(h*0.42) - 16 },
@@ -618,16 +740,35 @@
}); });
rescaleAll(); rescaleAll();
} }
/* A layout restored from localStorage was written against whatever window
size the deck was last presented at. Replayed in a smaller window it puts
every card off-screen, and there is no way back because the layout is
sticky — so clamp it into view and reject anything malformed. */
function sanitizeLayout(layout) {
if (!layout || typeof layout !== 'object') return null;
var vw = Math.max(MIN_W, window.innerWidth || 0);
var vh = Math.max(MIN_H, window.innerHeight || 0);
var out = {};
for (var i = 0; i < CARD_IDS.length; i++) {
var l = layout[CARD_IDS[i]];
if (!l) return null;
var cw = Math.min(Math.max(+l.w || 0, MIN_W), vw);
var ch = Math.min(Math.max(+l.h || 0, MIN_H), vh);
var cx = Math.min(Math.max(+l.x || 0, 0), Math.max(0, vw - MIN_W));
var cy = Math.min(Math.max(+l.y || 0, 0), Math.max(0, vh - 40));
if (!isFinite(cw) || !isFinite(ch) || !isFinite(cx) || !isFinite(cy)) return null;
out[CARD_IDS[i]] = { x: cx, y: cy, w: cw, h: ch };
}
return out;
}
function readLayout() { function readLayout() {
try { var saved = null;
var saved = localStorage.getItem(STORAGE_KEY); try { saved = JSON.parse(localStorage.getItem(STORAGE_KEY)); } catch(e) {}
if (saved) return JSON.parse(saved); return sanitizeLayout(saved) || defaultLayout();
} catch(e) {}
return defaultLayout();
} }
function saveLayout() { function saveLayout() {
var layout = {}; var layout = {};
['card-cur','card-nxt','card-notes','card-timer'].forEach(function(id){ CARD_IDS.forEach(function(id){
var el = document.getElementById(id); var el = document.getElementById(id);
if (el) { if (el) {
layout[id] = { layout[id] = {
@@ -857,14 +998,19 @@
* 'preview-ready', all subsequent navigation is via postMessage * 'preview-ready', all subsequent navigation is via postMessage
* (smooth, no reload, no flicker). * (smooth, no reload, no flicker).
*/ */
applyLayout(readLayout()); try {
iframeCur.src = deckUrl + '?preview=' + (idx + 1); iframeCur.src = deckUrl + '?preview=' + (idx + 1);
if (idx + 1 < total) iframeNxt.src = deckUrl + '?preview=' + (idx + 2); if (idx + 1 < total) iframeNxt.src = deckUrl + '?preview=' + (idx + 2);
/* Initialize notes/timer/count without touching iframes */ /* Initialize notes/timer/count without touching iframes */
notesBody.innerHTML = slideMeta[idx].notes || '<span class="empty">(这一页还没有逐字稿)</span>'; var m = slideMeta[idx] || {};
curMeta.textContent = (idx + 1) + '/' + total; notesBody.innerHTML = m.notes || '<span class="empty">(这一页还没有逐字稿)</span>';
nxtMeta.textContent = (idx + 2) + '/' + total; curMeta.textContent = (idx + 1) + '/' + total;
timerCount.textContent = (idx + 1) + ' / ' + total; nxtMeta.textContent = (idx + 2) + '/' + total;
timerCount.textContent = (idx + 1) + ' / ' + total;
} catch (e) {
/* Report it, but never let it blank the cards. */
if (window.console && console.error) console.error('[html-ppt] presenter init:', e);
}
})(); })();
</` + `script> </` + `script>
</body></html>`; </body></html>`;
@@ -948,6 +1094,62 @@
} }
}); });
/* ===== Touch navigation =====
* Phones have no arrow keys (#15). Swipe left for next, right for prev.
*
* Deliberately passive: we never call preventDefault, so pinch-zoom and
* any native scrolling keep working and the browser is free to scroll
* while we are still deciding. A gesture only counts as a swipe if it is
* single-finger, clearly horizontal, long enough and quick enough —
* otherwise it falls through untouched.
*/
(function initTouchNav(){
/* No capability sniffing on purpose. Touch listeners cost nothing on a
device that never fires them, and `ontouchstart in window` /
maxTouchPoints both misreport on touchscreen laptops and some
tablets — a guard here would silently remove the feature on exactly
the devices that need it. */
var SWIPE_MIN_PX = 50; // shorter than this is a tap or a wobble
var SWIPE_RATIO = 1.5; // must be this much more horizontal than vertical
var SWIPE_MAX_MS = 800; // slower than this is a drag, not a swipe
var x0 = 0, y0 = 0, t0 = 0, tracking = false;
function interactive(target) {
if (!target || !target.closest) return false;
/* Don't steal the gesture from the overview grid, the notes drawer,
or anything the author made scrollable or tappable. */
return !!target.closest('.overview, .notes-overlay, a, button, input, textarea, select, [data-no-swipe]');
}
document.addEventListener('touchstart', function(e){
if (e.touches.length !== 1 || interactive(e.target)) { tracking = false; return; }
x0 = e.touches[0].clientX;
y0 = e.touches[0].clientY;
t0 = Date.now();
tracking = true;
}, { passive: true });
document.addEventListener('touchmove', function(e){
/* A second finger means pinch-zoom — abandon the swipe. */
if (e.touches.length > 1) tracking = false;
}, { passive: true });
document.addEventListener('touchend', function(e){
if (!tracking) return;
tracking = false;
var t = e.changedTouches && e.changedTouches[0];
if (!t) return;
if (Date.now() - t0 > SWIPE_MAX_MS) return;
var dx = t.clientX - x0, dy = t.clientY - y0;
if (Math.abs(dx) < SWIPE_MIN_PX) return;
if (Math.abs(dx) < Math.abs(dy) * SWIPE_RATIO) return;
go(dx < 0 ? idx + 1 : idx - 1);
}, { passive: true });
document.addEventListener('touchcancel', function(){ tracking = false; }, { passive: true });
})();
// hash deep-link // hash deep-link
function fromHash(){ function fromHash(){
const m = /^#\/(\d+)/.exec(location.hash||''); const m = /^#\/(\d+)/.exec(location.hash||'');
@@ -4,6 +4,7 @@
--border:rgba(20,20,20,.14);--border-strong:rgba(20,20,20,.35); --border:rgba(20,20,20,.14);--border-strong:rgba(20,20,20,.35);
--text-1:#0a0a0a;--text-2:#333333;--text-3:#707070; --text-1:#0a0a0a;--text-2:#333333;--text-3:#707070;
--accent:#1a3a7a;--accent-2:#0a0a0a;--accent-3:#8a1a1a; --accent:#1a3a7a;--accent-2:#0a0a0a;--accent-3:#8a1a1a;
--accent-ink:#ffffff; /* text on --accent — 10.9:1 */
--good:#1a5a2a;--warn:#8a6a1a;--bad:#8a1a1a; --good:#1a5a2a;--warn:#8a6a1a;--bad:#8a1a1a;
--grad:linear-gradient(135deg,#1a3a7a,#0a0a0a); --grad:linear-gradient(135deg,#1a3a7a,#0a0a0a);
--grad-soft:linear-gradient(135deg,#e8edf8,#f5f3ea); --grad-soft:linear-gradient(135deg,#e8edf8,#f5f3ea);
@@ -4,6 +4,7 @@
--border:rgba(40,70,110,.12);--border-strong:rgba(40,70,110,.24); --border:rgba(40,70,110,.12);--border-strong:rgba(40,70,110,.24);
--text-1:#0e1f33;--text-2:#3a5778;--text-3:#6b819b; --text-1:#0e1f33;--text-2:#3a5778;--text-3:#6b819b;
--accent:#1e6fb0;--accent-2:#17b1b1;--accent-3:#6f8aa6; --accent:#1e6fb0;--accent-2:#17b1b1;--accent-3:#6f8aa6;
--accent-ink:#ffffff; /* text on --accent — 5.3:1 */
--good:#1aaf84;--warn:#d19030;--bad:#c5485a; --good:#1aaf84;--warn:#d19030;--bad:#c5485a;
--grad:linear-gradient(135deg,#1e6fb0,#17b1b1 60%,#5fb9d6); --grad:linear-gradient(135deg,#1e6fb0,#17b1b1 60%,#5fb9d6);
--grad-soft:linear-gradient(135deg,#e7eef7,#dff3f3); --grad-soft:linear-gradient(135deg,#e7eef7,#dff3f3);
@@ -4,6 +4,7 @@
--border:rgba(180,220,255,.14);--border-strong:rgba(180,220,255,.28); --border:rgba(180,220,255,.14);--border-strong:rgba(180,220,255,.28);
--text-1:#e8f0ff;--text-2:#b4c4e4;--text-3:#6a7a9e; --text-1:#e8f0ff;--text-2:#b4c4e4;--text-3:#6a7a9e;
--accent:#5ef2c6;--accent-2:#7aa2ff;--accent-3:#c984ff; --accent:#5ef2c6;--accent-2:#7aa2ff;--accent-3:#c984ff;
--accent-ink:#0b1024; /* text on --accent — 13.5:1 */
--good:#5ef2c6;--warn:#ffd27a;--bad:#ff8ab0; --good:#5ef2c6;--warn:#ffd27a;--bad:#ff8ab0;
--grad:linear-gradient(135deg,#5ef2c6,#7aa2ff 50%,#c984ff); --grad:linear-gradient(135deg,#5ef2c6,#7aa2ff 50%,#c984ff);
--grad-soft:linear-gradient(135deg,rgba(94,242,198,.2),rgba(201,132,255,.2)); --grad-soft:linear-gradient(135deg,rgba(94,242,198,.2),rgba(201,132,255,.2));
@@ -4,6 +4,7 @@
--border:#111111;--border-strong:#111111; --border:#111111;--border-strong:#111111;
--text-1:#111111;--text-2:#333333;--text-3:#666666; --text-1:#111111;--text-2:#333333;--text-3:#666666;
--accent:#e03c27;--accent-2:#f4c430;--accent-3:#1d4eaf; --accent:#e03c27;--accent-2:#f4c430;--accent-3:#1d4eaf;
--accent-ink:#000000; /* text on --accent — 4.8:1 */
--good:#1b8c3c;--warn:#f4c430;--bad:#e03c27; --good:#1b8c3c;--warn:#f4c430;--bad:#e03c27;
--grad:linear-gradient(135deg,#e03c27 0 33%,#f4c430 33% 66%,#1d4eaf 66% 100%); --grad:linear-gradient(135deg,#e03c27 0 33%,#f4c430 33% 66%,#1d4eaf 66% 100%);
--grad-soft:linear-gradient(135deg,#f4efe3,#e8e2d1); --grad-soft:linear-gradient(135deg,#f4efe3,#e8e2d1);
@@ -4,6 +4,7 @@
--border:rgba(190,220,255,.3);--border-strong:rgba(190,220,255,.55); --border:rgba(190,220,255,.3);--border-strong:rgba(190,220,255,.55);
--text-1:#e8f3ff;--text-2:#b8d4f0;--text-3:#7da8cf; --text-1:#e8f3ff;--text-2:#b8d4f0;--text-3:#7da8cf;
--accent:#ffffff;--accent-2:#aee1ff;--accent-3:#ffd27a; --accent:#ffffff;--accent-2:#aee1ff;--accent-3:#ffd27a;
--accent-ink:#0b1024; /* text on --accent — 18.9:1 */
--good:#8ef0a6;--warn:#ffd27a;--bad:#ff8a96; --good:#8ef0a6;--warn:#ffd27a;--bad:#ff8a96;
--grad:linear-gradient(135deg,#ffffff,#aee1ff); --grad:linear-gradient(135deg,#ffffff,#aee1ff);
--grad-soft:linear-gradient(135deg,#0a3260,#0b3a6f); --grad-soft:linear-gradient(135deg,#0a3260,#0b3a6f);
@@ -4,6 +4,7 @@
--border:rgba(76,79,105,.14);--border-strong:rgba(76,79,105,.28); --border:rgba(76,79,105,.14);--border-strong:rgba(76,79,105,.28);
--text-1:#4c4f69;--text-2:#6c6f85;--text-3:#9ca0b0; --text-1:#4c4f69;--text-2:#6c6f85;--text-3:#9ca0b0;
--accent:#8839ef;--accent-2:#1e66f5;--accent-3:#ea76cb; --accent:#8839ef;--accent-2:#1e66f5;--accent-3:#ea76cb;
--accent-ink:#ffffff; /* text on --accent — 5.4:1 */
--good:#40a02b;--warn:#df8e1d;--bad:#d20f39; --good:#40a02b;--warn:#df8e1d;--bad:#d20f39;
--grad:linear-gradient(135deg,#8839ef,#1e66f5 50%,#04a5e5); --grad:linear-gradient(135deg,#8839ef,#1e66f5 50%,#04a5e5);
--grad-soft:linear-gradient(135deg,#eff1f5,#e6e9ef); --grad-soft:linear-gradient(135deg,#eff1f5,#e6e9ef);
@@ -4,6 +4,7 @@
--border:rgba(205,214,244,.12);--border-strong:rgba(205,214,244,.24); --border:rgba(205,214,244,.12);--border-strong:rgba(205,214,244,.24);
--text-1:#cdd6f4;--text-2:#a6adc8;--text-3:#7f849c; --text-1:#cdd6f4;--text-2:#a6adc8;--text-3:#7f849c;
--accent:#cba6f7;--accent-2:#89b4fa;--accent-3:#f5c2e7; --accent:#cba6f7;--accent-2:#89b4fa;--accent-3:#f5c2e7;
--accent-ink:#0b1024; /* text on --accent — 9.3:1 */
--good:#a6e3a1;--warn:#f9e2af;--bad:#f38ba8; --good:#a6e3a1;--warn:#f9e2af;--bad:#f38ba8;
--grad:linear-gradient(135deg,#cba6f7,#89b4fa 50%,#94e2d5); --grad:linear-gradient(135deg,#cba6f7,#89b4fa 50%,#94e2d5);
--grad-soft:linear-gradient(135deg,#313244,#45475a); --grad-soft:linear-gradient(135deg,#313244,#45475a);
@@ -4,6 +4,7 @@
--border:rgba(10,37,64,.12);--border-strong:rgba(10,37,64,.28); --border:rgba(10,37,64,.12);--border-strong:rgba(10,37,64,.28);
--text-1:#0a2540;--text-2:#425466;--text-3:#8898aa; --text-1:#0a2540;--text-2:#425466;--text-3:#8898aa;
--accent:#0a2540;--accent-2:#1d4ed8;--accent-3:#64748b; --accent:#0a2540;--accent-2:#1d4ed8;--accent-3:#64748b;
--accent-ink:#ffffff; /* text on --accent — 15.5:1 */
--good:#0e9f6e;--warn:#d97706;--bad:#dc2626; --good:#0e9f6e;--warn:#d97706;--bad:#dc2626;
--grad:linear-gradient(135deg,#0a2540,#1d4ed8); --grad:linear-gradient(135deg,#0a2540,#1d4ed8);
--grad-soft:linear-gradient(135deg,#f0f4fb,#e4ecf7); --grad-soft:linear-gradient(135deg,#f0f4fb,#e4ecf7);
@@ -4,6 +4,7 @@
--border:rgba(255,0,170,.25);--border-strong:rgba(0,240,255,.55); --border:rgba(255,0,170,.25);--border-strong:rgba(0,240,255,.55);
--text-1:#f5f7ff;--text-2:#b4b8d4;--text-3:#6b6e8a; --text-1:#f5f7ff;--text-2:#b4b8d4;--text-3:#6b6e8a;
--accent:#ff2bd6;--accent-2:#00f0ff;--accent-3:#f9f871; --accent:#ff2bd6;--accent-2:#00f0ff;--accent-3:#f9f871;
--accent-ink:#0b1024; /* text on --accent — 5.9:1 */
--good:#39ff14;--warn:#f9f871;--bad:#ff2bd6; --good:#39ff14;--warn:#f9f871;--bad:#ff2bd6;
--grad:linear-gradient(135deg,#ff2bd6,#7a00ff 50%,#00f0ff); --grad:linear-gradient(135deg,#ff2bd6,#7a00ff 50%,#00f0ff);
--grad-soft:linear-gradient(135deg,rgba(255,43,214,.18),rgba(0,240,255,.18)); --grad-soft:linear-gradient(135deg,rgba(255,43,214,.18),rgba(0,240,255,.18));
@@ -4,6 +4,7 @@
--border:rgba(248,248,242,.12);--border-strong:rgba(248,248,242,.24); --border:rgba(248,248,242,.12);--border-strong:rgba(248,248,242,.24);
--text-1:#f8f8f2;--text-2:#bdbde0;--text-3:#6272a4; --text-1:#f8f8f2;--text-2:#bdbde0;--text-3:#6272a4;
--accent:#bd93f9;--accent-2:#ff79c6;--accent-3:#8be9fd; --accent:#bd93f9;--accent-2:#ff79c6;--accent-3:#8be9fd;
--accent-ink:#0b1024; /* text on --accent — 7.8:1 */
--good:#50fa7b;--warn:#f1fa8c;--bad:#ff5555; --good:#50fa7b;--warn:#f1fa8c;--bad:#ff5555;
--grad:linear-gradient(135deg,#bd93f9,#ff79c6 55%,#8be9fd); --grad:linear-gradient(135deg,#bd93f9,#ff79c6 55%,#8be9fd);
--grad-soft:linear-gradient(135deg,#343746,#44475a); --grad-soft:linear-gradient(135deg,#343746,#44475a);
@@ -4,6 +4,7 @@
--border:rgba(40,28,18,.12);--border-strong:rgba(40,28,18,.24); --border:rgba(40,28,18,.12);--border-strong:rgba(40,28,18,.24);
--text-1:#1b1410;--text-2:#5c4a3e;--text-3:#8a7868; --text-1:#1b1410;--text-2:#5c4a3e;--text-3:#8a7868;
--accent:#8a2a1c;--accent-2:#c97a4a;--accent-3:#1b1410; --accent:#8a2a1c;--accent-2:#c97a4a;--accent-3:#1b1410;
--accent-ink:#ffffff; /* text on --accent — 8.6:1 */
--good:#3f7d4f;--warn:#b07a1f;--bad:#8a2a1c; --good:#3f7d4f;--warn:#b07a1f;--bad:#8a2a1c;
--grad:linear-gradient(135deg,#8a2a1c,#c97a4a); --grad:linear-gradient(135deg,#8a2a1c,#c97a4a);
--grad-soft:linear-gradient(135deg,#faf7f2,#f3efe6); --grad-soft:linear-gradient(135deg,#faf7f2,#f3efe6);
@@ -4,6 +4,7 @@
--border:rgba(10,30,70,.22);--border-strong:#0a1e46; --border:rgba(10,30,70,.22);--border-strong:#0a1e46;
--text-1:#0a1e46;--text-2:#3a4a6a;--text-3:#8090a8; --text-1:#0a1e46;--text-2:#3a4a6a;--text-3:#8090a8;
--accent:#0a1e46;--accent-2:#1e5ac4;--accent-3:#c42a10; --accent:#0a1e46;--accent-2:#1e5ac4;--accent-3:#c42a10;
--accent-ink:#ffffff; /* text on --accent — 16.3:1 */
--good:#1a6a3a;--warn:#c47a10;--bad:#c42a10; --good:#1a6a3a;--warn:#c47a10;--bad:#c42a10;
--grad:linear-gradient(135deg,#0a1e46,#1e5ac4); --grad:linear-gradient(135deg,#0a1e46,#1e5ac4);
--grad-soft:linear-gradient(135deg,#eaf0fb,#f4f7fb); --grad-soft:linear-gradient(135deg,#eaf0fb,#f4f7fb);
@@ -4,6 +4,7 @@
--border:rgba(255,255,255,.14);--border-strong:rgba(255,255,255,.28); --border:rgba(255,255,255,.14);--border-strong:rgba(255,255,255,.28);
--text-1:#f2f4ff;--text-2:#c3c8e6;--text-3:#8287a8; --text-1:#f2f4ff;--text-2:#c3c8e6;--text-3:#8287a8;
--accent:#7dd3fc;--accent-2:#c084fc;--accent-3:#f0abfc; --accent:#7dd3fc;--accent-2:#c084fc;--accent-3:#f0abfc;
--accent-ink:#0b1024; /* text on --accent — 11.3:1 */
--good:#86efac;--warn:#fde68a;--bad:#fca5a5; --good:#86efac;--warn:#fde68a;--bad:#fca5a5;
--grad:linear-gradient(135deg,#7dd3fc,#c084fc 55%,#f0abfc); --grad:linear-gradient(135deg,#7dd3fc,#c084fc 55%,#f0abfc);
--grad-soft:linear-gradient(135deg,rgba(125,211,252,.18),rgba(192,132,252,.18)); --grad-soft:linear-gradient(135deg,rgba(125,211,252,.18),rgba(192,132,252,.18));
@@ -4,6 +4,7 @@
--border:rgba(235,219,178,.14);--border-strong:rgba(235,219,178,.28); --border:rgba(235,219,178,.14);--border-strong:rgba(235,219,178,.28);
--text-1:#ebdbb2;--text-2:#d5c4a1;--text-3:#928374; --text-1:#ebdbb2;--text-2:#d5c4a1;--text-3:#928374;
--accent:#fabd2f;--accent-2:#fe8019;--accent-3:#b8bb26; --accent:#fabd2f;--accent-2:#fe8019;--accent-3:#b8bb26;
--accent-ink:#0b1024; /* text on --accent — 11.1:1 */
--good:#b8bb26;--warn:#fabd2f;--bad:#fb4934; --good:#b8bb26;--warn:#fabd2f;--bad:#fb4934;
--grad:linear-gradient(135deg,#fe8019,#fabd2f 55%,#b8bb26); --grad:linear-gradient(135deg,#fe8019,#fabd2f 55%,#b8bb26);
--grad-soft:linear-gradient(135deg,#3c3836,#504945); --grad-soft:linear-gradient(135deg,#3c3836,#504945);
@@ -4,6 +4,7 @@
--border:rgba(40,30,20,.1);--border-strong:rgba(40,30,20,.3); --border:rgba(40,30,20,.1);--border-strong:rgba(40,30,20,.3);
--text-1:#1a1a18;--text-2:#5c564c;--text-3:#9c958a; --text-1:#1a1a18;--text-2:#5c564c;--text-3:#9c958a;
--accent:#d93a2a;--accent-2:#1a1a18;--accent-3:#c9a961; --accent:#d93a2a;--accent-2:#1a1a18;--accent-3:#c9a961;
--accent-ink:#ffffff; /* text on --accent — 4.6:1 */
--good:#4a6b3e;--warn:#c9a961;--bad:#d93a2a; --good:#4a6b3e;--warn:#c9a961;--bad:#d93a2a;
--grad:linear-gradient(135deg,#d93a2a,#1a1a18); --grad:linear-gradient(135deg,#d93a2a,#1a1a18);
--grad-soft:linear-gradient(135deg,#faeae6,#f5f3ea); --grad-soft:linear-gradient(135deg,#faeae6,#f5f3ea);
@@ -4,6 +4,7 @@
--border:rgba(10,10,10,.16);--border-strong:#0a0a0a; --border:rgba(10,10,10,.16);--border-strong:#0a0a0a;
--text-1:#0a0a0a;--text-2:#2a2a2a;--text-3:#6a6458; --text-1:#0a0a0a;--text-2:#2a2a2a;--text-3:#6a6458;
--accent:#ea5a1a;--accent-2:#0a0a0a;--accent-3:#c42a10; --accent:#ea5a1a;--accent-2:#0a0a0a;--accent-3:#c42a10;
--accent-ink:#0b1024; /* text on --accent — 5.4:1 */
--good:#2a6a2a;--warn:#ea5a1a;--bad:#c42a10; --good:#2a6a2a;--warn:#ea5a1a;--bad:#c42a10;
--grad:linear-gradient(135deg,#ea5a1a,#c42a10); --grad:linear-gradient(135deg,#ea5a1a,#c42a10);
--grad-soft:linear-gradient(135deg,#fbe4d0,#f5d6c0); --grad-soft:linear-gradient(135deg,#fbe4d0,#f5d6c0);
@@ -4,6 +4,7 @@
--border:#111111;--border-strong:#111111; --border:#111111;--border-strong:#111111;
--text-1:#111111;--text-2:#333333;--text-3:#666666; --text-1:#111111;--text-2:#333333;--text-3:#666666;
--accent:#ff3d8b;--accent-2:#37c2d7;--accent-3:#ffcc00; --accent:#ff3d8b;--accent-2:#37c2d7;--accent-3:#ffcc00;
--accent-ink:#0b1024; /* text on --accent — 5.6:1 */
--good:#6ac04c;--warn:#ffcc00;--bad:#ff3d8b; --good:#6ac04c;--warn:#ffcc00;--bad:#ff3d8b;
--grad:linear-gradient(135deg,#ff3d8b,#ffcc00 50%,#37c2d7); --grad:linear-gradient(135deg,#ff3d8b,#ffcc00 50%,#37c2d7);
--grad-soft:linear-gradient(135deg,#fdebc7,#fff1d1); --grad-soft:linear-gradient(135deg,#fdebc7,#fff1d1);
@@ -4,6 +4,7 @@
--border:rgba(60,40,20,.18);--border-strong:rgba(60,40,20,.4); --border:rgba(60,40,20,.18);--border-strong:rgba(60,40,20,.4);
--text-1:#201810;--text-2:#5a4830;--text-3:#9a8868; --text-1:#201810;--text-2:#5a4830;--text-3:#9a8868;
--accent:#d4902a;--accent-2:#2a7a7f;--accent-3:#c7502a; --accent:#d4902a;--accent-2:#2a7a7f;--accent-3:#c7502a;
--accent-ink:#0b1024; /* text on --accent — 7.0:1 */
--good:#5a7a3a;--warn:#d4902a;--bad:#c7502a; --good:#5a7a3a;--warn:#d4902a;--bad:#c7502a;
--grad:linear-gradient(135deg,#d4902a,#c7502a 55%,#2a7a7f); --grad:linear-gradient(135deg,#d4902a,#c7502a 55%,#2a7a7f);
--grad-soft:linear-gradient(135deg,#f4e0b6,#eac7a8); --grad-soft:linear-gradient(135deg,#f4e0b6,#eac7a8);
@@ -4,6 +4,7 @@
--border:rgba(17,18,22,.08);--border-strong:rgba(17,18,22,.16); --border:rgba(17,18,22,.08);--border-strong:rgba(17,18,22,.16);
--text-1:#0c0d10;--text-2:#55596a;--text-3:#9ca1b0; --text-1:#0c0d10;--text-2:#55596a;--text-3:#9ca1b0;
--accent:#111216;--accent-2:#3b3f4a;--accent-3:#6b6f7a; --accent:#111216;--accent-2:#3b3f4a;--accent-3:#6b6f7a;
--accent-ink:#ffffff; /* text on --accent — 18.7:1 */
--good:#1aaf6c;--warn:#c98500;--bad:#c13a3a; --good:#1aaf6c;--warn:#c98500;--bad:#c13a3a;
--grad:linear-gradient(135deg,#111216,#3b3f4a); --grad:linear-gradient(135deg,#111216,#3b3f4a);
--grad-soft:linear-gradient(135deg,#f5f5f6,#ffffff); --grad-soft:linear-gradient(135deg,#f5f5f6,#ffffff);
@@ -4,6 +4,7 @@
--border:#000000;--border-strong:#000000; --border:#000000;--border-strong:#000000;
--text-1:#000000;--text-2:#222222;--text-3:#555555; --text-1:#000000;--text-2:#222222;--text-3:#555555;
--accent:#ffd400;--accent-2:#ff5ca8;--accent-3:#3a7cff; --accent:#ffd400;--accent-2:#ff5ca8;--accent-3:#3a7cff;
--accent-ink:#0b1024; /* text on --accent — 13.2:1 */
--good:#00b36b;--warn:#ff9900;--bad:#ff3a30; --good:#00b36b;--warn:#ff9900;--bad:#ff3a30;
--grad:linear-gradient(135deg,#ffd400,#ff5ca8); --grad:linear-gradient(135deg,#ffd400,#ff5ca8);
--grad-soft:linear-gradient(135deg,#fffbd0,#fff); --grad-soft:linear-gradient(135deg,#fffbd0,#fff);
@@ -4,6 +4,7 @@
--border:rgba(0,0,0,.14);--border-strong:#0a0a0a; --border:rgba(0,0,0,.14);--border-strong:#0a0a0a;
--text-1:#0a0a0a;--text-2:#3a3a3a;--text-3:#7a7a7a; --text-1:#0a0a0a;--text-2:#3a3a3a;--text-3:#7a7a7a;
--accent:#e11d2d;--accent-2:#0a0a0a;--accent-3:#ffd100; --accent:#e11d2d;--accent-2:#0a0a0a;--accent-3:#ffd100;
--accent-ink:#ffffff; /* text on --accent — 4.8:1 */
--good:#0e7c3a;--warn:#ffd100;--bad:#e11d2d; --good:#0e7c3a;--warn:#ffd100;--bad:#e11d2d;
--grad:linear-gradient(90deg,#e11d2d 0%,#e11d2d 100%); --grad:linear-gradient(90deg,#e11d2d 0%,#e11d2d 100%);
--grad-soft:linear-gradient(135deg,#fde5e7,#f4f4f4); --grad-soft:linear-gradient(135deg,#fde5e7,#f4f4f4);
@@ -16,5 +17,5 @@
h1.title,h2.title,.h1,.h2{font-weight:700;text-transform:uppercase;letter-spacing:-.01em} h1.title,h2.title,.h1,.h2{font-weight:700;text-transform:uppercase;letter-spacing:-.01em}
.card{border:2px solid var(--text-1);box-shadow:6px 6px 0 var(--accent)} .card{border:2px solid var(--text-1);box-shadow:6px 6px 0 var(--accent)}
.divider-accent{background:var(--accent);height:6px;width:100%} .divider-accent{background:var(--accent);height:6px;width:100%}
.kicker{background:var(--accent);color:#fff;padding:4px 12px;display:inline-block;letter-spacing:.15em} .kicker{background:var(--accent);color:var(--accent-ink);padding:4px 12px;display:inline-block;letter-spacing:.15em}
.slide::before{content:"";position:absolute;left:0;top:0;bottom:0;width:8px;background:var(--accent);z-index:3} .slide::before{content:"";position:absolute;left:0;top:0;bottom:0;width:8px;background:var(--accent);z-index:3}
@@ -4,6 +4,7 @@
--border:rgba(236,239,244,.12);--border-strong:rgba(236,239,244,.24); --border:rgba(236,239,244,.12);--border-strong:rgba(236,239,244,.24);
--text-1:#eceff4;--text-2:#d8dee9;--text-3:#7b8394; --text-1:#eceff4;--text-2:#d8dee9;--text-3:#7b8394;
--accent:#88c0d0;--accent-2:#81a1c1;--accent-3:#b48ead; --accent:#88c0d0;--accent-2:#81a1c1;--accent-3:#b48ead;
--accent-ink:#0b1024; /* text on --accent — 9.4:1 */
--good:#a3be8c;--warn:#ebcb8b;--bad:#bf616a; --good:#a3be8c;--warn:#ebcb8b;--bad:#bf616a;
--grad:linear-gradient(135deg,#88c0d0,#81a1c1 50%,#b48ead); --grad:linear-gradient(135deg,#88c0d0,#81a1c1 50%,#b48ead);
--grad-soft:linear-gradient(135deg,#3b4252,#434c5e); --grad-soft:linear-gradient(135deg,#3b4252,#434c5e);
@@ -4,6 +4,7 @@
--border:rgba(20,30,50,.1);--border-strong:rgba(20,30,50,.22); --border:rgba(20,30,50,.1);--border-strong:rgba(20,30,50,.22);
--text-1:#0b0d12;--text-2:#4a5270;--text-3:#8b93a8; --text-1:#0b0d12;--text-2:#4a5270;--text-3:#8b93a8;
--accent:#0070f3;--accent-2:#7928ca;--accent-3:#ff4ecb; --accent:#0070f3;--accent-2:#7928ca;--accent-3:#ff4ecb;
--accent-ink:#ffffff; /* text on --accent — 4.6:1 */
--good:#0cce6b;--warn:#f5a524;--bad:#ee0000; --good:#0cce6b;--warn:#f5a524;--bad:#ee0000;
--grad:linear-gradient(135deg,#0070f3,#7928ca); --grad:linear-gradient(135deg,#0070f3,#7928ca);
--grad-soft:linear-gradient(135deg,#e8f0ff,#f3e8ff); --grad-soft:linear-gradient(135deg,#e8f0ff,#f3e8ff);
@@ -4,6 +4,7 @@
--border:rgba(20,20,40,.08);--border-strong:rgba(20,20,40,.2); --border:rgba(20,20,40,.08);--border-strong:rgba(20,20,40,.2);
--text-1:#0c0d10;--text-2:#4d5162;--text-3:#9096a8; --text-1:#0c0d10;--text-2:#4d5162;--text-3:#9096a8;
--accent:#ff4d8b;--accent-2:#7a5cff;--accent-3:#36b6ff; --accent:#ff4d8b;--accent-2:#7a5cff;--accent-3:#36b6ff;
--accent-ink:#0b1024; /* text on --accent — 6.0:1 */
--good:#1aaf6c;--warn:#f5a524;--bad:#e0445a; --good:#1aaf6c;--warn:#f5a524;--bad:#e0445a;
--grad:linear-gradient(90deg,#ff0080,#ff4d00,#ff9900,#ffe600,#00c853,#0091ea,#6200ea,#ff0080); --grad:linear-gradient(90deg,#ff0080,#ff4d00,#ff9900,#ffe600,#00c853,#0091ea,#6200ea,#ff0080);
--grad-soft:linear-gradient(135deg,#fff,#f8f8fb); --grad-soft:linear-gradient(135deg,#fff,#f8f8fb);
@@ -4,6 +4,7 @@
--border:rgba(120,70,20,.22);--border-strong:rgba(120,70,20,.45); --border:rgba(120,70,20,.22);--border-strong:rgba(120,70,20,.45);
--text-1:#2a1a08;--text-2:#6b4a22;--text-3:#a68656; --text-1:#2a1a08;--text-2:#6b4a22;--text-3:#a68656;
--accent:#e67e14;--accent-2:#c73a1f;--accent-3:#f2b544; --accent:#e67e14;--accent-2:#c73a1f;--accent-3:#f2b544;
--accent-ink:#0b1024; /* text on --accent — 6.6:1 */
--good:#3e8940;--warn:#e67e14;--bad:#c73a1f; --good:#3e8940;--warn:#e67e14;--bad:#c73a1f;
--grad:linear-gradient(135deg,#c73a1f,#e67e14 55%,#f2b544); --grad:linear-gradient(135deg,#c73a1f,#e67e14 55%,#f2b544);
--grad-soft:linear-gradient(135deg,#fde6c4,#fbd9a0); --grad-soft:linear-gradient(135deg,#fde6c4,#fbd9a0);
@@ -4,6 +4,7 @@
--border:rgba(224,222,244,.12);--border-strong:rgba(224,222,244,.24); --border:rgba(224,222,244,.12);--border-strong:rgba(224,222,244,.24);
--text-1:#e0def4;--text-2:#c4b8d8;--text-3:#6e6a86; --text-1:#e0def4;--text-2:#c4b8d8;--text-3:#6e6a86;
--accent:#ebbcba;--accent-2:#c4a7e7;--accent-3:#9ccfd8; --accent:#ebbcba;--accent-2:#c4a7e7;--accent-3:#9ccfd8;
--accent-ink:#0b1024; /* text on --accent — 11.2:1 */
--good:#31748f;--warn:#f6c177;--bad:#eb6f92; --good:#31748f;--warn:#f6c177;--bad:#eb6f92;
--grad:linear-gradient(135deg,#ebbcba,#c4a7e7 55%,#9ccfd8); --grad:linear-gradient(135deg,#ebbcba,#c4a7e7 55%,#9ccfd8);
--grad-soft:linear-gradient(135deg,#26233a,#2a2740); --grad-soft:linear-gradient(135deg,#26233a,#2a2740);
@@ -4,6 +4,7 @@
--border:#000000;--border-strong:#000000; --border:#000000;--border-strong:#000000;
--text-1:#000000;--text-2:#1a1a1a;--text-3:#4a4a4a; --text-1:#000000;--text-2:#1a1a1a;--text-3:#4a4a4a;
--accent:#000000;--accent-2:#000000;--accent-3:#ff2200; --accent:#000000;--accent-2:#000000;--accent-3:#ff2200;
--accent-ink:#ffffff; /* text on --accent — 21.0:1 */
--good:#008800;--warn:#ff9900;--bad:#ff0000; --good:#008800;--warn:#ff9900;--bad:#ff0000;
--grad:linear-gradient(135deg,#000,#222); --grad:linear-gradient(135deg,#000,#222);
--grad-soft:linear-gradient(135deg,#fff,#eee); --grad-soft:linear-gradient(135deg,#fff,#eee);
@@ -4,6 +4,7 @@
--border:rgba(120,70,110,.12);--border-strong:rgba(120,70,110,.22); --border:rgba(120,70,110,.12);--border-strong:rgba(120,70,110,.22);
--text-1:#3a1f33;--text-2:#6b4d62;--text-3:#a28a99; --text-1:#3a1f33;--text-2:#6b4d62;--text-3:#a28a99;
--accent:#f49bb8;--accent-2:#b5d5f0;--accent-3:#f7d08a; --accent:#f49bb8;--accent-2:#b5d5f0;--accent-3:#f7d08a;
--accent-ink:#0b1024; /* text on --accent — 9.2:1 */
--good:#9dd9a3;--warn:#f7d08a;--bad:#ef9a9a; --good:#9dd9a3;--warn:#f7d08a;--bad:#ef9a9a;
--grad:linear-gradient(135deg,#f49bb8,#b5d5f0 55%,#c4a0e8); --grad:linear-gradient(135deg,#f49bb8,#b5d5f0 55%,#c4a0e8);
--grad-soft:linear-gradient(135deg,#fbeef3,#eaf4fc); --grad-soft:linear-gradient(135deg,#fbeef3,#eaf4fc);
@@ -4,6 +4,7 @@
--border:rgba(88,110,117,.2);--border-strong:rgba(88,110,117,.4); --border:rgba(88,110,117,.2);--border-strong:rgba(88,110,117,.4);
--text-1:#073642;--text-2:#586e75;--text-3:#93a1a1; --text-1:#073642;--text-2:#586e75;--text-3:#93a1a1;
--accent:#268bd2;--accent-2:#2aa198;--accent-3:#d33682; --accent:#268bd2;--accent-2:#2aa198;--accent-3:#d33682;
--accent-ink:#0b1024; /* text on --accent — 5.1:1 */
--good:#859900;--warn:#b58900;--bad:#dc322f; --good:#859900;--warn:#b58900;--bad:#dc322f;
--grad:linear-gradient(135deg,#268bd2,#2aa198 50%,#859900); --grad:linear-gradient(135deg,#268bd2,#2aa198 50%,#859900);
--grad-soft:linear-gradient(135deg,#fdf6e3,#eee8d5); --grad-soft:linear-gradient(135deg,#fdf6e3,#eee8d5);
@@ -4,6 +4,7 @@
--border:rgba(120,60,20,.12);--border-strong:rgba(120,60,20,.22); --border:rgba(120,60,20,.12);--border-strong:rgba(120,60,20,.22);
--text-1:#2a160a;--text-2:#6b4630;--text-3:#a28572; --text-1:#2a160a;--text-2:#6b4630;--text-3:#a28572;
--accent:#e36a2d;--accent-2:#f2a341;--accent-3:#d94860; --accent:#e36a2d;--accent-2:#f2a341;--accent-3:#d94860;
--accent-ink:#0b1024; /* text on --accent — 5.7:1 */
--good:#5ea35a;--warn:#f2a341;--bad:#d94860; --good:#5ea35a;--warn:#f2a341;--bad:#d94860;
--grad:linear-gradient(135deg,#d94860,#e36a2d 50%,#f2a341); --grad:linear-gradient(135deg,#d94860,#e36a2d 50%,#f2a341);
--grad-soft:linear-gradient(135deg,#ffeedc,#ffe0d0); --grad-soft:linear-gradient(135deg,#ffeedc,#ffe0d0);
@@ -4,6 +4,7 @@
--border:#111111;--border-strong:#111111; --border:#111111;--border-strong:#111111;
--text-1:#111111;--text-2:#444444;--text-3:#888888; --text-1:#111111;--text-2:#444444;--text-3:#888888;
--accent:#d6001c;--accent-2:#111111;--accent-3:#888888; --accent:#d6001c;--accent-2:#111111;--accent-3:#888888;
--accent-ink:#ffffff; /* text on --accent — 5.4:1 */
--good:#0f8a2f;--warn:#d38a00;--bad:#d6001c; --good:#0f8a2f;--warn:#d38a00;--bad:#d6001c;
--grad:linear-gradient(135deg,#d6001c,#111); --grad:linear-gradient(135deg,#d6001c,#111);
--grad-soft:linear-gradient(135deg,#f4f4f4,#fff); --grad-soft:linear-gradient(135deg,#f4f4f4,#fff);
@@ -4,6 +4,7 @@
--border:rgba(0,255,120,.22);--border-strong:rgba(0,255,120,.42); --border:rgba(0,255,120,.22);--border-strong:rgba(0,255,120,.42);
--text-1:#8cff9a;--text-2:#4bd17a;--text-3:#2f8a4d; --text-1:#8cff9a;--text-2:#4bd17a;--text-3:#2f8a4d;
--accent:#00ff88;--accent-2:#67ffd0;--accent-3:#b6ff6b; --accent:#00ff88;--accent-2:#67ffd0;--accent-3:#b6ff6b;
--accent-ink:#0b1024; /* text on --accent — 14.1:1 */
--good:#00ff88;--warn:#ffe066;--bad:#ff6464; --good:#00ff88;--warn:#ffe066;--bad:#ff6464;
--grad:linear-gradient(135deg,#00ff88,#67ffd0); --grad:linear-gradient(135deg,#00ff88,#67ffd0);
--grad-soft:linear-gradient(135deg,#0a1b10,#0d2614); --grad-soft:linear-gradient(135deg,#0a1b10,#0d2614);
@@ -4,6 +4,7 @@
--border:rgba(192,202,245,.12);--border-strong:rgba(192,202,245,.24); --border:rgba(192,202,245,.12);--border-strong:rgba(192,202,245,.24);
--text-1:#c0caf5;--text-2:#a9b1d6;--text-3:#565f89; --text-1:#c0caf5;--text-2:#a9b1d6;--text-3:#565f89;
--accent:#7aa2f7;--accent-2:#bb9af7;--accent-3:#7dcfff; --accent:#7aa2f7;--accent-2:#bb9af7;--accent-3:#7dcfff;
--accent-ink:#0b1024; /* text on --accent — 7.5:1 */
--good:#9ece6a;--warn:#e0af68;--bad:#f7768e; --good:#9ece6a;--warn:#e0af68;--bad:#f7768e;
--grad:linear-gradient(135deg,#7aa2f7,#bb9af7 55%,#f7768e); --grad:linear-gradient(135deg,#7aa2f7,#bb9af7 55%,#f7768e);
--grad-soft:linear-gradient(135deg,#24283b,#2f334d); --grad-soft:linear-gradient(135deg,#24283b,#2f334d);
@@ -4,6 +4,7 @@
--border:rgba(255,110,199,.28);--border-strong:rgba(0,245,255,.5); --border:rgba(255,110,199,.28);--border-strong:rgba(0,245,255,.5);
--text-1:#fdf0ff;--text-2:#d4a8e8;--text-3:#8a6ba8; --text-1:#fdf0ff;--text-2:#d4a8e8;--text-3:#8a6ba8;
--accent:#ff6ec7;--accent-2:#00f5ff;--accent-3:#ffd166; --accent:#ff6ec7;--accent-2:#00f5ff;--accent-3:#ffd166;
--accent-ink:#0b1024; /* text on --accent — 7.5:1 */
--grad:linear-gradient(135deg,#ff6ec7 0%,#c94fff 35%,#00f5ff 100%); --grad:linear-gradient(135deg,#ff6ec7 0%,#c94fff 35%,#00f5ff 100%);
--grad-soft:linear-gradient(135deg,rgba(255,110,199,.25),rgba(0,245,255,.25)); --grad-soft:linear-gradient(135deg,rgba(255,110,199,.25),rgba(0,245,255,.25));
--radius:18px;--radius-sm:10px;--radius-lg:28px; --radius:18px;--radius-sm:10px;--radius-lg:28px;
@@ -4,6 +4,7 @@
--border:rgba(60,30,20,.1);--border-strong:rgba(60,30,20,.22); --border:rgba(60,30,20,.1);--border-strong:rgba(60,30,20,.22);
--text-1:#1a1210;--text-2:#4f3a32;--text-3:#a08d85; --text-1:#1a1210;--text-2:#4f3a32;--text-3:#a08d85;
--accent:#ff2742;--accent-2:#ff7a90;--accent-3:#ffb38a; --accent:#ff2742;--accent-2:#ff7a90;--accent-3:#ffb38a;
--accent-ink:#0b1024; /* text on --accent — 5.0:1 */
--good:#3ba55c;--warn:#f5a524;--bad:#ff2742; --good:#3ba55c;--warn:#f5a524;--bad:#ff2742;
--grad:linear-gradient(135deg,#ff2742,#ff7a90 55%,#ffb38a); --grad:linear-gradient(135deg,#ff2742,#ff7a90 55%,#ffb38a);
--grad-soft:linear-gradient(135deg,#fff6f1,#ffeae0); --grad-soft:linear-gradient(135deg,#fff6f1,#ffeae0);
@@ -4,6 +4,7 @@
--border:rgba(120,135,170,.32);--border-strong:rgba(80,100,140,.55); --border:rgba(120,135,170,.32);--border-strong:rgba(80,100,140,.55);
--text-1:#1a1f2e;--text-2:#4a536a;--text-3:#8590a6; --text-1:#1a1f2e;--text-2:#4a536a;--text-3:#8590a6;
--accent:#8a5cff;--accent-2:#3ccfd8;--accent-3:#ff84c4; --accent:#8a5cff;--accent-2:#3ccfd8;--accent-3:#ff84c4;
--accent-ink:#0b1024; /* text on --accent — 4.5:1 */
--grad:linear-gradient(135deg,#b8c4d8 0%,#f5f7fb 30%,#8a9ab8 55%,#e8ecf4 80%,#6b7a95 100%); --grad:linear-gradient(135deg,#b8c4d8 0%,#f5f7fb 30%,#8a9ab8 55%,#e8ecf4 80%,#6b7a95 100%);
--grad-soft:linear-gradient(135deg,#c9e4ff,#f5d6ff 50%,#d6fffa); --grad-soft:linear-gradient(135deg,#c9e4ff,#f5d6ff 50%,#d6fffa);
--radius:26px;--radius-sm:16px;--radius-lg:36px; --radius:26px;--radius-sm:16px;--radius-lg:36px;
@@ -48,12 +48,15 @@ layout twice in a row.
## 4. Scaffold the deck ## 4. Scaffold the deck
```bash ```bash
./scripts/new-deck.sh my-talk ./scripts/new-deck.sh my-talk # base template -> examples/my-talk/
./scripts/new-deck.sh my-talk . -t pitch-deck # full-deck template, into ./my-talk/
./scripts/new-deck.sh my-talk ~/decks # anywhere, inside the skill or not
``` ```
This copies `templates/deck.html` into `examples/my-talk/index.html` with This copies the chosen template into `<parent>/my-talk/index.html` and rewrites
paths rewritten. Add/remove `<section class="slide">` blocks to match your every `assets/` reference to the prefix that is correct for **that** location,
outline. then verifies each one resolves before reporting success. Add/remove
`<section class="slide">` blocks to match your outline.
## 5. Author each slide ## 5. Author each slide
@@ -66,6 +69,27 @@ For each outline item:
5. Set `data-title="..."` (used by the Overview grid). 5. Set `data-title="..."` (used by the Overview grid).
6. Add `<div class="notes">…</div>` with speaker notes. 6. Add `<div class="notes">…</div>` with speaker notes.
### Branding the deck with a logo
Declare it **once** on `<body>`, not per slide:
```html
<body data-logo="logo.svg" data-logo-position="bottom-right" data-logo-size="40px">
```
Position is one of `top-left` / `top-right` / `bottom-left` / `bottom-right`
(default `top-right`); `data-logo-size` sets the height. Drop the logo from an
individual slide with `<section class="slide" data-no-logo>` — the cover usually
already shows the brand at full size.
The image path is resolved relative to the deck's own HTML file, so keep the
file next to `index.html` (`examples/my-talk/logo.svg` → `data-logo="logo.svg"`).
Never hand-edit the `../` depth in asset paths — see the rule in section 10.
If the four presets don't fit, write the element yourself inside `.deck`:
`<img class="deck-logo" data-pos="top-left" src="logo.svg" alt="">`. That works
with `runtime.js` absent entirely, since the styling lives in `base.css`.
## 6. Add animations sparingly ## 6. Add animations sparingly
Rules of thumb: Rules of thumb:
@@ -8,6 +8,26 @@ To compose a new deck: open the file, copy the `<section class="slide">…</sect
block (or multiple blocks) into your deck HTML, and replace the demo data. block (or multiple blocks) into your deck HTML, and replace the demo data.
Shared CSS (base, theme, animations) is already wired by `deck.html`. Shared CSS (base, theme, animations) is already wired by `deck.html`.
## Working with images
The `image-*` layouts use real `<img>` elements. Framing is done by
`.img-frame` in `assets/base.css`, not by the image, so swapping in your own
photo needs nothing but a new `src`:
| | |
|---|---|
| `.img-frame` | the frame. Owns the aspect ratio and the crop; `<img>` inside it fills it with `object-fit: cover`. |
| `.img-frame.contain` | letterbox instead of crop. **Use this for screenshots, diagrams and logos** — cropping those loses information. |
| `--img-ratio` | frame aspect ratio, default `16/10`. |
| `--img-pos` | `object-position`, e.g. `top` to keep a headline in frame. |
| `.img-scrim` | bottom gradient so white text stays readable over a photo. |
| `.img-cap` | caption line under the frame. |
| `.img-tag` | small pill in the frame's top-left corner. |
Demo artwork lives in `assets/demo-images/` — hand-written SVG, ~1 KB each, so
the layouts render offline. Put your own images next to the deck's
`index.html` and reference them relatively (`src="shot.png"`).
## Openers & transitions ## Openers & transitions
| file | purpose | | file | purpose |
@@ -69,8 +89,13 @@ Shared CSS (base, theme, animations) is already wired by `deck.html`.
| file | purpose | | file | purpose |
|---|---| |---|---|
| `image-hero.html` | Full-bleed hero with Ken Burns gradient background. | | `image-single.html` | **One image, uncropped** (`.img-frame.contain`) + caption. Screenshots, diagrams, charts. |
| `image-grid.html` | 7-cell bento grid with gradient placeholders. | | `image-full-bleed.html` | **One image filling the slide**, gradient scrim, title over it. Photos. |
| `image-text-split.html` | **One image + text**, 50/50. Add `flip` to `.split` to put the image on the right. |
| `image-gallery.html` | **3–6 images**, uniform grid, one caption each. Mixed source ratios are normalised by the frame. |
| `image-compare.html` | **Two images side by side**, before/after, with labels and a conclusion line. |
| `image-hero.html` | Full-bleed hero with Ken Burns gradient background (no real image). |
| `image-grid.html` | 7-cell bento grid with gradient placeholders (no real image). |
## Closers ## Closers
@@ -88,6 +113,12 @@ Shared CSS (base, theme, animations) is already wired by `deck.html`.
- **Show plot**: `chart-bar.html` / `chart-line.html` / `chart-pie.html` / `chart-radar.html`. - **Show plot**: `chart-bar.html` / `chart-line.html` / `chart-pie.html` / `chart-radar.html`.
- **Show a diff or change**: `comparison.html`, `diff.html`, `pros-cons.html`. - **Show a diff or change**: `comparison.html`, `diff.html`, `pros-cons.html`.
- **Show a plan**: `timeline.html`, `roadmap.html`, `gantt.html`, `process-steps.html`. - **Show a plan**: `timeline.html`, `roadmap.html`, `gantt.html`, `process-steps.html`.
- **Show one image**: `image-single.html` for a screenshot/diagram (never cropped),
`image-full-bleed.html` for a photo that should carry the whole slide.
- **Show an image plus an argument**: `image-text-split.html`.
- **Show several images**: `image-gallery.html` (uniform, 3–6), or `image-grid.html`
for an uneven bento arrangement.
- **Show a change**: `image-compare.html` (two shots), `comparison.html` (text).
- **Show architecture**: `arch-diagram.html`, `flow-diagram.html`, `mindmap.html`. - **Show architecture**: `arch-diagram.html`, `flow-diagram.html`, `mindmap.html`.
- **Code / demo**: `code.html`, `terminal.html`. - **Code / demo**: `code.html`, `terminal.html`.
- **Closer**: `cta.html` → `thanks.html`. - **Closer**: `cta.html` → `thanks.html`.
@@ -6,10 +6,31 @@ defined in `assets/base.css`. Switch themes by changing the `href` of
`data-themes="a,b,c"` attribute on `<body>` or `<html>`. `data-themes="a,b,c"` attribute on `<body>` or `<html>`.
All themes define the same variables: `--bg`, `--bg-soft`, `--surface`, All themes define the same variables: `--bg`, `--bg-soft`, `--surface`,
`--surface-2`, `--border`, `--text-1/2/3`, `--accent`, `--accent-2/3`, `--surface-2`, `--border`, `--text-1/2/3`, `--accent`, `--accent-ink`, `--accent-2/3`,
`--good`, `--warn`, `--bad`, `--grad`, `--grad-soft`, `--radius*`, `--shadow*`, `--good`, `--warn`, `--bad`, `--grad`, `--grad-soft`, `--radius*`, `--shadow*`,
`--font-sans`, `--font-display`. `--font-sans`, `--font-display`.
### `--accent-ink` — the text colour on an accent fill
Anything that paints text *on top of* `--accent` (a filled button, a numbered
badge, a highlighted tag) must use `--accent-ink` for its `color`, never a
hardcoded `#fff` or `#000`. Accents across these 36 themes run from `#ffffff`
(`blueprint`) to `#000000` (`sharp-mono`), so no single literal is readable
against all of them: `#0b1024` on `minimal-white`'s near-black accent measured
**1.01:1** — invisible, and the layout still looked fine at a glance.
Each theme sets `--accent-ink` to whichever of white / near-black clears WCAG
AA against its own accent. The tightest in the set is `y2k-chrome` at 4.54:1;
every other theme is higher. The `:root` fallback in `base.css` — what you get
with no theme linked — is `#000000`, which measures 4.77:1 against the default
`--accent` (white would be 4.40:1, below AA).
```css
.my-badge{background:var(--accent);color:var(--accent-ink)}
```
If you add a theme, add `--accent-ink` with it.
## Light & calm ## Light & calm
| name | description | when to use | | name | description | when to use |
@@ -1,42 +1,213 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# html-ppt :: new-deck.sh — scaffold a new deck from templates/deck.html # html-ppt :: new-deck.sh — scaffold a new deck from a template
# #
# Usage: # Usage:
# new-deck.sh <name> [output-parent-dir] # new-deck.sh <name> [output-parent-dir] [-t <template>]
# #
# Creates <parent>/<name>/index.html with paths rewritten to point at the # Creates <parent>/<name>/index.html with the asset references rewritten so
# skill's shared assets/themes/animations. Defaults to ./examples/. # they resolve from wherever the deck actually lands — at any depth, inside
# the skill tree or outside it.
#
# <output-parent-dir> absolute, or relative to the current directory.
# Defaults to <skill>/examples.
# -t, --template "deck" (default), a full-deck name (e.g. pitch-deck),
# a single-page name (e.g. arch-diagram), or a path to
# any .html file / full-deck directory.
set -euo pipefail set -euo pipefail
NAME="${1:-}"
if [[ -z "$NAME" ]]; then
echo "usage: new-deck.sh <name> [parent-dir]" >&2
exit 1
fi
PARENT="${2:-examples}"
HERE="$(cd "$(dirname "$0")/.." && pwd)" HERE="$(cd "$(dirname "$0")/.." && pwd)"
TEMPLATE="$HERE/templates/deck.html"
if [[ ! -f "$TEMPLATE" ]]; then NAME=""
echo "error: template not found at $TEMPLATE" >&2 PARENT=""
TEMPLATE_ARG="deck"
while [[ $# -gt 0 ]]; do
case "$1" in
-t|--template)
TEMPLATE_ARG="${2:-}"
if [[ -z "$TEMPLATE_ARG" ]]; then
echo "error: --template needs a value" >&2
exit 1
fi
shift 2
;;
-h|--help)
sed -n '3,15p' "$0" | sed 's|^# \{0,1\}||'
exit 0
;;
-*)
echo "error: unknown option $1" >&2
exit 1
;;
*)
if [[ -z "$NAME" ]]; then
NAME="$1"
elif [[ -z "$PARENT" ]]; then
PARENT="$1"
else
echo "error: unexpected argument $1" >&2
exit 1
fi
shift
;;
esac
done
if [[ -z "$NAME" ]]; then
echo "usage: new-deck.sh <name> [parent-dir] [-t <template>]" >&2
exit 1 exit 1
fi fi
OUT_DIR="$HERE/$PARENT/$NAME" # ---------------------------------------------------------------- template
# Accept a bare name (deck / a full-decks dir / a single-page layout) or an
# explicit path. A full-deck resolves to a directory and is copied whole.
resolve_template() {
local t="$1"
if [[ -e "$t" ]]; then printf '%s' "$t"; return; fi
if [[ "$t" == "deck" && -f "$HERE/templates/deck.html" ]]; then
printf '%s' "$HERE/templates/deck.html"; return
fi
if [[ -d "$HERE/templates/full-decks/$t" ]]; then
printf '%s' "$HERE/templates/full-decks/$t"; return
fi
if [[ -f "$HERE/templates/single-page/$t.html" ]]; then
printf '%s' "$HERE/templates/single-page/$t.html"; return
fi
if [[ -f "$HERE/templates/$t" ]]; then
printf '%s' "$HERE/templates/$t"; return
fi
return 1
}
if ! TEMPLATE="$(resolve_template "$TEMPLATE_ARG")"; then
echo "error: no template named '$TEMPLATE_ARG'" >&2
echo " full-decks: $(ls "$HERE/templates/full-decks" 2>/dev/null | tr '\n' ' ')" >&2
echo " single-page: see $HERE/templates/single-page/" >&2
exit 1
fi
# ------------------------------------------------------------------ output
# Absolute parent is honoured as-is; a relative one resolves against the
# caller's current directory, which is what a shell user expects. Only the
# default lives inside the skill.
if [[ -z "$PARENT" ]]; then
PARENT_ABS="$HERE/examples"
elif [[ "$PARENT" == /* ]]; then
PARENT_ABS="$PARENT"
else
PARENT_ABS="$PWD/$PARENT"
fi
OUT_DIR="$PARENT_ABS/$NAME"
if [[ -e "$OUT_DIR" ]]; then if [[ -e "$OUT_DIR" ]]; then
echo "error: $OUT_DIR already exists" >&2 echo "error: $OUT_DIR already exists" >&2
exit 1 exit 1
fi fi
mkdir -p "$OUT_DIR" mkdir -p "$OUT_DIR"
OUT_DIR="$(cd "$OUT_DIR" && pwd)" # normalise ., .., symlinks
# templates/deck.html references ../assets/...; for examples/<name>/index.html # ---------------------------------------------------------------- rel path
# that same relative path (../../assets/...) needs one more ../. # Relative path from the deck directory back to the skill root, so the asset
sed 's|href="../assets/|href="../../assets/|g; s|src="../assets/|src="../../assets/|g; s|data-theme-base="../assets/|data-theme-base="../../assets/|g' \ # links are correct at any depth and from outside the skill tree.
"$TEMPLATE" > "$OUT_DIR/index.html" relpath() {
local target="${1%/}" base="${2%/}" up="" rest
while [[ "$base" != "/" && "$target" != "$base" && "$target" != "$base"/* ]]; do
base="$(dirname "$base")"
up="../$up"
done
rest="${target#"$base"}"
rest="${rest#/}"
up="${up%/}"
if [[ -z "$up" ]]; then printf '%s' "${rest:-.}"
elif [[ -z "$rest" ]]; then printf '%s' "$up"
else printf '%s/%s' "$up" "$rest"
fi
}
REL="$(relpath "$HERE" "$OUT_DIR")"
# Rewrite any run of ../ in front of assets/ to the computed prefix, so the
# same logic works for templates/deck.html (../assets/), single-page
# (../../assets/) and full-decks (../../../assets/) alike.
rewrite_assets() {
local file="$1"
sed -E -i.bak "s#([\"'(])(\.\./)*assets/#\1${REL}/assets/#g" "$file"
rm -f "$file.bak"
}
if [[ -d "$TEMPLATE" ]]; then
cp -R "$TEMPLATE/." "$OUT_DIR/"
while IFS= read -r f; do rewrite_assets "$f"; done \
< <(find "$OUT_DIR" -type f \( -name '*.html' -o -name '*.css' -o -name '*.js' \))
else
cp "$TEMPLATE" "$OUT_DIR/index.html"
rewrite_assets "$OUT_DIR/index.html"
fi
if [[ ! -f "$OUT_DIR/index.html" ]]; then
echo "error: template produced no index.html in $OUT_DIR" >&2
exit 1
fi
# Join a relative reference onto a directory the way a browser resolves it
# against a file:// URL: purely lexically. The shell's own ".." follows
# symlinks instead (/tmp -> /private/tmp), which would disagree.
lexjoin() {
local p="$1/$2" out="" seg oldifs="$IFS"
set -f; IFS='/'
for seg in $p; do
case "$seg" in
''|.) ;;
..) out="${out%/*}" ;;
*) out="$out/$seg" ;;
esac
done
IFS="$oldifs"; set +f
printf '%s' "${out:-/}"
}
# List every local href / src / url() reference in a file. The previous version
# of this check only enumerated references containing "assets/", which skipped
# the one reference every bundled deck has and every move breaks: its own
# <link rel="stylesheet" href="style.css">. Across the 15 full-decks that was
# 53 of 68 references actually checked — the 15 it missed were one style.css
# per deck.
#
# Blank lines are dropped by a separate sed stage, not by a '^$' branch inside
# the protocol grep. A fragment-only reference (url(#arrow), href="#top")
# collapses to an empty string once the #… is stripped, and under ugrep —
# which ships as `grep` on some machines — '^$' as one branch of an ERE
# alternation does not reliably match those empty lines (measured: 4 of 5
# survived). The empty string then resolves to the deck directory, which
# exists, so it was silently counted as a verified reference.
list_refs() {
grep -oE "(href|src)=(\"[^\"]*\"|'[^']*')|url\([^)]*\)" "$1" \
| sed -E 's/^(href|src)=//; s/^url\(//; s/\)$//' \
| tr -d "\"'" \
| sed -E 's/[?#].*$//' \
| grep -vE '^[a-zA-Z][a-zA-Z0-9+.-]*:|^//' \
| sed '/^[[:space:]]*$/d' \
| sort -u
}
# Self-check: every local reference must resolve to a real file. A broken path
# here is exactly the bug this script used to ship (#19) — fail loudly.
MISSING=0
CHECKED=0
while IFS= read -r ref; do
CHECKED=$((CHECKED + 1))
if [[ ! -e "$(lexjoin "$OUT_DIR" "$ref")" ]]; then
echo "error: unresolved reference: $ref" >&2
MISSING=1
fi
done < <(list_refs "$OUT_DIR/index.html")
[[ $MISSING -eq 0 ]] || exit 1
echo "✔ created $OUT_DIR/index.html" echo "✔ created $OUT_DIR/index.html"
echo " template: $TEMPLATE"
echo " assets: $REL/assets/"
echo " refs: $CHECKED local reference(s) verified"
echo "" echo ""
echo "next steps:" echo "next steps:"
echo " open $OUT_DIR/index.html" echo " open $OUT_DIR/index.html"
@@ -33,14 +33,37 @@ OUT="${3:-}"
ABS="$(cd "$(dirname "$FILE")" && pwd)/$(basename "$FILE")" ABS="$(cd "$(dirname "$FILE")" && pwd)/$(basename "$FILE")"
STEM="$(basename "${FILE%.*}")" STEM="$(basename "${FILE%.*}")"
# Count <section> elements whose class attribute carries "slide" as a whole
# token. A plain `class="slide"` match misses every slide with an extra class
# (slide is-active, slide dark, slide t-violet …), and a loose \bslide\b would
# also match class="slide-number".
count_slides() {
grep -Eco '<section[^>]*class="([^"]*[[:space:]])?slide([[:space:]][^"]*)?"' "$1" || true
}
if [[ "$COUNT" == "all" ]]; then if [[ "$COUNT" == "all" ]]; then
COUNT="$(grep -c 'class="slide"' "$FILE" || true)" COUNT="$(count_slides "$FILE")"
[[ -z "$COUNT" || "$COUNT" -lt 1 ]] && COUNT=1 if [[ -z "$COUNT" || "$COUNT" -lt 1 ]]; then
echo "warning: no <section class=\"slide\"> found in $FILE — rendering 1 page" >&2
COUNT=1
fi
fi fi
if [[ -z "$OUT" ]]; then if ! [[ "$COUNT" =~ ^[0-9]+$ ]] || [[ "$COUNT" -lt 1 ]]; then
if [[ "$COUNT" -gt 1 ]]; then echo "error: slide count must be a positive integer or 'all' (got: $COUNT)" >&2
OUT="$(dirname "$FILE")/${STEM}-png" exit 1
fi
if [[ -z "$OUT" && "$COUNT" -gt 1 ]]; then
OUT="$(dirname "$FILE")/${STEM}-png"
fi
# Chrome does not create the target directory; without this it exits 0 having
# written nothing.
if [[ -n "$OUT" ]]; then
if [[ "$OUT" == *.png ]]; then
mkdir -p "$(dirname "$OUT")"
else
mkdir -p "$OUT" mkdir -p "$OUT"
fi fi
fi fi
@@ -55,17 +78,37 @@ render_one() {
--virtual-time-budget=4000 \ --virtual-time-budget=4000 \
--window-size=1920,1080 \ --window-size=1920,1080 \
--screenshot="$target" \ --screenshot="$target" \
"$url" >/dev/null 2>&1 "$url" >/dev/null 2>&1 || true
# Chrome reports success even when it could not write the file, so the only
# trustworthy signal is the file itself.
if [[ ! -s "$target" ]]; then
echo " ✗ $target — not written" >&2
return 1
fi
echo " ✔ $target" echo " ✔ $target"
} }
FAILED=0
if [[ "$COUNT" == "1" ]]; then if [[ "$COUNT" == "1" ]]; then
OUT_FILE="${OUT:-$(dirname "$FILE")/${STEM}.png}" if [[ -z "$OUT" ]]; then
render_one "file://$ABS" "$OUT_FILE" OUT_FILE="$(dirname "$FILE")/${STEM}.png"
elif [[ "$OUT" == *.png ]]; then
OUT_FILE="$OUT"
else
OUT_FILE="$OUT/${STEM}.png"
fi
render_one "file://$ABS" "$OUT_FILE" || FAILED=$((FAILED + 1))
else else
for i in $(seq 1 "$COUNT"); do for i in $(seq 1 "$COUNT"); do
render_one "file://$ABS#/$i" "$OUT/${STEM}_$(printf '%02d' "$i").png" render_one "file://$ABS#/$i" "$OUT/${STEM}_$(printf '%02d' "$i").png" \
|| FAILED=$((FAILED + 1))
done done
fi fi
if [[ "$FAILED" -gt 0 ]]; then
echo "error: $FAILED of $COUNT slide(s) were not written from $FILE" >&2
exit 1
fi
echo "done: rendered $COUNT slide(s) from $FILE" echo "done: rendered $COUNT slide(s) from $FILE"
@@ -30,7 +30,7 @@
<section class="slide cover"> <section class="slide cover">
<p class="kicker" style="color:#a78bfa">HTML-PPT v2 · Full-Deck Gallery</p> <p class="kicker" style="color:#a78bfa">HTML-PPT v2 · Full-Deck Gallery</p>
<h1>14 full-deck templates</h1> <h1>15 full-deck templates</h1>
<p>Press → to browse. Each slide is a live iframe preview of a complete, multi-slide deck template. Open any <code>templates/full-decks/&lt;name&gt;/index.html</code> to see the full deck, or copy the folder to scaffold your own.</p> <p>Press → to browse. Each slide is a live iframe preview of a complete, multi-slide deck template. Open any <code>templates/full-decks/&lt;name&gt;/index.html</code> to see the full deck, or copy the folder to scaffold your own.</p>
<div class="legend"> <div class="legend">
<span class="tag">8 extracted from real decks</span> <span class="tag">8 extracted from real decks</span>
@@ -4,6 +4,7 @@
--border:rgba(60,45,20,.12);--border-strong:rgba(60,45,20,.24); --border:rgba(60,45,20,.12);--border-strong:rgba(60,45,20,.24);
--text-1:#2a2418;--text-2:#5a5140;--text-3:#8a7f68; --text-1:#2a2418;--text-2:#5a5140;--text-3:#8a7f68;
--accent:#2d7d6e;--accent-2:#d88a3a;--accent-3:#c4593f; --accent:#2d7d6e;--accent-2:#d88a3a;--accent-3:#c4593f;
--accent-ink:#ffffff; /* text on --accent — 4.9:1 */
--grad:linear-gradient(135deg,#2d7d6e,#4ea893); --grad:linear-gradient(135deg,#2d7d6e,#4ea893);
--radius:14px;--radius-lg:20px; --radius:14px;--radius-lg:20px;
--shadow:0 12px 30px rgba(60,45,20,.07); --shadow:0 12px 30px rgba(60,45,20,.07);
@@ -40,7 +41,7 @@
.tpl-course-module .mcq{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px 22px;margin-bottom:10px;display:flex;gap:14px;align-items:flex-start;cursor:pointer} .tpl-course-module .mcq{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px 22px;margin-bottom:10px;display:flex;gap:14px;align-items:flex-start;cursor:pointer}
.tpl-course-module .mcq .letter{flex:none;width:28px;height:28px;border-radius:50%;border:2px solid var(--text-3);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;color:var(--text-2)} .tpl-course-module .mcq .letter{flex:none;width:28px;height:28px;border-radius:50%;border:2px solid var(--text-3);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;color:var(--text-2)}
.tpl-course-module .mcq.correct{border-color:var(--accent);background:rgba(45,125,110,.06)} .tpl-course-module .mcq.correct{border-color:var(--accent);background:rgba(45,125,110,.06)}
.tpl-course-module .mcq.correct .letter{border-color:var(--accent);background:var(--accent);color:#fff} .tpl-course-module .mcq.correct .letter{border-color:var(--accent);background:var(--accent);color:var(--accent-ink)}
.tpl-course-module .pill-academic{display:inline-block;padding:4px 12px;border-radius:4px;background:var(--surface-2);border:1px solid var(--border);font-size:12px;color:var(--text-2);font-family:'JetBrains Mono',monospace} .tpl-course-module .pill-academic{display:inline-block;padding:4px 12px;border-radius:4px;background:var(--surface-2);border:1px solid var(--border);font-size:12px;color:var(--text-2);font-family:'JetBrains Mono',monospace}
.tpl-course-module .slide.full .h1{font-size:88px} .tpl-course-module .slide.full .h1{font-size:88px}
.tpl-course-module .deck-footer{color:var(--text-3)} .tpl-course-module .deck-footer{color:var(--text-3)}
@@ -4,6 +4,7 @@
--border:rgba(20,25,60,.08);--border-strong:rgba(20,25,60,.18); --border:rgba(20,25,60,.08);--border-strong:rgba(20,25,60,.18);
--text-1:#0d1130;--text-2:#4a5070;--text-3:#8a90ad; --text-1:#0d1130;--text-2:#4a5070;--text-3:#8a90ad;
--accent:#3b5bff;--accent-2:#7a46ff;--accent-3:#d94cff; --accent:#3b5bff;--accent-2:#7a46ff;--accent-3:#d94cff;
--accent-ink:#ffffff; /* text on --accent — 5.1:1 */
--grad:linear-gradient(135deg,#3b5bff 0%,#7a46ff 55%,#d94cff 100%); --grad:linear-gradient(135deg,#3b5bff 0%,#7a46ff 55%,#d94cff 100%);
--grad-soft:linear-gradient(135deg,#eef1ff,#f4edff 55%,#fbedff); --grad-soft:linear-gradient(135deg,#eef1ff,#f4edff 55%,#fbedff);
--radius:20px;--radius-lg:28px; --radius:20px;--radius-lg:28px;
@@ -5,6 +5,7 @@
--border:rgba(10,10,18,.08);--border-strong:rgba(10,10,18,.18); --border:rgba(10,10,18,.08);--border-strong:rgba(10,10,18,.18);
--text-1:#0a0a12;--text-2:#4a4a58;--text-3:#8a8a96; --text-1:#0a0a12;--text-2:#4a4a58;--text-3:#8a8a96;
--accent:#ff5a36;--accent-2:#ff8c5a;--accent-3:#ffb36b; --accent:#ff5a36;--accent-2:#ff8c5a;--accent-3:#ffb36b;
--accent-ink:#0b1024; /* text on --accent — 6.1:1 */
--grad:linear-gradient(120deg,#ff5a36 0%,#ff8c5a 60%,#ffb36b 100%); --grad:linear-gradient(120deg,#ff5a36 0%,#ff8c5a 60%,#ffb36b 100%);
--radius:22px;--radius-lg:32px; --radius:22px;--radius-lg:32px;
--shadow:0 20px 60px rgba(10,10,18,.1); --shadow:0 20px 60px rgba(10,10,18,.1);
@@ -4,6 +4,7 @@
--border:rgba(139,148,158,.22);--border-strong:rgba(139,148,158,.4); --border:rgba(139,148,158,.22);--border-strong:rgba(139,148,158,.4);
--text-1:#e6edf3;--text-2:#8b949e;--text-3:#6e7681; --text-1:#e6edf3;--text-2:#8b949e;--text-3:#6e7681;
--accent:#7ee787;--accent-2:#79c0ff;--accent-3:#ff7b72; --accent:#7ee787;--accent-2:#79c0ff;--accent-3:#ff7b72;
--accent-ink:#0b1024; /* text on --accent — 12.3:1 */
--grad:linear-gradient(120deg,#7ee787 0%,#79c0ff 60%,#d2a8ff 100%); --grad:linear-gradient(120deg,#7ee787 0%,#79c0ff 60%,#d2a8ff 100%);
--radius:14px;--radius-lg:20px; --radius:14px;--radius-lg:20px;
--shadow:0 20px 60px rgba(0,0,0,.5); --shadow:0 20px 60px rgba(0,0,0,.5);
@@ -4,6 +4,7 @@
--border:rgba(22,30,55,.09);--border-strong:rgba(22,30,55,.2); --border:rgba(22,30,55,.09);--border-strong:rgba(22,30,55,.2);
--text-1:#161e37;--text-2:#50586b;--text-3:#8b92a5; --text-1:#161e37;--text-2:#50586b;--text-3:#8b92a5;
--accent:#2e63eb;--accent-2:#0ea5b5;--accent-3:#f59e0b; --accent:#2e63eb;--accent-2:#0ea5b5;--accent-3:#f59e0b;
--accent-ink:#ffffff; /* text on --accent — 5.1:1 */
--good:#10b981;--warn:#f59e0b;--bad:#ef4444; --good:#10b981;--warn:#f59e0b;--bad:#ef4444;
--grad:linear-gradient(120deg,#2e63eb,#0ea5b5); --grad:linear-gradient(120deg,#2e63eb,#0ea5b5);
--radius:14px;--radius-lg:18px; --radius:14px;--radius-lg:18px;
@@ -9,7 +9,7 @@
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
</head> </head>
<body class="tpl-xhs-post"> <body class="tpl-xhs-post">
<div class="deck"> <div class="deck" data-w="810" data-h="1080">
<!-- 1. Cover --> <!-- 1. Cover -->
<section class="slide" data-title="Cover"> <section class="slide" data-title="Cover">
@@ -4,6 +4,7 @@
--border:rgba(90,40,30,.12);--border-strong:rgba(90,40,30,.24); --border:rgba(90,40,30,.12);--border-strong:rgba(90,40,30,.24);
--text-1:#3a1f18;--text-2:#6f4a3e;--text-3:#a68676; --text-1:#3a1f18;--text-2:#6f4a3e;--text-3:#a68676;
--accent:#ff6b8b;--accent-2:#ffa94d;--accent-3:#ffd166; --accent:#ff6b8b;--accent-2:#ffa94d;--accent-3:#ffd166;
--accent-ink:#0b1024; /* text on --accent — 6.9:1 */
--grad:linear-gradient(135deg,#ffd3e0,#ffe5c7 50%,#d6f0ff); --grad:linear-gradient(135deg,#ffd3e0,#ffe5c7 50%,#d6f0ff);
--good:#7bc67b;--warn:#ffb547;--bad:#ff6b6b; --good:#7bc67b;--warn:#ffb547;--bad:#ff6b6b;
--radius:24px;--radius-lg:32px; --radius:24px;--radius-lg:32px;
@@ -11,7 +12,15 @@
font-family:'Inter','Noto Sans SC','PingFang SC',sans-serif; font-family:'Inter','Noto Sans SC','PingFang SC',sans-serif;
} }
.tpl-xhs-post{background:#f0eae2;display:flex;align-items:center;justify-content:center;min-height:100vh} .tpl-xhs-post{background:#f0eae2;display:flex;align-items:center;justify-content:center;min-height:100vh}
.tpl-xhs-post .deck{width:810px;height:1080px;position:relative;background:transparent} /* Canvas size lives in one place: these two custom properties feed
* base.css (.deck{width:var(--deck-w)}) so the card is 810x1080 even with
* JS off, and the matching data-w/data-h on .deck feed runtime.js so the
* fit scale is computed against 810x1080 rather than the 1920x1080
* default. When only the CSS carried the size, the scale was still
* min(vw/1920, vh/1080) and the card rendered at 42% of a portrait
* window. Change one, change the other. */
.tpl-xhs-post{--deck-w:810px;--deck-h:1080px}
.tpl-xhs-post .deck{position:relative;background:transparent}
.tpl-xhs-post .slide{ .tpl-xhs-post .slide{
position:absolute;inset:0;width:810px;height:1080px;aspect-ratio:3/4; position:absolute;inset:0;width:810px;height:1080px;aspect-ratio:3/4;
padding:70px 64px;border-radius:28px;overflow:hidden; padding:70px 64px;border-radius:28px;overflow:hidden;
@@ -35,7 +44,7 @@
.tpl-xhs-post .hand-box{background:#fff;border:2.5px solid var(--text-1);border-radius:22px;padding:24px 28px;box-shadow:5px 5px 0 var(--text-1)} .tpl-xhs-post .hand-box{background:#fff;border:2.5px solid var(--text-1);border-radius:22px;padding:24px 28px;box-shadow:5px 5px 0 var(--text-1)}
.tpl-xhs-post .lede{color:var(--text-2);font-size:26px;line-height:1.55} .tpl-xhs-post .lede{color:var(--text-2);font-size:26px;line-height:1.55}
.tpl-xhs-post .big-emoji{font-size:180px;line-height:1;text-align:center} .tpl-xhs-post .big-emoji{font-size:180px;line-height:1;text-align:center}
.tpl-xhs-post .num-circle{display:inline-flex;align-items:center;justify-content:center;width:72px;height:72px;border-radius:50%;background:var(--accent);color:#fff;font-weight:900;font-size:36px;border:3px solid var(--text-1);box-shadow:4px 4px 0 var(--text-1)} .tpl-xhs-post .num-circle{display:inline-flex;align-items:center;justify-content:center;width:72px;height:72px;border-radius:50%;background:var(--accent);color:var(--accent-ink);font-weight:900;font-size:36px;border:3px solid var(--text-1);box-shadow:4px 4px 0 var(--text-1)}
.tpl-xhs-post .step-card{background:#fff;border:2.5px solid var(--text-1);border-radius:22px;padding:26px 28px;box-shadow:5px 5px 0 var(--text-1);margin-bottom:24px} .tpl-xhs-post .step-card{background:#fff;border:2.5px solid var(--text-1);border-radius:22px;padding:26px 28px;box-shadow:5px 5px 0 var(--text-1);margin-bottom:24px}
.tpl-xhs-post .step-card h4{font-size:28px;font-weight:800;margin:0 0 6px} .tpl-xhs-post .step-card h4{font-size:28px;font-weight:800;margin:0 0 6px}
.tpl-xhs-post .step-card p{font-size:18px;color:var(--text-2);margin:0} .tpl-xhs-post .step-card p{font-size:18px;color:var(--text-2);margin:0}
@@ -25,7 +25,8 @@ body{margin:0;overflow:hidden}
<script> <script>
const list=['cover','toc','section-divider','bullets','two-column','three-column','big-quote', const list=['cover','toc','section-divider','bullets','two-column','three-column','big-quote',
'stat-highlight','kpi-grid','table','code','diff','terminal','flow-diagram','timeline', 'stat-highlight','kpi-grid','table','code','diff','terminal','flow-diagram','timeline',
'roadmap','mindmap','comparison','pros-cons','todo-checklist','gantt','image-hero','image-grid', 'roadmap','mindmap','comparison','pros-cons','todo-checklist','gantt',
'image-single','image-full-bleed','image-text-split','image-gallery','image-compare','image-hero','image-grid',
'chart-bar','chart-line','chart-pie','chart-radar','arch-diagram','process-steps','cta','thanks']; 'chart-bar','chart-line','chart-pie','chart-radar','arch-diagram','process-steps','cta','thanks'];
let i=0; let i=0;
const frame=document.getElementById('frame'); const frame=document.getElementById('frame');
@@ -5,7 +5,7 @@
<link rel="stylesheet" id="theme-link" href="../../assets/themes/aurora.css"> <link rel="stylesheet" id="theme-link" href="../../assets/themes/aurora.css">
<link rel="stylesheet" href="../../assets/animations/animations.css"> <link rel="stylesheet" href="../../assets/animations/animations.css">
<style> <style>
.cta .btn{display:inline-flex;align-items:center;gap:10px;padding:20px 32px;border-radius:999px;background:var(--accent);color:#0b1024;font-weight:700;font-size:20px;box-shadow:var(--shadow-lg);text-decoration:none;border:none;cursor:pointer} .cta .btn{display:inline-flex;align-items:center;gap:10px;padding:20px 32px;border-radius:999px;background:var(--accent);color:var(--accent-ink);font-weight:700;font-size:20px;box-shadow:var(--shadow-lg);text-decoration:none;border:none;cursor:pointer}
.cta .btn.outline{background:transparent;color:var(--text-1);border:1.5px solid var(--border-strong)} .cta .btn.outline{background:transparent;color:var(--text-1);border:1.5px solid var(--border-strong)}
</style> </style>
</head><body class="single"> </head><body class="single">
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8"><title>Image Compare</title>
<link rel="stylesheet" href="../../assets/fonts.css">
<link rel="stylesheet" href="../../assets/base.css">
<link rel="stylesheet" id="theme-link" href="../../assets/themes/minimal-white.css">
<link rel="stylesheet" href="../../assets/animations/animations.css">
<style>
.cmp{display:grid;grid-template-columns:1fr 1fr;gap:30px;margin-top:26px;flex:1;min-height:0}
.cmp > div{display:flex;flex-direction:column;min-height:0}
/* Both sides share the leftover height, so the two shots stay exactly the same
size — a size difference would read as part of the before/after story. */
.cmp .img-frame{aspect-ratio:auto;flex:1;min-height:0}
.cmp .lbl{display:flex;align-items:center;gap:10px;margin:0 0 12px;font-size:15px;font-weight:600;letter-spacing:.02em}
.cmp .dot{width:10px;height:10px;border-radius:50%}
.cmp .before .dot{background:var(--text-3)}
.cmp .after .dot{background:var(--good)}
.cmp .after .img-frame{box-shadow:var(--shadow-lg)}
</style>
</head><body class="single">
<div class="deck"><section class="slide is-active" data-title="Image Compare">
<p class="kicker">Before / After · 07</p>
<h2 class="h2">改版前后,一眼看见差别</h2>
<div class="cmp anim-stagger-list" data-anim-target>
<div class="before">
<p class="lbl"><span class="dot"></span>改版前 · v2.4</p>
<figure class="img-frame"><img src="../../assets/demo-images/shot-before.svg" alt="改版前界面"></figure>
<p class="img-cap">信息密度高,重点不突出,首屏要滚动两屏才看到指标。</p>
</div>
<div class="after">
<p class="lbl"><span class="dot"></span>改版后 · v3.0</p>
<figure class="img-frame"><img src="../../assets/demo-images/shot-after.svg" alt="改版后界面"></figure>
<p class="img-cap">指标提到首屏,次要信息折叠,平均完成时长下降 38%。</p>
</div>
</div>
<div class="notes">两图对照。两侧务必用同一比例、同一拍摄/截图尺寸,否则观众看到的差别里混进了版式差别。结论写在图下一行,不要留给观众猜。</div>
</section></div>
<script src="../../assets/runtime.js"></script>
</body></html>
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8"><title>Image Full Bleed</title>
<link rel="stylesheet" href="../../assets/fonts.css">
<link rel="stylesheet" href="../../assets/base.css">
<link rel="stylesheet" id="theme-link" href="../../assets/themes/minimal-white.css">
<link rel="stylesheet" href="../../assets/animations/animations.css">
<style>
.bleed{position:absolute;inset:0}
.bleed .img-frame{border-radius:0;box-shadow:none;height:100%;aspect-ratio:auto}
.bleed .cap{position:absolute;left:96px;right:96px;bottom:88px;color:#fff;z-index:2}
.bleed .cap h1{font-size:84px;line-height:1.02;font-weight:800;letter-spacing:-.03em;margin:0}
.bleed .cap p{font-size:21px;opacity:.88;margin:16px 0 0;max-width:62ch}
.bleed .pill{background:rgba(255,255,255,.14);color:#fff;border:1px solid rgba(255,255,255,.24)}
</style>
</head><body class="single">
<div class="deck"><section class="slide is-active" data-title="Image Full Bleed" style="padding:0">
<div class="bleed">
<figure class="img-frame">
<img src="../../assets/demo-images/photo-landscape.svg" alt="发布会现场">
<div class="img-scrim"></div>
</figure>
<div class="cap">
<span class="pill">Keynote · 2026</span>
<h1 class="mt-s anim-rise-in" data-anim="rise-in">让画面先说话</h1>
<p>整页一张图 + 底部渐变压暗,文字永远读得清 —— 换成你自己的照片,版式不用改。</p>
</div>
</div>
<div class="notes">整页图:<code>.img-frame</code> 不设比例、高度撑满,配 <code>.img-scrim</code> 压暗底部保证白字可读。图片主体偏上时加 <code>style="--img-pos:top"</code>。</div>
</section></div>
<script src="../../assets/runtime.js"></script>
</body></html>
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8"><title>Image Gallery</title>
<link rel="stylesheet" href="../../assets/fonts.css">
<link rel="stylesheet" href="../../assets/base.css">
<link rel="stylesheet" id="theme-link" href="../../assets/themes/minimal-white.css">
<link rel="stylesheet" href="../../assets/animations/animations.css">
<style>
.gal{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:22px}
/* The grid takes the space left over from the title and splits it between two
equal rows. A fixed ratio (4:3, 16:9…) pushes the bottom row's captions off
the slide as soon as the window is shorter than 16:9. */
.gal{flex:1;min-height:0;grid-template-rows:1fr 1fr}
.gal > div{display:flex;flex-direction:column;min-height:0}
.gal .img-frame{aspect-ratio:auto;flex:1;min-height:0}
.gal figcaption{margin:10px 2px 0;font-size:14px;color:var(--text-3)}
</style>
</head><body class="single">
<div class="deck"><section class="slide is-active" data-title="Image Gallery">
<p class="kicker">Gallery · 06</p>
<h2 class="h2">多张图并列,每张一句话</h2>
<div class="gal anim-stagger-list" data-anim-target>
<div><figure class="img-frame"><img src="../../assets/demo-images/photo-landscape.svg" alt="场景一"></figure><figcaption>01 · 现场</figcaption></div>
<div><figure class="img-frame"><img src="../../assets/demo-images/photo-square.svg" alt="场景二"></figure><figcaption>02 · 细节</figcaption></div>
<div><figure class="img-frame"><img src="../../assets/demo-images/photo-portrait.svg" alt="场景三"></figure><figcaption>03 · 人物</figcaption></div>
<div><figure class="img-frame"><img src="../../assets/demo-images/photo-wide.svg" alt="场景四"></figure><figcaption>04 · 远景</figcaption></div>
<div><figure class="img-frame"><img src="../../assets/demo-images/shot-ui.svg" alt="场景五"></figure><figcaption>05 · 界面</figcaption></div>
<div><figure class="img-frame"><img src="../../assets/demo-images/shot-after.svg" alt="场景六"></figure><figcaption>06 · 数据</figcaption></div>
</div>
<div class="notes">2–6 张等大并列。六张原始比例各不相同(16:10 / 1:1 / 3:4 / 21:9),靠 <code>.img-frame</code> 统一成 4:3 —— 这正是网格版式最容易翻车的地方。少于 6 张就删格子,列数改 <code>repeat(2,1fr)</code>。</div>
</section></div>
<script src="../../assets/runtime.js"></script>
</body></html>
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8"><title>Image Single</title>
<link rel="stylesheet" href="../../assets/fonts.css">
<link rel="stylesheet" href="../../assets/base.css">
<link rel="stylesheet" id="theme-link" href="../../assets/themes/minimal-white.css">
<link rel="stylesheet" href="../../assets/animations/animations.css">
<style>
/* The figure takes whatever height is left after the title — so the slide
holds together at any window height instead of only at 16:9. */
.fig{display:flex;flex-direction:column;align-items:center;margin-top:22px;flex:1;min-height:0}
.fig .img-frame{aspect-ratio:auto;flex:1;min-height:0;width:min(1040px,100%)}
.fig .img-cap{text-align:center;max-width:70ch}
</style>
</head><body class="single">
<div class="deck"><section class="slide is-active" data-title="Image Single">
<p class="kicker">Figure · 01</p>
<h2 class="h2">一张图讲清楚一件事</h2>
<div class="fig anim-fade-up" data-anim="fade-up">
<figure class="img-frame contain">
<img src="../../assets/demo-images/shot-ui.svg" alt="产品主界面截图">
</figure>
<p class="img-cap">图 1 · 新版工作台。左侧是任务流,右侧是实时指标 —— 截图、图表、架构图都用这个版式。</p>
</div>
<div class="notes">单图版式用 <code>.img-frame.contain</code>:不裁剪,整张图完整可见。截图 / 示意图 / 图表一律用这个,不要用 cover。</div>
</section></div>
<script src="../../assets/runtime.js"></script>
</body></html>
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8"><title>Image + Text</title>
<link rel="stylesheet" href="../../assets/fonts.css">
<link rel="stylesheet" href="../../assets/base.css">
<link rel="stylesheet" id="theme-link" href="../../assets/themes/minimal-white.css">
<link rel="stylesheet" href="../../assets/animations/animations.css">
<style>
.split{display:grid;grid-template-columns:1fr 1fr;gap:56px;margin-top:18px;flex:1;min-height:0}
.split.flip > .col-img{order:2}
.split > div{display:flex;flex-direction:column;justify-content:center;min-height:0}
/* Height first, width follows the ratio. A tall image can then never push past
the slide's padding box, whatever its natural aspect ratio is. */
.split .img-frame{--img-ratio:4/5;height:100%;width:auto;max-width:100%;align-self:center}
.split ul{margin:18px 0 0;padding-left:20px}
.split li{margin:10px 0;color:var(--text-2);font-size:19px}
</style>
</head><body class="single">
<div class="deck"><section class="slide is-active" data-title="Image + Text">
<div class="split">
<div class="col-img anim-fade-right" data-anim="fade-right">
<figure class="img-frame">
<img src="../../assets/demo-images/photo-portrait.svg" alt="产品实拍">
<span class="img-tag">Product</span>
</figure>
</div>
<div class="col-text">
<p class="kicker">Product · 03</p>
<h2 class="h2">左图右文,最常用的一页</h2>
<ul class="anim-stagger-list" data-anim-target>
<li>图占一半,说明占一半 —— 视觉与论述各不抢戏</li>
<li>竖图、方图、横图都能放:框决定比例,图自己 <code>cover</code></li>
<li>想把图放右边:给 <code>.split</code> 加 <code>flip</code></li>
</ul>
</div>
</div>
<div class="notes">图文各半。框比例由 <code>--img-ratio</code> 控制,这里用 4/5 竖构图;图片换成横图也不会破版。</div>
</section></div>
<script src="../../assets/runtime.js"></script>
</body></html>
@@ -7,7 +7,7 @@
<style> <style>
.mm{position:relative;margin:30px auto 0;max-width:1200px;height:520px} .mm{position:relative;margin:30px auto 0;max-width:1200px;height:520px}
.mm .n{position:absolute;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:12px 18px;box-shadow:var(--shadow);font-weight:600} .mm .n{position:absolute;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:12px 18px;box-shadow:var(--shadow);font-weight:600}
.mm .root{top:calc(50% - 34px);left:calc(50% - 100px);width:200px;padding:22px;background:var(--accent);color:#fff;border:none;text-align:center;font-size:20px;border-radius:24px} .mm .root{top:calc(50% - 34px);left:calc(50% - 100px);width:200px;padding:22px;background:var(--accent);color:var(--accent-ink);border:none;text-align:center;font-size:20px;border-radius:24px}
.mm .n.sm{font-size:13px} .mm .n.sm{font-size:13px}
.mm svg{position:absolute;inset:0;width:100%;height:100%;pointer-events:none} .mm svg{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
.mm svg path{stroke:var(--border-strong);stroke-width:1.5;fill:none} .mm svg path{stroke:var(--border-strong);stroke-width:1.5;fill:none}
@@ -7,7 +7,7 @@
<style> <style>
.steps{display:grid;grid-template-columns:repeat(4,1fr);gap:22px;margin-top:24px} .steps{display:grid;grid-template-columns:repeat(4,1fr);gap:22px;margin-top:24px}
.step{position:relative;padding:24px 26px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);box-shadow:var(--shadow)} .step{position:relative;padding:24px 26px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);box-shadow:var(--shadow)}
.step .num{position:absolute;top:-24px;left:22px;width:48px;height:48px;border-radius:50%;background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:20px;box-shadow:var(--shadow)} .step .num{position:absolute;top:-24px;left:22px;width:48px;height:48px;border-radius:50%;background:var(--accent);color:var(--accent-ink);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:20px;box-shadow:var(--shadow)}
.step h4{margin:18px 0 8px;font-size:17px} .step h4{margin:18px 0 8px;font-size:17px}
.step p{font-size:13px;color:var(--text-2);line-height:1.6} .step p{font-size:13px;color:var(--text-2);line-height:1.6}
.step .tag{display:inline-block;margin-top:10px;font-size:11px;padding:3px 10px;border-radius:999px;background:var(--surface-2);color:var(--text-3)} .step .tag{display:inline-block;margin-top:10px;font-size:11px;padding:3px 10px;border-radius:999px;background:var(--surface-2);color:var(--text-3)}
@@ -10,7 +10,7 @@
.rm .col:last-child{border-right:none} .rm .col:last-child{border-right:none}
.rm .col .tag{display:inline-block;padding:3px 12px;border-radius:999px;font-size:11px;background:var(--surface-2);color:var(--text-2);margin-bottom:10px} .rm .col .tag{display:inline-block;padding:3px 12px;border-radius:999px;font-size:11px;background:var(--surface-2);color:var(--text-2);margin-bottom:10px}
.rm .col.now{background:color-mix(in srgb,var(--accent) 6%,var(--surface))} .rm .col.now{background:color-mix(in srgb,var(--accent) 6%,var(--surface))}
.rm .col.now .tag{background:var(--accent);color:#fff} .rm .col.now .tag{background:var(--accent);color:var(--accent-ink)}
.rm h4{font-size:15px;margin:8px 0 4px} .rm h4{font-size:15px;margin:8px 0 4px}
.rm ul{padding-left:18px;margin:8px 0 0;font-size:13px;color:var(--text-2)} .rm ul{padding-left:18px;margin:8px 0 0;font-size:13px;color:var(--text-2)}
.rm li{margin-bottom:6px;line-height:1.5} .rm li{margin-bottom:6px;line-height:1.5}
@@ -3,5 +3,5 @@
"name": "playwright浏览器自动化操作", "name": "playwright浏览器自动化操作",
"version": "20260605", "version": "20260605",
"keySource": "none", "keySource": "none",
"syncedAt": "2026-09-11T16:02:21Z" "syncedAt": "2026-09-14T16:02:20Z"
} }
@@ -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": "d155ba9ebfffe4742efefda8d68c2e0e8e490924", "commit": "3dc93da27e29048d601546ed610c903705f7fd6c",
"adapter": "skill-collection", "adapter": "skill-collection",
"sourcePath": "skills", "sourcePath": "skills",
"syncedAt": "2026-09-11T16:00:01Z" "syncedAt": "2026-09-14T15:59:59Z"
} }
+5 -5
View File
@@ -15,7 +15,7 @@ English | [中文](./README_CN.md)
## ❤️ Sponsors ## ❤️ Sponsors
This project is kept free and open source with the support of <a href="https://www.kimi.com/code/?aff=ppt-master">Kimi</a>, <a href="https://www.packyapi.ai/register?aff=ppt-master">PackyCode</a>, <a href="https://apikey.fun/register?aff=PPT-MASTER">APIKEY.FUN</a>, <a href="https://runapi.host/register?aff=WMLJ">RunAPI</a>, <a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624">YouYun ZhiSuan</a> and other sponsors. This project is kept free and open source with the support of <a href="https://www.kimi.com/code/?aff=ppt-master">Kimi</a>, <a href="https://www.packyapi.ai/register?aff=ppt-master">PackyCode</a>, <a href="https://apikey.fan/register?aff=PPT-MASTER">APIKEY.FAN</a>, <a href="https://runapi.host/register?aff=WMLJ">RunAPI</a>, <a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624">YouYun ZhiSuan</a> and other sponsors.
> **[Want to appear here?](SPONSORING.md)** > **[Want to appear here?](SPONSORING.md)**
@@ -38,8 +38,8 @@ Thanks to [Kimi](https://www.kimi.com/code/?aff=ppt-master) for sponsoring this
<td>Thanks to PackyCode for sponsoring this project! PackyCode is a reliable and efficient API relay service provider, offering relay services for Claude Code, Codex, Gemini, and more. PackyCode provides special discounts for our project users: register using <a href="https://www.packyapi.ai/register?aff=ppt-master">this link</a> and enter the promo code <strong>ppt-master</strong> during recharge to get 10% off.</td> <td>Thanks to PackyCode for sponsoring this project! PackyCode is a reliable and efficient API relay service provider, offering relay services for Claude Code, Codex, Gemini, and more. PackyCode provides special discounts for our project users: register using <a href="https://www.packyapi.ai/register?aff=ppt-master">this link</a> and enter the promo code <strong>ppt-master</strong> during recharge to get 10% off.</td>
</tr> </tr>
<tr> <tr>
<td width="180"><a href="https://apikey.fun/register?aff=PPT-MASTER"><img src="docs/assets/sponsors/apikey-fun.png" alt="APIKEY.FUN" width="150"></a></td> <td width="180"><a href="https://apikey.fan/register?aff=PPT-MASTER"><img src="docs/assets/sponsors/apikey-fan.png" alt="APIKEY.FAN" width="150"></a></td>
<td>Thanks to APIKEY.FUN for sponsoring this project! APIKEY.FUN is a professional enterprise-grade AI relay service committed to stable, efficient, and low-cost AI access for businesses and developers. The platform supports mainstream models including Claude, OpenAI, and Gemini, with prices as low as <strong>7% of official rates</strong>. Register through <a href="https://apikey.fun/register?aff=PPT-MASTER">our dedicated link</a> for an exclusive perk: <strong>up to 5% off on top-ups, permanently</strong>.</td> <td>Thanks to APIKEY.FAN for sponsoring this project! APIKEY.FAN is a professional enterprise-grade AI relay service committed to stable, efficient, and low-cost AI access for businesses and developers. The platform supports mainstream models including Claude, OpenAI, and Gemini, with prices as low as <strong>7% of official rates</strong>. Register through <a href="https://apikey.fan/register?aff=PPT-MASTER">our dedicated link</a> for an exclusive perk: <strong>up to 5% off on top-ups, permanently</strong>.</td>
</tr> </tr>
<tr> <tr>
<td width="180"><a href="https://runapi.host/register?aff=WMLJ"><img src="docs/assets/sponsors/runapi.png" alt="RunAPI" width="150"></a></td> <td width="180"><a href="https://runapi.host/register?aff=WMLJ"><img src="docs/assets/sponsors/runapi.png" alt="RunAPI" width="150"></a></td>
@@ -219,7 +219,7 @@ Never used one of these? Don't worry — in this project they play exactly one r
> **Model recommendation**: for the best results, use **[Kimi K3](https://www.kimi.com/code/?aff=ppt-master)** (or Claude) to drive the pipeline, paired with AI image generation — **`gpt-image-2`** (OpenAI) or **`gemini-3.1-flash-image`** (Google). Kimi Code, the project sponsor, is a great pick for pay-as-you-go access. > **Model recommendation**: for the best results, use **[Kimi K3](https://www.kimi.com/code/?aff=ppt-master)** (or Claude) to drive the pipeline, paired with AI image generation — **`gpt-image-2`** (OpenAI) or **`gemini-3.1-flash-image`** (Google). Kimi Code, the project sponsor, is a great pick for pay-as-you-go access.
**🔑 Want to use Claude / GPT / Gemini but don't have access yet?** Project sponsors **[PackyCode](https://www.packyapi.ai/register?aff=ppt-master)**, **[APIKEY.FUN](https://apikey.fun/register?aff=PPT-MASTER)** and **[RunAPI](https://runapi.host/register?aff=WMLJ)** offer pay-as-you-go access to Claude, GPT, Gemini and more — no subscription required, with exclusive discounts for our users (details at the top of this page). **🔑 Want to use Claude / GPT / Gemini but don't have access yet?** Project sponsors **[PackyCode](https://www.packyapi.ai/register?aff=ppt-master)**, **[APIKEY.FAN](https://apikey.fan/register?aff=PPT-MASTER)** and **[RunAPI](https://runapi.host/register?aff=WMLJ)** offer pay-as-you-go access to Claude, GPT, Gemini and more — no subscription required, with exclusive discounts for our users (details at the top of this page).
**🔀 Juggling several providers?** Once you hold keys from more than one of them, [cc-switch](https://github.com/farion1231/cc-switch) — a cross-platform desktop app — lets you one-click switch API providers for Claude Code, Codex, Gemini CLI and more, no manual config editing. **🔀 Juggling several providers?** Once you hold keys from more than one of them, [cc-switch](https://github.com/farion1231/cc-switch) — a cross-platform desktop app — lets you one-click switch API providers for Claude Code, Codex, Gemini CLI and more, no manual config editing.
@@ -410,7 +410,7 @@ PPT Master is currently built and maintained primarily by me. Every new template
&nbsp; &nbsp;
<a href="https://www.packyapi.ai/register?aff=ppt-master"><img src="docs/assets/sponsors/packycode.png" alt="PackyCode" height="40" /></a> <a href="https://www.packyapi.ai/register?aff=ppt-master"><img src="docs/assets/sponsors/packycode.png" alt="PackyCode" height="40" /></a>
&nbsp; &nbsp;
<a href="https://apikey.fun/register?aff=PPT-MASTER"><img src="docs/assets/sponsors/apikey-fun.png" alt="APIKEY.FUN" height="40" /></a> <a href="https://apikey.fan/register?aff=PPT-MASTER"><img src="docs/assets/sponsors/apikey-fan.png" alt="APIKEY.FAN" height="40" /></a>
&nbsp; &nbsp;
<a href="https://runapi.host/register?aff=WMLJ"><img src="docs/assets/sponsors/runapi.png" alt="RunAPI" height="40" /></a> <a href="https://runapi.host/register?aff=WMLJ"><img src="docs/assets/sponsors/runapi.png" alt="RunAPI" height="40" /></a>
&nbsp; &nbsp;
@@ -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": "b3073f8fcf5d2ebeda621a424097dde9dd771cea", "commit": "c6ae4896da10497464ad7e87f5833bfc14321621",
"adapter": "claude-skill", "adapter": "claude-skill",
"sourcePath": "skills/ppt-master", "sourcePath": "skills/ppt-master",
"syncedAt": "2026-09-11T16:00:01Z" "syncedAt": "2026-09-14T15:59:59Z"
} }
@@ -2,7 +2,7 @@
name: ppt-master name: ppt-master
description: "多格式源文档到高质量 SVG 页面再导出 PPTX 的多阶段演示文稿生成工作流。" description: "多格式源文档到高质量 SVG 页面再导出 PPTX 的多阶段演示文稿生成工作流。"
metadata: metadata:
version: "6.3.2" version: "6.4.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"
@@ -28,11 +28,11 @@ Thanks to [Kimi](https://www.kimi.com/code/?aff=ppt-master) for sponsoring PPT M
[PackyCode](https://www.packyapi.ai/register?aff=ppt-master) provides relay access to Claude Code, Codex, Gemini, and other services. Register through the dedicated link and enter the promo code **`ppt-master`** during recharge to receive 10% off. [PackyCode](https://www.packyapi.ai/register?aff=ppt-master) provides relay access to Claude Code, Codex, Gemini, and other services. Register through the dedicated link and enter the promo code **`ppt-master`** during recharge to receive 10% off.
### APIKEY.FUN ### APIKEY.FAN
<a href="https://apikey.fun/register?aff=PPT-MASTER"><img src="https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/sponsors/apikey-fun.png" alt="APIKEY.FUN" width="150"></a> <a href="https://apikey.fan/register?aff=PPT-MASTER"><img src="https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/sponsors/apikey-fan.png" alt="APIKEY.FAN" width="150"></a>
[APIKEY.FUN](https://apikey.fun/register?aff=PPT-MASTER) provides enterprise-grade access to Claude, OpenAI, Gemini, and other mainstream models. Register through the dedicated link to receive up to a permanent 5% discount on top-ups. [APIKEY.FAN](https://apikey.fan/register?aff=PPT-MASTER) provides enterprise-grade access to Claude, OpenAI, Gemini, and other mainstream models. Register through the dedicated link to receive up to a permanent 5% discount on top-ups.
### RunAPI ### RunAPI
@@ -28,11 +28,11 @@ PPT Master 始终免费开源。以下赞助方共同支持项目的持续维护
[PackyCode](https://www.packyapi.ai/register?aff=ppt-master) 提供 Claude Code、Codex、Gemini 等服务的中转接入。通过专属链接注册,并在充值时填写优惠码 **`ppt-master`**,即可享受 9 折优惠。 [PackyCode](https://www.packyapi.ai/register?aff=ppt-master) 提供 Claude Code、Codex、Gemini 等服务的中转接入。通过专属链接注册,并在充值时填写优惠码 **`ppt-master`**,即可享受 9 折优惠。
### APIKEY.FUN ### APIKEY.FAN
<a href="https://apikey.fun/register?aff=PPT-MASTER"><img src="https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/sponsors/apikey-fun.png" alt="APIKEY.FUN" width="150"></a> <a href="https://apikey.fan/register?aff=PPT-MASTER"><img src="https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/sponsors/apikey-fan.png" alt="APIKEY.FAN" width="150"></a>
[APIKEY.FUN](https://apikey.fun/register?aff=PPT-MASTER) 提供 Claude、OpenAI、Gemini 等主流模型的企业级接入服务。通过专属链接注册,最高可享永久充值 95 折优惠。 [APIKEY.FAN](https://apikey.fan/register?aff=PPT-MASTER) 提供 Claude、OpenAI、Gemini 等主流模型的企业级接入服务。通过专属链接注册,最高可享永久充值 95 折优惠。
### RunAPI ### RunAPI
@@ -107,7 +107,7 @@ When one semantic object continues across adjacent slides, the destination slide
|---|---| |---|---|
| Owner | `morph` belongs to the destination; `morph.from` is the immediately preceding stem in export order | | Owner | `morph` belongs to the destination; `morph.from` is the immediately preceding stem in export order |
| Source of pairs | `scaffold` never guesses identity — add pairs from the motion plan after inspecting final direct-root ids | | Source of pairs | `scaffold` never guesses identity — add pairs from the motion plan after inspecting final direct-root ids |
| Pair key | A stable identity whose `from`/`to` are unique direct-root `<g>` ids on the two slides, written without `!!` (export writes the Selection Pane name `!!<key>` on both); a root primitive with a static role marker is not pairable, and neither is a `Native-ready=yes` chart or table — `--native-charts-and-tables` turns it into a graphicFrame and the pair fails at export | | Pair key | A stable identity whose `from`/`to` are unique direct-root `<g>` ids on the two slides, written without `!!` (export writes the Selection Pane name `!!<key>` on both); a root primitive with a static role marker is not pairable, neither is a `Native-ready=yes` chart or table — `--native-charts-and-tables` turns it into a graphicFrame and the pair fails at export — nor a `data-pptx-placeholder` slot on a structured page, which export rewrites into a layout placeholder; pair a Slide-local group there |
| Destination effect | A destination with pairs sets `effect: morph` explicitly (`morph_by` omitted or `object`; `word`/`character` rejected; a CLI override that changes the effect fails) | | Destination effect | A destination with pairs sets `effect: morph` explicitly (`morph_by` omitted or `object`; `word`/`character` rejected; a CLI override that changes the effect fails) |
| Chains | A middle slide may continue an object into another Morph under the same key | | Chains | A middle slide may continue an object into another Morph under the same key |
| Uniqueness | One key never names two objects on a slide; one object never carries two keys; every `!!` key shared by adjacent Morph pages must be declared | | Uniqueness | One key never names two objects on a slide; one object never carries two keys; every `!!` key shared by adjacent Morph pages must be declared |
@@ -157,11 +157,11 @@ Repeating symmetric card grids without a page job is the failure mode these stru
**Hard rule — discovery does not expand compatibility**: follow `svg-effects.md` syntax and fallbacks. Source/backdrop blur, blend mode, `<mask>` / per-pixel masking, dense texture, and skew stay baked or alternative-only. **Hard rule — discovery does not expand compatibility**: follow `svg-effects.md` syntax and fallbacks. Source/backdrop blur, blend mode, `<mask>` / per-pixel masking, dense texture, and skew stay baked or alternative-only.
**Default — author motion endpoints while pages are still being written (may override when the deck has no continuous action)**: effects, transitions, and Morph pair keys are post-processing, but the two visible endpoint states are not. A sequence that should read as one action (slide-in, flip, push-in, progressive reveal, pan) is authored now as consecutive pages, each continuing endpoint in a compatible direct-root group; ids or geometry may differ, and `animations.json` binds them later. **Default — author motion endpoints while pages are still being written (may override when the deck has no continuous action)**: effects, transitions, and Morph pair keys are post-processing, but the two visible endpoint states are not. A sequence that should read as one action is authored now as consecutive pages, each continuing endpoint in a compatible direct-root group; geometry may differ, and `animations.json` binds them later.
| Motion endpoints | Rule | | Motion endpoints | Rule |
|---|---| |---|---|
| Activation | Only an explicit user motion instruction, an enabled Custom Animations outcome, or an existing `animations.json`; a §IX Motion suggestion alone does not activate it | | Activation | Effects and transitions need an explicit motion instruction, an enabled Custom Animations outcome, or an existing `animations.json`; a §IX Motion suggestion activates none, yet its Morph candidates are authored as compatible direct-root groups with recorded partner ids |
| Missing state | A deck exported without both states cannot gain the motion by a flag | | Missing state | A deck exported without both states cannot gain the motion by a flag |
| Extra page | Adding a page is a §IX roster change and returns to Strategist | | Extra page | Adding a page is a §IX roster change and returns to Strategist |
| Endpoint id | Fix the first endpoint's direct-root `<g id>` when that page is drawn and record the intended partner id with `workflow_log.py` (never in `notes/total.md`, which is read verbatim by TTS); the next page reuses it, so no endpoint is renamed after both pages exist (a rename reruns the gate) | | Endpoint id | Fix the first endpoint's direct-root `<g id>` when that page is drawn and record the intended partner id with `workflow_log.py` (never in `notes/total.md`, which is read verbatim by TTS); the next page reuses it, so no endpoint is renamed after both pages exist (a rename reruns the gate) |
@@ -20,7 +20,7 @@ Conditional Executor authority for value-driven SVG geometry, plot-area markers,
Construct in this order: (1) resolve data domain, categories, units, baseline, scale, and any radius/color/bin mapping; (2) establish the plot frame, axes/grid or radial frame, and legend needed to decode them; (3) calculate marks from the values, including cumulative, derived, or hierarchical geometry; (4) add data labels, axis/category labels, annotations, units, source notes, and visible exceptions from the page contract; (5) apply project typography, palette, effects, and container treatment without changing the encoding. Construct in this order: (1) resolve data domain, categories, units, baseline, scale, and any radius/color/bin mapping; (2) establish the plot frame, axes/grid or radial frame, and legend needed to decode them; (3) calculate marks from the values, including cumulative, derived, or hierarchical geometry; (4) add data labels, axis/category labels, annotations, units, source notes, and visible exceptions from the page contract; (5) apply project typography, palette, effects, and container treatment without changing the encoding.
**Perceptual reading**: choose the least ambiguous presentation of the same data. Preserve source or semantic order when it carries meaning; otherwise sort for the page's comparison task. Prefer direct series labels when legible; keep legends, grid lines, and ticks only when they materially reduce lookup effort. Comparable panels share domain, scale, and category order unless a disclosed difference is the message. Magnitude bars and columns start from zero; interval marks keep their authoritative domain; any non-zero baseline or axis break is explicit and never exaggerates. A dual-axis chart is valid only when both series share the exact time/category domain and units and identities stay unambiguous; otherwise separate the views. **Perceptual reading**: choose the least ambiguous presentation of the same data. Preserve source or semantic order when it carries meaning; otherwise sort for the page's comparison task; a right-to-left deck sets `axes.category.reverse` so categories read the way the page does. Prefer direct series labels when legible; keep legends, grid lines, and ticks only when they materially reduce lookup effort. Comparable panels share domain, scale, and category order unless a disclosed difference is the message. Magnitude bars and columns start from zero; interval marks keep their authoritative domain; any non-zero baseline or axis break is explicit and never exaggerates. A dual-axis chart is valid only when both series share the exact time/category domain and units and identities stay unambiguous; otherwise separate the views.
**Reference — chart color and annotation conventions**: a monochromatic depth scheme reads cleaner than rainbow — the primary series in the full theme color, a comparison series in the same hue at about 60% alpha, baselines as a gray dashed line, the accent color only on the key data points; positive / warning / negative trends use the deck's green / yellow / red polarity roles. Place direct data labels at bar ends or beside points instead of a legend where they stay legible; annotate genuine inflection points in words ("policy change", "product launch"); mark an industry average or target as a gray dashed comparison baseline; keep units and precision consistent within one chart; add a muted source note where attribution or provenance matters. **Reference — chart color and annotation conventions**: a monochromatic depth scheme reads cleaner than rainbow — the primary series in the full theme color, a comparison series in the same hue at about 60% alpha, baselines as a gray dashed line, the accent color only on the key data points; positive / warning / negative trends use the deck's green / yellow / red polarity roles. Place direct data labels at bar ends or beside points instead of a legend where they stay legible; annotate genuine inflection points in words ("policy change", "product launch"); mark an industry average or target as a gray dashed comparison baseline; keep units and precision consistent within one chart; add a muted source note where attribution or provenance matters.
@@ -10,7 +10,7 @@ Conditional Executor authority for image status handling, placement, crop behavi
| Status | Handling | | Status | Handling |
|---|---| |---|---|
| `Existing` (user-provided) | Reference from `../images/` | | `Existing` (user-provided) | Reference from `../images/`; a manifest-backed file also loads [`executor-web-image.md`](./executor-web-image.md) |
| `Generated` (Image_Generator) | Reference from `../images/`; a manifest-backed file also loads [`executor-web-image.md`](./executor-web-image.md) | | `Generated` (Image_Generator) | Reference from `../images/`; a manifest-backed file also loads [`executor-web-image.md`](./executor-web-image.md) |
| `Sourced` (Image_Searcher) | Reference from `../images/`; read `image_sources.json` for attribution — load [`executor-web-image.md`](./executor-web-image.md) | | `Sourced` (Image_Searcher) | Reference from `../images/`; read `image_sources.json` for attribution — load [`executor-web-image.md`](./executor-web-image.md) |
| `Needs-Manual` | Default uses a placeholder until Step 7; Quick blocks every required row, file presence notwithstanding | | `Needs-Manual` | Default uses a placeholder until Step 7; Quick blocks every required row, file presence notwithstanding |
@@ -8,7 +8,7 @@ Conditional late-stage authority for generating or validating the complete speak
## 1. Complete Speaker-notes Document ## 1. Complete Speaker-notes Document
Write the whole deck to `notes/total.md` in one batch: `# <number>_<page_title>` per page, `---` between pages; only the heading is stripped before TTS. `notes_to_audio.py` reads the body verbatim, so write prose only — no list/bullet markup, stage markers, key-point labels, duration lines, or other metadata. Keep one language; spell out digits or symbols when literal TTS would sound wrong (Chinese "百分之六十八" rather than "68%"). Write the whole deck to `notes/total.md` in one batch: `# <number>_<page_title>` per page, `---` between pages; only the heading is stripped before TTS. `notes_to_audio.py` reads the body verbatim, so write prose only — no list/bullet markup, stage markers, key-point labels, duration lines, or other metadata. Keep one language unless the confirmed contract is bilingual — then every page keeps the same segment order (first language, blank line, second language); spell out digits or symbols when literal TTS would sound wrong (Chinese "百分之六十八" rather than "68%").
**Pre-SVG narration branch**: when `notes/total.md` already exists from a final/literal script or Quick direct narrated video, validate it instead of regenerating. Retain every word and segment of a final/literal script; agent-authored Quick narration may be repaired only for final-SVG inconsistency and before audio. A `# Slide <number>` heading remains valid until Generate Step 7.1 resolves the roster. **Pre-SVG narration branch**: when `notes/total.md` already exists from a final/literal script or Quick direct narrated video, validate it instead of regenerating. Retain every word and segment of a final/literal script; agent-authored Quick narration may be repaired only for final-SVG inconsistency and before audio. A `# Slide <number>` heading remains valid until Generate Step 7.1 resolves the roster.
@@ -4,7 +4,7 @@
Conditional Executor authority for inline attribution on web-sourced images and their prepared derivatives. Conditional Executor authority for inline attribution on web-sourced images and their prepared derivatives.
**Trigger**: any placed `Status: Sourced` image, or a placed derivative whose filename has a copied `image_sources.json` record; Quick uses the same manifest without interaction. **Trigger**: any placed `Status: Sourced` image, or a placed image or derivative whose filename has an `image_sources.json` record (copied web provenance, or a user-supplied asset registered with its licence terms); Quick uses the same manifest without interaction.
## 1. Inline Attribution for Sourced Images ## 1. Inline Attribution for Sourced Images
@@ -48,6 +48,6 @@ An all-`web` deck never reads `image-generator.md`, and vice versa.
## 4. Credits and Handoff ## 4. Credits and Handoff
License and attribution data live only in `project/images/image_sources.json` — never in `notes/*.md` (TTS would speak them), `total.md`, or SVG `<title>` / `<desc>` (stripped on export). A closing sources page may summarize them, but it never replaces the per-slide credit: Executor renders inline credits per slide under [`executor-web-image.md`](./executor-web-image.md) and [`image-searcher.md`](./image-searcher.md) §7. License and attribution data live only in `project/images/image_sources.json` — never in `notes/*.md` (TTS would speak them), `total.md`, or SVG `<title>` / `<desc>` (stripped on export). A `user` asset that arrives with author or licence terms (a credits file, caption, or user note) is registered there too — `filename`, `author`, `license_name`, `source_page_url`, `license_tier` (`attribution-required` for CC BY / CC BY-SA, `no-attribution` for CC0, public domain, or the user's own work), `attribution_text` — so the same per-slide credit rule and checker binding cover it. A closing sources page may summarize them, but it never replaces the per-slide credit: Executor renders inline credits per slide under [`executor-web-image.md`](./executor-web-image.md) and [`image-searcher.md`](./image-searcher.md) §7.
SVG authoring consumes `project/images/*.{jpg,png,webp}` and `image_sources.json`. Default Executor never invokes `image_gen.py` / `image_search.py` / `slice_images.py` / `image_treat.py` — missing material returns to Strategist-owned preparation; Quick finishes acquisition and derivation before authoring and neither acquires, derives, nor reselects while drawing. Completion: every row, file, manifest, and provenance record verified; Default proceeds to Executor, Quick exports only with validated evidence and usable statuses; report only blocking recovery. SVG authoring consumes `project/images/*.{jpg,png,webp}` and `image_sources.json`. Default Executor never invokes `image_gen.py` / `image_search.py` / `slice_images.py` / `image_treat.py` — missing material returns to Strategist-owned preparation; Quick finishes acquisition and derivation before authoring and neither acquires, derives, nor reselects while drawing. Completion: every row, file, manifest, and provenance record verified; Default proceeds to Executor, Quick exports only with validated evidence and usable statuses; report only blocking recovery.
@@ -240,7 +240,7 @@ Match facial detail, anatomy, texture, and realism to the rendering and the row'
**Hard rule — decide by editability, not model capability**: Layer 1 text can never be edited, corrected, searched, restyled, or reflowed. Text that is part of the artwork and stable — decorative lettering, a wordmark, a hand-lettered phrase, figure-internal identifiers — may be Layer 1. Anything that must stay exact, searchable, editable, or may be reworded is Layer 2, whatever `text_policy` says: authoritative titles, chrome, navigation, footer, bullets, captions, data values. Bake title-like wording only when the approved plan treats those exact characters as stable artwork. When the headline must stay editable, use Primitive D and overlay it. **Hard rule — decide by editability, not model capability**: Layer 1 text can never be edited, corrected, searched, restyled, or reflowed. Text that is part of the artwork and stable — decorative lettering, a wordmark, a hand-lettered phrase, figure-internal identifiers — may be Layer 1. Anything that must stay exact, searchable, editable, or may be reworded is Layer 2, whatever `text_policy` says: authoritative titles, chrome, navigation, footer, bullets, captions, data values. Bake title-like wording only when the approved plan treats those exact characters as stable artwork. When the headline must stay editable, use Primitive D and overlay it.
**Hard rule — never pre-judge by script or length**: never push text to SVG, shorten a headline, or downgrade `embedded` to `none` on the assumption that a script or long string "won't render"; a multi-word phrase or two-line lockup qualifies exactly as one word does. Name the exact characters literally; do not re-read the generated image to verify them. Exception — CJK lettering: when `text_policy: embedded` and the string contains CJK characters, one look at the generated file is allowed, answering only whether the characters match the approved string exactly; a mismatch regenerates that item, and the look never reopens selection or taste; after three regenerations that still miss the same character, stop — keep that mark as native text, record the fallback in the notes, and never explain it on the page. **Hard rule — never pre-judge by script or length**: never push text to SVG, shorten a headline, or downgrade `embedded` to `none` on the assumption that a script or long string "won't render"; a multi-word phrase or two-line lockup qualifies exactly as one word does. Name the exact characters literally; do not re-read the generated image to verify them. Exception — non-Latin lettering: when `text_policy: embedded` and the string contains CJK, Arabic, Indic, Thai, or other non-Latin characters, one look at the generated file is allowed, answering only whether the characters match the approved string exactly; a mismatch regenerates that item, and the look never reopens selection or taste; after three regenerations that still miss the same character, stop — keep that mark as native text, record the fallback in the notes, and never explain it on the page.
**Reference — controlled, deck-aligned artistic authorship** (high expression on user request or a confirmed direction): give the model the exact string, communication role, placement/background relationship, deck identity, relative weight, and desired energy; the rendering, semantic colors, mood, and page hierarchy define the envelope. Glyph-native expression carries identity through silhouette, stroke construction, internal material/texture, contour-bound depth and light, and composition. Literal topic illustrations or detached decoration compete with the glyph; a lettering-plus-illustration lockup needs an explicit request or confirmed direction. Within the treatment let the model combine or omit gesture, material, dimensionality, texture, lighting, and hierarchy — possibility space, not a recipe. Never flatten the art to ease extraction (§4.3's gates protect delivery); when fit is uncertain use the lower density; keep a multi-line lockup as one element when its hierarchy is part of the art. **Reference — controlled, deck-aligned artistic authorship** (high expression on user request or a confirmed direction): give the model the exact string, communication role, placement/background relationship, deck identity, relative weight, and desired energy; the rendering, semantic colors, mood, and page hierarchy define the envelope. Glyph-native expression carries identity through silhouette, stroke construction, internal material/texture, contour-bound depth and light, and composition. Literal topic illustrations or detached decoration compete with the glyph; a lettering-plus-illustration lockup needs an explicit request or confirmed direction. Within the treatment let the model combine or omit gesture, material, dimensionality, texture, lighting, and hierarchy — possibility space, not a recipe. Never flatten the art to ease extraction (§4.3's gates protect delivery); when fit is uncertain use the lower density; keep a multi-line lockup as one element when its hierarchy is part of the art.
@@ -34,7 +34,7 @@ python3 ${SKILL_DIR}/scripts/stamp_native_fallbacks.py "<svg-file-or-directory>"
The hash is a synchronization receipt, not proof of semantic equivalence; never stamp stale JSON to satisfy validation. The hash is a synchronization receipt, not proof of semantic equivalence; never stamp stale JSON to satisfy validation.
**Hard rule — activation is the opt-in**: a marker only declares eligibility. Normal `svg_to_pptx.py` converts the fallback children into editable DrawingML shapes; `--native-charts-and-tables` emits the PowerPoint Chart/Table object and discards the fallback children, so everything the fallback shows lives in the payload — data, point emphasis, line/area treatment, spacing, chrome, and companion text — and the final checker's parity findings block that export until they do. A convertible marker stays even when a detail has no native field: it becomes companion text or an unmarked sibling object, never a simpler drawing. **Hard rule — activation is the opt-in**: a marker only declares eligibility. Normal `svg_to_pptx.py` converts the fallback children into editable DrawingML shapes (a structured `chart` / `table` placeholder slot has no shape path and needs the flag); `--native-charts-and-tables` emits the PowerPoint Chart/Table object and discards the fallback children, so everything the fallback shows lives in the payload — data, point emphasis, line/area treatment, spacing, chrome, and companion text — and the final checker's parity findings block that export until they do. A convertible marker stays even when a detail has no native field: it becomes companion text or an unmarked sibling object, never a simpler drawing.
| Replacement marker | Native output | Required metadata | | Replacement marker | Native output | Required metadata |
|---|---|---| |---|---|---|
@@ -67,4 +67,4 @@ Formula handling is not a user-confirmed policy, image resource, manifest, or `s
**Hard rule — repair LaTeX upstream**: unsupported source or an invalid marker blocks the page. Rewrite within the profile without changing the planned mathematics, or return it to the content owner. Never substitute a PNG, flatten structural math into ordinary text, hand-write OMML, or leave raw LaTeX visible. **Hard rule — repair LaTeX upstream**: unsupported source or an invalid marker blocks the page. Rewrite within the profile without changing the planned mathematics, or return it to the content owner. Never substitute a PNG, flatten structural math into ordinary text, hand-write OMML, or leave raw LaTeX visible.
**Validation**: the early/final SVG checker validates every marker, compiles its LaTeX, and applies the shared native-height envelope to page/module text bounds; native export repeats validation and uses that envelope for the generated text frame. Output is standard editable Office Math for PowerPoint 2010+; WPS, Keynote, and LibreOffice receive no embedded fallback and are outside the rendering/editability contract. **Validation**: planned LaTeX can be compile-checked before authoring ([`svg-pipeline.md`](../scripts/docs/svg-pipeline.md) § svg_to_pptx); the early/final SVG checker validates every marker, compiles its LaTeX, and applies the shared native-height envelope to page/module text bounds; native export repeats validation and uses that envelope for the generated text frame. Output is standard editable Office Math for PowerPoint 2010+; WPS, Keynote, and LibreOffice receive no embedded fallback and are outside the rendering/editability contract.
@@ -117,7 +117,7 @@ Operand count, preset choice, geometry, paint, rotation, and grouping come from
## 3. Fragment Generation ## 3. Fragment Generation
`render` emits one object; `render-batch --input -` emits several already-selected objects for one page or template construction from a JSON array with the `render` fields (required `preset`, `id`, `frame` `[x, y, w, h]`; optional `object_kind`, `name`, `fill`, `fill_opacity`, `stroke`, `stroke_width`, `stroke_opacity`, `stroke_linecap`, `stroke_linejoin`, `filter_id`, `adjustments` such as `{"adj": "val 42000"}` — the object form of `render --adjust NAME=FORMULA`, keyed by guide name). Paint comes from the page context with `spec_lock.md` roles as anchors (create-template: from the confirmed brief and template Design Spec); mirror/preserve input keeps source paint. The batch is transient input, never a project resource or multi-page plan, and never chooses layout. `render` emits one object; `render-batch --input -` emits several already-selected objects for one page or template construction from a JSON array with the `render` fields (required `preset`, `id`, `frame` `[x, y, w, h]`; optional `object_kind`, `name`, `fill`, `fill_opacity`, `stroke`, `stroke_width`, `stroke_opacity`, `stroke_linecap`, `stroke_linejoin`, `filter_id`, `adjustments` such as `{"adj": "val 42000"}` — the object form of `render --adjust NAME=FORMULA`, keyed by guide name; a list of `NAME=FORMULA` strings is accepted too). Paint comes from the page context with `spec_lock.md` roles as anchors (create-template: from the confirmed brief and template Design Spec); mirror/preserve input keeps source paint. The batch is transient input, never a project resource or multi-page plan, and never chooses layout.
--- ---
@@ -163,7 +163,7 @@ python3 skills/ppt-master/scripts/visualization_recall.py validate \
**Family selection**: user/template typography is authoritative. Delivery target: an explicit user/template target first, otherwise Windows Microsoft PowerPoint (owner: [`shared-standards-core.md`](./shared-standards-core.md) §4.1) — the authoring host's installed fonts never select a face; name concrete faces installed or approved on that target; at most four families; a brand/web face leads only after user-confirmed installation, otherwise export a safe face and keep it as a Design Spec reference (fonts are not embedded; CSS tails are preview aids, not PowerPoint fallbacks). Avoid near-equivalent splits (YaHei↔PingFang, SimSun↔Songti, Arial↔Helvetica↔Segoe UI, Times↔Times New Roman). Fonts in one deck form contrast (different family, weight, or proportion) or concord (one family throughout); across the direction set include both a concord and a contrast pairing unless the user or template fixes the stack, and never default to title = body without a reason. **Family selection**: user/template typography is authoritative. Delivery target: an explicit user/template target first, otherwise Windows Microsoft PowerPoint (owner: [`shared-standards-core.md`](./shared-standards-core.md) §4.1) — the authoring host's installed fonts never select a face; name concrete faces installed or approved on that target; at most four families; a brand/web face leads only after user-confirmed installation, otherwise export a safe face and keep it as a Design Spec reference (fonts are not embedded; CSS tails are preview aids, not PowerPoint fallbacks). Avoid near-equivalent splits (YaHei↔PingFang, SimSun↔Songti, Arial↔Helvetica↔Segoe UI, Times↔Times New Roman). Fonts in one deck form contrast (different family, weight, or proportion) or concord (one family throughout); across the direction set include both a concord and a contrast pairing unless the user or template fixes the stack, and never default to title = body without a reason.
**Reference — PPT-safe faces (recall, not a whitelist; one concrete named face per script — a stack carries at most one Latin face and one CJK face and never a fallback list of alternatives, because export takes the first named face of each script)**: CJK sans `Microsoft YaHei` / `SimHei`, CJK serif `SimSun` / `FangSong` / `KaiTi` (their macOS counterparts `PingFang SC` / `Heiti SC` / `Songti SC` are preview aliases, never the named face), Traditional Chinese sans `Microsoft JhengHei`, serif `PMingLiU` / `DFKai-SB`, Japanese sans `Yu Gothic` / `Meiryo`, serif `Yu Mincho`, Korean sans `Malgun Gothic`, serif `Batang` (text on another locale's CJK face takes that locale's glyph forms), Arabic `Segoe UI` / `Tahoma` / `Times New Roman` (one face carries both the Latin and Arabic text), Latin sans `Arial` / `Calibri` / `Segoe UI` / `Verdana` / `Trebuchet MS`, Latin serif `Times New Roman` / `Georgia` / `Cambria` / `Palatino` / `Garamond` (`Georgia` sets old-style figures whose digits sit at different heights — a hero-number or data role needs lining figures: `Times New Roman`, `Cambria`, or any listed sans or mono), mono `Consolas` / `Courier New`, display `Impact` / `Arial Black`. Let the locked style's character pick the axis and lead the title — `Microsoft YaHei` / `Arial` are the neutral members, never the automatic lead; a neutral sans title where the style asks for character is the failure to avoid. Non-pre-installed directions — retro/pixel Press Start 2P / VT323, rounded Nunito / Quicksand / OPPO Sans (safe substitute `Trebuchet MS` / `Verdana`), modern web Inter / HarmonyOS Sans / Source Han, calligraphic 隶书 / 华文行楷 / 华文新魏 (safe substitute `KaiTi` / `FangSong`, titles only), brand faces — need target installation or stay Design Spec references. **Reference — PPT-safe faces (recall, not a whitelist; one concrete named face per script — a stack carries at most one Latin face and one CJK face and never a fallback list of alternatives, because export takes the first named face of each script)**: CJK sans `Microsoft YaHei` / `SimHei`, CJK serif `SimSun` / `FangSong` / `KaiTi` (their macOS counterparts `PingFang SC` / `Heiti SC` / `Songti SC` are preview aliases, never the named face; a CJK deck that also sets Latin lines pairs both in one stack — `SimSun, Cambria` — so the theme's latin and ea slots differ), Traditional Chinese sans `Microsoft JhengHei`, serif `PMingLiU` / `DFKai-SB`, Japanese sans `Yu Gothic` / `Meiryo`, serif `Yu Mincho`, Korean sans `Malgun Gothic`, serif `Batang` (text on another locale's CJK face takes that locale's glyph forms), Arabic `Segoe UI` / `Tahoma` / `Times New Roman` (one face carries both the Latin and Arabic text), Hebrew `Segoe UI` / `Arial` / `David` (one face carries both the Latin and Hebrew text), Cyrillic and Greek ride the same Latin faces (`Arial` / `Times New Roman` / `Segoe UI` / `Calibri` / `Georgia`), Devanagari and other Indic scripts `Nirmala UI` / `Mangal`, Thai `Leelawadee UI` / `Tahoma`, Latin sans `Arial` / `Calibri` / `Segoe UI` / `Verdana` / `Trebuchet MS`, Latin serif `Times New Roman` / `Georgia` / `Cambria` / `Palatino` / `Garamond` (`Georgia` sets old-style figures whose digits sit at different heights — a hero-number or data role needs lining figures: `Times New Roman`, `Cambria`, or any listed sans or mono), mono `Consolas` / `Courier New`, math `Cambria Math` (formula preview runs only), display `Impact` / `Arial Black`. Let the locked style's character pick the axis and lead the title — `Microsoft YaHei` / `Arial` are the neutral members, never the automatic lead; a neutral sans title where the style asks for character is the failure to avoid. Non-pre-installed directions — retro/pixel Press Start 2P / VT323, rounded Nunito / Quicksand / OPPO Sans (safe substitute `Trebuchet MS` / `Verdana`), modern web Inter / HarmonyOS Sans / Source Han, calligraphic 隶书 / 华文行楷 / 华文新魏 (safe substitute `KaiTi` / `FangSong`, titles only), brand faces — need target installation or stay Design Spec references.
**Role extension after confirmation**: while composing the roster, add a lowercase snake_case role with an exact stack only for a recurring role that materially needs a different family (`annotation`, `footer`, `footnote`, `data`, `emphasis`, `quote`, `code`), coherent with the confirmed heading/body system and locked style; one-off garnish stays omitted and Default names any added role in one compact `Role rationale` line in §IV. **Role extension after confirmation**: while composing the roster, add a lowercase snake_case role with an exact stack only for a recurring role that materially needs a different family (`annotation`, `footer`, `footnote`, `data`, `emphasis`, `quote`, `code`), coherent with the confirmed heading/body system and locked style; one-off garnish stays omitted and Default names any added role in one compact `Role rationale` line in §IV.
@@ -177,6 +177,7 @@ python3 skills/ppt-master/scripts/visualization_recall.py validate \
| Subtitle | 1.2–1.5× | | Subtitle | 1.2–1.5× |
| Lead / subheading | 1.1–1.4× | | Lead / subheading | 1.1–1.4× |
| Body | 1× | | Body | 1× |
| Second-language line on a bilingual page | 0.75–0.9× |
| Annotation | 0.7–0.85× | | Annotation | 0.7–0.85× |
| Footnote / page number | 0.5–0.65× | | Footnote / page number | 0.5–0.65× |
@@ -21,7 +21,7 @@ still be the best result.
**When to run**: run `preset_shape_svg.py describe <name> --compact` only when **When to run**: run `preset_shape_svg.py describe <name> --compact` only when
an exact candidate needs objective adjustment, connector, path, an exact candidate needs objective adjustment, connector, path,
connection-site, or text rectangle facts (`--frame X Y W H` adds the text rectangle in page px). Authoring syntax and fragment connection-site, or text rectangle facts (`--frame X Y W H` adds the text rectangle in page px; ribbon, chevron, arrow, and callout bodies hold far less text than their frame, so read that rectangle before sizing a label). Authoring syntax and fragment
contracts remain in contracts remain in
[`native-shape-authoring.md`](./native-shape-authoring.md). [`native-shape-authoring.md`](./native-shape-authoring.md).
@@ -245,7 +245,7 @@ Serial post-processing and export belong to [`generate-pptx.md`](../workflows/ge
| Typography roles | Use the fewest semantic text roles that preserve hierarchy, and make their differences legible at slide-thumbnail scale. Consolidate near-neighbor sizes that serve the same role; otherwise distinguish roles through a deliberate combination of size, weight, color, position, and surrounding space. | | Typography roles | Use the fewest semantic text roles that preserve hierarchy, and make their differences legible at slide-thumbnail scale. Consolidate near-neighbor sizes that serve the same role; otherwise distinguish roles through a deliberate combination of size, weight, color, position, and surrounding space. |
| Viewing-distance legibility | Resolve delivery context and viewing distance before fixing density and type scale. Preserve necessary text at a readable scale by applying only actions the active route's content and page invariants permit: restructure, shorten, split, or reflow. If none is permitted, surface the unresolved fit instead of silently miniaturizing it. Do not turn this into one universal font-size floor: captions and metadata may be smaller when their role and context remain legible. | | Viewing-distance legibility | Resolve delivery context and viewing distance before fixing density and type scale. Preserve necessary text at a readable scale by applying only actions the active route's content and page invariants permit: restructure, shorten, split, or reflow. If none is permitted, surface the unresolved fit instead of silently miniaturizing it. Do not turn this into one universal font-size floor: captions and metadata may be smaller when their role and context remain legible. |
| Contrast and semantic encoding | Within the active profile's fidelity boundary, keep meaning-bearing text distinguishable from its actual background. For newly authored distinctions, combine luminance, weight, scale, shape, position, or explicit labeling; color may reinforce meaning but never carry a required distinction alone. When fidelity requires preserving source-only color encoding, reproduce it rather than inventing a cue. Reserve lower-contrast treatment for genuinely secondary metadata that remains legible. | | Contrast and semantic encoding | Within the active profile's fidelity boundary, keep meaning-bearing text distinguishable from its actual background. For newly authored distinctions, combine luminance, weight, scale, shape, position, or explicit labeling; color may reinforce meaning but never carry a required distinction alone. When fidelity requires preserving source-only color encoding, reproduce it rather than inventing a cue. Reserve lower-contrast treatment for genuinely secondary metadata that remains legible. |
| Natural wrapping | Break at semantic phrase or punctuation boundaries where possible. Reflow the text frame or adjust neighboring geometry before using any permitted local size reduction. Let the final line run naturally shorter; avoid mechanically equal lines or a stranded single-character / single-word line when an earlier natural break preserves meaning. | | Natural wrapping | Break at semantic phrase or punctuation boundaries where possible. An unspaced script (Thai, Lao, Khmer, Myanmar) breaks only at word boundaries the author chooses — never by inserting spaces or zero-width characters — and keeps native table cells single-line; it and any script with stacked marks (Vietnamese diacritics included) take leading toward the upper band. A right-to-left deck writes each line either in one direction or starting and ending with right-to-left letters — digits inside are safe, a leading digit, trailing neutral sign, or inline Latin run reorders between the left-to-right preview and PowerPoint. Reflow the text frame or adjust neighboring geometry before using any permitted local size reduction. Let the final line run naturally shorter; avoid mechanically equal lines or a stranded single-character / single-word line when an earlier natural break preserves meaning. |
| Content field | Establish the usable body frame before placing modules. Divide it into one or a small set of macro-regions from information weight and reading order: use unequal weight when the information differs, while true peers may share equal weight. Give each region its own local axes / micro-grid while retaining only the cross-region anchors the composition needs. On a dense page, let the planned content system organize that frame; create breathing room through gutters, module spacing, and intentional voids between semantic clusters. Unorganized residual space that leaves content stranded in one part of the frame is leftover blank, not negative space. | | Content field | Establish the usable body frame before placing modules. Divide it into one or a small set of macro-regions from information weight and reading order: use unequal weight when the information differs, while true peers may share equal weight. Give each region its own local axes / micro-grid while retaining only the cross-region anchors the composition needs. On a dense page, let the planned content system organize that frame; create breathing room through gutters, module spacing, and intentional voids between semantic clusters. Unorganized residual space that leaves content stranded in one part of the frame is leftover blank, not negative space. |
| Alignment and proximity | Establish shared axes from the current composition. Align related titles, copy, labels, images, and diagram nodes to those edges, centers, or baselines; group related elements more tightly than unrelated groups so spacing carries hierarchy. Break an axis only when the offset performs hierarchy, direction, or tension. | | Alignment and proximity | Establish shared axes from the current composition. Align related titles, copy, labels, images, and diagram nodes to those edges, centers, or baselines; group related elements more tightly than unrelated groups so spacing carries hierarchy. Break an axis only when the offset performs hierarchy, direction, or tension. |
| Visual weight | Judge weight from area, darkness, saturation, density, stroke, image detail, and elevation together. Distribute it to support the focal path; symmetry is optional, and deliberate imbalance may create direction. | | Visual weight | Judge weight from area, darkness, saturation, density, stroke, image detail, and elevation together. Distribute it to support the focal path; symmetry is optional, and deliberate imbalance may create direction. |
@@ -38,7 +38,7 @@ For specialized or regulated paper-figure subjects keep the prompt depth of [`im
**References describe visual intent**: AI rows carry subject + intent + composition without repeating rendering or HEX; web rows carry the exact subject, view/mood, focal/quiet region, crop safety, and positive quality cues, from which Image_Searcher derives a separate short provider query (complete entity names or disambiguation may use more words). When page use depends on stable composition, put subject/quiet zones, boundary or direction, intended overlap/seam, and approximate share in `Reference` or the §IX block, not only in `Image pattern`. **References describe visual intent**: AI rows carry subject + intent + composition without repeating rendering or HEX; web rows carry the exact subject, view/mood, focal/quiet region, crop safety, and positive quality cues, from which Image_Searcher derives a separate short provider query (complete entity names or disambiguation may use more words). When page use depends on stable composition, put subject/quiet zones, boundary or direction, intended overlap/seam, and approximate share in `Reference` or the §IX block, not only in `Image pattern`.
**Prepared-user fast path**: for initial imported or user-supplied assets confirmed as `provided`, copy the exact basename, derive `Dimensions` / `Ratio` from the row's EXIF-corrected `Width` / `Height` / `AspectRatio` in the latest `analysis/image_analysis.csv` (`SourceDisplayRatio` is source context, not the crop ratio), drop source-side directories, set `Acquire Via: user` and `Status: Existing`, and decide the rest normally; existing §VIII, lock, or provenance records override this inference, and assets declared `ai`, `web`, `slice`, or manual keep that provenance wherever they sit. **Prepared-user fast path**: for initial imported or user-supplied assets confirmed as `provided`, copy the exact basename, derive `Dimensions` / `Ratio` from the row's EXIF-corrected `Width` / `Height` / `AspectRatio` in the latest `analysis/image_analysis.csv` (`SourceDisplayRatio` is source context, not the crop ratio), drop source-side directories, set `Acquire Via: user` and `Status: Existing`, register any supplied licence terms in `image_sources.json` ([`image-base.md`](./image-base.md) §4), and decide the rest normally; existing §VIII, lock, or provenance records override this inference, and assets declared `ai`, `web`, `slice`, or manual keep that provenance wherever they sit.
**Image pattern** (one non-empty value per placed row, required by the checker): preferred expression in ordinary words, not locked geometry (an id from [`image-layout-patterns.md`](./image-layout-patterns.md) only when already known; Executor loads that catalog); Executor adjusts it freely as a Reference while keeping resource identity, must-use status, crop/content, and explicit constraints (layout-only changes need no upstream rewrite). A useful entry names the job the image does for the content or the page's shapes — the catalog is recall, an uncatalogued technique answers a job equally, plain split and full bleed stay valid, and a `no-crop` or supporting row keeps one concise suggestion. Choose narrative intent before dimensions, then derive `Dimensions` / `Ratio` from the intended region of the confirmed canvas (placement geometry itself is Executor's [`image-layout-spec.md`](./image-layout-spec.md)); a cutout, blurred crop, or desaturated copy requires that prepared asset. **Image pattern** (one non-empty value per placed row, required by the checker): preferred expression in ordinary words, not locked geometry (an id from [`image-layout-patterns.md`](./image-layout-patterns.md) only when already known; Executor loads that catalog); Executor adjusts it freely as a Reference while keeping resource identity, must-use status, crop/content, and explicit constraints (layout-only changes need no upstream rewrite). A useful entry names the job the image does for the content or the page's shapes — the catalog is recall, an uncatalogued technique answers a job equally, plain split and full bleed stay valid, and a `no-crop` or supporting row keeps one concise suggestion. Choose narrative intent before dimensions, then derive `Dimensions` / `Ratio` from the intended region of the confirmed canvas (placement geometry itself is Executor's [`image-layout-spec.md`](./image-layout-spec.md)); a cutout, blurred crop, or desaturated copy requires that prepared asset.
Loaded 100 of 185 files, more files were not shown because too many files have changed in this diff. Show more