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>
No files matched your search
@@ -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
|
||||
@@ -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.
|
||||
@@ -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 可用
|
||||
@@ -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之前先问——这真的必要吗?
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
})();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
})();
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
@@ -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 推荐环节
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 114 KiB |
@@ -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>
|
||||
|
After Width: | Height: | Size: 35 KiB |
@@ -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>
|
||||
|
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>
|
||||
|
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>
|
||||
|
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>
|
||||
|
After Width: | Height: | Size: 159 KiB |
@@ -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">τ²-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>
|
||||
|
After Width: | Height: | Size: 82 KiB |
@@ -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">τ²-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>
|
||||
|
After Width: | Height: | Size: 99 KiB |
@@ -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">τ²-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">τ²-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>
|
||||
|
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>
|
||||
|
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>
|
||||
|
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>
|
||||
|
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>
|
||||
|
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>
|
||||
|
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>
|
||||
|
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>
|
||||
model=<span class="str">"auto"</span>, <span class="cmt"># intelligently routes</span><br>
|
||||
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>
|
||||
|
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>
|
||||
model=<span class="str">"auto"</span>, <span class="cmt"># intelligently routes</span><br>
|
||||
messages=[{<br>
|
||||
<span class="str">"role"</span>: <span class="str">"user"</span>,<br>
|
||||
<span class="str">"content"</span>: <span class="str">"Hello!"</span><br>
|
||||
}]<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>
|
||||
|
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>
|
||||
model=<span class="str">"auto"</span>, <span class="cmt"># intelligently routes</span><br>
|
||||
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>
|
||||
|
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>
|
||||
|
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>
|
||||
|
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>
|
||||
|
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>
|
||||
|
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>
|
||||
|
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>
|
||||
|
After Width: | Height: | Size: 121 KiB |
@@ -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 帧验证是动画初始状态?
|
||||
@@ -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%的情况够用。
|
||||
@@ -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的征兆**。先做最简的版本,只在用户要求时加。
|
||||
@@ -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
|
||||
@@ -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的情况,优先问用户要,而不是硬上**。
|
||||
@@ -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 执行生成
|
||||
@@ -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, {...})`导出要共享的东西。
|
||||
@@ -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
|
||||
@@ -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` 残留,确认都清理了
|
||||
@@ -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(...);
|
||||
};
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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 倍) |
|
||||
@@ -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,结束。
|
||||
@@ -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)"
|
||||
@@ -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"
|
||||
@@ -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); });
|
||||
@@ -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); });
|
||||
@@ -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;
|
||||
@@ -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)`);
|
||||
})();
|
||||
@@ -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())
|
||||
@@ -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 边界条件"
|
||||
}
|
||||
]
|
||||