Merge branch 'main' of github.com:nexu-io/open-design

This commit is contained in:
pftom
2026-04-28 16:20:14 +08:00
37 changed files with 2055 additions and 264 deletions
+266
View File
@@ -0,0 +1,266 @@
# Contributing to Open Design
Thanks for thinking about contributing. OD is small on purpose — most of the value lives in **files** (skills, design systems, prompt fragments) rather than framework code. That means the highest-leverage contributions are usually one folder, one Markdown file, or one PR-sized adapter.
This guide tells you exactly where to look for each type of contribution and what bar a PR has to clear before we merge it.
<p align="center"><b>English</b> · <a href="CONTRIBUTING.zh-CN.md">简体中文</a></p>
---
## Three things you can ship in one afternoon
| If you want to… | You're really adding | Where it lives | Ship size |
|---|---|---|---|
| Make OD render a new kind of artifact (an invoice, an iOS Settings screen, a one-pager…) | a **Skill** | [`skills/<your-skill>/`](skills/) | one folder, ~2 files |
| Make OD speak a new brand's visual language | a **Design System** | [`design-systems/<brand>/DESIGN.md`](design-systems/) | one Markdown file |
| Hook up a new coding-agent CLI | an **Agent adapter** | [`daemon/agents.js`](daemon/agents.js) | ~10 lines in one array |
| Add a feature, fix a bug, lift a UX pattern from [`open-codesign`][ocod] | code | `src/`, `daemon/` | normal PR |
| Improve docs, port a section to 中文, fix typos | docs | `README.md`, `README.zh-CN.md`, `docs/`, `QUICKSTART.md` | one PR |
If you're not sure which bucket your idea is in, [open a discussion / issue first](https://github.com/nexu-io/open-design/issues/new) and we'll point you at the right surface.
---
## Local setup
The full one-page setup lives in [`QUICKSTART.md`](QUICKSTART.md). The TL;DR for contributors:
```bash
git clone https://github.com/nexu-io/open-design.git
cd open-design
pnpm install # or npm install
pnpm dev:all # daemon (:7456) + Vite (:5173)
pnpm typecheck # tsc -b --noEmit
pnpm build # production build
```
Node 18+ is required. macOS, Linux, and WSL2 are tested daily. Windows native should work but isn't a primary target — file an issue if it doesn't.
You don't need any agent CLI on your `PATH` to develop OD itself — the daemon will tell you "no agents found" and fall back to the **Anthropic API · BYOK** path, which is the fastest dev loop anyway.
---
## Adding a new Skill
A skill is a folder under [`skills/`](skills/) with a `SKILL.md` at the root, following Claude Code's [`SKILL.md` convention][skill] plus our optional `od:` extension. **No registration step.** Drop the folder in, restart the daemon, the picker shows it.
### Skill folder layout
```text
skills/your-skill/
├── SKILL.md # required
├── assets/template.html # optional but recommended — the seed file
├── references/ # optional — knowledge files the agent reads
│ ├── layouts.md
│ ├── components.md
│ └── checklist.md
└── example.html # strongly recommended — a real, hand-built sample
```
### `SKILL.md` frontmatter
The first three keys are the Claude Code base spec — `name`, `description`, `triggers`. Everything under `od:` is OD-specific and optional, but **`od.mode`** decides which group the skill shows up in (Prototype / Deck / Template / Design system).
```yaml
---
name: your-skill
description: |
One-paragraph elevator pitch. The agent reads this verbatim to decide
if the user's brief matches. Be concrete: surface, audience, what's in
the artifact, what's not.
triggers:
- "your trigger phrase"
- "another phrase"
- "中文触发词"
od:
mode: prototype # prototype | deck | template | design-system
platform: desktop # desktop | mobile
scenario: marketing # free-form tag for grouping
featured: 1 # any positive integer surfaces it under "Showcase examples"
preview:
type: html # html | jsx | pptx | markdown
entry: index.html
design_system:
requires: true # does the skill read the active DESIGN.md?
sections: [color, typography, layout, components]
example_prompt: "A copy-pastable prompt that nicely shows what this skill does."
---
# Your Skill
Body is free-form Markdown describing the workflow the agent should follow…
```
The full grammar — typed inputs, slider parameters, capability gating — lives in [`docs/skills-protocol.md`](docs/skills-protocol.md).
### Bar for merging a new skill
We're picky about skills because they're the user-facing surface. A new skill must:
1. **Ship a real `example.html`.** Hand-built, opens straight from disk, looks like something a designer would actually deliver. No lorem ipsum, no `<svg><rect/></svg>` placeholder hero. If you can't build the example yourself, the skill probably isn't ready.
2. **Pass the anti-AI-slop checklist** in the body. No purple gradients, no generic emoji icons, no rounded card with a left-border accent, no Inter as a *display* face, no invented stats. Read the **Anti-AI-slop machinery** section of the README for the full list.
3. **Honest placeholders.** When the agent doesn't have a real number, write `—` or a labelled grey block, not "10× faster".
4. **Have a `references/checklist.md`** with at least P0 gates (the stuff the agent has to pass before emitting `<artifact>`). Lift the format from [`skills/guizang-ppt/references/checklist.md`](skills/guizang-ppt/) or [`skills/dating-web/references/checklist.md`](skills/dating-web/).
5. **Add a screenshot** at `docs/screenshots/skills/<skill>.png` if the skill is featured. PNG, ~1024×640 retina, captured from the real `example.html` at zoomed-out browser scale.
6. **Be a single self-contained folder.** No CDN imports beyond what other skills already use; no fonts you didn't license; no images larger than ~250 KB.
If you fork an existing skill (e.g. start from `dating-web` and remix into a `recruiting-web`), keep the original LICENSE and authorship in `references/` and call it out in your PR description.
### Skills that already ship — pick one to imitate
- Visual showcase, single-screen prototype: [`skills/dating-web/`](skills/dating-web/), [`skills/digital-eguide/`](skills/digital-eguide/)
- Multi-frame mobile flow: [`skills/mobile-onboarding/`](skills/mobile-onboarding/), [`skills/gamified-app/`](skills/gamified-app/)
- Document / template (no design system required): [`skills/pm-spec/`](skills/pm-spec/), [`skills/weekly-update/`](skills/weekly-update/)
- Deck mode: [`skills/guizang-ppt/`](skills/guizang-ppt/) (bundled verbatim from [op7418/guizang-ppt-skill][guizang]) and [`skills/simple-deck/`](skills/simple-deck/)
---
## Adding a new Design System
A design system is a single [`DESIGN.md`](design-systems/README.md) file under `design-systems/<slug>/`. **One file, no code.** Drop it in, restart the daemon, the picker shows it grouped by category.
### Design system folder layout
```text
design-systems/your-brand/
└── DESIGN.md
```
### `DESIGN.md` shape
```markdown
# Design System Inspired by YourBrand
> Category: Developer Tools
> One-line summary that shows in the picker preview.
## 1. Visual Theme & Atmosphere
## 2. Color
- Primary: `#hex` / `oklch(...)`
-
## 3. Typography
## 4. Spacing & Grid
## 5. Layout & Composition
## 6. Components
## 7. Motion & Interaction
## 8. Voice & Brand
## 9. Anti-patterns
```
The 9-section schema is fixed — that's what skill bodies grep for. The first H1 becomes the picker label (the `Design System Inspired by` prefix is stripped automatically), and the `> Category: …` line decides which group it lands in. Existing categories are listed in [`design-systems/README.md`](design-systems/README.md); if your brand truly doesn't fit, you can introduce a new one, but **try existing categories first**.
### Bar for merging a new design system
1. **All 9 sections present.** Empty section bodies are fine for hard-to-find data (e.g. motion tokens), but the headings have to be there or the prompt grep breaks.
2. **Hex codes are real.** Sample directly from the brand's site or product, not from memory or AI guesses. The README's "brand-spec extraction" 5-step protocol applies to maintainers too.
3. **OKLch values for accent colors** are nice-to-have. They make palettes lerp predictably across light/dark.
4. **No marketing fluff.** The brand's tagline is not a design token. Cut it.
5. **Slug uses ASCII**`linear.app` becomes `linear-app`, `x.ai` becomes `x-ai`. The 69 imported systems already follow this convention; mirror it.
The 69 product systems we ship are imported from [`VoltAgent/awesome-design-md`][acd2] via [`scripts/sync-design-systems.mjs`](scripts/sync-design-systems.mjs). If your brand belongs upstream, **send the PR there first** — we'll pick it up automatically on the next sync. The `design-systems/` folder is for systems that don't fit upstream, plus our two hand-authored starters.
---
## Adding a new coding-agent CLI
Hooking up a new agent (e.g. some new shop's `foo-coder` CLI) is one entry in [`daemon/agents.js`](daemon/agents.js):
```javascript
{
id: 'foo',
name: 'Foo Coder',
bin: 'foo',
versionArgs: ['--version'],
buildArgs: (prompt) => ['exec', '-p', prompt],
streamFormat: 'plain', // or 'claude-stream-json' if it speaks that
}
```
That's it — daemon will detect it on `PATH`, the picker shows it, the chat path works. If the CLI emits **typed events** (like Claude Code's `--output-format stream-json`), wire a parser in [`daemon/claude-stream.js`](daemon/claude-stream.js) and set `streamFormat: 'claude-stream-json'`.
Bar for merging:
1. **A real session works end-to-end** with the new agent — paste the daemon log into the PR description showing it streamed an artifact through.
2. **`docs/agent-adapters.md`** is updated with the CLI's quirks (does it require a key file? does it support image input? what's its non-interactive flag?).
3. **The README's "Supported coding agents" table** gets one row.
---
## Code style
We're not pedantic about formatting (Prettier on save is fine), but two rules are non-negotiable because they show up in the prompt stack and the user-facing API:
1. **Single quotes in JS/TS.** Strings are single-quoted unless escaping makes them ugly. The codebase is already consistent — please match.
2. **Comments in English.** Even if the PR is translating something into 中文, code comments stay in English so we can keep one set of greppable references.
Beyond that:
- **Don't narrate.** No `// import the module`, no `// loop through items`. If the code reads obviously, the comment is noise. Save comments for non-obvious intent or constraints the code can't express.
- **TypeScript** for `src/`. The daemon (`daemon/`) is plain ESM JavaScript with JSDoc when types matter — keep it that way.
- **No new top-level dependencies** without a paragraph in the PR description on what we get vs. what bytes we ship. The dep list in [`package.json`](package.json) is small on purpose.
- **Run `pnpm typecheck`** before pushing. CI runs it; failing it earns a "please fix" comment.
---
## Commits & pull requests
- **One concern per PR.** Adding a skill + refactoring the parser + bumping a dep is three PRs.
- **Title is imperative + scope.** `add dating-web skill`, `fix daemon SSE backpressure when CLI hangs`, `docs: clarify .od layout`.
- **Body explains the why.** "What does this do" is usually obvious from the diff; "why does this need to exist" rarely is.
- **Reference an issue** if there is one. If there isn't and the PR is non-trivial, open one first so we can agree the change is wanted before you spend the time.
- **No squash-during-review.** Push fixups; we'll squash on merge.
- **No force-push to a shared branch** unless the reviewer asked.
We don't enforce a CLA. Apache-2.0 covers us; your contribution is licensed under the same.
---
## Reporting bugs
Open an issue with:
- What you ran (the exact `pnpm dev:all` / `npm start` invocation).
- Which agent CLI was selected (or whether you were on the BYOK path).
- The skill + design system pair that triggered it.
- The relevant **daemon stderr tail** — most "the artifact never rendered" reports get diagnosed in 30 seconds when we can see `spawn ENOENT` or the CLI's actual error.
- A screenshot if it's UI.
For prompt-stack bugs ("the agent emitted a purple gradient hero, the slop blacklist was supposed to forbid that"), include the **full assistant message** so we can see whether the violation was the model or the prompt.
---
## Asking questions
- Architecture question, design question, "is this a bug or a misuse" → [GitHub Discussions](https://github.com/nexu-io/open-design/discussions) (preferred — searchable for the next person).
- "How do I write a skill that does X" → Open a discussion. We'll answer it and turn the answer into [`docs/skills-protocol.md`](docs/skills-protocol.md) if it's a missing pattern.
---
## What we don't accept
To keep the project focused, please don't open PRs that:
- **Vendor a model runtime.** OD's whole bet is "your existing CLI is enough". We don't ship `pi-ai`, OpenAI keys, or model loaders.
- **Add a new framework to the frontend.** Vite + React 18 + TS is the line. No Next.js, Astro, Solid, Svelte rewrites.
- **Replace the daemon with a serverless function.** The daemon's whole point is owning a real `cwd` and spawning a real CLI. Vercel deployment of the SPA is fine; the daemon stays a daemon.
- **Add telemetry / analytics / phone-home.** OD is local-first. The only outbound calls are to providers the user explicitly configured.
- **Bundle a binary** without a license file and authorship attribution next to it.
If you're not sure whether your idea fits, open a discussion before writing the code.
---
## License
By contributing, you agree your contribution is licensed under the [Apache-2.0 License](LICENSE) of this repository, with the exception of files inside [`skills/guizang-ppt/`](skills/guizang-ppt/), which retain their original MIT license and authorship attribution to [op7418](https://github.com/op7418).
[skill]: https://docs.anthropic.com/en/docs/claude-code/skills
[guizang]: https://github.com/op7418/guizang-ppt-skill
[acd2]: https://github.com/VoltAgent/awesome-design-md
[ocod]: https://github.com/OpenCoworkAI/open-codesign
+265
View File
@@ -0,0 +1,265 @@
# 贡献指南 · Contributing to Open Design
谢谢你愿意参与。OD 是有意做小的 —— 大部分价值在 **文件** 里(skill、design system、提示词片段),而不是框架代码。这意味着收益最高的贡献往往就是一个文件夹、一份 Markdown,或者一个 PR 大小的 adapter。
这份指南会告诉你:每种贡献该往哪里看、合并之前 PR 需要过哪些线。
<p align="center"><a href="CONTRIBUTING.md">English</a> · <b>简体中文</b></p>
---
## 一个下午就能交付的三件事
| 你想要…… | 你其实在加的是 | 它住在哪 | 体量 |
|---|---|---|---|
| 让 OD 渲染一种新的 artifact(一份发票、一个 iOS 设置页、一张 one-pager……) | 一个 **Skill** | [`skills/<your-skill>/`](skills/) | 一个文件夹,约 2 个文件 |
| 让 OD 说一种新品牌的视觉语言 | 一套 **Design System** | [`design-systems/<brand>/DESIGN.md`](design-systems/) | 一个 Markdown 文件 |
| 接入一个新的 coding-agent CLI | 一个 **Agent adapter** | [`daemon/agents.js`](daemon/agents.js) | 一个数组里 ~10 行 |
| 加功能、修 bug、从 [`open-codesign`][ocod] 移植一个 UX 模式 | 代码 | `src/``daemon/` | 普通 PR |
| 改文档、补中文翻译、修错别字 | 文档 | `README.md``README.zh-CN.md``docs/``QUICKSTART.md` | 一个 PR |
不确定自己想做的属于哪一桶?[先开 issue / discussion](https://github.com/nexu-io/open-design/issues/new),我们告诉你该改哪个面。
---
## 本地起跑
完整的一页式 setup 在 [`QUICKSTART.md`](QUICKSTART.md)。给贡献者的 TL;DR
```bash
git clone https://github.com/nexu-io/open-design.git
cd open-design
pnpm install # 或 npm install
pnpm dev:all # daemon (:7456) + Vite (:5173)
pnpm typecheck # tsc -b --noEmit
pnpm build # 生产构建
```
要求 Node 18+。macOS、Linux、WSL2 每天都在跑。Windows 原生应该能跑但不是主要目标 —— 跑不起来请开 issue。
**开发 OD 本身不需要在 `PATH` 上装任何 agent CLI** —— daemon 会告诉你「找不到 agent」并落到 **Anthropic API · BYOK** 路径,反而是最快的开发循环。
---
## 加一个 Skill
一个 skill 就是 [`skills/`](skills/) 下的一个文件夹,根目录放一个 `SKILL.md`,遵循 Claude Code 的 [`SKILL.md` 规范][skill],再加上我们可选的 `od:` 扩展。**没有注册步骤。** 文件夹丢进来、重启 daemon、picker 里就出现了。
### Skill 文件夹结构
```text
skills/your-skill/
├── SKILL.md # 必须
├── assets/template.html # 可选但强烈推荐 —— seed 模板
├── references/ # 可选 —— agent 在规划阶段会读的知识文件
│ ├── layouts.md
│ ├── components.md
│ └── checklist.md
└── example.html # 强烈推荐 —— 一份手搓的真实样例
```
### `SKILL.md` 的 frontmatter
前三个字段是 Claude Code 的基础规范 —— `name``description``triggers``od:` 下面所有字段都是 OD 特有的、可选的,但 **`od.mode`** 决定 skill 出现在哪一组(Prototype / Deck / Template / Design system)。
```yaml
---
name: your-skill
description: |
一段电梯演讲。Agent 会原样读这段来判断用户的需求是否匹配。
写具体一点:surface、受众、artifact 里有什么、没有什么。
triggers:
- "your trigger phrase"
- "another phrase"
- "中文触发词"
od:
mode: prototype # prototype | deck | template | design-system
platform: desktop # desktop | mobile
scenario: marketing # 自由 tag,用来分组
featured: 1 # 任何正整数都会让它出现在「Showcase examples」
preview:
type: html # html | jsx | pptx | markdown
entry: index.html
design_system:
requires: true # 这个 skill 是否会读激活的 DESIGN.md
sections: [color, typography, layout, components]
example_prompt: "一段可复制粘贴的提示词,最能体现这个 skill 的能力。"
---
# Your Skill
正文是自由 Markdown,描述 agent 应该走的工作流……
```
完整 grammar —— 类型化输入、滑块参数、能力 gating —— 在 [`docs/skills-protocol.md`](docs/skills-protocol.md)。
### 合并新 skill 的硬线
Skill 是用户直接看到的面,所以我们对它挑剔。一个新 skill 必须:
1. **附一份真实的 `example.html`。** 手搓的、本地直接打开就能看、像设计师真的会交付的东西。不要 lorem ipsum,不要 `<svg><rect/></svg>` 占位 hero。如果你自己都不能搓出 example,这个 skill 大概率还没准备好。
2. **过 anti-AI-slop checklist**(写在 body 里)。不准紫色渐变、不准通用 emoji 图标、不准左 border 圆角卡片、不准把 Inter 当 *display* 字体、不准自编数据。完整黑名单看 README 的「Anti-AI-slop machinery」一节。
3. **诚实占位。** Agent 没真数字时写 `—` 或一个标注的灰块,绝不写「快 10 倍」。
4. **附 `references/checklist.md`**,至少要有 P0 关卡(agent emit `<artifact>` 之前必须过的硬线)。格式照搬 [`skills/guizang-ppt/references/checklist.md`](skills/guizang-ppt/) 或 [`skills/dating-web/references/checklist.md`](skills/dating-web/)。
5. **如果是 featured skill,加一张截图**`docs/screenshots/skills/<skill>.png`。PNG 格式,约 1024×640 retina,从真实 `example.html` 上以缩小后的浏览器倍率截。
6. **是一个自包含文件夹。** CDN 引入不能超过其他 skill 已经引入的;不准用没授权的字体;图片不要超过约 250 KB。
如果你 fork 了一个现有 skill(比如从 `dating-web` 改成 `recruiting-web`),保留原 LICENSE 和原作者归属在 `references/` 里,并在 PR 描述里点出来。
### 已有的 skill —— 挑一个像的来抄
- 视觉 showcase、单屏原型:[`skills/dating-web/`](skills/dating-web/)、[`skills/digital-eguide/`](skills/digital-eguide/)
- 多屏移动流程:[`skills/mobile-onboarding/`](skills/mobile-onboarding/)、[`skills/gamified-app/`](skills/gamified-app/)
- 文档 / 模板(不需要 design system):[`skills/pm-spec/`](skills/pm-spec/)、[`skills/weekly-update/`](skills/weekly-update/)
- Deck 模式:[`skills/guizang-ppt/`](skills/guizang-ppt/)(来自 [op7418/guizang-ppt-skill][guizang],原样捆绑)和 [`skills/simple-deck/`](skills/simple-deck/)
---
## 加一套 Design System
一套 design system 就是 `design-systems/<slug>/` 下的一个 [`DESIGN.md`](design-systems/README.md) 文件。**一个文件,零代码。** 丢进来、重启 daemon、picker 按 category 分组显示出来。
### Design system 文件夹结构
```text
design-systems/your-brand/
└── DESIGN.md
```
### `DESIGN.md` 形态
```markdown
# Design System Inspired by YourBrand
> Category: Developer Tools
> 一行总结,会显示在 picker 的预览里。
## 1. Visual Theme & Atmosphere
## 2. Color
- Primary: `#hex` / `oklch(...)`
-
## 3. Typography
## 4. Spacing & Grid
## 5. Layout & Composition
## 6. Components
## 7. Motion & Interaction
## 8. Voice & Brand
## 9. Anti-patterns
```
9 段式 schema 是固定的 —— skill body 会按这个结构 grep 内容。第一行 H1 会成为 picker 的标签(`Design System Inspired by` 前缀会被自动剥掉),`> Category: …` 那一行决定它落到哪个组。已有的 category 列表在 [`design-systems/README.md`](design-systems/README.md);如果你的品牌真的塞不进任何一个,可以新增 category,但**优先尝试现有 category**。
### 合并新 design system 的硬线
1. **9 个 section 都要在。** Section 内容空着可以(比如真的找不到 motion token),但标题必须保留,否则提示词的 grep 会断。
2. **Hex 是真的。** 直接从品牌官网或产品里取色,不准从记忆里掏,不准让 AI 猜。README 里那套 5 步「品牌资产协议」对维护者一样适用。
3. **强调色给 OKLch 是加分项。** 让色板在亮 / 暗模式之间能可预测地 lerp。
4. **不要营销废话。** 品牌的 tagline 不是设计 token。删掉。
5. **slug 用 ASCII** —— `linear.app` 写成 `linear-app``x.ai` 写成 `x-ai`。已经导入的 69 套都遵循这个约定,跟着写。
我们内置的 69 套产品系统是通过 [`scripts/sync-design-systems.mjs`](scripts/sync-design-systems.mjs) 从 [`VoltAgent/awesome-design-md`][acd2] 导入的。如果你的品牌应该归属在上游,**请先把 PR 发到那里** —— 我们下一次同步会自动收上来。`design-systems/` 文件夹用来放那些**不适合归到上游**的系统、加上我们手写的两套 starter。
---
## 接入一个新的 coding-agent CLI
接入一个新 agent(比如某个新 shop 的 `foo-coder` CLI)就是在 [`daemon/agents.js`](daemon/agents.js) 里加一项:
```javascript
{
id: 'foo',
name: 'Foo Coder',
bin: 'foo',
versionArgs: ['--version'],
buildArgs: (prompt) => ['exec', '-p', prompt],
streamFormat: 'plain', // 如果它说 claude-stream-json 就写那个
}
```
完事 —— daemon 会在 `PATH` 上检测到它、picker 显示出来、对话路径就通了。如果这个 CLI 吐 **类型化事件**(像 Claude Code 的 `--output-format stream-json`),在 [`daemon/claude-stream.js`](daemon/claude-stream.js) 里写一个 parser,并把 `streamFormat` 设成 `'claude-stream-json'`
合并硬线:
1. **真的跑通一次端到端会话** —— 把 daemon 日志贴在 PR 描述里,证明它流出了一个 artifact。
2. **更新 [`docs/agent-adapters.md`](docs/agent-adapters.md)**,写清楚这个 CLI 的怪癖(要不要 key 文件?支不支持图片输入?非交互模式的 flag 是什么?)。
3. **README 的「Supported coding agents」表里加一行**
---
## 代码风格
格式我们不抠(保存时跑 Prettier 就行),但有两条不能让 —— 因为它们出现在提示词栈和用户可见的 API 里:
1. **JS/TS 用单引号。** 字符串一律单引号,除非转义太丑。代码库已经是一致的,请保持一致。
2. **代码注释用英文。** 即使 PR 是把某段翻译成中文,代码注释也保留英文,这样我们能维护一份可 grep 的引用集。
除此之外:
- **不要写废话注释。** 不要 `// 引入这个模块`、不要 `// 遍历元素`。如果代码本身一眼能读,注释就是噪音。注释只用来说明非显而易见的意图、或者代码本身表达不出来的约束。
- **`src/` 用 TypeScript。** Daemon (`daemon/`) 是纯 ESM JavaScript,类型重要的地方用 JSDoc —— 保持这样。
- **不要随便加顶层依赖。** PR 描述里至少要有一段,说明引入它能换到什么、又新增了多少 bundle 字节。[`package.json`](package.json) 的依赖少是有意为之。
- **推之前跑 `pnpm typecheck`。** CI 会跑;挂了会换来一句「请修一下」。
---
## Commit 与 PR
- **一个 PR 只做一件事。** 加 skill + 重构 parser + 升依赖,是三个 PR。
- **标题用动词起头 + 范围。** `add dating-web skill``fix daemon SSE backpressure when CLI hangs``docs: clarify .od layout`
- **正文解释 why。** 「这个 PR 改了什么」从 diff 一般能看出来;「为什么要改」很少能。
- **如果有 issue,引用它。** 没有、且改动非平凡,请先开 issue 让我们先就「值不值得做」达成一致,再投入时间。
- **Review 期间不要 squash。** 推 fixup commitmerge 时我们会 squash。
- **不要 force-push 共享分支**,除非 reviewer 主动让你这么做。
我们不强制 CLA。Apache-2.0 已经覆盖;你的贡献按同样的 license 授权。
---
## 报 bug
开 issue 时请带上:
- 你跑的命令(精确到 `pnpm dev:all` / `npm start`)。
- 选中的 agent CLI 是哪个(或者你走的是 BYOK 路径)。
- 触发问题时的 skill + design system 组合。
- 相关的 **daemon stderr 末尾几行** —— 大多数「artifact 没渲染出来」的报告,看到 `spawn ENOENT` 或 CLI 实际报错后 30 秒就能定位。
- UI 问题贴一张截图。
提示词栈相关的 bug(「agent 吐了一个紫色渐变 hero,slop 黑名单不是禁了吗」),请贴 **完整的助手消息**,方便我们判断违规来自模型还是提示词。
---
## 提问
- 架构问题、设计问题、「这是 bug 还是误用」 → 请用 [GitHub Discussions](https://github.com/nexu-io/open-design/discussions)(首选 —— 下一个人能搜到)。
- 「我想写一个干 X 的 skill 怎么写」 → 开一个 discussion。我们会回答,且如果是缺失的模式,答案会被收进 [`docs/skills-protocol.md`](docs/skills-protocol.md)。
---
## 我们不接收的 PR
为了保持项目聚焦,请不要发以下类型的 PR:
- **Vendor 一个模型运行时。** OD 整个赌注就是「你已有的 CLI 就够了」。我们不带 `pi-ai`、不带 OpenAI key、不带模型加载器。
- **给前端加一个新框架。** Vite + React 18 + TS 是底线。不要 Next.js / Astro / Solid / Svelte 改写。
- **把 daemon 换成 serverless function。** Daemon 的存在意义就是拥有真实的 `cwd` 和 spawn 真实的 CLI。SPA 部署 Vercel 没问题,daemon 仍然是 daemon。
- **加 telemetry / 分析 / phone-home。** OD 是 local-first。唯一的对外请求是用户明确配置的 provider。
- **打包二进制** 而没有附 license 文件和原作者归属。
不确定自己的想法合不合适?开个 discussion 再写代码。
---
## License
提交贡献即代表你同意你的贡献按本仓库的 [Apache-2.0 License](LICENSE) 授权。例外是 [`skills/guizang-ppt/`](skills/guizang-ppt/) 下的所有文件,保留它们原始的 MIT license 和原作者 [op7418](https://github.com/op7418) 的归属。
[skill]: https://docs.anthropic.com/en/docs/claude-code/skills
[guizang]: https://github.com/op7418/guizang-ppt-skill
[acd2]: https://github.com/VoltAgent/awesome-design-md
[ocod]: https://github.com/OpenCoworkAI/open-codesign
+106 -68
View File
@@ -1,9 +1,9 @@
# Open Design
> **Claude Code, but for design.** A local-first, web-deployable open replica of Anthropic's [Claude Design][cd] — your existing coding agent (Claude Code, Codex, Cursor Agent, Gemini CLI, OpenCode, Qwen) becomes the design engine, driven by composable **Skills** and **71 brand-grade Design Systems**.
> **The open-source alternative to [Claude Design][cd].** Local-first, web-deployable, BYOK at every layer — your existing coding agent (Claude Code, Codex, Cursor Agent, Gemini CLI, OpenCode, Qwen) becomes the design engine, driven by **19 composable Skills** and **71 brand-grade Design Systems**.
<p align="center">
<img src="docs/assets/banner.svg" alt="Open Design banner — placeholder, replace with the live UI hero shot" width="100%" />
<img src="docs/assets/banner.png" alt="Open Design — editorial cover: design with the agent on your laptop" width="100%" />
</p>
<p align="center">
@@ -20,13 +20,13 @@
## Why this exists
Anthropic's [Claude Design][cd] (released 2026-04-17, Opus 4.7) showed what happens when an LLM stops writing prose and starts shipping design artifacts. It went viral — and stayed closed, paid-only, cloud-only, locked to Anthropic's model and Anthropic's skills.
Anthropic's [Claude Design][cd] (released 2026-04-17, Opus 4.7) showed what happens when an LLM stops writing prose and starts shipping design artifacts. It went viral — and stayed closed-source, paid-only, cloud-only, locked to Anthropic's model and Anthropic's skills. There is no checkout, no self-host, no Vercel deploy, no swap-in-your-own-agent.
**Open Design (OD) is the open substrate.** We don't build an agent — the strongest coding agents already live on your laptop. We wire them into a skill-driven design workflow that runs on `pnpm dev`, deploys to Vercel, and stays BYOK at every layer.
**Open Design (OD) is the open-source alternative.** Same loop, same artifact-first mental model, none of the lock-in. We don't ship an agent — the strongest coding agents already live on your laptop. We wire them into a skill-driven design workflow that runs on `pnpm dev`, deploys to Vercel, and stays BYOK at every layer.
Type `make me a magazine-style pitch deck for our seed round`. The interactive question form pops up before the model improvises a single pixel. The agent picks one of five curated visual directions. A live `TodoWrite` plan streams into the UI. The daemon builds a real on-disk project folder with a seed template, layout library, and self-check checklist. The agent reads them — pre-flight enforced — runs a five-dimensional critique against its own output, and emits a single `<artifact>` that renders in a sandboxed iframe seconds later.
That's not "AI tries to design something". That's an AI that has been trained, by the prompt stack, to behave like a senior designer with a working filesystem, a deterministic palette library, and a checklist culture.
That's not "AI tries to design something". That's an AI that has been trained, by the prompt stack, to behave like a senior designer with a working filesystem, a deterministic palette library, and a checklist culture — exactly the bar Claude Design set, but open and yours.
OD stands on four open-source shoulders:
@@ -52,51 +52,130 @@ OD stands on four open-source shoulders:
## Demo
> **Screenshots pending.** Every card below is a placeholder SVG — replace `docs/screenshots/*.svg` with real `.png`/`.jpg` captures and bump the `<img src=…>` extension when you do.
<table>
<tr>
<td width="50%">
<img src="docs/screenshots/01-entry-view.svg" alt="01 · Entry view — placeholder, replace with the real screenshot" /><br/>
<img src="docs/screenshots/01-entry-view.png" alt="01 · Entry view" /><br/>
<sub><b>Entry view</b> — pick a skill, pick a design system, type the brief. The same surface for prototypes, decks, mobile apps, dashboards, and editorial pages.</sub>
</td>
<td width="50%">
<img src="docs/screenshots/02-question-form.svg" alt="02 · Discovery form — placeholder, replace with the real screenshot" /><br/>
<img src="docs/screenshots/02-question-form.png" alt="02 · Turn-1 discovery form" /><br/>
<sub><b>Turn-1 discovery form</b> — before the model writes a pixel, OD locks the brief: surface, audience, tone, brand context, scale. 30 seconds of radios beats 30 minutes of redirects.</sub>
</td>
</tr>
<tr>
<td width="50%">
<img src="docs/screenshots/03-direction-picker.svg" alt="03 · Direction picker — placeholder, replace with the real screenshot" /><br/>
<img src="docs/screenshots/03-direction-picker.png" alt="03 · Direction picker" /><br/>
<sub><b>Direction picker</b> — when the user has no brand, the agent emits a second form with 5 curated directions (Monocle / Modern Minimal / Tech Utility / Brutalist / Soft Warm). One radio click → a deterministic palette + font stack, no model freestyle.</sub>
</td>
<td width="50%">
<img src="docs/screenshots/04-todo-progress.svg" alt="04 · Live todo progress — placeholder, replace with the real screenshot" /><br/>
<img src="docs/screenshots/04-todo-progress.png" alt="04 · Live todo progress" /><br/>
<sub><b>Live todo progress</b> — the agent's plan streams as a live card. <code>in_progress</code> → <code>completed</code> updates land in real time. The user can redirect cheaply, mid-flight.</sub>
</td>
</tr>
<tr>
<td width="50%">
<img src="docs/screenshots/05-preview-iframe.svg" alt="05 · Sandboxed preview — placeholder, replace with the real screenshot" /><br/>
<img src="docs/screenshots/05-preview-iframe.png" alt="05 · Sandboxed preview" /><br/>
<sub><b>Sandboxed preview</b> — every <code>&lt;artifact&gt;</code> renders in a clean srcdoc iframe. Editable in place via the file workspace; downloadable as HTML, PDF, ZIP.</sub>
</td>
<td width="50%">
<img src="docs/screenshots/06-design-systems-library.svg" alt="06 · 71-system library — placeholder, replace with the real screenshot" /><br/>
<img src="docs/screenshots/06-design-systems-library.png" alt="06 · 71-system library" /><br/>
<sub><b>71-system library</b> — every product system shows its 4-color signature. Click for the full <code>DESIGN.md</code>, swatch grid, and live showcase.</sub>
</td>
</tr>
<tr>
<td width="50%">
<img src="docs/screenshots/07-magazine-deck.svg" alt="07 · Magazine deck — placeholder, replace with the real screenshot" /><br/>
<img src="docs/screenshots/07-magazine-deck.png" alt="07 · Magazine deck" /><br/>
<sub><b>Deck mode (guizang-ppt)</b> — the bundled <a href="https://github.com/op7418/guizang-ppt-skill"><code>guizang-ppt-skill</code></a> drops in unchanged. Magazine layouts, WebGL hero backgrounds, single-file HTML output, PDF export.</sub>
</td>
<td width="50%">
<img src="docs/screenshots/08-mobile-app.svg" alt="08 · Mobile prototype — placeholder, replace with the real screenshot" /><br/>
<img src="docs/screenshots/08-mobile-app.png" alt="08 · Mobile prototype" /><br/>
<sub><b>Mobile prototype</b> — pixel-accurate iPhone 15 Pro chrome (Dynamic Island, status bar SVGs, home indicator). Multi-screen prototypes use the shared <code>/frames/</code> assets so the agent never re-draws a phone.</sub>
</td>
</tr>
</table>
## Skills
19 skills ship in the box. Each is a folder under [`skills/`](skills/) following the Claude Code [`SKILL.md`][skill] convention with an extended `od:` frontmatter (`mode`, `platform`, `scenario`, `preview`, `design_system`).
### Showcase examples
The visually distinctive skills you'll most likely run first. Each ships a real `example.html` you can open straight from the repo to see exactly what the agent will produce — no auth, no setup.
<table>
<tr>
<td width="50%" valign="top">
<a href="skills/dating-web/"><img src="docs/screenshots/skills/dating-web.png" alt="dating-web" /></a><br/>
<sub><b><a href="skills/dating-web/"><code>dating-web</code></a></b> · <i>prototype</i><br/>Consumer dating / matchmaking dashboard — left rail nav, ticker bar, KPIs, 30-day mutual-matches chart, editorial typography.</sub>
</td>
<td width="50%" valign="top">
<a href="skills/digital-eguide/"><img src="docs/screenshots/skills/digital-eguide.png" alt="digital-eguide" /></a><br/>
<sub><b><a href="skills/digital-eguide/"><code>digital-eguide</code></a></b> · <i>template</i><br/>Two-spread digital e-guide — cover (title, author, TOC teaser) + lesson spread with pull-quote and step list. Creator / lifestyle tone.</sub>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<a href="skills/email-marketing/"><img src="docs/screenshots/skills/email-marketing.png" alt="email-marketing" /></a><br/>
<sub><b><a href="skills/email-marketing/"><code>email-marketing</code></a></b> · <i>prototype</i><br/>Brand product-launch HTML email — masthead, hero image, headline lockup, CTA, specs grid. Centered single-column, table-fallback safe.</sub>
</td>
<td width="50%" valign="top">
<a href="skills/gamified-app/"><img src="docs/screenshots/skills/gamified-app.png" alt="gamified-app" /></a><br/>
<sub><b><a href="skills/gamified-app/"><code>gamified-app</code></a></b> · <i>prototype</i><br/>Three-frame gamified mobile-app prototype on a dark showcase stage — cover, today's quests with XP ribbons + level bar, quest detail.</sub>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<a href="skills/mobile-onboarding/"><img src="docs/screenshots/skills/mobile-onboarding.png" alt="mobile-onboarding" /></a><br/>
<sub><b><a href="skills/mobile-onboarding/"><code>mobile-onboarding</code></a></b> · <i>prototype</i><br/>Three-frame mobile onboarding flow — splash, value-prop, sign-in. Status bar, swipe dots, primary CTA.</sub>
</td>
<td width="50%" valign="top">
<a href="skills/motion-frames/"><img src="docs/screenshots/skills/motion-frames.png" alt="motion-frames" /></a><br/>
<sub><b><a href="skills/motion-frames/"><code>motion-frames</code></a></b> · <i>prototype</i><br/>Single-frame motion-design hero with looping CSS animations — rotating type ring, animated globe, ticking timer. Hand-off ready for HyperFrames.</sub>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<a href="skills/social-carousel/"><img src="docs/screenshots/skills/social-carousel.png" alt="social-carousel" /></a><br/>
<sub><b><a href="skills/social-carousel/"><code>social-carousel</code></a></b> · <i>prototype</i><br/>Three-card 1080×1080 social-media carousel — cinematic panels with display headlines that connect across the series, brand mark, loop affordance.</sub>
</td>
<td width="50%" valign="top">
<a href="skills/sprite-animation/"><img src="docs/screenshots/skills/sprite-animation.png" alt="sprite-animation" /></a><br/>
<sub><b><a href="skills/sprite-animation/"><code>sprite-animation</code></a></b> · <i>prototype</i><br/>Pixel / 8-bit animated explainer slide — full-bleed cream stage, animated pixel mascot, kinetic Japanese display type, looping CSS keyframes.</sub>
</td>
</tr>
</table>
### Design surfaces
| Skill | Mode | Default for | What it produces |
|---|---|---|---|
| [`web-prototype`](skills/web-prototype/) | prototype | desktop | Single-page HTML — landings, marketing, hero pages |
| [`saas-landing`](skills/saas-landing/) | prototype | desktop | Hero / features / pricing / CTA marketing layout |
| [`dashboard`](skills/dashboard/) | prototype | desktop | Admin / analytics with sidebar + data dense layout |
| [`pricing-page`](skills/pricing-page/) | prototype | desktop | Standalone pricing + comparison tables |
| [`docs-page`](skills/docs-page/) | prototype | desktop | 3-column documentation layout |
| [`blog-post`](skills/blog-post/) | prototype | desktop | Editorial long-form |
| [`mobile-app`](skills/mobile-app/) | prototype | mobile | iPhone 15 Pro / Pixel framed app screen(s) |
| [`simple-deck`](skills/simple-deck/) | deck | desktop | Minimal horizontal-swipe deck |
| [`guizang-ppt`](skills/guizang-ppt/) | deck | **default** for deck | Magazine-style web PPT — bundled from [op7418/guizang-ppt-skill][guizang] |
### Document / work-product surfaces
| Skill | Mode | What it produces |
|---|---|---|
| [`pm-spec`](skills/pm-spec/) | template | PM specification doc with TOC + decision log |
| [`weekly-update`](skills/weekly-update/) | template | Team weekly with progress / blockers / next |
| [`meeting-notes`](skills/meeting-notes/) | template | Meeting decision log |
| [`eng-runbook`](skills/eng-runbook/) | template | Incident runbook |
| [`finance-report`](skills/finance-report/) | template | Exec finance summary |
| [`hr-onboarding`](skills/hr-onboarding/) | template | Role onboarding plan |
| [`invoice`](skills/invoice/) | template | Single-page invoice |
| [`kanban-board`](skills/kanban-board/) | template | Board snapshot |
| [`team-okrs`](skills/team-okrs/) | template | OKR scoresheet |
Adding a skill takes one folder. Read [`docs/skills-protocol.md`](docs/skills-protocol.md) for the extended frontmatter, fork an existing skill, restart the daemon, it appears in the picker.
## Six load-bearing ideas
### 1 · We don't ship an agent. Yours is good enough.
@@ -320,59 +399,10 @@ open-design/
└── artifacts/ ← saved one-off renders
```
## Skills
19 skills ship in the box. Each is a folder under [`skills/`](skills/) following the Claude Code [`SKILL.md`][skill] convention with an extended `od:` frontmatter (`mode`, `platform`, `scenario`, `preview`, `design_system`).
### Showcase examples
The visually distinctive skills you'll most likely run first. Each ships a real `example.html` you can open straight from the repo to see what the agent will produce.
| Skill | Mode | What it produces |
|---|---|---|
| [`dating-web`](skills/dating-web/) | prototype | Consumer dating / matchmaking dashboard — left rail nav, ticker bar, KPIs, 30-day mutual-matches chart, editorial typography |
| [`digital-eguide`](skills/digital-eguide/) | template | Two-spread digital e-guide — cover (title, author, TOC teaser) + lesson spread with pull-quote and step list. Creator / lifestyle tone |
| [`email-marketing`](skills/email-marketing/) | prototype | Brand product-launch HTML email — masthead, hero image, headline lockup, CTA, specs grid. Centered single-column, table-fallback safe |
| [`gamified-app`](skills/gamified-app/) | prototype | Three-frame gamified mobile-app prototype on a dark showcase stage — cover, today's quests with XP ribbons + level bar, quest detail |
| [`mobile-onboarding`](skills/mobile-onboarding/) | prototype | Three-frame mobile onboarding flow — splash, value-prop, sign-in. Status bar, swipe dots, primary CTA |
| [`motion-frames`](skills/motion-frames/) | prototype | Single-frame motion-design hero with looping CSS animations — rotating type ring, animated globe, ticking timer. Hand-off ready for HyperFrames |
| [`social-carousel`](skills/social-carousel/) | prototype | Three-card 1080×1080 social-media carousel — cinematic panels with display headlines that connect across the series, brand mark, loop affordance |
| [`sprite-animation`](skills/sprite-animation/) | prototype | Pixel / 8-bit animated explainer slide — full-bleed cream stage, animated pixel mascot, kinetic Japanese display type, looping CSS keyframes |
### Design surfaces
| Skill | Mode | Default for | What it produces |
|---|---|---|---|
| [`web-prototype`](skills/web-prototype/) | prototype | desktop | Single-page HTML — landings, marketing, hero pages |
| [`saas-landing`](skills/saas-landing/) | prototype | desktop | Hero / features / pricing / CTA marketing layout |
| [`dashboard`](skills/dashboard/) | prototype | desktop | Admin / analytics with sidebar + data dense layout |
| [`pricing-page`](skills/pricing-page/) | prototype | desktop | Standalone pricing + comparison tables |
| [`docs-page`](skills/docs-page/) | prototype | desktop | 3-column documentation layout |
| [`blog-post`](skills/blog-post/) | prototype | desktop | Editorial long-form |
| [`mobile-app`](skills/mobile-app/) | prototype | mobile | iPhone 15 Pro / Pixel framed app screen(s) |
| [`simple-deck`](skills/simple-deck/) | deck | desktop | Minimal horizontal-swipe deck |
| [`guizang-ppt`](skills/guizang-ppt/) | deck | **default** for deck | Magazine-style web PPT — bundled from [op7418/guizang-ppt-skill][guizang] |
### Document / work-product surfaces
| Skill | Mode | What it produces |
|---|---|---|
| [`pm-spec`](skills/pm-spec/) | template | PM specification doc with TOC + decision log |
| [`weekly-update`](skills/weekly-update/) | template | Team weekly with progress / blockers / next |
| [`meeting-notes`](skills/meeting-notes/) | template | Meeting decision log |
| [`eng-runbook`](skills/eng-runbook/) | template | Incident runbook |
| [`finance-report`](skills/finance-report/) | template | Exec finance summary |
| [`hr-onboarding`](skills/hr-onboarding/) | template | Role onboarding plan |
| [`invoice`](skills/invoice/) | template | Single-page invoice |
| [`kanban-board`](skills/kanban-board/) | template | Board snapshot |
| [`team-okrs`](skills/team-okrs/) | template | OKR scoresheet |
Adding a skill takes one folder. Read [`docs/skills-protocol.md`](docs/skills-protocol.md) for the extended frontmatter, fork an existing skill, restart the daemon, it appears in the picker.
## Design Systems
<p align="center">
<img src="docs/assets/design-systems-library.svg" alt="71 design systems library — placeholder, replace with the real screenshot" width="100%" />
<img src="docs/assets/design-systems-library.png" alt="The 71 design systems library — style guide spread" width="100%" />
</p>
71 systems out of the box, each as a single [`DESIGN.md`](design-systems/README.md):
@@ -450,7 +480,7 @@ The whole machinery below is the [`huashu-design`](https://github.com/alchaincyf
| PPT skill reuse | N/A | Built-in | **[`guizang-ppt-skill`][guizang] drops in** |
| Minimum billing | Pro / Max / Team | BYOK | **BYOK** |
[cd]: https://www.anthropic.com/news/claude-design
[cd]: https://x.com/claudeai/status/2045156267690213649
[ocod]: https://github.com/OpenCoworkAI/open-codesign
[piai]: https://github.com/mariozechner/pi-ai
[acd]: https://github.com/VoltAgent/awesome-claude-design
@@ -479,7 +509,7 @@ Every external project this repo borrows from. Each link goes to the source so y
| Project | Role here |
|---|---|
| [`Claude Design`][cd] | The closed product this repo provides an open substrate for. |
| [`Claude Design`][cd] | The closed-source product this repo is the open-source alternative to. |
| [**`alchaincyf/huashu-design`**](https://github.com/alchaincyf/huashu-design) | The design-philosophy core. Junior-Designer workflow, the 5-step brand-asset protocol, anti-AI-slop checklist, 5-dimensional self-critique, and the "5 schools × 20 design philosophies" library behind our direction picker — all distilled into [`src/prompts/discovery.ts`](src/prompts/discovery.ts) and [`src/prompts/directions.ts`](src/prompts/directions.ts). |
| [**`op7418/guizang-ppt-skill`**][guizang] | Magazine-web-PPT skill bundled verbatim under [`skills/guizang-ppt/`](skills/guizang-ppt/) with original LICENSE preserved. Default for deck mode. P0/P1/P2 checklist culture borrowed for every other skill. |
| [**`multica-ai/multica`**](https://github.com/multica-ai/multica) | The daemon + adapter architecture. PATH-scan agent detection, local daemon as the only privileged process, agent-as-teammate worldview. We adopt the model; we do not vendor the code. |
@@ -508,7 +538,15 @@ Phased delivery → [`docs/roadmap.md`](docs/roadmap.md).
This is an early implementation — the closed loop (detect → pick skill + design system → chat → parse `<artifact>` → preview → save) runs end-to-end. The prompt stack and skill library are where most of the value lives, and they're stable. The component-level UI is shipping daily.
Issues, PRs, new skills, and new design systems are all welcome.
## Contributing
Issues, PRs, new skills, and new design systems are all welcome. The highest-leverage contributions are usually one folder, one Markdown file, or one PR-sized adapter:
- **Add a skill** — drop a folder into [`skills/`](skills/) following the [`SKILL.md`][skill] convention.
- **Add a design system** — drop a `DESIGN.md` into [`design-systems/<brand>/`](design-systems/) using the 9-section schema.
- **Wire up a new coding-agent CLI** — one entry in [`daemon/agents.js`](daemon/agents.js).
Full walkthrough, bar-for-merging, code style, and what we don't accept → [`CONTRIBUTING.md`](CONTRIBUTING.md) ([简体中文](CONTRIBUTING.zh-CN.md)).
## License
+106 -68
View File
@@ -1,9 +1,9 @@
# Open Design
> **给设计的 Claude Code。** 一个本地优先、可部署到 Vercel 的开源 [Claude Design][cd] 复刻 —— 你机器上已经装好的 coding agentClaude Code、Codex、Cursor Agent、Gemini CLI、OpenCode、Qwen)就是设计引擎,由可组合的 **Skills** 和 **71 套品牌级 Design System** 驱动。
> **[Claude Design][cd] 的开源替代品。** 本地优先、可部署到 Vercel、每一层都 BYOK —— 你机器上已经装好的 coding agentClaude Code、Codex、Cursor Agent、Gemini CLI、OpenCode、Qwen)就是设计引擎,由 **19 个可组合 Skills** 和 **71 套品牌级 Design System** 驱动。
<p align="center">
<img src="docs/assets/banner.svg" alt="Open Design banner —— 占位图,等待替换为真实产品截图" width="100%" />
<img src="docs/assets/banner.png" alt="Open Design 封面:与本地 AI 智能体共同设计" width="100%" />
</p>
<p align="center">
@@ -20,13 +20,13 @@
## 为什么要做这个
Anthropic 的 [Claude Design][cd]2026-04-17 发布,基于 Opus 4.7)让大家第一次看到:当一个 LLM 不再写废话、开始直接交付设计成品,会是什么样子。它瞬间出圈 —— 然后保持闭源、付费、只跑在云上、绑定 Anthropic 的模型和 Anthropic 的内部 skill。
Anthropic 的 [Claude Design][cd]2026-04-17 发布,基于 Opus 4.7)让大家第一次看到:当一个 LLM 不再写废话、开始直接交付设计成品,会是什么样子。它瞬间出圈 —— 然后保持**闭源**、付费、只跑在云上、绑定 Anthropic 的模型和 Anthropic 的内部 skill。没有 checkout,没有自托管,没有 Vercel 部署,也换不了自己的 agent。
**Open DesignOD)是它的开源底座** 我们不做 agent —— 你笔记本上最强的 coding agent 已经装好了。我们要做的,是把它接进一个 skill 驱动的设计工作流,跑在一个普通的 Web 应用里:本地 `pnpm dev`,云端 `vercel deploy`,每一层都 BYOK(自带 Key)。
**Open DesignOD是它的开源替代品** 同一套 loop、同一种「artifact-first」心智模型,但没有锁定。我们不做 agent —— 你笔记本上最强的 coding agent 已经装好了。我们要做的,是把它接进一个 skill 驱动的设计工作流,跑在一个普通的 Web 应用里:本地 `pnpm dev`,云端 `vercel deploy`,每一层都 BYOK(自带 Key)。
输入「帮我做一份杂志风的种子轮 pitch deck」。在模型挥洒第一个像素之前,**初始化问题表单**已经先跳出来。Agent 从 5 套精挑的视觉方向里选一个。一张活的 `TodoWrite` 计划卡片实时流入 UI。Daemon 在磁盘上构建出一个真实的项目目录,里面有 seed 模板、布局库、自检 checklist。Agent **强制 pre-flight** 读取它们,对自己的输出跑一轮**五维评审**,几秒后吐出一个 `<artifact>`,渲染在沙盒 iframe 里。
这不是「AI 试图做点设计」。这是一个被提示词栈训练得像高级设计师一样工作的 AI —— 有可用的文件系统、有确定性的色板库、有 checklist 文化。
这不是「AI 试图做点设计」。这是一个被提示词栈训练得像高级设计师一样工作的 AI —— 有可用的文件系统、有确定性的色板库、有 checklist 文化 —— 也就是 Claude Design 立下的那条线,只是这次它开源、归你
OD 站在四个开源项目的肩膀上:
@@ -52,51 +52,130 @@ OD 站在四个开源项目的肩膀上:
## 效果展示
> **截图位待补。** 下方每张卡片都是占位 SVG —— 截好真实图后把 `docs/screenshots/*.svg` 替换为 `.png` / `.jpg`,并把对应 `<img src=…>` 的扩展名同步改掉即可。
<table>
<tr>
<td width="50%">
<img src="docs/screenshots/01-entry-view.svg" alt="01 · 入口页 —— 占位图,等待替换为真实截图" /><br/>
<img src="docs/screenshots/01-entry-view.png" alt="01 · 入口页" /><br/>
<sub><b>入口页</b> —— 选 skill、选 design system、写一行需求。同一个表面服务原型、deck、移动端、dashboard、editorial 页面所有 mode。</sub>
</td>
<td width="50%">
<img src="docs/screenshots/02-question-form.svg" alt="02 · 初始化问题表单 —— 占位图,等待替换为真实截图" /><br/>
<img src="docs/screenshots/02-question-form.png" alt="02 · 初始化问题表单" /><br/>
<sub><b>初始化问题表单</b> —— 模型动笔之前,OD 先把需求锁住:surface、受众、调性、品牌上下文、规模。30 秒勾选项秒杀 30 分钟来回返工。</sub>
</td>
</tr>
<tr>
<td width="50%">
<img src="docs/screenshots/03-direction-picker.svg" alt="03 · 方向选择器 —— 占位图,等待替换为真实截图" /><br/>
<img src="docs/screenshots/03-direction-picker.png" alt="03 · 方向选择器" /><br/>
<sub><b>方向选择器</b> —— 用户没有品牌上下文时,agent 自动跳第二个表单,5 套精选方向(Monocle / Modern Minimal / Tech Utility / Brutalist / Soft Warm)一个 radio 选完,色板 + 字体栈直接锁定,没有 freestyle 空间。</sub>
</td>
<td width="50%">
<img src="docs/screenshots/04-todo-progress.svg" alt="04 · 实时 todo 进度 —— 占位图,等待替换为真实截图" /><br/>
<img src="docs/screenshots/04-todo-progress.png" alt="04 · 实时 todo 进度" /><br/>
<sub><b>实时 todo 进度</b> —— Agent 的计划以活卡片形式流入 UI。<code>in_progress</code> → <code>completed</code> 实时切换。用户能在中途以极低成本介入纠偏。</sub>
</td>
</tr>
<tr>
<td width="50%">
<img src="docs/screenshots/05-preview-iframe.svg" alt="05 · 沙盒预览 —— 占位图,等待替换为真实截图" /><br/>
<img src="docs/screenshots/05-preview-iframe.png" alt="05 · 沙盒预览" /><br/>
<sub><b>沙盒预览</b> —— 每个 <code>&lt;artifact&gt;</code> 都在干净的 srcdoc iframe 里渲染。可在文件工作区里就地编辑;可下载为 HTML / PDF / ZIP。</sub>
</td>
<td width="50%">
<img src="docs/screenshots/06-design-systems-library.svg" alt="06 · 71 套 design system 库 —— 占位图,等待替换为真实截图" /><br/>
<img src="docs/screenshots/06-design-systems-library.png" alt="06 · 71 套 design system 库" /><br/>
<sub><b>71 套 design system 库</b> —— 每套产品系统都展示 4 色色卡。点进去看完整的 <code>DESIGN.md</code>、色板网格、live showcase。</sub>
</td>
</tr>
<tr>
<td width="50%">
<img src="docs/screenshots/07-magazine-deck.svg" alt="07 · 杂志风 deck —— 占位图,等待替换为真实截图" /><br/>
<img src="docs/screenshots/07-magazine-deck.png" alt="07 · 杂志风 deck" /><br/>
<sub><b>Deck 模式(guizang-ppt</b> —— 内置的 <a href="https://github.com/op7418/guizang-ppt-skill"><code>guizang-ppt-skill</code></a> 原样接入。杂志版式、WebGL hero 背景、单文件 HTML 输出、可导 PDF。</sub>
</td>
<td width="50%">
<img src="docs/screenshots/08-mobile-app.svg" alt="08 · 移动端原型 —— 占位图,等待替换为真实截图" /><br/>
<img src="docs/screenshots/08-mobile-app.png" alt="08 · 移动端原型" /><br/>
<sub><b>移动端原型</b> —— 像素级精确的 iPhone 15 Pro chrome(灵动岛、状态栏 SVG、Home Indicator)。多屏原型直接复用 <code>/frames/</code> 共享资源,agent 永远不需要重新画一遍手机。</sub>
</td>
</tr>
</table>
## 内置 Skills
19 个 skill,每个一个文件夹,都遵循 Claude Code 的 [`SKILL.md`][skill] 规范,并叠加 OD 的 `od:` frontmatter`mode``platform``scenario``preview``design_system`)。
### 示例展示(Showcase examples
视觉表现最强、最适合上手第一跑的几条 skill。每条都附带可直接打开的 `example.html` —— 不用登录、不用配置,先看产出再下单。
<table>
<tr>
<td width="50%" valign="top">
<a href="skills/dating-web/"><img src="docs/screenshots/skills/dating-web.png" alt="dating-web" /></a><br/>
<sub><b><a href="skills/dating-web/"><code>dating-web</code></a></b> · <i>prototype</i><br/>消费级约会 / 婚恋仪表盘 —— 左侧栏、社区动态 ticker、头部 KPI、30 天双向匹配柱状图,editorial 字体,克制点缀色。</sub>
</td>
<td width="50%" valign="top">
<a href="skills/digital-eguide/"><img src="docs/screenshots/skills/digital-eguide.png" alt="digital-eguide" /></a><br/>
<sub><b><a href="skills/digital-eguide/"><code>digital-eguide</code></a></b> · <i>template</i><br/>两页数字 e-guide —— 封面(标题、作者、TOC 预告)+ 内文跨页(pull-quote + 步骤列表),创作者 / 生活方式风。</sub>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<a href="skills/email-marketing/"><img src="docs/screenshots/skills/email-marketing.png" alt="email-marketing" /></a><br/>
<sub><b><a href="skills/email-marketing/"><code>email-marketing</code></a></b> · <i>prototype</i><br/>品牌新品发布邮件 —— 顶部 wordmark、hero 图、标题锁排、主 CTA、规格网格。居中单列 + 表格降级,邮件客户端安全。</sub>
</td>
<td width="50%" valign="top">
<a href="skills/gamified-app/"><img src="docs/screenshots/skills/gamified-app.png" alt="gamified-app" /></a><br/>
<sub><b><a href="skills/gamified-app/"><code>gamified-app</code></a></b> · <i>prototype</i><br/>三屏游戏化移动 app 原型,黑色舞台 —— 封面 / 今日任务(XP 缎带 + 等级条)/ 任务详情。</sub>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<a href="skills/mobile-onboarding/"><img src="docs/screenshots/skills/mobile-onboarding.png" alt="mobile-onboarding" /></a><br/>
<sub><b><a href="skills/mobile-onboarding/"><code>mobile-onboarding</code></a></b> · <i>prototype</i><br/>三屏移动端引导流 —— splash、价值主张、登录。状态栏、滑动点、主 CTA。</sub>
</td>
<td width="50%" valign="top">
<a href="skills/motion-frames/"><img src="docs/screenshots/skills/motion-frames.png" alt="motion-frames" /></a><br/>
<sub><b><a href="skills/motion-frames/"><code>motion-frames</code></a></b> · <i>prototype</i><br/>单帧 motion 设计 hero,CSS 循环动画 —— 旋转字环、地球、计时器。可直接交给 HyperFrames 等关键帧导出。</sub>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<a href="skills/social-carousel/"><img src="docs/screenshots/skills/social-carousel.png" alt="social-carousel" /></a><br/>
<sub><b><a href="skills/social-carousel/"><code>social-carousel</code></a></b> · <i>prototype</i><br/>1080×1080 三连社媒轮播图 —— 三张电影感面板,标题前后呼应,品牌标识、loop 标记。</sub>
</td>
<td width="50%" valign="top">
<a href="skills/sprite-animation/"><img src="docs/screenshots/skills/sprite-animation.png" alt="sprite-animation" /></a><br/>
<sub><b><a href="skills/sprite-animation/"><code>sprite-animation</code></a></b> · <i>prototype</i><br/>像素 / 8-bit 动画解释器单帧 —— 米白通屏、像素吉祥物、动感日文标题、循环 CSS keyframes,可直接录屏成竖版视频。</sub>
</td>
</tr>
</table>
### 设计交付类
| Skill | Mode | 默认场景 | 产出 |
|---|---|---|---|
| [`web-prototype`](skills/web-prototype/) | prototype | 桌面 | 单页 HTML —— landing、营销、hero |
| [`saas-landing`](skills/saas-landing/) | prototype | 桌面 | hero / features / pricing / CTA 营销版式 |
| [`dashboard`](skills/dashboard/) | prototype | 桌面 | 带侧栏 + 数据密集型的后台 |
| [`pricing-page`](skills/pricing-page/) | prototype | 桌面 | 独立定价页 + 对比表 |
| [`docs-page`](skills/docs-page/) | prototype | 桌面 | 三栏文档版式 |
| [`blog-post`](skills/blog-post/) | prototype | 桌面 | 长文 editorial |
| [`mobile-app`](skills/mobile-app/) | prototype | 移动 | 带 iPhone 15 Pro / Pixel 外壳的 app 屏 |
| [`simple-deck`](skills/simple-deck/) | deck | 桌面 | 极简横滑 deck |
| [`guizang-ppt`](skills/guizang-ppt/) | deck | **deck 默认** | 杂志风网页 PPT —— 来自 [op7418/guizang-ppt-skill][guizang] |
### 文档与办公产物类
| Skill | Mode | 产出 |
|---|---|---|
| [`pm-spec`](skills/pm-spec/) | template | PM 规范文档 + 目录 + 决策日志 |
| [`weekly-update`](skills/weekly-update/) | template | 团队周报:进度 / 阻塞 / 下一步 |
| [`meeting-notes`](skills/meeting-notes/) | template | 会议决策纪要 |
| [`eng-runbook`](skills/eng-runbook/) | template | 故障 runbook |
| [`finance-report`](skills/finance-report/) | template | 高管财务摘要 |
| [`hr-onboarding`](skills/hr-onboarding/) | template | 岗位入职计划 |
| [`invoice`](skills/invoice/) | template | 单页发票 |
| [`kanban-board`](skills/kanban-board/) | template | 看板快照 |
| [`team-okrs`](skills/team-okrs/) | template | OKR 计分表 |
新增一个 skill 就是新增一个文件夹。读 [`docs/skills-protocol.md`](docs/skills-protocol.md) 了解扩展 frontmatterfork 一个现有 skill,重启 daemon 即生效。
## 六个底层设计
### 1 · 我们不带 agent,你的就够好
@@ -320,59 +399,10 @@ open-design/
└── artifacts/ ← 单次保存的 artifact
```
## 内置 Skills
19 个 skill,每个一个文件夹,都遵循 Claude Code 的 [`SKILL.md`][skill] 规范,并叠加 OD 的 `od:` frontmatter`mode``platform``scenario``preview``design_system`)。
### 示例展示(Showcase examples
视觉表现最强、最适合上手第一跑的几条 skill。每条都附带可直接打开的 `example.html`,先看产出再下单。
| Skill | Mode | 产出 |
|---|---|---|
| [`dating-web`](skills/dating-web/) | prototype | 消费级约会 / 婚恋仪表盘 —— 左侧栏、社区动态 ticker、头部 KPI、30 天双向匹配柱状图,editorial 字体,克制点缀色 |
| [`digital-eguide`](skills/digital-eguide/) | template | 两页数字 e-guide —— 封面(标题、作者、TOC 预告)+ 内文跨页(pull-quote + 步骤列表),创作者 / 生活方式风 |
| [`email-marketing`](skills/email-marketing/) | prototype | 品牌新品发布邮件 —— 顶部 wordmark、hero 图、标题锁排、主 CTA、规格网格。居中单列 + 表格降级,邮件客户端安全 |
| [`gamified-app`](skills/gamified-app/) | prototype | 三屏游戏化移动 app 原型,黑色舞台 —— 封面 / 今日任务(XP 缎带 + 等级条)/ 任务详情 |
| [`mobile-onboarding`](skills/mobile-onboarding/) | prototype | 三屏移动端引导流 —— splash、价值主张、登录。状态栏、滑动点、主 CTA |
| [`motion-frames`](skills/motion-frames/) | prototype | 单帧 motion 设计 hero,CSS 循环动画 —— 旋转字环、地球、计时器。可直接交给 HyperFrames 等关键帧导出 |
| [`social-carousel`](skills/social-carousel/) | prototype | 1080×1080 三连社媒轮播图 —— 三张电影感面板,标题前后呼应,品牌标识、loop 标记 |
| [`sprite-animation`](skills/sprite-animation/) | prototype | 像素 / 8-bit 动画解释器单帧 —— 米白通屏、像素吉祥物、动感日文标题、循环 CSS keyframes,可直接录屏成竖版视频 |
### 设计交付类
| Skill | Mode | 默认场景 | 产出 |
|---|---|---|---|
| [`web-prototype`](skills/web-prototype/) | prototype | 桌面 | 单页 HTML —— landing、营销、hero |
| [`saas-landing`](skills/saas-landing/) | prototype | 桌面 | hero / features / pricing / CTA 营销版式 |
| [`dashboard`](skills/dashboard/) | prototype | 桌面 | 带侧栏 + 数据密集型的后台 |
| [`pricing-page`](skills/pricing-page/) | prototype | 桌面 | 独立定价页 + 对比表 |
| [`docs-page`](skills/docs-page/) | prototype | 桌面 | 三栏文档版式 |
| [`blog-post`](skills/blog-post/) | prototype | 桌面 | 长文 editorial |
| [`mobile-app`](skills/mobile-app/) | prototype | 移动 | 带 iPhone 15 Pro / Pixel 外壳的 app 屏 |
| [`simple-deck`](skills/simple-deck/) | deck | 桌面 | 极简横滑 deck |
| [`guizang-ppt`](skills/guizang-ppt/) | deck | **deck 默认** | 杂志风网页 PPT —— 来自 [op7418/guizang-ppt-skill][guizang] |
### 文档与办公产物类
| Skill | Mode | 产出 |
|---|---|---|
| [`pm-spec`](skills/pm-spec/) | template | PM 规范文档 + 目录 + 决策日志 |
| [`weekly-update`](skills/weekly-update/) | template | 团队周报:进度 / 阻塞 / 下一步 |
| [`meeting-notes`](skills/meeting-notes/) | template | 会议决策纪要 |
| [`eng-runbook`](skills/eng-runbook/) | template | 故障 runbook |
| [`finance-report`](skills/finance-report/) | template | 高管财务摘要 |
| [`hr-onboarding`](skills/hr-onboarding/) | template | 岗位入职计划 |
| [`invoice`](skills/invoice/) | template | 单页发票 |
| [`kanban-board`](skills/kanban-board/) | template | 看板快照 |
| [`team-okrs`](skills/team-okrs/) | template | OKR 计分表 |
新增一个 skill 就是新增一个文件夹。读 [`docs/skills-protocol.md`](docs/skills-protocol.md) 了解扩展 frontmatterfork 一个现有 skill,重启 daemon 即生效。
## Design System
<p align="center">
<img src="docs/assets/design-systems-library.svg" alt="71 套 design system 库 —— 占位图,等待替换为真实截图" width="100%" />
<img src="docs/assets/design-systems-library.png" alt="71 套 Design Systems 库 — 编辑版式双页" width="100%" />
</p>
71 套开箱即用,每套一个 [`DESIGN.md`](design-systems/README.md)
@@ -450,7 +480,7 @@ open-design/
| PPT skill 复用 | N/A | 内置 | **[`guizang-ppt-skill`][guizang] 直接接入** |
| 计费门槛 | Pro / Max / Team | BYOK | **BYOK** |
[cd]: https://www.anthropic.com/news/claude-design
[cd]: https://x.com/claudeai/status/2045156267690213649
[ocod]: https://github.com/OpenCoworkAI/open-codesign
[piai]: https://github.com/mariozechner/pi-ai
[acd]: https://github.com/VoltAgent/awesome-claude-design
@@ -479,7 +509,7 @@ Daemon 启动时从 `PATH` 自动检测,无需配置。
| 项目 | 在这里的角色 |
|---|---|
| [`Claude Design`][cd] | 本仓库为提供开源底座的闭源产品。 |
| [`Claude Design`][cd] | 本仓库为提供开源替代的闭源产品。 |
| [**`alchaincyf/huashu-design`**(花叔的画术)](https://github.com/alchaincyf/huashu-design) | 设计哲学的核心。Junior-Designer 工作流、5 步品牌资产协议、anti-AI-slop checklist、五维自评审、以及方向选择器背后的「5 流派 × 20 种设计哲学」库 —— 全部蒸馏进 [`src/prompts/discovery.ts`](src/prompts/discovery.ts) 与 [`src/prompts/directions.ts`](src/prompts/directions.ts)。 |
| [**`op7418/guizang-ppt-skill`**(歸藏)][guizang] | Magazine-web-PPT skill 原样捆绑在 [`skills/guizang-ppt/`](skills/guizang-ppt/) 下,原 LICENSE 保留。Deck 模式默认。P0/P1/P2 checklist 文化也被借给了所有其他 skill。 |
| [**`multica-ai/multica`**](https://github.com/multica-ai/multica) | Daemon + adapter 架构。PATH 扫描式 agent 检测、本地 daemon 作为唯一特权进程、agent-as-teammate 世界观。我们采纳模型,不 vendor 代码。 |
@@ -508,7 +538,15 @@ Daemon 启动时从 `PATH` 自动检测,无需配置。
这是一个早期实现 —— 闭环(检测 → 选 skill + design system → 对话 → 解析 `<artifact>` → 预览 → 保存)已经端到端跑通。提示词栈和 skill 库是价值最重的部分,目前已稳定。组件级 UI 仍在每天迭代。
欢迎 issue、PR、新 skill、新 design system。
## 贡献
欢迎 issue、PR、新 skill、新 design system。收益最高的贡献往往就是一个文件夹、一份 Markdown,或者一个 PR 大小的 adapter
- **加一个 skill** —— 往 [`skills/`](skills/) 丢一个文件夹,遵循 [`SKILL.md`][skill] 规范。
- **加一套 design system** —— 往 [`design-systems/<brand>/`](design-systems/) 丢一份 `DESIGN.md`,用 9 段式 schema。
- **接入一个新的 coding-agent CLI** —— 在 [`daemon/agents.js`](daemon/agents.js) 里加一项。
完整流程、合并硬线、代码风格、我们不接收的 PR 类型 → [`CONTRIBUTING.zh-CN.md`](CONTRIBUTING.zh-CN.md)[English](CONTRIBUTING.md))。
## License
+99 -59
View File
@@ -2,9 +2,9 @@
// front-matter, returns listing. No watching in this MVP — re-scans on every
// GET /api/skills, which is fine for dozens of skills.
import { readdir, readFile, stat } from 'node:fs/promises';
import path from 'node:path';
import { parseFrontmatter } from './frontmatter.js';
import { readdir, readFile, stat } from "node:fs/promises";
import path from "node:path";
import { parseFrontmatter } from "./frontmatter.js";
export async function listSkills(skillsRoot) {
const out = [];
@@ -17,27 +17,40 @@ export async function listSkills(skillsRoot) {
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const dir = path.join(skillsRoot, entry.name);
const skillPath = path.join(dir, 'SKILL.md');
const skillPath = path.join(dir, "SKILL.md");
try {
const stats = await stat(skillPath);
if (!stats.isFile()) continue;
const raw = await readFile(skillPath, 'utf8');
const raw = await readFile(skillPath, "utf8");
const { data, body } = parseFrontmatter(raw);
const hasAttachments = await dirHasAttachments(dir);
const mode = data.od?.mode || inferMode(body, data.description);
out.push({
id: data.name || entry.name,
name: data.name || entry.name,
description: data.description || '',
description: data.description || "",
triggers: Array.isArray(data.triggers) ? data.triggers : [],
mode,
platform: normalizePlatform(data.od?.platform, mode, body, data.description),
platform: normalizePlatform(
data.od?.platform,
mode,
body,
data.description
),
scenario: normalizeScenario(data.od?.scenario, body, data.description),
previewType: data.od?.preview?.type || 'html',
previewType: data.od?.preview?.type || "html",
designSystemRequired: data.od?.design_system?.requires ?? true,
defaultFor: normalizeDefaultFor(data.od?.default_for),
upstream: typeof data.od?.upstream === 'string' ? data.od.upstream : null,
upstream:
typeof data.od?.upstream === "string" ? data.od.upstream : null,
featured: normalizeFeatured(data.od?.featured),
// Optional metadata hints used by 'Use this prompt' fast-create so
// the resulting project mirrors the shipped example.html. Each hint
// is only consumed when its kind matches the skill mode; missing
// hints fall back to the same defaults the new-project form uses.
fidelity: normalizeFidelity(data.od?.fidelity),
speakerNotes: normalizeBoolHint(data.od?.speaker_notes),
animations: normalizeBoolHint(data.od?.animations),
examplePrompt: derivePrompt(data),
body: hasAttachments ? withSkillRootPreamble(body, dir) : body,
dir,
@@ -56,15 +69,15 @@ export async function listSkills(skillsRoot) {
// open those files via absolute paths.
function withSkillRootPreamble(body, dir) {
const preamble = [
'> **Skill root (absolute):** `' + dir + '`',
'>',
'> This skill ships side files alongside `SKILL.md`. When the workflow',
'> below references relative paths such as `assets/template.html` or',
'> `references/layouts.md`, resolve them against the skill root above and',
'> open them via their full absolute path.',
'',
'',
].join('\n');
"> **Skill root (absolute):** `" + dir + "`",
">",
"> This skill ships side files alongside `SKILL.md`. When the workflow",
"> below references relative paths such as `assets/template.html` or",
"> `references/layouts.md`, resolve them against the skill root above and",
"> open them via their full absolute path.",
"",
"",
].join("\n");
return preamble + body;
}
@@ -72,7 +85,9 @@ async function dirHasAttachments(dir) {
try {
const entries = await readdir(dir, { withFileTypes: true });
return entries.some(
(e) => e.name !== 'SKILL.md' && (e.isDirectory() || /\.(md|html|css|js|json|txt)$/i.test(e.name)),
(e) =>
e.name !== "SKILL.md" &&
(e.isDirectory() || /\.(md|html|css|js|json|txt)$/i.test(e.name))
);
} catch {
return false;
@@ -85,14 +100,35 @@ function normalizeDefaultFor(value) {
return [String(value)];
}
// Optional `od.fidelity` hint for prototype skills. Only 'wireframe' and
// 'high-fidelity' are meaningful — anything else collapses to null so the
// caller falls back to the form default ('high-fidelity').
function normalizeFidelity(value) {
if (value === "wireframe" || value === "high-fidelity") return value;
return null;
}
// Coerce truthy / falsy strings ("true", "yes", "false", "no") and booleans
// to a real boolean. Returns null for anything we can't interpret so the
// caller knows to fall back to the form default.
function normalizeBoolHint(value) {
if (typeof value === "boolean") return value;
if (typeof value === "string") {
const v = value.trim().toLowerCase();
if (v === "true" || v === "yes" || v === "1") return true;
if (v === "false" || v === "no" || v === "0") return false;
}
return null;
}
// Coerce `od.featured` into a numeric priority. Lower numbers float to the
// top of the Examples gallery; `true` is treated as priority 1; anything
// missing/unrecognised becomes null so non-featured skills keep their
// natural alphabetical order.
function normalizeFeatured(value) {
if (value === true) return 1;
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value === 'string' && value.trim()) {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim()) {
const n = Number(value);
if (Number.isFinite(n)) return n;
}
@@ -105,66 +141,70 @@ function normalizeFeatured(value) {
// serves as a passable starter prompt.
function derivePrompt(data) {
const explicit = data.od?.example_prompt;
if (typeof explicit === 'string' && explicit.trim()) return explicit.trim();
const desc = typeof data.description === 'string' ? data.description.trim() : '';
if (!desc) return '';
const collapsed = desc.replace(/\s+/g, ' ').trim();
if (typeof explicit === "string" && explicit.trim()) return explicit.trim();
const desc =
typeof data.description === "string" ? data.description.trim() : "";
if (!desc) return "";
const collapsed = desc.replace(/\s+/g, " ").trim();
const firstSentence = collapsed.match(/^.+?[.!?。!?](?:\s|$)/)?.[0]?.trim();
return (firstSentence || collapsed).slice(0, 320);
}
function inferMode(body, description) {
const hay = `${description ?? ''}\n${body ?? ''}`.toLowerCase();
if (/\bppt|deck|slide|presentation|幻灯|投影/.test(hay)) return 'deck';
if (/\bdesign[- ]system|\bdesign\.md|\bdesign tokens/.test(hay)) return 'design-system';
if (/\btemplate\b/.test(hay)) return 'template';
return 'prototype';
const hay = `${description ?? ""}\n${body ?? ""}`.toLowerCase();
if (/\bppt|deck|slide|presentation|幻灯|投影/.test(hay)) return "deck";
if (/\bdesign[- ]system|\bdesign\.md|\bdesign tokens/.test(hay))
return "design-system";
if (/\btemplate\b/.test(hay)) return "template";
return "prototype";
}
// Validate platform tag — only desktop / mobile are meaningful for the
// Examples gallery. Falls back to autodetecting "mobile" from descriptions
// so legacy skills sort under the right pill without authoring changes.
function normalizePlatform(value, mode, body, description) {
if (value === 'desktop' || value === 'mobile') return value;
if (mode !== 'prototype') return null;
const hay = `${description ?? ''}\n${body ?? ''}`.toLowerCase();
if (/mobile|phone|ios|android|手机|移动端/.test(hay)) return 'mobile';
return 'desktop';
if (value === "desktop" || value === "mobile") return value;
if (mode !== "prototype") return null;
const hay = `${description ?? ""}\n${body ?? ""}`.toLowerCase();
if (/mobile|phone|ios|android|手机|移动端/.test(hay)) return "mobile";
return "desktop";
}
// Normalise a scenario tag to a small fixed vocabulary so the filter pills
// stay tidy. Unknown values pass through verbatim so authors can experiment;
// missing values default to "general".
const KNOWN_SCENARIOS = new Set([
'general',
'engineering',
'product',
'design',
'marketing',
'sales',
'finance',
'hr',
'operations',
'support',
'legal',
'education',
'personal',
"general",
"engineering",
"product",
"design",
"marketing",
"sales",
"finance",
"hr",
"operations",
"support",
"legal",
"education",
"personal",
]);
function normalizeScenario(value, body, description) {
if (typeof value === 'string') {
if (typeof value === "string") {
const v = value.trim().toLowerCase();
if (v) return v;
}
const hay = `${description ?? ''}\n${body ?? ''}`.toLowerCase();
if (/finance|invoice|expense|budget|p&l|revenue/.test(hay)) return 'finance';
if (/\bhr\b|onboarding|payroll|employee|人事/.test(hay)) return 'hr';
if (/marketing|campaign|brand|landing/.test(hay)) return 'marketing';
if (/runbook|incident|deploy|engineering|sre|api/.test(hay)) return 'engineering';
if (/spec|prd|roadmap|product manager|product team/.test(hay)) return 'product';
if (/design system|moodboard|mockup|ui kit/.test(hay)) return 'design';
if (/sales|quote|proposal|lead/.test(hay)) return 'sales';
if (/operations|ops|logistics|inventory/.test(hay)) return 'operations';
return 'general';
const hay = `${description ?? ""}\n${body ?? ""}`.toLowerCase();
if (/finance|invoice|expense|budget|p&l|revenue/.test(hay)) return "finance";
if (/\bhr\b|onboarding|payroll|employee|人事/.test(hay)) return "hr";
if (/marketing|campaign|brand|landing/.test(hay)) return "marketing";
if (/runbook|incident|deploy|engineering|sre|api/.test(hay))
return "engineering";
if (/spec|prd|roadmap|product manager|product team/.test(hay))
return "product";
if (/design system|moodboard|mockup|ui kit/.test(hay)) return "design";
if (/sales|quote|proposal|lead/.test(hay)) return "sales";
if (/operations|ops|logistics|inventory/.test(hay)) return "operations";
return "general";
}
// Surface the vocabulary so callers (frontend filter UI) could mirror it
// later if they want to. Not exported today, kept here for documentation.
+381
View File
@@ -0,0 +1,381 @@
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8' />
<title>Open Design — cover</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=Fraunces:ital,opsz,wght@0,9..144,300;0,9..144,500;0,9..144,700;1,9..144,500;1,9..144,700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500&display=swap' rel='stylesheet'>
<style>
:root {
--bg: #0d0a06;
--bg-2: #14100a;
--ink: #f3ead8;
--muted: #9b8f78;
--rule: #4a3f2c;
--accent: #f0833f;
--accent-2: #e85a2c;
--paper: #f6efdd;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
width: 1920px;
height: 1080px;
background: var(--bg);
color: var(--ink);
font-family: 'Inter', -apple-system, system-ui, sans-serif;
overflow: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
background:
radial-gradient(1200px 700px at 78% 22%, rgba(240,131,63,0.12), transparent 70%),
radial-gradient(900px 600px at 12% 80%, rgba(232,90,44,0.06), transparent 70%),
linear-gradient(180deg, #0e0b07 0%, #0a0805 100%);
padding: 56px 72px;
display: flex;
flex-direction: column;
}
.mono {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 12px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
}
header {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding-bottom: 22px;
border-bottom: 1px solid var(--rule);
}
header .l { text-align: left; }
header .r { text-align: right; }
header .center {
font-family: 'Fraunces', serif;
font-style: italic;
font-weight: 500;
font-size: 22px;
color: var(--ink);
letter-spacing: 0.01em;
}
header .center .dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
margin: 0 8px 4px 0;
vertical-align: middle;
}
main {
flex: 1;
display: grid;
grid-template-columns: 1.05fr 1fr;
column-gap: 64px;
padding-top: 56px;
align-items: stretch;
}
.left {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.eyebrow {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 28px;
}
.eyebrow .pill {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
letter-spacing: 0.22em;
color: #f6e9d4;
background: rgba(240,131,63,0.16);
border: 1px solid rgba(240,131,63,0.45);
padding: 6px 12px;
border-radius: 999px;
text-transform: uppercase;
}
.eyebrow .pill.alt {
color: var(--muted);
background: transparent;
border-color: var(--rule);
}
h1 {
font-family: 'Fraunces', serif;
font-weight: 500;
font-size: 116px;
line-height: 0.94;
letter-spacing: -0.025em;
color: var(--ink);
margin: 0 0 36px 0;
}
h1 em {
font-style: italic;
color: var(--accent);
font-weight: 500;
}
h1 .small {
font-size: 96px;
}
.lede {
font-family: 'Fraunces', serif;
font-weight: 300;
font-size: 22px;
line-height: 1.5;
color: #d8cdb6;
max-width: 640px;
margin: 0 0 32px 0;
}
.lede b {
font-weight: 500;
color: #f3ead8;
}
.meta {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0;
border-top: 1px solid var(--rule);
padding-top: 22px;
}
.meta .cell {
border-right: 1px solid var(--rule);
padding-right: 18px;
}
.meta .cell:last-child { border-right: none; }
.meta .num {
font-family: 'Fraunces', serif;
font-weight: 500;
font-size: 42px;
line-height: 1;
color: var(--ink);
letter-spacing: -0.01em;
}
.meta .num em {
font-style: italic;
color: var(--accent);
font-weight: 500;
}
.meta .lbl {
margin-top: 10px;
font-family: 'JetBrains Mono', monospace;
font-size: 10.5px;
letter-spacing: 0.18em;
color: var(--muted);
text-transform: uppercase;
line-height: 1.4;
}
/* right artifact collage */
.right {
position: relative;
}
.stage {
position: absolute;
inset: 0;
}
.card {
position: absolute;
border-radius: 18px;
overflow: hidden;
background: #1a140d;
box-shadow:
0 1px 0 rgba(255,255,255,0.04) inset,
0 24px 60px rgba(0,0,0,0.55),
0 6px 18px rgba(0,0,0,0.45);
border: 1px solid rgba(255,255,255,0.06);
}
.card img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.card .tag {
position: absolute;
top: 14px;
left: 14px;
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #ffd9b6;
background: rgba(20, 14, 8, 0.7);
padding: 6px 10px;
border-radius: 6px;
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
border: 1px solid rgba(255,217,182,0.18);
}
/* arrange three artifacts as an editorial collage */
.card.a { /* gamified-app — back-left */
width: 540px;
height: 320px;
top: 70px;
right: 280px;
transform: rotate(-3.5deg);
}
.card.b { /* digital-eguide — front-right hero */
width: 620px;
height: 360px;
top: 220px;
right: 0;
transform: rotate(2.5deg);
z-index: 3;
}
.card.c { /* dating-web — bottom-left */
width: 520px;
height: 300px;
top: 460px;
right: 230px;
transform: rotate(-1.5deg);
z-index: 2;
}
/* decorative marks */
.mark-circle {
position: absolute;
top: 36px;
right: 24px;
width: 86px;
height: 86px;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #ffb37a, var(--accent-2) 80%);
color: #2a1408;
font-family: 'Fraunces', serif;
font-style: italic;
font-weight: 700;
font-size: 14px;
line-height: 1.15;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
transform: rotate(8deg);
box-shadow: 0 14px 30px rgba(232,90,44,0.45);
z-index: 5;
}
.mark-circle span { padding: 0 10px; }
footer {
margin-top: 48px;
padding-top: 22px;
border-top: 1px solid var(--rule);
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
}
footer .l { text-align: left; }
footer .r { text-align: right; }
footer .c {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
letter-spacing: 0.28em;
color: var(--muted);
text-transform: uppercase;
}
.underline-accent {
position: relative;
display: inline-block;
}
.underline-accent::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: -6px;
height: 6px;
background: var(--accent);
border-radius: 4px;
opacity: 0.85;
}
</style>
</head>
<body>
<header>
<div class='mono l'>Open Design · Manifesto · 2026 Edition</div>
<div class='center'><span class='dot'></span>open.design</div>
<div class='mono r'>Cover · 01 / 08 · OSS Alternative</div>
</header>
<main>
<section class='left'>
<div>
<div class='eyebrow'>
<span class='pill'>· APACHE 2.0</span>
<span class='pill alt'>Local-first · BYOK</span>
</div>
<h1>
Design with the<br/>
<em>agent</em> already<br/>
<span class='small'>on your <span class='underline-accent'>laptop</span>.</span>
</h1>
<p class='lede'>
Open Design is the open-source alternative to Claude Design.
Your existing coding agent — <b>Claude Code · Codex · Cursor · Gemini · OpenCode · Qwen</b>
becomes the design engine, driven by 19 composable Skills and 71 brand-grade Design Systems.
</p>
</div>
<div class='meta'>
<div class='cell'>
<div class='num'>71</div>
<div class='lbl'>Design<br/>Systems</div>
</div>
<div class='cell'>
<div class='num'>19</div>
<div class='lbl'>Composable<br/>Skills</div>
</div>
<div class='cell'>
<div class='num'>06</div>
<div class='lbl'>Coding<br/>Agents</div>
</div>
<div class='cell'>
<div class='num'><em>0</em></div>
<div class='lbl'>Lock-in /<br/>Vendor Cloud</div>
</div>
</div>
</section>
<section class='right'>
<div class='stage'>
<div class='card a'>
<span class='tag'>· Hi-Fi Prototype · iPhone</span>
<img src='../../screenshots/skills/gamified-app.png' alt=''>
</div>
<div class='card b'>
<span class='tag'>· Digital E-guide · 64pp</span>
<img src='../../screenshots/skills/digital-eguide.png' alt=''>
</div>
<div class='card c'>
<span class='tag'>· Dating App · Web</span>
<img src='../../screenshots/skills/dating-web.png' alt=''>
</div>
<div class='mark-circle'><span>OPEN<br/>SOURCE</span></div>
</div>
</section>
</main>
<footer>
<div class='mono l'>BYOK at every layer · No cloud lock-in</div>
<div class='c'>· pnpm dev · vercel deploy · npm start ·</div>
<div class='mono r'>github.com/open-design</div>
</footer>
</body>
</html>
+520
View File
@@ -0,0 +1,520 @@
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8' />
<title>71 Design Systems — cover</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=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,500;0,9..144,700;1,9..144,500;1,9..144,700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500&display=swap' rel='stylesheet'>
<style>
:root {
--surface: #d9c3bd;
--surface-2: #e6d2cc;
--paper: #f5ecd9;
--paper-edge: #e8dcc4;
--ink: #1c1814;
--ink-soft: #4d4339;
--muted: #8a7e70;
--rule: #c8b9a1;
--accent: #c44a3a;
--accent-2: #d4593f;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
width: 1920px;
height: 1080px;
background: var(--surface);
color: var(--ink);
font-family: 'Inter', -apple-system, system-ui, sans-serif;
overflow: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
background:
radial-gradient(1100px 700px at 28% 12%, var(--surface-2), transparent 70%),
radial-gradient(900px 600px at 88% 90%, #cab1aa, transparent 70%),
var(--surface);
padding: 60px 80px;
display: flex;
gap: 56px;
align-items: stretch;
}
.page {
background: var(--paper);
flex: 1;
border-radius: 4px;
padding: 56px 60px;
box-shadow:
0 0 0 1px var(--paper-edge),
0 30px 60px rgba(60, 30, 24, 0.18),
0 8px 22px rgba(60, 30, 24, 0.12);
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
}
.page::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background:
linear-gradient(180deg, rgba(28,24,20,0.04), transparent 8%, transparent 92%, rgba(28,24,20,0.05));
}
.mono {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 11px;
letter-spacing: 0.22em;
color: var(--muted);
text-transform: uppercase;
}
.pageHead {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 14px;
border-bottom: 1px solid var(--rule);
}
.dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
margin-right: 8px;
vertical-align: middle;
transform: translateY(-1px);
}
/* ---------- LEFT (cover) page ---------- */
.cover h1 {
font-family: 'Fraunces', serif;
font-weight: 500;
font-size: 96px;
line-height: 0.94;
letter-spacing: -0.02em;
color: var(--ink);
margin: 64px 0 32px 0;
}
.cover h1 em {
font-style: italic;
color: var(--accent);
font-weight: 500;
}
.cover .tagline {
font-family: 'Fraunces', serif;
font-style: italic;
font-weight: 400;
font-size: 22px;
line-height: 1.45;
color: var(--ink-soft);
max-width: 480px;
margin: 0 0 36px 0;
}
.byline {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
letter-spacing: 0.22em;
color: var(--muted);
text-transform: uppercase;
padding: 14px 0;
border-top: 1px solid var(--rule);
border-bottom: 1px solid var(--rule);
margin-bottom: 36px;
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0;
margin-bottom: 44px;
}
.stats .cell { padding-right: 16px; }
.stats .cell + .cell { border-left: 1px solid var(--rule); padding-left: 24px; }
.stats .num {
font-family: 'Fraunces', serif;
font-weight: 500;
font-size: 64px;
line-height: 1;
color: var(--ink);
letter-spacing: -0.02em;
}
.stats .num em { font-style: italic; color: var(--accent); }
.stats .lbl {
margin-top: 10px;
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
letter-spacing: 0.22em;
color: var(--muted);
text-transform: uppercase;
line-height: 1.4;
}
.toc-title {
font-family: 'Fraunces', serif;
font-weight: 500;
font-size: 32px;
margin: 8px 0 18px 0;
color: var(--ink);
}
.toc-title em { font-style: italic; color: var(--accent); }
.toc {
display: flex;
flex-direction: column;
gap: 10px;
}
.toc-row {
display: flex;
align-items: baseline;
font-family: 'Fraunces', serif;
font-size: 17px;
color: var(--ink);
}
.toc-row .name { flex: 0 0 auto; }
.toc-row .dots {
flex: 1;
border-bottom: 1px dotted #b6a487;
margin: 0 10px;
transform: translateY(-4px);
}
.toc-row .pg {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
letter-spacing: 0.18em;
color: var(--muted);
}
.cover .footer {
margin-top: auto;
display: flex;
justify-content: space-between;
border-top: 1px solid var(--rule);
padding-top: 14px;
}
/* ---------- RIGHT (index) page ---------- */
.index .h2 {
font-family: 'Fraunces', serif;
font-weight: 500;
font-size: 56px;
line-height: 1;
letter-spacing: -0.02em;
color: var(--ink);
margin: 32px 0 12px 0;
}
.index .h2 em {
font-style: italic;
color: var(--accent);
font-weight: 500;
}
.index .sub {
font-family: 'Fraunces', serif;
font-style: italic;
font-weight: 400;
font-size: 18px;
color: var(--ink-soft);
line-height: 1.5;
max-width: 540px;
margin: 0 0 28px 0;
}
.drop {
float: left;
font-family: 'Fraunces', serif;
font-weight: 500;
font-size: 76px;
line-height: 0.85;
color: var(--accent);
margin: 8px 12px -4px 0;
}
.drop + p {
font-family: 'Fraunces', serif;
font-size: 16px;
line-height: 1.5;
color: var(--ink-soft);
margin: 0 0 18px 0;
}
.columns {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 36px;
row-gap: 6px;
margin: 18px 0 12px 0;
border-top: 1px solid var(--rule);
border-bottom: 1px solid var(--rule);
padding: 14px 0;
}
.item {
display: grid;
grid-template-columns: 26px 1fr auto;
align-items: baseline;
gap: 10px;
padding: 6px 0;
font-family: 'Fraunces', serif;
font-size: 16px;
color: var(--ink);
}
.item .n {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
letter-spacing: 0.12em;
color: var(--muted);
}
.item .swatch {
width: 14px;
height: 14px;
border-radius: 3px;
display: inline-block;
transform: translateY(2px);
margin-right: 6px;
box-shadow: 0 0 0 1px rgba(0,0,0,0.08) inset;
}
.item .name { font-weight: 500; }
.item .tag {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
}
.item.featured .name { color: var(--accent); font-style: italic; }
.spotlight {
display: grid;
grid-template-columns: 110px 1fr;
gap: 18px;
align-items: center;
background: #f0e3c8;
border: 1px solid var(--rule);
border-radius: 6px;
padding: 18px 20px;
margin-top: 18px;
}
.spotlight .swatches {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
}
.spotlight .sw {
height: 28px;
border-radius: 4px;
box-shadow: 0 0 0 1px rgba(0,0,0,0.06) inset;
}
.spotlight .label {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
letter-spacing: 0.18em;
color: var(--muted);
text-transform: uppercase;
margin-bottom: 4px;
}
.spotlight h3 {
font-family: 'Fraunces', serif;
font-weight: 500;
font-style: italic;
font-size: 22px;
margin: 0 0 6px 0;
color: var(--ink);
}
.spotlight h3 em { color: var(--accent); }
.spotlight p {
font-family: 'Fraunces', serif;
font-size: 14px;
line-height: 1.5;
color: var(--ink-soft);
margin: 0;
}
.index .footer {
margin-top: auto;
display: flex;
justify-content: space-between;
border-top: 1px solid var(--rule);
padding-top: 14px;
}
.stamp {
position: absolute;
top: 56px;
right: 56px;
width: 88px;
height: 88px;
border-radius: 50%;
background: var(--accent);
color: #fff5e9;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
transform: rotate(8deg);
font-family: 'Fraunces', serif;
font-weight: 500;
font-style: italic;
font-size: 13px;
line-height: 1.15;
box-shadow: 0 12px 28px rgba(196,74,58,0.35);
z-index: 2;
}
</style>
</head>
<body>
<!-- LEFT PAGE: cover -->
<article class='page cover'>
<div class='pageHead'>
<div class='mono'><span class='dot'></span>Style &amp; Format Guide for Designers</div>
<div class='mono'>2026 Edition</div>
</div>
<h1>The <em>71</em><br/>Systems<br/>Library.</h1>
<p class='tagline'>
Linear, Stripe, Vercel, Airbnb, Tesla, Notion, Anthropic, Apple, Cursor, Supabase, Figma —
seventy-one brand-grade systems, one open library, zero lock-in.
</p>
<div class='byline'>By Open Design · Maintained on GitHub · 19 / 04 / 2026</div>
<div class='stats'>
<div class='cell'>
<div class='num'>71</div>
<div class='lbl'>Design<br/>Systems</div>
</div>
<div class='cell'>
<div class='num'>19</div>
<div class='lbl'>Composable<br/>Skills</div>
</div>
<div class='cell'>
<div class='num'><em>1</em></div>
<div class='lbl'>Library, zero<br/>vendor cloud</div>
</div>
</div>
<h2 class='toc-title'>What's <em>inside</em>.</h2>
<div class='toc'>
<div class='toc-row'><span class='name'>Tokens, palettes, motion</span><span class='dots'></span><span class='pg'>04</span></div>
<div class='toc-row'><span class='name'>Pick a direction</span><span class='dots'></span><span class='pg'>12</span></div>
<div class='toc-row'><span class='name'>Tone &amp; typography</span><span class='dots'></span><span class='pg'>18</span></div>
<div class='toc-row'><span class='name'>71 systems index</span><span class='dots'></span><span class='pg'>24</span></div>
<div class='toc-row'><span class='name'>Bring-your-own-key</span><span class='dots'></span><span class='pg'>40</span></div>
<div class='toc-row'><span class='name'>The anti-AI-slop checklist</span><span class='dots'></span><span class='pg'>52</span></div>
</div>
<div class='footer'>
<div class='mono'>Tokens, palettes &amp; type</div>
<div class='mono'>01 / 64</div>
</div>
</article>
<!-- RIGHT PAGE: index -->
<article class='page index'>
<div class='pageHead'>
<div class='mono'><span class='dot'></span>Chapter 02 · Index</div>
<div class='mono'>71 entries · A → Z</div>
</div>
<h2 class='h2'>All systems —<br/><em>one library.</em></h2>
<p class='sub'>
Every system ships a deterministic OKLch palette, a font stack, and a tone profile.
Pick one tile and the agent inherits the whole brand.
</p>
<p>
<span class='drop'>S</span>
eventy-one product systems, two hand-authored starters, five visual directions. Imported and curated
from awesome-design-md, hand-tuned for Open Design's discovery loop. Drop one in,
every artifact downstream changes accordingly — no model freestyle.
</p>
<div class='columns'>
<div class='item featured'>
<span class='n'>03</span>
<span><span class='swatch' style='background:#5e6ad2'></span><span class='name'>Linear</span></span>
<span class='tag'>· graphite · violet</span>
</div>
<div class='item'>
<span class='n'>07</span>
<span><span class='swatch' style='background:#635bff'></span><span class='name'>Stripe</span></span>
<span class='tag'>· payments · indigo</span>
</div>
<div class='item'>
<span class='n'>09</span>
<span><span class='swatch' style='background:#000000'></span><span class='name'>Vercel</span></span>
<span class='tag'>· void · grayscale</span>
</div>
<div class='item'>
<span class='n'>14</span>
<span><span class='swatch' style='background:#ff385c'></span><span class='name'>Airbnb</span></span>
<span class='tag'>· rausch · rounded</span>
</div>
<div class='item'>
<span class='n'>18</span>
<span><span class='swatch' style='background:#cc0000'></span><span class='name'>Tesla</span></span>
<span class='tag'>· red · industrial</span>
</div>
<div class='item'>
<span class='n'>22</span>
<span><span class='swatch' style='background:#000000'></span><span class='name'>Notion</span></span>
<span class='tag'>· paper · serif</span>
</div>
<div class='item'>
<span class='n'>27</span>
<span><span class='swatch' style='background:#cc785c'></span><span class='name'>Anthropic</span></span>
<span class='tag'>· clay · serif</span>
</div>
<div class='item'>
<span class='n'>31</span>
<span><span class='swatch' style='background:#a8a8a8'></span><span class='name'>Apple</span></span>
<span class='tag'>· chrome · sf pro</span>
</div>
<div class='item'>
<span class='n'>34</span>
<span><span class='swatch' style='background:#1a1a1a'></span><span class='name'>Cursor</span></span>
<span class='tag'>· terminal · mono</span>
</div>
<div class='item'>
<span class='n'>41</span>
<span><span class='swatch' style='background:#3ecf8e'></span><span class='name'>Supabase</span></span>
<span class='tag'>· emerald · rounded</span>
</div>
<div class='item'>
<span class='n'>48</span>
<span><span class='swatch' style='background:#0acf83'></span><span class='name'>Figma</span></span>
<span class='tag'>· spectrum · canvas</span>
</div>
<div class='item'>
<span class='n'>57</span>
<span><span class='swatch' style='background:#000000'></span><span class='name'>OpenAI</span></span>
<span class='tag'>· void · sober</span>
</div>
</div>
<div class='spotlight'>
<div class='swatches'>
<span class='sw' style='background:#1c1816'></span>
<span class='sw' style='background:#5e6ad2'></span>
<span class='sw' style='background:#9b9bd6'></span>
<span class='sw' style='background:#f3ead8'></span>
</div>
<div>
<div class='label'>Spotlight · Linear · 03 / 71</div>
<h3>Graphite + electric <em>violet.</em></h3>
<p>IBM Plex Sans · Inter Display · 4-step OKLch palette · 16/24 grid · square radius. The agent inherits the full token tree the moment you tap the tile.</p>
</div>
</div>
<div class='footer'>
<div class='mono'>Chapter 02 · Index</div>
<div class='mono'>24 / 64</div>
</div>
<div class='stamp'><span>71<br/>SYSTEMS</span></div>
</article>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

+1 -1
View File
@@ -11,7 +11,7 @@ Every external project this spec leans on. Three questions per entry: what is it
### [Anthropic Claude Design][cd]
- **URL:** [claude.ai/design][cd] · [release announcement](https://www.infoq.cn/article/TH0QVHpvVGZ7VP3hAEmm) · [ifanr review](https://www.ifanr.com/1662860)
[cd]: https://www.anthropic.com/news/claude-design
[cd]: https://x.com/claudeai/status/2045156267690213649
- **What it is:** Anthropic's closed-source AI design product. Released 2026-04-17. Powered by Opus 4.7. Web-only (claude.ai). Generates prototypes, wireframes, decks, marketing pages, complex prototypes with voice/video/3D/shaders.
- **Why it matters to us:** Defines the category. Its viral moment (~60M X impressions week 1) proves the market.
- **What we borrow:** The high-level value prop — "natural language → editable visual design." Feature inspiration for modes (prototype, deck, marketing). UI ideas around inline editing and custom sliders.
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

+1 -1
View File
@@ -3,7 +3,7 @@
**Status:** Draft v0.1 · 2026-04-24
**Scope:** Product definition, scenarios, non-goals, high-level modules, and positioning against both [Anthropic's Claude Design][cd] and the existing open-source alternative ([Open CoDesign][ocod]).
[cd]: https://www.anthropic.com/news/claude-design
[cd]: https://x.com/claudeai/status/2045156267690213649
[ocod]: https://github.com/OpenCoworkAI/open-codesign
[guizang]: https://github.com/op7418/guizang-ppt-skill
[multica]: https://github.com/multica-ai/multica
+1
View File
@@ -21,6 +21,7 @@ od:
mode: prototype
platform: desktop
scenario: design
fidelity: wireframe
preview:
type: html
entry: index.html
+6 -2
View File
@@ -76,8 +76,12 @@ export const ChatComposer = forwardRef<ChatComposerHandle, Props>(
const importTriggerRef = useRef<HTMLButtonElement | null>(null);
// initialDraft is only honored on the first non-empty value the parent
// hands us. After we seed once, the composer is fully under user control
// — re-renders that pass the same prompt back must not reseed.
const seededRef = useRef(false);
// — re-renders that pass the same prompt back must not reseed. If the
// initial useState above already consumed a non-empty initialDraft we
// mark it seeded immediately, so an early clear by the user (typing or
// backspace before the parent stops passing initialDraft) does not get
// overwritten by the effect.
const seededRef = useRef(Boolean(initialDraft));
useEffect(() => {
if (seededRef.current) return;
+43 -22
View File
@@ -5,6 +5,8 @@ import type {
AppConfig,
DesignSystemSummary,
Project,
ProjectKind,
ProjectMetadata,
ProjectTemplate,
SkillSummary,
} from '../types';
@@ -15,7 +17,7 @@ import { ExamplesTab } from './ExamplesTab';
import { Icon } from './Icon';
import { LanguageMenu } from './LanguageMenu';
import { CenteredLoader } from './Loading';
import { NewProjectPanel, type CreateInput, type CreateTab } from './NewProjectPanel';
import { NewProjectPanel, type CreateInput } from './NewProjectPanel';
type TopTab = 'designs' | 'examples' | 'design-systems';
@@ -70,13 +72,6 @@ export function EntryView({
const t = useT();
const [topTab, setTopTab] = useState<TopTab>('designs');
const [previewSystemId, setPreviewSystemId] = useState<string | null>(null);
const [panelPreset, setPanelPreset] = useState<{
tab: CreateTab;
skillId: string | null;
name: string;
pendingPrompt?: string;
nonce: number;
} | null>(null);
const [sidebarWidth, setSidebarWidth] = useState<number>(() => loadSidebarWidth());
const [resizing, setResizing] = useState(false);
@@ -98,13 +93,16 @@ export function EntryView({
: t('settings.noAgentSelected');
}, [config.mode, config.model, config.baseUrl, currentAgent, t]);
// 'Use this prompt' on an example card is a fast path — skip the form and
// create the project immediately with sane defaults derived from the skill,
// seeding the chat composer with the example prompt via pendingPrompt.
function usePromptFromSkill(skill: SkillSummary) {
setPanelPreset({
tab: tabForSkill(skill),
skillId: skill.id,
onCreateProject({
name: skill.name,
skillId: skill.id,
designSystemId: null,
metadata: metadataForSkill(skill),
pendingPrompt: skill.examplePrompt || skill.description,
nonce: Date.now(),
});
}
@@ -118,10 +116,7 @@ export function EntryView({
);
function handleCreate(input: CreateInput) {
onCreateProject({
...input,
pendingPrompt: panelPreset?.pendingPrompt,
});
onCreateProject(input);
}
const startWidthRef = useRef(0);
@@ -177,15 +172,11 @@ export function EntryView({
</div>
</div>
<NewProjectPanel
key={panelPreset?.nonce ?? 'default'}
skills={skills}
designSystems={designSystems}
defaultDesignSystemId={defaultDesignSystemId}
templates={templates}
onCreate={handleCreate}
presetTab={panelPreset?.tab}
presetSkillId={panelPreset?.skillId ?? null}
presetName={panelPreset?.name}
loading={loading}
/>
<div className="entry-side-foot">
@@ -313,8 +304,38 @@ function TopTabButton({
);
}
function tabForSkill(skill: SkillSummary): CreateTab {
// Map a skill's declared mode to project metadata. Falls back to the same
// defaults the new-project form would apply (high-fidelity prototype, no
// speaker notes on decks, no template animations) so 'Use this prompt'
// produces a project indistinguishable from one created via the form. Per-
// skill hints in SKILL.md frontmatter (od.fidelity, od.speaker_notes,
// od.animations) override the defaults so each example reproduces the
// shipped example.html — e.g. wireframe-sketch declares fidelity:wireframe.
function metadataForSkill(skill: SkillSummary): ProjectMetadata {
const kind = kindForSkill(skill);
if (kind === 'prototype') {
return { kind, fidelity: skill.fidelity ?? 'high-fidelity' };
}
if (kind === 'deck') {
return {
kind,
speakerNotes:
typeof skill.speakerNotes === 'boolean' ? skill.speakerNotes : false,
};
}
if (kind === 'template') {
return {
kind,
animations:
typeof skill.animations === 'boolean' ? skill.animations : false,
};
}
return { kind: 'other' };
}
function kindForSkill(skill: SkillSummary): ProjectKind {
if (skill.mode === 'deck') return 'deck';
if (skill.mode === 'prototype') return 'prototype';
return 'template';
if (skill.mode === 'template') return 'template';
return 'other';
}
+20 -10
View File
@@ -141,13 +141,23 @@ function HtmlViewer({
};
}, [projectId, file.name, file.mtime, liveHtml, reloadKey]);
// Detect deck-shaped HTML even when the project's skill didn't declare
// `mode: deck`. Freeform projects often produce a deck because the user
// asked for one in plain prose; without this, prev/next and Present
// never surface and the deck becomes a static, unnavigable preview.
const looksLikeDeck = useMemo(() => {
if (!source) return false;
return /class\s*=\s*['"][^'"]*\bslide\b/i.test(source);
}, [source]);
const effectiveDeck = isDeck || looksLikeDeck;
const srcDoc = useMemo(
() => (source ? buildSrcdoc(source, { deck: isDeck }) : ''),
[source, isDeck],
() => (source ? buildSrcdoc(source, { deck: effectiveDeck }) : ''),
[source, effectiveDeck],
);
useEffect(() => {
if (!isDeck) {
if (!effectiveDeck) {
setSlideState(null);
return;
}
@@ -161,7 +171,7 @@ function HtmlViewer({
}
window.addEventListener('message', onMessage);
return () => window.removeEventListener('message', onMessage);
}, [isDeck]);
}, [effectiveDeck]);
function postSlide(action: 'next' | 'prev' | 'first' | 'last') {
const win = iframeRef.current?.contentWindow;
@@ -172,7 +182,7 @@ function HtmlViewer({
// Keyboard nav on the host, so the user can press ←/→ even when focus
// is on the chat composer or any other host control.
useEffect(() => {
if (!isDeck || mode !== 'preview') return;
if (!effectiveDeck || mode !== 'preview') return;
function onKey(e: KeyboardEvent) {
const target = e.target as HTMLElement | null;
if (target) {
@@ -195,7 +205,7 @@ function HtmlViewer({
}
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [isDeck, mode]);
}, [effectiveDeck, mode]);
useEffect(() => {
if (!presentMenuOpen) return;
@@ -309,7 +319,7 @@ function HtmlViewer({
setZoom((z) => Math.max(25, Math.min(200, z + delta)));
}
const showPresent = isDeck && source !== null;
const showPresent = effectiveDeck && source !== null;
const canShare = source !== null;
const exportTitle = file.name.replace(/\.html?$/i, '') || file.name;
const canPptx = canShare && Boolean(onExportAsPptx) && !streaming;
@@ -328,7 +338,7 @@ function HtmlViewer({
>
<Icon name="reload" size={14} />
</button>
{isDeck ? (
{effectiveDeck ? (
<span
className="deck-nav"
role="group"
@@ -502,12 +512,12 @@ function HtmlViewer({
role="menuitem"
onClick={() => {
setShareMenuOpen(false);
exportAsPdf(source ?? '', exportTitle, { deck: isDeck });
exportAsPdf(source ?? '', exportTitle, { deck: effectiveDeck });
}}
>
<span className="share-menu-icon"><Icon name="file" size={14} /></span>
<span>
{isDeck
{effectiveDeck
? t('fileViewer.exportPdfAllSlides')
: t('fileViewer.exportPdf')}
</span>
+3 -10
View File
@@ -28,9 +28,6 @@ interface Props {
defaultDesignSystemId: string | null;
templates: ProjectTemplate[];
onCreate: (input: CreateInput) => void;
presetTab?: CreateTab;
presetSkillId?: string | null;
presetName?: string;
loading?: boolean;
}
@@ -47,14 +44,11 @@ export function NewProjectPanel({
defaultDesignSystemId,
templates,
onCreate,
presetTab,
presetSkillId,
presetName,
loading = false,
}: Props) {
const t = useT();
const [tab, setTab] = useState<CreateTab>(presetTab ?? 'prototype');
const [name, setName] = useState(presetName ?? '');
const [tab, setTab] = useState<CreateTab>('prototype');
const [name, setName] = useState('');
// Design-system selection is now an *array* internally so the same
// component can drive both single-select and multi-select modes without
// duplicating state. Single-select coerces to length 0/1.
@@ -89,7 +83,6 @@ export function NewProjectPanel({
// pick a default-rendered skill (so the agent gets the right SKILL.md
// body) without requiring the user to choose one explicitly.
const skillIdForTab = useMemo(() => {
if (presetSkillId !== undefined) return presetSkillId;
if (tab === 'other') return null;
if (tab === 'prototype') {
const list = skills.filter((s) => s.mode === 'prototype');
@@ -104,7 +97,7 @@ export function NewProjectPanel({
?? null;
}
return null;
}, [tab, skills, presetSkillId]);
}, [tab, skills]);
const canCreate =
!loading && (tab !== 'template' || templateId != null);
+16 -3
View File
@@ -666,9 +666,22 @@ export function ProjectView({
[skills, project.skillId],
);
// Hand the pending prompt to ChatPane exactly once. After the first render
// we tell App to clear it so re-entering the project later doesn't reseed.
const initialDraft = project.pendingPrompt;
// Hand the pending prompt to ChatPane exactly once. We snapshot the value
// into local state on mount so it survives the ChatPane remount triggered
// when `activeConversationId` resolves from `null` to a real id (the
// `key={activeConversationId}` on ChatPane otherwise wipes the freshly
// seeded composer draft). Once the conversation id is in place — meaning
// ChatPane has remounted with the seed still available — we clear both
// the local snapshot and the persisted pendingPrompt so future
// conversation switches don't keep re-seeding the composer.
const [initialDraft, setInitialDraft] = useState<string | undefined>(
project.pendingPrompt,
);
useEffect(() => {
if (initialDraft && activeConversationId) {
setInitialDraft(undefined);
}
}, [initialDraft, activeConversationId]);
useEffect(() => {
if (project.pendingPrompt) onClearPendingPrompt();
}, [project.pendingPrompt, onClearPendingPrompt]);
+212 -20
View File
@@ -16,10 +16,10 @@
*/
export function buildSrcdoc(
html: string,
options: { deck?: boolean } = {},
options: { deck?: boolean } = {}
): string {
const head = html.trimStart().slice(0, 64).toLowerCase();
const isFullDoc = head.startsWith('<!doctype') || head.startsWith('<html');
const isFullDoc = head.startsWith("<!doctype") || head.startsWith("<html");
const wrapped = isFullDoc
? html
: `<!doctype html>
@@ -30,33 +30,174 @@ export function buildSrcdoc(
</head>
<body>${html}</body>
</html>`;
if (!options.deck) return wrapped;
return injectDeckBridge(wrapped);
const withShim = injectSandboxShim(wrapped);
if (!options.deck) return withShim;
return injectDeckBridge(withShim);
}
// Sandboxed iframes (we use `sandbox="allow-scripts"`) without
// `allow-same-origin` raise a SecurityError on first `localStorage` /
// `sessionStorage` access. Many freeform-generated decks call
// `localStorage.getItem(...)` at the top of their IIFE without a
// try/catch — when it throws, the whole script aborts and the deck
// becomes a static, unnavigable preview. We install a same-origin
// in-memory shim BEFORE any user script runs so those decks degrade
// gracefully (position just doesn't persist across reloads).
function injectSandboxShim(doc: string): string {
const shim = `<script>(function(){
function makeStore(){
var data = {};
var api = {
getItem: function(k){ return Object.prototype.hasOwnProperty.call(data, k) ? data[k] : null; },
setItem: function(k, v){ data[k] = String(v); },
removeItem: function(k){ delete data[k]; },
clear: function(){ data = {}; },
key: function(i){ return Object.keys(data)[i] || null; }
};
Object.defineProperty(api, 'length', { get: function(){ return Object.keys(data).length; } });
return api;
}
function tryShim(name){
var works = false;
try { works = !!window[name] && typeof window[name].getItem === 'function'; void window[name].length; }
catch (_) { works = false; }
if (works) return;
try { Object.defineProperty(window, name, { configurable: true, value: makeStore() }); }
catch (_) { try { window[name] = makeStore(); } catch (__) {} }
}
tryShim('localStorage');
tryShim('sessionStorage');
})();</script>`;
if (/<head[^>]*>/i.test(doc))
return doc.replace(/<head[^>]*>/i, (m) => `${m}${shim}`);
if (/<body[^>]*>/i.test(doc))
return doc.replace(/<body[^>]*>/i, (m) => `${m}${shim}`);
return shim + doc;
}
// The deck bridge supports three deck conventions found across our skills
// and freeform-generated artifacts:
// 1. Horizontal scroll decks (simple-deck, guizang-ppt) — slides laid out
// side-by-side, navigation = scrollTo({ left }).
// 2. Class-toggle decks (deck-framework, freeform pitches) — one slide
// carries `.active` or `.is-active`; siblings are display:none. Their
// own JS listens for ArrowRight/Left, so we drive them by dispatching
// synthetic KeyboardEvents.
// 3. Visibility-only decks — no class toggle, slides hidden via inline
// style. We fall back to keyboard dispatch + visibility detection.
//
// All three report `{ active, count }` back to the host so the toolbar can
// render a unified counter. A MutationObserver on each `.slide` lets us
// catch class changes from the deck's own keyboard handler.
//
// We also inject a small CSS override that fixes a common authoring
// mistake in fixed-canvas decks: a `.stage { display: grid; place-items:
// center }` only centers items within their grid cells, but the track
// itself stays `start`-aligned, so the 1920x1080 canvas top-lefts at
// (0,0) of the stage. Combined with `transform-origin: center center`,
// the scaled canvas ends up offset toward the bottom-right of any
// preview that's smaller than 1920x1080 — exactly what users see in the
// sandbox iframe. `place-content: center` centers the track itself.
function injectDeckBridge(doc: string): string {
const styleFix = `<style data-od-deck-fix>
.stage, .deck-stage, .deck-shell { place-content: center !important; }
</style>`;
const docWithStyle = /<\/head>/i.test(doc)
? doc.replace(/<\/head>/i, styleFix + "</head>")
: /<head[^>]*>/i.test(doc)
? doc.replace(/<head[^>]*>/i, (m) => m + styleFix)
: styleFix + doc;
doc = docWithStyle;
const script = `<script>(function(){
function slides(){ return document.querySelectorAll('.slide'); }
function scroller(){
if (document.body.scrollWidth > document.body.clientWidth + 1) return document.body;
if (document.body && document.body.scrollWidth > document.body.clientWidth + 1) return document.body;
return document.scrollingElement || document.documentElement;
}
function activeIndex(){
return Math.round(scroller().scrollLeft / window.innerWidth);
function isScrollDeck(){
var sc = scroller();
return !!(sc && sc.scrollWidth > sc.clientWidth + 1);
}
function go(i){
function findActiveByClass(list){
for (var i=0; i<list.length; i++) {
var cl = list[i].classList;
if (cl && (cl.contains('is-active') || cl.contains('active') || cl.contains('current'))) return i;
}
return -1;
}
function findActiveByVisibility(list){
for (var i=0; i<list.length; i++) {
try {
var cs = window.getComputedStyle(list[i]);
if (cs.display !== 'none' && cs.visibility !== 'hidden' && cs.opacity !== '0') return i;
} catch (_) {}
}
return -1;
}
function activeIndex(list){
if (!list || !list.length) return 0;
if (isScrollDeck()) {
var w = Math.max(1, window.innerWidth);
return Math.max(0, Math.min(list.length - 1, Math.round(scroller().scrollLeft / w)));
}
var byClass = findActiveByClass(list);
if (byClass >= 0) return byClass;
var byVis = findActiveByVisibility(list);
if (byVis >= 0) return byVis;
return 0;
}
function dispatchKey(key){
// Bubbles so any listener on window picks it up too. We dispatch on
// document only — dispatching on window/body in addition would cause
// bubbling to fire the same document-level listener twice.
var init = { key: key, code: key, bubbles: true, cancelable: true, composed: true };
try {
document.dispatchEvent(new KeyboardEvent('keydown', init));
document.dispatchEvent(new KeyboardEvent('keyup', init));
} catch (_) {}
}
function scrollGo(i){
var list = slides();
if (!list.length) return;
var next = Math.max(0, Math.min(list.length - 1, i));
scroller().scrollTo({ left: next * window.innerWidth, behavior: 'smooth' });
setTimeout(report, 360);
setTimeout(report, 380);
}
function go(action){
var list = slides();
if (!list.length) return;
if (isScrollDeck()) {
var i = activeIndex(list);
if (action === 'next') scrollGo(i + 1);
else if (action === 'prev') scrollGo(i - 1);
else if (action === 'first') scrollGo(0);
else if (action === 'last') scrollGo(list.length - 1);
return;
}
if (action === 'next') dispatchKey('ArrowRight');
else if (action === 'prev') dispatchKey('ArrowLeft');
else if (action === 'first') dispatchKey('Home');
else if (action === 'last') dispatchKey('End');
setTimeout(report, 280);
}
function gotoIndex(i){
var list = slides();
if (!list.length) return;
var target = Math.max(0, Math.min(list.length - 1, i));
if (isScrollDeck()) { scrollGo(target); return; }
var current = activeIndex(list);
var diff = target - current;
if (!diff) { report(); return; }
var key = diff > 0 ? 'ArrowRight' : 'ArrowLeft';
var n = Math.abs(diff);
for (var k = 0; k < n; k++) dispatchKey(key);
setTimeout(report, 320);
}
function report(){
try {
var list = slides();
window.parent.postMessage({
type: 'od:slide-state',
active: activeIndex(),
active: activeIndex(list),
count: list.length,
}, '*');
} catch (e) {}
@@ -64,18 +205,69 @@ function injectDeckBridge(doc: string): string {
window.addEventListener('message', function(ev){
var data = ev && ev.data;
if (!data || data.type !== 'od:slide') return;
var list = slides();
var i = activeIndex();
if (data.action === 'next') go(i + 1);
else if (data.action === 'prev') go(i - 1);
else if (data.action === 'first') go(0);
else if (data.action === 'last') go(list.length - 1);
else if (data.action === 'go' && typeof data.index === 'number') go(data.index);
if (data.action === 'go' && typeof data.index === 'number') gotoIndex(data.index);
else go(data.action);
});
// Report once on load and on every scroll-end so the host stays in sync.
window.addEventListener('load', function(){ setTimeout(report, 200); });
document.addEventListener('scroll', function(){ clearTimeout(window.__ocdReportT); window.__ocdReportT = setTimeout(report, 120); }, { passive: true, capture: true });
document.addEventListener('scroll', function(){
clearTimeout(window.__odReportT);
window.__odReportT = setTimeout(report, 120);
}, { passive: true, capture: true });
// Nudge the deck's own fit/resize listener after layout settles. Fixed-canvas
// decks (e.g. ".canvas { width: 1920px }" + "transform: scale(...)") compute
// their scale on first run, which fires when the iframe is still 0x0 in
// sandboxed previews — the deck's fit() then resolves to scale(0) / scale(1)
// and never recovers. Re-firing 'resize' lets the deck recompute, and a
// ResizeObserver picks up later layout settles (zoom toggle, sidebar drag).
function nudgeResize(){
try { window.dispatchEvent(new Event('resize')); }
catch (_) {}
}
// Aggressively nudge during the first second so the deck catches the
// iframe's first non-zero size; bail out early once the iframe reports a
// real width. Without this loop, fixed-canvas decks render at scale(0).
function chaseFirstLayout(){
var attempts = 0;
function tick(){
attempts += 1;
var w = window.innerWidth;
nudgeResize();
if (w > 0 && attempts >= 2) return; // one extra nudge after first non-zero
if (attempts < 30) setTimeout(tick, 50);
}
tick();
}
if (document.readyState === 'complete') chaseFirstLayout();
else window.addEventListener('load', chaseFirstLayout);
// Re-nudge whenever the iframe itself is resized by the host (e.g.
// user toggles zoom, resizes the chat sidebar, exits Present).
if (typeof ResizeObserver !== 'undefined') {
try {
var ro = new ResizeObserver(function(){ nudgeResize(); });
ro.observe(document.documentElement);
} catch (_) {}
}
// For class-toggle decks the deck's own keyboard handler updates classes
// on the slide elements; an attribute observer translates that into the
// host counter without depending on scroll events.
function observeSlides(){
var list = slides();
if (!list.length) { setTimeout(observeSlides, 150); return; }
try {
var mo = new MutationObserver(function(){
clearTimeout(window.__odReportT2);
window.__odReportT2 = setTimeout(report, 60);
});
for (var i = 0; i < list.length; i++) {
mo.observe(list[i], { attributes: true, attributeFilter: ['class', 'style', 'hidden', 'aria-hidden'] });
}
} catch (e) {}
setTimeout(report, 100);
}
observeSlides();
})();</script>`;
if (/<\/body>/i.test(doc)) return doc.replace(/<\/body>/i, `${script}</body>`);
if (/<\/body>/i.test(doc))
return doc.replace(/<\/body>/i, `${script}</body>`);
return doc + script;
}
+9
View File
@@ -88,6 +88,15 @@ export interface SkillSummary {
* the skill in its natural alphabetical position below all featured
* entries. Set via `od.featured` in the SKILL.md frontmatter. */
featured?: number | null;
/** Optional metadata hints, parsed from `od.fidelity`,
* `od.speaker_notes`, and `od.animations` in SKILL.md. Used by the
* Examples gallery's "Use this prompt" fast-create path to mirror the
* shipped `example.html` (e.g. wireframe-sketch declares
* `fidelity: wireframe`). Missing hints fall back to the same defaults
* the new-project form would apply. */
fidelity?: 'wireframe' | 'high-fidelity' | null;
speakerNotes?: boolean | null;
animations?: boolean | null;
hasBody: boolean;
examplePrompt: string;
}
View File
View File