first-commit
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
refresh-contributors-wall / Refresh contributors wall cache bust (push) Waiting to run
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
refresh-contributors-wall / Refresh contributors wall cache bust (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
# apps/landing-page/AGENTS.md
|
||||
|
||||
Follow the root `AGENTS.md` and `apps/AGENTS.md` first. This file only
|
||||
records module-level boundaries for `apps/landing-page/`.
|
||||
|
||||
## Purpose
|
||||
|
||||
`apps/landing-page` is a stand-alone static Astro site that renders
|
||||
the canonical Open Design marketing page in the **Atelier Zero** style.
|
||||
It is the deployable counterpart to:
|
||||
|
||||
- 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/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/`.
|
||||
|
||||
## What it is
|
||||
|
||||
- Astro static output. The route lives at `app/pages/index.astro` and
|
||||
uses React only at build time (`renderToStaticMarkup`) for the existing
|
||||
`app/page.tsx` component. The generated page is CDN-ready HTML/CSS plus
|
||||
a small inline enhancement script; no React runtime ships to browsers.
|
||||
- `astro.config.ts` always uses `output: 'static'` and emits to `out/`
|
||||
so it can be served by any CDN (Vercel, Cloudflare Pages, the daemon's
|
||||
static fallback) without a Node runtime.
|
||||
- All styles live in `app/globals.css`. Class names match the Atelier
|
||||
Zero CSS in the canonical example so visual parity is one-to-one.
|
||||
- All page imagery is referenced through `app/image-assets.ts`, which builds
|
||||
Cloudflare Image Resizing URLs for the R2 originals.
|
||||
|
||||
## What it is NOT
|
||||
|
||||
- Not part of `apps/web`. The web app is the product surface; the
|
||||
landing page is a marketing surface. They share design tokens but
|
||||
not state, routes, or runtime.
|
||||
- Not connected to `apps/daemon`. There is no `/api`, no `/artifacts`,
|
||||
no `/frames` — no proxy to set up.
|
||||
- Not multi-page. There is exactly one route (`/`) that renders the
|
||||
full landing page. If you need a second page, add it as a sibling
|
||||
Astro page route.
|
||||
|
||||
## Boundary constraints
|
||||
|
||||
- Must remain a static Astro output.
|
||||
- Must not import from `@open-design/web`, `@open-design/daemon`,
|
||||
`@open-design/desktop`, `@open-design/sidecar*`, or
|
||||
`@open-design/contracts`. Those are product runtime concerns.
|
||||
- Must not introduce a `src/` shell — keep all source under
|
||||
`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/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.
|
||||
|
||||
## Common commands
|
||||
|
||||
```bash
|
||||
pnpm --filter @open-design/landing-page dev # http://127.0.0.1:17574
|
||||
pnpm --filter @open-design/landing-page build # static export → out/
|
||||
pnpm --filter @open-design/landing-page typecheck
|
||||
```
|
||||
|
||||
## When to update this app
|
||||
|
||||
- New section added to the canonical landing page → port it here.
|
||||
- Asset regeneration in the skill → re-mirror PNGs into
|
||||
`public/assets/`.
|
||||
- Brand re-keying for a non-Open-Design tenant → fork the app, update
|
||||
copy, swap PNGs. Do not parameterize this app for multi-tenancy.
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Sticky Header — static markup rendered at build time. Headroom-style
|
||||
* hide/show and the live GitHub star count are attached by the tiny inline
|
||||
* script in `app/pages/index.astro`, so this marketing page ships no React
|
||||
* runtime to the browser.
|
||||
*/
|
||||
|
||||
const REPO = 'https://github.com/nexu-io/open-design';
|
||||
const REPO_RELEASES = `${REPO}/releases`;
|
||||
const REPO_SKILLS = `${REPO}/tree/main/skills`;
|
||||
const REPO_DESIGN_SYSTEMS = `${REPO}/tree/main/design-systems`;
|
||||
|
||||
const ext = {
|
||||
target: '_blank',
|
||||
rel: 'noreferrer noopener',
|
||||
} as const;
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className='nav' data-od-id='nav' data-nav-headroom>
|
||||
<div className='container nav-inner'>
|
||||
<a href='#top' className='brand'>
|
||||
<span className='brand-mark'>Ø</span>
|
||||
<span>Open Design</span>
|
||||
<span className='brand-meta'>
|
||||
<b>Studio Nº 01</b>Berlin / Open / Earth
|
||||
</span>
|
||||
</a>
|
||||
<nav>
|
||||
<ul className='nav-links'>
|
||||
<li>
|
||||
<a href={REPO_SKILLS} {...ext}>
|
||||
Skills<span className='num'>31</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={REPO_DESIGN_SYSTEMS} {...ext}>
|
||||
Systems<span className='num'>72</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='#agents'>
|
||||
Agents<span className='num'>12</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='#labs'>
|
||||
Labs<span className='num'>05</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='#contact'>Contact</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className='nav-side'>
|
||||
<a
|
||||
className='nav-cta ghost'
|
||||
href={REPO_RELEASES}
|
||||
aria-label='Download Open Design desktop'
|
||||
title='Download the desktop app'
|
||||
{...ext}
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
<a
|
||||
className='nav-cta'
|
||||
href={REPO}
|
||||
aria-label='Star Open Design on GitHub'
|
||||
title='Click to star us on GitHub'
|
||||
{...ext}
|
||||
>
|
||||
Star · <span data-github-stars>0</span>
|
||||
</a>
|
||||
<span className='status-dot' aria-hidden='true' />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Global wire — the slim editorial ticker between the hero and About.
|
||||
*
|
||||
* The cities row (top) is decorative and stays static. The contributors
|
||||
* row (bottom, reverse direction) renders a static fallback at build time;
|
||||
* `app/pages/index.astro` enhances it with a tiny inline GitHub fetch so
|
||||
* the browser never downloads React.
|
||||
*
|
||||
* GET https://api.github.com/repos/nexu-io/open-design/contributors
|
||||
*
|
||||
* Each entry becomes a `<a class='wire-item is-link'>` linking straight
|
||||
* to the contributor's GitHub profile. We:
|
||||
*
|
||||
* - filter out bot accounts (`type === 'Bot'` or `*[bot]` logins),
|
||||
* - keep the top N by contribution count,
|
||||
* - apply named editorial roles to known handles (kami, guizang…)
|
||||
* and fall back to "<count> commits" for everyone else,
|
||||
* - always append a trailing "@you · be next" link to the
|
||||
* contributors graph so the editorial CTA stays intact.
|
||||
*
|
||||
* If the fetch is blocked (offline, rate limited, network failure), the
|
||||
* fallback list stays visible — the section never goes empty.
|
||||
*/
|
||||
|
||||
const REPO = 'https://github.com/nexu-io/open-design';
|
||||
const REPO_CONTRIBUTORS_PAGE = `${REPO}/graphs/contributors`;
|
||||
|
||||
const ext = {
|
||||
target: '_blank',
|
||||
rel: 'noreferrer noopener',
|
||||
} as const;
|
||||
|
||||
const TRAILING_CTA: Contributor = {
|
||||
handle: 'you',
|
||||
role: 'be next',
|
||||
href: REPO_CONTRIBUTORS_PAGE,
|
||||
};
|
||||
|
||||
type Contributor = {
|
||||
handle: string;
|
||||
role: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
// 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/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' },
|
||||
{ handle: 'op7418', role: 'guizang', href: 'https://github.com/op7418' },
|
||||
{
|
||||
handle: 'alchaincyf',
|
||||
role: 'huashu',
|
||||
href: 'https://github.com/alchaincyf',
|
||||
},
|
||||
{
|
||||
handle: 'multica-ai',
|
||||
role: 'daemon',
|
||||
href: 'https://github.com/multica-ai',
|
||||
},
|
||||
{
|
||||
handle: 'OpenCoworkAI',
|
||||
role: 'codesign',
|
||||
href: 'https://github.com/OpenCoworkAI',
|
||||
},
|
||||
{ handle: 'nexu-io', role: 'studio', href: 'https://github.com/nexu-io' },
|
||||
TRAILING_CTA,
|
||||
];
|
||||
|
||||
type City = { name: string; coord: string };
|
||||
|
||||
export function Wire({ cities }: { cities: ReadonlyArray<City> }) {
|
||||
// Doubled tracks are required for the seamless `translateX(-50%)`
|
||||
// marquee loop defined in globals.css.
|
||||
const cityTrack = [...cities, ...cities];
|
||||
const contribTrack = [...FALLBACK, ...FALLBACK];
|
||||
|
||||
return (
|
||||
<section
|
||||
className='wire'
|
||||
data-od-id='wire'
|
||||
aria-label='Global wire — cities and contributors'
|
||||
>
|
||||
<div className='container wire-inner'>
|
||||
<div className='wire-left'>
|
||||
<span className='wire-mark' aria-hidden='true'>
|
||||
<span className='wire-pulse' />
|
||||
</span>
|
||||
<span className='wire-title'>
|
||||
<b>From the field</b>
|
||||
<span>
|
||||
Open · {cities.length} cities ·{' '}
|
||||
<span data-wire-contributors-count>{FALLBACK.length - 1}</span>{' '}
|
||||
contributors
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className='wire-rows'>
|
||||
<div className='wire-row'>
|
||||
<div className='marquee-track' aria-hidden='true'>
|
||||
{cityTrack.map((c, i) => (
|
||||
<span className='wire-item' key={`city-${i}`}>
|
||||
<span className='wire-dot'>·</span>
|
||||
<span className='wire-coord'>{c.coord}</span>
|
||||
<span className='wire-name'>{c.name}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='wire-row reverse'>
|
||||
<div className='marquee-track' data-wire-contributors-track>
|
||||
{contribTrack.map((c, i) => (
|
||||
<a
|
||||
className='wire-item is-link'
|
||||
key={`contrib-${i}-${c.handle}`}
|
||||
href={c.href}
|
||||
aria-label={`Open ${c.handle} on GitHub`}
|
||||
{...ext}
|
||||
>
|
||||
<span className='wire-dot'>·</span>
|
||||
<span className='wire-handle'>@{c.handle}</span>
|
||||
<span className='wire-role'>{c.role}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
const R2_PUBLIC_ORIGIN = 'https://static.open-design.ai';
|
||||
const IMAGE_RESIZING_ORIGIN = R2_PUBLIC_ORIGIN;
|
||||
const ASSET_PREFIX = 'landing/assets';
|
||||
|
||||
type ImageOptions = {
|
||||
width: number;
|
||||
quality?: number;
|
||||
};
|
||||
|
||||
export function r2Asset(name: string): string {
|
||||
return `${R2_PUBLIC_ORIGIN}/${ASSET_PREFIX}/${name}`;
|
||||
}
|
||||
|
||||
export function imageAsset(name: string, { width, quality = 85 }: ImageOptions): string {
|
||||
const options = `width=${width},quality=${quality},format=auto`;
|
||||
return `${IMAGE_RESIZING_ORIGIN}/cdn-cgi/image/${options}/${r2Asset(name)}`;
|
||||
}
|
||||
|
||||
export const heroImage = imageAsset('hero.png', { width: 1024, quality: 82 });
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,223 @@
|
||||
---
|
||||
import Page from '../page';
|
||||
import '../globals.css';
|
||||
import { createElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { heroImage } from '../image-assets';
|
||||
|
||||
const title = 'Open Design — Design with the agent already on your laptop.';
|
||||
const description =
|
||||
'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));
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#efe7d2" />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="canonical" href={canonical} />
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Open Design" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:image" content={heroImage} />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={heroImage} />
|
||||
</head>
|
||||
<body>
|
||||
<Fragment set:html={pageHtml} />
|
||||
<script is:inline>
|
||||
(() => {
|
||||
const formatStars = (count) => {
|
||||
if (!Number.isFinite(count) || count <= 0) return '0';
|
||||
if (count < 1000) return String(count);
|
||||
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) {
|
||||
let lastY = window.scrollY;
|
||||
const showTopThreshold = 100;
|
||||
const scrollDelta = 6;
|
||||
window.addEventListener(
|
||||
'scroll',
|
||||
() => {
|
||||
const y = window.scrollY;
|
||||
const delta = y - lastY;
|
||||
if (y <= showTopThreshold) nav.classList.remove('is-hidden');
|
||||
else if (delta > scrollDelta) nav.classList.add('is-hidden');
|
||||
else if (delta < -scrollDelta) nav.classList.remove('is-hidden');
|
||||
lastY = y;
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
}
|
||||
|
||||
const stars = document.querySelector('[data-github-stars]');
|
||||
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) => {
|
||||
const label = formatVersion(data);
|
||||
if (!label) return;
|
||||
for (const slot of versionSlots) slot.textContent = label;
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const enhanceWire = () => {
|
||||
const track = document.querySelector('[data-wire-contributors-track]');
|
||||
const count = document.querySelector('[data-wire-contributors-count]');
|
||||
if (!track) return;
|
||||
|
||||
const roleOverrides = {
|
||||
tw93: 'kami',
|
||||
op7418: 'guizang',
|
||||
alchaincyf: 'huashu',
|
||||
OpenCoworkAI: 'codesign',
|
||||
'nexu-io': 'studio',
|
||||
lewislulu: 'html-ppt',
|
||||
};
|
||||
const roleFor = (login, contributions) =>
|
||||
roleOverrides[login] ?? `${contributions} ${contributions === 1 ? 'commit' : 'commits'}`;
|
||||
const isContributor = (value) =>
|
||||
value &&
|
||||
typeof value.login === 'string' &&
|
||||
typeof value.html_url === 'string' &&
|
||||
typeof value.type === 'string' &&
|
||||
typeof value.contributions === 'number';
|
||||
const renderContributor = (contributor, index) => {
|
||||
const link = document.createElement('a');
|
||||
link.className = 'wire-item is-link';
|
||||
link.href = contributor.href;
|
||||
link.target = '_blank';
|
||||
link.rel = 'noreferrer noopener';
|
||||
link.setAttribute('aria-label', `Open ${contributor.handle} on GitHub`);
|
||||
link.dataset.liveWireItem = String(index);
|
||||
|
||||
const dot = document.createElement('span');
|
||||
dot.className = 'wire-dot';
|
||||
dot.textContent = '·';
|
||||
const handle = document.createElement('span');
|
||||
handle.className = 'wire-handle';
|
||||
handle.textContent = `@${contributor.handle}`;
|
||||
const role = document.createElement('span');
|
||||
role.className = 'wire-role';
|
||||
role.textContent = contributor.role;
|
||||
|
||||
link.append(dot, handle, role);
|
||||
return link;
|
||||
};
|
||||
|
||||
fetch('https://api.github.com/repos/nexu-io/open-design/contributors?per_page=12', {
|
||||
headers: { Accept: 'application/vnd.github+json' },
|
||||
})
|
||||
.then((r) => (r.ok ? r.json() : Promise.reject(new Error('http error'))))
|
||||
.then((data) => {
|
||||
if (!Array.isArray(data)) return;
|
||||
const live = data
|
||||
.filter(isContributor)
|
||||
.filter((c) => c.type !== 'Bot' && !c.login.endsWith('[bot]'))
|
||||
.slice(0, 12)
|
||||
.map((c) => ({
|
||||
handle: c.login,
|
||||
role: roleFor(c.login, c.contributions),
|
||||
href: c.html_url,
|
||||
}));
|
||||
if (live.length === 0) return;
|
||||
live.push({
|
||||
handle: 'you',
|
||||
role: 'be next',
|
||||
href: 'https://github.com/nexu-io/open-design/graphs/contributors',
|
||||
});
|
||||
if (count) count.textContent = String(Math.max(0, live.length - 1));
|
||||
track.replaceChildren(
|
||||
...[...live, ...live].map((contributor, index) => renderContributor(contributor, index)),
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const elements = document.querySelectorAll('[data-reveal]:not([data-revealed])');
|
||||
enhanceHeader();
|
||||
enhanceWire();
|
||||
if (elements.length === 0) return;
|
||||
|
||||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (reduceMotion || !('IntersectionObserver' in window)) {
|
||||
for (const el of elements) el.dataset.revealed = 'true';
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting) continue;
|
||||
entry.target.dataset.revealed = 'true';
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.12, rootMargin: '0px 0px -8% 0px' },
|
||||
);
|
||||
|
||||
for (const el of elements) observer.observe(el);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,13 @@
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
const site = process.env.OD_LANDING_SITE ?? 'https://open-design.dev';
|
||||
|
||||
export default defineConfig({
|
||||
output: 'static',
|
||||
site,
|
||||
srcDir: './app',
|
||||
outDir: './out',
|
||||
trailingSlash: 'always',
|
||||
integrations: [sitemap()],
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@open-design/landing-page",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "astro dev --host 127.0.0.1 --port 17574",
|
||||
"build": "astro check && astro build",
|
||||
"preview": "astro preview --host 127.0.0.1 --port 17574",
|
||||
"typecheck": "astro check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "^3.6.0",
|
||||
"astro": "^5.15.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@types/node": "^20.17.10",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "~24"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"incremental": true,
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowJs": true
|
||||
},
|
||||
"include": [
|
||||
".astro/types.d.ts",
|
||||
"astro.config.ts",
|
||||
"app/**/*",
|
||||
"out/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"out"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user