Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

demo.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

demo.tsxBlame939 lines · 1 contributor
52ad8b1Claude1/**
2 * Block L3 — public `/demo` landing page + companion JSON endpoints.
3 *
4 * Anonymous-friendly. Demonstrates Gluecron's AI features in real time by
5 * surfacing live counts off the audit log + `pr_comments` table for the
6 * seeded demo repos.
7 *
8 * Routes mounted from `src/app.tsx`. Must come BEFORE `routes/admin.tsx`
9 * in mount order so this handler wins over the legacy `/demo` redirect.
10 *
11 * GET /demo → SSR landing page (graceful no-JS,
12 * polls JSON endpoints every 30s
13 * when JS is available)
14 * GET /api/v2/demo/activity → combined activity feed JSON
15 * GET /api/v2/demo/queued → queued ai:build issues JSON
16 * GET /api/v2/demo/merges → recent auto-merges JSON
17 * GET /api/v2/demo/reviews → recent AI reviews JSON
18 *
19 * All JSON endpoints serve `Cache-Control: public, max-age=30` so the demo
20 * page is cheap to refresh and external dashboards can embed safely.
e3eb5ffClaude21 *
22 * 2026 polish: scoped `.demo-page-` CSS, four-step interactive walkthrough
23 * (mirrors /connect/claude), eyebrow + display headline + 1-line subtitle,
24 * 'Try this' prompts on each step, and a reset button on the live feed.
52ad8b1Claude25 */
26
27import { Hono } from "hono";
28import { Layout } from "../views/layout";
29import { softAuth } from "../middleware/auth";
30import type { AuthEnv } from "../middleware/auth";
31import { DEMO_USERNAME } from "../lib/demo-seed";
32import {
33 countAiReviewsSince,
34 listDemoActivityFeed,
35 listQueuedAiBuildIssues,
36 listRecentAiReviews,
37 listRecentAutoMerges,
38 type DemoActivityEntry,
39} from "../lib/demo-activity";
40
41const app = new Hono<AuthEnv>();
42
43const POLL_INTERVAL_MS = 30_000;
44
45function jsonCacheHeaders(): Record<string, string> {
46 return {
47 "Content-Type": "application/json; charset=utf-8",
48 "Cache-Control": "public, max-age=30",
49 };
50}
51
52// ───────────────────────────────────────────────────────────────────
53// JSON endpoints — cheap, public, cacheable.
54// ───────────────────────────────────────────────────────────────────
55
56app.get("/api/v2/demo/activity", async (c) => {
57 const feed = await listDemoActivityFeed(20);
58 return new Response(
59 JSON.stringify({
60 entries: feed.map((e) => ({
61 kind: e.kind,
62 repo: e.repo,
63 ref: e.ref,
64 at: e.at.toISOString(),
65 })),
66 }),
67 { status: 200, headers: jsonCacheHeaders() }
68 );
69});
70
71app.get("/api/v2/demo/queued", async (c) => {
72 const items = await listQueuedAiBuildIssues(5);
73 return new Response(
74 JSON.stringify({
75 items: items.map((i) => ({
76 repo: i.repo,
77 number: i.number,
78 title: i.title,
79 createdAt: i.createdAt.toISOString(),
80 })),
81 }),
82 { status: 200, headers: jsonCacheHeaders() }
83 );
84});
85
86app.get("/api/v2/demo/merges", async (c) => {
87 const items = await listRecentAutoMerges(5, 24);
88 return new Response(
89 JSON.stringify({
90 items: items.map((m) => ({
91 repo: m.repo,
92 number: m.number,
93 title: m.title,
94 mergedAt: m.mergedAt.toISOString(),
95 })),
96 }),
97 { status: 200, headers: jsonCacheHeaders() }
98 );
99});
100
101app.get("/api/v2/demo/reviews", async (c) => {
102 const [items, count] = await Promise.all([
103 listRecentAiReviews(5, 24),
104 countAiReviewsSince(24),
105 ]);
106 return new Response(
107 JSON.stringify({
108 count,
109 items: items.map((r) => ({
110 repo: r.repo,
111 prNumber: r.prNumber,
112 commentSnippet: r.commentSnippet,
113 createdAt: r.createdAt.toISOString(),
114 })),
115 }),
116 { status: 200, headers: jsonCacheHeaders() }
117 );
118});
119
120// ───────────────────────────────────────────────────────────────────
121// HTML landing page.
122// ───────────────────────────────────────────────────────────────────
123
124function relativeTime(d: Date): string {
125 const ms = Date.now() - d.getTime();
126 const s = Math.max(0, Math.floor(ms / 1000));
127 if (s < 60) return `${s}s ago`;
128 const m = Math.floor(s / 60);
129 if (m < 60) return `${m}m ago`;
130 const h = Math.floor(m / 60);
131 if (h < 48) return `${h}h ago`;
132 const days = Math.floor(h / 24);
133 return `${days}d ago`;
134}
135
136function activityLabel(kind: DemoActivityEntry["kind"]): string {
137 switch (kind) {
138 case "auto_merge.merged":
139 return "auto-merged";
140 case "ai_build.dispatched":
141 return "AI-build dispatched";
142 case "ai_review.posted":
143 return "AI review posted";
144 }
145}
146
147app.get("/demo", softAuth, async (c) => {
148 const user = c.get("user") ?? null;
149
150 // Server-side render the initial snapshot. The JS poller refreshes it
151 // every 30s but the no-JS experience still works.
152 const [queued, merges, reviewsCountAndList, feed] = await Promise.all([
153 listQueuedAiBuildIssues(5),
154 listRecentAutoMerges(5, 24),
155 Promise.all([listRecentAiReviews(5, 24), countAiReviewsSince(24)]),
156 listDemoActivityFeed(20),
157 ]);
158 const [reviews, reviewCount] = reviewsCountAndList;
159
160 const demoUserUrl = `/${DEMO_USERNAME}`;
161 const demoRepos: { name: string; tagline: string }[] = [
162 { name: "hello-python", tagline: "Tiny Python app — labels, issues, gates." },
163 { name: "todo-api", tagline: "Hono todo API — PRs, AI review, auto-merge." },
164 { name: "design-docs", tagline: "ADRs + architecture docs." },
165 ];
166
167 // Poller — refreshes the four tiles every 30s. Plain vanilla JS, no
e3eb5ffClaude168 // framework. Adds a reset action that re-fetches every tile immediately.
52ad8b1Claude169 const pollerScript = `
170(function(){try{
171 var INTERVAL=${POLL_INTERVAL_MS};
172 function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];});}
173 function rel(iso){try{var ms=Date.now()-new Date(iso).getTime();var s=Math.max(0,Math.floor(ms/1000));if(s<60)return s+'s ago';var m=Math.floor(s/60);if(m<60)return m+'m ago';var h=Math.floor(m/60);if(h<48)return h+'h ago';return Math.floor(h/24)+'d ago';}catch(e){return '';}}
174 function pollQueued(){fetch('/api/v2/demo/queued').then(function(r){return r.json();}).then(function(d){
175 var el=document.getElementById('tile-queued-list');if(!el)return;
7d0c65eClaude176 if(!d.items||d.items.length===0){el.innerHTML='<li class="demo-page-empty">Nothing building right now — tag an issue ai:build to watch it start.</li>';return;}
e3eb5ffClaude177 el.innerHTML=d.items.map(function(i){return '<li><a href="/${DEMO_USERNAME}/'+esc(i.repo)+'/issues/'+i.number+'">#'+i.number+' '+esc(i.title)+'</a> <span class="demo-page-meta">'+esc(i.repo)+' · '+rel(i.createdAt)+'</span></li>';}).join('');
52ad8b1Claude178 }).catch(function(){});}
179 function pollMerges(){fetch('/api/v2/demo/merges').then(function(r){return r.json();}).then(function(d){
180 var el=document.getElementById('tile-merges-list');if(!el)return;
7d0c65eClaude181 if(!d.items||d.items.length===0){el.innerHTML='<li class="demo-page-empty">No instant auto-merges yet — one fires the moment gates go green.</li>';return;}
e3eb5ffClaude182 el.innerHTML=d.items.map(function(i){return '<li><a href="/${DEMO_USERNAME}/'+esc(i.repo)+'/pulls/'+i.number+'">#'+i.number+' '+esc(i.title)+'</a> <span class="demo-page-meta">'+esc(i.repo)+' · '+rel(i.mergedAt)+'</span></li>';}).join('');
52ad8b1Claude183 }).catch(function(){});}
184 function pollReviews(){fetch('/api/v2/demo/reviews').then(function(r){return r.json();}).then(function(d){
185 var c=document.getElementById('tile-reviews-count');if(c)c.textContent=String(d.count||0);
186 var el=document.getElementById('tile-reviews-list');if(!el)return;
e3eb5ffClaude187 if(!d.items||d.items.length===0){el.innerHTML='<li class="demo-page-empty">No AI reviews in the last 24h.</li>';return;}
188 el.innerHTML=d.items.map(function(i){return '<li><a href="/${DEMO_USERNAME}/'+esc(i.repo)+'/pulls/'+i.prNumber+'">#'+i.prNumber+'</a> <span class="demo-page-snippet">'+esc(i.commentSnippet)+'</span> <span class="demo-page-meta">'+esc(i.repo)+' · '+rel(i.createdAt)+'</span></li>';}).join('');
52ad8b1Claude189 }).catch(function(){});}
190 function pollFeed(){fetch('/api/v2/demo/activity').then(function(r){return r.json();}).then(function(d){
191 var el=document.getElementById('demo-feed-list');if(!el)return;
e3eb5ffClaude192 if(!d.entries||d.entries.length===0){el.innerHTML='<li class="demo-page-empty">Quiet right now — push a commit to a demo repo to see live updates.</li>';return;}
193 el.innerHTML=d.entries.map(function(e){var label=e.kind==='auto_merge.merged'?'auto-merged':(e.kind==='ai_build.dispatched'?'AI-build dispatched':'AI review posted');var path=e.ref.type==='pr'?'pulls':'issues';return '<li><span class="demo-page-kind demo-page-kind-'+esc(e.kind.replace(/\\./g,'-'))+'">'+esc(label)+'</span> <a href="/${DEMO_USERNAME}/'+esc(e.repo)+'/'+path+'/'+e.ref.number+'">'+esc(e.repo)+' #'+e.ref.number+'</a> <span class="demo-page-meta">'+rel(e.at)+'</span></li>';}).join('');
52ad8b1Claude194 }).catch(function(){});}
195 function tick(){pollQueued();pollMerges();pollReviews();pollFeed();}
e3eb5ffClaude196 // Reset button — re-fetch every tile immediately, then flash the button.
197 var reset=document.getElementById('demo-reset');
198 if(reset){reset.addEventListener('click',function(e){
199 e.preventDefault();
200 tick();
201 var orig=reset.textContent;
202 reset.textContent='Refreshed';
203 reset.classList.add('is-done');
204 setTimeout(function(){reset.textContent=orig;reset.classList.remove('is-done');},1500);
205 });}
206 // Background polling — initial SSR snapshot stays visible until the
207 // first tick fires, so the page doesn't "flash" identical content.
52ad8b1Claude208 setInterval(tick,INTERVAL);
209}catch(e){}})();
210`.trim();
211
212 return c.html(
213 <Layout title="Live demo" user={user}>
214 <style dangerouslySetInnerHTML={{ __html: DEMO_CSS }} />
215 <div class="demo-page">
e3eb5ffClaude216 {/* ─── Hero ─── */}
217 <section class="demo-page-hero">
218 <div class="demo-page-hero-orb" aria-hidden="true" />
219 <div class="demo-page-hero-inner">
220 <div class="demo-page-eyebrow">
221 <span class="demo-page-pulse" aria-hidden="true" />
52ad8b1Claude222 Live · pulled from production · refreshes every 30s
223 </div>
e3eb5ffClaude224 <h1 class="demo-page-title">
225 Watch Claude{" "}
226 <span class="demo-page-title-grad">build software, live.</span>
52ad8b1Claude227 </h1>
e3eb5ffClaude228 <p class="demo-page-sub">
229 Every tile and step below is real audit data from the seeded
230 demo repos. No staging, no mocks.
52ad8b1Claude231 </p>
e3eb5ffClaude232 <div class="demo-page-hero-cta">
233 <a href="/register" class="demo-page-btn demo-page-btn-primary">
234 Sign up free <span aria-hidden="true">→</span>
235 </a>
236 <a href={demoUserUrl} class="demo-page-btn">
237 Browse the demo user
238 </a>
239 </div>
52ad8b1Claude240 </div>
e3eb5ffClaude241 </section>
242
243 {/* ─── 4-step interactive walkthrough ─── */}
244 <section class="demo-page-section" aria-labelledby="demo-walk-h">
245 <header class="demo-page-section-head">
246 <div>
247 <p class="demo-page-section-eyebrow">Walkthrough</p>
248 <h2 class="demo-page-section-title" id="demo-walk-h">
249 Four steps. Each one round-trips through production.
250 </h2>
251 <p class="demo-page-section-sub">
252 Click "Try this" on any step to jump into the live demo
253 surface that performs it.
254 </p>
255 </div>
256 </header>
257 <div class="demo-page-section-body">
258 <ol class="demo-page-walk">
259 {/* Step 1 — File an ai:build issue */}
260 <li class="demo-page-step">
261 <div class="demo-page-step-head">
262 <span class="demo-page-step-num">1</span>
263 <span class="demo-page-step-icon" aria-hidden="true">
264 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
265 <circle cx="12" cy="12" r="9" />
266 <line x1="12" y1="8" x2="12" y2="13" />
267 <line x1="12" y1="16" x2="12.01" y2="16" />
268 </svg>
269 </span>
270 <h3 class="demo-page-step-title">File an issue</h3>
271 </div>
272 <p class="demo-page-step-body">
273 Open a new issue on any demo repo and tag it{" "}
7d0c65eClaude274 <code>ai:build</code>. The autopilot picks it up in real
275 time — a draft PR appears within 90 seconds.
e3eb5ffClaude276 </p>
277 <p class="demo-page-step-try">
278 <span class="demo-page-try-label">Try this</span>
279 <a
280 class="demo-page-try-link"
281 href={`/${DEMO_USERNAME}/todo-api/issues/new`}
282 >
283 Open the new-issue page →
284 </a>
285 </p>
286 </li>
287
288 {/* Step 2 — Claude opens a PR */}
289 <li class="demo-page-step">
290 <div class="demo-page-step-head">
291 <span class="demo-page-step-num">2</span>
292 <span class="demo-page-step-icon" aria-hidden="true">
293 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
294 <circle cx="6" cy="6" r="3" />
295 <circle cx="6" cy="18" r="3" />
296 <line x1="6" y1="9" x2="6" y2="15" />
297 <path d="M18 9a9 9 0 0 0-9-9" />
298 <circle cx="18" cy="18" r="3" />
299 <line x1="18" y1="9" x2="18" y2="15" />
300 </svg>
301 </span>
302 <h3 class="demo-page-step-title">Claude opens a PR</h3>
303 </div>
304 <p class="demo-page-step-body">
305 The autopilot reads the issue, edits the repo, opens a
7d0c65eClaude306 branch, and pushes a PR linked back to the issue — all
307 happening right now. Watch it appear in the tile below.
e3eb5ffClaude308 </p>
309 <p class="demo-page-step-try">
310 <span class="demo-page-try-label">Try this</span>
311 <a class="demo-page-try-link" href="#tile-queued-h">
312 See the queue ↓
313 </a>
314 </p>
315 </li>
316
317 {/* Step 3 — AI review */}
318 <li class="demo-page-step">
319 <div class="demo-page-step-head">
320 <span class="demo-page-step-num">3</span>
321 <span class="demo-page-step-icon" aria-hidden="true">
322 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
323 <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
324 </svg>
325 </span>
326 <h3 class="demo-page-step-title">AI review lands</h3>
327 </div>
328 <p class="demo-page-step-body">
7d0c65eClaude329 Every PR gets a second-AI review pass within ~8 seconds
330 of opening — typed comments with line numbers, severity,
331 and a one-line summary at the top. No human needed.
e3eb5ffClaude332 </p>
333 <p class="demo-page-step-try">
334 <span class="demo-page-try-label">Try this</span>
335 <a class="demo-page-try-link" href="#tile-reviews-h">
7d0c65eClaude336 See reviews happening now ↓
e3eb5ffClaude337 </a>
338 </p>
339 </li>
340
341 {/* Step 4 — Auto-merge */}
342 <li class="demo-page-step">
343 <div class="demo-page-step-head">
344 <span class="demo-page-step-num">4</span>
345 <span class="demo-page-step-icon" aria-hidden="true">
346 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
347 <path d="M20 6L9 17l-5-5" />
348 </svg>
349 </span>
350 <h3 class="demo-page-step-title">Auto-merge when green</h3>
351 </div>
352 <p class="demo-page-step-body">
7d0c65eClaude353 The instant every gate goes green, the autopilot merges
354 the PR and closes the originating issue — no click, no
355 wait. Branch protection rules still apply.
e3eb5ffClaude356 </p>
357 <p class="demo-page-step-try">
358 <span class="demo-page-try-label">Try this</span>
359 <a class="demo-page-try-link" href="#tile-merges-h">
7d0c65eClaude360 Watch it merge in real time ↓
e3eb5ffClaude361 </a>
362 </p>
363 </li>
364 </ol>
52ad8b1Claude365 </div>
e3eb5ffClaude366 </section>
52ad8b1Claude367
e3eb5ffClaude368 {/* ─── Live tiles ─── */}
369 <div class="demo-page-tiles">
370 <section class="demo-page-tile" aria-labelledby="tile-queued-h">
371 <h2 id="tile-queued-h" class="demo-page-tile-title">
7d0c65eClaude372 Issues being built by AI right now
52ad8b1Claude373 </h2>
e3eb5ffClaude374 <ul id="tile-queued-list" class="demo-page-list">
52ad8b1Claude375 {queued.length === 0 ? (
7d0c65eClaude376 <li class="demo-page-empty">Nothing building right now — tag an issue ai:build to watch it start.</li>
52ad8b1Claude377 ) : (
378 queued.map((i) => (
379 <li>
380 <a href={`/${DEMO_USERNAME}/${i.repo}/issues/${i.number}`}>
381 #{i.number} {i.title}
382 </a>{" "}
e3eb5ffClaude383 <span class="demo-page-meta">
52ad8b1Claude384 {i.repo} · {relativeTime(i.createdAt)}
385 </span>
386 </li>
387 ))
388 )}
389 </ul>
390 </section>
391
e3eb5ffClaude392 <section class="demo-page-tile" aria-labelledby="tile-merges-h">
393 <h2 id="tile-merges-h" class="demo-page-tile-title">
7d0c65eClaude394 PRs auto-merged the instant gates passed
52ad8b1Claude395 </h2>
e3eb5ffClaude396 <ul id="tile-merges-list" class="demo-page-list">
52ad8b1Claude397 {merges.length === 0 ? (
e3eb5ffClaude398 <li class="demo-page-empty">
7d0c65eClaude399 No instant auto-merges yet — one fires the moment gates go green.
52ad8b1Claude400 </li>
401 ) : (
402 merges.map((m) => (
403 <li>
404 <a href={`/${DEMO_USERNAME}/${m.repo}/pulls/${m.number}`}>
405 #{m.number} {m.title}
406 </a>{" "}
e3eb5ffClaude407 <span class="demo-page-meta">
52ad8b1Claude408 {m.repo} · {relativeTime(m.mergedAt)}
409 </span>
410 </li>
411 ))
412 )}
413 </ul>
414 </section>
415
e3eb5ffClaude416 <section class="demo-page-tile" aria-labelledby="tile-reviews-h">
417 <h2 id="tile-reviews-h" class="demo-page-tile-title">
52ad8b1Claude418 AI reviews posted today
419 </h2>
e3eb5ffClaude420 <div class="demo-page-bigcount">
52ad8b1Claude421 <span id="tile-reviews-count">{reviewCount}</span>
e3eb5ffClaude422 <span class="demo-page-bigcount-label">in the last 24h</span>
52ad8b1Claude423 </div>
e3eb5ffClaude424 <ul
425 id="tile-reviews-list"
426 class="demo-page-list demo-page-list-small"
427 >
52ad8b1Claude428 {reviews.length === 0 ? (
e3eb5ffClaude429 <li class="demo-page-empty">
52ad8b1Claude430 No AI reviews in the last 24h.
431 </li>
432 ) : (
433 reviews.map((r) => (
434 <li>
435 <a href={`/${DEMO_USERNAME}/${r.repo}/pulls/${r.prNumber}`}>
436 #{r.prNumber}
437 </a>{" "}
e3eb5ffClaude438 <span class="demo-page-snippet">{r.commentSnippet}</span>{" "}
439 <span class="demo-page-meta">
52ad8b1Claude440 {r.repo} · {relativeTime(r.createdAt)}
441 </span>
442 </li>
443 ))
444 )}
445 </ul>
446 </section>
447 </div>
448
e3eb5ffClaude449 <section class="demo-page-section" aria-labelledby="demo-repos-h">
450 <header class="demo-page-section-head">
451 <div>
452 <p class="demo-page-section-eyebrow">Browse</p>
453 <h2 class="demo-page-section-title" id="demo-repos-h">
454 Dig into the demo repos
455 </h2>
456 </div>
457 </header>
458 <div class="demo-page-section-body">
459 <div class="demo-page-repos-grid">
460 {demoRepos.map((r) => (
461 <a class="demo-page-repo-card" href={`${demoUserUrl}/${r.name}`}>
462 <div class="demo-page-repo-name">
463 {DEMO_USERNAME}/{r.name}
464 </div>
465 <div class="demo-page-repo-tag">{r.tagline}</div>
466 </a>
467 ))}
468 </div>
52ad8b1Claude469 </div>
470 </section>
471
e3eb5ffClaude472 <section class="demo-page-section" aria-labelledby="demo-feed-h">
473 <header class="demo-page-section-head">
474 <div>
475 <p class="demo-page-section-eyebrow">Stream</p>
476 <h2 class="demo-page-section-title" id="demo-feed-h">
477 Live activity
478 </h2>
479 <p class="demo-page-section-sub">
7d0c65eClaude480 Happening right now — newest event first, auto-refreshes every 30s.
e3eb5ffClaude481 </p>
482 </div>
483 <button
484 type="button"
485 id="demo-reset"
486 class="demo-page-btn demo-page-reset"
487 aria-label="Refresh all tiles now"
488 >
489 Refresh now
490 </button>
491 </header>
492 <div class="demo-page-section-body">
493 <ul id="demo-feed-list" class="demo-page-feed-list">
494 {feed.length === 0 ? (
495 <li class="demo-page-empty">
496 Quiet right now — push a commit to a demo repo to see
497 live updates.
498 </li>
499 ) : (
500 feed.map((e) => {
501 const path = e.ref.type === "pr" ? "pulls" : "issues";
502 return (
503 <li>
504 <span
505 class={`demo-page-kind demo-page-kind-${e.kind.replace(/\./g, "-")}`}
506 >
507 {activityLabel(e.kind)}
508 </span>{" "}
509 <a href={`/${DEMO_USERNAME}/${e.repo}/${path}/${e.ref.number}`}>
510 {e.repo} #{e.ref.number}
511 </a>{" "}
512 <span class="demo-page-meta">{relativeTime(e.at)}</span>
513 </li>
514 );
515 })
516 )}
517 </ul>
518 </div>
52ad8b1Claude519 </section>
520 </div>
521 <script dangerouslySetInnerHTML={{ __html: pollerScript }} />
522 </Layout>
523 );
524});
525
e3eb5ffClaude526// Scoped CSS — every class prefixed `.demo-page-` so this surface can't
527// bleed into other views. Drop-in replacement for the legacy `.demo-*`
528// classes; the new prefix is wider so we can polish without collisions.
52ad8b1Claude529const DEMO_CSS = `
eed4684Claude530 .demo-page { max-width: 1680px; margin: 0 auto; padding: var(--space-6) var(--space-4); }
e3eb5ffClaude531
532 /* ─── Hero ─── */
533 .demo-page-hero {
534 position: relative;
535 margin-bottom: var(--space-5);
536 padding: var(--space-6) var(--space-6);
537 background: var(--bg-elevated);
538 border: 1px solid var(--border);
539 border-radius: 18px;
540 overflow: hidden;
541 }
542 .demo-page-hero::before {
543 content: '';
544 position: absolute;
545 top: 0; left: 0; right: 0;
546 height: 2px;
547 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
548 opacity: 0.7;
549 pointer-events: none;
550 }
551 .demo-page-hero-orb {
552 position: absolute;
553 inset: -25% -10% auto auto;
554 width: 480px; height: 480px;
555 background: radial-gradient(circle, rgba(140,109,255,0.22), rgba(54,197,214,0.10) 45%, transparent 70%);
556 filter: blur(80px);
557 opacity: 0.7;
558 pointer-events: none;
559 z-index: 0;
560 }
561 .demo-page-hero-inner { position: relative; z-index: 1; max-width: 720px; }
562 .demo-page-eyebrow {
563 font-size: 12px;
564 color: var(--text-muted);
565 margin-bottom: var(--space-2);
566 letter-spacing: 0.02em;
567 display: inline-flex;
568 align-items: center;
569 gap: 8px;
570 }
571 .demo-page-pulse {
572 display: inline-block;
573 width: 8px;
574 height: 8px;
575 border-radius: 50%;
576 background: #34d399;
577 box-shadow: 0 0 8px rgba(52,211,153,0.7);
578 animation: demo-page-pulse 1.6s infinite;
579 }
580 @keyframes demo-page-pulse {
581 0%, 100% { opacity: 1; }
582 50% { opacity: 0.4; }
583 }
584 .demo-page-title {
585 font-size: clamp(32px, 5vw, 52px);
586 font-family: var(--font-display);
587 font-weight: 800;
588 letter-spacing: -0.028em;
589 line-height: 1.05;
590 margin: 0 0 var(--space-3);
591 color: var(--text-strong);
592 }
593 .demo-page-title-grad {
594 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
595 -webkit-background-clip: text;
596 background-clip: text;
597 -webkit-text-fill-color: transparent;
598 color: transparent;
599 }
600 .demo-page-sub {
601 font-size: 16px;
602 color: var(--text-muted);
603 margin: 0 0 var(--space-4);
604 line-height: 1.55;
605 max-width: 620px;
606 }
607 .demo-page-hero-cta { display: flex; gap: 10px; flex-wrap: wrap; }
608
609 /* ─── Buttons ─── */
610 .demo-page-btn {
611 appearance: none;
612 border: 1px solid var(--border-strong);
613 background: var(--bg-secondary);
614 color: var(--text);
615 padding: 10px 16px;
616 border-radius: 10px;
617 font-family: inherit;
618 font-size: 14px;
619 font-weight: 500;
620 cursor: pointer;
621 transition: border-color 150ms ease, background 150ms ease, transform 150ms ease, color 150ms ease;
622 text-decoration: none;
623 display: inline-flex; align-items: center; gap: 8px;
624 }
625 .demo-page-btn:hover {
626 border-color: var(--border-focus);
627 background: rgba(255,255,255,0.03);
628 transform: translateY(-1px);
629 text-decoration: none;
630 }
631 .demo-page-btn-primary {
632 border-color: rgba(140,109,255,0.45);
633 background: linear-gradient(135deg, rgba(140,109,255,0.20), rgba(54,197,214,0.14));
634 color: var(--text-strong);
635 }
636 .demo-page-btn-primary:hover {
637 border-color: rgba(140,109,255,0.65);
638 background: linear-gradient(135deg, rgba(140,109,255,0.28), rgba(54,197,214,0.20));
639 }
640 .demo-page-reset {
641 padding: 6px 12px;
642 font-size: 12.5px;
643 border-radius: 8px;
644 }
645 .demo-page-reset.is-done {
646 border-color: rgba(52,211,153,0.45);
647 background: rgba(52,211,153,0.10);
648 color: #6ee7b7;
649 }
650
651 /* ─── Section cards ─── */
652 .demo-page-section {
653 margin-bottom: var(--space-5);
654 background: var(--bg-elevated);
655 border: 1px solid var(--border);
656 border-radius: 14px;
657 overflow: hidden;
658 }
659 .demo-page-section-head {
660 padding: var(--space-4) var(--space-5);
661 border-bottom: 1px solid var(--border);
662 display: flex;
663 align-items: flex-start;
664 justify-content: space-between;
665 gap: var(--space-3);
666 flex-wrap: wrap;
667 }
668 .demo-page-section-eyebrow {
669 font-size: 11px;
670 font-weight: 600;
671 letter-spacing: 0.08em;
672 text-transform: uppercase;
673 color: var(--text-faint);
674 margin: 0 0 6px;
675 }
676 .demo-page-section-title {
677 margin: 0;
678 font-family: var(--font-display);
679 font-size: 17px;
680 font-weight: 700;
681 letter-spacing: -0.018em;
682 color: var(--text-strong);
683 }
684 .demo-page-section-sub {
685 margin: 6px 0 0;
686 font-size: 12.5px;
687 color: var(--text-muted);
688 line-height: 1.5;
689 }
690 .demo-page-section-body { padding: var(--space-5); }
691
692 /* ─── 4-step walkthrough ─── */
693 .demo-page-walk {
694 list-style: none;
695 margin: 0;
696 padding: 0;
697 display: grid;
698 grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
699 gap: var(--space-3);
700 }
701 .demo-page-step {
702 padding: var(--space-4);
703 background: var(--bg-secondary);
704 border: 1px solid var(--border-subtle);
705 border-radius: 12px;
706 display: flex;
707 flex-direction: column;
708 gap: 10px;
709 transition: border-color 150ms ease, transform 150ms ease;
710 }
711 .demo-page-step:hover {
712 border-color: var(--border-strong);
713 transform: translateY(-1px);
714 }
715 .demo-page-step-head {
716 display: flex;
717 align-items: center;
718 gap: 10px;
719 }
720 .demo-page-step-num {
721 display: inline-flex;
722 align-items: center;
723 justify-content: center;
724 width: 28px; height: 28px;
725 border-radius: 50%;
726 background: linear-gradient(135deg, rgba(140,109,255,0.22), rgba(54,197,214,0.16));
727 color: #c5b3ff;
728 border: 1px solid rgba(140,109,255,0.40);
729 font-family: var(--font-display);
730 font-weight: 700;
731 font-size: 13px;
732 }
733 .demo-page-step-icon {
734 display: inline-flex;
735 align-items: center;
736 justify-content: center;
737 width: 26px; height: 26px;
738 border-radius: 8px;
739 background: rgba(54,197,214,0.10);
740 color: #5fd3e0;
741 box-shadow: inset 0 0 0 1px rgba(54,197,214,0.30);
742 }
743 .demo-page-step-title {
744 margin: 0;
745 font-family: var(--font-display);
746 font-size: 15px;
747 font-weight: 700;
748 color: var(--text-strong);
749 letter-spacing: -0.012em;
750 }
751 .demo-page-step-body {
752 margin: 0;
753 font-size: 13.5px;
754 color: var(--text-muted);
755 line-height: 1.55;
756 }
757 .demo-page-step-body code {
758 font-family: var(--font-mono);
759 font-size: 12px;
760 background: var(--bg-tertiary);
761 padding: 1px 5px;
762 border-radius: 4px;
763 color: var(--text-strong);
764 }
765 .demo-page-step-try {
766 margin: auto 0 0;
767 padding-top: 10px;
768 border-top: 1px dashed var(--border-subtle);
769 display: flex;
770 align-items: center;
771 justify-content: space-between;
772 gap: 8px;
773 font-size: 12.5px;
774 }
775 .demo-page-try-label {
776 font-size: 10.5px;
777 font-weight: 600;
778 letter-spacing: 0.08em;
779 text-transform: uppercase;
780 color: var(--text-faint);
781 }
782 .demo-page-try-link {
783 color: var(--accent, #8c6dff);
784 text-decoration: none;
785 font-weight: 600;
786 }
787 .demo-page-try-link:hover { text-decoration: underline; }
788
789 /* ─── Live tiles ─── */
790 .demo-page-tiles {
791 display: grid;
792 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
793 gap: var(--space-3);
794 margin-bottom: var(--space-5);
795 }
796 .demo-page-tile {
797 background: var(--bg-elevated);
798 border: 1px solid var(--border);
799 border-radius: 14px;
800 padding: var(--space-4) var(--space-4);
801 }
802 .demo-page-tile-title {
803 font-size: 11px;
804 font-weight: 600;
805 text-transform: uppercase;
806 letter-spacing: 0.08em;
807 color: var(--text-faint);
808 margin: 0 0 12px;
809 }
810 .demo-page-list {
811 list-style: none;
812 margin: 0;
813 padding: 0;
814 font-size: 14px;
815 line-height: 1.5;
816 }
817 .demo-page-list li {
818 padding: 6px 0;
819 border-bottom: 1px solid var(--border-subtle);
820 }
821 .demo-page-list li:last-child { border-bottom: 0; }
822 .demo-page-list-small li { font-size: 13px; }
823 .demo-page-list a {
824 color: var(--text-strong);
825 text-decoration: none;
826 font-weight: 500;
827 }
828 .demo-page-list a:hover {
829 color: var(--accent, #8c6dff);
830 text-decoration: underline;
831 }
832 .demo-page-meta { color: var(--text-muted); font-size: 12px; }
833 .demo-page-snippet { color: var(--text-muted); font-style: italic; font-size: 12px; }
834 .demo-page-empty { color: var(--text-muted); font-size: 13px; padding: 6px 0; }
835 .demo-page-bigcount {
836 display: flex;
837 align-items: baseline;
838 gap: 8px;
839 margin-bottom: 12px;
840 }
841 .demo-page-bigcount span:first-child {
842 font-size: 32px;
843 font-weight: 700;
844 color: var(--text-strong);
845 font-family: var(--font-display);
846 letter-spacing: -0.02em;
847 }
848 .demo-page-bigcount-label { color: var(--text-muted); font-size: 12px; }
849
850 /* ─── Demo repos grid ─── */
851 .demo-page-repos-grid {
852 display: grid;
853 grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
854 gap: var(--space-3);
855 }
856 .demo-page-repo-card {
857 display: block;
858 background: var(--bg-secondary);
859 border: 1px solid var(--border-subtle);
860 border-radius: 12px;
861 padding: var(--space-3) var(--space-4);
862 text-decoration: none;
863 color: inherit;
864 transition: border-color 150ms ease, transform 150ms ease;
865 }
866 .demo-page-repo-card:hover {
867 border-color: rgba(140,109,255,0.45);
868 transform: translateY(-1px);
869 text-decoration: none;
870 }
871 .demo-page-repo-name {
872 font-family: var(--font-mono);
873 font-weight: 600;
874 font-size: 13.5px;
875 color: var(--text-strong);
876 margin-bottom: 4px;
877 }
878 .demo-page-repo-tag {
879 font-size: 12.5px;
880 color: var(--text-muted);
881 line-height: 1.45;
882 }
883
884 /* ─── Live feed ─── */
885 .demo-page-feed-list {
886 list-style: none;
887 margin: 0;
888 padding: 0;
889 font-size: 13px;
890 line-height: 1.6;
891 }
892 .demo-page-feed-list li {
893 padding: 6px 0;
894 border-bottom: 1px solid var(--border-subtle);
895 display: flex;
896 align-items: center;
897 gap: 8px;
898 flex-wrap: wrap;
899 }
900 .demo-page-feed-list li:last-child { border-bottom: 0; }
901 .demo-page-feed-list a {
902 color: var(--text-strong);
903 text-decoration: none;
904 font-weight: 500;
905 }
906 .demo-page-feed-list a:hover {
907 color: var(--accent, #8c6dff);
908 text-decoration: underline;
909 }
910 .demo-page-kind {
911 display: inline-flex;
912 align-items: center;
913 padding: 2px 9px;
914 border-radius: 9999px;
915 font-size: 11px;
916 font-weight: 600;
917 letter-spacing: 0.01em;
918 }
919 .demo-page-kind-auto_merge-merged,
920 .demo-page-kind-auto-merge-merged {
921 background: rgba(52,211,153,0.12);
922 color: #34d399;
923 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.28);
924 }
925 .demo-page-kind-ai_build-dispatched,
926 .demo-page-kind-ai-build-dispatched {
927 background: rgba(140,109,255,0.15);
928 color: #b69dff;
929 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.32);
930 }
931 .demo-page-kind-ai_review-posted,
932 .demo-page-kind-ai-review-posted {
933 background: rgba(54,197,214,0.15);
934 color: #5fd3e0;
935 box-shadow: inset 0 0 0 1px rgba(54,197,214,0.32);
936 }
52ad8b1Claude937`;
938
939export default app;