-
Notifications
You must be signed in to change notification settings - Fork 10.7k
Expand file tree
/
Copy pathhome-enhancer.astro
More file actions
324 lines (305 loc) · 13.2 KB
/
Copy pathhome-enhancer.astro
File metadata and controls
324 lines (305 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
---
/**
* Client-side enhancers for the Atelier Zero landing page.
*
* Why a shared component: the homepage is rendered from two entry points
* (`app/pages/index.astro` for the canonical `/`, and
* `app/pages/[locale]/index.astro` for every prefixed locale like
* `/zh-CN/`). Both need the *exact same* enhancement behavior:
*
* - Headroom-style sticky header (hide on scroll down, reveal on up)
* - Live GitHub stars + latest release version injection
* - Live Wire contributors row (replaces the static fallback)
* - IntersectionObserver-driven `data-reveal` choreography, progressively
* enhanced so content stays visible when JavaScript is unavailable
*
* Inlining this script in only one of the entry points causes the
* "blank landing page" bug on localized homepages, so we centralize it
* here. Keep the inline tag (`is:inline`) — Astro otherwise tries to
* bundle the script and our hand-crafted DOM-only helpers don't need
* the extra weight.
*
* SAFETY: This file owns the contract for the `data-github-version`,
* `data-github-stars`, `data-wire-contributors-*`, and `data-reveal`
* attributes. If you rename any of them in `page.tsx` / `wire.tsx`,
* update the selectors below in lockstep.
*/
---
<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. 'OpenDesign 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: 'OpenDesign 0.3.0', tag_name: 'v0.3.0' } → 'v0.3.0'
// { name: 'OpenDesign 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) {
// Wrap the scroll listener in requestAnimationFrame so a burst of
// scroll events (trackpads fire >60Hz) collapses to one DOM
// mutation per frame. PSI attributed ~700ms of "forced reflow" to
// the un-throttled version on the previous build.
let lastY = window.scrollY;
let ticking = false;
const showTopThreshold = 100;
const scrollDelta = 6;
window.addEventListener(
'scroll',
() => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
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;
ticking = false;
});
},
{ 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(() => {});
};
// Platform-aware download buttons. Detects OS and — on macOS — the chip
// family (Apple Silicon vs Intel), resolves the matching asset from the
// latest GitHub release, and rewrites every participating CTA. Only marked
// chip targets display the detected platform label. Falls back to each
// server-rendered /download/ href when detection or the API fails.
const enhanceDownloadCta = () => {
// Page-only CTAs can opt out with `[data-download-page]`; the shared nav
// CTA is now a direct download and intentionally participates here.
const buttons = Array.from(
document.querySelectorAll('[data-download-cta]:not([data-download-page])'),
);
if (buttons.length === 0) return;
const ua = navigator.userAgent || '';
const platform = (
navigator.userAgentData?.platform ||
navigator.platform ||
''
).toLowerCase();
const isIpadOS = navigator.maxTouchPoints > 1 && /mac/.test(platform);
if (isIpadOS || /iPhone|iPad|iPod/i.test(ua)) return;
const isWin = /Windows|Win32|Win64|WOW64/i.test(ua);
const isMac = /Macintosh|Mac OS X/i.test(ua) && !/iPhone|iPad|iPod/i.test(ua);
const labelChip = (text) => {
for (const btn of buttons) {
if (!btn.hasAttribute('data-download-chip-target')) continue;
let chip = btn.querySelector('[data-download-chip]');
if (!chip) {
chip = document.createElement('span');
chip.setAttribute('data-download-chip', '');
chip.style.opacity = '0.7';
chip.style.fontWeight = '400';
const arrow = btn.querySelector('.arrow');
if (arrow) btn.insertBefore(chip, arrow);
else btn.appendChild(chip);
}
chip.textContent = ' · ' + text;
}
};
const resolveAsset = (chipLabel, matcher) => {
fetch('https://api.github.com/repos/nexu-io/open-design/releases/latest', {
headers: { Accept: 'application/vnd.github+json' },
})
.then((r) => (r.ok ? r.json() : null))
.then((rel) => {
if (!rel || !Array.isArray(rel.assets)) return;
const asset = rel.assets.find((a) => matcher(a.name || ''));
if (asset && asset.browser_download_url) {
for (const btn of buttons) {
btn.href = asset.browser_download_url;
btn.setAttribute('download', '');
}
labelChip(chipLabel);
}
})
.catch(() => {});
};
const detectMacArch = async () => {
// Chromium exposes the real architecture; Safari does not, so fall back
// to the WebGL renderer string (Apple Silicon reports an "Apple" GPU).
try {
const uad = navigator.userAgentData;
if (uad && typeof uad.getHighEntropyValues === 'function') {
const hv = await uad.getHighEntropyValues(['architecture']);
if (hv && hv.architecture) return hv.architecture === 'arm' ? 'arm64' : 'x64';
}
} catch (e) {}
try {
const cvs = document.createElement('canvas');
const gl = cvs.getContext('webgl') || cvs.getContext('experimental-webgl');
if (gl) {
const dbg = gl.getExtension('WEBGL_debug_renderer_info');
const renderer = dbg ? String(gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL)) : '';
if (/apple/i.test(renderer) && !/intel|amd|radeon|nvidia/i.test(renderer)) return 'arm64';
if (/intel|amd|radeon|nvidia/i.test(renderer)) return 'x64';
}
} catch (e) {}
return null;
};
if (isWin) {
resolveAsset('Windows', (n) => /win-x64-setup\.exe$/i.test(n));
} else if (isMac) {
detectMacArch().then((arch) => {
if (arch === 'arm64') resolveAsset('Apple Silicon', (n) => /mac-arm64\.dmg$/i.test(n));
else if (arch === 'x64') resolveAsset('Intel', (n) => /mac-x64\.dmg$/i.test(n));
// undetermined → keep the releases-page fallback
});
}
// Linux / other → keep the releases-page fallback
};
const elements = document.querySelectorAll('[data-reveal]:not([data-revealed])');
enhanceHeader();
enhanceWire();
enhanceDownloadCta();
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;
}
let observer;
try {
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);
document.documentElement.classList.add('reveal-ready');
} catch (error) {
observer?.disconnect();
document.documentElement.classList.remove('reveal-ready');
console.error('Reveal animation initialization failed', error);
}
})();
</script>