a46764fb1b
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
224 lines
9.4 KiB
Plaintext
224 lines
9.4 KiB
Plaintext
---
|
|
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>
|