chore: 1.0.0 release
@@ -0,0 +1,15 @@
|
|||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# macOS metadata
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Packaged artifacts
|
||||||
|
*.tgz
|
||||||
|
*.zip
|
||||||
|
|
||||||
|
# Local skill install metadata
|
||||||
|
_user_meta.json
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Binaryify Zhuang
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
# open-kimi-ppt-skills
|
||||||
|
|
||||||
|
[简体中文](README.md) | [English](README_EN.md)
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/open-kimi-ppt-skills)
|
||||||
|
[](https://nodejs.org)
|
||||||
|
|
||||||
|
逆向 Kimi Slides 实现的非官方演示文稿 Skill:让 AI Coding Agent 能够创建、编辑、复刻、读取并导出 PPT/PPTX。**每次生成默认同时交付两份成果:可继续编辑的 PPTD 项目和开箱即用的 PPTX 成品**(自动嵌入字体、写入淡入淡出切换动画),并提供本地在线 PPTD 编辑器支持随时手动导出。支持 Codex、Claude Code、Cursor、CodeBuddy 等任何兼容 SKILL.md 规范的 Agent。
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> 本项目通过逆向分析 Kimi Slides Skill、PPTD 格式以及公开网页编辑器的前端行为与通信协议实现,并非 Kimi 或 Moonshot AI 的官方项目,也未获得其认可或支持。项目依赖的公开前端资源和兼容协议可能随 Kimi 更新而失效,仅供学习与研究使用。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- **PPTD 生成**:让 Agent 生成完整、可继续编辑的 PPTD 项目,支持从零创作、风格迁移、模板复用、图片/PDF 复刻。
|
||||||
|
- **PPTX 生成**:默认同步生成 PPTX 成品,自动嵌入字体并写入淡入淡出切换动画。
|
||||||
|
- **视觉质检**:多模态模型在导出 PPTX 前自动导出整份页面图片、拼接总览图逐项核查(变形、遮挡、出界、对比度、排版、文字溢出),问题页面修复后复检,直至全部通过。
|
||||||
|
- **在线编辑**:通过浏览器查看和编辑本地 PPTD 项目,自动保存,可配置页面切换动画。
|
||||||
|
- **手动导出**:在编辑器中随时手动导出 PPTX。
|
||||||
|
- **格式互转**:将现有 PPTX 转换为 PPTD 后继续修改。
|
||||||
|
- **安全可控**:本地编辑仅在用户明确授权的项目目录内读写文件。
|
||||||
|
|
||||||
|
## 为什么选 open-kimi-ppt
|
||||||
|
|
||||||
|
常见 PPT Skill 大致分三类:用代码库直接拼 OOXML / pptxgenjs、整页生成图片再塞进 PPTX、或输出网页 HTML 翻页。open-kimi-ppt 走的是 **PPTD 中间层 + 真实可编辑 PPTX** 路线,兼顾 Agent 好写、人好看、PowerPoint 能改。
|
||||||
|
|
||||||
|
| | open-kimi-ppt | 代码拼 PPTX(如 pptxgenjs) | 整页图片 PPT | 网页 HTML PPT |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 交付物 | PPTD 项目 + PPTX | 多为仅 PPTX | 多为仅 PPTX | 单文件 HTML |
|
||||||
|
| Agent 友好度 | YAML 逐页描述,结构清晰 | 坐标/API 细节多,易排版翻车 | 依赖出图模型与提示词 | HTML/CSS 模板约束强 |
|
||||||
|
| PowerPoint 可编辑 | 文本、形状、图片可继续改 | 可编辑,但难二次精修 | 整页位图,难改字 | 不是原生 PPTX |
|
||||||
|
| 视觉质量 | 真实版式 + 导出前多模态质检 | 依赖 Agent 手调布局 | 画面统一,偏海报感 | 动效强,适合演示分享 |
|
||||||
|
| 二次编辑 | 浏览器可视化编辑 + 自动保存 | 主要靠改代码重导出 | 基本需重新出图 | 改 HTML 源码 |
|
||||||
|
| 适用场景 | 要交可改的正式 PPTX,又要好看 | 结构化汇报、模板填充 | 视觉统一的海报风讲稿 | 浏览器内演讲 / 发布会 |
|
||||||
|
|
||||||
|
更具体地说,相对其他方案的优势是:
|
||||||
|
|
||||||
|
1. **中间格式为 Agent 设计**:PPTD 用 YAML 描述主题、布局与元素,比直接写 OOXML / pptxgenjs 更稳,也比「整页渲一张图」更可局部修改。
|
||||||
|
2. **默认同交两份成果**:可继续迭代的 PPTD 项目 + 开箱即用的 PPTX(嵌字体、淡入淡出切换),不是只给半成品。
|
||||||
|
3. **PPTX 真能改**:导出后文本框、形状仍可在 PowerPoint / WPS 里编辑,不像图片型 PPT 只能当海报。
|
||||||
|
4. **有本地可视化编辑器**:浏览器里预览、微调、配切换动画并手动再导出,不需要每次都让 Agent 重跑全流程。
|
||||||
|
5. **导出前强制视觉质检**:整页截图 + 总览图检查遮挡、出界、对比度、溢出等问题,修完再出 PPTX。
|
||||||
|
6. **不绑官方模型,成本更低**:相对官方 Kimi Slides,你可以在任意兼容 Agent 里使用 DeepSeek 等低成本模型;即便模型不支持多模态,只要按 PPTD 规范生成,也能做出不错的成品(有多模态时再做视觉质检会更稳)。
|
||||||
|
|
||||||
|
[](docs/images/example-deepseek-liquid-glass.png)
|
||||||
|
|
||||||
|
*上图:在 CodeBuddy / WorkBuddy 中使用 DeepSeek-V4-Flash 生成 Apple Liquid Glass 风格 PPT 的实际效果。*
|
||||||
|
|
||||||
|
### 关于风格与主题
|
||||||
|
|
||||||
|
本 Skill **不预设固定主题或模板**。风格由你决定。
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> **强烈建议在 Prompt 里写明 PPT 风格,或直接附上参考 PPT / PPTX 模板。** 有风格约束或模板参照时,出品会明显更好、更稳定;只给主题不给风格时,Agent 会自行发挥,效果容易波动。
|
||||||
|
|
||||||
|
常见两种用法:
|
||||||
|
|
||||||
|
1. **在 Prompt 里描述风格**:例如「深色科技风」「杂志排版」「苹果 liquid glass」「极简留白 + 大字报」等;
|
||||||
|
2. **提供参考模板**:上传现有 PPT / PPTX / 截图,让 Agent 迁移配色、版式与风格。
|
||||||
|
|
||||||
|
两者可以一起用:先给模板定调,再用一句话补充本次要强化的风格。
|
||||||
|
|
||||||
|
## 界面预览
|
||||||
|
|
||||||
|
| 在线编辑 PPTD | 导出 PPTX |
|
||||||
|
| :---: | :---: |
|
||||||
|
| [](docs/images/editor-overview.png) | [](docs/images/export-pptx.png) |
|
||||||
|
|
||||||
|
## 什么是 PPTD
|
||||||
|
|
||||||
|
PPTD 是一种基于 YAML 的演示文稿 DSL,是 OOXML 之上的简化抽象层:保留主题、页面布局、元素位置等核心信息,去除了 Master 等复杂嵌套,每页自包含、所见即所得。完整的格式定义见 [reference/pptd.md](skills/open-kimi-ppt/reference/pptd.md)。
|
||||||
|
|
||||||
|
一个完整的 PPTD 项目目录结构如下:
|
||||||
|
|
||||||
|
```text
|
||||||
|
deck/
|
||||||
|
deck.pptd # 清单文件
|
||||||
|
pages/ # 每页一个 .page 文件
|
||||||
|
media/ # 本地媒体资源(如有)
|
||||||
|
deck.pptx # 默认同步生成的 PPTX 成品
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
需要 Node.js 18 或更高版本。也可以直接对 AI 说:`帮我从 github 安装 open-kimi-ppt skills`,或者说:`帮我安装 https://github.com/Binaryify/open-kimi-ppt-skill`。
|
||||||
|
|
||||||
|
直接通过 npx 安装:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx open-kimi-ppt-skills install
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以先全局安装 CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --global open-kimi-ppt-skills
|
||||||
|
open-kimi-ppt-skills install
|
||||||
|
```
|
||||||
|
|
||||||
|
默认安装到跨 Agent 的通用目录 `~/.agents/skills/open-kimi-ppt`,支持该目录的 Agent 可直接发现。如果你的 Agent 使用专属 Skills 目录,可通过 `--target` 指定:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Codex
|
||||||
|
open-kimi-ppt-skills install --target ~/.codex/skills
|
||||||
|
|
||||||
|
# Claude Code
|
||||||
|
open-kimi-ppt-skills install --target ~/.claude/skills
|
||||||
|
|
||||||
|
# Cursor
|
||||||
|
open-kimi-ppt-skills install --target ~/.cursor/skills
|
||||||
|
|
||||||
|
# CodeBuddy
|
||||||
|
open-kimi-ppt-skills install --target ~/.codebuddy/skills
|
||||||
|
```
|
||||||
|
|
||||||
|
### 更新
|
||||||
|
|
||||||
|
Skill 有更新时,用 `--force` 覆盖本地已安装版本即可:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx open-kimi-ppt-skills@latest install --force
|
||||||
|
```
|
||||||
|
|
||||||
|
若安装时指定过 `--target`,更新时带上相同路径:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx open-kimi-ppt-skills@latest install --force --target ~/.claude/skills
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以直接对 AI 说:`帮我更新 open-kimi-ppt skill`。更新只替换 Skill 文件,不会影响你已生成的 PPTD / PPTX 项目。
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
### 让 Agent 生成 PPT
|
||||||
|
|
||||||
|
安装完成后,直接向 Agent 描述需求即可。**默认交付物始终是两份**:完整的 PPTD 项目目录(可继续编辑)和对应的 PPTX 成品文件;只有明确要求只输出 PPTD 时才会跳过 PPTX 生成。
|
||||||
|
|
||||||
|
为了更稳定的出品,Prompt 里最好带上风格(如「深色产品发布风」),或附上参考 PPT 模板;只写主题、不给风格时效果更容易波动。
|
||||||
|
|
||||||
|
#### Prompt 示例
|
||||||
|
|
||||||
|
**示例 1:小米 YU7(约 8 页,图片作背景)**
|
||||||
|
|
||||||
|
```text
|
||||||
|
使用 open-kimi-ppt 做一个介绍小米 yu7的 PPT,要求图片做背景,素材从网上找,8 页左右
|
||||||
|
```
|
||||||
|
|
||||||
|
| 在线编辑 PPTD | 导出 PPTX |
|
||||||
|
| :---: | :---: |
|
||||||
|
| [](docs/images/example-yu7-editor.png) | [](docs/images/example-yu7-pptx.png) |
|
||||||
|
|
||||||
|
**示例 2:DJI Pocket 4(图片作背景)**
|
||||||
|
|
||||||
|
```text
|
||||||
|
使用 open-kimi-ppt 帮我生成DJI Pocket4 的 PPT,要求图片做背景,素材从网上找
|
||||||
|
```
|
||||||
|
|
||||||
|
| 在线编辑 PPTD | 导出 PPTX |
|
||||||
|
| :---: | :---: |
|
||||||
|
| [](docs/images/example-dji-pocket4-editor.png) | [](docs/images/example-dji-pocket4-pptx.png) |
|
||||||
|
|
||||||
|
### 在线编辑与手动导出
|
||||||
|
|
||||||
|
启动本地编辑器:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx open-kimi-ppt-skills serve
|
||||||
|
```
|
||||||
|
|
||||||
|
然后打开 <http://127.0.0.1:55173/>,选择包含 `.pptd` 清单、`pages/` 和 `media/` 的完整项目文件夹,即可在浏览器中查看、编辑项目并导出 PPTX。仓库自带的 [example/dji-pocket4](example/dji-pocket4) 是一个完整的 18 页示例项目,可直接打开体验。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动后自动打开浏览器
|
||||||
|
npx open-kimi-ppt-skills serve --open
|
||||||
|
|
||||||
|
# 使用其他端口
|
||||||
|
npx open-kimi-ppt-skills serve --port 56000
|
||||||
|
```
|
||||||
|
|
||||||
|
可写目录需要使用支持 File System Access API 的 Chromium 系浏览器;其他浏览器会回退为只读文件夹上传。按 `Ctrl+C` 停止服务。
|
||||||
|
|
||||||
|
## 工作原理与安全边界
|
||||||
|
|
||||||
|
- CLI 只在 `127.0.0.1` 启动静态文件服务,不会监听局域网地址。
|
||||||
|
- 浏览器只在用户主动授权后读取完整 PPTD 项目目录。
|
||||||
|
- 保存回调只允许修改 `.pptd` 和 `.page` 文件,并拒绝绝对路径与 `..` 路径越界。
|
||||||
|
- PPTD 内容由本地宿主交给公开的 Kimi 网页编辑器处理;远程图片、字体和编辑器资源仍可能从对应服务器加载。
|
||||||
|
- 本项目不会提供或注入 Kimi 登录令牌,也不会访问用户的 Kimi 私有文稿。
|
||||||
|
|
||||||
|
## 兼容性说明
|
||||||
|
|
||||||
|
这是针对当前公开实现的兼容宿主,不是稳定的官方 SDK。Kimi 更新前端资源哈希、PPTD 格式或 iframe/RPC 协议后,本项目可能需要同步升级。成功生成 PPTX 也不代表 PowerPoint、WPS 和 Keynote 对所有动画效果都能完全一致地播放。
|
||||||
|
|
||||||
|
## 本地开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --global .
|
||||||
|
npm test
|
||||||
|
npm run pack:check
|
||||||
|
```
|
||||||
|
|
||||||
|
## 声明
|
||||||
|
|
||||||
|
Kimi、Kimi Slides 及相关商标归其权利人所有。
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
# open-kimi-ppt-skills
|
||||||
|
|
||||||
|
[简体中文](README.md) | [English](README_EN.md)
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/open-kimi-ppt-skills)
|
||||||
|
[](https://nodejs.org)
|
||||||
|
|
||||||
|
An unofficial presentation skill for AI coding agents, reverse-engineered from Kimi Slides. It lets your agent create, edit, replicate, read, and export PPT/PPTX files. **Every run delivers two outputs by default: an editable PPTD project and a ready-to-use PPTX** — fonts embedded and fade transitions included — plus a local in-browser PPTD editor with manual PPTX export. Works with Codex, Claude Code, Cursor, CodeBuddy, and any agent that supports the SKILL.md format.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> This project is implemented by reverse-engineering the Kimi Slides skill, the PPTD format, and the frontend behavior and communication protocol of the publicly accessible web editor. It is not an official Kimi or Moonshot AI project and is not endorsed or supported by them. Public frontend resources and compatibility contracts used by this project may change without notice. Provided for learning and research purposes only.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **PPTD generation**: let your agent generate complete, editable PPTD projects — from scratch, with style transfer, template reuse, or replication from images/PDFs.
|
||||||
|
- **PPTX generation**: produce a matching PPTX by default, with fonts embedded and fade transitions written automatically.
|
||||||
|
- **Visual QA**: with a multimodal model, the skill exports every page as an image, stitches them into an overview sheet, and checks each page (distortion, occlusion, out-of-bounds elements, contrast, layout consistency, text overflow) before PPTX export — fixing and re-checking until every page passes.
|
||||||
|
- **Online editing**: view and edit local PPTD projects in a browser, with autosave and configurable slide transitions.
|
||||||
|
- **Manual export**: export PPTX manually from the editor at any time.
|
||||||
|
- **Format conversion**: convert existing PPTX files to PPTD for further editing.
|
||||||
|
- **Secure by design**: local editing only reads and writes project directories explicitly authorized by the user.
|
||||||
|
|
||||||
|
## Why open-kimi-ppt
|
||||||
|
|
||||||
|
Most PPT skills fall into three buckets: assemble OOXML / pptxgenjs in code, render each slide as a full-bleed image, or ship a swipeable HTML deck. open-kimi-ppt takes a **PPTD intermediate layer + real editable PPTX** path — easy for agents to write, good to look at, and still editable in PowerPoint.
|
||||||
|
|
||||||
|
| | open-kimi-ppt | Code-built PPTX (e.g. pptxgenjs) | Full-slide image PPT | Web HTML PPT |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| Deliverable | PPTD project + PPTX | Usually PPTX only | Usually PPTX only | Single HTML file |
|
||||||
|
| Agent-friendly | Clear per-page YAML | Lots of coordinates/API detail | Depends on image models & prompts | Strong HTML/CSS template constraints |
|
||||||
|
| Editable in PowerPoint | Text, shapes, images stay editable | Editable, but hard to refine later | Flat bitmaps — hard to reword | Not native PPTX |
|
||||||
|
| Visual quality | Real layouts + multimodal QA before export | Relies on agent layout tuning | Cohesive, poster-like | Strong motion; great for live demos |
|
||||||
|
| Re-editing | Browser visual editor + autosave | Mostly re-run code | Usually regenerate images | Edit HTML source |
|
||||||
|
| Best for | Formal PPTX you still need to tweak | Structured reports / template fills | Visually unified poster decks | In-browser talks / launches |
|
||||||
|
|
||||||
|
In short:
|
||||||
|
|
||||||
|
1. **DSL built for agents** — PPTD describes theme, layout, and elements in YAML, more stable than raw OOXML / pptxgenjs, and more locally editable than full-slide images.
|
||||||
|
2. **Two deliverables by default** — an iterable PPTD project plus a ready-to-open PPTX (embedded fonts, fade transitions).
|
||||||
|
3. **Truly editable PPTX** — text boxes and shapes remain editable in PowerPoint / WPS, unlike image-only decks.
|
||||||
|
4. **Local visual editor** — preview, tweak, set transitions, and re-export in the browser without rerunning the whole agent flow.
|
||||||
|
5. **Visual QA before export** — full-page screenshots plus an overview sheet catch occlusion, overflow, contrast, and layout issues before PPTX is written.
|
||||||
|
6. **Not locked to the official model — lower cost** — unlike official Kimi Slides, you can run this in any compatible agent with cheaper models such as DeepSeek. Even without multimodal vision, a model that follows the PPTD spec can still produce strong decks (multimodal helps more with the visual QA pass).
|
||||||
|
|
||||||
|
[](docs/images/example-deepseek-liquid-glass.png)
|
||||||
|
|
||||||
|
*Above: an Apple Liquid Glass-style deck generated with DeepSeek-V4-Flash in CodeBuddy / WorkBuddy.*
|
||||||
|
|
||||||
|
### Style and themes
|
||||||
|
|
||||||
|
This skill **does not ship a fixed theme or template**. You choose the look.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> **Best results come from stating a PPT style in the prompt, or attaching a reference PPT / PPTX template.** With a style constraint or template to follow, output quality is clearly better and more stable. Topic-only prompts leave the agent free to invent a look, so results vary more.
|
||||||
|
|
||||||
|
Common approaches:
|
||||||
|
|
||||||
|
1. **Describe the style in the prompt** — e.g. dark tech, magazine layout, Apple liquid glass, minimal big-type poster slides;
|
||||||
|
2. **Provide a reference template** — upload an existing PPT / PPTX / screenshot and ask the agent to transfer colors, layout, and overall style.
|
||||||
|
|
||||||
|
You can combine both: lock the look with a template, then add one line about the style you want to emphasize.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
| Edit PPTD online | Export PPTX |
|
||||||
|
| :---: | :---: |
|
||||||
|
| [](docs/images/editor-overview.png) | [](docs/images/export-pptx.png) |
|
||||||
|
|
||||||
|
## What is PPTD
|
||||||
|
|
||||||
|
PPTD is a YAML-based presentation DSL — a simplified abstraction layer over OOXML. It preserves the essentials (theme, page layout, element positions) while dropping complex nesting such as Masters; every page is self-contained — what you see is what you get. See [reference/pptd.md](skills/open-kimi-ppt/reference/pptd.md) for the complete definition.
|
||||||
|
|
||||||
|
A complete PPTD project looks like this:
|
||||||
|
|
||||||
|
```text
|
||||||
|
deck/
|
||||||
|
deck.pptd # manifest
|
||||||
|
pages/ # one .page file per slide
|
||||||
|
media/ # local media assets (if any)
|
||||||
|
deck.pptx # PPTX generated by default
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Node.js 18 or later is required. You can also ask your agent: `Install the open-kimi-ppt skills from GitHub for me.`, or: `Install https://github.com/Binaryify/open-kimi-ppt-skill for me.`
|
||||||
|
|
||||||
|
Install directly with npx:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx open-kimi-ppt-skills install
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, install the CLI globally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --global open-kimi-ppt-skills
|
||||||
|
open-kimi-ppt-skills install
|
||||||
|
```
|
||||||
|
|
||||||
|
By default the installer targets the shared, agent-agnostic directory `~/.agents/skills/open-kimi-ppt`, which compatible agents discover directly. If your agent uses its own skills directory, point `--target` at it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Codex
|
||||||
|
open-kimi-ppt-skills install --target ~/.codex/skills
|
||||||
|
|
||||||
|
# Claude Code
|
||||||
|
open-kimi-ppt-skills install --target ~/.claude/skills
|
||||||
|
|
||||||
|
# Cursor
|
||||||
|
open-kimi-ppt-skills install --target ~/.cursor/skills
|
||||||
|
|
||||||
|
# CodeBuddy
|
||||||
|
open-kimi-ppt-skills install --target ~/.codebuddy/skills
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update
|
||||||
|
|
||||||
|
When the skill is updated, overwrite your local install with `--force`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx open-kimi-ppt-skills@latest install --force
|
||||||
|
```
|
||||||
|
|
||||||
|
If you originally installed with `--target`, pass the same path again:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx open-kimi-ppt-skills@latest install --force --target ~/.claude/skills
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also ask your agent: `Update the open-kimi-ppt skill for me.` Updating only replaces the skill files; it does not touch PPTD / PPTX projects you already generated.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Generate a presentation with your agent
|
||||||
|
|
||||||
|
Once installed, just describe what you need. **You always get two deliverables by default**: the complete, editable PPTD project directory and the matching PPTX file. PPTX generation is skipped only when you explicitly ask for PPTD-only output.
|
||||||
|
|
||||||
|
For more stable quality, put a style in the prompt (e.g. “dark product-launch look”) or attach a reference PPT template; topic-only prompts without style guidance tend to vary more.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Use open-kimi-ppt to create a liquid-glass-style deck about the history of Apple.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Edit online and export manually
|
||||||
|
|
||||||
|
Start the local editor:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx open-kimi-ppt-skills serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open <http://127.0.0.1:55173/> and choose a complete project folder containing the `.pptd` manifest, `pages/`, and `media/` to view, edit, and export PPTX in the browser. The bundled [example/dji-pocket4](example/dji-pocket4) project — a complete 18-page deck — is ready to open for a quick tour.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Open the browser after startup
|
||||||
|
npx open-kimi-ppt-skills serve --open
|
||||||
|
|
||||||
|
# Use another port
|
||||||
|
npx open-kimi-ppt-skills serve --port 56000
|
||||||
|
```
|
||||||
|
|
||||||
|
Writable folder access requires a Chromium-based browser with the File System Access API. Other browsers fall back to read-only folder upload. Press `Ctrl+C` to stop the server.
|
||||||
|
|
||||||
|
## How it works and security boundaries
|
||||||
|
|
||||||
|
- The CLI serves static files on `127.0.0.1` only and does not listen on LAN interfaces.
|
||||||
|
- The browser reads a complete PPTD project directory only after explicit user authorization.
|
||||||
|
- Save callbacks may only modify `.pptd` and `.page` files; absolute paths and `..` traversal are rejected.
|
||||||
|
- The local host passes PPTD content to the public Kimi web editor. Remote images, fonts, and editor resources may still be fetched from their respective servers.
|
||||||
|
- This project does not provide or inject Kimi login tokens and does not access private Kimi documents.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
This is a compatibility host for the current public implementation, not a stable official SDK. Updates to Kimi frontend asset hashes, the PPTD format, or the iframe/RPC protocol may require a corresponding project update. Successfully generating a PPTX does not guarantee identical animation playback in PowerPoint, WPS, and Keynote.
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --global .
|
||||||
|
npm test
|
||||||
|
npm run pack:check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Legal
|
||||||
|
|
||||||
|
Kimi, Kimi Slides, and related trademarks belong to their respective owners.
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { cpSync, existsSync, mkdtempSync, mkdirSync, renameSync, rmSync } from "node:fs";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
import { basename, dirname, join, resolve } from "node:path";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { startEditorServer } from "../lib/editor-server.js";
|
||||||
|
|
||||||
|
const SKILL_NAME = "open-kimi-ppt";
|
||||||
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const sourceDirectory = join(packageRoot, "skills", SKILL_NAME);
|
||||||
|
|
||||||
|
function printHelp(command) {
|
||||||
|
if (command === "serve") {
|
||||||
|
console.log(`Start the local PPTD editor and exporter.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
open-kimi-ppt-skills serve [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--port <number> HTTP port (default: 55173)
|
||||||
|
--open Open the editor in the default browser
|
||||||
|
-h, --help Show this help
|
||||||
|
`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Install ${SKILL_NAME} for your AI coding agent or start its local editor.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
open-kimi-ppt-skills [install] [options]
|
||||||
|
open-kimi-ppt-skills serve [options]
|
||||||
|
|
||||||
|
Install options:
|
||||||
|
--target <directory> Skills directory (default: ~/.agents/skills)
|
||||||
|
--force Replace an existing installation
|
||||||
|
|
||||||
|
Run "open-kimi-ppt-skills serve --help" for server options.
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArguments(arguments_) {
|
||||||
|
const args = [...arguments_];
|
||||||
|
const command = args[0] === "install" || args[0] === "serve" ? args.shift() : "install";
|
||||||
|
const options = command === "serve"
|
||||||
|
? { command, open: false, port: 55173 }
|
||||||
|
: { command, force: false, target: undefined };
|
||||||
|
|
||||||
|
while (args.length > 0) {
|
||||||
|
const argument = args.shift();
|
||||||
|
|
||||||
|
if (command === "install" && argument === "--force") {
|
||||||
|
options.force = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command === "install" && argument === "--target") {
|
||||||
|
const target = args.shift();
|
||||||
|
if (!target || target.startsWith("-")) {
|
||||||
|
throw new Error("--target requires a directory");
|
||||||
|
}
|
||||||
|
options.target = resolve(target);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command === "serve" && argument === "--port") {
|
||||||
|
const port = Number(args.shift());
|
||||||
|
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
||||||
|
throw new Error("--port must be an integer between 1 and 65535");
|
||||||
|
}
|
||||||
|
options.port = port;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command === "serve" && argument === "--open") {
|
||||||
|
options.open = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argument === "--help" || argument === "-h") {
|
||||||
|
options.help = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`unknown argument: ${argument}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openBrowser(url) {
|
||||||
|
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
||||||
|
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
||||||
|
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
||||||
|
child.on("error", (error) => console.warn(`Could not open the browser: ${error.message}`));
|
||||||
|
child.unref();
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultSkillsDirectory() {
|
||||||
|
return join(homedir(), ".agents", "skills");
|
||||||
|
}
|
||||||
|
|
||||||
|
function installSkill({ force, target }) {
|
||||||
|
if (!existsSync(join(sourceDirectory, "SKILL.md"))) {
|
||||||
|
throw new Error(`packaged skill is incomplete: ${sourceDirectory}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillsDirectory = target ?? defaultSkillsDirectory();
|
||||||
|
const destination = join(skillsDirectory, SKILL_NAME);
|
||||||
|
|
||||||
|
if (existsSync(destination) && !force) {
|
||||||
|
throw new Error(
|
||||||
|
`${destination} already exists; rerun with --force to replace it`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync(skillsDirectory, { recursive: true });
|
||||||
|
const stagingRoot = mkdtempSync(join(skillsDirectory, `.${SKILL_NAME}-`));
|
||||||
|
const stagedSkill = join(stagingRoot, SKILL_NAME);
|
||||||
|
|
||||||
|
try {
|
||||||
|
cpSync(sourceDirectory, stagedSkill, {
|
||||||
|
recursive: true,
|
||||||
|
filter: (source) => ![".DS_Store", "_user_meta.json"].includes(basename(source)),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
rmSync(destination, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
renameSync(stagedSkill, destination);
|
||||||
|
} finally {
|
||||||
|
rmSync(stagingRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Installed ${SKILL_NAME} to ${destination}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const options = parseArguments(process.argv.slice(2));
|
||||||
|
if (options.help) {
|
||||||
|
printHelp(options.command);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.command === "install") {
|
||||||
|
installSkill(options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { server, url } = await startEditorServer({ port: options.port });
|
||||||
|
console.log(`Open Kimi PPT editor is running at ${url}`);
|
||||||
|
console.log("Press Ctrl+C to stop the server.");
|
||||||
|
if (options.open) openBrowser(url);
|
||||||
|
|
||||||
|
const shutdown = () => server.close(() => process.exit(0));
|
||||||
|
process.once("SIGINT", shutdown);
|
||||||
|
process.once("SIGTERM", shutdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(`Error: ${error.message}`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
|
After Width: | Height: | Size: 855 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 858 KiB |
@@ -0,0 +1,798 @@
|
|||||||
|
import {
|
||||||
|
assertWritableChangePath,
|
||||||
|
basename,
|
||||||
|
dirname,
|
||||||
|
extractPagePaths,
|
||||||
|
joinDeckPath,
|
||||||
|
normalizeRelativePath,
|
||||||
|
titleFromManifest,
|
||||||
|
} from "./lib.js";
|
||||||
|
|
||||||
|
const EDITOR_ORIGIN = "https://www.kimi.com";
|
||||||
|
const PENPAL_MODULE = "https://statics.moonshot.cn/neo-design/assets/penpal-C4NjirZE.js";
|
||||||
|
const MAX_TEXT_BYTES = 20 * 1024 * 1024;
|
||||||
|
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
||||||
|
const MAX_CHANGE_COUNT = 600;
|
||||||
|
|
||||||
|
const elements = {
|
||||||
|
frame: document.querySelector("#editor"),
|
||||||
|
openFolder: document.querySelector("#open-folder"),
|
||||||
|
openDemo: document.querySelector("#open-demo"),
|
||||||
|
reload: document.querySelector("#reload-deck"),
|
||||||
|
documentTitle: document.querySelector("#document-title"),
|
||||||
|
documentPath: document.querySelector("#document-path"),
|
||||||
|
connectionStatus: document.querySelector("#connection-status"),
|
||||||
|
connectionLabel: document.querySelector("#connection-label"),
|
||||||
|
saveState: document.querySelector("#save-state"),
|
||||||
|
loadingCard: document.querySelector("#loading-card"),
|
||||||
|
loadingTitle: document.querySelector("#loading-title"),
|
||||||
|
loadingMessage: document.querySelector("#loading-message"),
|
||||||
|
activityPanel: document.querySelector("#activity-panel"),
|
||||||
|
activityList: document.querySelector("#activity-list"),
|
||||||
|
toggleActivity: document.querySelector("#toggle-activity"),
|
||||||
|
closeActivity: document.querySelector("#close-activity"),
|
||||||
|
openDialog: document.querySelector("#open-dialog"),
|
||||||
|
closeOpenDialog: document.querySelector("#close-open-dialog"),
|
||||||
|
uploadDropzone: document.querySelector("#upload-dropzone"),
|
||||||
|
chooseWritableFolder: document.querySelector("#choose-writable-folder"),
|
||||||
|
uploadFolder: document.querySelector("#upload-folder"),
|
||||||
|
deckDialog: document.querySelector("#deck-dialog"),
|
||||||
|
deckOptions: document.querySelector("#deck-options"),
|
||||||
|
folderFallback: document.querySelector("#folder-fallback"),
|
||||||
|
toastRegion: document.querySelector("#toast-region"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
remote: null,
|
||||||
|
connection: null,
|
||||||
|
source: "demo",
|
||||||
|
directoryHandle: null,
|
||||||
|
fileIndex: new Map(),
|
||||||
|
memoryFiles: new Map(),
|
||||||
|
manifestPath: "presentation.pptd",
|
||||||
|
manifestDirectory: "",
|
||||||
|
manifestContent: "",
|
||||||
|
deckTitle: "演示文稿",
|
||||||
|
lastDeckPayload: null,
|
||||||
|
saveQueue: Promise.resolve(),
|
||||||
|
imageCache: new Map(),
|
||||||
|
dragDepth: 0,
|
||||||
|
readOnlyFallback: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function setConnection(kind, label) {
|
||||||
|
elements.connectionStatus.className = `status-pill status-${kind}`;
|
||||||
|
elements.connectionLabel.textContent = label;
|
||||||
|
document.documentElement.dataset.connection = kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSaveState(label, kind = "idle") {
|
||||||
|
elements.saveState.textContent = label;
|
||||||
|
elements.saveState.dataset.kind = kind;
|
||||||
|
document.documentElement.dataset.saveState = kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLoading(visible, title, message) {
|
||||||
|
elements.loadingCard.classList.toggle("is-hidden", !visible);
|
||||||
|
elements.loadingCard.setAttribute("aria-hidden", String(!visible));
|
||||||
|
if (title) elements.loadingTitle.textContent = title;
|
||||||
|
if (message) elements.loadingMessage.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(date = new Date()) {
|
||||||
|
return new Intl.DateTimeFormat("zh-CN", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
}).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addActivity(message, tone = "info") {
|
||||||
|
const item = document.createElement("li");
|
||||||
|
item.dataset.tone = tone;
|
||||||
|
item.innerHTML = `<time>${formatTime()}</time><span></span>`;
|
||||||
|
item.querySelector("span").textContent = message;
|
||||||
|
elements.activityList.prepend(item);
|
||||||
|
while (elements.activityList.children.length > 40) {
|
||||||
|
elements.activityList.lastElementChild.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toast(message, tone = "info") {
|
||||||
|
const item = document.createElement("div");
|
||||||
|
item.className = `toast toast-${tone}`;
|
||||||
|
item.textContent = message;
|
||||||
|
elements.toastRegion.append(item);
|
||||||
|
requestAnimationFrame(() => item.classList.add("is-visible"));
|
||||||
|
setTimeout(() => {
|
||||||
|
item.classList.remove("is-visible");
|
||||||
|
setTimeout(() => item.remove(), 220);
|
||||||
|
}, 3200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function editorQuery() {
|
||||||
|
return new URLSearchParams({
|
||||||
|
sdkMode: "ppt-editor",
|
||||||
|
pptPlatform: "neodeck-local",
|
||||||
|
functional: JSON.stringify({
|
||||||
|
fullscreen: true,
|
||||||
|
present: true,
|
||||||
|
export: true,
|
||||||
|
close: false,
|
||||||
|
annotation: false,
|
||||||
|
feedback: false,
|
||||||
|
share: false,
|
||||||
|
versionHistory: false,
|
||||||
|
}),
|
||||||
|
sdkSaveMode: "external",
|
||||||
|
sdkImageMode: "external",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function demoDeck() {
|
||||||
|
const manifest = JSON.stringify({
|
||||||
|
version: "v2",
|
||||||
|
title: "NeoDeck Local",
|
||||||
|
size: [960, 540],
|
||||||
|
pages: ["pages/01.page", "pages/02.page"],
|
||||||
|
});
|
||||||
|
const pages = [
|
||||||
|
{
|
||||||
|
path: "pages/01.page",
|
||||||
|
content: JSON.stringify({
|
||||||
|
pageType: "content",
|
||||||
|
background: { color: "#f7f8fc" },
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
elementId: "eyebrow",
|
||||||
|
elementType: "text",
|
||||||
|
bounds: [92, 96, 776, 40],
|
||||||
|
content: {
|
||||||
|
text: '<p><span style="font-size:16px;color:#6c5ce7;font-weight:700;letter-spacing:2px">NEODECK LOCAL</span></p>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
elementId: "title",
|
||||||
|
elementType: "text",
|
||||||
|
bounds: [92, 155, 776, 142],
|
||||||
|
content: {
|
||||||
|
text: '<p><span style="font-size:52px;color:#171923;font-weight:700">直接打开和保存<br/>本地 PPTD</span></p>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
elementId: "subtitle",
|
||||||
|
elementType: "text",
|
||||||
|
bounds: [94, 330, 660, 58],
|
||||||
|
content: {
|
||||||
|
text: '<p><span style="font-size:20px;color:#667085">第三方宿主 · 本地文件 · Kimi neo-ppt 编辑内核</span></p>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
animations: [
|
||||||
|
{
|
||||||
|
elementId: "title",
|
||||||
|
effect: "fade-in",
|
||||||
|
trigger: "onClick",
|
||||||
|
direction: "up",
|
||||||
|
easing: "ease-in-out",
|
||||||
|
durationMs: 700,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "pages/02.page",
|
||||||
|
content: JSON.stringify({
|
||||||
|
pageType: "content",
|
||||||
|
background: { color: "#171923" },
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
elementId: "second-title",
|
||||||
|
elementType: "text",
|
||||||
|
bounds: [100, 120, 760, 96],
|
||||||
|
content: {
|
||||||
|
text: '<p><span style="font-size:42px;color:#ffffff;font-weight:700">文件系统桥接已经就绪</span></p>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
elementId: "steps",
|
||||||
|
elementType: "text",
|
||||||
|
bounds: [102, 250, 730, 165],
|
||||||
|
content: {
|
||||||
|
text: '<p><span style="font-size:22px;color:#cbd5e1">① 选择完整的 PPTD 项目文件夹</span></p><p><span style="font-size:22px;color:#cbd5e1">② 编辑器自动读取清单、页面与素材</span></p><p><span style="font-size:22px;color:#cbd5e1">③ 自动保存变更到原目录</span></p>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
id: "neodeck-demo",
|
||||||
|
title: "NeoDeck Local",
|
||||||
|
manifestPath: "presentation.pptd",
|
||||||
|
manifestContent: manifest,
|
||||||
|
pages,
|
||||||
|
basePath: "",
|
||||||
|
isCreate: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireRemote() {
|
||||||
|
if (!state.remote) throw new Error("编辑器还没有连接完成");
|
||||||
|
return state.remote;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setDeck(payload, sourceLabel) {
|
||||||
|
const remote = requireRemote();
|
||||||
|
setLoading(true, "正在载入 PPTD", sourceLabel);
|
||||||
|
state.lastDeckPayload = payload;
|
||||||
|
state.manifestContent = payload.manifestContent;
|
||||||
|
state.manifestPath = payload.manifestPath;
|
||||||
|
state.manifestDirectory = dirname(payload.manifestPath);
|
||||||
|
state.deckTitle = payload.title;
|
||||||
|
elements.documentTitle.textContent = payload.title;
|
||||||
|
elements.documentPath.textContent = sourceLabel;
|
||||||
|
elements.reload.disabled = false;
|
||||||
|
|
||||||
|
await remote.setPPTD(payload.id, {
|
||||||
|
pptdContent: payload.manifestContent,
|
||||||
|
pages: payload.pages,
|
||||||
|
basePath: payload.basePath,
|
||||||
|
pptdPath: payload.manifestPath,
|
||||||
|
isCreate: true,
|
||||||
|
});
|
||||||
|
await remote.setEditable(true);
|
||||||
|
const status = await remote.getSlideStatus();
|
||||||
|
setLoading(false);
|
||||||
|
addActivity(`已载入「${payload.title}」,共 ${payload.pages.length} 页`, "success");
|
||||||
|
document.documentElement.dataset.deckStatus = "ready";
|
||||||
|
window.dispatchEvent(new CustomEvent("neodeck:ready", { detail: status }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openDemo() {
|
||||||
|
state.source = "demo";
|
||||||
|
state.directoryHandle = null;
|
||||||
|
state.fileIndex.clear();
|
||||||
|
state.imageCache.clear();
|
||||||
|
state.memoryFiles.clear();
|
||||||
|
state.readOnlyFallback = false;
|
||||||
|
const payload = demoDeck();
|
||||||
|
state.memoryFiles.set(payload.manifestPath, payload.manifestContent);
|
||||||
|
for (const page of payload.pages) state.memoryFiles.set(page.path, page.content);
|
||||||
|
await setDeck(payload, "内置示例 · 修改保存在本页内存");
|
||||||
|
setSaveState("示例 · 内存保存", "memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function indexDirectory(directoryHandle) {
|
||||||
|
const index = new Map();
|
||||||
|
async function walk(handle, prefix = "") {
|
||||||
|
for await (const [name, entry] of handle.entries()) {
|
||||||
|
if (name === ".DS_Store") continue;
|
||||||
|
const path = prefix ? `${prefix}/${name}` : name;
|
||||||
|
if (entry.kind === "directory") await walk(entry, path);
|
||||||
|
else index.set(normalizeRelativePath(path), entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await walk(directoryHandle);
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function indexFallbackFiles(fileList) {
|
||||||
|
const index = new Map();
|
||||||
|
const files = [...fileList];
|
||||||
|
const firstPath = files[0]?.webkitRelativePath || files[0]?.name || "";
|
||||||
|
const rootName = firstPath.includes("/") ? firstPath.split("/")[0] : "";
|
||||||
|
for (const file of files) {
|
||||||
|
let path = file.webkitRelativePath || file.name;
|
||||||
|
if (rootName && path.startsWith(`${rootName}/`)) path = path.slice(rootName.length + 1);
|
||||||
|
index.set(normalizeRelativePath(path), { kind: "file", getFile: async () => file });
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chooseManifest(paths) {
|
||||||
|
if (paths.length === 1) return paths[0];
|
||||||
|
elements.deckOptions.replaceChildren();
|
||||||
|
const choice = new Promise((resolve) => {
|
||||||
|
for (const path of paths) {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.className = "deck-option";
|
||||||
|
button.type = "button";
|
||||||
|
button.innerHTML = `<span class="deck-file-icon">P</span><span><strong></strong><small></small></span><span>›</span>`;
|
||||||
|
button.querySelector("strong").textContent = basename(path);
|
||||||
|
button.querySelector("small").textContent = path;
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
elements.deckDialog.close();
|
||||||
|
resolve(path);
|
||||||
|
});
|
||||||
|
elements.deckOptions.append(button);
|
||||||
|
}
|
||||||
|
elements.deckDialog.addEventListener(
|
||||||
|
"close",
|
||||||
|
() => {
|
||||||
|
if (elements.deckDialog.returnValue === "cancel") resolve(null);
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
elements.deckDialog.showModal();
|
||||||
|
return choice;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function textFromIndexedFile(path) {
|
||||||
|
const entry = state.fileIndex.get(normalizeRelativePath(path));
|
||||||
|
if (!entry) throw new Error(`找不到文件:${path}`);
|
||||||
|
const file = await entry.getFile();
|
||||||
|
if (file.size > MAX_TEXT_BYTES) throw new Error(`文件超过 20 MiB:${path}`);
|
||||||
|
return file.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadManifest(manifestPath, sourceLabel) {
|
||||||
|
const manifestContent = await textFromIndexedFile(manifestPath);
|
||||||
|
const pagePaths = extractPagePaths(manifestContent);
|
||||||
|
const manifestDirectory = dirname(manifestPath);
|
||||||
|
const pages = [];
|
||||||
|
const missing = [];
|
||||||
|
for (const pagePath of pagePaths.slice(0, 500)) {
|
||||||
|
const indexedPath = joinDeckPath(manifestDirectory, pagePath);
|
||||||
|
try {
|
||||||
|
pages.push({ path: pagePath, content: await textFromIndexedFile(indexedPath) });
|
||||||
|
} catch {
|
||||||
|
missing.push(pagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pages.length === 0) throw new Error(".pptd 清单引用的页面均无法读取");
|
||||||
|
if (missing.length) toast(`跳过 ${missing.length} 个无法读取的页面`, "warning");
|
||||||
|
|
||||||
|
const fallbackTitle = basename(manifestPath).replace(/\.pptd$/i, "");
|
||||||
|
const title = titleFromManifest(manifestContent, fallbackTitle);
|
||||||
|
await setDeck(
|
||||||
|
{
|
||||||
|
id: fallbackTitle,
|
||||||
|
title,
|
||||||
|
manifestPath,
|
||||||
|
manifestContent,
|
||||||
|
pages,
|
||||||
|
basePath: manifestDirectory ? `${manifestDirectory}/` : "",
|
||||||
|
isCreate: true,
|
||||||
|
},
|
||||||
|
sourceLabel,
|
||||||
|
);
|
||||||
|
setSaveState(state.readOnlyFallback ? "只读打开" : "自动保存已开启", state.readOnlyFallback ? "readonly" : "saved");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openDirectoryHandle(directoryHandle, label = directoryHandle.name) {
|
||||||
|
setLoading(true, "正在扫描文件夹", label);
|
||||||
|
let permission = await directoryHandle.queryPermission?.({ mode: "readwrite" });
|
||||||
|
if (permission !== "granted") permission = await directoryHandle.requestPermission?.({ mode: "readwrite" });
|
||||||
|
if (permission !== "granted") throw new Error("没有获得文件夹读写权限");
|
||||||
|
|
||||||
|
state.source = "directory";
|
||||||
|
state.directoryHandle = directoryHandle;
|
||||||
|
state.readOnlyFallback = false;
|
||||||
|
state.fileIndex = await indexDirectory(directoryHandle);
|
||||||
|
state.imageCache.clear();
|
||||||
|
const manifests = [...state.fileIndex.keys()].filter((path) => path.toLowerCase().endsWith(".pptd"));
|
||||||
|
if (manifests.length === 0) throw new Error("文件夹里没有找到 .pptd 清单文件");
|
||||||
|
const manifestPath = await chooseManifest(manifests.sort());
|
||||||
|
if (!manifestPath) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await loadManifest(manifestPath, `${label}/${manifestPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openFallbackFiles(files) {
|
||||||
|
state.source = "fallback";
|
||||||
|
state.directoryHandle = null;
|
||||||
|
state.readOnlyFallback = true;
|
||||||
|
state.fileIndex = indexFallbackFiles(files);
|
||||||
|
state.imageCache.clear();
|
||||||
|
const manifests = [...state.fileIndex.keys()].filter((path) => path.toLowerCase().endsWith(".pptd"));
|
||||||
|
if (manifests.length === 0) throw new Error("文件夹里没有找到 .pptd 清单文件");
|
||||||
|
const manifestPath = await chooseManifest(manifests.sort());
|
||||||
|
if (manifestPath) await loadManifest(manifestPath, `${manifestPath} · 只读兼容模式`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openDirectoryHandleReadOnly(directoryHandle, label = directoryHandle.name) {
|
||||||
|
setLoading(true, "正在扫描文件夹", label);
|
||||||
|
state.source = "fallback";
|
||||||
|
state.directoryHandle = null;
|
||||||
|
state.readOnlyFallback = true;
|
||||||
|
state.fileIndex = await indexDirectory(directoryHandle);
|
||||||
|
state.imageCache.clear();
|
||||||
|
const manifests = [...state.fileIndex.keys()].filter((path) => path.toLowerCase().endsWith(".pptd"));
|
||||||
|
if (manifests.length === 0) throw new Error("文件夹里没有找到 .pptd 清单文件");
|
||||||
|
const manifestPath = await chooseManifest(manifests.sort());
|
||||||
|
if (manifestPath) await loadManifest(manifestPath, `${label}/${manifestPath} · 拖放只读模式`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pickDirectory() {
|
||||||
|
try {
|
||||||
|
if ("showDirectoryPicker" in window) {
|
||||||
|
const directoryHandle = await window.showDirectoryPicker({ id: "neodeck-project", mode: "readwrite" });
|
||||||
|
setOpenDialog(false);
|
||||||
|
await openDirectoryHandle(directoryHandle);
|
||||||
|
} else {
|
||||||
|
elements.folderFallback.click();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.name === "AbortError") return;
|
||||||
|
handleError(error, "打开文件夹失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openDroppedFolder(handles) {
|
||||||
|
const usableHandles = handles.filter(Boolean);
|
||||||
|
if (usableHandles.length !== 1 || usableHandles[0].kind !== "directory") {
|
||||||
|
throw new Error("只能拖入一个完整的 PPTD 项目文件夹,不能单独拖入 .pptd 文件");
|
||||||
|
}
|
||||||
|
|
||||||
|
const directoryHandle = usableHandles[0];
|
||||||
|
try {
|
||||||
|
setOpenDialog(false);
|
||||||
|
await openDirectoryHandle(directoryHandle);
|
||||||
|
} catch (error) {
|
||||||
|
setOpenDialog(false);
|
||||||
|
toast("无法以可写模式载入,正在尝试只读打开", "warning");
|
||||||
|
await openDirectoryHandleReadOnly(directoryHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveIndexedPath(requestedPath) {
|
||||||
|
if (typeof requestedPath !== "string" || !requestedPath.trim()) return null;
|
||||||
|
if (/^(?:data:image\/|https?:\/\/|blob:)/i.test(requestedPath)) return requestedPath;
|
||||||
|
let candidate = requestedPath.replace(/^file:\/\/+/, "").replaceAll("\\", "/");
|
||||||
|
try {
|
||||||
|
candidate = decodeURIComponent(candidate);
|
||||||
|
} catch {
|
||||||
|
// Preserve literal percent signs.
|
||||||
|
}
|
||||||
|
|
||||||
|
const directCandidates = [];
|
||||||
|
try {
|
||||||
|
directCandidates.push(normalizeRelativePath(candidate));
|
||||||
|
} catch {
|
||||||
|
const parts = candidate.split("/").filter(Boolean);
|
||||||
|
for (let index = 0; index < parts.length; index += 1) {
|
||||||
|
try {
|
||||||
|
directCandidates.push(normalizeRelativePath(parts.slice(index).join("/")));
|
||||||
|
} catch {
|
||||||
|
// Keep looking for a safe suffix.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
directCandidates.push(joinDeckPath(state.manifestDirectory, candidate));
|
||||||
|
} catch {
|
||||||
|
// Absolute paths may still match a safe indexed suffix below.
|
||||||
|
}
|
||||||
|
for (const path of directCandidates) if (state.fileIndex.has(path)) return path;
|
||||||
|
for (const path of state.fileIndex.keys()) {
|
||||||
|
if (directCandidates.some((candidatePath) => path.endsWith(`/${candidatePath}`) || path === candidatePath)) return path;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileToDataUrl(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.addEventListener("load", () => resolve(String(reader.result)));
|
||||||
|
reader.addEventListener("error", () => reject(reader.error || new Error("图片读取失败")));
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveImage(requestedPath) {
|
||||||
|
if (/^(?:data:image\/|https?:\/\/|blob:)/i.test(requestedPath)) return requestedPath;
|
||||||
|
const path = resolveIndexedPath(requestedPath);
|
||||||
|
if (!path) return "";
|
||||||
|
const entry = state.fileIndex.get(path);
|
||||||
|
const file = await entry.getFile();
|
||||||
|
if (file.size > MAX_IMAGE_BYTES) return "";
|
||||||
|
const cacheKey = `${path}:${file.lastModified}:${file.size}`;
|
||||||
|
if (!state.imageCache.has(cacheKey)) state.imageCache.set(cacheKey, fileToDataUrl(file));
|
||||||
|
return state.imageCache.get(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getImages(payload = {}) {
|
||||||
|
const paths = Array.isArray(payload.filePath) ? payload.filePath : [];
|
||||||
|
const result = await Promise.all(paths.map((path) => resolveImage(path).catch(() => "")));
|
||||||
|
const misses = result.filter((value) => !value).length;
|
||||||
|
if (misses) addActivity(`${paths.length} 张图片中有 ${misses} 张未找到`, "warning");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getHandleForPath(root, path, create = false) {
|
||||||
|
const parts = normalizeRelativePath(path).split("/");
|
||||||
|
const fileName = parts.pop();
|
||||||
|
let directory = root;
|
||||||
|
for (const part of parts) directory = await directory.getDirectoryHandle(part, { create });
|
||||||
|
return { directory, fileName, file: await directory.getFileHandle(fileName, { create }) };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readBackup(path) {
|
||||||
|
try {
|
||||||
|
const { file } = await getHandleForPath(state.directoryHandle, path, false);
|
||||||
|
return { existed: true, bytes: await (await file.getFile()).arrayBuffer() };
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.name === "NotFoundError") return { existed: false, bytes: null };
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeFile(path, content) {
|
||||||
|
const { file } = await getHandleForPath(state.directoryHandle, path, true);
|
||||||
|
const writable = await file.createWritable();
|
||||||
|
try {
|
||||||
|
await writable.write(content);
|
||||||
|
} finally {
|
||||||
|
await writable.close();
|
||||||
|
}
|
||||||
|
state.fileIndex.set(path, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFile(path) {
|
||||||
|
const parts = normalizeRelativePath(path).split("/");
|
||||||
|
const fileName = parts.pop();
|
||||||
|
let directory = state.directoryHandle;
|
||||||
|
for (const part of parts) directory = await directory.getDirectoryHandle(part, { create: false });
|
||||||
|
try {
|
||||||
|
await directory.removeEntry(fileName);
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.name !== "NotFoundError") throw error;
|
||||||
|
}
|
||||||
|
state.fileIndex.delete(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rollbackChanges(backups) {
|
||||||
|
const entries = [...backups.entries()].reverse();
|
||||||
|
for (const [path, backup] of entries) {
|
||||||
|
if (backup.existed) await writeFile(path, backup.bytes);
|
||||||
|
else await deleteFile(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistChanges(payload = {}) {
|
||||||
|
const changes = Array.isArray(payload.changes) ? payload.changes : [];
|
||||||
|
if (changes.length === 0) return { fileContent: payload.fileContent, lastModifiedTime: Date.now() };
|
||||||
|
if (changes.length > MAX_CHANGE_COUNT) throw new Error(`一次保存不能超过 ${MAX_CHANGE_COUNT} 个文件`);
|
||||||
|
|
||||||
|
const normalized = changes.map((change) => {
|
||||||
|
const path = assertWritableChangePath(state.manifestDirectory, change.path);
|
||||||
|
const operation = change.operate === "delete" ? "delete" : "put";
|
||||||
|
const content = operation === "put" ? String(change.content ?? "") : null;
|
||||||
|
if (content && new Blob([content]).size > MAX_TEXT_BYTES) throw new Error(`保存内容超过 20 MiB:${path}`);
|
||||||
|
return { path, operation, content };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (state.source === "demo") {
|
||||||
|
for (const change of normalized) {
|
||||||
|
if (change.operation === "delete") state.memoryFiles.delete(change.path);
|
||||||
|
else state.memoryFiles.set(change.path, change.content);
|
||||||
|
}
|
||||||
|
setSaveState("刚刚保存到内存", "memory");
|
||||||
|
addActivity(`已在内存保存 ${normalized.length} 项变更`, "success");
|
||||||
|
return { fileContent: payload.fileContent, lastModifiedTime: Date.now() };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.readOnlyFallback || !state.directoryHandle) {
|
||||||
|
throw new Error("当前是只读兼容模式,请用支持文件夹授权的 Chromium 浏览器打开");
|
||||||
|
}
|
||||||
|
|
||||||
|
const permission = await state.directoryHandle.queryPermission({ mode: "readwrite" });
|
||||||
|
if (permission !== "granted") throw new Error("项目文件夹的写入权限已经失效");
|
||||||
|
|
||||||
|
setSaveState("正在保存…", "saving");
|
||||||
|
const backups = new Map();
|
||||||
|
try {
|
||||||
|
for (const change of normalized) backups.set(change.path, await readBackup(change.path));
|
||||||
|
for (const change of normalized) {
|
||||||
|
if (change.operation === "delete") await deleteFile(change.path);
|
||||||
|
else await writeFile(change.path, change.content);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await rollbackChanges(backups).catch((rollbackError) => {
|
||||||
|
addActivity(`回滚未完全成功:${rollbackError.message}`, "error");
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSaveState("刚刚已保存", "saved");
|
||||||
|
addActivity(`已保存 ${normalized.length} 个文件变更`, "success");
|
||||||
|
return { fileContent: payload.fileContent, lastModifiedTime: Date.now() };
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSave(payload) {
|
||||||
|
const operation = state.saveQueue.then(() => persistChanges(payload));
|
||||||
|
state.saveQueue = operation.catch(() => undefined);
|
||||||
|
return operation.catch((error) => {
|
||||||
|
setSaveState("保存失败", "error");
|
||||||
|
addActivity(error.message, "error");
|
||||||
|
toast(`保存失败:${error.message}`, "error");
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleFullscreen(value) {
|
||||||
|
if (value === false || document.fullscreenElement) {
|
||||||
|
if (document.fullscreenElement) await document.exitFullscreen();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await document.documentElement.requestFullscreen();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError(error, context = "操作失败") {
|
||||||
|
const message = error?.message || String(error);
|
||||||
|
setLoading(false);
|
||||||
|
addActivity(`${context}:${message}`, "error");
|
||||||
|
toast(`${context}:${message}`, "error");
|
||||||
|
console.error(context, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectEditor() {
|
||||||
|
setConnection("connecting", "连接中");
|
||||||
|
addActivity("正在连接公开 neo-ppt 编辑器");
|
||||||
|
elements.frame.src = `${EDITOR_ORIGIN}/neo-ppt/?${editorQuery()}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [{ i: connect, r: WindowMessenger }] = await Promise.all([
|
||||||
|
import(PENPAL_MODULE),
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => reject(new Error("编辑器 iframe 加载超时")), 20_000);
|
||||||
|
elements.frame.addEventListener(
|
||||||
|
"load",
|
||||||
|
() => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const messenger = new WindowMessenger({
|
||||||
|
remoteWindow: elements.frame.contentWindow,
|
||||||
|
allowedOrigins: [EDITOR_ORIGIN],
|
||||||
|
});
|
||||||
|
state.connection = connect({
|
||||||
|
messenger,
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
toast("当前文稿由 NeoDeck Local 托管");
|
||||||
|
},
|
||||||
|
reenter() {
|
||||||
|
return state.lastDeckPayload ? setDeck(state.lastDeckPayload, elements.documentPath.textContent) : undefined;
|
||||||
|
},
|
||||||
|
toggleFullScreen: toggleFullscreen,
|
||||||
|
showFeedback() {},
|
||||||
|
sendPrompt() {
|
||||||
|
toast("AI 提示词回传尚未接入", "warning");
|
||||||
|
},
|
||||||
|
showMessage(payload) {
|
||||||
|
const message = typeof payload === "string" ? payload : payload?.message || payload?.content;
|
||||||
|
if (message) toast(message);
|
||||||
|
},
|
||||||
|
hideMessage() {},
|
||||||
|
onSave,
|
||||||
|
getImages,
|
||||||
|
setAnnotationMode() {},
|
||||||
|
setAnnotationCurrentPage() {},
|
||||||
|
upsertAnnotation() {},
|
||||||
|
removeAnnotation() {},
|
||||||
|
clearAnnotations() {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
state.remote = await Promise.race([
|
||||||
|
state.connection.promise,
|
||||||
|
new Promise((_, reject) => setTimeout(() => reject(new Error("RPC 握手超时")), 20_000)),
|
||||||
|
]);
|
||||||
|
await state.remote.setSlideConfig({ editable: true, locale: "zh-CN", theme: "light" });
|
||||||
|
setConnection("ready", "已连接");
|
||||||
|
addActivity("编辑器 RPC 握手完成", "success");
|
||||||
|
await openDemo();
|
||||||
|
} catch (error) {
|
||||||
|
setConnection("error", "连接失败");
|
||||||
|
setLoading(true, "无法连接编辑器", "请检查网络,或 Kimi 是否更新了公开前端资源。");
|
||||||
|
handleError(error, "编辑器连接失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActivityPanel(open) {
|
||||||
|
elements.activityPanel.classList.toggle("is-open", open);
|
||||||
|
elements.activityPanel.setAttribute("aria-hidden", String(!open));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOpenDialog(open) {
|
||||||
|
if (open && !elements.openDialog.open) elements.openDialog.showModal();
|
||||||
|
if (!open && elements.openDialog.open) elements.openDialog.close();
|
||||||
|
elements.openDialog.dataset.dropState = "idle";
|
||||||
|
elements.uploadDropzone.classList.remove("is-dragging");
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.openFolder.addEventListener("click", () => setOpenDialog(true));
|
||||||
|
elements.closeOpenDialog.addEventListener("click", () => setOpenDialog(false));
|
||||||
|
elements.chooseWritableFolder.addEventListener("click", pickDirectory);
|
||||||
|
elements.uploadFolder.addEventListener("click", () => elements.folderFallback.click());
|
||||||
|
elements.uploadDropzone.addEventListener("click", () => elements.folderFallback.click());
|
||||||
|
elements.openDialog.addEventListener("click", (event) => {
|
||||||
|
if (event.target === elements.openDialog) setOpenDialog(false);
|
||||||
|
});
|
||||||
|
elements.openDemo.addEventListener("click", () => openDemo().catch((error) => handleError(error, "示例载入失败")));
|
||||||
|
elements.reload.addEventListener("click", () => {
|
||||||
|
if (!state.lastDeckPayload) return;
|
||||||
|
const reload = state.source === "directory" ? loadManifest(state.manifestPath, elements.documentPath.textContent) : setDeck(state.lastDeckPayload, elements.documentPath.textContent);
|
||||||
|
reload.catch((error) => handleError(error, "重新载入失败"));
|
||||||
|
});
|
||||||
|
elements.toggleActivity.addEventListener("click", () => setActivityPanel(!elements.activityPanel.classList.contains("is-open")));
|
||||||
|
elements.closeActivity.addEventListener("click", () => setActivityPanel(false));
|
||||||
|
elements.folderFallback.addEventListener("change", () => {
|
||||||
|
if (elements.folderFallback.files?.length) {
|
||||||
|
setOpenDialog(false);
|
||||||
|
openFallbackFiles(elements.folderFallback.files).catch((error) => handleError(error, "上传文件夹失败"));
|
||||||
|
}
|
||||||
|
elements.folderFallback.value = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("dragenter", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
state.dragDepth += 1;
|
||||||
|
setOpenDialog(true);
|
||||||
|
elements.openDialog.dataset.dropState = "active";
|
||||||
|
elements.uploadDropzone.classList.add("is-dragging");
|
||||||
|
});
|
||||||
|
window.addEventListener("dragover", (event) => event.preventDefault());
|
||||||
|
window.addEventListener("dragleave", () => {
|
||||||
|
state.dragDepth = Math.max(0, state.dragDepth - 1);
|
||||||
|
if (state.dragDepth === 0) {
|
||||||
|
elements.openDialog.dataset.dropState = "idle";
|
||||||
|
elements.uploadDropzone.classList.remove("is-dragging");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.addEventListener("drop", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
state.dragDepth = 0;
|
||||||
|
elements.openDialog.dataset.dropState = "loading";
|
||||||
|
elements.uploadDropzone.classList.remove("is-dragging");
|
||||||
|
try {
|
||||||
|
const handles = await Promise.all([...event.dataTransfer.items].map((item) => item.getAsFileSystemHandle?.()));
|
||||||
|
if (handles.some(Boolean)) await openDroppedFolder(handles);
|
||||||
|
else if (event.dataTransfer.files?.length) {
|
||||||
|
const files = [...event.dataTransfer.files];
|
||||||
|
const roots = new Set(files.map((file) => file.webkitRelativePath?.split("/")[0]).filter(Boolean));
|
||||||
|
const isSingleFolder = files.length > 0 && roots.size === 1 && files.every((file) => file.webkitRelativePath?.includes("/"));
|
||||||
|
if (!isSingleFolder) throw new Error("只能拖入一个完整的 PPTD 项目文件夹,不能单独拖入 .pptd 文件");
|
||||||
|
setOpenDialog(false);
|
||||||
|
await openFallbackFiles(files);
|
||||||
|
} else throw new Error("请拖入包含 .pptd、pages 和 media 的完整项目文件夹");
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, "拖放打开失败");
|
||||||
|
} finally {
|
||||||
|
elements.openDialog.dataset.dropState = "idle";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("beforeunload", (event) => {
|
||||||
|
if (elements.saveState.dataset.kind === "saving") {
|
||||||
|
event.preventDefault();
|
||||||
|
event.returnValue = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.neoDeck = {
|
||||||
|
openDemo,
|
||||||
|
getSlideStatus: () => state.remote?.getSlideStatus(),
|
||||||
|
get status() {
|
||||||
|
return {
|
||||||
|
connected: Boolean(state.remote),
|
||||||
|
source: state.source,
|
||||||
|
title: state.deckTitle,
|
||||||
|
manifestPath: state.manifestPath,
|
||||||
|
indexedFiles: state.fileIndex.size,
|
||||||
|
saveState: elements.saveState.dataset.kind,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
connectEditor();
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="color-scheme" content="light" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="一个可打开、编辑并保存本地 PPTD 项目的 neo-ppt 第三方宿主。"
|
||||||
|
/>
|
||||||
|
<title>NeoDeck Local</title>
|
||||||
|
<link rel="stylesheet" href="./styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-shell">
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="brand" aria-label="NeoDeck Local">
|
||||||
|
<span class="brand-mark">N</span>
|
||||||
|
<span class="brand-copy">
|
||||||
|
<strong>NeoDeck</strong>
|
||||||
|
<small>LOCAL</small>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toolbar" aria-label="文件工具栏">
|
||||||
|
<button class="button button-primary" id="open-folder" type="button">
|
||||||
|
<span class="button-icon" aria-hidden="true">+</span>
|
||||||
|
打开 PPTD 文件夹
|
||||||
|
</button>
|
||||||
|
<button class="button" id="open-demo" type="button">示例</button>
|
||||||
|
<button class="icon-button" id="reload-deck" type="button" title="重新载入当前文稿" disabled>
|
||||||
|
↻
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="document-meta" aria-live="polite">
|
||||||
|
<div class="document-title" id="document-title">等待编辑器连接</div>
|
||||||
|
<div class="document-path" id="document-path">当前使用演示文稿</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="topbar-actions">
|
||||||
|
<span class="status-pill status-connecting" id="connection-status">
|
||||||
|
<span class="status-dot"></span>
|
||||||
|
<span id="connection-label">连接中</span>
|
||||||
|
</span>
|
||||||
|
<span class="save-state" id="save-state">未保存到本地</span>
|
||||||
|
<button class="icon-button" id="toggle-activity" type="button" title="打开活动记录">☰</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="editor-stage">
|
||||||
|
<iframe
|
||||||
|
id="editor"
|
||||||
|
title="neo-ppt 编辑器"
|
||||||
|
allow="fullscreen; clipboard-read; clipboard-write"
|
||||||
|
></iframe>
|
||||||
|
|
||||||
|
<section class="loading-card" id="loading-card" aria-live="polite">
|
||||||
|
<span class="loading-orbit" aria-hidden="true"></span>
|
||||||
|
<h1 id="loading-title">正在连接 neo-ppt</h1>
|
||||||
|
<p id="loading-message">编辑器就绪后会自动载入示例文稿。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="activity-panel" id="activity-panel" aria-hidden="true">
|
||||||
|
<div class="activity-header">
|
||||||
|
<div>
|
||||||
|
<strong>活动记录</strong>
|
||||||
|
<small>连接、加载与保存状态</small>
|
||||||
|
</div>
|
||||||
|
<button class="icon-button" id="close-activity" type="button" aria-label="关闭活动记录">×</button>
|
||||||
|
</div>
|
||||||
|
<ol class="activity-list" id="activity-list"></ol>
|
||||||
|
<div class="activity-footer">
|
||||||
|
文件只会在你授权的项目文件夹内读写;远程编辑器不会获得任意磁盘访问权限。
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dialog class="open-dialog" id="open-dialog" aria-labelledby="open-dialog-title">
|
||||||
|
<div class="open-dialog-shell">
|
||||||
|
<div class="open-dialog-header">
|
||||||
|
<div class="dialog-heading">
|
||||||
|
<span class="dialog-mark">P</span>
|
||||||
|
<div>
|
||||||
|
<h2 id="open-dialog-title">打开 PPTD 项目文件夹</h2>
|
||||||
|
<p>请选择或拖入包含完整项目结构的文件夹</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="icon-button" id="close-open-dialog" type="button" aria-label="关闭打开文稿弹窗">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="upload-dropzone" id="upload-dropzone" type="button">
|
||||||
|
<span class="upload-illustration" aria-hidden="true">
|
||||||
|
<span class="upload-file">P</span>
|
||||||
|
<span class="upload-arrow">↑</span>
|
||||||
|
</span>
|
||||||
|
<strong>拖动 PPTD 项目文件夹到这里</strong>
|
||||||
|
<span>仅接受一个完整文件夹,不支持单独的 .pptd 文件</span>
|
||||||
|
<em>点击上传完整文件夹(只读)</em>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="open-choice-row">
|
||||||
|
<button class="open-choice open-choice-primary" id="choose-writable-folder" type="button">
|
||||||
|
<span class="choice-icon" aria-hidden="true">▣</span>
|
||||||
|
<span>
|
||||||
|
<strong>选择项目文件夹</strong>
|
||||||
|
<small>授权后支持自动保存</small>
|
||||||
|
</span>
|
||||||
|
<span aria-hidden="true">›</span>
|
||||||
|
</button>
|
||||||
|
<button class="open-choice" id="upload-folder" type="button">
|
||||||
|
<span class="choice-icon choice-icon-muted" aria-hidden="true">↑</span>
|
||||||
|
<span>
|
||||||
|
<strong>上传项目文件夹</strong>
|
||||||
|
<small>兼容模式,只读打开</small>
|
||||||
|
</span>
|
||||||
|
<span aria-hidden="true">›</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="open-dialog-note">
|
||||||
|
<span aria-hidden="true">ⓘ</span>
|
||||||
|
<span>PPTD 项目由 .pptd 清单、pages 页面目录和 media 素材目录组成,必须上传整个文件夹。若提示含系统文件,请移除 .DS_Store。</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<dialog class="deck-dialog" id="deck-dialog">
|
||||||
|
<form method="dialog">
|
||||||
|
<div class="dialog-heading">
|
||||||
|
<span class="dialog-mark">P</span>
|
||||||
|
<div>
|
||||||
|
<h2>选择 PPTD 文稿</h2>
|
||||||
|
<p>这个文件夹里发现了多个 .pptd 清单,请选择一个。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="deck-options" id="deck-options"></div>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button class="button" value="cancel">取消</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<input id="folder-fallback" type="file" webkitdirectory multiple hidden />
|
||||||
|
<div class="toast-region" id="toast-region" aria-live="polite"></div>
|
||||||
|
|
||||||
|
<script type="module" src="./app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
export function normalizeRelativePath(value) {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
throw new TypeError("文件路径必须是字符串");
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = value.trim().replaceAll("\\", "/");
|
||||||
|
try {
|
||||||
|
path = decodeURIComponent(path);
|
||||||
|
} catch {
|
||||||
|
// Keep the original path when it contains a literal percent sign.
|
||||||
|
}
|
||||||
|
|
||||||
|
path = path.replace(/^file:\/\/+/, "").replace(/^\.\//, "");
|
||||||
|
if (!path || path.includes("\0") || path.startsWith("/") || /^[A-Za-z]:\//.test(path)) {
|
||||||
|
throw new Error(`不允许的绝对路径:${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
for (const part of path.split("/")) {
|
||||||
|
if (!part || part === ".") continue;
|
||||||
|
if (part === "..") throw new Error(`不允许越过项目目录:${value}`);
|
||||||
|
parts.push(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length === 0) throw new Error(`无效文件路径:${value}`);
|
||||||
|
return parts.join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dirname(path) {
|
||||||
|
const normalized = normalizeRelativePath(path);
|
||||||
|
const index = normalized.lastIndexOf("/");
|
||||||
|
return index === -1 ? "" : normalized.slice(0, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function basename(path) {
|
||||||
|
const normalized = normalizeRelativePath(path);
|
||||||
|
return normalized.slice(normalized.lastIndexOf("/") + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function joinDeckPath(base, path) {
|
||||||
|
const child = normalizeRelativePath(path);
|
||||||
|
if (!base) return child;
|
||||||
|
const normalizedBase = normalizeRelativePath(base);
|
||||||
|
if (child === normalizedBase || child.startsWith(`${normalizedBase}/`)) return child;
|
||||||
|
return normalizeRelativePath(`${normalizedBase}/${child}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unquoteYamlScalar(value) {
|
||||||
|
const text = value.trim();
|
||||||
|
if (text.startsWith('"')) {
|
||||||
|
const match = text.match(/^"(?:\\.|[^"\\])*"/);
|
||||||
|
if (match) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(match[0]);
|
||||||
|
} catch {
|
||||||
|
return match[0].slice(1, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (text.startsWith("'")) {
|
||||||
|
const end = text.indexOf("'", 1);
|
||||||
|
if (end !== -1) return text.slice(1, end).replaceAll("''", "'");
|
||||||
|
}
|
||||||
|
return text.replace(/\s+#.*$/, "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractPagePaths(manifestText) {
|
||||||
|
const trimmed = manifestText.trim();
|
||||||
|
if (!trimmed) throw new Error("PPTD manifest 是空文件");
|
||||||
|
|
||||||
|
if (trimmed.startsWith("{")) {
|
||||||
|
const parsed = JSON.parse(trimmed);
|
||||||
|
if (!Array.isArray(parsed.pages)) throw new Error("PPTD manifest 缺少 pages 数组");
|
||||||
|
return parsed.pages.map((path) => normalizeRelativePath(String(path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = manifestText.replaceAll("\r\n", "\n").split("\n");
|
||||||
|
const pageHeader = lines.findIndex((line) => /^(\s*)pages\s*:\s*(?:#.*)?$/.test(line));
|
||||||
|
if (pageHeader === -1) throw new Error("PPTD manifest 缺少 pages 列表");
|
||||||
|
|
||||||
|
const headerIndent = lines[pageHeader].match(/^\s*/)[0].length;
|
||||||
|
const pages = [];
|
||||||
|
for (let index = pageHeader + 1; index < lines.length; index += 1) {
|
||||||
|
const line = lines[index];
|
||||||
|
if (!line.trim() || line.trimStart().startsWith("#")) continue;
|
||||||
|
const indent = line.match(/^\s*/)[0].length;
|
||||||
|
if (indent <= headerIndent && !/^\s*-/.test(line)) break;
|
||||||
|
const match = line.match(/^\s*-\s*(.+?)\s*$/);
|
||||||
|
if (!match) break;
|
||||||
|
const path = unquoteYamlScalar(match[1]);
|
||||||
|
if (path) pages.push(normalizeRelativePath(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pages.length === 0) throw new Error("PPTD manifest 的 pages 列表为空");
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function titleFromManifest(manifestText, fallback) {
|
||||||
|
const trimmed = manifestText.trim();
|
||||||
|
if (trimmed.startsWith("{")) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(trimmed).title || fallback;
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const match = manifestText.match(/^title\s*:\s*(.+?)\s*$/m);
|
||||||
|
return match ? unquoteYamlScalar(match[1]) : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertWritableChangePath(base, requestedPath) {
|
||||||
|
const path = joinDeckPath(base, requestedPath);
|
||||||
|
if (!/\.(?:pptd|page)$/i.test(path)) {
|
||||||
|
throw new Error(`为安全起见,仅允许编辑 .pptd/.page:${requestedPath}`);
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
@@ -0,0 +1,783 @@
|
|||||||
|
:root {
|
||||||
|
font-family:
|
||||||
|
Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
|
||||||
|
"Microsoft YaHei", sans-serif;
|
||||||
|
color: #1d2433;
|
||||||
|
background: #eef1f6;
|
||||||
|
font-synthesis: none;
|
||||||
|
--purple: #6c5ce7;
|
||||||
|
--purple-dark: #5746d8;
|
||||||
|
--border: #e3e7ef;
|
||||||
|
--muted: #7b8495;
|
||||||
|
--shadow: 0 18px 60px rgba(34, 42, 61, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 56px minmax(0, 1fr);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar {
|
||||||
|
z-index: 30;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto minmax(180px, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
box-shadow: 0 1px 0 rgba(25, 32, 48, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
min-width: 126px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-mark,
|
||||||
|
.dialog-mark {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 31px;
|
||||||
|
height: 31px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 800;
|
||||||
|
background: linear-gradient(145deg, #8172f2, #5b49da 72%);
|
||||||
|
box-shadow: 0 7px 18px rgba(108, 92, 231, 0.27);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-copy {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-copy strong {
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-copy small {
|
||||||
|
color: #9aa2b1;
|
||||||
|
font-size: 8px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar,
|
||||||
|
.topbar-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button,
|
||||||
|
.icon-button {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: #3a4251;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover,
|
||||||
|
.icon-button:hover {
|
||||||
|
border-color: #cfd5e1;
|
||||||
|
background: #f7f8fb;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:focus-visible,
|
||||||
|
.icon-button:focus-visible,
|
||||||
|
.deck-option:focus-visible {
|
||||||
|
outline: 3px solid rgba(108, 92, 231, 0.24);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled,
|
||||||
|
.icon-button:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: default;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 13px;
|
||||||
|
border-radius: 9px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-primary {
|
||||||
|
border-color: var(--purple);
|
||||||
|
color: #fff;
|
||||||
|
background: var(--purple);
|
||||||
|
box-shadow: 0 5px 13px rgba(108, 92, 231, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-primary:hover {
|
||||||
|
border-color: var(--purple-dark);
|
||||||
|
background: var(--purple-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 9px;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-meta {
|
||||||
|
min-width: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-title,
|
||||||
|
.document-path {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-title {
|
||||||
|
color: #2f3746;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-path {
|
||||||
|
margin-top: 2px;
|
||||||
|
color: #9aa2b1;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid #e7eaf0;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #697386;
|
||||||
|
background: #f8f9fb;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 650;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f5a623;
|
||||||
|
box-shadow: 0 0 0 3px rgba(245, 166, 35, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-ready .status-dot {
|
||||||
|
background: #20b486;
|
||||||
|
box-shadow: 0 0 0 3px rgba(32, 180, 134, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error .status-dot {
|
||||||
|
background: #ef5b5b;
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 91, 91, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-state {
|
||||||
|
min-width: 76px;
|
||||||
|
color: #8992a2;
|
||||||
|
font-size: 11px;
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-state[data-kind="saving"] {
|
||||||
|
color: #8b72e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-state[data-kind="saved"] {
|
||||||
|
color: #15956f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-state[data-kind="error"] {
|
||||||
|
color: #d94848;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-stage {
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f0f2f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-card {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 18;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 50% 42%, rgba(108, 92, 231, 0.08), transparent 34%),
|
||||||
|
#f7f8fb;
|
||||||
|
transition: opacity 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-card.is-hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-card h1 {
|
||||||
|
margin: 19px 0 7px;
|
||||||
|
color: #252c3a;
|
||||||
|
font-size: 19px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-card p {
|
||||||
|
margin: 0;
|
||||||
|
color: #8a93a3;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-orbit {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border: 3px solid rgba(108, 92, 231, 0.14);
|
||||||
|
border-top-color: var(--purple);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 800ms linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-panel {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 35;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
bottom: 12px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||||
|
width: min(350px, calc(100% - 24px));
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(220, 224, 232, 0.9);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.98);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateX(calc(100% + 24px));
|
||||||
|
transition: 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-panel.is-open {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-header div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-header strong {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-header small {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 9px 16px;
|
||||||
|
overflow: auto;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-list li {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 62px minmax(0, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
color: #535d6d;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-list time {
|
||||||
|
color: #a0a8b5;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-list li[data-tone="success"] span {
|
||||||
|
color: #148264;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-list li[data-tone="warning"] span {
|
||||||
|
color: #a16a0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-list li[data-tone="error"] span {
|
||||||
|
color: #cc4545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-footer {
|
||||||
|
padding: 13px 16px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
color: #8b94a3;
|
||||||
|
background: #fafbfc;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-dialog,
|
||||||
|
.deck-dialog {
|
||||||
|
width: min(520px, calc(100vw - 32px));
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 18px;
|
||||||
|
color: #273041;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-dialog {
|
||||||
|
width: min(620px, calc(100vw - 32px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-dialog::backdrop,
|
||||||
|
.deck-dialog::backdrop {
|
||||||
|
background: rgba(24, 29, 42, 0.38);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-dialog form {
|
||||||
|
padding: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-dialog-shell {
|
||||||
|
padding: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-dialog-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-dropzone {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
margin: 22px 0 14px;
|
||||||
|
padding: 28px 22px 24px;
|
||||||
|
border: 1.5px dashed #c9c2f3;
|
||||||
|
border-radius: 16px;
|
||||||
|
color: #353d4c;
|
||||||
|
background: linear-gradient(180deg, #faf9ff, #f7f7fb);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-dropzone:hover,
|
||||||
|
.upload-dropzone.is-dragging {
|
||||||
|
border-color: var(--purple);
|
||||||
|
background: #f3f0ff;
|
||||||
|
box-shadow: inset 0 0 0 3px rgba(108, 92, 231, 0.08);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-dropzone strong {
|
||||||
|
margin-top: 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-dropzone > span:not(.upload-illustration) {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #81899a;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-dropzone em {
|
||||||
|
margin-top: 12px;
|
||||||
|
color: var(--purple);
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-illustration {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 68px;
|
||||||
|
height: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 auto auto 5px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 42px;
|
||||||
|
height: 52px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(145deg, #8172f2, #5b49da);
|
||||||
|
box-shadow: 0 10px 25px rgba(108, 92, 231, 0.26);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 1px;
|
||||||
|
bottom: -2px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border: 4px solid #faf9ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: var(--purple);
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 5px 16px rgba(54, 62, 82, 0.14);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-choice-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-choice {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 11px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 13px;
|
||||||
|
color: #303849;
|
||||||
|
text-align: left;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-choice:hover {
|
||||||
|
border-color: #cfc8f7;
|
||||||
|
background: #faf9ff;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-choice > span:nth-child(2) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-choice strong {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-choice small {
|
||||||
|
margin-top: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #8b94a3;
|
||||||
|
font-size: 10px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-icon {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #fff;
|
||||||
|
background: var(--purple);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-icon-muted {
|
||||||
|
color: #6659c8;
|
||||||
|
background: #eeeafd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-dialog-note {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 11px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #7b8495;
|
||||||
|
background: #f6f7f9;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-heading h2,
|
||||||
|
.dialog-heading p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-heading h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-heading p {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-options {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 340px;
|
||||||
|
margin: 20px 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-option {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 11px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 11px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: inherit;
|
||||||
|
text-align: left;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-option:hover {
|
||||||
|
border-color: #cfc8f7;
|
||||||
|
background: #faf9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-option > span:nth-child(2) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-option strong,
|
||||||
|
.deck-option small {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-option strong {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-option small {
|
||||||
|
margin-top: 3px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-file-icon {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
background: #f06a45;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-region {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 100;
|
||||||
|
right: 18px;
|
||||||
|
bottom: 18px;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
max-width: min(420px, calc(100vw - 36px));
|
||||||
|
padding: 11px 14px;
|
||||||
|
border: 1px solid #e3e7ef;
|
||||||
|
border-radius: 11px;
|
||||||
|
color: #394253;
|
||||||
|
background: rgba(255, 255, 255, 0.98);
|
||||||
|
box-shadow: 0 12px 34px rgba(35, 43, 60, 0.18);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
transition: 180ms ease;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.is-visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-error {
|
||||||
|
border-color: #f1c7c7;
|
||||||
|
color: #b43e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-warning {
|
||||||
|
border-color: #efd8ad;
|
||||||
|
color: #966512;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 920px) {
|
||||||
|
.topbar {
|
||||||
|
grid-template-columns: auto auto 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-meta {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-actions {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-state {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 650px) {
|
||||||
|
.app-shell {
|
||||||
|
grid-template-rows: 52px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-copy,
|
||||||
|
.toolbar .button:not(.button-primary),
|
||||||
|
.toolbar .icon-button,
|
||||||
|
.status-pill {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-primary {
|
||||||
|
max-width: 185px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-choice-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
version: v2
|
||||||
|
title: DJI Osmo Pocket 4 产品深度解读
|
||||||
|
size: [960, 540]
|
||||||
|
theme:
|
||||||
|
colors:
|
||||||
|
bg: "#0C0C0E"
|
||||||
|
surface: "#17171C"
|
||||||
|
line: "#2A2A31"
|
||||||
|
text: "#F2F0EA"
|
||||||
|
muted: "#8E8E96"
|
||||||
|
accent: "#D9A441"
|
||||||
|
textStyles:
|
||||||
|
kicker:
|
||||||
|
fontSize: 10
|
||||||
|
color: "$accent"
|
||||||
|
letterSpacing: 3
|
||||||
|
pageTitle:
|
||||||
|
fontSize: 36
|
||||||
|
color: "$text"
|
||||||
|
lineHeight: 1.35
|
||||||
|
body:
|
||||||
|
fontSize: 13
|
||||||
|
color: "$muted"
|
||||||
|
lineHeight: 1.75
|
||||||
|
statNum:
|
||||||
|
fontSize: 20
|
||||||
|
color: "$text"
|
||||||
|
statLabel:
|
||||||
|
fontSize: 10
|
||||||
|
color: "$muted"
|
||||||
|
headerLabel:
|
||||||
|
fontSize: 9
|
||||||
|
color: "$muted"
|
||||||
|
letterSpacing: 3
|
||||||
|
pages:
|
||||||
|
- pages/01_cover.page
|
||||||
|
- pages/02_highlights.page
|
||||||
|
- pages/03_toc.page
|
||||||
|
- pages/04_chapter1.page
|
||||||
|
- pages/05_cmos.page
|
||||||
|
- pages/06_dlog.page
|
||||||
|
- pages/07_slowmo.page
|
||||||
|
- pages/08_zoom.page
|
||||||
|
- pages/09_chapter2.page
|
||||||
|
- pages/10_tracking.page
|
||||||
|
- pages/11_control.page
|
||||||
|
- pages/12_gimbal.page
|
||||||
|
- pages/13_chapter3.page
|
||||||
|
- pages/14_audio.page
|
||||||
|
- pages/15_battery.page
|
||||||
|
- pages/16_accessories.page
|
||||||
|
- pages/17_specs.page
|
||||||
|
- pages/18_price.page
|
||||||
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 331 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 471 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 231 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 119 KiB |
@@ -0,0 +1,66 @@
|
|||||||
|
pageType: cover
|
||||||
|
background:
|
||||||
|
type: solid
|
||||||
|
color: "$bg"
|
||||||
|
elements:
|
||||||
|
- elementId: hero
|
||||||
|
elementType: image
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
src: "media/hero_sunset.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
- elementId: overlay
|
||||||
|
elementType: shape
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
shapeName: rect
|
||||||
|
fill:
|
||||||
|
type: gradient
|
||||||
|
gradientType: linear
|
||||||
|
angle: 90
|
||||||
|
stops:
|
||||||
|
- {position: 0, color: "#0C0C0E59"}
|
||||||
|
- {position: 0.55, color: "#0C0C0E40"}
|
||||||
|
- {position: 1, color: "#0C0C0EF2"}
|
||||||
|
- elementId: meta
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 40, 620, 16]
|
||||||
|
content:
|
||||||
|
fontSize: 10.5
|
||||||
|
color: "#F2F0EACC"
|
||||||
|
letterSpacing: 2
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: DJI 大疆创新 · 2026.04.16 深圳发布
|
||||||
|
- elementId: title-bar
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 262, 56, 4]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 282, 780, 200]
|
||||||
|
content:
|
||||||
|
fontSize: 76
|
||||||
|
color: "$text"
|
||||||
|
lineHeight: 1.24
|
||||||
|
align: [left, top]
|
||||||
|
text: |
|
||||||
|
<p><strong>一寸万象</strong></p>
|
||||||
|
<p><strong>光影随行</strong></p>
|
||||||
|
- elementId: subtitle
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 490, 700, 18]
|
||||||
|
content:
|
||||||
|
fontSize: 13
|
||||||
|
color: "#F2F0EACC"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: Osmo Pocket 4 · 一英寸口袋云台相机 产品深度解读
|
||||||
|
- elementId: price-note
|
||||||
|
elementType: text
|
||||||
|
bounds: [580, 510, 348, 14]
|
||||||
|
content:
|
||||||
|
fontSize: 10
|
||||||
|
color: "#8E8E96CC"
|
||||||
|
wrap: false
|
||||||
|
align: [right, middle]
|
||||||
|
text: 标准套装 ¥2999 起 · 全能套装 ¥3799
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
pageType: content
|
||||||
|
background:
|
||||||
|
type: solid
|
||||||
|
color: "$bg"
|
||||||
|
elements:
|
||||||
|
- elementId: hd-bar
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 34, 4, 12]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: hd-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 32, 260, 15]
|
||||||
|
content:
|
||||||
|
style: "$headerLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: DJI OSMO POCKET 4
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [856, 510, 72, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [right, middle]
|
||||||
|
text: 02 / 18
|
||||||
|
- elementId: kicker
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 92, 400, 14]
|
||||||
|
content:
|
||||||
|
style: "$kicker"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 核心亮点 KEY HIGHLIGHTS
|
||||||
|
- elementId: statement
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 122, 430, 130]
|
||||||
|
content:
|
||||||
|
fontSize: 42
|
||||||
|
color: "$text"
|
||||||
|
lineHeight: 1.4
|
||||||
|
align: [left, top]
|
||||||
|
text: |
|
||||||
|
<p><strong>把电影机,</strong></p>
|
||||||
|
<p><strong>装进口袋。</strong></p>
|
||||||
|
- elementId: desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 272, 400, 52]
|
||||||
|
content:
|
||||||
|
fontSize: 12.5
|
||||||
|
color: "$muted"
|
||||||
|
lineHeight: 1.8
|
||||||
|
align: [left, top]
|
||||||
|
text: 延续 Pocket 系列便携形态,一英寸大底与全面进化的影像规格,让专业级创作随手可得。
|
||||||
|
- elementId: divider
|
||||||
|
elementType: shape
|
||||||
|
bounds: [486, 96, 1, 380]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: n1
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 100, 392, 48]
|
||||||
|
content:
|
||||||
|
fontSize: 40
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><strong>1<span style="font-size:22px; color:$accent;"> 英寸 CMOS</span></strong></p>
|
||||||
|
- elementId: n1-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 152, 392, 16]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 堆栈式传感器 · f/2.0 大光圈
|
||||||
|
- elementId: row-line1
|
||||||
|
elementType: shape
|
||||||
|
bounds: [520, 190, 392, 1]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: n2
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 196, 392, 48]
|
||||||
|
content:
|
||||||
|
fontSize: 40
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><strong>4K/240<span style="font-size:22px; color:$accent;"> fps</span></strong></p>
|
||||||
|
- elementId: n2-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 248, 392, 16]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 慢动作录制 · 上代的 2 倍
|
||||||
|
- elementId: row-line2
|
||||||
|
elementType: shape
|
||||||
|
bounds: [520, 286, 392, 1]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: n3
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 292, 392, 48]
|
||||||
|
content:
|
||||||
|
fontSize: 40
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><strong>14<span style="font-size:22px; color:$accent;"> 档</span></strong></p>
|
||||||
|
- elementId: n3-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 344, 392, 16]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 动态范围 · 10-bit D-Log 色彩
|
||||||
|
- elementId: row-line3
|
||||||
|
elementType: shape
|
||||||
|
bounds: [520, 382, 392, 1]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: n4
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 388, 392, 48]
|
||||||
|
content:
|
||||||
|
fontSize: 40
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><strong>107<span style="font-size:22px; color:$accent;"> GB</span></strong></p>
|
||||||
|
- elementId: n4-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 440, 392, 16]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 内置存储 · 800MB/s 极速导出
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
pageType: table_of_contents
|
||||||
|
background:
|
||||||
|
type: solid
|
||||||
|
color: "$bg"
|
||||||
|
elements:
|
||||||
|
- elementId: hd-bar
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 34, 4, 12]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: hd-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 32, 260, 15]
|
||||||
|
content:
|
||||||
|
style: "$headerLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: DJI OSMO POCKET 4
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [856, 510, 72, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [right, middle]
|
||||||
|
text: 03 / 18
|
||||||
|
- elementId: kicker
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 88, 400, 14]
|
||||||
|
content:
|
||||||
|
style: "$kicker"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 目录 CONTENTS
|
||||||
|
- elementId: toc1-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 132, 72, 34]
|
||||||
|
content:
|
||||||
|
fontSize: 26
|
||||||
|
color: "$accent"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>01</strong>"
|
||||||
|
- elementId: toc1-title
|
||||||
|
elementType: text
|
||||||
|
bounds: [136, 132, 420, 30]
|
||||||
|
content:
|
||||||
|
fontSize: 20
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>影像画质</strong>"
|
||||||
|
- elementId: toc1-sub
|
||||||
|
elementType: text
|
||||||
|
bounds: [136, 166, 430, 16]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 一英寸 CMOS · 14 档动态范围 · 4K/240fps · 2 倍无损变焦
|
||||||
|
- elementId: toc1-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 200, 480, 1]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: toc2-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 222, 72, 34]
|
||||||
|
content:
|
||||||
|
fontSize: 26
|
||||||
|
color: "$accent"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>02</strong>"
|
||||||
|
- elementId: toc2-title
|
||||||
|
elementType: text
|
||||||
|
bounds: [136, 222, 420, 30]
|
||||||
|
content:
|
||||||
|
fontSize: 20
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>智能与操控</strong>"
|
||||||
|
- elementId: toc2-sub
|
||||||
|
elementType: text
|
||||||
|
bounds: [136, 256, 430, 16]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 智能跟随 7.0 · 旋屏开拍 · 屏下双按键 · 三轴增稳
|
||||||
|
- elementId: toc2-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 290, 480, 1]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: toc3-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 312, 72, 34]
|
||||||
|
content:
|
||||||
|
fontSize: 26
|
||||||
|
color: "$accent"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>03</strong>"
|
||||||
|
- elementId: toc3-title
|
||||||
|
elementType: text
|
||||||
|
bounds: [136, 312, 420, 30]
|
||||||
|
content:
|
||||||
|
fontSize: 20
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>音频与生态</strong>"
|
||||||
|
- elementId: toc3-sub
|
||||||
|
elementType: text
|
||||||
|
bounds: [136, 346, 430, 16]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: OsmoAudio 四声道 · 107GB 存储 · 240 分钟续航 · 配件
|
||||||
|
- elementId: toc3-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 380, 480, 1]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: toc4-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 402, 72, 34]
|
||||||
|
content:
|
||||||
|
fontSize: 26
|
||||||
|
color: "$accent"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>04</strong>"
|
||||||
|
- elementId: toc4-title
|
||||||
|
elementType: text
|
||||||
|
bounds: [136, 402, 420, 30]
|
||||||
|
content:
|
||||||
|
fontSize: 20
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>规格与售价</strong>"
|
||||||
|
- elementId: toc4-sub
|
||||||
|
elementType: text
|
||||||
|
bounds: [136, 436, 430, 16]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 全参数一览 · 标准套装 ¥2999 · 全能套装 ¥3799
|
||||||
|
- elementId: toc-img
|
||||||
|
elementType: image
|
||||||
|
bounds: [600, 96, 312, 312]
|
||||||
|
src: "media/product_sq.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
- elementId: toc-img-caption
|
||||||
|
elementType: text
|
||||||
|
bounds: [600, 416, 312, 14]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [center, middle]
|
||||||
|
text: Osmo Pocket 4 · 一英寸口袋云台相机
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
pageType: chapter
|
||||||
|
background:
|
||||||
|
type: solid
|
||||||
|
color: "$bg"
|
||||||
|
elements:
|
||||||
|
- elementId: bg-img
|
||||||
|
elementType: image
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
src: "media/vlog_dusk.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
- elementId: dim
|
||||||
|
elementType: shape
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "#0C0C0E80"}
|
||||||
|
- elementId: bottom-grad
|
||||||
|
elementType: shape
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
shapeName: rect
|
||||||
|
fill:
|
||||||
|
type: gradient
|
||||||
|
gradientType: linear
|
||||||
|
angle: 90
|
||||||
|
stops:
|
||||||
|
- {position: 0, color: "#0C0C0E00"}
|
||||||
|
- {position: 1, color: "#0C0C0ED9"}
|
||||||
|
- elementId: ch-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 118, 400, 190]
|
||||||
|
content:
|
||||||
|
fontSize: 150
|
||||||
|
color: "$accent"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>01</strong>"
|
||||||
|
- elementId: ch-bar
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 316, 56, 4]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: ch-eng
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 334, 400, 16]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$accent"
|
||||||
|
letterSpacing: 4
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: IMAGE QUALITY
|
||||||
|
- elementId: ch-title
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 356, 500, 64]
|
||||||
|
content:
|
||||||
|
fontSize: 50
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>影像画质</strong>"
|
||||||
|
- elementId: ch-sub
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 434, 500, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 13
|
||||||
|
color: "#F2F0EACC"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 一英寸大底的全面进化
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [856, 510, 72, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "#F2F0EA99"
|
||||||
|
wrap: false
|
||||||
|
align: [right, middle]
|
||||||
|
text: 04 / 18
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
pageType: content
|
||||||
|
background:
|
||||||
|
type: solid
|
||||||
|
color: "$bg"
|
||||||
|
elements:
|
||||||
|
- elementId: side-img
|
||||||
|
elementType: image
|
||||||
|
bounds: [492, 0, 468, 540]
|
||||||
|
src: "media/night_vlog_bl.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
- elementId: hd-bar
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 34, 4, 12]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: hd-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 32, 260, 15]
|
||||||
|
content:
|
||||||
|
style: "$headerLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: DJI OSMO POCKET 4
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [856, 510, 72, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "#F2F0EA99"
|
||||||
|
wrap: false
|
||||||
|
align: [right, middle]
|
||||||
|
text: 05 / 18
|
||||||
|
- elementId: kicker
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 88, 400, 14]
|
||||||
|
content:
|
||||||
|
style: "$kicker"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 影像画质 IMAGE QUALITY
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 116, 430, 100]
|
||||||
|
content:
|
||||||
|
style: "$pageTitle"
|
||||||
|
align: [left, top]
|
||||||
|
text: |
|
||||||
|
<p><strong>一英寸 CMOS,</strong></p>
|
||||||
|
<p><strong>夜景更纯净</strong></p>
|
||||||
|
- elementId: body
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 236, 396, 96]
|
||||||
|
content:
|
||||||
|
style: "$body"
|
||||||
|
align: [left, top]
|
||||||
|
text: 全新升级的一英寸堆栈式 CMOS,配合 f/2.0 大光圈,感光性能与浅景深效果显著提升——暗光夜景纯净通透,人像虚化柔和立体。
|
||||||
|
- elementId: stats-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 352, 396, 1]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: stat1
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 368, 124, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>f/2.0</strong>"
|
||||||
|
- elementId: stat1-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 398, 124, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 最大光圈
|
||||||
|
- elementId: stat2
|
||||||
|
elementType: text
|
||||||
|
bounds: [184, 368, 124, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>3700 万</strong>"
|
||||||
|
- elementId: stat2-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [184, 398, 124, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 照片像素
|
||||||
|
- elementId: stat3
|
||||||
|
elementType: text
|
||||||
|
bounds: [320, 368, 124, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>7680×4320</strong>"
|
||||||
|
- elementId: stat3-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [320, 398, 124, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 照片最高分辨率
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
pageType: content
|
||||||
|
background:
|
||||||
|
type: solid
|
||||||
|
color: "$bg"
|
||||||
|
elements:
|
||||||
|
- elementId: side-img
|
||||||
|
elementType: image
|
||||||
|
bounds: [0, 0, 468, 540]
|
||||||
|
src: "media/horse_dusk_bl.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
- elementId: hd-bar
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 34, 4, 12]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: hd-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 32, 260, 15]
|
||||||
|
content:
|
||||||
|
style: "$headerLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: DJI OSMO POCKET 4
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [856, 510, 72, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [right, middle]
|
||||||
|
text: 06 / 18
|
||||||
|
- elementId: kicker
|
||||||
|
elementType: text
|
||||||
|
bounds: [536, 88, 376, 14]
|
||||||
|
content:
|
||||||
|
style: "$kicker"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 影像画质 IMAGE QUALITY
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [536, 116, 376, 100]
|
||||||
|
content:
|
||||||
|
style: "$pageTitle"
|
||||||
|
align: [left, top]
|
||||||
|
text: |
|
||||||
|
<p><strong>14 档动态范围,</strong></p>
|
||||||
|
<p><strong>光影层次分明</strong></p>
|
||||||
|
- elementId: body
|
||||||
|
elementType: text
|
||||||
|
bounds: [536, 236, 376, 110]
|
||||||
|
content:
|
||||||
|
style: "$body"
|
||||||
|
align: [left, top]
|
||||||
|
text: 大光比场景下,高光与阴影细节兼得。支持 10-bit D-Log 专业色彩模式(上代为 D-Log M),色彩过渡平滑自然,为后期调色留足电影级空间。
|
||||||
|
- elementId: stats-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [536, 360, 376, 1]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: stat1
|
||||||
|
elementType: text
|
||||||
|
bounds: [536, 376, 120, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>14 档</strong>"
|
||||||
|
- elementId: stat1-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [536, 406, 120, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 动态范围
|
||||||
|
- elementId: stat2
|
||||||
|
elementType: text
|
||||||
|
bounds: [664, 376, 120, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>10-bit</strong>"
|
||||||
|
- elementId: stat2-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [664, 406, 120, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: D-Log 色彩
|
||||||
|
- elementId: stat3
|
||||||
|
elementType: text
|
||||||
|
bounds: [792, 376, 120, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>+2 档</strong>"
|
||||||
|
- elementId: stat3-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [792, 406, 120, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 低光能力提升
|
||||||
|
- elementId: footnote
|
||||||
|
elementType: text
|
||||||
|
bounds: [536, 498, 376, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 数据来源:DJI 官方新闻稿,2026-04
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
pageType: content
|
||||||
|
background:
|
||||||
|
type: solid
|
||||||
|
color: "$bg"
|
||||||
|
elements:
|
||||||
|
- elementId: img1
|
||||||
|
elementType: image
|
||||||
|
bounds: [512, 48, 400, 232]
|
||||||
|
src: "media/slowmo_skate.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
- elementId: img2
|
||||||
|
elementType: image
|
||||||
|
bounds: [512, 296, 400, 196]
|
||||||
|
src: "media/slowmo_rollers.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
- elementId: hd-bar
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 34, 4, 12]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: hd-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 32, 260, 15]
|
||||||
|
content:
|
||||||
|
style: "$headerLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: DJI OSMO POCKET 4
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [856, 510, 72, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [right, middle]
|
||||||
|
text: 07 / 18
|
||||||
|
- elementId: kicker
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 88, 400, 14]
|
||||||
|
content:
|
||||||
|
style: "$kicker"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 影像画质 IMAGE QUALITY
|
||||||
|
- elementId: big-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 112, 420, 132]
|
||||||
|
content:
|
||||||
|
color: "$accent"
|
||||||
|
align: [left, middle]
|
||||||
|
wrap: false
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:104px; color:$accent;"><strong>240</strong></span><span style="font-size:36px; color:$text;"><strong> fps</strong></span></p>
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 262, 420, 40]
|
||||||
|
content:
|
||||||
|
fontSize: 26
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>4K 高帧率,慢放每一瞬</strong>"
|
||||||
|
- elementId: body
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 312, 400, 92]
|
||||||
|
content:
|
||||||
|
style: "$body"
|
||||||
|
align: [left, top]
|
||||||
|
text: 慢动作规格由 4K/120fps 翻倍至 4K/240fps,最高 10 倍慢放——曾专属于万元级电影机的规格,如今装进 190g 的口袋机身。
|
||||||
|
- elementId: stats-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 424, 400, 1]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: stat1
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 440, 124, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>10×</strong>"
|
||||||
|
- elementId: stat1-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 470, 124, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 最高慢放
|
||||||
|
- elementId: stat2
|
||||||
|
elementType: text
|
||||||
|
bounds: [184, 440, 124, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>2×</strong>"
|
||||||
|
- elementId: stat2-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [184, 470, 124, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 较上代提升
|
||||||
|
- elementId: stat3
|
||||||
|
elementType: text
|
||||||
|
bounds: [320, 440, 140, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>4K/120</strong>"
|
||||||
|
- elementId: stat3-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [320, 470, 140, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: Pocket 3 规格
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
pageType: content
|
||||||
|
background:
|
||||||
|
type: solid
|
||||||
|
color: "$bg"
|
||||||
|
elements:
|
||||||
|
- elementId: side-img
|
||||||
|
elementType: image
|
||||||
|
bounds: [528, 48, 384, 444]
|
||||||
|
src: "media/portrait.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
- elementId: hd-bar
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 34, 4, 12]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: hd-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 32, 260, 15]
|
||||||
|
content:
|
||||||
|
style: "$headerLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: DJI OSMO POCKET 4
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [856, 510, 72, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [right, middle]
|
||||||
|
text: 08 / 18
|
||||||
|
- elementId: kicker
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 88, 400, 14]
|
||||||
|
content:
|
||||||
|
style: "$kicker"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 影像画质 IMAGE QUALITY
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 116, 430, 100]
|
||||||
|
content:
|
||||||
|
style: "$pageTitle"
|
||||||
|
align: [left, top]
|
||||||
|
text: |
|
||||||
|
<p><strong>2 倍无损变焦,</strong></p>
|
||||||
|
<p><strong>人像更有电影感</strong></p>
|
||||||
|
- elementId: body
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 236, 400, 130]
|
||||||
|
content:
|
||||||
|
style: "$body"
|
||||||
|
align: [left, top]
|
||||||
|
text: 一键切换 1×/2× 无损变焦,特写人像锐利清晰;最高 4× 数码变焦,远景近物皆在眼前。内置多种胶片影调风格,一键复刻电影质感;新增美肤滤镜,磨皮、美白与肤色冷暖随心调,搭配外置补光灯,逆光暗光也能柔光美颜。
|
||||||
|
- elementId: stats-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 392, 400, 1]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: stat1
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 408, 124, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>2×</strong>"
|
||||||
|
- elementId: stat1-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 438, 124, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 无损变焦
|
||||||
|
- elementId: stat2
|
||||||
|
elementType: text
|
||||||
|
bounds: [184, 408, 124, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>4×</strong>"
|
||||||
|
- elementId: stat2-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [184, 438, 124, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 数码变焦
|
||||||
|
- elementId: stat3
|
||||||
|
elementType: text
|
||||||
|
bounds: [320, 408, 140, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>1 键</strong>"
|
||||||
|
- elementId: stat3-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [320, 438, 140, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 变焦按键切换
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
pageType: chapter
|
||||||
|
background:
|
||||||
|
type: solid
|
||||||
|
color: "$bg"
|
||||||
|
elements:
|
||||||
|
- elementId: bg-img
|
||||||
|
elementType: image
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
src: "media/film_dancer.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
- elementId: dim
|
||||||
|
elementType: shape
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "#0C0C0E80"}
|
||||||
|
- elementId: bottom-grad
|
||||||
|
elementType: shape
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
shapeName: rect
|
||||||
|
fill:
|
||||||
|
type: gradient
|
||||||
|
gradientType: linear
|
||||||
|
angle: 90
|
||||||
|
stops:
|
||||||
|
- {position: 0, color: "#0C0C0E00"}
|
||||||
|
- {position: 1, color: "#0C0C0ED9"}
|
||||||
|
- elementId: ch-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 118, 400, 190]
|
||||||
|
content:
|
||||||
|
fontSize: 150
|
||||||
|
color: "$accent"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>02</strong>"
|
||||||
|
- elementId: ch-bar
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 316, 56, 4]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: ch-eng
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 334, 400, 16]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$accent"
|
||||||
|
letterSpacing: 4
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: SMART & CONTROL
|
||||||
|
- elementId: ch-title
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 356, 500, 64]
|
||||||
|
content:
|
||||||
|
fontSize: 50
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>智能与操控</strong>"
|
||||||
|
- elementId: ch-sub
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 434, 500, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 13
|
||||||
|
color: "#F2F0EACC"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 一个人,就是一支摄制组
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [856, 510, 72, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "#F2F0EA99"
|
||||||
|
wrap: false
|
||||||
|
align: [right, middle]
|
||||||
|
text: 09 / 18
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
pageType: content
|
||||||
|
background:
|
||||||
|
type: solid
|
||||||
|
color: "$bg"
|
||||||
|
elements:
|
||||||
|
- elementId: side-img
|
||||||
|
elementType: image
|
||||||
|
bounds: [528, 48, 384, 444]
|
||||||
|
src: "media/activetrack.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
- elementId: hd-bar
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 34, 4, 12]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: hd-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 32, 260, 15]
|
||||||
|
content:
|
||||||
|
style: "$headerLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: DJI OSMO POCKET 4
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [856, 510, 72, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [right, middle]
|
||||||
|
text: 10 / 18
|
||||||
|
- elementId: kicker
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 88, 400, 14]
|
||||||
|
content:
|
||||||
|
style: "$kicker"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 智能与操控 SMART & CONTROL
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 116, 430, 100]
|
||||||
|
content:
|
||||||
|
style: "$pageTitle"
|
||||||
|
align: [left, top]
|
||||||
|
text: |
|
||||||
|
<p><strong>智能跟随 7.0,</strong></p>
|
||||||
|
<p><strong>镜头锁定主角</strong></p>
|
||||||
|
- elementId: body
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 236, 400, 136]
|
||||||
|
content:
|
||||||
|
style: "$body"
|
||||||
|
align: [left, top]
|
||||||
|
text: ActiveTrack 7.0 支持最高 4 倍变焦跟随,人群中也能精准锁定主体。开启「锁定主角追焦」,镜头自动锁定目标并持续跟焦,轻点屏幕即可切换对象;「登记主角优先」,让重要的人始终清晰。挥挥手,手势控制即刻开拍。
|
||||||
|
- elementId: stats-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 396, 400, 1]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: stat1
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 412, 124, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>7.0</strong>"
|
||||||
|
- elementId: stat1-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 442, 124, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 智能跟随
|
||||||
|
- elementId: stat2
|
||||||
|
elementType: text
|
||||||
|
bounds: [184, 412, 124, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>4×</strong>"
|
||||||
|
- elementId: stat2-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [184, 442, 124, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 变焦跟随
|
||||||
|
- elementId: stat3
|
||||||
|
elementType: text
|
||||||
|
bounds: [320, 412, 140, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>0 接触</strong>"
|
||||||
|
- elementId: stat3-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [320, 442, 140, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 手势控制开拍
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
pageType: content
|
||||||
|
background:
|
||||||
|
type: solid
|
||||||
|
color: "$bg"
|
||||||
|
elements:
|
||||||
|
- elementId: side-img
|
||||||
|
elementType: image
|
||||||
|
bounds: [528, 48, 384, 444]
|
||||||
|
src: "media/inhand.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
- elementId: hd-bar
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 34, 4, 12]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: hd-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 32, 260, 15]
|
||||||
|
content:
|
||||||
|
style: "$headerLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: DJI OSMO POCKET 4
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [856, 510, 72, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [right, middle]
|
||||||
|
text: 11 / 18
|
||||||
|
- elementId: kicker
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 88, 400, 14]
|
||||||
|
content:
|
||||||
|
style: "$kicker"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 智能与操控 SMART & CONTROL
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 116, 460, 52]
|
||||||
|
content:
|
||||||
|
style: "$pageTitle"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>轻转屏幕,即刻开拍</strong>"
|
||||||
|
- elementId: body
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 180, 420, 74]
|
||||||
|
content:
|
||||||
|
style: "$body"
|
||||||
|
align: [left, top]
|
||||||
|
text: 三轴云台随旋屏即刻响应,横竖拍无缝切换;2.0 英寸 OLED 触控屏,1000 尼特亮度,阳光下依然清晰可见。
|
||||||
|
- elementId: f1-name
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 272, 400, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 15
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>屏下双按键</strong>"
|
||||||
|
- elementId: f1-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 298, 420, 16]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 变焦键单击切换 1×/2×、双击 4×;自定义键长按触发预设
|
||||||
|
- elementId: f1-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 326, 420, 1]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: f2-name
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 348, 400, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 15
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>五维摇杆</strong>"
|
||||||
|
- elementId: f2-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 374, 420, 16]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 单击返回菜单,双击云台回中,三击切换前后镜头朝向
|
||||||
|
- elementId: f2-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 402, 420, 1]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: f3-name
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 424, 400, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 15
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>旋转屏开拍</strong>"
|
||||||
|
- elementId: f3-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 450, 420, 16]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 轻转屏幕云台即刻响应,竖拍横拍随心切换
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
pageType: content
|
||||||
|
background:
|
||||||
|
type: solid
|
||||||
|
color: "$bg"
|
||||||
|
elements:
|
||||||
|
- elementId: side-img
|
||||||
|
elementType: image
|
||||||
|
bounds: [0, 0, 468, 540]
|
||||||
|
src: "media/tripod_street_bl.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
- elementId: hd-bar
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 34, 4, 12]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: hd-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 32, 260, 15]
|
||||||
|
content:
|
||||||
|
style: "$headerLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: DJI OSMO POCKET 4
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [856, 510, 72, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [right, middle]
|
||||||
|
text: 12 / 18
|
||||||
|
- elementId: kicker
|
||||||
|
elementType: text
|
||||||
|
bounds: [536, 88, 376, 14]
|
||||||
|
content:
|
||||||
|
style: "$kicker"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 智能与操控 SMART & CONTROL
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [536, 116, 376, 100]
|
||||||
|
content:
|
||||||
|
style: "$pageTitle"
|
||||||
|
align: [left, top]
|
||||||
|
text: |
|
||||||
|
<p><strong>三轴机械增稳,</strong></p>
|
||||||
|
<p><strong>稳如滑轨</strong></p>
|
||||||
|
- elementId: body
|
||||||
|
elementType: text
|
||||||
|
bounds: [536, 236, 376, 116]
|
||||||
|
content:
|
||||||
|
style: "$body"
|
||||||
|
align: [left, top]
|
||||||
|
text: 三轴机械增稳云台,增稳精度 ±0.005°,最大操控转速 180°/s。走动、奔跑、车载跟拍,画面始终平稳顺滑——机械增稳不裁切画面,画质零损失。
|
||||||
|
- elementId: stats-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [536, 364, 376, 1]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: stat1
|
||||||
|
elementType: text
|
||||||
|
bounds: [536, 380, 120, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>±0.005°</strong>"
|
||||||
|
- elementId: stat1-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [536, 410, 120, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 增稳精度
|
||||||
|
- elementId: stat2
|
||||||
|
elementType: text
|
||||||
|
bounds: [664, 380, 120, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>180°/s</strong>"
|
||||||
|
- elementId: stat2-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [664, 410, 120, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 最大操控转速
|
||||||
|
- elementId: stat3
|
||||||
|
elementType: text
|
||||||
|
bounds: [792, 380, 120, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>-120°~70°</strong>"
|
||||||
|
- elementId: stat3-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [792, 410, 120, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 云台俯仰范围
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
pageType: chapter
|
||||||
|
background:
|
||||||
|
type: solid
|
||||||
|
color: "$bg"
|
||||||
|
elements:
|
||||||
|
- elementId: bg-img
|
||||||
|
elementType: image
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
src: "media/picnic.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
- elementId: dim
|
||||||
|
elementType: shape
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "#0C0C0E99"}
|
||||||
|
- elementId: bottom-grad
|
||||||
|
elementType: shape
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
shapeName: rect
|
||||||
|
fill:
|
||||||
|
type: gradient
|
||||||
|
gradientType: linear
|
||||||
|
angle: 90
|
||||||
|
stops:
|
||||||
|
- {position: 0, color: "#0C0C0E00"}
|
||||||
|
- {position: 1, color: "#0C0C0ED9"}
|
||||||
|
- elementId: ch-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 118, 400, 190]
|
||||||
|
content:
|
||||||
|
fontSize: 150
|
||||||
|
color: "$accent"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>03</strong>"
|
||||||
|
- elementId: ch-bar
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 316, 56, 4]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: ch-eng
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 334, 400, 16]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$accent"
|
||||||
|
letterSpacing: 4
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: AUDIO & ECOSYSTEM
|
||||||
|
- elementId: ch-title
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 356, 500, 64]
|
||||||
|
content:
|
||||||
|
fontSize: 50
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>音频与生态</strong>"
|
||||||
|
- elementId: ch-sub
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 434, 560, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 13
|
||||||
|
color: "#F2F0EACC"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 从声音、存储到续航的全链路体验
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [856, 510, 72, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "#F2F0EA99"
|
||||||
|
wrap: false
|
||||||
|
align: [right, middle]
|
||||||
|
text: 13 / 18
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
pageType: content
|
||||||
|
background:
|
||||||
|
type: solid
|
||||||
|
color: "$bg"
|
||||||
|
elements:
|
||||||
|
- elementId: img1
|
||||||
|
elementType: image
|
||||||
|
bounds: [520, 110, 392, 186]
|
||||||
|
src: "media/osmoaudio.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
- elementId: img2
|
||||||
|
elementType: image
|
||||||
|
bounds: [520, 312, 392, 180]
|
||||||
|
src: "media/mic3.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
- elementId: hd-bar
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 34, 4, 12]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: hd-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 32, 260, 15]
|
||||||
|
content:
|
||||||
|
style: "$headerLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: DJI OSMO POCKET 4
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [856, 510, 72, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [right, middle]
|
||||||
|
text: 14 / 18
|
||||||
|
- elementId: kicker
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 88, 400, 14]
|
||||||
|
content:
|
||||||
|
style: "$kicker"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 音频与生态 AUDIO & ECOSYSTEM
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 116, 430, 100]
|
||||||
|
content:
|
||||||
|
style: "$pageTitle"
|
||||||
|
align: [left, top]
|
||||||
|
text: |
|
||||||
|
<p><strong>OsmoAudio,</strong></p>
|
||||||
|
<p><strong>声临其境</strong></p>
|
||||||
|
- elementId: body
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 236, 400, 136]
|
||||||
|
content:
|
||||||
|
style: "$body"
|
||||||
|
align: [left, top]
|
||||||
|
text: OsmoAudio 直连 DJI Mic 3 发射器,无需接收器即可四声道同步录制;新增空间音频与变焦拾音,声音随画面远近变化,临场感倍增。机身三麦克风阵列,48kHz 16-bit 高清收音。
|
||||||
|
- elementId: stats-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 396, 400, 1]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: stat1
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 412, 124, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>4 声道</strong>"
|
||||||
|
- elementId: stat1-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 442, 124, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 同步录制
|
||||||
|
- elementId: stat2
|
||||||
|
elementType: text
|
||||||
|
bounds: [184, 412, 124, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>空间音频</strong>"
|
||||||
|
- elementId: stat2-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [184, 442, 124, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 变焦拾音
|
||||||
|
- elementId: stat3
|
||||||
|
elementType: text
|
||||||
|
bounds: [320, 412, 140, 26]
|
||||||
|
content:
|
||||||
|
style: "$statNum"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>Mic 3</strong>"
|
||||||
|
- elementId: stat3-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [320, 442, 140, 14]
|
||||||
|
content:
|
||||||
|
style: "$statLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 无线直连
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
pageType: content
|
||||||
|
background:
|
||||||
|
type: solid
|
||||||
|
color: "$bg"
|
||||||
|
elements:
|
||||||
|
- elementId: hd-bar
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 34, 4, 12]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: hd-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 32, 260, 15]
|
||||||
|
content:
|
||||||
|
style: "$headerLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: DJI OSMO POCKET 4
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [856, 510, 72, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [right, middle]
|
||||||
|
text: 15 / 18
|
||||||
|
- elementId: kicker
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 88, 400, 14]
|
||||||
|
content:
|
||||||
|
style: "$kicker"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 音频与生态 AUDIO & ECOSYSTEM
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 114, 500, 48]
|
||||||
|
content:
|
||||||
|
fontSize: 34
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>存储与续航,一步到位</strong>"
|
||||||
|
- elementId: grid-vline
|
||||||
|
elementType: shape
|
||||||
|
bounds: [479, 202, 1, 276]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: grid-hline
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 336, 864, 1]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: c1-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 210, 420, 56]
|
||||||
|
content:
|
||||||
|
fontSize: 48
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><strong>107<span style="font-size:22px; color:$accent;"> GB</span></strong></p>
|
||||||
|
- elementId: c1-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 278, 420, 36]
|
||||||
|
content:
|
||||||
|
fontSize: 11.5
|
||||||
|
color: "$muted"
|
||||||
|
lineHeight: 1.55
|
||||||
|
align: [left, top]
|
||||||
|
text: 内置存储空间,另支持 microSD 卡扩展至 1TB
|
||||||
|
- elementId: c2-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [504, 210, 408, 56]
|
||||||
|
content:
|
||||||
|
fontSize: 48
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><strong>800<span style="font-size:22px; color:$accent;"> MB/s</span></strong></p>
|
||||||
|
- elementId: c2-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [504, 278, 408, 36]
|
||||||
|
content:
|
||||||
|
fontSize: 11.5
|
||||||
|
color: "$muted"
|
||||||
|
lineHeight: 1.55
|
||||||
|
align: [left, top]
|
||||||
|
text: USB 3.1 有线极速导出,Wi-Fi 6 无线传输
|
||||||
|
- elementId: c3-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 356, 420, 56]
|
||||||
|
content:
|
||||||
|
fontSize: 48
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><strong>240<span style="font-size:22px; color:$accent;"> min</span></strong></p>
|
||||||
|
- elementId: c3-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 424, 420, 36]
|
||||||
|
content:
|
||||||
|
fontSize: 11.5
|
||||||
|
color: "$muted"
|
||||||
|
lineHeight: 1.55
|
||||||
|
align: [left, top]
|
||||||
|
text: 满电连续拍摄时长,全天创作不断电
|
||||||
|
- elementId: c4-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [504, 356, 408, 56]
|
||||||
|
content:
|
||||||
|
fontSize: 48
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><strong>18<span style="font-size:22px; color:$accent;"> min</span></strong></p>
|
||||||
|
- elementId: c4-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [504, 424, 408, 36]
|
||||||
|
content:
|
||||||
|
fontSize: 11.5
|
||||||
|
color: "$muted"
|
||||||
|
lineHeight: 1.55
|
||||||
|
align: [left, top]
|
||||||
|
text: PD 快充充至 80%,32 分钟充满 100%
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
pageType: content
|
||||||
|
background:
|
||||||
|
type: solid
|
||||||
|
color: "$bg"
|
||||||
|
elements:
|
||||||
|
- elementId: acc-img
|
||||||
|
elementType: image
|
||||||
|
bounds: [48, 108, 500, 364]
|
||||||
|
src: "media/accessories.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
- elementId: hd-bar
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 34, 4, 12]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: hd-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 32, 260, 15]
|
||||||
|
content:
|
||||||
|
style: "$headerLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: DJI OSMO POCKET 4
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [856, 510, 72, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [right, middle]
|
||||||
|
text: 16 / 18
|
||||||
|
- elementId: kicker
|
||||||
|
elementType: text
|
||||||
|
bounds: [588, 88, 324, 14]
|
||||||
|
content:
|
||||||
|
style: "$kicker"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 音频与生态 AUDIO & ECOSYSTEM
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [588, 114, 324, 44]
|
||||||
|
content:
|
||||||
|
fontSize: 30
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>配件全家桶</strong>"
|
||||||
|
- elementId: body
|
||||||
|
elementType: text
|
||||||
|
bounds: [588, 166, 324, 44]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$muted"
|
||||||
|
lineHeight: 1.65
|
||||||
|
align: [left, top]
|
||||||
|
text: 丰富配件覆盖补光、收音、续航与收纳,按需组合,适配多元创作场景。
|
||||||
|
- elementId: a1-mark
|
||||||
|
elementType: shape
|
||||||
|
bounds: [588, 234, 12, 2]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: a1
|
||||||
|
elementType: text
|
||||||
|
bounds: [608, 226, 304, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 12.5
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: DJI Mic 3 发射器
|
||||||
|
- elementId: a2-mark
|
||||||
|
elementType: shape
|
||||||
|
bounds: [588, 262, 12, 2]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: a2
|
||||||
|
elementType: text
|
||||||
|
bounds: [608, 254, 304, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 12.5
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 外置补光灯 · 美颜柔光镜
|
||||||
|
- elementId: a3-mark
|
||||||
|
elementType: shape
|
||||||
|
bounds: [588, 290, 12, 2]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: a3
|
||||||
|
elementType: text
|
||||||
|
bounds: [608, 282, 304, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 12.5
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 增广镜 · 磁吸 ND 镜套装
|
||||||
|
- elementId: a4-mark
|
||||||
|
elementType: shape
|
||||||
|
bounds: [588, 318, 12, 2]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: a4
|
||||||
|
elementType: text
|
||||||
|
bounds: [608, 310, 304, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 12.5
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 迷你三脚架 · 拓展转接件
|
||||||
|
- elementId: a5-mark
|
||||||
|
elementType: shape
|
||||||
|
bounds: [588, 346, 12, 2]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: a5
|
||||||
|
elementType: text
|
||||||
|
bounds: [608, 338, 304, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 12.5
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 续航手柄 · 1/4″ 螺纹手柄
|
||||||
|
- elementId: a6-mark
|
||||||
|
elementType: shape
|
||||||
|
bounds: [588, 374, 12, 2]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: a6
|
||||||
|
elementType: text
|
||||||
|
bounds: [608, 366, 304, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 12.5
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 收纳包 · 保护壳 · 云台夹
|
||||||
|
- elementId: acc-note
|
||||||
|
elementType: text
|
||||||
|
bounds: [588, 416, 324, 56]
|
||||||
|
content:
|
||||||
|
fontSize: 10.5
|
||||||
|
color: "$muted"
|
||||||
|
lineHeight: 1.6
|
||||||
|
align: [left, top]
|
||||||
|
text: 全能套装一站配齐核心配件;所有配件均可在 DJI 官方商城单独选购。
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
pageType: content
|
||||||
|
background:
|
||||||
|
type: solid
|
||||||
|
color: "$bg"
|
||||||
|
elements:
|
||||||
|
- elementId: hd-bar
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 34, 4, 12]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: hd-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 32, 260, 15]
|
||||||
|
content:
|
||||||
|
style: "$headerLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: DJI OSMO POCKET 4
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [856, 510, 72, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [right, middle]
|
||||||
|
text: 17 / 18
|
||||||
|
- elementId: kicker
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 88, 400, 14]
|
||||||
|
content:
|
||||||
|
style: "$kicker"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 规格与售价 SPECS & PRICE
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 114, 500, 44]
|
||||||
|
content:
|
||||||
|
fontSize: 34
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>技术规格全览</strong>"
|
||||||
|
- elementId: spec-table
|
||||||
|
elementType: table
|
||||||
|
bounds: [48, 176, 540, 322]
|
||||||
|
columnWidths: [0.3, 0.7]
|
||||||
|
rowHeights: [0.083, 0.083, 0.083, 0.083, 0.083, 0.083, 0.083, 0.083, 0.084, 0.084, 0.084, 0.084]
|
||||||
|
style:
|
||||||
|
fontSize: 10.5
|
||||||
|
bodyColor: "$text"
|
||||||
|
headerBold: false
|
||||||
|
firstColumnColor: "$muted"
|
||||||
|
border: [null, null, {style: solid, width: 0.75, color: "$line"}, null]
|
||||||
|
rows:
|
||||||
|
- - content: {text: 机身尺寸, align: [left, middle]}
|
||||||
|
- content: {text: 144.2 × 44.4 × 33.5 mm, align: [left, middle]}
|
||||||
|
- - content: {text: 机身重量, align: [left, middle]}
|
||||||
|
- content: {text: 190.5 g, align: [left, middle]}
|
||||||
|
- - content: {text: 传感器, align: [left, middle]}
|
||||||
|
- content: {text: 1 英寸堆栈式 CMOS, align: [left, middle]}
|
||||||
|
- - content: {text: 镜头, align: [left, middle]}
|
||||||
|
- content: {text: 等效 20 mm · f/2.0 光圈, align: [left, middle]}
|
||||||
|
- - content: {text: 照片, align: [left, middle]}
|
||||||
|
- content: {text: 约 3700 万像素(7680 × 4320), align: [left, middle]}
|
||||||
|
- - content: {text: 视频, align: [left, middle]}
|
||||||
|
- content: {text: 4K/240fps 慢动作 · 1080p/240fps, align: [left, middle]}
|
||||||
|
- - content: {text: 动态范围, align: [left, middle]}
|
||||||
|
- content: {text: 14 档 · 10-bit D-Log 色彩, align: [left, middle]}
|
||||||
|
- - content: {text: 屏幕, align: [left, middle]}
|
||||||
|
- content: {text: 2.0″ OLED 触控旋转屏 · 1000 尼特, align: [left, middle]}
|
||||||
|
- - content: {text: 存储, align: [left, middle]}
|
||||||
|
- content: {text: 内置 107GB · microSD 扩展至 1TB, align: [left, middle]}
|
||||||
|
- - content: {text: 电池, align: [left, middle]}
|
||||||
|
- content: {text: 1545mAh · 240 分钟续航 · 18 分钟充至 80%, align: [left, middle]}
|
||||||
|
- - content: {text: 传输, align: [left, middle]}
|
||||||
|
- content: {text: USB 3.1 800MB/s · Wi-Fi 6 · 蓝牙, align: [left, middle]}
|
||||||
|
- - content: {text: 音频, align: [left, middle]}
|
||||||
|
- content: {text: 三麦克风 · OsmoAudio 四声道, align: [left, middle]}
|
||||||
|
- elementId: product-img
|
||||||
|
elementType: image
|
||||||
|
bounds: [628, 176, 284, 160]
|
||||||
|
src: "media/product_black.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
- elementId: product-caption
|
||||||
|
elementType: text
|
||||||
|
bounds: [628, 344, 284, 14]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [center, middle]
|
||||||
|
text: Osmo Pocket 4 标准套装
|
||||||
|
- elementId: product-price
|
||||||
|
elementType: text
|
||||||
|
bounds: [628, 366, 284, 44]
|
||||||
|
content:
|
||||||
|
color: "$accent"
|
||||||
|
align: [center, middle]
|
||||||
|
wrap: false
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:30px; color:$accent;"><strong>¥2999</strong></span><span style="font-size:12px; color:$muted;"> 起</span></p>
|
||||||
|
- elementId: product-note
|
||||||
|
elementType: text
|
||||||
|
bounds: [628, 420, 284, 60]
|
||||||
|
content:
|
||||||
|
fontSize: 10
|
||||||
|
color: "$muted"
|
||||||
|
lineHeight: 1.6
|
||||||
|
align: [center, top]
|
||||||
|
text: |
|
||||||
|
<p style="text-align:center">2026.04.16 国行现货开售</p>
|
||||||
|
<p style="text-align:center">全球其他地区 04.22 起发售</p>
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
pageType: final
|
||||||
|
background:
|
||||||
|
type: solid
|
||||||
|
color: "$bg"
|
||||||
|
elements:
|
||||||
|
- elementId: hd-bar
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 34, 4, 12]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$accent"}
|
||||||
|
- elementId: hd-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 32, 260, 15]
|
||||||
|
content:
|
||||||
|
style: "$headerLabel"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: DJI OSMO POCKET 4
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [856, 510, 72, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [right, middle]
|
||||||
|
text: 18 / 18
|
||||||
|
- elementId: kicker
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 88, 400, 14]
|
||||||
|
content:
|
||||||
|
style: "$kicker"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 规格与售价 SPECS & PRICE
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 114, 560, 48]
|
||||||
|
content:
|
||||||
|
fontSize: 34
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>2999 元起,增配降价</strong>"
|
||||||
|
- elementId: p1-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 196, 200, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 15
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>标准套装</strong>"
|
||||||
|
- elementId: p1-price
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 224, 300, 56]
|
||||||
|
content:
|
||||||
|
fontSize: 44
|
||||||
|
color: "$accent"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>¥2999</strong>"
|
||||||
|
- elementId: p1-items
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 290, 380, 60]
|
||||||
|
content:
|
||||||
|
fontSize: 10.5
|
||||||
|
color: "$muted"
|
||||||
|
lineHeight: 1.7
|
||||||
|
align: [left, top]
|
||||||
|
text: |
|
||||||
|
<p>主相机 · 云台夹 · 1/4″ 螺纹手柄</p>
|
||||||
|
<p>便携收纳袋 · USB-C PD 快充线(USB 3.1)</p>
|
||||||
|
- elementId: price-vline
|
||||||
|
elementType: shape
|
||||||
|
bounds: [479, 196, 1, 154]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: p2-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 196, 200, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 15
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>全能套装</strong>"
|
||||||
|
- elementId: p2-price
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 224, 300, 56]
|
||||||
|
content:
|
||||||
|
fontSize: 44
|
||||||
|
color: "$accent"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: "<strong>¥3799</strong>"
|
||||||
|
- elementId: p2-items
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 290, 392, 60]
|
||||||
|
content:
|
||||||
|
fontSize: 10.5
|
||||||
|
color: "$muted"
|
||||||
|
lineHeight: 1.7
|
||||||
|
align: [left, top]
|
||||||
|
text: |
|
||||||
|
<p>含标准套装全部配件,另加 DJI Mic 3 发射器、</p>
|
||||||
|
<p>补光灯、增广镜、迷你三脚架、收纳包</p>
|
||||||
|
- elementId: mid-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [48, 384, 864, 1]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$line"}
|
||||||
|
- elementId: closing
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 406, 864, 48]
|
||||||
|
content:
|
||||||
|
fontSize: 28
|
||||||
|
color: "$text"
|
||||||
|
wrap: false
|
||||||
|
align: [center, middle]
|
||||||
|
text: "<strong>大片生活,即刻呈现。</strong>"
|
||||||
|
- elementId: teaser
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 462, 864, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 13
|
||||||
|
color: "$accent"
|
||||||
|
wrap: false
|
||||||
|
align: [center, middle]
|
||||||
|
text: 彩蛋 · 双摄版 Osmo Pocket 4P,年内登场
|
||||||
|
- elementId: source
|
||||||
|
elementType: text
|
||||||
|
bounds: [48, 510, 720, 12]
|
||||||
|
content:
|
||||||
|
fontSize: 9
|
||||||
|
color: "$muted"
|
||||||
|
wrap: false
|
||||||
|
align: [left, middle]
|
||||||
|
text: 资料来源:DJI 官网新闻稿(2026-04-16)· DJI 官方商城 · 中关村在线;图片来自 DJI 官方 CDN
|
||||||
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 285 KiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 477 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 249 KiB |
@@ -0,0 +1,80 @@
|
|||||||
|
pageType: cover
|
||||||
|
background:
|
||||||
|
type: image
|
||||||
|
src: "media/bg_cover.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
elements:
|
||||||
|
- elementId: overlay
|
||||||
|
elementType: shape
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
shapeName: rect
|
||||||
|
fill:
|
||||||
|
type: gradient
|
||||||
|
gradientType: linear
|
||||||
|
angle: 0
|
||||||
|
stops:
|
||||||
|
- {position: 0, color: "#000000F2"}
|
||||||
|
- {position: 0.5, color: "#00000099"}
|
||||||
|
- {position: 1, color: "#00000000"}
|
||||||
|
- elementId: meta-top
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 46, 500, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$w85"
|
||||||
|
letterSpacing: 3
|
||||||
|
text: "XIAOMI EV · 小米汽车"
|
||||||
|
- elementId: accent-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [60, 86, 56, 3]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$primary"}
|
||||||
|
- elementId: cover-title
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 150, 660, 130]
|
||||||
|
content:
|
||||||
|
style: "$coverTitle"
|
||||||
|
align: [left, middle]
|
||||||
|
text: "小米 YU7"
|
||||||
|
- elementId: cover-en
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 282, 660, 34]
|
||||||
|
content:
|
||||||
|
fontSize: 20
|
||||||
|
color: "$w70"
|
||||||
|
letterSpacing: 10
|
||||||
|
text: "XIAOMI YU7"
|
||||||
|
- elementId: slogan
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 330, 640, 46]
|
||||||
|
content:
|
||||||
|
fontSize: 17
|
||||||
|
color: "$white"
|
||||||
|
letterSpacing: 2
|
||||||
|
text: "御风而行 · 豪华高性能纯电 SUV"
|
||||||
|
- elementId: desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 380, 560, 44]
|
||||||
|
content:
|
||||||
|
fontSize: 13
|
||||||
|
color: "$w70"
|
||||||
|
lineHeight: 1.7
|
||||||
|
text: |
|
||||||
|
传承家族设计 · 澎湃性能 · 越级智能 · 全维安全
|
||||||
|
重新定义豪华高性能 SUV 新标准
|
||||||
|
- elementId: bottom-meta
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 488, 500, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$w60"
|
||||||
|
letterSpacing: 1
|
||||||
|
text: "2025.06 上市 · 小米汽车科技有限公司"
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [850, 500, 60, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$w50"
|
||||||
|
align: [right, middle]
|
||||||
|
text: "01 / 08"
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
pageType: content
|
||||||
|
background:
|
||||||
|
type: image
|
||||||
|
src: "media/bg_data.png"
|
||||||
|
fit: {mode: cover}
|
||||||
|
elements:
|
||||||
|
- elementId: overlay
|
||||||
|
elementType: shape
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
shapeName: rect
|
||||||
|
fill:
|
||||||
|
type: gradient
|
||||||
|
gradientType: linear
|
||||||
|
angle: 90
|
||||||
|
stops:
|
||||||
|
- {position: 0, color: "#000000E6"}
|
||||||
|
- {position: 0.65, color: "#000000B3"}
|
||||||
|
- {position: 1, color: "#00000080"}
|
||||||
|
- elementId: label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 48, 500, 22]
|
||||||
|
content:
|
||||||
|
style: "$label"
|
||||||
|
text: "YU7 AT A GLANCE · 一图看懂"
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 88, 700, 58]
|
||||||
|
content:
|
||||||
|
style: "$pageTitle"
|
||||||
|
text: "核心实力,一眼读懂"
|
||||||
|
- elementId: subtitle
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 148, 760, 26]
|
||||||
|
content:
|
||||||
|
fontSize: 15
|
||||||
|
color: "$w70"
|
||||||
|
text: "五大核心指标,重新定义豪华高性能纯电 SUV 的标准"
|
||||||
|
- elementId: divider
|
||||||
|
elementType: line
|
||||||
|
bounds: [60, 196, 840, 1]
|
||||||
|
viewBox: [840, 1]
|
||||||
|
points: "0,0.5 840,0.5"
|
||||||
|
border: {style: solid, width: 1, color: "#FFFFFF2E"}
|
||||||
|
- elementId: stat1-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [60, 236, 40, 3]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$primary"}
|
||||||
|
- elementId: stat2-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [230, 236, 40, 3]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$primary"}
|
||||||
|
- elementId: stat3-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [400, 236, 40, 3]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$primary"}
|
||||||
|
- elementId: stat4-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [570, 236, 40, 3]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$primary"}
|
||||||
|
- elementId: stat5-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [740, 236, 40, 3]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$primary"}
|
||||||
|
- elementId: stat1
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 252, 150, 60]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:38px; color:#FFFFFF; font-weight:700;">23.35</span><span style="font-size:16px; color:#FFFFFFB3;">万起</span></p>
|
||||||
|
- elementId: stat1-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 318, 150, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w85"
|
||||||
|
text: "起售价格"
|
||||||
|
- elementId: stat1-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 344, 152, 40]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$w50"
|
||||||
|
lineHeight: 1.5
|
||||||
|
text: "官网在售价(2026.08)"
|
||||||
|
- elementId: stat2
|
||||||
|
elementType: text
|
||||||
|
bounds: [230, 252, 150, 60]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:38px; color:#FFFFFF; font-weight:700;">835</span><span style="font-size:16px; color:#FFFFFFB3;">km</span></p>
|
||||||
|
- elementId: stat2-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [230, 318, 150, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w85"
|
||||||
|
text: "CLTC 最高续航"
|
||||||
|
- elementId: stat2-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [230, 344, 152, 40]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$w50"
|
||||||
|
lineHeight: 1.5
|
||||||
|
text: "全系长续航,入门即长续航"
|
||||||
|
- elementId: stat3
|
||||||
|
elementType: text
|
||||||
|
bounds: [400, 252, 150, 60]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:38px; color:#FFFFFF; font-weight:700;">897</span><span style="font-size:16px; color:#FFFFFFB3;">V</span></p>
|
||||||
|
- elementId: stat3-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [400, 318, 150, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w85"
|
||||||
|
text: "Max 峰值电压"
|
||||||
|
- elementId: stat3-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [400, 344, 152, 40]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$w50"
|
||||||
|
lineHeight: 1.5
|
||||||
|
text: "全系碳化硅高压平台"
|
||||||
|
- elementId: stat4
|
||||||
|
elementType: text
|
||||||
|
bounds: [570, 252, 150, 60]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:38px; color:#FFFFFF; font-weight:700;">0.245</span></p>
|
||||||
|
- elementId: stat4-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [570, 318, 150, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w85"
|
||||||
|
text: "超低风阻系数"
|
||||||
|
- elementId: stat4-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [570, 344, 152, 40]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$w50"
|
||||||
|
lineHeight: 1.5
|
||||||
|
text: "10 组风道 · 40+ 处优化"
|
||||||
|
- elementId: stat5
|
||||||
|
elementType: text
|
||||||
|
bounds: [740, 252, 150, 60]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:38px; color:#FFFFFF; font-weight:700;">3944</span><span style="font-size:16px; color:#FFFFFFB3;">km</span></p>
|
||||||
|
- elementId: stat5-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [740, 318, 150, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w85"
|
||||||
|
text: "24h 耐力挑战"
|
||||||
|
- elementId: stat5-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [740, 344, 152, 40]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$w50"
|
||||||
|
lineHeight: 1.5
|
||||||
|
text: "中汽中心·华诚认证"
|
||||||
|
- elementId: note
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 468, 840, 40]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$w50"
|
||||||
|
lineHeight: 1.6
|
||||||
|
text: |
|
||||||
|
数据来源:<a href="https://www.xiaomiev.com/yu7">小米汽车官网 xiaomiev.com/yu7</a>,2026 年 8 月抓取。续航、电压等数据来自小米汽车实验室,配置区分车型版本。
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [850, 500, 60, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$w50"
|
||||||
|
align: [right, middle]
|
||||||
|
text: "02 / 08"
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
pageType: content
|
||||||
|
background:
|
||||||
|
type: image
|
||||||
|
src: "media/bg_exterior.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
elements:
|
||||||
|
- elementId: overlay
|
||||||
|
elementType: shape
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
shapeName: rect
|
||||||
|
fill:
|
||||||
|
type: gradient
|
||||||
|
gradientType: linear
|
||||||
|
angle: 0
|
||||||
|
stops:
|
||||||
|
- {position: 0, color: "#000000F0"}
|
||||||
|
- {position: 0.6, color: "#0000008C"}
|
||||||
|
- {position: 1, color: "#00000000"}
|
||||||
|
- elementId: label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 48, 500, 22]
|
||||||
|
content:
|
||||||
|
style: "$label"
|
||||||
|
text: "EXTERIOR DESIGN · 外观设计"
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 88, 640, 56]
|
||||||
|
content:
|
||||||
|
style: "$pageTitle"
|
||||||
|
text: "优雅动感,一脉相承"
|
||||||
|
- elementId: lead
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 148, 560, 44]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w70"
|
||||||
|
lineHeight: 1.7
|
||||||
|
text: |
|
||||||
|
1:3 头身比与舒展的宽体低趴姿态,搭配流畅车身曲线;
|
||||||
|
镂空水滴大灯与光环尾灯,一眼经典,再看独特。
|
||||||
|
- elementId: row1-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 232, 220, 52]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:36px; color:#FFFFFF; font-weight:700;">1:3</span><span style="font-size:15px; color:#FFFFFFB3;"> 头身比</span></p>
|
||||||
|
- elementId: row1-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [300, 236, 380, 44]
|
||||||
|
content:
|
||||||
|
fontSize: 13
|
||||||
|
color: "$w70"
|
||||||
|
lineHeight: 1.6
|
||||||
|
text: "延续汽车工业沉淀百年的长车头豪华比例,蓄势待发"
|
||||||
|
- elementId: row2-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 302, 220, 52]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:36px; color:#FFFFFF; font-weight:700;">3.11</span><span style="font-size:15px; color:#FFFFFFB3;">㎡ 蚌式铝机盖</span></p>
|
||||||
|
- elementId: row2-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [300, 306, 380, 44]
|
||||||
|
content:
|
||||||
|
fontSize: 13
|
||||||
|
color: "$w70"
|
||||||
|
lineHeight: 1.6
|
||||||
|
text: "量产最大蚌式铝机盖,与整车型面浑然一体"
|
||||||
|
- elementId: row3-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 372, 220, 52]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:36px; color:#FFFFFF; font-weight:700;">0.245</span><span style="font-size:15px; color:#FFFFFFB3;"> 风阻系数</span></p>
|
||||||
|
- elementId: row3-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [300, 376, 380, 44]
|
||||||
|
content:
|
||||||
|
fontSize: 13
|
||||||
|
color: "$w70"
|
||||||
|
lineHeight: 1.6
|
||||||
|
text: "10 组贯穿式风道 · 40+ 处空气动力学优化,带来更好续航"
|
||||||
|
- elementId: extra
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 450, 560, 40]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$w60"
|
||||||
|
letterSpacing: 1
|
||||||
|
lineHeight: 1.7
|
||||||
|
text: "镂空水滴大灯 · 光环尾灯 · 电动内翻式门把手 · 22° 上翘鸭尾"
|
||||||
|
- elementId: note
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 492, 560, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$w50"
|
||||||
|
text: |
|
||||||
|
图源:<a href="https://www.xiaomiev.com/yu7">小米汽车官网</a> · 风阻数据来自中国汽研重庆风洞实验室
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [850, 500, 60, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$w50"
|
||||||
|
align: [right, middle]
|
||||||
|
text: "03 / 08"
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
pageType: content
|
||||||
|
background:
|
||||||
|
type: image
|
||||||
|
src: "media/bg_performance.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
elements:
|
||||||
|
- elementId: overlay
|
||||||
|
elementType: shape
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
shapeName: rect
|
||||||
|
fill:
|
||||||
|
type: gradient
|
||||||
|
gradientType: linear
|
||||||
|
angle: 0
|
||||||
|
stops:
|
||||||
|
- {position: 0, color: "#000000F0"}
|
||||||
|
- {position: 0.55, color: "#00000096"}
|
||||||
|
- {position: 1, color: "#00000026"}
|
||||||
|
- elementId: label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 48, 520, 22]
|
||||||
|
content:
|
||||||
|
style: "$label"
|
||||||
|
text: "PERFORMANCE & HANDLING · 性能与操控"
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 88, 640, 56]
|
||||||
|
content:
|
||||||
|
style: "$pageTitle"
|
||||||
|
text: "自信从容,强大内蕴"
|
||||||
|
- elementId: lead
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 148, 480, 44]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w70"
|
||||||
|
lineHeight: 1.7
|
||||||
|
text: |
|
||||||
|
从电机到底盘,以强悍性能与稳定掌控,
|
||||||
|
回应每一种向前的渴望。
|
||||||
|
- elementId: p1-title
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 226, 460, 30]
|
||||||
|
content:
|
||||||
|
fontSize: 18
|
||||||
|
color: "$white"
|
||||||
|
bold: true
|
||||||
|
letterSpacing: 1
|
||||||
|
text: "小米超级电机 V6s Plus"
|
||||||
|
- elementId: p1-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 258, 460, 40]
|
||||||
|
content:
|
||||||
|
fontSize: 13
|
||||||
|
color: "$w70"
|
||||||
|
lineHeight: 1.6
|
||||||
|
text: "分段磁钢 + 电压过调制技术,转速更快、功率更高,性能全面提升"
|
||||||
|
- elementId: p2-title
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 310, 460, 30]
|
||||||
|
content:
|
||||||
|
fontSize: 18
|
||||||
|
color: "$white"
|
||||||
|
bold: true
|
||||||
|
letterSpacing: 1
|
||||||
|
text: "小米蛟龙底盘"
|
||||||
|
- elementId: p2-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 342, 460, 40]
|
||||||
|
content:
|
||||||
|
fontSize: 13
|
||||||
|
color: "$w70"
|
||||||
|
lineHeight: 1.6
|
||||||
|
text: "闭式双腔空气弹簧 + 连续阻尼可变减振器,运动与舒适兼得"
|
||||||
|
- elementId: p3-title
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 394, 460, 30]
|
||||||
|
content:
|
||||||
|
fontSize: 18
|
||||||
|
color: "$white"
|
||||||
|
bold: true
|
||||||
|
letterSpacing: 1
|
||||||
|
text: "Brembo® 前四活塞固定卡钳"
|
||||||
|
- elementId: p3-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 426, 460, 40]
|
||||||
|
content:
|
||||||
|
fontSize: 13
|
||||||
|
color: "$w70"
|
||||||
|
lineHeight: 1.6
|
||||||
|
text: "强劲制动力,兼顾性能与安全,山路与高速皆得心应手"
|
||||||
|
- elementId: endurance-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [620, 320, 300, 80]
|
||||||
|
content:
|
||||||
|
align: [right, middle]
|
||||||
|
text: |
|
||||||
|
<p style="text-align:right"><span style="font-size:54px; color:#FFFFFF; font-weight:700;">3944</span><span style="font-size:22px; color:#FF6900; font-weight:700;"> km</span></p>
|
||||||
|
- elementId: endurance-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [620, 402, 300, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w85"
|
||||||
|
align: [right, middle]
|
||||||
|
text: "24 小时耐力挑战总行驶里程"
|
||||||
|
- elementId: endurance-note
|
||||||
|
elementType: text
|
||||||
|
bounds: [620, 428, 300, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$w50"
|
||||||
|
align: [right, middle]
|
||||||
|
text: "YU7 Max 测试车 · 中汽中心·华诚认证"
|
||||||
|
- elementId: note
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 490, 700, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$w50"
|
||||||
|
text: |
|
||||||
|
数据来源:<a href="https://www.xiaomiev.com/yu7">小米汽车官网 xiaomiev.com/yu7</a> · 配置区分车型版本
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [850, 500, 60, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$w50"
|
||||||
|
align: [right, middle]
|
||||||
|
text: "04 / 08"
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
pageType: content
|
||||||
|
background:
|
||||||
|
type: image
|
||||||
|
src: "media/bg_cabin.png"
|
||||||
|
fit: {mode: cover}
|
||||||
|
elements:
|
||||||
|
- elementId: overlay
|
||||||
|
elementType: shape
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
shapeName: rect
|
||||||
|
fill:
|
||||||
|
type: gradient
|
||||||
|
gradientType: linear
|
||||||
|
angle: 0
|
||||||
|
stops:
|
||||||
|
- {position: 0, color: "#00000000"}
|
||||||
|
- {position: 0.45, color: "#00000059"}
|
||||||
|
- {position: 1, color: "#000000E6"}
|
||||||
|
- elementId: label
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 48, 380, 22]
|
||||||
|
content:
|
||||||
|
style: "$label"
|
||||||
|
align: [right, middle]
|
||||||
|
text: "SMART CABIN · 智能座舱"
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 88, 380, 56]
|
||||||
|
content:
|
||||||
|
style: "$pageTitle"
|
||||||
|
align: [right, middle]
|
||||||
|
text: "澎湃智能座舱"
|
||||||
|
- elementId: lead
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 148, 380, 40]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w70"
|
||||||
|
align: [right, middle]
|
||||||
|
lineHeight: 1.6
|
||||||
|
text: "小米澎湃 OS · 人车家全生态,科技、豪华与舒适皆备"
|
||||||
|
- elementId: c1-title
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 214, 380, 28]
|
||||||
|
content:
|
||||||
|
fontSize: 17
|
||||||
|
color: "$white"
|
||||||
|
bold: true
|
||||||
|
align: [right, middle]
|
||||||
|
letterSpacing: 1
|
||||||
|
text: "小米天际屏全景显示"
|
||||||
|
- elementId: c1-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 244, 380, 34]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$w70"
|
||||||
|
align: [right, middle]
|
||||||
|
lineHeight: 1.6
|
||||||
|
text: "三块 Mini LED · 1.1m 超宽 · 108PPD · 1200nits"
|
||||||
|
- elementId: c2-title
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 288, 380, 28]
|
||||||
|
content:
|
||||||
|
fontSize: 17
|
||||||
|
color: "$white"
|
||||||
|
bold: true
|
||||||
|
align: [right, middle]
|
||||||
|
letterSpacing: 1
|
||||||
|
text: "第三代骁龙8 移动平台"
|
||||||
|
- elementId: c2-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 318, 380, 34]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$w70"
|
||||||
|
align: [right, middle]
|
||||||
|
lineHeight: 1.6
|
||||||
|
text: "4nm 制程 · 开机快、启动快、升级更快"
|
||||||
|
- elementId: c3-title
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 362, 380, 28]
|
||||||
|
content:
|
||||||
|
fontSize: 17
|
||||||
|
color: "$white"
|
||||||
|
bold: true
|
||||||
|
align: [right, middle]
|
||||||
|
letterSpacing: 1
|
||||||
|
text: "超级小爱 · 五音区语音交互"
|
||||||
|
- elementId: c3-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 392, 380, 34]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$w70"
|
||||||
|
align: [right, middle]
|
||||||
|
lineHeight: 1.6
|
||||||
|
text: "端侧大模型 · 全车声纹识别 · 车外语音控制"
|
||||||
|
- elementId: c4-title
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 436, 380, 28]
|
||||||
|
content:
|
||||||
|
fontSize: 17
|
||||||
|
color: "$white"
|
||||||
|
bold: true
|
||||||
|
align: [right, middle]
|
||||||
|
letterSpacing: 1
|
||||||
|
text: "25 扬声器豪华音响系统"
|
||||||
|
- elementId: c4-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [520, 466, 380, 34]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$w70"
|
||||||
|
align: [right, middle]
|
||||||
|
lineHeight: 1.6
|
||||||
|
text: "支持杜比全景声 · 私享音乐座舱"
|
||||||
|
- elementId: note
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 492, 440, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$w50"
|
||||||
|
text: |
|
||||||
|
图源:<a href="https://www.xiaomiev.com/yu7">小米汽车官网</a> · 配置区分车型版本
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [850, 500, 60, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$w50"
|
||||||
|
align: [right, middle]
|
||||||
|
text: "05 / 08"
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
pageType: content
|
||||||
|
background:
|
||||||
|
type: image
|
||||||
|
src: "media/bg_had.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
elements:
|
||||||
|
- elementId: overlay
|
||||||
|
elementType: shape
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
shapeName: rect
|
||||||
|
fill:
|
||||||
|
type: gradient
|
||||||
|
gradientType: linear
|
||||||
|
angle: 90
|
||||||
|
stops:
|
||||||
|
- {position: 0, color: "#000000E6"}
|
||||||
|
- {position: 0.7, color: "#000000A6"}
|
||||||
|
- {position: 1, color: "#00000073"}
|
||||||
|
- elementId: label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 48, 520, 22]
|
||||||
|
content:
|
||||||
|
style: "$label"
|
||||||
|
text: "XIAOMI HAD · 智能驾驶"
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 88, 700, 56]
|
||||||
|
content:
|
||||||
|
style: "$pageTitle"
|
||||||
|
text: "全场景智能驾驶,AI 全面赋能"
|
||||||
|
- elementId: lead
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 148, 620, 44]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w70"
|
||||||
|
lineHeight: 1.7
|
||||||
|
text: |
|
||||||
|
自研底层算法,坚持长期投入;全系标配旗舰级辅助驾驶硬件,
|
||||||
|
让驾驶更聪明、更安全。
|
||||||
|
- elementId: s1-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [60, 236, 40, 3]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$primary"}
|
||||||
|
- elementId: s2-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [270, 236, 40, 3]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$primary"}
|
||||||
|
- elementId: s3-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [480, 236, 40, 3]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$primary"}
|
||||||
|
- elementId: s4-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [690, 236, 40, 3]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$primary"}
|
||||||
|
- elementId: sensor1
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 252, 180, 56]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:40px; color:#FFFFFF; font-weight:700;">1</span><span style="font-size:15px; color:#FFFFFFB3;"> 颗</span></p>
|
||||||
|
- elementId: sensor1-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 314, 180, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w85"
|
||||||
|
text: "激光雷达 · 全系标配"
|
||||||
|
- elementId: sensor2
|
||||||
|
elementType: text
|
||||||
|
bounds: [270, 252, 180, 56]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:40px; color:#FFFFFF; font-weight:700;">1</span><span style="font-size:15px; color:#FFFFFFB3;"> 颗</span></p>
|
||||||
|
- elementId: sensor2-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [270, 314, 180, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w85"
|
||||||
|
text: "4D 毫米波雷达"
|
||||||
|
- elementId: sensor3
|
||||||
|
elementType: text
|
||||||
|
bounds: [480, 252, 180, 56]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:40px; color:#FFFFFF; font-weight:700;">11</span><span style="font-size:15px; color:#FFFFFFB3;"> 颗</span></p>
|
||||||
|
- elementId: sensor3-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [480, 314, 180, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w85"
|
||||||
|
text: "高清摄像头"
|
||||||
|
- elementId: sensor4
|
||||||
|
elementType: text
|
||||||
|
bounds: [690, 252, 180, 56]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:40px; color:#FFFFFF; font-weight:700;">12</span><span style="font-size:15px; color:#FFFFFFB3;"> 颗</span></p>
|
||||||
|
- elementId: sensor4-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [690, 314, 180, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w85"
|
||||||
|
text: "超声波雷达"
|
||||||
|
- elementId: point1
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 372, 400, 36]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w70"
|
||||||
|
lineHeight: 1.6
|
||||||
|
text: |
|
||||||
|
<p><span style="color:#FF6900; font-weight:700;">●</span> NVIDIA DRIVE AGX Thor™ 车载计算平台</p>
|
||||||
|
- elementId: point2
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 410, 400, 36]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w70"
|
||||||
|
lineHeight: 1.6
|
||||||
|
text: |
|
||||||
|
<p><span style="color:#FF6900; font-weight:700;">●</span> 7 颗超透防眩摄像头,暗光环境、异形障碍物看得更准</p>
|
||||||
|
- elementId: point3
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 448, 400, 36]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w70"
|
||||||
|
lineHeight: 1.6
|
||||||
|
text: |
|
||||||
|
<p><span style="color:#FF6900; font-weight:700;">●</span> 支持自动紧急转向辅助 · 全项低速防碰撞辅助</p>
|
||||||
|
- elementId: note
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 492, 640, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$w50"
|
||||||
|
text: |
|
||||||
|
来源:<a href="https://www.xiaomiev.com/yu7">小米汽车官网</a> · 辅助驾驶不能代替驾驶员,请随时注意路况
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [850, 500, 60, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$w50"
|
||||||
|
align: [right, middle]
|
||||||
|
text: "06 / 08"
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
pageType: content
|
||||||
|
background:
|
||||||
|
type: image
|
||||||
|
src: "media/bg_safety.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
elements:
|
||||||
|
- elementId: overlay
|
||||||
|
elementType: shape
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
shapeName: rect
|
||||||
|
fill:
|
||||||
|
type: gradient
|
||||||
|
gradientType: linear
|
||||||
|
angle: 90
|
||||||
|
stops:
|
||||||
|
- {position: 0, color: "#000000E6"}
|
||||||
|
- {position: 0.35, color: "#0000008C"}
|
||||||
|
- {position: 0.75, color: "#00000059"}
|
||||||
|
- {position: 1, color: "#000000CC"}
|
||||||
|
- elementId: label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 48, 520, 22]
|
||||||
|
content:
|
||||||
|
style: "$label"
|
||||||
|
text: "SAFETY · 安全设计"
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 88, 700, 56]
|
||||||
|
content:
|
||||||
|
style: "$pageTitle"
|
||||||
|
text: "铠甲笼式防护,全面可靠"
|
||||||
|
- elementId: lead
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 148, 700, 44]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w70"
|
||||||
|
lineHeight: 1.7
|
||||||
|
text: |
|
||||||
|
以中欧双五星、中保研 3G+ 为设计目标,被动安全保护 720° 全面升级。
|
||||||
|
- elementId: a1-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [60, 240, 40, 3]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$primary"}
|
||||||
|
- elementId: a2-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [270, 240, 40, 3]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$primary"}
|
||||||
|
- elementId: a3-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [480, 240, 40, 3]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$primary"}
|
||||||
|
- elementId: a4-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [690, 240, 40, 3]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$primary"}
|
||||||
|
- elementId: s1-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 256, 180, 56]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:40px; color:#FFFFFF; font-weight:700;">2200</span><span style="font-size:15px; color:#FFFFFFB3;"> MPa</span></p>
|
||||||
|
- elementId: s1-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 318, 180, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w85"
|
||||||
|
text: "小米超强钢"
|
||||||
|
- elementId: s1-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 344, 180, 44]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$w50"
|
||||||
|
lineHeight: 1.5
|
||||||
|
text: "内嵌式防滚架 · A 柱承载 +35%"
|
||||||
|
- elementId: s2-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [270, 256, 180, 56]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:40px; color:#FFFFFF; font-weight:700;">20</span><span style="font-size:15px; color:#FFFFFFB3;"> 项</span></p>
|
||||||
|
- elementId: s2-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [270, 318, 180, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w85"
|
||||||
|
text: "主动安全配置"
|
||||||
|
- elementId: s2-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [270, 344, 180, 44]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$w50"
|
||||||
|
lineHeight: 1.5
|
||||||
|
text: "全系标配 · 全项低速防碰撞辅助"
|
||||||
|
- elementId: s3-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [480, 256, 180, 56]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:40px; color:#FFFFFF; font-weight:700;">50</span><span style="font-size:15px; color:#FFFFFFB3;"> 项+</span></p>
|
||||||
|
- elementId: s3-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [480, 318, 180, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w85"
|
||||||
|
text: "被动安全开发测试"
|
||||||
|
- elementId: s3-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [480, 344, 180, 44]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$w50"
|
||||||
|
lineHeight: 1.5
|
||||||
|
text: "覆盖 C-NCAP / C-IASI 全工况"
|
||||||
|
- elementId: s4-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [690, 256, 180, 56]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:40px; color:#FFFFFF; font-weight:700;">12.1</span><span style="font-size:15px; color:#FFFFFFB3;">㎡</span></p>
|
||||||
|
- elementId: s4-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [690, 318, 180, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w85"
|
||||||
|
text: "电池气凝胶隔热"
|
||||||
|
- elementId: s4-desc
|
||||||
|
elementType: text
|
||||||
|
bounds: [690, 344, 180, 44]
|
||||||
|
content:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$w50"
|
||||||
|
lineHeight: 1.5
|
||||||
|
text: "防弹涂层 · 18 层硬核物理防护"
|
||||||
|
- elementId: note
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 492, 700, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$w50"
|
||||||
|
text: |
|
||||||
|
来源:<a href="https://www.xiaomiev.com/yu7">小米汽车官网</a> · 数据来自小米汽车实验室,配置区分车型版本
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [850, 500, 60, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$w50"
|
||||||
|
align: [right, middle]
|
||||||
|
text: "07 / 08"
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
pageType: final
|
||||||
|
background:
|
||||||
|
type: image
|
||||||
|
src: "media/bg_final.jpg"
|
||||||
|
fit: {mode: cover}
|
||||||
|
elements:
|
||||||
|
- elementId: overlay
|
||||||
|
elementType: shape
|
||||||
|
bounds: [0, 0, 960, 540]
|
||||||
|
shapeName: rect
|
||||||
|
fill:
|
||||||
|
type: gradient
|
||||||
|
gradientType: linear
|
||||||
|
angle: 90
|
||||||
|
stops:
|
||||||
|
- {position: 0, color: "#00000070"}
|
||||||
|
- {position: 0.55, color: "#000000AD"}
|
||||||
|
- {position: 1, color: "#000000F0"}
|
||||||
|
- elementId: label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 46, 520, 22]
|
||||||
|
content:
|
||||||
|
style: "$label"
|
||||||
|
text: "SPACE & ECOLOGY · 空间与生态"
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 84, 720, 56]
|
||||||
|
content:
|
||||||
|
style: "$pageTitle"
|
||||||
|
text: "全能实力,从容远行"
|
||||||
|
- elementId: lead
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 144, 720, 30]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w70"
|
||||||
|
text: "宽敞空间、多变场景与全场景生态,让出行不再精打细算"
|
||||||
|
- elementId: st1-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [60, 208, 40, 3]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$primary"}
|
||||||
|
- elementId: st2-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [300, 208, 40, 3]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$primary"}
|
||||||
|
- elementId: st3-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [540, 208, 40, 3]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$primary"}
|
||||||
|
- elementId: space1-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 224, 200, 56]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:38px; color:#FFFFFF; font-weight:700;">141</span><span style="font-size:15px; color:#FFFFFFB3;"> L</span></p>
|
||||||
|
- elementId: space1-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 286, 200, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w85"
|
||||||
|
text: "电动前备箱"
|
||||||
|
- elementId: space2-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [300, 224, 200, 56]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:38px; color:#FFFFFF; font-weight:700;">1970</span><span style="font-size:15px; color:#FFFFFFB3;"> L</span></p>
|
||||||
|
- elementId: space2-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [300, 286, 200, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w85"
|
||||||
|
text: "最大储物容积"
|
||||||
|
- elementId: space3-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [540, 224, 200, 56]
|
||||||
|
content:
|
||||||
|
align: [left, middle]
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:38px; color:#FFFFFF; font-weight:700;">36</span><span style="font-size:15px; color:#FFFFFFB3;"> 处</span></p>
|
||||||
|
- elementId: space3-label
|
||||||
|
elementType: text
|
||||||
|
bounds: [540, 286, 200, 22]
|
||||||
|
content:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w85"
|
||||||
|
text: "全车收纳位"
|
||||||
|
- elementId: eco
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 340, 840, 34]
|
||||||
|
content:
|
||||||
|
fontSize: 15
|
||||||
|
color: "$w85"
|
||||||
|
text: |
|
||||||
|
<p>基于小米澎湃 OS 的人车家全生态 · 磁吸生态配件 · 车顶滑轨 · 27W 供电螺纹接口</p>
|
||||||
|
- elementId: gt-line
|
||||||
|
elementType: shape
|
||||||
|
bounds: [60, 392, 56, 3]
|
||||||
|
shapeName: rect
|
||||||
|
fill: {type: solid, color: "$primary"}
|
||||||
|
- elementId: gt
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 404, 620, 34]
|
||||||
|
content:
|
||||||
|
fontSize: 17
|
||||||
|
color: "$white"
|
||||||
|
bold: true
|
||||||
|
letterSpacing: 1
|
||||||
|
text: "YU7 GT 即将预售 —— 纽博格林北环赛道 SUV 圈速新纪录"
|
||||||
|
- elementId: cta
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 448, 420, 36]
|
||||||
|
content:
|
||||||
|
fontSize: 20
|
||||||
|
color: "$primary"
|
||||||
|
bold: true
|
||||||
|
letterSpacing: 2
|
||||||
|
text: "立即预约试驾 →"
|
||||||
|
- elementId: note
|
||||||
|
elementType: text
|
||||||
|
bounds: [60, 492, 700, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$w50"
|
||||||
|
text: |
|
||||||
|
图源与数据:<a href="https://www.xiaomiev.com/yu7">小米汽车官网 xiaomiev.com/yu7</a>
|
||||||
|
- elementId: page-num
|
||||||
|
elementType: text
|
||||||
|
bounds: [850, 500, 60, 20]
|
||||||
|
content:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$w50"
|
||||||
|
align: [right, middle]
|
||||||
|
text: "08 / 08"
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
version: v2
|
||||||
|
title: 小米 YU7 · 豪华高性能纯电 SUV
|
||||||
|
size: [960, 540]
|
||||||
|
theme:
|
||||||
|
colors:
|
||||||
|
primary: "#FF6900"
|
||||||
|
white: "#FFFFFF"
|
||||||
|
w85: "#FFFFFFD9"
|
||||||
|
w70: "#FFFFFFB3"
|
||||||
|
w60: "#FFFFFF99"
|
||||||
|
w50: "#FFFFFF80"
|
||||||
|
w40: "#FFFFFF66"
|
||||||
|
dark: "#000000"
|
||||||
|
textStyles:
|
||||||
|
label:
|
||||||
|
fontSize: 12
|
||||||
|
color: "$primary"
|
||||||
|
bold: true
|
||||||
|
letterSpacing: 4
|
||||||
|
pageTitle:
|
||||||
|
fontSize: 42
|
||||||
|
color: "$white"
|
||||||
|
bold: true
|
||||||
|
letterSpacing: 1
|
||||||
|
coverTitle:
|
||||||
|
fontSize: 86
|
||||||
|
color: "$white"
|
||||||
|
bold: true
|
||||||
|
letterSpacing: 8
|
||||||
|
body:
|
||||||
|
fontSize: 15
|
||||||
|
color: "$w70"
|
||||||
|
lineHeight: 1.6
|
||||||
|
statNum:
|
||||||
|
fontSize: 38
|
||||||
|
color: "$white"
|
||||||
|
bold: true
|
||||||
|
statLabel:
|
||||||
|
fontSize: 14
|
||||||
|
color: "$w85"
|
||||||
|
note:
|
||||||
|
fontSize: 11
|
||||||
|
color: "$w50"
|
||||||
|
pages:
|
||||||
|
- pages/1_cover.page
|
||||||
|
- pages/2_overview.page
|
||||||
|
- pages/3_exterior.page
|
||||||
|
- pages/4_performance.page
|
||||||
|
- pages/5_cabin.page
|
||||||
|
- pages/6_had.page
|
||||||
|
- pages/7_safety.page
|
||||||
|
- pages/8_space_final.page
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { createReadStream, statSync } from "node:fs";
|
||||||
|
import { createServer } from "node:http";
|
||||||
|
import { dirname, extname, resolve, sep } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const defaultEditorDirectory = resolve(packageRoot, "editor");
|
||||||
|
|
||||||
|
const contentTypes = new Map([
|
||||||
|
[".css", "text/css; charset=utf-8"],
|
||||||
|
[".html", "text/html; charset=utf-8"],
|
||||||
|
[".js", "text/javascript; charset=utf-8"],
|
||||||
|
[".json", "application/json; charset=utf-8"],
|
||||||
|
[".svg", "image/svg+xml"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
function respond(response, statusCode, message) {
|
||||||
|
response.writeHead(statusCode, {
|
||||||
|
"Content-Type": "text/plain; charset=utf-8",
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
});
|
||||||
|
response.end(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createEditorServer({ editorDirectory = defaultEditorDirectory } = {}) {
|
||||||
|
const root = resolve(editorDirectory);
|
||||||
|
const rootPrefix = `${root}${sep}`;
|
||||||
|
|
||||||
|
return createServer((request, response) => {
|
||||||
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
||||||
|
respond(response, 405, "Method Not Allowed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pathname;
|
||||||
|
try {
|
||||||
|
pathname = decodeURIComponent(new URL(request.url ?? "/", "http://localhost").pathname);
|
||||||
|
} catch {
|
||||||
|
respond(response, 400, "Bad Request");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname.endsWith("/")) pathname += "index.html";
|
||||||
|
const filePath = resolve(root, `.${pathname}`);
|
||||||
|
if (filePath !== root && !filePath.startsWith(rootPrefix)) {
|
||||||
|
respond(response, 403, "Forbidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stats;
|
||||||
|
try {
|
||||||
|
stats = statSync(filePath);
|
||||||
|
} catch {
|
||||||
|
respond(response, 404, "Not Found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stats.isFile()) {
|
||||||
|
respond(response, 404, "Not Found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.writeHead(200, {
|
||||||
|
"Content-Type": contentTypes.get(extname(filePath).toLowerCase()) ?? "application/octet-stream",
|
||||||
|
"Content-Length": stats.size,
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (request.method === "HEAD") {
|
||||||
|
response.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = createReadStream(filePath);
|
||||||
|
stream.on("error", () => response.destroy());
|
||||||
|
stream.pipe(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startEditorServer({ host = "127.0.0.1", port = 55173 } = {}) {
|
||||||
|
const server = createEditorServer();
|
||||||
|
|
||||||
|
return new Promise((resolvePromise, reject) => {
|
||||||
|
const onError = (error) => reject(error);
|
||||||
|
server.once("error", onError);
|
||||||
|
server.listen(port, host, () => {
|
||||||
|
server.off("error", onError);
|
||||||
|
const address = server.address();
|
||||||
|
const actualPort = typeof address === "object" && address ? address.port : port;
|
||||||
|
resolvePromise({ server, url: `http://${host}:${actualPort}/` });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "open-kimi-ppt-skills",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Unofficial reverse-engineered Kimi Slides skill for AI coding agents with local PPTD editing and PPTX export.",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "binaryify",
|
||||||
|
"homepage": "https://github.com/binaryify/open-kimi-ppt-skill#readme",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/binaryify/open-kimi-ppt-skill.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/binaryify/open-kimi-ppt-skill/issues"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"ppt",
|
||||||
|
"pptx",
|
||||||
|
"slides",
|
||||||
|
"presentation",
|
||||||
|
"pptd",
|
||||||
|
"kimi",
|
||||||
|
"agent-skill",
|
||||||
|
"codex",
|
||||||
|
"claude-code",
|
||||||
|
"cursor",
|
||||||
|
"codebuddy"
|
||||||
|
],
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"open-kimi-ppt-skills": "bin/open-kimi-ppt-skills.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"bin/",
|
||||||
|
"editor/",
|
||||||
|
"lib/",
|
||||||
|
"skills/open-kimi-ppt/SKILL.md",
|
||||||
|
"skills/open-kimi-ppt/reference/",
|
||||||
|
"skills/open-kimi-ppt/scripts/",
|
||||||
|
"README.md",
|
||||||
|
"README_EN.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "node --test",
|
||||||
|
"pack:check": "npm pack --dry-run",
|
||||||
|
"prepublishOnly": "npm test"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
---
|
||||||
|
name: open-kimi-ppt
|
||||||
|
description: Create, edit, replicate, read, and export presentations. For every PPT task, the default deliverables are BOTH (1) a self-contained PPTD project folder containing the .pptd manifest plus pages/media dependencies and (2) a locally generated .pptx with embedded fonts and fade slide transitions. Use for any presentation, PowerPoint, PPT/PPTX, slide deck, PPTD, infographic, or poster task unless the user explicitly requests another format. Deliver with normal local file/folder links using absolute paths.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Definition
|
||||||
|
open-kimi-ppt is a presentation creation and export skill built around Moonshot AI's PPTD format and browser-side PPTX writer. It defines a YAML-format intermediate DSL (`.pptd`) that abstracts OOXML and keeps each page self-contained.
|
||||||
|
|
||||||
|
**The default output is not PPTD-only.** Unless the user explicitly opts out, always produce both:
|
||||||
|
|
||||||
|
1. the complete editable PPTD project directory (`.pptd` + `pages/` + `media/` and other referenced dependencies);
|
||||||
|
2. the matching locally generated `.pptx`, with font embedding enabled and fade slide transitions applied by default.
|
||||||
|
|
||||||
|
Existing PPTX files may also be converted into PPTD for editing, after which both outputs are delivered again.
|
||||||
|
|
||||||
|
## The pptd format
|
||||||
|
The .pptd format is a simplified abstraction layer over OOXML that follows basic YAML syntax. This abstraction preserves the core content of OOXML (theme, page layout, element positions and definitions, etc.) while removing complex nesting logic such as Masters; every page is self-contained — what you see is what you get. Read reference/pptd.md for the complete definition of this DSL.
|
||||||
|
|
||||||
|
## PPT production workflow
|
||||||
|
|
||||||
|
### step1. Read the context thoroughly
|
||||||
|
Read **all files uploaded by the user**, the provided URLs, and the pptd format guide `reference/pptd.md` to fully understand the user's requirements.
|
||||||
|
|
||||||
|
### step2. Understand the user's requirements
|
||||||
|
Understand the user's requirements based on the context:
|
||||||
|
1. First determine the purpose of the request
|
||||||
|
- Create a PPT: create a new presentation (from scratch, or from an existing pptx template)
|
||||||
|
- Edit a PPT: edit the user's uploaded PPT (local modifications, single-page beautification, etc.)
|
||||||
|
- Replicate a PPT: replicate a presentation from a non-pptx format (images, PDF, etc.) into pptd format
|
||||||
|
|
||||||
|
2. Then determine the design direction
|
||||||
|
- Self-directed design: no preference, or only simple style constraints given; you need to fill in or create the design
|
||||||
|
- Design system: the user provides a complete and detailed design scheme covering all color, font, layout, and component specifications
|
||||||
|
- Use a template: a template is provided and must be used
|
||||||
|
- Style transfer: a style reference source is provided (images, web pages, etc.)
|
||||||
|
|
||||||
|
3. Then determine the input type
|
||||||
|
- Topic only: only a PPT topic direction or content requirements for the presentation are given, with no concrete content
|
||||||
|
- Full document: the user provides a complete document (paper, research report, press release, etc.)
|
||||||
|
- Outline: the user provides a page-by-page outline, speech script, or similar content
|
||||||
|
* When the "user input type" is [Full document] or [Outline] and it is not specified whether expansion is allowed: since a page-by-page outline, speech script, or user document can hardly support the full content of a presentation, prefer using search to expand with more relevant material, cases, etc., unless the user explicitly says not to expand
|
||||||
|
|
||||||
|
4. Page count
|
||||||
|
- If the user requests a specific page count, the user's requirement takes priority
|
||||||
|
- Page-by-page outline/script provided: match the number of pages in the outline/script
|
||||||
|
- When a complete and relatively structured document is provided / when only a topic is provided: decide the page count yourself based on the document content / search results
|
||||||
|
|
||||||
|
### step3. Generate the presentation based on the user's requirements
|
||||||
|
|
||||||
|
Before generating, first read `reference/pptd.md` to understand the pptd format definition and constraints.
|
||||||
|
|
||||||
|
#### Replicating a PPT
|
||||||
|
- Analyze the images to estimate element positions, fonts and sizes, etc., and **replicate 1:1 as closely as possible**.
|
||||||
|
- When an image contains elements that are hard to replicate directly and cannot be approximated with icons/shapes (e.g., photos, avatars), you may use tools such as bash or python to crop and screenshot the original image
|
||||||
|
|
||||||
|
#### Editing a PPT
|
||||||
|
- Convert the user's uploaded pptx file to .pptd format
|
||||||
|
- Review the converted pages (structure and key visual details). Read a few key pages individually afterwards.
|
||||||
|
- Locate the pages to edit, and be careful not to affect parts outside the intended scope.
|
||||||
|
> Conversion from pptx to pptd is not perfectly lossless. If the user later reports format errors, garbled content, etc., compare against the original pptx and repair the pptd with reference to the comparison
|
||||||
|
|
||||||
|
#### Generating a PPT
|
||||||
|
When generating a PPT, adopt different production approaches for different user [design directions]
|
||||||
|
##### Self-directed design
|
||||||
|
1. Read the design guide `reference/slides_categories.md`, and read the scenario document corresponding to the user's query
|
||||||
|
2. Produce the presentation based on the above
|
||||||
|
|
||||||
|
#### Generating content in other formats
|
||||||
|
- When the user explicitly asks for an infographic, poster, or a highly visual single-page design, read `reference/general-poster.md` and implement it as a single-page or few-page editable PPTD; when the user only asks for an image, still build it with PPTD first, then output the image via screenshot or rendering. Do not load this reference file for ordinary PPT requests.
|
||||||
|
|
||||||
|
##### Design system
|
||||||
|
1. Read the general constraints section of the `reference/slides_categories.md` guide, and read the scenario document corresponding to the user's query as the design foundation
|
||||||
|
2. Read the user-provided design system document as the presentation style. It is strictly forbidden to reference or mix in other design styles
|
||||||
|
3. Produce the presentation with reference to the above
|
||||||
|
|
||||||
|
##### Using a template
|
||||||
|
1. Convert the user's uploaded pptx file into pptd form
|
||||||
|
2. Review the converted pages to understand the template's visual style (color scheme, font style, element characteristics, layout characteristics, content density, etc.)
|
||||||
|
3. Identify page types; focus on reading special pages such as the cover, summary pages, and section dividers (single-page screenshots, .page files), extracting their page layouts, content structures, reusable components (icons, shapes, smartart, reusable body layout schemes, etc.), and element styles (e.g., whitespace/line/card separators, square/rounded corners, etc.)
|
||||||
|
4. Produce the presentation using the template
|
||||||
|
|
||||||
|
##### Style transfer
|
||||||
|
1. Analyze the reference file's visual style (color scheme, font style, element characteristics, layout characteristics, content density, etc.), page layouts, content structures, reusable components (icons, shapes, smartart, reusable body layout schemes, etc.), and element styles (e.g., whitespace/line/card separators, square/rounded corners, etc.).
|
||||||
|
- If the user provides a style reference URL, do not only read the text content; refer to and learn from the page's visual effect more to help understand the style
|
||||||
|
2. Produce the presentation using the reference file's style characteristics. You are encouraged to reuse illustrations, fonts, font-size hierarchies, elements, etc. from the original pdf/url
|
||||||
|
|
||||||
|
### step4. PPT validation
|
||||||
|
1. Validate the generated pptd against the format definition in `reference/pptd.md` (required fields, types, bounds, theme tokens, resource paths, etc.) and repair issues over multiple rounds
|
||||||
|
2. Visual review with exported page images — **required before PPTX export when the model supports image input (multimodal)**:
|
||||||
|
- Run `scripts/export_images.py`. It loads the deck into Kimi's public editor, chooses 导出 → 图片, downloads the images ZIP, unzips it, and stitches all pages into one overview image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ~/.agents/skills/open-kimi-ppt/scripts/export_images.py \
|
||||||
|
/abs/path/project/deck.pptd \
|
||||||
|
--output /abs/path/project/.qa-images
|
||||||
|
```
|
||||||
|
|
||||||
|
The script prints a JSON summary mapping each stitched label (`P1`…`Pn`, 1-based page order) to its `.page` file.
|
||||||
|
- Read the stitched overview image (`.qa-images/overview.jpg`) and check every page against this list:
|
||||||
|
1. 图片是否清晰、不变形(无拉伸、压缩、模糊)
|
||||||
|
2. 文字是否压在关键画面(人脸、产品主体、Logo 等)上
|
||||||
|
3. 元素坐标是否超出页面边界
|
||||||
|
4. 边界与配色对比是否足够(文字与背景、相邻色块之间)
|
||||||
|
5. 排版是否统一(对齐、间距、字号层级、页边距)
|
||||||
|
6. 文字是否可能溢出文本框(文本过长、行距过密、字号过大)
|
||||||
|
7. 内容是否被上层元素遮挡
|
||||||
|
- For any suspicious page, read its full-resolution image (`.qa-images/pages/<n>.jpeg`) to confirm the problem before editing.
|
||||||
|
- Fix issues in the corresponding `.page` file, then re-run `scripts/export_images.py --force` and review the new overview; repeat until every page passes.
|
||||||
|
- Do not export the PPTX until the visual review passes. `.qa-images/` is an intermediate QA artifact and may be deleted after delivery.
|
||||||
|
3. When the model cannot read images, fall back to a structural review of the generated pages (bounds, overflow-prone long text, contrast, hierarchy, layout density) over multiple rounds, and state that image-based visual QA was skipped.
|
||||||
|
|
||||||
|
### step5. PPT output and delivery
|
||||||
|
1. Always produce a self-contained project directory. Keep the `.pptd` manifest and every referenced dependency together; never deliver a standalone manifest without its referenced files. Use this layout unless an existing project already has a valid equivalent structure:
|
||||||
|
|
||||||
|
```text
|
||||||
|
deck/
|
||||||
|
deck.pptd
|
||||||
|
pages/
|
||||||
|
*.page
|
||||||
|
media/
|
||||||
|
* # when the deck has local media
|
||||||
|
deck.pptx # generated by default
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Generate the `.pptx` by default after PPTD validation, even when the user only asks to create or edit a presentation. Skip PPTX export only when the user explicitly requests PPTD-only output or the environment cannot run the exporter; in the latter case, report the exact blocker and still deliver the complete PPTD project.
|
||||||
|
3. Deliver with normal clickable local links using absolute paths. In the final response, link all of the following:
|
||||||
|
- the project directory;
|
||||||
|
- the `.pptd` manifest;
|
||||||
|
- the `pages/` directory and `media/` directory when present;
|
||||||
|
- the generated `.pptx` file.
|
||||||
|
4. PPTX conversion: use `scripts/export_pptx.py`. It opens a temporary localhost SDK host, loads the `.pptd` into Kimi's public editor, invokes the same browser-side OOXML writer as the official Download tab, saves the resulting PPTX locally, and validates the ZIP/slide structure.
|
||||||
|
5. Default PPTX options:
|
||||||
|
- page transition: `fade` (淡入淡出), written to every slide after the official browser export;
|
||||||
|
- font embedding: enabled whenever the official writer exposes/supports it;
|
||||||
|
- these defaults may be explicitly overridden with `--transition none` or `--no-embed-fonts`.
|
||||||
|
6. Export command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ~/.agents/skills/open-kimi-ppt/scripts/export_pptx.py \
|
||||||
|
/abs/path/project/deck.pptd \
|
||||||
|
--output /abs/path/project/deck.pptx
|
||||||
|
```
|
||||||
|
|
||||||
|
A project directory may be passed instead of the manifest only when it contains exactly one `.pptd` file.
|
||||||
|
Existing output files are not overwritten unless `--force` is passed.
|
||||||
|
7. Local export requirements and boundaries:
|
||||||
|
- requires `python3`, PyYAML, `npm`, `agent-browser`, Chrome/Chromium, and network access to `www.kimi.com` plus `statics.moonshot.cn`; the image-based visual QA step additionally requires Pillow, auto-installed with `pip --user` when missing;
|
||||||
|
- before browser export, `export_pptx.py` checks `agent-browser --version`; when it is missing or below `0.33.2`, it installs `agent-browser@latest` globally with npm, then verifies the resulting version before continuing;
|
||||||
|
- the PPTD document itself is provided to the public editor iframe through the localhost SDK bridge, not uploaded to a server-side PPTX conversion endpoint;
|
||||||
|
- remote images, icons, or fonts referenced by the deck may still be fetched from their respective hosts;
|
||||||
|
- local PNG/JPEG/GIF/SVG files inside the PPTD project are supplied to the iframe as data URLs;
|
||||||
|
- do not claim PowerPoint/WPS/Keynote playback compatibility solely because ZIP validation succeeds.
|
||||||
|
8. After export, verify that the output exists and report the generated path. Confirm that every slide has exactly one root-level fade transition in valid CT_Slide order (`cSld`, optional `clrMapOvr`, `transition`, optional `timing/extLst`) and that the PPTX ZIP passes integrity checks. A byte-string search for `<p:fade>` is insufficient because Office ignores transitions nested inside `cSld`. For higher-risk decks, additionally inspect font parts and representative rendered/opened pages as appropriate.
|
||||||
|
9. When the user wants to open, edit, save, or export a PPTD project manually, start the local browser editor with `npx open-kimi-ppt-skills serve`. Ask the user to open `http://127.0.0.1:55173/` and authorize the complete PPTD project directory. Use a Chromium-based browser for writable access; folder-upload fallback is read-only. The local host only serves the editor shell, while the embedded public Kimi editor and remote assets still require network access.
|
||||||
|
10. After completing and delivering any presentation, always end the final response with a concise optional next step telling the user that they can run `npx open-kimi-ppt-skills serve` to view or edit the PPTD project, configure slide transition animations, and export PPTX manually. Keep this reminder in addition to, not instead of, the required project and file links.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Font system
|
||||||
|
|
||||||
|
## Selection principles
|
||||||
|
|
||||||
|
1. Language matching: when the user's query is in Chinese or a Chinese PPT deliverable is requested, both Chinese and English fonts need to be specified; otherwise, only English fonts need to be set
|
||||||
|
2. Selection approach: it is recommended to prioritize highly readable fonts for body text, and to use stylized fonts plus special treatments (all caps, widened letter spacing, bold, italics, etc.) in titles or special pages to strengthen the style
|
||||||
|
3. The font combination must support the overall visual style positioning
|
||||||
|
4. Name consistency: **make sure font names are exactly identical, including capitalization and spaces, so that the model can use them correctly**
|
||||||
|
|
||||||
|
## Font list
|
||||||
|
|
||||||
|
### English fonts
|
||||||
|
|
||||||
|
| Font name | Font type | Style and characteristics | Suitable scenarios | Stylized font |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Liter | Sans-serif | Modern neo-grotesque style; low contrast, balanced proportions, optimized for screens; clean and rational | Tech, product | No |
|
||||||
|
| HedvigLettersSans | Sans-serif | A "non-designer perspective" design; slightly irregular with a distinctive personality and strong brand character | Creative design, branding | No |
|
||||||
|
| Oranienbaum | Serif | Modern high-contrast serif; strongly geometric with elegant lines and a classical temperament | Culture, art, fashion | No |
|
||||||
|
| QuattrocentoSans | Sans-serif | Classic, elegant sans-serif; gentle, highly readable, clear at small sizes | Academic, corporate, education | No |
|
||||||
|
| SortsMillGoudy | Serif | Revival of the classical print style of Goudy Old Style; soft serifs, pleasant to read | Literature, humanities | No |
|
||||||
|
| Unna | Serif | Neoclassical serif with a pronounced vertical rhythm; elegant yet forceful | Literature, publishing, academia | Yes |
|
||||||
|
| Coda | Sans-serif | Rounded and friendly; soft curves, high openness | Business, friendly brand tone | Yes |
|
||||||
|
| Jersey15 | Sans-serif (sports style) | Sports jersey style; squared structure with a noticeable grid feel (supports English + digits only) | Sports, tech showcase pages | Yes |
|
||||||
|
| Jersey20Charted | Pixel font (grid shading) | Sports-number style with a shaded grid that reinforces an athletic texture (supports English + digits only) | Sports, mechanical, decorative showcase pages | Yes |
|
||||||
|
|
||||||
|
### Chinese fonts
|
||||||
|
|
||||||
|
| Font name | Font type | Style and characteristics | Suitable scenarios | Usage restriction |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| MiSans | Sans-serif (heiti) | Xiaomi system font; clean and modern, variable weights, excellent on-screen rendering (multilingual support) | Tech, corporate, product | No |
|
||||||
|
| Noto Sans SC | Sans-serif (heiti) | A branch of Source Han Sans; well-regulated structure, neutral style, extremely broad coverage. **Because it is so widely used, use it as little as possible** | Report-style | No |
|
||||||
|
| 思源宋体 | Serif (songti) | Source Han Serif; refined songti structure with stroke contrast; elegant reading experience (multilingual support) | Literature, design, formal presentations | No |
|
||||||
|
| 阿里妈妈刀隶体 | Calligraphy (clerical script) | Alimama DaoLi; clerical-script style with blade-like strokes; strength and archaic simplicity combined | China-chic, culture, art displays | No |
|
||||||
|
| 阿里妈妈东方大楷 | Calligraphy (regular script) | Alimama Dongfang Dakai; based on the Yan style of calligraphy; full and rounded, heavy and powerful | Culture, brand launches, guofeng (Chinese-style) themes | No |
|
||||||
|
| 阿里妈妈数黑体 | Sans-serif (heiti) | Alimama ShuHei; geometric heiti; uniform and regular with a strong commercial feel | Business, tech, e-commerce | No |
|
||||||
|
| 站酷文艺体 | Handwriting | Zcool WenYi; clean and fresh with a slight handwritten feel and a strong literary/artistic vibe | Light design, lifestyle | No |
|
||||||
|
| 飞波正点体 | Calligraphy (brush) | Feibo Zhengdian; brush-writing style with heavy strokes, full of power | Film posters, e-commerce, brand display | No |
|
||||||
|
| 得意黑 | Sans-serif (slanted heiti) | Smiley Sans; tall, slanted heiti combining humanist and geometric qualities with a strong modern feel. **Does not support a non-italic upright style** | Creative tech, brand display | No |
|
||||||
|
| 霞鹜新致宋 | Serif (songti) | LXGW XinZhiSong; based on IPAmj Mincho; bright, elegant, well-structured | Literature, classical style, print style | No |
|
||||||
|
|
||||||
|
### Mixed CJK–Latin fonts
|
||||||
|
|
||||||
|
| Font name | Font type | Style and characteristics | Suitable scenarios | Usage restriction |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 精品点阵体 | Pixel font | Jingpin Dianzhen; 9×9 dot-matrix pixel style with an extremely retro electronic feel | Games, tech, pixel art | Yes |
|
||||||
|
| LXGW Bright | Serif (fangsong/kaiti) | LXGW WenKai family; combines fangsong and kaiti characteristics; gentle and clear letterforms | Literature, education, humanities | No |
|
||||||
|
| ZCOOL KuaiLe | Handwriting (rounded) | ZCOOL KuaiLe; lively, cute, playful and cartoonish; youthful energy | Anime, children, entertainment | No |
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
# General Infographics and Posters
|
||||||
|
|
||||||
|
Use this guide only when the user explicitly requests an infographic, poster, or a highly visual single-page design. Do not load this guide for ordinary PPT requests.
|
||||||
|
|
||||||
|
This guide defines only the design decision process and quality baselines; it does not prescribe fixed color schemes, fonts, maps, timelines, grids, or page skeletons. Design independently based on the subject, audience, materials, and usage scenario; never copy a sample by default just because it is more detailed.
|
||||||
|
|
||||||
|
## Production Workflow
|
||||||
|
|
||||||
|
### 1. Understand the Subject and Communication Goals
|
||||||
|
|
||||||
|
First confirm:
|
||||||
|
|
||||||
|
- What this poster should make readers see, understand, remember, or do.
|
||||||
|
- The primary audience, viewing distance, display medium, and available reading time.
|
||||||
|
- The size, aspect ratio, colors, style, content, and output format explicitly requested by the user.
|
||||||
|
- The core relationship the material truly needs to express: time, space, process, contrast, hierarchy, object structure, or pure spirit/atmosphere conveyance.
|
||||||
|
- Whether the available images, data, and verifiable facts are sufficient to support the chosen form of expression.
|
||||||
|
|
||||||
|
### 2. Explore Different Directions First, Then Choose One
|
||||||
|
|
||||||
|
Before writing any `.page` file or placing elements, internally form 2–3 visual directions with substantive differences. They should differ in at least two of the following:
|
||||||
|
|
||||||
|
- Primary visual focus: image, typography, object, data, map/relationship diagram, or abstract graphics.
|
||||||
|
- Information structure: linear narrative, radial hub, local zoom, layered section, juxtaposed comparison, image matrix, or a single scene.
|
||||||
|
- Spatial organization: full-bleed, off-axis, symmetric, high-density, generous whitespace, or zoned structure.
|
||||||
|
|
||||||
|
At least one candidate direction should treat the entire canvas as one visual field, scene, or object relationship, rather than first splitting the content into containers of equal width and height. Cards may appear among the candidates, but must not automatically become the default just because the information has three or four items.
|
||||||
|
|
||||||
|
Before finalizing a direction, compare which primary visual medium best carries the content. Whenever people, artifacts, materials, environments, or event scenes are important evidence for the subject, actually examine at least 2 existing images, search results, generated images, proofs, or reference screenshots that differ substantively in composition or visual language; whenever process, hierarchy, system, propagation, causality, contrast, or spatial relationships are central, compare at least 2 editable vector or SmartArt-like figure drafts with clearly different structures. After comparing subject recognizability, thumbnail memorability, information capacity, compositional coexistence, and editability, choose photography/real-object imagery, generated images, custom vectors, structured figures, or a hybrid solution. Do not default to pure vectors just because PPTD vectors are easier to implement or because searching/generating assets takes time, and do not force in images unrelated to the content just to "have assets."
|
||||||
|
|
||||||
|
Choose one direction based on the subject's characteristics, the user's tone, asset quality, and the primary reading task. You don't need to deliver three designs, but don't start building the moment you see the first workable option.
|
||||||
|
|
||||||
|
Available grammars include but are not limited to typography-led, photography-led, object/section annotation, archival collage, data field, spatial map, or relationship diagram. These are angles for thinking, not templates that must be applied.
|
||||||
|
|
||||||
|
### 3. Write `style.md` in the Project
|
||||||
|
|
||||||
|
For a newly created poster/infographic, create a brief `style.md` in the PPTD project root before writing `.page` files. It is the design brief for the current task — not a new skill file, not a hard process gate, and not an extra deliverable.
|
||||||
|
|
||||||
|
Review the user's assets, actual image candidates, and the fonts available in `reference/fonts.md` before finalizing `style.md`. Keep the content short, specific, and actionable; it typically includes:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Style Brief
|
||||||
|
- Goal and audience:
|
||||||
|
- Output scenario and canvas:
|
||||||
|
- Core message in one sentence:
|
||||||
|
- Chosen visual concept/metaphor:
|
||||||
|
- Primary visual focus:
|
||||||
|
- Poster-level visual move (why this is not an ordinary content page):
|
||||||
|
- Visual DNA (background/material, typographic/geometric temperament, how images coexist with structured information):
|
||||||
|
- Primary visual medium and asset/SmartArt-like figure candidates compared, with reasons for the choice:
|
||||||
|
- Reading order and composition:
|
||||||
|
- Color scheme and each color's role:
|
||||||
|
- Chinese and English fonts, size hierarchy, and usage:
|
||||||
|
- Image selection, cropping, and treatment:
|
||||||
|
- Graphics/chart/connector language (if needed):
|
||||||
|
- Information density, whitespace, and alignment:
|
||||||
|
- Tropes to avoid:
|
||||||
|
```
|
||||||
|
|
||||||
|
- Explain why this direction was chosen, and record in one sentence the main reason the other directions were not chosen; also state which trope the chosen direction is most likely to degenerate into (for example, an ordinary technical report, a retro propaganda page, a commercial promotion, or a uniform card wall), so it can be checked against during screenshot review.
|
||||||
|
- When the user has already provided a complete design system, template, or strong style constraints, `style.md` only converts them into actionable decisions; do not invent new styles that conflict with the user's requirements.
|
||||||
|
- After viewing actual images or discovering asset limitations, you may update `style.md` once before refining the pages. Do not turn it into a lengthy process log.
|
||||||
|
- Creating a new `style.md` is not mandatory when making local edits to an existing artifact; it is only needed when redesigning the overall visual.
|
||||||
|
|
||||||
|
## Canvas Ratio and Orientation
|
||||||
|
|
||||||
|
Determine the output scenario and canvas first, then design the grid, font sizes, and content structure. Do not equate a poster with portrait orientation by default, and do not default to inheriting PPT's 16:9.
|
||||||
|
|
||||||
|
1. When the user explicitly specifies a size, orientation, or ratio, strictly use the user's requirement.
|
||||||
|
2. When the user has not specified, choose on your own based on the deployment scenario, information richness, primary reading order, and relationship structure.
|
||||||
|
3. When there is too much information, prioritize distilling, layering, or splitting into a few pages; do not cram everything into a single page with extremely small font sizes.
|
||||||
|
4. Once the canvas is chosen, compose directly for that ratio; do not build it in 16:9 first and then stretch or crop the whole thing into another ratio.
|
||||||
|
|
||||||
|
| Common ratio | PPTD reference size | Common scenarios |
|
||||||
|
|---|---:|---|
|
||||||
|
| 9:16 portrait | `[540, 960]` | Mobile reading, social media, vertical narrative |
|
||||||
|
| 3:4 portrait | `[720, 960]` | Print-like posters, person/object hero visuals |
|
||||||
|
| 1:1 square | `[720, 720]` | Social media, centered structures, moderate information volume |
|
||||||
|
| 4:3 landscape | `[720, 540]` | Higher-density text-image content, document embedding |
|
||||||
|
| 16:9 landscape | `[960, 540]` | Widescreen display, horizontal relationships and parallel multi-column layouts |
|
||||||
|
|
||||||
|
A PPTD canvas can use any `[width, height]`; the sizes in the table are references, not enumerated restrictions.
|
||||||
|
|
||||||
|
## Content Relationships and Graphic Selection
|
||||||
|
|
||||||
|
First state the page's core message in one sentence, then choose the graphic that best expresses it. Use a structure diagram only when the relationship itself is central; do not add maps, timelines, routes, flowcharts, matrices, or indexes just to look rich. Parallel relationships do not automatically equal cards: first consider groups of objects in the same scene, sequences of images/symbols, scale differences, spatial adjacency, color bands, rule lines, or typographic rhythm.
|
||||||
|
|
||||||
|
When the core message is process, hierarchy, system, propagation, causality, contrast, or spatial relationships, treat structured graphics (SmartArt-like figures), maps, routes, or method diagrams as primary visual material. The chosen structure diagram should occupy enough area and participate in the whole-page composition, implemented with editable PPTD elements where possible; generating native OOXML SmartArt objects is not required, but do not let it degenerate into a row of rounded boxes, generic icons, and thin arrows just for the sake of "editability." It can form one continuous visual field together with the main object, scene imagery, close-up details, and anchored captions.
|
||||||
|
|
||||||
|
| Core relationship | Possible expressions |
|
||||||
|
|---|---|
|
||||||
|
| Time evolution, stage changes | Timeline, milestones, before/after states |
|
||||||
|
| Paths, propagation, migration | Maps, routes, flows, or networks |
|
||||||
|
| Steps, mechanisms, methods | Process, swimlanes, input–process–output |
|
||||||
|
| Differences between objects or viewpoints | Juxtaposed comparison, matrix, scale axis |
|
||||||
|
| Hierarchy, composition, systems | Tree hierarchy, architecture, layered section |
|
||||||
|
| Physical objects, products, paper method details | Object annotation, local zoom, visual anatomy |
|
||||||
|
| Multiple facts or samples | Image matrix, taxonomy map, data field |
|
||||||
|
| Emotion, ideas, attitudes | Single strong image, pure typographic composition, abstract form |
|
||||||
|
|
||||||
|
- When the user explicitly requests a "map," provide a geographic base or outline that can establish spatial relationships; do not pass off an abstract polyline as a map.
|
||||||
|
- For routes, propagation, and migration, choose on your own among a full-bleed map, a local locator, a radial network, land-sea layering, or multiple branch routes based on semantics; do not default to thin horizontal lines.
|
||||||
|
- For architecture diagrams, UML, and paper method figures, first ensure the nodes, interfaces, directions, grouping, and legends are correct before visual beautification.
|
||||||
|
|
||||||
|
## Design Freedom and Quality Baselines
|
||||||
|
|
||||||
|
- Colors, fonts, image treatment, and composition arise from the subject and `style.md`. Unless the user explicitly requests it, do not default to black-white-silver-gray, a single-point fluorescent accent, giant cropped type, archival micro-labels, or thin-line timelines.
|
||||||
|
- A poster should first establish one dominant visual grammar, with other elements serving it. Do not cram the hero image, giant type, map, timeline, icon matrix, and data charts in all at once just to seem "rich."
|
||||||
|
- Establish at least one visual memory point that remains recognizable in a full-page thumbnail or at a distance, carried by the hero image, giant type, a large object, a full-bleed color field, a scene, or the core relationship diagram. If the thumbnail shows only a title and a set of regular boxes, recompose.
|
||||||
|
- When producing multiple pieces on different subjects in succession, do not default to reusing the previous one's main composition, accent-color roles, image proportion, and linear graphics.
|
||||||
|
- Grids, whitespace, cards, collages, asymmetry, or full-bleed are all optional means, not the default correct answer. Avoid stuffing every category of content below the title into equal-width, equal-height rectangles with rounded corners, outlines, or shadows, forming a "three-card/four-card + CTA" slide or app-interface skeleton. Use containers only when the container itself has clear semantics and will not form a uniform card wall; otherwise prefer organizing content with layout zones, proportion, color blocks, rule lines, spatial adjacency, object groups, or typographic rhythm.
|
||||||
|
- A page may have very little text, or it may have several high-density modules; but the core message, primary visual focus, and reading path must be clear. Organize rich content primarily through scale hierarchy, local zooms, captions, and spatial relationships; do not disguise sparse content as rich with repeated cards or oversized padding, and do not compress a large number of facts into unreadably small text.
|
||||||
|
- Body text, annotations, legends, and sources must be readable in the actual output scenario. When there is too much information, cut, layer, or split pages; do not solve it with tiny font sizes.
|
||||||
|
- Data, facts, dates, locations, labels, units, and sources must stay faithful to the user's materials or verifiable information. For precise figures that cannot be confirmed, use qualitative statements, approximations, or omit them; do not fabricate statistics, coordinates, issue numbers, or publication details for an "archival feel."
|
||||||
|
|
||||||
|
### Few-shot: Multiple Complete Briefs and Anonymous Visual-Move Fragments
|
||||||
|
|
||||||
|
The following complete samples are used to expand the design moves, primary visual media, and risk awareness you can draw on; they are not fixed visual families, subject mappings, or templates awaiting selection. The concept name, visual DNA, colors, fonts, canvas, and composition of an actual brief must be re-derived from the current subject, assets, and reading relationships; you may recombine a few compatible moves across samples, or devise your own entirely — do not just pick the most similar label and copy the whole set.
|
||||||
|
|
||||||
|
#### Poster Editorial
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Style Brief — Poster Editorial
|
||||||
|
- Goal and audience: designed for quick browsing and short dwell; make the subject memorable first, then let readers discover archival details.
|
||||||
|
- Output scenario and canvas: choose landscape or portrait based on the actual medium; do not take a 16:9 layout and crop it.
|
||||||
|
- Core message: string together the object, time, and context with one judgment that takes a position; do not use section-style empty titles.
|
||||||
|
- Visual concept: "a contemporary, editorially curated object archive"; calm, sharp, with print tension, without imitating a real magazine cover.
|
||||||
|
- Primary visual focus: choose only one of three as the protagonist — a high-contrast object image, an object silhouette, or giant type.
|
||||||
|
- Reading order and composition: use a rigorous grid with a clear dense-versus-sparse counterpoint; the title, exhibit item, and micro captions form three scales, but not every poster is required to have a route or timeline.
|
||||||
|
- Color: Editorial White `#F7F6F2` and Deep Black `#0B0B0B` carry the main relationship; silver-gray is used only for secondary information; choose at most one small-area accent from Signal Red `#F20505`, Fluoro Lime `#D6FF00`, or Acid Orange `#FF4A1C`.
|
||||||
|
- Fonts: the main title uses 得意黑/阿里妈妈数黑体 with HedvigLettersSans; the body uses MiSans/Liter; at most three font families, and the numbering font does not carry body text.
|
||||||
|
- Image treatment: prefer images with strong contours, close-ups, and room for bold cropping; they may be converted to black and white or desaturated, but keep the object's recognition points.
|
||||||
|
- Graphic language: use hairlines, short labels, and small nodes only when content relationships need them; maps must have a spatial base, and data charts must have real data.
|
||||||
|
- Information density: keep one clear visual breathing area; archival details may be dense, but body text and captions must be readable at the actual output size.
|
||||||
|
- Must avoid: rounded-card walls, evenly distributed accent colors, fake logos/issue numbers/archives, and stacking giant type, a map, a timeline, and an icon matrix all at once just to seem rich.
|
||||||
|
- Reason for choosing: when the subject needs to be highly memorable while retaining traceable details, this direction is superior to a generic commercial infographic.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Geometric Composition
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Style Brief — Geometric Composition
|
||||||
|
- Visual concept: translate the core concept into tension among circles, squares, lines, proportions, and glyphs; do not use geometric blocks to decorate empty space.
|
||||||
|
- Primary visual focus: one geometric relationship or glyph composition occupying the main area, not multiple evenly distributed colored rectangles.
|
||||||
|
- Poster-level visual move: let the title pass through, embed into, or cut across the main geometry, so text and composition together become the image, instead of the title hovering above the content.
|
||||||
|
- Visual DNA: decide color roles and typographic temperament from the subject; establish order with proportion, axes, numbering, and scale, with factual information taking its place along the geometric relationships.
|
||||||
|
- Information organization: bind concept differences, proportions, stages, or a work index to shape relationships; secondary material is tucked into the edges or a narrow index band.
|
||||||
|
- Must avoid: evenly tiling primary colors, a children's-building-block feel, random circles and squares unrelated to the content, and degenerating the geometric composition into a colored card wall.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Material Evidence
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Style Brief — Material Evidence
|
||||||
|
- Visual concept: treat materials, artifacts, surface traces, or the making process as evidence, letting tactile quality and provenance relationships tell the content together.
|
||||||
|
- Primary visual focus: a sufficiently large macro, scan, section, or artifact detail; other images relate to it as slices, sequences, or provenance evidence.
|
||||||
|
- Poster-level visual move: let the texture or object span the main extent of the canvas, with captions anchored directly to visible details; do not shrink high-quality assets into specimen cards.
|
||||||
|
- Visual DNA: extract color, grain, and line language from the actual object; fonts and labels serve the material's temperament; maps, genealogies, or processes appear only when relationships need them.
|
||||||
|
- Information organization: organize facts through source labels, local zooms, material genealogy, craft steps, or propagation paths, keeping one clear breathing area.
|
||||||
|
- Must avoid: uniform brown retro templates, travel souvenir albums, forged old archives, decorative seals, and a row of equally sized object cards.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Dark-Field Signals
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Style Brief — Dark-Field Signals
|
||||||
|
- Visual concept: in a dark field, use localized light sources, trajectories, scales, or rhythmic signals to establish a sense of direction; black is space, not background fill.
|
||||||
|
- Primary visual focus: one brightly lit object, motion image, glowing trail, or high-contrast glyph form, with other elements building distance and speed around it.
|
||||||
|
- Poster-level visual move: let a signal line, path, or band of light cross the canvas and connect fact nodes, so the subject and direction of motion remain recognizable from a distance.
|
||||||
|
- Visual DNA: accent colors serve only nodes, directions, numbers, or warnings; numeric fonts, coordinates, and scanning elements must serve reading rather than create tech noise.
|
||||||
|
- Information organization: organize route, time, frequency, cohort, or status data along a common coordinate field; keep a stable high-contrast safe zone for body text.
|
||||||
|
- Must avoid: full-page neon, bloom effects, cyber dashboards, small gray text on black, and packing every data group into a glowing panel.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Scientific Observation
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Style Brief — Scientific Observation
|
||||||
|
- Visual concept: treat the page as a verifiable observation field, using objects, sections, relationships, and scale to explain a mechanism, rather than presenting a popular-science column.
|
||||||
|
- Primary visual focus: a main specimen, an ecological section, a system relationship, or a local zoom; readers see the object first, then understand the mechanism along the annotations.
|
||||||
|
- Poster-level visual move: let scale, depth, hierarchy, flow direction, or causality run through the whole page, with imagery and legend sharing the same spatial coordinates.
|
||||||
|
- Visual DNA: colors distinguish objects, relationships, and risks; annotation lines, units, legends, sources, and confidence boundaries stay readable and checkable.
|
||||||
|
- Information organization: mechanisms, specimens, and evidence are organized through spatial adjacency and connecting relationships; use a small amount of comparison when necessary, and do not default to three-column cards.
|
||||||
|
- Must avoid: technical-report front pages, white dashboards, equal-width info boxes, decorative data, and unverifiable precise figures.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Dynamic Collage
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Style Brief — Dynamic Collage
|
||||||
|
- Visual concept: use directional glyphs, cropped imagery, color bands, and event fragments to create a manifesto feel, with rhythm coming from content conflicts rather than decorative noise.
|
||||||
|
- Primary visual focus: one boldly cropped title, figure/object silhouette, or high-contrast image; do not let multiple assets compete evenly for attention.
|
||||||
|
- Poster-level visual move: interweave the title, imagery, and a diagonal or offset structure to form a direction and posture recognizable from a distance.
|
||||||
|
- Visual DNA: maintain one dominant contrast relationship; collage edges, numbering, event nodes, and short phrases share a unified logic of angle, weight, and spacing.
|
||||||
|
- Information organization: compress viewpoints, impacts, events, or stages into a few directional band sequences; body text returns to a stable grid to stay readable.
|
||||||
|
- Must avoid: promotional ads, price tags, meaningless torn paper, slogan stacking, too many angles, and enlarging all text at once.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Detachable Anonymous Move Fragments
|
||||||
|
|
||||||
|
The following provide only local compositional knowledge, not complete directions; do not name the fragments, and do not match subjects to them one by one. First state the current page's core message and dominant relationship, then adapt one or two of these moves as needed, or devise your own entirely:
|
||||||
|
|
||||||
|
- Let the title pass through, embed into, or cut across a main geometry that carries information; facts take their place along proportions, axes, numbering, or shape relationships — the geometry is not just space-filling decoration.
|
||||||
|
- Let objects, textures, macros, or sections span the main extent of the canvas, with captions anchored directly to visible details; other images form slice, sequence, or provenance relationships rather than shrinking into a row of specimen cards.
|
||||||
|
- Let routes, light bands, or motion trails cross the canvas and connect fact nodes while retaining the necessary geographic, scene, branch, and time information; paths can be formed by imagery, terrain, or spatial relationships rather than defaulting to thin-line charts.
|
||||||
|
- Let specimens, sections, scale, legends, and causal relationships share one continuous observation field; when immersion matters, preserve the continuity of the environment rather than degenerating into a "main image left + evidence box right" technical report.
|
||||||
|
- Interweave a recognizable title or object, cropped imagery, and a directional structure; events and viewpoints unfold along a unified rhythm, and avoid letting the title's recognizability drown in collage noise.
|
||||||
|
|
||||||
|
In an actual task, regenerate `style.md` from the subject, assets, and reading relationships, using a concept name unique to the current task. Do not first look through the fragments for the "most similar category," and do not write "selected the so-and-so family" in the brief. Keep one dominant visual grammar, with other moves only as compatible support; the final direction must explain why it suits the current content, rather than merely swapping the sample's subject text and accent color.
|
||||||
|
|
||||||
|
## Imagery, Copyright, and Authenticity
|
||||||
|
|
||||||
|
- Prefer images with a clear subject, usable room for cropping, and genuine semantic relevance to the content. Ensure objects and evidence are correct before stylizing.
|
||||||
|
- When relying on images to determine composition, actually view the candidate images before deciding subject placement, crop lines, and text safe areas; do not decide from keywords or imagination alone.
|
||||||
|
- Once a good enough candidate image has been found or generated, let it truly enter the main composition; do not swap specific people, artifacts, materials, or environments back into generic vector icons, silhouettes, or schematic stand-ins just to save implementation effort.
|
||||||
|
- When choosing photography or real-object imagery as the primary visual grammar, let key images occupy enough area on the canvas to create a memorable impression; do not shrink all high-quality assets into small circular images inside cards or decorative thumbnails. Multiple images can form an overall relationship through cropping, overlapping, matrixing, sequencing, or local zoom.
|
||||||
|
- When choosing custom vectors as the primary visual grammar, let contours, proportions, connections, scale, or annotations express the specific information of the current subject; do not pass off generic icons, default process boxes, or decorative geometry as content. Complex vector figures should likewise have a clear visual focus and a thumbnail memory point.
|
||||||
|
- When mixing imagery and vectors, define the division of labor clearly: imagery carries object evidence, materials, people, or environments; vectors carry relationships, paths, scale, legends, and annotations — and anchor them to each other within the same composition, rather than splitting into two unrelated blocks above and below.
|
||||||
|
- Use self-owned, licensed, officially citable, or generated images. Do not forge real magazines, institutions, brand logos, issue numbers, watermarks, or attribution.
|
||||||
|
- AI images must not pose as historical archives, scientific evidence, or real data. Label them "AI-generated illustration" when necessary.
|
||||||
|
|
||||||
|
## Editable Delivery and Verification
|
||||||
|
|
||||||
|
- Keep text, shapes, connectors, data labels, and legends as editable PPTD elements as much as possible; do not merge the entire infographic into a single bitmap.
|
||||||
|
- When the user only wants images such as PNG or JPG, still complete the editable source in PPTD first, then screenshot or render it to an image.
|
||||||
|
- Check out-of-bounds, overflow, overlap, and structural issues against the pptd format rules; visually review focus, reading order, contrast, font sizes, and image cropping.
|
||||||
|
- Look at the full-page overview first, then the details: the overview should reveal the subject and at least one visual memory point; if the main impression is a title, regular cards, and button-like color blocks, restructure first instead of only fine-tuning font sizes and spacing.
|
||||||
|
- After fixing problem pages, validate and review again, and only then package the PPTX or output images.
|
||||||
|
|
||||||
|
## Pre-Production Checklist
|
||||||
|
|
||||||
|
- Have you explored different directions based on the subject and written a specific, actionable `style.md` in the project?
|
||||||
|
- Do the canvas ratio and orientation fit the user's requirements, the deployment scenario, the content structure, and the reading order?
|
||||||
|
- Are the core message, primary visual focus, and dominant visual grammar clear?
|
||||||
|
- Did you actually compare hero-image or structure-diagram candidates before finalizing, and does the chosen asset/figure truly enter the main composition?
|
||||||
|
- Is there a poster-level visual move recognizable at thumbnail size, rather than an ordinary content page, card wall, or app interface?
|
||||||
|
- Do the map, timeline, routes, flowcharts, matrix, and index genuinely express core relationships, rather than serving as decoration or density filler?
|
||||||
|
- Do the colors, fonts, image treatment, composition, and graphic language come from the current subject, rather than defaulting to reusing the previous poster case?
|
||||||
|
- Are facts, data, dates, locations, units, captions, sources, and copyright boundaries complete and verifiable?
|
||||||
|
- Are body text, annotations, and legends readable at the actual output size, without relying on extremely small fonts to cram in information?
|
||||||
|
- Does the visual result in the screenshot truly match `style.md`, rather than merely passing the syntax check?
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
## Parameter Value Conventions
|
||||||
|
|
||||||
|
* The `adjustments` parameters are described in the order and quantity defined by OOXML
|
||||||
|
* Values generally range over [0, 100000], representing a percentage (100000 = 100%)
|
||||||
|
* Angle parameters are in units of 1/60000 of a degree in OOXML; conversion formula: `OOXML value = degrees × 60000` (for example, `16200000` = 270°)
|
||||||
|
* The parameter array must remain complete (intermediate values cannot be omitted), or be omitted entirely to use default values
|
||||||
|
* `-` indicates the shape has no adjustable parameters
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Basic Shapes
|
||||||
|
|
||||||
|
| shapeName | Description | adjustments parameters | Default values |
|
||||||
|
| --------- | --- | -------------- | ---- |
|
||||||
|
| rect | Rectangle | - | - |
|
||||||
|
| roundRect | Rounded rectangle | [corner radius] | [16667] |
|
||||||
|
| ellipse | Ellipse | - | - |
|
||||||
|
| triangle | Triangle | [horizontal position of apex] | [50000] |
|
||||||
|
| rtTriangle | Right triangle | - | - |
|
||||||
|
| parallelogram | Parallelogram | [slant offset] | [25000] |
|
||||||
|
| trapezoid | Trapezoid | [top edge inset] | [25000] |
|
||||||
|
| nonIsoscelesTrapezoid | Non-isosceles trapezoid | [left offset, right offset] | [25000, 25000] |
|
||||||
|
| diamond | Diamond | - | - |
|
||||||
|
| pentagon | Regular pentagon | [horizontal factor, vertical factor] | [105146, 110557] |
|
||||||
|
| hexagon | Hexagon | [inset offset, vertical factor] | [25000, 115470] |
|
||||||
|
| heptagon | Heptagon | [horizontal factor, vertical factor] | [102572, 105210] |
|
||||||
|
| octagon | Octagon | [corner cut size] | [29289] |
|
||||||
|
| decagon | Decagon | [vertical factor] | [105146] |
|
||||||
|
| dodecagon | Dodecagon | - | - |
|
||||||
|
| plus | Plus sign | [arm width ratio] | [25000] |
|
||||||
|
| homePlate | Five-sided arrow | [arrowhead tip offset] | [50000] |
|
||||||
|
| chevron | V-shaped arrow | [V tip offset] | [50000] |
|
||||||
|
| pie | Pie | [start angle°, end angle°] | [0, 16200000] |
|
||||||
|
| pieWedge | Pie wedge | - | - |
|
||||||
|
| arc | Arc | [start angle°, end angle°] | [16200000, 0] |
|
||||||
|
| chord | Chord | [start angle°, end angle°] | [2700000, 16200000] |
|
||||||
|
| blockArc | Block arc | [start angle°, end angle°, thickness] | [10800000, 0, 25000] |
|
||||||
|
| teardrop | Teardrop | [tail extension ratio] | [100000] |
|
||||||
|
| frame | Frame | [border thickness] | [12500] |
|
||||||
|
| halfFrame | Half frame | [horizontal thickness, vertical thickness] | [33333, 33333] |
|
||||||
|
| corner | Corner | [horizontal thickness, vertical thickness] | [50000, 50000] |
|
||||||
|
| diagStripe | Diagonal stripe | [stripe width] | [50000] |
|
||||||
|
| foldedCorner | Folded corner | - | - |
|
||||||
|
| donut | Donut | [ring width ratio] | [25000] |
|
||||||
|
| noSmoking | Prohibition symbol | [slash width] | [18750] |
|
||||||
|
| heart | Heart | - | - |
|
||||||
|
| lightningBolt | Lightning bolt | - | - |
|
||||||
|
| sun | Sun | [inner radius ratio of rays] | [25000] |
|
||||||
|
| moon | Moon | [crescent width] | [50000] |
|
||||||
|
| cloud | Cloud | - | - |
|
||||||
|
| smileyFace | Smiley face | [mouth curvature] | [4653] |
|
||||||
|
| bevel | Bevel | [bevel width] | [12500] |
|
||||||
|
| can | Cylinder | [elliptical lid height] | [25000] |
|
||||||
|
| cube | Cube | [perspective depth] | [25000] |
|
||||||
|
| funnel | Funnel | - | - |
|
||||||
|
| gear6 | Six-tooth gear | [tooth height, tooth width] | [15000, 3526] |
|
||||||
|
| gear9 | Nine-tooth gear | [tooth height, tooth width] | [10000, 1763] |
|
||||||
|
| plaque | Plaque | [corner radius] | [16667] |
|
||||||
|
| doubleWave | Double wave | [wave amplitude, horizontal offset] | [6250, 0] |
|
||||||
|
| wave | Wave | [wave amplitude, horizontal offset] | [12500, 0] |
|
||||||
|
| lineInv | Inverse line | - | - |
|
||||||
|
|
||||||
|
## Rectangle Variants
|
||||||
|
|
||||||
|
| shapeName | Description | adjustments parameters | Default values |
|
||||||
|
| --------- | --- | -------------- | ---- |
|
||||||
|
| round1Rect | Single-rounded-corner rectangle | [corner radius] | [16667] |
|
||||||
|
| round2DiagRect | Rectangle with two diagonally rounded corners | [corner 1 radius, corner 2 radius] | [16667, 0] |
|
||||||
|
| round2SameRect | Rectangle with two same-side rounded corners | [top corner radius, bottom corner radius] | [16667, 0] |
|
||||||
|
| snip1Rect | Single-cut-corner rectangle | [corner cut size] | [16667] |
|
||||||
|
| snip2DiagRect | Rectangle with two diagonally cut corners | [corner 1 cut size, corner 2 cut size] | [0, 16667] |
|
||||||
|
| snip2SameRect | Rectangle with two same-side cut corners | [top cut size, bottom cut size] | [16667, 0] |
|
||||||
|
| snipRoundRect | Rectangle with one rounded and one cut corner | [corner radius, corner cut size] | [16667, 16667] |
|
||||||
|
|
||||||
|
## Stars and Bursts
|
||||||
|
|
||||||
|
| shapeName | Description | adjustments parameters | Default values |
|
||||||
|
| --------- | --- | -------------- | ---- |
|
||||||
|
| star4 | 4-point star | [inner radius ratio] | [12500] |
|
||||||
|
| star5 | 5-point star | [inner radius ratio, horizontal factor, vertical factor] | [19098, 105146, 110557] |
|
||||||
|
| star6 | 6-point star | [inner radius ratio, horizontal factor] | [28868, 115470] |
|
||||||
|
| star7 | 7-point star | [inner radius ratio, horizontal factor, vertical factor] | [34601, 102572, 105210] |
|
||||||
|
| star8 | 8-point star | [inner radius ratio] | [37500] |
|
||||||
|
| star10 | 10-point star | [inner radius ratio, horizontal factor] | [42533, 105146] |
|
||||||
|
| star12 | 12-point star | [inner radius ratio] | [37500] |
|
||||||
|
| star16 | 16-point star | [inner radius ratio] | [37500] |
|
||||||
|
| star24 | 24-point star | [inner radius ratio] | [37500] |
|
||||||
|
| star32 | 32-point star | [inner radius ratio] | [37500] |
|
||||||
|
| irregularSeal1 | Burst 1 | - | - |
|
||||||
|
| irregularSeal2 | Burst 2 | - | - |
|
||||||
|
|
||||||
|
## Arrow Shapes
|
||||||
|
|
||||||
|
| shapeName | Description | adjustments parameters | Default values |
|
||||||
|
| --------- | --- | -------------- | ---- |
|
||||||
|
| rightArrow | Right arrow | [shaft width, arrowhead length] | [50000, 50000] |
|
||||||
|
| leftArrow | Left arrow | [shaft width, arrowhead length] | [50000, 50000] |
|
||||||
|
| upArrow | Up arrow | [shaft width, arrowhead length] | [50000, 50000] |
|
||||||
|
| downArrow | Down arrow | [shaft width, arrowhead length] | [50000, 50000] |
|
||||||
|
| leftRightArrow | Left-right arrow | [shaft width, arrowhead length] | [50000, 50000] |
|
||||||
|
| upDownArrow | Up-down arrow | [shaft width, up arrowhead length, shaft width, down arrowhead length] | [50000, 50000, 50000, 50000] |
|
||||||
|
| quadArrow | Quad arrow | [shaft width, arrowhead width, arrowhead length] | [22500, 22500, 22500] |
|
||||||
|
| leftRightUpArrow | Left-right-up arrow | [shaft width, arrowhead width, arrowhead length] | [25000, 25000, 25000] |
|
||||||
|
| leftUpArrow | Left-up arrow | [shaft width, arrowhead width, arrowhead length] | [25000, 25000, 25000] |
|
||||||
|
| bentArrow | Bent arrow | [shaft width, arrowhead width, arrowhead length, bend position] | [25000, 25000, 25000, 43750] |
|
||||||
|
| bentUpArrow | Bent up arrow | [shaft width, arrowhead width, arrowhead length] | [25000, 25000, 25000] |
|
||||||
|
| uturnArrow | U-turn arrow | [shaft width, arrowhead width, arrowhead length, bend radius, shaft length] | [25000, 25000, 25000, 43750, 75000] |
|
||||||
|
| circularArrow | Circular arrow | [arrowhead width, start angle°, end angle°, arc angle°, arrow tip width] | [12500, 1142319, 20457681, 10800000, 12500] |
|
||||||
|
| leftCircularArrow | Left circular arrow | [arrowhead width, start angle°, end angle°, arc angle°, arrow tip width] | [12500, -1142319, 1142319, 10800000, 12500] |
|
||||||
|
| leftRightCircularArrow | Left-right circular arrow | [arrowhead width, start angle°, end angle°, arc angle°, arrow tip width] | [12500, 1142319, 20457681, 11942319, 12500] |
|
||||||
|
| curvedRightArrow | Curved right arrow | [arrowhead width, curvature, arrowhead length] | [25000, 50000, 25000] |
|
||||||
|
| curvedLeftArrow | Curved left arrow | [arrowhead width, curvature, arrowhead length] | [25000, 50000, 25000] |
|
||||||
|
| curvedUpArrow | Curved up arrow | [arrowhead width, curvature, arrowhead length] | [25000, 50000, 25000] |
|
||||||
|
| curvedDownArrow | Curved down arrow | [arrowhead width, curvature, arrowhead length] | [25000, 50000, 25000] |
|
||||||
|
| stripedRightArrow | Striped right arrow | [shaft width, arrowhead length] | [50000, 50000] |
|
||||||
|
| notchedRightArrow | Notched right arrow | [shaft width, arrowhead length] | [50000, 50000] |
|
||||||
|
| swooshArrow | Swoosh arrow | [tail width, arrowhead length] | [25000, 16667] |
|
||||||
|
|
||||||
|
## Arrow Callouts
|
||||||
|
|
||||||
|
| shapeName | Description | adjustments parameters | Default values |
|
||||||
|
| --------- | --- | -------------- | ---- |
|
||||||
|
| rightArrowCallout | Right arrow callout | [shaft width, arrowhead width, arrowhead length, box width] | [25000, 25000, 25000, 64977] |
|
||||||
|
| leftArrowCallout | Left arrow callout | [shaft width, arrowhead width, arrowhead length, box width] | [25000, 25000, 25000, 64977] |
|
||||||
|
| upArrowCallout | Up arrow callout | [shaft width, arrowhead width, arrowhead length, box height] | [25000, 25000, 25000, 64977] |
|
||||||
|
| downArrowCallout | Down arrow callout | [shaft width, arrowhead width, arrowhead length, box height] | [25000, 25000, 25000, 64977] |
|
||||||
|
| leftRightArrowCallout | Left-right arrow callout | [shaft width, arrowhead width, arrowhead length, box width] | [25000, 25000, 25000, 48123] |
|
||||||
|
| upDownArrowCallout | Up-down arrow callout | [shaft width, arrowhead width, arrowhead length, box height] | [25000, 25000, 25000, 48123] |
|
||||||
|
| quadArrowCallout | Quad arrow callout | [shaft width, arrowhead width, arrowhead length, box size] | [18515, 18515, 18515, 48123] |
|
||||||
|
|
||||||
|
## Callouts
|
||||||
|
|
||||||
|
| shapeName | Description | adjustments parameters | Default values |
|
||||||
|
| --------- | --- | -------------- | ---- |
|
||||||
|
| wedgeRectCallout | Rectangle callout | [tip X offset, tip Y offset] | [-20833, 62500] |
|
||||||
|
| wedgeRoundRectCallout | Rounded rectangle callout | [tip X offset, tip Y offset, corner radius] | [-20833, 62500, 16667] |
|
||||||
|
| wedgeEllipseCallout | Ellipse callout | [tip X offset, tip Y offset] | [-20833, 62500] |
|
||||||
|
| cloudCallout | Cloud callout | [tip X offset, tip Y offset] | [-20833, 62500] |
|
||||||
|
| borderCallout1 | Line callout 1 | [callout line Y1, X1, Y2, X2] | [18750, -8333, 112500, -38333] |
|
||||||
|
| borderCallout2 | Line callout 2 | [line Y1, X1, knee point Y, X, end point Y, X] | [18750, -8333, 18750, -16667, 112500, -46667] |
|
||||||
|
| borderCallout3 | Line callout 3 | [line Y1, X1, knee point 1 Y, X, knee point 2 Y, X, end point Y, X] | [18750, -8333, 18750, -16667, 100000, -16667, 112963, -8333] |
|
||||||
|
| accentCallout1 | Accent line callout 1 | Same as borderCallout1 | [18750, -8333, 112500, -38333] |
|
||||||
|
| accentCallout2 | Accent line callout 2 | Same as borderCallout2 | [18750, -8333, 18750, -16667, 112500, -46667] |
|
||||||
|
| accentCallout3 | Accent line callout 3 | Same as borderCallout3 | [18750, -8333, 18750, -16667, 100000, -16667, 112963, -8333] |
|
||||||
|
| accentBorderCallout1 | Bordered accent line callout 1 | Same as borderCallout1 | [18750, -8333, 112500, -38333] |
|
||||||
|
| accentBorderCallout2 | Bordered accent line callout 2 | Same as borderCallout2 | [18750, -8333, 18750, -16667, 112500, -46667] |
|
||||||
|
| accentBorderCallout3 | Bordered accent line callout 3 | Same as borderCallout3 | [18750, -8333, 18750, -16667, 100000, -16667, 112963, -8333] |
|
||||||
|
| callout1 | Borderless callout 1 | Same as borderCallout1 | [18750, -8333, 112500, -38333] |
|
||||||
|
| callout2 | Borderless callout 2 | Same as borderCallout2 | [18750, -8333, 18750, -16667, 112500, -46667] |
|
||||||
|
| callout3 | Borderless callout 3 | Same as borderCallout3 | [18750, -8333, 18750, -16667, 100000, -16667, 112963, -8333] |
|
||||||
|
|
||||||
|
> **Callout tip offset note**: The X/Y offset values of callout shapes use the shape center as the origin, in units of a percentage of the shape's width/height. Negative values mean the tip is on the left side/above the shape.
|
||||||
|
|
||||||
|
## Brackets and Braces
|
||||||
|
|
||||||
|
| shapeName | Description | adjustments parameters | Default values |
|
||||||
|
| --------- | --- | -------------- | ---- |
|
||||||
|
| leftBrace | Left brace | [curvature, middle point position] | [8333, 50000] |
|
||||||
|
| rightBrace | Right brace | [curvature, middle point position] | [8333, 50000] |
|
||||||
|
| leftBracket | Left bracket | [curvature] | [8333] |
|
||||||
|
| rightBracket | Right bracket | [curvature] | [8333] |
|
||||||
|
| bracePair | Brace pair | [curvature] | [8333] |
|
||||||
|
| bracketPair | Bracket pair | [curvature] | [16667] |
|
||||||
|
|
||||||
|
## Ribbons
|
||||||
|
|
||||||
|
| shapeName | Description | adjustments parameters | Default values |
|
||||||
|
| --------- | --- | -------------- | ---- |
|
||||||
|
| ribbon | Down-curved ribbon | [fold height, ribbon curvature] | [16667, 50000] |
|
||||||
|
| ribbon2 | Up-curved ribbon | [fold height, ribbon curvature] | [16667, 50000] |
|
||||||
|
| ellipseRibbon | Curved-surface down ribbon | [curvature, middle height, fold height] | [25000, 50000, 12500] |
|
||||||
|
| ellipseRibbon2 | Curved-surface up ribbon | [curvature, middle height, fold height] | [25000, 50000, 12500] |
|
||||||
|
| leftRightRibbon | Left-right ribbon | [fold height, ribbon curvature, fold width] | [50000, 50000, 16667] |
|
||||||
|
|
||||||
|
## Scrolls
|
||||||
|
|
||||||
|
| shapeName | Description | adjustments parameters | Default values |
|
||||||
|
| --------- | --- | -------------- | ---- |
|
||||||
|
| horizontalScroll | Horizontal scroll | [scroll size] | [12500] |
|
||||||
|
| verticalScroll | Vertical scroll | [scroll size] | [12500] |
|
||||||
|
|
||||||
|
## Math Symbols
|
||||||
|
|
||||||
|
| shapeName | Description | adjustments parameters | Default values |
|
||||||
|
| --------- | --- | -------------- | ---- |
|
||||||
|
| mathPlus | Plus sign | [line thickness] | [23520] |
|
||||||
|
| mathMinus | Minus sign | [line thickness] | [23520] |
|
||||||
|
| mathMultiply | Multiplication sign | [line thickness] | [23520] |
|
||||||
|
| mathDivide | Division sign | [line thickness, dot spacing, dot size] | [23520, 5880, 11760] |
|
||||||
|
| mathEqual | Equal sign | [line thickness, line spacing] | [23520, 11760] |
|
||||||
|
| mathNotEqual | Not-equal sign | [line thickness, slash angle°, line spacing] | [23520, 6600000, 11760] |
|
||||||
|
|
||||||
|
## Chart Shapes
|
||||||
|
|
||||||
|
| shapeName | Description |
|
||||||
|
| --------- | --- |
|
||||||
|
| chartPlus | Chart plus |
|
||||||
|
| chartStar | Chart star |
|
||||||
|
| chartX | Chart X |
|
||||||
|
|
||||||
|
## Tab Shapes
|
||||||
|
|
||||||
|
| shapeName | Description |
|
||||||
|
| --------- | --- |
|
||||||
|
| cornerTabs | Corner tabs |
|
||||||
|
| squareTabs | Square tabs |
|
||||||
|
| plaqueTabs | Plaque tabs |
|
||||||
|
|
||||||
|
## Action Buttons
|
||||||
|
|
||||||
|
| shapeName | Description |
|
||||||
|
| --------- | --- |
|
||||||
|
| actionButtonBackPrevious | Back/Previous button |
|
||||||
|
| actionButtonBeginning | Beginning button |
|
||||||
|
| actionButtonBlank | Blank button |
|
||||||
|
| actionButtonDocument | Document button |
|
||||||
|
| actionButtonEnd | End button |
|
||||||
|
| actionButtonForwardNext | Forward/Next button |
|
||||||
|
| actionButtonHelp | Help button |
|
||||||
|
| actionButtonHome | Home button |
|
||||||
|
| actionButtonInformation | Information button |
|
||||||
|
| actionButtonMovie | Movie button |
|
||||||
|
| actionButtonReturn | Return button |
|
||||||
|
| actionButtonSound | Sound button |
|
||||||
|
|
||||||
|
## Flowchart Shapes
|
||||||
|
|
||||||
|
| shapeName | Description |
|
||||||
|
| --------- | --- |
|
||||||
|
| flowChartProcess | Flowchart: Process |
|
||||||
|
| flowChartAlternateProcess | Flowchart: Alternate process |
|
||||||
|
| flowChartDecision | Flowchart: Decision |
|
||||||
|
| flowChartDocument | Flowchart: Document |
|
||||||
|
| flowChartMultidocument | Flowchart: Multidocument |
|
||||||
|
| flowChartInputOutput | Flowchart: Data |
|
||||||
|
| flowChartPredefinedProcess | Flowchart: Predefined process |
|
||||||
|
| flowChartInternalStorage | Flowchart: Internal storage |
|
||||||
|
| flowChartManualInput | Flowchart: Manual input |
|
||||||
|
| flowChartManualOperation | Flowchart: Manual operation |
|
||||||
|
| flowChartPreparation | Flowchart: Preparation |
|
||||||
|
| flowChartDelay | Flowchart: Delay |
|
||||||
|
| flowChartTerminator | Flowchart: Terminator |
|
||||||
|
| flowChartConnector | Flowchart: Connector |
|
||||||
|
| flowChartOffpageConnector | Flowchart: Off-page connector |
|
||||||
|
| flowChartPunchedCard | Flowchart: Punched card |
|
||||||
|
| flowChartPunchedTape | Flowchart: Punched tape |
|
||||||
|
| flowChartCollate | Flowchart: Collate |
|
||||||
|
| flowChartSort | Flowchart: Sort |
|
||||||
|
| flowChartExtract | Flowchart: Extract |
|
||||||
|
| flowChartMerge | Flowchart: Merge |
|
||||||
|
| flowChartOr | Flowchart: Or |
|
||||||
|
| flowChartSummingJunction | Flowchart: Summing junction |
|
||||||
|
| flowChartOnlineStorage | Flowchart: Online storage |
|
||||||
|
| flowChartMagneticDisk | Flowchart: Magnetic disk |
|
||||||
|
| flowChartMagneticDrum | Flowchart: Magnetic drum |
|
||||||
|
| flowChartMagneticTape | Flowchart: Magnetic tape |
|
||||||
|
| flowChartOfflineStorage | Flowchart: Offline storage |
|
||||||
|
| flowChartDisplay | Flowchart: Display |
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# PPT category guide
|
||||||
|
|
||||||
|
1. **Follow the general rules**: the general rules apply to all scenarios and all pages, and take effect together with the style guidance documents
|
||||||
|
2. **Determine the scenario**: choose the matching scenario based on the user's input
|
||||||
|
3. **Read the scenario document**: read the document for that scenario and design according to its expressive focus and approach
|
||||||
|
|
||||||
|
## step1. General rules
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
1. **Every page has a clear reader task**: what this page should make the reader understand, believe, decide, or do — think this through before designing.
|
||||||
|
2. **Paging has rhythm**: decide for yourself whether a table of contents or section dividers are needed; the reader should feel a change of rhythm as they flip through — some pages are taken in at a glance, others are worth stopping to read carefully.
|
||||||
|
3. **Use charts and shape combinations flexibly**: if a body of information can be expressed through a complex chart that goes beyond what the current chart syntax can express, you are encouraged to flexibly use shapes and other means to construct the expression.
|
||||||
|
4. **Master-level output**: every PPT is a carefully crafted work of art that could be entered into a competition; pay close attention to every detail of layout, typography, and color — details determine success or failure.
|
||||||
|
5. **Use image search/generation sensibly**: use image search/generation tools to obtain images and place them in suitable positions. But image abuse is strictly forbidden. If the user's uploaded files contain useful images, use them on suitable pages.
|
||||||
|
6. **Defer to the user and the subject**: user-specified templates, brand guidelines, color schemes, fonts, and style references take priority over this guide.
|
||||||
|
7. **Source attribution**: pages involving external facts and data must state the source, date or time period, and measurement basis. Source text should use an <a href="url"> hyperlink pointing to the original report or data page. When citing specific sources in footnotes, likewise use <a href="url"> hyperlinks to the original source to strengthen professional credibility.
|
||||||
|
|
||||||
|
### Strictly forbidden
|
||||||
|
- **Evidence boundaries**: do not fabricate data, citations, customer cases, experimental results, or sources; when material is missing, clearly mark it as a placeholder, an assumption, or to-be-supplied information.
|
||||||
|
- **Classic AI patterns**: it is strictly forbidden to use cards to build hierarchy or alignment (rounded rectangles, rectangular cards, cards with a colored side strip): lines, whitespace, and font-size contrast are better solutions; it is strictly forbidden to use the AI color scheme where red, purple, yellow, and green are all gathered on one page;
|
||||||
|
|
||||||
|
## step2. Scenario determination
|
||||||
|
Based on the user's input, analyze the presentation's audience and reader tasks, determine the scenario it belongs to, and read the corresponding style document.
|
||||||
|
> Choose one primary scenario. When truly necessary, you may add one auxiliary scenario, but the primary scenario must prevail.
|
||||||
|
|
||||||
|
| Scenario type | Typical queries | Reader task | Style document |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Analysis & decision | Consulting, finance, industry research, strategy, market opportunities, business analysis, investment analysis | Compare options, form judgments, support decisions | `reference/slides_categories/analysis-decision.md` |
|
||||||
|
| Business proposal | Marketing plans, sales proposals, fundraising pitches, partnership/investment promotion, product proposals, business plans | Understand the value, believe in the plan, take action | `reference/slides_categories/business-plan.md` |
|
||||||
|
| Management reporting | Work reports, project retrospectives, quarterly summaries, OKR, management briefings | Grasp the current state, surface problems, confirm actions | `reference/slides_categories/management-report.md` |
|
||||||
|
| Academic research | Graduate research projects, thesis defenses, research projects, proposal reports, mid-term reports, final/concluding reports | Evaluate the problem, method, evidence, and contribution | `reference/slides_categories/academic-research.md` |
|
||||||
|
| Education & training / knowledge popularization | K-12 courseware, teaching demonstrations, vocational training, patient education, professional popular science | Understand, remember, apply, or act correctly | `reference/slides_categories/education-training.md` |
|
||||||
|
| Tech & engineering | Engineering plans, architecture reviews, R&D reports, AI / data / ops / security | See the structure, dependencies, metrics, and trade-offs clearly | `reference/slides_categories/tech-engineering.md` |
|
||||||
|
| Brand / creative showcase | Brand stories, design proposals, portfolios, cultural events | Build perception, leave a memory, form identification | `reference/slides_categories/brand-creative.md` |
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# Academic Research
|
||||||
|
|
||||||
|
**Benchmark references**: presentation templates from universities such as MIT, ETH Zürich, Tsinghua University, and Peking University; LaTeX Beamer themes such as Metropolis, Focus, Blei, and Auriga; research figure plates from journals such as Nature, Science, Cell, and NEJM; and presentations at top academic conferences.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
### Narrative: capture the value in the first minute; keep progress visible at all times
|
||||||
|
Follow the narrative logic of research background, evidence gap, research question, experimental hypotheses, core evidence, conclusion, and contribution — letting the committee/advisor grasp the value within the first minute.
|
||||||
|
|
||||||
|
Through proper section planning, adding transition pages at key positions, building navigation bars into the body skeleton, and similar methods, keep the committee/advisor aware of the presentation's progress at all times.
|
||||||
|
|
||||||
|
### Visuals: simple but not cheap
|
||||||
|
Presentations in academic scenarios are often plain, but simplicity does not mean simplistic, much less cheap. Use appropriate, refined colors that are expected in spirit yet unexpected in choice, paired with meticulous layouts to build a premium feel.
|
||||||
|
1. **Keep a minimal style**: decorative elements and decorative images may only be used in areas such as the cover, section transition pages, closing pages, and page background images; strictly forbid using decorative images to fill body areas. Icons are not a cure-all for filling whitespace either — use them only when adding an icon brings a clear benefit.
|
||||||
|
2. **A premium feel is achieved through meticulous detail design**: a unified page skeleton, footnote markers for data sources and references, captions for figures and tables, proper page numbering, exquisitely designed font-size hierarchy and typefaces...
|
||||||
|
3. **Colors expected in spirit, unexpected in choice**:
|
||||||
|
- When the user precisely mentions a school/institution/organization/conference name/journal name, you may download the official logo, use the logo's primary color as the primary color, and insert the logo at reasonable positions. If you cannot obtain the logo, do not use the logo's primary color, to avoid deviation.
|
||||||
|
- Reject the most common palettes of this scenario:
|
||||||
|
* Medical research does not use hospital blue/medical green; turn instead to the hematoxylin purple #684765 and eosin pink #C9828B of pathology slides.
|
||||||
|
* Climate and earth sciences do not use environmental green and ocean blue; adopt rock-strata gray #334047, mineral ochre #B66A3C, and sulfur yellow #C2A33A instead.
|
||||||
|
- Unless the user explicitly requests it, do not use crude, simplistic black, white, or light-blue backgrounds; align toward more refined backgrounds, such as titanium gray #E7E8E5, archival paper #F1E9DA, or ivory paper #F7F3E8.
|
||||||
|
4. **High density at large font sizes**
|
||||||
|
- Academic presentations should maintain high density at appropriate font sizes while retaining some breathing room. Unlike the small font sizes and single line spacing of industry research and finance, academic-research body text should use larger font sizes (above 10) and suitable line spacing (around 1.2), unless the deliverable is a reading-type artifact such as an academic poster. At the same time, it should have the "fullness" of industry research and finance: leave some breathing room between regions, but keep the inside of each region full.
|
||||||
|
- Strictly forbid a region planned for 100px that ends up holding only 70px of content.
|
||||||
|
|
||||||
|
### Start from the research field; consider the visual focus
|
||||||
|
Academic presentations in different research fields have different emphases: think about what truly matters in that field, and decide what each page should focus on.
|
||||||
|
- Basic research and theoretical derivation: formulas, definitions, propositions, and proof steps are the focus; the rigorous, complete reasoning process occupies large areas.
|
||||||
|
- Computer science and engineering: system diagrams, code, run traces, ablation experiments, and performance curves form the page skeleton.
|
||||||
|
- Experimental research: research charts, images, and tables are the focus. Formulas, charts, tables, and images belonging to the same research conclusion should be as compact as possible.
|
||||||
|
- Empirical analysis: essentially also experimental research, but social-science work should be more narrative-driven than science-and-engineering work.
|
||||||
|
- Humanities/architecture/arts: archives, maps, manuscripts, image details, chronological threads, and textual evidence... Beyond academic rigor, there should also be brand-level refinement — go read `brand-creative.md` again.
|
||||||
|
- Use shapes, lines, and arrows as annotations to emphasize or explain chart and image content.
|
||||||
|
|
||||||
|
## Prohibitions
|
||||||
|
|
||||||
|
1. **No cards by default**: unless the user explicitly requests it, strictly forbid using rounded rectangles or rectangular cards to build hierarchy or alignment: line segments, whitespace, and font/size differences are better solutions.
|
||||||
|
2. **No evenly divided compositions**: unless no other layout is available, do not default to one-third splits, four-way splits, or 2×2 matrices — including three-part formulas such as "three columns + title + conclusion."
|
||||||
|
3. **No mediocre, common, or AI-typical color schemes**: strictly forbid any blue-and-white pairing, blue-purple gradients, cyan-purple neon, rainbow flares, glassmorphism cards, or glowing borders — unless the user explicitly requests them.
|
||||||
|
4. **No overly small font sizes**: unless the user requests it or the deliverable is a reading-type artifact (posters, etc.), body text must not use small font sizes.
|
||||||
|
5. **No whitespace inside regions**: strictly forbid any case where the content cannot fill its planned region. It is not enough for the text box to be full — the actual text content, as seen in a screenshot, must fill the region. Failure to fill is the most serious kind of crudeness; it feels like no care went into the layout at all.
|
||||||
|
|
||||||
|
## Visual References
|
||||||
|
|
||||||
|
### ETH Swiss Lab Posters
|
||||||
|
|
||||||
|
**Reference objects**: ETH Zürich official PowerPoint and LaTeX templates, Metropolis.
|
||||||
|
|
||||||
|
**Core visual**: like a set of Swiss research posters pinned to a lab wall. A strict grid, giant conclusions, large solid-color swatches, and full-bleed research images form strong proportions; the pages are quiet but not conservative.
|
||||||
|
|
||||||
|
**Type and grid**:
|
||||||
|
- Use a neutral sans-serif typeface; cover titles 64–88 pt, page titles 36–48 pt, body text 16–19 pt, sources 9–11 pt.
|
||||||
|
- Use clearly asymmetric proportions, commonly 3:9, 4:8, or a full-page main image.
|
||||||
|
|
||||||
|
**Research figures**:
|
||||||
|
- The main figure occupies 60%–75% of the page, with axes, error bars, legend, and source fully preserved.
|
||||||
|
- Photography, micrographs, and remote-sensing images may be laid out full-bleed; titles sit inside opaque color blocks rather than directly over complex imagery.
|
||||||
|
- Highlight only one result per page.
|
||||||
|
|
||||||
|
**Page grammar**:
|
||||||
|
1. Cover: a full-bleed image of the research setting or subject, paired with solid swatches of the primary color to build an asymmetric character.
|
||||||
|
2. Content pages: the flow advances horizontally along a twelve-column grid; **cards are strictly forbidden**.
|
||||||
|
3. Results pages: giant conclusion on the left, complete main figure on the right; on the next page, swap left and right.
|
||||||
|
4. Limitations page: a thick colored line cuts apart "proven" and "not yet certain."
|
||||||
|
5. Closing page: return to the cover's color-swatch proportions and answer the research questions one by one.
|
||||||
|
|
||||||
|
**Recurring motif**: asymmetric solid-color swatches, section numbers, page numbers, notes, reference footnote markers, and thin tick lines along the page edges.
|
||||||
|
|
||||||
|
**Prohibitions**: rounded cards, centered titles, soft shadows, blue-and-white business style, multi-colored charts, and enlarging the institution's emblem into a giant watermark.
|
||||||
|
|
||||||
|
### Tsinghua / MIT-Style Elite-University Defense
|
||||||
|
|
||||||
|
**Reference objects**: the official visual identity systems of Tsinghua University, MIT, and similar institutions; department defense templates; course handouts and research reports.
|
||||||
|
|
||||||
|
**Core visual**: the whole deck carries the orderliness of formal departmental defense materials. Identity is established through the school color, school name, or department emblem; everything else recedes behind the content. Pages are stable, precise, restrained; formulas, figures, and arguments always hold the visual center.
|
||||||
|
|
||||||
|
**Palette**:
|
||||||
|
- When the user explicitly mentions a school or institution, extract one primary color from the official emblem — never hard-code a color value from memory. On ordinary body pages, keep the school color within 3%–8% of the area; section pages may use the school color across the entire page.
|
||||||
|
- Keep scientific color schemes on figures when they carry clear semantics; when they do not, use the school color, light and dark grays, and one necessary contrast color — do not add colors just to enrich the picture.
|
||||||
|
|
||||||
|
**Type and grid**:
|
||||||
|
- Chinese uses a neutral hei (sans-serif) typeface; English and formulas may pair with an academic serif or math typeface. Cover titles 48–64 pt, page titles 32–40 pt, body text 17–21 pt, captions and sources 9–11 pt.
|
||||||
|
- Titles, body text, figure captions, and footnotes each keep their own fixed baselines.
|
||||||
|
- Body pages may set an extremely thin header or footer rule, with a short title, section name, and page number fixed in place — or a section navigation.
|
||||||
|
|
||||||
|
**Research content**:
|
||||||
|
- Formulas keep their numbering, conditions, and symbol explanations; key derivations may appear step by step, but each page must form a complete local argument.
|
||||||
|
- Tables preferably adopt the three-line table structure from papers, highlighting only the rows and columns directly relevant to the conclusion.
|
||||||
|
|
||||||
|
**Page grammar**:
|
||||||
|
1. Cover: small school or department emblem, title, author, advisor, department, and date; establish identity with a thin line in the school color — no giant watermarks.
|
||||||
|
2. Section pages: a full page of school color or a refined background color, holding only the section number, the title, and one research question. Pair with a decorative subtle-pattern background image to enrich the visual effect.
|
||||||
|
3. Summary page: answer item by item by research question, marking the corresponding evidence or section number after each item.
|
||||||
|
4. References page: use a font size slightly smaller than body text but larger than footnotes, and place the references in the official citation format, in columns or a single column.
|
||||||
|
5. Closing page: keep the school color, department emblem, and contact information, presenting only "Questions" or one final conclusion.
|
||||||
|
|
||||||
|
**Recurring motif**: the short school-color line, fixed title baseline, section name, page numbers, figure captions, and reference footnote markers.
|
||||||
|
|
||||||
|
**Prohibitions**: faking the school color from memory, enlarging the emblem to fill whitespace, semi-transparent emblem watermarks, blue gradient headers, repeating large color bands on every page, rounded info cards, figures without captions, and squeezing body text and figures for the sake of tradition.
|
||||||
|
|
||||||
|
### Humanities & Social Sciences Archival Special Issue
|
||||||
|
|
||||||
|
**Reference objects**: humanities and social-science academic monographs from university presses, twentieth-century academic journals, archival catalogues, and the vintage-print paper, layout, and image treatments in `brand-creative.md`. Absorb only the parts that serve the presentation of evidence.
|
||||||
|
|
||||||
|
**Core visual**: the whole deck reads like an archival research special issue typeset with contemporary layout. Warm paper tones, serif type, marginal annotations, document numbers, and thin separator lines form a stable skeleton; archives, maps, manuscripts, interviews, and statistical materials appear as evidence, not as decoration filling the picture.
|
||||||
|
|
||||||
|
**Palette**:
|
||||||
|
- Background: archival paper #F1E9DA; text: ink black #27231F; auxiliary information: old-paper gray #9B9489.
|
||||||
|
- Choose one accent color by theme: annotation vermilion #8C3B36 or binding ink green #355C52. Pick only one for the whole presentation, and keep it within 3%–8% of the area on ordinary pages.
|
||||||
|
- Original archives and artworks may keep their original colors; figures, annotations, and navigation still obey the unified palette.
|
||||||
|
|
||||||
|
**Type and grid**:
|
||||||
|
- Chinese titles and body text preferably use a modern Song (serif) typeface or another clear serif; numbers, numbering, sources, and archival reference numbers may use sans-serif or monospaced fonts.
|
||||||
|
- Cover titles 48–68 pt, page titles 34–44 pt, body text 17–20 pt, long quotations 21–28 pt, figure notes and footnotes 9–11 pt; body line spacing 1.3–1.45.
|
||||||
|
- Commonly use unequal-width layouts of 4:8 or 5:7, reserving a narrow margin for dates, keywords, document numbers, or annotations. Rely on whitespace and thin lines to organize content — no vintage lace borders and no cards.
|
||||||
|
|
||||||
|
**Archives, quotations, and data**:
|
||||||
|
- Archive scans should preserve paper edges, page numbers, seals, and collection or accession numbers; when emphasis is needed, point out the evidence with thin boxes, leader lines, and magnified details.
|
||||||
|
- Label every archival image with author, title, date, holding institution, and reference number; label every interview excerpt with respondent number, time, and place.
|
||||||
|
- Statistical charts stay modern and clearly drawn, with no aged textures overlaid. Use the accent color only to highlight data series directly relevant to the argument.
|
||||||
|
- Long quotations excerpt only the passages that support the argument, with explanation and provenance beside them; avoid piling full-page stacks of original text.
|
||||||
|
|
||||||
|
**Page grammar**:
|
||||||
|
1. Cover: the title styled like the title page of an academic monograph, paired with an archive, map, or artwork detail occupying 35%–50% of the picture; also place author, institution, date, and volume/issue-style metadata.
|
||||||
|
2. Research-question page: use a three-to-five-line abstract-style lead-in, with locations, dates, subjects, and material scope listed in the margin.
|
||||||
|
3. Literature-review page: organize the literature along time or divergences of opinion, connecting scholars, arguments, and response or engagement relationships with thin lines — do not build walls of author cards.
|
||||||
|
4. Archival-evidence page: a large original on the left; transcription, translation, interpretation, and provenance in order on the right; use the same numbers to connect original and analysis.
|
||||||
|
5. Empirical-analysis page: the main figure or table occupies two-thirds; the other side holds conclusions, variable descriptions, and limitations; quantitative material also follows the special-issue skeleton.
|
||||||
|
6. Map-and-time page: use one main map or one main timeline; the accent color marks only the places and events relevant to the argument.
|
||||||
|
7. Argument page: a clear judgment sentence at the top, with textual evidence, visual evidence, and the researcher's interpretation laid out in order below.
|
||||||
|
8. Conclusion page: answer the research questions with consecutively numbered short paragraphs, each attaching the corresponding archive number, figure number, or section number.
|
||||||
|
|
||||||
|
**Recurring motif**: book-page-style page numbers, thin footnote rules, running heads, archival reference numbers, section tabs, and a unified figure-caption format.
|
||||||
|
|
||||||
|
**Prohibitions**: casually tilted clip art, low-contrast body text, treating archival material as background texture, and historical photos without sources.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Analysis & Decision
|
||||||
|
|
||||||
|
## Solid, compact content with extremely high-density layout
|
||||||
|
|
||||||
|
1. Unless the presentation is explicitly intended for on-stage presenting, treat it as a reading-type deck by default. Design the entire work around this goal, and demonstrate the standard of a top-tier research report or consulting firm.
|
||||||
|
2. Every page has enough depth: the core conclusion can be grasped quickly, while reading the full page takes 5 minutes.
|
||||||
|
3. Support cascades from conclusion to evidence: once the title states a judgment, the page should carry several self-standing supporting judgments that connect the conclusion to the evidence, each with one verifiable sentence of support.
|
||||||
|
4. Body-page titles are preferably declarative sentences ("Europe's electric heavy-truck batteries reach 48 GWh by 2030 — 16x growth in six years under a strong policy push"), but decide whether to use a declarative sentence based on the actual content.
|
||||||
|
|
||||||
|
## Make data and structured information the body of the argument
|
||||||
|
|
||||||
|
1. Data is the core support of the argument: search thoroughly around the topic to obtain real data, and present it fully through all kinds of charts and tables; the source of every piece of data must be stated — never fabricate data.
|
||||||
|
2. Charts come complete with axes, units, legends, sources, and measurement basis, so that a chart stands on its own apart from the body text.
|
||||||
|
3. Mark data inflection points and key values on charts and explain their meaning; accompany each chart with text stating the judgment it proves, the findings visible on it, the implications for the decision, and the source and boundaries.
|
||||||
|
4. Encourage using SmartArt shape combinations to present information; when structural relationships exist between pieces of content — sequence, comparison, cycle, causality, and so on — prefer making them visible with a structural diagram rather than describing them in text.
|
||||||
|
5. Encourage drawing and combining shapes to build complex charts and SmartArt, supported by text explanations — plain text alone is too flat.
|
||||||
|
|
||||||
|
## Key Requirements
|
||||||
|
1. Create a top-tier visual experience: every detail of typesetting, fonts, colors, and alignment must be treated with care; accept no defaults — details determine success or failure.
|
||||||
|
2. Unless explicitly requested, strictly forbid using rounded rectangles or rectangular cards to build hierarchy or alignment: line segments, whitespace, and font/size differences are better solutions.
|
||||||
|
3. A single basic chart must not occupy half or more of the width; encourage laying out multiple charts or other content on one page.
|
||||||
|
4. Unless no other layout is available, forbid evenly divided compositions; do not default to one-third splits, four-way splits, or 2×2 matrices — including three-part formulas such as "three columns + title + conclusion."
|
||||||
|
|
||||||
|
## Visual References
|
||||||
|
|
||||||
|
1. White background + single primary-color skeleton: pure white pages, with only one primary color throughout the deck (e.g., navy, ink green, or another color common in financial-consulting style) carrying the structural skeleton — title emphasis, table headers, navigation highlights, main chart series, and numbered markers — supported by a 2–3-step light-tint ladder of the same hue and neutral grays.
|
||||||
|
2. The title is the conclusion: each page's main title is a complete judgment sentence; the heading format (kicker → main title → subtitle, or title + conclusion-sentence subtitle) is fixed throughout; in typography, bold serif titles and sans-serif body text form two clearly divided roles, with a strictly consistent hierarchy.
|
||||||
|
3. If headers and footers exist, they must stay stable: section navigation always visible (current section highlighted; sections distinguished by navigation position, not by switching colors), a fixed "Source:" line + page number in the footer; every element keeps the same coordinates from page to page, so nothing jumps when flipping.
|
||||||
|
4. Density rhythm: body pages are packed at high density, with charts/tables/structural diagrams as the main body and text serving the evidence; covers, section pages, and conclusion pages use whitespace as breathing points.
|
||||||
|
5. Position discipline: conclusion → top title; evidence → charts/tables in the middle; interpretation → notes beside the chart or a sidebar; sources and measurement basis → the source line at the bottom; action recommendations → the closing page only. Every type of information sits in its own place, consistent throughout.
|
||||||
|
6. De-default charts: reassign all series colors to the primary-color ladder + grays; remove vertical gridlines (at most keep extremely light horizontal lines); data labels mark only key points and endpoints; actual vs. forecast is distinguished by solid vs. dashed/hatched; directly below each chart, place its source + measurement-basis note.
|
||||||
|
7. Mark key values directly on the chart: key totals with big-number callouts, inflection points/gaps/growth rates with arrows or leader lines + one-sentence annotations, ratings with Harvey balls or badges — placed right on the chart area, not piled into text paragraphs.
|
||||||
|
8. Table conventions: dark header with white text, extremely thin horizontal separators (no vertical lines or heavy borders), numbers right-aligned and text left-aligned, subject names in the first column bolded; large tables get an interpretation panel beside them.
|
||||||
|
9. Line and alignment discipline: all elements align to a unified left edge and invisible column lines; separation uses only extremely thin lines, emphasis uses only primary-color short bars or thick vertical bars, never thick frames; panels are square-cornered with no shadows; rounded corners are allowed only on small labels.
|
||||||
|
10. Clean surfaces: no shadows, no gradients, no textures, no glow effects, no default colors; the texture is achieved entirely through whitespace, alignment, a single-hue color scale, and line discipline.
|
||||||
|
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
# Brand / Creative Showcase
|
||||||
|
|
||||||
|
**Benchmark references**: top magazines such as Monocle, Wallpaper, and Vogue; international design agencies such as Pentagram; and outstanding brand annual reports, brand books, and creative proposals.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
### Identify the subject; build a deck whose subject is recognizable at a glance
|
||||||
|
If the presentation's subject is a specific brand, product, company, person, city, building, event, or IP, first extract the subject's identifying features:
|
||||||
|
- Extract visual identifiers such as icons and the brand concept from the user's uploaded reference materials.
|
||||||
|
- Search and visit official websites, officially published annual reports, creative manuals, and icons. Do not just read the text — rely more on referencing and studying the pages' visual effects to help understand the style.
|
||||||
|
Even if all text content were removed, the association with the subject should still be found through other visual features.
|
||||||
|
|
||||||
|
### Take the essence, discard the dross
|
||||||
|
Not every subject has an aesthetic aligned with top magazines like Monocle, Wallpaper, or Vogue. When extracting the subject's style, accurately identify which items may be referenced and which are prohibited. Designs that fail to meet the benchmark's standard must be explicitly forbidden. If the brand itself lacks design quality, you may keep only the brand's primary color as the theme or accent color, or keep icons as decoration.
|
||||||
|
|
||||||
|
### Build a top-tier visual experience
|
||||||
|
Use strong font-size contrast and extreme asymmetric layouts to build an exaggerated style and a top-tier visual experience. Reject every safe, mediocre, conservative option: better ugly than mediocre.
|
||||||
|
1. **Make extreme choices**: oversized type, giant images, intense whitespace, high-density multi-columns, a single vivid color, or heavy lines. Build an ultimate style: old-newspaper front page, Memphis, Constructivism, Swiss style...
|
||||||
|
2. **Vary the pages**: cover, opinion, data, and image pages each have different compositions. There is no need to strictly follow one identical "page skeleton," but the deck's consistency should still be recognizable through palette, typeface, font sizes, same-style decorative elements, and the same grid alignment.
|
||||||
|
3. **Have a visual rationale**: the visual language must come from the deck's subject, brand, industry, era, or material, and must help the audience understand the content.
|
||||||
|
|
||||||
|
## Prohibitions
|
||||||
|
1. **No cards by default**: unless the user explicitly requests it, strictly forbid using rounded rectangles or rectangular cards to build hierarchy or alignment: line segments, whitespace, and font/size differences are better solutions.
|
||||||
|
2. **No evenly divided compositions**: unless no other layout is available, do not default to one-third splits, four-way splits, or 2×2 matrices — including three-part formulas such as "three columns + title + conclusion."
|
||||||
|
3. **No mediocre, common, or AI-typical color schemes**: strictly forbid any blue-and-white pairing, blue-purple gradients, cyan-purple neon, rainbow flares, glassmorphism cards, or glowing borders — unless the user explicitly requests them.
|
||||||
|
4. **No elements that clash with the overall style**: no styles from outside the chosen style may appear, such as using rounded icons or rounded rectangles within a sharp style.
|
||||||
|
|
||||||
|
## Visual References
|
||||||
|
|
||||||
|
Visual references are only "references": they show you something good so you know what good looks like — they are not directly usable. You should design visuals according to the user's actual needs, not directly apply templates.
|
||||||
|
|
||||||
|
### Klein Blue Swiss Posters
|
||||||
|
|
||||||
|
**Core visual**: the whole deck reads like a set of modernist exhibition posters. Strict left alignment, a twelve-column grid, extreme font-size differences, giant numerals, and large areas of whitespace. The picture uses only paper white, near-black, and Klein blue.
|
||||||
|
|
||||||
|
**Palette**:
|
||||||
|
- Background: warm paper white #F7F5EF, roughly 70%–85%.
|
||||||
|
- Text: near-black #101010.
|
||||||
|
- Accent: Klein blue #0038B8, occupying 5%–12% on ordinary pages; section pages may use a blue block occupying 30%–40% of the picture.
|
||||||
|
- Auxiliary gray: #D8D7D2, only for gridlines and secondary information.
|
||||||
|
- No gradients and no second accent color.
|
||||||
|
|
||||||
|
**Type**:
|
||||||
|
- Cover title: ultra-heavy sans-serif, 112–136 pt, line height 0.85–0.95, and may be slightly cropped by the canvas edge.
|
||||||
|
- Content-page titles: 48–64 pt.
|
||||||
|
- Body text: 17–20 pt.
|
||||||
|
- Page numbers, sources, and labels: 9–11 pt, with widened letter spacing.
|
||||||
|
- Everything left-aligned, using a single sans-serif family only, building hierarchy through weight changes.
|
||||||
|
|
||||||
|
**Grid and geometry**:
|
||||||
|
- Left and right margins equal 5% of the canvas width; use a twelve-column grid.
|
||||||
|
- Main compositions use 2:8, 3:7, or full-page whitespace.
|
||||||
|
- Use only right-angle rectangles, true circles, 0.5 pt thin lines, and 4 pt thick lines.
|
||||||
|
- No rounded corners, shadows, outlined icons, or floating cards.
|
||||||
|
|
||||||
|
**Image treatment**:
|
||||||
|
- Product screenshots all go into a fixed 16:10 slot.
|
||||||
|
- Photography is uniformly high-contrast black and white; a blue color surface may only be overlaid locally.
|
||||||
|
- At most one main image per page; no photo collages.
|
||||||
|
|
||||||
|
**Page grammar**:
|
||||||
|
1. Cover: the title occupies the left 65%; a blue color surface running the full height sits on the right; at the top, only one line of 10 pt meta-information.
|
||||||
|
2. Section pages: the whole page holds only the section numeral and one title line; the numeral is 120 pt blue.
|
||||||
|
3. Opinion page: one 64–88 pt conclusion, with a tiny annotation in the bottom-right corner.
|
||||||
|
4. Data page: a 110 pt numeral occupies the left third; a simplified chart sits on the right.
|
||||||
|
5. Architecture page: all nodes align to the twelve-column grid; connecting lines are uniformly 1 pt; only the core nodes are blue.
|
||||||
|
6. Closing page: return to the cover's blue color surface and giant title, closing the loop.
|
||||||
|
|
||||||
|
**Recurring motif**: a blue vertical line and a two-digit section number fixed at the top-left corner of every page.
|
||||||
|
|
||||||
|
**Prohibitions**: rounded cards, three evenly divided columns, colorful small icons, multi-colored charts, gradients, soft shadows, centered titles.
|
||||||
|
|
||||||
|
### Stencil-Printed Indie Magazine
|
||||||
|
|
||||||
|
**Core visual**: the whole deck feels like a hand-trimmed, scanned, and bound indie magazine. Heavy poster type, two-color overprinting, halftone images, cut-and-paste blocks, and slight tilts. Preserve paper and printing imperfections rather than chasing the slickness of a digital interface.
|
||||||
|
|
||||||
|
**Palette**:
|
||||||
|
- Paper: warm beige #F1E2C2.
|
||||||
|
- Primary ink: cobalt blue #1646B8.
|
||||||
|
- Second spot color: fluorescent pink #FF4F87.
|
||||||
|
- Text and rules: ink black #171512.
|
||||||
|
- Orange-yellow #F1A51B may appear on only one or two section pages, within 5% of the area.
|
||||||
|
- No transparent gradients; colors appear only as flats, overprints, and halftones.
|
||||||
|
|
||||||
|
**Type**:
|
||||||
|
- Cover title: ultra-heavy condensed poster type, 88–120 pt, allowed to rotate -3 degrees and cross the canvas edge.
|
||||||
|
- Page titles: 42–58 pt, may be reversed white on a black ground.
|
||||||
|
- Body text: 14–17 pt, in a clear sans-serif or serif face.
|
||||||
|
- Annotations: 10–12 pt monospaced, mimicking typewriter labels.
|
||||||
|
|
||||||
|
**Grid and geometry**:
|
||||||
|
- Use unequal-width two and three columns and cut-and-paste modules.
|
||||||
|
- Images and text blocks may rotate from -4 to +4 degrees.
|
||||||
|
- Main rules 2–3 pt; crop marks 0.75 pt.
|
||||||
|
- Color blocks stay right-angled; local overruns and mutual overlaps are allowed.
|
||||||
|
|
||||||
|
**Image treatment**:
|
||||||
|
- All photos are first converted to high-contrast black and white, then given either a blue or a pink halftone treatment.
|
||||||
|
- Halftone dot size stays consistent; color misregistration of 2–4 pixels is allowed.
|
||||||
|
- People and buildings may be cropped brutally, but never past the point of losing the recognizable subject.
|
||||||
|
- Never drop full-color photos in directly.
|
||||||
|
|
||||||
|
**Page grammar**:
|
||||||
|
1. Cover: the title occupies more than half the picture; at the bottom, a blue-and-pink duotone photo of a printing press or bookshop; a tilted small label is pasted at the top-right corner.
|
||||||
|
2. Contents page: five colored paper strips, each holding only a section number and a short title.
|
||||||
|
3. History page: the timeline looks like tickets clipped onto paper; years use 40–56 pt bold type.
|
||||||
|
4. Craft page: five steps spread horizontally, each containing a halftone image, a number, and one line of explanation.
|
||||||
|
5. City-case page: one large image occupies 60%; text presses against the image's side like a newspaper sidebar.
|
||||||
|
6. Map page: the map stays as black line art; key cities are marked with solid pink circles.
|
||||||
|
7. Closing page: four large numbered circles present the action steps; a black manifesto line sits at the bottom.
|
||||||
|
|
||||||
|
**Recurring motif**: misregistered blue-and-pink overprinting, the crop line at the top-left corner, and a fixed volume/issue number.
|
||||||
|
|
||||||
|
**Prohibitions**: glassmorphism, soft shadows, slick gradients, refined rounded corners, full-color photography, business clip icons, and perfectly symmetrical grids.
|
||||||
|
|
||||||
|
### Brutalist Newspaper
|
||||||
|
|
||||||
|
**Core visual**: the whole deck reads like a special-edition newspaper about an industry turning point. Giant mastheads, narrow margins, unequal-width multi-columns, thick-and-thin rules, high-density body text, and black-and-white halftone photos. Red is used only for the most critical data and warnings.
|
||||||
|
|
||||||
|
**Palette**:
|
||||||
|
- Newsprint white: #F5F1E8.
|
||||||
|
- Ink black: #111111.
|
||||||
|
- News red: #C8102E, kept within 3%–8% of a page's area.
|
||||||
|
- Sidebar paper yellow: #E7DDCA.
|
||||||
|
- Secondary text: #55514A.
|
||||||
|
- No blue-purple gradients and no colored status blocks.
|
||||||
|
|
||||||
|
**Type**:
|
||||||
|
- Masthead: ultra-heavy sans-serif, 72–100 pt, all caps where supported or otherwise set as ultra-bold short text.
|
||||||
|
- Headlines: 44–72 pt, tight leading.
|
||||||
|
- Body text: 11–14 pt serif, line spacing 1.25–1.4.
|
||||||
|
- Data and labels: monospaced, 10–14 pt.
|
||||||
|
- Pull quotes: 28–40 pt serif italic or bold.
|
||||||
|
|
||||||
|
**Grid and geometry**:
|
||||||
|
- Margins are 3.5%–4% of the canvas width.
|
||||||
|
- Content pages use three to five unequal-width columns.
|
||||||
|
- Lines come in four grades: 0.5 pt, 1 pt, 3 pt, and 5 pt.
|
||||||
|
- All modules are right-angled, organized by rules; no floating cards.
|
||||||
|
|
||||||
|
**Image treatment**:
|
||||||
|
- Photography is uniformly converted to high-contrast black-and-white halftone, keeping visible grain.
|
||||||
|
- Every image carries a 9–10 pt caption and source.
|
||||||
|
- You may mark an image with a red circle, red underline, or red stamp — choose only one of these mark types per page.
|
||||||
|
|
||||||
|
**Page grammar**:
|
||||||
|
1. Cover: a 5 pt red line at the top, a masthead spanning the full page below it; the main headline uses 80–96 pt; three short news briefs at the bottom.
|
||||||
|
2. Overview page: four large numbers in a row, each 64–88 pt, with three columns of observations below.
|
||||||
|
3. Ranking page: a black-and-white halftone photo in the left third; horizontal bar charts in the right two-thirds.
|
||||||
|
4. Cost page: a full-width server-room banner across the middle, with a line chart and a narrow annotation column below.
|
||||||
|
5. Policy page: a black-and-white landscape image at the top; below, a dense three-column table compares different regions.
|
||||||
|
6. Timeline page: one thick black main line with six red nodes; event descriptions distributed above and below.
|
||||||
|
7. Closing page: only one 64 pt conclusion and a red full stop.
|
||||||
|
|
||||||
|
**Recurring motif**: the top red line, the fixed masthead, black column rules, and edition information at the bottom-right corner.
|
||||||
|
|
||||||
|
**Prohibitions**: rounded corners, shadows, illustrated icons, colored cards, loose business layouts, default table styles, and unsourced data.
|
||||||
|
|
||||||
|
### Memphis Pop Posters
|
||||||
|
|
||||||
|
**Core visual**: the whole deck uses thick black outlines, giant poster type, checkerboards, lightning bolts, waves, dots, and geometric shapes that break the frame. The picture is lively while text areas stay clean — like a blend of eighties pop posters and contemporary youth-event visuals.
|
||||||
|
|
||||||
|
**Palette**:
|
||||||
|
- Background: cream white #FFF3D8.
|
||||||
|
- Ink: deep black-purple #151427.
|
||||||
|
- Fluorescent pink #FF3EA5.
|
||||||
|
- Bright yellow #FFD63D.
|
||||||
|
- Lake blue #05C7E8.
|
||||||
|
- Mint green #23D6A2.
|
||||||
|
- At most three high-saturation colors per page; deep black-purple must be used for outlines and body text to steady the picture.
|
||||||
|
|
||||||
|
**Type**:
|
||||||
|
- Cover title: ultra-heavy rounded or comic-style display type, 92–128 pt, with a 3–5 pt dark outline and a hard-offset color block.
|
||||||
|
- Section words: 80–110 pt, all caps where supported or a single giant character.
|
||||||
|
- Page titles: 36–48 pt.
|
||||||
|
- Body text: 15–18 pt neutral sans-serif; no script faces.
|
||||||
|
- Numbers: 72–100 pt, in heavy black type.
|
||||||
|
|
||||||
|
**Grid and geometry**:
|
||||||
|
- Compositions are clearly asymmetric; main objects may rotate from -6 to +6 degrees.
|
||||||
|
- Geometric shapes use 3–5 pt dark outlines.
|
||||||
|
- Each page carries one geometric shape breaking the frame, occupying 20%–40% of the picture.
|
||||||
|
- Decoration concentrates in corners and edges, never covering body text.
|
||||||
|
|
||||||
|
**Image treatment**:
|
||||||
|
- People are uniformly rendered as flat illustrations with thick black outlines or a high-saturation screen-print effect.
|
||||||
|
- Multi-person pages use a four-panel series, with busts of consistent size and viewing angle.
|
||||||
|
- Venue maps use hand-drawn icons, but route and schedule information stays clear.
|
||||||
|
|
||||||
|
**Page grammar**:
|
||||||
|
1. Cover: the title fills the upper half; crowd silhouettes and the stage sit at the bottom; checkerboards, lightning bolts, and dots occupy the four corners, while the center stays clear.
|
||||||
|
2. Section pages: one giant word or character, against a background of one frame-breaking circle and one wave.
|
||||||
|
3. Numbers page: four numbers in a row, each on a different background color, all with the same thick outline.
|
||||||
|
4. Lineup page: a four-person panel series in the upper half; names, styles, and set times in the lower half.
|
||||||
|
5. Venue map: a radial map in the center, with colored labels around it marking stages and facilities.
|
||||||
|
6. Schedule page: the timeline stays horizontal and regular, colors distinguish the stages, and decoration retreats to the edges.
|
||||||
|
7. Ticket-pricing page: three tiers as hard-edged poster blocks, prices 64–80 pt, no floating shadows.
|
||||||
|
8. Closing page: a full-bleed sunset illustration with an 88 pt handwritten-feel line on top, repeating the cover's checkerboard corners.
|
||||||
|
|
||||||
|
**Recurring motif**: checkerboard at the top-right corner, pink lightning bolts, and thick black outlines — the checkerboard must appear on at least five pages.
|
||||||
|
|
||||||
|
**Prohibitions**: soft shadows, low-contrast text, all colors appearing at once, script body text, decoration covering data, and ordinary business icons.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Business Proposal
|
||||||
|
|
||||||
|
## A clear storyline with strong persuasion
|
||||||
|
|
||||||
|
- The storyline of the whole deck is clear and builds step by step.
|
||||||
|
- If a page has a core claim, that most important sentence enjoys the visual privilege of being "marked."
|
||||||
|
- **Emotion has a curve**: the deck's emotion is not flat — from unease (the pain point) to hope (the solution) to belief (the evidence) to urge (the action).
|
||||||
|
- Make good use of data support: assist the proposal with data, tables, and charts at the right positions. Data must be truthful and reliable, with sources stated.
|
||||||
|
|
||||||
|
## Tell a more complete story through SmartArt structural diagrams
|
||||||
|
|
||||||
|
**SmartArt is the visualization of narrative: boldly use complex structural diagrams to present multi-layered relationships.**
|
||||||
|
|
||||||
|
- Decompose structures from information relationships — sequence, comparison, cycle, causality, and so on — and prefer making them visible with structural diagrams rather than describing them in text.
|
||||||
|
|
||||||
|
## Page turns have breathing room
|
||||||
|
|
||||||
|
- Encourage interspersing refined section pages and accent pages among the content pages to markedly elevate the reader's experience. Section pages may use images as backgrounds, covered by a gradient overlay.
|
||||||
|
- When the topic suits it, search for images generously and place well-chosen images at the right spots — one picture is worth a thousand words.
|
||||||
|
|
||||||
|
## Key Requirements
|
||||||
|
1. Create a top-tier visual experience: treat every detail of typesetting, fonts, and colors with care, crafting it like a work of art — details determine success or failure.
|
||||||
|
2. Unless the user explicitly requests it, **strictly forbid using rounded rectangles or rectangular cards** to build hierarchy or alignment: line segments, whitespace, and font/size differences are better solutions.
|
||||||
|
3. Unless no other layout is available, forbid evenly divided compositions; do not default to one-third splits, four-way splits, or 2×2 matrices — including three-part formulas such as "three columns + title + conclusion."
|
||||||
|
|
||||||
|
## Visual References
|
||||||
|
|
||||||
|
1. Type is attitude: display type (titles, manifesto lines, big numbers) and body type (body text, notes, sources) have a clear division of labor by family or weight; display type needs poster-level tension, daring extreme contrast in size, weight, and letter spacing — the title itself is a visual work.
|
||||||
|
2. Fonts and the color scheme must be matched and harmonious; reference each other when designing.
|
||||||
|
3. Rhythm and accent pages: rotate the page-type sequence, never two consecutive pages with the same skeleton; manifesto pages, section divider pages, and big-number hero pages handle emotional gear-shifts and visual emphasis.
|
||||||
|
4. Carefully designed layouts: page layouts are ingeniously designed, with beauty and soul — not just simple arrangements of text.
|
||||||
|
5. Corner and edge visuals: encourage building shapes into images that aid explanation, or crafting refined, purely decorative symbols out of shapes in empty areas.
|
||||||
|
6. De-default charts and tables: all series colors come from the brand palette; data labels mark only key points and sit directly beside the graphic; tables are separated only by thin horizontal lines — no heavy frames, no zebra striping.
|
||||||
|
7. Details are quality: pixel-level alignment and letter spacing that hold up under scrutiny at any magnification; the texture is achieved entirely through typesetting, alignment, color blocks, and line discipline — no defaults anywhere; detail is the dividing line between master and mediocrity.
|
||||||
|
8. Motifs and text/image separation: establish 2–5 recurring visual motifs throughout the deck (decorations, icons, and illustrations share one language) to form recognizability and an artistic signature.
|
||||||
|
|
||||||
|
### Color Palette Reference
|
||||||
|
|
||||||
|
**Colors expected in spirit, unexpected in choice**:
|
||||||
|
- When the user precisely mentions a company/product/brand name, prefer taking the brand's VI primary color as the structural primary color, and design a matching palette around it.
|
||||||
|
- Reject the most common, most mediocre formulaic palettes of each scenario (tech blue-purple gradients, hospital blue-and-white, festive bright-red-and-gold, real-estate sales-office gold-brown, green-leaf environmental style, etc.); reference palettes for some scenarios:
|
||||||
|
* Tech/AI/SaaS/entertainment roadshows: the launch-event color clash of ink black #141414 base + fluorescent yellow-green #C6F24E; or mist white #F2F4F7 base + indigo #2B3A8E + lime #A8E05F; or a dark-base clash of deep purple-black #1A1228 base + neon magenta #E91E8C + electric blue #2D9CDB.
|
||||||
|
* Consumer goods/F&B/food franchise recruitment: creamy apricot #F3D9A4 base + baking brown #6B4226 + brick red #A63A2E; or off-white #F6EFE2 base + ink green #1E3B33 + terracotta #C15F3C; or soil brown #4E3428 base + wheat gold #C9A227 + moss green #5D6B4F.
|
||||||
|
* Luxury/fashion/beauty: the editorial palette of ink black #0D0D0D base + ivory #F5F0E6 + burgundy #6D1F2C; or ivory #F5F0E6 base + ink black #0D0D0D + burgundy #6D1F2C; or the minimalist clash of cool white #F8F8F6 base + charcoal black #1A1A1A + cobalt blue #1F3A93.
|
||||||
|
* Medical/health products: warm white #F5F0E1 base + deep pine green #1F4A3D + apricot yellow #E8B54A; or warm white #F7F5F0 base + deep indigo #1B2A52 + mint #7FC7B5; or charcoal gray #333A42 base + coral pink #E8A09A + light beige #EFE9E0.
|
||||||
|
* Finance/legal/professional services: off-white #F7F3E8 base + ink green #123B2F + brass #B08D3E; or warm gray #C9C4BC base + ink black #1A1A2E + burgundy #7B2D43; or ink black #1C1C1C base + antique bronze #9C6B30 + parchment white #F4EDE1.
|
||||||
|
* Industry/construction/energy/logistics (heavy B2B industries): charcoal black #232323 base + safety orange #E8590C + steel gray #868E96; or sandy white #F3EDE0 base + deep petroleum teal #0F4C5C + warm copper #B5651D; or light gray #D9DDE3 base + midnight blue #101D42 + signal yellow #F5B700.
|
||||||
|
* Education/knowledge-payment products: warm white #FAF6EE base + ink purple #3D2C4F + cream yellow #F2C14E; or off-white #F5F1EA base + deep indigo #22315C + coral orange #F08A5D; or paper white #F7F3E8 base + pine green #2F5233 + apricot yellow #E8B54A.
|
||||||
|
- Align background colors toward something more textured, e.g.: ink black #141414, deep ink green #0E2A22, warm paper white #F7F3E8, mist gray #E9E7E2, deep wine red #3B1219.
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
# Education & Training
|
||||||
|
|
||||||
|
Scope: courseware, job-skill training, operation guides, teacher materials, study handbooks, public education, onboarding training, capability building, and knowledge popularization. The reader may be listening live or reading independently; their task is not merely to understand a page, but to form understanding they can retell, methods they can execute, and actions they can transfer.
|
||||||
|
|
||||||
|
> Core position: design the learning first, the pages second; complete the content skeleton first, the visual system second. Reference samples only provide design cues worth borrowing — they are not default templates, nor styles that must be replicated. Courseware should carry enough content to support learning; do not turn body content into posters or whitespace albums for the sake of a "premium feel."
|
||||||
|
|
||||||
|
## 1. Define the Learning Task First; Do Not Pick a Style First
|
||||||
|
|
||||||
|
Before production, write down the following information; if any of it is missing, fill it in with reasonable assumptions and mark it in the design document.
|
||||||
|
|
||||||
|
- **Learner:** age, level of expertise, prior knowledge, easily confused terms, accessibility needs.
|
||||||
|
- **Usage:** live projection, printed handouts, self-study reading, or mixed use; roughly how long each page stays in view.
|
||||||
|
- **Entry state:** what the learner does not know now, cannot do now, or is prone to getting wrong at which step.
|
||||||
|
- **Exit capability:** after the material, what the learner can explain, differentiate, judge, operate, check, or retell. Goals must be observable.
|
||||||
|
- **Material boundaries:** which parts are external facts, and which are team inferences, fictional exercises, illustrative data, or to-be-filled material.
|
||||||
|
- **Real task:** in what work, classroom, or life scenario the learner will next apply this method.
|
||||||
|
|
||||||
|
Do not first decide "use blue, use rounded corners, use some Sample" and then cram content into the pages. Every visual choice must be explainable as: it helped the learner see which relationship, remember which step, or complete which judgment.
|
||||||
|
|
||||||
|
## 2. General Prohibitions
|
||||||
|
|
||||||
|
The following are red lines running through the entire deck; they take effect before all layout and visual rules, and unless the user explicitly requests otherwise, none may be violated.
|
||||||
|
|
||||||
|
- **No cards by default:** unless the user explicitly requests it, strictly forbid using rounded rectangles or rectangular cards to build hierarchy or alignment. Line segments, whitespace, and font-size/weight differences are better solutions.
|
||||||
|
- **No evenly divided compositions:** unless no other layout is available, do not default to one-third splits, four-way splits, or 2×2 matrices — including formulaic patterns such as "title + three parallel blocks + conclusion."
|
||||||
|
- **No mediocre, common, or AI-typical color schemes:** unless the user explicitly requests them, strictly forbid blue-and-white pairings, blue-purple gradients, cyan-purple neon, rainbow flares, glassmorphism cards, and glowing borders.
|
||||||
|
- **No elements that clash with the overall style:** no styles from outside the chosen style may appear, such as using rounded icons or rounded rectangles within a sharp style.
|
||||||
|
|
||||||
|
## 3. The Teaching Loop: Every Chapter and Every Page Needs a Take-Away Action
|
||||||
|
|
||||||
|
The default learning path is: **orientation → comprehension → demonstration → practice → feedback → transfer**. Not every short material needs a separate page for each stage, but when a stage is missing, you must be able to say that the task itself does not call for it — not that the page ran out of space.
|
||||||
|
|
||||||
|
- **Orientation:** explain why this is worth learning, what the learner can do after completing it, and how it relates to the chapters before and after. Use a real problem or scenario instead of grand slogans.
|
||||||
|
- **Comprehension:** first give one core definition the learner can retell, then the boundaries, composition, counterexamples, or relationships. New terms must not appear only in figure captions.
|
||||||
|
- **Demonstration:** show the process and the intermediate judgments, not just the final answer; put the key forks, the basis for each choice, and the common misconceptions on the same reading path.
|
||||||
|
- **Practice:** specify the input, the actions, and the completion criteria. Practice scenarios may be fictional, but must be labeled "practice scenario / illustrative material."
|
||||||
|
- **Feedback:** explain why an answer is right or wrong, where the error lies, and how to check next time. Answer pages and question pages must be distinguishable at a glance.
|
||||||
|
- **Transfer:** connect to real use with checklists, templates, job-aid cards, or next-step actions; do not substitute a slogan for transfer.
|
||||||
|
|
||||||
|
Each page carries only one primary learning action:
|
||||||
|
|
||||||
|
- Concept pages get the learner to state clearly "what it is and where its boundaries lie."
|
||||||
|
- Method pages get the learner to know "when to use it and how to choose."
|
||||||
|
- Process pages get the learner to know "what to do first, what next, and how to check."
|
||||||
|
- Demonstration / case pages let the learner see the judgment process.
|
||||||
|
- Practice pages get the learner to act or make a choice.
|
||||||
|
- Feedback / review pages correct and distill principles.
|
||||||
|
|
||||||
|
Titles must foretell the page's takeaway. Prefer question sentences, conclusion sentences, or "action + object"; avoid mere section labels like "Background," "Overview," or "Related content." Put all key definitions, steps, and answer criteria into the material — do not rely on the presenter to fill them in live.
|
||||||
|
|
||||||
|
## 4. Default Density for Courseware: Full Content; Whitespace Has a Job
|
||||||
|
|
||||||
|
Courseware is not a poster, nor a full script shrunk down and plastered across pages. The default density is "medium, leaning full": the learner can spot the main object at a glance, and can also find, on the same page, the support needed to understand or act.
|
||||||
|
|
||||||
|
- Body pages usually have at least three layers: learning question / conclusion title → one main exhibit → one layer of explanation, task, check, or source.
|
||||||
|
- As a starting point, about 60% or more of a body page's main content area should be occupied by meaningful text, figures, tables, flows, or exercises; this is a density guardrail, not a hard ratio. Projection pages may reduce text and self-study pages may add explanation, but neither may use emptiness to mask insufficient content.
|
||||||
|
- Whitespace is for grouping, establishing reading order, and highlighting key points — it is not the page's main product. Except for covers, section openers, key manifestos, or closing pages, do not produce consecutive pages of "one sentence + a big blank."
|
||||||
|
- A page may have quiet areas, but only with an explicit reason: a pause, a transition, a memory anchor, or an emotional close. The reason should be writable into the page outline, not explained after the fact.
|
||||||
|
- High-density pages reduce decoration and add grouping and labels; low-density pages add retellable definitions, examples, check questions, or next steps — not random graphics.
|
||||||
|
- One set of courseware may have many page types, but the grid, the title anchor, the type hierarchy, the navigation position, and the color semantics stay stable. A unified system does not mean copying the same template onto every page.
|
||||||
|
|
||||||
|
Let density vary with the learning action:
|
||||||
|
|
||||||
|
| Page type | What it carries | Default density | What must be visible |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Route / opening | Learning questions, goals, chapter relationships | Low to medium | Where we come from and where we are going |
|
||||||
|
| Core concept | Definitions, boundaries, main relationships | Medium | One definition and one intuitive anchor |
|
||||||
|
| Demonstration / case | Material, process, judgments, conclusion | Medium to high | The difference between evidence and interpretation |
|
||||||
|
| Practice / worksheet | Input, operations, completion criteria | Medium to high | What the learner must do right now |
|
||||||
|
| Feedback / review | Answers, reasons, error correction, principles | Medium | Why this judgment is made |
|
||||||
|
| Transfer / summary | Checklists, templates, resources, next steps | Medium | How to use this after leaving the material |
|
||||||
|
|
||||||
|
## 5. Division of Labor: Text, Graphics, Examples, and Exercises Do Not Impersonate Each Other
|
||||||
|
|
||||||
|
- Text explains meaning, boundaries, conditions, and the basis for judgments.
|
||||||
|
- Graphics reveal real relationships: flows for sequence, trees or nesting for hierarchy, parallel regions for comparison, one-way chains for causality, and cycles only where a genuine loop exists.
|
||||||
|
- Examples lower abstraction, but facts, results, and explanations must be layered; a case's teaching role is labeled first (demonstration, comparison, diagnosis, or transfer).
|
||||||
|
- Exercises test mastery; do not fill pages with irrelevant games.
|
||||||
|
|
||||||
|
One structural diagram expresses one primary relationship. Every connector needs a direction or a meaning; delete decorative arrows, meaningless rings, and nodes that exist only to fill the page. A key model is explained in full at first appearance, then keeps the same name, shape, color, and reading direction.
|
||||||
|
|
||||||
|
External facts, data, quotations, and method sources stay close to their corresponding exhibits, stating at least the source entity and the necessary measurement basis. Illustrative data, team inferences, and fictional material must be explicitly labeled. Without reliable data, use schematic structures or qualitative comparisons — never generate realistic-looking precise values.
|
||||||
|
|
||||||
|
Images are used only when "seeing works better than describing." An image may serve as observational evidence or establish a situation; purely decorative images must not crowd out the main learning object. The content of screenshots, photos, illustrations, and charts must be chosen fresh for each new topic — do not copy an object just because a reference sample used it.
|
||||||
|
|
||||||
|
## 6. Order of Visual Decisions: Samples Are a Reference Library, Not an Answer Key
|
||||||
|
|
||||||
|
Make visual decisions in this order:
|
||||||
|
|
||||||
|
1. Complete the learning objectives, the chapter path, and the page-by-page outline.
|
||||||
|
2. Select each page's main learning object and density tier — do not pick colors or motifs first.
|
||||||
|
3. Based on the topic, audience, scenario, and evidence types, write a one-page visual brief: temperament, grid, type hierarchy, color roles, graphic grammar, image strategy, component boundaries, headers/footers, and density.
|
||||||
|
4. When a reference sample is needed, extract only "explainable design decisions," then translate them into the current topic.
|
||||||
|
5. Form one main system plus several page-type variants; do not stitch together the motifs, fonts, cards, and colors of multiple samples.
|
||||||
|
|
||||||
|
### The Correct Way to Use Reference Samples
|
||||||
|
|
||||||
|
When you receive a Sample, template, or screenshot, first look at the page's actual visual effect (do not just extract the copy), then write a "reference extraction card" recording only:
|
||||||
|
|
||||||
|
- **Borrowable functions:** e.g., a stable grid, role color codes, step navigation, evidence zoning.
|
||||||
|
- **Borrowable tone:** e.g., restrained, friendly, rigorous, live-event feel, or public-communication feel.
|
||||||
|
- **Borrowable graphic grammar:** e.g., parallel modules, time paths, nested hierarchy, or photo frames.
|
||||||
|
- **Objects that cannot be inherited:** the sample's brands, photos, characters, specific copy, proprietary color values, and decorative motifs.
|
||||||
|
- **Applicable page types:** only note whether it suits concepts, cases, exercises, or steps — do not expand it into a whole-deck template.
|
||||||
|
|
||||||
|
One material borrows at most two or three mutually compatible features from a single reference sample; after borrowing, re-name the semantics of colors and components. Without a sample, still build the system yourself from the learning task. Sample 1 enjoys no default priority; it is only one reference direction for editorial-style courseware.
|
||||||
|
|
||||||
|
## 7. Reference Directions (Inspiration Only; New Directions May Be Generated)
|
||||||
|
|
||||||
|
These directions help you understand design choices quickly; they are not fixed palettes, font tables, or page templates. Take only the parts that fit the task.
|
||||||
|
|
||||||
|
### Direction A: Editorial Study Handbook (May Reference Sample 1)
|
||||||
|
|
||||||
|
- **Borrowable:** a clear title–exhibit–explanation hierarchy; a stable reading grid; few and explicit accent colors; cases, methods, and sources placed in their own zones; images separated from text.
|
||||||
|
- **Suits:** concept teaching, methodology, case reading, blended courses, and materials that require self-study.
|
||||||
|
- **No need to copy:** the blue-and-white palette, geometrically cropped images, capsule labels, fixed hairlines, large chapter whitespace, or any specific photo subject.
|
||||||
|
- **Density note:** body pages are mainly medium density; cases and exercises can be fuller; only opening and closing pages drop density noticeably.
|
||||||
|
|
||||||
|
### Direction B: Role / System Infographics
|
||||||
|
|
||||||
|
- **Borrowable:** stable color codes distinguishing roles or modules; nesting, sectors, matrices, or interlocking blocks expressing real relationships; action text kept tight against its owning module.
|
||||||
|
- **Suits:** multi-role collaboration, public education, system composition, division of responsibilities, and action maps.
|
||||||
|
- **No need to copy:** colorful collages, full-page color blocks, flat silhouettes, right-angled modules, or any specific color combinations.
|
||||||
|
- **Density note:** the smaller the module, the shorter its text; relationship-diagram pages keep the necessary gaps; role-action pages may be denser, but meaning must never ride on color alone.
|
||||||
|
|
||||||
|
### Direction C: Steps / Work Records
|
||||||
|
|
||||||
|
- **Borrowable:** a persistently visible progress cue; numbered steps; clear containers for photos, screenshots, or worksheets; layered actions, risks, and check items.
|
||||||
|
- **Suits:** operation guides, compliance, safety, job skills, and on-site training.
|
||||||
|
- **No need to copy:** file folders, paper textures, monospaced labels, fixed bottom stacks, or any skeuomorphic decoration.
|
||||||
|
- **Density note:** each step page answers at least "what to do, why, and how to check"; route and review pages are not emptied for decoration.
|
||||||
|
|
||||||
|
### Direction D: Evidence / Data Editorial (Build Your Own as Needed)
|
||||||
|
|
||||||
|
- **Borrowable:** making charts, tables, or screenshots the main exhibit; the title gives the learning question or conclusion first; side text explains the measurement basis, the observation points, and the next step.
|
||||||
|
- **Suits:** professional popular-science content, results interpretation, before-after comparison, research training, and courses needing evidence support.
|
||||||
|
- **No need to copy:** dashboard black backgrounds, rainbow charts, giant numbers, dense annotations, or industry-specific brand styles.
|
||||||
|
- **Density note:** data pages may be full, but each chart serves only one question; without data, do not force charts just to "look like a report."
|
||||||
|
|
||||||
|
If the topic fits none of these directions, establish a new reference direction and write down "what it helps the learner accomplish" — rather than adding more decorative rules to it.
|
||||||
|
|
||||||
|
## 8. Page Skeletons: Choose the Learning Action First, Then the Layout
|
||||||
|
|
||||||
|
Every page is written as a minimal closed loop of "title + main exhibit + support layer + action / check + source." The following page types are composable — not all must be used.
|
||||||
|
|
||||||
|
- **Learning route:** learning questions, chapter order, and exit capabilities; use short sentences and a path, not a long table of contents.
|
||||||
|
- **Concept page:** one definition + boundaries / composition + one intuitive example or structural diagram.
|
||||||
|
- **Method map:** the overall framework + current step / current module + a cue for what unfolds next.
|
||||||
|
- **Demonstration page:** input material + intermediate judgments + basis for choices + result; put the error-prone forks on the same path.
|
||||||
|
- **Case-evidence page:** a case-role label + factual material + interpretation + transferable judgments + source.
|
||||||
|
- **Comparison / error-prone page:** two approaches or states placed side by side on the same skeleton + judgment criteria + common mistakes.
|
||||||
|
- **Practice page:** task instructions + input / scenario + work area + completion criteria; do not hide the answer on the same page.
|
||||||
|
- **Feedback page:** the reference approach + reasons + error location + how to check next time; make clear that this is the answer or the feedback.
|
||||||
|
- **Transfer page:** checklists, templates, job-aid cards, resources, and next-step actions; avoid merely writing "apply what you learned."
|
||||||
|
|
||||||
|
A presentation version may move extended explanation into speaker notes, but the page must still keep the learning question, the main exhibit, the task / judgment criteria, and the necessary sources. A self-study version writes the definitions and conditions needed for understanding on the page itself.
|
||||||
|
|
||||||
|
## 9. Minimum Constraints for the Visual System
|
||||||
|
|
||||||
|
### Grid and Hierarchy
|
||||||
|
|
||||||
|
- First fix the main content area, the title anchor, the content's left axis, and the page-number / source position; then decide the page variants.
|
||||||
|
- Each page has only one first visual entry: the learning question or principle is seen first, the main exhibit second, the explanation / task / source third.
|
||||||
|
- Titles, body text, figure notes, and sources use the same type family or a clear two-family relationship; the Chinese fallback must genuinely work. Do not force in a font that does not support Chinese just to imitate a sample.
|
||||||
|
- Titles and body text establish hierarchy through size, weight, and position. Projection materials prioritize body-text readability; self-study materials may add explanation, but body text is never pressed down to footnote size.
|
||||||
|
- Ordinary body text reaches at least 4.5:1 contrast against the background; when contrast is insufficient, change the text color or background directly — do not patch it with shadows and strokes.
|
||||||
|
|
||||||
|
### Color
|
||||||
|
|
||||||
|
- Without brand guidelines, first choose a neutral background, one structural color, and one necessary semantic accent: few and stable colors are easier to learn from than many and lively ones.
|
||||||
|
- Every color carries a fixed meaning (chapter, role, state, focus, or link) and must not decorate randomly; always also encode with text, shape, position, or icons alongside color.
|
||||||
|
- A page is usually governed by a neutral background + one main structural color; add a second color family only when a genuine role / category mapping exists.
|
||||||
|
- Body text on dark grounds uses high-contrast light colors; body text on light grounds uses near-black. Small type never sits on high-saturation colors.
|
||||||
|
|
||||||
|
**Starting point when there is no sample (not a fixed style):** adopt a near-white or light-gray background, near-black body text, one structural accent color, and one low-frequency semantic color; build the title-and-body hierarchy with a neutral sans-serif; pair a fixed title left axis with a "main exhibit + explanation / task" layout; keep body pages medium-leaning-full, with only covers, section openers, and closing pages noticeably dropping density. Then adjust by topic, audience, and material evidence — do not treat this starting set as a new template.
|
||||||
|
|
||||||
|
### Cards, Components, and Graphics
|
||||||
|
|
||||||
|
- Cards are used only for grouping, carrying states, or forming operable regions; continuously stacking cards is not a default layout.
|
||||||
|
- Rounding, right angles, strokes, shadows, and capsules are all component grammar — once decided, stay consistent throughout; do not take one from each different Sample.
|
||||||
|
- Shadows, textures, gradients, and decorative icons are used only when they serve a hierarchical or situational function. Anything whose removal does not hurt understanding should be removed.
|
||||||
|
- Default Office charts, SmartArt, rainbow palettes, thick black table frames, and untuned bullet lists must be redone.
|
||||||
|
|
||||||
|
### Images and Charts
|
||||||
|
|
||||||
|
- Every image must state "what to see and why it needs to be seen"; evidence images, operation screenshots, and situational images are each labeled with their purpose.
|
||||||
|
- Charts carry "see the relationship," side text carries "how to interpret and what to do next"; do not repeat the chart's labels verbatim.
|
||||||
|
- Charts keep the necessary units, time range, legend, key labels, measurement basis, and source; delete non-essential gridlines, frames, and decoration.
|
||||||
|
- Tables use clear headers, horizontal separators, correct alignment, and the necessary focus rows; do not use full rows of high-saturation color or color alone to express states.
|
||||||
|
|
||||||
|
## 10. Model Execution Protocol
|
||||||
|
|
||||||
|
### 1. Content First
|
||||||
|
|
||||||
|
First write the learner, entry state, exit capability, usage, evidence boundaries, and practice tasks; then write the chapter path. Record at least, for every page:
|
||||||
|
|
||||||
|
> Chapter / page type / learning action / title / main exhibit / explanation or task / feedback or check / source / density tier
|
||||||
|
|
||||||
|
Check whether every chapter has comprehension, demonstration, application, feedback, and transfer; if not, first add the content or note why it does not apply.
|
||||||
|
|
||||||
|
### 2. Reference-Sample Extraction
|
||||||
|
|
||||||
|
If there is a Sample, template, or screenshot, finish the reference extraction card before writing the visual brief. The card must not directly copy the object layer (brands, photos, copy, proprietary color values, proprietary motifs); keep only the transferable structure, tone, and graphic relationships. Samples 1, 2, and 3 are all merely candidate references and must not become implicit defaults.
|
||||||
|
|
||||||
|
### 3. Visual Brief
|
||||||
|
|
||||||
|
The visual brief states at least:
|
||||||
|
|
||||||
|
- the audience and usage environment;
|
||||||
|
- the topic's temperament and density targets;
|
||||||
|
- one main grid and two to six page types;
|
||||||
|
- the hierarchy of title / body / source;
|
||||||
|
- the semantic division of colors and the contrast plan;
|
||||||
|
- the graphic grammars for flows, hierarchy, comparison, and evidence respectively;
|
||||||
|
- the usage boundaries for images, screenshots, and illustrations;
|
||||||
|
- the fixed positions of header, footer, page number, source, and navigation;
|
||||||
|
- which elements appear only on covers, section pages, or closing pages.
|
||||||
|
|
||||||
|
### 4. Page-by-Page Generation
|
||||||
|
|
||||||
|
- Write titles that foretell the learning takeaway.
|
||||||
|
- Choose the single main learning object.
|
||||||
|
- Complete the definitions, boundaries, labels, tasks, check criteria, and sources.
|
||||||
|
- Apply the grid, hierarchy, and components per the visual brief — do not copy a Sample page by page.
|
||||||
|
- Delete graphics, images, quotes, and repeated text that serve no teaching function.
|
||||||
|
- Check content and overflow first, then make visual fine-tuning; do not mask excessive content by shrinking font size.
|
||||||
|
|
||||||
|
### 5. Pre-Delivery Validation
|
||||||
|
|
||||||
|
Use the existing PPTD / PPTX validation and screenshot workflow to check overflow, overlap, truncation, garbled text, low contrast, missing page numbers, and missing sources. Review key pages individually at minimum; do not declare completion from thumbnails alone.
|
||||||
|
|
||||||
|
## 11. Delivery Acceptance and Reverse Checks
|
||||||
|
|
||||||
|
### Content and Teaching
|
||||||
|
|
||||||
|
- Reading only the titles, can you state the complete learning path — not a string of section names?
|
||||||
|
- With the title covered, can the main exhibit still say what it teaches and what to do?
|
||||||
|
- Does each page carry only one main learning action, and does the explanation truly lead to action?
|
||||||
|
- Do exercises specify input, actions, and completion criteria? Does feedback explain the reasons and the error-correction method?
|
||||||
|
- Are facts, illustrative material, inferences, and fictional material clearly distinguished? Does external evidence have sources and measurement basis?
|
||||||
|
- Do the same concepts, roles, steps, and states keep the same name, color, and shape throughout?
|
||||||
|
|
||||||
|
### Density and Visuals
|
||||||
|
|
||||||
|
- Do body pages have enough content to support learning, rather than big titles plus blanks? Do consecutive sparse pages have an explicit teaching reason?
|
||||||
|
- Does each page have one visual entry and one quiet area; are the title, charts, cards, and big images all competing for first attention at the same time?
|
||||||
|
- With color removed, are steps, roles, states, and categories still legible?
|
||||||
|
- With the reference sample's photos, color values, and motifs removed, is the courseware still a self-consistent system?
|
||||||
|
- Is there only one main visual grammar, while different learning actions may use different page types?
|
||||||
|
- Are images sharp, relevant, unstretched, with no text pressed over complex areas?
|
||||||
|
- Do charts, tables, and structural diagrams express real relationships, rather than filling the page?
|
||||||
|
|
||||||
|
### The Final Cut
|
||||||
|
|
||||||
|
Ask item by item: "If I delete this, what does the learner lose?" If the answer is only "the page will look emptier," delete it. What truly needs filling is not decoration, but definitions, examples, judgment criteria, exercises, feedback, or transfer actions.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Management Reporting
|
||||||
|
|
||||||
|
## Clear and direct content; no empty talk
|
||||||
|
|
||||||
|
- A manager who flips through the whole deck reading only the page titles should grasp how the period went, why, and what is being asked — the title itself states a status or conclusion, such as "Q2 revenue reached 92% of target; the shortfall comes from North-region renewals," rather than a section label like "Business Performance Report."
|
||||||
|
- Open the first page and know how the period went overall: besides the title, the cover presents the single most critical status line of the whole deck — fulfillment rate of core metrics, a red/green/yellow traffic-light overview, or the biggest gap.
|
||||||
|
- Information is truthful and never fabricated: user-provided numbers and facts are neither exaggerated nor selectively clipped; any baseline that exists in the material travels alongside the results, and anything not in the material is not invented on the user's behalf; use numbers, charts, and tables flexibly to present key information.
|
||||||
|
- Professional wording that fits the user's identity: no layman's language, and no one-size-fits-all generic business-speak.
|
||||||
|
|
||||||
|
## Visualize information with structural diagrams
|
||||||
|
|
||||||
|
**Progress, data, responsibilities, risks, and dependencies are the core relationships of a management report — extract the various relationships from the content and express them with structural diagrams and frameworks.**
|
||||||
|
|
||||||
|
## Key Requirements
|
||||||
|
1. Create a top-tier visual experience: treat every detail of typesetting, fonts, and colors with care, crafting it like a work of art.
|
||||||
|
2. Unless the user explicitly requests it, **strictly forbid using rounded rectangles or rectangular cards** to build hierarchy or alignment: line segments, whitespace, and font/size differences are better solutions.
|
||||||
|
3. Unless no other layout is available, forbid evenly divided compositions; do not default to one-third splits, four-way splits, or 2×2 matrices — including three-part formulas such as "three columns + title + conclusion."
|
||||||
|
|
||||||
|
## Visual References
|
||||||
|
|
||||||
|
1. Fonts split into families: display type and body type have a clear division of labor (different weights of the same sans-serif family, or sans-serif titles paired with another family for body text); hierarchy is built on font-size and weight ratios; functional text such as numbering, labels, and footnotes is uniformly set small in caps / with added letter spacing; font sizes flex with content volume while the hierarchy ratios stay unchanged.
|
||||||
|
2. Fonts and the color scheme must be matched and harmonious; reference each other when designing.
|
||||||
|
3. Information-position discipline: conclusion/title → fixed title position; evidence (charts, numbered cards, photos) → a fixed area in the right column or lower half; measurement basis, sources, footnotes → small type in a fixed corner.
|
||||||
|
4. De-default charts: remove axes, grids, legend boxes, and default colors; reassign all series colors to same-family shades; bars are flat and rounded, with no stroke and no shadow; data labels keep only key points or move to the text column; key metrics are marked directly with big-number callouts; charts and interpretive text are strictly zoned — no overprinting, no interleaving.
|
||||||
|
5. Encourage using rich SmartArt to improve information efficiency and convey professionalism.
|
||||||
|
6. Unified corner radius and texture: all rectangular components (cards, panels, photos, bar tops) share one corner-radius scale with no mixing throughout the deck; flat, no drop shadows, no 3D, no gradient text; atmospheric layers such as grain or light flares, if used, stay low-intensity, fixed in position, and beneath the content.
|
||||||
|
7. Motifs run through the whole deck: establish 1–2 visual motifs (numbered badges, connecting dashed lines, decorative shapes, etc.) and reuse their variants across page types; when a page lacks a visual anchor, reuse a motif first instead of introducing new elements.
|
||||||
|
|
||||||
|
### Color Palette Reference
|
||||||
|
|
||||||
|
**Colors expected in spirit, unexpected in choice**:
|
||||||
|
- Reject the most common, most mediocre formulaic palettes of each scenario (Office default blue, tech blue-purple gradients, promotion-season red-and-gold, gray-blue template colors, etc.); suggested palettes for some scenarios:
|
||||||
|
* Operations/finance/board reporting: off-white #F7F3E8 base + ink green #123B2F + brass #B08D3E; or light stone #E7E2D8 base + deep navy #16283C + copper orange #C0652B; or ink black #1A1A2E base + brass #B08D3E + off-white #F7F3E8.
|
||||||
|
* Business operations review (retail/manufacturing/consumer/supply chain): warm white #F8F5EF base + wine red #5E1F2D + beige gold #D8C3A5; or off-white #F6EFE2 base + ink green #1E3B33 + terracotta #C15F3C; or light steel #C3CCD4 base + deep blue-gray #1F3240 + signal yellow #F0A202.
|
||||||
|
* Internet/tech/growth monthly reports: cool white #F4F7F6 base + graphite gray #2B2D42 + pine green #4C7A5A; or mist white #EDF1F7 base + deep indigo #1C2B5A + sky azure #4E9FE6; or ink black #101418 base + celadon green #3FA68C + cool white #F4F7F6.
|
||||||
|
* HR/administration/government-affairs reporting: warm white #FAF6EE base + ink purple #3D2C4F + cream yellow #F2C14E; or paper white #F7F3E8 base + pine green #2F5233 + apricot yellow #E8B54A; or off-white #F6F4EF base + deep navy #1B2A4A + silver gray #AEB4BC.
|
||||||
|
- Align background colors toward something more stable and restrained, e.g.: warm white #F7F3E8, light stone #ECEAE4, mist gray #E9E7E2; section/accent pages may use deep ink blue #16283C or ink black #1A1A2E.
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
# Tech & Engineering
|
||||||
|
|
||||||
|
Scope: sharing and presenting technical solutions, system architecture reviews, AI / data platform proposals, security design reviews, production incident retrospectives, technology selection, testing and validation, API / SDK documentation, and product technical training, among others.
|
||||||
|
|
||||||
|
Readers may be fellow engineers, but they may also be product partners or customers. The goal is for readers to understand how the system works, judge whether the solution is good, and see the trade-offs, risks, and next steps clearly.
|
||||||
|
|
||||||
|
## 1. Core Character
|
||||||
|
|
||||||
|
**Medium-to-high information density, with emphasis on technical explanation and on processes and mechanisms.** Pages are built around architectures, flows, sequences, data, comparisons, and real evidence; text explains the why, while visuals reveal the relationships.
|
||||||
|
|
||||||
|
- **Professional**: no empty talk of "high availability" or "high performance." Write out the metrics, environment, boundaries, dependencies, trade-offs, failure conditions, and recovery paths.
|
||||||
|
- **Concise**: a relationship one diagram can explain must not meander into an essay; nor may necessary assumptions, evidence, and limitations be omitted just to keep the layout clean.
|
||||||
|
- **Concrete**: architecture diagrams must show call directions, data flow, control flow, and trust or failure boundaries; code, configuration, and logs appear only when they genuinely support the argument.
|
||||||
|
- **Evidence first**: every page advances one judgment. The title gives the conclusion, the body gives the evidence, and it explains what the evidence means for the solution, the risks, or the next steps.
|
||||||
|
- **Restrained**: the wow factor comes from making a complex problem clear — not from glow, particles, walls of cards, and hollow "tech vibes."
|
||||||
|
|
||||||
|
## 2. General Prohibitions
|
||||||
|
|
||||||
|
The following are red lines running through the entire deck; they take effect before all layout and visual rules, and unless the user explicitly requests otherwise, none may be violated.
|
||||||
|
|
||||||
|
- **No cards by default**: unless the user explicitly requests it, strictly forbid using rounded rectangles or rectangular cards to build hierarchy or alignment. Line segments, whitespace, and font-size/weight differences are better solutions.
|
||||||
|
- **No evenly divided compositions**: unless no other layout is available, do not default to one-third splits, four-way splits, or 2×2 matrices — including formulaic patterns such as "title + three parallel blocks + conclusion."
|
||||||
|
- **No mediocre, common, or AI-typical color schemes**: unless the user explicitly requests them, strictly forbid blue-and-white pairings, blue-purple gradients, cyan-purple neon, rainbow flares, glassmorphism cards, and glowing borders.
|
||||||
|
- **No elements that clash with the overall style**: no styles from outside the chosen style may appear, such as using rounded icons or rounded rectangles within a sharp style.
|
||||||
|
|
||||||
|
## 3. How to Present Technical Content
|
||||||
|
|
||||||
|
### 3.1 Explain how the system works first
|
||||||
|
|
||||||
|
When first explaining a system or module, cover these first:
|
||||||
|
|
||||||
|
- inputs, outputs, and success criteria;
|
||||||
|
- the scope of the system and its external dependencies;
|
||||||
|
- the normal path, exception paths, and fallback paths;
|
||||||
|
- where data comes from, what processing it goes through, and where it ends up;
|
||||||
|
- which positions are the critical boundaries for performance, security, cost, or reliability.
|
||||||
|
|
||||||
|
### 3.2 Put design rationale and costs side by side
|
||||||
|
|
||||||
|
- Explain why A was chosen and B was not; the comparison dimensions must come from real goals.
|
||||||
|
- Write out the costs corresponding to the benefits, including performance, cost, complexity, team capability, vendor lock-in, observability, and migration risk.
|
||||||
|
- Give the conditions under which it works and the conditions under which it fails; do not present a local optimum as the universal optimum.
|
||||||
|
- Distinguish current state, target state, candidate options, hypothetical inference, and verified results.
|
||||||
|
- Launch plans specify validation gates, the gradual rollout method, rollback conditions, and recovery paths.
|
||||||
|
|
||||||
|
### 3.3 Evidence must be verifiable
|
||||||
|
|
||||||
|
- Metrics come with their source, time, version, test environment, load, sample, and measurement basis.
|
||||||
|
- Show P50 / P95 / P99, variability, outliers, or confidence intervals as the question demands — not just averages.
|
||||||
|
- Team inferences, illustrative data, and yet-to-be-verified assumptions must be explicitly labeled.
|
||||||
|
- When evidence is missing, use "to be filled in," "assumption," or a placeholder; never fabricate benchmarks, logs, cases, or failure causes.
|
||||||
|
- Explain terms and abbreviations at their first appearance, and keep naming consistent afterward.
|
||||||
|
|
||||||
|
## 4. Narrative Skeletons by Engineering Task
|
||||||
|
|
||||||
|
One primary scenario determines the whole deck's structure; other scenarios only supply necessary additions — never stack another visual language on top.
|
||||||
|
|
||||||
|
| Engineering task | What the reader must judge | Recommended narrative order |
|
||||||
|
|---|---|---|
|
||||||
|
| System architecture review | Whether boundaries, dependencies, bottlenecks, and the evolution path are sound | Goals and constraints → current state → problems → alternatives → target architecture → migration and validation |
|
||||||
|
| AI / data platform proposal | Whether the data, models, and service chain are trustworthy; how quality, latency, and cost are traded off | User tasks → data and model pipeline → core mechanisms → evaluation evidence → risk guardrails → launch loop |
|
||||||
|
| Security design review | Whether trust boundaries, attack paths, controls, and residual risk are clear | Assets and boundaries → threats → attack paths → controls → residual risk → monitoring and response |
|
||||||
|
| Production incident retrospective | What the impact was, how the failure happened, and whether the fixes can prevent recurrence | Impact summary → timeline → direct cause → systemic factors → fixes → prevention validation |
|
||||||
|
| Technology selection review | Which option better fits the goals, and which assumptions the conclusion is sensitive to | Goals and criteria → candidates → like-for-like comparison → trade-offs → recommendation → exit conditions |
|
||||||
|
| Testing / validation strategy | Whether risks are covered, and whether test layers and release gates suffice | Risk model → layered validation → environment and data → gates → coverage gaps → release decision |
|
||||||
|
| External tech talk | Why the core mechanism is valuable, whether the evidence is credible, and where the boundaries lie | Problem → mechanism → technical implementation → evidence → limitations → adoption path |
|
||||||
|
|
||||||
|
When no better structure fits, start from "decision summary → goals and constraints → current state / problems → mechanism / architecture → key evidence → alternatives and trade-offs → implementation / validation → risks and next steps," then trim and adapt to the scenario.
|
||||||
|
|
||||||
|
## 5. Page Rhythm and Information Density
|
||||||
|
|
||||||
|
- Default to medium-high density, but keep only one main judgment and one main evidence object per page.
|
||||||
|
- Alternate text-explanation pages and visual-evidence pages; avoid consecutive stacks of structurally identical bullet lists.
|
||||||
|
- For complex concepts, give the whole first, then expand layer by layer along stable coordinates; do not redraw the same architecture on every page.
|
||||||
|
- Show the normal path, exception paths, and migration path in layers; keep the unchanged parts and highlight only what changes.
|
||||||
|
- Accent pages prioritize key architectures, key comparisons, incident causality, or the final decision — do not manufacture climaxes with big-type slogans.
|
||||||
|
- Section transitions appear only when a genuine change of rhythm is needed; short materials are not force-fitted with tables of contents and section covers.
|
||||||
|
- End by returning to the decision, the validation results, or the next steps — never replace the conclusion with a lone "Thank you."
|
||||||
|
|
||||||
|
### Common page types
|
||||||
|
|
||||||
|
- **Concept / background page:** the necessary explanation paired with one main figure, whitespace spread around the main object.
|
||||||
|
- **Architecture / flow page:** graphics dominate; text keeps only the conclusion, the legend, and necessary side notes.
|
||||||
|
- **Comparison / selection page:** options compared on the same scale, dimensions, and coordinates.
|
||||||
|
- **Metrics / benchmark page:** charts lead; test conditions, baselines, and conclusions stay close to the chart.
|
||||||
|
- **Incident / timeline page:** chronological order is the main axis; impact, evidence, and handling stay close to their corresponding events.
|
||||||
|
- **Implementation / migration page:** phases, dependencies, gates, and rollback points are laid out according to their real relationships.
|
||||||
|
- **Appendix / sources page:** may be denser, but must stay scannable via columns, numbering, and stable line spacing.
|
||||||
|
|
||||||
|
## 6. Architecture Diagrams and Flowcharts
|
||||||
|
|
||||||
|
### 6.1 The relationship determines the graphic
|
||||||
|
|
||||||
|
| Relationship | Preferred graphic | Information that must be conveyed |
|
||||||
|
|---|---|---|
|
||||||
|
| System regions and boundaries | Nested or side-by-side right-angled regions | System scope, external dependencies, trust / failure boundaries |
|
||||||
|
| Calls and data flow | Node chains or networks with directional arrows | The two parties of each call, protocol / data, sync or async |
|
||||||
|
| Multi-role interaction | Sequence diagrams | Roles, message directions, waits, and exception branches |
|
||||||
|
| Deployment and failure domains | Topology diagrams | Regions / clusters / instances, redundancy, and isolation scope |
|
||||||
|
| State transitions | State machines | States, trigger conditions, transitions, and error states |
|
||||||
|
| Option comparison | Side-by-side topologies or matrices at the same scale | Identical evaluation dimensions, structural differences, and costs |
|
||||||
|
| Incident causality | Timeline + causal chain / fault tree | Events, evidence, direct causes, contributing factors, and control gaps |
|
||||||
|
| Validation coverage | Layered matrices or mapping diagrams | Risks, test layers, pass criteria, and coverage gaps |
|
||||||
|
|
||||||
|
### 6.2 Diagramming discipline
|
||||||
|
|
||||||
|
- Arrows must have direction and meaning; label the protocol, data, frequency, capacity, or trigger conditions where necessary.
|
||||||
|
- Distinguish data flow, control flow, and exception paths; redundant encoding with color, line style, and labels is allowed.
|
||||||
|
- External boundaries, internal modules, and critical paths use different but stable grammars.
|
||||||
|
- Connectors terminate at node edges and never cross through text; when there are too many crossings, rearrange, layer, or split the page.
|
||||||
|
- Bold the critical paths or use the accent color; other paths recede to neutral; no glow and no 3D arrows.
|
||||||
|
- For complex architectures, overview first, then zoom into details; keep naming, colors, and coordinate cues consistent.
|
||||||
|
- Legends explain only the encodings that cannot be labeled in place; anything that can sit beside its node or path is labeled directly.
|
||||||
|
|
||||||
|
## 7. Data Charts, Tables, Code, and Screenshots
|
||||||
|
|
||||||
|
### Data charts
|
||||||
|
|
||||||
|
- Trends use lines, comparisons use bars or side-by-side dot plots, composition uses stacks, latency distributions use histograms / box plots / quantile plots, and capacity-cost relationships may use scatter plots or sensitivity matrices.
|
||||||
|
- Remove default frames, 3D effects, gradients, heavy gridlines, and meaningless legends.
|
||||||
|
- Main series use the structural color or critical-path color; baselines and secondary series use grayscale.
|
||||||
|
- Key values, inflection points, and anomalies are labeled directly, with the "why" and the "what it means."
|
||||||
|
- Test environment, version, load, sample, units, and baseline appear alongside the chart.
|
||||||
|
|
||||||
|
### Tables
|
||||||
|
|
||||||
|
- Headers, grouping, status, numbers, and notes form clear hierarchy; default themes are not kept.
|
||||||
|
- Notes left-aligned, numbers aligned by decimal point or unit, and status columns fixed in position.
|
||||||
|
- Use thin separators and light group backgrounds; no thick outer frames, rainbow headers, or large red/green fills.
|
||||||
|
- Options are compared on the same dimensions; missing and non-comparable items are explicitly marked.
|
||||||
|
|
||||||
|
### Code, configuration, and screenshots
|
||||||
|
|
||||||
|
- Keep only the minimal excerpt that supports the argument, and first tell the reader what to look at.
|
||||||
|
- Code, interfaces, paths, and fields use monospaced fonts; syntax highlighting only accentuates the key lines or changes.
|
||||||
|
- For configuration comparison, prefer a minimal diff or a key-fields table; logs are timestamped, identify the source component, and have sensitive data redacted or anonymized.
|
||||||
|
- Crop screenshots tightly to the evidence boundary, keeping them right-angled rectangles; do not uniformly add device frames, rounded corners, or shadows.
|
||||||
|
- Side-by-side screenshots keep equal height and width, identical zoom, and the same baseline.
|
||||||
|
|
||||||
|
## 8. Visual System
|
||||||
|
|
||||||
|
### 8.1 Layout
|
||||||
|
|
||||||
|
- A title conveys the conclusion in one sentence, and its visual weight must not exceed the main evidence.
|
||||||
|
- Use stable safe margins and a title axis; ordinary pages prefer a single column, a left-right split, or text above and figure below.
|
||||||
|
- Architecture nodes land on a regular grid; pages of the same kind keep the same skeleton.
|
||||||
|
- Body content is organized by whitespace, alignment, and thin rules by default; cards are used only when content needs delimiting.
|
||||||
|
- Panels are used only for system scope, code, configuration, or key conclusions — mostly right-angled or minimally rounded, with thin strokes and light fills, and no generic shadows.
|
||||||
|
- Reading-type materials may keep the document name, section, page number, version, and confidentiality level; live-presented materials keep only the necessary identity information.
|
||||||
|
|
||||||
|
### 8.2 Colors and fonts
|
||||||
|
|
||||||
|
- Reviews, printing, and long-form reading usually use light backgrounds; dark-room launches may use a restrained dark background.
|
||||||
|
- When the brand has existing visual assets, inherit them first; without brand constraints, build the palette along the lines of "background color, structural color, critical-path color, status colors, neutrals."
|
||||||
|
- Each color carries a stable meaning throughout the deck; critical states are additionally distinguished by text, line style, or shape.
|
||||||
|
- Titles and body text prefer highly legible sans-serif faces; when a document feel is needed, titles may use a restrained serif.
|
||||||
|
- Code uses monospaced fonts; one deck usually uses no more than two body-type families.
|
||||||
|
- When content grows, first condense the text, split pages, or adjust the text-to-graphic ratio — only then shrink font size slightly.
|
||||||
|
|
||||||
|
### 8.3 Lines, icons, and effects
|
||||||
|
|
||||||
|
- Ordinary borders and auxiliary lines are thin and even; only critical paths get visibly bolder.
|
||||||
|
- Within one diagram, arrows, corners, endpoints, dash rhythms, and corner radii stay consistent.
|
||||||
|
- Technical structures prefer right angles or minimal rounding; circles and curves appear only when semantics require them.
|
||||||
|
- Architecture, flows, data, screenshots, and real product visuals take precedence over decorative illustration.
|
||||||
|
- Icons must come from one stroke system and serve identification; do not mix emoji, 3D, realistic, and system-default icons.
|
||||||
|
- Body surfaces stay clean: no noise, particles, grid-light effects, glassmorphism, or meaningless gradients.
|
||||||
|
|
||||||
|
## 9. Sample References
|
||||||
|
|
||||||
|
Samples are only for extracting mechanisms; wholesale replication is forbidden. Organize the Style first according to the current readers, evidence, brand, and usage; precise color values, fonts, covers, section pages, and footers are not inherited by default.
|
||||||
|
|
||||||
|
### Sample A: Warm Orange & Cool Blue — White-Base Engineering Document Style
|
||||||
|
|
||||||
|
- **Traits:** white background with black body text; the warm color carries titles and critical paths, the cool color carries nodes and structure; fixed header and page numbers; suited to reading and printing.
|
||||||
|
- **Borrowable:** the warm/cool semantic division, a stable document axis, reuse of architecture coordinates, and hierarchical distinction through boundaries / nodes / paths.
|
||||||
|
- **Good for reference:** architecture reviews, technical white papers, platform proposals, and materials that need to unfold the same architecture step by step.
|
||||||
|
- **Not inherited automatically:** the orange and blue color values, Inter, the gradient-arc cover, orange table headers, and the fixed header position.
|
||||||
|
|
||||||
|
### Sample B: Four-Color Flat — Engineering Walkthrough Style
|
||||||
|
|
||||||
|
- **Traits:** white background with dark-gray text, a few high-saturation semantic colors, flat structural diagrams, and solid-color section pages; suited to live walkthroughs and cross-role communication.
|
||||||
|
- **Borrowable:** semantic color coding, solid-color section pauses, text/graphic zoning, direct labeling, and equal-width comparison columns.
|
||||||
|
- **Good for reference:** product technical training, role collaboration, and talks balancing concepts and flows.
|
||||||
|
- **Not inherited automatically:** the fixed four colors, solid-per-chapter backgrounds, the dual-ended footer, and specific brand fonts.
|
||||||
|
|
||||||
|
### Sample C: White Base & Deep Ink Blue — Two-Color Short-Line Engineering Walkthrough
|
||||||
|
|
||||||
|
- **Traits:** deep ink-blue titles, short colored lines, and a white base forming a stable walkthrough skeleton; explanation pages alternate with evidence pages; visuals float directly on the white ground.
|
||||||
|
- **Borrowable:** the fixed title axis, weak navigation, alternation of explanation and evidence, and reduced container noise.
|
||||||
|
- **Good for reference:** live tech talks, building concepts step by step, and materials centered on screenshots and mechanism diagrams.
|
||||||
|
- **Not inherited automatically:** the orange/green section colors, diagonal section color blocks, the deep-purple ending, the fixed short-line sizes, and the font combination.
|
||||||
|
|
||||||
|
Do not write "choose Sample A / B / C" in the plan. If the result still clearly looks like a direct replica of some Sample once the text is hidden, keep adjusting — but do not break relationship expression and consistency just to be novel.
|
||||||
|
|
||||||
|
## 10. Pre-Delivery Checklist
|
||||||
|
|
||||||
|
- Are inputs, outputs, dependencies, boundaries, the normal path, exception paths, and fallback paths clear?
|
||||||
|
- Do key connectors have direction and meaning, and are current state, target state, assumptions, and verified results distinguished?
|
||||||
|
- Do metrics carry environment, version, sample, load, units, baseline, and time window?
|
||||||
|
- Does each page carry only one main judgment, and does the evidence truly support the title?
|
||||||
|
- Does the visual weight fall on the evidence, and are colors, shapes, line styles, and naming consistent?
|
||||||
|
- Are there unsourced data, default Office charts, walls of cards, decorative "tech vibes," or Sample replicas?
|
||||||
|
- Was technical evidence altered, weakened, or falsified for the sake of visual unity?
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Open Kimi PPT Local Export</title>
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10;
|
||||||
|
top: 8px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgb(0 0 0 / 72%);
|
||||||
|
color: white;
|
||||||
|
font: 12px/1.5 system-ui, sans-serif;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="status">starting</div>
|
||||||
|
<iframe id="editor" allow="fullscreen; clipboard-read; clipboard-write"></iframe>
|
||||||
|
<script type="module">
|
||||||
|
import {
|
||||||
|
i as connect,
|
||||||
|
r as WindowMessenger,
|
||||||
|
} from "https://statics.moonshot.cn/neo-design/assets/penpal-C4NjirZE.js";
|
||||||
|
|
||||||
|
const status = document.querySelector("#status");
|
||||||
|
const frame = document.querySelector("#editor");
|
||||||
|
const editorOrigin = "https://www.kimi.com";
|
||||||
|
|
||||||
|
const setStatus = (value) => {
|
||||||
|
status.textContent = value;
|
||||||
|
document.documentElement.dataset.deckStatus = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizePath = (value) => {
|
||||||
|
if (typeof value !== "string") return "";
|
||||||
|
if (/^(?:data:image\/|https?:\/\/|blob:)/i.test(value)) return value;
|
||||||
|
let candidate = value.replace(/^file:\/\/+/, "").replaceAll("\\", "/");
|
||||||
|
try {
|
||||||
|
candidate = decodeURIComponent(candidate);
|
||||||
|
} catch {
|
||||||
|
// Preserve literal percent signs.
|
||||||
|
}
|
||||||
|
const parts = [];
|
||||||
|
for (const part of candidate.split("/")) {
|
||||||
|
if (!part || part === ".") continue;
|
||||||
|
if (part === "..") parts.pop();
|
||||||
|
else parts.push(part);
|
||||||
|
}
|
||||||
|
return parts.join("/");
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveImage = (requestedPath, imageMap) => {
|
||||||
|
if (/^(?:data:image\/|https?:\/\/|blob:)/i.test(requestedPath || "")) {
|
||||||
|
return requestedPath;
|
||||||
|
}
|
||||||
|
const normalized = normalizePath(requestedPath);
|
||||||
|
if (imageMap[normalized]) return imageMap[normalized];
|
||||||
|
const suffix = Object.keys(imageMap).find(
|
||||||
|
(path) => normalized.endsWith(`/${path}`) || path.endsWith(`/${normalized}`),
|
||||||
|
);
|
||||||
|
return suffix ? imageMap[suffix] : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await fetch("./payload.json", { cache: "no-store" }).then((response) => {
|
||||||
|
if (!response.ok) throw new Error(`payload HTTP ${response.status}`);
|
||||||
|
return response.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
const query = new URLSearchParams({
|
||||||
|
sdkMode: "ppt-editor",
|
||||||
|
pptPlatform: "open-kimi-ppt-local-export",
|
||||||
|
functional: JSON.stringify({
|
||||||
|
fullscreen: false,
|
||||||
|
present: false,
|
||||||
|
export: true,
|
||||||
|
close: false,
|
||||||
|
annotation: false,
|
||||||
|
feedback: false,
|
||||||
|
share: false,
|
||||||
|
versionHistory: false,
|
||||||
|
}),
|
||||||
|
sdkSaveMode: "external",
|
||||||
|
sdkImageMode: "external",
|
||||||
|
});
|
||||||
|
|
||||||
|
frame.addEventListener(
|
||||||
|
"load",
|
||||||
|
async () => {
|
||||||
|
setStatus("iframe-loaded");
|
||||||
|
try {
|
||||||
|
const messenger = new WindowMessenger({
|
||||||
|
remoteWindow: frame.contentWindow,
|
||||||
|
allowedOrigins: [editorOrigin],
|
||||||
|
});
|
||||||
|
const connection = connect({
|
||||||
|
messenger,
|
||||||
|
methods: {
|
||||||
|
close() {},
|
||||||
|
reenter() {},
|
||||||
|
toggleFullScreen(value) {
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
showFeedback() {},
|
||||||
|
sendPrompt() {},
|
||||||
|
showMessage() {},
|
||||||
|
hideMessage() {},
|
||||||
|
onSave(savePayload) {
|
||||||
|
return {
|
||||||
|
fileContent: savePayload?.fileContent,
|
||||||
|
lastModifiedTime: Date.now(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getImages(imagePayload = {}) {
|
||||||
|
const paths = Array.isArray(imagePayload.filePath) ? imagePayload.filePath : [];
|
||||||
|
return paths.map((path) => resolveImage(path, payload.imageMap || {}));
|
||||||
|
},
|
||||||
|
setAnnotationMode() {},
|
||||||
|
setAnnotationCurrentPage() {},
|
||||||
|
upsertAnnotation() {},
|
||||||
|
removeAnnotation() {},
|
||||||
|
clearAnnotations() {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const remote = await connection.promise;
|
||||||
|
window.exportRemote = remote;
|
||||||
|
await remote.setSlideConfig({
|
||||||
|
editable: true,
|
||||||
|
locale: "zh-CN",
|
||||||
|
theme: "light",
|
||||||
|
slideId: payload.id,
|
||||||
|
});
|
||||||
|
await remote.setPPTD(payload.id, {
|
||||||
|
pptdContent: payload.manifestContent,
|
||||||
|
pages: payload.pages,
|
||||||
|
pptdPath: payload.manifestPath,
|
||||||
|
basePath: "",
|
||||||
|
isCreate: true,
|
||||||
|
});
|
||||||
|
await remote.setEditable(true);
|
||||||
|
window.exportSlideStatus = await remote.getSlideStatus();
|
||||||
|
setStatus("ready");
|
||||||
|
} catch (error) {
|
||||||
|
window.exportHostError = String(error?.stack || error);
|
||||||
|
setStatus("error");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
frame.src = `${editorOrigin}/neo-ppt/?${query}`;
|
||||||
|
} catch (error) {
|
||||||
|
window.exportHostError = String(error?.stack || error);
|
||||||
|
setStatus("error");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,427 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Export a PPTD project as page images through Kimi's public editor for visual QA.
|
||||||
|
|
||||||
|
Reuses the same localhost SDK host and agent-browser flow as export_pptx.py, but
|
||||||
|
chooses 图片 in the export dialog, captures the images ZIP, unzips it, and stitches
|
||||||
|
all pages into a single overview image that a multimodal model can review.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||||
|
|
||||||
|
from export_pptx import (
|
||||||
|
HOST_TEMPLATE,
|
||||||
|
BrowserSession,
|
||||||
|
ExportError,
|
||||||
|
build_payload,
|
||||||
|
ensure_agent_browser,
|
||||||
|
find_download,
|
||||||
|
find_manifest,
|
||||||
|
log,
|
||||||
|
ref_by_name,
|
||||||
|
serve,
|
||||||
|
wait_for_export_dialog,
|
||||||
|
)
|
||||||
|
|
||||||
|
IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".webp"}
|
||||||
|
OVERVIEW_COLUMNS = 3
|
||||||
|
OVERVIEW_THUMB_WIDTH = 640
|
||||||
|
OVERVIEW_LABEL_HEIGHT = 32
|
||||||
|
OVERVIEW_GAP = 12
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_pillow() -> Tuple[Any, Any, Any]:
|
||||||
|
try:
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
return Image, ImageDraw, ImageFont
|
||||||
|
except ImportError:
|
||||||
|
log("Pillow is required for stitching; installing pillow with pip --user")
|
||||||
|
process = subprocess.run(
|
||||||
|
[sys.executable, "-m", "pip", "install", "--user", "pillow"],
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
timeout=300,
|
||||||
|
)
|
||||||
|
if process.returncode != 0:
|
||||||
|
raise ExportError(f"failed to install Pillow:\n{process.stdout[-2000:]}")
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
return Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
|
||||||
|
def is_image_zip(path: Path) -> bool:
|
||||||
|
if not path.is_file() or path.name.endswith(".crdownload"):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(path) as archive:
|
||||||
|
return any(
|
||||||
|
Path(name).suffix.lower() in IMAGE_SUFFIXES
|
||||||
|
for name in archive.namelist()
|
||||||
|
)
|
||||||
|
except (OSError, zipfile.BadZipFile):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def page_sort_key(path: Path) -> Tuple[int, str]:
|
||||||
|
match = re.match(r"(\d+)", path.stem)
|
||||||
|
return (int(match.group(1)) if match else sys.maxsize, path.name)
|
||||||
|
|
||||||
|
|
||||||
|
def unzip_images(archive_path: Path, pages_dir: Path) -> List[Path]:
|
||||||
|
pages_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
images: List[Path] = []
|
||||||
|
with zipfile.ZipFile(archive_path) as archive:
|
||||||
|
for info in archive.infolist():
|
||||||
|
if info.is_dir() or Path(info.filename).suffix.lower() not in IMAGE_SUFFIXES:
|
||||||
|
continue
|
||||||
|
name = Path(info.filename).name
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
target = pages_dir / name
|
||||||
|
with archive.open(info) as source, target.open("wb") as out:
|
||||||
|
shutil.copyfileobj(source, out)
|
||||||
|
images.append(target)
|
||||||
|
images.sort(key=page_sort_key)
|
||||||
|
if not images:
|
||||||
|
raise ExportError(f"no page images found in: {archive_path}")
|
||||||
|
return images
|
||||||
|
|
||||||
|
|
||||||
|
def label_font(image_font: Any) -> Any:
|
||||||
|
try:
|
||||||
|
return image_font.load_default(size=18)
|
||||||
|
except TypeError: # older Pillow without the size argument
|
||||||
|
return image_font.load_default()
|
||||||
|
|
||||||
|
|
||||||
|
def stitch_overview(
|
||||||
|
images: Sequence[Path],
|
||||||
|
output: Path,
|
||||||
|
image_cls: Any,
|
||||||
|
draw_cls: Any,
|
||||||
|
image_font: Any,
|
||||||
|
) -> Path:
|
||||||
|
thumbs: List[Tuple[str, Any]] = []
|
||||||
|
for index, path in enumerate(images, start=1):
|
||||||
|
with image_cls.open(path) as opened:
|
||||||
|
frame = opened.convert("RGB")
|
||||||
|
ratio = OVERVIEW_THUMB_WIDTH / frame.width
|
||||||
|
thumb = frame.resize(
|
||||||
|
(OVERVIEW_THUMB_WIDTH, max(1, round(frame.height * ratio)))
|
||||||
|
)
|
||||||
|
thumbs.append((f"P{index}", thumb))
|
||||||
|
|
||||||
|
columns = OVERVIEW_COLUMNS
|
||||||
|
rows = math.ceil(len(thumbs) / columns)
|
||||||
|
cell_height = OVERVIEW_LABEL_HEIGHT + max(thumb.height for _, thumb in thumbs)
|
||||||
|
width = columns * OVERVIEW_THUMB_WIDTH + (columns + 1) * OVERVIEW_GAP
|
||||||
|
height = rows * cell_height + (rows + 1) * OVERVIEW_GAP
|
||||||
|
|
||||||
|
overview = image_cls.new("RGB", (width, height), "#e5e7eb")
|
||||||
|
draw = draw_cls.Draw(overview)
|
||||||
|
font = label_font(image_font)
|
||||||
|
for position, (label, thumb) in enumerate(thumbs):
|
||||||
|
column = position % columns
|
||||||
|
row = position // columns
|
||||||
|
x = OVERVIEW_GAP + column * (OVERVIEW_THUMB_WIDTH + OVERVIEW_GAP)
|
||||||
|
y = OVERVIEW_GAP + row * (cell_height + OVERVIEW_GAP)
|
||||||
|
draw.rectangle(
|
||||||
|
(x, y, x + OVERVIEW_THUMB_WIDTH, y + OVERVIEW_LABEL_HEIGHT - 4),
|
||||||
|
fill="#111827",
|
||||||
|
)
|
||||||
|
draw.text((x + 8, y + 5), label, fill="#ffffff", font=font)
|
||||||
|
overview.paste(thumb, (x, y + OVERVIEW_LABEL_HEIGHT))
|
||||||
|
|
||||||
|
overview.save(output, "JPEG", quality=85)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
OOPIF_URL_HINT = "kimi.com/neo-ppt"
|
||||||
|
|
||||||
|
# The export dialog's 图片 format option is a plain <div class="radio-group-item">
|
||||||
|
# without an ARIA role, so agent-browser's interactive snapshot never exposes it
|
||||||
|
# and cross-origin iframe rules block page-level eval. Clicking it requires CDP.
|
||||||
|
IMAGE_FORMAT_CLICK_JS = """
|
||||||
|
(() => {
|
||||||
|
const items = [...document.querySelectorAll('.radio-group-item')];
|
||||||
|
const pool = items.length
|
||||||
|
? items
|
||||||
|
: [...document.querySelectorAll('div,span,label,button')].filter(
|
||||||
|
(el) => el.children.length === 0
|
||||||
|
);
|
||||||
|
const target = pool.find((el) => el.textContent.trim() === '图片');
|
||||||
|
if (!target) return null;
|
||||||
|
target.click();
|
||||||
|
return 'clicked';
|
||||||
|
})()
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
ACTIVE_FORMAT_JS = """
|
||||||
|
(() => {
|
||||||
|
const active = document.querySelector('.radio-group-item.active');
|
||||||
|
return active ? active.textContent.trim() : null;
|
||||||
|
})()
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_websocket() -> Any:
|
||||||
|
try:
|
||||||
|
import websocket
|
||||||
|
|
||||||
|
return websocket
|
||||||
|
except ImportError:
|
||||||
|
log("websocket-client is required for dialog automation; installing with pip --user")
|
||||||
|
process = subprocess.run(
|
||||||
|
[sys.executable, "-m", "pip", "install", "--user", "websocket-client"],
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
timeout=300,
|
||||||
|
)
|
||||||
|
if process.returncode != 0:
|
||||||
|
raise ExportError(f"failed to install websocket-client:\n{process.stdout[-2000:]}")
|
||||||
|
import websocket
|
||||||
|
|
||||||
|
return websocket
|
||||||
|
|
||||||
|
|
||||||
|
def browser_cdp_url(browser: BrowserSession) -> str:
|
||||||
|
process = browser.run(["get", "cdp-url"], timeout=30)
|
||||||
|
match = re.search(r"ws://\S+", process.stdout)
|
||||||
|
if not match:
|
||||||
|
raise ExportError(
|
||||||
|
f"could not determine the browser CDP URL:\n{process.stdout[-500:]}"
|
||||||
|
)
|
||||||
|
return match.group(0)
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_in_iframe(cdp_url: str, url_hint: str, expression: str) -> Any:
|
||||||
|
websocket = ensure_websocket()
|
||||||
|
|
||||||
|
def call(socket: Any, request_id: int, method: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
socket.send(json.dumps({"id": request_id, "method": method, "params": params}))
|
||||||
|
while True:
|
||||||
|
message = json.loads(socket.recv())
|
||||||
|
if message.get("id") != request_id:
|
||||||
|
continue
|
||||||
|
if "error" in message:
|
||||||
|
raise ExportError(f"CDP {method} failed: {message['error']}")
|
||||||
|
return message.get("result", {})
|
||||||
|
|
||||||
|
# websocket-client honors http_proxy env vars; the CDP endpoint is local,
|
||||||
|
# so strip proxy settings instead of tunneling localhost through the proxy.
|
||||||
|
proxy_env = ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY", "all_proxy", "ALL_PROXY")
|
||||||
|
saved_proxy = {name: os.environ.pop(name) for name in proxy_env if name in os.environ}
|
||||||
|
try:
|
||||||
|
socket = websocket.create_connection(cdp_url, timeout=30, suppress_origin=True)
|
||||||
|
finally:
|
||||||
|
os.environ.update(saved_proxy)
|
||||||
|
try:
|
||||||
|
targets = call(socket, 1, "Target.getTargets", {}).get("targetInfos", [])
|
||||||
|
target = next(
|
||||||
|
(item for item in targets if url_hint in str(item.get("url", ""))), None
|
||||||
|
)
|
||||||
|
if target is None:
|
||||||
|
visible = ", ".join(
|
||||||
|
f"{item.get('type')}:{str(item.get('url', ''))[:80]}" for item in targets
|
||||||
|
)
|
||||||
|
raise ExportError(
|
||||||
|
f"no browser target matches {url_hint!r}; observed: {visible}"
|
||||||
|
)
|
||||||
|
attached = call(
|
||||||
|
socket,
|
||||||
|
2,
|
||||||
|
"Target.attachToTarget",
|
||||||
|
{"targetId": target["targetId"], "flatten": True},
|
||||||
|
)
|
||||||
|
session_id = attached["sessionId"]
|
||||||
|
socket.send(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"sessionId": session_id,
|
||||||
|
"method": "Runtime.evaluate",
|
||||||
|
"params": {"expression": expression, "returnByValue": True},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
while True:
|
||||||
|
message = json.loads(socket.recv())
|
||||||
|
if message.get("id") != 3:
|
||||||
|
continue
|
||||||
|
if "error" in message:
|
||||||
|
raise ExportError(f"CDP Runtime.evaluate failed: {message['error']}")
|
||||||
|
result = message.get("result", {})
|
||||||
|
if result.get("exceptionDetails"):
|
||||||
|
details = result["exceptionDetails"]
|
||||||
|
raise ExportError(f"iframe script failed: {details.get('text')}")
|
||||||
|
return result.get("result", {}).get("value")
|
||||||
|
finally:
|
||||||
|
socket.close()
|
||||||
|
|
||||||
|
|
||||||
|
def select_image_format(browser: BrowserSession) -> None:
|
||||||
|
cdp_url = browser_cdp_url(browser)
|
||||||
|
value = evaluate_in_iframe(cdp_url, OOPIF_URL_HINT, IMAGE_FORMAT_CLICK_JS)
|
||||||
|
if value != "clicked":
|
||||||
|
raise ExportError("could not find the 图片 option in the export dialog")
|
||||||
|
deadline = time.monotonic() + 10
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
active = evaluate_in_iframe(cdp_url, OOPIF_URL_HINT, ACTIVE_FORMAT_JS)
|
||||||
|
if active == "图片":
|
||||||
|
return
|
||||||
|
time.sleep(0.3)
|
||||||
|
raise ExportError(f"image format was not activated; active option: {active!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def export_images(
|
||||||
|
source: Path,
|
||||||
|
output: Path,
|
||||||
|
keep_download: bool = False,
|
||||||
|
force: bool = False,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
manifest = find_manifest(source)
|
||||||
|
payload = build_payload(manifest)
|
||||||
|
output = output.expanduser().resolve()
|
||||||
|
if output.exists() and any(output.iterdir()) and not force:
|
||||||
|
raise ExportError(
|
||||||
|
f"output directory already exists (pass --force to replace it): {output}"
|
||||||
|
)
|
||||||
|
agent_browser = ensure_agent_browser()
|
||||||
|
image_cls, draw_cls, image_font = ensure_pillow()
|
||||||
|
|
||||||
|
log(f"manifest: {manifest}")
|
||||||
|
with tempfile.TemporaryDirectory(prefix="open-kimi-ppt-images-") as temp_name:
|
||||||
|
temp_dir = Path(temp_name)
|
||||||
|
download_dir = temp_dir / "downloads"
|
||||||
|
download_dir.mkdir()
|
||||||
|
shutil.copy2(HOST_TEMPLATE, temp_dir / HOST_TEMPLATE.name)
|
||||||
|
(temp_dir / "payload.json").write_text(
|
||||||
|
json.dumps(payload, ensure_ascii=False), encoding="utf-8"
|
||||||
|
)
|
||||||
|
server, thread, url = serve(temp_dir)
|
||||||
|
session = f"open-kimi-ppt-images-{os.getpid()}-{uuid.uuid4().hex[:8]}"
|
||||||
|
browser = BrowserSession(agent_browser, session, temp_dir, download_dir)
|
||||||
|
try:
|
||||||
|
log("opening the public Kimi slide editor")
|
||||||
|
browser.open(url)
|
||||||
|
browser.run(
|
||||||
|
[
|
||||||
|
"wait",
|
||||||
|
"--fn",
|
||||||
|
'document.documentElement.dataset.deckStatus === "ready"',
|
||||||
|
],
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
browser.run(["set", "viewport", "1280", "720"])
|
||||||
|
snapshot = browser.snapshot()
|
||||||
|
export_ref = ref_by_name(snapshot, "导出", "button")
|
||||||
|
browser.run(["click", f"@{export_ref}"])
|
||||||
|
dialog = wait_for_export_dialog(browser)
|
||||||
|
|
||||||
|
select_image_format(browser)
|
||||||
|
dialog = wait_for_export_dialog(browser)
|
||||||
|
|
||||||
|
download_ref = ref_by_name(dialog, "下载", "button")
|
||||||
|
log("rendering page images in the browser")
|
||||||
|
result = browser.run(
|
||||||
|
["download", f"@{download_ref}", str(temp_dir / "browser-output.zip")],
|
||||||
|
timeout=300,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
log("download capture reported a timeout; checking browser output files")
|
||||||
|
downloaded = find_download(
|
||||||
|
(download_dir, temp_dir), timeout=240, accept=is_image_zip
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
browser.close()
|
||||||
|
server.shutdown()
|
||||||
|
server.server_close()
|
||||||
|
thread.join(timeout=2)
|
||||||
|
|
||||||
|
if output.exists():
|
||||||
|
shutil.rmtree(output)
|
||||||
|
output.mkdir(parents=True)
|
||||||
|
images = unzip_images(downloaded, output / "pages")
|
||||||
|
if keep_download:
|
||||||
|
shutil.copy2(downloaded, output / "browser-raw.zip")
|
||||||
|
overview = stitch_overview(
|
||||||
|
images, output / "overview.jpg", image_cls, draw_cls, image_font
|
||||||
|
)
|
||||||
|
|
||||||
|
page_paths = [entry["path"] for entry in payload["pages"]]
|
||||||
|
mapping = [
|
||||||
|
{
|
||||||
|
"index": index,
|
||||||
|
"image": f"pages/{path.name}",
|
||||||
|
"page": page_paths[index - 1] if index - 1 < len(page_paths) else None,
|
||||||
|
}
|
||||||
|
for index, path in enumerate(images, start=1)
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
"pages": len(images),
|
||||||
|
"overview": str(overview),
|
||||||
|
"output": str(output),
|
||||||
|
"images": mapping,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=(
|
||||||
|
"Export a PPTD project as page images via Kimi's public editor, unzip "
|
||||||
|
"them, and stitch an overview image for visual QA."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parser.add_argument("input", type=Path, help=".pptd manifest or project directory")
|
||||||
|
parser.add_argument(
|
||||||
|
"--output",
|
||||||
|
"-o",
|
||||||
|
type=Path,
|
||||||
|
help="output directory (default: <project>/.qa-images)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--keep-browser-raw",
|
||||||
|
action="store_true",
|
||||||
|
help="also keep the downloaded images ZIP beside the overview",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--force",
|
||||||
|
action="store_true",
|
||||||
|
help="replace an existing output directory",
|
||||||
|
)
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||||
|
args = parse_args(argv)
|
||||||
|
try:
|
||||||
|
manifest = find_manifest(args.input)
|
||||||
|
output = args.output or manifest.parent / ".qa-images"
|
||||||
|
summary = export_images(args.input, output, args.keep_browser_raw, args.force)
|
||||||
|
except (ExportError, OSError, subprocess.SubprocessError) as exc:
|
||||||
|
print(f"open-kimi-ppt image export failed: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,662 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Export a PPTD project through Kimi's public browser-side PPTX writer.
|
||||||
|
|
||||||
|
The script uses a temporary localhost SDK host and agent-browser. It never uploads
|
||||||
|
the PPTD project as a document. Referenced remote resources may still be fetched by
|
||||||
|
the official editor. Local image files are exposed to the iframe as data URLs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
import zipfile
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError as exc: # pragma: no cover - environment diagnostic
|
||||||
|
raise SystemExit(
|
||||||
|
"PyYAML is required. Install it with: python3 -m pip install --user pyyaml"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
SKILL_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
HOST_TEMPLATE = Path(__file__).with_name("export_host.html")
|
||||||
|
IMAGE_MIME = {
|
||||||
|
".png": "image/png",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".gif": "image/gif",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
}
|
||||||
|
MAX_IMAGE_BYTES = 20 * 1024 * 1024
|
||||||
|
MAX_EMBEDDED_MEDIA_BYTES = 200 * 1024 * 1024
|
||||||
|
PPTX_CONTENT_TYPE = (
|
||||||
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"
|
||||||
|
)
|
||||||
|
FADE_TRANSITION_XML = (
|
||||||
|
'<p:transition spd="fast" advClick="1"><p:fade/></p:transition>'
|
||||||
|
)
|
||||||
|
MIN_AGENT_BROWSER_VERSION = (0, 33, 2)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class QuietHandler(SimpleHTTPRequestHandler):
|
||||||
|
def log_message(self, _format: str, *_args: Any) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def log(message: str) -> None:
|
||||||
|
print(f"[open-kimi-ppt] {message}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_version(output: str) -> Tuple[int, int, int]:
|
||||||
|
match = re.search(r"(\d+)\.(\d+)\.(\d+)\b", output)
|
||||||
|
if not match:
|
||||||
|
raise ExportError(f"could not parse agent-browser version from: {output.strip()}")
|
||||||
|
return tuple(int(part) for part in match.groups())
|
||||||
|
|
||||||
|
|
||||||
|
def read_agent_browser_version(executable: str) -> Tuple[int, int, int]:
|
||||||
|
process = subprocess.run(
|
||||||
|
[executable, "--version"],
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
if process.returncode != 0:
|
||||||
|
raise ExportError(f"agent-browser --version failed:\n{process.stdout[-2000:]}")
|
||||||
|
return parse_version(process.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_agent_browser() -> str:
|
||||||
|
executable = shutil.which("agent-browser")
|
||||||
|
version = read_agent_browser_version(executable) if executable else None
|
||||||
|
if version is not None and version >= MIN_AGENT_BROWSER_VERSION:
|
||||||
|
log(f"agent-browser version: {'.'.join(map(str, version))}")
|
||||||
|
return executable
|
||||||
|
|
||||||
|
npm = shutil.which("npm")
|
||||||
|
if not npm:
|
||||||
|
reason = "not installed" if version is None else ".".join(map(str, version))
|
||||||
|
raise ExportError(
|
||||||
|
f"agent-browser {reason}; npm is required to install agent-browser@latest"
|
||||||
|
)
|
||||||
|
|
||||||
|
current = "not installed" if version is None else ".".join(map(str, version))
|
||||||
|
minimum = ".".join(map(str, MIN_AGENT_BROWSER_VERSION))
|
||||||
|
log(f"agent-browser {current} is below {minimum}; installing agent-browser@latest")
|
||||||
|
process = subprocess.run(
|
||||||
|
[npm, "install", "-g", "agent-browser@latest"],
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
timeout=300,
|
||||||
|
)
|
||||||
|
if process.returncode != 0:
|
||||||
|
raise ExportError(f"failed to install agent-browser@latest:\n{process.stdout[-4000:]}")
|
||||||
|
|
||||||
|
executable = shutil.which("agent-browser")
|
||||||
|
if not executable:
|
||||||
|
raise ExportError("agent-browser@latest installed, but executable is not on PATH")
|
||||||
|
version = read_agent_browser_version(executable)
|
||||||
|
if version < MIN_AGENT_BROWSER_VERSION:
|
||||||
|
raise ExportError(
|
||||||
|
"agent-browser@latest is still below the required version "
|
||||||
|
f"{minimum}: {'.'.join(map(str, version))}"
|
||||||
|
)
|
||||||
|
log(f"agent-browser upgraded to {'.'.join(map(str, version))}")
|
||||||
|
return executable
|
||||||
|
|
||||||
|
|
||||||
|
def find_manifest(source: Path) -> Path:
|
||||||
|
source = source.expanduser().resolve()
|
||||||
|
if source.is_file():
|
||||||
|
if source.suffix.lower() != ".pptd":
|
||||||
|
raise ExportError(f"input must be a .pptd file or project directory: {source}")
|
||||||
|
return source
|
||||||
|
if not source.is_dir():
|
||||||
|
raise ExportError(f"input does not exist: {source}")
|
||||||
|
manifests = sorted(source.rglob("*.pptd"))
|
||||||
|
if not manifests:
|
||||||
|
raise ExportError(f"no .pptd manifest found under: {source}")
|
||||||
|
if len(manifests) > 1:
|
||||||
|
choices = "\n ".join(str(path) for path in manifests[:20])
|
||||||
|
raise ExportError(
|
||||||
|
"multiple .pptd manifests found; pass one manifest explicitly:\n " + choices
|
||||||
|
)
|
||||||
|
return manifests[0]
|
||||||
|
|
||||||
|
|
||||||
|
def read_yaml_mapping(path: Path) -> Tuple[str, Dict[str, Any]]:
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
try:
|
||||||
|
value = yaml.safe_load(text)
|
||||||
|
except yaml.YAMLError as exc:
|
||||||
|
raise ExportError(f"invalid YAML in {path}: {exc}") from exc
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise ExportError(f"expected a YAML mapping in {path}")
|
||||||
|
return text, value
|
||||||
|
|
||||||
|
|
||||||
|
def safe_project_path(root: Path, relative: str) -> Path:
|
||||||
|
if not isinstance(relative, str) or not relative.strip():
|
||||||
|
raise ExportError("page path must be a non-empty string")
|
||||||
|
candidate = (root / relative).resolve()
|
||||||
|
try:
|
||||||
|
candidate.relative_to(root)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ExportError(f"project path escapes the PPTD directory: {relative}") from exc
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
|
||||||
|
def build_image_map(root: Path) -> Dict[str, str]:
|
||||||
|
image_map: Dict[str, str] = {}
|
||||||
|
total = 0
|
||||||
|
for path in sorted(root.rglob("*")):
|
||||||
|
if not path.is_file() or path.suffix.lower() not in IMAGE_MIME:
|
||||||
|
continue
|
||||||
|
size = path.stat().st_size
|
||||||
|
if size > MAX_IMAGE_BYTES:
|
||||||
|
log(f"skip local image over 20 MiB: {path.relative_to(root)}")
|
||||||
|
continue
|
||||||
|
if total + size > MAX_EMBEDDED_MEDIA_BYTES:
|
||||||
|
raise ExportError(
|
||||||
|
"local image payload exceeds 200 MiB; reduce media size or use remote URLs"
|
||||||
|
)
|
||||||
|
data = base64.b64encode(path.read_bytes()).decode("ascii")
|
||||||
|
rel = path.relative_to(root).as_posix()
|
||||||
|
image_map[rel] = f"data:{IMAGE_MIME[path.suffix.lower()]};base64,{data}"
|
||||||
|
total += size
|
||||||
|
if image_map:
|
||||||
|
log(f"prepared {len(image_map)} local image resource(s), {total} bytes")
|
||||||
|
return image_map
|
||||||
|
|
||||||
|
|
||||||
|
def build_payload(manifest: Path) -> Dict[str, Any]:
|
||||||
|
manifest_text, manifest_data = read_yaml_mapping(manifest)
|
||||||
|
if manifest_data.get("version") != "v2":
|
||||||
|
raise ExportError("local PPTX export currently requires PPTD version: v2")
|
||||||
|
page_paths = manifest_data.get("pages")
|
||||||
|
if not isinstance(page_paths, list) or not page_paths:
|
||||||
|
raise ExportError("PPTD manifest must contain a non-empty pages list")
|
||||||
|
|
||||||
|
root = manifest.parent.resolve()
|
||||||
|
pages: List[Dict[str, str]] = []
|
||||||
|
for entry in page_paths:
|
||||||
|
page_path = safe_project_path(root, entry)
|
||||||
|
if not page_path.is_file():
|
||||||
|
raise ExportError(f"missing page file: {entry}")
|
||||||
|
page_text, page_data = read_yaml_mapping(page_path)
|
||||||
|
if not isinstance(page_data.get("elements"), list):
|
||||||
|
raise ExportError(f"page elements must be an array: {entry}")
|
||||||
|
pages.append({"path": str(entry), "content": page_text})
|
||||||
|
|
||||||
|
title = str(manifest_data.get("title") or manifest.stem)
|
||||||
|
return {
|
||||||
|
"id": f"local-export-{uuid.uuid4().hex}",
|
||||||
|
"title": title,
|
||||||
|
"manifestPath": manifest.name,
|
||||||
|
"manifestContent": manifest_text,
|
||||||
|
"pages": pages,
|
||||||
|
"imageMap": build_image_map(root),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def json_result(output: str) -> Dict[str, Any]:
|
||||||
|
for line in reversed(output.splitlines()):
|
||||||
|
line = line.strip()
|
||||||
|
if not line.startswith("{"):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
value = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return value
|
||||||
|
raise ExportError(f"agent-browser returned no JSON object:\n{output[-2000:]}")
|
||||||
|
|
||||||
|
|
||||||
|
class BrowserSession:
|
||||||
|
def __init__(self, executable: str, session: str, cwd: Path, download_dir: Path):
|
||||||
|
self.executable = executable
|
||||||
|
self.session = session
|
||||||
|
self.cwd = cwd
|
||||||
|
self.download_dir = download_dir
|
||||||
|
self.env = os.environ.copy()
|
||||||
|
self.env.setdefault("AGENT_BROWSER_DEFAULT_TIMEOUT", "60000")
|
||||||
|
self.env.setdefault("AGENT_BROWSER_IDLE_TIMEOUT_MS", "180000")
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self,
|
||||||
|
args: Sequence[str],
|
||||||
|
*,
|
||||||
|
timeout: int = 90,
|
||||||
|
check: bool = True,
|
||||||
|
) -> subprocess.CompletedProcess[str]:
|
||||||
|
command = [self.executable, "--session", self.session, *args]
|
||||||
|
process = subprocess.run(
|
||||||
|
command,
|
||||||
|
cwd=self.cwd,
|
||||||
|
env=self.env,
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
if check and process.returncode != 0:
|
||||||
|
raise ExportError(
|
||||||
|
f"agent-browser command failed ({process.returncode}): "
|
||||||
|
f"{' '.join(args)}\n{process.stdout[-4000:]}"
|
||||||
|
)
|
||||||
|
return process
|
||||||
|
|
||||||
|
def open(self, url: str) -> None:
|
||||||
|
self.run(
|
||||||
|
["--download-path", str(self.download_dir), "open", url], timeout=90
|
||||||
|
)
|
||||||
|
|
||||||
|
def snapshot(self) -> Dict[str, Any]:
|
||||||
|
process = self.run(["snapshot", "-i", "-C", "--json"])
|
||||||
|
return json_result(process.stdout)
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
self.run(["close"], timeout=20, check=False)
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot_data(snapshot: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
data = snapshot.get("data")
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ExportError(f"invalid agent-browser snapshot: {snapshot}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def ref_by_name(snapshot: Dict[str, Any], name: str, role: Optional[str] = None) -> str:
|
||||||
|
refs = snapshot_data(snapshot).get("refs")
|
||||||
|
if not isinstance(refs, dict):
|
||||||
|
raise ExportError("snapshot contains no interactive refs")
|
||||||
|
matches = []
|
||||||
|
for ref, metadata in refs.items():
|
||||||
|
if not isinstance(metadata, dict) or metadata.get("name") != name:
|
||||||
|
continue
|
||||||
|
if role is not None and str(metadata.get("role", "")).lower() != role.lower():
|
||||||
|
continue
|
||||||
|
matches.append(ref)
|
||||||
|
if not matches:
|
||||||
|
raise ExportError(f"could not find {role or 'element'} named {name!r}")
|
||||||
|
return matches[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def switch_state(snapshot: Dict[str, Any]) -> Optional[Tuple[str, bool, bool]]:
|
||||||
|
text = str(snapshot_data(snapshot).get("snapshot") or "")
|
||||||
|
match = re.search(r"switch \[(?P<attrs>[^\]]*?)ref=(?P<ref>e\d+)\]", text)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
attrs = match.group("attrs")
|
||||||
|
return match.group("ref"), "checked=true" in attrs, "disabled" in attrs
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_export_dialog(browser: BrowserSession, timeout: float = 20.0) -> Dict[str, Any]:
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
last: Optional[Dict[str, Any]] = None
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
last = browser.snapshot()
|
||||||
|
try:
|
||||||
|
ref_by_name(last, "下载", "button")
|
||||||
|
return last
|
||||||
|
except ExportError:
|
||||||
|
time.sleep(0.35)
|
||||||
|
raise ExportError(f"export dialog did not become ready: {last}")
|
||||||
|
|
||||||
|
|
||||||
|
def is_pptx(path: Path) -> bool:
|
||||||
|
if not path.is_file() or path.name.endswith(".crdownload"):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(path) as archive:
|
||||||
|
if "ppt/presentation.xml" not in archive.namelist():
|
||||||
|
return False
|
||||||
|
content_types = archive.read("[Content_Types].xml")
|
||||||
|
return PPTX_CONTENT_TYPE.encode("utf-8") in content_types
|
||||||
|
except (OSError, KeyError, zipfile.BadZipFile):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def find_download(
|
||||||
|
search_roots: Iterable[Path],
|
||||||
|
timeout: float = 150.0,
|
||||||
|
accept: Callable[[Path], bool] = is_pptx,
|
||||||
|
) -> Path:
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
last_sizes: Dict[Path, int] = {}
|
||||||
|
stable: Dict[Path, int] = {}
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
candidates: List[Path] = []
|
||||||
|
for root in search_roots:
|
||||||
|
if not root.exists():
|
||||||
|
continue
|
||||||
|
candidates.extend(path for path in root.rglob("*") if path.is_file())
|
||||||
|
for path in sorted(candidates, key=lambda item: item.stat().st_mtime, reverse=True):
|
||||||
|
try:
|
||||||
|
size = path.stat().st_size
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if size == last_sizes.get(path) and size > 0:
|
||||||
|
stable[path] = stable.get(path, 0) + 1
|
||||||
|
else:
|
||||||
|
stable[path] = 0
|
||||||
|
last_sizes[path] = size
|
||||||
|
if stable[path] >= 1 and accept(path):
|
||||||
|
return path
|
||||||
|
time.sleep(0.5)
|
||||||
|
visible = "\n ".join(str(path) for path in last_sizes) or "(none)"
|
||||||
|
raise ExportError(f"timed out waiting for download; observed files:\n {visible}")
|
||||||
|
|
||||||
|
|
||||||
|
def replace_transition(slide_xml: bytes, transition: str) -> bytes:
|
||||||
|
text = slide_xml.decode("utf-8")
|
||||||
|
pattern = re.compile(
|
||||||
|
r"<p:transition\b[^>]*(?:/>|>.*?</p:transition>)", re.DOTALL
|
||||||
|
)
|
||||||
|
text = pattern.sub("", text)
|
||||||
|
if transition == "none":
|
||||||
|
return text.encode("utf-8")
|
||||||
|
|
||||||
|
# CT_Slide requires transition as a direct child after cSld/clrMapOvr and
|
||||||
|
# before timing/extLst. Searching for the first p:extLst is incorrect:
|
||||||
|
# shapes may contain their own nested extLst inside cSld, causing Office to
|
||||||
|
# ignore a transition inserted there.
|
||||||
|
color_map = re.search(
|
||||||
|
r"<p:clrMapOvr\b[^>]*(?:/>|>.*?</p:clrMapOvr>)", text, re.DOTALL
|
||||||
|
)
|
||||||
|
common_slide = re.search(
|
||||||
|
r"<p:cSld\b[^>]*(?:/>|>.*?</p:cSld>)", text, re.DOTALL
|
||||||
|
)
|
||||||
|
anchor = color_map or common_slide
|
||||||
|
if anchor is None:
|
||||||
|
raise ExportError("slide XML has no cSld/clrMapOvr insertion anchor")
|
||||||
|
position = anchor.end()
|
||||||
|
return (text[:position] + FADE_TRANSITION_XML + text[position:]).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def root_child_names(slide_xml: bytes) -> List[str]:
|
||||||
|
try:
|
||||||
|
root = ET.fromstring(slide_xml)
|
||||||
|
except ET.ParseError as exc:
|
||||||
|
raise ExportError(f"invalid slide XML: {exc}") from exc
|
||||||
|
return [child.tag.rsplit("}", 1)[-1] for child in root]
|
||||||
|
|
||||||
|
|
||||||
|
def has_direct_fade_transition(slide_xml: bytes) -> bool:
|
||||||
|
try:
|
||||||
|
root = ET.fromstring(slide_xml)
|
||||||
|
except ET.ParseError as exc:
|
||||||
|
raise ExportError(f"invalid slide XML: {exc}") from exc
|
||||||
|
transition = next(
|
||||||
|
(child for child in root if child.tag.rsplit("}", 1)[-1] == "transition"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if transition is None:
|
||||||
|
return False
|
||||||
|
return any(child.tag.rsplit("}", 1)[-1] == "fade" for child in transition)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_transition_order(slide_xml: bytes, transition: str) -> None:
|
||||||
|
names = root_child_names(slide_xml)
|
||||||
|
transition_indexes = [index for index, name in enumerate(names) if name == "transition"]
|
||||||
|
if transition == "none":
|
||||||
|
if transition_indexes:
|
||||||
|
raise ExportError("transition=none left a root-level transition")
|
||||||
|
return
|
||||||
|
if len(transition_indexes) != 1 or not has_direct_fade_transition(slide_xml):
|
||||||
|
raise ExportError("slide does not contain exactly one root-level fade transition")
|
||||||
|
transition_index = transition_indexes[0]
|
||||||
|
for required_before in ("cSld", "clrMapOvr"):
|
||||||
|
if required_before in names and names.index(required_before) > transition_index:
|
||||||
|
raise ExportError(f"{required_before} appears after transition")
|
||||||
|
for required_after in ("timing", "extLst"):
|
||||||
|
if required_after in names and names.index(required_after) < transition_index:
|
||||||
|
raise ExportError(f"{required_after} appears before transition")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_transitions(pptx: Path, transition: str) -> int:
|
||||||
|
temporary = pptx.with_name(f".{pptx.name}.{uuid.uuid4().hex}.tmp")
|
||||||
|
slide_count = 0
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(pptx, "r") as source, zipfile.ZipFile(temporary, "w") as target:
|
||||||
|
target.comment = source.comment
|
||||||
|
for info in source.infolist():
|
||||||
|
data = source.read(info.filename)
|
||||||
|
if re.fullmatch(r"ppt/slides/slide\d+\.xml", info.filename):
|
||||||
|
data = replace_transition(data, transition)
|
||||||
|
slide_count += 1
|
||||||
|
target.writestr(info, data, compress_type=info.compress_type)
|
||||||
|
if slide_count == 0:
|
||||||
|
raise ExportError("exported PPTX contains no slide XML")
|
||||||
|
temporary.replace(pptx)
|
||||||
|
finally:
|
||||||
|
temporary.unlink(missing_ok=True)
|
||||||
|
return slide_count
|
||||||
|
|
||||||
|
|
||||||
|
def verify_output(pptx: Path, transition: str, expect_fonts: bool) -> Dict[str, Any]:
|
||||||
|
if not is_pptx(pptx):
|
||||||
|
raise ExportError(f"output is not a valid PPTX ZIP: {pptx}")
|
||||||
|
with zipfile.ZipFile(pptx) as archive:
|
||||||
|
broken = archive.testzip()
|
||||||
|
if broken:
|
||||||
|
raise ExportError(f"PPTX CRC check failed at: {broken}")
|
||||||
|
slide_names = [
|
||||||
|
name
|
||||||
|
for name in archive.namelist()
|
||||||
|
if re.fullmatch(r"ppt/slides/slide\d+\.xml", name)
|
||||||
|
]
|
||||||
|
slide_xml = {name: archive.read(name) for name in slide_names}
|
||||||
|
for data in slide_xml.values():
|
||||||
|
validate_transition_order(data, transition)
|
||||||
|
transition_hits = sum(has_direct_fade_transition(data) for data in slide_xml.values())
|
||||||
|
if transition == "fade" and transition_hits != len(slide_names):
|
||||||
|
raise ExportError("fade transition was not written to every slide")
|
||||||
|
fonts = [
|
||||||
|
name
|
||||||
|
for name in archive.namelist()
|
||||||
|
if name.startswith("ppt/fonts/") and not name.endswith("/")
|
||||||
|
]
|
||||||
|
if expect_fonts and not fonts:
|
||||||
|
log(
|
||||||
|
"warning: embed-fonts was enabled, but the official writer produced no font part"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"slides": len(slide_names),
|
||||||
|
"fadeTransitions": transition_hits,
|
||||||
|
"fontParts": len(fonts),
|
||||||
|
"bytes": pptx.stat().st_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def serve(directory: Path) -> Tuple[ThreadingHTTPServer, threading.Thread, str]:
|
||||||
|
handler = lambda *args, **kwargs: QuietHandler( # noqa: E731
|
||||||
|
*args, directory=str(directory), **kwargs
|
||||||
|
)
|
||||||
|
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
||||||
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
host, port = server.server_address
|
||||||
|
return server, thread, f"http://{host}:{port}/export_host.html"
|
||||||
|
|
||||||
|
|
||||||
|
def export_pptx(
|
||||||
|
source: Path,
|
||||||
|
output: Path,
|
||||||
|
transition: str,
|
||||||
|
embed_fonts: bool,
|
||||||
|
keep_download: bool = False,
|
||||||
|
force: bool = False,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
manifest = find_manifest(source)
|
||||||
|
payload = build_payload(manifest)
|
||||||
|
output = output.expanduser().resolve()
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if output.exists() and not force:
|
||||||
|
raise ExportError(f"output already exists (pass --force to replace it): {output}")
|
||||||
|
agent_browser = ensure_agent_browser()
|
||||||
|
|
||||||
|
log(f"manifest: {manifest}")
|
||||||
|
log(
|
||||||
|
f"defaults: transition={transition}, embed_fonts={'on' if embed_fonts else 'off'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="open-kimi-ppt-export-") as temp_name:
|
||||||
|
temp_dir = Path(temp_name)
|
||||||
|
download_dir = temp_dir / "downloads"
|
||||||
|
download_dir.mkdir()
|
||||||
|
shutil.copy2(HOST_TEMPLATE, temp_dir / HOST_TEMPLATE.name)
|
||||||
|
(temp_dir / "payload.json").write_text(
|
||||||
|
json.dumps(payload, ensure_ascii=False), encoding="utf-8"
|
||||||
|
)
|
||||||
|
server, thread, url = serve(temp_dir)
|
||||||
|
session = f"open-kimi-ppt-export-{os.getpid()}-{uuid.uuid4().hex[:8]}"
|
||||||
|
browser = BrowserSession(agent_browser, session, temp_dir, download_dir)
|
||||||
|
try:
|
||||||
|
log("opening the public Kimi slide editor")
|
||||||
|
browser.open(url)
|
||||||
|
browser.run(
|
||||||
|
[
|
||||||
|
"wait",
|
||||||
|
"--fn",
|
||||||
|
'document.documentElement.dataset.deckStatus === "ready"',
|
||||||
|
],
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
browser.run(["set", "viewport", "1280", "720"])
|
||||||
|
snapshot = browser.snapshot()
|
||||||
|
export_ref = ref_by_name(snapshot, "导出", "button")
|
||||||
|
browser.run(["click", f"@{export_ref}"])
|
||||||
|
dialog = wait_for_export_dialog(browser)
|
||||||
|
|
||||||
|
state = switch_state(dialog)
|
||||||
|
if state is not None:
|
||||||
|
switch_ref, checked, disabled = state
|
||||||
|
if disabled and checked != embed_fonts:
|
||||||
|
log("warning: the official font switch is disabled for this deck")
|
||||||
|
elif checked != embed_fonts:
|
||||||
|
browser.run(["click", f"@{switch_ref}"])
|
||||||
|
dialog = wait_for_export_dialog(browser)
|
||||||
|
elif embed_fonts:
|
||||||
|
log("warning: the official export dialog exposed no font switch")
|
||||||
|
|
||||||
|
download_ref = ref_by_name(dialog, "下载", "button")
|
||||||
|
log("generating PPTX in the browser")
|
||||||
|
result = browser.run(
|
||||||
|
["download", f"@{download_ref}", str(temp_dir / "browser-output.pptx")],
|
||||||
|
timeout=180,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
log("download capture reported a timeout; checking browser output files")
|
||||||
|
downloaded = find_download((download_dir, temp_dir), timeout=90)
|
||||||
|
shutil.copy2(downloaded, output)
|
||||||
|
if keep_download:
|
||||||
|
debug_copy = output.with_name(f"{output.stem}.browser-raw.pptx")
|
||||||
|
if debug_copy.exists() and not force:
|
||||||
|
raise ExportError(
|
||||||
|
f"raw debug output already exists (pass --force): {debug_copy}"
|
||||||
|
)
|
||||||
|
shutil.copy2(downloaded, debug_copy)
|
||||||
|
finally:
|
||||||
|
browser.close()
|
||||||
|
server.shutdown()
|
||||||
|
server.server_close()
|
||||||
|
thread.join(timeout=2)
|
||||||
|
|
||||||
|
slide_count = patch_transitions(output, transition)
|
||||||
|
summary = verify_output(output, transition, embed_fonts)
|
||||||
|
summary["transitionPatchedSlides"] = slide_count
|
||||||
|
summary["output"] = str(output)
|
||||||
|
return summary
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=(
|
||||||
|
"Export a PPTD project to PPTX using Kimi's public browser-side writer. "
|
||||||
|
"Defaults: fade transition and embedded fonts."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parser.add_argument("input", type=Path, help=".pptd manifest or project directory")
|
||||||
|
parser.add_argument("--output", "-o", type=Path, help="output .pptx path")
|
||||||
|
parser.add_argument(
|
||||||
|
"--transition",
|
||||||
|
choices=("fade", "none"),
|
||||||
|
default="fade",
|
||||||
|
help="slide transition written to every slide (default: fade)",
|
||||||
|
)
|
||||||
|
font_group = parser.add_mutually_exclusive_group()
|
||||||
|
font_group.add_argument(
|
||||||
|
"--embed-fonts",
|
||||||
|
dest="embed_fonts",
|
||||||
|
action="store_true",
|
||||||
|
default=True,
|
||||||
|
help="embed fonts when available (default)",
|
||||||
|
)
|
||||||
|
font_group.add_argument(
|
||||||
|
"--no-embed-fonts",
|
||||||
|
dest="embed_fonts",
|
||||||
|
action="store_false",
|
||||||
|
help="disable font embedding",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--keep-browser-raw",
|
||||||
|
action="store_true",
|
||||||
|
help="also keep the unpatched browser download beside the output",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--force",
|
||||||
|
action="store_true",
|
||||||
|
help="replace an existing output file",
|
||||||
|
)
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||||
|
args = parse_args(argv)
|
||||||
|
try:
|
||||||
|
manifest = find_manifest(args.input)
|
||||||
|
output = args.output or manifest.with_suffix(".pptx")
|
||||||
|
summary = export_pptx(
|
||||||
|
args.input,
|
||||||
|
output,
|
||||||
|
args.transition,
|
||||||
|
args.embed_fonts,
|
||||||
|
args.keep_browser_raw,
|
||||||
|
args.force,
|
||||||
|
)
|
||||||
|
except (ExportError, OSError, subprocess.SubprocessError) as exc:
|
||||||
|
print(f"open-kimi-ppt export failed: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
version: v2
|
||||||
|
title: Open Kimi PPT Local Export Test
|
||||||
|
size: [960, 540]
|
||||||
|
pages:
|
||||||
|
- pages/01.page
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
pageType: content
|
||||||
|
background: {type: solid, color: "#F7F8FC"}
|
||||||
|
elements:
|
||||||
|
- elementId: title
|
||||||
|
elementType: text
|
||||||
|
bounds: [100, 180, 760, 120]
|
||||||
|
content:
|
||||||
|
align: [center, middle]
|
||||||
|
fontFamily: MiSans
|
||||||
|
text: |
|
||||||
|
<p><span style="font-size:42px;color:#171923;font-weight:700">本地导出验证</span></p>
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPTS_DIR = Path(__file__).resolve().parents[1] / "scripts"
|
||||||
|
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||||
|
|
||||||
|
SPEC = importlib.util.spec_from_file_location("export_images", SCRIPTS_DIR / "export_images.py")
|
||||||
|
MODULE = importlib.util.module_from_spec(SPEC)
|
||||||
|
assert SPEC and SPEC.loader
|
||||||
|
SPEC.loader.exec_module(MODULE)
|
||||||
|
|
||||||
|
|
||||||
|
def make_images_zip(path: Path, names=("1.jpeg", "10.jpeg", "2.jpeg")) -> None:
|
||||||
|
# 1x1 white JPEG, the smallest valid payload Pillow can open.
|
||||||
|
import base64
|
||||||
|
|
||||||
|
pixel = base64.b64decode(
|
||||||
|
"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////"
|
||||||
|
"////////////////////////////////////////////2wBDAf//////////////////"
|
||||||
|
"////////////////////////////////////////////wAARCAABAAEDASIAAhEBAxEB"
|
||||||
|
"/8QAFQABAQAAAAAAAAAAAAAAAAAAAAX/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oADAMB"
|
||||||
|
"AAIQAxAAAAGf/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABBQJ//8QAFBEBAAAA"
|
||||||
|
"AAAAAAAAAAAAAAAAAP/aAAgBAwEBPwF//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgB"
|
||||||
|
"AgEBPwF//8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQAGPwJ//8QAFBABAAAAAAAA"
|
||||||
|
"AAAAAAAAAAAAAP/aAAgBAQABPyF//9oADAMBAAIAAwAAABCf/8QAFBEBAAAAAAAAAAAA"
|
||||||
|
"AAAAAAAAAP/aAAgBAwEPEBB//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEPEBB/"
|
||||||
|
"/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABPxB//9k="
|
||||||
|
)
|
||||||
|
with zipfile.ZipFile(path, "w") as archive:
|
||||||
|
for name in names:
|
||||||
|
archive.writestr(name, pixel)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportImagesTests(unittest.TestCase):
|
||||||
|
def test_page_sort_key_orders_numeric_stems(self):
|
||||||
|
paths = [Path("10.jpeg"), Path("2.jpeg"), Path("cover.jpeg"), Path("1.jpeg")]
|
||||||
|
ordered = sorted(paths, key=MODULE.page_sort_key)
|
||||||
|
self.assertEqual(
|
||||||
|
[path.name for path in ordered],
|
||||||
|
["1.jpeg", "2.jpeg", "10.jpeg", "cover.jpeg"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_is_image_zip_accepts_image_entries_only(self):
|
||||||
|
with tempfile.TemporaryDirectory() as name:
|
||||||
|
root = Path(name)
|
||||||
|
good = root / "images.zip"
|
||||||
|
make_images_zip(good)
|
||||||
|
self.assertTrue(MODULE.is_image_zip(good))
|
||||||
|
|
||||||
|
bad = root / "text.zip"
|
||||||
|
with zipfile.ZipFile(bad, "w") as archive:
|
||||||
|
archive.writestr("readme.txt", "hello")
|
||||||
|
self.assertFalse(MODULE.is_image_zip(bad))
|
||||||
|
self.assertFalse(MODULE.is_image_zip(root / "missing.zip"))
|
||||||
|
|
||||||
|
def test_unzip_images_flattens_and_sorts(self):
|
||||||
|
with tempfile.TemporaryDirectory() as name:
|
||||||
|
root = Path(name)
|
||||||
|
archive_path = root / "images.zip"
|
||||||
|
make_images_zip(archive_path, names=("1.jpeg", "10.jpeg", "2.jpeg", "note.txt"))
|
||||||
|
images = MODULE.unzip_images(archive_path, root / "pages")
|
||||||
|
self.assertEqual(
|
||||||
|
[path.name for path in images], ["1.jpeg", "2.jpeg", "10.jpeg"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stitch_overview_grid(self):
|
||||||
|
try:
|
||||||
|
image_cls, draw_cls, image_font = MODULE.ensure_pillow()
|
||||||
|
except MODULE.ExportError:
|
||||||
|
self.skipTest("Pillow is not available")
|
||||||
|
with tempfile.TemporaryDirectory() as name:
|
||||||
|
root = Path(name)
|
||||||
|
images = []
|
||||||
|
for index in range(1, 5):
|
||||||
|
path = root / f"{index}.jpeg"
|
||||||
|
image = image_cls.new("RGB", (320, 180), (index * 40 % 255, 30, 60))
|
||||||
|
image.save(path, "JPEG")
|
||||||
|
images.append(path)
|
||||||
|
overview = MODULE.stitch_overview(
|
||||||
|
images, root / "overview.jpg", image_cls, draw_cls, image_font
|
||||||
|
)
|
||||||
|
self.assertTrue(overview.is_file())
|
||||||
|
with image_cls.open(overview) as result:
|
||||||
|
self.assertEqual(
|
||||||
|
result.width,
|
||||||
|
3 * MODULE.OVERVIEW_THUMB_WIDTH + 4 * MODULE.OVERVIEW_GAP,
|
||||||
|
)
|
||||||
|
rows = 2
|
||||||
|
cell = MODULE.OVERVIEW_LABEL_HEIGHT + 360
|
||||||
|
self.assertEqual(result.height, rows * cell + (rows + 1) * MODULE.OVERVIEW_GAP)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import importlib.util
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "export_pptx.py"
|
||||||
|
SPEC = importlib.util.spec_from_file_location("export_pptx", SCRIPT)
|
||||||
|
MODULE = importlib.util.module_from_spec(SPEC)
|
||||||
|
assert SPEC and SPEC.loader
|
||||||
|
SPEC.loader.exec_module(MODULE)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportPptxTests(unittest.TestCase):
|
||||||
|
def test_parse_agent_browser_version(self):
|
||||||
|
self.assertEqual(MODULE.parse_version("agent-browser 0.33.2"), (0, 33, 2))
|
||||||
|
self.assertEqual(MODULE.parse_version("v1.4.0-beta.1"), (1, 4, 0))
|
||||||
|
|
||||||
|
@patch.object(MODULE.subprocess, "run")
|
||||||
|
@patch.object(MODULE.shutil, "which")
|
||||||
|
def test_old_agent_browser_is_upgraded(self, which, run):
|
||||||
|
which.side_effect = ["/bin/agent-browser", "/bin/npm", "/bin/agent-browser"]
|
||||||
|
run.side_effect = [
|
||||||
|
MODULE.subprocess.CompletedProcess([], 0, "agent-browser 0.17.1\n"),
|
||||||
|
MODULE.subprocess.CompletedProcess([], 0, "changed 1 package\n"),
|
||||||
|
MODULE.subprocess.CompletedProcess([], 0, "agent-browser 0.33.2\n"),
|
||||||
|
]
|
||||||
|
self.assertEqual(MODULE.ensure_agent_browser(), "/bin/agent-browser")
|
||||||
|
self.assertEqual(run.call_args_list[1].args[0], [
|
||||||
|
"/bin/npm", "install", "-g", "agent-browser@latest"
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_fade_is_inserted_before_timing(self):
|
||||||
|
source = (
|
||||||
|
b'<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
|
b'<p:sld xmlns:p="urn:test"><p:cSld><p:spTree><p:extLst/>'
|
||||||
|
b'</p:spTree></p:cSld><p:clrMapOvr/><p:timing/><p:extLst/></p:sld>'
|
||||||
|
)
|
||||||
|
result_bytes = MODULE.replace_transition(source, "fade")
|
||||||
|
result = result_bytes.decode("utf-8")
|
||||||
|
self.assertIn("<p:transition", result)
|
||||||
|
self.assertIn("<p:fade/>", result)
|
||||||
|
self.assertGreater(result.index("<p:transition"), result.index("<p:clrMapOvr"))
|
||||||
|
self.assertLess(result.index("<p:transition"), result.index("<p:timing"))
|
||||||
|
MODULE.validate_transition_order(result_bytes, "fade")
|
||||||
|
|
||||||
|
def test_existing_transition_is_replaced_or_removed(self):
|
||||||
|
source = (
|
||||||
|
b'<p:sld xmlns:p="urn:test"><p:cSld/>'
|
||||||
|
b'<p:transition><p:wipe/></p:transition><p:extLst/></p:sld>'
|
||||||
|
)
|
||||||
|
faded = MODULE.replace_transition(source, "fade").decode("utf-8")
|
||||||
|
self.assertNotIn("p:wipe", faded)
|
||||||
|
self.assertEqual(faded.count("<p:transition"), 1)
|
||||||
|
MODULE.validate_transition_order(faded.encode("utf-8"), "fade")
|
||||||
|
cleared = MODULE.replace_transition(source, "none").decode("utf-8")
|
||||||
|
self.assertNotIn("p:transition", cleared)
|
||||||
|
MODULE.validate_transition_order(cleared.encode("utf-8"), "none")
|
||||||
|
|
||||||
|
def test_nested_transition_is_relocated_to_slide_root(self):
|
||||||
|
source = (
|
||||||
|
b'<p:sld xmlns:p="urn:test"><p:cSld><p:spTree>'
|
||||||
|
b'<p:transition><p:fade/></p:transition><p:extLst/>'
|
||||||
|
b'</p:spTree></p:cSld><p:clrMapOvr/><p:extLst/></p:sld>'
|
||||||
|
)
|
||||||
|
result = MODULE.replace_transition(source, "fade")
|
||||||
|
MODULE.validate_transition_order(result, "fade")
|
||||||
|
self.assertEqual(MODULE.root_child_names(result), [
|
||||||
|
"cSld", "clrMapOvr", "transition", "extLst"
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_patch_transitions_preserves_a_valid_zip(self):
|
||||||
|
with tempfile.TemporaryDirectory() as name:
|
||||||
|
deck = Path(name) / "test.pptx"
|
||||||
|
with zipfile.ZipFile(deck, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||||
|
archive.writestr(
|
||||||
|
"[Content_Types].xml",
|
||||||
|
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
|
||||||
|
'<Override PartName="/ppt/presentation.xml" '
|
||||||
|
f'ContentType="{MODULE.PPTX_CONTENT_TYPE}"/></Types>',
|
||||||
|
)
|
||||||
|
archive.writestr("ppt/presentation.xml", "<p:presentation xmlns:p=\"urn:test\"/>")
|
||||||
|
archive.writestr(
|
||||||
|
"ppt/slides/slide1.xml",
|
||||||
|
'<p:sld xmlns:p="urn:test"><p:cSld/></p:sld>',
|
||||||
|
)
|
||||||
|
self.assertEqual(MODULE.patch_transitions(deck, "fade"), 1)
|
||||||
|
with zipfile.ZipFile(deck) as archive:
|
||||||
|
self.assertIsNone(archive.testzip())
|
||||||
|
slide = archive.read("ppt/slides/slide1.xml")
|
||||||
|
self.assertIn(b"<p:fade/>", slide)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import {
|
||||||
|
assertWritableChangePath,
|
||||||
|
extractPagePaths,
|
||||||
|
joinDeckPath,
|
||||||
|
normalizeRelativePath,
|
||||||
|
titleFromManifest,
|
||||||
|
} from "../editor/lib.js";
|
||||||
|
|
||||||
|
const yaml = `---
|
||||||
|
version: v2
|
||||||
|
title: "测试文稿"
|
||||||
|
pages:
|
||||||
|
- pages/01.page
|
||||||
|
- 'pages/02.page' # comment
|
||||||
|
theme:
|
||||||
|
colors: {}
|
||||||
|
`;
|
||||||
|
|
||||||
|
test("parses PPTD manifests and keeps file access inside the project", () => {
|
||||||
|
assert.deepEqual(extractPagePaths(yaml), ["pages/01.page", "pages/02.page"]);
|
||||||
|
assert.deepEqual(extractPagePaths('{"pages":["a.page","folder/b.page"]}'), ["a.page", "folder/b.page"]);
|
||||||
|
assert.equal(titleFromManifest(yaml, "fallback"), "测试文稿");
|
||||||
|
assert.equal(joinDeckPath("deck", "pages/01.page"), "deck/pages/01.page");
|
||||||
|
assert.equal(joinDeckPath("deck", "deck/pages/01.page"), "deck/pages/01.page");
|
||||||
|
assert.equal(assertWritableChangePath("deck", "pages/01.page"), "deck/pages/01.page");
|
||||||
|
assert.throws(() => normalizeRelativePath("../secret"), /不允许越过/);
|
||||||
|
assert.throws(() => normalizeRelativePath("/absolute/path"), /绝对路径/);
|
||||||
|
assert.throws(() => assertWritableChangePath("deck", "media/image.png"), /仅允许编辑/);
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { once } from "node:events";
|
||||||
|
import test from "node:test";
|
||||||
|
import { createEditorServer } from "../lib/editor-server.js";
|
||||||
|
|
||||||
|
async function withServer(callback) {
|
||||||
|
const server = createEditorServer();
|
||||||
|
server.listen(0, "127.0.0.1");
|
||||||
|
await once(server, "listening");
|
||||||
|
const address = server.address();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await callback(`http://127.0.0.1:${address.port}`);
|
||||||
|
} finally {
|
||||||
|
server.close();
|
||||||
|
await once(server, "close");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("serves the PPTD editor and its JavaScript modules", async () => {
|
||||||
|
await withServer(async (url) => {
|
||||||
|
const index = await fetch(`${url}/`);
|
||||||
|
assert.equal(index.status, 200);
|
||||||
|
assert.match(index.headers.get("content-type"), /^text\/html/);
|
||||||
|
assert.match(await index.text(), /打开 PPTD 文件夹/);
|
||||||
|
|
||||||
|
const app = await fetch(`${url}/app.js`);
|
||||||
|
assert.equal(app.status, 200);
|
||||||
|
assert.match(app.headers.get("content-type"), /^text\/javascript/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 404 for files outside the packaged editor", async () => {
|
||||||
|
await withServer(async (url) => {
|
||||||
|
const response = await fetch(`${url}/missing.js`);
|
||||||
|
assert.equal(response.status, 404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("supports HEAD requests without a response body", async () => {
|
||||||
|
await withServer(async (url) => {
|
||||||
|
const response = await fetch(`${url}/styles.css`, { method: "HEAD" });
|
||||||
|
assert.equal(response.status, 200);
|
||||||
|
assert.equal(await response.text(), "");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import test from "node:test";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const cli = join(projectRoot, "bin", "open-kimi-ppt-skills.js");
|
||||||
|
|
||||||
|
function runCli(args, env = process.env) {
|
||||||
|
return spawnSync(process.execPath, [cli, ...args], {
|
||||||
|
encoding: "utf8",
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("installs the packaged skill into a custom skills directory", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "open-kimi-ppt-test-"));
|
||||||
|
const target = join(root, "skills");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = runCli(["install", "--target", target]);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(existsSync(join(target, "open-kimi-ppt", "SKILL.md")), true);
|
||||||
|
assert.equal(existsSync(join(target, "open-kimi-ppt", "scripts", "export_pptx.py")), true);
|
||||||
|
assert.equal(existsSync(join(target, "open-kimi-ppt", "_user_meta.json")), false);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("installs into ~/.agents/skills when no target is provided", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "open-kimi-ppt-test-"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = runCli([], { ...process.env, HOME: root, USERPROFILE: root, CODEX_HOME: undefined });
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(existsSync(join(root, ".agents", "skills", "open-kimi-ppt", "SKILL.md")), true);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("refuses to overwrite an existing installation without --force", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "open-kimi-ppt-test-"));
|
||||||
|
const target = join(root, "skills");
|
||||||
|
|
||||||
|
try {
|
||||||
|
assert.equal(runCli(["--target", target]).status, 0);
|
||||||
|
const secondInstall = runCli(["--target", target]);
|
||||||
|
assert.equal(secondInstall.status, 1);
|
||||||
|
assert.match(secondInstall.stderr, /already exists/);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("replaces an existing installation with --force", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "open-kimi-ppt-test-"));
|
||||||
|
const target = join(root, "skills");
|
||||||
|
const skillFile = join(target, "open-kimi-ppt", "SKILL.md");
|
||||||
|
|
||||||
|
try {
|
||||||
|
assert.equal(runCli(["--target", target]).status, 0);
|
||||||
|
writeFileSync(skillFile, "modified", "utf8");
|
||||||
|
|
||||||
|
const result = runCli(["--target", target, "--force"]);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.match(readFileSync(skillFile, "utf8"), /^---\nname: open-kimi-ppt/m);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||