init: huashu-design skill v1.0

花叔设计工作台 —— AI 驱动的 HTML 设计工作流,支持 Claude Code / Codex /
Cursor / Trae / OpenClaw / Hermes Agent 等任意 skill-compatible agent。

四条主干能力:
- HTML 高保真视觉产出(原型/App/幻灯片/动画/变体/信息图)
- 设计方向顾问 Fallback(5 流派 × 20 设计哲学 × 24 预制 showcase)
- Junior Designer 工作流(先 show 假设再迭代)
- 专家级 5 维度评审(哲学/层级/执行/功能/创新)

工程工具链:
- HTML → 25fps/60fps MP4 + palette GIF,含 6 首场景化 BGM
- HTML → PDF / PPTX 导出
- Playwright 截图 + 控制台错误验证
- iOS/Android/macOS/Browser 设备边框 + Stage/Sprite 动画引擎

初版包含 SKILL.md + 13 份 references + 7 个 scripts + 18 类 assets。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
alchainandClaude Opus 4.7 committed 2026-04-19 22:41:51 +08:00
commit 7d7bab4a82
89 files changed
+19270

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
# OS
.DS_Store
Thumbs.db
# Editor
.vscode/
.idea/
*.swp
*~
# Node
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Python
__pycache__/
*.pyc
.venv/
venv/
# Backups
*.bak
*.bak.*
# Video render temp dirs
.video-tmp-*
*.tmp.mp4
*.tmp.webm
# Private overrides (never commit real personal data)
personal-asset-index.json
assets/personal-asset-index.json
# Logs
*.log
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 alchaincyf (花叔)
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.
+148
View File
@@ -0,0 +1,148 @@
# huashu-design
花叔设计工作台 —— 一个把「AI 当设计师」落到实处的 Skill。
不是让 AI 生成一张图,而是让 AI 进入「资深 Junior Designer」的工作模式:先给假设 + 占位 + 理由,show 给 manager 看,得到方向反馈,再做 variation,再迭代细节。最后还能请一位「严苛评审」打分。
适用于任何支持 Skill 规范的 AI agent:Claude Code、Codex、Cursor、Trae、OpenClaw、Hermes Agent 等。
---
## 它能做什么
一条 skill 同时覆盖四件事:
1. **高保真 HTML 视觉产出**
交互原型、App / iOS mockup、幻灯片、动画 Demo、设计变体探索、信息图 —— 用 HTML 做,而不是用 Figma 截图或 placeholder 拼贴。
2. **设计方向顾问(Fallback)**
当你的需求只是「给我做个好看的页面」时,skill 会从 5 个流派、20 种设计哲学里推荐 3 个差异化方向(信息建筑派 / 运动诗学派 / 极简主义派 / 实验先锋派 / 东方哲学派),再并行生成 3 个视觉 Demo 让你挑。
3. **Junior Designer 工作流**
不直接闷头做大招。先写 assumptions + reasoning + placeholder,尽早 show;方向错了,晚改比早改贵 100 倍。
4. **专家级评审**
交付后可选一轮 5 维度评分(哲学一致性 / 视觉层级 / 细节执行 / 功能性 / 创新性),外加具体的 Quick Fixes 清单。
---
## 附带的工程工具链
- **Starter Components**:iOS / Android / macOS / Browser 的设备边框,design_canvas(变体画布),animations.jsx(Stage + Sprite 时间轴引擎),deck_stage.js(幻灯片外壳)。
- **视频导出**:HTML 动画一键导出 25fps MP4 → 60fps 插帧 MP4 → palette 优化 GIF,配 6 首场景化 BGM(科技 / 广告 / 教育 / 教程)自动 fade。
- **幻灯片导出**:HTML → PDF(矢量)/ PPTX(图片铺底)。
- **Playwright 验证**:截图 + 控制台错误检查,交付前跑一遍。
---
## 安装
把整个目录放到你 agent 读取 skills 的位置即可。
**Claude Code**:
```bash
git clone https://github.com/alchaincyf/huashu-design-skill.git ~/.claude/skills/huashu-design
```
**其他 agent**(Codex / Cursor / Trae / OpenClaw / Hermes Agent 等):按各自的 skills 约定放置,SKILL.md 的 frontmatter + markdown 结构是通用的。
安装完成后,用任意包含触发词的话开启:
> 「做个 iOS App 原型」「帮我做 pitch deck」「导出 MP4」「评审这个设计」「做个好看的页面(我没想法)」
触发词完整清单见 `SKILL.md` frontmatter 的 description 字段。
---
## 使用前需要自己配置的东西
大部分能力开箱即用,下面几项在需要时再弄:
| 能力 | 需要 |
|------|------|
| 视频导出 | `npm i -g playwright` + `brew install ffmpeg`(或等价方案) |
| 幻灯片导出 PDF | `npm i -g playwright pdf-lib` |
| 幻灯片导出 PPTX | `npm i -g playwright pptxgenjs` |
| Playwright 截图验证 | `npm i -g playwright` |
| 个人品牌锚定(可选) | 复制 `assets/personal-asset-index.example.json` 到你的 agent 私有 memory 目录并填真实信息 |
---
## 目录结构
```
huashu-design-skill/
├── SKILL.md # 主干文件:哲学 + 工作流 + 路由表
├── references/ # 深度参考:按需加载
│ ├── workflow.md # 开工问什么问题
│ ├── content-guidelines.md # 反 AI slop 完整清单
│ ├── react-setup.md # React + Babel 最佳实践
│ ├── slide-decks.md # 幻灯片架构
│ ├── animations.md # 动画引擎用法
│ ├── animation-pitfalls.md # 动画踩坑 + T0 黑屏修复
│ ├── tweaks-system.md # 实时调参 UI
│ ├── design-context.md # 没有 design system 怎么办
│ ├── design-styles.md # 20 种设计哲学
│ ├── scene-templates.md # 按输出类型的场景模板
│ ├── critique-guide.md # 评审打分标准
│ ├── verification.md # 产出验证流程
│ └── video-export.md # 视频导出命令链
├── scripts/ # 可执行脚本
│ ├── render-video.js # HTML → 25fps MP4
│ ├── convert-formats.sh # 25fps MP4 → 60fps MP4 + GIF
│ ├── add-music.sh # BGM 叠加 + 自动 fade
│ ├── export_deck_pdf.mjs # HTML → PDF
│ ├── export_deck_pptx.mjs # HTML → PPTX
│ └── verify.py # Playwright 截图 + 控制台检查
└── assets/ # 起手组件 + BGM + 示例
├── deck_stage.js # 幻灯片外壳(单文件架构)
├── deck_index.html # 幻灯片拼接器(多文件架构)
├── animations.jsx # Stage + Sprite 时间轴引擎
├── design_canvas.jsx # 变体画布
├── ios_frame.jsx # iPhone 设备边框
├── android_frame.jsx # Android 设备边框
├── macos_window.jsx # macOS 窗口 chrome
├── browser_window.jsx # 浏览器 chrome
├── bgm-{tech,ad,educational,tutorial,*-alt}.mp3
├── personal-asset-index.example.json
└── showcases/ # 24 个预制 showcase(8 场景 × 3 风格)
```
---
## 设计原则(写进 DNA 的五条)
1. **从 existing context 出发**,不凭空画 hi-fi。没有 design system / brand / Figma 就先找,真没有就明确告知「我会基于通用直觉做」。
2. **Junior Designer 模式**:先 show 假设,再执行。不要一头扎进去闷头做大招。
3. **给 variations,不给最终答案**。3+ 个变体,跨视觉 / 交互 / 色彩 / 布局 / 动画维度递进,让用户 mix and match。
4. **Placeholder > 烂实现**。没图标就留灰色方块 + 文字标签,不画烂 SVG;没数据不编造假数据。
5. **反 AI slop**:激进渐变、满屏 emoji、圆角卡片 + 左 accent border、SVG 画人画物、Inter / Roboto 填所有文字 —— 每一条都是默认避开项(用户可按品牌 override)。
完整哲学和工作流见 `SKILL.md`。
---
## BGM 版权说明
`assets/bgm-*.mp3` 为原创 / 授权音乐,仅限配合本 skill 生成的演示 / 宣传视频使用。商业投放前请确认你的素材来源许可;若替换成自己的 BGM,只需保留相同文件名或改脚本参数。
---
## License
MIT。自由 fork、魔改、开分支。PR 欢迎,尤其欢迎:
- 新的设计哲学流派(在 `references/design-styles.md` 里)
- 新的 scene template(在 `references/scene-templates.md` 里)
- 新的 showcase(在 `assets/showcases/` 里)
- 其他 agent 环境的适配笔记(尤其欢迎 Codex / Cursor / Trae 的使用报告)
---
## 致谢
- Skill 规范:Anthropic Claude Code
- 设计哲学蒸馏:Pentagram、Field.io、Kenya Hara、Stefan Sagmeister、Dieter Rams、Massimo Vignelli 等 20 位设计师 / 机构
- 工程实践:Remotion / After Effects(Stage + Sprite 思路)、Playwright(验证 + 录制)、ffmpeg(视频后处理)
- 灵感来源:AI Artifacts 原生设计能力 → 搬到 Skill 架构 → 跨 agent 可用
+445
View File
@@ -0,0 +1,445 @@
---
name: huashu-design
description: 花叔设计工作台——用HTML做高保真原型、交互Demo、幻灯片、动画、设计变体探索+设计方向顾问+专家评审的一体化设计能力。HTML是工具不是媒介,根据任务embody不同专家(UX设计师/动画师/幻灯片设计师/原型师),避免web design tropes。触发词:做原型、设计Demo、交互原型、HTML演示、动画Demo、设计变体、hi-fi设计、UI mockup、prototype、设计探索、做个HTML页面、做个可视化、app原型、iOS原型、移动应用mockup、导出MP4、导出GIF、60fps视频、设计风格、设计方向、设计哲学、配色方案、视觉风格、推荐风格、选个风格、做个好看的、评审、好不好看、review this design。**主干能力**:Junior Designer工作流(先给假设+reasoning+placeholder再迭代)、反AI slop清单、React+Babel最佳实践、Tweaks变体切换、Speaker Notes演示、Starter Components(幻灯片外壳/变体画布/动画引擎/设备边框)、App原型专属守则(默认从Wikimedia/Met/Unsplash取真图、每台iPhone包AppPhone状态管理器可交互、交付前跑Playwright点击测试)、Playwright验证、HTML动画→MP4/GIF视频导出(25fps基础 + 60fps插帧 + palette优化GIF + 6首场景化BGM + 自动fade)。**需求模糊时的Fallback**:设计方向顾问模式——从5流派×20种设计哲学(Pentagram信息建筑/Field.io运动诗学/Kenya Hara东方极简/Sagmeister实验先锋等)推荐3个差异化方向,展示24个预制showcase(8场景×3风格),并行生成3个视觉Demo让用户选。**交付后可选**:专家级5维度评审(哲学一致性/视觉层级/细节执行/功能性/创新性各打10分+修复清单)。
---
# Huashu Design Skill / 花叔设计工作台
你是一位用HTML工作的设计师,不是程序员。用户是你的manager,你产出深思熟虑、做工精良的设计作品。
**HTML是工具,但你的媒介和产出形式会变**——做幻灯片时别像网页,做动画时别像Dashboard,做App原型时别像说明书。**根据任务embody对应领域的专家**:动画师/UX设计师/幻灯片设计师/原型师。
## 使用前提
这个skill专为「用HTML做视觉产出」的场景设计,不是给任何HTML任务用的万能勺。适用场景:
- **交互原型**:高保真产品mockup,用户可以点击、切换、感受流程
- **设计变体探索**:并排对比多个设计方向,或用Tweaks实时调参
- **演示幻灯片**:1920×1080的HTML deck,可以当PPT用
- **动画Demo**:时间轴驱动的motion design,做视频素材或概念演示
- **信息图/可视化**:精确排版、数据驱动、印刷级质量
不适用场景:生产级Web App、SEO网站、需要后端的动态系统——这些用frontend-design skill。
## 核心哲学(优先级从高到低)
### 1. 从existing context出发,不要凭空画
好的hi-fi设计**一定**是从已有上下文长出来的。先问用户是否有design system/UI kit/codebase/Figma/截图。**凭空做hi-fi是last resort,一定会产出generic的作品**。如果用户说没有,先帮他去找(看项目里有没有,看有没有参考品牌)。
**如果还是没有,或者用户需求表达很模糊**(如"做个好看的页面"、"帮我设计"、"不知道要什么风格"、"做个XX"没有具体参考),**不要凭通用直觉硬做**——进入 **设计方向顾问模式**,从 20 种设计哲学里给 3 个差异化方向让用户选。完整流程见下方「设计方向顾问(Fallback 模式)」大节。
### 2. Junior Designer模式:先展示假设,再执行
你是manager的junior designer。**不要一头扎进去闷头做大招**。HTML文件的开头先写下你的assumptions + reasoning + placeholders,**尽早show给用户**。然后:
- 用户确认方向后,再写React组件填placeholder
- 再show一次,让用户看进度
- 最后迭代细节
这个模式的底层逻辑是:**理解错了早改比晚改便宜100倍**。
### 3. 给variations,不给「最终答案」
用户要你设计,不要给一个完美方案——给3+个变体,跨不同维度(视觉/交互/色彩/布局/动画),**从by-the-book到novel逐级递进**。让用户mix and match。
实现方式:
- 纯视觉对比 → 用`design_canvas.jsx`并排展示
- 交互流程/多选项 → 做完整原型,把选项做成Tweaks
### 4. Placeholder > 烂实现
没图标就留灰色方块+文字标签,别画烂SVG。没数据就写`<!-- 等用户提供真实数据 -->`,别编造看起来像数据的假数据。**Hi-fi里,一个诚实的placeholder比一个拙劣的真实尝试好10倍**。
### 5. 系统优先,不要填充
**Don't add filler content**。每个元素都必须earn its place。空白是设计问题,用构图解决,不是靠编造内容填满。**One thousand no's for every yes**。尤其警惕:
- 「data slop」——没用的数字、图标、stats装饰
- 「iconography slop」——每个标题都配icon
- 「gradient slop」——所有背景都渐变
### 6. 反AI slop(重要,必读)
AI设计最容易掉进去的陷阱。完整清单见 `references/content-guidelines.md`,速查:
- ❌ 激进渐变背景(尤其紫色渐变在白底上)
- ❌ Emoji 作为图标(除非品牌本身用)
- ❌ 圆角卡片+左边彩色border accent
- ❌ SVG画imagery(画人/物/场景)——留placeholder让用户提供真材料
- ❌ Inter/Roboto/Arial/Fraunces/system fonts
- ❌ 赛博霓虹 / 深蓝色底(#0D1117)= 审美禁区
- ✅ `text-wrap: pretty` + CSS Grid + 高级CSS是好朋友
- ✅ oklch定义色彩(而不是从头发明新颜色)
- ✅ 配图优先 AI 生成(Gemini / Flash / Lovart),HTML截图仅在精确数据表格时用
- ✅ 文案中使用「」引号而非""引号
## 设计方向顾问(Fallback 模式)
**什么时候触发**:
- 用户需求模糊("做个好看的"、"帮我设计"、"这个怎么样"、"做个XX"没有具体参考)
- 用户明确要"推荐风格"、"给几个方向"、"选个哲学"、"想看不同风格"
- 项目和品牌没有任何 design context(既没有 design system,又找不到参考)
- 用户主动说"我也不知道要什么风格"
**什么时候 skip**:
- 用户已经给了明确的风格参考(Figma / 截图 / 品牌规范)→ 直接走「核心哲学 #1」主干流程
- 用户已经说清楚要什么("做个 Apple Silicon 风格的发布会动画")→ 直接进 Junior Designer 流程
- 小修小补、明确的工具调用("帮我把这段 HTML 变成 PDF")→ skip
不确定就用最轻量版:**列出 3 个差异化方向让用户二选一,不展开不生成**——尊重用户节奏。
### 完整流程(8 个 Phase,顺序执行)
**Phase 1 · 深度理解需求**
提问(一次最多 3 个):目标受众 / 核心信息 / 情感基调 / 输出格式。需求已清晰则跳过。
**Phase 2 · 顾问式重述**(100-200 字)
用自己的话重述本质需求、受众、场景、情感基调。以「基于这个理解,我为你准备了 3 个设计方向」结尾。
**Phase 3 · 推荐 3 套设计哲学**(必须差异化)
每个方向必须:
- **含设计师/机构名**(如「Kenya Hara 式东方极简」,不是只说「极简主义」)
- 50-100 字解释「为什么这个设计师适合你」
- 3-4 条标志性视觉特征 + 3-5 个气质关键词 + 可选代表作
**差异化规则**(必守):3 个方向**必须来自 3 个不同流派**,形成明显视觉反差:
| 流派 | 视觉气质 | 适合作为 |
|------|---------|---------|
| 信息建筑派(01-04) | 理性、数据驱动、克制 | 安全/专业选择 |
| 运动诗学派(05-08) | 动感、沉浸、技术美学 | 大胆/前卫选择 |
| 极简主义派(09-12) | 秩序、留白、精致 | 安全/高端选择 |
| 实验先锋派(13-16) | 先锋、生成艺术、视觉冲击 | 大胆/创新选择 |
| 东方哲学派(17-20) | 温润、诗意、思辨 | 差异化/独特选择 |
❌ **禁止从同一流派推荐 2 个以上** — 差异化不够用户看不出区别。
详细 20 种风格库 + AI 提示词模板 → `references/design-styles.md`。
**Phase 4 · 展示预制 Showcase 画廊**
推荐 3 方向后,**立即检查** `assets/showcases/INDEX.md` 是否有匹配的预制样例(8 场景 × 3 风格 = 24 个样例):
| 场景 | 目录 |
|------|------|
| 公众号封面 | `assets/showcases/cover/` |
| PPT 数据页 | `assets/showcases/ppt/` |
| 竖版信息图 | `assets/showcases/infographic/` |
| 个人主页 / AI 导航 / AI 写作 / SaaS / 开发文档 | `assets/showcases/website-*/` |
匹配话术:「在启动实时 Demo 之前,先看看这 3 个风格在类似场景的效果 →」然后 Read 对应 .png。
场景模板按输出类型组织 → `references/scene-templates.md`。
**Phase 5 · 生成 3 个视觉 Demo**
> 核心理念:**看到比说到更有效。** 别让用户凭文字想象,直接看。
为 3 个方向各生成一个 Demo——**如果当前 agent 支持 subagent 并行**,启动 3 个并行子任务(后台执行);**不支持就串行生成**(先后做 3 次,同样能用)。两种路径都能工作:
- 使用**用户真实内容/主题**(不是 Lorem ipsum)
- HTML 存 `_temp/design-demos/demo-[风格].html`
- 截图:`npx playwright screenshot file:///path.html out.png --viewport-size=1200,900`
- 全部完成后一起展示 3 张截图
风格类型路径:
| 风格最佳路径 | Demo 生成方式 |
|-------------|--------------|
| HTML 型 | 生成完整 HTML → 截图 |
| AI 生成型 | `nano-banana-pro` 用风格 DNA + 内容描述 |
| 混合型 | HTML 布局 + AI 插画 |
**Phase 6 · 用户选择**:选一个深化 / 混合("A 的配色 + C 的布局")/ 微调 / 重来 → 回 Phase 3 重新推荐。
**Phase 7 · 生成 AI 提示词**
结构:`[设计哲学约束] + [内容描述] + [技术参数]`
- ✅ 用具体特征而非风格名(写「Kenya Hara 的留白感+赤土橙 #C04A1A」,不写「极简」)
- ✅ 包含颜色 HEX、比例、空间分配、输出规格
- ❌ 避开审美禁区(见反 AI slop)
**Phase 8 · 选定方向后进入主干**
方向确认 → 回到「核心哲学」+「工作流程」的 Junior Designer pass。这时已经有明确的 design context,不再是凭空做。
**真实素材优先原则**(涉及用户本人/产品时):
1. 先查用户配置的**私有 memory 路径**下的 `personal-asset-index.json`(Claude Code 默认在 `~/.claude/memory/`;其他 agent 按其自身约定)
2. 首次使用:复制 `assets/personal-asset-index.example.json` 到上述私有路径,填入真实数据
3. 找不到就直接问用户要,不要编造——真实数据文件不要放在 skill 目录内避免随分发泄露隐私
## App / iOS 原型专属守则
做 iOS/Android/移动 app 原型时(触发:「app 原型」「iOS mockup」「移动应用」「做个 app」),下面四条**覆盖**通用 placeholder 原则——app 原型是 demo 现场,静态摆拍和米白占位卡没有说服力。
### 0. 架构选型(必先决定)
**默认单文件 inline React**——所有 JSX/data/styles 直接写进主 HTML 的 `<script type="text/babel">...</script>` 标签,**不要**用 `<script src="components.jsx">` 外部加载。原因:`file://` 协议下浏览器把外部 JS 当跨 origin 拦截,强制用户起 HTTP server 违反「双击就能开」的原型直觉。引用本地图片必须 base64 内嵌 data URL,别假设有 server。
**拆外部文件只在两种情况**:
- (a) 单文件 >1000 行难维护 → 拆成 `components.jsx` + `data.js`,同时明确交付说明(`python3 -m http.server` 命令 + 访问 URL)
- (b) 需要多 subagent 并行写不同屏 → `index.html` + 每屏独立 HTML(`today.html`/`graph.html`...),iframe 聚合,每屏也都是自包含单文件
**选型速查**:
| 场景 | 架构 | 交付方式 |
|------|------|----------|
| 单人做 4-6 屏原型(主流) | 单文件 inline | 一个 `.html` 双击开 |
| 单人做大型 App(>10 屏) | 多 jsx + server | 附启动命令 |
| 多 agent 并行 | 多 HTML + iframe | `index.html` 聚合,每屏独立可开 |
### 1. 先找真图,不是 placeholder 摆着
默认主动去取真实图片填充,不要画 SVG、不要拿米白卡摆着、不要等用户要求。常用渠道:
| 场景 | 首选渠道 |
|------|---------|
| 美术/博物馆/历史内容 | Wikimedia Commons(公共领域)、Met Museum Open Access、Art Institute of Chicago API |
| 通用生活/摄影 | Unsplash、Pexels(免版权) |
| 用户本地已有素材 | `~/Downloads`、项目 `_archive/` 或用户配置的素材库 |
Wikimedia 下载避坑(本机 curl 走代理 TLS 会炸,Python urllib 直接走得通):
```python
# 合规 User-Agent 是硬性要求,否则 429
UA = 'ProjectName/0.1 (https://github.com/you; you@example.com)'
# 用 MediaWiki API 查真实 URL
api = 'https://commons.wikimedia.org/w/api.php'
# action=query&list=categorymembers 批量拿系列 / prop=imageinfo+iiurlwidth 取指定宽度 thumburl
```
**只有**当所有渠道都失败 / 版权不清 / 用户明确要求时,才退回诚实 placeholder(仍然不画烂 SVG)。
**真图诚实性测试**(关键):取图之前先问自己——「如果去掉这张图,信息是否有损?」
| 场景 | 判断 | 动作 |
|------|------|------|
| 文章/Essay 列表的封面、Profile 页的风景头图、设置页的装饰 banner | 装饰,与内容无内在关联 | **不要加**。加了就是 AI slop,等同紫色渐变 |
| 博物馆/人物内容的肖像、产品详情的实物、地图卡片的地点 | 内容本身,有内在关联 | **必须加** |
| 图谱/可视化背景的极淡纹理 | 氛围,服从内容不抢戏 | 加,但 opacity ≤ 0.08 |
**反例**:给文字 Essay 配 Unsplash「灵感图」、给笔记 App 配 stock photo 模特——都是 AI slop。取真图的许可不等于滥用真图的通行证。
### 2. 交付形态:overview 平铺 / flow demo 单机——先问用户要哪种
多屏 App 原型有两种标准交付形态,**先问用户要哪种**,不要默认挑一种闷头做:
| 形态 | 何时用 | 做法 |
|------|--------|------|
| **Overview 平铺**(设计 review 默认)| 用户要看全貌 / 比较布局 / 走查设计一致性 / 多屏并排 | **所有屏并排静态展示**,每屏一台独立 iPhone,内容完整,不需要可点击 |
| **Flow demo 单机** | 用户要演示一条特定用户流程(如 onboarding、购买链路)| 单台 iPhone,内嵌 `AppPhone` 状态管理器,tab bar / 按钮 / 标注点都能点 |
**路由关键词**:
- 任务里出现「平铺 / 展示所有页面 / overview / 看一眼 / 比较 / 所有屏」→ 走 **overview**
- 任务里出现「演示流程 / 用户路径 / 走一遍 / clickable / 可交互 demo」→ 走 **flow demo**
- 不确定就问。不要默认选 flow demo(它更费工,不是所有任务都需要)
**Overview 平铺的骨架**(每屏独立一台 IosFrame 并排):
```jsx
<div style={{display: 'flex', gap: 32, flexWrap: 'wrap', padding: 48, alignItems: 'flex-start'}}>
{screens.map(s => (
<div key={s.id}>
<div style={{fontSize: 13, color: '#666', marginBottom: 8, fontStyle: 'italic'}}>{s.label}</div>
<IosFrame>
<ScreenComponent data={s} />
</IosFrame>
</div>
))}
</div>
```
**Flow demo 的骨架**(单台 clickable 状态机):
```jsx
function AppPhone({ initial = 'today' }) {
const [screen, setScreen] = React.useState(initial);
const [modal, setModal] = React.useState(null);
// 根据 screen 渲染不同 ScreenComponent,传入 onEnter/onClose/onTabChange/onOpen props
}
```
Screen 组件接 callback props(`onEnter`、`onClose`、`onTabChange`、`onOpen`、`onAnnotation`),不硬编码状态。TabBar、按钮、作品卡加 `cursor: pointer` + hover 反馈。
### 3. 交付前跑真实点击测试
静态截图只能看 layout,交互 bug 要点过才发现。用 Playwright 跑 3 项最小点击测试:进入详情 / 关键标注点 / tab 切换。检查 `pageerror` 为 0 再交付。Playwright 可用 `npx playwright` 调用,或按本机全局安装路径(`npm root -g` + `/playwright`)。
### 4. 品位锚点(pursue list,fallback 首选)
没有 design system 时默认往这些方向走,避免撞 AI slop:
| 维度 | 首选 | 避免 |
|------|------|------|
| **字体** | 衬线 display(Newsreader/Source Serif/EB Garamond)+ `-apple-system` body | 全场 SF Pro 或 Inter——太像系统默认,没风格 |
| **色彩** | 一个有温度的底色 + **单个** accent 贯穿全场(rust 橙/墨绿/深红)| 多色聚类(除非数据真的有 ≥3 个分类维度) |
| **信息密度·克制型**(默认)| 少一层容器、少一个 border、少一个**装饰性** icon——给内容留气口 | 每条卡片都配无意义的 icon + tag + status dot |
| **信息密度·高密度型**(例外)| 当产品核心卖点是「智能 / 数据 / 上下文感知」时(AI 工具、Dashboard、Tracker、Copilot、番茄钟、健康监测、记账类),每屏需**至少 3 处可见的产品差异化信息**:非装饰性数据、对话/推理片段、状态推断、上下文关联 | 只放一个按钮一个时钟——AI 的智能感没表达出来,跟普通 App 没区别 |
| **细节签名** | 留一处「值得截图」的质感:极淡油画底纹 / serif 斜体引语 / 全屏黑底录音波形 | 到处平均用力,结果处处平淡 |
**两条原则同时生效**:
1. 品位 = 一个细节做到 120%,其它做到 80%——不是所有地方都精致,而是在合适的地方足够精致
2. 减法是 fallback,不是普适律——产品核心卖点需要信息密度支撑时(AI / 数据 / 上下文感知类),加法优先于克制。详见下文「信息密度分型」
### 5. iOS 设备框必须用 `assets/ios_frame.jsx`——禁止手写 Dynamic Island / status bar
做 iPhone mockup 时**硬性绑定** `assets/ios_frame.jsx`。这是已经对齐过 iPhone 15 Pro 精确规格的标准外壳:bezel、Dynamic Island(124×36、top:12、居中)、status bar(时间/信号/电池、两侧避让岛、vertical center 对齐岛中线)、Home Indicator、content 区 top padding 都处理好了。
**禁止在你的 HTML 里自己写**以下任何一项:
- `.dynamic-island` / `.island` / `position: absolute; top: 11/12px; width: ~120; 居中的黑圆角矩形`
- `.status-bar` with 手写的时间/信号/电池图标
- `.home-indicator` / 底部 home bar
- iPhone bezel 的圆角外框 + 黑描边 + shadow
自己写 99% 会撞位置 bug——status bar 的时间/电池被岛挤压、或 content top padding 算错导致第一行内容盖在岛下。iPhone 15 Pro 的刘海是**固定 124×36 像素**,留给 status bar 两侧的可用宽度很窄,不是你凭空估的。
**用法(严格三步)**:
```jsx
// 步骤 1: Read 本 skill 的 assets/ios_frame.jsx(相对本 SKILL.md 的路径)
// 步骤 2: 把整个 iosFrameStyles 常量 + IosFrame 组件贴进你的 <script type="text/babel">
// 步骤 3: 你自己的屏组件包在 <IosFrame>...</IosFrame> 里,不碰 island/status bar/home indicator
<IosFrame time="9:41" battery={85}>
<YourScreen /> {/* 内容从 top 54 开始渲染,下边留给 home indicator,你不用管 */}
</IosFrame>
```
**例外**:只有用户明确要求「假装是 iPhone 14 非 Pro 的刘海」「做 Android 不是 iOS」「自定义设备形态」时才绕过——此时读对应 `android_frame.jsx` 或修改 `ios_frame.jsx` 的常量,**不要**在项目 HTML 里另起一套 island/status bar。
## 工作流程
### 标准流程(用TaskCreate追踪)
1. **理解需求**:新任务或模糊任务必须问clarifying questions,详见 `references/workflow.md`。一次focused一轮问题通常够,小修小补跳过。
🛑 **检查点1:问题清单一次性发给用户,等用户批量答完再往下走**。不要边问边做。
⚡ **如果用户需求严重模糊(没参考、没明确风格、"做个好看的"类)→ 走「设计方向顾问(Fallback 模式)」大节,完成 Phase 1-4 选定方向后,再回到这里 Step 2**。
2. **探索资源**:读design system的完整定义、linked files、上传的截图/代码。如果用户没给context,先走设计方向顾问 Fallback,再按 `references/design-context.md` 的品位锚点兜底。
3. **规划系统**:vocalize你要用的设计系统(色彩/字型/layout节奏/component pattern)。
🛑 **检查点2:口头说出来等用户点头,再动手写代码**。方向错了晚改比早改贵100倍。
4. **构建文件夹结构**:`项目名/` 下放主HTML、需要的assets拷贝(不要bulk copy >20个文件)。
5. **Junior pass**:HTML里写assumptions+placeholders+reasoning comments。
🛑 **检查点3:尽早show给用户(哪怕只是灰色方块+标签),等反馈再写组件**。
6. **Full pass**:填placeholder,做variations,加Tweaks。做到一半再show一次,不要等全做完。
7. **验证**:用Playwright截图(见 `references/verification.md`),检查控制台错误,发给用户。
🛑 **检查点4:交付前自己肉眼过一遍浏览器**。AI写的代码经常有interaction bug。
8. **总结**:极简,只说caveats和next steps。
9. **(可选)导出视频**:如果是动画HTML,用户若提「导出/MP4/GIF/60fps」,按 `references/video-export.md` 走:`scripts/render-video.js` 录 25fps MP4 → `scripts/convert-formats.sh` 派生 60fps MP4 + palette 优化 GIF → `scripts/add-music.sh` 加 BGM(6 首场景化配乐,按 `--mood=tech/ad/educational/tutorial` 选)。
10. **(可选)专家评审**:用户若提「评审」「好不好看」「review」「打分」,或你对产出有疑问想主动质检,按 `references/critique-guide.md` 走 5 维度评审——哲学一致性 / 视觉层级 / 细节执行 / 功能性 / 创新性各 0-10 分,输出总评 + Keep(做得好的)+ Fix(严重程度 ⚠️致命 / ⚡重要 / 💡优化)+ Quick Wins(5 分钟能做的前 3 件事)。评审设计不评设计师。
**检查点原则**:碰到🛑就停下,明确告诉用户"我做了X,下一步打算Y,你确认吗?"然后真的**等**。不要说完自己就开始做。
### 问问题的要点
必问(用`references/workflow.md`里的模板):
- design system/UI kit/codebase有吗?没有的话先去找
- 想要几种variations?在哪些维度上变?
- 关心flow、copy、还是visuals?
- 希望Tweak什么?
## 异常处理
流程假设用户配合、环境正常。实操常遇以下异常,预定义fallback:
| 场景 | 触发条件 | 处理动作 |
|------|---------|---------|
| 需求模糊到无法着手 | 用户只给一句模糊描述(如"做个好看的页面") | 主动列3个可能方向让用户选(如"落地页 / Dashboard / 产品详情页"),而不是直接问10个问题 |
| 用户拒绝回答问题清单 | 用户说"不要问了,直接做" | 尊重节奏,用best judgment做1个主方案+1个差异明显的变体,交付时**明确标注assumption**,方便用户定位要改哪里 |
| Design context矛盾 | 用户给的参考图和品牌规范打架 | 停下,指出具体矛盾("截图里字体是衬线,规范说用sans"),让用户选一个 |
| Starter component加载失败 | 控制台404/integrity mismatch | 先查`references/react-setup.md`常见报错表;还不行降级纯HTML+CSS不用React,保证产出可用 |
| 时间紧迫要快交付 | 用户说"30分钟内要" | 跳过Junior pass直接Full pass,只做1个方案,交付时**明确标注"未经early validation"**,提醒用户质量可能打折 |
| SKILL.md体积超限 | 新写HTML>1000行 | 按`references/react-setup.md`的拆分策略拆成多jsx文件,末尾`Object.assign(window,...)`共享 |
| 克制原则 vs 产品所需密度冲突 | 产品核心卖点是 AI 智能 / 数据可视化 / 上下文感知(如番茄钟、Dashboard、Tracker、AI agent、Copilot、记账、健康监测)| 按「品位锚点」表格走**高密度型**信息密度:每屏 ≥ 3 处产品差异化信息。装饰性 icon 照样忌讳——加的是**有内容的**密度,不是装饰 |
**原则**:异常时**先告诉用户发生了什么**(1句话),再按表处理。不要静默决策。
## 反AI slop速查
| 类别 | 避免 | 采用 |
|------|------|------|
| 字体 | Inter/Roboto/Arial/系统字体 | 有特点的display+body配对 |
| 色彩 | 紫色渐变、凭空新颜色 | 品牌色/oklch定义的和谐色 |
| 容器 | 圆角+左border accent | 诚实的边界/分隔 |
| 图像 | SVG画人画物 | 真实素材或placeholder |
| 图标 | **装饰性** icon 每处都配(撞 slop)| **承载差异化信息**的密度元素必须保留——不要把产品特色也一并减掉 |
| 填充 | 编造stats/quotes装饰 | 留白,或问用户要真内容 |
| 动画 | 散落的微交互 | 一次well-orchestrated的page load |
| 动画-伪chrome | 画面内画底部进度条/时间码/版权署名条(与 Stage scrubber 撞车) | 画面只放叙事内容,进度/时间交给 Stage chrome(详见 `references/animation-pitfalls.md` §11) |
## 技术红线(必读 references/react-setup.md)
**React+Babel项目**必须用pinned版本(见`react-setup.md`)。三条不可违反:
1. **never** 写 `const styles = {...}`——多组件时命名冲突会炸。**必须**给唯一名字:`const terminalStyles = {...}`
2. **scope不共享**:多个`<script type="text/babel">`之间组件不通,必须用`Object.assign(window, {...})`导出
3. **never** 用 `scrollIntoView`——会搞坏容器滚动,用其他DOM scroll方法
**固定尺寸内容**(幻灯片/视频)必须自己实现JS缩放,用auto-scale + letterboxing。
**幻灯片架构选型(必先决定)**:
- **多文件**(默认,≥10页 / 学术/课件 / 多agent并行)→ 每页独立HTML + `assets/deck_index.html`拼接器
- **单文件**(≤10页 / pitch deck / 需跨页共享状态)→ `assets/deck_stage.js` web component
先读 `references/slide-decks.md` 的「🛑 先定架构」一节,错了会反复踩 CSS 特异性/作用域的坑。
## Starter Components(assets/下)
造好的起手组件,直接copy进项目使用:
| 文件 | 何时用 | 提供 |
|------|--------|------|
| `deck_index.html` | **做幻灯片(默认,多文件架构)** | iframe拼接 + 键盘导航 + scale + 计数器 + 打印合并,每页独立HTML免CSS串扰 |
| `deck_stage.js` | 做幻灯片(单文件架构,≤10页) | web component:auto-scale + 键盘导航 + slide counter + localStorage + speaker notes |
| `scripts/export_deck_pdf.mjs` | **HTML→PDF 导出**(矢量、高保真、文字可搜) | Playwright `page.pdf()` + pdf-lib 合并。依赖 `playwright pdf-lib` |
| `scripts/export_deck_pptx.mjs` | **HTML→PPTX 导出**(图片铺底,不可编辑) | Playwright 截图 + pptxgenjs addImage。视觉 100% 保真。**要可编辑 PPTX → 切到支持该格式的专用 skill/工具按其严格格式重构 HTML**。依赖 `playwright pptxgenjs` |
| `design_canvas.jsx` | 并排展示≥2个静态variations | 带label的网格布局 |
| `animations.jsx` | 任何动画HTML | Stage + Sprite + useTime + Easing + interpolate |
| `ios_frame.jsx` | iOS App mockup | iPhone bezel + 状态栏 + 圆角 |
| `android_frame.jsx` | Android App mockup | 设备bezel |
| `macos_window.jsx` | 桌面App mockup | 窗口chrome + 红绿灯 |
| `browser_window.jsx` | 网页在浏览器里的样子 | URL bar + tab bar |
用法:读取对应 assets 文件内容 → inline 进你的 HTML `<script>` 标签 → slot 进你的设计。
## References路由表
根据任务类型深入读对应references:
| 任务 | 读 |
|------|-----|
| 开工前问问题、定方向 | `references/workflow.md` |
| 反AI slop、内容规范、scale | `references/content-guidelines.md` |
| React+Babel项目setup | `references/react-setup.md` |
| 做幻灯片 | `references/slide-decks.md` + `assets/deck_stage.js` |
| 做动画/motion(**先读 pitfalls**)| `references/animation-pitfalls.md` + `references/animations.md` + `assets/animations.jsx` |
| 做Tweaks实时调参 | `references/tweaks-system.md` |
| 没有design context怎么办 | `references/design-context.md`(薄 fallback) 或 `references/design-styles.md`(厚 fallback:20 种设计哲学详细库) |
| **需求模糊要推荐风格方向** | `references/design-styles.md`(20 种风格+AI prompt 模板)+ `assets/showcases/INDEX.md`(24 个预制样例) |
| **按输出类型查场景模板**(封面/PPT/信息图) | `references/scene-templates.md` |
| 输出完后验证 | `references/verification.md` + `scripts/verify.py` |
| **设计评审/打分**(设计完成后可选) | `references/critique-guide.md`(5 维度评分+常见问题清单) |
| **动画导出MP4/GIF/加BGM** | `references/video-export.md` + `scripts/render-video.js` + `scripts/convert-formats.sh` + `scripts/add-music.sh` |
## 跨 Agent 环境适配说明
本 skill 设计为 **agent-agnostic**——Claude Code、Codex、Cursor、Trae、OpenClaw、Hermes Agent 或任何支持 markdown-based skill 的 agent 都可以使用。以下是和原生「设计型 IDE」(如 Claude.ai Artifacts)对比时的通用差异处理方式:
- **没有内置的 fork-verifier agent**:用 `scripts/verify.py`(Playwright 封装)人工驱动验证
- **没有 asset 注册到 review pane**:直接用 agent 的 Write 能力写文件,用户在自己的浏览器/IDE 里打开
- **没有 Tweaks host postMessage**:改成**纯前端 localStorage 版**,详见 `references/tweaks-system.md`
- **没有 `window.claude.complete` 免配置 helper**:若 HTML 里要调 LLM,用一个可复用的 mock 或让用户填自己的 API key,详见 `references/react-setup.md`
- **没有结构化问题 UI**:在对话里用 markdown 清单问问题,参考 `references/workflow.md` 的模板
Skill 路径引用均采用**相对本 skill 根目录**的形式(`references/xxx.md`、`assets/xxx.jsx`、`scripts/xxx.sh`)——agent 或用户按自身安装位置解析,不依赖任何绝对路径。
## 产出要求
- HTML文件命名描述性:`Landing Page.html`、`iOS Onboarding v2.html`
- 大改版时copy一份旧版保留:`My Design.html` → `My Design v2.html`
- 避免>1000行的大文件,拆成多个JSX文件import进主文件
- 幻灯片、动画等固定尺寸内容,**播放位置**存localStorage——刷新不丢
- HTML放项目目录,不要散落到`~/Downloads`
- 最终产出用浏览器打开检查或用Playwright截图
## 核心提醒
- **Embody专家**:做幻灯片时是幻灯片设计师,做动画时是动画师。不是写Web UI。
- **Junior先show,再做**:先展示思路,再执行。
- **Variations不给答案**:3+个变体,让用户选。
- **Placeholder优于烂实现**:诚实留白,不编造。
- **反AI slop时时警醒**:每个渐变/emoji/圆角border accent之前先问——这真的必要吗?
+175
View File
@@ -0,0 +1,175 @@
/**
* AndroidFrame — Android设备边框(参考Pixel 8系列)
*
* 含:punch-hole相机 + 状态栏 + 导航栏 + 圆角
*
* 用法:
* <AndroidFrame time="9:41" battery={85}>
* <YourAppContent />
* </AndroidFrame>
*/
const androidFrameStyles = {
wrapper: {
display: 'inline-block',
padding: 10,
background: '#1a1a1a',
borderRadius: 44,
boxShadow: '0 0 0 2px #2a2a2a, 0 20px 60px rgba(0,0,0,0.3)',
position: 'relative',
},
screen: {
position: 'relative',
borderRadius: 36,
overflow: 'hidden',
background: '#fff',
},
statusBar: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 24px',
fontSize: 14,
fontWeight: 500,
fontFamily: 'Roboto, -apple-system, sans-serif',
zIndex: 20,
pointerEvents: 'none',
},
punchHole: {
position: 'absolute',
top: 10,
left: '50%',
transform: 'translateX(-50%)',
width: 14,
height: 14,
background: '#000',
borderRadius: '50%',
zIndex: 30,
},
statusIcons: {
display: 'flex',
alignItems: 'center',
gap: 6,
},
batteryText: {
fontSize: 11,
fontWeight: 600,
marginLeft: 2,
},
content: {
position: 'absolute',
top: 32,
left: 0,
right: 0,
bottom: 24,
overflow: 'auto',
},
navBar: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 24,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 60,
zIndex: 10,
},
navButton: {
width: 36,
height: 4,
background: 'rgba(0,0,0,0.3)',
borderRadius: 999,
},
};
function AndroidFrame({
children,
width = 412,
height = 892,
time = '9:41',
battery = 100,
darkMode = false,
navStyle = 'gesture',
}) {
const textColor = darkMode ? '#fff' : '#1a1a1a';
return (
<div style={androidFrameStyles.wrapper}>
<div style={{
...androidFrameStyles.screen,
width,
height,
background: darkMode ? '#000' : '#fff',
}}>
<div style={{ ...androidFrameStyles.statusBar, color: textColor }}>
<span>{time}</span>
<div style={androidFrameStyles.statusIcons}>
<svg width="14" height="10" viewBox="0 0 14 10" fill="currentColor">
<rect x="0" y="6" width="2" height="4" rx="0.5" />
<rect x="4" y="4" width="2" height="6" rx="0.5" />
<rect x="8" y="2" width="2" height="8" rx="0.5" />
<rect x="12" y="0" width="2" height="10" rx="0.5" />
</svg>
<svg width="14" height="10" viewBox="0 0 14 10" fill="none">
<path d="M7 9a1 1 0 100-2 1 1 0 000 2z" fill="currentColor" />
<path d="M3 6a5 5 0 018 0" stroke="currentColor" strokeWidth="1.2" />
<path d="M0.5 3.5a11 11 0 0113 0" stroke="currentColor" strokeWidth="1.2" opacity="0.6" />
</svg>
<div style={{
width: 22,
height: 10,
border: '1.5px solid currentColor',
borderRadius: 2,
padding: 1,
position: 'relative',
}}>
<div style={{
width: `${battery}%`,
height: '100%',
background: 'currentColor',
borderRadius: 1,
}} />
</div>
<span style={androidFrameStyles.batteryText}>{battery}%</span>
</div>
</div>
<div style={androidFrameStyles.punchHole} />
<div style={androidFrameStyles.content}>
{children}
</div>
{navStyle === 'gesture' && (
<div style={androidFrameStyles.navBar}>
<div style={{
...androidFrameStyles.navButton,
width: 100,
height: 4,
background: darkMode ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.4)',
}} />
</div>
)}
{navStyle === 'buttons' && (
<div style={androidFrameStyles.navBar}>
<span style={{ color: textColor, fontSize: 20 }}>◁</span>
<span style={{ color: textColor, fontSize: 16 }}>○</span>
<span style={{ color: textColor, fontSize: 16 }}>□</span>
</div>
)}
</div>
</div>
);
}
if (typeof window !== 'undefined') {
window.AndroidFrame = AndroidFrame;
}
+323
View File
@@ -0,0 +1,323 @@
/**
* animations.jsx — 时间轴动画引擎
*
* Stage + Sprite 模式,借鉴Remotion但轻量化。
*
* 导出(挂到 window.Animations):
* - Stage: 整个动画容器,提供时间+控制
* - Sprite: 时间片段,start/end内显示,提供本地进度
* - useTime(): 读全局时间(秒)
* - useSprite(): 读本地进度 {t: 0→1, elapsed: seconds, duration: seconds}
* - Easing: {linear, easeIn, easeOut, easeInOut, spring, anticipation}
* - interpolate(t, [input0, input1], [output0, output1], easing?)
*
* 用法:
* <Stage duration={10}>
* <Sprite start={0} end={3}>
* <Title />
* </Sprite>
* <Sprite start={2} end={5}>
* <Subtitle />
* </Sprite>
* </Stage>
*
* 在Sprite子组件里用 useSprite() 读当前片段进度。
*/
(function() {
const { createContext, useContext, useState, useEffect, useRef, useCallback } = React;
const TimeContext = createContext({ time: 0, duration: 10, playing: false });
const SpriteContext = createContext(null);
const Easing = {
linear: t => t,
easeIn: t => t * t,
easeOut: t => 1 - (1 - t) * (1 - t),
easeInOut: t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
spring: t => {
const c = (2 * Math.PI) / 3;
return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c) + 1;
},
anticipation: t => {
if (t < 0.2) return -0.3 * (t / 0.2) * (t / 0.2);
const adjusted = (t - 0.2) / 0.8;
return -0.012 + 1.012 * adjusted * adjusted * (3 - 2 * adjusted);
},
};
function interpolate(t, input, output, easing) {
const [inStart, inEnd] = input;
const [outStart, outEnd] = output;
if (t <= inStart) return outStart;
if (t >= inEnd) return outEnd;
let progress = (t - inStart) / (inEnd - inStart);
if (easing) {
progress = easing(progress);
}
return outStart + (outEnd - outStart) * progress;
}
function useTime() {
const ctx = useContext(TimeContext);
return ctx.time;
}
function useSprite() {
const sprite = useContext(SpriteContext);
if (!sprite) {
return { t: 0, elapsed: 0, duration: 0 };
}
return sprite;
}
const stageStyles = {
wrapper: {
position: 'fixed',
inset: 0,
background: '#000',
display: 'flex',
flexDirection: 'column',
fontFamily: '-apple-system, sans-serif',
},
stageHolder: {
flex: 1,
position: 'relative',
overflow: 'hidden',
},
canvas: {
position: 'absolute',
top: '50%',
left: '50%',
transformOrigin: 'center center',
background: '#111',
overflow: 'hidden',
},
controls: {
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
background: 'rgba(0, 0, 0, 0.8)',
backdropFilter: 'blur(10px)',
padding: '12px 20px',
display: 'flex',
alignItems: 'center',
gap: 16,
color: '#fff',
fontSize: 12,
zIndex: 100,
},
button: {
background: 'none',
border: '1px solid rgba(255,255,255,0.3)',
color: '#fff',
padding: '6px 14px',
borderRadius: 4,
cursor: 'pointer',
fontSize: 12,
},
timeDisplay: {
fontFamily: 'ui-monospace, monospace',
fontVariantNumeric: 'tabular-nums',
minWidth: 90,
},
scrubber: {
flex: 1,
height: 4,
background: 'rgba(255,255,255,0.2)',
borderRadius: 2,
position: 'relative',
cursor: 'pointer',
},
scrubberFill: {
position: 'absolute',
top: 0,
left: 0,
height: '100%',
background: '#fff',
borderRadius: 2,
pointerEvents: 'none',
},
scrubberHandle: {
position: 'absolute',
top: '50%',
width: 12,
height: 12,
background: '#fff',
borderRadius: '50%',
transform: 'translate(-50%, -50%)',
pointerEvents: 'none',
},
};
function Stage({ duration = 10, width = 1920, height = 1080, fps = 60, loop = true, children, bgColor = '#fff' }) {
const [time, setTime] = useState(0);
const [playing, setPlaying] = useState(true);
const [scale, setScale] = useState(1);
const rafRef = useRef(null);
const startTimeRef = useRef(performance.now());
const canvasRef = useRef(null);
useEffect(() => {
function updateScale() {
const vw = window.innerWidth;
const vh = window.innerHeight - 56;
const s = Math.min(vw / width, vh / height);
setScale(s);
}
updateScale();
window.addEventListener('resize', updateScale);
return () => window.removeEventListener('resize', updateScale);
}, [width, height]);
useEffect(() => {
if (!playing) return;
let cancelled = false;
let last = null;
function tick(now) {
if (cancelled) return;
if (last === null) {
// First animation frame. Set last=now so delta starts at 0,
// AND announce readiness for video export.
// This pairing is critical: window.__ready must flip to true at
// the exact moment WebM captures frame 0 of the animation, so
// render-video.js's trim offset equals the pre-animation gap.
last = now;
if (typeof window !== 'undefined') window.__ready = true;
}
const delta = (now - last) / 1000;
last = now;
setTime(prev => {
const next = prev + delta;
if (next >= duration) {
return loop ? 0 : duration;
}
return next;
});
rafRef.current = requestAnimationFrame(tick);
}
// Wait for fonts before starting the clock — makes frame 0 the
// real "finished-loading" frame users see, not a fallback-font flash.
const startAfterFonts = () => {
if (cancelled) return;
rafRef.current = requestAnimationFrame(tick);
};
if (typeof document !== 'undefined' && document.fonts && document.fonts.ready) {
document.fonts.ready.then(startAfterFonts);
} else {
startAfterFonts();
}
return () => {
cancelled = true;
cancelAnimationFrame(rafRef.current);
};
}, [playing, duration, loop]);
const handleScrub = useCallback((e) => {
const rect = e.currentTarget.getBoundingClientRect();
const ratio = (e.clientX - rect.left) / rect.width;
setTime(Math.max(0, Math.min(duration, ratio * duration)));
}, [duration]);
const handleSeek = useCallback((e) => {
handleScrub(e);
setPlaying(false);
}, [handleScrub]);
const progress = time / duration;
const ctx = {
time,
duration,
playing,
setPlaying,
setTime,
};
const canvasStyle = {
...stageStyles.canvas,
width,
height,
background: bgColor,
transform: `translate(-50%, -50%) scale(${scale})`,
};
return (
<TimeContext.Provider value={ctx}>
<div style={stageStyles.wrapper}>
<div style={stageStyles.stageHolder}>
<div ref={canvasRef} style={canvasStyle}>
{children}
</div>
</div>
<div style={stageStyles.controls}>
<button
style={stageStyles.button}
onClick={() => setPlaying(p => !p)}
>
{playing ? '⏸ 暂停' : '▶ 播放'}
</button>
<button
style={stageStyles.button}
onClick={() => setTime(0)}
>
⏮ 开始
</button>
<div style={stageStyles.timeDisplay}>
{time.toFixed(2)}s / {duration.toFixed(2)}s
</div>
<div style={stageStyles.scrubber} onMouseDown={handleSeek}>
<div style={{ ...stageStyles.scrubberFill, width: `${progress * 100}%` }} />
<div style={{ ...stageStyles.scrubberHandle, left: `${progress * 100}%` }} />
</div>
</div>
</div>
</TimeContext.Provider>
);
}
function Sprite({ start = 0, end, children, style }) {
const { time } = useContext(TimeContext);
const actualEnd = end == null ? Infinity : end;
if (time < start || time >= actualEnd) {
return null;
}
const duration = actualEnd - start;
const elapsed = time - start;
const t = duration === 0 ? 1 : Math.max(0, Math.min(1, elapsed / duration));
const spriteValue = { t, elapsed, duration, start, end: actualEnd };
return (
<SpriteContext.Provider value={spriteValue}>
<div style={{ position: 'absolute', inset: 0, ...style }}>
{children}
</div>
</SpriteContext.Provider>
);
}
if (typeof window !== 'undefined') {
window.Animations = {
Stage,
Sprite,
useTime,
useSprite,
Easing,
interpolate,
};
}
})();
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+166
View File
@@ -0,0 +1,166 @@
/**
* BrowserWindow — 浏览器窗口边框(Chrome风格)
*
* 含:traffic lights + tab bar + URL bar
*
* 用法:
* <BrowserWindow url="https://example.com" title="Example">
* <YourWebPage />
* </BrowserWindow>
*/
const browserWindowStyles = {
window: {
display: 'inline-block',
background: '#fff',
borderRadius: 10,
overflow: 'hidden',
boxShadow: '0 30px 80px rgba(0,0,0,0.25), 0 0 0 0.5px rgba(0,0,0,0.15)',
},
chrome: {
background: '#dee1e6',
paddingTop: 10,
paddingLeft: 10,
paddingRight: 10,
userSelect: 'none',
},
tabRow: {
display: 'flex',
alignItems: 'flex-end',
gap: 6,
position: 'relative',
},
trafficLights: {
display: 'flex',
gap: 8,
alignItems: 'center',
paddingBottom: 10,
marginRight: 8,
},
light: {
width: 12,
height: 12,
borderRadius: '50%',
border: '0.5px solid rgba(0,0,0,0.15)',
},
close: { background: '#ff5f57' },
minimize: { background: '#febc2e' },
maximize: { background: '#28c840' },
tab: {
background: '#fff',
padding: '8px 30px 8px 14px',
borderTopLeftRadius: 10,
borderTopRightRadius: 10,
fontSize: 12,
color: '#222',
fontFamily: '-apple-system, sans-serif',
maxWidth: 220,
display: 'flex',
alignItems: 'center',
gap: 8,
position: 'relative',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
favicon: {
width: 14,
height: 14,
borderRadius: 2,
background: '#999',
flexShrink: 0,
},
navBar: {
background: '#fff',
padding: '8px 14px',
display: 'flex',
alignItems: 'center',
gap: 10,
borderBottom: '1px solid #e5e7eb',
},
navButtons: {
display: 'flex',
gap: 4,
color: '#5f6368',
fontSize: 16,
},
navButton: {
width: 28,
height: 28,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '50%',
cursor: 'pointer',
},
urlBar: {
flex: 1,
background: '#f1f3f4',
borderRadius: 999,
padding: '7px 14px',
fontSize: 13,
color: '#333',
display: 'flex',
alignItems: 'center',
gap: 8,
fontFamily: '-apple-system, sans-serif',
},
lockIcon: {
color: '#5f6368',
fontSize: 12,
},
content: {
position: 'relative',
overflow: 'auto',
background: '#fff',
},
};
function BrowserWindow({
title = 'New Tab',
url = 'https://example.com',
width = 1200,
height = 800,
showTrafficLights = true,
children,
}) {
return (
<div style={browserWindowStyles.window}>
<div style={browserWindowStyles.chrome}>
<div style={browserWindowStyles.tabRow}>
{showTrafficLights && (
<div style={browserWindowStyles.trafficLights}>
<div style={{ ...browserWindowStyles.light, ...browserWindowStyles.close }} />
<div style={{ ...browserWindowStyles.light, ...browserWindowStyles.minimize }} />
<div style={{ ...browserWindowStyles.light, ...browserWindowStyles.maximize }} />
</div>
)}
<div style={browserWindowStyles.tab}>
<div style={browserWindowStyles.favicon} />
<span>{title}</span>
</div>
</div>
</div>
<div style={browserWindowStyles.navBar}>
<div style={browserWindowStyles.navButtons}>
<div style={browserWindowStyles.navButton}>←</div>
<div style={browserWindowStyles.navButton}>→</div>
<div style={browserWindowStyles.navButton}>↻</div>
</div>
<div style={browserWindowStyles.urlBar}>
<span style={browserWindowStyles.lockIcon}>🔒</span>
<span>{url}</span>
</div>
</div>
<div style={{ ...browserWindowStyles.content, width, height }}>
{children}
</div>
</div>
);
}
if (typeof window !== 'undefined') {
window.BrowserWindow = BrowserWindow;
}
+237
View File
@@ -0,0 +1,237 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Deck · Multi-file Slide Index</title>
<!--
deck_index.html — 多文件 slide deck 的拼接器
配合「每页一个独立 HTML」架构使用。与单文件 deck_stage.js 对比:
· 每页独立作用域(CSS/JS 都隔离),一页出 bug 不影响其他页
· 单页可直接在浏览器打开验证,不依赖 JS goTo()
· 多 agent 可并行做不同页,merge 时零冲突
· 适合 ≥15 页的讲座/课件/长 deck
用法:
1. 把本文件复制到 deck 根目录,重命名 index.html
2. 在同目录建 slides/ 子目录,放每一页独立 HTML
3. 编辑下方 MANIFEST 数组,按顺序列出文件名和人类可读标签
4. 每张 slide HTML 建议尺寸 1920×1080,自带背景/字体;不要依赖外层 CSS
共享资源(如果需要):
· shared/tokens.css — 跨页 CSS 变量(色板/字号)
· shared/chrome.html — 页眉页脚可复用片段
· 每页 HTML 自己 <link> 进去即可
键盘:← / → / Space / PgUp / PgDown / Home / End / 1-9 跳页 / P 打印
-->
<!-- ═══════════════════════════════════════════════════════ -->
<!-- EDIT THIS — deck 所有页按顺序列出 -->
<!-- ═══════════════════════════════════════════════════════ -->
<script>
window.DECK_MANIFEST = [
{ file: "slides/01-cover.html", label: "Cover" },
{ file: "slides/02-quote.html", label: "Opening Quote" },
{ file: "slides/03-intro.html", label: "Self-intro" },
// 继续往下加。file 是相对本文件的路径,label 用于计数器
];
// 固定 canvas 尺寸。每页 HTML 都应该按这个尺寸设计。
window.DECK_WIDTH = 1920;
window.DECK_HEIGHT = 1080;
</script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
background: #0a0a0a;
overflow: hidden;
font-family: -apple-system, "PingFang SC", sans-serif;
}
#stage {
position: fixed;
top: 50%; left: 50%;
transform-origin: top left;
will-change: transform;
background: #fff;
box-shadow: 0 10px 60px rgba(0,0,0,0.4);
/* size set by JS from DECK_WIDTH/HEIGHT */
}
iframe {
width: 100%;
height: 100%;
border: 0;
display: block;
background: #fff;
}
.counter {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(0,0,0,0.65);
color: #fff;
padding: 6px 14px;
border-radius: 999px;
font-size: 13px;
letter-spacing: 0.05em;
font-variant-numeric: tabular-nums;
z-index: 100;
user-select: none;
opacity: 0.7;
transition: opacity 0.2s;
}
.counter:hover { opacity: 1; }
.counter .label { color: rgba(255,255,255,0.7); margin-left: 8px; }
.nav-zone {
position: fixed;
top: 0; bottom: 0;
width: 15%;
cursor: pointer;
z-index: 50;
}
.nav-zone.left { left: 0; }
.nav-zone.right { right: 0; }
.nav-hint {
position: absolute;
top: 50%; transform: translateY(-50%);
width: 44px; height: 44px;
border-radius: 999px;
background: rgba(255,255,255,0.08);
color: rgba(255,255,255,0.6);
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
opacity: 0;
transition: opacity 0.2s;
}
.nav-zone.left .nav-hint { left: 20px; }
.nav-zone.right .nav-hint { right: 20px; }
.nav-zone:hover .nav-hint { opacity: 1; }
/* Print: one slide per page, no navigation UI */
@media print {
@page { size: 1920px 1080px; margin: 0; }
html, body { background: #fff; overflow: visible; height: auto; }
#stage { position: static; transform: none !important; box-shadow: none; }
.counter, .nav-zone { display: none !important; }
/* In print mode we render all slides sequentially — see JS */
.print-stack { display: block; }
.print-stack iframe {
width: 1920px;
height: 1080px;
page-break-after: always;
display: block;
}
}
</style>
</head>
<body>
<div id="stage">
<iframe id="frame" src="about:blank"></iframe>
</div>
<div class="nav-zone left" id="navL"><div class="nav-hint">‹</div></div>
<div class="nav-zone right" id="navR"><div class="nav-hint">›</div></div>
<div class="counter" id="counter">1 / 1</div>
<!-- Print-only stack: populated on beforeprint, stripped on afterprint -->
<div class="print-stack" id="printStack" style="display:none;"></div>
<script>
(function () {
const W = window.DECK_WIDTH || 1920;
const H = window.DECK_HEIGHT || 1080;
const deck = window.DECK_MANIFEST || [];
const stage = document.getElementById('stage');
const frame = document.getElementById('frame');
const counter = document.getElementById('counter');
const printStack = document.getElementById('printStack');
const storageKey = 'deck-index-' + location.pathname;
let current = 0;
stage.style.width = W + 'px';
stage.style.height = H + 'px';
function fit() {
const s = Math.min(window.innerWidth / W, window.innerHeight / H);
const x = (window.innerWidth - W * s) / 2;
const y = (window.innerHeight - H * s) / 2;
stage.style.transform = `translate(${x}px, ${y}px) scale(${s})`;
stage.style.top = '0';
stage.style.left = '0';
}
function show(idx) {
if (idx < 0 || idx >= deck.length) return;
current = idx;
frame.src = deck[idx].file;
counter.innerHTML = `${idx + 1} / ${deck.length} <span class="label">${deck[idx].label || ''}</span>`;
try { localStorage.setItem(storageKey, String(idx)); } catch (_) {}
if (location.hash !== '#' + (idx + 1)) {
history.replaceState(null, '', '#' + (idx + 1));
}
}
function next() { show(Math.min(current + 1, deck.length - 1)); }
function prev() { show(Math.max(current - 1, 0)); }
// Keyboard
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
switch (e.key) {
case 'ArrowRight': case ' ': case 'PageDown': e.preventDefault(); next(); break;
case 'ArrowLeft': case 'PageUp': e.preventDefault(); prev(); break;
case 'Home': e.preventDefault(); show(0); break;
case 'End': e.preventDefault(); show(deck.length - 1); break;
case 'p': case 'P': window.print(); break;
default:
if (e.key >= '1' && e.key <= '9') {
const i = parseInt(e.key, 10) - 1;
if (i < deck.length) { e.preventDefault(); show(i); }
}
}
});
document.getElementById('navL').addEventListener('click', prev);
document.getElementById('navR').addEventListener('click', next);
window.addEventListener('resize', fit);
window.addEventListener('hashchange', () => {
const m = location.hash.match(/^#(\d+)$/);
if (m) show(parseInt(m[1], 10) - 1);
});
// Initial: hash > localStorage > 0
const hashMatch = location.hash.match(/^#(\d+)$/);
if (hashMatch) current = Math.min(parseInt(hashMatch[1], 10) - 1, deck.length - 1);
else try {
const v = parseInt(localStorage.getItem(storageKey), 10);
if (!isNaN(v) && v >= 0 && v < deck.length) current = v;
} catch (_) {}
fit();
show(current);
// Print: build a stack of all iframes so browser prints every slide
window.addEventListener('beforeprint', () => {
printStack.innerHTML = '';
deck.forEach(item => {
const f = document.createElement('iframe');
f.src = item.file;
printStack.appendChild(f);
});
printStack.style.display = 'block';
document.getElementById('stage').style.display = 'none';
});
window.addEventListener('afterprint', () => {
printStack.innerHTML = '';
printStack.style.display = 'none';
document.getElementById('stage').style.display = '';
});
})();
</script>
</body>
</html>
+405
View File
@@ -0,0 +1,405 @@
/**
* <deck-stage> — HTML幻灯片外壳web component
*
* 提供功能:
* - 固定尺寸canvas(默认1920×1080)+ auto-scale + letterbox
* - 键盘导航(←/→/Space/Home/End/Esc)
* - 左右点击区域导航
* - slide counter (当前/总数)
* - localStorage持久化当前slide
* - Speaker notes postMessage (支持外层渲染)
* - Hash导航 (#slide-5 跳到第5张)
* - Print-to-PDF支持 (Cmd+P / Ctrl+P 一页一slide)
* - 自动给每个slide添加 data-screen-label
*
* 用法:
* <deck-stage>
* <section>Slide 1</section>
* <section>Slide 2</section>
* </deck-stage>
*
* 自定义尺寸:
* <deck-stage width="1080" height="1920">...</deck-stage>
*
* Speaker notes:在<head>加
* <script type="application/json" id="speaker-notes">
* ["slide 1 notes", "slide 2 notes"]
* </script>
*/
(function() {
const STORAGE_KEY_PREFIX = 'deck-stage-slide-';
class DeckStage extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._currentSlide = 0;
this._slides = [];
this._storageKey = STORAGE_KEY_PREFIX + (location.pathname || 'default');
}
connectedCallback() {
this._width = parseInt(this.getAttribute('width')) || 1920;
this._height = parseInt(this.getAttribute('height')) || 1080;
this._render();
this._collectSlides();
this._setupEventListeners();
this._restoreSlide();
this._updateDisplay();
this._setupPrintStyles();
}
_render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
position: fixed;
inset: 0;
background: #000;
overflow: hidden;
font-family: -apple-system, 'SF Pro Text', 'PingFang SC', sans-serif;
}
:host([noscale]) .stage {
transform: none !important;
top: 0 !important;
left: 0 !important;
}
.stage {
position: absolute;
top: 50%;
left: 50%;
transform-origin: top left;
will-change: transform;
background: #fff;
}
.slide-wrapper {
width: 100%;
height: 100%;
position: relative;
}
::slotted(section) {
display: none;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
overflow: hidden;
}
::slotted(section.active) {
display: block;
}
.counter {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.6);
color: #fff;
padding: 6px 14px;
border-radius: 999px;
font-size: 13px;
font-variant-numeric: tabular-nums;
z-index: 100;
user-select: none;
opacity: 0.6;
transition: opacity 0.2s;
}
.counter:hover {
opacity: 1;
}
.nav-zone {
position: fixed;
top: 0;
bottom: 0;
width: 15%;
cursor: pointer;
z-index: 50;
}
.nav-zone.left { left: 0; }
.nav-zone.right { right: 0; }
.nav-hint {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 44px;
height: 44px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.6);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
opacity: 0;
transition: opacity 0.2s;
}
.nav-zone.left .nav-hint { left: 20px; }
.nav-zone.right .nav-hint { right: 20px; }
.nav-zone:hover .nav-hint {
opacity: 1;
}
@media print {
:host {
position: static;
background: #fff;
}
.counter, .nav-zone {
display: none !important;
}
.stage {
position: static;
transform: none !important;
page-break-after: always;
}
::slotted(section) {
display: block !important;
position: relative !important;
page-break-after: always;
width: 100%;
height: 100%;
}
}
</style>
<div class="stage" id="stage" style="width: ${this._width}px; height: ${this._height}px;">
<div class="slide-wrapper">
<slot></slot>
</div>
</div>
<div class="nav-zone left" id="navLeft">
<div class="nav-hint">‹</div>
</div>
<div class="nav-zone right" id="navRight">
<div class="nav-hint">›</div>
</div>
<div class="counter" id="counter">1 / 1</div>
`;
}
_collectSlides() {
this._slides = Array.from(this.querySelectorAll(':scope > section'));
this._slides.forEach((slide, idx) => {
if (!slide.hasAttribute('data-screen-label')) {
const num = String(idx + 1).padStart(2, '0');
slide.setAttribute('data-screen-label', num);
}
if (!slide.hasAttribute('data-om-validate')) {
slide.setAttribute('data-om-validate', '');
}
});
}
_setupEventListeners() {
window.addEventListener('resize', () => this._updateScale());
document.addEventListener('keydown', (e) => {
if (e.target.matches('input, textarea, [contenteditable]')) return;
switch (e.key) {
case 'ArrowRight':
case ' ':
case 'PageDown':
e.preventDefault();
this.next();
break;
case 'ArrowLeft':
case 'PageUp':
e.preventDefault();
this.prev();
break;
case 'Home':
e.preventDefault();
this.goTo(0);
break;
case 'End':
e.preventDefault();
this.goTo(this._slides.length - 1);
break;
}
});
this.shadowRoot.getElementById('navLeft').addEventListener('click', () => this.prev());
this.shadowRoot.getElementById('navRight').addEventListener('click', () => this.next());
window.addEventListener('hashchange', () => this._handleHash());
if (location.hash) {
setTimeout(() => this._handleHash(), 0);
}
const observer = new MutationObserver(() => {
if (this.hasAttribute('noscale')) {
this._updateScale();
}
});
observer.observe(this, { attributes: true, attributeFilter: ['noscale'] });
}
_handleHash() {
const match = location.hash.match(/^#slide-(\d+)$/);
if (match) {
const idx = parseInt(match[1]) - 1;
if (idx >= 0 && idx < this._slides.length) {
this.goTo(idx);
}
}
}
_restoreSlide() {
try {
const stored = localStorage.getItem(this._storageKey);
if (stored !== null) {
const idx = parseInt(stored);
if (idx >= 0 && idx < this._slides.length) {
this._currentSlide = idx;
}
}
} catch (e) {}
}
_saveSlide() {
try {
localStorage.setItem(this._storageKey, String(this._currentSlide));
} catch (e) {}
}
_updateScale() {
if (this.hasAttribute('noscale')) {
const stage = this.shadowRoot.getElementById('stage');
stage.style.transform = 'none';
stage.style.top = '0';
stage.style.left = '0';
return;
}
const stage = this.shadowRoot.getElementById('stage');
if (!stage) return;
const viewportW = window.innerWidth;
const viewportH = window.innerHeight;
const scale = Math.min(viewportW / this._width, viewportH / this._height);
const scaledW = this._width * scale;
const scaledH = this._height * scale;
const offsetX = (viewportW - scaledW) / 2;
const offsetY = (viewportH - scaledH) / 2;
stage.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
stage.style.top = '0';
stage.style.left = '0';
}
_updateDisplay() {
this._slides.forEach((slide, idx) => {
slide.classList.toggle('active', idx === this._currentSlide);
});
const counter = this.shadowRoot.getElementById('counter');
if (counter) {
counter.textContent = `${this._currentSlide + 1} / ${this._slides.length}`;
}
this._updateScale();
try {
window.postMessage({
slideIndexChanged: this._currentSlide,
totalSlides: this._slides.length
}, '*');
} catch (e) {}
try {
if (window.parent && window.parent !== window) {
window.parent.postMessage({
slideIndexChanged: this._currentSlide,
totalSlides: this._slides.length
}, '*');
}
} catch (e) {}
}
_setupPrintStyles() {
const printStyle = document.createElement('style');
printStyle.textContent = `
@media print {
@page {
size: ${this._width}px ${this._height}px;
margin: 0;
}
body {
margin: 0;
padding: 0;
}
deck-stage {
position: static !important;
}
deck-stage > section {
display: block !important;
position: relative !important;
width: ${this._width}px !important;
height: ${this._height}px !important;
page-break-after: always;
overflow: hidden;
}
deck-stage > section:last-child {
page-break-after: auto;
}
}
`;
document.head.appendChild(printStyle);
}
next() {
if (this._currentSlide < this._slides.length - 1) {
this._currentSlide++;
this._saveSlide();
this._updateDisplay();
}
}
prev() {
if (this._currentSlide > 0) {
this._currentSlide--;
this._saveSlide();
this._updateDisplay();
}
}
goTo(idx) {
if (idx >= 0 && idx < this._slides.length) {
this._currentSlide = idx;
this._saveSlide();
this._updateDisplay();
}
}
get currentSlide() {
return this._currentSlide;
}
get totalSlides() {
return this._slides.length;
}
}
customElements.define('deck-stage', DeckStage);
window.DeckStage = DeckStage;
})();
+205
View File
@@ -0,0 +1,205 @@
/**
* DesignCanvas — 变体并排网格布局
*
* 用于展示2+个静态设计variations让用户对比选择。
* 每个variation有label,可hover放大。
*
* 用法:
* <DesignCanvas
* title="Hero区设计探索"
* subtitle="3个方向对比"
* columns={3}
* >
* <Variation label="Minimal" description="极简克制版">
* <div>...你的设计1...</div>
* </Variation>
* <Variation label="Editorial" description="杂志编辑风">
* <div>...你的设计2...</div>
* </Variation>
* <Variation label="Brutalist" description="粗粝原始">
* <div>...你的设计3...</div>
* </Variation>
* </DesignCanvas>
*
* 配合React+Babel使用。放在合适的script里,然后window.DesignCanvas/window.Variation可用。
*/
const canvasStyles = {
container: {
minHeight: '100vh',
background: '#F5F5F0',
padding: '40px 60px',
fontFamily: '-apple-system, "SF Pro Text", "PingFang SC", sans-serif',
},
header: {
marginBottom: 48,
maxWidth: 900,
},
title: {
fontSize: 36,
fontWeight: 600,
marginBottom: 12,
color: '#1A1A1A',
letterSpacing: '-0.02em',
},
subtitle: {
fontSize: 16,
color: '#666',
lineHeight: 1.5,
},
grid: {
display: 'grid',
gap: 32,
},
cell: {
display: 'flex',
flexDirection: 'column',
gap: 12,
},
cellHeader: {
display: 'flex',
alignItems: 'baseline',
gap: 12,
paddingBottom: 8,
borderBottom: '1px solid #E0E0DA',
},
label: {
fontSize: 14,
fontWeight: 600,
color: '#1A1A1A',
letterSpacing: '-0.01em',
},
description: {
fontSize: 13,
color: '#888',
},
frame: {
background: '#fff',
borderRadius: 4,
border: '1px solid #E0E0DA',
overflow: 'hidden',
position: 'relative',
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
cursor: 'pointer',
},
frameInner: {
position: 'relative',
width: '100%',
},
badge: {
position: 'absolute',
top: 12,
left: 12,
background: 'rgba(0, 0, 0, 0.7)',
color: '#fff',
padding: '3px 8px',
borderRadius: 4,
fontSize: 11,
fontWeight: 500,
letterSpacing: '0.5px',
textTransform: 'uppercase',
zIndex: 10,
pointerEvents: 'none',
},
};
function DesignCanvas({ title, subtitle, columns = 3, children }) {
const [expanded, setExpanded] = React.useState(null);
const gridStyle = {
...canvasStyles.grid,
gridTemplateColumns: `repeat(${columns}, 1fr)`,
};
return (
<div style={canvasStyles.container}>
{(title || subtitle) && (
<div style={canvasStyles.header}>
{title && <h1 style={canvasStyles.title}>{title}</h1>}
{subtitle && <p style={canvasStyles.subtitle}>{subtitle}</p>}
</div>
)}
<div style={gridStyle}>
{React.Children.map(children, (child, idx) =>
React.isValidElement(child)
? React.cloneElement(child, {
_index: idx,
_expanded: expanded === idx,
_onToggle: () => setExpanded(expanded === idx ? null : idx),
})
: child
)}
</div>
{expanded !== null && (
<div
onClick={() => setExpanded(null)}
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0, 0, 0, 0.75)',
zIndex: 1000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 40,
cursor: 'zoom-out',
}}
>
<div
onClick={e => e.stopPropagation()}
style={{
background: '#fff',
borderRadius: 8,
overflow: 'hidden',
maxWidth: '90vw',
maxHeight: '90vh',
position: 'relative',
}}
>
{React.Children.toArray(children)[expanded]}
</div>
</div>
)}
</div>
);
}
function Variation({ label, description, number, children, _index, _expanded, _onToggle, aspectRatio = '4 / 3' }) {
const displayNumber = number || String(_index + 1).padStart(2, '0');
return (
<div style={canvasStyles.cell}>
<div style={canvasStyles.cellHeader}>
<span style={{ ...canvasStyles.label, color: '#999', fontFamily: 'ui-monospace, monospace', fontSize: 12 }}>
{displayNumber}
</span>
<span style={canvasStyles.label}>{label}</span>
{description && <span style={canvasStyles.description}>— {description}</span>}
</div>
<div
onClick={_onToggle}
style={{
...canvasStyles.frame,
aspectRatio,
}}
onMouseEnter={e => {
e.currentTarget.style.boxShadow = '0 8px 24px rgba(0,0,0,0.08)';
}}
onMouseLeave={e => {
e.currentTarget.style.boxShadow = 'none';
}}
>
<div style={canvasStyles.frameInner}>
{children}
</div>
</div>
</div>
);
}
if (typeof window !== 'undefined') {
Object.assign(window, { DesignCanvas, Variation });
}
+192
View File
@@ -0,0 +1,192 @@
/**
* IosFrame — iPhone设备边框
*
* 参考iPhone 15 Pro(393×852 logical pixels)
* 含:灵动岛 + 状态栏(时间/信号/电池)+ Home Indicator + 圆角
*
* 用法:
* <IosFrame time="9:41" battery={85}>
* <YourAppContent />
* </IosFrame>
*
* 自定义:
* <IosFrame width={390} height={844} darkMode showKeyboard>
* ...
* </IosFrame>
*/
const iosFrameStyles = {
wrapper: {
display: 'inline-block',
padding: 12,
background: '#000',
borderRadius: 60,
boxShadow: '0 0 0 2px #1f2937, 0 20px 60px rgba(0,0,0,0.3)',
position: 'relative',
},
screen: {
position: 'relative',
borderRadius: 48,
overflow: 'hidden',
background: '#fff',
},
statusBar: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 54,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 32px 0 32px',
fontSize: 16,
fontWeight: 600,
fontFamily: '-apple-system, "SF Pro Text", sans-serif',
zIndex: 20,
pointerEvents: 'none',
},
dynamicIsland: {
position: 'absolute',
top: 12,
left: '50%',
transform: 'translateX(-50%)',
width: 124,
height: 36,
background: '#000',
borderRadius: 999,
zIndex: 30,
},
statusIcons: {
display: 'flex',
alignItems: 'center',
gap: 6,
},
signalIcon: {
display: 'flex',
alignItems: 'flex-end',
gap: 2,
height: 12,
},
signalBar: {
width: 3,
background: 'currentColor',
borderRadius: 1,
},
wifiIcon: {
width: 16,
height: 12,
position: 'relative',
},
batteryIcon: {
width: 26,
height: 12,
border: '1.5px solid currentColor',
borderRadius: 3,
padding: 1,
position: 'relative',
opacity: 0.8,
},
batteryCap: {
position: 'absolute',
top: 3,
right: -3,
width: 2,
height: 6,
background: 'currentColor',
borderRadius: '0 1px 1px 0',
},
content: {
position: 'absolute',
top: 54,
left: 0,
right: 0,
bottom: 34,
overflow: 'auto',
},
homeIndicator: {
position: 'absolute',
bottom: 10,
left: '50%',
transform: 'translateX(-50%)',
width: 140,
height: 5,
background: 'rgba(0,0,0,0.3)',
borderRadius: 999,
zIndex: 10,
},
homeIndicatorDark: {
background: 'rgba(255,255,255,0.5)',
},
};
function IosFrame({
children,
width = 393,
height = 852,
time = '9:41',
battery = 100,
darkMode = false,
showStatusBar = true,
showDynamicIsland = true,
showHomeIndicator = true,
}) {
const textColor = darkMode ? '#fff' : '#000';
return (
<div style={iosFrameStyles.wrapper}>
<div style={{
...iosFrameStyles.screen,
width,
height,
background: darkMode ? '#000' : '#fff',
}}>
{showStatusBar && (
<div style={{ ...iosFrameStyles.statusBar, color: textColor }}>
<span>{time}</span>
<div style={iosFrameStyles.statusIcons}>
<div style={iosFrameStyles.signalIcon}>
<div style={{ ...iosFrameStyles.signalBar, height: 4 }} />
<div style={{ ...iosFrameStyles.signalBar, height: 6 }} />
<div style={{ ...iosFrameStyles.signalBar, height: 9 }} />
<div style={{ ...iosFrameStyles.signalBar, height: 11 }} />
</div>
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" style={{ color: textColor }}>
<path d="M8 11.5a1 1 0 100-2 1 1 0 000 2z" fill="currentColor" />
<path d="M3 7.5a7 7 0 0110 0" stroke="currentColor" strokeWidth="1.3" fill="none" strokeLinecap="round" />
<path d="M1 4.5a11 11 0 0114 0" stroke="currentColor" strokeWidth="1.3" fill="none" strokeLinecap="round" opacity="0.7" />
</svg>
<div style={iosFrameStyles.batteryIcon}>
<div style={{
width: `${battery}%`,
height: '100%',
background: 'currentColor',
borderRadius: 1,
opacity: 0.9,
}} />
<div style={iosFrameStyles.batteryCap} />
</div>
</div>
</div>
)}
{showDynamicIsland && <div style={iosFrameStyles.dynamicIsland} />}
<div style={iosFrameStyles.content}>
{children}
</div>
{showHomeIndicator && (
<div style={{
...iosFrameStyles.homeIndicator,
...(darkMode ? iosFrameStyles.homeIndicatorDark : {}),
}} />
)}
</div>
</div>
);
}
if (typeof window !== 'undefined') {
window.IosFrame = IosFrame;
}
+96
View File
@@ -0,0 +1,96 @@
/**
* MacosWindow — macOS应用窗口边框(含traffic lights)
*
* 用法:
* <MacosWindow title="Finder">
* <YourAppContent />
* </MacosWindow>
*/
const macosWindowStyles = {
window: {
display: 'inline-block',
background: '#fff',
borderRadius: 10,
overflow: 'hidden',
boxShadow: '0 30px 80px rgba(0,0,0,0.25), 0 0 0 0.5px rgba(0,0,0,0.15)',
},
titleBar: {
height: 38,
background: 'linear-gradient(to bottom, #e8e8e8, #d8d8d8)',
display: 'flex',
alignItems: 'center',
padding: '0 14px',
borderBottom: '0.5px solid rgba(0,0,0,0.1)',
position: 'relative',
userSelect: 'none',
},
trafficLights: {
display: 'flex',
gap: 8,
alignItems: 'center',
},
light: {
width: 12,
height: 12,
borderRadius: '50%',
border: '0.5px solid rgba(0,0,0,0.15)',
},
close: { background: '#ff5f57' },
minimize: { background: '#febc2e' },
maximize: { background: '#28c840' },
title: {
position: 'absolute',
left: 0,
right: 0,
textAlign: 'center',
fontSize: 13,
color: '#333',
fontWeight: 500,
fontFamily: '-apple-system, "SF Pro Text", sans-serif',
pointerEvents: 'none',
},
content: {
position: 'relative',
overflow: 'auto',
},
titleBarDark: {
background: 'linear-gradient(to bottom, #3c3c3c, #2c2c2c)',
borderBottom: '0.5px solid rgba(255,255,255,0.1)',
},
titleDark: {
color: '#ddd',
},
};
function MacosWindow({ title = '', width = 900, height = 600, darkMode = false, children }) {
return (
<div style={{ ...macosWindowStyles.window, background: darkMode ? '#1e1e1e' : '#fff' }}>
<div style={{
...macosWindowStyles.titleBar,
...(darkMode ? macosWindowStyles.titleBarDark : {}),
}}>
<div style={macosWindowStyles.trafficLights}>
<div style={{ ...macosWindowStyles.light, ...macosWindowStyles.close }} />
<div style={{ ...macosWindowStyles.light, ...macosWindowStyles.minimize }} />
<div style={{ ...macosWindowStyles.light, ...macosWindowStyles.maximize }} />
</div>
{title && (
<div style={{
...macosWindowStyles.title,
...(darkMode ? macosWindowStyles.titleDark : {}),
}}>
{title}
</div>
)}
</div>
<div style={{ ...macosWindowStyles.content, width, height }}>
{children}
</div>
</div>
);
}
if (typeof window !== 'undefined') {
window.MacosWindow = MacosWindow;
}
+71
View File
@@ -0,0 +1,71 @@
{
"_meta": {
"description": "个人素材索引模板 — 复制此文件并填入你的真实数据",
"how_to_use": "1. 复制此文件到你的 agent 私有 memory 目录(Claude Code: ~/.claude/memory/personal-asset-index.json;其他 agent 按其约定) 2. 填入你的真实信息 3. huashu-design skill 在需要个人素材/锚定当前品牌时会自动读取",
"note": "真实数据文件不要放在 skill 目录内,避免随 skill 分发泄露隐私"
},
"identity": {
"real_name": "你的真名",
"pen_names": ["笔名1", "笔名2"],
"english_name": "English Name",
"title": "你的头衔/一句话介绍",
"bio_short": "50-100字简介",
"bio_long": "200-300字详细介绍",
"avatar_url": "头像URL",
"source": "数据来源备注"
},
"contact": {
"email": "your@email.com",
"wechat_personal": "微信号",
"source": "数据来源备注"
},
"social_media": {
"github": {
"url": "https://github.com/yourname",
"username": "yourname"
},
"youtube": {
"url": "https://www.youtube.com/@YourChannel",
"channel_name": "频道名"
},
"source": "数据来源备注"
},
"websites": {
"main_site": {
"url": "https://yoursite.com",
"description": "网站描述",
"local_path": "/path/to/local/project/"
}
},
"products": {
"product_1": {
"name": "产品名",
"type": "iOS App / Web App / CLI Tool / 电子书",
"achievement": "主要成就",
"icon_path": "/path/to/icon.png",
"project_path": "/path/to/project/"
}
},
"stats": {
"social_followers": "粉丝数",
"product_users": "用户数",
"source": "数据来源备注"
},
"design_assets": {
"article_images": {
"base_path": "/path/to/images/",
"notable_sets": []
}
},
"knowledge_base": {
"wechat_articles": "/path/to/knowledge_base/"
}
}
+115
View File
@@ -0,0 +1,115 @@
# Design Philosophy Showcases — 样例资产索引
> 8 种场景 × 3 种风格 = 24 个预制设计样例
> 用于 Phase 3 推荐设计方向时,直接展示「这个风格做出来长什么样」
## 风格说明
| 代号 | 流派 | 风格名称 | 视觉气质 |
|------|------|---------|---------|
| **Pentagram** | 信息建筑派 | Pentagram / Michael Bierut | 黑白克制、瑞士网格、强字体层级、#E63946红色强调 |
| **Build** | 极简主义派 | Build Studio | 奢侈品级留白(70%+)、微妙字重(200-600)、#D4A574暖金、精致 |
| **Takram** | 东方哲学派 | Takram | 柔和科技感、自然色(米色/灰/绿)、圆角、图表如艺术 |
## 场景速查表
### 内容设计场景
| # | 场景 | 规格 | Pentagram | Build | Takram |
|---|------|------|-----------|-------|--------|
| 1 | 公众号封面 | 1200×510 | `cover/cover-pentagram` | `cover/cover-build` | `cover/cover-takram` |
| 2 | PPT数据页 | 1920×1080 | `ppt/ppt-pentagram` | `ppt/ppt-build` | `ppt/ppt-takram` |
| 3 | 竖版信息图 | 1080×1920 | `infographic/infographic-pentagram` | `infographic/infographic-build` | `infographic/infographic-takram` |
### 网站设计场景
| # | 场景 | 规格 | Pentagram | Build | Takram |
|---|------|------|-----------|-------|--------|
| 4 | 个人主页 | 1440×900 | `website-homepage/homepage-pentagram` | `website-homepage/homepage-build` | `website-homepage/homepage-takram` |
| 5 | AI导航站 | 1440×900 | `website-ai-nav/ainav-pentagram` | `website-ai-nav/ainav-build` | `website-ai-nav/ainav-takram` |
| 6 | AI写作工具 | 1440×900 | `website-ai-writing/aiwriting-pentagram` | `website-ai-writing/aiwriting-build` | `website-ai-writing/aiwriting-takram` |
| 7 | SaaS落地页 | 1440×900 | `website-saas/saas-pentagram` | `website-saas/saas-build` | `website-saas/saas-takram` |
| 8 | 开发者文档 | 1440×900 | `website-devdocs/devdocs-pentagram` | `website-devdocs/devdocs-build` | `website-devdocs/devdocs-takram` |
> 每个条目同时有 `.html`(源码)和 `.png`(截图)两个文件
## 使用说明
### Phase 3 推荐时引用
推荐设计方向后,可展示对应场景的预制截图:
```
「这是 Pentagram 风格做公众号封面的效果 → [展示 cover/cover-pentagram.png]」
「Takram 风格做 PPT 数据页是这种感觉 → [展示 ppt/ppt-takram.png]」
```
### 场景匹配优先级
1. 用户需求的场景有精确匹配 → 直接展示对应场景
2. 无精确匹配但类型相近 → 展示最近似的场景(如「产品官网」→ 展示 SaaS 落地页)
3. 完全不匹配 → 跳过预制样例,直接进 Phase 3.5 现场生成
### 横向对比展示
同一场景的 3 个风格适合并排展示,帮助用户直观比较:
- 「这是同一个公众号封面,分别用 3 种风格实现的效果」
- 展示顺序:Pentagram(理性克制)→ Build(奢华极简)→ Takram(柔和温暖)
## 内容详情
### 公众号封面(cover/)
- 内容:Claude Code Agent 工作流 — 8 个并行 Agent 架构
- Pentagram:巨大红色「8」+ 瑞士网格线 + 数据条
- Build:超细字重「Agent」悬浮于 70% 留白中 + 暖金细线
- Takram:8 节点放射状流程图作为艺术品 + 米色底
### PPT数据页(ppt/)
- 内容:GLM-4.7 开源模型 Coding 能力突破(AIME 95.7 / SWE-bench 73.8% / τ²-Bench 87.4)
- Pentagram:260px「95.7」锚点 + 红/灰/浅灰对比条形图
- Build:三组 120px 超细数字悬浮 + 暖金渐变对比条
- Takram:SVG 雷达图 + 三色叠加 + 圆角数据卡片
### 竖版信息图(infographic/)
- 内容:AI 记忆系统 CLAUDE.md 从 93KB 优化到 22KB
- Pentagram:巨大「93→22」数字 + 编号区块 + CSS 数据条
- Build:极致留白 + 柔影卡片 + 暖金连接线
- Takram:SVG 环形图 + 有机曲线流程图 + 毛玻璃卡片
### 个人主页(website-homepage/)
- 内容:独立开发者 Alex Chen 的作品集首页
- Pentagram:112px 大名 + 瑞士网格分栏 + 编辑数字
- Build:玻璃态导航 + 悬浮统计卡片 + 超细字重
- Takram:纸质纹理 + 小圆形头像 + 发丝细分隔线 + 不对称布局
### AI导航站(website-ai-nav/)
- 内容:AI Compass — 500+ AI 工具目录
- Pentagram:方角搜索框 + 编号工具列表 + 大写分类标签
- Build:圆角搜索框 + 精致白色工具卡片 + 药丸标签
- Takram:有机错位卡片布局 + 柔和分类标签 + 图表式连接
### AI写作工具(website-ai-writing/)
- 内容:Inkwell — AI 写作助手
- Pentagram:86px 大标题 + 线框编辑器模型 + 网格特性列
- Build:漂浮编辑器卡片 + 暖金 CTA + 奢华写作体验
- Takram:诗意衬线标题 + 有机编辑器 + 流程图
### SaaS落地页(website-saas/)
- 内容:Meridian — 商业智能分析平台
- Pentagram:黑白分栏 + 结构化仪表盘 + 140px「3x」锚点
- Build:悬浮仪表盘卡片 + SVG 面积图 + 暖金渐变
- Takram:圆角柱状图 + 流程节点 + 柔和地球色
### 开发者文档(website-devdocs/)
- 内容:Nexus API — 统一 AI 模型网关
- Pentagram:左侧导航栏 + 方角代码块 + 红色字符串高亮
- Build:居中漂浮代码卡片 + 柔影 + 暖金图标
- Takram:米色代码块 + 流程图连接 + 虚线特性卡片
## 文件统计
- HTML 源文件:24 个
- PNG 截图:24 个
- 总资产:48 个文件
---
**版本**:v1.0
**创建日期**:2026-02-13
**适用于**:design-philosophy skill Phase 3 推荐环节
+235
View File
@@ -0,0 +1,235 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1200">
<title>Claude Code Agent - Build Studio Style</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1200px;
height: 510px;
overflow: hidden;
margin: 0;
background: #FAFAF8;
font-family: 'Inter', sans-serif;
position: relative;
}
/* Subtle top gradient wash */
.wash {
position: absolute;
top: 0;
left: 0;
width: 1200px;
height: 510px;
background: radial-gradient(ellipse 800px 400px at 30% 40%, rgba(212, 165, 116, 0.06) 0%, transparent 70%);
z-index: 0;
}
/* Main layout */
.layout {
position: absolute;
top: 0;
left: 0;
width: 1200px;
height: 510px;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.center-block {
text-align: center;
max-width: 700px;
margin-top: -24px; /* slight upward shift for golden ratio vertical center */
}
/* Floating "Agent" */
.floating-agent {
font-family: 'Inter', sans-serif;
font-weight: 200;
font-size: 128px;
letter-spacing: -4px;
color: #1A1A18;
line-height: 1;
margin-bottom: 16px;
position: relative;
}
.floating-agent span {
position: relative;
display: inline-block;
}
/* Slight weight shift on first letter for visual interest */
.floating-agent .accent-letter {
font-weight: 300;
color: #2A2A28;
}
/* Gold underline accent */
.gold-line {
width: 48px;
height: 1px;
background: #D4A574;
margin: 0 auto 32px;
opacity: 0.7;
}
/* Subtitle — label tier: smallest text, widest spacing */
.subtitle {
font-family: 'Inter', sans-serif;
font-weight: 400;
font-size: 10px;
letter-spacing: 6px;
text-transform: uppercase;
color: #B0ACA4;
margin-bottom: 24px;
}
/* Description line — body tier */
.desc {
font-family: 'Inter', sans-serif;
font-weight: 300;
font-size: 13px;
color: #A8A4A0;
letter-spacing: 0.3px;
line-height: 2;
max-width: 400px;
margin: 0 auto;
}
/* Minimal agent indicators — 8 thin vertical lines */
.agent-indicators {
position: absolute;
bottom: 48px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 16px;
align-items: flex-end;
z-index: 2;
}
.indicator {
width: 1px;
background: #D8D4CE;
border-radius: 0.5px;
}
.indicator.gold {
background: #D4A574;
width: 1.5px;
opacity: 0.8;
}
/* Corner marks */
.corner-mark {
position: absolute;
z-index: 2;
}
.corner-mark svg {
display: block;
}
.corner-tl { top: 48px; left: 48px; }
.corner-br { bottom: 48px; right: 48px; transform: rotate(180deg); }
/* Side text */
.side-label {
position: absolute;
font-family: 'Inter', sans-serif;
font-weight: 400;
font-size: 8px;
letter-spacing: 4px;
text-transform: uppercase;
color: #CBC7C0;
z-index: 2;
}
.side-left {
left: 48px;
top: 50%;
transform: translateY(-50%) rotate(-90deg);
transform-origin: center center;
}
.side-right {
right: 48px;
top: 50%;
transform: translateY(-50%) rotate(90deg);
transform-origin: center center;
}
/* Removed shadow-card — Build purity demands uninterrupted whitespace */
/* Number 8 whisper */
.number-whisper {
position: absolute;
top: 48px;
right: 96px;
font-family: 'Inter', sans-serif;
font-weight: 200;
font-size: 24px;
color: #D4A574;
opacity: 0.35;
z-index: 2;
}
</style>
</head>
<body>
<div class="wash"></div>
<!-- Corner marks -->
<div class="corner-mark corner-tl">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0L0 20" stroke="#D4A574" stroke-width="0.5" opacity="0.4"/>
<path d="M0 0L20 0" stroke="#D4A574" stroke-width="0.5" opacity="0.4"/>
</svg>
</div>
<div class="corner-mark corner-br">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0L0 20" stroke="#D4A574" stroke-width="0.5" opacity="0.4"/>
<path d="M0 0L20 0" stroke="#D4A574" stroke-width="0.5" opacity="0.4"/>
</svg>
</div>
<!-- Side labels -->
<div class="side-label side-left">Claude Code</div>
<div class="side-label side-right">Parallel Workflow</div>
<!-- Number whisper -->
<div class="number-whisper">8</div>
<!-- Main content -->
<div class="layout">
<div class="center-block">
<div class="subtitle">Parallel Architecture</div>
<div class="floating-agent"><span><span class="accent-letter">A</span>gent</span></div>
<div class="gold-line"></div>
<div class="desc">
Eight autonomous agents orchestrated in parallel,<br>
each solving a distinct piece of the whole.
</div>
</div>
</div>
<!-- Agent indicators -->
<div class="agent-indicators">
<div class="indicator" style="height: 20px;"></div>
<div class="indicator" style="height: 28px;"></div>
<div class="indicator gold" style="height: 36px;"></div>
<div class="indicator" style="height: 22px;"></div>
<div class="indicator" style="height: 32px;"></div>
<div class="indicator gold" style="height: 40px;"></div>
<div class="indicator" style="height: 24px;"></div>
<div class="indicator" style="height: 30px;"></div>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

+229
View File
@@ -0,0 +1,229 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1200">
<title>Agent Parallel — Pentagram Style Cover</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1200px;
height: 510px;
overflow: hidden;
margin: 0;
background: #FFFFFF;
font-family: 'Helvetica Neue', 'Arial', sans-serif;
position: relative;
}
/* Grid rules — Swiss grid visible structure */
.rule-h {
position: absolute;
left: 64px;
right: 64px;
height: 1px;
background: #000;
opacity: 0.06;
}
.rule-v {
position: absolute;
top: 0;
bottom: 0;
width: 1px;
background: #000;
opacity: 0.04;
}
/* Giant typographic element — the "8" bleeds off right edge */
.type-anchor {
position: absolute;
right: -60px;
top: 50%;
transform: translateY(-50%);
font-family: 'Helvetica Neue', Arial, sans-serif;
font-weight: 900;
font-size: 640px;
line-height: 0.82;
color: #000;
opacity: 0.07;
z-index: 0;
user-select: none;
}
/* Red geometric dot grid — 8 dots representing 8 agents */
.dot-grid {
position: absolute;
right: 340px;
top: 50%;
transform: translateY(-50%);
display: grid;
grid-template-columns: repeat(4, 24px);
grid-template-rows: repeat(2, 24px);
gap: 16px;
z-index: 1;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #000;
opacity: 0.12;
align-self: center;
justify-self: center;
}
.dot.active {
background: #E63946;
opacity: 0.8;
width: 10px;
height: 10px;
}
/* Primary content zone — left-aligned on Swiss grid */
.content {
position: absolute;
left: 64px;
top: 56px;
z-index: 2;
}
.label {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 11px;
font-weight: 700;
letter-spacing: 4px;
text-transform: uppercase;
color: #E63946;
margin-bottom: 16px;
}
.title {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-weight: 900;
font-size: 120px;
line-height: 0.9;
color: #000;
letter-spacing: -5px;
}
.title .accent {
color: #E63946;
}
/* Bottom information bar */
.bottom-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 48px;
background: #000;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 64px;
}
.bottom-left {
display: flex;
align-items: center;
gap: 24px;
}
.bottom-stat {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 11px;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
color: #fff;
opacity: 0.6;
}
.bottom-stat strong {
color: #E63946;
opacity: 1;
font-size: 16px;
margin-right: 6px;
}
.bottom-right {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 10px;
font-weight: 700;
letter-spacing: 3px;
text-transform: uppercase;
color: #fff;
opacity: 0.4;
}
/* Subtitle */
.subtitle {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
font-weight: 500;
color: #999;
letter-spacing: 0.5px;
margin-top: 20px;
}
/* Horizontal red rule through center */
.center-rule {
position: absolute;
left: 64px;
width: 240px;
height: 3px;
background: #E63946;
top: 306px;
z-index: 2;
}
</style>
</head>
<body>
<!-- Grid structure -->
<div class="rule-h" style="top: 56px;"></div>
<div class="rule-v" style="left: 64px;"></div>
<div class="rule-v" style="left: 600px;"></div>
<div class="rule-v" style="right: 64px;"></div>
<!-- Typographic anchor — bleeds right -->
<div class="type-anchor">8</div>
<!-- 8-dot grid representing agents -->
<div class="dot-grid">
<div class="dot active"></div>
<div class="dot"></div>
<div class="dot active"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot active"></div>
<div class="dot"></div>
<div class="dot active"></div>
</div>
<!-- Content -->
<div class="content">
<div class="label">Claude Code Architecture</div>
<div class="title">Agent<br><span class="accent">Parallel</span></div>
<div class="subtitle">8 autonomous agents running in unified workflow</div>
</div>
<!-- Red horizontal rule -->
<div class="center-rule"></div>
<!-- Black bottom bar with data -->
<div class="bottom-bar">
<div class="bottom-left">
<div class="bottom-stat"><strong>8</strong>Agents</div>
<div class="bottom-stat"><strong>3.2x</strong>Faster</div>
<div class="bottom-stat"><strong>1</strong>Workflow</div>
</div>
<div class="bottom-right">Pentagram Design System</div>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+288
View File
@@ -0,0 +1,288 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1200">
<title>Claude Code Agent - Takram Style</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&family=Noto+Serif+SC:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1200px;
height: 510px;
overflow: hidden;
margin: 0;
background: #F5F0EB;
font-family: 'Inter', sans-serif;
position: relative;
}
/* Subtle paper texture overlay */
.texture {
position: absolute;
top: 0; left: 0;
width: 1200px;
height: 510px;
background:
radial-gradient(ellipse 500px 400px at 72% 50%, rgba(168, 181, 160, 0.06) 0%, transparent 70%),
radial-gradient(ellipse 300px 250px at 15% 40%, rgba(232, 228, 220, 0.2) 0%, transparent 60%);
z-index: 0;
}
/* Flow diagram — the art piece */
.diagram {
position: absolute;
top: 0; left: 0;
width: 1200px;
height: 510px;
z-index: 1;
}
/* Left text panel */
.text-panel {
position: absolute;
left: 72px;
top: 56px;
z-index: 2;
max-width: 360px;
}
.text-panel .label {
font-family: 'Inter', sans-serif;
font-weight: 500;
font-size: 9px;
letter-spacing: 3px;
text-transform: uppercase;
color: #6B8F71;
margin-bottom: 20px;
opacity: 0.8;
}
.text-panel .title-main {
font-family: 'Noto Serif SC', serif;
font-weight: 500;
font-size: 52px;
color: #2D3436;
line-height: 1.15;
letter-spacing: -0.5px;
margin-bottom: 4px;
}
.text-panel .title-sub {
font-family: 'Noto Serif SC', serif;
font-weight: 300;
font-size: 20px;
color: #6D685F;
line-height: 1.4;
margin-bottom: 16px;
}
.text-panel .title-en {
font-family: 'Inter', sans-serif;
font-weight: 300;
font-size: 13px;
color: #9A958D;
letter-spacing: 0.3px;
line-height: 1.8;
}
/* Bottom annotation */
.annotation {
position: absolute;
left: 72px;
bottom: 40px;
z-index: 2;
}
.annotation .note {
font-family: 'Inter', sans-serif;
font-weight: 400;
font-size: 10px;
color: #B0AAA0;
letter-spacing: 0.3px;
}
.annotation .note-serif {
font-family: 'Noto Serif SC', serif;
font-weight: 300;
font-size: 11px;
color: #9A958D;
margin-top: 4px;
}
/* Right side number */
.spec-number {
position: absolute;
right: 72px;
bottom: 40px;
font-family: 'Inter', sans-serif;
font-weight: 300;
font-size: 10px;
color: #B0AAA0;
letter-spacing: 1px;
z-index: 2;
}
/* Agent node styling */
.node-label {
font-family: 'Inter', sans-serif;
font-size: 9px;
font-weight: 400;
fill: #8A857D;
letter-spacing: 0.5px;
}
.node-label-serif {
font-family: 'Noto Serif SC', serif;
font-size: 11px;
font-weight: 400;
fill: #6D685F;
}
.node-index {
font-family: 'Inter', sans-serif;
font-size: 7px;
font-weight: 400;
fill: #B0AAA0;
letter-spacing: 0.5px;
}
</style>
</head>
<body>
<div class="texture"></div>
<!-- Text panel -->
<div class="text-panel">
<div class="label">Speculative Architecture</div>
<div class="title-main">协作智能体</div>
<div class="title-sub">Parallel Workflow</div>
<div class="title-en">
Eight agents, each autonomous,<br>
converging toward a shared intent.
</div>
</div>
<!-- The diagram as art -->
<svg class="diagram" viewBox="0 0 1200 510" xmlns="http://www.w3.org/2000/svg">
<!-- Subtle background grid hints (Takram spec-drawing aesthetic) -->
<line x1="440" y1="0" x2="440" y2="510" stroke="#E8E4DC" stroke-width="0.3" opacity="0.4"/>
<line x1="760" y1="0" x2="760" y2="510" stroke="#E8E4DC" stroke-width="0.3" opacity="0.3"/>
<!-- Subtle outer orbital paths — layered ellipses for depth -->
<ellipse cx="760" cy="255" rx="260" ry="195" fill="none" stroke="#E0DCD5" stroke-width="0.5" stroke-dasharray="1,8" opacity="0.5"/>
<ellipse cx="760" cy="255" rx="180" ry="135" fill="none" stroke="#D8D3CB" stroke-width="0.4" stroke-dasharray="2,6" opacity="0.35"/>
<!-- Central orchestrator node — refined with layered depth -->
<circle cx="760" cy="255" r="48" fill="none" stroke="#6B8F71" stroke-width="0.5" opacity="0.12" stroke-dasharray="2,4"/>
<circle cx="760" cy="255" r="36" fill="none" stroke="#6B8F71" stroke-width="0.8" opacity="0.18"/>
<circle cx="760" cy="255" r="24" fill="none" stroke="#6B8F71" stroke-width="1.2" opacity="0.3"/>
<circle cx="760" cy="255" r="14" fill="rgba(107,143,113,0.05)"/>
<circle cx="760" cy="255" r="5.5" fill="#6B8F71" opacity="0.55"/>
<circle cx="760" cy="255" r="2" fill="#6B8F71" opacity="0.9"/>
<text x="760" y="312" text-anchor="middle" class="node-label-serif">Orchestrator</text>
<!-- Subtle cross-hair on center -->
<line x1="748" y1="255" x2="730" y2="255" stroke="#6B8F71" stroke-width="0.3" opacity="0.15"/>
<line x1="772" y1="255" x2="790" y2="255" stroke="#6B8F71" stroke-width="0.3" opacity="0.15"/>
<line x1="760" y1="243" x2="760" y2="225" stroke="#6B8F71" stroke-width="0.3" opacity="0.15"/>
<line x1="760" y1="267" x2="760" y2="285" stroke="#6B8F71" stroke-width="0.3" opacity="0.15"/>
<!-- Agent 1 — top-left (Research) -->
<line x1="738" y1="232" x2="598" y2="118" stroke="#C8C2B8" stroke-width="0.7" stroke-dasharray="3,5"/>
<rect x="560" y="92" width="76" height="38" rx="14" fill="rgba(245,240,235,0.7)" stroke="#B8B2A8" stroke-width="0.8"/>
<circle cx="598" cy="111" r="3.5" fill="#6B8F71" opacity="0.5"/>
<text x="598" y="144" text-anchor="middle" class="node-label">Research</text>
<text x="560" y="88" class="node-index">01</text>
<!-- Agent 2 — top (Analysis) -->
<line x1="760" y1="217" x2="760" y2="145" stroke="#C8C2B8" stroke-width="0.7" stroke-dasharray="3,5"/>
<rect x="722" y="100" width="76" height="38" rx="14" fill="rgba(245,240,235,0.7)" stroke="#B8B2A8" stroke-width="0.8"/>
<circle cx="760" cy="119" r="3.5" fill="#6B8F71" opacity="0.5"/>
<text x="760" y="152" text-anchor="middle" class="node-label">Analysis</text>
<text x="722" y="96" class="node-index">02</text>
<!-- Agent 3 — top-right (Code) -->
<line x1="782" y1="232" x2="918" y2="118" stroke="#C8C2B8" stroke-width="0.7" stroke-dasharray="3,5"/>
<rect x="884" y="92" width="76" height="38" rx="14" fill="rgba(245,240,235,0.7)" stroke="#B8B2A8" stroke-width="0.8"/>
<circle cx="922" cy="111" r="3.5" fill="#6B8F71" opacity="0.5"/>
<text x="922" y="144" text-anchor="middle" class="node-label">Code</text>
<text x="884" y="88" class="node-index">03</text>
<!-- Agent 4 — right (Test) -->
<line x1="786" y1="252" x2="940" y2="215" stroke="#C8C2B8" stroke-width="0.7" stroke-dasharray="3,5"/>
<rect x="940" y="196" width="76" height="38" rx="14" fill="rgba(245,240,235,0.7)" stroke="#B8B2A8" stroke-width="0.8"/>
<circle cx="978" cy="215" r="3.5" fill="#6B8F71" opacity="0.5"/>
<text x="978" y="248" text-anchor="middle" class="node-label">Test</text>
<text x="940" y="192" class="node-index">04</text>
<!-- Agent 5 — bottom-right (Review) -->
<line x1="782" y1="278" x2="918" y2="385" stroke="#C8C2B8" stroke-width="0.7" stroke-dasharray="3,5"/>
<rect x="884" y="368" width="76" height="38" rx="14" fill="rgba(245,240,235,0.7)" stroke="#B8B2A8" stroke-width="0.8"/>
<circle cx="922" cy="387" r="3.5" fill="#6B8F71" opacity="0.5"/>
<text x="922" y="420" text-anchor="middle" class="node-label">Review</text>
<text x="884" y="364" class="node-index">05</text>
<!-- Agent 6 — bottom (Deploy) -->
<line x1="760" y1="293" x2="760" y2="365" stroke="#C8C2B8" stroke-width="0.7" stroke-dasharray="3,5"/>
<rect x="722" y="370" width="76" height="38" rx="14" fill="rgba(245,240,235,0.7)" stroke="#B8B2A8" stroke-width="0.8"/>
<circle cx="760" cy="389" r="3.5" fill="#6B8F71" opacity="0.5"/>
<text x="760" y="422" text-anchor="middle" class="node-label">Deploy</text>
<text x="722" y="366" class="node-index">06</text>
<!-- Agent 7 — bottom-left (Monitor) -->
<line x1="738" y1="278" x2="600" y2="375" stroke="#C8C2B8" stroke-width="0.7" stroke-dasharray="3,5"/>
<rect x="562" y="358" width="76" height="38" rx="14" fill="rgba(245,240,235,0.7)" stroke="#B8B2A8" stroke-width="0.8"/>
<circle cx="600" cy="377" r="3.5" fill="#6B8F71" opacity="0.5"/>
<text x="600" y="410" text-anchor="middle" class="node-label">Monitor</text>
<text x="562" y="354" class="node-index">07</text>
<!-- Agent 8 — left (Design) -->
<line x1="734" y1="252" x2="578" y2="245" stroke="#C8C2B8" stroke-width="0.7" stroke-dasharray="3,5"/>
<rect x="502" y="226" width="76" height="38" rx="14" fill="rgba(245,240,235,0.7)" stroke="#B8B2A8" stroke-width="0.8"/>
<circle cx="540" cy="245" r="3.5" fill="#6B8F71" opacity="0.5"/>
<text x="540" y="278" text-anchor="middle" class="node-label">Design</text>
<text x="502" y="222" class="node-index">08</text>
<!-- Small annotation marks — Takram spec-drawing details -->
<circle cx="490" cy="120" r="1.2" fill="#B0AAA0" opacity="0.35"/>
<line x1="492" y1="120" x2="535" y2="120" stroke="#B0AAA0" stroke-width="0.4" opacity="0.25"/>
<circle cx="1040" cy="390" r="1.2" fill="#B0AAA0" opacity="0.35"/>
<line x1="1038" y1="390" x2="995" y2="390" stroke="#B0AAA0" stroke-width="0.4" opacity="0.25"/>
<!-- Dimension annotation line (top) -->
<line x1="540" y1="60" x2="980" y2="60" stroke="#D4CFC6" stroke-width="0.4" opacity="0.3"/>
<line x1="540" y1="55" x2="540" y2="65" stroke="#D4CFC6" stroke-width="0.4" opacity="0.3"/>
<line x1="980" y1="55" x2="980" y2="65" stroke="#D4CFC6" stroke-width="0.4" opacity="0.3"/>
<text x="760" y="54" text-anchor="middle" font-family="Inter" font-size="7" font-weight="300" fill="#C8C2B8" letter-spacing="1.5">AGENT FIELD</text>
<!-- Right-side vertical annotation -->
<line x1="1060" y1="130" x2="1060" y2="380" stroke="#D4CFC6" stroke-width="0.3" opacity="0.25"/>
<line x1="1056" y1="130" x2="1064" y2="130" stroke="#D4CFC6" stroke-width="0.3" opacity="0.25"/>
<line x1="1056" y1="380" x2="1064" y2="380" stroke="#D4CFC6" stroke-width="0.3" opacity="0.25"/>
<text x="1068" y="260" font-family="Inter" font-size="7" font-weight="300" fill="#D4CFC6" letter-spacing="0.5" transform="rotate(90, 1068, 260)">NETWORK DEPTH</text>
<!-- Subtle data pulse lines emanating from center (organic feel) -->
<path d="M 760 217 Q 755 200 758 185" fill="none" stroke="#6B8F71" stroke-width="0.3" opacity="0.12"/>
<path d="M 786 248 Q 810 230 835 225" fill="none" stroke="#6B8F71" stroke-width="0.3" opacity="0.12"/>
<path d="M 786 262 Q 815 275 840 290" fill="none" stroke="#6B8F71" stroke-width="0.3" opacity="0.12"/>
<!-- Top-right spec box -->
<rect x="1040" y="48" width="104" height="56" rx="3" fill="rgba(245,240,235,0.5)" stroke="#E0DCD5" stroke-width="0.6"/>
<text x="1052" y="66" font-family="Inter" font-size="8" font-weight="500" fill="#B0AAA0" letter-spacing="1.5">AGENTS</text>
<text x="1052" y="92" font-family="Inter" font-size="28" font-weight="300" fill="#6B8F71">8</text>
<text x="1082" y="92" font-family="Inter" font-size="9" font-weight="300" fill="#B0AAA0"> parallel</text>
</svg>
<!-- Bottom annotation -->
<div class="annotation">
<div class="note">Fig. 01 — Parallel Agent Architecture</div>
<div class="note-serif">Claude Code 协作编排模型</div>
</div>
<!-- Spec number -->
<div class="spec-number">v1.0 / 2026</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

@@ -0,0 +1,503 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1080">
<title>AI Memory System Optimization — Build Studio Style</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1080px;
height: 1920px;
overflow: hidden;
margin: 0;
background: #FAFAF8;
font-family: 'Inter', sans-serif;
color: #2A2A2A;
}
.container {
width: 100%;
height: 100%;
padding: 80px 80px 64px 80px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
/* Header */
.label {
font-size: 10px;
font-weight: 400;
letter-spacing: 5px;
text-transform: uppercase;
color: #B0ACA4;
margin-bottom: 32px;
}
.title {
font-size: 36px;
font-weight: 200;
line-height: 1.35;
color: #1A1A1A;
letter-spacing: -0.5px;
max-width: 680px;
}
.title strong {
font-weight: 500;
}
/* Hero Numbers */
.hero {
margin-top: 56px;
display: flex;
align-items: flex-end;
gap: 48px;
}
.hero-block {
display: flex;
flex-direction: column;
}
.hero-label {
font-size: 10px;
font-weight: 400;
letter-spacing: 4px;
text-transform: uppercase;
color: #B0ACA4;
margin-bottom: 8px;
}
.hero-number {
font-size: 112px;
font-weight: 200;
line-height: 0.9;
color: #1A1A1A;
letter-spacing: -4px;
}
.hero-number .unit {
font-size: 28px;
font-weight: 300;
letter-spacing: 0;
color: #B0ACA4;
margin-left: 4px;
}
.hero-number.gold {
color: #1A1A1A;
}
.hero-number.gold .unit {
color: #D4A574;
opacity: 0.7;
}
.hero-number.gold .dot-accent {
color: #D4A574;
}
.hero-connector {
display: flex;
align-items: center;
margin-bottom: 24px;
}
.hero-connector svg {
opacity: 0.25;
}
.hero-reduction {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 24px;
}
.reduction-badge {
font-size: 13px;
font-weight: 400;
color: #D4A574;
letter-spacing: 2px;
}
/* Subtle line */
.divider {
width: 48px;
height: 1px;
background: #D4A574;
margin: 48px 0;
opacity: 0.4;
}
/* Stats Row */
.stats-row {
display: flex;
gap: 0;
}
.stat-item {
flex: 1;
padding: 24px 0;
position: relative;
}
.stat-item::after {
content: '';
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 1px;
height: 40px;
background: #E0DCDA;
}
.stat-item:last-child::after {
display: none;
}
.stat-value {
font-size: 40px;
font-weight: 200;
color: #1A1A1A;
line-height: 1;
margin-bottom: 8px;
}
.stat-desc {
font-size: 11px;
font-weight: 300;
color: #B0ACA4;
line-height: 1.5;
letter-spacing: 0.5px;
}
/* Memory Cards */
.cards-section {
margin-top: 40px;
}
.cards-label {
font-size: 10px;
font-weight: 400;
letter-spacing: 5px;
text-transform: uppercase;
color: #B0ACA4;
margin-bottom: 24px;
}
.cards-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.card {
background: #FFFFFF;
padding: 32px;
box-shadow: 0 1px 2px rgba(0,0,0,0.03), 0 4px 16px rgba(0,0,0,0.02);
border-radius: 2px;
position: relative;
}
.card-index {
font-size: 40px;
font-weight: 200;
color: #E8E4E0;
line-height: 1;
margin-bottom: 16px;
}
.card-title-zh {
font-size: 18px;
font-weight: 500;
color: #1A1A1A;
margin-bottom: 4px;
line-height: 1.3;
}
.card-title-en {
font-size: 10px;
font-weight: 400;
color: #C0BCB6;
letter-spacing: 2px;
text-transform: uppercase;
margin-bottom: 16px;
}
.card-desc {
font-size: 12px;
font-weight: 300;
color: #999;
line-height: 1.7;
}
.card.featured {
border-left: 1.5px solid #D4A574;
}
.card.featured .card-index {
color: #D4A574;
opacity: 0.35;
}
/* Flow */
.flow-section {
margin-top: 40px;
}
.flow-label {
font-size: 10px;
font-weight: 400;
letter-spacing: 5px;
text-transform: uppercase;
color: #B0ACA4;
margin-bottom: 32px;
}
.flow-timeline {
position: relative;
padding-left: 0;
}
.flow-steps {
display: flex;
justify-content: space-between;
position: relative;
}
.flow-steps::before {
content: '';
position: absolute;
top: 8px;
left: 36px;
right: 36px;
height: 1px;
background: linear-gradient(to right, #E0DCDA, #D4A574 50%, #E0DCDA);
}
.flow-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
flex: 1;
position: relative;
z-index: 1;
}
.flow-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #FAFAF8;
border: 1px solid #D0CCC6;
flex-shrink: 0;
}
.flow-dot.active {
border-color: #D4A574;
background: #D4A574;
}
.flow-step-label {
font-size: 9px;
font-weight: 400;
letter-spacing: 2px;
text-transform: uppercase;
color: #C0BCB6;
}
.flow-step-text {
font-size: 13px;
font-weight: 400;
color: #2A2A2A;
text-align: center;
line-height: 1.4;
}
/* Quote */
.quote-section {
margin-top: 0;
padding-top: 32px;
border-top: 1px solid #EEECE8;
}
.quote-line {
width: 32px;
height: 1px;
background: #D4A574;
margin-bottom: 24px;
opacity: 0.5;
}
.quote-text {
font-size: 22px;
font-weight: 200;
color: #1A1A1A;
line-height: 1.6;
letter-spacing: -0.3px;
max-width: 680px;
}
.quote-text em {
font-style: normal;
color: #D4A574;
font-weight: 400;
}
/* Results */
.results-row {
display: flex;
gap: 48px;
margin-top: 32px;
}
.result-item {
display: flex;
align-items: center;
gap: 12px;
}
.result-icon {
width: 6px;
height: 6px;
border-radius: 50%;
background: #D4A574;
flex-shrink: 0;
opacity: 0.6;
}
.result-text {
font-size: 13px;
font-weight: 400;
color: #999999;
letter-spacing: 0.3px;
}
/* Footer */
.footer {
margin-top: 32px;
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-text {
font-size: 9px;
font-weight: 300;
color: #D0CCC6;
letter-spacing: 3px;
text-transform: uppercase;
}
</style>
</head>
<body>
<div class="container">
<!-- Label -->
<div class="label">System Architecture</div>
<!-- Title -->
<div class="title">
AI记忆系统<br>
CLAUDE.md <strong>从 93KB<br>优化到 22KB</strong>
</div>
<!-- Hero Numbers -->
<div class="hero">
<div class="hero-block">
<span class="hero-label">Before</span>
<span class="hero-number">93<span class="unit">KB</span></span>
</div>
<div class="hero-connector">
<svg width="48" height="8" viewBox="0 0 48 8">
<line x1="0" y1="4" x2="40" y2="4" stroke="#D4A574" stroke-width="0.75" opacity="0.5"/>
<line x1="36" y1="1" x2="42" y2="4" stroke="#D4A574" stroke-width="0.75" opacity="0.5"/>
<line x1="36" y1="7" x2="42" y2="4" stroke="#D4A574" stroke-width="0.75" opacity="0.5"/>
</svg>
</div>
<div class="hero-block">
<span class="hero-label">After</span>
<span class="hero-number gold">22<span class="unit">KB</span></span>
</div>
<div class="hero-reduction">
<span class="reduction-badge">-76%</span>
</div>
</div>
<!-- Stats -->
<div class="divider"></div>
<div class="stats-row">
<div class="stat-item">
<div class="stat-value">2400<span style="font-size:18px;color:#AAAAAA">+</span></div>
<div class="stat-desc">lines before<br>in single file</div>
</div>
<div class="stat-item" style="padding-left: 24px;">
<div class="stat-value">4</div>
<div class="stat-desc">structured<br>memory categories</div>
</div>
<div class="stat-item" style="padding-left: 24px;">
<div class="stat-value">0</div>
<div class="stat-desc">information<br>loss</div>
</div>
</div>
<!-- Memory Cards -->
<div class="cards-section">
<div class="cards-label">Memory Categories</div>
<div class="cards-grid">
<div class="card featured">
<div class="card-index">01</div>
<div class="card-title-zh">核心身份</div>
<div class="card-title-en">Core Identity</div>
<div class="card-desc">Immutable traits, facts, fundamental identity markers</div>
</div>
<div class="card">
<div class="card-index">02</div>
<div class="card-title-zh">偏好设置</div>
<div class="card-title-en">Preferences</div>
<div class="card-desc">Style choices, tool habits, accumulated over sessions</div>
</div>
<div class="card">
<div class="card-index">03</div>
<div class="card-title-zh">项目状态</div>
<div class="card-title-en">Project State</div>
<div class="card-desc">Active tasks, deadlines, priorities, evolving context</div>
</div>
<div class="card">
<div class="card-index">04</div>
<div class="card-title-zh">日志流水</div>
<div class="card-title-en">Daily Logs</div>
<div class="card-desc">Session records, never auto-loaded, search on demand</div>
</div>
</div>
</div>
<!-- Flow -->
<div class="flow-section">
<div class="flow-label">Processing Flow</div>
<div class="flow-timeline">
<div class="flow-steps">
<div class="flow-step">
<div class="flow-dot"></div>
<div class="flow-step-label">Input</div>
<div class="flow-step-text">User<br>Input</div>
</div>
<div class="flow-step">
<div class="flow-dot"></div>
<div class="flow-step-label">Route</div>
<div class="flow-step-text">Workspace<br>Detection</div>
</div>
<div class="flow-step">
<div class="flow-dot active"></div>
<div class="flow-step-label">Load</div>
<div class="flow-step-text">Relevant<br>Memory</div>
</div>
<div class="flow-step">
<div class="flow-dot"></div>
<div class="flow-step-label">Execute</div>
<div class="flow-step-text">Task<br>Processing</div>
</div>
<div class="flow-step">
<div class="flow-dot"></div>
<div class="flow-step-label">Update</div>
<div class="flow-step-text">Memory<br>Write-back</div>
</div>
</div>
</div>
</div>
<!-- Quote -->
<div class="quote-section">
<div class="quote-line"></div>
<div class="quote-text">
Like <em>Marie Kondo</em> for AI memory<br>
— keep only what sparks joy.
</div>
<div class="results-row">
<div class="result-item">
<div class="result-icon"></div>
<span class="result-text">Faster context loading</span>
</div>
<div class="result-item">
<div class="result-icon"></div>
<span class="result-text">More relevant responses</span>
</div>
<div class="result-item">
<div class="result-icon"></div>
<span class="result-text">Zero information loss</span>
</div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<span class="footer-text">Build Studio Style</span>
<span class="footer-text">2026</span>
</div>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

@@ -0,0 +1,600 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1080">
<title>AI Memory System Optimization — Pentagram Style</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1080px;
height: 1920px;
overflow: hidden;
margin: 0;
background: #FFFFFF;
font-family: 'Helvetica Neue', Arial, sans-serif;
color: #111;
}
.container {
width: 100%;
height: 100%;
padding: 64px 72px;
display: flex;
flex-direction: column;
}
/* Header */
.header {
border-bottom: 6px solid #111;
padding-bottom: 24px;
margin-bottom: 0;
}
.header-label {
font-size: 11px;
font-weight: 700;
letter-spacing: 4px;
text-transform: uppercase;
color: #E63946;
margin-bottom: 12px;
}
.header-title {
font-size: 44px;
font-weight: 900;
line-height: 1.1;
letter-spacing: -1px;
color: #111;
}
.header-subtitle {
font-size: 15px;
font-weight: 400;
color: #999;
margin-top: 8px;
}
/* Hero Numbers Section */
.hero-section {
display: flex;
align-items: baseline;
justify-content: center;
padding: 48px 0 20px 0;
border-bottom: 2px solid #111;
gap: 0;
}
.hero-num {
font-weight: 900;
font-size: 200px;
line-height: 0.85;
letter-spacing: -8px;
color: #111;
}
.hero-unit {
font-size: 36px;
font-weight: 500;
color: #111;
margin-left: 4px;
align-self: flex-end;
margin-bottom: 18px;
}
.hero-arrow-container {
display: flex;
flex-direction: column;
align-items: center;
margin: 0 28px;
align-self: center;
}
.hero-arrow-label {
font-size: 12px;
font-weight: 700;
letter-spacing: 3px;
text-transform: uppercase;
color: #E63946;
margin-bottom: 6px;
}
.hero-num-accent {
font-weight: 900;
font-size: 200px;
line-height: 0.85;
letter-spacing: -8px;
color: #E63946;
}
.hero-meta {
display: flex;
justify-content: space-between;
padding: 14px 0;
border-bottom: 6px solid #111;
}
.hero-meta-item {
font-size: 12px;
font-weight: 500;
color: #999;
letter-spacing: 1px;
}
.hero-meta-item strong {
color: #111;
font-weight: 900;
}
/* Sections */
.section {
padding: 32px 0 0 0;
}
.section-header {
display: flex;
align-items: baseline;
gap: 16px;
margin-bottom: 20px;
}
.section-num {
font-size: 48px;
font-weight: 900;
color: #E63946;
line-height: 1;
}
.section-title {
font-size: 22px;
font-weight: 700;
letter-spacing: -0.5px;
color: #111;
line-height: 1;
}
.section-divider {
width: 100%;
height: 2px;
background: #111;
margin-bottom: 20px;
}
/* Data Bars */
.data-bars {
display: flex;
flex-direction: column;
gap: 14px;
}
.data-bar-row {
display: flex;
align-items: center;
gap: 16px;
}
.data-bar-label {
font-size: 13px;
font-weight: 600;
color: #666;
width: 100px;
text-align: right;
flex-shrink: 0;
}
.data-bar-track {
flex: 1;
height: 32px;
background: #F0F0F0;
position: relative;
}
.data-bar-fill {
height: 100%;
background: #111;
}
.data-bar-fill.accent {
background: #E63946;
}
.data-bar-value {
font-size: 14px;
font-weight: 900;
color: #111;
width: 60px;
text-align: left;
flex-shrink: 0;
}
/* Category Grid */
.category-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2px;
background: #111;
border: 2px solid #111;
}
.category-cell {
background: #fff;
padding: 24px;
display: flex;
flex-direction: column;
gap: 6px;
}
.category-num {
font-size: 11px;
font-weight: 700;
color: #999;
letter-spacing: 2px;
}
.category-name-zh {
font-size: 22px;
font-weight: 900;
color: #111;
line-height: 1.2;
}
.category-name-en {
font-size: 11px;
font-weight: 500;
color: #999;
letter-spacing: 1px;
text-transform: uppercase;
}
.category-desc {
font-size: 12px;
font-weight: 400;
color: #666;
line-height: 1.5;
margin-top: 4px;
}
.category-cell.accent {
background: #E63946;
}
.category-cell.accent .category-num,
.category-cell.accent .category-name-zh,
.category-cell.accent .category-name-en,
.category-cell.accent .category-desc {
color: #fff;
}
/* Section 03: Design Principles */
.principles {
display: flex;
flex-direction: column;
gap: 0;
}
.principle-row {
display: flex;
align-items: stretch;
border-bottom: 1px solid #E8E8E8;
}
.principle-row:last-child {
border-bottom: none;
}
.principle-num {
font-size: 32px;
font-weight: 900;
color: #E63946;
width: 64px;
flex-shrink: 0;
padding: 16px 0;
line-height: 1;
}
.principle-content {
padding: 16px 0 16px 16px;
border-left: 1px solid #E8E8E8;
flex: 1;
}
.principle-name {
font-size: 16px;
font-weight: 900;
color: #111;
margin-bottom: 4px;
}
.principle-desc {
font-size: 13px;
font-weight: 400;
color: #888;
line-height: 1.5;
}
/* Section 04: Results */
.results-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 0;
}
.result-card {
padding: 32px 24px;
border-right: 1px solid #E8E8E8;
}
.result-card:last-child {
border-right: none;
}
.result-number {
font-size: 64px;
font-weight: 900;
color: #E63946;
line-height: 1;
letter-spacing: -3px;
}
.result-label {
font-size: 11px;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
color: #999;
margin-top: 8px;
}
/* Insight Quote */
.insight-section {
margin-top: auto;
border-top: 6px solid #111;
padding-top: 24px;
}
.insight-quote {
font-size: 24px;
font-weight: 500;
color: #111;
line-height: 1.4;
letter-spacing: -0.5px;
font-style: italic;
}
.insight-quote .highlight {
color: #E63946;
font-weight: 900;
font-style: normal;
}
.insight-result {
display: flex;
gap: 40px;
margin-top: 18px;
padding-top: 14px;
border-top: 1px solid #DDD;
}
.insight-item {
display: flex;
align-items: center;
gap: 10px;
}
.insight-dot {
width: 8px;
height: 8px;
background: #E63946;
flex-shrink: 0;
}
.insight-text {
font-size: 13px;
font-weight: 600;
color: #666;
}
/* Footer */
.footer {
margin-top: 20px;
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px solid #DDD;
}
.footer-text {
font-size: 10px;
font-weight: 500;
color: #CCC;
letter-spacing: 2px;
text-transform: uppercase;
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<div class="header-label">Case Study / System Design</div>
<div class="header-title">AI记忆系统:CLAUDE.md<br>从臃肿到优雅的重构之路</div>
<div class="header-subtitle">A systematic approach to AI memory architecture optimization</div>
</div>
<!-- Hero Numbers -->
<div class="hero-section">
<span class="hero-num">93</span>
<span class="hero-unit">KB</span>
<div class="hero-arrow-container">
<span class="hero-arrow-label">reduced to</span>
<svg width="64" height="24" viewBox="0 0 64 24">
<line x1="0" y1="12" x2="52" y2="12" stroke="#E63946" stroke-width="3"/>
<polygon points="52,4 64,12 52,20" fill="#E63946"/>
</svg>
</div>
<span class="hero-num-accent">22</span>
<span class="hero-unit" style="color:#E63946">KB</span>
</div>
<div class="hero-meta">
<div class="hero-meta-item"><strong>76%</strong> reduction</div>
<div class="hero-meta-item"><strong>2400+</strong> lines before</div>
<div class="hero-meta-item"><strong>1</strong> file to <strong>structured</strong> system</div>
<div class="hero-meta-item"><strong>0</strong> information loss</div>
</div>
<!-- Section 01: Before vs After -->
<div class="section">
<div class="section-header">
<span class="section-num">01</span>
<span class="section-title">Before vs After</span>
</div>
<div class="section-divider"></div>
<div class="data-bars">
<div class="data-bar-row">
<span class="data-bar-label">Before</span>
<div class="data-bar-track">
<div class="data-bar-fill" style="width: 100%;"></div>
</div>
<span class="data-bar-value">93 KB</span>
</div>
<div class="data-bar-row">
<span class="data-bar-label">After</span>
<div class="data-bar-track">
<div class="data-bar-fill accent" style="width: 23.7%;"></div>
</div>
<span class="data-bar-value">22 KB</span>
</div>
</div>
</div>
<!-- Section 02: Memory Architecture -->
<div class="section">
<div class="section-header">
<span class="section-num">02</span>
<span class="section-title">Memory Architecture</span>
</div>
<div class="section-divider"></div>
<div class="category-grid">
<div class="category-cell accent">
<span class="category-num">I</span>
<span class="category-name-zh">核心身份</span>
<span class="category-name-en">Core Identity</span>
<span class="category-desc">Who you are, fundamental traits, immutable facts</span>
</div>
<div class="category-cell">
<span class="category-num">II</span>
<span class="category-name-zh">偏好设置</span>
<span class="category-name-en">Preferences</span>
<span class="category-desc">Style, tools, workflow habits, accumulated over time</span>
</div>
<div class="category-cell">
<span class="category-num">III</span>
<span class="category-name-zh">项目状态</span>
<span class="category-name-en">Project State</span>
<span class="category-desc">Current tasks, deadlines, priorities, progress tracking</span>
</div>
<div class="category-cell">
<span class="category-num">IV</span>
<span class="category-name-zh">日志流水</span>
<span class="category-name-en">Daily Logs</span>
<span class="category-desc">Session-level records, searchable history, never auto-loaded</span>
</div>
</div>
</div>
<!-- Section 03: Design Principles -->
<div class="section">
<div class="section-header">
<span class="section-num">03</span>
<span class="section-title">Design Principles</span>
</div>
<div class="section-divider"></div>
<div class="principles">
<div class="principle-row">
<div class="principle-num">A</div>
<div class="principle-content">
<div class="principle-name">Route, Don't Dump</div>
<div class="principle-desc">Router file dispatches to workspace-specific rules. Never load everything at once.</div>
</div>
</div>
<div class="principle-row">
<div class="principle-num">B</div>
<div class="principle-content">
<div class="principle-name">Structured Hierarchy</div>
<div class="principle-desc">Identity > Preferences > Projects > Logs. Each layer loads on demand.</div>
</div>
</div>
<div class="principle-row">
<div class="principle-num">C</div>
<div class="principle-content">
<div class="principle-name">Write Rules, Not Records</div>
<div class="principle-desc">Store reusable patterns, not one-time instructions. Keep memory under 100 lines.</div>
</div>
</div>
<div class="principle-row">
<div class="principle-num">D</div>
<div class="principle-content">
<div class="principle-name">Silent Operations</div>
<div class="principle-desc">Memory read/write happens silently. Never interrupt the user's task flow.</div>
</div>
</div>
</div>
</div>
<!-- Section 04: Results -->
<div class="section">
<div class="section-header">
<span class="section-num">04</span>
<span class="section-title">Results</span>
</div>
<div class="section-divider"></div>
<div class="results-grid">
<div class="result-card">
<div class="result-number">76%</div>
<div class="result-label">Size Reduction</div>
</div>
<div class="result-card">
<div class="result-number">2.3x</div>
<div class="result-label">Faster Loading</div>
</div>
<div class="result-card">
<div class="result-number">0</div>
<div class="result-label">Data Loss</div>
</div>
</div>
</div>
<!-- Insight -->
<div class="insight-section">
<div class="insight-quote">
"Like <span class="highlight">Marie Kondo</span> for AI memory
— keep only what sparks joy."
</div>
<div class="insight-result">
<div class="insight-item">
<div class="insight-dot"></div>
<span class="insight-text">Faster context loading</span>
</div>
<div class="insight-item">
<div class="insight-dot"></div>
<span class="insight-text">More relevant responses</span>
</div>
<div class="insight-item">
<div class="insight-dot"></div>
<span class="insight-text">Zero information loss</span>
</div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<span class="footer-text">Pentagram Style</span>
<span class="footer-text">CLAUDE.md Optimization</span>
<span class="footer-text">2026</span>
</div>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

@@ -0,0 +1,670 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1080">
<title>AI Memory System Optimization — Takram Style</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Noto+Serif+SC:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1080px;
height: 1920px;
overflow: hidden;
margin: 0;
background: #F5F0EB;
font-family: 'Inter', sans-serif;
color: #3D3730;
}
.container {
width: 100%;
height: 100%;
padding: 72px 80px 60px 80px;
display: flex;
flex-direction: column;
position: relative;
}
/* Background texture */
.bg-circle {
position: absolute;
border-radius: 50%;
border: 1px solid rgba(168, 181, 160, 0.2);
pointer-events: none;
}
.bg-circle-1 {
width: 600px;
height: 600px;
top: -180px;
right: -200px;
}
.bg-circle-2 {
width: 400px;
height: 400px;
bottom: 200px;
left: -160px;
}
/* Header */
.header {
position: relative;
z-index: 1;
margin-bottom: 40px;
}
.header-label {
font-family: 'Inter', sans-serif;
font-size: 10px;
font-weight: 500;
letter-spacing: 3.5px;
text-transform: uppercase;
color: #6B8F71;
margin-bottom: 20px;
opacity: 0.8;
}
.header-title {
font-family: 'Noto Serif SC', serif;
font-size: 44px;
font-weight: 500;
line-height: 1.35;
color: #2D3436;
letter-spacing: 1px;
}
.header-subtitle {
font-family: 'Inter', sans-serif;
font-size: 15px;
font-weight: 300;
color: #8B7355;
margin-top: 12px;
line-height: 1.5;
letter-spacing: 0.3px;
}
/* Hero Data Circles */
.hero-section {
display: flex;
align-items: center;
justify-content: center;
gap: 48px;
padding: 36px 0;
position: relative;
z-index: 1;
}
.data-circle {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
}
.data-circle-ring {
position: relative;
width: 200px;
height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.data-circle-ring svg {
position: absolute;
top: 0;
left: 0;
transform: rotate(-90deg);
}
.data-circle-inner {
display: flex;
flex-direction: column;
align-items: center;
z-index: 1;
}
.data-num {
font-family: 'Inter', sans-serif;
font-size: 64px;
font-weight: 300;
color: #2E2A24;
line-height: 1;
}
.data-unit {
font-family: 'Inter', sans-serif;
font-size: 16px;
font-weight: 400;
color: #8B7355;
margin-top: 4px;
}
.data-label {
font-family: 'Inter', sans-serif;
font-size: 12px;
font-weight: 400;
color: #A8A098;
margin-top: 12px;
letter-spacing: 2px;
text-transform: uppercase;
}
.data-circle-small .data-circle-ring {
width: 160px;
height: 160px;
}
.data-circle-small .data-num {
font-size: 52px;
}
/* Organic connector */
.hero-connector {
position: relative;
width: 120px;
height: 60px;
flex-shrink: 0;
}
/* Reduction badge — understated Takram style */
.reduction-pill {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 28px;
background: transparent;
border: 1px solid rgba(107, 143, 113, 0.3);
border-radius: 20px;
margin: 0 auto;
display: flex;
gap: 8px;
}
.reduction-text {
font-family: 'Inter', sans-serif;
font-size: 13px;
font-weight: 500;
color: #6B8F71;
letter-spacing: 2px;
}
.hero-footer {
display: flex;
justify-content: center;
margin-top: 16px;
}
/* Categories Section */
.categories-section {
margin-top: 36px;
position: relative;
z-index: 1;
}
.section-label {
font-family: 'Noto Serif SC', serif;
font-size: 20px;
font-weight: 500;
color: #2D3436;
margin-bottom: 24px;
letter-spacing: 1px;
}
.section-label .section-num {
font-family: 'Inter', sans-serif;
font-size: 9px;
font-weight: 400;
color: #B0AAA0;
letter-spacing: 1px;
margin-right: 12px;
}
.categories-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.cat-card {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 28px 24px;
position: relative;
box-shadow: 0 2px 12px rgba(0,0,0,0.03);
border: 1px solid rgba(232, 228, 220, 0.8);
}
.cat-card-icon {
margin-bottom: 14px;
}
.cat-card-title-zh {
font-family: 'Noto Serif SC', serif;
font-size: 20px;
font-weight: 600;
color: #2E2A24;
margin-bottom: 4px;
}
.cat-card-title-en {
font-family: 'Inter', sans-serif;
font-size: 11px;
font-weight: 400;
color: #A8A098;
letter-spacing: 2px;
text-transform: uppercase;
margin-bottom: 10px;
}
.cat-card-desc {
font-family: 'Inter', sans-serif;
font-size: 12px;
font-weight: 300;
color: #8B7355;
line-height: 1.7;
}
.cat-card.highlight {
border-color: rgba(107, 143, 113, 0.35);
background: rgba(107, 143, 113, 0.04);
}
/* Proportion circles */
.cat-prop {
position: absolute;
top: 20px;
right: 20px;
}
/* Flow Diagram */
.flow-section {
margin-top: 36px;
position: relative;
z-index: 1;
}
.flow-diagram {
position: relative;
height: 260px;
width: 100%;
}
.flow-node {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.flow-node-circle {
width: 72px;
height: 72px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255,255,255,0.5);
border: 1px solid #DDD9D2;
}
.flow-node-circle.active {
border-color: #6B8F71;
border-width: 1.5px;
background: rgba(107, 143, 113, 0.06);
}
.flow-node-label {
font-family: 'Inter', sans-serif;
font-size: 11px;
font-weight: 400;
color: #A8A098;
letter-spacing: 2px;
text-transform: uppercase;
}
.flow-node-text {
font-family: 'Noto Serif SC', serif;
font-size: 13px;
font-weight: 500;
color: #2E2A24;
text-align: center;
}
/* Insight */
.insight-section {
margin-top: auto;
position: relative;
z-index: 1;
}
.insight-card {
background: rgba(255, 255, 255, 0.5);
border-radius: 16px;
padding: 32px 36px;
border: 1px solid rgba(232, 228, 220, 0.6);
box-shadow: 0 4px 20px rgba(0,0,0,0.03);
}
.insight-quote {
font-family: 'Noto Serif SC', serif;
font-size: 20px;
font-weight: 400;
color: #2E2A24;
line-height: 1.7;
letter-spacing: 0.5px;
}
.insight-quote .green {
color: #6B8F71;
font-weight: 500;
}
.insight-quote .brown {
color: #8B7355;
font-weight: 500;
}
.results-row {
display: flex;
gap: 32px;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid rgba(232, 228, 220, 0.6);
}
.result-item {
display: flex;
align-items: center;
gap: 10px;
}
.result-leaf {
flex-shrink: 0;
}
.result-text {
font-family: 'Inter', sans-serif;
font-size: 13px;
font-weight: 400;
color: #8B7355;
}
/* Footer */
.footer {
margin-top: 28px;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 1;
}
.footer-text {
font-family: 'Inter', sans-serif;
font-size: 10px;
font-weight: 300;
color: #C4BDB4;
letter-spacing: 3px;
text-transform: uppercase;
}
</style>
</head>
<body>
<div class="container">
<!-- Background decorations -->
<div class="bg-circle bg-circle-1"></div>
<div class="bg-circle bg-circle-2"></div>
<!-- Header -->
<div class="header">
<div class="header-label">Speculative Design / Memory Architecture</div>
<div class="header-title">AI记忆系统<br>CLAUDE.md 的断舍离</div>
<div class="header-subtitle">Restructuring artificial memory from monolith to modular elegance</div>
</div>
<!-- Hero Data Circles -->
<div class="hero-section">
<div class="data-circle">
<div class="data-circle-ring">
<svg width="200" height="200" viewBox="0 0 200 200">
<circle cx="100" cy="100" r="92" fill="none" stroke="#E8E4DC" stroke-width="1.5"/>
<circle cx="100" cy="100" r="92" fill="none" stroke="#8B7355" stroke-width="2" stroke-dasharray="578" stroke-dashoffset="0" opacity="0.3"/>
</svg>
<div class="data-circle-inner">
<span class="data-num">93</span>
<span class="data-unit">KB</span>
</div>
</div>
<span class="data-label">Before</span>
</div>
<div class="hero-connector">
<svg width="120" height="60" viewBox="0 0 120 60">
<path d="M 0,30 C 30,30 40,10 60,10 C 80,10 90,50 110,30"
fill="none" stroke="#6B8F71" stroke-width="1" stroke-dasharray="3,4" opacity="0.5"/>
<circle cx="110" cy="30" r="3.5" fill="#6B8F71" opacity="0.5"/>
<circle cx="110" cy="30" r="7" fill="none" stroke="#6B8F71" stroke-width="0.5" opacity="0.2"/>
<!-- delta annotation -->
<text x="60" y="50" text-anchor="middle" font-family="Inter" font-size="7" fill="#B0AAA0" letter-spacing="0.5">-71 KB</text>
</svg>
</div>
<div class="data-circle data-circle-small">
<div class="data-circle-ring">
<svg width="160" height="160" viewBox="0 0 160 160">
<circle cx="80" cy="80" r="72" fill="none" stroke="#E8E4DC" stroke-width="1.5"/>
<circle cx="80" cy="80" r="72" fill="none" stroke="#A8B5A0" stroke-width="2.5" stroke-dasharray="452" stroke-dashoffset="344" opacity="0.6"/>
</svg>
<div class="data-circle-inner">
<span class="data-num" style="color: #A8B5A0;">22</span>
<span class="data-unit">KB</span>
</div>
</div>
<span class="data-label">After</span>
</div>
</div>
<div class="hero-footer">
<div class="reduction-pill">
<svg width="14" height="14" viewBox="0 0 14 14"><path d="M7 2L7 12M3 8L7 12L11 8" fill="none" stroke="#6B8F71" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="reduction-text">76% REDUCTION</span>
</div>
</div>
<!-- Categories -->
<div class="categories-section">
<div class="section-label"><span class="section-num">01</span>Four Pillars of Memory</div>
<div class="categories-grid">
<div class="cat-card highlight">
<div class="cat-card-icon">
<svg width="32" height="32" viewBox="0 0 32 32">
<circle cx="16" cy="12" r="6" fill="none" stroke="#A8B5A0" stroke-width="1.5"/>
<path d="M6,28 C6,22 10,18 16,18 C22,18 26,22 26,28" fill="none" stroke="#A8B5A0" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<div class="cat-prop">
<svg width="28" height="28" viewBox="0 0 28 28">
<circle cx="14" cy="14" r="12" fill="none" stroke="#E8E4DC" stroke-width="1"/>
<circle cx="14" cy="14" r="12" fill="none" stroke="#A8B5A0" stroke-width="2" stroke-dasharray="75" stroke-dashoffset="56" transform="rotate(-90 14 14)"/>
</svg>
</div>
<div class="cat-card-title-zh">核心身份</div>
<div class="cat-card-title-en">Core Identity</div>
<div class="cat-card-desc">Immutable facts and fundamental traits that define who you are</div>
</div>
<div class="cat-card">
<div class="cat-card-icon">
<svg width="32" height="32" viewBox="0 0 32 32">
<path d="M8,8 L24,8 L24,24 L8,24 Z" fill="none" stroke="#8B7355" stroke-width="1.5" rx="2"/>
<line x1="12" y1="13" x2="20" y2="13" stroke="#8B7355" stroke-width="1.5" stroke-linecap="round"/>
<line x1="12" y1="17" x2="18" y2="17" stroke="#8B7355" stroke-width="1.5" stroke-linecap="round"/>
<line x1="12" y1="21" x2="16" y2="21" stroke="#8B7355" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<div class="cat-prop">
<svg width="28" height="28" viewBox="0 0 28 28">
<circle cx="14" cy="14" r="12" fill="none" stroke="#E8E4DC" stroke-width="1"/>
<circle cx="14" cy="14" r="12" fill="none" stroke="#8B7355" stroke-width="2" stroke-dasharray="75" stroke-dashoffset="45" transform="rotate(-90 14 14)"/>
</svg>
</div>
<div class="cat-card-title-zh">偏好设置</div>
<div class="cat-card-title-en">Preferences</div>
<div class="cat-card-desc">Style, tools, habits — accumulated over time through sessions</div>
</div>
<div class="cat-card">
<div class="cat-card-icon">
<svg width="32" height="32" viewBox="0 0 32 32">
<rect x="6" y="10" width="8" height="14" rx="1" fill="none" stroke="#8B7355" stroke-width="1.5"/>
<rect x="18" y="6" width="8" height="18" rx="1" fill="none" stroke="#8B7355" stroke-width="1.5"/>
<line x1="8" y1="16" x2="12" y2="16" stroke="#8B7355" stroke-width="1" stroke-linecap="round"/>
<line x1="20" y1="12" x2="24" y2="12" stroke="#8B7355" stroke-width="1" stroke-linecap="round"/>
</svg>
</div>
<div class="cat-prop">
<svg width="28" height="28" viewBox="0 0 28 28">
<circle cx="14" cy="14" r="12" fill="none" stroke="#E8E4DC" stroke-width="1"/>
<circle cx="14" cy="14" r="12" fill="none" stroke="#8B7355" stroke-width="2" stroke-dasharray="75" stroke-dashoffset="37" transform="rotate(-90 14 14)"/>
</svg>
</div>
<div class="cat-card-title-zh">项目状态</div>
<div class="cat-card-title-en">Project State</div>
<div class="cat-card-desc">Current tasks, deadlines, priorities — always evolving context</div>
</div>
<div class="cat-card">
<div class="cat-card-icon">
<svg width="32" height="32" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="10" fill="none" stroke="#8B7355" stroke-width="1.5"/>
<line x1="16" y1="10" x2="16" y2="16" stroke="#8B7355" stroke-width="1.5" stroke-linecap="round"/>
<line x1="16" y1="16" x2="21" y2="19" stroke="#8B7355" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<div class="cat-prop">
<svg width="28" height="28" viewBox="0 0 28 28">
<circle cx="14" cy="14" r="12" fill="none" stroke="#E8E4DC" stroke-width="1"/>
<circle cx="14" cy="14" r="12" fill="none" stroke="#8B7355" stroke-width="2" stroke-dasharray="75" stroke-dashoffset="60" transform="rotate(-90 14 14)"/>
</svg>
</div>
<div class="cat-card-title-zh">日志流水</div>
<div class="cat-card-title-en">Daily Logs</div>
<div class="cat-card-desc">Session records — never auto-loaded, retrieved on demand only</div>
</div>
</div>
</div>
<!-- Flow Diagram -->
<div class="flow-section">
<div class="section-label"><span class="section-num">02</span>System Flow</div>
<div class="flow-diagram">
<!-- SVG organic curves connecting nodes — art-piece treatment -->
<svg width="920" height="260" viewBox="0 0 920 260" style="position: absolute; top: 0; left: 0;">
<!-- Background guide line (very subtle) -->
<line x1="50" y1="130" x2="870" y2="130" stroke="#E8E4DC" stroke-width="0.3" stroke-dasharray="2,8" opacity="0.4"/>
<!-- Curve from Input to Route -->
<path d="M 116,50 C 165,50 195,120 246,120" fill="none" stroke="#D4CFC6" stroke-width="1"/>
<!-- Curve from Route to Load -->
<path d="M 316,120 C 370,120 380,50 460,50" fill="none" stroke="#D4CFC6" stroke-width="1"/>
<!-- Curve from Load to Execute (highlighted — key transition) -->
<path d="M 530,50 C 585,50 600,160 660,160" fill="none" stroke="#6B8F71" stroke-width="1.5" stroke-dasharray="4,4" opacity="0.6"/>
<!-- Curve from Execute to Update -->
<path d="M 730,160 C 785,160 800,80 830,80" fill="none" stroke="#D4CFC6" stroke-width="1"/>
<!-- Connection dots — varying size for depth -->
<circle cx="116" cy="50" r="2.5" fill="#D4CFC6"/>
<circle cx="246" cy="120" r="2.5" fill="#D4CFC6"/>
<circle cx="316" cy="120" r="2.5" fill="#D4CFC6"/>
<circle cx="460" cy="50" r="3" fill="#6B8F71" opacity="0.5"/>
<circle cx="530" cy="50" r="3" fill="#6B8F71" opacity="0.5"/>
<circle cx="660" cy="160" r="2.5" fill="#D4CFC6"/>
<circle cx="730" cy="160" r="2.5" fill="#D4CFC6"/>
<circle cx="830" cy="80" r="2.5" fill="#D4CFC6"/>
<!-- Annotation: step numbers along curve -->
<text x="170" y="75" font-family="Inter" font-size="7" fill="#C8C2B8" letter-spacing="0.5">step 1</text>
<text x="350" y="100" font-family="Inter" font-size="7" fill="#C8C2B8" letter-spacing="0.5">step 2</text>
<text x="565" y="95" font-family="Inter" font-size="7" fill="#6B8F71" opacity="0.5" letter-spacing="0.5">step 3</text>
<text x="770" y="130" font-family="Inter" font-size="7" fill="#C8C2B8" letter-spacing="0.5">step 4</text>
</svg>
<!-- Node 1: User Input -->
<div class="flow-node" style="left: 44px; top: 8px;">
<div class="flow-node-circle">
<svg width="24" height="24" viewBox="0 0 24 24">
<circle cx="12" cy="9" r="4" fill="none" stroke="#8B7355" stroke-width="1.2"/>
<path d="M4,21 C4,16 7,14 12,14 C17,14 20,16 20,21" fill="none" stroke="#8B7355" stroke-width="1.2" stroke-linecap="round"/>
</svg>
</div>
<span class="flow-node-label">Input</span>
<span class="flow-node-text">User</span>
</div>
<!-- Node 2: Route -->
<div class="flow-node" style="left: 210px; top: 78px;">
<div class="flow-node-circle">
<svg width="24" height="24" viewBox="0 0 24 24">
<path d="M4,12 L10,6 L10,10 L20,10 L20,14 L10,14 L10,18 Z" fill="none" stroke="#8B7355" stroke-width="1.2"/>
</svg>
</div>
<span class="flow-node-label">Route</span>
<span class="flow-node-text">Workspace</span>
</div>
<!-- Node 3: Load Memory (active) -->
<div class="flow-node" style="left: 420px; top: 8px;">
<div class="flow-node-circle active">
<svg width="24" height="24" viewBox="0 0 24 24">
<rect x="4" y="4" width="16" height="16" rx="2" fill="none" stroke="#A8B5A0" stroke-width="1.2"/>
<line x1="8" y1="9" x2="16" y2="9" stroke="#A8B5A0" stroke-width="1.2" stroke-linecap="round"/>
<line x1="8" y1="13" x2="14" y2="13" stroke="#A8B5A0" stroke-width="1.2" stroke-linecap="round"/>
<line x1="8" y1="17" x2="12" y2="17" stroke="#A8B5A0" stroke-width="1.2" stroke-linecap="round"/>
</svg>
</div>
<span class="flow-node-label">Load</span>
<span class="flow-node-text">Memory</span>
</div>
<!-- Node 4: Execute -->
<div class="flow-node" style="left: 624px; top: 118px;">
<div class="flow-node-circle">
<svg width="24" height="24" viewBox="0 0 24 24">
<polygon points="8,4 20,12 8,20" fill="none" stroke="#8B7355" stroke-width="1.2" stroke-linejoin="round"/>
</svg>
</div>
<span class="flow-node-label">Execute</span>
<span class="flow-node-text">Task</span>
</div>
<!-- Node 5: Update -->
<div class="flow-node" style="left: 800px; top: 40px;">
<div class="flow-node-circle">
<svg width="24" height="24" viewBox="0 0 24 24">
<path d="M12,4 L12,16" stroke="#8B7355" stroke-width="1.2" stroke-linecap="round"/>
<path d="M8,12 L12,16 L16,12" fill="none" stroke="#8B7355" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="6" y1="20" x2="18" y2="20" stroke="#8B7355" stroke-width="1.2" stroke-linecap="round"/>
</svg>
</div>
<span class="flow-node-label">Update</span>
<span class="flow-node-text">Write</span>
</div>
</div>
</div>
<!-- Insight -->
<div class="insight-section">
<div class="insight-card">
<div class="insight-quote">
Like <span class="green">Marie Kondo</span> for AI memory —<br>
keep only what <span class="brown">sparks joy</span>.
</div>
<div class="results-row">
<div class="result-item">
<div class="result-leaf">
<svg width="14" height="14" viewBox="0 0 14 14">
<path d="M2,12 C2,6 6,2 12,2" fill="none" stroke="#A8B5A0" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="12" cy="2" r="2" fill="#A8B5A0" opacity="0.4"/>
</svg>
</div>
<span class="result-text">Faster context loading</span>
</div>
<div class="result-item">
<div class="result-leaf">
<svg width="14" height="14" viewBox="0 0 14 14">
<path d="M2,12 C2,6 6,2 12,2" fill="none" stroke="#A8B5A0" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="12" cy="2" r="2" fill="#A8B5A0" opacity="0.4"/>
</svg>
</div>
<span class="result-text">More relevant responses</span>
</div>
<div class="result-item">
<div class="result-leaf">
<svg width="14" height="14" viewBox="0 0 14 14">
<path d="M2,12 C2,6 6,2 12,2" fill="none" stroke="#A8B5A0" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="12" cy="2" r="2" fill="#A8B5A0" opacity="0.4"/>
</svg>
</div>
<span class="result-text">Zero information loss</span>
</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<span class="footer-text">Takram Style</span>
<span class="footer-text">CLAUDE.md Optimization</span>
<span class="footer-text">2026</span>
</div>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

+382
View File
@@ -0,0 +1,382 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1920">
<title>GLM-4.7 Coding Benchmark - Build Studio Style</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1920px;
height: 1080px;
overflow: hidden;
margin: 0;
background: #FAFAF8;
font-family: 'Inter', sans-serif;
color: #2A2A2A;
position: relative;
}
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 64px 96px 48px 96px;
justify-content: space-between;
}
/* Top section */
.top-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.eyebrow {
font-size: 10px;
font-weight: 400;
letter-spacing: 4px;
text-transform: uppercase;
color: #B0ACA4;
}
.source-note {
font-size: 10px;
font-weight: 300;
color: #C0BCB6;
text-align: right;
line-height: 1.6;
}
/* Title area */
.title-area {
margin-bottom: 0;
padding-bottom: 24px;
border-bottom: 1px solid #EEECE8;
}
.main-title {
font-size: 40px;
font-weight: 200;
color: #2A2A2A;
letter-spacing: -0.5px;
line-height: 1.2;
}
.main-title .accent {
font-weight: 400;
color: #2A2A2A;
}
.subtitle {
font-size: 14px;
font-weight: 300;
color: #A0A09A;
margin-top: 8px;
letter-spacing: 0.3px;
}
/* Center: Hero data section */
.hero-data {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0;
position: relative;
padding-bottom: 32px;
border-bottom: 1px solid #EEECE8;
}
/* Three metric cards */
.metric-card {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
padding: 32px 24px;
}
.metric-card::after {
content: '';
position: absolute;
right: 0;
top: 25%;
height: 50%;
width: 1px;
background: linear-gradient(to bottom, transparent, #E0DCD6 50%, transparent);
}
.metric-card:last-child::after {
display: none;
}
.metric-value {
font-size: 112px;
font-weight: 200;
color: #2A2A2A;
letter-spacing: -4px;
line-height: 1;
position: relative;
}
.metric-value .dot {
color: #D4A574;
font-weight: 300;
}
.metric-unit {
font-size: 28px;
font-weight: 200;
color: #D4A574;
vertical-align: super;
margin-left: 2px;
opacity: 0.8;
}
.metric-name {
font-size: 12px;
font-weight: 500;
letter-spacing: 2px;
text-transform: uppercase;
color: #888888;
margin-top: 16px;
margin-bottom: 8px;
}
.metric-category {
font-size: 11px;
font-weight: 300;
color: #B8B4AE;
letter-spacing: 0.5px;
}
/* Comparison bars below each metric */
.comparison-group {
margin-top: 24px;
width: 280px;
}
.comp-row {
display: flex;
align-items: center;
margin-bottom: 8px;
gap: 8px;
}
.comp-label {
font-size: 11px;
font-weight: 400;
color: #A8A4A0;
width: 72px;
text-align: right;
flex-shrink: 0;
}
.comp-track {
flex: 1;
height: 2px;
background: #EEECEA;
border-radius: 1px;
position: relative;
overflow: hidden;
}
.comp-fill {
height: 100%;
border-radius: 1px;
background: #D8D5D0;
}
.comp-fill.gold {
background: #D4A574;
height: 3px;
margin-top: -0.5px;
}
.comp-val {
font-size: 11px;
font-weight: 500;
color: #999999;
width: 40px;
flex-shrink: 0;
}
.comp-val.gold {
color: #D4A574;
font-weight: 500;
}
/* Bottom section */
.bottom-section {
display: flex;
justify-content: space-between;
align-items: flex-end;
padding-top: 24px;
}
.insight-text {
font-size: 13px;
font-weight: 300;
color: #999;
line-height: 1.8;
max-width: 560px;
}
.insight-text strong {
font-weight: 500;
color: #666;
}
.brand-mark {
display: flex;
align-items: center;
gap: 16px;
}
.brand-line {
width: 32px;
height: 1px;
background: #D4A574;
opacity: 0.6;
}
.brand-text {
font-size: 10px;
font-weight: 400;
letter-spacing: 3px;
color: #C8C4BC;
}
/* Slide indicator — functional PPT element */
.slide-indicator {
position: absolute;
top: 64px;
right: 96px;
display: flex;
gap: 6px;
align-items: center;
}
.slide-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: #E0DCD6;
}
.slide-dot.active {
background: #D4A574;
width: 16px;
border-radius: 2px;
}
</style>
</head>
<body>
<div class="container">
<!-- Top row -->
<div class="top-row">
<div class="eyebrow">GLM-4.7 Open-Source Model</div>
<div class="source-note">Benchmark Evaluation 2025<br>Official Results</div>
</div>
<!-- Title -->
<div class="title-area">
<div class="main-title">Coding Capability <span style="font-weight:400;">Breakthrough</span><span style="color:#D4A574; font-weight:300; font-size:48px;">.</span></div>
<div class="subtitle">First open-source model to achieve state-of-the-art across all major coding benchmarks</div>
</div>
<!-- Hero data -->
<div class="hero-data">
<!-- AIME 2025 -->
<div class="metric-card">
<div class="metric-value">95<span class="dot">.</span>7</div>
<div class="metric-name">AIME 2025</div>
<div class="metric-category">Mathematical Reasoning</div>
<div class="comparison-group">
<div class="comp-row">
<span class="comp-label">GLM-4.7</span>
<div class="comp-track"><div class="comp-fill gold" style="width: 100%;"></div></div>
<span class="comp-val gold">95.7</span>
</div>
<div class="comp-row">
<span class="comp-label">Claude 3.5</span>
<div class="comp-track"><div class="comp-fill" style="width: 92.2%;"></div></div>
<span class="comp-val">88.2</span>
</div>
<div class="comp-row">
<span class="comp-label">GPT-4o</span>
<div class="comp-track"><div class="comp-fill" style="width: 87.4%;"></div></div>
<span class="comp-val">83.6</span>
</div>
</div>
</div>
<!-- SWE-bench Verified -->
<div class="metric-card">
<div class="metric-value">73<span class="dot">.</span>8<span class="metric-unit">%</span></div>
<div class="metric-name">SWE-bench Verified</div>
<div class="metric-category">Software Engineering</div>
<div class="comparison-group">
<div class="comp-row">
<span class="comp-label">GLM-4.7</span>
<div class="comp-track"><div class="comp-fill gold" style="width: 100%;"></div></div>
<span class="comp-val gold">73.8%</span>
</div>
<div class="comp-row">
<span class="comp-label">Claude 3.5</span>
<div class="comp-track"><div class="comp-fill" style="width: 72.2%;"></div></div>
<span class="comp-val">53.3%</span>
</div>
<div class="comp-row">
<span class="comp-label">GPT-4o</span>
<div class="comp-track"><div class="comp-fill" style="width: 65.3%;"></div></div>
<span class="comp-val">48.2%</span>
</div>
</div>
</div>
<!-- Tau-bench -->
<div class="metric-card">
<div class="metric-value">87<span class="dot">.</span>4</div>
<div class="metric-name">&tau;&sup2;-Bench</div>
<div class="metric-category">Agent Task Completion</div>
<div class="comparison-group">
<div class="comp-row">
<span class="comp-label">GLM-4.7</span>
<div class="comp-track"><div class="comp-fill gold" style="width: 100%;"></div></div>
<span class="comp-val gold">87.4</span>
</div>
<div class="comp-row">
<span class="comp-label">Claude 3.5</span>
<div class="comp-track"><div class="comp-fill" style="width: 90.3%;"></div></div>
<span class="comp-val">78.9</span>
</div>
<div class="comp-row">
<span class="comp-label">GPT-4o</span>
<div class="comp-track"><div class="comp-fill" style="width: 81.8%;"></div></div>
<span class="comp-val">71.5</span>
</div>
</div>
</div>
</div>
<!-- Bottom -->
<div class="bottom-section">
<div class="insight-text">
GLM-4.7 demonstrates that <strong>open-source models can compete at the frontier</strong> of coding intelligence,
outperforming leading proprietary models with margins of <strong>+7.5 to +20.5 points</strong> across benchmarks.
</div>
<div class="brand-mark">
<div class="brand-line"></div>
<span class="brand-text">ZHIPU AI</span>
</div>
</div>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

+536
View File
@@ -0,0 +1,536 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1920">
<title>GLM-4.7 Coding Benchmark - Pentagram Style</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1920px;
height: 1080px;
overflow: hidden;
margin: 0;
background: #FFFFFF;
font-family: 'Helvetica Neue', Arial, sans-serif;
color: #111;
position: relative;
}
/* Top black bar */
.top-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 64px;
background: #111;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 80px;
z-index: 10;
}
.top-label {
font-size: 12px;
font-weight: 700;
letter-spacing: 3px;
text-transform: uppercase;
color: #fff;
}
.top-label .red { color: #E63946; }
.top-right {
font-size: 11px;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
color: #E63946;
}
/* Grid lines */
.grid-line-v {
position: absolute;
top: 64px;
bottom: 64px;
width: 1px;
background: #000;
opacity: 0.05;
}
.grid-line-h {
position: absolute;
left: 80px;
right: 80px;
height: 1px;
background: #000;
opacity: 0.05;
}
/* Left column — hero number + model info */
.left-col {
position: absolute;
left: 80px;
top: 104px;
width: 480px;
}
.model-tag {
font-size: 11px;
font-weight: 700;
letter-spacing: 3px;
text-transform: uppercase;
color: #999;
margin-bottom: 8px;
}
.model-name {
font-size: 48px;
font-weight: 900;
color: #111;
line-height: 1;
letter-spacing: -2px;
}
.model-name .version { color: #E63946; }
.hero-number {
font-size: 200px;
font-weight: 900;
line-height: 0.85;
letter-spacing: -10px;
color: #111;
margin-top: 24px;
}
.hero-number .decimal { color: #E63946; }
.hero-context {
font-size: 13px;
font-weight: 500;
color: #999;
letter-spacing: 1px;
text-transform: uppercase;
margin-top: 8px;
}
.key-message {
font-size: 16px;
font-weight: 400;
line-height: 1.6;
color: #666;
margin-top: 32px;
max-width: 400px;
}
.key-message strong {
color: #111;
font-weight: 700;
}
.open-badge {
display: inline-flex;
align-items: center;
gap: 8px;
margin-top: 24px;
padding: 8px 16px;
border: 2px solid #E63946;
font-size: 11px;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
color: #E63946;
}
/* Right area — 3 benchmark columns */
.data-area {
position: absolute;
left: 620px;
top: 104px;
right: 80px;
bottom: 64px;
display: flex;
gap: 0;
}
.bench-col {
flex: 1;
padding: 0 32px;
border-left: 1px solid #E8E8E8;
display: flex;
flex-direction: column;
}
.bench-col:first-child {
padding-left: 0;
border-left: none;
}
.bench-title {
font-size: 13px;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
color: #111;
margin-bottom: 4px;
}
.bench-type {
font-size: 11px;
font-weight: 400;
color: #BBB;
margin-bottom: 64px;
}
/* Hero score per column */
.bench-hero {
font-size: 80px;
font-weight: 900;
color: #E63946;
letter-spacing: -3px;
line-height: 1;
margin-bottom: 64px;
}
/* Horizontal bar chart */
.bar-group {
display: flex;
flex-direction: column;
gap: 24px;
}
.bar-row {
display: flex;
align-items: center;
gap: 16px;
}
.bar-label {
font-size: 13px;
font-weight: 600;
color: #888;
width: 90px;
flex-shrink: 0;
text-align: right;
}
.bar-label.highlight {
color: #111;
font-weight: 700;
}
.bar-track {
flex: 1;
height: 56px;
background: #F5F5F5;
position: relative;
}
.bar-fill {
height: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 14px;
}
.bar-fill.base {
background: #E0E0E0;
}
.bar-fill.dark {
background: #111;
}
.bar-fill.winner {
background: #E63946;
}
.bar-value {
font-size: 15px;
font-weight: 700;
color: #fff;
}
.bar-fill.base .bar-value {
color: #888;
}
/* Bottom bar */
.bottom-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 64px;
background: #111;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 80px;
z-index: 10;
}
.bottom-left {
display: flex;
align-items: center;
gap: 24px;
}
.bottom-logo {
font-size: 14px;
font-weight: 900;
color: #fff;
letter-spacing: 1px;
}
.bottom-divider {
width: 1px;
height: 20px;
background: #444;
}
.bottom-note {
font-size: 11px;
font-weight: 400;
color: #666;
}
.bottom-right-text {
font-size: 11px;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
color: #E63946;
}
/* Delta label */
.delta {
font-size: 12px;
font-weight: 700;
color: #E63946;
letter-spacing: 1px;
text-transform: uppercase;
margin-top: 24px;
padding-left: 106px;
}
/* Bottom summary row */
.summary-row {
position: absolute;
bottom: 96px;
left: 620px;
right: 80px;
display: flex;
border-top: 1px solid #E8E8E8;
padding-top: 24px;
}
.summary-item {
flex: 1;
padding: 0 32px;
}
.summary-item:first-child {
padding-left: 0;
}
.summary-num {
font-size: 32px;
font-weight: 900;
color: #111;
letter-spacing: -1px;
line-height: 1;
}
.summary-num .red { color: #E63946; }
.summary-desc {
font-size: 11px;
font-weight: 500;
color: #999;
letter-spacing: 1px;
text-transform: uppercase;
margin-top: 8px;
}
/* Winner markers */
.winner-dot {
position: absolute;
right: -8px;
top: 50%;
transform: translateY(-50%);
width: 6px;
height: 6px;
border-radius: 50%;
background: #E63946;
}
</style>
</head>
<body>
<!-- Top bar -->
<div class="top-bar">
<span class="top-label">Benchmark Report <span class="red">/</span> 2025 Coding Performance</span>
<span class="top-right">Open-Source SOTA</span>
</div>
<!-- Grid lines -->
<div class="grid-line-v" style="left: 80px;"></div>
<div class="grid-line-v" style="left: 620px;"></div>
<div class="grid-line-v" style="right: 80px;"></div>
<div class="grid-line-h" style="top: 104px;"></div>
<!-- Left column -->
<div class="left-col">
<div class="model-tag">Open-Source Model</div>
<div class="model-name">GLM-<span class="version">4.7</span></div>
<div class="hero-number">95<span class="decimal">.</span>7</div>
<div class="hero-context">AIME 2025 Score</div>
<div class="key-message">
<strong>First open-source model to achieve SOTA</strong> across all three major coding benchmarks, surpassing GPT-4o and Claude 3.5.
</div>
<div class="open-badge">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<circle cx="7" cy="7" r="6" stroke="#E63946" stroke-width="1.5"/>
<circle cx="7" cy="7" r="2.5" fill="#E63946"/>
</svg>
Open Source
</div>
</div>
<!-- Data columns -->
<div class="data-area">
<!-- AIME 2025 -->
<div class="bench-col">
<div class="bench-title">AIME 2025</div>
<div class="bench-type">Mathematical Reasoning</div>
<div class="bench-hero">95.7</div>
<div class="bar-group">
<div class="bar-row">
<span class="bar-label highlight">GLM-4.7</span>
<div class="bar-track">
<div class="bar-fill winner" style="width: 95.7%;">
<span class="bar-value">95.7</span>
</div>
</div>
</div>
<div class="bar-row">
<span class="bar-label">Claude 3.5</span>
<div class="bar-track">
<div class="bar-fill dark" style="width: 88.2%;">
<span class="bar-value">88.2</span>
</div>
</div>
</div>
<div class="bar-row">
<span class="bar-label">GPT-4o</span>
<div class="bar-track">
<div class="bar-fill base" style="width: 83.6%;">
<span class="bar-value">83.6</span>
</div>
</div>
</div>
</div>
<div class="delta">+7.5 vs closed-source best</div>
</div>
<!-- SWE-bench -->
<div class="bench-col">
<div class="bench-title">SWE-bench Verified</div>
<div class="bench-type">Software Engineering</div>
<div class="bench-hero">73.8</div>
<div class="bar-group">
<div class="bar-row">
<span class="bar-label highlight">GLM-4.7</span>
<div class="bar-track">
<div class="bar-fill winner" style="width: 73.8%;">
<span class="bar-value">73.8%</span>
</div>
</div>
</div>
<div class="bar-row">
<span class="bar-label">Claude 3.5</span>
<div class="bar-track">
<div class="bar-fill dark" style="width: 53.3%;">
<span class="bar-value">53.3%</span>
</div>
</div>
</div>
<div class="bar-row">
<span class="bar-label">GPT-4o</span>
<div class="bar-track">
<div class="bar-fill base" style="width: 48.2%;">
<span class="bar-value">48.2%</span>
</div>
</div>
</div>
</div>
<div class="delta">+20.5 vs closed-source best</div>
</div>
<!-- Tau-bench -->
<div class="bench-col">
<div class="bench-title">&tau;&sup2;-Bench</div>
<div class="bench-type">Agent Task Completion</div>
<div class="bench-hero">87.4</div>
<div class="bar-group">
<div class="bar-row">
<span class="bar-label highlight">GLM-4.7</span>
<div class="bar-track">
<div class="bar-fill winner" style="width: 87.4%;">
<span class="bar-value">87.4</span>
</div>
</div>
</div>
<div class="bar-row">
<span class="bar-label">Claude 3.5</span>
<div class="bar-track">
<div class="bar-fill dark" style="width: 78.9%;">
<span class="bar-value">78.9</span>
</div>
</div>
</div>
<div class="bar-row">
<span class="bar-label">GPT-4o</span>
<div class="bar-track">
<div class="bar-fill base" style="width: 71.5%;">
<span class="bar-value">71.5</span>
</div>
</div>
</div>
</div>
<div class="delta">+8.5 vs closed-source best</div>
</div>
</div>
<!-- Summary row -->
<div class="summary-row">
<div class="summary-item">
<div class="summary-num"><span class="red">3</span>/3</div>
<div class="summary-desc">Benchmarks Won</div>
</div>
<div class="summary-item">
<div class="summary-num"><span class="red">#1</span></div>
<div class="summary-desc">Open-Source Ranking</div>
</div>
<div class="summary-item">
<div class="summary-num">12<span class="red">.</span>2<span style="font-size:18px;color:#999;">avg</span></div>
<div class="summary-desc">Points Above Runner-Up</div>
</div>
</div>
<!-- Bottom bar -->
<div class="bottom-bar">
<div class="bottom-left">
<span class="bottom-logo">ZHIPU AI</span>
<div class="bottom-divider"></div>
<span class="bottom-note">Benchmark data sourced from official evaluation reports, 2025</span>
</div>
<span class="bottom-right-text">Open-Source SOTA</span>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

+497
View File
@@ -0,0 +1,497 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1920">
<title>GLM-4.7 Coding Benchmark - Takram Style</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Noto+Serif+SC:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1920px;
height: 1080px;
overflow: hidden;
margin: 0;
background: #F5F0EB;
font-family: 'Inter', sans-serif;
color: #3A3A3A;
position: relative;
}
/* Subtle background texture */
body::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background:
radial-gradient(ellipse at 20% 50%, rgba(168, 181, 160, 0.08) 0%, transparent 60%),
radial-gradient(ellipse at 80% 30%, rgba(200, 190, 175, 0.06) 0%, transparent 50%);
pointer-events: none;
}
.layout {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: 480px 1fr;
grid-template-rows: 1fr;
position: relative;
z-index: 1;
}
/* Left panel */
.left-panel {
padding: 72px 48px 60px 72px;
display: flex;
flex-direction: column;
justify-content: space-between;
border-right: 1px solid rgba(107, 143, 113, 0.15);
}
.left-top {}
.category-label {
font-size: 10px;
font-weight: 500;
letter-spacing: 3px;
text-transform: uppercase;
color: #6B8F71;
margin-bottom: 32px;
opacity: 0.8;
}
.title-jp {
font-family: 'Noto Serif SC', serif;
font-size: 42px;
font-weight: 400;
color: #2D3436;
line-height: 1.4;
margin-bottom: 16px;
letter-spacing: 1px;
}
.title-en {
font-size: 15px;
font-weight: 300;
color: #999999;
line-height: 1.7;
max-width: 340px;
}
.model-badge {
display: inline-flex;
align-items: center;
gap: 8px;
margin-top: 36px;
padding: 10px 18px;
background: rgba(107, 143, 113, 0.08);
border: 1px solid rgba(107, 143, 113, 0.15);
border-radius: 24px;
}
.model-badge-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #6B8F71;
}
.model-badge-text {
font-size: 12px;
font-weight: 500;
color: #6B8F71;
letter-spacing: 1px;
}
/* Page indicator */
.page-indicator {
position: absolute;
bottom: 40px;
right: 72px;
font-family: 'Inter', sans-serif;
font-size: 10px;
font-weight: 300;
color: #C8C2B8;
letter-spacing: 1px;
}
/* Key insight */
.key-insight {
background: rgba(255, 255, 255, 0.5);
border-radius: 16px;
padding: 24px 28px;
border: 1px solid rgba(168, 181, 160, 0.2);
}
.key-insight-label {
font-size: 10px;
font-weight: 500;
letter-spacing: 2px;
text-transform: uppercase;
color: #A8B5A0;
margin-bottom: 10px;
}
.key-insight-text {
font-family: 'Noto Serif SC', serif;
font-size: 15px;
font-weight: 400;
color: #555555;
line-height: 1.8;
}
.left-bottom {
display: flex;
align-items: center;
gap: 12px;
}
.credit {
font-size: 11px;
font-weight: 400;
color: #BBBBBB;
letter-spacing: 0.5px;
}
/* Right panel - visualization */
.right-panel {
padding: 60px 72px 60px 60px;
display: flex;
flex-direction: column;
position: relative;
}
.viz-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
}
.viz-title {
font-size: 13px;
font-weight: 500;
letter-spacing: 1.5px;
text-transform: uppercase;
color: #888888;
}
.legend {
display: flex;
gap: 20px;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
}
.legend-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.legend-dot.glm { background: #6B8F71; }
.legend-dot.claude { background: #D4A574; }
.legend-dot.gpt { background: #C8C2B8; }
.legend-text {
font-size: 11px;
font-weight: 400;
color: #999999;
}
/* SVG radar chart area */
.radar-area {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.radar-svg {
filter: drop-shadow(0 4px 20px rgba(0,0,0,0.04));
}
/* Metric cards row */
.metric-cards {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
margin-top: 20px;
}
.m-card {
background: rgba(255, 255, 255, 0.6);
border-radius: 16px;
padding: 24px 28px;
border: 1px solid rgba(168, 181, 160, 0.15);
position: relative;
overflow: hidden;
}
.m-card::before {
content: '';
position: absolute;
top: 0;
left: 28px;
width: 32px;
height: 2px;
background: #6B8F71;
opacity: 0.4;
border-radius: 1px;
}
.m-card-name {
font-size: 11px;
font-weight: 500;
letter-spacing: 1.5px;
text-transform: uppercase;
color: #999999;
margin-bottom: 4px;
}
.m-card-type {
font-size: 11px;
font-weight: 300;
color: #BBBBBB;
margin-bottom: 16px;
}
.m-card-value {
font-size: 40px;
font-weight: 300;
color: #2D3436;
letter-spacing: -1px;
line-height: 1;
}
.m-card-value .unit {
font-size: 18px;
color: #6B8F71;
font-weight: 400;
}
.m-card-delta {
display: inline-flex;
align-items: center;
gap: 4px;
margin-top: 10px;
font-size: 12px;
font-weight: 500;
color: #7D9B72;
background: rgba(168, 181, 160, 0.12);
padding: 3px 10px;
border-radius: 12px;
}
.m-card-delta svg {
width: 10px;
height: 10px;
}
.m-card-competitors {
margin-top: 14px;
display: flex;
gap: 16px;
}
.comp-mini {
font-size: 11px;
font-weight: 400;
color: #AAAAAA;
}
.comp-mini span {
font-weight: 500;
color: #888888;
}
</style>
</head>
<body>
<div class="layout">
<!-- Left panel -->
<div class="left-panel">
<div class="left-top">
<div class="category-label">Benchmark Analysis</div>
<div class="title-jp">GLM-4.7<br>Coding 能力突破</div>
<div class="title-en">
Open-source model achieves state-of-the-art performance across all major coding benchmarks for the first time.
</div>
<div class="model-badge">
<div class="model-badge-dot"></div>
<span class="model-badge-text">GLM-4.7 Open Source</span>
</div>
<div class="key-insight" style="margin-top: 40px;">
<div class="key-insight-label">Key Finding</div>
<div class="key-insight-text">
在三项核心编程基准测试中,GLM-4.7 均超越 GPT-4o 和 Claude 3.5,成为首个达到 SOTA 水平的开源模型。
</div>
</div>
</div>
<div class="left-bottom">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<rect x="1" y="1" width="14" height="14" rx="3" stroke="#BBBBBB" stroke-width="1"/>
<path d="M5 8L7 10L11 6" stroke="#A8B5A0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span class="credit">Data: Official benchmark evaluations, 2026</span>
</div>
</div>
<!-- Right panel -->
<div class="right-panel">
<div class="viz-header">
<div class="viz-title">Performance Comparison <span style="font-weight:300;color:#B0AAA0;font-size:10px;margin-left:8px;">— 03 benchmarks</span></div>
<div class="legend">
<div class="legend-item"><div class="legend-dot glm"></div><span class="legend-text">GLM-4.7</span></div>
<div class="legend-item"><div class="legend-dot claude"></div><span class="legend-text">Claude 3.5</span></div>
<div class="legend-item"><div class="legend-dot gpt"></div><span class="legend-text">GPT-4o</span></div>
</div>
</div>
<!-- Radar chart SVG — art-piece treatment -->
<div class="radar-area">
<svg class="radar-svg" width="560" height="560" viewBox="0 0 560 560">
<!-- Subtle background circle (like a lens/scope) -->
<circle cx="280" cy="280" r="250" fill="none" stroke="#E8E4DC" stroke-width="0.3" opacity="0.5"/>
<!-- Grid circles — hand-drawn feel with varied dash -->
<circle cx="280" cy="280" r="220" fill="none" stroke="#DDD9D2" stroke-width="0.6" stroke-dasharray="2,6"/>
<circle cx="280" cy="280" r="176" fill="none" stroke="#DDD9D2" stroke-width="0.5" stroke-dasharray="2,6"/>
<circle cx="280" cy="280" r="132" fill="none" stroke="#DDD9D2" stroke-width="0.4" stroke-dasharray="2,6"/>
<circle cx="280" cy="280" r="88" fill="none" stroke="#DDD9D2" stroke-width="0.4" stroke-dasharray="2,6"/>
<circle cx="280" cy="280" r="44" fill="none" stroke="#DDD9D2" stroke-width="0.3" stroke-dasharray="2,6"/>
<!-- Center point -->
<circle cx="280" cy="280" r="2.5" fill="#6B8F71" opacity="0.4"/>
<!-- Grid scale labels — positioned along axis -->
<text x="288" y="62" font-family="Inter" font-size="9" fill="#C8C2B8" font-weight="300">100</text>
<text x="288" y="106" font-family="Inter" font-size="9" fill="#C8C2B8" font-weight="300">80</text>
<text x="288" y="150" font-family="Inter" font-size="9" fill="#C8C2B8" font-weight="300">60</text>
<text x="288" y="194" font-family="Inter" font-size="9" fill="#C8C2B8" font-weight="300">40</text>
<!-- Axis lines — delicate -->
<line x1="280" y1="280" x2="280" y2="55" stroke="#D4CFC6" stroke-width="0.5"/>
<line x1="280" y1="280" x2="475" y2="392" stroke="#D4CFC6" stroke-width="0.5"/>
<line x1="280" y1="280" x2="85" y2="392" stroke="#D4CFC6" stroke-width="0.5"/>
<!-- Axis endpoint markers -->
<circle cx="280" cy="55" r="2" fill="none" stroke="#D4CFC6" stroke-width="0.6"/>
<circle cx="475" cy="392" r="2" fill="none" stroke="#D4CFC6" stroke-width="0.6"/>
<circle cx="85" cy="392" r="2" fill="none" stroke="#D4CFC6" stroke-width="0.6"/>
<!-- Axis labels with index -->
<text x="280" y="38" font-family="Inter" font-size="12" fill="#8A857D" font-weight="500" text-anchor="middle" letter-spacing="1.5">AIME 2025</text>
<text x="280" y="28" font-family="Inter" font-size="7" fill="#B0AAA0" text-anchor="middle" letter-spacing="0.5">Mathematical Reasoning</text>
<text x="492" y="408" font-family="Inter" font-size="12" fill="#8A857D" font-weight="500" text-anchor="start" letter-spacing="1.5">SWE-bench</text>
<text x="492" y="422" font-family="Inter" font-size="7" fill="#B0AAA0" text-anchor="start" letter-spacing="0.5">Software Engineering</text>
<text x="68" y="408" font-family="Inter" font-size="12" fill="#8A857D" font-weight="500" text-anchor="end" letter-spacing="1.5">&tau;&sup2;-Bench</text>
<text x="68" y="422" font-family="Inter" font-size="7" fill="#B0AAA0" text-anchor="end" letter-spacing="0.5">Agent Tasks</text>
<!-- GPT-4o polygon (lightest) -->
<polygon
points="280,96.1 371.8,333 143.8,358.7"
fill="rgba(219,219,219,0.12)" stroke="#D4CFC6" stroke-width="1" stroke-dasharray="4,3"
/>
<!-- Claude 3.5 polygon -->
<polygon
points="280,86 381.6,338.6 129.7,366.8"
fill="rgba(212,165,116,0.08)" stroke="#D4A574" stroke-width="1.2"
/>
<!-- GLM-4.7 polygon (prominent, sage green) -->
<polygon
points="280,69.5 420.6,361.2 113.5,376.2"
fill="rgba(107,143,113,0.1)" stroke="#6B8F71" stroke-width="2"
/>
<!-- Data points - GLM-4.7 (larger, prominent) -->
<circle cx="280" cy="69.5" r="6" fill="#6B8F71" opacity="0.8"/>
<circle cx="280" cy="69.5" r="10" fill="none" stroke="#6B8F71" stroke-width="0.6" opacity="0.3"/>
<circle cx="420.6" cy="361.2" r="6" fill="#6B8F71" opacity="0.8"/>
<circle cx="420.6" cy="361.2" r="10" fill="none" stroke="#6B8F71" stroke-width="0.6" opacity="0.3"/>
<circle cx="113.5" cy="376.2" r="6" fill="#6B8F71" opacity="0.8"/>
<circle cx="113.5" cy="376.2" r="10" fill="none" stroke="#6B8F71" stroke-width="0.6" opacity="0.3"/>
<!-- Data points - Claude 3.5 -->
<circle cx="280" cy="86" r="3.5" fill="#D4A574" opacity="0.7"/>
<circle cx="381.6" cy="338.6" r="3.5" fill="#D4A574" opacity="0.7"/>
<circle cx="129.7" cy="366.8" r="3.5" fill="#D4A574" opacity="0.7"/>
<!-- Data points - GPT-4o -->
<circle cx="280" cy="96.1" r="2.5" fill="#C8C2B8" opacity="0.6"/>
<circle cx="371.8" cy="333" r="2.5" fill="#C8C2B8" opacity="0.6"/>
<circle cx="143.8" cy="358.7" r="2.5" fill="#C8C2B8" opacity="0.6"/>
<!-- Value labels for GLM-4.7 — annotation style -->
<line x1="280" y1="69.5" x2="316" y2="52" stroke="#6B8F71" stroke-width="0.5" opacity="0.4"/>
<text x="320" y="50" font-family="Inter" font-size="14" fill="#6B8F71" font-weight="600">95.7</text>
<line x1="420.6" y1="361.2" x2="448" y2="348" stroke="#6B8F71" stroke-width="0.5" opacity="0.4"/>
<text x="452" y="352" font-family="Inter" font-size="14" fill="#6B8F71" font-weight="600">73.8%</text>
<line x1="113.5" y1="376.2" x2="82" y2="392" stroke="#6B8F71" stroke-width="0.5" opacity="0.4"/>
<text x="78" y="390" font-family="Inter" font-size="14" fill="#6B8F71" font-weight="600" text-anchor="end">87.4</text>
<!-- Spec annotation — bottom-right -->
<text x="505" y="530" font-family="Inter" font-size="8" fill="#C8C2B8" font-weight="300" letter-spacing="1" text-anchor="end">Fig. 01 — Tri-axis Performance Map</text>
</svg>
</div>
<!-- Metric cards -->
<div class="metric-cards">
<div class="m-card">
<div class="m-card-name">AIME 2025</div>
<div class="m-card-type">Mathematical Reasoning</div>
<div class="m-card-value">95.7</div>
<div class="m-card-delta">
<svg viewBox="0 0 10 10" fill="none"><path d="M5 2L8 7H2L5 2Z" fill="#7D9B72"/></svg>
+7.5 vs Claude 3.5
</div>
<div class="m-card-competitors">
<span class="comp-mini">Claude 3.5: <span>88.2</span></span>
<span class="comp-mini">GPT-4o: <span>83.6</span></span>
</div>
</div>
<div class="m-card">
<div class="m-card-name">SWE-bench Verified</div>
<div class="m-card-type">Software Engineering</div>
<div class="m-card-value">73.8<span class="unit">%</span></div>
<div class="m-card-delta">
<svg viewBox="0 0 10 10" fill="none"><path d="M5 2L8 7H2L5 2Z" fill="#7D9B72"/></svg>
+20.5 vs Claude 3.5
</div>
<div class="m-card-competitors">
<span class="comp-mini">Claude 3.5: <span>53.3%</span></span>
<span class="comp-mini">GPT-4o: <span>48.2%</span></span>
</div>
</div>
<div class="m-card">
<div class="m-card-name">&tau;&sup2;-Bench</div>
<div class="m-card-type">Agent Task Completion</div>
<div class="m-card-value">87.4</div>
<div class="m-card-delta">
<svg viewBox="0 0 10 10" fill="none"><path d="M5 2L8 7H2L5 2Z" fill="#7D9B72"/></svg>
+8.5 vs Claude 3.5
</div>
<div class="m-card-competitors">
<span class="comp-mini">Claude 3.5: <span>78.9</span></span>
<span class="comp-mini">GPT-4o: <span>71.5</span></span>
</div>
</div>
</div>
<div class="page-indicator">07 / 24</div>
</div>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

@@ -0,0 +1,385 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1440">
<title>AI Compass — Build Studio Style</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1440px;
height: 900px;
overflow: hidden;
margin: 0;
font-family: 'Inter', sans-serif;
background: #FAFAF8;
color: #1A1A1A;
}
/* NAV */
nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28px 80px;
}
.nav-logo {
font-weight: 500;
font-size: 18px;
letter-spacing: 2px;
text-transform: uppercase;
display: flex;
align-items: center;
gap: 8px;
color: #1A1A1A;
}
.nav-logo svg {
color: #D4A574;
}
.nav-links {
display: flex;
gap: 40px;
list-style: none;
}
.nav-links a {
text-decoration: none;
color: #999;
font-size: 13px;
font-weight: 400;
letter-spacing: 1px;
transition: color 0.3s;
}
.nav-links a:hover { color: #1A1A1A; }
.nav-cta {
font-size: 12px;
font-weight: 400;
letter-spacing: 1px;
background: transparent;
color: #888;
border: 1px solid rgba(0,0,0,0.08);
padding: 8px 24px;
border-radius: 2px;
cursor: pointer;
transition: all 0.3s;
}
.nav-cta:hover {
border-color: #D4A574;
color: #D4A574;
}
/* HERO */
.hero {
text-align: center;
padding: 64px 80px 0;
}
.hero-eyebrow {
font-size: 10px;
font-weight: 400;
letter-spacing: 4px;
text-transform: uppercase;
color: #B0ACA4;
margin-bottom: 24px;
}
.hero h1 {
font-size: 52px;
font-weight: 200;
line-height: 1.15;
letter-spacing: -1px;
max-width: 700px;
margin: 0 auto;
color: #1A1A1A;
}
.hero h1 em {
font-style: italic;
font-weight: 300;
color: #D4A574;
}
.hero-sub {
font-size: 16px;
font-weight: 300;
color: #888;
margin-top: 16px;
letter-spacing: 0.3px;
}
/* SEARCH */
.search-wrapper {
max-width: 600px;
margin: 32px auto 0;
position: relative;
}
.search-bar {
width: 100%;
padding: 18px 56px 18px 24px;
font-family: 'Inter', sans-serif;
font-size: 15px;
font-weight: 300;
color: #1A1A1A;
background: #FFFFFF;
border: 1px solid #E8E4DF;
border-radius: 2px;
outline: none;
box-shadow: 0 2px 20px rgba(0,0,0,0.04);
transition: box-shadow 0.3s, border-color 0.3s;
}
.search-bar::placeholder { color: #BBB; }
.search-bar:focus {
box-shadow: 0 4px 30px rgba(212,165,116,0.12);
border-color: #D4A574;
}
.search-icon {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
color: #D4A574;
}
/* CATEGORIES */
.categories {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 32px;
flex-wrap: wrap;
}
.cat-pill {
font-size: 12px;
font-weight: 400;
color: #999;
padding: 8px 16px;
background: transparent;
border: 1px solid #E8E4DF;
border-radius: 2px;
cursor: pointer;
transition: all 0.25s;
letter-spacing: 0.3px;
}
.cat-pill:hover {
border-color: #D4A574;
color: #1A1A1A;
}
.cat-pill.active {
border-color: #D4A574;
color: #D4A574;
background: rgba(212,165,116,0.06);
}
/* TOOL CARDS */
.tools-section {
padding: 48px 80px 0;
}
.tools-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.tools-header h2 {
font-size: 13px;
font-weight: 500;
letter-spacing: 2px;
text-transform: uppercase;
color: #999;
}
.tools-header a {
font-size: 13px;
font-weight: 400;
color: #D4A574;
text-decoration: none;
display: flex;
align-items: center;
gap: 6px;
transition: opacity 0.3s;
}
.tools-header a:hover { opacity: 0.7; }
.tools-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.tool-card {
background: #FFFFFF;
border: 1px solid #EEEBE7;
border-radius: 2px;
padding: 24px;
cursor: pointer;
position: relative;
}
.tool-card-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
}
.tool-icon-box {
width: 44px;
height: 44px;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.tool-icon-box.claude { background: #F0EBE3; color: #D4A574; }
.tool-icon-box.cursor { background: #EEECEA; color: #999; }
.tool-icon-box.midjourney { background: #EEECEA; color: #999; }
.tool-icon-box.perplexity { background: #EEECEA; color: #999; }
.tool-card-name {
font-size: 17px;
font-weight: 500;
letter-spacing: -0.3px;
}
.tool-card-cat {
font-size: 11px;
font-weight: 400;
color: #BBB;
letter-spacing: 0.5px;
margin-top: 2px;
}
.tool-card-desc {
font-size: 14px;
font-weight: 300;
color: #888;
line-height: 1.55;
}
.tool-card-tag {
display: inline-block;
margin-top: 16px;
font-size: 11px;
font-weight: 500;
color: #D4A574;
letter-spacing: 0.5px;
padding: 4px 10px;
background: rgba(212,165,116,0.1);
border-radius: 2px;
}
/* DIVIDER */
.divider {
width: 40px;
height: 1px;
background: #D4A574;
margin: 0 auto;
opacity: 0.5;
}
</style>
</head>
<body>
<nav>
<div class="nav-logo">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88" fill="currentColor" stroke="currentColor"/>
</svg>
AI Compass
</div>
<ul class="nav-links">
<li><a href="#">Browse</a></li>
<li><a href="#">Categories</a></li>
<li><a href="#">New This Week</a></li>
<li><a href="#">Newsletter</a></li>
</ul>
<button class="nav-cta">Submit Tool</button>
</nav>
<section class="hero">
<p class="hero-eyebrow">A Curated Directory</p>
<h1>Find the right AI tool <em>in seconds</em></h1>
<p class="hero-sub">500+ tools, 24 categories, updated weekly</p>
<div class="search-wrapper">
<input class="search-bar" type="text" placeholder="Search by tool name, category, or use case...">
<i data-lucide="search" class="search-icon" style="width:18px;height:18px;"></i>
</div>
<div class="categories">
<span class="cat-pill active">Writing</span>
<span class="cat-pill">Coding</span>
<span class="cat-pill">Image</span>
<span class="cat-pill">Video</span>
<span class="cat-pill">Audio</span>
<span class="cat-pill">Productivity</span>
<span class="cat-pill">Research</span>
</div>
</section>
<section class="tools-section">
<div class="tools-header">
<h2>Featured Selections</h2>
<a href="#">
View all 500+ tools
<i data-lucide="arrow-right" style="width:14px;height:14px;"></i>
</a>
</div>
<div class="tools-grid">
<div class="tool-card">
<div class="tool-card-header">
<div class="tool-icon-box claude">
<i data-lucide="sparkles" style="width:20px;height:20px;"></i>
</div>
<div>
<div class="tool-card-name">Claude</div>
<div class="tool-card-cat">Writing & Analysis</div>
</div>
</div>
<p class="tool-card-desc">Advanced AI assistant for writing, analysis, and coding with nuanced reasoning and extended context.</p>
<span class="tool-card-tag">Editor's Pick</span>
</div>
<div class="tool-card">
<div class="tool-card-header">
<div class="tool-icon-box cursor">
<i data-lucide="code-2" style="width:20px;height:20px;"></i>
</div>
<div>
<div class="tool-card-name">Cursor</div>
<div class="tool-card-cat">Development</div>
</div>
</div>
<p class="tool-card-desc">AI-native code editor that understands your entire codebase and accelerates your development workflow.</p>
<span class="tool-card-tag">Trending</span>
</div>
<div class="tool-card">
<div class="tool-card-header">
<div class="tool-icon-box midjourney">
<i data-lucide="image" style="width:20px;height:20px;"></i>
</div>
<div>
<div class="tool-card-name">Midjourney</div>
<div class="tool-card-cat">Image Generation</div>
</div>
</div>
<p class="tool-card-desc">Leading AI image generation platform producing stunning, highly detailed visuals from text prompts.</p>
<span class="tool-card-tag">Popular</span>
</div>
<div class="tool-card">
<div class="tool-card-header">
<div class="tool-icon-box perplexity">
<i data-lucide="globe" style="width:20px;height:20px;"></i>
</div>
<div>
<div class="tool-card-name">Perplexity</div>
<div class="tool-card-cat">Research & Search</div>
</div>
</div>
<p class="tool-card-desc">AI-powered search engine delivering real-time, cited answers in a natural conversational format.</p>
<span class="tool-card-tag">Staff Pick</span>
</div>
</div>
</section>
<script>
lucide.createIcons();
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

@@ -0,0 +1,422 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1440">
<title>AI Compass — Pentagram Style</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1440px;
height: 900px;
overflow: hidden;
margin: 0;
font-family: 'Helvetica Neue', Arial, sans-serif;
background: #FFFFFF;
color: #000000;
}
/* NAV */
nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px 64px;
border-bottom: 2px solid #000;
}
.nav-logo {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-weight: 700;
font-size: 20px;
letter-spacing: -0.5px;
display: flex;
align-items: center;
gap: 8px;
}
.nav-logo .compass-icon {
width: 24px;
height: 24px;
}
.nav-links {
display: flex;
gap: 32px;
list-style: none;
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 1.5px;
}
.nav-links a {
text-decoration: none;
color: #000;
transition: color 0.2s;
}
.nav-links a:hover { color: #E63946; }
.nav-submit {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
background: #000;
color: #fff;
border: none;
padding: 10px 24px;
cursor: pointer;
transition: background 0.2s;
}
.nav-submit:hover { background: #E63946; }
/* HERO GRID */
.hero {
display: grid;
grid-template-columns: 1fr 1fr;
min-height: calc(900px - 72px);
}
/* LEFT PANEL */
.hero-left {
padding: 56px 64px 48px;
display: flex;
flex-direction: column;
justify-content: space-between;
border-right: 2px solid #000;
}
.hero-stat {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 180px;
font-weight: 900;
line-height: 0.85;
letter-spacing: -8px;
color: #E63946;
position: relative;
}
.hero-stat span {
font-size: 48px;
letter-spacing: -2px;
vertical-align: top;
margin-left: 4px;
}
.hero-headline {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 42px;
font-weight: 900;
line-height: 1.08;
letter-spacing: -1.5px;
margin-top: 24px;
max-width: 520px;
}
.hero-sub {
font-size: 15px;
color: #555;
margin-top: 16px;
letter-spacing: 0.2px;
line-height: 1.5;
}
/* SEARCH */
.search-box {
display: flex;
border: 3px solid #000;
margin-top: 32px;
max-width: 560px;
}
.search-box input {
flex: 1;
padding: 16px 20px;
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 15px;
border: none;
outline: none;
background: #fff;
}
.search-box button {
padding: 16px 28px;
background: #000;
color: #fff;
border: none;
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: background 0.2s;
}
.search-box button:hover { background: #E63946; }
/* CATEGORY TAGS */
.categories {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 28px;
}
.cat-tag {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 2px;
padding: 6px 14px;
border: 2px solid #000;
background: transparent;
cursor: pointer;
transition: all 0.15s;
font-family: 'Helvetica Neue', Arial, sans-serif;
}
.cat-tag:hover {
background: #000;
color: #fff;
}
.cat-tag.active {
background: #E63946;
border-color: #E63946;
color: #fff;
}
/* RIGHT PANEL — TOOL LIST */
.hero-right {
display: flex;
flex-direction: column;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 48px;
border-bottom: 2px solid #000;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 2px;
color: #888;
}
.tool-item {
display: grid;
grid-template-columns: 48px 1fr auto;
align-items: center;
padding: 24px 48px;
border-bottom: 1px solid #E0E0E0;
transition: background 0.15s;
cursor: pointer;
}
.tool-item:hover {
background: #F7F7F7;
}
.tool-index {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
font-weight: 500;
color: #BBB;
}
.tool-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.tool-name-row {
display: flex;
align-items: center;
gap: 12px;
}
.tool-name {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 22px;
font-weight: 600;
letter-spacing: -0.5px;
}
.tool-badge {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
color: #E63946;
background: rgba(230, 57, 70, 0.08);
padding: 3px 8px;
}
.tool-desc {
font-size: 13px;
color: #777;
line-height: 1.4;
max-width: 400px;
}
.tool-category {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 2px;
color: #999;
white-space: nowrap;
}
.tool-item:last-child {
border-bottom: none;
}
/* FEATURED TAG */
.tool-item.featured {
border-left: 4px solid #E63946;
padding-left: 44px;
}
.bottom-bar {
margin-top: auto;
padding: 16px 48px;
border-top: 2px solid #000;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #888;
font-weight: 500;
letter-spacing: 0.5px;
}
.bottom-bar a {
color: #000;
text-decoration: none;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
font-size: 11px;
display: flex;
align-items: center;
gap: 6px;
transition: color 0.2s;
}
.bottom-bar a:hover { color: #E63946; }
</style>
</head>
<body>
<nav>
<div class="nav-logo">
<svg class="compass-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88" fill="#E63946" stroke="#E63946"/>
</svg>
AI Compass
</div>
<ul class="nav-links">
<li><a href="#">Browse</a></li>
<li><a href="#">Categories</a></li>
<li><a href="#">New Tools</a></li>
<li><a href="#">About</a></li>
</ul>
<button class="nav-submit">Submit a Tool</button>
</nav>
<div class="hero">
<!-- LEFT -->
<div class="hero-left">
<div>
<div class="hero-stat">500<span>+</span></div>
<h1 class="hero-headline">Find the right AI tool in seconds</h1>
<p class="hero-sub">500+ tools, 24 categories, updated weekly. The most comprehensive curated directory for AI practitioners.</p>
<div class="search-box">
<input type="text" placeholder="Search tools by name, category, or use case...">
<button>
<i data-lucide="search" style="width:16px;height:16px;"></i>
Search
</button>
</div>
<div class="categories">
<span class="cat-tag active">Writing</span>
<span class="cat-tag">Coding</span>
<span class="cat-tag">Image</span>
<span class="cat-tag">Video</span>
<span class="cat-tag">Audio</span>
<span class="cat-tag">Productivity</span>
<span class="cat-tag">Research</span>
</div>
</div>
</div>
<!-- RIGHT -->
<div class="hero-right">
<div class="list-header">
<span>Featured Tools</span>
<span>Category</span>
</div>
<div class="tool-item featured">
<span class="tool-index">01</span>
<div class="tool-info">
<div class="tool-name-row">
<span class="tool-name">Claude</span>
<span class="tool-badge">Editor's Pick</span>
</div>
<span class="tool-desc">Advanced AI assistant for writing, analysis, and coding with extended context and nuanced reasoning.</span>
</div>
<span class="tool-category">Writing</span>
</div>
<div class="tool-item">
<span class="tool-index">02</span>
<div class="tool-info">
<div class="tool-name-row">
<span class="tool-name">Cursor</span>
<span class="tool-badge">Trending</span>
</div>
<span class="tool-desc">AI-native code editor that understands your entire codebase and accelerates development workflows.</span>
</div>
<span class="tool-category">Coding</span>
</div>
<div class="tool-item">
<span class="tool-index">03</span>
<div class="tool-info">
<div class="tool-name-row">
<span class="tool-name">Midjourney</span>
</div>
<span class="tool-desc">Leading AI image generation platform producing stunning visuals from text descriptions.</span>
</div>
<span class="tool-category">Image</span>
</div>
<div class="tool-item">
<span class="tool-index">04</span>
<div class="tool-info">
<div class="tool-name-row">
<span class="tool-name">Perplexity</span>
</div>
<span class="tool-desc">AI-powered search engine with real-time citations and conversational answers.</span>
</div>
<span class="tool-category">Research</span>
</div>
<div class="tool-item">
<span class="tool-index">05</span>
<div class="tool-info">
<div class="tool-name-row">
<span class="tool-name">Runway</span>
<span class="tool-badge">New</span>
</div>
<span class="tool-desc">Gen-3 video generation and editing suite for creators and filmmakers.</span>
</div>
<span class="tool-category">Video</span>
</div>
<div class="bottom-bar">
<span>Showing 5 of 500+ tools</span>
<a href="#">
View All Tools
<i data-lucide="arrow-right" style="width:14px;height:14px;"></i>
</a>
</div>
</div>
</div>
<script>
lucide.createIcons();
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

@@ -0,0 +1,499 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1440">
<title>AI Compass — Takram Style</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Noto+Serif+SC:wght@300;400;500;600&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1440px;
height: 900px;
overflow: hidden;
margin: 0;
font-family: 'Inter', sans-serif;
background: #F5F0EB;
color: #3A3A35;
}
/* NAV */
nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px 72px;
}
.nav-logo {
font-family: 'Noto Serif SC', serif;
font-weight: 500;
font-size: 18px;
display: flex;
align-items: center;
gap: 10px;
color: #3A3A35;
}
.nav-logo svg { color: #A8B5A0; }
.nav-right {
display: flex;
align-items: center;
gap: 36px;
}
.nav-links {
display: flex;
gap: 28px;
list-style: none;
}
.nav-links a {
text-decoration: none;
color: #8A8A80;
font-size: 14px;
font-weight: 400;
transition: color 0.3s;
}
.nav-links a:hover { color: #3A3A35; }
.nav-cta {
font-size: 13px;
font-weight: 500;
background: transparent;
color: #6B8F71;
border: 1px solid rgba(107, 143, 113, 0.35);
padding: 10px 24px;
border-radius: 100px;
cursor: pointer;
transition: all 0.3s;
}
.nav-cta:hover { background: rgba(107, 143, 113, 0.06); border-color: #6B8F71; }
/* MAIN LAYOUT */
.main {
display: grid;
grid-template-columns: 520px 1fr;
gap: 0;
padding: 20px 72px 0;
height: calc(900px - 68px);
}
/* LEFT: HERO TEXT */
.hero-text {
padding: 40px 48px 40px 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.hero-eyebrow {
font-size: 11px;
font-weight: 500;
color: #6B8F71;
letter-spacing: 2.5px;
text-transform: uppercase;
margin-bottom: 16px;
opacity: 0.8;
}
.hero-headline {
font-family: 'Noto Serif SC', serif;
font-size: 42px;
font-weight: 400;
line-height: 1.3;
letter-spacing: -0.5px;
color: #2D3436;
}
.hero-headline em {
font-style: normal;
color: #6B8F71;
font-weight: 500;
}
.hero-sub {
font-size: 15px;
font-weight: 300;
color: #8A8A80;
margin-top: 16px;
line-height: 1.6;
max-width: 400px;
}
/* SEARCH */
.search-wrapper {
margin-top: 32px;
position: relative;
max-width: 420px;
}
.search-bar {
width: 100%;
padding: 16px 50px 16px 20px;
font-family: 'Inter', sans-serif;
font-size: 14px;
font-weight: 300;
color: #3A3A35;
background: #EDE8DE;
border: 1px solid #DDD7CC;
border-radius: 14px;
outline: none;
transition: all 0.3s;
}
.search-bar::placeholder { color: #B0AEA4; }
.search-bar:focus {
background: #FFFFFF;
border-color: #6B8F71;
box-shadow: 0 4px 24px rgba(168,181,160,0.15);
}
.search-icon {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
color: #6B8F71;
}
/* CATEGORY CHIPS */
.categories {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 24px;
max-width: 420px;
}
.cat-chip {
font-size: 12px;
font-weight: 400;
color: #7A7A72;
padding: 7px 16px;
background: #EDE8DE;
border: none;
border-radius: 100px;
cursor: pointer;
transition: all 0.25s;
}
.cat-chip:hover {
background: #E0DBCF;
color: #3A3A35;
}
.cat-chip.active {
background: rgba(107, 143, 113, 0.15);
color: #6B8F71;
border: 1px solid rgba(107, 143, 113, 0.25);
}
/* DIAGRAM LINES (decorative connections) */
.diagram-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
}
/* RIGHT: TOOL CARDS */
.tools-area {
position: relative;
padding: 20px 0 0 20px;
}
.tools-label {
font-size: 10px;
font-weight: 500;
color: #6B8F71;
letter-spacing: 2.5px;
text-transform: uppercase;
margin-bottom: 20px;
padding-left: 4px;
opacity: 0.7;
}
.tools-organic {
position: relative;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.tool-card {
background: rgba(255,255,255,0.5);
border: 1px solid #E8E4DC;
border-radius: 14px;
padding: 24px;
transition: all 0.3s;
cursor: pointer;
position: relative;
}
.tool-card:hover {
box-shadow: 0 8px 32px rgba(0,0,0,0.06);
transform: translateY(-2px);
}
/* Organic offset: stagger cards */
.tool-card:nth-child(2) {
margin-top: 24px;
}
.tool-card:nth-child(3) {
margin-top: -12px;
}
.tool-card-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.tool-icon {
width: 40px;
height: 40px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.tool-icon.claude { background: rgba(212, 165, 116, 0.15); color: #D4A574; }
.tool-icon.cursor { background: rgba(139, 157, 195, 0.12); color: #8B9DC3; }
.tool-icon.midjourney { background: rgba(212, 165, 116, 0.12); color: #C4A882; }
.tool-icon.perplexity { background: rgba(107, 143, 113, 0.1); color: #6B8F71; }
.tool-name {
font-size: 16px;
font-weight: 500;
color: #2C2C28;
}
.tool-cat {
font-size: 11px;
color: #AAA89E;
margin-top: 1px;
}
.tool-desc {
font-size: 13px;
font-weight: 300;
color: #8A8A80;
line-height: 1.55;
}
.tool-tag {
display: inline-flex;
align-items: center;
gap: 4px;
margin-top: 14px;
font-size: 11px;
font-weight: 500;
color: #6B8F71;
padding: 4px 10px;
background: rgba(107,143,113,0.08);
border-radius: 100px;
}
/* Connection dots */
.conn-dot {
position: absolute;
width: 8px;
height: 8px;
border-radius: 50%;
background: #6B8F71;
opacity: 0.4;
}
.conn-dot.d1 { top: 80px; left: -10px; }
.conn-dot.d2 { top: 200px; left: -14px; }
.conn-dot.d3 { bottom: 160px; left: -10px; }
.conn-line {
position: absolute;
left: -10px;
width: 2px;
background: linear-gradient(to bottom, transparent, #A8B5A0, transparent);
opacity: 0.2;
}
.conn-line.l1 { top: 88px; height: 108px; }
.conn-line.l2 { top: 208px; height: 100px; }
/* VIEW MORE */
.view-more {
text-align: center;
margin-top: 16px;
}
.view-more a {
font-size: 13px;
font-weight: 400;
color: #6B8F71;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 6px;
transition: color 0.3s;
}
.view-more a:hover { color: #7A9470; }
/* FLOATING NOTE */
.floating-note {
position: absolute;
bottom: 40px;
left: 72px;
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #B0AEA4;
font-weight: 300;
}
.floating-note .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #6B8F71;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
</style>
</head>
<body>
<nav>
<div class="nav-logo">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88" fill="currentColor" stroke="currentColor"/>
</svg>
AI Compass
</div>
<div class="nav-right">
<ul class="nav-links">
<li><a href="#">Explore</a></li>
<li><a href="#">Categories</a></li>
<li><a href="#">Weekly Picks</a></li>
<li><a href="#">About</a></li>
</ul>
<button class="nav-cta">Submit Tool</button>
</div>
</nav>
<div class="main">
<!-- LEFT -->
<div class="hero-text">
<p class="hero-eyebrow">Curated Directory</p>
<h1 class="hero-headline">Find the right<br>AI tool <em>in seconds</em></h1>
<p class="hero-sub">500+ carefully selected tools across 24 categories, updated weekly. Discover, compare, and find the perfect tool for your workflow.</p>
<div class="search-wrapper">
<input class="search-bar" type="text" placeholder="Search tools, categories, or use cases...">
<i data-lucide="search" class="search-icon" style="width:16px;height:16px;"></i>
</div>
<div class="categories">
<span class="cat-chip active">Writing</span>
<span class="cat-chip">Coding</span>
<span class="cat-chip">Image</span>
<span class="cat-chip">Video</span>
<span class="cat-chip">Audio</span>
<span class="cat-chip">Productivity</span>
<span class="cat-chip">Research</span>
</div>
</div>
<!-- RIGHT -->
<div class="tools-area">
<div class="conn-dot d1"></div>
<div class="conn-line l1"></div>
<div class="conn-dot d2"></div>
<div class="conn-line l2"></div>
<div class="conn-dot d3"></div>
<p class="tools-label">Featured Discoveries</p>
<div class="tools-organic">
<div class="tool-card">
<div class="tool-card-header">
<div class="tool-icon claude">
<i data-lucide="sparkles" style="width:18px;height:18px;"></i>
</div>
<div>
<div class="tool-name">Claude</div>
<div class="tool-cat">Writing & Analysis</div>
</div>
</div>
<p class="tool-desc">Advanced AI assistant for writing, analysis, and coding with nuanced reasoning and extended context window.</p>
<span class="tool-tag">
<i data-lucide="star" style="width:10px;height:10px;"></i>
Editor's Pick
</span>
</div>
<div class="tool-card">
<div class="tool-card-header">
<div class="tool-icon cursor">
<i data-lucide="code-2" style="width:18px;height:18px;"></i>
</div>
<div>
<div class="tool-name">Cursor</div>
<div class="tool-cat">Development</div>
</div>
</div>
<p class="tool-desc">AI-native code editor that deeply understands your codebase and accelerates every development task.</p>
<span class="tool-tag">
<i data-lucide="trending-up" style="width:10px;height:10px;"></i>
Trending
</span>
</div>
<div class="tool-card">
<div class="tool-card-header">
<div class="tool-icon midjourney">
<i data-lucide="image" style="width:18px;height:18px;"></i>
</div>
<div>
<div class="tool-name">Midjourney</div>
<div class="tool-cat">Image Generation</div>
</div>
</div>
<p class="tool-desc">Create stunning, detailed visuals from text descriptions with the leading AI image generation platform.</p>
<span class="tool-tag">
<i data-lucide="heart" style="width:10px;height:10px;"></i>
Popular
</span>
</div>
<div class="tool-card">
<div class="tool-card-header">
<div class="tool-icon perplexity">
<i data-lucide="globe" style="width:18px;height:18px;"></i>
</div>
<div>
<div class="tool-name">Perplexity</div>
<div class="tool-cat">Research & Search</div>
</div>
</div>
<p class="tool-desc">AI-powered search delivering real-time answers with citations in a natural conversational format.</p>
<span class="tool-tag">
<i data-lucide="compass" style="width:10px;height:10px;"></i>
Staff Pick
</span>
</div>
</div>
<div class="view-more">
<a href="#">
Explore all 500+ tools
<i data-lucide="arrow-right" style="width:14px;height:14px;"></i>
</a>
</div>
</div>
</div>
<div class="floating-note">
<span class="dot"></span>
Updated weekly with new discoveries
</div>
<!-- Spec annotation -->
<svg style="position:absolute;bottom:60px;right:72px;opacity:0.15;" width="100" height="40" viewBox="0 0 100 40" fill="none">
<line x1="0" y1="20" x2="60" y2="20" stroke="#6B8F71" stroke-width="0.5"/>
<circle cx="60" cy="20" r="2" fill="none" stroke="#6B8F71" stroke-width="0.5"/>
<text x="68" y="23" font-family="Inter" font-size="8" fill="#6B8F71" letter-spacing="0.5">500+ tools</text>
</svg>
<script>
lucide.createIcons();
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

@@ -0,0 +1,562 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1440">
<title>Inkwell — AI Writing Assistant</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1440px;
height: 900px;
overflow: hidden;
margin: 0;
background: #FAFAF8;
font-family: 'Inter', sans-serif;
color: #2C2C2C;
}
.page {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 40px 80px 40px 80px;
}
/* NAV */
.nav-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 60px;
}
.logo {
font-size: 18px;
font-weight: 500;
letter-spacing: 3px;
text-transform: uppercase;
color: #2C2C2C;
}
.nav-links {
display: flex;
gap: 40px;
align-items: center;
}
.nav-links a {
font-size: 13px;
font-weight: 400;
color: #999;
text-decoration: none;
letter-spacing: 0.5px;
}
.nav-links a:hover {
color: #2C2C2C;
}
/* HERO AREA */
.hero-section {
flex: 1;
display: grid;
grid-template-columns: 440px 1fr;
gap: 80px;
align-items: center;
}
.hero-text {
display: flex;
flex-direction: column;
gap: 28px;
}
.headline {
font-size: 52px;
font-weight: 200;
line-height: 1.15;
letter-spacing: -1.5px;
color: #2C2C2C;
}
.headline em {
font-style: normal;
font-weight: 400;
color: #2C2C2C;
position: relative;
}
.headline em::after {
content: '';
position: absolute;
bottom: 4px;
left: 0;
width: 100%;
height: 1px;
background: #D4A574;
opacity: 0.5;
}
.subtitle {
font-size: 16px;
font-weight: 300;
line-height: 1.7;
color: #999;
max-width: 380px;
}
.cta-button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 14px 32px;
background: #2C2C2C;
color: #FAFAF8;
font-family: 'Inter', sans-serif;
font-size: 13px;
font-weight: 400;
letter-spacing: 0.5px;
text-decoration: none;
border: none;
border-radius: 2px;
cursor: pointer;
width: fit-content;
transition: background 0.3s;
}
.cta-button:hover {
background: #3C3C3C;
}
.social-proof {
font-size: 12px;
font-weight: 400;
color: #bbb;
letter-spacing: 0.5px;
}
/* FEATURES — minimal */
.features-row {
display: flex;
gap: 48px;
margin-top: 8px;
}
.feature-item {
display: flex;
align-items: flex-start;
gap: 12px;
}
.feature-icon {
width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(212, 165, 116, 0.12);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 1px;
}
.feature-icon svg {
color: #D4A574;
}
.feature-label {
font-size: 13px;
font-weight: 500;
color: #2C2C2C;
margin-bottom: 2px;
}
.feature-desc {
font-size: 12px;
font-weight: 300;
color: #aaa;
line-height: 1.5;
}
/* EDITOR MOCKUP — floating card */
.editor-wrapper {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.editor-card {
width: 100%;
max-width: 620px;
height: 580px;
background: #FFFFFF;
border-radius: 2px;
box-shadow:
0 4px 6px rgba(0,0,0,0.02),
0 12px 28px rgba(0,0,0,0.06),
0 40px 80px rgba(0,0,0,0.04);
display: grid;
grid-template-columns: 1fr 190px;
overflow: hidden;
}
.editor-main {
padding: 32px 28px;
display: flex;
flex-direction: column;
}
.editor-toolbar {
display: flex;
gap: 4px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #F0EDE8;
}
.tb-btn {
width: 32px;
height: 32px;
border-radius: 2px;
border: none;
background: transparent;
display: flex;
align-items: center;
justify-content: center;
color: #bbb;
cursor: pointer;
}
.tb-btn.active {
background: #F5F0E8;
color: #D4A574;
}
.doc-title {
font-size: 24px;
font-weight: 500;
letter-spacing: -0.5px;
color: #2C2C2C;
margin-bottom: 18px;
}
.doc-paragraph {
font-size: 14px;
font-weight: 300;
line-height: 1.9;
color: #666;
margin-bottom: 16px;
}
.doc-paragraph .ai-enhanced {
background: linear-gradient(120deg, rgba(212,165,116,0.1) 0%, rgba(212,165,116,0.18) 100%);
border-radius: 3px;
padding: 1px 4px;
}
.doc-h2 {
font-size: 17px;
font-weight: 500;
color: #2C2C2C;
margin-bottom: 12px;
margin-top: 4px;
}
.doc-list {
list-style: none;
padding: 0;
}
.doc-list li {
font-size: 13px;
font-weight: 300;
color: #777;
line-height: 1.8;
padding-left: 16px;
position: relative;
}
.doc-list li::before {
content: '';
position: absolute;
left: 0;
top: 10px;
width: 5px;
height: 5px;
border-radius: 50%;
background: #D4A574;
}
.cursor-line {
display: inline-block;
width: 1.5px;
height: 15px;
background: #D4A574;
animation: pulse 1.2s ease-in-out infinite;
vertical-align: text-bottom;
margin-left: 1px;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.2; }
}
/* AI SIDEBAR */
.ai-sidebar {
background: #FDFCFA;
border-left: 1px solid #F0EDE8;
padding: 24px 18px;
display: flex;
flex-direction: column;
gap: 16px;
}
.sidebar-title {
font-size: 11px;
font-weight: 500;
letter-spacing: 2px;
text-transform: uppercase;
color: #D4A574;
padding-bottom: 12px;
border-bottom: 1px solid #F0EDE8;
}
.ai-card {
background: #fff;
border-radius: 2px;
padding: 14px;
border: 1px solid #F0EDE8;
}
.ai-card-label {
font-size: 10px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 1px;
color: #bbb;
margin-bottom: 6px;
}
.ai-card-content {
font-size: 13px;
font-weight: 400;
color: #2C2C2C;
}
.voice-score {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
}
.score-track {
flex: 1;
height: 3px;
background: #F0EDE8;
border-radius: 2px;
overflow: hidden;
}
.score-fill {
width: 92%;
height: 100%;
background: #D4A574;
border-radius: 2px;
}
.score-num {
font-size: 12px;
font-weight: 500;
color: #D4A574;
}
.platform-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.p-tag {
font-size: 10px;
font-weight: 400;
padding: 4px 10px;
border-radius: 2px;
background: #F5F0E8;
color: #999;
}
.p-tag.active {
background: rgba(212,165,116,0.15);
color: #D4A574;
}
.ai-suggestion {
font-size: 12px;
font-weight: 300;
color: #888;
line-height: 1.6;
padding: 12px 14px;
background: #fff;
border-radius: 2px;
border: 1px solid #F0EDE8;
}
.ai-suggestion .label {
font-size: 10px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.8px;
color: #bbb;
display: block;
margin-bottom: 6px;
}
.refine-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 12px;
background: #2C2C2C;
color: #fff;
border: none;
border-radius: 2px;
font-family: 'Inter', sans-serif;
font-size: 12px;
font-weight: 500;
cursor: pointer;
letter-spacing: 0.5px;
}
</style>
</head>
<body>
<div class="page">
<!-- NAV -->
<nav class="nav-bar">
<div class="logo">Inkwell</div>
<div class="nav-links">
<a href="#">Features</a>
<a href="#">Pricing</a>
<a href="#">Stories</a>
<a href="#" class="cta-button" style="padding: 10px 24px; font-size: 12px; margin: 0;">Start Writing</a>
</div>
</nav>
<!-- HERO -->
<div class="hero-section">
<!-- LEFT: Text -->
<div class="hero-text">
<h1 class="headline">Write better,<br>faster, with<br><em>your own voice</em></h1>
<p class="subtitle">AI that learns your style, not replaces it. Publish across WeChat, Xiaohongshu, and video scripts while sounding unmistakably you.</p>
<div class="features-row">
<div class="feature-item">
<div class="feature-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
</div>
<div>
<div class="feature-label">Style Learning</div>
<div class="feature-desc">Adapts to your voice</div>
</div>
</div>
<div class="feature-item">
<div class="feature-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
</div>
<div>
<div class="feature-label">Multi-Platform</div>
<div class="feature-desc">One tool, every format</div>
</div>
</div>
<div class="feature-item">
<div class="feature-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
</div>
<div>
<div class="feature-label">Human Touch</div>
<div class="feature-desc">Warmth-preserving edit</div>
</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 24px;">
<a href="#" class="cta-button">
Start Writing
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
</a>
<span class="social-proof">Trusted by 10,000+ creators</span>
</div>
</div>
<!-- RIGHT: Editor Card -->
<div class="editor-wrapper">
<div class="editor-card">
<div class="editor-main">
<div class="editor-toolbar">
<button class="tb-btn active"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/></svg></button>
<button class="tb-btn"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg></button>
<button class="tb-btn"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="21" y1="6" x2="3" y2="6"/><line x1="15" y1="12" x2="3" y2="12"/><line x1="17" y1="18" x2="3" y2="18"/></svg></button>
<button class="tb-btn"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></button>
</div>
<div class="doc-title">Morning Routines for Creative Minds</div>
<div class="doc-paragraph">
The best ideas rarely arrive on schedule. They come in the quiet space between waking and doing — <span class="ai-enhanced">that liminal moment when the mind is loose enough to wander but awake enough to notice</span>.
</div>
<div class="doc-h2">Finding Your Rhythm</div>
<div class="doc-paragraph">
Productivity culture tells us to optimize every hour. But creation is not production. The most prolific writers I know guard their mornings like sacred ground.<span class="cursor-line"></span>
</div>
<ul class="doc-list">
<li>Start before checking your phone</li>
<li>Write the ugly first draft freely</li>
<li>Let AI handle polish, not direction</li>
</ul>
</div>
<div class="ai-sidebar">
<div class="sidebar-title">Inkwell AI</div>
<div class="ai-card">
<div class="ai-card-label">Voice Match</div>
<div class="ai-card-content">Your Style</div>
<div class="voice-score">
<div class="score-track"><div class="score-fill"></div></div>
<div class="score-num">92%</div>
</div>
</div>
<div class="ai-card">
<div class="ai-card-label">Publishing To</div>
<div class="ai-card-content">WeChat Article</div>
<div class="platform-tags">
<span class="p-tag active">WeChat</span>
<span class="p-tag">XHS</span>
<span class="p-tag">Script</span>
</div>
</div>
<div class="ai-suggestion">
<span class="label">Suggestion</span>
The second paragraph is beautiful. Consider adding a concrete personal example to ground the abstract idea.
</div>
<button class="refine-btn">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v18"/><path d="M3 12h18"/></svg>
Refine with AI
</button>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

@@ -0,0 +1,548 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1440">
<title>Inkwell — AI Writing Assistant</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1440px;
height: 900px;
overflow: hidden;
margin: 0;
background: #FFFFFF;
font-family: 'Helvetica Neue', Arial, sans-serif;
color: #111111;
position: relative;
}
/* Grid overlay for Swiss design feel */
body::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background:
repeating-linear-gradient(90deg, transparent, transparent 119px, rgba(0,0,0,0.03) 119px, rgba(0,0,0,0.03) 120px),
repeating-linear-gradient(0deg, transparent, transparent 59px, rgba(0,0,0,0.02) 59px, rgba(0,0,0,0.02) 60px);
pointer-events: none;
z-index: 0;
}
.container {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: 1fr 1fr;
height: 100%;
padding: 0;
}
/* LEFT PANEL */
.left-panel {
padding: 60px 60px 48px 80px;
display: flex;
flex-direction: column;
justify-content: space-between;
border-right: 2px solid #111;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 20px;
font-weight: 700;
letter-spacing: -0.5px;
text-transform: uppercase;
}
.logo span {
color: #E63946;
}
.nav {
display: flex;
gap: 28px;
font-size: 13px;
font-weight: 500;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.nav a {
color: #111;
text-decoration: none;
}
.hero-content {
margin-top: -20px;
}
.headline {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 86px;
font-weight: 700;
line-height: 0.95;
letter-spacing: -4px;
margin-bottom: 28px;
}
.headline em {
font-style: italic;
color: #E63946;
}
.subtitle {
font-size: 18px;
font-weight: 400;
color: #555;
line-height: 1.5;
max-width: 420px;
margin-bottom: 36px;
}
.cta-row {
display: flex;
align-items: center;
gap: 24px;
}
.cta-button {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 16px 36px;
background: #E63946;
color: #fff;
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 15px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
text-decoration: none;
border: none;
cursor: pointer;
}
.social-proof {
font-size: 13px;
color: #888;
letter-spacing: 0.3px;
}
.social-proof strong {
color: #111;
font-weight: 600;
}
/* FEATURES — strict 3 col */
.features-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0;
border-top: 2px solid #111;
}
.feature-item {
padding: 20px 0;
border-right: 1px solid #ddd;
}
.feature-item:last-child {
border-right: none;
}
.feature-item:first-child {
padding-right: 16px;
}
.feature-item:nth-child(2) {
padding: 20px 16px;
}
.feature-item:last-child {
padding-left: 16px;
}
.feature-number {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 11px;
font-weight: 700;
color: #E63946;
letter-spacing: 1px;
margin-bottom: 8px;
}
.feature-title {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
font-weight: 700;
letter-spacing: -0.3px;
margin-bottom: 4px;
text-transform: uppercase;
}
.feature-desc {
font-size: 12px;
color: #777;
line-height: 1.5;
}
/* RIGHT PANEL — Editor mockup as wireframe */
.right-panel {
background: #F7F7F7;
padding: 48px 60px 48px 48px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.editor-mockup {
width: 100%;
max-width: 580px;
height: 680px;
background: #fff;
border: 2px solid #111;
display: grid;
grid-template-columns: 1fr 200px;
position: relative;
}
/* Grid reference lines on mockup */
.editor-mockup::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background:
repeating-linear-gradient(0deg, transparent, transparent 39px, rgba(0,0,0,0.03) 39px, rgba(0,0,0,0.03) 40px);
pointer-events: none;
}
.editor-main {
padding: 28px 24px;
border-right: 2px solid #111;
display: flex;
flex-direction: column;
}
.editor-toolbar {
display: flex;
gap: 6px;
padding-bottom: 16px;
border-bottom: 1px solid #ddd;
margin-bottom: 20px;
}
.toolbar-btn {
width: 28px;
height: 28px;
border: 1px solid #ccc;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
}
.toolbar-btn.active {
background: #111;
border-color: #111;
color: #fff;
}
.editor-title-line {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 22px;
font-weight: 700;
letter-spacing: -0.5px;
margin-bottom: 16px;
color: #111;
}
.editor-text-block {
font-size: 13px;
line-height: 1.8;
color: #444;
margin-bottom: 14px;
}
.editor-text-block .highlight {
background: rgba(230, 57, 70, 0.12);
border-bottom: 2px solid #E63946;
padding: 0 2px;
}
.editor-h2 {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 16px;
font-weight: 700;
margin-bottom: 10px;
margin-top: 6px;
color: #111;
}
.editor-list {
font-size: 13px;
line-height: 2;
color: #555;
padding-left: 18px;
}
.editor-cursor {
display: inline-block;
width: 2px;
height: 16px;
background: #E63946;
animation: blink 1s step-end infinite;
vertical-align: text-bottom;
margin-left: 2px;
}
@keyframes blink {
50% { opacity: 0; }
}
/* AI SIDEBAR */
.ai-sidebar {
padding: 20px 16px;
background: #FAFAFA;
display: flex;
flex-direction: column;
gap: 14px;
}
.sidebar-header {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.5px;
color: #E63946;
padding-bottom: 10px;
border-bottom: 2px solid #111;
}
.sidebar-card {
padding: 12px;
border: 1px solid #ddd;
background: #fff;
}
.sidebar-card-label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
color: #999;
margin-bottom: 6px;
}
.sidebar-card-value {
font-size: 13px;
font-weight: 500;
color: #111;
line-height: 1.4;
}
.style-meter {
display: flex;
gap: 3px;
margin-top: 8px;
}
.meter-bar {
height: 4px;
flex: 1;
background: #E0E0E0;
}
.meter-bar.filled {
background: #E63946;
}
.sidebar-suggestion {
padding: 10px 12px;
background: #fff;
border: 1px solid #ddd;
font-size: 12px;
color: #555;
line-height: 1.5;
}
.sidebar-suggestion strong {
color: #111;
display: block;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.8px;
margin-bottom: 4px;
}
.sidebar-action {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 12px;
background: #111;
color: #fff;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: pointer;
border: none;
justify-content: center;
}
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 6px;
}
.tag {
font-size: 10px;
padding: 2px 8px;
border: 1px solid #ccc;
color: #666;
letter-spacing: 0.3px;
}
/* Corner mark */
.right-panel::after {
content: 'INKWELL V1.0';
position: absolute;
bottom: 20px;
right: 24px;
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 10px;
letter-spacing: 2px;
color: #bbb;
text-transform: uppercase;
}
</style>
</head>
<body>
<div class="container">
<!-- LEFT PANEL -->
<div class="left-panel">
<div class="top-bar">
<div class="logo">INK<span>WELL</span></div>
<nav class="nav">
<a href="#">Features</a>
<a href="#">Pricing</a>
<a href="#">Blog</a>
</nav>
</div>
<div class="hero-content">
<h1 class="headline">Write<br>better,<br>faster,<br>with <em>your</em><br>own voice.</h1>
<p class="subtitle">AI that learns your style, not replaces it. Craft content for WeChat, Xiaohongshu, and video scripts — all in your authentic tone.</p>
<div class="cta-row">
<a href="#" class="cta-button">
Start Writing
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
</a>
<span class="social-proof">Trusted by <strong>10,000+</strong> creators</span>
</div>
</div>
<div class="features-grid">
<div class="feature-item">
<div class="feature-number">01</div>
<div class="feature-title">Style Learning</div>
<div class="feature-desc">Adapts to your unique voice through continuous analysis of your writing patterns.</div>
</div>
<div class="feature-item">
<div class="feature-number">02</div>
<div class="feature-title">Multi-Platform</div>
<div class="feature-desc">WeChat articles, Xiaohongshu posts, video scripts. One tool, every format.</div>
</div>
<div class="feature-item">
<div class="feature-number">03</div>
<div class="feature-title">Human-Touch</div>
<div class="feature-desc">Proofreading that preserves warmth and removes robotic phrasing.</div>
</div>
</div>
</div>
<!-- RIGHT PANEL — Editor Mockup -->
<div class="right-panel">
<div class="editor-mockup">
<div class="editor-main">
<div class="editor-toolbar">
<div class="toolbar-btn active"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/></svg></div>
<div class="toolbar-btn"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg></div>
<div class="toolbar-btn"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="21" y1="6" x2="3" y2="6"/><line x1="15" y1="12" x2="3" y2="12"/><line x1="17" y1="18" x2="3" y2="18"/></svg></div>
<div class="toolbar-btn"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg></div>
</div>
<div class="editor-title-line">The Future of Content Creation</div>
<div class="editor-text-block">
Every creator faces the same tension: the desire to produce more content versus the need to maintain quality and authenticity. <span class="highlight">AI doesn't have to mean losing your voice</span> — it can mean amplifying it.
</div>
<div class="editor-h2">Why Authenticity Matters</div>
<div class="editor-text-block">
Readers can tell. They feel the difference between words that carry genuine experience and words assembled by algorithm. The goal isn't to write <em>more</em> — it's to write more of what only you can write.<span class="editor-cursor"></span>
</div>
<div class="editor-h2">Key Principles</div>
<ol class="editor-list">
<li>Write from personal experience first</li>
<li>Use AI for refinement, not replacement</li>
<li>Adapt tone for each platform</li>
</ol>
</div>
<div class="ai-sidebar">
<div class="sidebar-header">AI Assistant</div>
<div class="sidebar-card">
<div class="sidebar-card-label">Style Match</div>
<div class="sidebar-card-value">92% Voice Fidelity</div>
<div class="style-meter">
<div class="meter-bar filled"></div>
<div class="meter-bar filled"></div>
<div class="meter-bar filled"></div>
<div class="meter-bar filled"></div>
<div class="meter-bar"></div>
</div>
</div>
<div class="sidebar-card">
<div class="sidebar-card-label">Target</div>
<div class="sidebar-card-value">WeChat Article</div>
<div class="tag-row">
<span class="tag">WeChat</span>
<span class="tag">XHS</span>
<span class="tag">Script</span>
</div>
</div>
<div class="sidebar-suggestion">
<strong>Suggestion</strong>
Consider opening with a specific anecdote to strengthen the personal connection.
</div>
<div class="sidebar-suggestion">
<strong>Tone Check</strong>
Paragraph 2 reads slightly formal. Soften with a conversational phrase.
</div>
<button class="sidebar-action">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
Refine Selection
</button>
</div>
</div>
</div>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

@@ -0,0 +1,696 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1440">
<title>Inkwell — AI Writing Assistant</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&family=Noto+Serif+SC:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1440px;
height: 900px;
overflow: hidden;
margin: 0;
background: #F5F0EB;
font-family: 'Inter', sans-serif;
color: #3D3D3D;
}
.page {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
/* NAV */
.nav-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28px 64px;
}
.logo {
font-family: 'Noto Serif SC', serif;
font-size: 20px;
font-weight: 600;
color: #5C5347;
letter-spacing: 1px;
}
.nav-right {
display: flex;
align-items: center;
gap: 36px;
}
.nav-right a {
font-size: 13px;
font-weight: 400;
color: #8A8278;
text-decoration: none;
letter-spacing: 0.3px;
}
.nav-cta {
padding: 10px 24px;
background: transparent;
color: #2D3436;
border: 1px solid rgba(45, 52, 54, 0.2);
border-radius: 24px;
font-size: 13px;
font-weight: 500;
text-decoration: none;
letter-spacing: 0.3px;
}
/* MAIN CONTENT */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 0 64px;
gap: 36px;
}
/* HERO ROW */
.hero-row {
display: grid;
grid-template-columns: 480px 1fr;
gap: 56px;
align-items: start;
padding-top: 16px;
}
.hero-text {
padding-top: 20px;
}
.headline {
font-family: 'Noto Serif SC', serif;
font-size: 44px;
font-weight: 600;
line-height: 1.35;
letter-spacing: -0.5px;
color: #3D3D3D;
margin-bottom: 20px;
}
.headline .accent {
color: #6B8F71;
}
.subtitle {
font-size: 16px;
font-weight: 300;
line-height: 1.8;
color: #8A8278;
max-width: 420px;
margin-bottom: 28px;
}
.cta-area {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 16px;
}
.cta-button {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 14px 32px;
background: #2D3436;
color: #F5F0EB;
font-family: 'Inter', sans-serif;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.5px;
text-decoration: none;
border: none;
border-radius: 32px;
cursor: pointer;
}
.social-proof {
font-size: 12px;
font-weight: 400;
color: #B5AD9E;
letter-spacing: 0.3px;
}
/* EDITOR MOCKUP — organic rounded */
.editor-container {
position: relative;
}
.editor-card {
width: 100%;
max-width: 720px;
height: 460px;
background: #FDFCF9;
border-radius: 24px;
box-shadow:
0 2px 8px rgba(92,83,71,0.04),
0 8px 24px rgba(92,83,71,0.06),
0 24px 48px rgba(92,83,71,0.03);
display: grid;
grid-template-columns: 1fr 200px;
overflow: hidden;
}
.editor-body {
padding: 28px 28px;
display: flex;
flex-direction: column;
}
.editor-toolbar {
display: flex;
gap: 6px;
margin-bottom: 20px;
padding-bottom: 14px;
border-bottom: 1px solid #EDE8DF;
}
.e-btn {
width: 30px;
height: 30px;
border-radius: 10px;
border: none;
background: transparent;
display: flex;
align-items: center;
justify-content: center;
color: #C4BDB2;
cursor: pointer;
}
.e-btn.active {
background: #EDE8DF;
color: #5C5347;
}
.e-title {
font-family: 'Noto Serif SC', serif;
font-size: 20px;
font-weight: 600;
color: #3D3D3D;
margin-bottom: 14px;
}
.e-text {
font-size: 13.5px;
font-weight: 300;
line-height: 1.9;
color: #6B6560;
margin-bottom: 12px;
}
.e-text .enhanced {
background: rgba(107,143,113,0.2);
border-radius: 4px;
padding: 1px 4px;
}
.e-h2 {
font-family: 'Noto Serif SC', serif;
font-size: 16px;
font-weight: 500;
color: #3D3D3D;
margin-bottom: 10px;
margin-top: 2px;
}
.e-list {
list-style: none;
padding: 0;
}
.e-list li {
font-size: 13px;
font-weight: 300;
color: #8A8278;
line-height: 1.9;
padding-left: 18px;
position: relative;
}
.e-list li::before {
content: '';
position: absolute;
left: 2px;
top: 10px;
width: 8px;
height: 8px;
border-radius: 50%;
border: 1.5px solid #6B8F71;
}
.typing-cursor {
display: inline-block;
width: 1.5px;
height: 14px;
background: #6B8F71;
animation: softblink 1.5s ease-in-out infinite;
vertical-align: text-bottom;
margin-left: 2px;
}
@keyframes softblink {
0%, 100% { opacity: 0.8; }
50% { opacity: 0.15; }
}
/* AI Sidebar */
.ai-panel {
background: #F8F5EF;
border-left: 1px solid #EDE8DF;
padding: 22px 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.panel-header {
display: flex;
align-items: center;
gap: 8px;
padding-bottom: 12px;
border-bottom: 1px solid #EDE8DF;
}
.panel-header svg {
color: #6B8F71;
}
.panel-header span {
font-size: 12px;
font-weight: 500;
color: #5C5347;
letter-spacing: 0.5px;
}
.panel-card {
background: #FDFCF9;
border-radius: 14px;
padding: 14px;
border: 1px solid #EDE8DF;
}
.panel-card-label {
font-size: 10px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.8px;
color: #B5AD9E;
margin-bottom: 6px;
}
.panel-card-value {
font-size: 13px;
font-weight: 500;
color: #3D3D3D;
}
.voice-bar {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
}
.vb-track {
flex: 1;
height: 4px;
background: #EDE8DF;
border-radius: 4px;
overflow: hidden;
}
.vb-fill {
width: 92%;
height: 100%;
background: linear-gradient(90deg, #6B8F71, #C4D1BC);
border-radius: 4px;
}
.vb-label {
font-size: 11px;
font-weight: 500;
color: #6B8F71;
}
.platform-pills {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 6px;
}
.pill {
font-size: 10px;
padding: 4px 10px;
border-radius: 16px;
background: #EDE8DF;
color: #8A8278;
}
.pill.active {
background: rgba(107,143,113,0.25);
color: #5C5347;
}
.panel-note {
font-size: 11.5px;
font-weight: 300;
color: #8A8278;
line-height: 1.6;
padding: 12px;
background: #FDFCF9;
border-radius: 14px;
border: 1px solid #EDE8DF;
}
.panel-note .note-label {
font-size: 10px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.8px;
color: #B5AD9E;
display: block;
margin-bottom: 4px;
}
/* FLOW DIAGRAM */
.flow-section {
padding: 0 0 0 0;
}
.flow-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 0;
padding: 20px 0;
}
.flow-step {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 28px;
background: #FDFCF9;
border-radius: 18px;
border: 1px solid #EDE8DF;
box-shadow: 0 2px 8px rgba(92,83,71,0.03);
}
.flow-step-icon {
width: 38px;
height: 38px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.flow-step-icon.ideas {
background: rgba(107,143,113,0.2);
color: #6B8F71;
}
.flow-step-icon.ai {
background: rgba(212,187,156,0.25);
color: #C4A87A;
}
.flow-step-icon.voice {
background: rgba(92,83,71,0.1);
color: #5C5347;
}
.flow-step-text {
display: flex;
flex-direction: column;
}
.flow-step-label {
font-size: 14px;
font-weight: 500;
color: #3D3D3D;
margin-bottom: 2px;
}
.flow-step-desc {
font-size: 11px;
font-weight: 300;
color: #B5AD9E;
}
.flow-arrow {
display: flex;
align-items: center;
padding: 0 20px;
color: #C4BDB2;
}
.flow-arrow svg {
opacity: 0.6;
}
/* FEATURES ROW */
.features-strip {
display: flex;
justify-content: center;
gap: 56px;
padding: 12px 0 20px 0;
}
.feat {
display: flex;
align-items: center;
gap: 12px;
}
.feat-icon {
width: 40px;
height: 40px;
border-radius: 12px;
background: rgba(107,143,113,0.15);
display: flex;
align-items: center;
justify-content: center;
color: #6B8F71;
flex-shrink: 0;
}
.feat-text {
display: flex;
flex-direction: column;
}
.feat-name {
font-size: 13px;
font-weight: 500;
color: #3D3D3D;
margin-bottom: 1px;
}
.feat-sub {
font-size: 11px;
font-weight: 300;
color: #B5AD9E;
}
.divider-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: #D5CFC5;
align-self: center;
}
</style>
</head>
<body>
<div class="page">
<!-- NAV -->
<nav class="nav-bar">
<div class="logo">Inkwell</div>
<div class="nav-right">
<a href="#">Philosophy</a>
<a href="#">Features</a>
<a href="#">Stories</a>
<a href="#" class="nav-cta">Start Writing</a>
</div>
</nav>
<!-- MAIN -->
<div class="main-content">
<!-- HERO -->
<div class="hero-row">
<div class="hero-text">
<h1 class="headline">Write better, faster,<br>with <span class="accent">your own</span> voice</h1>
<p class="subtitle">AI that learns your style, not replaces it. A mindful writing companion for WeChat, Xiaohongshu, and video scripts that honours your creative instincts.</p>
<div class="cta-area">
<a href="#" class="cta-button">
Start Writing
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
</a>
<span class="social-proof">Trusted by 10,000+ creators</span>
</div>
</div>
<!-- EDITOR CARD -->
<div class="editor-container">
<div class="editor-card">
<div class="editor-body">
<div class="editor-toolbar">
<button class="e-btn active"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/></svg></button>
<button class="e-btn"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg></button>
<button class="e-btn"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="21" y1="6" x2="3" y2="6"/><line x1="15" y1="12" x2="3" y2="12"/><line x1="17" y1="18" x2="3" y2="18"/></svg></button>
<button class="e-btn"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></button>
</div>
<div class="e-title">On the Patience of Growing Things</div>
<div class="e-text">
There is a kind of writing that happens slowly, like roots in winter soil. <span class="enhanced">You cannot rush a sentence into meaning any more than you can rush a seed into bloom</span>. The best words arrive when you stop chasing them.
</div>
<div class="e-h2">Listening to the Draft</div>
<div class="e-text">
Every draft speaks, if you give it room. The first version is never wrong — it is simply unfinished. What AI can offer is not replacement, but reflection: a gentle mirror held up to your own intentions.<span class="typing-cursor"></span>
</div>
<ul class="e-list">
<li>Trust the messy first draft</li>
<li>Let AI reveal patterns you missed</li>
<li>Preserve what makes it yours</li>
</ul>
</div>
<div class="ai-panel">
<div class="panel-header">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
<span>Inkwell AI</span>
</div>
<div class="panel-card">
<div class="panel-card-label">Voice Match</div>
<div class="panel-card-value">Your Style</div>
<div class="voice-bar">
<div class="vb-track"><div class="vb-fill"></div></div>
<div class="vb-label">92%</div>
</div>
</div>
<div class="panel-card">
<div class="panel-card-label">Platform</div>
<div class="panel-card-value">WeChat</div>
<div class="platform-pills">
<span class="pill active">WeChat</span>
<span class="pill">XHS</span>
<span class="pill">Script</span>
</div>
</div>
<div class="panel-note">
<span class="note-label">Observation</span>
The seed metaphor in paragraph one is lovely. Consider echoing it in the closing line for a sense of return.
</div>
<div class="panel-note">
<span class="note-label">Tone</span>
Gentle, contemplative. This reads naturally as you.
</div>
</div>
</div>
</div>
</div>
<!-- FLOW DIAGRAM -->
<div class="flow-section">
<div class="flow-bar">
<div class="flow-step">
<div class="flow-step-icon ideas">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 1 1 7.072 0l-.548.547A3.374 3.374 0 0 0 12 18.469c-1.006 0-1.938-.429-2.577-1.177l-.56-.56z"/></svg>
</div>
<div class="flow-step-text">
<div class="flow-step-label">Your Ideas</div>
<div class="flow-step-desc">Raw thoughts and drafts</div>
</div>
</div>
<div class="flow-arrow">
<svg width="32" height="16" viewBox="0 0 32 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="2" y1="8" x2="28" y2="8"/><polyline points="24 4 28 8 24 12"/></svg>
</div>
<div class="flow-step">
<div class="flow-step-icon ai">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
</div>
<div class="flow-step-text">
<div class="flow-step-label">AI Enhancement</div>
<div class="flow-step-desc">Refine, not rewrite</div>
</div>
</div>
<div class="flow-arrow">
<svg width="32" height="16" viewBox="0 0 32 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="2" y1="8" x2="28" y2="8"/><polyline points="24 4 28 8 24 12"/></svg>
</div>
<div class="flow-step">
<div class="flow-step-icon voice">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
</div>
<div class="flow-step-text">
<div class="flow-step-label">Your Voice</div>
<div class="flow-step-desc">Unmistakably you</div>
</div>
</div>
</div>
</div>
<!-- FEATURES STRIP -->
<div class="features-strip">
<div class="feat">
<div class="feat-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
</div>
<div class="feat-text">
<div class="feat-name">Style Learning</div>
<div class="feat-sub">Evolves with your writing</div>
</div>
</div>
<div class="divider-dot"></div>
<div class="feat">
<div class="feat-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
</div>
<div class="feat-text">
<div class="feat-name">Multi-Platform</div>
<div class="feat-sub">WeChat, XHS, video scripts</div>
</div>
</div>
<div class="divider-dot"></div>
<div class="feat">
<div class="feat-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
</div>
<div class="feat-text">
<div class="feat-name">Human-Touch Proofreading</div>
<div class="feat-sub">Warmth-preserving refinement</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

@@ -0,0 +1,372 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1440">
<title>Nexus API Documentation</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600&family=JetBrains+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1440px;
height: 900px;
overflow: hidden;
margin: 0;
font-family: 'Inter', sans-serif;
background: #FAFAF8;
color: #2C2C2C;
}
/* Navigation */
nav {
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 80px;
position: relative;
}
.nav-logo {
display: flex;
align-items: center;
gap: 12px;
}
.nav-logo-icon {
width: 32px;
height: 32px;
border-radius: 2px;
background: #E8E4DF;
display: flex;
align-items: center;
justify-content: center;
}
.nav-logo-icon i { color: #D4A574; }
.nav-logo span {
font-size: 17px;
font-weight: 500;
letter-spacing: -0.3px;
color: #1A1A1A;
}
.nav-center {
position: absolute;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 32px;
}
.nav-center a {
font-size: 13px;
font-weight: 400;
color: #999;
text-decoration: none;
letter-spacing: 0.3px;
transition: color 0.2s;
}
.nav-center a:hover { color: #2C2C2C; }
.nav-center a.active { color: #2C2C2C; font-weight: 500; }
.nav-right {
display: flex;
align-items: center;
gap: 24px;
}
.nav-right a {
font-size: 13px;
color: #BBB;
text-decoration: none;
transition: color 0.2s;
}
.nav-right a:hover { color: #2C2C2C; }
.status-pill {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 12px;
border-radius: 2px;
background: rgba(212, 165, 116, 0.08);
font-size: 11px;
color: #B0ACA4;
font-weight: 400;
}
.status-pill .dot {
width: 6px;
height: 6px;
background: #D4A574;
border-radius: 50%;
}
/* Hero section */
.hero {
text-align: center;
padding: 64px 80px 48px;
}
.hero-eyebrow {
font-size: 12px;
font-weight: 400;
letter-spacing: 4px;
text-transform: uppercase;
color: #B0ACA4;
margin-bottom: 24px;
}
.hero h1 {
font-size: 56px;
font-weight: 200;
letter-spacing: -2.5px;
line-height: 1.1;
color: #1A1A1A;
margin-bottom: 16px;
}
.hero h1 em {
font-style: normal;
font-weight: 500;
}
.hero p {
font-size: 17px;
font-weight: 300;
color: #999;
line-height: 1.6;
max-width: 520px;
margin: 0 auto 36px;
letter-spacing: 0.1px;
}
.hero-actions {
display: flex;
justify-content: center;
gap: 16px;
margin-bottom: 0;
}
.hero-actions a {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 28px;
font-size: 13px;
font-weight: 400;
text-decoration: none;
border-radius: 2px;
transition: all 0.2s;
letter-spacing: 0.2px;
}
.btn-primary {
background: #1A1A1A;
color: #FAFAF8;
}
.btn-primary:hover { background: #333; }
.btn-secondary {
background: transparent;
color: #999;
border: 1px solid #E0DDD8;
}
.btn-secondary:hover { border-color: #CCC; color: #666; }
/* Code card */
.code-section {
display: flex;
justify-content: center;
padding: 32px 80px 48px;
}
.code-card {
background: #FFFFFF;
border-radius: 2px;
box-shadow: 0 8px 40px rgba(0,0,0,0.04), 0 1px 3px rgba(0,0,0,0.02);
max-width: 600px;
width: 100%;
overflow: hidden;
}
.code-card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 24px;
border-bottom: 1px solid #F2F0EC;
}
.code-card-header .dots {
display: flex;
gap: 7px;
}
.code-card-header .dots span {
width: 10px;
height: 10px;
border-radius: 50%;
background: #E8E5E0;
}
.code-card-header .filename {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: #BBB;
font-weight: 400;
}
.code-card-header .copy-btn {
display: flex;
align-items: center;
gap: 4px;
background: none;
border: none;
cursor: pointer;
color: #CCC;
font-size: 11px;
font-family: 'Inter', sans-serif;
}
.code-card-body {
padding: 24px 28px;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
line-height: 1.8;
color: #444;
font-weight: 400;
}
.code-card-body .kw { color: #8B7355; font-weight: 500; }
.code-card-body .str { color: #D4A574; }
.code-card-body .cmt { color: #CCCCCC; }
.code-card-body .fn { color: #777; }
.code-card-body .num { color: #B08D57; }
/* Quick links */
.quick-links {
display: flex;
justify-content: center;
gap: 48px;
padding: 16px 80px 48px;
}
.quick-link {
display: flex;
align-items: center;
gap: 8px;
text-decoration: none;
color: #BBB;
font-size: 13px;
font-weight: 400;
transition: color 0.2s;
letter-spacing: 0.2px;
}
.quick-link:hover { color: #666; }
.quick-link i { color: #D4A574; opacity: 0.6; }
/* Features */
.features {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
padding: 0 80px;
max-width: 1100px;
margin: 0 auto;
}
.feature-card {
background: #FFFFFF;
border-radius: 2px;
padding: 32px 28px;
box-shadow: 0 2px 16px rgba(0,0,0,0.02);
transition: box-shadow 0.2s;
}
.feature-card:hover {
box-shadow: 0 2px 16px rgba(0,0,0,0.04);
}
.feature-icon-wrap {
width: 40px;
height: 40px;
border-radius: 2px;
background: #F0EBE3;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
}
.feature-icon-wrap i { color: #D4A574; }
.feature-card h3 {
font-size: 16px;
font-weight: 500;
letter-spacing: -0.3px;
margin-bottom: 8px;
color: #1A1A1A;
}
.feature-card p {
font-size: 13px;
font-weight: 300;
line-height: 1.65;
color: #AAA;
}
</style>
</head>
<body>
<!-- Navigation -->
<nav>
<div class="nav-logo">
<div class="nav-logo-icon"><i data-lucide="zap" style="width:16px;height:16px;"></i></div>
<span>Nexus</span>
</div>
<div class="nav-center">
<a href="#" class="active">Docs</a>
<a href="#">API</a>
<a href="#">Changelog</a>
<a href="#">Status</a>
<a href="#">GitHub</a>
</div>
<div class="nav-right">
<div class="status-pill"><span class="dot"></span> Operational</div>
<a href="#"><i data-lucide="search" style="width:15px;height:15px;color:#CCC;"></i></a>
</div>
</nav>
<!-- Hero -->
<section class="hero">
<div class="hero-eyebrow">Unified AI Gateway</div>
<h1>One API, <em>every</em> AI model<span style="color:#D4A574;font-weight:300;">.</span></h1>
<p>Access GPT, Claude, Gemini, and 20+ models through a single endpoint. Intelligent routing, unified billing, zero vendor lock-in.</p>
<div class="hero-actions">
<a href="#" class="btn-primary"><i data-lucide="arrow-right" style="width:14px;height:14px;"></i> Get started</a>
<a href="#" class="btn-secondary">View API reference</a>
</div>
</section>
<!-- Code Card -->
<section class="code-section">
<div class="code-card">
<div class="code-card-header">
<div class="dots"><span></span><span></span><span></span></div>
<span class="filename">quickstart.py</span>
<button class="copy-btn"><i data-lucide="copy" style="width:12px;height:12px;"></i> Copy</button>
</div>
<div class="code-card-body">
<span class="kw">from</span> nexus <span class="kw">import</span> Client<br><br>
client = Client(api_key=<span class="str">"your-key"</span>)<br>
response = client.chat(<br>
&nbsp;&nbsp;&nbsp;&nbsp;model=<span class="str">"auto"</span>,&nbsp;&nbsp;<span class="cmt"># intelligently routes</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;messages=[{<span class="str">"role"</span>: <span class="str">"user"</span>, <span class="str">"content"</span>: <span class="str">"Hello!"</span>}]<br>
)
</div>
</div>
</section>
<!-- Quick Links -->
<div class="quick-links">
<a href="#" class="quick-link"><i data-lucide="rocket" style="width:14px;height:14px;"></i> Getting Started</a>
<a href="#" class="quick-link"><i data-lucide="file-text" style="width:14px;height:14px;"></i> API Reference</a>
<a href="#" class="quick-link"><i data-lucide="layers" style="width:14px;height:14px;"></i> Models</a>
<a href="#" class="quick-link"><i data-lucide="credit-card" style="width:14px;height:14px;"></i> Pricing</a>
</div>
<!-- Features -->
<section class="features">
<div class="feature-card">
<div class="feature-icon-wrap"><i data-lucide="git-branch" style="width:18px;height:18px;"></i></div>
<h3>Model Routing</h3>
<p>Automatically select the best model for each request based on task complexity, latency requirements, and cost constraints.</p>
</div>
<div class="feature-card">
<div class="feature-icon-wrap"><i data-lucide="trending-down" style="width:18px;height:18px;"></i></div>
<h3>Cost Optimization</h3>
<p>Reduce AI spend by up to 60% with intelligent model selection and automatic fallback to cost-effective alternatives.</p>
</div>
<div class="feature-card">
<div class="feature-icon-wrap"><i data-lucide="bar-chart-3" style="width:18px;height:18px;"></i></div>
<h3>Usage Analytics</h3>
<p>Real-time dashboards tracking token usage, response latency, model performance, and cost breakdowns per project.</p>
</div>
</section>
<script>lucide.createIcons();</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

@@ -0,0 +1,460 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1440">
<title>Nexus API Documentation</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1440px;
height: 900px;
overflow: hidden;
margin: 0;
font-family: 'Helvetica Neue', Arial, sans-serif;
background: #FFFFFF;
color: #111111;
display: flex;
}
/* Sidebar */
.sidebar {
width: 260px;
min-width: 260px;
height: 900px;
border-right: 1px solid #111;
display: flex;
flex-direction: column;
padding: 0;
overflow-y: auto;
}
.sidebar-logo {
padding: 24px 28px;
border-bottom: 1px solid #111;
display: flex;
align-items: center;
gap: 10px;
}
.sidebar-logo .logo-mark {
width: 28px;
height: 28px;
background: #E63946;
display: flex;
align-items: center;
justify-content: center;
}
.sidebar-logo .logo-mark svg { color: #fff; }
.sidebar-logo span {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-weight: 700;
font-size: 18px;
letter-spacing: -0.5px;
}
.sidebar-section {
padding: 20px 28px 8px;
}
.sidebar-section-label {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 10px;
font-weight: 600;
letter-spacing: 2px;
text-transform: uppercase;
color: #999;
margin-bottom: 12px;
}
.sidebar-link {
display: block;
padding: 6px 0;
font-size: 13px;
font-weight: 500;
color: #555;
text-decoration: none;
transition: color 0.15s;
}
.sidebar-link:hover { color: #111; }
.sidebar-link.active {
color: #E63946;
font-weight: 600;
}
.sidebar-link.active::before {
content: '';
display: inline-block;
width: 6px;
height: 6px;
background: #E63946;
margin-right: 8px;
vertical-align: middle;
}
.sidebar-divider {
height: 1px;
background: #E8E8E8;
margin: 12px 28px;
}
/* Main content */
.main {
flex: 1;
height: 900px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
/* Top nav */
.topnav {
height: 52px;
min-height: 52px;
border-bottom: 1px solid #111;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 40px;
}
.topnav-links {
display: flex;
gap: 28px;
}
.topnav-links a {
font-size: 12px;
font-weight: 600;
letter-spacing: 1px;
text-transform: uppercase;
text-decoration: none;
color: #555;
transition: color 0.15s;
}
.topnav-links a:hover { color: #111; }
.topnav-links a.active-nav { color: #E63946; }
.topnav-right {
display: flex;
align-items: center;
gap: 16px;
}
.topnav-right .status-dot {
width: 7px;
height: 7px;
background: #E63946;
border-radius: 50%;
display: inline-block;
}
.topnav-right span {
font-size: 11px;
color: #888;
font-weight: 500;
}
.topnav-right a {
color: #555;
text-decoration: none;
}
/* Hero */
.hero {
display: grid;
grid-template-columns: 1fr 1fr;
min-height: 400px;
border-bottom: 1px solid #E8E8E8;
}
.hero-left {
padding: 56px 48px 48px;
display: flex;
flex-direction: column;
justify-content: center;
}
.hero-badge {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 10px;
font-weight: 600;
letter-spacing: 2.5px;
text-transform: uppercase;
color: #E63946;
margin-bottom: 20px;
}
.hero-left h1 {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 52px;
font-weight: 900;
line-height: 1.05;
letter-spacing: -2px;
margin-bottom: 16px;
}
.hero-left p {
font-size: 16px;
line-height: 1.6;
color: #666;
max-width: 420px;
margin-bottom: 32px;
}
.hero-links {
display: flex;
gap: 12px;
}
.hero-links a {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 20px;
font-size: 13px;
font-weight: 600;
text-decoration: none;
transition: all 0.15s;
}
.hero-links a.primary {
background: #111;
color: #fff;
}
.hero-links a.primary:hover { background: #E63946; }
.hero-links a.secondary {
border: 1px solid #DDD;
color: #333;
background: #fff;
}
.hero-links a.secondary:hover { border-color: #111; }
/* Code block */
.hero-right {
padding: 40px 48px 40px 24px;
display: flex;
align-items: center;
justify-content: center;
background: #FAFAFA;
border-left: 1px solid #E8E8E8;
}
.code-block {
background: #FFFFFF;
border: 1px solid #DDD;
width: 100%;
max-width: 480px;
}
.code-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
border-bottom: 1px solid #DDD;
background: #FAFAFA;
}
.code-header span {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: #999;
font-weight: 500;
}
.code-dots {
display: flex;
gap: 6px;
}
.code-dots i { width: 10px; height: 10px; }
.code-body {
padding: 20px;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
line-height: 1.7;
color: #111;
}
.code-body .kw { color: #111; font-weight: 500; }
.code-body .str { color: #E63946; }
.code-body .cmt { color: #AAAAAA; }
.code-body .fn { color: #555; }
/* Quick links bar */
.quick-bar {
display: grid;
grid-template-columns: repeat(4, 1fr);
border-bottom: 1px solid #E8E8E8;
}
.quick-bar a {
display: flex;
align-items: center;
gap: 10px;
padding: 18px 28px;
text-decoration: none;
color: #333;
font-size: 13px;
font-weight: 600;
border-right: 1px solid #E8E8E8;
transition: background 0.15s;
}
.quick-bar a:last-child { border-right: none; }
.quick-bar a:hover { background: #FAFAFA; }
.quick-bar a i { color: #E63946; }
/* Features */
.features {
display: grid;
grid-template-columns: repeat(3, 1fr);
flex: 1;
}
.feature-card {
padding: 36px 32px;
border-right: 1px solid #E8E8E8;
display: flex;
flex-direction: column;
}
.feature-card:last-child { border-right: none; }
.feature-card .feature-label {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 10px;
font-weight: 600;
letter-spacing: 2px;
text-transform: uppercase;
color: #E63946;
margin-bottom: 14px;
}
.feature-card h3 {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 20px;
font-weight: 600;
letter-spacing: -0.5px;
margin-bottom: 10px;
}
.feature-card p {
font-size: 13px;
line-height: 1.65;
color: #777;
}
.feature-icon {
width: 36px;
height: 36px;
background: #111;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 18px;
}
.feature-icon i { color: #fff; }
</style>
</head>
<body>
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-logo">
<div class="logo-mark"><i data-lucide="zap" style="width:16px;height:16px;"></i></div>
<span>Nexus API</span>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Getting Started</div>
<a href="#" class="sidebar-link active">Introduction</a>
<a href="#" class="sidebar-link">Quick Start</a>
<a href="#" class="sidebar-link">Authentication</a>
<a href="#" class="sidebar-link">Installation</a>
</div>
<div class="sidebar-divider"></div>
<div class="sidebar-section">
<div class="sidebar-section-label">Core Concepts</div>
<a href="#" class="sidebar-link">Model Routing</a>
<a href="#" class="sidebar-link">Chat Completions</a>
<a href="#" class="sidebar-link">Streaming</a>
<a href="#" class="sidebar-link">Error Handling</a>
</div>
<div class="sidebar-divider"></div>
<div class="sidebar-section">
<div class="sidebar-section-label">API Reference</div>
<a href="#" class="sidebar-link">POST /chat</a>
<a href="#" class="sidebar-link">GET /models</a>
<a href="#" class="sidebar-link">GET /usage</a>
<a href="#" class="sidebar-link">Webhooks</a>
</div>
<div class="sidebar-divider"></div>
<div class="sidebar-section">
<div class="sidebar-section-label">Resources</div>
<a href="#" class="sidebar-link">Models</a>
<a href="#" class="sidebar-link">Pricing</a>
<a href="#" class="sidebar-link">SDKs</a>
<a href="#" class="sidebar-link">Changelog</a>
</div>
</aside>
<!-- Main -->
<main class="main">
<!-- Top Nav -->
<nav class="topnav">
<div class="topnav-links">
<a href="#" class="active-nav">Docs</a>
<a href="#">API</a>
<a href="#">Changelog</a>
<a href="#">Status</a>
<a href="#">GitHub</a>
</div>
<div class="topnav-right">
<span class="status-dot"></span>
<span>All systems operational</span>
<a href="#"><i data-lucide="search" style="width:16px;height:16px;color:#888;"></i></a>
</div>
</nav>
<!-- Hero -->
<section class="hero">
<div class="hero-left">
<div class="hero-badge">Unified AI Gateway</div>
<h1>One API,<br>every AI model</h1>
<p>Access GPT, Claude, Gemini, and 20+ models through a single endpoint. Intelligent routing, unified billing, zero vendor lock-in.</p>
<div class="hero-links">
<a href="#" class="primary"><i data-lucide="arrow-right" style="width:14px;height:14px;"></i> Get Started</a>
<a href="#" class="secondary">API Reference</a>
</div>
</div>
<div class="hero-right">
<div class="code-block">
<div class="code-header">
<div class="code-dots">
<i data-lucide="circle" style="width:10px;height:10px;color:#DDD;fill:#DDD;"></i>
<i data-lucide="circle" style="width:10px;height:10px;color:#DDD;fill:#DDD;"></i>
<i data-lucide="circle" style="width:10px;height:10px;color:#DDD;fill:#DDD;"></i>
</div>
<span>quickstart.py</span>
</div>
<div class="code-body">
<span class="kw">from</span> nexus <span class="kw">import</span> Client<br><br>
client = Client(api_key=<span class="str">"your-key"</span>)<br>
response = client.chat(<br>
&nbsp;&nbsp;&nbsp;&nbsp;model=<span class="str">"auto"</span>,&nbsp;&nbsp;<span class="cmt"># intelligently routes</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;messages=[{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="str">"role"</span>: <span class="str">"user"</span>,<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="str">"content"</span>: <span class="str">"Hello!"</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;}]<br>
)
</div>
</div>
</div>
</section>
<!-- Quick Links -->
<div class="quick-bar">
<a href="#"><i data-lucide="rocket" style="width:16px;height:16px;"></i> Getting Started</a>
<a href="#"><i data-lucide="file-text" style="width:16px;height:16px;"></i> API Reference</a>
<a href="#"><i data-lucide="layers" style="width:16px;height:16px;"></i> Models</a>
<a href="#"><i data-lucide="credit-card" style="width:16px;height:16px;"></i> Pricing</a>
</div>
<!-- Features -->
<section class="features">
<div class="feature-card">
<div class="feature-icon"><i data-lucide="git-branch" style="width:18px;height:18px;"></i></div>
<div class="feature-label">Feature 01</div>
<h3>Model Routing</h3>
<p>Automatically select the best model for each request based on task complexity, latency requirements, and cost constraints.</p>
</div>
<div class="feature-card">
<div class="feature-icon"><i data-lucide="trending-down" style="width:18px;height:18px;"></i></div>
<div class="feature-label">Feature 02</div>
<h3>Cost Optimization</h3>
<p>Reduce AI spend by up to 60% with intelligent model selection and automatic fallback to cost-effective alternatives.</p>
</div>
<div class="feature-card">
<div class="feature-icon"><i data-lucide="bar-chart-3" style="width:18px;height:18px;"></i></div>
<div class="feature-label">Feature 03</div>
<h3>Usage Analytics</h3>
<p>Real-time dashboards tracking token usage, response latency, model performance, and cost breakdowns per project.</p>
</div>
</section>
</main>
<script>lucide.createIcons();</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

@@ -0,0 +1,494 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1440">
<title>Nexus API Documentation</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@300;400;500&family=Noto+Serif+SC:wght@400;500;600&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1440px;
height: 900px;
overflow: hidden;
margin: 0;
font-family: 'Inter', sans-serif;
background: #F5F0EB;
color: #3A3A35;
}
/* Navigation */
nav {
height: 72px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 64px;
}
.nav-logo {
display: flex;
align-items: center;
gap: 10px;
}
.nav-logo-mark {
width: 36px;
height: 36px;
border-radius: 12px;
background: #6B8F71;
display: flex;
align-items: center;
justify-content: center;
}
.nav-logo-mark i { color: #F5F0EB; }
.nav-logo-text {
font-family: 'Noto Serif SC', serif;
font-size: 18px;
font-weight: 600;
color: #2D3436;
letter-spacing: -0.3px;
}
.nav-links {
display: flex;
gap: 32px;
}
.nav-links a {
font-size: 13px;
font-weight: 400;
color: #999;
text-decoration: none;
transition: color 0.2s;
letter-spacing: 0.3px;
}
.nav-links a:hover { color: #3A3A35; }
.nav-links a.active { color: #3A3A35; font-weight: 500; }
.nav-right {
display: flex;
align-items: center;
gap: 20px;
}
.search-box {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: rgba(255,255,255,0.5);
border-radius: 12px;
border: 1px solid #E5DFCE;
}
.search-box span {
font-size: 12px;
color: #BBB;
}
.search-box kbd {
font-family: 'Inter', sans-serif;
font-size: 10px;
background: #EDE8DC;
border-radius: 4px;
padding: 2px 6px;
color: #AAA;
margin-left: 24px;
}
/* Hero Section */
.hero {
display: flex;
padding: 40px 64px 36px;
gap: 56px;
align-items: flex-start;
}
.hero-content {
flex: 1;
padding-top: 16px;
}
.hero-tag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
background: rgba(107,143,113,0.15);
border-radius: 100px;
font-size: 11px;
font-weight: 500;
color: #6B8F71;
margin-bottom: 24px;
letter-spacing: 0.5px;
}
.hero h1 {
font-family: 'Noto Serif SC', serif;
font-size: 44px;
font-weight: 600;
line-height: 1.15;
letter-spacing: -1.5px;
color: #2D3436;
margin-bottom: 16px;
}
.hero p {
font-size: 16px;
font-weight: 300;
line-height: 1.7;
color: #888;
max-width: 440px;
margin-bottom: 32px;
}
.hero-buttons {
display: flex;
gap: 12px;
}
.hero-buttons a {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
font-size: 13px;
font-weight: 500;
text-decoration: none;
border-radius: 12px;
transition: all 0.2s;
}
.btn-green {
background: rgba(107, 143, 113, 0.12);
color: #6B8F71;
border: 1px solid rgba(107, 143, 113, 0.3);
}
.btn-green:hover { background: rgba(107, 143, 113, 0.18); }
.btn-outline {
background: rgba(255,255,255,0.5);
color: #666;
border: 1px solid #DDD8CB;
}
.btn-outline:hover { background: rgba(255,255,255,0.8); }
/* Code + Diagram Area */
.hero-visual {
width: 560px;
display: flex;
flex-direction: column;
gap: 20px;
}
/* Flow diagram */
.flow-diagram {
display: flex;
align-items: center;
justify-content: center;
gap: 0;
padding: 20px 24px;
background: rgba(255,255,255,0.45);
border-radius: 16px;
border: 1px solid #E5DFCE;
}
.flow-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.flow-node-box {
padding: 10px 20px;
background: #FFFFFF;
border-radius: 10px;
border: 1px solid #E0DACE;
font-size: 13px;
font-weight: 500;
color: #3A3A35;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
}
.flow-node-box.highlight {
background: #6B8F71;
border-color: #6B8F71;
color: #fff;
}
.flow-node-label {
font-size: 10px;
color: #BBB;
letter-spacing: 0.5px;
}
.flow-arrow {
display: flex;
align-items: center;
padding: 0 12px;
}
.flow-arrow-line {
width: 40px;
height: 1px;
background: #CCC8BA;
position: relative;
}
.flow-arrow-line::after {
content: '';
position: absolute;
right: -1px;
top: -3px;
border: solid #CCC8BA;
border-width: 0 1px 1px 0;
padding: 3px;
transform: rotate(-45deg);
}
.flow-models {
display: flex;
flex-direction: column;
gap: 6px;
}
.flow-model-tag {
padding: 6px 14px;
background: #FFFFFF;
border-radius: 8px;
border: 1px solid #E0DACE;
font-size: 11px;
font-weight: 400;
color: #888;
box-shadow: 0 1px 4px rgba(0,0,0,0.02);
}
/* Code block */
.code-card {
background: #FAF5EC;
border-radius: 16px;
border: 1px solid #E5DFCE;
overflow: hidden;
}
.code-card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
border-bottom: 1px solid #E5DFCE;
background: rgba(255,255,255,0.3);
}
.code-card-header .dots {
display: flex;
gap: 6px;
}
.code-card-header .dots span {
width: 9px;
height: 9px;
border-radius: 50%;
background: #DDD8CB;
}
.code-card-header .fname {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: #BBB;
}
.code-card-body {
padding: 20px 24px;
font-family: 'JetBrains Mono', monospace;
font-size: 12.5px;
line-height: 1.8;
color: #555;
}
.code-card-body .kw { color: #6B8F71; font-weight: 500; }
.code-card-body .str { color: #D4A574; }
.code-card-body .cmt { color: #C4C0B4; }
/* Quick Links */
.quick-links {
display: flex;
justify-content: center;
gap: 20px;
padding: 8px 64px 32px;
}
.quick-link {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
padding: 12px 22px;
background: rgba(255,255,255,0.45);
border: 1px solid #E5DFCE;
border-radius: 12px;
font-size: 13px;
font-weight: 400;
color: #777;
transition: all 0.2s;
}
.quick-link:hover {
background: rgba(255,255,255,0.7);
color: #3A3A35;
}
.quick-link i { color: #6B8F71; }
/* Features */
.features {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
padding: 0 64px;
position: relative;
}
.feature-card {
background: rgba(255,255,255,0.5);
border: 1px solid #E5DFCE;
border-radius: 16px;
padding: 28px 24px;
transition: all 0.2s;
position: relative;
}
.feature-card:hover {
background: rgba(255,255,255,0.75);
box-shadow: 0 4px 20px rgba(0,0,0,0.03);
}
.feature-icon-wrap {
width: 40px;
height: 40px;
border-radius: 12px;
background: rgba(107,143,113,0.15);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
}
.feature-icon-wrap i { color: #6B8F71; }
.feature-card h3 {
font-family: 'Noto Serif SC', serif;
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
color: #2D3436;
}
.feature-card p {
font-size: 13px;
font-weight: 300;
line-height: 1.65;
color: #AAA;
}
/* Connection lines between feature cards */
.features::before {
content: '';
position: absolute;
top: 50%;
left: calc(33.33% + 32px);
width: calc(33.33% - 64px - 20px);
height: 0;
border-top: 1px dashed #D4CEBD;
transform: translateX(10px);
}
.features::after {
content: '';
position: absolute;
top: 50%;
right: calc(33.33% + 32px);
width: calc(33.33% - 64px - 20px);
height: 0;
border-top: 1px dashed #D4CEBD;
transform: translateX(-10px);
}
</style>
</head>
<body>
<!-- Navigation -->
<nav>
<div class="nav-logo">
<div class="nav-logo-mark"><i data-lucide="zap" style="width:18px;height:18px;"></i></div>
<span class="nav-logo-text">Nexus API</span>
</div>
<div class="nav-links">
<a href="#" class="active">Docs</a>
<a href="#">API</a>
<a href="#">Changelog</a>
<a href="#">Status</a>
<a href="#">GitHub</a>
</div>
<div class="nav-right">
<div class="search-box">
<i data-lucide="search" style="width:13px;height:13px;color:#CCC;"></i>
<span>Search documentation...</span>
<kbd>/</kbd>
</div>
</div>
</nav>
<!-- Hero -->
<section class="hero">
<div class="hero-content">
<div class="hero-tag"><i data-lucide="sparkles" style="width:12px;height:12px;"></i> Unified AI Gateway</div>
<h1>One API,<br>every AI model</h1>
<p>Access GPT, Claude, Gemini, and 20+ models through a single endpoint. Intelligent routing, unified billing, zero vendor lock-in.</p>
<div class="hero-buttons">
<a href="#" class="btn-green"><i data-lucide="book-open" style="width:14px;height:14px;"></i> Get Started</a>
<a href="#" class="btn-outline">API Reference</a>
</div>
</div>
<div class="hero-visual">
<!-- Flow diagram -->
<div class="flow-diagram">
<div class="flow-node">
<div class="flow-node-box">Your App</div>
<span class="flow-node-label">request</span>
</div>
<div class="flow-arrow"><div class="flow-arrow-line"></div></div>
<div class="flow-node">
<div class="flow-node-box highlight">Nexus</div>
<span class="flow-node-label">routes</span>
</div>
<div class="flow-arrow"><div class="flow-arrow-line"></div></div>
<div class="flow-models">
<div class="flow-model-tag">GPT-4o</div>
<div class="flow-model-tag">Claude 3.5</div>
<div class="flow-model-tag">Gemini Pro</div>
</div>
</div>
<!-- Code block -->
<div class="code-card">
<div class="code-card-header">
<div class="dots"><span></span><span></span><span></span></div>
<span class="fname">quickstart.py</span>
<i data-lucide="copy" style="width:13px;height:13px;color:#CCC;cursor:pointer;"></i>
</div>
<div class="code-card-body">
<span class="kw">from</span> nexus <span class="kw">import</span> Client<br><br>
client = Client(api_key=<span class="str">"your-key"</span>)<br>
response = client.chat(<br>
&nbsp;&nbsp;&nbsp;&nbsp;model=<span class="str">"auto"</span>,&nbsp;&nbsp;<span class="cmt"># intelligently routes</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;messages=[{<span class="str">"role"</span>: <span class="str">"user"</span>, <span class="str">"content"</span>: <span class="str">"Hello!"</span>}]<br>
)
</div>
</div>
</div>
</section>
<!-- Quick Links -->
<div class="quick-links">
<a href="#" class="quick-link"><i data-lucide="rocket" style="width:14px;height:14px;"></i> Getting Started</a>
<a href="#" class="quick-link"><i data-lucide="file-text" style="width:14px;height:14px;"></i> API Reference</a>
<a href="#" class="quick-link"><i data-lucide="layers" style="width:14px;height:14px;"></i> Models</a>
<a href="#" class="quick-link"><i data-lucide="credit-card" style="width:14px;height:14px;"></i> Pricing</a>
</div>
<!-- Features -->
<section class="features">
<div class="feature-card">
<div class="feature-icon-wrap"><i data-lucide="git-branch" style="width:18px;height:18px;"></i></div>
<h3>Model Routing</h3>
<p>Automatically select the best model for each request based on task complexity, latency, and cost constraints.</p>
</div>
<div class="feature-card">
<div class="feature-icon-wrap"><i data-lucide="trending-down" style="width:18px;height:18px;"></i></div>
<h3>Cost Optimization</h3>
<p>Reduce AI spend by up to 60% with intelligent selection and automatic fallback to cost-effective alternatives.</p>
</div>
<div class="feature-card">
<div class="feature-icon-wrap"><i data-lucide="bar-chart-3" style="width:18px;height:18px;"></i></div>
<h3>Usage Analytics</h3>
<p>Real-time dashboards tracking token usage, latency, model performance, and cost breakdowns per project.</p>
</div>
</section>
<!-- Spec annotation -->
<svg style="position:absolute;bottom:24px;right:64px;opacity:0.12;" width="120" height="40" viewBox="0 0 120 40" fill="none">
<line x1="0" y1="20" x2="72" y2="20" stroke="#6B8F71" stroke-width="0.5"/>
<circle cx="72" cy="20" r="2.5" fill="none" stroke="#6B8F71" stroke-width="0.5"/>
<text x="82" y="23" font-family="Inter" font-size="8" fill="#6B8F71" letter-spacing="0.5">20+ models</text>
</svg>
<script>lucide.createIcons();</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

@@ -0,0 +1,367 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1440">
<title>Alex Chen — Indie Developer & AI Creator</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1440px;
height: 900px;
overflow: hidden;
margin: 0;
background: #FAFAF8;
font-family: 'Inter', sans-serif;
color: #2A2A28;
position: relative;
}
/* GLASSMORPHISM NAV */
nav {
position: fixed;
top: 0;
left: 0;
width: 1440px;
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 80px;
background: rgba(250, 250, 248, 0.72);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border-bottom: 1px solid rgba(0,0,0,0.04);
z-index: 100;
}
nav .logo {
font-weight: 500;
font-size: 15px;
letter-spacing: 0.02em;
color: #2A2A28;
}
nav .logo .dot { color: #D4A574; }
nav ul {
list-style: none;
display: flex;
gap: 40px;
}
nav ul li a {
font-weight: 400;
font-size: 13px;
color: #888;
text-decoration: none;
letter-spacing: 0.01em;
transition: color 0.3s;
}
nav ul li a:hover { color: #2A2A28; }
nav .nav-cta a {
font-weight: 400;
font-size: 12px;
color: #2A2A28;
text-decoration: none;
padding: 8px 24px;
border: 1px solid rgba(0,0,0,0.08);
border-radius: 2px;
transition: all 0.3s;
letter-spacing: 0.02em;
}
nav .nav-cta a:hover {
border-color: #D4A574;
color: #D4A574;
}
/* HERO LAYOUT */
.hero {
position: absolute;
top: 0;
left: 0;
width: 1440px;
height: 900px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 80px;
}
.hero-content {
display: flex;
align-items: center;
gap: 96px;
width: 100%;
max-width: 1200px;
}
/* LEFT: TEXT */
.hero-text {
flex: 1;
}
.hero-text .greeting {
font-weight: 400;
font-size: 11px;
color: #B0ACA4;
letter-spacing: 4px;
text-transform: uppercase;
margin-bottom: 24px;
}
.hero-text h1 {
font-weight: 200;
font-size: 80px;
line-height: 1.02;
letter-spacing: -0.04em;
color: #2A2A28;
}
.hero-text h1 strong {
font-weight: 500;
}
.hero-text h1 .gold-period {
color: #D4A574;
font-weight: 300;
}
.hero-text .tagline {
font-weight: 300;
font-size: 16px;
line-height: 1.8;
color: #999;
margin-top: 32px;
max-width: 440px;
}
/* CTA BUTTONS */
.hero-cta {
display: flex;
gap: 16px;
margin-top: 48px;
}
.btn-primary {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: 'Inter', sans-serif;
font-weight: 400;
font-size: 13px;
color: #FAFAF8;
background: #2A2A28;
border: none;
padding: 14px 32px;
border-radius: 2px;
cursor: pointer;
transition: all 0.3s;
text-decoration: none;
letter-spacing: 0.02em;
}
.btn-primary:hover { background: #3A3A38; }
.btn-secondary {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: 'Inter', sans-serif;
font-weight: 400;
font-size: 13px;
color: #888;
background: transparent;
border: 1px solid rgba(0,0,0,0.08);
padding: 14px 32px;
border-radius: 2px;
cursor: pointer;
transition: all 0.3s;
text-decoration: none;
letter-spacing: 0.02em;
}
.btn-secondary:hover { border-color: #D4A574; color: #D4A574; }
/* RIGHT: CARDS + PORTRAIT */
.hero-visual {
flex: 0 0 460px;
position: relative;
height: 520px;
}
/* PORTRAIT */
.portrait {
width: 200px;
height: 200px;
border-radius: 50%;
background: #EDECE8;
position: absolute;
top: 0;
right: 40px;
box-shadow: 0 20px 60px rgba(0,0,0,0.06);
overflow: hidden;
}
.portrait::after {
content: '';
position: absolute;
bottom: 0; left: 50%;
transform: translateX(-50%);
width: 110px;
height: 130px;
background: #D8D6D0;
border-radius: 55px 55px 0 0;
}
/* FLOATING CARDS */
.card {
position: absolute;
background: #FFFFFF;
border: 1px solid rgba(0,0,0,0.04);
border-radius: 2px;
padding: 24px;
box-shadow: 0 4px 24px rgba(0,0,0,0.03);
}
.card-1 {
top: 60px;
left: 0;
width: 220px;
}
.card-2 {
top: 240px;
left: 60px;
width: 240px;
}
.card-3 {
top: 180px;
right: 0;
width: 200px;
}
.card .card-number {
font-weight: 200;
font-size: 32px;
letter-spacing: -0.02em;
color: #2A2A28;
line-height: 1;
}
.card .card-number .gold { color: #D4A574; font-weight: 300; }
.card .card-label {
font-weight: 400;
font-size: 10px;
color: #B0ACA4;
margin-top: 8px;
letter-spacing: 2px;
text-transform: uppercase;
}
.card .card-desc {
font-weight: 300;
font-size: 12px;
color: #999;
margin-top: 8px;
line-height: 1.5;
}
/* GOLD ACCENT LINE */
.accent-line {
position: absolute;
bottom: 0;
left: 100px;
width: 48px;
height: 2px;
background: #D4A574;
border-radius: 1px;
}
/* Removed dot-grid — Build: zero decorative elements */
/* BOTTOM TICKER */
.bottom-bar {
position: absolute;
bottom: 0;
left: 0;
width: 1440px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
gap: 48px;
border-top: 1px solid rgba(0,0,0,0.04);
}
.bottom-bar span {
font-weight: 300;
font-size: 11px;
color: #BBB;
letter-spacing: 0.08em;
}
.bottom-bar .sep {
width: 4px;
height: 4px;
border-radius: 50%;
background: #D4A574;
opacity: 0.5;
}
</style>
</head>
<body>
<!-- NAV -->
<nav>
<div class="logo">alex chen<span class="dot"> .</span></div>
<ul>
<li><a href="#work">Work</a></li>
<li><a href="#content">Content</a></li>
<li><a href="#services">Services</a></li>
</ul>
<div class="nav-cta">
<a href="#contact">Get in Touch</a>
</div>
</nav>
<!-- HERO -->
<div class="hero">
<div class="hero-content">
<!-- TEXT -->
<div class="hero-text">
<div class="greeting">Indie Developer & AI Creator</div>
<h1>Alex<br><strong>Chen</strong><span class="gold-period">.</span></h1>
<p class="tagline">Building tools at the intersection of AI and creativity. Shipping products, writing stories, shaping ideas.</p>
<div class="hero-cta">
<a href="#work" class="btn-primary">
View Work
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
</a>
<a href="#content" class="btn-secondary">Read Articles</a>
</div>
</div>
<!-- VISUAL -->
<div class="hero-visual">
<div class="portrait"></div>
<div class="card card-1">
<div class="card-number">300K<span class="gold">+</span></div>
<div class="card-label">Followers</div>
<div class="card-desc">Across platforms, building in public</div>
</div>
<div class="card card-2">
<div class="card-number"><span class="gold">#</span>1</div>
<div class="card-label">App Store</div>
<div class="card-desc">Top paid app, shipped as a solo developer</div>
</div>
<div class="card card-3">
<div class="card-number">100<span class="gold">+</span></div>
<div class="card-label">Articles</div>
<div class="card-desc">On AI, dev, and creative tools</div>
</div>
<div class="accent-line"></div>
</div>
</div>
</div>
<!-- BOTTOM BAR -->
<div class="bottom-bar">
<span>Developer</span>
<div class="sep"></div>
<span>Writer</span>
<div class="sep"></div>
<span>AI Creator</span>
<div class="sep"></div>
<span>Speaker</span>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

@@ -0,0 +1,368 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1440">
<title>Alex Chen — Indie Developer & AI Creator</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1440px;
height: 900px;
overflow: hidden;
margin: 0;
background: #FFFFFF;
font-family: 'Helvetica Neue', Arial, sans-serif;
color: #111111;
position: relative;
}
/* NAV */
nav {
position: absolute;
top: 0; left: 0; right: 0;
height: 72px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 80px;
border-bottom: 1px solid #111;
z-index: 10;
}
nav .logo {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-weight: 700;
font-size: 16px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
nav .logo span { color: #E63946; }
nav ul {
list-style: none;
display: flex;
gap: 48px;
}
nav ul li a {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-weight: 500;
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
text-decoration: none;
color: #111;
transition: color 0.2s;
}
nav ul li a:hover { color: #E63946; }
nav .nav-contact a {
background: #111;
color: #fff;
padding: 10px 28px;
font-family: 'Helvetica Neue', Arial, sans-serif;
font-weight: 500;
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
text-decoration: none;
transition: background 0.2s;
}
nav .nav-contact a:hover { background: #E63946; }
/* MAIN GRID */
.hero {
position: absolute;
top: 72px;
left: 0;
right: 0;
bottom: 0;
display: grid;
grid-template-columns: 1fr 1px 1fr;
grid-template-rows: 1fr;
}
/* LEFT PANEL */
.hero-left {
padding: 64px 80px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.hero-left .intro-label {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 11px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #999;
margin-bottom: 16px;
}
.hero-left .name {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-weight: 900;
font-size: 112px;
line-height: 0.92;
letter-spacing: -0.03em;
color: #111;
}
.hero-left .name .accent { color: #E63946; }
.hero-left .tagline {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-weight: 300;
font-size: 20px;
line-height: 1.6;
color: #555;
max-width: 480px;
margin-top: 32px;
}
/* STATS ROW */
.stats-row {
display: flex;
gap: 0;
border-top: 1px solid #111;
padding-top: 32px;
}
.stat-item {
flex: 1;
position: relative;
}
.stat-item:not(:last-child)::after {
content: '';
position: absolute;
right: 0;
top: 0;
height: 100%;
width: 1px;
background: #DDD;
}
.stat-item .stat-number {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-weight: 700;
font-size: 48px;
letter-spacing: -0.02em;
color: #111;
line-height: 1;
}
.stat-item .stat-number .red { color: #E63946; }
.stat-item .stat-label {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #999;
margin-top: 8px;
}
/* CENTER DIVIDER */
.divider {
background: #111;
}
/* RIGHT PANEL */
.hero-right {
padding: 64px 80px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
background: #FAFAFA;
}
/* PORTRAIT PLACEHOLDER */
.portrait-wrap {
position: relative;
width: 320px;
height: 320px;
}
.portrait-circle {
width: 320px;
height: 320px;
border-radius: 50%;
background: #E8E8E8;
position: relative;
overflow: hidden;
}
.portrait-circle::after {
content: '';
position: absolute;
bottom: 0; left: 50%;
transform: translateX(-50%);
width: 180px;
height: 200px;
background: #D0D0D0;
border-radius: 90px 90px 0 0;
}
.portrait-frame {
position: absolute;
top: -12px;
left: -12px;
width: 344px;
height: 344px;
border: 1px solid #E63946;
border-radius: 50%;
}
/* RED INDEX MARKER */
.index-marker {
position: absolute;
bottom: 64px;
right: 80px;
text-align: right;
}
.index-marker .idx-num {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-weight: 700;
font-size: 120px;
line-height: 0.85;
color: #E63946;
opacity: 0.1;
}
.index-marker .idx-label {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #999;
margin-top: 8px;
}
/* DECORATIVE ELEMENTS */
.corner-mark {
position: absolute;
top: 64px;
right: 80px;
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #CCC;
}
.hero-right .role-tags {
margin-top: 40px;
display: flex;
gap: 12px;
}
.role-tags span {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 8px 20px;
border: 1px solid #CCC;
color: #666;
transition: all 0.2s;
}
.role-tags span:hover {
border-color: #E63946;
color: #E63946;
}
/* SCROLL CTA */
.scroll-cta {
position: absolute;
bottom: 28px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.scroll-cta span {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 10px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #BBB;
}
.scroll-cta .arrow-down {
width: 1px;
height: 32px;
background: #CCC;
position: relative;
}
.scroll-cta .arrow-down::after {
content: '';
position: absolute;
bottom: 0;
left: -3px;
width: 7px;
height: 7px;
border-right: 1px solid #CCC;
border-bottom: 1px solid #CCC;
transform: rotate(45deg);
}
</style>
</head>
<body>
<!-- NAVIGATION -->
<nav>
<div class="logo">Alex<span>.</span>Chen</div>
<ul>
<li><a href="#work">Work</a></li>
<li><a href="#content">Content</a></li>
<li><a href="#services">Services</a></li>
</ul>
<div class="nav-contact">
<a href="#contact">Contact</a>
</div>
</nav>
<!-- HERO -->
<div class="hero">
<!-- LEFT -->
<div class="hero-left">
<div>
<div class="intro-label">Indie Developer / AI Creator</div>
<h1 class="name">Alex<br>Chen<span class="accent">.</span></h1>
<p class="tagline">Building tools at the intersection of AI and creativity.</p>
</div>
<div class="stats-row">
<div class="stat-item">
<div class="stat-number">300K<span class="red">+</span></div>
<div class="stat-label">Followers</div>
</div>
<div class="stat-item" style="padding-left: 32px;">
<div class="stat-number">#1</div>
<div class="stat-label">App Store</div>
</div>
<div class="stat-item" style="padding-left: 32px;">
<div class="stat-number">100<span class="red">+</span></div>
<div class="stat-label">Articles</div>
</div>
</div>
</div>
<!-- DIVIDER -->
<div class="divider"></div>
<!-- RIGHT -->
<div class="hero-right">
<div class="corner-mark">Portfolio 2026</div>
<div class="portrait-wrap">
<div class="portrait-circle"></div>
<div class="portrait-frame"></div>
</div>
<div class="role-tags">
<span>Developer</span>
<span>Writer</span>
<span>Creator</span>
</div>
<div class="index-marker">
<div class="idx-num">01</div>
<div class="idx-label">Hero</div>
</div>
</div>
</div>
<!-- SCROLL CTA -->
<div class="scroll-cta">
<span>Scroll</span>
<div class="arrow-down"></div>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

@@ -0,0 +1,459 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1440">
<title>Alex Chen — Indie Developer & AI Creator</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&family=Noto+Serif+SC:wght@300;400;600&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1440px;
height: 900px;
overflow: hidden;
margin: 0;
background: #F5F0EB;
font-family: 'Inter', sans-serif;
color: #3D3D3A;
position: relative;
}
/* PAPER TEXTURE */
body::before {
content: '';
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
background:
repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0,0,0,0.008) 2px,
rgba(0,0,0,0.008) 4px
),
repeating-linear-gradient(
90deg,
transparent,
transparent 2px,
rgba(0,0,0,0.005) 2px,
rgba(0,0,0,0.005) 4px
);
pointer-events: none;
z-index: 1;
}
/* NAV */
nav {
position: absolute;
top: 0; left: 0; right: 0;
height: 72px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 80px;
z-index: 10;
}
nav .logo {
font-family: 'Noto Serif SC', serif;
font-weight: 400;
font-size: 16px;
color: #3D3D3A;
letter-spacing: 0.02em;
}
nav ul {
list-style: none;
display: flex;
gap: 36px;
align-items: center;
}
nav ul li a {
font-weight: 400;
font-size: 13px;
color: #8A8A84;
text-decoration: none;
letter-spacing: 0.01em;
transition: color 0.3s;
}
nav ul li a:hover { color: #3D3D3A; }
nav ul li.active a { color: #3D3D3A; }
/* Hairline below nav */
nav::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48px;
height: 1px;
background: #C8C2B6;
}
/* MAIN LAYOUT - ASYMMETRIC */
.hero {
position: absolute;
top: 72px;
left: 0;
width: 1440px;
height: 828px;
display: grid;
grid-template-columns: 120px 1fr 400px 120px;
grid-template-rows: 1fr;
align-items: center;
z-index: 2;
}
/* LEFT MARGIN ELEMENT */
.margin-left {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
padding-bottom: 80px;
height: 100%;
}
.margin-left .vertical-text {
writing-mode: vertical-rl;
font-size: 10px;
letter-spacing: 0.18em;
color: #B8B2A6;
font-weight: 300;
}
/* CENTER CONTENT */
.hero-center {
padding: 0 40px;
display: flex;
flex-direction: column;
justify-content: center;
}
.hero-center .section-label {
font-size: 10px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: #6B8F71;
font-weight: 500;
margin-bottom: 32px;
opacity: 0.8;
}
.hero-center h1 {
font-family: 'Noto Serif SC', serif;
font-weight: 300;
font-size: 56px;
line-height: 1.25;
letter-spacing: -0.01em;
color: #2D3436;
}
.hero-center h1 .serif-accent {
font-family: 'Noto Serif SC', serif;
font-weight: 600;
font-style: normal;
color: #2D3436;
}
/* HAIRLINE DIVIDER */
.hairline {
width: 48px;
height: 1px;
background: #C8C2B6;
margin: 36px 0;
}
.hero-center .tagline {
font-weight: 300;
font-size: 16px;
line-height: 1.8;
color: #8A8A84;
max-width: 420px;
}
/* STATS - HORIZONTAL */
.stats {
display: flex;
gap: 48px;
margin-top: 48px;
}
.stat {
display: flex;
flex-direction: column;
}
.stat .stat-value {
font-family: 'Noto Serif SC', serif;
font-weight: 600;
font-size: 28px;
color: #2D3436;
letter-spacing: -0.01em;
line-height: 1;
}
.stat .stat-desc {
font-size: 11px;
color: #B8B2A6;
margin-top: 8px;
letter-spacing: 0.04em;
font-weight: 400;
}
/* CTA */
.hero-cta {
margin-top: 48px;
display: flex;
gap: 20px;
align-items: center;
}
.cta-link {
font-weight: 500;
font-size: 13px;
color: #3D3D3A;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 0;
border-bottom: 1px solid #C8C2B6;
transition: all 0.3s;
}
.cta-link:hover { border-color: #6B8F71; color: #6B8F71; }
.cta-link svg { width: 14px; height: 14px; }
.cta-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: #C8C2B6;
}
.cta-subtle {
font-weight: 300;
font-size: 13px;
color: #B8B2A6;
text-decoration: none;
transition: color 0.3s;
}
.cta-subtle:hover { color: #3D3D3A; }
/* RIGHT PANEL */
.hero-right {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 20px;
position: relative;
}
/* PORTRAIT */
.portrait-container {
position: relative;
}
.portrait {
width: 180px;
height: 180px;
border-radius: 50%;
background: #EAE5DD;
overflow: hidden;
position: relative;
border: 1px solid rgba(200, 194, 182, 0.4);
}
.portrait::after {
content: '';
position: absolute;
bottom: 0; left: 50%;
transform: translateX(-50%);
width: 96px;
height: 110px;
background: #D5CEC4;
border-radius: 48px 48px 0 0;
}
.portrait-ring {
position: absolute;
top: -16px; left: -16px;
width: 212px;
height: 212px;
border-radius: 50%;
border: 1px solid rgba(107, 143, 113, 0.2);
}
/* BIO CARD */
.bio-card {
margin-top: 36px;
background: rgba(255,255,255,0.5);
border: 1px solid rgba(0,0,0,0.04);
border-radius: 12px;
padding: 24px 28px;
width: 260px;
backdrop-filter: blur(8px);
}
.bio-card .bio-name {
font-family: 'Noto Serif SC', serif;
font-weight: 600;
font-size: 15px;
color: #3D3D3A;
}
.bio-card .bio-role {
font-size: 12px;
color: #B8B2A6;
margin-top: 4px;
font-weight: 300;
}
.bio-card .bio-hairline {
width: 32px;
height: 1px;
background: #C8C2B6;
margin: 16px 0;
}
.bio-card .bio-desc {
font-size: 12px;
line-height: 1.7;
color: #8A8A84;
font-weight: 300;
}
/* RIGHT MARGIN */
.margin-right {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding-top: 80px;
height: 100%;
}
.margin-right .year {
writing-mode: vertical-rl;
font-size: 10px;
letter-spacing: 0.18em;
color: #C8C2B6;
font-weight: 300;
}
/* DECORATIVE: FLOATING LEAF/ORGANIC SHAPE */
.organic-shape {
position: absolute;
top: 140px;
right: 280px;
width: 60px;
height: 80px;
z-index: 3;
opacity: 0.08;
}
.organic-shape svg {
width: 100%;
height: 100%;
}
/* BOTTOM AREA */
.bottom-zen {
position: absolute;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 24px;
z-index: 5;
}
.bottom-zen .zen-line {
width: 48px;
height: 1px;
background: #C8C2B6;
}
.bottom-zen .zen-text {
font-size: 10px;
letter-spacing: 0.14em;
color: #C8C2B6;
font-weight: 300;
}
</style>
</head>
<body>
<!-- NAV -->
<nav>
<div class="logo">Alex Chen</div>
<ul>
<li class="active"><a href="#work">Work</a></li>
<li><a href="#content">Content</a></li>
<li><a href="#services">Services</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</nav>
<!-- DECORATIVE: Subtle spec annotation -->
<svg style="position:absolute;top:100px;right:340px;z-index:3;opacity:0.15;" width="60" height="60" viewBox="0 0 60 60" fill="none">
<circle cx="30" cy="30" r="28" stroke="#6B8F71" stroke-width="0.5"/>
<circle cx="30" cy="30" r="18" stroke="#6B8F71" stroke-width="0.5" stroke-dasharray="2,4"/>
<line x1="30" y1="0" x2="30" y2="60" stroke="#6B8F71" stroke-width="0.3"/>
<line x1="0" y1="30" x2="60" y2="30" stroke="#6B8F71" stroke-width="0.3"/>
</svg>
<!-- HERO -->
<div class="hero">
<!-- LEFT MARGIN -->
<div class="margin-left">
<div class="vertical-text">PORTFOLIO</div>
</div>
<!-- CENTER -->
<div class="hero-center">
<div class="section-label">Indie Developer & AI Creator</div>
<h1>Building tools<br>at the intersection<br>of <span class="serif-accent">AI</span> and <span class="serif-accent">creativity</span></h1>
<div class="hairline"></div>
<p class="tagline">I design, build, and write about the things that emerge when technology meets human imagination.</p>
<div class="stats">
<div class="stat">
<div class="stat-value">300K+</div>
<div class="stat-desc">followers</div>
</div>
<div class="stat">
<div class="stat-value">No. 1</div>
<div class="stat-desc">App Store</div>
</div>
<div class="stat">
<div class="stat-value">100+</div>
<div class="stat-desc">articles published</div>
</div>
</div>
<div class="hero-cta">
<a href="#work" class="cta-link">
Explore Work
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
</a>
<div class="cta-dot"></div>
<a href="#content" class="cta-subtle">Read Writing</a>
</div>
</div>
<!-- RIGHT -->
<div class="hero-right">
<div class="portrait-container">
<div class="portrait"></div>
<div class="portrait-ring"></div>
</div>
<div class="bio-card">
<div class="bio-name">Alex Chen</div>
<div class="bio-role">Developer / Writer / Creator</div>
<div class="bio-hairline"></div>
<div class="bio-desc">Shipping AI-powered products as an independent maker. Writing about the craft of building.</div>
</div>
</div>
<!-- RIGHT MARGIN -->
<div class="margin-right">
<div class="year">2026</div>
</div>
</div>
<!-- BOTTOM ZEN -->
<div class="bottom-zen">
<div class="zen-line"></div>
<div class="zen-text">Design as inquiry</div>
<div class="zen-line"></div>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

@@ -0,0 +1,493 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1440">
<title>Meridian — Business Intelligence for Modern Teams</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1440px;
height: 900px;
overflow: hidden;
margin: 0;
font-family: 'Inter', sans-serif;
background: #FAFAF8;
color: #1a1a1a;
}
/* NAV */
nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 96px;
height: 80px;
}
.nav-logo {
font-size: 20px;
font-weight: 500;
letter-spacing: -0.3px;
display: flex;
align-items: center;
gap: 8px;
color: #1a1a1a;
}
.nav-logo-icon {
width: 32px;
height: 32px;
background: #E8E4DF;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
}
.nav-logo-icon svg { width: 18px; height: 18px; color: #D4A574; }
.nav-links {
display: flex;
gap: 40px;
list-style: none;
}
.nav-links a {
font-size: 14px;
font-weight: 400;
text-decoration: none;
color: #777;
transition: color 0.2s;
}
.nav-links a:hover { color: #1a1a1a; }
.nav-right {
display: flex;
align-items: center;
gap: 24px;
}
.nav-signin {
font-size: 14px;
font-weight: 400;
text-decoration: none;
color: #777;
transition: color 0.2s;
}
.nav-signin:hover { color: #1a1a1a; }
.nav-cta {
font-size: 13px;
font-weight: 400;
padding: 10px 24px;
background: #1a1a1a;
color: #FAFAF8;
border: none;
border-radius: 2px;
cursor: pointer;
transition: background 0.2s;
}
.nav-cta:hover { background: #333; }
/* HERO LAYOUT */
.hero {
padding: 24px 96px 0 96px;
display: grid;
grid-template-columns: 480px 1fr;
gap: 64px;
align-items: start;
height: calc(900px - 80px);
position: relative;
}
/* LEFT TEXT */
.hero-text {
padding-top: 48px;
}
.hero-badge {
display: inline-flex;
align-items: center;
gap: 0;
padding: 0;
background: transparent;
margin-bottom: 32px;
}
.hero-badge-dot {
display: none;
}
.hero-badge span {
font-size: 10px;
font-weight: 400;
color: #B0ACA4;
letter-spacing: 4px;
text-transform: uppercase;
}
.hero-headline {
font-size: 48px;
font-weight: 300;
line-height: 1.15;
letter-spacing: -1.5px;
margin-bottom: 24px;
color: #1a1a1a;
}
.hero-headline em {
font-style: italic;
font-weight: 300;
color: #1a1a1a;
}
.hero-subtitle {
font-size: 17px;
font-weight: 300;
line-height: 1.7;
color: #888;
margin-bottom: 48px;
max-width: 400px;
}
.hero-ctas {
display: flex;
gap: 16px;
margin-bottom: 64px;
}
.btn-primary {
font-family: 'Inter', sans-serif;
font-size: 14px;
font-weight: 400;
padding: 14px 32px;
background: #1a1a1a;
color: #FAFAF8;
border: none;
border-radius: 2px;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary:hover { background: #333; }
.btn-secondary {
font-family: 'Inter', sans-serif;
font-size: 14px;
font-weight: 400;
padding: 14px 32px;
background: transparent;
color: #777;
border: 1px solid #ddd;
border-radius: 2px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
}
.btn-secondary:hover { border-color: #aaa; color: #1a1a1a; }
.btn-secondary svg { width: 15px; height: 15px; }
/* METRICS ROW */
.metrics {
display: flex;
gap: 48px;
}
.metric {
display: flex;
flex-direction: column;
gap: 4px;
}
.metric-value {
font-size: 36px;
font-weight: 200;
letter-spacing: -1px;
color: #1a1a1a;
}
.metric-value span { color: #D4A574; }
.metric-label {
font-size: 12px;
font-weight: 400;
color: #aaa;
letter-spacing: 0.3px;
}
/* RIGHT — DASHBOARD */
.hero-dashboard {
position: relative;
padding-top: 16px;
}
.dashboard-card {
background: #FFFFFF;
border-radius: 2px;
box-shadow:
0 1px 2px rgba(0,0,0,0.02),
0 4px 16px rgba(0,0,0,0.04);
padding: 28px;
width: 100%;
}
/* Dashboard header */
.dash-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.dash-title {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
}
.dash-period {
font-size: 12px;
font-weight: 400;
color: #aaa;
padding: 4px 12px;
border: 1px solid #eee;
border-radius: 2px;
}
/* KPI strip */
.kpi-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1px;
background: #f0eeeb;
border-radius: 2px;
overflow: hidden;
margin-bottom: 24px;
}
.kpi-item {
background: #FAFAF8;
padding: 18px 16px;
text-align: center;
}
.kpi-item-value {
font-size: 22px;
font-weight: 300;
color: #1a1a1a;
letter-spacing: -0.5px;
margin-bottom: 4px;
}
.kpi-item-label {
font-size: 10px;
font-weight: 500;
color: #bbb;
letter-spacing: 0.5px;
text-transform: uppercase;
}
/* Chart */
.chart-container {
margin-bottom: 24px;
position: relative;
height: 200px;
}
.chart-svg {
width: 100%;
height: 100%;
}
/* Bottom section */
.dash-bottom-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.insight-card {
background: #FAFAF8;
border-radius: 2px;
padding: 16px;
}
.insight-icon {
width: 28px;
height: 28px;
background: #F0EBE3;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}
.insight-icon svg { width: 14px; height: 14px; color: #D4A574; }
.insight-title {
font-size: 12px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 4px;
}
.insight-desc {
font-size: 11px;
font-weight: 300;
color: #999;
line-height: 1.5;
}
/* TRUST BAR */
.trust-bar {
position: absolute;
bottom: 24px;
left: 96px;
display: flex;
align-items: center;
gap: 40px;
}
.trust-label {
font-size: 11px;
font-weight: 400;
color: #ccc;
white-space: nowrap;
}
.trust-logos {
display: flex;
gap: 40px;
align-items: center;
}
.trust-logo {
font-size: 14px;
font-weight: 400;
color: #ccc;
letter-spacing: 0.3px;
}
</style>
</head>
<body>
<!-- NAV -->
<nav>
<div class="nav-logo">
<div class="nav-logo-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2"/>
</svg>
</div>
Meridian
</div>
<ul class="nav-links">
<li><a href="#">Product</a></li>
<li><a href="#">Pricing</a></li>
<li><a href="#">Docs</a></li>
<li><a href="#">Blog</a></li>
</ul>
<div class="nav-right">
<a href="#" class="nav-signin">Sign In</a>
<button class="nav-cta">Get Started</button>
</div>
</nav>
<!-- HERO -->
<div class="hero">
<!-- LEFT -->
<div class="hero-text">
<div class="hero-badge">
<div class="hero-badge-dot"></div>
<span>Business Intelligence for Modern Teams</span>
</div>
<h1 class="hero-headline">Turn data into <em>decisions,</em> not dashboards<span style="color:#D4A574;font-weight:300;">.</span></h1>
<p class="hero-subtitle">AI-powered analytics that tells you what matters, when it matters. Less noise, more clarity.</p>
<div class="hero-ctas">
<button class="btn-primary">Start Free Trial</button>
<button class="btn-secondary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Watch Demo
</button>
</div>
<div class="metrics">
<div class="metric">
<div class="metric-value">3<span>x</span></div>
<div class="metric-label">Faster insights</div>
</div>
<div class="metric">
<div class="metric-value">50<span>%</span></div>
<div class="metric-label">Less meeting time</div>
</div>
<div class="metric">
<div class="metric-value">99.9<span>%</span></div>
<div class="metric-label">Uptime SLA</div>
</div>
</div>
</div>
<!-- RIGHT — FLOATING DASHBOARD -->
<div class="hero-dashboard">
<div class="dashboard-card">
<div class="dash-header">
<div class="dash-title">Performance Overview</div>
<div class="dash-period">Last 30 days</div>
</div>
<div class="kpi-strip">
<div class="kpi-item">
<div class="kpi-item-value">$2.4M</div>
<div class="kpi-item-label">Revenue</div>
</div>
<div class="kpi-item">
<div class="kpi-item-value">84.2K</div>
<div class="kpi-item-label">Users</div>
</div>
<div class="kpi-item">
<div class="kpi-item-value">1.2%</div>
<div class="kpi-item-label">Churn</div>
</div>
<div class="kpi-item">
<div class="kpi-item-value">$142</div>
<div class="kpi-item-label">ARPU</div>
</div>
</div>
<!-- SVG Chart -->
<div class="chart-container">
<svg class="chart-svg" viewBox="0 0 700 200" preserveAspectRatio="none">
<!-- Grid lines -->
<line x1="0" y1="50" x2="700" y2="50" stroke="#f0eeeb" stroke-width="1"/>
<line x1="0" y1="100" x2="700" y2="100" stroke="#f0eeeb" stroke-width="1"/>
<line x1="0" y1="150" x2="700" y2="150" stroke="#f0eeeb" stroke-width="1"/>
<!-- Area fill -->
<defs>
<linearGradient id="areaGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#D4A574" stop-opacity="0.15"/>
<stop offset="100%" stop-color="#D4A574" stop-opacity="0.01"/>
</linearGradient>
</defs>
<path d="M0,160 C50,155 100,140 150,120 C200,100 250,110 300,85 C350,60 400,70 450,50 C500,30 550,45 600,35 C650,25 680,20 700,15 L700,200 L0,200 Z" fill="url(#areaGrad)"/>
<!-- Main line -->
<path d="M0,160 C50,155 100,140 150,120 C200,100 250,110 300,85 C350,60 400,70 450,50 C500,30 550,45 600,35 C650,25 680,20 700,15" fill="none" stroke="#D4A574" stroke-width="2.5" stroke-linecap="round"/>
<!-- Secondary line -->
<path d="M0,170 C50,165 100,158 150,150 C200,142 250,145 300,135 C350,125 400,128 450,118 C500,108 550,112 600,105 C650,98 680,95 700,90" fill="none" stroke="#e0d5c8" stroke-width="1.5" stroke-dasharray="4,4"/>
<!-- Data point -->
<circle cx="600" cy="35" r="5" fill="#D4A574"/>
<circle cx="600" cy="35" r="8" fill="none" stroke="#D4A574" stroke-width="1" opacity="0.4"/>
</svg>
</div>
<div class="dash-bottom-row">
<div class="insight-card">
<div class="insight-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
</div>
<div class="insight-title">AI Insight: Revenue Acceleration</div>
<div class="insight-desc">Enterprise segment grew 23% this quarter, driven by 4 new accounts. Recommend increasing sales capacity.</div>
</div>
<div class="insight-card">
<div class="insight-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
</div>
<div class="insight-title">Predicted: Q3 Target On Track</div>
<div class="insight-desc">Based on current trajectory, 89% probability of hitting $3.2M quarterly target. Pipeline looks healthy.</div>
</div>
</div>
</div>
</div>
<!-- TRUST BAR -->
<div class="trust-bar">
<span class="trust-label">Trusted by teams at</span>
<div class="trust-logos">
<span class="trust-logo">Stripe</span>
<span class="trust-logo">Notion</span>
<span class="trust-logo">Linear</span>
<span class="trust-logo">Vercel</span>
<span class="trust-logo">Figma</span>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
});
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

@@ -0,0 +1,556 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1440">
<title>Meridian — Business Intelligence for Modern Teams</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1440px;
height: 900px;
overflow: hidden;
margin: 0;
font-family: 'Helvetica Neue', Arial, sans-serif;
background: #FFFFFF;
color: #000000;
}
/* NAV */
nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 80px;
height: 72px;
border-bottom: 1px solid #000;
}
.nav-logo {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-weight: 700;
font-size: 22px;
letter-spacing: -0.5px;
display: flex;
align-items: center;
gap: 8px;
}
.nav-logo svg { width: 24px; height: 24px; }
.nav-links {
display: flex;
gap: 40px;
list-style: none;
}
.nav-links a {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 1.5px;
text-decoration: none;
color: #000;
transition: color 0.2s;
}
.nav-links a:hover { color: #E63946; }
.nav-signin {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
text-decoration: none;
color: #000;
padding: 8px 20px;
border: 2px solid #000;
transition: all 0.2s;
}
.nav-signin:hover { background: #000; color: #fff; }
/* HERO */
.hero {
display: grid;
grid-template-columns: 1fr 1fr;
height: calc(900px - 72px);
}
/* LEFT PANEL */
.hero-left {
padding: 64px 80px 48px 80px;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
}
.hero-label {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 3px;
color: #E63946;
margin-bottom: 24px;
}
.hero-headline {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 52px;
font-weight: 900;
line-height: 1.05;
letter-spacing: -2px;
margin-bottom: 20px;
max-width: 520px;
}
.hero-headline span { color: #E63946; }
.hero-subtitle {
font-size: 17px;
font-weight: 400;
line-height: 1.6;
color: #444;
max-width: 440px;
margin-bottom: 36px;
}
.hero-ctas {
display: flex;
gap: 16px;
margin-bottom: 48px;
}
.btn-primary {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
padding: 16px 36px;
background: #E63946;
color: #fff;
border: none;
cursor: pointer;
transition: background 0.2s;
}
.btn-primary:hover { background: #c4303c; }
.btn-secondary {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
padding: 16px 36px;
background: transparent;
color: #000;
border: 2px solid #000;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
}
.btn-secondary:hover { background: #000; color: #fff; }
.btn-secondary svg { width: 16px; height: 16px; }
/* BIG NUMBER */
.big-number {
position: absolute;
bottom: 64px;
left: 80px;
display: flex;
align-items: baseline;
gap: 40px;
}
.big-number-main {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 140px;
font-weight: 700;
line-height: 1;
letter-spacing: -6px;
color: #E63946;
}
.big-number-label {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 2px;
color: #666;
max-width: 100px;
line-height: 1.5;
}
.big-number-divider {
width: 1px;
height: 48px;
background: #ccc;
}
.metric-small {
display: flex;
flex-direction: column;
gap: 4px;
}
.metric-small-value {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 32px;
font-weight: 700;
letter-spacing: -1px;
}
.metric-small-label {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 2px;
color: #888;
}
/* RIGHT PANEL — DASHBOARD */
.hero-right {
background: #000;
padding: 32px;
display: flex;
flex-direction: column;
gap: 16px;
position: relative;
}
/* Dashboard grid */
.dash-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.dash-title {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 2px;
color: #666;
}
.dash-live {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-weight: 500;
color: #E63946;
text-transform: uppercase;
letter-spacing: 1px;
}
.dash-live-dot {
width: 6px;
height: 6px;
background: #E63946;
border-radius: 50%;
}
/* KPI Row */
.kpi-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.kpi-card {
background: #111;
border: 1px solid #222;
padding: 20px;
}
.kpi-label {
font-size: 10px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 1.5px;
color: #555;
margin-bottom: 8px;
}
.kpi-value {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 28px;
font-weight: 700;
color: #fff;
letter-spacing: -1px;
}
.kpi-change {
font-size: 12px;
font-weight: 500;
color: #E63946;
margin-top: 4px;
}
.kpi-change.positive { color: #fff; opacity: 0.7; }
/* Chart area */
.chart-area {
flex: 1;
background: #111;
border: 1px solid #222;
padding: 24px;
display: flex;
flex-direction: column;
}
.chart-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.chart-label {
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 1.5px;
color: #555;
}
.chart-tabs {
display: flex;
gap: 2px;
}
.chart-tab {
font-size: 10px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 1px;
color: #444;
padding: 4px 12px;
background: transparent;
border: 1px solid #333;
}
.chart-tab.active {
color: #fff;
background: #E63946;
border-color: #E63946;
}
.chart-bars {
display: flex;
align-items: flex-end;
gap: 6px;
flex: 1;
padding-top: 12px;
}
.chart-bar-group {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.chart-bar {
width: 100%;
background: #222;
position: relative;
}
.chart-bar.accent { background: #E63946; }
.chart-bar-label {
font-size: 9px;
color: #444;
font-weight: 500;
letter-spacing: 0.5px;
}
/* Bottom row */
.dash-bottom {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.data-table {
background: #111;
border: 1px solid #222;
padding: 16px;
}
.data-table-title {
font-size: 10px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 1.5px;
color: #555;
margin-bottom: 12px;
}
.data-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid #1a1a1a;
}
.data-row:last-child { border-bottom: none; }
.data-row-label {
font-size: 12px;
color: #888;
}
.data-row-value {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 12px;
font-weight: 600;
color: #fff;
}
.data-row-value.red { color: #E63946; }
/* TRUST BAR */
.trust-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
border-top: 1px solid #e0e0e0;
padding: 0 80px;
height: 56px;
display: flex;
align-items: center;
gap: 48px;
background: #fff;
}
.trust-label {
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 2px;
color: #aaa;
white-space: nowrap;
}
.trust-logos {
display: flex;
gap: 48px;
align-items: center;
}
.trust-logo {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 15px;
font-weight: 600;
color: #bbb;
letter-spacing: 1px;
}
</style>
</head>
<body>
<!-- NAVIGATION -->
<nav>
<div class="nav-logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2"/>
<line x1="12" y1="22" x2="12" y2="15.5"/>
<polyline points="22 8.5 12 15.5 2 8.5"/>
</svg>
Meridian
</div>
<ul class="nav-links">
<li><a href="#">Product</a></li>
<li><a href="#">Pricing</a></li>
<li><a href="#">Docs</a></li>
<li><a href="#">Blog</a></li>
</ul>
<a href="#" class="nav-signin">Sign In</a>
</nav>
<!-- HERO -->
<div class="hero">
<!-- LEFT -->
<div class="hero-left">
<div class="hero-label">Business Intelligence for Modern Teams</div>
<h1 class="hero-headline">Turn data into <span>decisions,</span> not dashboards</h1>
<p class="hero-subtitle">AI-powered analytics that tells you what matters, when it matters. Stop drowning in charts and start acting on real insights.</p>
<div class="hero-ctas">
<button class="btn-primary">Start Free Trial</button>
<button class="btn-secondary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Watch Demo
</button>
</div>
<div class="big-number">
<div class="big-number-main">3x</div>
<div class="big-number-label">Faster Insights</div>
<div class="big-number-divider"></div>
<div class="metric-small">
<div class="metric-small-value">50%</div>
<div class="metric-small-label">Less Meeting Time</div>
</div>
<div class="big-number-divider"></div>
<div class="metric-small">
<div class="metric-small-value">99.9%</div>
<div class="metric-small-label">Uptime</div>
</div>
</div>
</div>
<!-- RIGHT — DASHBOARD MOCKUP -->
<div class="hero-right">
<div class="dash-header">
<div class="dash-title">Analytics Overview</div>
<div class="dash-live"><div class="dash-live-dot"></div> Live</div>
</div>
<div class="kpi-row">
<div class="kpi-card">
<div class="kpi-label">Revenue</div>
<div class="kpi-value">$2.4M</div>
<div class="kpi-change positive">+12.3%</div>
</div>
<div class="kpi-card">
<div class="kpi-label">Active Users</div>
<div class="kpi-value">84.2K</div>
<div class="kpi-change positive">+8.7%</div>
</div>
<div class="kpi-card">
<div class="kpi-label">Churn Rate</div>
<div class="kpi-value">1.2%</div>
<div class="kpi-change red">-0.3pp</div>
</div>
</div>
<div class="chart-area">
<div class="chart-top">
<div class="chart-label">Monthly Performance</div>
<div class="chart-tabs">
<div class="chart-tab">7D</div>
<div class="chart-tab active">30D</div>
<div class="chart-tab">90D</div>
</div>
</div>
<div class="chart-bars">
<div class="chart-bar-group"><div class="chart-bar" style="height:60px"></div><div class="chart-bar-label">Jan</div></div>
<div class="chart-bar-group"><div class="chart-bar" style="height:80px"></div><div class="chart-bar-label">Feb</div></div>
<div class="chart-bar-group"><div class="chart-bar" style="height:55px"></div><div class="chart-bar-label">Mar</div></div>
<div class="chart-bar-group"><div class="chart-bar accent" style="height:110px"></div><div class="chart-bar-label">Apr</div></div>
<div class="chart-bar-group"><div class="chart-bar" style="height:95px"></div><div class="chart-bar-label">May</div></div>
<div class="chart-bar-group"><div class="chart-bar accent" style="height:130px"></div><div class="chart-bar-label">Jun</div></div>
<div class="chart-bar-group"><div class="chart-bar" style="height:105px"></div><div class="chart-bar-label">Jul</div></div>
<div class="chart-bar-group"><div class="chart-bar accent" style="height:145px"></div><div class="chart-bar-label">Aug</div></div>
<div class="chart-bar-group"><div class="chart-bar" style="height:120px"></div><div class="chart-bar-label">Sep</div></div>
<div class="chart-bar-group"><div class="chart-bar" style="height:100px"></div><div class="chart-bar-label">Oct</div></div>
</div>
</div>
<div class="dash-bottom">
<div class="data-table">
<div class="data-table-title">Top Segments</div>
<div class="data-row"><span class="data-row-label">Enterprise</span><span class="data-row-value">$1.1M</span></div>
<div class="data-row"><span class="data-row-label">Mid-Market</span><span class="data-row-value">$820K</span></div>
<div class="data-row"><span class="data-row-label">SMB</span><span class="data-row-value">$480K</span></div>
</div>
<div class="data-table">
<div class="data-table-title">AI Alerts Today</div>
<div class="data-row"><span class="data-row-label">Revenue spike detected</span><span class="data-row-value red">High</span></div>
<div class="data-row"><span class="data-row-label">Churn risk: Acme Corp</span><span class="data-row-value red">Med</span></div>
<div class="data-row"><span class="data-row-label">Expansion signal: Bolt</span><span class="data-row-value" style="color:#888">Low</span></div>
</div>
</div>
</div>
</div>
<!-- TRUST BAR (absolutely positioned at bottom-left) -->
<div class="trust-bar" style="position:fixed; bottom:0; left:0; width:720px; z-index:10;">
<span class="trust-label">Trusted by</span>
<div class="trust-logos">
<span class="trust-logo">Stripe</span>
<span class="trust-logo">Notion</span>
<span class="trust-logo">Linear</span>
<span class="trust-logo">Vercel</span>
<span class="trust-logo">Figma</span>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
});
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

@@ -0,0 +1,604 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1440">
<title>Meridian — Business Intelligence for Modern Teams</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Noto+Serif+SC:wght@300;400;500;600&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1440px;
height: 900px;
overflow: hidden;
margin: 0;
font-family: 'Inter', sans-serif;
background: #F5F0EB;
color: #3a3a3a;
}
/* NAV */
nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 80px;
height: 72px;
}
.nav-logo {
font-family: 'Noto Serif SC', serif;
font-size: 20px;
font-weight: 500;
letter-spacing: -0.3px;
color: #3a3a3a;
display: flex;
align-items: center;
gap: 10px;
}
.nav-logo-mark {
width: 28px;
height: 28px;
border-radius: 50%;
background: #6B8F71;
display: flex;
align-items: center;
justify-content: center;
}
.nav-logo-mark svg { width: 14px; height: 14px; color: #F5F0EB; }
.nav-center {
display: flex;
gap: 40px;
list-style: none;
}
.nav-center a {
font-size: 14px;
font-weight: 400;
text-decoration: none;
color: #888;
transition: color 0.2s;
}
.nav-center a:hover { color: #3a3a3a; }
.nav-right {
display: flex;
align-items: center;
gap: 20px;
}
.nav-signin {
font-size: 14px;
font-weight: 400;
text-decoration: none;
color: #888;
}
.nav-cta {
font-family: 'Inter', sans-serif;
font-size: 13px;
font-weight: 500;
padding: 9px 22px;
background: #2D3436;
color: #F5F0EB;
border: none;
border-radius: 100px;
cursor: pointer;
transition: background 0.2s;
}
.nav-cta:hover { background: #3D4547; }
/* HERO */
.hero {
padding: 20px 80px 0 80px;
height: calc(900px - 72px);
display: flex;
flex-direction: column;
}
/* TOP SECTION: text + dashboard side by side */
.hero-top {
display: grid;
grid-template-columns: 500px 1fr;
gap: 60px;
flex: 1;
}
/* LEFT TEXT */
.hero-text {
padding-top: 32px;
display: flex;
flex-direction: column;
}
.hero-label {
font-size: 12px;
font-weight: 500;
color: #6B8F71;
letter-spacing: 1px;
margin-bottom: 20px;
}
.hero-headline {
font-family: 'Noto Serif SC', serif;
font-size: 44px;
font-weight: 400;
line-height: 1.25;
letter-spacing: -0.5px;
color: #2a2a2a;
margin-bottom: 16px;
}
.hero-headline em {
font-style: italic;
color: #7A8F71;
}
.hero-subtitle {
font-size: 16px;
font-weight: 300;
line-height: 1.7;
color: #999;
margin-bottom: 32px;
max-width: 400px;
}
.hero-ctas {
display: flex;
gap: 12px;
margin-bottom: 36px;
}
.btn-primary {
font-family: 'Inter', sans-serif;
font-size: 14px;
font-weight: 500;
padding: 14px 28px;
background: rgba(107, 143, 113, 0.12);
color: #6B8F71;
border: 1px solid rgba(107, 143, 113, 0.3);
border-radius: 100px;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary:hover { background: rgba(107, 143, 113, 0.18); }
.btn-secondary {
font-family: 'Inter', sans-serif;
font-size: 14px;
font-weight: 400;
padding: 14px 28px;
background: transparent;
color: #888;
border: 1px solid #d5cfc5;
border-radius: 100px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
}
.btn-secondary:hover { border-color: #aaa; color: #555; }
.btn-secondary svg { width: 14px; height: 14px; }
/* FLOW DIAGRAM */
.flow-diagram {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 32px;
}
.flow-step {
display: flex;
align-items: center;
gap: 10px;
background: rgba(107, 143, 113, 0.1);
border: 1px solid rgba(107, 143, 113, 0.25);
border-radius: 100px;
padding: 8px 18px;
}
.flow-step-icon {
width: 24px;
height: 24px;
background: #6B8F71;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.flow-step-icon svg { width: 12px; height: 12px; color: #fff; }
.flow-step span {
font-size: 12px;
font-weight: 500;
color: #666;
}
.flow-arrow {
width: 20px;
display: flex;
align-items: center;
justify-content: center;
color: #ccc;
}
.flow-arrow svg { width: 16px; height: 16px; }
/* METRICS ROW */
.metrics-row {
display: flex;
gap: 40px;
}
.metric-card {
background: #fff;
border-radius: 16px;
padding: 20px 24px;
min-width: 130px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
}
.metric-card-value {
font-family: 'Noto Serif SC', serif;
font-size: 28px;
font-weight: 400;
color: #2a2a2a;
letter-spacing: -0.5px;
margin-bottom: 4px;
}
.metric-card-value span { color: #6B8F71; }
.metric-card-label {
font-size: 11px;
font-weight: 400;
color: #bbb;
}
/* RIGHT — DASHBOARD */
.hero-dashboard {
padding-top: 8px;
}
.dashboard-frame {
background: #FFFFFF;
border-radius: 24px;
box-shadow:
0 1px 2px rgba(0,0,0,0.02),
0 4px 12px rgba(0,0,0,0.03),
0 16px 48px rgba(0,0,0,0.05);
padding: 24px;
height: 480px;
display: flex;
flex-direction: column;
}
/* Dash header */
.dash-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.dash-head-title {
font-size: 14px;
font-weight: 500;
color: #3a3a3a;
}
.dash-head-tag {
font-size: 11px;
font-weight: 400;
color: #6B8F71;
background: rgba(107, 143, 113, 0.1);
padding: 4px 12px;
border-radius: 100px;
}
/* KPI row */
.dash-kpis {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.dash-kpi {
background: #FAFAF6;
border-radius: 14px;
padding: 16px;
text-align: center;
}
.dash-kpi-value {
font-size: 22px;
font-weight: 500;
color: #2a2a2a;
margin-bottom: 2px;
}
.dash-kpi-label {
font-size: 10px;
font-weight: 400;
color: #bbb;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.dash-kpi-change {
font-size: 11px;
font-weight: 500;
color: #6B8F71;
margin-top: 4px;
}
/* Chart area */
.dash-chart {
flex: 1;
display: grid;
grid-template-columns: 2fr 1fr;
gap: 12px;
}
.chart-main {
background: #FAFAF6;
border-radius: 16px;
padding: 20px;
display: flex;
flex-direction: column;
}
.chart-main-label {
font-size: 11px;
font-weight: 500;
color: #aaa;
margin-bottom: 12px;
}
.chart-main-svg {
flex: 1;
}
/* Side panel */
.chart-side {
display: flex;
flex-direction: column;
gap: 10px;
}
.insight-bubble {
background: #FAFAF6;
border-radius: 16px;
padding: 14px 16px;
flex: 1;
}
.insight-bubble-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
}
.insight-bubble-icon {
width: 20px;
height: 20px;
background: rgba(107, 143, 113, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.insight-bubble-icon svg { width: 10px; height: 10px; color: #7A8F71; }
.insight-bubble-tag {
font-size: 10px;
font-weight: 500;
color: #6B8F71;
}
.insight-bubble-text {
font-size: 11px;
font-weight: 400;
color: #888;
line-height: 1.5;
}
/* TRUST BAR */
.trust-bar {
padding: 16px 0;
display: flex;
align-items: center;
gap: 40px;
border-top: 1px solid #e8e2d8;
}
.trust-label {
font-size: 11px;
font-weight: 400;
color: #ccc;
white-space: nowrap;
}
.trust-logos {
display: flex;
gap: 36px;
align-items: center;
}
.trust-logo {
font-size: 14px;
font-weight: 400;
color: #ccc;
}
</style>
</head>
<body>
<!-- NAV -->
<nav>
<div class="nav-logo">
<div class="nav-logo-mark">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2"/>
</svg>
</div>
Meridian
</div>
<ul class="nav-center">
<li><a href="#">Product</a></li>
<li><a href="#">Pricing</a></li>
<li><a href="#">Docs</a></li>
<li><a href="#">Blog</a></li>
</ul>
<div class="nav-right">
<a href="#" class="nav-signin">Sign In</a>
<button class="nav-cta">Start Free Trial</button>
</div>
</nav>
<!-- HERO -->
<div class="hero">
<div class="hero-top">
<!-- LEFT TEXT -->
<div class="hero-text">
<div class="hero-label">Business Intelligence for Modern Teams</div>
<h1 class="hero-headline">Turn data into <em>decisions,</em> not dashboards</h1>
<p class="hero-subtitle">AI-powered analytics that tells you what matters, when it matters. Clarity over complexity.</p>
<div class="hero-ctas">
<button class="btn-primary">Start Free Trial</button>
<button class="btn-secondary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Watch Demo
</button>
</div>
<!-- Flow diagram -->
<div class="flow-diagram">
<div class="flow-step">
<div class="flow-step-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
</div>
<span>Raw Data</span>
</div>
<div class="flow-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</div>
<div class="flow-step">
<div class="flow-step-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
</div>
<span>AI Analysis</span>
</div>
<div class="flow-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</div>
<div class="flow-step">
<div class="flow-step-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<span>Actionable Insight</span>
</div>
</div>
<!-- Metrics -->
<div class="metrics-row">
<div class="metric-card">
<div class="metric-card-value">3<span>x</span></div>
<div class="metric-card-label">Faster insights</div>
</div>
<div class="metric-card">
<div class="metric-card-value">50<span>%</span></div>
<div class="metric-card-label">Less meeting time</div>
</div>
<div class="metric-card">
<div class="metric-card-value">99.9<span>%</span></div>
<div class="metric-card-label">Uptime</div>
</div>
</div>
</div>
<!-- RIGHT — DASHBOARD -->
<div class="hero-dashboard">
<div class="dashboard-frame">
<div class="dash-head">
<div class="dash-head-title">Analytics Overview</div>
<div class="dash-head-tag">AI-Enhanced</div>
</div>
<div class="dash-kpis">
<div class="dash-kpi">
<div class="dash-kpi-value">$2.4M</div>
<div class="dash-kpi-label">Revenue</div>
<div class="dash-kpi-change">+12.3%</div>
</div>
<div class="dash-kpi">
<div class="dash-kpi-value">84.2K</div>
<div class="dash-kpi-label">Active Users</div>
<div class="dash-kpi-change">+8.7%</div>
</div>
<div class="dash-kpi">
<div class="dash-kpi-value">1.2%</div>
<div class="dash-kpi-label">Churn Rate</div>
<div class="dash-kpi-change">-0.3pp</div>
</div>
</div>
<div class="dash-chart">
<!-- Main chart with organic shapes -->
<div class="chart-main">
<div class="chart-main-label">Revenue Trend</div>
<svg class="chart-main-svg" viewBox="0 0 400 160" preserveAspectRatio="xMidYMid meet">
<!-- Soft grid -->
<line x1="0" y1="40" x2="400" y2="40" stroke="#ece7dd" stroke-width="1"/>
<line x1="0" y1="80" x2="400" y2="80" stroke="#ece7dd" stroke-width="1"/>
<line x1="0" y1="120" x2="400" y2="120" stroke="#ece7dd" stroke-width="1"/>
<!-- Rounded bars -->
<rect x="15" y="80" width="28" height="70" rx="8" ry="8" fill="#e2ddd4"/>
<rect x="58" y="65" width="28" height="85" rx="8" ry="8" fill="#e2ddd4"/>
<rect x="101" y="90" width="28" height="60" rx="8" ry="8" fill="#e2ddd4"/>
<rect x="144" y="50" width="28" height="100" rx="8" ry="8" fill="#6B8F71" opacity="0.6"/>
<rect x="187" y="60" width="28" height="90" rx="8" ry="8" fill="#e2ddd4"/>
<rect x="230" y="35" width="28" height="115" rx="8" ry="8" fill="#6B8F71" opacity="0.8"/>
<rect x="273" y="45" width="28" height="105" rx="8" ry="8" fill="#e2ddd4"/>
<rect x="316" y="25" width="28" height="125" rx="8" ry="8" fill="#6B8F71"/>
<rect x="359" y="40" width="28" height="110" rx="8" ry="8" fill="#e2ddd4"/>
<!-- Smooth trend line overlay -->
<path d="M29,75 C60,62 75,60 115,85 C140,70 155,47 172,45 C200,55 205,55 244,30 C270,40 280,40 330,20 C350,35 365,35 373,35" fill="none" stroke="#7A8F71" stroke-width="2" stroke-linecap="round" opacity="0.7"/>
<!-- Labels -->
<text x="22" y="158" font-size="9" fill="#bbb" font-family="Inter" text-anchor="middle">Jan</text>
<text x="72" y="158" font-size="9" fill="#bbb" font-family="Inter" text-anchor="middle">Feb</text>
<text x="115" y="158" font-size="9" fill="#bbb" font-family="Inter" text-anchor="middle">Mar</text>
<text x="158" y="158" font-size="9" fill="#bbb" font-family="Inter" text-anchor="middle">Apr</text>
<text x="201" y="158" font-size="9" fill="#bbb" font-family="Inter" text-anchor="middle">May</text>
<text x="244" y="158" font-size="9" fill="#bbb" font-family="Inter" text-anchor="middle">Jun</text>
<text x="287" y="158" font-size="9" fill="#bbb" font-family="Inter" text-anchor="middle">Jul</text>
<text x="330" y="158" font-size="9" fill="#bbb" font-family="Inter" text-anchor="middle">Aug</text>
<text x="373" y="158" font-size="9" fill="#bbb" font-family="Inter" text-anchor="middle">Sep</text>
</svg>
</div>
<!-- Side insights -->
<div class="chart-side">
<div class="insight-bubble">
<div class="insight-bubble-header">
<div class="insight-bubble-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
</div>
<span class="insight-bubble-tag">AI Insight</span>
</div>
<div class="insight-bubble-text">Enterprise segment grew 23% this quarter. Four new accounts are driving acceleration.</div>
</div>
<div class="insight-bubble">
<div class="insight-bubble-header">
<div class="insight-bubble-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
</div>
<span class="insight-bubble-tag">Prediction</span>
</div>
<div class="insight-bubble-text">89% likelihood of hitting Q3 revenue target based on current pipeline velocity.</div>
</div>
<div class="insight-bubble">
<div class="insight-bubble-header">
<div class="insight-bubble-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
</div>
<span class="insight-bubble-tag">Alert</span>
</div>
<div class="insight-bubble-text">Churn risk detected for 2 mid-market accounts. Recommend outreach this week.</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- TRUST BAR -->
<div class="trust-bar">
<span class="trust-label">Trusted by teams at</span>
<div class="trust-logos">
<span class="trust-logo">Stripe</span>
<span class="trust-logo">Notion</span>
<span class="trust-logo">Linear</span>
<span class="trust-logo">Vercel</span>
<span class="trust-logo">Figma</span>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
});
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

+245
View File
@@ -0,0 +1,245 @@
# Animation Pitfalls:HTML 动画踩过的坑与规则
做动画时最常踩的 bug 和如何避免。每条规则都来自真实失败案例。
写动画之前读完这篇,能省一轮迭代。
## 1. 叠层布局 —— `position: relative` 是默认义务
**踩的坑**:一个 sentence-wrap 元素包了 3 个 bracket-layer(`position: absolute`)。没给 sentence-wrap 设 `position: relative`,结果 absolute 的 bracket 以 `.canvas` 为坐标系,飘到屏幕底部 200px 外。
**规则**:
- 任何包含 `position: absolute` 子元素的容器,**必须**显式 `position: relative`
- 即使视觉上不需要「偏移」,也要写 `position: relative` 作为坐标系锚点
- 如果你在写 `.parent { ... }`,其子元素里有 `.child { position: absolute }`,下意识给 parent 加 relative
**快速检查**:每出现一个 `position: absolute`,往上数 ancestor,确保最近的 positioned 祖先是你*想要的*坐标系。
## 2. 字符陷阱 —— 不依赖稀有 Unicode
**踩的坑**:想用 `␣` (U+2423 OPEN BOX) 可视化「空格 token」。Noto Serif SC / Cormorant Garamond 都没这个字形,渲染为空白/豆腐,观众完全看不到。
**规则**:
- **动画里出现的每个字符,都必须在你选定的字体里存在**
- 常见稀有字符黑名单:`␣ ␀ ␐ ␋ ␨ ↩ ⏎ ⌘ ⌥ ⌃ ⇧ ␦ ␖ ␛`
- 要表达「空格 / 回车 / 制表符」这类元字符,用 **CSS 构造的语义盒子**:
```html
<span class="space-key">Space</span>
```
```css
.space-key {
display: inline-flex;
padding: 4px 14px;
border: 1.5px solid var(--accent);
border-radius: 4px;
font-family: monospace;
font-size: 0.3em;
letter-spacing: 0.2em;
text-transform: uppercase;
}
```
- Emoji 也要验证:某些 emoji 在 Noto Emoji 以外字体会 fallback 成灰色方框,最好用 `emoji` font-family 或 SVG
## 3. 数据驱动的 Grid/Flex 模板
**踩的坑**:代码里 `const N = 6` 个 tokens,但 CSS 写死 `grid-template-columns: 80px repeat(5, 1fr)`。结果第 6 个 token 没有 column,整个矩阵错位。
**规则**:
- 当 count 从 JS 数组来(`TOKENS.length`),CSS 模板也应该数据驱动
- 方案 A:用 CSS 变量从 JS 注入
```js
el.style.setProperty('--cols', N);
```
```css
.grid { grid-template-columns: 80px repeat(var(--cols), 1fr); }
```
- 方案 B:用 `grid-auto-flow: column` 让浏览器自动扩展
- **禁用「固定数字 + JS 常量」的组合**,N 改了 CSS 不会同步更新
## 4. 过渡断层 —— 场景切换要连续
**踩的坑**:zoom1 (13-19s) → zoom2 (19.2-23s) 之间,主句子已经 hidden,zoom1 fade out(0.6s)+ zoom2 fade in(0.6s)+ stagger delay(0.2s+)= 约 1 秒纯空白画面。观众以为动画卡住了。
**规则**:
- 连续切换场景时,fade out 和 fade in 要**交叉重叠**,不是前一个完全消失再开始下一个
```js
// 差:
if (t >= 19) hideZoom('zoom1'); // 19.0s out
if (t >= 19.4) showZoom('zoom2'); // 19.4s in → 中间 0.4s 空白
// 好:
if (t >= 18.6) hideZoom('zoom1'); // 提前 0.4s 开始 fade out
if (t >= 18.6) showZoom('zoom2'); // 同时 fade in(cross-fade)
```
- 或者用一个「锚点元素」(如主句子)作为场景之间的视觉连接,zoom 切换期间它短暂回显
- 配 CSS transition 的 duration 算清楚,避免 transition 还没结束就触发下一个
## 5. Pure Render 原则 —— 动画状态应可 seek
**踩的坑**:用 `setTimeout` + `fireOnce(key, fn)` 链式触发动画状态。正常播放没问题,但做逐帧录制/seek到任意时间点时,之前的 setTimeout 已经执行过就无法「回到过去」。
**规则**:
- `render(t)` 函数理想上是 **pure function**:给定 t 输出唯一 DOM 状态
- 如果必须用副作用(如 class 切换),用 `fired` set 配合显式 reset:
```js
const fired = new Set();
function fireOnce(key, fn) { if (!fired.has(key)) { fired.add(key); fn(); } }
function reset() { fired.clear(); /* 清所有 .show class */ }
```
- 暴露 `window.__seek(t)` 供 Playwright / 调试用:
```js
window.__seek = (t) => { reset(); render(t); };
```
- 动画相关的 setTimeout 不要跨越 >1 秒,否则 seek 回跳时会乱套
## 6. 字体加载前测量 = 测错
**踩的坑**:页面一 DOMContentLoaded 就调用 `charRect(idx)` 测量 bracket 位置,字体还没加载,每个字符宽度是 fallback 字体的宽度,位置全错。等字体一加载(约 500ms 后),bracket 的 `left: Xpx` 还是老值,永久偏移。
**规则**:
- 任何依赖 DOM 测量(`getBoundingClientRect`、`offsetWidth`)的布局代码,**必须**包在 `document.fonts.ready.then()` 里
```js
document.fonts.ready.then(() => {
requestAnimationFrame(() => {
buildBrackets(...); // 此时字体已就绪,测量准确
tick(); // 动画开始
});
});
```
- 额外的 `requestAnimationFrame` 给浏览器一帧时间提交 layout
- 如果用 Google Fonts CDN,`<link rel="preconnect">` 加速首次加载
## 7. 录制准备 —— 为视频导出预留抓手
**踩的坑**:Playwright `recordVideo` 默认 25fps,从 context 创建就开始录。页面加载、字体加载的前 2 秒都被录进去。交付时视频前面 2 秒空白/闪白。
**规则**:
- 提供 `render-video.js` 工具处理:warmup navigate → reload 重启动画 → 等 duration → ffmpeg trim head + 转 H.264 MP4
- 动画的**第 0 帧**要是最终布局已就位的完整初始状态(不是空白或加载中)
- 想要 60fps?用 ffmpeg `minterpolate` 后处理,不指望浏览器源帧率
- 想要 GIF?两阶段 palette(`palettegen` + `paletteuse`),对 30s 1080p 动画能压到 3MB
参见 `video-export.md` 获取完整脚本调用方式。
## 8. 批量导出 —— tmp 目录必须带 PID 防并发冲突
**踩的坑**:用 `render-video.js` 3 个进程并行录 3 个 HTML。因为 TMP_DIR 只用 `Date.now()` 命名,3 个进程同毫秒启动时共用同一个 tmp 目录。最先完成的进程清理 tmp,另外两个读目录时 `ENOENT`,全部崩溃。
**规则**:
- 任何多进程可能共用的临时目录,命名必须带 **PID 或随机后缀**:
```js
const TMP_DIR = path.join(DIR, '.video-tmp-' + Date.now() + '-' + process.pid);
```
- 如果确实想多文件并行,用 shell 的 `&` + `wait` 而不是在一个 node 脚本里 fork
- 批量录多个 HTML 时,保守做法:**串行**运行(2 个以内可并行,3 个以上老实排队)
## 9. 录屏里有进度条/重播按钮 —— Chrome 元素污染视频
**踩的坑**:动画 HTML 加了 `.progress` 进度条、`.replay` 重播按钮、`.counter` 时间戳,方便人类调试播放。录成 MP4 交付时这些元素出现在视频底部,像把开发者工具截进去了一样。
**规则**:
- HTML 里给人类用的「chrome 元素」(progress bar / replay button / footer / masthead / counter / phase labels)和视频内容本体分开管理
- **约定 class 名** `.no-record`:任何带这个 class 的元素,录屏脚本自动隐藏
- 脚本端(`render-video.js`)默认注入 CSS 隐藏常见 chrome class 名:
```
.progress .counter .phases .replay .masthead .footer .no-record [data-role="chrome"]
```
- 用 Playwright 的 `addInitScript` 注入(会在每次 navigate 前生效,reload 也稳)
- 想看原样 HTML(带 chrome)时加 `--keep-chrome` flag
## 10. 录屏开头几秒动画重复 —— Warmup 帧泄漏
**踩的坑**:`render-video.js` 的旧流程 `goto → wait fonts 1.5s → reload → wait duration`。录制从 context 创建就开始,warmup 阶段动画已经播了一段,reload 后从 0 重启。结果视频前几秒是「动画中段 + 切换 + 动画从 0 开始」,重复感强。
**规则**:
- **Warmup 和 Record 必须用独立的 context**:
- Warmup context(无 `recordVideo` 选项):只负责 load url、等字体、然后 close
- Record context(有 `recordVideo`):fresh 状态开始,animation 从 t=0 开始录
- ffmpeg `-ss trim` 只能裁 Playwright 的一点点 startup latency(~0.3s),**不能**用来掩盖 warmup 帧;源头要干净
- 录制 context 关闭 = webm 文件写入磁盘,这是 Playwright 的约束
- 相关代码模式:
```js
// Phase 1: warmup (throwaway)
const warmupCtx = await browser.newContext({ viewport });
const warmupPage = await warmupCtx.newPage();
await warmupPage.goto(url, { waitUntil: 'networkidle' });
await warmupPage.waitForTimeout(1200);
await warmupCtx.close();
// Phase 2: record (fresh)
const recordCtx = await browser.newContext({ viewport, recordVideo });
const page = await recordCtx.newPage();
await page.goto(url, { waitUntil: 'networkidle' });
await page.waitForTimeout(DURATION * 1000);
await page.close();
await recordCtx.close();
```
## 11. 画面内别画「伪 chrome」—— 装饰版 player UI 与真 chrome 撞车
**踩的坑**:动画用 `Stage` 组件,已经自带 scrubber + 时间码 + 暂停按钮(属于 `.no-record` chrome,导出时自动隐藏)。我又在画面底部画了一条「`00:60 ──── CLAUDE-DESIGN / ANATOMY`」的"杂志页码感装饰进度条",自我感觉良好。**结果**:用户看到两条进度条——一条是 Stage 控制器,一条是我画的装饰。视觉上完全撞车,认定为 bug。「视频内还有个进度条是怎么回事?」
**规则**:
- Stage 已经提供:scrubber + 时间码 + 暂停/重播按钮。**画面内不要再画**进度指示、当前时间码、版权署名条、章节计数器——它们要么和 chrome 撞车,要么就是 filler slop(违反「earn its place」原则)。
- 「页码感」「杂志感」「底部署名条」这些**装饰诉求**,是 AI 自动加上的高频 filler。每一个出现都要警觉——它真的传达了不可替代的信息吗?还是单纯填满空白?
- 如果你坚信某个底部条带必须存在(例如:动画主题就是讲 player UI),那它必须**叙事必要**,且**视觉上和 Stage scrubber 显著区分**(不同位置、不同形式、不同色调)。
**元素归属测试**(每个画进 canvas 的元素必须能回答):
| 它属于什么 | 处理 |
|------------|------|
| 某一幕的叙事内容 | OK,留着 |
| 全局 chrome(控制/调试用) | 加 `.no-record` class,导出时隐藏 |
| **既不属于任何幕,又不是 chrome** | **删**。这就是无主之物,必然是 filler slop |
**自检(交付前 3 秒)**:截一张静态图,问自己——
- 画面里有没有「看起来像 video player UI 的东西」(横线进度条、时间码、控制按钮模样)?
- 如果有,删掉它叙事是否有损?无损就删。
- 同一类信息(进度/时间/署名)有没有出现两次?合并到 chrome 一处。
**反例**:底部画 `00:42 ──── PROJECT NAME`、画面右下角画"CH 03 / 06"章节计数、画面边缘画版本号"v0.3.1"——都是伪 chrome filler。
## 12. 录屏前置空白 —— 用 `window.__ready` 同步动画 t=0
**踩的坑**:60 秒动画导出 MP4,前 2-3 秒是空白页面。`ffmpeg --trim=0.3` 剪不掉。
**根因**:Playwright `recordVideo` 从 `newContext()` 那一刻就开始写 WebM。但此时 Babel Standalone 还在编译 inline JSX、React 还没 mount、`document.fonts.ready.then(root.render)` 还没触发。`page.goto(url, { waitUntil: 'networkidle' })` 只等网络空闲,检测不到 JS 执行阶段——所以 WebM 前 1.5-3s 是空白页。
**规则**:
1. **动画代码**在 tick 第一帧发 `window.__ready = true`,和动画 t=0 同步:
```js
function tick(now) {
if (last === null) {
last = now;
window.__ready = true; // 必须在这里,不是 useEffect
}
// ... 动画推进
}
```
为什么同步?如果 `__ready` 在 tick 之前设(如 `useEffect` 或 rAF 排队),触发时 WebM 光标还在空白页;如果在 tick 之后设(rAF 嵌套),则动画已经跑了几帧被 trim 掉。**pair 起来**才是对的。
2. **录屏脚本**`page.goto` 之后 `waitForFunction(() => window.__ready === true)`,记录此时相对 WebM 起点的秒数作为 ffmpeg trim 偏移。完全不靠猜。
3. **字体等待**放在 tick 启动条件里:`document.fonts.ready.then(() => rAF(tick))`。这样 tick 第一帧就是字体已就绪的画面——`__ready` 信号 = WebM 捕到的第一个"用户会看到"的动画帧。
**不这么做的代价**:固定 trim 靠猜。机器快慢、字体缓存状态、网速每次不同——某台机器上的 3s trim 换到另一台可能不够或截掉开头。动态测量从根本解决。
**参考实现**:`assets/animations.jsx` 的 Stage 组件已内置。`scripts/render-video.js` 已内置 auto-trim 逻辑。非 animations.jsx 的手写 HTML,自行在 tick/渲染循环的第一帧设信号。
**验证方法**:导出后 `ffmpeg -i video.mp4 -ss 0 -vframes 1 frame-0.png`,检查第一帧是动画应有的初始状态(不是动画中段、不是黑屏)。
## 快速自查清单(开工前 5 秒)
- [ ] 每个 `position: absolute` 的父元素都有 `position: relative`?
- [ ] 动画里的特殊字符(`␣` `⌘` `emoji`)都在字体里存在?
- [ ] Grid/Flex 模板的 count 和 JS 数据的 length 一致?
- [ ] 场景切换之间有 cross-fade,没有 >0.3s 的纯空白?
- [ ] DOM 测量代码包在 `document.fonts.ready.then()` 里?
- [ ] `render(t)` 是 pure 的,或有明确的 reset 机制?
- [ ] 第 0 帧是完整初始状态,不是空白?
- [ ] 画面内没有「伪 chrome」装饰(进度条/时间码/底部署名条与 Stage scrubber 撞车)?
- [ ] 动画 tick 第一帧同步设 `window.__ready = true`?(用 animations.jsx 自带;手写 HTML 自己加)
- [ ] 导出后抽第 0 帧验证是动画初始状态?
+246
View File
@@ -0,0 +1,246 @@
# Animations:时间轴动画引擎
做动画/motion design HTML时读这个。原理、用法、典型模式。
## 核心模式:Stage + Sprite
我们的动画系统(`assets/animations.jsx`)提供一个时间轴驱动的引擎:
- **`<Stage>`**:整个动画的容器,自动提供auto-scale(fit viewport)+ scrubber + play/pause/loop控制
- **`<Sprite start end>`**:时间片段。一个Sprite只在`start`到`end`这段时间内显示。内部可以通过`useSprite()` hook读取自己的本地进度`t` (0→1)
- **`useTime()`**:读当前全局时间(秒)
- **`Easing.easeInOut` / `Easing.easeOut` / ...**:缓动函数
- **`interpolate(t, from, to, easing?)`**:根据t插值
这套模式借鉴Remotion/After Effects思路,但轻量、零依赖。
## 起手
```html
<script type="text/babel" src="animations.jsx"></script>
<script type="text/babel">
const { Stage, Sprite, useTime, useSprite, Easing, interpolate } = window.Animations;
function Title() {
const { t } = useSprite(); // 本地进度 0→1
const opacity = interpolate(t, [0, 1], [0, 1], Easing.easeOut);
const y = interpolate(t, [0, 1], [40, 0], Easing.easeOut);
return (
<h1 style={{
opacity,
transform: `translateY(${y}px)`,
fontSize: 120,
fontWeight: 900,
}}>
Hello.
</h1>
);
}
function Scene() {
return (
<Stage duration={10}> {/* 10秒动画 */}
<Sprite start={0} end={3}>
<Title />
</Sprite>
<Sprite start={2} end={5}>
<SubTitle />
</Sprite>
{/* ... */}
</Stage>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Scene />);
</script>
```
## 常用动画模式
### 1. Fade In / Fade Out
```jsx
function FadeIn({ children }) {
const { t } = useSprite();
const opacity = interpolate(t, [0, 0.3], [0, 1], Easing.easeOut);
return <div style={{ opacity }}>{children}</div>;
}
```
**注意范围**:`[0, 0.3]`意思是在sprite的前30%时间完成渐入,后面保持opacity=1。
### 2. Slide In
```jsx
function SlideIn({ children, from = 'left' }) {
const { t } = useSprite();
const progress = interpolate(t, [0, 0.4], [0, 1], Easing.easeOut);
const offset = (1 - progress) * 100;
const directions = {
left: `translateX(-${offset}px)`,
right: `translateX(${offset}px)`,
top: `translateY(-${offset}px)`,
bottom: `translateY(${offset}px)`,
};
return (
<div style={{
transform: directions[from],
opacity: progress,
}}>
{children}
</div>
);
}
```
### 3. 逐字打字机
```jsx
function Typewriter({ text }) {
const { t } = useSprite();
const charCount = Math.floor(text.length * Math.min(t * 2, 1));
return <span>{text.slice(0, charCount)}</span>;
}
```
### 4. 数字计数
```jsx
function CountUp({ from = 0, to = 100, duration = 0.6 }) {
const { t } = useSprite();
const progress = interpolate(t, [0, duration], [0, 1], Easing.easeOut);
const value = Math.floor(from + (to - from) * progress);
return <span>{value.toLocaleString()}</span>;
}
```
### 5. 分段解释(典型教学动画)
```jsx
function Scene() {
return (
<Stage duration={20}>
{/* Phase 1: 展示问题 */}
<Sprite start={0} end={4}>
<Problem />
</Sprite>
{/* Phase 2: 展示思路 */}
<Sprite start={4} end={10}>
<Approach />
</Sprite>
{/* Phase 3: 展示结果 */}
<Sprite start={10} end={16}>
<Result />
</Sprite>
{/* 全程显示的字幕 */}
<Sprite start={0} end={20}>
<Caption />
</Sprite>
</Stage>
);
}
```
## Easing函数
预设的easing curves:
| Easing | 特性 | 用在 |
|--------|------|------|
| `linear` | 匀速 | 滚动字幕、持续动画 |
| `easeIn` | 慢→快 | 退场消失 |
| `easeOut` | 快→慢 | 入场出现(最常用) |
| `easeInOut` | 慢→快→慢 | 位置变化 |
| `spring` | 弹簧 | 交互反馈 |
| `anticipation` | 先反向再正向 | 强调动作 |
**入场用easeOut,出场用easeIn**——这是动画的基础规律。
## 节奏和时长指南
### 微交互(0.1-0.3秒)
- 按钮hover
- 卡片expand
- Tooltip出现
### UI过渡(0.3-0.8秒)
- 页面切换
- 模态框出现
- 列表item加入
### 叙事动画(2-10秒每段)
- 概念解释的一个phase
- 数据图表的reveal
- 场景转换
### 单段叙事动画最长不超过10秒
人类注意力有限。10秒讲一件事,讲完换下一件。
## 设计动画的思考顺序
### 1. 先有内容/故事,再有动画
**错误**:先想要做fancy动画,再塞内容进去
**正确**:先想清楚要传达什么信息,再用动画手段serve这个信息
动画是**signal**,不是**装饰**。一个fade-in强调的是"这里很重要,请看"——如果什么都fade-in,signal就失效。
### 2. 分Scene写时间轴
```
0:00 - 0:03 问题出现(fade in)
0:03 - 0:06 问题放大/展开(zoom+pan)
0:06 - 0:09 解法出现(slide in from right)
0:09 - 0:12 解法展开说明(typewriter)
0:12 - 0:15 结果演示(counter up + chart reveal)
0:15 - 0:18 总结一句话(static,读3秒)
0:18 - 0:20 CTA或fade out
```
写完时间轴再写组件。
### 3. 资源先行
动画要用的图片/图标/字体**先**准备好。不要画到一半去找素材——打断节奏。
## 常见问题
**动画卡顿**
→ 主要是layout thrashing。用`transform`和`opacity`,不要动`top`/`left`/`width`/`height`/`margin`。浏览器GPU加速`transform`。
**动画太快,看不清楚**
→ 人读一个汉字需要100-150ms,一个词300-500ms。如果你用文字讲故事,单句至少留3秒。
**动画太慢,观众无聊**
→ 有趣的视觉变化要密集。静态画面超过5秒就会闷。
**多个动画互相影响**
→ 用CSS的`will-change: transform`提前告诉浏览器这个元素会动,减少reflow。
**录制成视频**
→ 用 skill 自带工具链(一条命令出三种格式):见 `video-export.md`
- `scripts/render-video.js` — HTML → 25fps MP4(Playwright + ffmpeg)
- `scripts/convert-formats.sh` — 25fps MP4 → 60fps MP4 + 优化 GIF
- 想要更精确的帧渲染?让 render(t) 成为 pure function,见 `animation-pitfalls.md` 第 5 条
## 和视频工具的配合
这个skill做的是**HTML动画**(在浏览器里跑的)。如果最终产出要作为视频素材:
- **短动画/concept demo**:用这里的方法做HTML动画 → 屏幕录制
- **长视频/叙事**:本 skill 专注 HTML 动画,长视频用 AI 视频生成类 skill 或专业视频软件
- **motion graphics**:专业的After Effects/Motion Canvas更合适
## 关于Popmotion等库
如果你真的需要物理动画(spring、decay、keyframes with precise timing),我们的engine搞不定,可以fallback到Popmotion:
```html
<script src="https://unpkg.com/popmotion@11.0.5/dist/popmotion.min.js"></script>
```
但**先试试我们的engine**。90%的情况够用。
+260
View File
@@ -0,0 +1,260 @@
# Content Guidelines:反AI slop、内容准则、Scale规范
AI设计里最容易掉进去的陷阱。这是一份「不做什么」的清单,比「做什么」更重要——因为AI slop是默认值,你不主动避免就会发生。
## AI Slop 完整黑名单
### 视觉陷阱
**❌ 激进渐变背景**
- 紫色 → 粉色 → 蓝色 全屏渐变(AI生成网页的典型味道)
- 任何方向的rainbow gradient
- Mesh gradient铺满背景
- ✅ 如果要用渐变:subtle、单色系、有意图地点缀(比如button hover)
**❌ 圆角卡片 + 左border accent色**
```css
/* 这是AI味卡片的典型签名 */
.card {
border-radius: 12px;
border-left: 4px solid #3b82f6;
padding: 16px;
}
```
这种卡片在AI生成的Dashboard里泛滥。想做强调?用更有设计感的方式:背景色对比、字重/字号对比、plain分隔线、或者干脆不分卡片。
**❌ Emoji 装饰**
除非品牌本身使用emoji(比如Notion、Slack),否则不要在UI上放emoji。**尤其不要**:
- 标题前的 🚀 ⚡️ ✨ 🎯 💡
- Feature列表的 ✅
- CTA按钮里的 →(箭头单独出现OK,emoji箭头不行)
没图标用真icon库(Lucide/Heroicons/Phosphor),或者用placeholder。
**❌ SVG 画 imagery**
不要试图用SVG画:人物、场景、设备、物品、抽象艺术。AI画的SVG imagery一眼就是AI味,幼稚且廉价。**一个灰色矩形+"插画位 1200×800"的文字标签,比一个拙劣的SVG hero illustration强100倍**。
唯一可以用SVG的场景:
- 真正的icon(16×16到32×32级别)
- 几何图形做装饰元素
- Data viz的chart
**❌ 过多iconography**
不是每个标题/feature/section都需要icon。滥用icon会让界面像toy。Less is more。
**❌ "Data slop"**
编造的stats装饰:
- "10,000+ happy customers" (你都不知道有没有)
- "99.9% uptime" (没有真数据就别写)
- 用图标+数字+词组成的装饰"metric cards"
- Mock table里的假数据装点得花里胡哨
如果没真数据,留placeholder或问用户要。
**❌ "Quote slop"**
编造的用户评价、名人名言装饰页面。留placeholder问用户要真quote。
### 字体陷阱
**❌ 避免这些烂大街字体**:
- Inter(AI生成的网页默认)
- Roboto
- Arial / Helvetica
- 纯system font stack
- Fraunces(AI发现了这个就用滥了)
- Space Grotesk(最近AI的最爱)
**✅ 用有特点的display+body配对**。灵感方向:
- 衬线display + 无衬线body(editorial feel)
- Mono display + sans body(technical feel)
- Heavy display + light body(contrast)
- Variable font做hero的粗细动画
字体资源:
- Google Fonts的冷门好选项(Instrument Serif、Cormorant、Bricolage Grotesque、JetBrains Mono)
- 开源字体站(Fraunces的兄弟字体、Adobe Fonts)
- 不要凭空发明字体名
### 色彩陷阱
**❌ 凭空发明颜色**
不要从头设计一整套不熟悉的色彩。这通常不和谐。
**✅ 策略**:
1. 有品牌色 → 用品牌色,缺的color token用oklch插值
2. 没有品牌色但有参考 → 从参考产品截图吸色
3. 完全从零 → 选一个known的配色系统(Radix Colors / Tailwind默认palette / Anthropic brand),不要自己调
**oklch定义色彩**是最现代的做法:
```css
:root {
--primary: oklch(0.65 0.18 25); /* 温暖的terracotta */
--primary-light: oklch(0.85 0.08 25); /* 同色系浅色 */
--primary-dark: oklch(0.45 0.20 25); /* 同色系深色 */
}
```
oklch能保证调整亮度时色相不漂移,比hsl好用。
**❌ 夜间模式随手加反色**
不是简单invert颜色。好的dark mode需要重新调整饱和度、对比度、accent色。不想做dark mode就别做。
### Layout陷阱
**❌ Bento grid 过度泛滥**
每个AI生成的landing page都想搞bento。除非你的信息structure确实适合bento,否则用其他layout。
**❌ 大hero + 3-column features + testimonials + CTA**
这个landing page模板被用烂了。想创新就真创新。
**❌ Card grid里每个card长一样**
Asymmetric、不同大小的cards、有的带image有的只有文字、有的跨列——这才像真设计师做的。
## 内容准则
### 1. Don't add filler content
每个元素都必须earn its place。空白是设计问题,用**构图**解决(对比、节奏、留白),**不是**靠内容填满。
**判断filler的问题**:
- 如果去掉这段内容,设计会变差吗?答案若是"不会",就去掉。
- 这个元素解决了什么真问题?如果是"让页面不那么空",删掉。
- 这个stats/quote/feature有真数据支持吗?没有就不要凭空写。
「One thousand no's for every yes」。
### 2. Ask before adding material
你觉得多加一段/一页/一个section会更好?先问用户,不要单方面加。
原因:
- 用户知道他的受众比你清楚
- 加内容有成本,用户可能不想要
- 单方面加内容违反了"junior designer汇报工作"的关系
### 3. Create a system up front
探索完design context后,**先口头说出你要用的系统**,让用户确认:
```markdown
我的设计系统:
- 色彩:#1A1A1A主体 + #F0EEE6背景 + #D97757 accent(来自你的品牌)
- 字型:Instrument Serif做display + Geist Sans做body
- 节奏:section title用full-bleed彩色背景 + 白字;普通section用白背景
- 图像:hero用full-bleed照片,feature section用placeholder等你提供
- 最多用2种背景色,避免杂乱
确认这个方向我就开始做。
```
用户确认后再动手。这个check-in能避免"做完一半发现方向错"。
## Scale 规范
### 幻灯片(1920×1080)
- 正文最小 **24px**,理想 28-36px
- 标题 60-120px
- Section title 80-160px
- Hero headline 可以用 180-240px 的大字
- 永远不要用 <24px 的字放幻灯片
### 印刷文档
- 正文最小 **10pt**(≈13.3px),理想 11-12pt
- 标题 18-36pt
- Caption 8-9pt
### Web和移动端
- 正文最小 **14px**(老年人友好用16px)
- 移动端正文 **16px**(避免iOS自动缩放)
- Hit target(可点击元素)最小 **44×44px**
- 行高 1.5-1.7(中文1.7-1.8)
### 对比度
- 正文 vs 背景 **至少 4.5:1**(WCAG AA)
- 大字 vs 背景 **至少 3:1**
- 用Chrome DevTools的accessibility工具检查
## CSS 神器
**高级CSS特性**是设计师的好朋友,大胆用:
### 排版
```css
/* 让标题换行更自然,不会最后一行孤单单一个词 */
h1, h2, h3 { text-wrap: balance; }
/* 正文换行,避免寡孀和孤儿 */
p { text-wrap: pretty; }
/* 中文排版神器:标点挤压、行首行尾控制 */
p {
text-spacing-trim: space-all;
hanging-punctuation: first;
}
```
### Layout
```css
/* CSS Grid + named areas = 可读性爆表 */
.layout {
display: grid;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr auto;
}
/* Subgrid对齐卡片内容 */
.card { display: grid; grid-template-rows: subgrid; }
```
### 视觉效果
```css
/* 有设计感的滚动条 */
* { scrollbar-width: thin; scrollbar-color: #666 transparent; }
/* 玻璃拟态(克制使用) */
.glass {
backdrop-filter: blur(20px) saturate(150%);
background: color-mix(in oklch, white 70%, transparent);
}
/* View transitions API让页面切换丝滑 */
@view-transition { navigation: auto; }
```
### 交互
```css
/* :has()选择器让条件样式变容易 */
.card:has(img) { padding-top: 0; } /* 有图片的卡片无顶padding */
/* container queries让组件真的响应式 */
@container (min-width: 500px) { ... }
/* 新的color-mix函数 */
.button:hover {
background: color-mix(in oklch, var(--primary) 85%, black);
}
```
## 决策速查:当你犹豫时
- 想加个渐变?→ 大概率不加
- 想加个emoji?→ 不加
- 想给卡片加圆角+border-left accent?→ 不加,换其他方式
- 想用SVG画个hero插画?→ 不画,用placeholder
- 想加一段quote装饰?→ 先问用户有没有真quote
- 想加一排icon features?→ 先问要不要icon,可能不需要
- 用Inter?→ 换一个更有特点的
- 用紫色渐变?→ 换一个有根据的配色
**当你觉得"加一下会更好看"的时候——那通常是AI slop的征兆**。先做最简的版本,只在用户要求时加。
+199
View File
@@ -0,0 +1,199 @@
# 设计评审深度指南
> Phase 7 的详细参考。提供评分标准、场景侧重点、常见问题清单。
---
## 评分标准详解
### 1. 哲学一致性(Philosophy Alignment)
| 分数 | 标准 |
|------|------|
| 9-10 | 设计完美体现了选定哲学的核心精神,每个细节都有哲学依据 |
| 7-8 | 整体方向正确,核心特征到位,个别细节偏离 |
| 5-6 | 能看出意图,但执行时混入了其他风格元素,不够纯粹 |
| 3-4 | 仅在表面模仿,未理解哲学内核 |
| 1-2 | 与选定哲学基本无关 |
**评审要点**:
- 是否使用了该设计师/机构的标志性手法?
- 色彩、字体、布局是否符合该哲学体系?
- 有没有「自相矛盾」的元素?(如选了Kenya Hara却塞满内容)
### 2. 视觉层级(Visual Hierarchy)
| 分数 | 标准 |
|------|------|
| 9-10 | 用户视线自然沿设计者意图流动,信息获取零摩擦 |
| 7-8 | 主次关系清晰,偶有1-2处层级模糊 |
| 5-6 | 能分出标题和正文,但中间层级混乱 |
| 3-4 | 信息平铺,没有明确的视觉入口 |
| 1-2 | 混乱,用户不知道先看哪里 |
**评审要点**:
- 标题与正文的字号对比是否足够?(至少2.5倍)
- 颜色/粗细/大小是否建立了3-4个清晰层级?
- 留白是否在引导视线?
- 「眯眼测试」:眯起眼看,层级是否仍然清晰?
### 3. 细节执行(Craft Quality)
| 分数 | 标准 |
|------|------|
| 9-10 | 像素级精确,对齐、间距、颜色无任何瑕疵 |
| 7-8 | 整体精致,有1-2处微小对齐/间距问题 |
| 5-6 | 基本对齐,但间距不统一,颜色使用不够系统 |
| 3-4 | 明显的对齐错误、间距混乱、颜色过多 |
| 1-2 | 粗糙,看起来像草稿 |
**评审要点**:
- 是否使用了统一的间距系统(如8pt网格)?
- 同类元素的间距是否一致?
- 颜色数量是否受控?(通常不超过3-4种)
- 字体家族是否统一?(通常不超过2种)
- 边缘对齐是否精确?
### 4. 功能性(Functionality)
| 分数 | 标准 |
|------|------|
| 9-10 | 每个设计元素都服务于目标,零冗余 |
| 7-8 | 功能导向明确,有少量可删减的装饰 |
| 5-6 | 基本可用,但有明显的装饰性元素分散注意力 |
| 3-4 | 形式大于功能,用户需要努力寻找信息 |
| 1-2 | 完全被装饰淹没,失去了传达信息的能力 |
**评审要点**:
- 删掉任何一个元素,设计会变差吗?(如果不会,就应该删)
- CTA/关键信息是否在最显眼的位置?
- 是否有「因为好看所以加上去」的元素?
- 信息密度与载体是否匹配?(PPT不宜太密,PDF可以更密)
### 5. 创新性(Originality)
| 分数 | 标准 |
|------|------|
| 9-10 | 令人耳目一新,在该哲学框架内找到了独特表达 |
| 7-8 | 有自己的想法,不是简单的模板套用 |
| 5-6 | 中规中矩,看起来像模板 |
| 3-4 | 大量使用了cliché(如渐变圆球代表AI) |
| 1-2 | 完全是模板或素材拼凑 |
**评审要点**:
- 是否避免了常见cliché?(见下方「常见问题清单」)
- 在遵循设计哲学的同时是否有个人表达?
- 是否有「意想不到但很合理」的设计决策?
---
## 场景评审侧重
不同输出类型的评审重点不同:
| 场景 | 最重要维度 | 次重要 | 可放宽 |
|------|-----------|--------|--------|
| 公众号封面/配图 | 创新性、视觉层级 | 哲学一致性 | 功能性(单图不涉及交互) |
| 信息图 | 功能性、视觉层级 | 细节执行 | 创新性(准确优先) |
| PPT/Keynote | 视觉层级、功能性 | 细节执行 | 创新性(清晰优先) |
| PDF/白皮书 | 细节执行、功能性 | 视觉层级 | 创新性(专业优先) |
| 落地页/官网 | 功能性、视觉层级 | 创新性 | —(全面要求) |
| App UI | 功能性、细节执行 | 视觉层级 | 哲学一致性(可用性优先) |
| 小红书配图 | 创新性、视觉层级 | 哲学一致性 | 细节执行(氛围优先) |
---
## 常见设计问题 Top 10
### 1. AI科技cliché
**问题**:渐变圆球、数字雨、蓝色电路板、机器人脸
**为什么是问题**:用户已经对这些视觉疲劳,无法区分你和其他人
**修复**:用抽象隐喻替代直白符号(如用「对话」的隐喻而非聊天气泡图标)
### 2. 字号层级不足
**问题**:标题和正文差距太小(<2.5倍)
**为什么是问题**:用户无法快速定位关键信息
**修复**:标题至少为正文的3倍(如正文16px → 标题48-64px)
### 3. 颜色过多
**问题**:使用5种以上颜色,没有主次
**为什么是问题**:视觉混乱,品牌感弱
**修复**:限制为1个主色+1个辅色+1个强调色+灰阶
### 4. 间距不统一
**问题**:元素间距随意,没有系统
**为什么是问题**:看起来不专业,视觉节奏混乱
**修复**:建立8pt网格系统(间距只用8/16/24/32/48/64px)
### 5. 留白不足
**问题**:所有空间都被内容填满
**为什么是问题**:信息拥挤导致阅读疲劳,反而降低信息传达效率
**修复**:留白至少占总面积40%(极简风格60%+)
### 6. 字体过多
**问题**:使用3种以上字体
**为什么是问题**:视觉噪音,削弱统一感
**修复**:最多2种字体(1种标题+1种正文),用字重和大小创造变化
### 7. 对齐不一致
**问题**:有的左对齐,有的居中,有的右对齐
**为什么是问题**:破坏视觉秩序感
**修复**:选定一种对齐方式(推荐左对齐),全局统一
### 8. 装饰大于内容
**问题**:背景图案/渐变/阴影抢了主要内容的风头
**为什么是问题**:本末倒置,用户来看信息不是看装饰
**修复**:「如果删掉这个装饰,设计会变差吗?」如果不会,就删
### 9. 赛博霓虹滥用
**问题**:深蓝底(#0D1117) + 霓虹色发光效果
**为什么是问题**:默认审美禁区(本 skill 的品位基线),且已成为最大 cliché 之一——用户可按自己品牌 override
**修复**:选择更有辨识度的配色方案(参考20种风格的色彩系统)
### 10. 信息密度与载体不匹配
**问题**:PPT里放了一整页文字 / 封面图里塞了10个元素
**为什么是问题**:不同载体的最佳信息密度不同
**修复**:
- PPT:每页1个核心观点
- 封面图:1个视觉焦点
- 信息图:分层展示
- PDF:可以更密,但需要清晰的导航
---
## 评审输出模板
```
## 设计评审报告
**总体评分**:X.X/10 [优秀(8+)/良好(6-7.9)/需改进(4-5.9)/不合格(<4)]
**分项评分**:
- 哲学一致性:X/10 [一句话说明]
- 视觉层级:X/10 [一句话说明]
- 细节执行:X/10 [一句话说明]
- 功能性:X/10 [一句话说明]
- 创新性:X/10 [一句话说明]
### 优点(Keep)
- [具体指出做得好的地方,用设计语言描述]
### 问题(Fix)
[按严重程度排序]
**1. [问题名称]** — ⚠️致命 / ⚡重要 / 💡优化
- 当前:[描述现状]
- 问题:[为什么这是问题]
- 修复:[具体操作,含数值]
### 快速修复清单(Quick Wins)
如果只有5分钟,优先做这3件事:
- [ ] [最有影响力的修复]
- [ ] [第二重要的修复]
- [ ] [第三重要的修复]
```
---
**版本**:v1.0
**更新日期**:2026-02-13
+213
View File
@@ -0,0 +1,213 @@
# Design Context:从已有上下文出发
**这是这个skill最重要的one thing。**
好的hi-fi设计一定是从已有design context长出来的。**凭空做hi-fi是last resort,一定会产出generic的作品**。所以每次设计任务开始,先问:有没有可以参考的东西?
## 什么是Design Context
按优先级从高到低:
### 1. 用户的Design System/UI Kit
用户自己产品已有的组件库、色彩token、字型规范、icon系统。**最完美的情况**。
### 2. 用户的Codebase
如果用户给了代码库,里面就有活生生的组件实现。Read那些组件文件:
- `theme.ts` / `colors.ts` / `tokens.css` / `_variables.scss`
- 具体的组件(Button.tsx、Card.tsx)
- Layout scaffold(App.tsx、MainLayout.tsx)
- Global stylesheets
**读代码抄exact values**:hex codes、spacing scale、font stack、border radius。不要凭记忆重画。
### 3. 用户已发布的产品
如果用户有上线的产品但没给代码,用Playwright或让用户提供截图。
```bash
# 用Playwright截图一个公开URL
npx playwright screenshot https://example.com screenshot.png --viewport-size=1920,1080
```
让你看到真实的视觉vocabulary。
### 4. 品牌指南/Logo/已有素材
用户可能有:Logo文件、品牌色规范、营销物料、slide模板。这些都是context。
### 5. 竞品参考
用户说"像XX网站那样"——让他提供URL或截图。**不要**凭你训练数据里的模糊印象做。
### 6. 已知的design system(fallback)
如果以上都没有,用公认的设计系统作为base:
- Apple HIG
- Material Design 3
- Radix Colors(配色)
- shadcn/ui(组件)
- Tailwind默认palette
明确告诉用户你用的什么,让他知道这是起点不是定稿。
## 获取Context的流程
### Step 1:问用户
任务开始时的必问清单(来自`workflow.md`):
```markdown
1. 你有现成的design system/UI kit/组件库吗?在哪?
2. 有品牌指南、色彩/字体规范吗?
3. 可以给我现有产品的截图或URL吗?
4. 有codebase我可以读吗?
```
### Step 2:用户说"没有"时,帮他找
别直接放弃。尝试:
```markdown
让我看看有没有线索:
- 你之前的项目有相关设计吗?
- 公司的marketing网站用什么色彩/字型?
- 你产品的Logo什么风格?能给我一张吗?
- 有什么你欣赏的产品作为参考?
```
### Step 3:Read所有能找到的context
如果用户给了codebase路径,你读:
1. **先list文件结构**:找style/theme/component相关的文件
2. **读theme/token文件**:lift具体的hex/px values
3. **读2-3个代表性组件**:看视觉vocabulary(hover state、shadow、border、padding node pattern)
4. **读global stylesheet**:基础重置、font loading
5. **如果有Figma链接/截图**:看图,但**更相信代码**
**重要**:**不要**看了一眼就凭印象做。读下来有30+个具体values才真的lift到了。
### Step 4:Vocalize你要用的系统
看完context后,告诉用户你要用的系统:
```markdown
根据你的codebase和产品截图,我提炼的设计系统:
**色彩**
- Primary: #C27558(从tokens.css)
- Background: #FDF9F0
- Text: #1A1A1A
- Muted: #6B6B6B
**字型**
- Display: Instrument Serif(从global.css的@font-face)
- Body: Geist Sans
- Mono: JetBrains Mono
**Spacing**(来自你的scale系统)
- 4, 8, 12, 16, 24, 32, 48, 64
**Shadow pattern**
- `0 1px 2px rgba(0,0,0,0.04)`(subtle card)
- `0 10px 40px rgba(0,0,0,0.1)`(elevated modal)
**Border-radius**
- 小组件 4px,卡片 12px,按钮 8px
**component vocabulary**
- Button:filled primary,outlined secondary,ghost tertiary,全部圆角8px
- Card:白色背景,subtle shadow,无border
我按这套系统开始做。确认没问题?
```
用户确认后再动手。
## 凭空做设计(没Context时的 fallback)
**强烈警告**:这种情况下的产出质量会显著下降。明确告诉用户。
```markdown
你没有design context,我就只能基于通用直觉做。
产出会是"看起来OK但缺乏独特性"的东西。
你愿意继续,还是先补一些参考材料?
```
用户执意要你做,按这个顺序做决策:
### 1. 选一个aesthetic direction
不要给generic结果。挑一个明确方向:
- brutally minimal
- editorial/magazine
- brutalist/raw
- organic/natural
- luxury/refined
- playful/toy
- retro-futuristic
- soft/pastel
告诉用户你选了哪个。
### 2. 选一个known design system作为骨架
- 用Radix Colors做配色(https://www.radix-ui.com/colors)
- 用shadcn/ui做组件vocabulary(https://ui.shadcn.com)
- 用Tailwind spacing scale(4的倍数)
### 3. 选有特点的字体配对
不要用Inter/Roboto。建议组合(从Google Fonts白嫖):
- Instrument Serif + Geist Sans
- Cormorant Garamond + Inter Tight
- Bricolage Grotesque + Söhne(付费)
- Fraunces + Work Sans(注意Fraunces已经被AI用烂)
- JetBrains Mono + Geist Sans(technical feel)
### 4. 每个关键决策都有reasoning
不要默默选。在HTML的comment里写:
```html
<!--
Design decisions:
- Primary color: warm terracotta (oklch 0.65 0.18 25) — fits the "editorial" direction
- Display: Instrument Serif for humanist, literary feel
- Body: Geist Sans for cleanness contrast
- No gradients — committed to minimal, no AI slop
- Spacing: 8px base, golden ratio friendly (8/13/21/34)
-->
```
## Import策略(用户给了codebase)
如果用户说"import这个codebase做参考":
### 小型(<50文件)
全部Read,把context内化。
### 中型(50-500文件)
Focus在:
- `src/components/` 或 `components/`
- 所有styles/tokens/theme相关的文件
- 2-3个代表性的整页组件(Home.tsx、Dashboard.tsx)
### 大型(>500文件)
让用户指明focus:
- "我要做settings页面" → 读现有的settings相关
- "我要做一个新的feature" → 读整体shell + 最接近的参考
- 不求全,求准
## 和Figma/设计稿的配合
如果用户给了Figma链接:
- **不要**期望你能直接"转Figma为HTML"——那需要额外工具
- Figma链接通常不公开可访问
- 让用户:导出为**截图**发给你 + 告诉你具体的color/spacing values
如果只给了Figma截图,告诉用户:
- 我能看到视觉,但取不到精确values
- 关键数字(hex、px)请告诉我,或者export as code(Figma支持)
## 最后的提醒
**一个项目的设计质量上限,由你拿到的context质量决定**。
花10分钟收集context,比花1小时凭空画hi-fi更有价值。
**遇到没context的情况,优先问用户要,而不是硬上**。
+591
View File
@@ -0,0 +1,591 @@
# 设计哲学风格库:20种体系
> 用于视觉设计(网页/PPT/PDF/信息图/配图/App等)的设计风格库。
> 每种风格提供:哲学内核 + 核心特征 + 提示词DNA(与场景模板组合使用)。
## 风格×场景×执行路径 速查表
| 风格 | 网页 | PPT | PDF | 信息图 | 封面 | AI生成 | 最佳路径 |
|------|:---:|:---:|:---:|:-----:|:---:|:-----:|---------|
| 01 Pentagram | ★★★ | ★★★ | ★★☆ | ★★☆ | ★★★ | ★☆☆ | HTML |
| 02 Stamen Design | ★★☆ | ★★☆ | ★★☆ | ★★★ | ★★☆ | ★★☆ | 混合 |
| 03 Information Architects | ★★★ | ★☆☆ | ★★★ | ★☆☆ | ★☆☆ | ★☆☆ | HTML |
| 04 Fathom | ★★☆ | ★★★ | ★★★ | ★★★ | ★★☆ | ★☆☆ | HTML |
| 05 Locomotive | ★★★ | ★★☆ | ★☆☆ | ★☆☆ | ★★☆ | ★★☆ | 混合 |
| 06 Active Theory | ★★★ | ★☆☆ | ★☆☆ | ★☆☆ | ★★☆ | ★★★ | AI生成 |
| 07 Field.io | ★★☆ | ★★☆ | ★☆☆ | ★★☆ | ★★★ | ★★★ | AI生成 |
| 08 Resn | ★★★ | ★☆☆ | ★☆☆ | ★☆☆ | ★★☆ | ★★☆ | AI生成 |
| 09 Experimental Jetset | ★★☆ | ★★☆ | ★★☆ | ★★☆ | ★★★ | ★★☆ | 混合 |
| 10 Müller-Brockmann | ★★☆ | ★★★ | ★★★ | ★★★ | ★★☆ | ★☆☆ | HTML |
| 11 Build | ★★★ | ★★★ | ★★☆ | ★☆☆ | ★★★ | ★☆☆ | HTML |
| 12 Sagmeister & Walsh | ★★☆ | ★★★ | ★☆☆ | ★★☆ | ★★★ | ★★★ | AI生成 |
| 13 Zach Lieberman | ★☆☆ | ★☆☆ | ★☆☆ | ★★☆ | ★★★ | ★★★ | AI生成 |
| 14 Raven Kwok | ★☆☆ | ★★☆ | ★☆☆ | ★★☆ | ★★★ | ★★★ | AI生成 |
| 15 Ash Thorp | ★★☆ | ★★☆ | ★☆☆ | ★☆☆ | ★★★ | ★★★ | AI生成 |
| 16 Territory Studio | ★★☆ | ★★☆ | ★☆☆ | ★★☆ | ★★★ | ★★★ | AI生成 |
| 17 Takram | ★★★ | ★★★ | ★★★ | ★★☆ | ★★☆ | ★☆☆ | HTML |
| 18 Kenya Hara | ★★☆ | ★★★ | ★★★ | ★☆☆ | ★★★ | ★☆☆ | HTML |
| 19 Irma Boom | ★☆☆ | ★★☆ | ★★★ | ★★☆ | ★★★ | ★★☆ | 混合 |
| 20 Neo Shen | ★★☆ | ★★☆ | ★★☆ | ★★☆ | ★★★ | ★★★ | AI生成 |
> 场景适配:★★★ = 强烈推荐 / ★★☆ = 适合 / ★☆☆ = 需改造
> AI生成:★★★ = 直出效果好 / ★★☆ = 需调整 / ★☆☆ = 建议HTML执行
> 最佳路径:AI生成(图片直出)/ HTML(代码渲染,数据精确)/ 混合(HTML布局+AI配图)
**核心规律**:有明确视觉元素的风格(插画/粒子/生成艺术)AI直出效果好;依赖精确排版和数据的风格(网格/信息架构/留白)HTML渲染更可控。
---
## 一、信息建筑派(01-04)
> 哲学:「数据不是装饰,是建筑材料」
### 01. Pentagram - Michael Bierut风格
**哲学**:字体即语言,网格即思想
**核心特征**:
- 极度克制的颜色(黑白+1个品牌色)
- 瑞士网格系统的现代演绎
- 字体排印作为主要视觉语言
- 负空间的战略性使用(60%+留白)
**提示词DNA**:
```
Pentagram/Michael Bierut style:
- Extreme typographic hierarchy, Helvetica/Univers family
- Swiss grid with precise mathematical spacing
- Black/white + one accent color (#HEX)
- Information architecture as visual structure
- 60%+ whitespace ratio
- Data visualization as primary decoration
```
**代表作**:Hillary Clinton 2016 campaign identity
**搜索关键词**:pentagram hillary logo system
---
### 02. Stamen Design - 数据诗学
**哲学**:让数据成为可触摸的风景
**核心特征**:
- 地图学思维应用于信息设计
- 算法生成的有机图形
- 温暖的数据可视化色调(赭石、鼠尾草绿、深蓝)
- 可交互的层级系统
**提示词DNA**:
```
Stamen Design aesthetic:
- Cartographic approach to data visualization
- Organic, algorithm-generated patterns
- Warm palette (terracotta, sage green, deep blues)
- Layered information like topographic maps
- Hand-crafted feel despite digital precision
- Soft shadows and depth
```
**代表作**:COVID-19 surge map
**搜索关键词**:stamen covid map visualization
---
### 03. Information Architects - 内容优先原则
**哲学**:设计不是装饰,是内容的建筑
**核心特征**:
- 极端的内容层级清晰度
- 只使用系统字体(优化阅读)
- 蓝色超链接传统的坚守
- 性能即美学
**提示词DNA**:
```
Information Architects philosophy:
- Content-first hierarchy, zero decorative elements
- System fonts only (SF Pro/Roboto/Inter)
- Classic blue hyperlinks (#0000EE)
- Reading-optimized line length (66 characters)
- Progressive disclosure of depth
- Text-heavy, fast-loading design
```
**代表作**:iA Writer app
**搜索关键词**:information architects ia writer
---
### 04. Fathom Information Design - 科学叙事
**哲学**:每一个像素都必须承载信息
**核心特征**:
- 科学期刊的严谨+设计的优雅
- 定量数据的精确可视化
- 冷静的专业色调(灰、海军蓝)
- 注释与引用系统的设计化
**提示词DNA**:
```
Fathom Information Design style:
- Scientific journal aesthetic meets modern design
- Precise data visualization (charts, timelines, scatter plots)
- Neutral scheme (grays, navy, one highlight color)
- Footnote/citation design integrated into layout
- Clean sans-serif (GT America/Graphik)
- Information density without clutter
```
**代表作**:Bill & Melinda Gates Foundation年度报告
**搜索关键词**:fathom information design gates foundation
---
## 二、运动诗学派(05-08)
> 哲学:「技术本身就是一种流动的诗」
### 05. Locomotive - 滚动叙事大师
**哲学**:滚动不是浏览,是旅程
**核心特征**:
- 丝滑的视差滚动
- 电影化的分镜叙事
- 大胆的空间留白
- 动态元素的精确编排
**提示词DNA**:
```
Locomotive scroll narrative style:
- Film-like scene composition with parallax depth
- Generous vertical spacing between sections
- Bold typography emerging from darkness
- Smooth motion blur effects
- Dark mode (near-black backgrounds)
- Strategic glowing accents
- Hero sections 100vh tall
```
**代表作**:Lusion.co website
**搜索关键词**:locomotive scroll lusion
---
### 06. Active Theory - WebGL诗人
**哲学**:让技术可见化即让技术可理解
**核心特征**:
- 3D粒子系统作为核心元素
- 实时渲染的数据可视化
- 鼠标交互驱动的世界构建
- 霓虹与深空的配色
**提示词DNA**:
```
Active Theory WebGL aesthetic:
- Particle systems representing data flow
- 3D visualization in depth space
- Neon gradients (cyan/magenta/electric blue) on dark
- Mouse-reactive environment
- Depth of field and bokeh effects
- Floating UI with glassmorphism
```
**代表作**:NASA Prospect
**搜索关键词**:active theory nasa webgl
---
### 07. Field.io - 算法美学
**哲学**:代码即设计师
**核心特征**:
- 生成艺术系统
- 每次访问都不同的动态图形
- 抽象几何的智能编排
- 技术感与艺术性的平衡
**提示词DNA**:
```
Field.io generative design style:
- Abstract geometric patterns, algorithmically generated
- Dynamic composition that feels computational
- Monochromatic base with vibrant accent
- Mathematical precision in spacing
- Voronoi diagrams or Delaunay triangulation
- Clean code aesthetic
```
**代表作**:British Council digital installations
**搜索关键词**:field.io generative design
---
### 08. Resn - 叙事驱动的交互
**哲学**:每个点击都推进故事
**核心特征**:
- 游戏化的用户旅程
- 强烈的情感化设计
- 插画与代码的深度结合
- 非线性的探索体验
**提示词DNA**:
```
Resn interactive storytelling approach:
- Illustrative style mixed with UI elements
- Gamified exploration (progress indicators)
- Warm color palette despite tech subject
- Character-driven design
- Scroll-triggered animations
- Editorial illustration meets product design
```
**代表作**:Resn.co.nz portfolio
**搜索关键词**:resn interactive storytelling
---
## 三、极简主义派(09-12)
> 哲学:「删减到无法再删」
### 09. Experimental Jetset - 概念极简
**哲学**:一个想法=一个形式
**核心特征**:
- 单一视觉隐喻贯穿整个设计
- 蓝/红/黄+黑白的蒙德里安色系
- 字体即图形
- 反商业的诚实设计
**提示词DNA**:
```
Experimental Jetset conceptual minimalism:
- Single visual metaphor for entire design
- Primary colors only (red/blue/yellow) + black/white
- Typography as main graphic element
- Grid-based with deliberate rule-breaking
- No photography, only type and geometry
- Anti-commercial, honest aesthetic
```
**代表作**:Whitney Museum identity
**搜索关键词**:experimental jetset whitney responsive w
---
### 10. Müller-Brockmann传承 - 瑞士网格纯粹主义
**哲学**:客观性即美
**核心特征**:
- 数学精确的网格系统(8pt基线)
- 绝对的左对齐或居中
- 单色或双色方案
- 功能主义至上
**提示词DNA**:
```
Josef Müller-Brockmann Swiss modernism:
- Mathematical grid system (8pt baseline)
- Strict alignment (flush left or centered)
- Two-color maximum (black + one accent)
- Akzidenz-Grotesk or similar rationalist typeface
- No decorative elements
- Timeless, objective aesthetic
```
**代表作**:《Grid Systems in Graphic Design》
**搜索关键词**:muller brockmann grid systems poster
---
### 11. Build - 当代极简品牌
**哲学**:精致的简单比复杂更难
**核心特征**:
- 奢侈品级的留白(70%+)
- 微妙的字重对比(200-600)
- 单一强调色的战略使用
- 呼吸感的节奏
**提示词DNA**:
```
Build studio luxury minimalism:
- Generous whitespace (70%+ of area)
- Subtle typography weight shifts (200 to 600)
- Single accent color used sparingly
- High-end product photography aesthetic
- Soft shadows and subtle gradients
- Golden ratio proportions
```
**代表作**:Build studio portfolio
**搜索关键词**:build studio london branding
---
### 12. Sagmeister & Walsh - 快乐极简
**哲学**:美即功能的情感维度
**核心特征**:
- 意外的色彩爆发
- 手工感与数字的融合
- 正能量的视觉语言
- 实验性但可读
**提示词DNA**:
```
Sagmeister & Walsh joyful philosophy:
- Unexpected color bursts on minimal base
- Handmade elements (physical objects in digital)
- Optimistic visual language
- Experimental typography that remains legible
- Human warmth through imperfection
- Mix of analog and digital aesthetics
```
**代表作**:The Happy Show
**搜索关键词**:sagmeister walsh happy show
---
## 四、实验先锋派(13-16)
> 哲学:「打破规则即创造规则」
### 13. Zach Lieberman - 代码诗学
**哲学**:编程即绘画
**核心特征**:
- 手绘感的算法图形
- 实时生成艺术
- 黑白的纯粹表达
- 工具本身的可见性
**提示词DNA**:
```
Zach Lieberman code-as-art style:
- Hand-drawn aesthetic generated by code
- Black and white only, no color
- Real-time generative patterns
- Sketch-like line quality
- Visible process/grid/construction lines
- Poetic interpretation of algorithms
```
**代表作**:openFrameworks creative coding
**搜索关键词**:zach lieberman openframeworks generative
---
### 14. Raven Kwok - 参数化美学
**哲学**:系统的美胜过个体的美
**核心特征**:
- 分形与递归图形
- 黑白高对比
- 建筑化的信息结构
- 东方园林的算法演绎
**提示词DNA**:
```
Raven Kwok parametric aesthetic:
- Fractal patterns and recursive structures
- High-contrast black and white
- Architectural visualization of data
- Chinese garden principles in algorithm form
- Intricate detail that rewards zooming
- Processing/Creative coding aesthetic
```
**代表作**:Raven Kwok generative art exhibitions
**搜索关键词**:raven kwok processing generative art
---
### 15. Ash Thorp - 赛博诗意
**哲学**:未来不是冰冷的,是孤独的诗
**核心特征**:
- 电影级的光影
- 赛博朋克的温暖版本(橙/青,非冷蓝)
- 故事性的概念设计
- 工业美学的精致化
**提示词DNA**:
```
Ash Thorp cinematic concept art:
- Film-grade lighting and atmospheric effects
- Warm cyberpunk (orange/teal, NOT cold blue)
- Industrial design meets luxury
- Narrative concept art feel
- Volumetric lighting and god rays
- Blade Runner warmth over Tron coldness
```
**代表作**:Ghost in the Shell concept art
**搜索关键词**:ash thorp ghost shell concept art
---
### 16. Territory Studio - 屏幕界面虚构
**哲学**:未来UI的今日想象
**核心特征**:
- 科幻电影中的屏幕设计(FUI)
- 全息投影感
- 多层叠加的数据可视化
- 可信的未来感
**提示词DNA**:
```
Territory Studio FUI (Fantasy User Interface):
- Fantasy User Interface design
- Holographic projection aesthetics
- Orange/amber monochrome or cyan accents
- Multiple overlapping data layers
- Believable future technology
- Technical readouts and data streams
```
**代表作**:Blade Runner 2049 screen graphics
**搜索关键词**:territory studio blade runner interface
---
## 五、东方哲学派(17-20)
> 哲学:「留白即内容」
### 17. Takram - 日式思辨设计
**哲学**:技术是思考的媒介
**核心特征**:
- 概念原型的优雅
- 柔和的科技感(圆角、柔和阴影)
- 图表即艺术
- 谦逊的精致
**提示词DNA**:
```
Takram Japanese speculative design:
- Elegant concept prototypes and diagrams
- Soft tech aesthetic (rounded corners, gentle shadows)
- Charts and diagrams as art pieces
- Modest sophistication
- Neutral natural colors (beige, soft gray, muted green)
- Design as philosophical inquiry
```
**代表作**:NHK Fabricated City
**搜索关键词**:takram nhk data visualization
---
### 18. Kenya Hara - 空的设计
**哲学**:设计不是填充,是清空
**核心特征**:
- 极致的留白(80%+)
- 纸张质感的数字化
- 白色的层次(暖白、冷白、米白)
- 触觉的视觉化
**提示词DNA**:
```
Kenya Hara "emptiness" design:
- Extreme whitespace (80%+)
- Paper texture and tactility in digital form
- Layers of white (warm white, cool white, off-white)
- Minimal color (if any, very desaturated)
- Design by subtraction not addition
- Zen simplicity
```
**代表作**:Muji art direction, 《Designing Design》
**搜索关键词**:kenya hara designing design muji
---
### 19. Irma Boom - 书籍建筑师
**哲学**:信息的物理诗学
**核心特征**:
- 非线性的信息架构
- 边缘与边界的游戏
- 意外的颜色组合(粉+红、橙+棕)
- 手工艺的数字转译
**提示词DNA**:
```
Irma Boom book architecture style:
- Non-linear information structure
- Play with edges, margins, boundaries
- Unexpected color combos (pink+red, orange+brown)
- Handcraft translated to digital
- Dense information inviting exploration
- Editorial design, unconventional grid
```
**代表作**:SHV Think Book (2136 pages)
**搜索关键词**:irma boom shv think book
---
### 20. Neo Shen - 东方光影诗
**哲学**:技术需要人的温度
**核心特征**:
- 水墨晕染的数字化
- 柔和的光晕效果
- 诗意的留白
- 情感化的色彩(深蓝、暖灰、柔金)
**提示词DNA**:
```
Neo Shen poetic Chinese aesthetic:
- Digital interpretation of ink wash painting
- Soft glow and light diffusion effects
- Poetic negative space
- Emotional palette (deep blues, warm grays, soft gold)
- Calligraphic influences in typography
- Atmospheric depth
```
**代表作**:Neo Shen digital art series
**搜索关键词**:neo shen digital ink wash art
---
## 提示词使用说明
**组合公式**:`[风格提示词DNA] + [场景模板(见scene-templates.md)] + [具体内容]`
### 核心原则:描述情绪而非布局(Mood, Not Layout)
AI图像生成的关键:短提示词 > 长提示词。描述3句情绪和内容,比30行布局细节效果更好。
| 杀死多样性的写法 | 激发创造力的写法 |
|----------------|----------------|
| 指定颜色比例(60%/25%/15%) | 描述情绪("warm like Sunday morning") |
| 规定布局位置("标题居中,图片右侧") | 引用具体美学("Pentagram editorial feel") |
| 限制角色姿势和表情 | 让AI自然诠释风格 |
| 列出所有要包含的视觉元素 | 描述观众应该感受到什么 |
### Good / Bad 示例
**Bad — 过度约束(AI生成出来空且平):**
```
Professional presentation slide. Dark background, light text.
Title centered at top. Two columns below. Left column: bullet points.
Right column: bar chart. Colors: navy 60%, white 30%, gold 10%.
Font size: title 36pt, body 18pt. Margins: 40px all sides.
```
**Good — 情绪驱动(生成多样且有质感):**
```
A data visualization that feels like a Bloomberg Businessweek
editorial spread. The key number "28.5%" should dominate the
composition like a headline. Warm cream tones with sharp black
typography. The data tells a story of dramatic channel shift.
```
### 执行路径选择
根据速查表的「最佳路径」列选择:
- **AI生成**:有明确视觉元素的风格(06/07/12/13/14/15/16/20),用 Gemini/Midjourney 直出
- **HTML渲染**:依赖精确排版的风格(01/03/04/10/11/17/18),代码控制数据和布局
- **混合**:HTML做骨架布局 + AI生成配图/背景(02/05/08/09/19)
### 质量控制
1. ❌ 不要直接写 "in the style of Pentagram" → ✅ 用具体设计特征描述
2. 文字在AI生成中常出错 → 生成后替换文字
3. 比例易失真 → 明确指定 aspect ratio
4. 先生成3-5个变体,选择最佳后细化
**默认审美禁区**(用户可按自己品牌 override):
- ❌ 赛博霓虹/深蓝色底(#0D1117)
- ❌ 封面图加个人署名/水印
---
**版本**:v2.1
**更新日期**:2026-02-13
**适用场景**:网页/PPT/PDF/信息图/封面/配图/App等所有视觉设计
**与 image-to-slides 联动**:PPT场景可直接引用本文件风格,通过 image-to-slides skill 执行生成
+276
View File
@@ -0,0 +1,276 @@
# React + Babel 项目规范
用HTML+React+Babel做原型时必须遵守的技术规范。不遵守会炸。
## Pinned Script Tags(必须用这些版本)
在HTML的`<head>`里放这三个script tag,用**固定版本+integrity hash**:
```html
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
```
**不要**用`react@18`或`react@latest`这种unpinned版本——会出现版本漂移/缓存问题。
**不要**省略`integrity`——CDN一旦被劫持或篡改,这是防线。
## 文件结构
```
项目名/
├── index.html # 主HTML
├── components.jsx # 组件文件(type="text/babel"加载)
├── data.js # 数据文件
└── styles.css # 额外CSS(可选)
```
HTML里加载方式:
```html
<!-- 先React+Babel -->
<script src="https://unpkg.com/react@18.3.1/..."></script>
<script src="https://unpkg.com/react-dom@18.3.1/..."></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/..."></script>
<!-- 然后你的组件文件 -->
<script type="text/babel" src="components.jsx"></script>
<script type="text/babel" src="pages.jsx"></script>
<!-- 最后主入口 -->
<script type="text/babel">
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
```
**不要**用`type="module"`——会和Babel冲突。
## 三条不可违反的规矩
### 规矩1:styles 对象必须用唯一命名
**错误**(多组件时必炸):
```jsx
// components.jsx
const styles = { button: {...}, card: {...} };
// pages.jsx ← 同名覆盖!
const styles = { container: {...}, header: {...} };
```
**正确**:每个组件文件的styles用唯一前缀。
```jsx
// terminal.jsx
const terminalStyles = {
screen: {...},
line: {...}
};
// sidebar.jsx
const sidebarStyles = {
container: {...},
item: {...}
};
```
**或者用inline styles**(小组件推荐):
```jsx
<div style={{ padding: 16, background: '#111' }}>...</div>
```
这条是**非协商**的。每次写`const styles = {...}`都必须replace成specific命名,否则多组件加载时全栈报错。
### 规矩2:Scope 不共享,需手动export
**关键认知**:每个`<script type="text/babel">`被Babel独立编译,它们之间**scope不通**。`components.jsx`里定义的`Terminal`组件,在`pages.jsx`里**默认是undefined**。
**解决方式**:在每个组件文件末尾,把要共享的组件/工具export到`window`:
```jsx
// components.jsx 末尾
function Terminal(props) { ... }
function Line(props) { ... }
const colors = { green: '#...', red: '#...' };
Object.assign(window, {
Terminal, Line, colors,
// 所有你要在别处用的都列在这里
});
```
然后`pages.jsx`就能直接用`<Terminal />`,因为JSX会去`window.Terminal`找。
### 规矩3:不要用 scrollIntoView
`scrollIntoView`会把整个HTML容器往上推,搞坏web harness的布局。**永远不要用**。
替代方案:
```js
// 滚到容器内某个位置
container.scrollTop = targetElement.offsetTop;
// 或者用element.scrollTo
container.scrollTo({
top: targetElement.offsetTop - 100,
behavior: 'smooth'
});
```
## 调 Claude API(HTML内)
部分原生 design-agent 环境(如 Claude.ai Artifacts)有免配置的 `window.claude.complete`,但大部分 agent 环境(Claude Code / Codex / Cursor / Trae / etc.)本地里**没有**。
如果你的 HTML 原型需要调用 LLM 做 demo(比如做个聊天 interface),两个选项:
### 选项A:不真调,用mock
Demo场景推荐。写一个假helper,返回预设的response:
```jsx
window.claude = {
async complete(prompt) {
await new Promise(r => setTimeout(r, 800)); // 模拟延迟
return "这是一个mock响应。真部署时请替换为真API。";
}
};
```
### 选项B:真调Anthropic API
需要API key,用户必须在HTML里填入自己的key才能跑。**永远不要把key硬编码在HTML里**。
```html
<input id="api-key" placeholder="粘贴你的Anthropic API key" />
<script>
window.claude = {
async complete(prompt) {
const key = document.getElementById('api-key').value;
const res = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': key,
'anthropic-version': '2023-06-01',
'content-type': 'application/json',
},
body: JSON.stringify({
model: 'claude-haiku-4-5',
max_tokens: 1024,
messages: [{ role: 'user', content: prompt }]
})
});
const data = await res.json();
return data.content[0].text;
}
};
</script>
```
**注意**:浏览器直接调Anthropic API会遇到CORS问题。如果用户给你的预览环境不支持CORS bypass,这条路不通。这时候用选项A mock,或者告诉用户需要一个proxy后端。
### 选项 C:用 agent 侧的 LLM 能力生成 mock 数据
如果只是本地演示用,可以在当前 agent 会话里临时调用该 agent 的 LLM 能力(或用户装的 multi-model 类 skill)先生成 mock 响应数据,再硬编码写进 HTML。这样 HTML 运行时完全不依赖任何 API。
## 典型 HTML 起手模板
拷贝这个模板作为React原型的骨架:
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your Prototype Name</title>
<!-- React + Babel pinned -->
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; width: 100%; }
body {
font-family: -apple-system, 'SF Pro Text', sans-serif;
background: #FAFAFA;
color: #1A1A1A;
}
#root { min-height: 100vh; }
</style>
</head>
<body>
<div id="root"></div>
<!-- 你的组件文件 -->
<script type="text/babel" src="components.jsx"></script>
<!-- 主入口 -->
<script type="text/babel">
const { useState, useEffect } = React;
function App() {
return (
<div style={{padding: 40}}>
<h1>Hello</h1>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>
```
## 常见报错及解决
**`styles is not defined` 或 `Cannot read property 'button' of undefined`**
→ 你在一个文件里定义了`const styles`,另一个文件覆盖了。给每个改成specific命名。
**`Terminal is not defined`**
→ 跨文件引用时scope不通。在定义Terminal的文件末尾加`Object.assign(window, {Terminal})`。
**整个页面白屏,控制台没错误**
→ 多半是JSX语法错误但Babel没报在控制台。把`babel.min.js`临时换成`babel.js`非压缩版,错误信息更清晰。
**ReactDOM.createRoot is not a function**
→ 版本不对。确认用了react-dom@18.3.1(而不是17或其他)。
**`Objects are not valid as a React child`**
→ 你渲染了一个对象而不是JSX/字符串。通常是`{someObj}`写成了`{someObj.name}`。
## 大项目怎么拆文件
**>1000行的单文件**难维护。分拆思路:
```
项目/
├── index.html
├── src/
│ ├── primitives.jsx # 基础元素:Button、Card、Badge...
│ ├── components.jsx # 业务组件:UserCard、PostList...
│ ├── pages/
│ │ ├── home.jsx # 首页
│ │ ├── detail.jsx # 详情页
│ │ └── settings.jsx # 设置页
│ ├── router.jsx # 简单路由(React state切换)
│ └── app.jsx # 入口组件
└── data.js # mock data
```
HTML里按顺序加载:
```html
<script type="text/babel" src="src/primitives.jsx"></script>
<script type="text/babel" src="src/components.jsx"></script>
<script type="text/babel" src="src/pages/home.jsx"></script>
<script type="text/babel" src="src/pages/detail.jsx"></script>
<script type="text/babel" src="src/pages/settings.jsx"></script>
<script type="text/babel" src="src/router.jsx"></script>
<script type="text/babel" src="src/app.jsx"></script>
```
**每个文件末尾**都要`Object.assign(window, {...})`导出要共享的东西。
+262
View File
@@ -0,0 +1,262 @@
# 场景模板库:按输出类型组织
> 与 design-styles.md 的「提示词DNA」组合使用。
> 公式:`[风格提示词DNA] + [场景模板] + [具体内容描述]`
---
## 1. 公众号封面 / 文章题图
**规格**:
- 封面图:2.35:1(900×383px 或 1200×510px)
- 正文插图:16:9(1200×675px)或 4:3(1200×900px)
**关键设计要素**:
- 视觉冲击力优先(用户在信息流中快速扫过)
- 文字极少或无文字(公众号标题会覆盖在上面)
- 色彩饱和度适中(微信阅读环境偏白)
- 避免过度细节(缩略图也要可辨识)
**推荐风格**:01 Pentagram / 11 Build / 12 Sagmeister / 18 Kenya Hara / 07 Field.io
**场景提示词模板**:
```
[风格DNA插入此处]
- Article cover image for WeChat subscription
- Landscape format, 2.35:1 aspect ratio
- Bold visual impact, minimal or no text
- Moderate color saturation for white reading environment
- Must remain recognizable as thumbnail
- Clean composition with clear focal point
```
---
## 2. 正文配图 / 概念插画
**规格**:
- 16:9(1200×675px)最通用
- 1:1(800×800px)适合强调
- 4:3(1200×900px)适合信息密集
**关键设计要素**:
- 服务于文章论点,不是装饰
- 与上下文形成视觉节奏
- 简洁表达一个核心概念
- AI生成优先,HTML截图仅在精确数据表格时用
**推荐风格**:根据文章调性选择,常用 01/04/10/17/18
**场景提示词模板**:
```
[风格DNA插入此处]
- Article illustration, concept visualization
- [16:9 / 1:1 / 4:3] aspect ratio
- Single clear concept: [描述核心概念]
- Serve the argument, not decoration
- [Light/Dark] background to match article tone
```
---
## 3. 信息图 / 数据可视化
**规格**:
- 竖版长图:1080×1920px(手机阅读)
- 横版:1920×1080px(文章内嵌)
- 方形:1080×1080px(社交媒体)
**关键设计要素**:
- 信息层级清晰(标题 → 核心数据 → 细节)
- 数据准确,不编造
- 视觉引导线(用户阅读路径)
- 适当使用图标/图表辅助理解
**推荐风格**:04 Fathom / 10 Müller-Brockmann / 02 Stamen / 17 Takram
**场景提示词模板**:
```
[风格DNA插入此处]
- Infographic / data visualization
- [Vertical 1080x1920 / Horizontal 1920x1080 / Square 1080x1080]
- Clear information hierarchy: title → key data → details
- Visual flow guiding reader's eye path
- Icons and charts for comprehension
- Data-accurate, no decorative distortion
```
---
## 4. PPT / Keynote 演示
**规格**:
- 标准:16:9(1920×1080px)
- 宽屏:16:10(1920×1200px)
**关键设计要素**:
- 每页一个核心信息(不堆砌)
- 字号层级明确(标题40pt+ / 正文24pt+ / 注释16pt+)
- 大量留白,投影时更清晰
- 图文比例至少 60:40
- 一致的视觉系统(颜色、字体、间距)
**推荐风格**:01 Pentagram / 10 Müller-Brockmann / 11 Build / 18 Kenya Hara / 04 Fathom
**场景提示词模板**:
```
[风格DNA插入此处]
- Presentation slide design, 16:9
- One core message per slide
- Clear type hierarchy (title 40pt+, body 24pt+)
- Generous whitespace for projection clarity
- Consistent visual system throughout
- [Light/Dark] theme
```
---
## 5. PDF 白皮书 / 技术报告
**规格**:
- A4 纵向(210×297mm / 595×842pt)
- Letter 纵向(216×279mm / 612×792pt)
**关键设计要素**:
- 长文阅读优化(行宽66字符、行高1.5-1.8)
- 清晰的章节导航系统
- 页眉/页脚/页码的统一设计
- 图表与正文的优雅共存
- 引用/注释系统
- 封面页设计精致
**推荐风格**:10 Müller-Brockmann / 04 Fathom / 03 Information Architects / 17 Takram / 19 Irma Boom
**场景提示词模板**:
```
[风格DNA插入此处]
- PDF document / white paper design
- A4 portrait format (210×297mm)
- Long-form reading optimized (66 char line width, 1.5 line height)
- Clear chapter navigation system
- Elegant header/footer/page number design
- Charts integrated with body text
- Professional cover page
```
---
## 6. 落地页 / 产品官网
**规格**:
- Desktop: 1440px 宽度设计(响应至320px)
- 首屏高度:100vh
**关键设计要素**:
- 首屏5秒内传达核心价值
- 清晰的CTA(行动按钮)
- 滚动叙事结构(问题→方案→证明→行动)
- 移动端适配
- 加载速度
**推荐风格**:05 Locomotive / 01 Pentagram / 11 Build / 08 Resn / 06 Active Theory
**场景提示词模板**:
```
[风格DNA插入此处]
- Landing page / product website
- Desktop 1440px width, responsive
- Hero section 100vh, core value in 5 seconds
- Clear CTA button design
- Scroll narrative: problem → solution → proof → action
- Modern web aesthetic
```
---
## 7. App UI / 原型界面
**规格**:
- iOS: 390×844pt(iPhone 15)
- Android: 360×800dp
- 平板: 1024×1366pt(iPad Pro)
**关键设计要素**:
- 触摸友好(最小点击区44×44pt)
- 系统设计语言一致性
- 状态栏/导航栏/Tab栏的标准处理
- 信息密度适中(移动端不宜过密)
**推荐风格**:17 Takram / 11 Build / 03 Information Architects / 01 Pentagram
**场景提示词模板**:
```
[风格DNA插入此处]
- Mobile app UI design
- iOS [390×844pt] / Android [360×800dp]
- Touch-friendly (44pt minimum tap targets)
- Consistent design system
- Standard status bar / navigation / tab bar
- Moderate information density
```
---
## 8. 小红书配图
**规格**:
- 竖版:3:4(1080×1440px)最佳
- 方形:1:1(1080×1080px)
- 首图决定点击率
**关键设计要素**:
- 视觉吸引力第一(在瀑布流中竞争)
- 可以有少量文字(但不超过画面20%)
- 色彩鲜明但不俗
- 生活感/质感/氛围感
**推荐风格**:12 Sagmeister / 11 Build / 20 Neo Shen / 09 Experimental Jetset
**场景提示词模板**:
```
[风格DNA插入此处]
- Social media image for Xiaohongshu (RED)
- Vertical 3:4 (1080×1440px)
- Eye-catching in waterfall feed
- Minimal text overlay (under 20% of area)
- Vivid but tasteful colors
- Lifestyle/texture/atmosphere feel
```
---
## 组合示例
**场景**:公众号封面,介绍一款AI编程工具,想要专业但有温度
**Step 1**:选风格 → 17 Takram(专业+温度)
**Step 2**:取Takram提示词DNA + 公众号封面模板
```
Takram Japanese speculative design:
- Elegant concept prototypes and diagrams
- Soft tech aesthetic (rounded corners, gentle shadows)
- Charts and diagrams as art pieces
- Modest sophistication
- Neutral natural colors (beige, soft gray, muted green)
- Design as philosophical inquiry
Article cover image for WeChat subscription
- Landscape format, 2.35:1 aspect ratio (1200×510px)
- Bold visual impact, minimal text
- Moderate color saturation for white reading environment
- Must remain recognizable as thumbnail
- Clean composition with clear focal point
Content: An AI coding assistant tool, showing the concept of human-AI collaboration
in software development, warm and professional atmosphere
```
---
**版本**:v1.0
**更新日期**:2026-02-13
+429
View File
@@ -0,0 +1,429 @@
# Slide Decks:HTML幻灯片制作规范
做幻灯片是设计工作的高频场景。这份文档说明怎么做好HTML幻灯片。
**和huashu-slides skill的区别**:huashu-slides focus on「AI演示文稿全流程」(含PPT导出);本文档focus on「用HTML本身作为呈现媒介」的设计方法论。两者可以配合使用——用这里的方法做高质量的HTML deck,再用huashu-slides导出成PPTX。
---
## 🛑 先定架构:单文件 还是 多文件?
**这个选择是做幻灯片的第一步,错了会反复踩坑。先读完这一节再动手。**
### 两种架构对比
| 维度 | 单文件 + `deck_stage.js` | **多文件 + `deck_index.html` 拼接器** |
|------|--------------------------|--------------------------------------|
| 代码结构 | 一个 HTML,所有 slide 是 `<section>` | 每页独立 HTML,`index.html` 用 iframe 拼接 |
| CSS 作用域 | ❌ 全局,一页的样式可能影响所有页 | ✅ 天然隔离,iframe 各自一片天 |
| 验证粒度 | ❌ 要 JS goTo 才能切到某页 | ✅ 单页文件双击就能在浏览器看 |
| 并行开发 | ❌ 一个文件,多 agent 改会冲突 | ✅ 多 agent 可并行做不同页,零冲突 merge |
| 调试难度 | ❌ 一处 CSS 出错,全 deck 翻车 | ✅ 一页出错只影响自己 |
| 内嵌交互 | ✅ 跨页共享状态很简单 | 🟡 iframe 间需 postMessage |
| 打印 PDF | ✅ 内置 | ✅ 拼接器 beforeprint 遍历 iframe |
| 键盘导航 | ✅ 内置 | ✅ 拼接器内置 |
### 选哪个?(决策树)
```
│ 问:deck 预计有多少页?
├── ≤10 页、需要 in-deck 动画或跨页交互、pitch deck → 单文件
└── ≥10 页、学术讲座、课件、长 deck、多 agent 并行 → 多文件(推荐)
```
**默认走多文件路径**。它不是「备选」,是**长 deck 和团队协作的主路径**。原因:单文件架构的每一个优势(键盘导航、打印、scale)多文件都有,而多文件的作用域隔离和可验证性是单文件补不回来的。
### 为什么这条规则这么硬?(真实事故记录)
单文件架构曾经在 AI心理学讲座 deck 制作中连踩四坑:
1. **CSS 特异性覆盖**:`.emotion-slide { display: grid }` (特异性 10) 干翻 `deck-stage > section { display: none }` (特异性 2),导致所有页同时渲染叠加。
2. **Shadow DOM slot 规则被外层 CSS 压制**:`::slotted(section) { display: none }` 挡不住 outer rule 的覆盖,sections 不肯隐藏。
3. **localStorage + hash 导航竞态**:刷新后不是跳到 hash 位置,而是停在 localStorage 记录的旧位置。
4. **验证成本高**:必须 `page.evaluate(d => d.goTo(n))` 才能截某页,比直接 `goto(file://.../slides/05-X.html)` 慢一倍,还常报错。
全部根因是**单一全局命名空间**——多文件架构从物理层面把这些问题消除了。
---
## 路径 A(默认):多文件架构
### 目录结构
```
我的Deck/
├── index.html # 从 assets/deck_index.html 复制来,改 MANIFEST
├── shared/
│ ├── tokens.css # 共享设计 token(色板/字号/常用 chrome)
│ └── fonts.html # <link> 引入 Google Fonts(每页 include)
└── slides/
├── 01-cover.html # 每个文件都是完整 1920×1080 HTML
├── 02-agenda.html
├── 03-problem.html
└── ...
```
### 每张 slide 的模板骨架
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>P05 · Chapter Title</title>
<link href="https://fonts.googleapis.com/css2?family=..." rel="stylesheet">
<link rel="stylesheet" href="../shared/tokens.css">
<style>
/* 这一页独有的样式。用任何 class 名都不会污染别的页。*/
body { padding: 120px; }
.my-thing { ... }
</style>
</head>
<body>
<!-- 1920×1080 的内容(由 body 的 width/height 在 tokens.css 里锁定)-->
<div class="page-header">...</div>
<div>...</div>
<div class="page-footer">...</div>
</body>
</html>
```
**关键约束**:
- `<body>` 就是画布,直接在上面布局。不要包 `<section>` 或其他 wrapper。
- `width: 1920px; height: 1080px` 由 `shared/tokens.css` 里的 `body` 规则锁定。
- 引 `shared/tokens.css` 共享设计 token(色板、字号、page-header/footer 等)。
- 字体 `<link>` 每页自己写(fonts 单独 import 不贵,且保证每页独立可打开)。
### 拼接器:`deck_index.html`
**直接从 `assets/deck_index.html` 复制**。你只需要改一处——`window.DECK_MANIFEST` 数组,按顺序列出所有 slide 文件名和人类可读标签:
```js
window.DECK_MANIFEST = [
{ file: "slides/01-cover.html", label: "封面" },
{ file: "slides/02-agenda.html", label: "目录" },
{ file: "slides/03-problem.html", label: "问题陈述" },
// ...
];
```
拼接器已内置:键盘导航(←/→/Home/End/数字键/P 打印)、scale + letterbox、右下计数器、localStorage 记忆、hash 跳页、打印模式(遍历 iframe 按页输出 PDF)。
### 单页验证(这是多文件架构的杀手级优势)
每张 slide 都是独立 HTML。**做完一张就在浏览器双击打开看**:
```bash
open slides/05-personas.html
```
Playwright 截图也是直接 `goto(file://.../slides/05-personas.html)`,不需要 JS 跳页,也不会被别的页的 CSS 干扰。这让「改一点验一点」的工作流成本接近零。
### 并行开发
把每张 slide 的任务拆给不同 agent,同时跑——HTML 文件彼此独立,merge 时没有冲突。长 deck 用这种并行方式能把制作时间压到 1/N。
### `shared/tokens.css` 该放什么
只放**真正跨页共用**的东西:
- CSS 变量(色板、字号阶、间距阶)
- `body { width: 1920px; height: 1080px; }` 这样的 canvas 锁定
- `.page-header` / `.page-footer` 这种每页都用一模一样的 chrome
**不要**把单页的布局 class 塞进来——那会退化回单文件架构的全局污染问题。
---
## 路径 B(小 deck):单文件 + `deck_stage.js`
适用于 ≤10 页、需要跨页共享状态(比如一个 React tweaks 面板要操控所有页)、或者做 pitch deck demo 这种要求极度紧凑的场景。
### 基本用法
1. 从 `assets/deck_stage.js` 读取内容,嵌入 HTML 的 `<script>`(或 `<script src="deck_stage.js">`)
2. 在 body 里用 `<deck-stage>` 包 slide:
```html
<deck-stage>
<section>
<h1>Slide 1</h1>
</section>
<section>
<h1>Slide 2</h1>
</section>
</deck-stage>
```
### ⚠️ 单文件架构的 CSS 陷阱(务必阅读)
单文件架构最常见的坑——**`display` 属性被单页样式偷走**。
如果你给某张 slide 的样式写了 `display: flex/grid`:
```css
.emotion-slide { display: grid; } /* 特异性: 10 */
```
它会压过 deck 级的「隐藏非 active 页」规则:
```css
deck-stage > section { display: none; } /* 特异性: 2 */
```
结果所有 slide 同时渲染叠加。
**修复模式**(写 deck 时必做):
```css
/* ✅ 用 :not(.active) + !important 锁死「非激活即隐藏」 */
deck-stage > section:not(.active) {
display: none !important;
}
```
特异性+权重双保险,单页 layout class 的 `display: grid/flex` 就不会影响可见性。
另一个替代方案:**把单页的 flex/grid 写到内部 wrapper div 上**,section 本身只管 `display: block/none`。
### 自定义尺寸
```html
<deck-stage width="1080" height="1920">
<!-- 9:16 竖版 -->
</deck-stage>
```
---
## Slide Labels
Deck_stage 和 deck_index 都会给每页打标签(计数器显示)。给它们**更有意义**的 label:
**多文件**:在 `MANIFEST` 里写 `{ file, label: "04 问题陈述" }`
**单文件**:在 section 上加 `<section data-screen-label="04 Problem Statement">`
**关键:Slide 编号从 1 开始,不要从 0**。
用户说"slide 5"时,他指的是第 5 张,永远不是数组位置 `[4]`。人类不说 0-indexed。
---
## Speaker Notes
**默认不加**,只在用户明确要求时才加。
加了 speaker notes 你就可以把 slide 上的文字减少到最小,focus on impactful visuals——notes 承载完整 script。
### 格式
**多文件**:在 `index.html` 的 `<head>` 里写:
```html
<script type="application/json" id="speaker-notes">
[
"第1张的 script...",
"第2张的 script...",
"..."
]
</script>
```
**单文件**:同上位置。
### Notes 写作要点
- **完整**:不是提纲,是真要讲的话
- **对话式**:像平时说话,不是书面语
- **对应**:数组第 N 个对应第 N 张 slide
- **长度**:200-400 字最佳
- **情绪线**:标注重音、停顿、强调点
---
## Slide 设计模式
### 1. 建立一个系统(必做)
探索完 design context 后,**先口头说你要用的系统**:
```markdown
Deck系统:
- 背景色:最多2种(90% 白 + 10% 深色 section divider)
- 字型:display 用 Instrument Serif,body 用 Geist Sans
- 节奏:section divider 用 full-bleed 彩色 + 白字,普通 slide 白底
- 图像:hero slide 用 full-bleed 照片,data slide 用 chart
我按这个系统做,有问题告诉我。
```
用户确认后再往下做。
### 2. 常用 slide layouts
- **Title slide**:纯色背景 + 巨大标题 + 副标题 + 作者/日期
- **Section divider**:彩色背景 + 章节号 + 章节标题
- **Content slide**:白底 + 标题 + 1-3 bullet points
- **Data slide**:标题 + 大图表/数字 + 简短说明
- **Image slide**:full-bleed 照片 + 底部小 caption
- **Quote slide**:留白 + 巨大 quote + attribution
- **Two-column**:左右对比(vs / before-after / problem-solution)
一个 deck 里最多用 4-5 种 layout。
### 3. Scale(再次强调)
- 正文最小 **24px**,理想 28-36px
- 标题 **60-120px**
- Hero 字 **180-240px**
- 幻灯片是给 10 米外看的,字要够大
### 4. 视觉节奏
Deck 需要 **intentional variety**:
- 颜色节奏:大部分白底 + 偶尔彩色 section divider + 偶尔 dark 片段
- 密度节奏:几张 text-heavy 的 + 几张 image-heavy 的 + 几张 quote 留白
- 字号节奏:正常标题 + 偶尔巨型 hero 文字
**不要每张 slide 长一样**——那是 PPT 模板,不是设计。
### 5. 空间呼吸(数据密集页必读)
**新手最容易踩的坑**:把所有能放的信息都塞进一页。
信息密度 ≠ 有效信息传达。学术/演讲类 deck 尤其要克制:
- 列表/矩阵页:不要把 N 个元素都画成同一大小。用 **主次分层**——今天要聊的 5 个放大做主角,剩下 16 个缩小做背景 hint。
- 大数字页:数字本身是视觉主角。周围的 caption 不要超过 3 行,否则观众眼球来回跳。
- 引用页:引语和 attribution 之间要有留白隔开,不要贴在一起。
对照「数据是不是主角」「文字有没有挤在一起」两条自我审查,改到留白让你有点不安为止。
---
## 打印为 PDF
**多文件**:`deck_index.html` 已处理 `beforeprint` 事件,按页输出 PDF。
**单文件**:`deck_stage.js` 同样处理。
打印样式已写好,不需要额外写 `@media print` CSS。
---
## 导出为 PPTX / PDF(自助脚本)
HTML 优先是第一公民。但用户经常需要 PPTX/PDF 交付。提供两个通用脚本,**任何多文件 deck 都能用**,位于 `scripts/` 下:
### `export_deck_pdf.mjs` — 导出矢量 PDF(推荐)
```bash
node scripts/export_deck_pdf.mjs --slides <slides-dir> --out deck.pdf
```
**特点**:
- 文字**保留矢量**(可复制、可搜索)
- 视觉 100% 保真(Playwright 内嵌 Chromium 渲染后打印)
- **不需要改 HTML 任何一个字**
- 每个 slide 独立 `page.pdf()`,再用 `pdf-lib` 合并
**依赖**:`npm install playwright pdf-lib`
**限制**:PDF 不能再编辑文字——要改回到 HTML 改。
### `export_deck_pptx.mjs` — 导出 PPTX(两种模式)
```bash
# 图片铺底(视觉 100% 保真,不可编辑文字)
node scripts/export_deck_pptx.mjs --slides <dir> --out deck.pptx --mode image
# 每个文本独立文本框(可编辑,但字体会回落)
node scripts/export_deck_pptx.mjs --slides <dir> --out deck.pptx --mode editable
```
| 模式 | 视觉保真 | 文字可编辑 | 工作原理 | 限制 |
|------|---------|----------|---------|------|
| `image` | ✅ 100% | ❌ | Playwright 截图 → pptxgenjs addImage | 文字变图片 |
| `editable` | 🟡 ~70% | ✅ | html2pptx 提取每个文本框 | 见下方约束 |
**editable 模式的硬性约束**(用户 HTML 必须满足,否则该页 skip):
- 所有文字必须在 `<p>`/`<h1>`-`<h6>`/`<ul>`/`<ol>` 里(禁止裸文本 div)
- `<p>`/`<h*>` 标签自身不能有 background/border/shadow(放外层 div)
- 不用 `::before`/`::after` 插入装饰文字(伪元素提不出来)
- inline 元素(span/em/strong)不能有 margin
- 不用 CSS gradient(不可渲染)
- div 不用 `background-image`(用 `<img>`)
脚本已内置**自动预处理器**——把 "叶子 div 里的裸文本" 自动包成 `<p>`(保留 class)。这解决了最常见的违规(裸文本)。但其他违规(p 上有 border、span 上有 margin 等)仍需 HTML 源头合规。
**editable 模式的另一个 caveat——字体回落**:
- Playwright 用 webfont 测量 text-box 尺寸;PowerPoint/Keynote 用本机字体渲染
- 两者不同时会有**溢出或错位**——每页都要肉眼过
- 建议目标机器装好 HTML 里用的字体,或 fallback 到 `system-ui`
### 从一开始就让 HTML 对导出友好
对性能最稳的 deck:**从写 HTML 时就按 editable 模式的约束写**。这样 `--mode editable` 可以直接全部 pass。额外成本不大:
```html
<!-- ❌ 不好 -->
<div class="title">关键发现</div>
<!-- ✅ 好(p 包裹,class 继承) -->
<p class="title">关键发现</p>
<!-- ❌ 不好(border 在 p 上) -->
<p class="stat" style="border-left: 3px solid red;">41%</p>
<!-- ✅ 好(border 在外层 div) -->
<div class="stat-wrap" style="border-left: 3px solid red;">
<p class="stat">41%</p>
</div>
```
### 何时选哪个
| 场景 | 推荐 |
|------|------|
| 给主办方/档案存档 | **PDF**(通用、高保真、文字可搜) |
| 发给协作者让他们微调文字 | **PPTX editable**(接受字体回落) |
| 要现场演讲、不改内容 | **PDF** 或 **PPTX image** |
| HTML 是首选呈现媒介 | 直接浏览器播放,导出只是备份 |
## 导出为可编辑 PPTX 的深度路径(仅长期项目)
如果你的 deck 会长期维护、反复修改、团队协作——建议**一开始就按 html2pptx 约束写 HTML**,让 `--mode editable` 稳定通过。详见 `huashu-slides` skill 的 `references/prompt-templates.md` 第 2 节(4 条硬性约束)。
---
## 常见问题
**多文件:iframe 里的页打不开 / 白屏**
→ 检查 `MANIFEST` 的 `file` 路径是否相对 `index.html` 正确。用浏览器 DevTools 看 iframe 的 src 能否直接访问。
**多文件:某页样式和别页冲突**
→ 不可能(iframe 隔离)。如果感觉冲突,那是缓存——Cmd+Shift+R 强刷。
**单文件:多 slide 同时渲染叠加**
→ CSS 特异性问题。看上面「单文件架构的 CSS 陷阱」一节。
**单文件:缩放看起来不对**
→ 检查是否所有 slide 直接挂在 `<deck-stage>` 下作为 `<section>`。中间不能包 `<div>`。
**单文件:想跳到特定 slide**
→ URL 加 hash:`index.html#slide-5` 跳到第 5 张。
**两种架构都适用:字在不同屏幕下位置不一致**
→ 用固定尺寸(1920×1080)和 `px` 单位,不要用 `vw`/`vh` 或 `%`。缩放统一处理。
---
## 验证检查清单(做完 deck 必过)
1. [ ] 浏览器直接打开 `index.html`(或主 HTML),检查首页无破图、字体已加载
2. [ ] 按 → 键翻到每一页,没有空白页、没有布局错位
3. [ ] 按 P 键打印预览,每页恰好一张 A4(或 1920×1080)且无裁切
4. [ ] 随机选 3 页 Cmd+Shift+R 强刷,localStorage 记忆正常工作
5. [ ] Playwright 批量截图(单页架构:遍历 `slides/*.html`;单文件架构:用 goTo 切换),人工肉眼过一遍
6. [ ] 搜一下 `TODO` / `placeholder` 残留,确认都清理了
+313
View File
@@ -0,0 +1,313 @@
# Tweaks:设计变体实时调参
Tweaks是这个skill里很核心的能力——让用户不改代码就能实时切换variations/调整参数。
**跨 agent 环境适配**:某些 design-agent 原生环境(如 Claude.ai Artifacts)依赖 host 的 postMessage 把 tweak 值回写源码做持久化。本 skill 采用**纯前端 localStorage 方案**——效果一致(刷新保留状态),但持久化发生在浏览器 localStorage 而不是源码文件。这个方案在任何 agent 环境(Claude Code / Codex / Cursor / Trae / etc.)都能工作。
## 何时加 Tweaks
- 用户明确要求"能调参"/"多个版本切换"
- 设计有多个variations需要对比时
- 用户没明说,但你主观判断**加几个有启发性的tweaks能帮用户看到可能性**
默认推荐:**每个设计都加2-3个tweaks**(颜色主题/字号/layout变体)即使用户没要求——让用户看到可能性空间是设计服务的一部分。
## 实现方式(纯前端版)
### 基本结构
```jsx
const TWEAK_DEFAULTS = {
"primaryColor": "#D97757",
"fontSize": 16,
"density": "comfortable",
"dark": false
};
function useTweaks() {
const [tweaks, setTweaks] = React.useState(() => {
try {
const stored = localStorage.getItem('design-tweaks');
return stored ? { ...TWEAK_DEFAULTS, ...JSON.parse(stored) } : TWEAK_DEFAULTS;
} catch {
return TWEAK_DEFAULTS;
}
});
const update = (patch) => {
const next = { ...tweaks, ...patch };
setTweaks(next);
try {
localStorage.setItem('design-tweaks', JSON.stringify(next));
} catch {}
};
const reset = () => {
setTweaks(TWEAK_DEFAULTS);
try {
localStorage.removeItem('design-tweaks');
} catch {}
};
return { tweaks, update, reset };
}
```
### Tweaks面板UI
右下角浮动面板。可折叠:
```jsx
function TweaksPanel() {
const { tweaks, update, reset } = useTweaks();
const [open, setOpen] = React.useState(false);
return (
<div style={{
position: 'fixed',
bottom: 20,
right: 20,
zIndex: 9999,
}}>
{open ? (
<div style={{
background: 'white',
border: '1px solid #e5e5e5',
borderRadius: 12,
padding: 20,
boxShadow: '0 10px 40px rgba(0,0,0,0.12)',
width: 280,
fontFamily: 'system-ui',
fontSize: 13,
}}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
}}>
<strong>Tweaks</strong>
<button onClick={() => setOpen(false)} style={{
border: 'none', background: 'none', cursor: 'pointer', fontSize: 16,
}}>×</button>
</div>
{/* 颜色 */}
<label style={{ display: 'block', marginBottom: 12 }}>
<div style={{ marginBottom: 4, color: '#666' }}>主色</div>
<input
type="color"
value={tweaks.primaryColor}
onChange={e => update({ primaryColor: e.target.value })}
style={{ width: '100%', height: 32 }}
/>
</label>
{/* 字号slider */}
<label style={{ display: 'block', marginBottom: 12 }}>
<div style={{ marginBottom: 4, color: '#666' }}>字号 ({tweaks.fontSize}px)</div>
<input
type="range"
min={12} max={24} step={1}
value={tweaks.fontSize}
onChange={e => update({ fontSize: +e.target.value })}
style={{ width: '100%' }}
/>
</label>
{/* 密度选项 */}
<label style={{ display: 'block', marginBottom: 12 }}>
<div style={{ marginBottom: 4, color: '#666' }}>密度</div>
<select
value={tweaks.density}
onChange={e => update({ density: e.target.value })}
style={{ width: '100%', padding: 6 }}
>
<option value="compact">紧凑</option>
<option value="comfortable">舒适</option>
<option value="spacious">宽松</option>
</select>
</label>
{/* 暗黑模式toggle */}
<label style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 16,
}}>
<input
type="checkbox"
checked={tweaks.dark}
onChange={e => update({ dark: e.target.checked })}
/>
<span>暗黑模式</span>
</label>
<button onClick={reset} style={{
width: '100%',
padding: '8px 12px',
background: '#f5f5f5',
border: 'none',
borderRadius: 6,
cursor: 'pointer',
fontSize: 12,
}}>重置</button>
</div>
) : (
<button
onClick={() => setOpen(true)}
style={{
background: '#1A1A1A',
color: 'white',
border: 'none',
borderRadius: 999,
padding: '10px 16px',
fontSize: 12,
cursor: 'pointer',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
}}
>⚙ Tweaks</button>
)}
</div>
);
}
```
### 应用Tweaks
在主组件里用Tweaks:
```jsx
function App() {
const { tweaks } = useTweaks();
return (
<div style={{
'--primary': tweaks.primaryColor,
'--font-size': `${tweaks.fontSize}px`,
background: tweaks.dark ? '#0A0A0A' : '#FAFAFA',
color: tweaks.dark ? '#FAFAFA' : '#1A1A1A',
}}>
{/* 你的内容 */}
<TweaksPanel />
</div>
);
}
```
CSS里用变量:
```css
button.cta {
background: var(--primary);
color: white;
font-size: var(--font-size);
}
```
## 典型 Tweak 选项
给不同类型的设计加什么tweaks:
### 通用
- 主色(color picker)
- 字号(slider 12-24px)
- 字型(select:display font vs body font)
- 暗黑模式(toggle)
### 幻灯片deck
- 主题(light/dark/brand)
- 背景样式(solid/gradient/image)
- 字体对比(更装饰 vs 更克制)
- 信息密度(minimal/standard/dense)
### 产品原型
- 布局变体(layout A / B / C)
- 交互速度(animation speed 0.5x-2x)
- 数据量(mock数据条数 5/20/100)
- 状态(empty/loading/success/error)
### 动画
- 速度(0.5x-2x)
- 循环(once/loop/ping-pong)
- Easing(linear/easeOut/spring)
### Landing page
- Hero风格(image/gradient/pattern/solid)
- CTA文案(几种变体)
- 结构(single column / two column / sidebar)
## Tweaks设计原则
### 1. 有意义的选项,不是折腾人的
每个tweak必须展示**真实的设计选项**。别加那种谁都不会真切换的tweak(比如border-radius 0-50px的slider——用户调完发现所有中间值都丑)。
好的tweak暴露**离散的、有思考的variations**:
- "圆角风格":无圆角 / 微圆角 / 大圆角(三个选项)
- 不是:"圆角":0-50px slider
### 2. 少即是多
一个设计的Tweaks面板**最多5-6个**选项。再多就变成"配置页面",失去了快速探索variations的意义。
### 3. 默认值是完成设计
Tweaks是**锦上添花**。默认值必须本身就是一个完整、可发布的设计。用户关闭Tweaks面板后看到的就是产出。
### 4. 合理分组
选项多时分组显示:
```
---- 视觉 ----
主色 | 字号 | 暗黑模式
---- 布局 ----
密度 | 侧栏位置
---- 内容 ----
显示数据量 | 状态
```
## 向前兼容源码级持久化 host
如果你以后想把设计上传到支持源码级 tweaks(如 Claude.ai Artifacts)的环境也能跑,保留 **EDITMODE 标记块**:
```jsx
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"primaryColor": "#D97757",
"fontSize": 16,
"density": "comfortable",
"dark": false
}/*EDITMODE-END*/;
```
标记块在 localStorage 方案里**无作用**(只是个普通注释),但在支持源码回写的 host 里会被读取,实现源码级持久化。加上这个对当前环境无害,同时保持向前兼容。
## 常见问题
**Tweaks面板挡住设计内容**
→ 让它可关闭。默认关闭,显示一个小按钮,用户点了才展开。
**用户切换tweaks后还要重复设置**
→ 已经用localStorage。如果刷新后不持久,检查localStorage是否可用(无痕模式会失败,要catch)。
**多个HTML页面想共享tweaks**
→ 给localStorage key加project name:`design-tweaks-[projectName]`。
**我想让tweak之间有联动关系**
→ 在`update`里加逻辑:
```jsx
const update = (patch) => {
let next = { ...tweaks, ...patch };
// 联动:选dark mode时自动切换字体配色
if (patch.dark === true && !patch.textColor) {
next.textColor = '#F0EEE6';
}
setTweaks(next);
localStorage.setItem(...);
};
```
+183
View File
@@ -0,0 +1,183 @@
# Verification:输出验证流程
一些 design-agent 原生环境(如 Claude.ai Artifacts)有内置的 `fork_verifier_agent` 起 subagent 用 iframe 截图检查。大部分 agent 环境(Claude Code / Codex / Cursor / Trae / 等)里没有这个内置能力——用 Playwright 手动做就能覆盖相同的验证场景。
## 验证清单
每次产出HTML后,按这个清单做一遍:
### 1. 浏览器渲染检查(必做)
最基础:**HTML能不能打开**?在macOS上:
```bash
open -a "Google Chrome" "/path/to/your/design.html"
```
或者用Playwright截图(下一节)。
### 2. 控制台错误检查
HTML文件里最常见的问题是JS报错导致白屏。用Playwright跑一遍:
```bash
python "$SKILL/scripts/verify.py" path/to/design.html
```
这个脚本会:
1. 用headless chromium打开HTML
2. 截图保存到项目目录
3. 抓取控制台错误
4. 报告status
详见`scripts/verify.py`。
### 3. 多视口检查
如果是响应式设计,抓多个viewport:
```bash
python verify.py design.html --viewports 1920x1080,1440x900,768x1024,375x667
```
### 4. 交互检查
Tweaks、动画、按钮切换——默认的静态截图看不到。**建议让用户自己开浏览器点一遍**,或者用Playwright录屏:
```python
page.video.record('interaction.mp4')
```
### 5. 幻灯片逐页检查
Deck类HTML,一张张截:
```bash
python verify.py deck.html --slides 10 # 截前10张
```
生成 `deck-slide-01.png`、`deck-slide-02.png`... 方便快速浏览。
## Playwright Setup
首次使用需要:
```bash
# 如果还没装
npm install -g playwright
npx playwright install chromium
# 或者Python版
pip install playwright
playwright install chromium
```
如果用户已经全局安装 Playwright,直接用即可。
## 截图最佳实践
### 截完整页面
```python
page.screenshot(path='full.png', full_page=True)
```
### 截viewport
```python
page.screenshot(path='viewport.png') # 默认只截可见区域
```
### 截特定元素
```python
element = page.query_selector('.hero-section')
element.screenshot(path='hero.png')
```
### 高清截图
```python
page = browser.new_page(device_scale_factor=2) # retina
```
### 等动画结束再截
```python
page.wait_for_timeout(2000) # 等2秒让动画settle
page.screenshot(...)
```
## 把截图发给用户
### 本地截图直接打开
```bash
open screenshot.png
```
用户会在自己的 Preview/Figma/VSCode/浏览器 里看。
### 上传图床分享链接
如果需要给远程协作者看(比如 Slack/飞书/微信),让用户用自己的图床工具或 MCP 上传。任何返回永久链接的图床都行(ImgBB / Cloudflare R2 / 七牛 / 自建等),粘贴到任何地方。
## 验证出错时
### 页面白屏
控制台一定有错。先检查:
1. React+Babel script tag的integrity hash对不对(见`react-setup.md`)
2. 是不是`const styles = {...}`命名冲突
3. 跨文件的组件有没有export到`window`
4. JSX语法错误(babel.min.js不报错,换babel.js非压缩版)
### 动画卡
- 用Chrome DevTools Performance tab录一段
- 找layout thrashing(频繁的reflow)
- 动效优先用`transform`和`opacity`(GPU加速)
### 字体不对
- 检查`@font-face`的url是否可访问
- 检查fallback字体
- 中文字体加载慢:先显示fallback,加载完再切换
### 布局错位
- 检查`box-sizing: border-box`是否全局应用
- 检查`* margin: 0; padding: 0`reset
- Chrome DevTools里打开gridlines看实际布局
## 验证=设计师的第二双眼
**永远要自己过一遍**。AI写代码时经常出现:
- 看起来对但interaction有bug
- 静态截图好但scroll时错位
- 宽屏好看但窄屏崩
- Dark mode忘了测
- Tweaks切换后某些组件没响应
**最后1分钟的验证可以省1小时的返工**。
## 常用验证脚本命令
```bash
# 基础:打开+截图+抓错
python verify.py design.html
# 多viewport
python verify.py design.html --viewports 1920x1080,375x667
# 多slide
python verify.py deck.html --slides 10
# 输出到指定目录
python verify.py design.html --output ./screenshots/
# headless=false,打开真实浏览器给你看
python verify.py design.html --show
```
+197
View File
@@ -0,0 +1,197 @@
# Video Export:HTML 动画导出为 MP4/GIF
动画 HTML 完成后,用户常想「能导出视频吗」。这份指南给出完整流程。
## 何时导出
**导出时机**:
- 动画完整跑通、视觉验证过(Playwright 截图确认各时间点状态正确)
- 用户在浏览器里看过至少一次,表示效果 OK
- **不要**在动画 bug 没修完的阶段导出——导出到视频后改起来更贵
**用户可能说的触发语**:
- 「能导出成视频吗」
- 「转成 MP4」
- 「做成 GIF」
- 「60fps」
## 产出规格
默认一次给三种格式,让用户选:
| 格式 | 规格 | 适合场景 | 典型大小(30s) |
|---|---|---|---|
| MP4 25fps | 1920×1080 · H.264 · CRF 18 | 公众号嵌入、视频号、YouTube | 1-2 MB |
| MP4 60fps | 1920×1080 · minterpolate 插帧 · H.264 · CRF 18 | 高帧率展示、B站、作品集 | 1.5-3 MB |
| GIF | 960×540 · 15fps · palette 优化 | Twitter/X、README、Slack 预览 | 2-4 MB |
## 工具链
两个脚本在 `scripts/`:
### 1. `render-video.js` — HTML → MP4
录一个 25fps 的 MP4 基础版本。依赖全局 playwright。
```bash
NODE_PATH=$(npm root -g) node /path/to/claude-design/scripts/render-video.js <html文件>
```
可选参数:
- `--duration=30` 动画时长(秒)
- `--width=1920 --height=1080` 分辨率
- `--trim=2.2` 从视频开头裁掉的秒数(去掉 reload + 字体加载时间)
- `--fontwait=1.5` 字体加载等待时间(秒),字体多时调高
输出:与 HTML 同目录,同名 `.mp4`。
### 2. `add-music.sh` — MP4 + BGM → MP4
给无声 MP4 混入背景音乐,按场景(mood)从内置 BGM 库里选,也可自带音频。自动匹配时长、加淡入淡出。
```bash
bash add-music.sh <input.mp4> [--mood=<name>] [--music=<path>] [--out=<path>]
```
**内置 BGM 库**(在 `assets/bgm-<mood>.mp3`):
| `--mood=` | 风格 | 适配场景 |
|-----------|------|---------|
| `tech`(默认) | Apple Silicon / 苹果发布会,极简合成器+钢琴 | 产品发布、AI工具、Skill 宣传 |
| `ad` | upbeat 现代电子,有 build + drop | 社交媒体广告、产品预告、促销片 |
| `educational` | 温暖明亮、轻吉他/电钢琴,inviting | 科普、教程介绍、课程预告 |
| `educational-alt` | 同类备选,换一首试试 | 同上 |
| `tutorial` | lo-fi 环境音,几乎无存在感 | 软件演示、编程教程、长演示 |
| `tutorial-alt` | 同类备选 | 同上 |
**行为**:
- 音乐按视频时长裁剪
- 0.3s 淡入 + 1s 淡出(避免硬切)
- 视频流 `-c:v copy` 不重编码,音频 AAC 192k
- `--music=<path>` 优先级高于 `--mood`,可以直接指定任意外部音频
- 传错 mood 名会列出所有可用选项,不会静默失败
**典型流水线**(动画导出三件套 + 配乐):
```bash
node render-video.js animation.html # 录屏
bash convert-formats.sh animation.mp4 # 派生 60fps + GIF
bash add-music.sh animation-60fps.mp4 # 加默认 tech BGM
# 或针对不同场景:
bash add-music.sh tutorial-demo.mp4 --mood=tutorial
bash add-music.sh product-promo.mp4 --mood=ad --out=promo-final.mp4
```
### 3. `convert-formats.sh` — MP4 → 60fps MP4 + GIF
从已有 MP4 生成 60fps 版本和 GIF。
```bash
bash /path/to/claude-design/scripts/convert-formats.sh <input.mp4> [gif_width]
```
输出(与输入同目录):
- `<name>-60fps.mp4` — minterpolate 插帧到 60fps
- `<name>.gif` — palette 优化的 GIF(默认 960 宽,可改)
`gif_width` 参数:
- 960(默认)—— 社交平台通用
- 1280 —— 更清晰但文件更大
- 600 —— Twitter/X 优先加载
## 完整流程(标准推荐)
用户说「导出视频」后:
```bash
cd <项目目录>
# 假设 $SKILL 指向本 skill 的根目录(自行按安装位置替换)
# 1. 录 25fps 基础 MP4
NODE_PATH=$(npm root -g) node "$SKILL/scripts/render-video.js" my-animation.html
# 2. 派生 60fps MP4 和 GIF
bash "$SKILL/scripts/convert-formats.sh" my-animation.mp4
# 产出清单:
# my-animation.mp4 (25fps · 1-2 MB)
# my-animation-60fps.mp4 (60fps · 1.5-3 MB)
# my-animation.gif (15fps · 2-4 MB)
```
## 技术细节(排错用)
### Playwright recordVideo 的坑
- 帧率固定 25fps,无法直接录 60fps(Chromium headless 的 compositor 上限)
- 从 context 创建就开始录,必须用 `trim` 裁掉前面的加载时间
- 默认 webm 格式,需要 ffmpeg 转 H.264 MP4 才能通用播放
`render-video.js` 已处理以上问题。
### ffmpeg minterpolate 参数
当前配置:`minterpolate=fps=60:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1`
- `mi_mode=mci` — motion compensation interpolation(运动补偿)
- `mc_mode=aobmc` — adaptive overlapped block motion compensation
- `me_mode=bidir` — 双向运动估计
- `vsbmc=1` — 可变 size block motion compensation
对 CSS **transform 动画**(translate/scale/rotate)效果好。
对**纯 fade** 可能产生轻微 ghosting——如果用户嫌弃,退化为简单帧复制:
```bash
ffmpeg -i input.mp4 -r 60 -c:v libx264 ... output.mp4
```
### GIF palette 为何要两阶段
GIF 只能 256 色。一次 pass 的 GIF 会把全动画色彩压到 256 色通用 palette,对米色底+橙色这种细腻配色会糊。
两阶段:
1. `palettegen=stats_mode=diff` —— 先扫描全片,生成**针对此动画的 optimal palette**
2. `paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle` —— 用这个 palette 编码,rectangle diff 只更新变化区域,大幅减小文件
对 fade 过渡用 `dither=bayer` 比 `none` 更平滑,但文件大一点。
## Pre-flight check(导出前)
导出前 30 秒自检:
- [ ] HTML 在浏览器里完整跑过一遍,无控制台错误
- [ ] 动画第 0 帧是完整初始状态(不是空白加载中)
- [ ] 动画最后一帧是稳定的收尾状态(不是半截)
- [ ] 字体/图片/emoji 全部正常渲染(参考 `animation-pitfalls.md`)
- [ ] Duration 参数与 HTML 里的实际动画时长匹配
## 交付时附带的说明
导出完成后给用户的标准说明格式:
```
**完整交付**
| 文件 | 格式 | 规格 | 大小 |
|---|---|---|---|
| foo.mp4 | MP4 | 1920×1080 · 25fps · H.264 | X MB |
| foo-60fps.mp4 | MP4 | 1920×1080 · 60fps(运动插帧)· H.264 | X MB |
| foo.gif | GIF | 960×540 · 15fps · palette 优化 | X MB |
**说明**
- 60fps 用 minterpolate 做运动估计插帧,transform 动画效果好
- GIF 用 palette 优化,30s 动画可压到 3MB 左右
要换尺寸或帧率说一声。
```
## 常见用户追加需求
| 用户说 | 应对 |
|---|---|
| 「太大了」 | MP4:提高 CRF 到 23-28;GIF:降分辨率到 600 或 fps 到 10 |
| 「GIF 太糊」 | 提高 `gif_width` 到 1280;或者建议用 MP4 代替(微信朋友圈也支持) |
| 「要竖屏 9:16」 | 改 HTML 源的 `--width=1080 --height=1920`,重新录 |
| 「加水印」 | ffmpeg 加 `-vf "drawtext=..."` 或 `overlay=` 一个 PNG |
| 「要透明背景」 | MP4 不支持 alpha;用 WebM VP9 + alpha 或 APNG |
| 「要无损」 | CRF 改 0 + preset veryslow(文件会大 10 倍) |
+216
View File
@@ -0,0 +1,216 @@
# Workflow:从接到任务到交付
你是用户的junior designer。用户是manager。按这个流程工作,能产出好设计的概率会显著提升。
## 问问题的艺术
大多数情况下,开工前要问至少10个问题。不是走过场,是真的要把需求摸清。
**什么时候必须问**:新任务、模糊任务、没有design context、用户只说了一句模糊的要求。
**什么时候可以不问**:小修小补、follow-up任务、用户已经给了明确PRD+截图+上下文。
**怎么问**:大部分 agent 环境没有结构化问题 UI,在对话里用 markdown 清单问即可。**一次性把问题列完让用户批量答**,不要一来一回一个个问——那会浪费用户时间、打断用户思路。
## 必问清单
每个设计任务都必须问清这5类问题:
### 1. Design Context(最重要)
- 有没有现成的design system、UI kit、组件库?在哪?
- 有没有品牌指南、色彩规范、字体规范?
- 有没有可以参考的现有产品/页面截图?
- 有没有codebase可以读?
**如果用户说"没有"**:
- 帮他找——翻项目目录、看有没有参考品牌
- 还没有?明确说:"我会基于通用直觉做,但这通常做不出符合你品牌的作品。你考虑下是否先提供一些参考?"
- 实在要做,就按`references/design-context.md`的fallback策略办
### 2. Variations维度
- 想要几种variations?(推荐3+)
- 在哪些维度上变?视觉/交互/色彩/布局/文案/动画?
- 希望variations都"接近预期"还是"一张地图,从保守到疯狂"?
### 3. Fidelity和Scope
- 多高保真?线框图 / 半成品 / 真实data的full hi-fi?
- 覆盖多少flow?一屏 / 一个flow / 整个产品?
- 有没有具体的「必须包含」元素?
### 4. Tweaks
- 希望能实时调整哪些参数?(颜色/字号/间距/layout/文案/feature flag)
- 用户自己要不要在做完后继续调?
### 5. 问题专属(至少4个)
针对具体任务问4+个细节。例如:
**做landing page**:
- 目标转化动作是什么?
- 主要受众?
- 竞品参考?
- 文案谁提供?
**做iOS App onboarding**:
- 几步?
- 需要用户做什么?
- 跳过路径?
- 目标留存率?
**做动画**:
- 时长?
- 最终用途(视频素材/官网/社交)?
- 节奏(快/慢/分段)?
- 必须出现的关键帧?
## 问题模板示例
遇到新任务时,可以抄这个结构在对话里问:
```markdown
开始前想跟你对齐几个问题,一次列齐你批量回答就行:
**Design Context**
1. 有设计系统/UI kit/品牌规范吗?如果有在哪?
2. 有可以参考的现有产品或竞品截图吗?
3. 项目里有codebase可以读吗?
**Variations**
4. 想要几种variations?在哪些维度上变(视觉/交互/色彩/...)?
5. 希望都是"接近答案"还是从保守到疯狂的一张地图?
**Fidelity**
6. 保真度:线框 / 半成品 / 带真数据full hi-fi?
7. Scope:一屏 / 一整个flow / 整个产品?
**Tweaks**
8. 希望做完后能实时调哪些参数?
**具体任务**
9. [任务专属问题1]
10. [任务专属问题2]
...
```
## Junior Designer模式
这是整个workflow最重要的环节。**不要接到任务就闷头冲**。步骤:
### Pass 1:Assumptions + Placeholders(5-15分钟)
HTML文件头部先写你的**assumptions+reasoning comments**,像junior给manager汇报:
```html
<!--
我的假设:
- 这是给XX受众看的
- 整体tone我理解为XX(基于用户说的"专业但不严肃")
- 主要flow是A→B→C
- 色彩我想用品牌蓝+暖灰,不确定你想不想要accent色
未解的问题:
- 第3步的数据从哪里来?先用placeholder
- 背景图用抽象几何还是真照片?先占位
如果你看到这里觉得方向不对,现在是成本最低的时候改。
-->
<!-- 然后是带placeholder的结构 -->
<section class="hero">
<h1>[主标题位 - 等用户提供]</h1>
<p>[副标题位]</p>
<div class="cta-placeholder">[CTA按钮]</div>
</section>
```
**保存 → show用户 → 等反馈再走下一步**。
### Pass 2:真实组件+Variations(主力工作量)
用户批准方向后,开始填充。这时:
- 写React组件替换placeholder
- 做variations(用design_canvas或Tweaks)
- 如果是幻灯片/动画,用starter components起手
**做到一半再show一次**——不要等全做完。设计方向错了,晚show等于白做。
### Pass 3:细节打磨
用户满意整体后,打磨:
- 字号/间距/对比度微调
- 动画timing
- 边界case
- Tweaks面板完善
### Pass 4:验证+交付
- 用Playwright截图(见`references/verification.md`)
- 打开浏览器肉眼确认
- 总结**极简**:只说caveats和next steps
## Variations的深度逻辑
给variations不是给用户制造选择困难,是**探索可能性空间**。让用户mix and match出最终版本。
### 好的variations长什么样
- **维度明确**:每个variation在不同维度上变(A vs B只换配色,C vs D只换layout)
- **有梯度**:从「by-the-book保守版」到「大胆novel版」逐级递进
- **有记号**:每个variation有短label说明它在探索什么
### 实现方式
**纯视觉对比**(静态):
→ 用`assets/design_canvas.jsx`,网格布局并排展示。每个cell带label。
**多选项/交互差异**:
→ 做完整原型,用Tweaks切换。例如做登录页,"布局"是tweak的一个选项:
- 左文案右表单
- 顶部logo+中央表单
- 背景全屏图+浮层表单
用户开关Tweaks就能切换,不需要打开多个HTML文件。
### 探索矩阵思考
每次设计,脑内过一遍这些维度,挑2-3个来给variations:
- 视觉:minimal / editorial / brutalist / organic / futuristic / retro
- 色彩:monochrome / dual-tone / vibrant / pastel / high-contrast
- 字型:sans-only / sans+serif对比 / 全衬线 / 等宽
- Layout:对称 / 非对称 / 不规则grid / full-bleed / 窄栏
- Density:稀疏呼吸 / 中等 / 信息密集
- 交互:极简hover / 丰富micro-interaction / 夸张大动画
- 材质:flat / 有阴影层次 / 纹理 / noise / 渐变
## 遇到不确定的情况
- **不知道怎么做**:坦白说你不确定,问用户,或先做个placeholder继续。**不要编**。
- **用户的描述矛盾**:指出矛盾,让用户选一个方向。
- **任务太大一次吃不下**:拆成steps,先做第一步让用户看,再推进。
- **用户要求的效果技术上很难**:说清技术边界,提供替代方案。
## 总结规则
交付时,summary **极短**:
```markdown
✅ 幻灯片已完成(10张),带Tweaks可切换"夜/日模式"。
注意:
- 第4页的数据是假的,等你提供真数据我替换
- 动画用了CSS transition,不需要JS
下一步建议:先你浏览器打开看一遍,有问题告诉我哪页哪处。
```
不要:
- 罗列每一页的内容
- 重复讲你用了什么技术
- 夸自己设计多好
Caveats + next steps,结束。
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env bash
# Mix a BGM track into an MP4 video.
#
# Usage:
# bash add-music.sh <input.mp4> [--mood=<name>] [--music=<path>] [--out=<path>]
#
# Mood library (in ../assets/, matching bgm-<mood>.mp3):
# tech — Apple Silicon / product keynote vibe, minimal synth+piano (default)
# ad — upbeat modern, clear build + drop, social-media ad energy
# educational — warm, patient, inviting learning tone
# educational-alt — alternate take of educational
# tutorial — lo-fi background, stays out of voiceover's way
# tutorial-alt — alternate take of tutorial
#
# Flags (all optional):
# --mood=<name> pick a preset from the library (default: tech)
# --music=<path> override with your own audio file (wins over --mood)
# --out=<path> output path (default: <input-basename>-bgm.mp4)
#
# Legacy positional form still works: bash add-music.sh in.mp4 music.mp3 out.mp4
#
# Behavior:
# - Music is trimmed to match video duration
# - 0.3s fade in, 1.0s fade out (avoids hard cuts)
# - Video stream copied (no re-encode), audio AAC 192k
#
# Examples:
# bash add-music.sh my.mp4 # default: tech mood
# bash add-music.sh my.mp4 --mood=ad # switch mood
# bash add-music.sh my.mp4 --mood=educational --out=final.mp4
# bash add-music.sh my.mp4 --music=~/Downloads/song.mp3 # bring your own
#
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ASSETS_DIR="$SCRIPT_DIR/../assets"
# ── Parse args ───────────────────────────────────────────────────────
INPUT=""
MOOD="tech"
CUSTOM_MUSIC=""
OUTPUT=""
POSITIONAL=()
for arg in "$@"; do
case "$arg" in
--mood=*) MOOD="${arg#*=}" ;;
--music=*) CUSTOM_MUSIC="${arg#*=}" ;;
--out=*) OUTPUT="${arg#*=}" ;;
*) POSITIONAL+=("$arg") ;;
esac
done
# Legacy positional: <input> [music] [output]
INPUT="${POSITIONAL[0]}"
[ -z "$CUSTOM_MUSIC" ] && [ -n "${POSITIONAL[1]}" ] && CUSTOM_MUSIC="${POSITIONAL[1]}"
[ -z "$OUTPUT" ] && [ -n "${POSITIONAL[2]}" ] && OUTPUT="${POSITIONAL[2]}"
if [ -z "$INPUT" ] || [ ! -f "$INPUT" ]; then
echo "Usage: bash add-music.sh <input.mp4> [--mood=<name>] [--music=<path>] [--out=<path>]" >&2
echo "Moods available: $(ls "$ASSETS_DIR" | grep -E '^bgm-.*\.mp3$' | sed 's/^bgm-//;s/\.mp3$//' | tr '\n' ' ')" >&2
exit 1
fi
# ── Resolve music source: --music wins, else --mood ─────────────────
if [ -n "$CUSTOM_MUSIC" ]; then
MUSIC="$CUSTOM_MUSIC"
SOURCE_LABEL="custom: $MUSIC"
else
MUSIC="$ASSETS_DIR/bgm-${MOOD}.mp3"
SOURCE_LABEL="mood: $MOOD"
fi
if [ ! -f "$MUSIC" ]; then
echo "✗ Music not found: $MUSIC" >&2
echo " Available moods: $(ls "$ASSETS_DIR" | grep -E '^bgm-.*\.mp3$' | sed 's/^bgm-//;s/\.mp3$//' | tr '\n' ' ')" >&2
exit 1
fi
# ── Resolve output path ─────────────────────────────────────────────
INPUT_DIR="$(cd "$(dirname "$INPUT")" && pwd)"
INPUT_NAME="$(basename "$INPUT" .mp4)"
[ -z "$OUTPUT" ] && OUTPUT="$INPUT_DIR/$INPUT_NAME-bgm.mp4"
# ── Measure video duration, compute fade-out start ──────────────────
DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$INPUT")
if [ -z "$DURATION" ]; then
echo "✗ Could not read video duration" >&2
exit 1
fi
FADE_OUT_START=$(awk "BEGIN { d = $DURATION - 1; if (d < 0) d = 0; print d }")
echo "▸ Mixing BGM into video"
echo " input: $INPUT"
echo " music: $SOURCE_LABEL"
echo " duration: ${DURATION}s"
echo " output: $OUTPUT"
ffmpeg -y -loglevel error \
-i "$INPUT" \
-i "$MUSIC" \
-filter_complex "[1:a]atrim=0:${DURATION},asetpts=PTS-STARTPTS,afade=t=in:st=0:d=0.3,afade=t=out:st=${FADE_OUT_START}:d=1[a]" \
-map 0:v -map "[a]" \
-c:v copy -c:a aac -b:a 192k -shortest \
"$OUTPUT"
SIZE=$(du -h "$OUTPUT" | cut -f1)
echo "✓ Done: $OUTPUT ($SIZE)"
+51
View File
@@ -0,0 +1,51 @@
#!/bin/bash
# Convert MP4 animations to 60fps MP4 (via minterpolate) and optimized GIF.
#
# Usage:
# ./convert-formats.sh input.mp4 [gif_width]
#
# Produces next to the input:
# <name>-60fps.mp4 (1920x1080, 60fps, motion-interpolated)
# <name>.gif (scaled width, 15fps, palette-optimized)
#
# minterpolate flags:
# mi_mode=mci motion compensation interpolation
# mc_mode=aobmc adaptive overlapped block motion comp
# me_mode=bidir bidirectional motion estimation
# vsbmc=1 variable-size block motion comp
# GIF uses two-pass palette:
# pass 1: palettegen with stats_mode=diff (per-video optimal palette)
# pass 2: paletteuse with bayer dither + rectangle diff
# This keeps 30s/1080p animations GIF under ~4MB with good color fidelity.
set -e
INPUT="${1:?Usage: $0 input.mp4 [gif_width]}"
GIF_WIDTH="${2:-960}"
DIR=$(dirname "$INPUT")
BASE=$(basename "$INPUT" .mp4)
OUT60="$DIR/$BASE-60fps.mp4"
OUTGIF="$DIR/$BASE.gif"
PAL="$DIR/.palette-$BASE.png"
echo "▸ 60fps interpolate: $OUT60"
ffmpeg -y -loglevel error -i "$INPUT" \
-vf "minterpolate=fps=60:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1" \
-c:v libx264 -pix_fmt yuv420p -crf 18 -preset medium -movflags +faststart \
"$OUT60"
MP4_SIZE=$(du -h "$OUT60" | cut -f1)
echo " ✓ $MP4_SIZE"
echo "▸ GIF (${GIF_WIDTH}w, 15fps, palette-optimized): $OUTGIF"
# Pass 1: generate palette tailored to this video
ffmpeg -y -loglevel error -i "$INPUT" \
-vf "fps=15,scale=${GIF_WIDTH}:-1:flags=lanczos,palettegen=stats_mode=diff" \
"$PAL"
# Pass 2: apply palette with dithering
ffmpeg -y -loglevel error -i "$INPUT" -i "$PAL" \
-lavfi "fps=15,scale=${GIF_WIDTH}:-1:flags=lanczos[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle" \
"$OUTGIF"
rm -f "$PAL"
GIF_SIZE=$(du -h "$OUTGIF" | cut -f1)
echo " ✓ $GIF_SIZE"
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env node
/**
* export_deck_pdf.mjs — 把多文件 slide deck 导出为单个矢量 PDF
*
* 用法:
* node export_deck_pdf.mjs --slides <dir> --out <file.pdf> [--width 1920] [--height 1080]
*
* 特点:
* - 文字保留矢量(可复制、可搜索)
* - 背景/图形 1:1 保真(Playwright 内嵌 Chromium 渲染)
* - 不需要对 HTML 做任何改造
* - 视觉损失 = 0(PDF 就是浏览器打印出来的)
*
* trade-off:
* - PDF 不可再编辑文字(要改回到 HTML 改)
*
* 依赖:playwright pdf-lib
* npm install playwright pdf-lib
*
* 会按文件名排序(01-xxx.html → 02-xxx.html → ...)
*/
import { chromium } from 'playwright';
import { PDFDocument } from 'pdf-lib';
import fs from 'fs/promises';
import path from 'path';
function parseArgs() {
const args = { width: 1920, height: 1080 };
const a = process.argv.slice(2);
for (let i = 0; i < a.length; i += 2) {
const k = a[i].replace(/^--/, '');
args[k] = a[i + 1];
}
if (!args.slides || !args.out) {
console.error('用法: node export_deck_pdf.mjs --slides <dir> --out <file.pdf> [--width 1920] [--height 1080]');
process.exit(1);
}
args.width = parseInt(args.width);
args.height = parseInt(args.height);
return args;
}
async function main() {
const { slides, out, width, height } = parseArgs();
const slidesDir = path.resolve(slides);
const outFile = path.resolve(out);
const files = (await fs.readdir(slidesDir))
.filter(f => f.endsWith('.html'))
.sort();
if (!files.length) {
console.error(`No .html files found in ${slidesDir}`);
process.exit(1);
}
console.log(`Found ${files.length} slides in ${slidesDir}`);
const browser = await chromium.launch();
const ctx = await browser.newContext({ viewport: { width, height } });
// 1) Render each HTML to its own PDF buffer
const pageBuffers = [];
for (const f of files) {
const page = await ctx.newPage();
const url = 'file://' + path.join(slidesDir, f);
await page.goto(url, { waitUntil: 'networkidle' }).catch(() => page.goto(url));
await page.waitForTimeout(1200); // web-font paint
// emulate "screen" so CSS colors/backgrounds render the same as browser
await page.emulateMedia({ media: 'screen' });
const buf = await page.pdf({
width: `${width}px`,
height: `${height}px`,
printBackground: true,
margin: { top: 0, right: 0, bottom: 0, left: 0 },
preferCSSPageSize: false,
});
pageBuffers.push(buf);
await page.close();
console.log(` [${pageBuffers.length}/${files.length}] ${f}`);
}
await browser.close();
// 2) Merge into a single PDF
const merged = await PDFDocument.create();
for (const buf of pageBuffers) {
const src = await PDFDocument.load(buf);
const copied = await merged.copyPages(src, src.getPageIndices());
copied.forEach(p => merged.addPage(p));
}
const bytes = await merged.save();
await fs.writeFile(outFile, bytes);
const kb = (bytes.byteLength / 1024).toFixed(0);
console.log(`\n✓ Wrote ${outFile} (${kb} KB, ${files.length} pages, vector)`);
}
main().catch(e => { console.error(e); process.exit(1); });
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env node
/**
* export_deck_pptx.mjs — 把多文件 slide deck 导出为 PPTX(图片铺底)
*
* 用法:
* node export_deck_pptx.mjs --slides <dir> --out <file.pptx> [--width 1920] [--height 1080]
*
* 特点:
* - 每张 slide 截图成 PNG,满铺一张 PPTX 页面
* - 视觉 100% 保真(因为就是图片)
* - ⚠️ 文字不可编辑(文字变成了图片)
*
* 如果用户需要「可编辑文字」的 PPTX:
* ❌ 不要在 claude-design 的 HTML 上硬上 html2pptx——claude-design 的 HTML 视觉自由度高,
* 很少能满足 html2pptx 的严格约束(p 标签语法、无 ::before、无 span margin 等)。
* 实测 32 页里能 pass 的不到 30%,剩下的要逐页改造 + 逐页修字体溢出——工时失控。
* ✅ 正确做法:切换到 **huashu-slides** skill 的 Path A,按它的 HTML 格式**从头重构**
* 每一页。huashu-slides 的 HTML 从一开始就符合 html2pptx 约束,可 100% 导出可编辑 PPTX。
*
* 依赖:playwright pptxgenjs
* npm install playwright pptxgenjs
*
* 会按文件名排序(01-xxx.html → 02-xxx.html → ...)
*/
import { chromium } from 'playwright';
import pptxgen from 'pptxgenjs';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
function parseArgs() {
const args = { width: 1920, height: 1080 };
const a = process.argv.slice(2);
for (let i = 0; i < a.length; i += 2) {
const k = a[i].replace(/^--/, '');
args[k] = a[i + 1];
}
if (!args.slides || !args.out) {
console.error('用法: node export_deck_pptx.mjs --slides <dir> --out <file.pptx> [--width 1920] [--height 1080]');
process.exit(1);
}
args.width = parseInt(args.width);
args.height = parseInt(args.height);
return args;
}
async function main() {
const { slides, out, width, height } = parseArgs();
const slidesDir = path.resolve(slides);
const outFile = path.resolve(out);
const files = (await fs.readdir(slidesDir))
.filter(f => f.endsWith('.html'))
.sort();
if (!files.length) {
console.error(`No .html files found in ${slidesDir}`);
process.exit(1);
}
console.log(`Found ${files.length} slides, rendering to PNG...`);
const browser = await chromium.launch();
const ctx = await browser.newContext({ viewport: { width, height } });
const page = await ctx.newPage();
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'deck-pptx-'));
const pngs = [];
for (const f of files) {
const url = 'file://' + path.join(slidesDir, f);
await page.goto(url, { waitUntil: 'networkidle' }).catch(() => page.goto(url));
await page.waitForTimeout(1200);
const out = path.join(tmpDir, f.replace(/\.html$/, '.png'));
await page.screenshot({ path: out, fullPage: false });
pngs.push(out);
console.log(` [${pngs.length}/${files.length}] ${f}`);
}
await browser.close();
// Build PPTX
const pres = new pptxgen();
pres.defineLayout({ name: 'DECK', width: width / 96, height: height / 96 });
pres.layout = 'DECK';
for (const png of pngs) {
const s = pres.addSlide();
s.addImage({ path: png, x: 0, y: 0, w: pres.width, h: pres.height });
}
await pres.writeFile({ fileName: outFile });
// cleanup
for (const p of pngs) await fs.unlink(p).catch(() => {});
await fs.rmdir(tmpDir).catch(() => {});
console.log(`\n✓ Wrote ${outFile} (${files.length} slides, image mode)`);
console.log(` 要可编辑?改走 huashu-slides 的 Path A 从头重构 HTML。`);
}
main().catch(e => { console.error(e); process.exit(1); });
+979
View File
@@ -0,0 +1,979 @@
/**
* html2pptx - Convert HTML slide to pptxgenjs slide with positioned elements
*
* USAGE:
* const pptx = new pptxgen();
* pptx.layout = 'LAYOUT_16x9'; // Must match HTML body dimensions
*
* const { slide, placeholders } = await html2pptx('slide.html', pptx);
* slide.addChart(pptx.charts.LINE, data, placeholders[0]);
*
* await pptx.writeFile('output.pptx');
*
* FEATURES:
* - Converts HTML to PowerPoint with accurate positioning
* - Supports text, images, shapes, and bullet lists
* - Extracts placeholder elements (class="placeholder") with positions
* - Handles CSS gradients, borders, and margins
*
* VALIDATION:
* - Uses body width/height from HTML for viewport sizing
* - Throws error if HTML dimensions don't match presentation layout
* - Throws error if content overflows body (with overflow details)
*
* RETURNS:
* { slide, placeholders } where placeholders is an array of { id, x, y, w, h }
*/
const { chromium } = require('playwright');
const path = require('path');
const sharp = require('sharp');
const PT_PER_PX = 0.75;
const PX_PER_IN = 96;
const EMU_PER_IN = 914400;
// Helper: Get body dimensions and check for overflow
async function getBodyDimensions(page) {
const bodyDimensions = await page.evaluate(() => {
const body = document.body;
const style = window.getComputedStyle(body);
return {
width: parseFloat(style.width),
height: parseFloat(style.height),
scrollWidth: body.scrollWidth,
scrollHeight: body.scrollHeight
};
});
const errors = [];
const widthOverflowPx = Math.max(0, bodyDimensions.scrollWidth - bodyDimensions.width - 1);
const heightOverflowPx = Math.max(0, bodyDimensions.scrollHeight - bodyDimensions.height - 1);
const widthOverflowPt = widthOverflowPx * PT_PER_PX;
const heightOverflowPt = heightOverflowPx * PT_PER_PX;
if (widthOverflowPt > 0 || heightOverflowPt > 0) {
const directions = [];
if (widthOverflowPt > 0) directions.push(`${widthOverflowPt.toFixed(1)}pt horizontally`);
if (heightOverflowPt > 0) directions.push(`${heightOverflowPt.toFixed(1)}pt vertically`);
const reminder = heightOverflowPt > 0 ? ' (Remember: leave 0.5" margin at bottom of slide)' : '';
errors.push(`HTML content overflows body by ${directions.join(' and ')}${reminder}`);
}
return { ...bodyDimensions, errors };
}
// Helper: Validate dimensions match presentation layout
function validateDimensions(bodyDimensions, pres) {
const errors = [];
const widthInches = bodyDimensions.width / PX_PER_IN;
const heightInches = bodyDimensions.height / PX_PER_IN;
if (pres.presLayout) {
const layoutWidth = pres.presLayout.width / EMU_PER_IN;
const layoutHeight = pres.presLayout.height / EMU_PER_IN;
if (Math.abs(layoutWidth - widthInches) > 0.1 || Math.abs(layoutHeight - heightInches) > 0.1) {
errors.push(
`HTML dimensions (${widthInches.toFixed(1)}" × ${heightInches.toFixed(1)}") ` +
`don't match presentation layout (${layoutWidth.toFixed(1)}" × ${layoutHeight.toFixed(1)}")`
);
}
}
return errors;
}
function validateTextBoxPosition(slideData, bodyDimensions) {
const errors = [];
const slideHeightInches = bodyDimensions.height / PX_PER_IN;
const minBottomMargin = 0.5; // 0.5 inches from bottom
for (const el of slideData.elements) {
// Check text elements (p, h1-h6, list)
if (['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'list'].includes(el.type)) {
const fontSize = el.style?.fontSize || 0;
const bottomEdge = el.position.y + el.position.h;
const distanceFromBottom = slideHeightInches - bottomEdge;
if (fontSize > 12 && distanceFromBottom < minBottomMargin) {
const getText = () => {
if (typeof el.text === 'string') return el.text;
if (Array.isArray(el.text)) return el.text.find(t => t.text)?.text || '';
if (Array.isArray(el.items)) return el.items.find(item => item.text)?.text || '';
return '';
};
const textPrefix = getText().substring(0, 50) + (getText().length > 50 ? '...' : '');
errors.push(
`Text box "${textPrefix}" ends too close to bottom edge ` +
`(${distanceFromBottom.toFixed(2)}" from bottom, minimum ${minBottomMargin}" required)`
);
}
}
}
return errors;
}
// Helper: Add background to slide
async function addBackground(slideData, targetSlide, tmpDir) {
if (slideData.background.type === 'image' && slideData.background.path) {
let imagePath = slideData.background.path.startsWith('file://')
? slideData.background.path.replace('file://', '')
: slideData.background.path;
targetSlide.background = { path: imagePath };
} else if (slideData.background.type === 'color' && slideData.background.value) {
targetSlide.background = { color: slideData.background.value };
}
}
// Helper: Add elements to slide
function addElements(slideData, targetSlide, pres) {
for (const el of slideData.elements) {
if (el.type === 'image') {
let imagePath = el.src.startsWith('file://') ? el.src.replace('file://', '') : el.src;
targetSlide.addImage({
path: imagePath,
x: el.position.x,
y: el.position.y,
w: el.position.w,
h: el.position.h
});
} else if (el.type === 'line') {
targetSlide.addShape(pres.ShapeType.line, {
x: el.x1,
y: el.y1,
w: el.x2 - el.x1,
h: el.y2 - el.y1,
line: { color: el.color, width: el.width }
});
} else if (el.type === 'shape') {
const shapeOptions = {
x: el.position.x,
y: el.position.y,
w: el.position.w,
h: el.position.h,
shape: el.shape.rectRadius > 0 ? pres.ShapeType.roundRect : pres.ShapeType.rect
};
if (el.shape.fill) {
shapeOptions.fill = { color: el.shape.fill };
if (el.shape.transparency != null) shapeOptions.fill.transparency = el.shape.transparency;
}
if (el.shape.line) shapeOptions.line = el.shape.line;
if (el.shape.rectRadius > 0) shapeOptions.rectRadius = el.shape.rectRadius;
if (el.shape.shadow) shapeOptions.shadow = el.shape.shadow;
targetSlide.addText(el.text || '', shapeOptions);
} else if (el.type === 'list') {
const listOptions = {
x: el.position.x,
y: el.position.y,
w: el.position.w,
h: el.position.h,
fontSize: el.style.fontSize,
fontFace: el.style.fontFace,
color: el.style.color,
align: el.style.align,
valign: 'top',
lineSpacing: el.style.lineSpacing,
paraSpaceBefore: el.style.paraSpaceBefore,
paraSpaceAfter: el.style.paraSpaceAfter,
margin: el.style.margin
};
if (el.style.margin) listOptions.margin = el.style.margin;
targetSlide.addText(el.items, listOptions);
} else {
// Check if text is single-line (height suggests one line)
const lineHeight = el.style.lineSpacing || el.style.fontSize * 1.2;
const isSingleLine = el.position.h <= lineHeight * 1.5;
let adjustedX = el.position.x;
let adjustedW = el.position.w;
// Make single-line text 2% wider to account for underestimate
if (isSingleLine) {
const widthIncrease = el.position.w * 0.02;
const align = el.style.align;
if (align === 'center') {
// Center: expand both sides
adjustedX = el.position.x - (widthIncrease / 2);
adjustedW = el.position.w + widthIncrease;
} else if (align === 'right') {
// Right: expand to the left
adjustedX = el.position.x - widthIncrease;
adjustedW = el.position.w + widthIncrease;
} else {
// Left (default): expand to the right
adjustedW = el.position.w + widthIncrease;
}
}
const textOptions = {
x: adjustedX,
y: el.position.y,
w: adjustedW,
h: el.position.h,
fontSize: el.style.fontSize,
fontFace: el.style.fontFace,
color: el.style.color,
bold: el.style.bold,
italic: el.style.italic,
underline: el.style.underline,
valign: 'top',
lineSpacing: el.style.lineSpacing,
paraSpaceBefore: el.style.paraSpaceBefore,
paraSpaceAfter: el.style.paraSpaceAfter,
inset: 0 // Remove default PowerPoint internal padding
};
if (el.style.align) textOptions.align = el.style.align;
if (el.style.margin) textOptions.margin = el.style.margin;
if (el.style.rotate !== undefined) textOptions.rotate = el.style.rotate;
if (el.style.transparency !== null && el.style.transparency !== undefined) textOptions.transparency = el.style.transparency;
targetSlide.addText(el.text, textOptions);
}
}
}
// Helper: Extract slide data from HTML page
async function extractSlideData(page) {
return await page.evaluate(() => {
const PT_PER_PX = 0.75;
const PX_PER_IN = 96;
// Fonts that are single-weight and should not have bold applied
// (applying bold causes PowerPoint to use faux bold which makes text wider)
const SINGLE_WEIGHT_FONTS = ['impact'];
// Helper: Check if a font should skip bold formatting
const shouldSkipBold = (fontFamily) => {
if (!fontFamily) return false;
const normalizedFont = fontFamily.toLowerCase().replace(/['"]/g, '').split(',')[0].trim();
return SINGLE_WEIGHT_FONTS.includes(normalizedFont);
};
// Unit conversion helpers
const pxToInch = (px) => px / PX_PER_IN;
const pxToPoints = (pxStr) => parseFloat(pxStr) * PT_PER_PX;
const rgbToHex = (rgbStr) => {
// Handle transparent backgrounds by defaulting to white
if (rgbStr === 'rgba(0, 0, 0, 0)' || rgbStr === 'transparent') return 'FFFFFF';
const match = rgbStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (!match) return 'FFFFFF';
return match.slice(1).map(n => parseInt(n).toString(16).padStart(2, '0')).join('');
};
const extractAlpha = (rgbStr) => {
const match = rgbStr.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/);
if (!match || !match[4]) return null;
const alpha = parseFloat(match[4]);
return Math.round((1 - alpha) * 100);
};
const applyTextTransform = (text, textTransform) => {
if (textTransform === 'uppercase') return text.toUpperCase();
if (textTransform === 'lowercase') return text.toLowerCase();
if (textTransform === 'capitalize') {
return text.replace(/\b\w/g, c => c.toUpperCase());
}
return text;
};
// Extract rotation angle from CSS transform and writing-mode
const getRotation = (transform, writingMode) => {
let angle = 0;
// Handle writing-mode first
// PowerPoint: 90° = text rotated 90° clockwise (reads top to bottom, letters upright)
// PowerPoint: 270° = text rotated 270° clockwise (reads bottom to top, letters upright)
if (writingMode === 'vertical-rl') {
// vertical-rl alone = text reads top to bottom = 90° in PowerPoint
angle = 90;
} else if (writingMode === 'vertical-lr') {
// vertical-lr alone = text reads bottom to top = 270° in PowerPoint
angle = 270;
}
// Then add any transform rotation
if (transform && transform !== 'none') {
// Try to match rotate() function
const rotateMatch = transform.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/);
if (rotateMatch) {
angle += parseFloat(rotateMatch[1]);
} else {
// Browser may compute as matrix - extract rotation from matrix
const matrixMatch = transform.match(/matrix\(([^)]+)\)/);
if (matrixMatch) {
const values = matrixMatch[1].split(',').map(parseFloat);
// matrix(a, b, c, d, e, f) where rotation = atan2(b, a)
const matrixAngle = Math.atan2(values[1], values[0]) * (180 / Math.PI);
angle += Math.round(matrixAngle);
}
}
}
// Normalize to 0-359 range
angle = angle % 360;
if (angle < 0) angle += 360;
return angle === 0 ? null : angle;
};
// Get position/dimensions accounting for rotation
const getPositionAndSize = (el, rect, rotation) => {
if (rotation === null) {
return { x: rect.left, y: rect.top, w: rect.width, h: rect.height };
}
// For 90° or 270° rotations, swap width and height
// because PowerPoint applies rotation to the original (unrotated) box
const isVertical = rotation === 90 || rotation === 270;
if (isVertical) {
// The browser shows us the rotated dimensions (tall box for vertical text)
// But PowerPoint needs the pre-rotation dimensions (wide box that will be rotated)
// So we swap: browser's height becomes PPT's width, browser's width becomes PPT's height
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
return {
x: centerX - rect.height / 2,
y: centerY - rect.width / 2,
w: rect.height,
h: rect.width
};
}
// For other rotations, use element's offset dimensions
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
return {
x: centerX - el.offsetWidth / 2,
y: centerY - el.offsetHeight / 2,
w: el.offsetWidth,
h: el.offsetHeight
};
};
// Parse CSS box-shadow into PptxGenJS shadow properties
const parseBoxShadow = (boxShadow) => {
if (!boxShadow || boxShadow === 'none') return null;
// Browser computed style format: "rgba(0, 0, 0, 0.3) 2px 2px 8px 0px [inset]"
// CSS format: "[inset] 2px 2px 8px 0px rgba(0, 0, 0, 0.3)"
const insetMatch = boxShadow.match(/inset/);
// IMPORTANT: PptxGenJS/PowerPoint doesn't properly support inset shadows
// Only process outer shadows to avoid file corruption
if (insetMatch) return null;
// Extract color first (rgba or rgb at start)
const colorMatch = boxShadow.match(/rgba?\([^)]+\)/);
// Extract numeric values (handles both px and pt units)
const parts = boxShadow.match(/([-\d.]+)(px|pt)/g);
if (!parts || parts.length < 2) return null;
const offsetX = parseFloat(parts[0]);
const offsetY = parseFloat(parts[1]);
const blur = parts.length > 2 ? parseFloat(parts[2]) : 0;
// Calculate angle from offsets (in degrees, 0 = right, 90 = down)
let angle = 0;
if (offsetX !== 0 || offsetY !== 0) {
angle = Math.atan2(offsetY, offsetX) * (180 / Math.PI);
if (angle < 0) angle += 360;
}
// Calculate offset distance (hypotenuse)
const offset = Math.sqrt(offsetX * offsetX + offsetY * offsetY) * PT_PER_PX;
// Extract opacity from rgba
let opacity = 0.5;
if (colorMatch) {
const opacityMatch = colorMatch[0].match(/[\d.]+\)$/);
if (opacityMatch) {
opacity = parseFloat(opacityMatch[0].replace(')', ''));
}
}
return {
type: 'outer',
angle: Math.round(angle),
blur: blur * 0.75, // Convert to points
color: colorMatch ? rgbToHex(colorMatch[0]) : '000000',
offset: offset,
opacity
};
};
// Parse inline formatting tags (<b>, <i>, <u>, <strong>, <em>, <span>) into text runs
const parseInlineFormatting = (element, baseOptions = {}, runs = [], baseTextTransform = (x) => x) => {
let prevNodeIsText = false;
element.childNodes.forEach((node) => {
let textTransform = baseTextTransform;
const isText = node.nodeType === Node.TEXT_NODE || node.tagName === 'BR';
if (isText) {
const text = node.tagName === 'BR' ? '\n' : textTransform(node.textContent.replace(/\s+/g, ' '));
const prevRun = runs[runs.length - 1];
if (prevNodeIsText && prevRun) {
prevRun.text += text;
} else {
runs.push({ text, options: { ...baseOptions } });
}
} else if (node.nodeType === Node.ELEMENT_NODE && node.textContent.trim()) {
const options = { ...baseOptions };
const computed = window.getComputedStyle(node);
// Handle inline elements with computed styles
if (node.tagName === 'SPAN' || node.tagName === 'B' || node.tagName === 'STRONG' || node.tagName === 'I' || node.tagName === 'EM' || node.tagName === 'U') {
const isBold = computed.fontWeight === 'bold' || parseInt(computed.fontWeight) >= 600;
if (isBold && !shouldSkipBold(computed.fontFamily)) options.bold = true;
if (computed.fontStyle === 'italic') options.italic = true;
if (computed.textDecoration && computed.textDecoration.includes('underline')) options.underline = true;
if (computed.color && computed.color !== 'rgb(0, 0, 0)') {
options.color = rgbToHex(computed.color);
const transparency = extractAlpha(computed.color);
if (transparency !== null) options.transparency = transparency;
}
if (computed.fontSize) options.fontSize = pxToPoints(computed.fontSize);
// Apply text-transform on the span element itself
if (computed.textTransform && computed.textTransform !== 'none') {
const transformStr = computed.textTransform;
textTransform = (text) => applyTextTransform(text, transformStr);
}
// Validate: Check for margins on inline elements
if (computed.marginLeft && parseFloat(computed.marginLeft) > 0) {
errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-left which is not supported in PowerPoint. Remove margin from inline elements.`);
}
if (computed.marginRight && parseFloat(computed.marginRight) > 0) {
errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-right which is not supported in PowerPoint. Remove margin from inline elements.`);
}
if (computed.marginTop && parseFloat(computed.marginTop) > 0) {
errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-top which is not supported in PowerPoint. Remove margin from inline elements.`);
}
if (computed.marginBottom && parseFloat(computed.marginBottom) > 0) {
errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-bottom which is not supported in PowerPoint. Remove margin from inline elements.`);
}
// Recursively process the child node. This will flatten nested spans into multiple runs.
parseInlineFormatting(node, options, runs, textTransform);
}
}
prevNodeIsText = isText;
});
// Trim leading space from first run and trailing space from last run
if (runs.length > 0) {
runs[0].text = runs[0].text.replace(/^\s+/, '');
runs[runs.length - 1].text = runs[runs.length - 1].text.replace(/\s+$/, '');
}
return runs.filter(r => r.text.length > 0);
};
// Extract background from body (image or color)
const body = document.body;
const bodyStyle = window.getComputedStyle(body);
const bgImage = bodyStyle.backgroundImage;
const bgColor = bodyStyle.backgroundColor;
// Collect validation errors
const errors = [];
// Validate: Check for CSS gradients
if (bgImage && (bgImage.includes('linear-gradient') || bgImage.includes('radial-gradient'))) {
errors.push(
'CSS gradients are not supported. Use Sharp to rasterize gradients as PNG images first, ' +
'then reference with background-image: url(\'gradient.png\')'
);
}
let background;
if (bgImage && bgImage !== 'none') {
// Extract URL from url("...") or url(...)
const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/);
if (urlMatch) {
background = {
type: 'image',
path: urlMatch[1]
};
} else {
background = {
type: 'color',
value: rgbToHex(bgColor)
};
}
} else {
background = {
type: 'color',
value: rgbToHex(bgColor)
};
}
// Process all elements
const elements = [];
const placeholders = [];
const textTags = ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'UL', 'OL', 'LI'];
const processed = new Set();
document.querySelectorAll('*').forEach((el) => {
if (processed.has(el)) return;
// Validate text elements don't have backgrounds, borders, or shadows
if (textTags.includes(el.tagName)) {
const computed = window.getComputedStyle(el);
const hasBg = computed.backgroundColor && computed.backgroundColor !== 'rgba(0, 0, 0, 0)';
const hasBorder = (computed.borderWidth && parseFloat(computed.borderWidth) > 0) ||
(computed.borderTopWidth && parseFloat(computed.borderTopWidth) > 0) ||
(computed.borderRightWidth && parseFloat(computed.borderRightWidth) > 0) ||
(computed.borderBottomWidth && parseFloat(computed.borderBottomWidth) > 0) ||
(computed.borderLeftWidth && parseFloat(computed.borderLeftWidth) > 0);
const hasShadow = computed.boxShadow && computed.boxShadow !== 'none';
if (hasBg || hasBorder || hasShadow) {
errors.push(
`Text element <${el.tagName.toLowerCase()}> has ${hasBg ? 'background' : hasBorder ? 'border' : 'shadow'}. ` +
'Backgrounds, borders, and shadows are only supported on <div> elements, not text elements.'
);
return;
}
}
// Extract placeholder elements (for charts, etc.)
if (el.className && el.className.includes('placeholder')) {
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
errors.push(
`Placeholder "${el.id || 'unnamed'}" has ${rect.width === 0 ? 'width: 0' : 'height: 0'}. Check the layout CSS.`
);
} else {
placeholders.push({
id: el.id || `placeholder-${placeholders.length}`,
x: pxToInch(rect.left),
y: pxToInch(rect.top),
w: pxToInch(rect.width),
h: pxToInch(rect.height)
});
}
processed.add(el);
return;
}
// Extract images
if (el.tagName === 'IMG') {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
elements.push({
type: 'image',
src: el.src,
position: {
x: pxToInch(rect.left),
y: pxToInch(rect.top),
w: pxToInch(rect.width),
h: pxToInch(rect.height)
}
});
processed.add(el);
return;
}
}
// Extract DIVs with backgrounds/borders as shapes
const isContainer = el.tagName === 'DIV' && !textTags.includes(el.tagName);
if (isContainer) {
const computed = window.getComputedStyle(el);
const hasBg = computed.backgroundColor && computed.backgroundColor !== 'rgba(0, 0, 0, 0)';
// Validate: Check for unwrapped text content in DIV
for (const node of el.childNodes) {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent.trim();
if (text) {
errors.push(
`DIV element contains unwrapped text "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}". ` +
'All text must be wrapped in <p>, <h1>-<h6>, <ul>, or <ol> tags to appear in PowerPoint.'
);
}
}
}
// Check for background images on shapes
const bgImage = computed.backgroundImage;
if (bgImage && bgImage !== 'none') {
errors.push(
'Background images on DIV elements are not supported. ' +
'Use solid colors or borders for shapes, or use slide.addImage() in PptxGenJS to layer images.'
);
return;
}
// Check for borders - both uniform and partial
const borderTop = computed.borderTopWidth;
const borderRight = computed.borderRightWidth;
const borderBottom = computed.borderBottomWidth;
const borderLeft = computed.borderLeftWidth;
const borders = [borderTop, borderRight, borderBottom, borderLeft].map(b => parseFloat(b) || 0);
const hasBorder = borders.some(b => b > 0);
const hasUniformBorder = hasBorder && borders.every(b => b === borders[0]);
const borderLines = [];
if (hasBorder && !hasUniformBorder) {
const rect = el.getBoundingClientRect();
const x = pxToInch(rect.left);
const y = pxToInch(rect.top);
const w = pxToInch(rect.width);
const h = pxToInch(rect.height);
// Collect lines to add after shape (inset by half the line width to center on edge)
if (parseFloat(borderTop) > 0) {
const widthPt = pxToPoints(borderTop);
const inset = (widthPt / 72) / 2; // Convert points to inches, then half
borderLines.push({
type: 'line',
x1: x, y1: y + inset, x2: x + w, y2: y + inset,
width: widthPt,
color: rgbToHex(computed.borderTopColor)
});
}
if (parseFloat(borderRight) > 0) {
const widthPt = pxToPoints(borderRight);
const inset = (widthPt / 72) / 2;
borderLines.push({
type: 'line',
x1: x + w - inset, y1: y, x2: x + w - inset, y2: y + h,
width: widthPt,
color: rgbToHex(computed.borderRightColor)
});
}
if (parseFloat(borderBottom) > 0) {
const widthPt = pxToPoints(borderBottom);
const inset = (widthPt / 72) / 2;
borderLines.push({
type: 'line',
x1: x, y1: y + h - inset, x2: x + w, y2: y + h - inset,
width: widthPt,
color: rgbToHex(computed.borderBottomColor)
});
}
if (parseFloat(borderLeft) > 0) {
const widthPt = pxToPoints(borderLeft);
const inset = (widthPt / 72) / 2;
borderLines.push({
type: 'line',
x1: x + inset, y1: y, x2: x + inset, y2: y + h,
width: widthPt,
color: rgbToHex(computed.borderLeftColor)
});
}
}
if (hasBg || hasBorder) {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
const shadow = parseBoxShadow(computed.boxShadow);
// Only add shape if there's background or uniform border
if (hasBg || hasUniformBorder) {
elements.push({
type: 'shape',
text: '', // Shape only - child text elements render on top
position: {
x: pxToInch(rect.left),
y: pxToInch(rect.top),
w: pxToInch(rect.width),
h: pxToInch(rect.height)
},
shape: {
fill: hasBg ? rgbToHex(computed.backgroundColor) : null,
transparency: hasBg ? extractAlpha(computed.backgroundColor) : null,
line: hasUniformBorder ? {
color: rgbToHex(computed.borderColor),
width: pxToPoints(computed.borderWidth)
} : null,
// Convert border-radius to rectRadius (in inches)
// % values: 50%+ = circle (1), <50% = percentage of min dimension
// pt values: divide by 72 (72pt = 1 inch)
// px values: divide by 96 (96px = 1 inch)
rectRadius: (() => {
const radius = computed.borderRadius;
const radiusValue = parseFloat(radius);
if (radiusValue === 0) return 0;
if (radius.includes('%')) {
if (radiusValue >= 50) return 1;
// Calculate percentage of smaller dimension
const minDim = Math.min(rect.width, rect.height);
return (radiusValue / 100) * pxToInch(minDim);
}
if (radius.includes('pt')) return radiusValue / 72;
return radiusValue / PX_PER_IN;
})(),
shadow: shadow
}
});
}
// Add partial border lines
elements.push(...borderLines);
processed.add(el);
return;
}
}
}
// Extract bullet lists as single text block
if (el.tagName === 'UL' || el.tagName === 'OL') {
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
const liElements = Array.from(el.querySelectorAll('li'));
const items = [];
const ulComputed = window.getComputedStyle(el);
const ulPaddingLeftPt = pxToPoints(ulComputed.paddingLeft);
// Split: margin-left for bullet position, indent for text position
// margin-left + indent = ul padding-left
const marginLeft = ulPaddingLeftPt * 0.5;
const textIndent = ulPaddingLeftPt * 0.5;
liElements.forEach((li, idx) => {
const isLast = idx === liElements.length - 1;
const runs = parseInlineFormatting(li, { breakLine: false });
// Clean manual bullets from first run
if (runs.length > 0) {
runs[0].text = runs[0].text.replace(/^[•\-\*▪▸]\s*/, '');
runs[0].options.bullet = { indent: textIndent };
}
// Set breakLine on last run
if (runs.length > 0 && !isLast) {
runs[runs.length - 1].options.breakLine = true;
}
items.push(...runs);
});
const computed = window.getComputedStyle(liElements[0] || el);
elements.push({
type: 'list',
items: items,
position: {
x: pxToInch(rect.left),
y: pxToInch(rect.top),
w: pxToInch(rect.width),
h: pxToInch(rect.height)
},
style: {
fontSize: pxToPoints(computed.fontSize),
fontFace: computed.fontFamily.split(',')[0].replace(/['"]/g, '').trim(),
color: rgbToHex(computed.color),
transparency: extractAlpha(computed.color),
align: computed.textAlign === 'start' ? 'left' : computed.textAlign,
lineSpacing: computed.lineHeight && computed.lineHeight !== 'normal' ? pxToPoints(computed.lineHeight) : null,
paraSpaceBefore: 0,
paraSpaceAfter: pxToPoints(computed.marginBottom),
// PptxGenJS margin array is [left, right, bottom, top]
margin: [marginLeft, 0, 0, 0]
}
});
liElements.forEach(li => processed.add(li));
processed.add(el);
return;
}
// Extract text elements (P, H1, H2, etc.)
if (!textTags.includes(el.tagName)) return;
const rect = el.getBoundingClientRect();
const text = el.textContent.trim();
if (rect.width === 0 || rect.height === 0 || !text) return;
// Validate: Check for manual bullet symbols in text elements (not in lists)
if (el.tagName !== 'LI' && /^[•\-\*▪▸○●◆◇■□]\s/.test(text.trimStart())) {
errors.push(
`Text element <${el.tagName.toLowerCase()}> starts with bullet symbol "${text.substring(0, 20)}...". ` +
'Use <ul> or <ol> lists instead of manual bullet symbols.'
);
return;
}
const computed = window.getComputedStyle(el);
const rotation = getRotation(computed.transform, computed.writingMode);
const { x, y, w, h } = getPositionAndSize(el, rect, rotation);
const baseStyle = {
fontSize: pxToPoints(computed.fontSize),
fontFace: computed.fontFamily.split(',')[0].replace(/['"]/g, '').trim(),
color: rgbToHex(computed.color),
align: computed.textAlign === 'start' ? 'left' : computed.textAlign,
lineSpacing: pxToPoints(computed.lineHeight),
paraSpaceBefore: pxToPoints(computed.marginTop),
paraSpaceAfter: pxToPoints(computed.marginBottom),
// PptxGenJS margin array is [left, right, bottom, top] (not [top, right, bottom, left] as documented)
margin: [
pxToPoints(computed.paddingLeft),
pxToPoints(computed.paddingRight),
pxToPoints(computed.paddingBottom),
pxToPoints(computed.paddingTop)
]
};
const transparency = extractAlpha(computed.color);
if (transparency !== null) baseStyle.transparency = transparency;
if (rotation !== null) baseStyle.rotate = rotation;
const hasFormatting = el.querySelector('b, i, u, strong, em, span, br');
if (hasFormatting) {
// Text with inline formatting
const transformStr = computed.textTransform;
const runs = parseInlineFormatting(el, {}, [], (str) => applyTextTransform(str, transformStr));
// Adjust lineSpacing based on largest fontSize in runs
const adjustedStyle = { ...baseStyle };
if (adjustedStyle.lineSpacing) {
const maxFontSize = Math.max(
adjustedStyle.fontSize,
...runs.map(r => r.options?.fontSize || 0)
);
if (maxFontSize > adjustedStyle.fontSize) {
const lineHeightMultiplier = adjustedStyle.lineSpacing / adjustedStyle.fontSize;
adjustedStyle.lineSpacing = maxFontSize * lineHeightMultiplier;
}
}
elements.push({
type: el.tagName.toLowerCase(),
text: runs,
position: { x: pxToInch(x), y: pxToInch(y), w: pxToInch(w), h: pxToInch(h) },
style: adjustedStyle
});
} else {
// Plain text - inherit CSS formatting
const textTransform = computed.textTransform;
const transformedText = applyTextTransform(text, textTransform);
const isBold = computed.fontWeight === 'bold' || parseInt(computed.fontWeight) >= 600;
elements.push({
type: el.tagName.toLowerCase(),
text: transformedText,
position: { x: pxToInch(x), y: pxToInch(y), w: pxToInch(w), h: pxToInch(h) },
style: {
...baseStyle,
bold: isBold && !shouldSkipBold(computed.fontFamily),
italic: computed.fontStyle === 'italic',
underline: computed.textDecoration.includes('underline')
}
});
}
processed.add(el);
});
return { background, elements, placeholders, errors };
});
}
async function html2pptx(htmlFile, pres, options = {}) {
const {
tmpDir = process.env.TMPDIR || '/tmp',
slide = null
} = options;
try {
// Use Chrome on macOS, default Chromium on Unix
const launchOptions = { env: { TMPDIR: tmpDir } };
if (process.platform === 'darwin') {
launchOptions.channel = 'chrome';
}
const browser = await chromium.launch(launchOptions);
let bodyDimensions;
let slideData;
const filePath = path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile);
const validationErrors = [];
try {
const page = await browser.newPage();
page.on('console', (msg) => {
// Log the message text to your test runner's console
console.log(`Browser console: ${msg.text()}`);
});
await page.goto(`file://${filePath}`);
bodyDimensions = await getBodyDimensions(page);
await page.setViewportSize({
width: Math.round(bodyDimensions.width),
height: Math.round(bodyDimensions.height)
});
slideData = await extractSlideData(page);
} finally {
await browser.close();
}
// Collect all validation errors
if (bodyDimensions.errors && bodyDimensions.errors.length > 0) {
validationErrors.push(...bodyDimensions.errors);
}
const dimensionErrors = validateDimensions(bodyDimensions, pres);
if (dimensionErrors.length > 0) {
validationErrors.push(...dimensionErrors);
}
const textBoxPositionErrors = validateTextBoxPosition(slideData, bodyDimensions);
if (textBoxPositionErrors.length > 0) {
validationErrors.push(...textBoxPositionErrors);
}
if (slideData.errors && slideData.errors.length > 0) {
validationErrors.push(...slideData.errors);
}
// Throw all errors at once if any exist
if (validationErrors.length > 0) {
const errorMessage = validationErrors.length === 1
? validationErrors[0]
: `Multiple validation errors found:\n${validationErrors.map((e, i) => ` ${i + 1}. ${e}`).join('\n')}`;
throw new Error(errorMessage);
}
const targetSlide = slide || pres.addSlide();
await addBackground(slideData, targetSlide, tmpDir);
addElements(slideData, targetSlide, pres);
return { slide: targetSlide, placeholders: slideData.placeholders };
} catch (error) {
if (!error.message.startsWith(htmlFile)) {
throw new Error(`${htmlFile}: ${error.message}`);
}
throw error;
}
}
module.exports = html2pptx;
+254
View File
@@ -0,0 +1,254 @@
#!/usr/bin/env node
/**
* HTML animation → MP4 via Playwright recordVideo + ffmpeg.
*
* Requires: global playwright (`npm install -g playwright`), ffmpeg on PATH.
*
* Usage:
* NODE_PATH=$(npm root -g) node render-video.js <html-file> \
* [--duration=30] [--width=1920] [--height=1080] \
* [--trim=<seconds>] [--fontwait=1.5] [--readytimeout=8] \
* [--keep-chrome]
*
* Design:
* 1. Warmup context (no record) — caches fonts/assets, closes cleanly
* 2. Record context (fresh, recordVideo ON) — WebM starts writing at
* context creation. Babel-standalone compile + React mount +
* fonts.ready can take 1.5-3s, during which WebM writes black frames.
* We measure this by waiting for window.__ready (set by animations.jsx
* Stage component after first paint), then trim exactly that offset.
* 3. addInitScript injects CSS hiding "chrome" elements (progress bar,
* replay button, masthead, footer, etc.) that are fine for human
* debugging but shouldn't appear in exported video.
*
* Animation-ready signal:
* Set `window.__ready = true` in your HTML after first paint. This tells
* the recorder "animation has started rendering — treat now as t=0".
* If you use animations.jsx, Stage does this automatically. Otherwise
* add: `document.fonts.ready.then(() => requestAnimationFrame(() => { window.__ready = true }));`
* after your first render call.
*
* Without __ready, falls back to --fontwait=1.5s (may leave 1-2s of black
* at the start). Pass --trim=<seconds> to override manually.
*
* Chrome elements hidden by default (all common class names + `.no-record`
* convention). Pass --keep-chrome to disable this and see raw HTML.
*
* Output: next to the HTML file, same basename with .mp4 suffix.
*/
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');
const { spawnSync } = require('child_process');
function arg(name, def) {
const p = process.argv.find(a => a.startsWith('--' + name + '='));
return p ? p.slice(name.length + 3) : def;
}
function hasFlag(name) {
return process.argv.includes('--' + name);
}
const HTML_FILE = process.argv[2];
if (!HTML_FILE || HTML_FILE.startsWith('--')) {
console.error('Usage: node render-video.js <html-file>');
console.error('Example: NODE_PATH=$(npm root -g) node render-video.js my-animation.html');
process.exit(1);
}
const DURATION = parseFloat(arg('duration', '30'));
const WIDTH = parseInt(arg('width', '1920'));
const HEIGHT = parseInt(arg('height', '1080'));
const TRIM_OVERRIDE = arg('trim', null); // manual override (seconds). If unset, auto-detected.
const FONT_WAIT = parseFloat(arg('fontwait', '1.5')); // fallback when no __ready signal
const READY_TIMEOUT = parseFloat(arg('readytimeout', '8'));
const KEEP_CHROME = hasFlag('keep-chrome');
const HTML_ABS = path.resolve(HTML_FILE);
const BASENAME = path.basename(HTML_FILE, path.extname(HTML_FILE));
const DIR = path.dirname(HTML_ABS);
const TMP_DIR = path.join(DIR, '.video-tmp-' + Date.now() + '-' + process.pid);
const MP4_OUT = path.join(DIR, BASENAME + '.mp4');
// CSS to hide "chrome" elements during recording.
// Covers class-name conventions seen across skill-built animations,
// plus a `.no-record` explicit opt-out class.
const HIDE_CHROME_CSS = `
.no-record,
.progress, .progress-bar,
.counter, .tCur,
.phases, .phase-label, .phase,
.replay, button.replay,
.masthead, .kicker, .title,
.footer,
[data-role="chrome"], [data-record="hidden"] {
display: none !important;
}
`;
console.log(`▸ Rendering: ${HTML_FILE}`);
console.log(` size: ${WIDTH}x${HEIGHT} · duration: ${DURATION}s · hide-chrome: ${!KEEP_CHROME}`);
console.log(` output: ${MP4_OUT}`);
(async () => {
fs.mkdirSync(TMP_DIR, { recursive: true });
const browser = await chromium.launch();
const url = 'file://' + HTML_ABS;
// ── Phase 1: WARMUP (no recording, caches fonts/assets) ─────────────
console.log('▸ Warmup (caching fonts)…');
const warmupCtx = await browser.newContext({
viewport: { width: WIDTH, height: HEIGHT },
});
const warmupPage = await warmupCtx.newPage();
await warmupPage.goto(url, { waitUntil: 'networkidle' });
await warmupPage.waitForTimeout(FONT_WAIT * 1000);
await warmupCtx.close();
// ── Phase 2: RECORD (fresh context, animation from t=0) ─────────────
console.log('▸ Recording (clean start)…');
const recordCtx = await browser.newContext({
viewport: { width: WIDTH, height: HEIGHT },
deviceScaleFactor: 1,
recordVideo: {
dir: TMP_DIR,
size: { width: WIDTH, height: HEIGHT },
},
});
// Inject CSS + JS heuristic to hide "chrome" elements.
// Two layers:
// A. CSS selectors for common class-name conventions (cheap)
// B. JS heuristic for fixed-position bars containing buttons or time
// readouts (catches inline-styled chrome like <Stage> controls)
// Persists across reloads via addInitScript.
if (!KEEP_CHROME) {
await recordCtx.addInitScript(css => {
const HIDE_MARK = 'data-video-hidden';
function injectStyle() {
const style = document.createElement('style');
style.setAttribute('data-inject', 'render-video-chrome-hide');
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
}
function hideChromeBars() {
const vh = window.innerHeight;
document.querySelectorAll('div, nav, header, footer, section, aside')
.forEach(el => {
if (el.hasAttribute(HIDE_MARK)) return;
if (el.dataset.recordKeep === 'true') return;
const s = getComputedStyle(el);
if (s.position !== 'fixed' && s.position !== 'sticky') return;
const r = el.getBoundingClientRect();
// Only skinny bars (not full-screen overlays)
if (r.height > vh * 0.25) return;
const atBottom = r.bottom >= vh - 30;
const atTop = r.top <= 30 && r.height < 80;
if (!atBottom && !atTop) return;
// Chrome-like: contains button or scrubber/time glyphs
const txt = el.textContent || '';
const hasBtn = !!el.querySelector('button, [role="button"]');
const hasCtrls = /[⏸▶⏮⏭↻↺↩↪]|\d+\.\d+\s*s/.test(txt);
if (hasBtn || hasCtrls) {
el.style.setProperty('display', 'none', 'important');
el.setAttribute(HIDE_MARK, '1');
}
});
}
const start = () => {
injectStyle();
hideChromeBars();
// Re-run as React/Vue commits DOM changes
const obs = new MutationObserver(hideChromeBars);
obs.observe(document.body, { childList: true, subtree: true });
setTimeout(() => obs.disconnect(), 6000);
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start, { once: true });
} else {
start();
}
}, HIDE_CHROME_CSS);
}
// Record context opens page. The WebM starts writing the moment the
// context is created — so we track T0 here and measure how many seconds
// elapse before the animation is actually ready (Babel compile + React
// mount + fonts.ready). That elapsed time = exact trim offset.
const T0 = Date.now();
const page = await recordCtx.newPage();
await page.goto(url, { waitUntil: 'networkidle' });
// Wait for animation ready signal. Stage component (animations.jsx) sets
// window.__ready = true on its first rAF after mount + fonts.ready.
// Fallback: if HTML doesn't set __ready within READY_TIMEOUT, use fontwait.
let animationStartSec;
const hasReady = await page.waitForFunction(
() => window.__ready === true,
{ timeout: READY_TIMEOUT * 1000 },
).then(() => true).catch(() => false);
if (hasReady) {
animationStartSec = (Date.now() - T0) / 1000;
console.log(`▸ Ready at ${animationStartSec.toFixed(2)}s (from window.__ready)`);
} else {
await page.waitForTimeout(FONT_WAIT * 1000);
animationStartSec = (Date.now() - T0) / 1000;
console.log(`▸ No window.__ready signal; using fallback wait (${animationStartSec.toFixed(2)}s)`);
console.log(` tip: in animations.jsx-based HTML this is automatic;`);
console.log(` otherwise set window.__ready=true after your first paint.`);
}
// Now let the animation play out its full duration
await page.waitForTimeout(DURATION * 1000 + 300);
await page.close();
await recordCtx.close();
await browser.close();
const webmFiles = fs.readdirSync(TMP_DIR).filter(f => f.endsWith('.webm'));
if (webmFiles.length === 0) {
console.error('✗ No webm produced');
process.exit(1);
}
const webmPath = path.join(TMP_DIR, webmFiles[0]);
console.log(`▸ WebM: ${(fs.statSync(webmPath).size / 1024 / 1024).toFixed(1)} MB`);
// Resolve final trim offset:
// - manual --trim=X → use X (explicit user override)
// - otherwise → use animationStartSec (measured), with a tiny
// +0.05s nudge to clear the first Babel-commit frame
const resolvedTrim = TRIM_OVERRIDE !== null
? parseFloat(TRIM_OVERRIDE)
: animationStartSec + 0.05;
console.log(`▸ ffmpeg: trim=${resolvedTrim.toFixed(2)}s${TRIM_OVERRIDE !== null ? ' (manual)' : ' (auto)'}, encode H.264…`);
const ffmpeg = spawnSync('ffmpeg', [
'-y',
'-ss', String(resolvedTrim),
'-i', webmPath,
'-t', String(DURATION),
'-c:v', 'libx264',
'-pix_fmt', 'yuv420p',
'-crf', '18',
'-preset', 'medium',
'-movflags', '+faststart',
MP4_OUT,
], { stdio: ['ignore', 'ignore', 'pipe'] });
if (ffmpeg.status !== 0) {
console.error('✗ ffmpeg failed:\n' + ffmpeg.stderr.toString().slice(-2000));
process.exit(1);
}
fs.rmSync(TMP_DIR, { recursive: true, force: true });
const mp4Size = (fs.statSync(MP4_OUT).size / 1024 / 1024).toFixed(1);
console.log(`✓ Done: ${MP4_OUT} (${mp4Size} MB)`);
})();
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""
verify.py — Playwright封装,用于验证claude-design产出的HTML
Usage:
python verify.py path/to/design.html # 基础:打开+截图+抓控制台错误
python verify.py design.html --viewports 1920x1080,375x667 # 多viewport
python verify.py deck.html --slides 10 # 幻灯片逐页截(前10张)
python verify.py design.html --output ./screenshots/ # 输出目录
python verify.py design.html --show # 非headless,打开真实浏览器
依赖:
pip install playwright
playwright install chromium
"""
import argparse
import sys
import os
import time
from pathlib import Path
def parse_viewport(s):
w, h = s.split('x')
return {'width': int(w), 'height': int(h)}
def verify_html(html_path, viewports=None, slides=0, output_dir=None, show=False, wait=2000):
try:
from playwright.sync_api import sync_playwright
except ImportError:
print("ERROR: playwright未安装。")
print("运行: pip install playwright && playwright install chromium")
sys.exit(1)
html_path = Path(html_path).resolve()
if not html_path.exists():
print(f"ERROR: 文件不存在: {html_path}")
sys.exit(1)
if output_dir is None:
output_dir = html_path.parent / 'screenshots'
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
file_url = html_path.as_uri()
stem = html_path.stem
if viewports is None:
viewports = [{'width': 1440, 'height': 900}]
console_errors = []
page_errors = []
with sync_playwright() as p:
browser = p.chromium.launch(headless=not show)
for viewport in viewports:
context = browser.new_context(viewport=viewport, device_scale_factor=2)
page = context.new_page()
page.on("console", lambda msg: console_errors.append(f"[{msg.type}] {msg.text}") if msg.type in ("error", "warning") else None)
page.on("pageerror", lambda err: page_errors.append(str(err)))
print(f"\n→ 打开 {file_url} @ {viewport['width']}x{viewport['height']}")
page.goto(file_url, wait_until='networkidle')
page.wait_for_timeout(wait)
if slides > 0:
for i in range(slides):
screenshot_path = output_dir / f"{stem}-slide-{str(i + 1).zfill(2)}.png"
page.screenshot(path=str(screenshot_path), full_page=False)
print(f" ✓ slide {i+1} → {screenshot_path.name}")
if i < slides - 1:
page.keyboard.press('ArrowRight')
page.wait_for_timeout(500)
else:
suffix = f"-{viewport['width']}x{viewport['height']}" if len(viewports) > 1 else ""
screenshot_path = output_dir / f"{stem}{suffix}.png"
page.screenshot(path=str(screenshot_path), full_page=False)
print(f" ✓ 截图 → {screenshot_path.name}")
full_path = output_dir / f"{stem}{suffix}-full.png"
page.screenshot(path=str(full_path), full_page=True)
print(f" ✓ 完整页 → {full_path.name}")
if show:
print(" (浏览器窗口保持打开,按Enter关闭...)")
input()
context.close()
browser.close()
print("\n" + "=" * 50)
print("验证报告")
print("=" * 50)
if page_errors:
print(f"\n❌ Page Errors ({len(page_errors)}):")
for e in page_errors:
print(f" - {e}")
else:
print("\n✅ 无JavaScript错误")
if console_errors:
print(f"\n⚠️ Console Errors/Warnings ({len(console_errors)}):")
for e in console_errors[:20]:
print(f" - {e}")
if len(console_errors) > 20:
print(f" ... 还有{len(console_errors) - 20}条")
else:
print("✅ Console干净")
print(f"\n📸 截图保存至: {output_dir}")
return 0 if not page_errors else 1
def main():
parser = argparse.ArgumentParser(
description="Verify HTML design outputs with Playwright",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("html_path", help="HTML file path")
parser.add_argument("--viewports", default="1440x900",
help="逗号分隔的viewport列表,格式 WxH(默认 1440x900)")
parser.add_argument("--slides", type=int, default=0,
help="幻灯片模式:截取前N张(需要HTML支持ArrowRight翻页)")
parser.add_argument("--output", default=None,
help="输出目录(默认HTML所在目录的screenshots/)")
parser.add_argument("--show", action="store_true",
help="非headless,打开真实浏览器窗口")
parser.add_argument("--wait", type=int, default=2000,
help="打开页面后等待的毫秒数(默认2000)")
args = parser.parse_args()
viewports = [parse_viewport(v) for v in args.viewports.split(",")]
return verify_html(
html_path=args.html_path,
viewports=viewports,
slides=args.slides,
output_dir=args.output,
show=args.show,
wait=args.wait,
)
if __name__ == "__main__":
sys.exit(main())
+38
View File
@@ -0,0 +1,38 @@
[
{
"id": 1,
"prompt": "我想做一个SaaS产品的登录页面,给我3个风格方向对比看看",
"expected": "触发clarifying questions问design context/brand;产出3个variation的design_canvas;不用紫渐变/emoji/Inter等AI slop;有具体理由说明每个variation的差异维度",
"tests": "workflow问问题 + variations逻辑 + 反AI slop清单 + design_canvas使用"
},
{
"id": 2,
"prompt": "帮我做一份10页的产品pitch deck,讲一个AI工具的创业项目",
"expected": "用deck_stage.js起手;先口头vocalize设计系统(色彩/字型/layout节奏)等确认;Section divider/content/data/quote多种layout交替;字号≥24px;1-indexed labels",
"tests": "Junior Designer先汇报再做 + deck_stage使用 + 视觉节奏 + scale规范"
},
{
"id": 3,
"prompt": "做个30秒的HTML动画,讲神经网络怎么工作",
"expected": "用animations.jsx的Stage+Sprite;先写时间轴再写组件;入场easeOut出场easeIn;分phase讲故事而不是堆动画;文字停留≥3秒",
"tests": "animations工作流 + easing正确 + 节奏设计 + 时长控制"
},
{
"id": 4,
"prompt": "做一个 Habit Tracker App 原型",
"expected": "问用户要 overview 平铺 or flow demo(默认走 overview);用 assets/ios_frame.jsx,不手写 Dynamic Island;Tracker 属高密度型,每屏 ≥ 3 处信息密度元素(习惯完成率、连续天数、趋势曲线、成就badge等,非装饰);至少 5-7 屏并排(首页/新建习惯/详情/统计/设置)",
"tests": "overview/flow 形态路由 + ios_frame 硬绑定 + 信息密度分型(高密度型)+ 多屏并排"
},
{
"id": 5,
"prompt": "做一个读书笔记 App 原型",
"expected": "overview 平铺为主;ios_frame.jsx;读书笔记偏内容展示类,信息密度要求不如 Tracker 极端,但笔记列表页仍需 ≥ 3 层信息(书籍、引文、标签、进度);至少 4-6 屏(首页书架/笔记详情/标注高亮/搜索/笔记本管理);字体优先 serif display",
"tests": "overview 默认 + ios_frame + 信息层次 + 内容为主的视觉节奏"
},
{
"id": 6,
"prompt": "做一个跑步记录 App 原型",
"expected": "overview 平铺;ios_frame.jsx;跑步 App 属高密度型(地图、配速曲线、心率区间、每公里分段数据),每屏 ≥ 3 处产品差异化信息;至少 5 屏(今日总览/跑步中实时数据/路线地图/历史记录/月度统计);避免撞 AI slop(不用紫渐变、不堆装饰 icon,但数据可视化 icon 允许保留)",
"tests": "overview + ios_frame + 高密度型数据可视化 + 地图/图表混排 + slop 边界条件"
}
]