feat(skills): open-design-landing rename, kami skills, landing OG (#428)

* feat(skills): open-design-landing rename, kami skills, landing OG

- Rename editorial-collage skills to open-design-landing and -deck; refresh examples and compose script layout
- Add kami-deck and kami-landing skills with HTML examples
- Landing page: og.astro, index wiring, and style tweaks; package.json bump
- Web i18n: German and Russian copy for renamed and new skills
- Daemon test: update skill-asset-rewrite expectations for new paths
- Design systems: README and atelier-zero doc touch-ups
- Cross-skill SKILL.md reference updates

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(landing-page): document version-slot invariant and deprecation timeline

Address P3 review notes on PR #428:
- Note the `data-github-version` wrapper invariant (version string only)
  near the canonical URL block in `app/page.tsx`.
- Expand the `formatVersion` helper comment in `app/pages/index.astro`
  with concrete `release.name` / `tag_name` example shapes for each
  branch of the regex fallback.
- Tighten the `EditorialCollageDeckInputs` deprecation in
  `skills/open-design-landing-deck/schema.ts` to a specific removal
  version (v0.4.0) and add a "Migrating from editorial-collage-deck"
  section to the skill README.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* docs(landing-page, skills): clarify version slot script and rename migrations

- Describe GitHub version slots as driven by the inline enhancement script,
  not React hydration.
- Add editorial-collage → open-design-landing migration notes; fix README
  link copy (Astro static landing app).
- Extend deck README migration table with shared asset path renames.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(daemon): alias deprecated editorial-collage skill ids

The PR renames the editorial-collage / editorial-collage-deck skills
to open-design-landing / open-design-landing-deck, but the daemon
persists exact skill_id strings on projects and resolves them via
listSkills().find((s) => s.id === storedId). After the rename, any
project saved against an old id silently composes without the intended
skill prompt because the listing no longer exposes that id.

Add a SKILL_ID_ALIASES map in skills.ts plus a findSkillById() helper
that rewrites deprecated ids to their current canonical form, then
route every server-side lookup (skill detail, example HTML, asset
proxy, system-prompt composer) through it. Cover the alias map, the
resolver, and end-to-end resolution against a temp skills directory
with a regression test.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(kami-deck): route host od:slide messages through local go()

The host bridge classifies kami-deck as class-driven because go() toggles
.slide.active, but the visible slide is moved by deck.style.transform
which the bridge cannot drive. Listen for od:slide messages and dispatch
them through the local go() so toolbar next/prev and initialSlideIndex
restore actually shift the deck.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(kami-deck): sync deck transform with host-driven .active changes

The previous fix added a local od:slide listener but the host bridge in
apps/web/src/runtime/srcdoc.ts also listens for the same message and
calls setActive() (toggles .slide.active) without driving the deck
transform. Both listeners fired, the bridge re-read the just-toggled
active class, and overshot by one — and the bridge's restoreInitialSlide
path could move .active without a message at all, leaving the deck on
the original transform.

Stop the bridge from double-handling by calling stopImmediatePropagation
in the local listener (registered first because the bridge script is
appended to </body>), and add a MutationObserver that pulls the deck
transform onto whichever slide currently carries .active so the bridge's
direct setActive calls (notably the initial-slide restore) move the deck
too.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(i18n): align French content with renamed/new skills

PR #434 (French localization) merged into main with French copy for the
old editorial-collage / editorial-collage-deck skill ids; this branch
renamed those to open-design-landing / open-design-landing-deck and
added kami-deck and kami-landing. Update content.fr.ts to track the
rename and add French copy for the new kami skills so the
LOCALIZED_CONTENT_IDS coverage test passes once main is merged.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(open-design-landing-deck): sync deck transform with host-driven .active changes

Apply the same fix that landed in skills/kami-deck/example.html
(commits 96b255b, 8cbca30) to the open-design-landing-deck composer
runtime: the host bridge classifies this deck as class-driven because
go() toggles .slide.active, but the visible slide is moved by
deck.style.transform which the bridge can't drive. Add an od:slide
message listener that calls stopImmediatePropagation() and routes nav
through the local go(), plus a MutationObserver that pulls the deck
transform onto whichever slide carries .active so the bridge's direct
setActive calls (notably restoreInitialSlide) move the deck too.

Regenerates example.html via scripts/compose.ts; the regeneration also
picks up upstream nav-cta and brand-meta CSS additions in the sister
open-design-landing styles.css that the example had drifted from.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* docs(open-design-landing): align deploy story with Astro landing app

- Update SKILL contract: apps/landing-page is Astro static; clarify
  nextjs-app output_format as a historical enum label and <out>/nextjs
  as a legacy folder name.
- Replace optional-deploy section with fork + pnpm --filter landing-page build.
- Fix styles.css header and regenerate landing + deck example.html so
  inlined comments match.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(deck-runtime): bypass interaction lock for host/observer slide sync

The slide deck runtimes for kami-deck and open-design-landing-deck
gate go() behind a 700ms `lock` so wheel/key/touch input bursts can't
overshoot the transform transition. But applying the same gate to the
host bridge's od:slide messages and the MutationObserver watching
`.slide.active` creates a startup race: go(0) at the end of init sets
lock=true, and any host-driven `.active` change inside that window
(notably restoreInitialSlide) fires the observer, which calls go(i),
which exits at the lock guard — leaving the visible deck on slide 1
while the host counter advances to N.

Split the actual state update into an unthrottled `applySlide(n)`
helper that updates transform, `.active`, dot nav, and the progress
bar. Keep `lock` only on the user-input path through `go()`. Route
the message listener, the MutationObserver, and the initial render
through `applySlide` directly so host-driven sync always reaches the
deck transform regardless of the throttle state.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Tom Huang 2026-05-04 19:22:46 +08:00 committed by GitHub
parent bf533c3d72
commit aefba56a3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 5263 additions and 1132 deletions

View file

@ -17,7 +17,7 @@ import {
sanitizeCustomModel,
spawnEnvForAgent,
} from './agents.js';
import { listSkills } from './skills.js';
import { findSkillById, listSkills } from './skills.js';
import { listCodexPets, readCodexPetSpritesheet } from './codex-pets.js';
import { syncCommunityPets } from './community-pets-sync.js';
import { listDesignSystems, readDesignSystem } from './design-systems.js';
@ -1198,7 +1198,7 @@ export async function startServer({ port = 7456, host = process.env.OD_BIND_HOST
app.get('/api/skills/:id', async (req, res) => {
try {
const skills = await listSkills(SKILLS_DIR);
const skill = skills.find((s) => s.id === req.params.id);
const skill = findSkillById(skills, req.params.id);
if (!skill) return res.status(404).json({ error: 'skill not found' });
const { dir: _dir, ...serializable } = skill;
res.json(serializable);
@ -1380,7 +1380,7 @@ export async function startServer({ port = 7456, host = process.env.OD_BIND_HOST
app.get('/api/skills/:id/example', async (req, res) => {
try {
const skills = await listSkills(SKILLS_DIR);
const skill = skills.find((s) => s.id === req.params.id);
const skill = findSkillById(skills, req.params.id);
if (!skill) {
return res.status(404).type('text/plain').send('skill not found');
}
@ -1441,7 +1441,7 @@ export async function startServer({ port = 7456, host = process.env.OD_BIND_HOST
app.get('/api/skills/:id/assets/*', async (req, res) => {
try {
const skills = await listSkills(SKILLS_DIR);
const skill = skills.find((s) => s.id === req.params.id);
const skill = findSkillById(skills, req.params.id);
if (!skill) {
return res.status(404).type('text/plain').send('skill not found');
}
@ -2231,8 +2231,9 @@ export async function startServer({ port = 7456, host = process.env.OD_BIND_HOST
let skillCraftRequires = [];
let activeSkillDir = null;
if (effectiveSkillId) {
const skill = (await listSkills(SKILLS_DIR)).find(
(s) => s.id === effectiveSkillId,
const skill = findSkillById(
await listSkills(SKILLS_DIR),
effectiveSkillId,
);
if (skill) {
skillBody = skill.body;

View file

@ -8,6 +8,36 @@ import path from "node:path";
import { parseFrontmatter } from "./frontmatter.js";
import { SKILLS_CWD_ALIAS } from "./cwd-aliases.js";
// Persisted skill ids on existing projects can outlive a folder rename.
// listSkills() derives the id from the SKILL.md frontmatter `name`, so once
// a skill is renamed the old id stops resolving and composeSystemPrompt
// silently drops the skill body for projects saved against the old id.
// This map forwards deprecated ids to their current canonical id; callers
// resolve through findSkillById() before scanning the listing. Leave entries
// here for at least one stable release after a rename so on-disk projects
// keep composing with the intended skill prompt.
export const SKILL_ID_ALIASES = Object.freeze({
"editorial-collage": "open-design-landing",
"editorial-collage-deck": "open-design-landing-deck",
});
export function resolveSkillId(id) {
if (typeof id !== "string" || id.length === 0) return id;
return SKILL_ID_ALIASES[id] ?? id;
}
// Lookup helper that mirrors `skills.find((s) => s.id === id)` but first
// rewrites any deprecated id to its current canonical form. Use this at
// every site that resolves a stored or external skill id; calling
// `.find()` directly will silently miss aliased ids.
export function findSkillById(skills, id) {
if (!Array.isArray(skills) || typeof id !== "string" || id.length === 0) {
return undefined;
}
const canonical = resolveSkillId(id);
return skills.find((s) => s.id === canonical);
}
export async function listSkills(skillsRoot) {
const out = [];
let entries = [];

View file

@ -4,8 +4,8 @@ import { rewriteSkillAssetUrls } from '../src/server.js';
describe('rewriteSkillAssetUrls', () => {
it('rewrites ./assets/<file> img sources to the daemon route', () => {
const html = `<img src='./assets/hero.png' alt='' />`;
expect(rewriteSkillAssetUrls(html, 'editorial-collage')).toBe(
`<img src='/api/skills/editorial-collage/assets/hero.png' alt='' />`,
expect(rewriteSkillAssetUrls(html, 'open-design-landing')).toBe(
`<img src='/api/skills/open-design-landing/assets/hero.png' alt='' />`,
);
});
@ -17,9 +17,9 @@ describe('rewriteSkillAssetUrls', () => {
});
it('rewrites sibling skill asset references', () => {
const html = `<img src='../editorial-collage/assets/hero.png' /><a href="../skill-two/assets/guide.pdf"></a>`;
const html = `<img src='../open-design-landing/assets/hero.png' /><a href="../skill-two/assets/guide.pdf"></a>`;
expect(rewriteSkillAssetUrls(html, 'foo')).toBe(
`<img src='/api/skills/editorial-collage/assets/hero.png' /><a href="/api/skills/skill-two/assets/guide.pdf"></a>`,
`<img src='/api/skills/open-design-landing/assets/hero.png' /><a href="/api/skills/skill-two/assets/guide.pdf"></a>`,
);
});

View file

@ -0,0 +1,119 @@
// @ts-nocheck
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import {
SKILL_ID_ALIASES,
findSkillById,
listSkills,
resolveSkillId,
} from '../src/skills.js';
// Regression coverage for the editorial-collage → open-design-landing rename.
// The daemon persists the chosen skill_id verbatim on a project row and
// resolves it later by id, so a folder/frontmatter rename without a
// compatibility shim would silently drop the skill prompt for projects
// saved against the old id. These tests pin the alias map and the lookup
// helper that every server-side resolver must go through.
let skillsRoot;
beforeAll(async () => {
skillsRoot = await mkdtemp(path.join(tmpdir(), 'od-skills-aliases-'));
// Mimic the on-disk shape the production registry expects: one
// directory per skill, each with a SKILL.md whose frontmatter `name`
// becomes the canonical id returned by listSkills().
await mkdir(path.join(skillsRoot, 'open-design-landing'), { recursive: true });
await writeFile(
path.join(skillsRoot, 'open-design-landing', 'SKILL.md'),
'---\nname: open-design-landing\ndescription: Atelier Zero landing.\n---\n\nbody\n',
'utf8',
);
await mkdir(path.join(skillsRoot, 'open-design-landing-deck'), {
recursive: true,
});
await writeFile(
path.join(skillsRoot, 'open-design-landing-deck', 'SKILL.md'),
'---\nname: open-design-landing-deck\ndescription: Atelier Zero deck.\n---\n\nbody\n',
'utf8',
);
// An untouched skill so we can prove the helper still resolves
// non-aliased ids and does not match by accident.
await mkdir(path.join(skillsRoot, 'simple-deck'), { recursive: true });
await writeFile(
path.join(skillsRoot, 'simple-deck', 'SKILL.md'),
'---\nname: simple-deck\ndescription: Plain deck.\n---\n\nbody\n',
'utf8',
);
});
afterAll(async () => {
if (skillsRoot) await rm(skillsRoot, { recursive: true, force: true });
});
describe('SKILL_ID_ALIASES', () => {
it('maps the editorial-collage rename to its current canonical id', () => {
expect(SKILL_ID_ALIASES['editorial-collage']).toBe('open-design-landing');
expect(SKILL_ID_ALIASES['editorial-collage-deck']).toBe(
'open-design-landing-deck',
);
});
it('is frozen so callers cannot mutate the deprecation list at runtime', () => {
expect(Object.isFrozen(SKILL_ID_ALIASES)).toBe(true);
});
});
describe('resolveSkillId', () => {
it('forwards deprecated ids to their canonical replacement', () => {
expect(resolveSkillId('editorial-collage')).toBe('open-design-landing');
expect(resolveSkillId('editorial-collage-deck')).toBe(
'open-design-landing-deck',
);
});
it('passes non-aliased ids through unchanged', () => {
expect(resolveSkillId('simple-deck')).toBe('simple-deck');
expect(resolveSkillId('totally-unknown')).toBe('totally-unknown');
});
it('returns the input unchanged for empty / non-string ids', () => {
expect(resolveSkillId('')).toBe('');
expect(resolveSkillId(undefined)).toBeUndefined();
expect(resolveSkillId(null)).toBeNull();
});
});
describe('findSkillById', () => {
it('resolves a project saved with the old editorial-collage id to the renamed skill', async () => {
const skills = await listSkills(skillsRoot);
const skill = findSkillById(skills, 'editorial-collage');
expect(skill).toBeDefined();
expect(skill.id).toBe('open-design-landing');
expect(skill.body).toContain('body');
});
it('resolves a project saved with the old editorial-collage-deck id to the renamed deck skill', async () => {
const skills = await listSkills(skillsRoot);
const skill = findSkillById(skills, 'editorial-collage-deck');
expect(skill).toBeDefined();
expect(skill.id).toBe('open-design-landing-deck');
});
it('still resolves current ids exactly', async () => {
const skills = await listSkills(skillsRoot);
expect(findSkillById(skills, 'open-design-landing')?.id).toBe(
'open-design-landing',
);
expect(findSkillById(skills, 'simple-deck')?.id).toBe('simple-deck');
});
it('returns undefined for unknown ids and missing inputs', async () => {
const skills = await listSkills(skillsRoot);
expect(findSkillById(skills, 'definitely-not-a-skill')).toBeUndefined();
expect(findSkillById(skills, '')).toBeUndefined();
expect(findSkillById(null, 'open-design-landing')).toBeUndefined();
});
});

View file

@ -9,10 +9,10 @@ records module-level boundaries for `apps/landing-page/`.
the canonical Open Design marketing page in the **Atelier Zero** style.
It is the deployable counterpart to:
- Skill: `skills/editorial-collage/` — agent workflow + the source-of-truth
- Skill: `skills/open-design-landing/` — agent workflow + the source-of-truth
`example.html` known-good rendering.
- Design system: `design-systems/atelier-zero/DESIGN.md` — token spec.
- Image assets: `skills/editorial-collage/assets/*.png` are uploaded to
- Image assets: `skills/open-design-landing/assets/*.png` are uploaded to
Cloudflare R2 (`open-design-static`) and served through
`static.open-design.ai` with Image Resizing (`format=auto`). Do not
commit local mirrored PNGs into `apps/landing-page/public/assets/`.
@ -52,7 +52,7 @@ It is the deployable counterpart to:
`app/`. If a component grows beyond ~80 lines, extract it to
`app/_components/<name>.tsx`.
- Must not depend on any non-Google web font.
- When the canonical `skills/editorial-collage/example.html` changes,
- When the canonical `skills/open-design-landing/example.html` changes,
the corresponding section JSX in `app/page.tsx` and rules in
`app/globals.css` must be updated to match. The two files are kept
in lockstep.

View file

@ -44,7 +44,7 @@ type Contributor = {
// SSR-safe initial list. Used until the GitHub fetch resolves AND as
// the permanent fallback when the network is unavailable. Mirrors the
// canonical wire row in `skills/editorial-collage/example.html` so
// canonical wire row in `skills/open-design-landing/example.html` so
// hydration is byte-stable against the static reference rendering.
const FALLBACK: ReadonlyArray<Contributor> = [
{ handle: 'tw93', role: 'kami', href: 'https://github.com/tw93' },

View file

@ -1,7 +1,7 @@
/*
* Atelier Zero landing page styles.
*
* Mirrors `skills/editorial-collage/example.html` <style> block 1:1, plus
* Mirrors `skills/open-design-landing/example.html` <style> block 1:1, plus
* a Google Fonts import so we do not need to inject <link rel='preconnect'>
* tags into the document head from a server component.
*
@ -300,6 +300,12 @@ body::before {
font-size: 13px;
font-weight: 500;
text-decoration: none;
white-space: nowrap;
flex-shrink: 0;
}
.nav-cta [data-github-stars],
.nav-cta [data-github-version] {
font-variant-numeric: tabular-nums;
}
.nav-cta::after {
content: '★';
@ -453,9 +459,12 @@ body::before {
padding: 0;
min-height: calc(100vh - 140px);
display: flex;
flex-direction: column;
align-items: stretch;
border-bottom: 1px solid var(--line);
}
.hero > .container { flex: 0 0 auto; }
.hero > .container.hero-grid { flex: 1 1 auto; }
.hero::before {
content: '';
position: absolute;
@ -1898,6 +1907,14 @@ footer {
@media (max-width: 1200px) {
.topbar-inner .mid { display: none; }
}
/* nav: between 1080 and 1180 the brand tail + 5 nav links + 2 CTAs + dot
* crowd the row. Drop the brand sub-meta first, then tighten link spacing,
* so the Star CTA never has to compress. */
@media (max-width: 1180px) {
.nav-inner { gap: 18px; }
.brand-meta { display: none; }
.nav-links { gap: 28px; }
}
@media (max-width: 1080px) {
.container { padding: 0 32px; }
.hero h1 { font-size: clamp(36px, 4.6vw, 54px); }

View file

@ -1,7 +1,7 @@
/*
* Open Design Atelier Zero landing page.
*
* Mirrors `skills/editorial-collage/example.html` 1:1. When the canonical
* Mirrors `skills/open-design-landing/example.html` 1:1. When the canonical
* example.html changes, mirror the diff here and into `app/globals.css`.
*
* Static React component rendered by Astro. The Header and Wire components
@ -28,7 +28,12 @@ const arrowPlus = (
const NBSP = '\u00A0';
// Canonical project URLs. Keep in sync with skills/editorial-collage/example.html.
// Canonical project URLs. Keep in sync with skills/open-design-landing/example.html.
//
// `data-github-version` invariant: every wrapper must contain ONLY the version
// string (e.g. `v0.3.0`), never any surrounding label or punctuation. The
// inline enhancement script in `app/pages/index.astro` assigns `textContent`
// on each slot, so any extra text inside the wrapper would be clobbered.
const REPO = 'https://github.com/nexu-io/open-design';
const REPO_RELEASES = `${REPO}/releases`;
const REPO_ISSUES = `${REPO}/issues`;
@ -117,7 +122,7 @@ export default function Page() {
<span className='right'>
<a className='topbar-link' href={REPO_RELEASES} {...ext}>
<span className='pulse' />
Live · v0.2.0
Live · <span data-github-version>v0.3.0</span>
</a>
<span className='locale-switch'>
<b>EN</b>
@ -155,11 +160,10 @@ export default function Page() {
<span className='dot'>.</span>
</h1>
<p className='lead' data-reveal>
The open-source alternative to Anthropic&rsquo;s Claude Design.
12 coding agents Claude, Codex, Cursor, Gemini and friends
drive 31 composable skills and 72 brand-grade design systems.
Generate web pages, slide decks, mobile prototypes, images, even
short videos all running on your own laptop.
The open-source alternative to Claude Design. Your existing
coding agent Claude · Codex · Cursor · Gemini · OpenCode ·
Qwen becomes the design engine, driven by 31 composable
skills and 72 brand-grade design systems.
</p>
<div className='hero-actions' data-reveal>
<a className='btn btn-primary' href={REPO} {...ext}>
@ -1011,7 +1015,9 @@ export default function Page() {
</div>
<div className='cta-foot'>
<span className='stamp'> Live</span>
<span>v0.2.0 / Apache-2.0</span>
<span>
<span data-github-version>v0.3.0</span> / Apache-2.0
</span>
<span style={{ marginLeft: 'auto' }}>
52.5200° N · 13.4050° E
</span>
@ -1080,7 +1086,9 @@ export default function Page() {
{...ext}
>
Download desktop
<span className='meta'>macOS · v0.2.0</span>
<span className='meta'>
macOS · <span data-github-version>v0.3.0</span>
</span>
</a>
</div>
<div className='foot-col'>

View file

@ -5,9 +5,9 @@ import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { heroImage } from '../image-assets';
const title = 'Open Design — Designing intelligence with skills, taste, and code.';
const title = 'Open Design — Design with the agent already on your laptop.';
const description =
'Open Design is the open-source alternative to Claude Design. 12 coding-agent CLIs · 31 composable skills · 72 brand-grade design systems. Local-first, web-deployable, BYOK at every layer.';
'The open-source alternative to Claude Design. Your existing coding agent — Claude · Codex · Cursor · Gemini · OpenCode · Qwen — becomes the design engine, driven by 31 composable skills and 72 brand-grade design systems.';
const canonical = new URL(Astro.url.pathname, Astro.site).toString();
const pageHtml = renderToStaticMarkup(createElement(Page));
---
@ -44,6 +44,31 @@ const pageHtml = renderToStaticMarkup(createElement(Page));
return `${(count / 1000).toFixed(1).replace(/\.0$/, '')}K`;
};
// Pull a clean 'v0.3.0'-style label from a GitHub release record.
// We prefer release.name (e.g. 'Open Design 0.3.0') because that's
// what we hand-author; fall back to tag_name (e.g.
// 'open-design-v0.3.0') with the project prefix stripped.
//
// Expected input shapes (release.name / release.tag_name):
// { name: 'Open Design 0.3.0', tag_name: 'v0.3.0' } → 'v0.3.0'
// { name: 'Open Design v0.3.0', tag_name: 'open-design-v0.3.0' } → 'v0.3.0'
// { name: '0.3.0-beta.1', tag_name: 'open-design_0.3.0' } → 'v0.3.0-beta.1' (name wins)
// { name: null, tag_name: 'open-design-v0.3.0' } → 'v0.3.0' (tag fallback)
// { name: null, tag_name: null } → null (caller skips)
const formatVersion = (release) => {
const fromTag = (tag) => {
if (typeof tag !== 'string') return null;
const cleaned = tag.replace(/^open-design[-_]?v?/i, '').trim();
return cleaned ? `v${cleaned.replace(/^v/, '')}` : null;
};
const fromName = (name) => {
if (typeof name !== 'string') return null;
const m = name.match(/(\d+\.\d+\.\d+(?:[-+][\w.]+)?)/);
return m ? `v${m[1]}` : null;
};
return fromName(release?.name) ?? fromTag(release?.tag_name) ?? null;
};
const enhanceHeader = () => {
const nav = document.querySelector('[data-nav-headroom]');
if (nav) {
@ -65,15 +90,33 @@ const pageHtml = renderToStaticMarkup(createElement(Page));
}
const stars = document.querySelector('[data-github-stars]');
if (!stars) return;
fetch('https://api.github.com/repos/nexu-io/open-design', {
if (stars) {
fetch('https://api.github.com/repos/nexu-io/open-design', {
headers: { Accept: 'application/vnd.github+json' },
})
.then((r) => (r.ok ? r.json() : Promise.reject(new Error('http error'))))
.then((data) => {
if (typeof data?.stargazers_count === 'number') {
stars.textContent = formatStars(data.stargazers_count);
}
})
.catch(() => {});
}
// Latest stable release powers every "v0.x.y" badge on the page
// (topbar pulse, hero CTA-foot, footer download). Hits one
// unauthenticated API call per page view; the static fallback in
// each slot keeps the layout sane if the request fails or 403s.
const versionSlots = document.querySelectorAll('[data-github-version]');
if (versionSlots.length === 0) return;
fetch('https://api.github.com/repos/nexu-io/open-design/releases/latest', {
headers: { Accept: 'application/vnd.github+json' },
})
.then((r) => (r.ok ? r.json() : Promise.reject(new Error('http error'))))
.then((data) => {
if (typeof data?.stargazers_count === 'number') {
stars.textContent = formatStars(data.stargazers_count);
}
const label = formatVersion(data);
if (!label) return;
for (const slot of versionSlots) slot.textContent = label;
})
.catch(() => {});
};

View file

@ -0,0 +1,424 @@
---
/*
* Open Design — OG image (1200×630).
*
* Stand-alone Astro route at `/og/` used purely as a screenshotable
* surface to produce the canonical og:image. Render with a real
* browser at viewport 1200×630, capture a full-page screenshot, then
* upload the resulting PNG to R2 and reference it from `index.astro`'s
* og:image meta.
*
* Visual reference: the dark "Manifesto / 2026 Edition" cover plate
* (chocolate background, big serif headline, four-up stats footer,
* device collage on the right). Self-contained — does not import
* globals.css so the marketing page styles never leak in.
*/
import { heroImage } from '../image-assets';
const title = 'Open Design — Design with the agent already on your laptop.';
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{title}</title>
<meta name="viewport" content="width=1200, initial-scale=1" />
<meta name="robots" content="noindex" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@500;600;700;800;900&family=Inter:wght@400;500;600&family=Playfair+Display:ital,wght@0,600;0,700;1,500;1,600;1,700&family=JetBrains+Mono:wght@400;500&display=swap"
/>
<style>
:root {
--ink: #14110b;
--ink-2: #1c1810;
--ink-3: #2a241a;
--paper: #efe7d2;
--paper-warm: #d8cfb6;
--paper-mute: #8d8472;
--paper-faint: #5b5648;
--coral: #ed6f5c;
--coral-soft: #f08e7c;
--mustard: #e9b94a;
--line: rgba(239, 231, 210, 0.14);
--line-soft: rgba(239, 231, 210, 0.07);
--serif: 'Playfair Display', 'Times New Roman', serif;
--sans: 'Inter Tight', 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
--body: 'Inter', -apple-system, system-ui, sans-serif;
--mono: 'JetBrains Mono', 'SF Mono', Menlo, monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
background: var(--ink);
color: var(--paper);
font-family: var(--body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.og {
position: relative;
width: 1200px;
height: 630px;
overflow: hidden;
background:
radial-gradient(circle at 88% 22%, rgba(237, 111, 92, 0.10) 0, transparent 38%),
radial-gradient(circle at 12% 90%, rgba(233, 185, 74, 0.06) 0, transparent 42%),
linear-gradient(180deg, #14110b 0%, #1a1610 60%, #14110b 100%);
}
/* paper-grain overlay, identical mood to the live site */
.og::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background-image:
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.95 0 0 0 0 0.90 0 0 0 0 0.78 0 0 0 0.05 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
background-size: 240px 240px;
mix-blend-mode: screen;
opacity: 0.55;
}
/* ===== top metadata strip ===== */
.topbar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 38px;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding: 0 36px;
font-family: var(--mono);
font-size: 10.5px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--paper-mute);
border-bottom: 1px solid var(--line-soft);
}
.topbar .center {
font-family: var(--serif);
font-style: italic;
font-size: 14px;
letter-spacing: 0;
text-transform: none;
color: var(--paper);
}
.topbar .center::before {
content: '·';
color: var(--coral);
margin-right: 6px;
}
.topbar .right {
text-align: right;
}
/* ===== body grid ===== */
.grid {
position: absolute;
inset: 38px 0 0 0;
display: grid;
grid-template-columns: 1fr 1fr;
padding: 36px 56px 28px;
gap: 48px;
}
.copy {
display: flex;
flex-direction: column;
align-items: flex-start;
}
/* tag chips */
.tags {
display: inline-flex;
gap: 10px;
margin-bottom: 28px;
}
.tag {
font-family: var(--mono);
font-size: 9.5px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--paper-warm);
padding: 5px 10px;
border: 1px solid var(--line);
border-radius: 999px;
background: rgba(239, 231, 210, 0.04);
}
.tag.coral {
color: var(--coral);
border-color: rgba(237, 111, 92, 0.35);
background: rgba(237, 111, 92, 0.08);
}
/* big serif headline */
.headline {
font-family: var(--serif);
font-weight: 600;
font-size: 64px;
line-height: 1.04;
letter-spacing: -0.02em;
color: var(--paper);
}
.headline em {
font-style: italic;
font-weight: 600;
color: var(--coral);
}
.headline u {
text-decoration: none;
position: relative;
display: inline-block;
}
.headline u::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 6px;
height: 2px;
background: var(--coral);
opacity: 0.85;
}
.headline .dot {
color: var(--coral);
}
/* lead copy */
.lead {
margin-top: 22px;
max-width: 480px;
font-family: var(--body);
font-size: 14.5px;
line-height: 1.55;
color: var(--paper-warm);
}
.lead b {
color: var(--paper);
font-weight: 600;
}
/* ===== device art ===== */
.art {
position: relative;
align-self: stretch;
}
.art-frame {
position: absolute;
inset: -8px -56px 56px 0;
border-radius: 14px;
overflow: hidden;
background:
radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.05) 0, transparent 55%),
linear-gradient(180deg, #1f1b13, #15110a);
box-shadow:
0 30px 80px -20px rgba(0, 0, 0, 0.6),
inset 0 0 1px rgba(239, 231, 210, 0.06);
}
.art-frame img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
opacity: 0.92;
mix-blend-mode: lighten;
filter: contrast(1.05) saturate(0.92) brightness(0.96);
}
.art-frame::after {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(circle at 25% 12%, rgba(237, 111, 92, 0.10) 0, transparent 45%),
linear-gradient(180deg, transparent 30%, rgba(20, 17, 11, 0.55) 100%);
pointer-events: none;
}
/* annotations on art */
.annot {
position: absolute;
font-family: var(--mono);
font-size: 9.5px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--paper-mute);
z-index: 2;
}
.annot.tl {
top: 14px;
left: 18px;
}
.annot.tr {
top: 14px;
right: 14px;
color: var(--paper-warm);
}
.annot.br {
bottom: 64px;
right: 14px;
color: var(--coral);
}
/* OS coral coin */
.coin {
position: absolute;
top: 78px;
right: 36px;
width: 78px;
height: 78px;
border-radius: 999px;
background: radial-gradient(circle at 30% 28%, #f5876f 0%, #ed6f5c 55%, #c54a3a 100%);
box-shadow:
0 0 0 1px rgba(237, 111, 92, 0.35),
0 0 32px rgba(237, 111, 92, 0.55),
0 18px 40px -10px rgba(237, 111, 92, 0.45);
display: grid;
place-items: center;
font-family: var(--mono);
font-size: 10.5px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #14110b;
text-align: center;
line-height: 1.1;
font-weight: 600;
z-index: 4;
}
/* ===== bottom stats strip ===== */
.stats {
position: absolute;
left: 56px;
right: 56px;
bottom: 38px;
padding-top: 18px;
border-top: 1px solid var(--line);
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
max-width: 540px;
}
.stat {
display: flex;
flex-direction: column;
gap: 6px;
}
.stat .num {
font-family: var(--serif);
font-size: 38px;
line-height: 1;
font-weight: 600;
color: var(--paper);
}
.stat.zero .num {
font-style: italic;
color: var(--coral);
}
.stat .lbl {
font-family: var(--mono);
font-size: 9px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--paper-mute);
line-height: 1.4;
}
</style>
</head>
<body>
<div class="og">
<div class="topbar">
<span>Open Design · Manifesto · 2026 Edition</span>
<span class="center">open.design</span>
<span class="right">Cover · 01 / 08 · OSS Alternative</span>
</div>
<div class="grid">
<div class="copy">
<div class="tags">
<span class="tag coral">Apache 2.0</span>
<span class="tag">Local-first</span>
<span class="tag">BYOK</span>
</div>
<h1 class="headline">
Design with the<br />
<em>agent</em> already<br />
on your <u>laptop</u><span class="dot">.</span>
</h1>
<p class="lead">
Open Design is the open-source alternative to Claude Design.
Your existing coding agent — <b>Claude · Codex · Cursor · Gemini · OpenCode · Qwen</b>
— becomes the design engine, driven by 31 composable skills and
72 brand-grade design systems.
</p>
</div>
<div class="art">
<span class="annot tl">Fig. 01 / OD-26</span>
<span class="annot tr">Plate Nº 08</span>
<div class="art-frame">
<img src={heroImage} alt="" />
</div>
<span class="annot br">Composed in Open Design</span>
</div>
</div>
<span class="coin">Open<br />Source</span>
<div class="stats">
<div class="stat">
<span class="num">72</span>
<span class="lbl">Design<br />Systems</span>
</div>
<div class="stat">
<span class="num">31</span>
<span class="lbl">Composable<br />Skills</span>
</div>
<div class="stat">
<span class="num">12</span>
<span class="lbl">Coding<br />Agents</span>
</div>
<div class="stat zero">
<span class="num">0</span>
<span class="lbl">Lock-in /<br />Vendor Cloud</span>
</div>
</div>
</div>
</body>
</html>

View file

@ -1,6 +1,6 @@
{
"name": "@open-design/landing-page",
"version": "0.2.0",
"version": "0.3.0",
"private": true,
"type": "module",
"scripts": {

View file

@ -32,15 +32,15 @@ export const FR_SKILL_COPY: Record<string, { description?: string; examplePrompt
examplePrompt:
'Une page de documentation — navigation à gauche, zone article scrollable, table des matières à droite.',
},
'editorial-collage': {
'open-design-landing': {
examplePrompt:
'Concevez une landing page éditoriale dans le style Atelier Zero / Monocle — canvas papier chaud, collage surréaliste plâtre + architecture, grande typographie display serif italique mixée, chiffres romains comme marqueurs de sections et un seul accent corail.',
'Concevez la landing page marketing Open Design dans le style Atelier Zero / Monocle — canvas papier chaud, collage surréaliste plâtre + architecture, grande typographie display serif italique mixée, chiffres romains comme marqueurs de sections et un seul accent corail.',
},
'editorial-collage-deck': {
'open-design-landing-deck': {
examplePrompt:
'Créez un pitch deck de 11 slides pour “Lumen Field”, un studio de soundscapes de concentration. Cover avec hero plate, deux séparateurs de section, deux slides produit avec bullets, une slide stats avec 12 soundscapes / 4 presets / 1 Daily Ritual, une citation client, un CTA final et une end-card. Réutilisez la bibliothèque dimages deditorial-collage.',
'Créez le pitch deck Open Design dans le style Atelier Zero — cover avec hero plate, séparateurs de section en chiffres romains, slide stats (31 Skills · 72 systèmes · 12 CLIs), citation client, CTA et end-card mega italic-serif. Pagination horizontal-swipe comme un magazine imprimé.',
description:
'Crée un slide deck single-file dans le langage visuel Atelier Zero : papier chaud, spans accent en serif italique, points finaux corail et plaques de collage surréalistes. Le deck utilise une pagination scroll-snap, une navigation par flèches et espace, un HUD live avec compteur de slides et progress bar, et hérite du stylesheet canonique ainsi que de la bibliothèque dimages à 16 slots du Skill frère `editorial-collage`.',
'Crée un slide deck single-file dans le style Atelier Zero (papier chaud, spans accent en serif italique, points finaux corail, plaques de collage surréalistes). Pagination magazine horizontale avec navigation par flèches et espace, HUD live avec compteur de slides et progress bar ; partage le stylesheet et la bibliothèque dimages à 16 slots avec le Skill frère `open-design-landing`.',
},
'email-marketing': {
examplePrompt:
@ -104,6 +104,18 @@ export const FR_SKILL_COPY: Record<string, { description?: string; examplePrompt
examplePrompt:
'Créez une facture dun studio de design freelance pour un client sur un projet didentité de marque — trois lignes, acompte de 10 %, TVA de 9 %.',
},
'kami-deck': {
examplePrompt:
'Créez un deck de conférence en six slides dans le style kami (紙) — parchemin chaud, encre bleue sur la cover, une seule graisse de serif, swipe magazine horizontal.',
description:
'Génère un slide deck prêt à imprimer dans le design system kami : parchemin chaud (ou encre bleue sur cover et chapitres), serif dans une seule graisse, accent encre bleue ≤5 % par slide, sans italique. Pagination magazine horizontale (←/→ · molette · swipe · ESC pour la vue densemble). Un seul fichier HTML autonome, uniquement Google Fonts.',
},
'kami-landing': {
examplePrompt:
'Concevez un one-pager studio dans le style kami — canvas parchemin, accent encre bleue, éditorial comme un whitepaper.',
description:
'Génère un one-pager prêt à imprimer dans le style kami (紙) : parchemin chaud, accent encre bleue, serif dans une seule graisse, sans italique, sans gris froids. Se lit comme un whitepaper ou un one-pager studio, pas comme une UI dapp. Multilingue (EN · zh-CN · ja). Un seul fichier HTML sans dépendances.',
},
'kanban-board': {
examplePrompt:
'Créez un Kanban board pour une équipe growth de 5 personnes en plein sprint — Backlog, Doing, Review, Done.',

View file

@ -32,15 +32,15 @@ export const RU_SKILL_COPY: Record<string, { description?: string; examplePrompt
examplePrompt:
'Страница документации — левая навигация, прокручиваемая область статьи, оглавление справа.',
},
'editorial-collage': {
'open-design-landing': {
examplePrompt:
'Спроектируйте редакционный лендинг в стиле Atelier Zero / Monocle — теплая бумажная основа, сюрреалистичный коллаж из гипса и архитектуры, сверхкрупная смешанная display-типографика с курсивной антиквой, римские цифры как маркеры секций и один коралловый акцент.',
'Спроектируйте маркетинговый лендинг Open Design в стиле Atelier Zero / Monocle — теплая бумажная основа, сюрреалистичный коллаж из гипса и архитектуры, сверхкрупная смешанная display-типографика с курсивной антиквой, римские цифры как маркеры секций и один коралловый акцент.',
},
'editorial-collage-deck': {
'open-design-landing-deck': {
examplePrompt:
'Создайте pitch deck из 11 слайдов для «Lumen Field», студии фокусных soundscape. Обложка с hero-плашкой, два разделителя секций, два продуктовых слайда с буллетами, слайд со статистикой 12 soundscapes / 4 presets / 1 daily ritual, клиентская цитата, финальный CTA и end-card. Переиспользуйте библиотеку изображений из editorial-collage.',
'Создайте pitch deck Open Design в стиле Atelier Zero — обложка с hero-плашкой, римские разделители секций, слайд со статистикой (31 скилл · 72 системы · 12 CLI), клиентская цитата, финальный CTA и mega-italic-serif end-card. Горизонтальная свайп-пагинация как в печатном журнале.',
description:
'Создает однофайловую презентацию в визуальном языке Atelier Zero: теплая бумага, акцентные spans курсивной антиквой, коралловые финальные точки и сюрреалистичные коллажные плашки. Deck использует scroll-snap-пагинацию, навигацию стрелками и пробелом, live HUD со счетчиком слайдов и progress bar, а также наследует каноническую таблицу стилей и 16-слотовую библиотеку изображений sibling-навыка `editorial-collage`.',
'Создает однофайловую презентацию в стиле Atelier Zero: теплая бумага, акцентные spans курсивной антиквой, коралловые финальные точки, сюрреалистичные коллажные плашки. Горизонтальная журнальная пагинация с навигацией стрелками и пробелом, live HUD со счетчиком слайдов и progress bar; разделяет stylesheet и 16-слотовую библиотеку изображений с sibling-навыком `open-design-landing`.',
},
'email-marketing': {
examplePrompt:
@ -104,6 +104,18 @@ export const RU_SKILL_COPY: Record<string, { description?: string; examplePrompt
examplePrompt:
'Создайте счет от фриланс-дизайн-студии клиенту за проект по фирменному стилю — три позиции, аванс 10%, НДС 9%.',
},
'kami-deck': {
examplePrompt:
'Создайте шестислайдовую презентацию в стиле kami (紙) — теплый пергамент, синие чернила на обложке, одно начертание антиквы, горизонтальная журнальная прокрутка.',
description:
'Генерирует презентацию уровня печати в дизайн-системе kami: теплый пергамент (или синие чернила для обложки и глав), антиква одного кегля, акцент синими чернилами ≤5% на слайд, без курсива. Горизонтальная пагинация как в журнале. Один самодостаточный HTML-файл, только Google Fonts.',
},
'kami-landing': {
examplePrompt:
'Спроектируйте одностраничный studio one-pager в стиле kami — пергамент, акцент синими чернилами, редакционный тон whitepaper.',
description:
'Создает одностраничный документ уровня печати в стиле kami (紙): теплый пергамент, акцент синими чернилами, антиква одного кегля, без курсива, без холодных серых. Читается как whitepaper или студийный one-pager, не как UI приложения. Мультиязычность (EN · zh-CN · ja). Один HTML-файл без зависимостей.',
},
'kanban-board': {
examplePrompt:
'Создайте канбан-доску для команды роста из 5 человек в разгар спринта — Backlog, Doing, Review, Done.',

View file

@ -81,15 +81,15 @@ const DE_SKILL_COPY: Record<string, LocalizedSkillCopy> = {
examplePrompt:
'Eine Dokumentationsseite — linke Navigation, scrollbarer Artikelbereich, rechte Inhaltsübersicht.',
},
'editorial-collage': {
'open-design-landing': {
examplePrompt:
'Entwerfen Sie eine Editorial-Landingpage im Atelier-Zero- / Monocle-Stil — warme Papierleinwand, surreale Plaster-und-Architektur-Collage, übergroße gemischte Italic-Serif-Display-Type, römische Ziffern als Sektionsmarker und ein einziger Korallenakzent.',
'Entwerfen Sie die Open-Design-Marketing-Landingpage im Atelier-Zero- / Monocle-Stil — warme Papierleinwand, surreale Plaster-und-Architektur-Collage, übergroße gemischte Italic-Serif-Display-Type, römische Ziffern als Sektionsmarker und ein einziger Korallenakzent.',
},
'editorial-collage-deck': {
'open-design-landing-deck': {
examplePrompt:
'Erstellen Sie ein 11-Slide-Pitch-Deck für „Lumen Field“, ein Studio für Fokus-Soundscapes. Cover mit Hero-Plate, zwei Sektions-Trenner, zwei Produkt-Content-Slides mit Bullets, ein Stats-Slide mit 12 Soundscapes / 4 Presets / 1 Daily Ritual, ein Kundenzitat, ein abschließendes CTA und eine End-Card. Wiederverwenden Sie die Bildbibliothek von editorial-collage.',
'Erstellen Sie das Open-Design-Pitch-Deck im Atelier-Zero-Stil — Cover mit Hero-Plate, römische Sektions-Trenner, Stats-Slide (31 Skills · 72 Systeme · 12 CLIs), Kundenzitat, CTA und Mega-Italic-Serif-End-Card. Horizontal-Swipe-Pagination wie eine Print-Magazine.',
description:
'Erstellt ein Single-File-Slide-Deck in der Atelier-Zero-Bildsprache (warmes Papier, italic-serif Akzent-Spans, korallenfarbene Schluss-Dots, surreale Collage-Platten). Das Deck nutzt Scroll-Snap-Pagination, Pfeiltasten- + Leertaste-Navigation, ein Live-HUD mit Slide-Zähler und Fortschrittsbalken und erbt das kanonische Stylesheet sowie die 16-Slot-Bildbibliothek der Schwester-Skill `editorial-collage`.',
'Erstellt ein Single-File-Slide-Deck im Atelier-Zero-Stil (warmes Papier, italic-serif Akzent-Spans, korallenfarbene Schluss-Dots, surreale Collage-Platten). Horizontale Magazin-Pagination mit Pfeiltasten- und Leertaste-Navigation, Live-HUD mit Slide-Zähler und Fortschrittsbalken; teilt sich Stylesheet und 16-Slot-Bildbibliothek mit der Schwester-Skill `open-design-landing`.',
},
'email-marketing': {
examplePrompt:
@ -153,6 +153,18 @@ const DE_SKILL_COPY: Record<string, LocalizedSkillCopy> = {
examplePrompt:
'Erstellen Sie eine Rechnung eines freiberuflichen Designstudios an einen Kunden für ein Brand-Identity-Projekt — drei Positionen, 10% Retainer, 9% Umsatzsteuer.',
},
'kami-deck': {
examplePrompt:
'Erstellen Sie ein sechsteiliges Konferenz-Deck im kami-Stil (紙) — warmes Pergament, Tintenblau auf dem Cover, eine Serifenschnittstärke, horizontaler Magazin-Swipe.',
description:
'Erzeugt ein druckreifes Slide-Deck im kami-Designsystem: warmes Pergament (oder Tintenblau auf Cover/Kapitel), Serif nur in einer Schnittstärke, Tintenblau-Akzent ≤5% pro Folie, ohne Kursiv. Horizontale Magazin-Pagination (←/→ · Rad · Wischen · ESC-Übersicht). Eine eigenständige HTML-Datei, nur Google Fonts.',
},
'kami-landing': {
examplePrompt:
'Entwerfen Sie eine einseitige Studio-One-Pager im kami-Stil — Pergament-Leinwand, Tintenblau-Akzent, editorial wie ein Whitepaper.',
description:
'Erzeugt eine druckreife Einseiter im kami-Stil (紙): warmes Pergament, Tintenblau-Akzent, Serif in einer Schnittstärke, kein Kursiv, keine kühlen Grautöne. Liest sich wie Whitepaper oder Studio-One-Pager, nicht wie App-UI. Mehrsprachig (EN · zh-CN · ja). Eine eigenständige HTML-Datei ohne Abhängigkeiten.',
},
'kanban-board': {
examplePrompt:
'Erstellen Sie ein Kanban-Board für ein 5-köpfiges Growth-Team mitten im Sprint — Backlog, Doing, Review, Done.',

View file

@ -12,8 +12,9 @@ will read it as part of its system prompt.
collage system: warm paper canvas, plaster-and-architecture imagery,
oversized italic-mixed display type, Roman-numeral section markers,
side rails of rotated micro-text, coordinate annotations, single
coral accent. Pairs with [`skills/editorial-collage/`](../skills/editorial-collage/)
for the canonical landing-page rendering.
coral accent. Pairs with [`skills/open-design-landing/`](../skills/open-design-landing/)
and [`skills/open-design-landing-deck/`](../skills/open-design-landing-deck/)
for the canonical landing-page and slide-deck renderings.
- **`kami/`** — 紙 / 纸. Editorial paper system distilled from
[`tw93/kami`](https://github.com/tw93/kami) (MIT). Warm parchment canvas,
ink-blue accent, serif at one weight, no italic, no cool grays. Pairs with

View file

@ -244,7 +244,7 @@ the page-of-008 counter on the right.
### Anti-patterns specific to AI-generated imagery
This system is paired with `gpt-image-fal` / `gpt-image-azure` via the
editorial-collage skill. Several common image-model defaults will
open-design-landing skill. Several common image-model defaults will
silently break the Atelier Zero aesthetic, so they are forbidden in
every collage prompt and rejected on visual review:
@ -288,7 +288,7 @@ generated to match these constraints:
dotted matrices, numbered tags. Never typography that conflicts
with on-page copy.
See `skills/editorial-collage/assets/imagegen-prompts.md` for the
See `skills/open-design-landing/assets/imagegen-prompts.md` for the
working prompt pack and per-section variants. All renders should be
at 16:9 (heroes) or 1:1 (cards / about / cta), saved as PNG, ≥1024px
on the long edge.

View file

@ -20,7 +20,7 @@ od:
mode: prototype
platform: desktop
scenario: personal
featured: 1
featured: 5
preview:
type: html
entry: index.html

View file

@ -22,7 +22,7 @@ od:
mode: prototype
platform: desktop
scenario: marketing
featured: 2
featured: 6
preview:
type: html
entry: index.html

View file

@ -1,698 +0,0 @@
#!/usr/bin/env -S npx -y tsx
/**
* editorial-collage-deck slide deck composer.
*
* Reads `inputs.json` (matching `../schema.ts`) and writes a single
* self-contained HTML file: a scroll-snap deck where every slide
* occupies one viewport. Reuses the Atelier Zero stylesheet from the
* sister `editorial-collage` skill, then layers deck-specific rules
* (snap container, slide layout, HUD, keyboard nav).
*
* Usage:
* npx tsx scripts/compose.ts <inputs.json> <output.html>
*
* Re-generate the canonical example:
* npx tsx scripts/compose.ts inputs.example.json example.html
*/
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { resolve, dirname, isAbsolute } from 'node:path';
import { fileURLToPath } from 'node:url';
import type {
EditorialCollageDeckInputs,
Slide,
CoverSlide,
SectionSlide,
ContentSlide,
StatsSlide,
QuoteSlide,
CTASlide,
EndSlide,
MixedText,
} from '../schema';
const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const SISTER_STYLES = resolve(SKILL_ROOT, '..', 'editorial-collage', 'styles.css');
/* ------------------------------------------------------------------ *
* helpers
* ------------------------------------------------------------------ */
function mixed(text: MixedText): string {
return text
.map((seg) => {
if (seg.dot) return `<span class='dot'>${seg.text}</span>`;
if (seg.em) return `<em>${seg.text}</em>`;
return seg.text;
})
.join('');
}
function ext(href: string): string {
return /^(https?:|mailto:|\/\/)/i.test(href) ? ` target='_blank' rel='noreferrer noopener'` : '';
}
const ARROW_OUT = `<svg viewBox='0 0 24 24'><path d='M5 19L19 5M19 5H8M19 5v11'/></svg>`;
function imgFor(slot: string | undefined, assets: string): string {
if (!slot) return '';
return `<img src='${assets}${slot}.png' alt='' />`;
}
/* ------------------------------------------------------------------ *
* deck-specific stylesheet (layered on top of editorial-collage CSS)
* ------------------------------------------------------------------ */
const DECK_CSS = `
/* deck container — scroll-snap pagination */
html, body { height: 100%; }
body { overflow: hidden; }
.deck {
height: 100vh;
overflow-y: scroll;
scroll-snap-type: y mandatory;
scroll-behavior: smooth;
position: relative;
}
.slide {
height: 100vh;
scroll-snap-align: start;
scroll-snap-stop: always;
display: grid;
align-items: stretch;
position: relative;
padding: 0;
border-bottom: 1px solid var(--line-soft);
/* Clip art / oversized content so it cannot bleed into adjacent slides
* at narrow / tall viewports (1/1 aspect-ratio art often exceeds 100vh
* minus padding). */
overflow: hidden;
}
.slide-inner {
max-width: 1360px;
margin: 0 auto;
padding: 80px 80px 64px;
width: 100%;
height: 100%;
display: grid;
align-content: center;
gap: 28px;
position: relative;
min-height: 0;
}
/* Cap art panels so they fit inside the slide minus the inner padding. */
.s-cover .art,
.s-content .art,
.s-quote .art {
max-height: calc(100vh - 160px);
min-height: 0;
}
/* HUD — fixed top bar + slide counter + keyboard hint */
.deck-hud {
position: fixed;
top: 18px;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 32px;
z-index: 50;
font-family: var(--sans);
font-size: 10.5px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--ink-faint);
pointer-events: none;
}
.deck-hud .left { display: inline-flex; gap: 14px; align-items: center; pointer-events: auto; }
.deck-hud .right { display: inline-flex; gap: 14px; align-items: center; pointer-events: auto; }
.deck-hud .mark {
width: 24px; height: 24px; border-radius: 50%;
border: 1px solid var(--ink); display: inline-flex;
align-items: center; justify-content: center;
font-family: var(--serif); font-style: italic; font-size: 12px;
color: var(--ink); background: rgba(239,231,210,0.85);
backdrop-filter: blur(2px);
}
.deck-hud .counter {
font-family: var(--mono);
letter-spacing: 0.04em;
color: var(--ink);
background: rgba(239,231,210,0.85);
padding: 4px 8px;
border: 1px solid var(--line);
border-radius: 6px;
backdrop-filter: blur(2px);
}
.deck-hud .keys {
background: rgba(239,231,210,0.85);
padding: 4px 10px;
border: 1px solid var(--line);
border-radius: 6px;
backdrop-filter: blur(2px);
}
/* progress bar at bottom */
.deck-progress {
position: fixed;
left: 0; right: 0; bottom: 0;
height: 2px;
background: var(--line-soft);
z-index: 50;
}
.deck-progress .bar {
height: 100%;
background: var(--coral);
width: 0%;
transition: width 240ms ease;
}
/* ---------- COVER slide ---------- */
.s-cover .slide-inner {
grid-template-columns: 1.1fr 0.9fr;
align-content: center;
gap: 60px;
}
.s-cover .copy {
display: flex; flex-direction: column; gap: 22px;
}
.s-cover .eyebrow {
font-family: var(--sans); font-size: 11px; font-weight: 600;
letter-spacing: 0.22em; text-transform: uppercase;
color: var(--coral); display: inline-flex; align-items: center; gap: 12px;
}
.s-cover .eyebrow::before {
content: ''; width: 18px; height: 1px;
background: var(--coral); display: inline-block;
}
.s-cover h1 {
font-family: var(--sans);
font-weight: 800;
font-size: clamp(40px, 5.2vw, 80px);
line-height: 1.02;
letter-spacing: -0.028em;
color: var(--ink);
margin: 0;
}
.s-cover h1 em {
font-family: var(--serif);
font-style: italic; font-weight: 500;
letter-spacing: -0.018em;
}
.s-cover h1 .dot { color: var(--coral); }
.s-cover .subtitle {
font-family: var(--serif); font-style: italic; font-weight: 500;
font-size: 22px; color: var(--ink-soft); margin-top: -8px;
}
.s-cover .lead {
font-family: var(--body); font-size: 17px;
color: var(--ink-soft); max-width: 42ch; line-height: 1.55;
}
.s-cover .meta {
margin-top: 32px;
font-family: var(--mono); font-size: 11px; letter-spacing: 0.06em;
color: var(--ink-faint);
}
.s-cover .art {
position: relative; aspect-ratio: 1 / 1; max-width: 620px;
margin-left: auto; margin-right: 0;
border: 1px solid var(--line-soft); border-radius: 14px;
overflow: hidden; background: var(--bone);
}
.s-cover .art img { width: 100%; height: 100%; object-fit: contain; }
/* ---------- SECTION divider slide ---------- */
.s-section .slide-inner {
grid-template-columns: 1fr;
align-content: center;
text-align: center;
gap: 32px;
}
.s-section .roman {
font-family: var(--serif); font-style: italic; font-weight: 500;
font-size: clamp(80px, 10vw, 160px);
color: var(--coral); line-height: 1; letter-spacing: -0.02em;
}
.s-section h2 {
font-family: var(--sans); font-weight: 800;
font-size: clamp(54px, 6.6vw, 100px);
letter-spacing: -0.028em; line-height: 1.0; color: var(--ink);
max-width: 18ch; margin: 0 auto;
}
.s-section h2 em {
font-family: var(--serif); font-style: italic; font-weight: 500;
}
.s-section h2 .dot { color: var(--coral); }
.s-section .lead {
font-family: var(--body); font-size: 17px;
color: var(--ink-soft); max-width: 50ch; margin: 0 auto;
}
/* ---------- CONTENT slide ---------- */
.s-content .slide-inner { gap: 48px; }
.s-content.layout-left .slide-inner { grid-template-columns: 1fr 0.9fr; }
.s-content.layout-right .slide-inner { grid-template-columns: 0.9fr 1fr; }
.s-content.layout-right .copy { order: 2; }
.s-content.layout-right .art { order: 1; }
.s-content.layout-full .slide-inner { grid-template-columns: 1fr; max-width: 980px; }
.s-content .copy { display: flex; flex-direction: column; gap: 22px; }
.s-content .eyebrow {
font-family: var(--sans); font-size: 11px; font-weight: 600;
letter-spacing: 0.22em; text-transform: uppercase; color: var(--coral);
display: inline-flex; align-items: center; gap: 12px;
}
.s-content .eyebrow::before {
content: ''; width: 18px; height: 1px;
background: var(--coral); display: inline-block;
}
.s-content h2 {
font-family: var(--sans); font-weight: 800;
font-size: clamp(40px, 4.6vw, 64px);
letter-spacing: -0.024em; line-height: 1.05;
color: var(--ink); margin: 0;
}
.s-content h2 em { font-family: var(--serif); font-style: italic; font-weight: 500; }
.s-content h2 .dot { color: var(--coral); }
.s-content .body {
font-family: var(--body); font-size: 16px;
color: var(--ink-soft); max-width: 56ch; line-height: 1.55;
}
.s-content .body code { font-family: var(--mono); font-size: 14px; background: var(--bone); padding: 1px 6px; border-radius: 4px; }
.s-content ul {
list-style: none; padding: 0; margin: 0;
display: flex; flex-direction: column; gap: 12px;
}
.s-content li {
font-family: var(--sans); font-size: 15px;
color: var(--ink-soft); display: flex; gap: 14px; align-items: flex-start;
}
.s-content li::before {
content: ''; width: 12px; height: 1px;
background: var(--coral); margin-top: 11px; flex-shrink: 0;
}
.s-content .art {
position: relative; aspect-ratio: 1 / 1;
border: 1px solid var(--line-soft); border-radius: 14px;
overflow: hidden; background: var(--bone);
}
.s-content .art img { width: 100%; height: 100%; object-fit: contain; }
/* ---------- STATS slide ---------- */
.s-stats .slide-inner { grid-template-columns: 1fr; gap: 60px; }
.s-stats .head { display: flex; flex-direction: column; gap: 22px; }
.s-stats .eyebrow {
font-family: var(--sans); font-size: 11px; font-weight: 600;
letter-spacing: 0.22em; text-transform: uppercase; color: var(--coral);
display: inline-flex; align-items: center; gap: 12px;
}
.s-stats .eyebrow::before { content: ''; width: 18px; height: 1px; background: var(--coral); display: inline-block; }
.s-stats h2 {
font-family: var(--sans); font-weight: 800;
font-size: clamp(44px, 5vw, 72px);
letter-spacing: -0.026em; line-height: 1.05; max-width: 18ch; margin: 0;
}
.s-stats h2 em { font-family: var(--serif); font-style: italic; font-weight: 500; }
.s-stats h2 .dot { color: var(--coral); }
.s-stats .grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 36px;
border-top: 1px solid var(--line);
padding-top: 36px;
}
.s-stats .stat { display: flex; flex-direction: column; gap: 10px; }
.s-stats .stat .num {
font-family: var(--sans); font-weight: 800;
font-size: clamp(80px, 9vw, 140px); line-height: 1;
letter-spacing: -0.04em; color: var(--ink);
}
.s-stats .stat .num em { color: var(--coral); font-family: var(--serif); font-style: italic; font-weight: 500; }
.s-stats .stat .label {
font-family: var(--sans); font-size: 12px;
letter-spacing: 0.18em; text-transform: uppercase;
color: var(--ink); font-weight: 600;
}
.s-stats .stat .sub {
font-family: var(--body); font-size: 13px;
color: var(--ink-mute); max-width: 26ch; line-height: 1.5;
}
.s-stats .caption {
font-family: var(--mono); font-size: 11px;
color: var(--ink-faint); letter-spacing: 0.04em;
}
/* ---------- QUOTE slide ---------- */
.s-quote .slide-inner { grid-template-columns: 1.4fr 0.8fr; gap: 60px; align-items: center; }
.s-quote.no-art .slide-inner { grid-template-columns: 1fr; max-width: 980px; }
.s-quote blockquote {
font-family: var(--sans); font-weight: 700;
font-size: clamp(36px, 4vw, 56px);
letter-spacing: -0.022em; line-height: 1.15;
color: var(--ink); margin: 0;
position: relative;
}
.s-quote blockquote em { font-family: var(--serif); font-style: italic; font-weight: 500; }
.s-quote .author {
margin-top: 38px; display: flex; align-items: center; gap: 16px;
}
.s-quote .author .avatar {
width: 48px; height: 48px; border-radius: 50%; background: var(--ink);
color: var(--paper); font-family: var(--serif); font-style: italic; font-size: 22px;
display: inline-flex; align-items: center; justify-content: center;
}
.s-quote .author p { font-family: var(--sans); font-size: 14px; font-weight: 600; }
.s-quote .author p span { display: block; color: var(--ink-mute); font-weight: 400; }
.s-quote .art {
position: relative; aspect-ratio: 1 / 1;
border: 1px solid var(--line-soft); border-radius: 14px;
overflow: hidden; background: var(--bone);
}
.s-quote .art img { width: 100%; height: 100%; object-fit: contain; }
/* ---------- CTA slide ---------- */
.s-cta .slide-inner { grid-template-columns: 1fr; max-width: 980px; gap: 32px; text-align: left; }
.s-cta .eyebrow {
font-family: var(--sans); font-size: 11px; font-weight: 600;
letter-spacing: 0.22em; text-transform: uppercase; color: var(--coral);
display: inline-flex; align-items: center; gap: 12px;
}
.s-cta .eyebrow::before { content: ''; width: 18px; height: 1px; background: var(--coral); display: inline-block; }
.s-cta h2 {
font-family: var(--sans); font-weight: 800;
font-size: clamp(54px, 6.4vw, 96px);
letter-spacing: -0.028em; line-height: 1.0; color: var(--ink); margin: 0;
}
.s-cta h2 em { font-family: var(--serif); font-style: italic; font-weight: 500; }
.s-cta h2 .dot { color: var(--coral); }
.s-cta .body { font-family: var(--body); font-size: 17px; color: var(--ink-soft); max-width: 50ch; line-height: 1.55; }
.s-cta .actions { display: inline-flex; gap: 14px; margin-top: 12px; }
/* ---------- END slide ---------- */
.s-end .slide-inner {
grid-template-columns: 1fr;
align-content: end;
padding-bottom: 72px;
text-align: left;
gap: 16px;
max-width: none;
padding-left: 64px; padding-right: 64px;
}
.s-end .word {
font-family: var(--sans); font-weight: 900;
font-size: clamp(80px, 14vw, 220px);
letter-spacing: -0.04em; line-height: 1.05;
color: var(--ink); white-space: nowrap;
overflow-x: hidden;
padding-bottom: 0.18em;
}
.s-end .word em { font-family: var(--serif); font-style: italic; font-weight: 500; color: var(--coral); }
.s-end .footer {
border-top: 1px solid var(--line);
padding-top: 22px;
font-family: var(--sans); font-size: 11px;
letter-spacing: 0.18em; text-transform: uppercase;
color: var(--ink-faint);
}
/* responsive */
@media (max-width: 1080px) {
.slide-inner { padding: 48px 56px; }
.s-cover .slide-inner,
.s-content.layout-left .slide-inner,
.s-content.layout-right .slide-inner,
.s-quote .slide-inner { grid-template-columns: 1fr; gap: 36px; }
.s-content.layout-right .copy { order: 1; }
.s-content.layout-right .art { order: 2; }
}
@media (max-width: 640px) {
.slide-inner { padding: 36px 24px; }
.deck-hud { padding: 0 16px; font-size: 9.5px; letter-spacing: 0.14em; }
.deck-hud .keys { display: none; }
}
`;
/* ------------------------------------------------------------------ *
* slide renderers
* ------------------------------------------------------------------ */
function renderCover(s: CoverSlide, assets: string): string {
return `<section class='slide s-cover' data-slide-kind='cover'>
<div class='slide-inner'>
<div class='copy'>
<span class='eyebrow'>${s.eyebrow}</span>
<h1>${mixed(s.title)}</h1>
${s.subtitle ? `<div class='subtitle'>${s.subtitle}</div>` : ''}
<p class='lead'>${s.lead}</p>
${s.meta ? `<div class='meta'>${s.meta}</div>` : ''}
</div>
<div class='art'>${imgFor(s.image_slot, assets)}</div>
</div>
</section>`;
}
function renderSection(s: SectionSlide): string {
return `<section class='slide s-section' data-slide-kind='section'>
<div class='slide-inner'>
<div class='roman'>${s.roman}</div>
<h2>${mixed(s.title)}</h2>
${s.lead ? `<p class='lead'>${s.lead}</p>` : ''}
</div>
</section>`;
}
function renderContent(s: ContentSlide, assets: string): string {
const layout = s.layout ?? 'left';
const hasArt = !!s.image_slot;
return `<section class='slide s-content layout-${layout}${hasArt ? '' : ' no-art'}' data-slide-kind='content'>
<div class='slide-inner'>
<div class='copy'>
${s.eyebrow ? `<span class='eyebrow'>${s.eyebrow}</span>` : ''}
<h2>${mixed(s.title)}</h2>
${s.body ? `<p class='body'>${s.body}</p>` : ''}
${s.bullets && s.bullets.length ? `<ul>${s.bullets.map((b) => `<li>${b}</li>`).join('')}</ul>` : ''}
</div>
${hasArt ? `<div class='art'>${imgFor(s.image_slot, assets)}</div>` : ''}
</div>
</section>`;
}
function renderStats(s: StatsSlide): string {
const stats = s.stats
.map(
(st) =>
`<div class='stat'>
<div class='num'>${st.value}</div>
<div class='label'>${st.label}</div>
${st.sub ? `<div class='sub'>${st.sub}</div>` : ''}
</div>`,
)
.join('\n ');
return `<section class='slide s-stats' data-slide-kind='stats'>
<div class='slide-inner'>
<div class='head'>
${s.eyebrow ? `<span class='eyebrow'>${s.eyebrow}</span>` : ''}
<h2>${mixed(s.title)}</h2>
</div>
<div class='grid'>
${stats}
</div>
${s.caption ? `<div class='caption'>${s.caption}</div>` : ''}
</div>
</section>`;
}
function renderQuote(s: QuoteSlide, assets: string): string {
const hasArt = !!s.image_slot;
return `<section class='slide s-quote${hasArt ? '' : ' no-art'}' data-slide-kind='quote'>
<div class='slide-inner'>
<div>
<blockquote>&ldquo;${mixed(s.quote)}&rdquo;</blockquote>
<div class='author'>
<span class='avatar'>${s.author.initial}</span>
<p>${s.author.name}<br/><span>${s.author.title}</span></p>
</div>
</div>
${hasArt ? `<div class='art'>${imgFor(s.image_slot, assets)}</div>` : ''}
</div>
</section>`;
}
function renderCTA(s: CTASlide): string {
return `<section class='slide s-cta' data-slide-kind='cta'>
<div class='slide-inner'>
${s.eyebrow ? `<span class='eyebrow'>${s.eyebrow}</span>` : ''}
<h2>${mixed(s.title)}</h2>
${s.body ? `<p class='body'>${s.body}</p>` : ''}
<div class='actions'>
<a class='btn btn-primary' href='${s.primary.href}'${ext(s.primary.href)}>
${s.primary.label}
<span class='arrow'>${ARROW_OUT}</span>
</a>
${
s.secondary
? `<a class='btn btn-ghost' href='${s.secondary.href}'${ext(s.secondary.href)}>
${s.secondary.label}
<span class='arrow'>${ARROW_OUT}</span>
</a>`
: ''
}
</div>
</div>
</section>`;
}
function renderEnd(s: EndSlide): string {
return `<section class='slide s-end' data-slide-kind='end'>
<div class='slide-inner'>
<div class='word'>${mixed(s.mega)}</div>
${s.footer ? `<div class='footer'>${s.footer}</div>` : ''}
</div>
</section>`;
}
function renderSlide(s: Slide, assets: string): string {
switch (s.kind) {
case 'cover': return renderCover(s, assets);
case 'section': return renderSection(s);
case 'content': return renderContent(s, assets);
case 'stats': return renderStats(s);
case 'quote': return renderQuote(s, assets);
case 'cta': return renderCTA(s);
case 'end': return renderEnd(s);
}
}
/* ------------------------------------------------------------------ *
* runtime script (keyboard nav + counter + progress)
* ------------------------------------------------------------------ */
const RUNTIME_SCRIPT = `
<script>
/*
* Deck runtime keyboard nav, counter update, progress bar.
*
* /PageUp/k previous slide
* /PageDown/Space/j next slide
* Home/End first / last slide
* Updates .deck-hud .counter and .deck-progress .bar live as the
* user scrolls or paginates.
*/
(function () {
var deck = document.querySelector('.deck');
if (!deck) return;
var slides = Array.prototype.slice.call(document.querySelectorAll('.slide'));
var counter = document.querySelector('.deck-hud .counter');
var bar = document.querySelector('.deck-progress .bar');
var total = slides.length;
function indexFromScroll() {
var y = deck.scrollTop;
var h = deck.clientHeight;
var i = Math.round(y / h);
if (i < 0) i = 0;
if (i > total - 1) i = total - 1;
return i;
}
function update() {
var i = indexFromScroll();
if (counter) counter.textContent = String(i + 1).padStart(2, '0') + ' / ' + String(total).padStart(2, '0');
if (bar) bar.style.width = ((i + 1) / total * 100) + '%';
}
function goto(i) {
if (i < 0) i = 0;
if (i > total - 1) i = total - 1;
slides[i].scrollIntoView({ behavior: 'smooth', block: 'start' });
}
deck.addEventListener('scroll', update, { passive: true });
document.addEventListener('keydown', function (e) {
var i = indexFromScroll();
if (e.key === 'ArrowDown' || e.key === 'PageDown' || e.key === ' ' || e.key === 'j') {
e.preventDefault(); goto(i + 1);
} else if (e.key === 'ArrowUp' || e.key === 'PageUp' || e.key === 'k') {
e.preventDefault(); goto(i - 1);
} else if (e.key === 'Home') {
e.preventDefault(); goto(0);
} else if (e.key === 'End') {
e.preventDefault(); goto(total - 1);
}
});
update();
})();
</script>`;
/* ------------------------------------------------------------------ *
* top-level
* ------------------------------------------------------------------ */
export function renderDeck(inputs: EditorialCollageDeckInputs, baseCss: string): string {
const assets = inputs.imagery.assets_path.replace(/\/?$/, '/');
const slides = inputs.slides.map((s) => renderSlide(s, assets)).join('\n ');
const total = inputs.slides.length;
return [
`<!DOCTYPE html>`,
`<html lang='${inputs.brand.locale ?? 'en'}'>`,
`<head>`,
`<meta charset='utf-8' />`,
`<meta name='viewport' content='width=device-width, initial-scale=1' />`,
`<title>${inputs.deck_title}</title>`,
`<meta name='description' content='${inputs.brand.description}' />`,
`<link rel='preconnect' href='https://fonts.googleapis.com' />`,
`<link rel='preconnect' href='https://fonts.gstatic.com' crossorigin />`,
`<link href='https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700;800;900&family=Inter:wght@300;400;500;600&family=Playfair+Display:ital,wght@0,500;0,600;1,400;1,500;1,600;1,700&family=JetBrains+Mono:wght@400;500&display=swap' rel='stylesheet' />`,
`<style>${baseCss}${DECK_CSS}</style>`,
`</head>`,
`<body>`,
`<div class='deck-hud'>`,
` <div class='left'>`,
` <span class='mark'>${inputs.brand.mark}</span>`,
` <span>${inputs.deck_title}</span>`,
` </div>`,
` <div class='right'>`,
` <span class='keys'>← / → · Space</span>`,
` <span class='counter'>01 / ${String(total).padStart(2, '0')}</span>`,
` </div>`,
`</div>`,
`<div class='deck'>`,
` ${slides}`,
`</div>`,
`<div class='deck-progress'><div class='bar'></div></div>`,
RUNTIME_SCRIPT,
`</body>`,
`</html>`,
``,
].join('\n');
}
async function main(): Promise<void> {
const [, , inputsArg, outputArg] = process.argv;
if (!inputsArg || !outputArg) {
console.error('Usage: npx tsx scripts/compose.ts <inputs.json> <output.html>');
process.exit(1);
}
const inputsPath = isAbsolute(inputsArg) ? inputsArg : resolve(process.cwd(), inputsArg);
const outputPath = isAbsolute(outputArg) ? outputArg : resolve(process.cwd(), outputArg);
const [inputsRaw, css] = await Promise.all([
readFile(inputsPath, 'utf8'),
readFile(SISTER_STYLES, 'utf8'),
]);
const inputs = JSON.parse(inputsRaw) as EditorialCollageDeckInputs;
const html = renderDeck(inputs, css);
await mkdir(dirname(outputPath), { recursive: true });
await writeFile(outputPath, html, 'utf8');
console.log(
`✓ wrote ${outputPath} (${(html.length / 1024).toFixed(1)} KB, ${inputs.slides.length} slides)`,
);
}
const isMain = import.meta.url === `file://${process.argv[1]}`;
if (isMain) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}

View file

@ -19,7 +19,7 @@ od:
mode: prototype
platform: desktop
scenario: marketing
featured: 3
featured: 7
preview:
type: html
entry: index.html

View file

@ -22,7 +22,7 @@ od:
mode: prototype
platform: mobile
scenario: personal
featured: 4
featured: 12
preview:
type: html
entry: index.html

View file

@ -13,7 +13,7 @@ od:
mode: image
surface: image
scenario: personal
featured: 3
featured: 11
preview:
type: image
entry: final/spritesheet.png

View file

@ -0,0 +1,72 @@
# kami-deck
Sister skill to [`kami-landing`](../kami-landing/). Produces a single
self-contained HTML file: a horizontal magazine-style swipe deck in
the **kami (紙 / 纸)** design system — print rhythm, ink-blue accent,
serif at one weight, no italic, no cool grays.
> **Read first** — agent contract, schema, and self-check live in
> [`SKILL.md`](./SKILL.md). This README is the human quick-start.
## What you get
- N viewport-sized slides laid out horizontally on a transformed
flex track.
- **Cover / chapter / end slides** flip background to ink-blue
(`#1B365D`) with ivory text. **All other slides** stay on
parchment (`#f5f4ed`) with serif at weight 500.
- **Per-slide chrome strip**: brand mark · deck title · live
slide counter (`01 / 09`).
- **Tabular-nums** on every counter, metric, and date.
- **Ink-blue progress bar** at the bottom that fills as you advance.
- **Dot indicator** near the bottom; click to jump.
- **ESC overview grid** with scaled thumbnails.
- **Keyboard / wheel / touch nav** — same model as `guizang-ppt`.
- **Multilingual stack** — EN / zh-CN / ja, set on `:root` via the
`language` parameter.
## 30-second tour
The skill is "agent-driven, no script": there's no `compose.ts`. The
agent reads `SKILL.md`, gathers the brief, then writes
`out/index.html` directly using the tokens from
[`design-systems/kami/DESIGN.md`](../../design-systems/kami/DESIGN.md)
and the layout primitives in [`example.html`](./example.html).
To preview the canonical Open Design instance:
```bash
open example.html
```
To start a fresh project:
1. Open the skill in your agent (Claude · Cursor · Codex · …).
2. Answer two rounds of brief questions (identity + content).
3. Write the file. Done.
## Files
```text
skills/kami-deck/
├── SKILL.md # ← agent contract (read this first)
├── README.md # ← you are here
└── example.html # canonical Open Design rendering (9 slides)
```
## Boundaries
- No second accent color. No italic. No cool blue-grays. No hard
drop shadows.
- One self-contained HTML file. No router, no external JS bundle.
- Cover / chapter / end slides only — no other slide kind goes dark.
- Tag fills must be solid hex (kami's print invariant), not `rgba()`.
## See also
- [`kami-landing`](../kami-landing/) — long-form one-pager sister.
- [`design-systems/kami/DESIGN.md`](../../design-systems/kami/DESIGN.md) — token spec.
- [`open-design-landing-deck`](../open-design-landing-deck/) — same
swipe nav model, different visual language (Atelier Zero).
- Upstream: [`tw93/kami`](https://github.com/tw93/kami) — original
Claude skill (MIT) the design system adapts.

196
skills/kami-deck/SKILL.md Normal file
View file

@ -0,0 +1,196 @@
---
name: kami-deck
description: >
Produce a print-grade slide deck in the kami (紙 / 纸) design system —
warm parchment background (or ink-blue for cover / chapter slides),
serif at one weight, ink-blue accent ≤ 5% per slide, no italic.
Horizontal magazine swipe pagination (←/→ · wheel · swipe · ESC
overview). One self-contained HTML file, zero dependencies beyond
Google Fonts.
triggers:
- kami deck
- 紙 deck
- 纸 deck
- paper slides
- white paper deck
- editorial deck
- print-style slides
- kami slides
od:
category: brand-deck
surface: web
mode: deck
scenario: marketing
featured: 4
audience: founders, researchers, design studios, conference talks
tone: editorial, restrained, print-first
scale: 6-15 viewport-locked slides
preview:
type: html
entry: index.html
design_system:
requires: false
craft:
requires:
- typographic-rhythm
- pixel-discipline
inputs:
- id: brand
label: Brand identity (shared across slides)
- id: deck_title
label: Deck title shown in the per-slide chrome
- id: slides
label: Ordered list of typed slides (cover · chapter · content · stats · quote · cta · end)
- id: language
label: Primary language stack
parameters:
language:
type: enum
values: [en, zh-CN, ja]
default: en
description: Sets `--serif` to Charter / TsangerJinKai02 / YuMincho respectively.
outputs:
- path: <out>/index.html
description: Self-contained kami deck with horizontal swipe pagination.
capabilities_required:
- file-write
example_prompt: |
Build me a 9-slide kami-style internal deck for "Hokuto Research" —
a Q1 portfolio review. Cover slide on ink-blue with the firm name.
Chapter dividers between Macro, Equities, and Outlook. Three content
slides with ink-blue numbered headings. One stats slide showing
AUM / IRR / fund count. One closing CTA. End card with the firm
signature. Japanese language stack.
---
# kami-deck
Sister skill to [`kami-landing`](../kami-landing/). Produces a single
self-contained HTML file: a horizontal magazine-style swipe deck in
the **kami (紙 / 纸)** design system — print rhythm, ink-blue accent,
serif at one weight, no italic, no cool grays.
The navigation model is intentionally borrowed from the
[`guizang-ppt`](../guizang-ppt/) skill — `←/→` arrow keys, wheel /
swipe, ESC for the overview grid. The aesthetic stays kami: parchment
content slides, ink-blue cover and chapter slides, serif everywhere.
> **Design system source of truth:**
> [`design-systems/kami/DESIGN.md`](../../design-systems/kami/DESIGN.md).
> Read it before shipping. Tokens, type rules, and forbidden colors
> all live there. Slide-specific scale ratios (macro × 1.6,
> letter-spacing × 0.6 vs. print) are documented in §3 "Hierarchy"
> and §5 "Layout Principles · Slides".
## What you get
- N viewport-sized slides (6-15 is the sweet spot) laid out
horizontally on one transformed flex track.
- **Cover and chapter slides** flip background to ink-blue
(`#1B365D`) with ivory text — the only place dark theme is used.
- **Content / stats / quote / CTA slides** stay on parchment
(`#f5f4ed`) with serif at weight 500.
- **Per-slide chrome strip**: brand mark · deck title · live slide
counter (`01 / 09`).
- **Tabular-nums** on every counter, metric, page number.
- **Coral-free** — kami's accent is ink-blue. Progress bar and dot
nav are ink-blue too.
- **Keyboard / wheel / touch nav**, ESC overview grid, dot indicator.
- **Multilingual stack** — EN / zh-CN / ja, set on `:root` via
the `language` parameter.
## Slide types
| Kind | Background | Use it for |
| :---------- | :--------- | :-------------------------------------------------------- |
| `cover` | ink-blue | Title plate at the start. Centered serif title + tagline. |
| `chapter` | ink-blue | Roman/Arabic numeral chapter divider. |
| `content` | parchment | Section number + title + body + optional bullets. |
| `stats` | parchment | 3-4 metric cells (value · label · sub). |
| `quote` | parchment | Pull quote with ink-blue left rule + author signature. |
| `cta` | parchment | Closing pitch + 1-2 buttons. |
| `end` | ink-blue | Mega serif kicker word + colophon footer. |
A typical 11-slide deck:
```
1. cover — ink-blue title plate
2. chapter — "01 / Why now"
3. content — manifesto
4. content — capabilities + bullets
5. stats — 4 numbers
6. chapter — "02 / How it feels"
7. content — method
8. content — selected work
9. quote — testimonial
10. cta — primary action
11. end — ink-blue kicker
```
## Workflow
### 1. Gather the brief
Ask in two rounds (don't dump the whole list at once):
1. Identity round — name, mark, tagline, location, edition, language.
2. Content round — for each slide, kind + the typed fields.
### 2. Pick the language stack
Same as [`kami-landing`](../kami-landing/SKILL.md#2-pick-the-language-stack):
EN → Charter, zh-CN → TsangerJinKai02 / Source Han Serif, ja →
YuMincho. JA also overrides `--olive` to `#4d4c48` because YuMincho
strokes are thinner.
### 3. Write `index.html`
Output a single file with all CSS inline. Mirror the structure of
[`example.html`](./example.html). Use only the tokens from
`design-systems/kami/DESIGN.md`.
The runtime script (keyboard / wheel / touch nav, dot indicator,
progress bar, ESC overview) should match the model documented in
[`open-design-landing-deck/scripts/compose.ts`](../open-design-landing-deck/scripts/compose.ts).
Do **not** reuse the open-design-landing-deck CSS; the visual
language is different.
### 4. Self-check
- [ ] All cover / chapter / end slides use ink-blue background
(`#1B365D`) with ivory text. All other slides are on
parchment.
- [ ] Ink-blue covers ≤ 5% of any parchment slide's surface.
- [ ] Slide titles use serif weight 500 only. No italic.
- [ ] All numeric stacks (counter, metrics, page numbers) carry
`font-variant-numeric: tabular-nums`.
- [ ] Press `→` / `Space` / scroll. Smoothly slides one viewport
to the right; dot nav advances; the ink-blue progress bar
ticks forward.
- [ ] Press `Esc`. Overview grid appears with scaled thumbnails.
- [ ] Resize to 1080px and 640px. Cover / content collapse to a
single column; dot nav still works.
- [ ] Lighthouse: contrast AA, font-display swap, no layout shift.
## Boundaries
- **Do not** introduce a second accent color. Pick ink-blue or
pick nothing.
- **Do not** use italic anywhere — emphasis swaps to ink-blue.
- **Do not** use `rgba()` for tag fills; pre-blend over parchment
and use solid hex from the table in
`design-systems/kami/DESIGN.md` §2.
- **Do not** add a router. This is a single-file artifact.
- **Do not** reuse Atelier Zero collage imagery (the open-design-landing
visual system). Kami is gradient-free, image-light, and hierarchy
is carried by type.
## See also
- [`kami-landing`](../kami-landing/) — long-form one-pager sister skill.
- [`design-systems/kami/DESIGN.md`](../../design-systems/kami/DESIGN.md) — token spec.
- [`open-design-landing-deck`](../open-design-landing-deck/) — same
horizontal swipe nav model, different visual language (Atelier Zero).
- Upstream: [`tw93/kami`](https://github.com/tw93/kami) — original
Claude skill (MIT). Kami's slides.py template documents the macro
× 1.6 / micro × 0.6 ratios this skill applies.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,69 @@
# kami-landing
A drop-in skill that turns a brief into a print-grade kami one-pager —
warm parchment canvas, ink-blue accent, serif at one weight, no
italic, no cool grays. The output reads like a white paper or studio
one-pager, not an app UI.
> **Read first** — the agent contract, schema, and self-check live in
> [`SKILL.md`](./SKILL.md). This README is the human quick-start.
## What you get
A single self-contained HTML file with:
- **Warm parchment canvas** (`#f5f4ed`), never `#ffffff`.
- **Single chromatic accent** — ink-blue (`#1B365D`), constrained to
≤ 5% of visible surface.
- **Serif at weight 500** for hierarchy. No italic anywhere.
- **Tight print rhythm** — line-heights 1.101.55, language-aware
letter-spacing.
- **Tabular-nums** on every numeric stack.
- **Solid-hex tag fills** (no `rgba()`, which print renderers
double-paint).
- **1px rings + whisper shadows** for depth — no hard drop shadows.
- **Multilingual** by design (EN / zh-CN / ja stacks selectable via
the `language` parameter).
## 30-second tour
The skill is "agent-driven, no script": there's no `compose.ts`. The
agent reads `SKILL.md`, gathers the brief, then writes
`out/index.html` directly using the tokens and components catalogued
in [`design-systems/kami/DESIGN.md`](../../design-systems/kami/DESIGN.md).
To preview the canonical Open Design instance:
```bash
open example.html
```
To start a fresh project:
1. Open the skill in your agent (Claude · Cursor · Codex · …).
2. Answer two rounds of brief questions (identity + content).
3. Write the file. Done.
## Files
```text
skills/kami-landing/
├── SKILL.md # ← agent contract (read this first)
├── README.md # ← you are here
└── example.html # canonical Open Design rendering
```
## Boundaries
- No external JavaScript. The page is paper, not an app.
- No hard drop shadows, no neumorphism, no `backdrop-filter`.
- No second accent color. No italic. No cool blue-grays.
- One `.tag.brush` per page maximum (it's the only sanctioned gradient).
## See also
- [`design-systems/kami/DESIGN.md`](../../design-systems/kami/DESIGN.md) — the full token spec.
- [`skills/kami-deck/`](../kami-deck/) — sibling skill that produces a
slide deck in the same kami language.
- Upstream: [`tw93/kami`](https://github.com/tw93/kami) — original
Claude skill (MIT) that the design system adapts.

View file

@ -0,0 +1,234 @@
---
name: kami-landing
description: >
Produce a print-grade single-page kami (紙 / 纸) document — warm
parchment canvas, ink-blue accent, serif at one weight, no italic,
no cool grays. The output reads like a professional white paper or
studio one-pager, not an app UI. Multilingual by design (EN ·
zh-CN · ja). One self-contained HTML file, zero dependencies.
triggers:
- kami
- 紙
- 纸
- paper one-pager
- 白皮书
- white paper
- parchment landing
- editorial document
- print-grade page
- kami landing
od:
category: brand-page
surface: web
mode: prototype
platform: desktop
scenario: marketing
featured: 3
audience: founders, design studios, OSS maintainers, researchers
tone: editorial, restrained, print-first
scale: viewport-anchored long-form single page
preview:
type: html
entry: index.html
reload: debounce-100
design_system:
requires: false
craft:
requires:
- typographic-rhythm
- pixel-discipline
inputs:
- id: brand
label: Brand identity
description: Name, tagline, location, edition / version, primary URL.
- id: hero
label: Hero / cover block
description: Eyebrow + headline (one line, ≤ 6 words at display size) + tagline + 3 hero meta tokens.
- id: manifesto
label: Manifesto paragraph + signature
- id: metrics
label: 3-6 metric tiles (value · label · sub)
- id: chapters
label: 3-5 numbered chapters (title + lede + body)
- id: footer
label: License · year · contact + 3-column site index
parameters:
output_format:
type: enum
values: [standalone-html]
default: standalone-html
language:
type: enum
values: [en, zh-CN, ja]
default: en
description: >
Sets the primary serif stack on `:root`. EN uses Charter,
zh-CN uses TsangerJinKai02 / Source Han Serif, ja uses
YuMincho. Mixed-script content is allowed inline; the browser
resolves per-glyph fallback automatically.
outputs:
- path: <out>/index.html
description: Self-contained HTML, kami CSS inlined, zero JS, zero external dependencies beyond Google Fonts.
capabilities_required:
- file-write
example_prompt: |
Build me a kami-style one-pager for "Lumen Field", an indie studio
shipping a soundscape app for focus. Hero headline "Soundscapes for
focused work.", manifesto paragraph + signature "by Lumen Field,
Berlin", 3 metric tiles (12 soundscapes / 4 presets / 1 daily ritual),
three numbered chapters covering the studio, the app, and the roadmap.
English-language stack.
---
# kami-landing
Produce a single-page document in the **kami (紙 / 纸)** design system.
The aesthetic borrows from editorial print, technical white papers,
and old typewritten correspondence — the goal is *good content on
good paper*, not *modern app UI*.
> **Design system source of truth:** [`design-systems/kami/DESIGN.md`](../../design-systems/kami/DESIGN.md).
> Read it before shipping. Tokens, type rules, the "ten invariants",
> and forbidden colors all live there.
## What you get
A single self-contained HTML file with:
- **Warm parchment canvas** (`#f5f4ed`) — never `#ffffff`.
- **Single chromatic accent** — ink-blue (`#1B365D`), used on the
section number, the headline accent word, the left rule of the
manifesto, and the metric values. Anywhere else, ink-blue must
cover ≤ 5% of the document surface area.
- **Serif at one weight (500) for hierarchy** — Charter (EN),
TsangerJinKai02 / Source Han Serif (CN), or YuMincho (JA),
selected by the `language` parameter. **No italic anywhere.**
- **Tight print rhythm** — line-heights 1.101.55, letter-spacing
per language (0 for EN, 0.35px for CN, 0.02em for JA).
- **Numeric stacks set in `font-variant-numeric: tabular-nums`** so
metric columns and pagination digits sit cleanly aligned.
- **Depth via 1px rings + whisper shadows** (`0 4px 24px rgba(0,0,0,0.05)`).
No hard drop shadows, no neumorphism, no backdrop-filter blurs.
- **Tag fills as solid hex** (e.g. `#E4ECF5`), never `rgba()`
print renderers double-paint alpha tags.
- **Responsive** at 1280 / 980 / 768 / 560.
## Page structure
```text
1. Eyebrow row — locale switcher · edition · version (12px sans uppercase)
2. Hero — display headline (96106px serif 500), tagline (21px),
three hero-token chips (paper-tinted)
3. Manifesto — pull paragraph in serif 400, 20px, 1.65 LH, with
ink-blue left-rule and signature footer
4. Metrics row — 3-6 cells: value (24px serif 500 ink-blue, tabular-nums),
label (12px serif 500 olive)
5. Chapters — numbered (`01`, `02`, …) ink-blue serif 500 14px,
section title 28-32px, body 14-15px
6. Footer — kicker word (mega serif 500), license · year · contact,
three-column site index in 12px serif 500
```
## Workflow contract
### 1. Gather brand brief
Use `AskQuestion` (or equivalent) to collect the brand brief in
chunks. Don't dump the whole input list on the user; ask in two
rounds:
1. Identity round — name, tagline, location, edition / version,
primary URL, dominant language.
2. Content round — manifesto paragraph + signature, 3-6 metric
tiles, 3-5 chapter (title + lede + body) entries.
### 2. Pick the language stack
The `language` parameter controls which `--serif` stack is set on
`:root`. Pick based on the dominant language of the manifesto and
chapter body copy:
| `language` | `--serif` | Notes |
| :--------- | :-------------------------------------------------------- | :------------------------------------- |
| `en` | Charter, Georgia, Palatino, Times New Roman, serif | default |
| `zh-CN` | TsangerJinKai02, Source Han Serif SC, Songti SC, Georgia | letter-spacing 0.35px on body |
| `ja` | YuMincho, Hiragino Mincho ProN, Source Han Serif JP | also override `--olive` to `#4d4c48` (YuMincho strokes are thinner) |
Inline mixed-script content is fine — the browser per-glyph fallback
chain handles it. Do **not** chain all three families inside one
`font-family` declaration; that dilutes character.
### 3. Write `index.html`
Output a single file with all CSS inline. Mirror the structure of
[`example.html`](./example.html) and use only the tokens from
`design-systems/kami/DESIGN.md`. Do **not** invent new colors,
weights, or font families.
Component primitives the agent can drop in (all defined in the
example's `<style>` block):
- `.eyebrow`, `.label` — sans-serif overlines
- `.metric` — value + label vertical pair
- `.section-num` + `.section-title` + `.section-lede`
- `.tag.standard`, `.tag.brush` — solid-hex tags (one brush max per page)
- `.quote` — left-rule serif 500 quote
- `ul.dash` — en-dash bullets in ink-blue
- `.code` — ivory-bg, 1px-border code block
- `.footer-kicker` — mega serif 500 word
Tag every editable element with `data-od-id="<unique-slug>"` so the
host app's comment mode can target it.
### 4. Self-check before delivering
- [ ] Page background is parchment (`#f5f4ed`), never `#ffffff`.
- [ ] Ink-blue (`#1B365D`) covers ≤ 5% of visible surface — count
section numbers, the manifesto rule, the metric values, the
headline accent. Total ≤ 5%.
- [ ] All grays are warm (R ≈ G > B). No `slate-*`, no `#f3f4f6`.
- [ ] Serif weight stays at 500 — no `font-weight: 700` or `900`
anywhere on serif text.
- [ ] No `font-style: italic` anywhere. Emphasis swaps to ink-blue
color or a `.tag` instead.
- [ ] All numeric stacks (metric values, pagination, dates, financial
figures) carry `font-variant-numeric: tabular-nums`.
- [ ] All tag fills are solid hex (e.g. `#E4ECF5`), never `rgba()`.
- [ ] Shadows: at most a `1px` ring or a `0 4px 24px rgba(0,0,0,0.05)`
whisper. No hard drop shadows.
- [ ] Headline ≤ 6 words at display size; CJK ≤ 8 characters.
- [ ] At 768px and 560px the layout collapses to one column without
horizontal scroll.
## Files in this skill
```text
skills/kami-landing/
├── SKILL.md # this contract
├── README.md # human quick-start
└── example.html # canonical Open Design rendering
```
## Boundaries
- **Do not** invent new colors or typefaces. The kami palette is
fixed; if a brief demands a brand color, push back or render the
brand color as a single `.tag.brush` accent.
- **Do not** introduce a second accent color. Pick ink-blue or pick
nothing.
- **Do not** mix all three font stacks in one declaration; pick the
dominant language, override `--serif` on `:root`, and let the
browser per-glyph fallback resolve mixed-script inline content.
- **Do not** use `rgba()` for tag fills — print renderers
double-paint alpha tags. Use the pre-blended solid hex from the
table in `design-systems/kami/DESIGN.md` §2.
- **Do not** add JavaScript for animation. The page is paper, not
an app — motion belongs to the reader scrolling.
## See also
- [`design-systems/kami/DESIGN.md`](../../design-systems/kami/DESIGN.md) — the full token spec.
- [`skills/kami-deck/`](../kami-deck/) — sister skill that produces a
slide deck in the same kami language.
- Upstream: [`tw93/kami`](https://github.com/tw93/kami) — original
Claude skill (MIT) that the design system adapts.

View file

@ -0,0 +1,673 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
<title>Open Design — Designing intelligence on warm paper.</title>
<meta name='description' content='Open Design as a kami one-pager. Warm parchment canvas, ink-blue accent, serif at one weight, no italic, no cool grays.' />
<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=Source+Serif+4:wght@400;500&family=Source+Sans+3:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap' rel='stylesheet' />
<style>
/*
* kami (紙 / 纸) — canonical one-pager stylesheet.
*
* SINGLE SOURCE OF TRUTH for the kami-landing skill's visual system.
* Tokens, type rules, and the "ten invariants" are defined by
* design-systems/kami/DESIGN.md. Do not invent new tokens here;
* extend the design system first.
*
* Typeface choice: Source Serif 4 (Charter family) is the EN default
* because Charter itself is not Google-hosted. The fallback chain
* resolves to Charter on macOS and to Georgia / Palatino elsewhere.
*/
:root {
/* ----- Surface ----- */
--parchment: #f5f4ed;
--ivory: #faf9f5;
--warm-sand: #e8e6dc;
/* ----- Brand (single chromatic accent) ----- */
--brand: #1B365D;
--brand-light: #2D5A8A;
/* ----- Text (four levels — no fifth) ----- */
--near-black: #141413;
--dark-warm: #3d3d3a;
--olive: #504e49;
--stone: #6b6a64;
/* ----- Border ----- */
--border: #e8e6dc;
--border-soft: #e5e3d8;
/* ----- Tag tints (solid hex, NEVER rgba) ----- */
--tag-08: #EEF2F7;
--tag-14: #E4ECF5;
--tag-22: #D0DCE9;
--tag-30: #D6E1EE;
/* ----- Type stacks ----- */
--serif: 'Source Serif 4', Charter, Georgia, Palatino, 'Times New Roman', serif;
--sans: 'Source Sans 3', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
--mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', Consolas, Monaco, monospace;
/* ----- Motion ----- */
--whisper: 0 4px 24px rgba(0, 0, 0, 0.05);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { background: var(--parchment); color: var(--near-black); }
body {
font-family: var(--serif);
font-weight: 400;
font-size: 14px;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: 'liga', 'calt';
}
strong { font-weight: 500; }
img { max-width: 100%; display: block; }
a { color: var(--brand); text-decoration: none; border-bottom: 1px solid currentColor; }
a:hover { color: var(--brand-light); }
.shell {
max-width: 1120px;
margin: 0 auto;
padding: 88px 64px 120px;
position: relative;
}
/* ---------- eyebrow strip (top meta) ---------- */
.eyebrow-row {
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--sans);
font-size: 12px;
font-weight: 500;
letter-spacing: 1.2px;
text-transform: uppercase;
color: var(--stone);
border-bottom: 1px solid var(--border);
padding-bottom: 18px;
margin-bottom: 88px;
}
.eyebrow-row .lang {
display: inline-flex;
gap: 16px;
}
.eyebrow-row .lang a {
color: var(--stone);
border-bottom: none;
}
.eyebrow-row .lang a.active { color: var(--brand); }
.eyebrow-row .meta {
display: inline-flex;
gap: 22px;
font-variant-numeric: tabular-nums;
}
.eyebrow-row .meta b { color: var(--near-black); font-weight: 500; }
/* ---------- hero ---------- */
.hero {
display: grid;
grid-template-columns: 1.45fr 0.55fr;
gap: 64px;
align-items: end;
margin-bottom: 96px;
}
.hero-copy h1 {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(60px, 7.4vw, 106px);
line-height: 1.05;
letter-spacing: -1.2px;
color: var(--near-black);
margin-bottom: 28px;
}
.hero-copy h1 .ink { color: var(--brand); }
.hero-copy .tagline {
font-family: var(--serif);
font-weight: 500;
font-size: 21px;
line-height: 1.45;
color: var(--olive);
max-width: 38ch;
}
.hero-tokens {
display: flex;
flex-direction: column;
gap: 14px;
align-items: flex-end;
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.4px;
color: var(--stone);
text-transform: uppercase;
}
.hero-tokens .row {
display: inline-flex;
align-items: center;
gap: 10px;
background: var(--ivory);
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px 14px;
font-variant-numeric: tabular-nums;
}
.hero-tokens .row b {
color: var(--brand);
font-weight: 500;
font-family: var(--serif);
font-size: 14px;
}
.hero-tokens .row span { font-family: var(--sans); }
/* ---------- manifesto (pull paragraph + signature) ---------- */
.manifesto {
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
padding: 44px 0 40px;
margin-bottom: 88px;
display: grid;
grid-template-columns: 1fr 1.85fr;
gap: 56px;
align-items: start;
}
.manifesto .label {
font-family: var(--sans);
font-size: 12px;
font-weight: 500;
letter-spacing: 1.2px;
text-transform: uppercase;
color: var(--stone);
}
.manifesto .body {
font-family: var(--serif);
font-weight: 400;
font-size: 20px;
line-height: 1.65;
letter-spacing: 0.05em;
color: var(--olive);
border-left: 2px solid var(--brand);
padding: 4px 0 4px 24px;
}
.manifesto .body strong { color: var(--near-black); font-weight: 500; }
.manifesto .signature {
margin-top: 22px;
font-family: var(--serif);
font-weight: 500;
font-size: 14px;
color: var(--dark-warm);
display: inline-flex;
gap: 10px;
align-items: baseline;
}
.manifesto .signature span { color: var(--stone); }
/* ---------- metrics row ---------- */
.metrics {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0;
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
margin-bottom: 88px;
}
.metric {
padding: 28px 22px 26px;
display: flex;
flex-direction: column;
gap: 6px;
border-right: 1px solid var(--border-soft);
}
.metric:last-child { border-right: 0; }
.metric .value {
font-family: var(--serif);
font-weight: 500;
font-size: 36px;
line-height: 1;
color: var(--brand);
font-variant-numeric: tabular-nums;
letter-spacing: -0.5px;
}
.metric .label {
font-family: var(--serif);
font-weight: 500;
font-size: 13px;
color: var(--near-black);
margin-top: 8px;
}
.metric .sub {
font-family: var(--serif);
font-weight: 400;
font-size: 12px;
color: var(--olive);
line-height: 1.5;
max-width: 28ch;
}
/* ---------- chapters ---------- */
.chapters {
display: grid;
grid-template-columns: 1fr;
gap: 72px;
margin-bottom: 96px;
}
.chapter {
display: grid;
grid-template-columns: 1fr 2.6fr;
gap: 56px;
align-items: start;
}
.chapter .head { display: flex; flex-direction: column; gap: 14px; }
.chapter .num {
font-family: var(--serif);
font-weight: 500;
font-size: 14px;
color: var(--brand);
letter-spacing: 0.4px;
font-variant-numeric: tabular-nums;
}
.chapter .title {
font-family: var(--serif);
font-weight: 500;
font-size: 28px;
line-height: 1.2;
color: var(--near-black);
letter-spacing: 0.4px;
}
.chapter .lede {
font-family: var(--serif);
font-weight: 500;
font-size: 14px;
color: var(--olive);
line-height: 1.5;
max-width: 30ch;
}
.chapter .body {
display: flex;
flex-direction: column;
gap: 16px;
}
.chapter .body p {
font-family: var(--serif);
font-weight: 400;
font-size: 14px;
color: var(--dark-warm);
line-height: 1.55;
max-width: 62ch;
}
.chapter .body p strong { color: var(--near-black); font-weight: 500; }
.chapter .body code {
font-family: var(--mono);
font-size: 12.5px;
color: var(--brand);
background: var(--tag-08);
padding: 1px 6px;
border-radius: 3px;
}
.chapter ul.dash {
list-style: none;
padding: 0;
margin: 4px 0 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.chapter ul.dash li {
position: relative;
padding-left: 16px;
font-family: var(--serif);
font-weight: 400;
font-size: 14px;
color: var(--dark-warm);
line-height: 1.55;
}
.chapter ul.dash li::before {
content: '\2013';
position: absolute;
left: 0;
color: var(--brand);
}
/* ---------- chapter aside (pull-out card) ---------- */
.chapter-aside {
background: var(--ivory);
border: 1px solid var(--border);
border-radius: 8px;
padding: 22px 22px 20px;
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 14px;
transition: box-shadow 0.2s;
}
.chapter-aside:hover { box-shadow: var(--whisper); }
.chapter-aside .a-label {
font-family: var(--sans);
font-size: 12px;
font-weight: 500;
letter-spacing: 0.4px;
text-transform: uppercase;
color: var(--stone);
}
.chapter-aside .a-body {
font-family: var(--mono);
font-size: 12px;
color: var(--near-black);
white-space: pre;
overflow-x: auto;
background: var(--parchment);
border: 1px solid var(--border-soft);
border-radius: 6px;
padding: 12px 14px;
line-height: 1.65;
}
.chapter-aside .a-body .k { color: var(--brand); }
.chapter-aside .a-body .c { color: var(--stone); }
/* ---------- tag system ---------- */
.tag {
display: inline-block;
font-family: var(--sans);
font-size: 12px;
font-weight: 500;
padding: 2px 7px;
border-radius: 2px;
color: var(--brand);
background: var(--tag-08);
letter-spacing: 0.4px;
}
.tag.standard { background: var(--tag-14); padding: 2px 8px; border-radius: 4px; }
.tag.brush {
background: linear-gradient(to right, #D6E1EE, #E4ECF5 70%, #EEF2F7);
}
.tag-row { display: inline-flex; gap: 8px; flex-wrap: wrap; align-items: center; }
/* ---------- footer ---------- */
.footer {
border-top: 1px solid var(--border);
padding-top: 56px;
display: grid;
grid-template-columns: 1.4fr 0.85fr 0.85fr 0.9fr;
gap: 48px;
align-items: start;
}
.footer .kicker {
font-family: var(--serif);
font-weight: 500;
font-size: 56px;
line-height: 1.05;
letter-spacing: -0.6px;
color: var(--near-black);
margin-bottom: 14px;
}
.footer .kicker .ink { color: var(--brand); }
.footer .colophon {
font-family: var(--serif);
font-weight: 500;
font-size: 13px;
color: var(--olive);
max-width: 32ch;
line-height: 1.55;
}
.footer .col h4 {
font-family: var(--sans);
font-size: 12px;
font-weight: 500;
letter-spacing: 1.2px;
text-transform: uppercase;
color: var(--stone);
margin-bottom: 16px;
}
.footer .col ul { list-style: none; padding: 0; margin: 0; }
.footer .col li {
font-family: var(--serif);
font-weight: 500;
font-size: 13px;
color: var(--dark-warm);
margin-bottom: 8px;
}
.footer .col li a { color: var(--dark-warm); border-bottom: none; }
.footer .col li a:hover { color: var(--brand); }
.footer .col li small {
display: block;
font-family: var(--sans);
font-size: 11px;
font-weight: 500;
color: var(--stone);
letter-spacing: 0.4px;
margin-top: 1px;
}
.legal {
margin-top: 56px;
border-top: 1px solid var(--border-soft);
padding-top: 22px;
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--mono);
font-size: 11px;
color: var(--stone);
letter-spacing: 0.4px;
font-variant-numeric: tabular-nums;
}
.legal b { color: var(--near-black); font-weight: 500; }
/* ---------- responsive ---------- */
@media (max-width: 1080px) {
.shell { padding: 64px 48px 96px; }
.hero { grid-template-columns: 1fr; gap: 36px; align-items: start; }
.hero-tokens { flex-direction: row; flex-wrap: wrap; align-items: flex-start; }
.manifesto { grid-template-columns: 1fr; gap: 22px; }
.metrics { grid-template-columns: repeat(2, 1fr); }
.metric:nth-child(2n) { border-right: 0; }
.metric:nth-child(-n+2) { border-bottom: 1px solid var(--border-soft); }
.chapter { grid-template-columns: 1fr; gap: 22px; }
.footer { grid-template-columns: 1fr 1fr; gap: 36px; }
.footer .kicker { font-size: 44px; }
}
@media (max-width: 640px) {
.shell { padding: 48px 24px 72px; }
.hero-copy h1 { font-size: 46px; line-height: 1.08; letter-spacing: -0.6px; }
.hero-copy .tagline { font-size: 17px; }
.metrics { grid-template-columns: 1fr; }
.metric { border-right: 0; border-bottom: 1px solid var(--border-soft); }
.metric:last-child { border-bottom: 0; }
.footer { grid-template-columns: 1fr; gap: 28px; }
.footer .kicker { font-size: 36px; }
.chapter .body p { font-size: 13.5px; }
.legal { flex-direction: column; gap: 10px; align-items: flex-start; }
}
</style>
</head>
<body>
<main class='shell'>
<!-- ============ EYEBROW ROW ============ -->
<div class='eyebrow-row' data-od-id='eyebrow-row'>
<div class='lang'>
<a href='#' class='active'>EN</a>
<a href='#'>中文</a>
<a href='#'>日本語</a>
</div>
<div class='meta'>
<span><b>Vol. 01</b> · Issue Nº 26</span>
<span><b>v0.2.0</b></span>
<span>Apache-2.0</span>
<span>MMXXVI</span>
</div>
</div>
<!-- ============ HERO ============ -->
<section class='hero' data-od-id='hero'>
<div class='hero-copy'>
<h1>Designing intelligence on warm <span class='ink'>paper</span>.</h1>
<p class='tagline'>The open-source studio for editorial documents, white papers, and one-pagers — typeset by your own coding agent.</p>
</div>
<div class='hero-tokens'>
<span class='row'><b>31</b> <span>Skills</span></span>
<span class='row'><b>72</b> <span>Systems</span></span>
<span class='row'><b>12</b> <span>Agents · BYOK</span></span>
</div>
</section>
<!-- ============ MANIFESTO ============ -->
<section class='manifesto' data-od-id='manifesto'>
<span class='label'>Manifesto · Nº 01</span>
<div>
<p class='body'>
We treat your existing coding agent as a <strong>creative collaborator</strong>, not a black box. Open Design gives it 31 composable skills and 72 brand-grade design systems, then steps out of the way. The output is a real file — not a prompt — that you can hand to a client tomorrow.
</p>
<div class='signature'>
— Open Design Studio
<span>Berlin · Open · Earth · 52.5200° N · 13.4050° E</span>
</div>
</div>
</section>
<!-- ============ METRICS ============ -->
<section class='metrics' data-od-id='metrics'>
<div class='metric'>
<div class='value'>31</div>
<div class='label'>Skills</div>
<div class='sub'>file-based, shippable today, drop-in compatible with Claude Code.</div>
</div>
<div class='metric'>
<div class='value'>72</div>
<div class='label'>Design systems</div>
<div class='sub'>portable DESIGN.md tokens — Linear, Vercel, Stripe, Apple, kami…</div>
</div>
<div class='metric'>
<div class='value'>12</div>
<div class='label'>Agent CLIs</div>
<div class='sub'>auto-detected on your $PATH; switch backends in one keystroke.</div>
</div>
<div class='metric'>
<div class='value'>3</div>
<div class='label'>Commands</div>
<div class='sub'>from <code>git clone</code> to first artifact, locally and offline.</div>
</div>
</section>
<!-- ============ CHAPTERS ============ -->
<section class='chapters' data-od-id='chapters'>
<article class='chapter' data-od-id='chapter-01'>
<div class='head'>
<p class='num'>01</p>
<h2 class='title'>What it is</h2>
<p class='lede'>A local-first design studio for the agent you already trust.</p>
</div>
<div class='body'>
<p>
Open Design is the <strong>open-source alternative to Anthropic's Claude Design</strong>. It runs on your laptop. Your agent reads a folder of <code>SKILL.md</code> files and a folder of <code>DESIGN.md</code> systems, then produces real files — landing pages, decks, white papers, one-pagers, mobile prototypes, dashboards.
</p>
<p>
Skills supply behavior. Systems supply taste. Adapters bridge agents. <strong>BYOK respects your wallet.</strong> Every output is portable HTML or Markdown — no proprietary file format, no vendor lock-in.
</p>
<ul class='dash'>
<li>Files, not opaque prompts — every skill is a folder of Markdown.</li>
<li>Deterministic visual directions, not random generation.</li>
<li>Sandboxed iframe preview, real <code>cwd</code>, exportable artifacts.</li>
</ul>
<div class='chapter-aside'>
<div class='a-label'>Three commands · 30 seconds</div>
<pre class='a-body'><span class="c"># Clone, install, launch the local daemon.</span>
git clone <span class="k">https://github.com/nexu-io/open-design</span>
pnpm install
pnpm tools-dev</pre>
</div>
</div>
</article>
<article class='chapter' data-od-id='chapter-02'>
<div class='head'>
<p class='num'>02</p>
<h2 class='title'>How it feels</h2>
<p class='lede'>Editorial discipline, not chat-window improvisation.</p>
</div>
<div class='body'>
<p>
A new project starts with a 30-second question form: brand, audience, scale, language. The agent picks one of five visual directions in OKLch, locks the type stack, and writes the artifact to disk. <strong>You can read every file it touched.</strong>
</p>
<p>
Every iteration is reviewed in a sandboxed iframe preview, with comment-mode anchors on every editable element so you can give targeted feedback instead of restating the whole brief. Re-runs are deterministic — same brief, same output.
</p>
<p>
The result is the difference between <em class='tag standard'>looks AI</em> and <em class='tag standard'>looks shipped</em>. <span class='tag brush'>One brush tag per page</span> by convention; everything else stays solid hex.
</p>
</div>
</article>
<article class='chapter' data-od-id='chapter-03'>
<div class='head'>
<p class='num'>03</p>
<h2 class='title'>What ships next</h2>
<p class='lede'>Q2 2026 — packaging, multi-tenant tokens, daemon hardening.</p>
</div>
<div class='body'>
<p>
The roadmap is public. Three threads we're pulling on right now:
</p>
<ul class='dash'>
<li><strong>Packaged desktop builds</strong> — signed Mac, Windows NSIS, Linux AppImage, all sourced from <code>tools-pack</code>.</li>
<li><strong>Multi-tenant brand tokens</strong> — one running daemon, many tenants; <code>OD_DATA_DIR</code> and <code>OD_MEDIA_CONFIG_DIR</code> already laid the groundwork.</li>
<li><strong>Daemon-side artifact persistence</strong> — every render is a file on disk under <code>.od/artifacts/</code> with a stable SHA, so design history survives every agent restart.</li>
</ul>
<p>
Nothing in this list requires a paid plan. <strong>Open Design will stay Apache-2.0 forever</strong>; the studio earns by selling brand-grade design systems, not by gating the runtime.
</p>
</div>
</article>
</section>
<!-- ============ FOOTER ============ -->
<footer class='footer' data-od-id='footer'>
<div>
<h2 class='kicker'>Open <span class='ink'>Design.</span></h2>
<p class='colophon'>
Designed and shipped on warm paper from Berlin · Open · Earth. Apache-2.0 licensed for any use, commercial or not.
</p>
</div>
<div class='col'>
<h4>Source</h4>
<ul>
<li><a href='#'>github.com/nexu-io</a><small>/open-design</small></li>
<li><a href='#'>Releases<small>v0.2.0 · MMXXVI</small></a></li>
<li><a href='#'>Issues<small>open · 23</small></a></li>
</ul>
</div>
<div class='col'>
<h4>Skills</h4>
<ul>
<li><a href='#'>kami-landing<small>this page</small></a></li>
<li><a href='#'>kami-deck<small>slide companion</small></a></li>
<li><a href='#'>open-design-landing<small>brand marketing</small></a></li>
</ul>
</div>
<div class='col'>
<h4>Studio</h4>
<ul>
<li><a href='#'>Manifesto<small>v0.2.0 · 2.4k chars</small></a></li>
<li><a href='#'>Roadmap<small>Q2 / Q3 2026</small></a></li>
<li><a href='#'>Contact<small>open an issue</small></a></li>
</ul>
</div>
</footer>
<div class='legal'>
<span><b>Open Design</b> · Apache-2.0 · MMXXVI</span>
<span>Composed in kami · 紙 · 纸 · paper-first</span>
</div>
</main>
</body>
</html>

View file

@ -16,7 +16,7 @@ od:
mode: prototype
platform: mobile
scenario: design
featured: 5
featured: 13
preview:
type: html
entry: index.html

View file

@ -1,10 +1,11 @@
# editorial-collage-deck
# open-design-landing-deck
Sister skill to [`editorial-collage`](../editorial-collage/). Produces
a single-file slide deck in the **Atelier Zero** design language —
warm-paper background, italic-serif emphasis, coral terminating dots,
surreal collage plates — with scroll-snap pagination and arrow-key
navigation.
Sister skill to [`open-design-landing`](../open-design-landing/).
Produces a single-file slide deck in the **Atelier Zero** design
language — warm-paper background, italic-serif emphasis, coral
terminating dots, surreal collage plates — paginated as a horizontal
magazine swipe deck (←/→ · wheel · touch · ESC overview), the same
nav model as [`guizang-ppt`](../guizang-ppt/).
> **Read first** — agent contract, schema, and self-check live in
> [`SKILL.md`](./SKILL.md). This README is the human quick-start.
@ -19,17 +20,19 @@ npx tsx scripts/compose.ts inputs.example.json example.html
open example.html
```
The deck assumes 16 collage assets at `../editorial-collage/assets/`
The deck assumes 16 collage assets at `../open-design-landing/assets/`
(the sister skill ships them). Use ←/→ · Space · PageUp/PageDown ·
Home/End to navigate.
Home/End to navigate, ESC for the overview grid.
## What you get
- N viewport-height slides (the worked example has 11) with
`scroll-snap-type: y mandatory` for clean pagination.
- HUD at top: brand mark · deck title · keyboard hint · live
`NN / TT` counter.
- N viewport-sized slides (the worked example has 11) laid out
horizontally on a `transform: translateX(...)` flex track.
- Per-slide chrome strip (top + bottom): brand mark · deck title ·
location · live `NN / TT` counter.
- Coral progress bar at the bottom that fills as you advance.
- Dot indicator near the bottom (click to jump).
- ESC overview grid with scaled thumbnails.
- 7 slide kinds: `cover`, `section`, `content`, `stats`, `quote`,
`cta`, `end`. Mix freely.
- Same 16-slot image library as the landing-page sister skill —
@ -38,7 +41,7 @@ Home/End to navigate.
## Files
```text
skills/editorial-collage-deck/
skills/open-design-landing-deck/
├── SKILL.md # ← agent contract (read this first)
├── README.md # ← you are here
├── schema.ts # typed slide variants + brand block (re-exports from sister)
@ -52,7 +55,7 @@ skills/editorial-collage-deck/
1. Copy `inputs.example.json` to your project as `inputs.json`.
2. Edit `brand` (or copy from a sister-skill `inputs.json` you already have).
3. Set `deck_title` (the kicker shown in the HUD).
3. Set `deck_title` (the kicker shown in the chrome strip).
4. Build the `slides` array. Each entry is one of seven kinds — see
[`schema.ts`](./schema.ts) for the full type. A typical pitch:
@ -78,21 +81,38 @@ skills/editorial-collage-deck/
The deck inherits the sister skill's 16-slot image library. Set
`inputs.imagery.assets_path` to wherever those PNGs live; the example
uses `'../editorial-collage/assets/'`.
uses `'../open-design-landing/assets/'`.
To regenerate or stub:
```bash
# Generate via gpt-image-2 (fal.ai)
FAL_KEY=fal-... npx tsx ../editorial-collage/scripts/imagegen.ts \
../editorial-collage/inputs.example.json \
--out=../editorial-collage/assets/
FAL_KEY=fal-... npx tsx ../open-design-landing/scripts/imagegen.ts \
../open-design-landing/inputs.example.json \
--out=../open-design-landing/assets/
# Or paper-textured SVG placeholders
npx tsx ../editorial-collage/scripts/placeholder.ts ../editorial-collage/assets/
npx tsx ../open-design-landing/scripts/placeholder.ts ../open-design-landing/assets/
```
## Migrating from `editorial-collage-deck`
This skill replaces the older `editorial-collage-deck` skill. The renames
are mechanical:
| Old | New |
| --- | --- |
| skill folder `editorial-collage-deck/` | `open-design-landing-deck/` |
| shared assets `../editorial-collage/assets/` | `../open-design-landing/assets/` |
| TS type `EditorialCollageDeckInputs` | `OpenDesignLandingDeckInputs` |
The `EditorialCollageDeckInputs` alias re-exported from
[`schema.ts`](./schema.ts) is a temporary bridge: it is kept for the
**v0.3.x** line and removed in the next minor release (**v0.4.0**).
Update imports before then.
## See also
- [`editorial-collage`](../editorial-collage/) — landing page sister skill.
- [`open-design-landing`](../open-design-landing/) — landing page sister skill.
- [`guizang-ppt`](../guizang-ppt/) — the magazine-deck navigation pattern this skill borrows.
- [`design-systems/atelier-zero/DESIGN.md`](../../design-systems/atelier-zero/DESIGN.md) — design tokens.

View file

@ -1,12 +1,14 @@
---
name: editorial-collage-deck
name: open-design-landing-deck
description: >
Produce a single-file slide deck in the Atelier Zero visual language
(warm-paper background, italic-serif emphasis spans, coral terminating
dots, surreal collage plates). The deck uses scroll-snap pagination,
arrow-key + space navigation, a live HUD with slide counter and
progress bar, and inherits the canonical stylesheet + 16-slot image
library from the sister `editorial-collage` skill.
dots, surreal collage plates) — Open Design's brand deck recipe.
The deck uses **horizontal magazine-style swipe pagination** (←/→,
wheel, swipe), a per-slide chrome strip with brand mark and slide
counter, an ESC overview grid, a coral progress bar, and inherits
the canonical stylesheet + 16-slot image library from the sister
`open-design-landing` skill.
triggers:
- slide deck
- 演示文稿
@ -14,12 +16,20 @@ triggers:
- keynote
- editorial slides
- atelier zero deck
- open design deck
- open design landing deck
od:
category: brand-deck
surface: web
mode: deck
scenario: marketing
featured: 2
audience: founders pitching, conference talks, internal reviews
tone: editorial, restrained, premium
scale: 6-15 viewport-locked slides
preview:
type: html
entry: index.html
craft:
requires:
- typographic-rhythm
@ -29,7 +39,7 @@ inputs:
label: Brand identity (shared across slides)
schema_path: ./schema.ts#BrandBlock
- id: deck_title
label: Kicker shown in the HUD top bar
label: Kicker shown in the per-slide top chrome
description: e.g. `'Open Design · Vol. 01 / Issue Nº 26'`.
- id: slides
label: Ordered list of typed slides
@ -39,7 +49,7 @@ inputs:
schema_path: ./schema.ts#Slide
- id: imagery
label: Image library (defaults to sister skill's assets)
schema_path: ../editorial-collage/schema.ts#ImageryConfig
schema_path: ../open-design-landing/schema.ts#ImageryConfig
parameters:
slides_recommended_count:
type: number
@ -56,34 +66,45 @@ example_prompt: |
studio. Cover with hero plate, two section dividers, two product
content slides with bullets, a stats slide showing 12 soundscapes / 4
presets / 1 daily ritual, a customer quote, a closing CTA, and an end
card. Reuse the editorial-collage image library.
card. Reuse the open-design-landing image library.
---
# editorial-collage-deck
# open-design-landing-deck
Sister skill to [`editorial-collage`](../editorial-collage/). Same
Sister skill to [`open-design-landing`](../open-design-landing/). Same
Atelier Zero visual system (warm paper, Inter Tight + Playfair Display,
italic-serif emphasis, coral dots), but paginated as a slide deck
instead of a long landing page.
italic-serif emphasis, coral dots), but paginated as a **horizontal
magazine-style swipe deck** instead of a long scrolling page.
The navigation model is intentionally borrowed from the
[`guizang-ppt`](../guizang-ppt/) skill — `←/→` arrow keys, wheel /
swipe, ESC for the overview grid — so it feels like a print magazine
flipping page by page rather than a web slide deck scrolling.
```text
inputs.json + ../editorial-collage/styles.css
inputs.json + ../open-design-landing/styles.css
└──────────► scripts/compose.ts
<out>/index.html
(one viewport per slide, scroll-snap)
(one viewport per slide, horizontal swipe)
```
## What you get
- A single self-contained HTML file with N viewport-height slides.
- **Keyboard navigation**: ←/→ · ↑/↓ · PageUp/PageDown · Space · Home/End.
- **HUD top bar**: brand mark, deck title, key hint, live slide counter.
- A single self-contained HTML file with N viewport-sized slides laid
out horizontally on one transformed flex track.
- **Keyboard navigation**: `←/→` · `↑/↓` · PageUp/PageDown · Space ·
Home/End.
- **Wheel + touch swipe** (with momentum guard so a single trackpad
flick doesn't overshoot).
- **Per-slide chrome strip**: brand mark, deck title, location,
Roman-numeral year, live slide counter (`01 / 11`).
- **Coral progress bar** at the bottom that fills as you advance.
- **Scroll-snap pagination** with `scroll-snap-stop: always` so each
slide settles cleanly.
- **Dot indicator** strip near the bottom; click any dot to jump.
- **ESC overview grid** — scaled thumbnails of every slide, click to
jump. Mirrors `guizang-ppt`'s overview UX.
- Reuses the **same 16-slot image library** as the sister skill — no
duplicate assets.
@ -122,29 +143,29 @@ A typical 11-slide pitch:
Start from [`inputs.example.json`](./inputs.example.json) (the Open
Design pitch deck). The brand block, image strategy, and assets path
mirror the sister skill — if you already filled out an
`editorial-collage` brief, copy `brand` and `imagery` over verbatim.
`open-design-landing` brief, copy `brand` and `imagery` over verbatim.
For each slide, pick a `kind` and fill the typed fields from
[`schema.ts`](./schema.ts). `MixedText` (sans-serif baseline + italic-serif
emphasis spans + coral terminating dot) is the same encoding used by
the sister skill — see its `inputs.example.json` for examples.
[`schema.ts`](./schema.ts). `MixedText` (sans-serif baseline +
italic-serif emphasis spans + coral terminating dot) is the same
encoding used by the sister skill — see its `inputs.example.json`.
### 2. (Optional) generate or stub imagery
This skill does **not** ship its own image generator or placeholder
script — it shares the 16-slot library from `editorial-collage`. To
script — it shares the 16-slot library from `open-design-landing`. To
regenerate or stub:
```bash
# generate via gpt-image-2 (fal.ai)
FAL_KEY=... npx tsx ../editorial-collage/scripts/imagegen.ts ../editorial-collage/inputs.example.json --out=../editorial-collage/assets/
FAL_KEY=... npx tsx ../open-design-landing/scripts/imagegen.ts ../open-design-landing/inputs.example.json --out=../open-design-landing/assets/
# or paper-textured SVG placeholders
npx tsx ../editorial-collage/scripts/placeholder.ts ../editorial-collage/assets/
npx tsx ../open-design-landing/scripts/placeholder.ts ../open-design-landing/assets/
```
Set your deck's `inputs.imagery.assets_path` to wherever those PNGs
live (default in the example: `../editorial-collage/assets/`).
live (default in the example: `../open-design-landing/assets/`).
### 3. Compose the deck
@ -153,40 +174,49 @@ npx tsx scripts/compose.ts inputs.json out/index.html
```
The composer reads `inputs.json`, loads the canonical Atelier Zero
stylesheet from `../editorial-collage/styles.css`, layers deck-specific
rules (scroll-snap container, slide layout grid, HUD, keyboard nav)
on top, and writes one self-contained HTML file.
stylesheet from `../open-design-landing/styles.css`, layers
deck-specific rules on top (horizontal flex track, slide layouts,
HUD, dot nav, ESC overview, keyboard / wheel / touch handlers), and
writes one self-contained HTML file.
### 4. Self-check
- [ ] Open the HTML in a fresh browser tab; slide 1 (cover) shows
with HUD `01 / N` in the corner.
- [ ] Press `→` (or Space). Smoothly advances to slide 2 with
`02 / N` in the counter and the coral progress bar filling.
with chrome strip top-right showing `01 / N`.
- [ ] Press `→` (or Space, or scroll-down). Smoothly slides one
viewport to the right; dot nav advances; the coral progress bar
ticks forward.
- [ ] Press `End`. Jumps to the final slide.
- [ ] Press `Home`. Returns to slide 1.
- [ ] `prefers-reduced-motion: reduce` (DevTools → Rendering): smooth
scroll still works, but page transitions are instant.
- [ ] Resize to 1080px and 640px. Slides stack appropriately; no
horizontal scrollbar; HUD shrinks gracefully.
- [ ] Press `Esc`. Overview grid appears with scaled thumbnails;
click any tile to jump and dismiss the overview.
- [ ] Resize to 1080px and 640px. Cover / content slides collapse to
a single column; dot nav still works; chrome strips shrink.
- [ ] `prefers-reduced-motion: reduce` (DevTools → Rendering): page
transitions stay snappy and don't induce motion sickness.
- [ ] Lighthouse: contrast AA, font-display swap, no layout shift.
## Boundaries
- **Reuse the sister skill's stylesheet.** The composer reads
`../editorial-collage/styles.css` at compile time. Do not maintain a
duplicate copy here; if Atelier Zero tokens evolve, edit them once
in the sister skill.
`../open-design-landing/styles.css` at compile time. Do not
maintain a duplicate copy here; if Atelier Zero tokens evolve, edit
them once in the sister skill.
- **Reuse the sister skill's image library.** No need to re-prompt or
re-render — the same 16 plates work for both surfaces.
- **Keep slides single-viewport.** If a slide's content does not fit
100vh at 1280×800 it will overflow and feel cramped. Trim copy or
split into two slides.
100vh × 100vw at 1280×800 it will overflow and feel cramped. Trim
copy or split into two slides.
- **Do not switch to vertical scroll-snap.** The horizontal swipe
posture is what makes this skill feel like a magazine spread; a
vertical scroller would just be a long landing page.
- **Do not add a router.** This is a single-file artifact. Multi-page
decks are out of scope; for a multi-deck experience, render each
deck separately and link from a parent index.
## See also
- [`editorial-collage`](../editorial-collage/) — landing page sister skill.
- [`open-design-landing`](../open-design-landing/) — landing page sister skill.
- [`guizang-ppt`](../guizang-ppt/) — the magazine-deck navigation
pattern this skill borrows.
- [`design-systems/atelier-zero/DESIGN.md`](../../design-systems/atelier-zero/DESIGN.md) — token spec.

View file

@ -1,6 +1,6 @@
{
"$schema": "./schema.ts",
"_doc": "Worked example — Open Design pitch deck. 11 slides covering cover, two sections, four content slides, one stats, one quote, one CTA, one end. Reuses brand identity and assets from the sister editorial-collage skill. Run `npx tsx scripts/compose.ts inputs.example.json example.html` to build.",
"_doc": "Worked example — Open Design pitch deck. 11 slides covering cover, two sections, four content slides, one stats, one quote, one CTA, one end. Reuses brand identity and assets from the sister open-design-landing skill. Run `npx tsx scripts/compose.ts inputs.example.json example.html` to build.",
"brand": {
"name": "Open Design",
@ -11,7 +11,7 @@
"description": "Open Design pitch deck — Vol. 01.",
"locale": "en",
"edition": "Vol. 01 / Issue Nº 26",
"version": "v0.2.0",
"version": "v0.3.0",
"license": "Apache-2.0",
"primary_url": "https://github.com/nexu-io/open-design",
"primary_url_label": "Star · 0K",
@ -23,7 +23,7 @@
"founded": "Est. MMXXVI",
"rails": { "right": "", "left": "" },
"languages": ["EN"],
"status": "Live · v0.2.0"
"status": "Live · v0.3.0"
},
"deck_title": "Open Design · Vol. 01 / Issue Nº 26",
@ -114,7 +114,7 @@
{ "value": "12", "label": "Agents", "sub": "auto-detected on your $PATH" },
{ "value": "3", "label": "Commands","sub": "from clone to first artifact" }
],
"caption": "Open Design v0.2.0 · Apache-2.0 · MMXXVI"
"caption": "Open Design v0.3.0 · Apache-2.0 · MMXXVI"
},
{
@ -204,7 +204,7 @@
"imagery": {
"strategy": "bring-your-own",
"assets_path": "../editorial-collage/assets/",
"assets_path": "../open-design-landing/assets/",
"provider": "fal"
}
}

View file

@ -1,17 +1,17 @@
/**
* editorial-collage-deck input schema.
* open-design-landing-deck input schema.
*
* Sister skill to `editorial-collage`. Produces a single-file slide
* deck (scroll-snap pagination + arrow-key nav) in the Atelier Zero
* visual language, reusing the same `styles.css` + the same 16-slot
* image library.
* Sister skill to `open-design-landing`. Produces a single-file slide
* deck (horizontal swipe pagination, magazine-style) in the Atelier
* Zero visual language, reusing the same `styles.css` + the same
* 16-slot image library.
*
* The schema is intentionally smaller than the landing page schema:
* a deck is an ordered array of typed slides, each driving one
* viewport-height frame. Brand identity is shared across slides.
* viewport-height/width frame. Brand identity is shared across slides.
*/
import type { MixedText, BrandBlock, ImageryConfig } from '../editorial-collage/schema';
import type { MixedText, BrandBlock, ImageryConfig } from '../open-design-landing/schema';
export type { MixedText, BrandBlock, ImageryConfig };
@ -109,7 +109,7 @@ export type Slide =
/* ---------- top-level ---------- */
export interface EditorialCollageDeckInputs {
export interface OpenDesignLandingDeckInputs {
$schema?: string;
brand: BrandBlock;
/** Deck-wide title shown in the HUD — `'Open Design · Vol. 01'`. */
@ -117,3 +117,12 @@ export interface EditorialCollageDeckInputs {
slides: Slide[];
imagery: ImageryConfig;
}
/**
* @deprecated Use `OpenDesignLandingDeckInputs`.
*
* Backwards-compat alias kept for the v0.3.x line and removed in the next
* minor (v0.4.0). Migration steps live in `README.md` under
* "Migrating from `editorial-collage-deck`".
*/
export type EditorialCollageDeckInputs = OpenDesignLandingDeckInputs;

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
# editorial-collage
# open-design-landing
Reusable skill that produces a world-class editorial landing page in
the **Atelier Zero** design language — the warm-paper, italic-serif,
@ -73,7 +73,7 @@ Every section has scroll-reveal motion (IntersectionObserver, respects
## Files
```text
skills/editorial-collage/
skills/open-design-landing/
├── SKILL.md # ← agent contract (read this first)
├── README.md # ← you are here
├── schema.ts # typed inputs (single source of truth)
@ -102,8 +102,18 @@ The `example.html` in this folder is the pre-rendered known-good demo —
useful as a visual reference and for QA against the live composer
output.
## Migrating from `editorial-collage`
This skill replaces the older `editorial-collage` folder:
- **Path:** `skills/editorial-collage/``skills/open-design-landing/`.
- **Shared assets:** downstream paths such as `../editorial-collage/assets/`
(for example from the slide-deck skill) should use
[`../open-design-landing/assets/`](./assets/) — see
[`open-design-landing-deck`](../open-design-landing-deck/README.md).
## See also
- [`design-systems/atelier-zero/DESIGN.md`](../../design-systems/atelier-zero/DESIGN.md) — colors, type, motion tokens.
- [`apps/landing-page/`](../../apps/landing-page/) — Next.js 16 deployable counterpart of this skill.
- [`skills/editorial-collage-deck/`](../editorial-collage-deck/) — sibling skill that produces a slide deck in the same visual language.
- [`apps/landing-page/`](../../apps/landing-page/) — Astro static site that mirrors this skills markup at deploy time.
- [`skills/open-design-landing-deck/`](../open-design-landing-deck/) — sibling skill that produces a slide deck in the same visual language.

View file

@ -1,12 +1,14 @@
---
name: editorial-collage
name: open-design-landing
description: >
Produce a world-class single-page editorial landing site in the
Atelier Zero visual language (Monocle / Apartamento / Études editorial
collage). The agent fills a typed `inputs.json` from a brand brief,
collage) — the same aesthetic Open Design uses for its own marketing
surface. The agent fills a typed `inputs.json` from a brand brief,
optionally generates 16 collage assets via gpt-image-2, then runs a
pure-function composer that emits a self-contained HTML file plus a
ready-to-deploy Next.js app. Drop-in scroll-reveal motion and a
pure-function composer that emits a self-contained HTML file; a
separate path can mirror the Astro marketing site in `apps/landing-page/`.
Drop-in scroll-reveal motion and a
Headroom-style sticky nav are wired automatically.
triggers:
- landing page
@ -15,9 +17,12 @@ triggers:
- magazine layout
- hero collage
- atelier zero
- open design landing
od:
category: brand-page
surface: web
scenario: marketing
featured: 1
audience: founders, design studios, OSS maintainers
tone: editorial, restrained, premium
scale: viewport-anchored long-form single page
@ -71,9 +76,9 @@ parameters:
default: standalone-html
description: >
`standalone-html` writes one self-contained .html (CSS inlined,
scripts inline, images relative). `nextjs-app` clones the
`apps/landing-page/` scaffold and wires the same content. `both`
writes both products into the output dir.
scripts inline, images relative). `nextjs-app` is the historical
enum label for cloning the Astro-based `apps/landing-page/` tree and
wiring the same content. `both` writes both products into the output dir.
image_strategy:
type: enum
values: [generate, placeholder, bring-your-own]
@ -96,7 +101,7 @@ outputs:
description: 16 collage assets, generated or placeholder per strategy.
- path: <out>/nextjs/
when: output_format in [nextjs-app, both]
description: Next.js 16 App Router scaffold mirroring apps/landing-page.
description: Astro static tree mirroring apps/landing-page (folder name is historical).
capabilities_required:
- file-write
- http-fetch # only when image_strategy=generate
@ -108,15 +113,18 @@ example_prompt: |
presets / 1 daily ritual. Use the placeholder image strategy.
---
# editorial-collage
# open-design-landing
Build a single-page editorial landing site (or a slide deck — see the
sibling [`editorial-collage-deck`](../editorial-collage-deck/) skill)
sibling [`open-design-landing-deck`](../open-design-landing-deck/) skill)
in the **Atelier Zero** design system: warm-paper background, Inter
Tight + Playfair Display, italic serif emphasis spans, dotted hairline
rules, coral terminating dots, scroll-reveal motion, and 16 surreal
collage plates.
This is the canonical Open Design marketing-page recipe — the example
output is the very page you see at [open-design](https://github.com/nexu-io/open-design).
The skill is fully **parameterized**. The agent fills one typed
`inputs.json` from the user's brief; the composer turns that JSON +
the canonical [`styles.css`](./styles.css) into a deployable artifact.
@ -238,17 +246,18 @@ self-contained HTML file. The page includes:
- Inline Headroom nav script (mirrors `header.tsx`).
- Inline GitHub star-count fetcher (auto-detects from `brand.primary_url`).
### 4. (Optional) Generate the Next.js scaffold
### 4. (Optional) Mirror the deployable Astro site
For deployable production output, **fork the `apps/landing-page/`
module**: copy it to your project root, swap the JSX in `app/page.tsx`
for content from your `inputs.json`, and copy your `<out>/assets/*.png`
into `public/assets/`. The Next.js variant supports `next build`
static `out/` export ready for any CDN.
For deployable production output, **fork the `apps/landing-page/`**
package: copy it into your workspace, align `app/page.tsx` with content
from your `inputs.json`, and copy your `<out>/assets/*.png` into the
paths expected by `app/image-assets.ts` / R2 URLs. Build with
`pnpm --filter @open-design/landing-page build` for a static `out/`
export ready for any CDN.
> A future iteration will bundle a `scripts/compose-nextjs.ts` that
> emits the entire `apps/landing-page/` tree from `inputs.json` so this
> step is one command. Until then, fork-and-edit is the supported path.
> A future iteration may bundle a composer that emits the full
> `apps/landing-page/` tree from `inputs.json` in one command. Until
> then, fork-and-edit is the supported path.
---
@ -272,7 +281,7 @@ Before marking done, the agent **must** verify:
## Files in this skill
```text
skills/editorial-collage/
skills/open-design-landing/
├── SKILL.md # this contract
├── README.md # quick-start
├── schema.ts # typed inputs (single source of truth)
@ -301,12 +310,12 @@ skills/editorial-collage/
- **Do not** wrap the composed HTML in a framework that injects its
own stylesheet ordering — Atelier Zero relies on stylesheet-order
cascade for paper texture and z-index of side rails.
- **Do not** add a separate stylesheet file for the Next.js variant;
copy `styles.css` verbatim into `app/globals.css` so visual parity
- **Do not** add a separate stylesheet file for the Astro landing-page
fork; copy `styles.css` verbatim into `app/globals.css` so visual parity
stays one-to-one.
## See also
- [`design-systems/atelier-zero/DESIGN.md`](../../design-systems/atelier-zero/DESIGN.md) — token spec.
- [`apps/landing-page/`](../../apps/landing-page/) — deployable Next.js counterpart.
- [`skills/editorial-collage-deck/`](../editorial-collage-deck/) — sibling slides skill that reuses this design system.
- [`apps/landing-page/`](../../apps/landing-page/) — deployable Astro static counterpart.
- [`skills/open-design-landing-deck/`](../open-design-landing-deck/) — sibling slides skill that reuses this design system.

View file

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View file

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View file

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View file

Before

Width:  |  Height:  |  Size: 2 MiB

After

Width:  |  Height:  |  Size: 2 MiB

View file

@ -1,6 +1,6 @@
{
"$schema": "https://open-design.dev/schemas/image-manifest.v1.json",
"skill": "editorial-collage",
"skill": "open-design-landing",
"design_system": "atelier-zero",
"default_quality": "high",
"slots": [

View file

@ -1,6 +1,6 @@
# Atelier Zero — Image Generation Prompt Pack
This pack is consumed by the `editorial-collage` skill. Every page-level
This pack is consumed by the `open-design-landing` skill. Every page-level
image is rendered with `gpt-image-fal` (preferred) or `gpt-image-azure`.
The pack has three layers:

View file

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View file

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View file

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View file

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View file

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View file

Before

Width:  |  Height:  |  Size: 983 KiB

After

Width:  |  Height:  |  Size: 983 KiB

View file

Before

Width:  |  Height:  |  Size: 994 KiB

After

Width:  |  Height:  |  Size: 994 KiB

View file

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View file

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View file

@ -11,12 +11,12 @@
<style>/*
* Atelier Zero — canonical landing-page stylesheet.
*
* This file is the SINGLE SOURCE OF TRUTH for the editorial-collage
* This file is the SINGLE SOURCE OF TRUTH for the open-design-landing
* skill's visual system. It is consumed by:
*
* 1. `scripts/compose.ts` — inlined into the standalone HTML output.
* 2. `apps/landing-page/app/globals.css` — copied verbatim for the
* Next.js deployable counterpart.
* Astro static deployable counterpart.
* 3. `example.html` — the pre-rendered known-good demo.
*
* If you change tokens, layout, motion, or component styles, edit them
@ -64,19 +64,13 @@ body {
position: relative;
}
/*
* Paper texture overlay across the whole page. Rendered above .shell so
* the multiply blend applies uniformly to every section — including the
* opaque .topbar and .nav — instead of only to transparent sections
* (hero, about, …). Equal z-index to .side-rail; the rails appear later
* in document order and therefore stay on top of the texture.
*/
/* paper texture overlay across the whole page */
body::before {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
z-index: 3;
z-index: 1;
background-image:
radial-gradient(circle at 12% 18%, rgba(106, 92, 56, 0.07) 0, transparent 28%),
radial-gradient(circle at 88% 72%, rgba(106, 92, 56, 0.06) 0, transparent 32%),
@ -291,6 +285,12 @@ body::before {
font-size: 13px;
font-weight: 500;
text-decoration: none;
white-space: nowrap;
flex-shrink: 0;
}
.nav-cta [data-github-stars],
.nav-cta [data-github-version] {
font-variant-numeric: tabular-nums;
}
.nav-cta::after {
content: '★';
@ -429,9 +429,12 @@ body::before {
padding: 0;
min-height: calc(100vh - 140px);
display: flex;
flex-direction: column;
align-items: stretch;
border-bottom: 1px solid var(--line);
}
.hero > .container { flex: 0 0 auto; }
.hero > .container.hero-grid { flex: 1 1 auto; }
.hero::before {
content: '';
position: absolute;
@ -1812,6 +1815,14 @@ footer {
@media (max-width: 1200px) {
.topbar-inner .mid { display: none; }
}
/* nav: between 1080 and 1180 the brand tail + 5 nav links + 2 CTAs + dot
* crowd the row. Drop the brand sub-meta first, then tighten link spacing,
* so the Star CTA never has to compress. */
@media (max-width: 1180px) {
.nav-inner { gap: 18px; }
.brand-meta { display: none; }
.nav-links { gap: 28px; }
}
@media (max-width: 1080px) {
.container { padding: 0 32px; }
.hero h1 { font-size: clamp(36px, 4.6vw, 54px); }
@ -1884,7 +1895,7 @@ footer {
<span>Apache-2.0 · Made on Earth</span>
</span>
<span class='right'>
<a class='topbar-link' href='https://github.com/nexu-io/open-design/releases' target='_blank' rel='noreferrer noopener'><span class='pulse'></span>Live · v0.2.0</a>
<a class='topbar-link' href='https://github.com/nexu-io/open-design/releases' target='_blank' rel='noreferrer noopener'><span class='pulse'></span>Live · v0.3.0</a>
<span><b>EN</b> · DE · 中文 · 日本語</span>
</span>
</div>
@ -2403,6 +2414,15 @@ footer {
</div>
<span>guizang-ppt</span>
<small>Decks</small>
</a>
<a class='partner' data-reveal href='https://github.com/multica-ai/multica' target='_blank' rel='noreferrer noopener'>
<div class='glyph'>
<svg viewBox='0 0 80 30' fill='none' stroke='currentColor' stroke-width='2'>
<rect x='6' y='6' width='4' height='18'/><rect x='14' y='6' width='4' height='18'/><rect x='22' y='6' width='4' height='18'/><rect x='30' y='6' width='4' height='18'/>
</svg>
</div>
<span>multica-ai</span>
<small>Daemon</small>
</a>
<a class='partner' data-reveal href='https://github.com/OpenCoworkAI/open-codesign' target='_blank' rel='noreferrer noopener'>
<div class='glyph'>
@ -2469,7 +2489,7 @@ footer {
</div>
<div class='cta-foot'>
<span class='stamp'>● Live</span>
<span>v0.2.0 / Apache-2.0</span>
<span>v0.3.0 / Apache-2.0</span>
<span style='margin-left:auto;'>52.5200° N · 13.4050° E</span>
</div>
</div>
@ -2491,7 +2511,7 @@ footer {
<span>Open Design</span>
</a>
<p style='margin-top:18px;'>The open-source alternative to Claude Design. Built on the shoulders of <a class='inline-link' href='https://github.com/alchaincyf/huashu-design' target='_blank' rel='noreferrer noopener'>huashu-design</a>, <a class='inline-link' href='https://github.com/op7418/guizang-ppt-skill' target='_blank' rel='noreferrer noopener'>guizang-ppt</a>, <a class='inline-link' href='https://github.com/multica-ai/multica' target='_blank' rel='noreferrer noopener'>multica-ai</a>, and <a class='inline-link' href='https://github.com/OpenCoworkAI/open-codesign' target='_blank' rel='noreferrer noopener'>open-codesign</a>.</p>
<a class='foot-cta' href='https://github.com/nexu-io/open-design/releases' target='_blank' rel='noreferrer noopener'>Download desktop<span class='meta'>macOS · v0.2.0</span></a>
<a class='foot-cta' href='https://github.com/nexu-io/open-design/releases' target='_blank' rel='noreferrer noopener'>Download desktop<span class='meta'>macOS · v0.3.0</span></a>
</div>
<div class='foot-col'>
<h5>Studio</h5>

View file

@ -11,7 +11,7 @@
"description": "Open Design is the open-source alternative to Claude Design. 12 coding-agent CLIs · 31 composable skills · 72 brand-grade design systems. Local-first, web-deployable, BYOK at every layer.",
"locale": "en",
"edition": "Vol. 01 / Issue Nº 26",
"version": "v0.2.0",
"version": "v0.3.0",
"license": "Apache-2.0",
"primary_url": "https://github.com/nexu-io/open-design",
"primary_url_label": "Star · 0K",
@ -28,7 +28,7 @@
"left": "Skills · Systems · Agents · BYOK · Local-first"
},
"languages": ["EN", "DE", "中文", "日本語"],
"status": "Live · v0.2.0"
"status": "Live · v0.3.0"
},
"nav": [
@ -322,7 +322,7 @@
"brand_cta": {
"label": "Download desktop",
"href": "https://github.com/nexu-io/open-design/releases",
"meta": "macOS · v0.2.0"
"meta": "macOS · v0.3.0"
},
"columns": [
{ "title": "Studio", "links": [

View file

@ -1,5 +1,5 @@
/**
* editorial-collage input schema.
* open-design-landing input schema.
*
* This is the contract between users and `scripts/compose.ts`. A valid
* `inputs.json` matching `EditorialCollageInputs` is enough to produce
@ -367,7 +367,7 @@ export interface FooterBlock {
/**
* Optional CTA rendered under the brand description in the footer
* (e.g. `{ label: 'Download desktop', href: 'https://.../releases',
* meta: 'macOS · v0.2.0' }`). When `brand.download_url` is set this is
* meta: 'macOS · v0.3.0' }`). When `brand.download_url` is set this is
* filled in automatically; explicit values take precedence.
*/
brand_cta?: { label: string; href: string; meta?: string };

View file

@ -1,6 +1,6 @@
#!/usr/bin/env -S npx -y tsx
/**
* editorial-collage HTML composer.
* open-design-landing HTML composer.
*
* Reads `inputs.json` (matching `../schema.ts`) and writes a single
* self-contained HTML file with the Atelier Zero stylesheet inlined,

View file

@ -1,6 +1,6 @@
#!/usr/bin/env -S npx -y tsx
/**
* editorial-collage gpt-image-2 generator (fal.ai backend).
* open-design-landing gpt-image-2 generator (fal.ai backend).
*
* Generates the 16 collage assets defined in `assets/image-manifest.json`
* by composing per-slot prompts (style anchor + brand variables +

View file

@ -1,6 +1,6 @@
#!/usr/bin/env -S npx -y tsx
/**
* editorial-collage SVG framework placeholder generator.
* open-design-landing SVG framework placeholder generator.
*
* When `imagery.strategy === 'placeholder'`, this script writes one
* paper-textured SVG file per slot in `assets/image-manifest.json`.

View file

@ -1,12 +1,12 @@
/*
* Atelier Zero canonical landing-page stylesheet.
*
* This file is the SINGLE SOURCE OF TRUTH for the editorial-collage
* This file is the SINGLE SOURCE OF TRUTH for the open-design-landing
* skill's visual system. It is consumed by:
*
* 1. `scripts/compose.ts` inlined into the standalone HTML output.
* 2. `apps/landing-page/app/globals.css` copied verbatim for the
* Next.js deployable counterpart.
* Astro static deployable counterpart.
* 3. `example.html` the pre-rendered known-good demo.
*
* If you change tokens, layout, motion, or component styles, edit them
@ -275,6 +275,12 @@ body::before {
font-size: 13px;
font-weight: 500;
text-decoration: none;
white-space: nowrap;
flex-shrink: 0;
}
.nav-cta [data-github-stars],
.nav-cta [data-github-version] {
font-variant-numeric: tabular-nums;
}
.nav-cta::after {
content: '★';
@ -413,9 +419,12 @@ body::before {
padding: 0;
min-height: calc(100vh - 140px);
display: flex;
flex-direction: column;
align-items: stretch;
border-bottom: 1px solid var(--line);
}
.hero > .container { flex: 0 0 auto; }
.hero > .container.hero-grid { flex: 1 1 auto; }
.hero::before {
content: '';
position: absolute;
@ -1796,6 +1805,14 @@ footer {
@media (max-width: 1200px) {
.topbar-inner .mid { display: none; }
}
/* nav: between 1080 and 1180 the brand tail + 5 nav links + 2 CTAs + dot
* crowd the row. Drop the brand sub-meta first, then tighten link spacing,
* so the Star CTA never has to compress. */
@media (max-width: 1180px) {
.nav-inner { gap: 18px; }
.brand-meta { display: none; }
.nav-links { gap: 28px; }
}
@media (max-width: 1080px) {
.container { padding: 0 32px; }
.hero h1 { font-size: clamp(36px, 4.6vw, 54px); }