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.tsxBlame940 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;
e3eb5ffClaude176 if(!d.items||d.items.length===0){el.innerHTML='<li class="demo-page-empty">No queued AI builds — quiet right now.</li>';return;}
177 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;
e3eb5ffClaude181 if(!d.items||d.items.length===0){el.innerHTML='<li class="demo-page-empty">No auto-merges in the last 24h.</li>';return;}
182 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{" "}
274 <code>ai:build</code>. The autopilot picks it up on the
275 next 5-minute sweep.
276 </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
306 branch, and pushes a PR linked back to the issue. You
307 can see every PR Claude has opened in the tile below.
308 </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">
329 Every PR gets a second-AI review pass — typed comments
330 with line numbers, severity, and a one-line summary at
331 the top. No human needed for the routine stuff.
332 </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">
336 Read today's reviews ↓
337 </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">
353 Once every gate is green, the autopilot merges the PR
354 and closes the originating issue. Branch protection
355 rules still apply — Claude can't merge anything you
356 couldn't.
357 </p>
358 <p class="demo-page-step-try">
359 <span class="demo-page-try-label">Try this</span>
360 <a class="demo-page-try-link" href="#tile-merges-h">
361 Watch the auto-merge tile ↓
362 </a>
363 </p>
364 </li>
365 </ol>
52ad8b1Claude366 </div>
e3eb5ffClaude367 </section>
52ad8b1Claude368
e3eb5ffClaude369 {/* ─── Live tiles ─── */}
370 <div class="demo-page-tiles">
371 <section class="demo-page-tile" aria-labelledby="tile-queued-h">
372 <h2 id="tile-queued-h" class="demo-page-tile-title">
52ad8b1Claude373 Issues queued for AI build
374 </h2>
e3eb5ffClaude375 <ul id="tile-queued-list" class="demo-page-list">
52ad8b1Claude376 {queued.length === 0 ? (
e3eb5ffClaude377 <li class="demo-page-empty">No queued AI builds — quiet right now.</li>
52ad8b1Claude378 ) : (
379 queued.map((i) => (
380 <li>
381 <a href={`/${DEMO_USERNAME}/${i.repo}/issues/${i.number}`}>
382 #{i.number} {i.title}
383 </a>{" "}
e3eb5ffClaude384 <span class="demo-page-meta">
52ad8b1Claude385 {i.repo} · {relativeTime(i.createdAt)}
386 </span>
387 </li>
388 ))
389 )}
390 </ul>
391 </section>
392
e3eb5ffClaude393 <section class="demo-page-tile" aria-labelledby="tile-merges-h">
394 <h2 id="tile-merges-h" class="demo-page-tile-title">
52ad8b1Claude395 PRs auto-merged in the last 24h
396 </h2>
e3eb5ffClaude397 <ul id="tile-merges-list" class="demo-page-list">
52ad8b1Claude398 {merges.length === 0 ? (
e3eb5ffClaude399 <li class="demo-page-empty">
52ad8b1Claude400 No auto-merges in the last 24h.
401 </li>
402 ) : (
403 merges.map((m) => (
404 <li>
405 <a href={`/${DEMO_USERNAME}/${m.repo}/pulls/${m.number}`}>
406 #{m.number} {m.title}
407 </a>{" "}
e3eb5ffClaude408 <span class="demo-page-meta">
52ad8b1Claude409 {m.repo} · {relativeTime(m.mergedAt)}
410 </span>
411 </li>
412 ))
413 )}
414 </ul>
415 </section>
416
e3eb5ffClaude417 <section class="demo-page-tile" aria-labelledby="tile-reviews-h">
418 <h2 id="tile-reviews-h" class="demo-page-tile-title">
52ad8b1Claude419 AI reviews posted today
420 </h2>
e3eb5ffClaude421 <div class="demo-page-bigcount">
52ad8b1Claude422 <span id="tile-reviews-count">{reviewCount}</span>
e3eb5ffClaude423 <span class="demo-page-bigcount-label">in the last 24h</span>
52ad8b1Claude424 </div>
e3eb5ffClaude425 <ul
426 id="tile-reviews-list"
427 class="demo-page-list demo-page-list-small"
428 >
52ad8b1Claude429 {reviews.length === 0 ? (
e3eb5ffClaude430 <li class="demo-page-empty">
52ad8b1Claude431 No AI reviews in the last 24h.
432 </li>
433 ) : (
434 reviews.map((r) => (
435 <li>
436 <a href={`/${DEMO_USERNAME}/${r.repo}/pulls/${r.prNumber}`}>
437 #{r.prNumber}
438 </a>{" "}
e3eb5ffClaude439 <span class="demo-page-snippet">{r.commentSnippet}</span>{" "}
440 <span class="demo-page-meta">
52ad8b1Claude441 {r.repo} · {relativeTime(r.createdAt)}
442 </span>
443 </li>
444 ))
445 )}
446 </ul>
447 </section>
448 </div>
449
e3eb5ffClaude450 <section class="demo-page-section" aria-labelledby="demo-repos-h">
451 <header class="demo-page-section-head">
452 <div>
453 <p class="demo-page-section-eyebrow">Browse</p>
454 <h2 class="demo-page-section-title" id="demo-repos-h">
455 Dig into the demo repos
456 </h2>
457 </div>
458 </header>
459 <div class="demo-page-section-body">
460 <div class="demo-page-repos-grid">
461 {demoRepos.map((r) => (
462 <a class="demo-page-repo-card" href={`${demoUserUrl}/${r.name}`}>
463 <div class="demo-page-repo-name">
464 {DEMO_USERNAME}/{r.name}
465 </div>
466 <div class="demo-page-repo-tag">{r.tagline}</div>
467 </a>
468 ))}
469 </div>
52ad8b1Claude470 </div>
471 </section>
472
e3eb5ffClaude473 <section class="demo-page-section" aria-labelledby="demo-feed-h">
474 <header class="demo-page-section-head">
475 <div>
476 <p class="demo-page-section-eyebrow">Stream</p>
477 <h2 class="demo-page-section-title" id="demo-feed-h">
478 Live activity
479 </h2>
480 <p class="demo-page-section-sub">
481 Newest event first. Auto-refreshes every 30s.
482 </p>
483 </div>
484 <button
485 type="button"
486 id="demo-reset"
487 class="demo-page-btn demo-page-reset"
488 aria-label="Refresh all tiles now"
489 >
490 Refresh now
491 </button>
492 </header>
493 <div class="demo-page-section-body">
494 <ul id="demo-feed-list" class="demo-page-feed-list">
495 {feed.length === 0 ? (
496 <li class="demo-page-empty">
497 Quiet right now — push a commit to a demo repo to see
498 live updates.
499 </li>
500 ) : (
501 feed.map((e) => {
502 const path = e.ref.type === "pr" ? "pulls" : "issues";
503 return (
504 <li>
505 <span
506 class={`demo-page-kind demo-page-kind-${e.kind.replace(/\./g, "-")}`}
507 >
508 {activityLabel(e.kind)}
509 </span>{" "}
510 <a href={`/${DEMO_USERNAME}/${e.repo}/${path}/${e.ref.number}`}>
511 {e.repo} #{e.ref.number}
512 </a>{" "}
513 <span class="demo-page-meta">{relativeTime(e.at)}</span>
514 </li>
515 );
516 })
517 )}
518 </ul>
519 </div>
52ad8b1Claude520 </section>
521 </div>
522 <script dangerouslySetInnerHTML={{ __html: pollerScript }} />
523 </Layout>
524 );
525});
526
e3eb5ffClaude527// Scoped CSS — every class prefixed `.demo-page-` so this surface can't
528// bleed into other views. Drop-in replacement for the legacy `.demo-*`
529// classes; the new prefix is wider so we can polish without collisions.
52ad8b1Claude530const DEMO_CSS = `
a6dc91cClaude531 .demo-page { max-width: 1320px; margin: 0 auto; padding: var(--space-6) var(--space-4); }
e3eb5ffClaude532
533 /* ─── Hero ─── */
534 .demo-page-hero {
535 position: relative;
536 margin-bottom: var(--space-5);
537 padding: var(--space-6) var(--space-6);
538 background: var(--bg-elevated);
539 border: 1px solid var(--border);
540 border-radius: 18px;
541 overflow: hidden;
542 }
543 .demo-page-hero::before {
544 content: '';
545 position: absolute;
546 top: 0; left: 0; right: 0;
547 height: 2px;
548 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
549 opacity: 0.7;
550 pointer-events: none;
551 }
552 .demo-page-hero-orb {
553 position: absolute;
554 inset: -25% -10% auto auto;
555 width: 480px; height: 480px;
556 background: radial-gradient(circle, rgba(140,109,255,0.22), rgba(54,197,214,0.10) 45%, transparent 70%);
557 filter: blur(80px);
558 opacity: 0.7;
559 pointer-events: none;
560 z-index: 0;
561 }
562 .demo-page-hero-inner { position: relative; z-index: 1; max-width: 720px; }
563 .demo-page-eyebrow {
564 font-size: 12px;
565 color: var(--text-muted);
566 margin-bottom: var(--space-2);
567 letter-spacing: 0.02em;
568 display: inline-flex;
569 align-items: center;
570 gap: 8px;
571 }
572 .demo-page-pulse {
573 display: inline-block;
574 width: 8px;
575 height: 8px;
576 border-radius: 50%;
577 background: #34d399;
578 box-shadow: 0 0 8px rgba(52,211,153,0.7);
579 animation: demo-page-pulse 1.6s infinite;
580 }
581 @keyframes demo-page-pulse {
582 0%, 100% { opacity: 1; }
583 50% { opacity: 0.4; }
584 }
585 .demo-page-title {
586 font-size: clamp(32px, 5vw, 52px);
587 font-family: var(--font-display);
588 font-weight: 800;
589 letter-spacing: -0.028em;
590 line-height: 1.05;
591 margin: 0 0 var(--space-3);
592 color: var(--text-strong);
593 }
594 .demo-page-title-grad {
595 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
596 -webkit-background-clip: text;
597 background-clip: text;
598 -webkit-text-fill-color: transparent;
599 color: transparent;
600 }
601 .demo-page-sub {
602 font-size: 16px;
603 color: var(--text-muted);
604 margin: 0 0 var(--space-4);
605 line-height: 1.55;
606 max-width: 620px;
607 }
608 .demo-page-hero-cta { display: flex; gap: 10px; flex-wrap: wrap; }
609
610 /* ─── Buttons ─── */
611 .demo-page-btn {
612 appearance: none;
613 border: 1px solid var(--border-strong);
614 background: var(--bg-secondary);
615 color: var(--text);
616 padding: 10px 16px;
617 border-radius: 10px;
618 font-family: inherit;
619 font-size: 14px;
620 font-weight: 500;
621 cursor: pointer;
622 transition: border-color 150ms ease, background 150ms ease, transform 150ms ease, color 150ms ease;
623 text-decoration: none;
624 display: inline-flex; align-items: center; gap: 8px;
625 }
626 .demo-page-btn:hover {
627 border-color: var(--border-focus);
628 background: rgba(255,255,255,0.03);
629 transform: translateY(-1px);
630 text-decoration: none;
631 }
632 .demo-page-btn-primary {
633 border-color: rgba(140,109,255,0.45);
634 background: linear-gradient(135deg, rgba(140,109,255,0.20), rgba(54,197,214,0.14));
635 color: var(--text-strong);
636 }
637 .demo-page-btn-primary:hover {
638 border-color: rgba(140,109,255,0.65);
639 background: linear-gradient(135deg, rgba(140,109,255,0.28), rgba(54,197,214,0.20));
640 }
641 .demo-page-reset {
642 padding: 6px 12px;
643 font-size: 12.5px;
644 border-radius: 8px;
645 }
646 .demo-page-reset.is-done {
647 border-color: rgba(52,211,153,0.45);
648 background: rgba(52,211,153,0.10);
649 color: #6ee7b7;
650 }
651
652 /* ─── Section cards ─── */
653 .demo-page-section {
654 margin-bottom: var(--space-5);
655 background: var(--bg-elevated);
656 border: 1px solid var(--border);
657 border-radius: 14px;
658 overflow: hidden;
659 }
660 .demo-page-section-head {
661 padding: var(--space-4) var(--space-5);
662 border-bottom: 1px solid var(--border);
663 display: flex;
664 align-items: flex-start;
665 justify-content: space-between;
666 gap: var(--space-3);
667 flex-wrap: wrap;
668 }
669 .demo-page-section-eyebrow {
670 font-size: 11px;
671 font-weight: 600;
672 letter-spacing: 0.08em;
673 text-transform: uppercase;
674 color: var(--text-faint);
675 margin: 0 0 6px;
676 }
677 .demo-page-section-title {
678 margin: 0;
679 font-family: var(--font-display);
680 font-size: 17px;
681 font-weight: 700;
682 letter-spacing: -0.018em;
683 color: var(--text-strong);
684 }
685 .demo-page-section-sub {
686 margin: 6px 0 0;
687 font-size: 12.5px;
688 color: var(--text-muted);
689 line-height: 1.5;
690 }
691 .demo-page-section-body { padding: var(--space-5); }
692
693 /* ─── 4-step walkthrough ─── */
694 .demo-page-walk {
695 list-style: none;
696 margin: 0;
697 padding: 0;
698 display: grid;
699 grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
700 gap: var(--space-3);
701 }
702 .demo-page-step {
703 padding: var(--space-4);
704 background: var(--bg-secondary);
705 border: 1px solid var(--border-subtle);
706 border-radius: 12px;
707 display: flex;
708 flex-direction: column;
709 gap: 10px;
710 transition: border-color 150ms ease, transform 150ms ease;
711 }
712 .demo-page-step:hover {
713 border-color: var(--border-strong);
714 transform: translateY(-1px);
715 }
716 .demo-page-step-head {
717 display: flex;
718 align-items: center;
719 gap: 10px;
720 }
721 .demo-page-step-num {
722 display: inline-flex;
723 align-items: center;
724 justify-content: center;
725 width: 28px; height: 28px;
726 border-radius: 50%;
727 background: linear-gradient(135deg, rgba(140,109,255,0.22), rgba(54,197,214,0.16));
728 color: #c5b3ff;
729 border: 1px solid rgba(140,109,255,0.40);
730 font-family: var(--font-display);
731 font-weight: 700;
732 font-size: 13px;
733 }
734 .demo-page-step-icon {
735 display: inline-flex;
736 align-items: center;
737 justify-content: center;
738 width: 26px; height: 26px;
739 border-radius: 8px;
740 background: rgba(54,197,214,0.10);
741 color: #5fd3e0;
742 box-shadow: inset 0 0 0 1px rgba(54,197,214,0.30);
743 }
744 .demo-page-step-title {
745 margin: 0;
746 font-family: var(--font-display);
747 font-size: 15px;
748 font-weight: 700;
749 color: var(--text-strong);
750 letter-spacing: -0.012em;
751 }
752 .demo-page-step-body {
753 margin: 0;
754 font-size: 13.5px;
755 color: var(--text-muted);
756 line-height: 1.55;
757 }
758 .demo-page-step-body code {
759 font-family: var(--font-mono);
760 font-size: 12px;
761 background: var(--bg-tertiary);
762 padding: 1px 5px;
763 border-radius: 4px;
764 color: var(--text-strong);
765 }
766 .demo-page-step-try {
767 margin: auto 0 0;
768 padding-top: 10px;
769 border-top: 1px dashed var(--border-subtle);
770 display: flex;
771 align-items: center;
772 justify-content: space-between;
773 gap: 8px;
774 font-size: 12.5px;
775 }
776 .demo-page-try-label {
777 font-size: 10.5px;
778 font-weight: 600;
779 letter-spacing: 0.08em;
780 text-transform: uppercase;
781 color: var(--text-faint);
782 }
783 .demo-page-try-link {
784 color: var(--accent, #8c6dff);
785 text-decoration: none;
786 font-weight: 600;
787 }
788 .demo-page-try-link:hover { text-decoration: underline; }
789
790 /* ─── Live tiles ─── */
791 .demo-page-tiles {
792 display: grid;
793 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
794 gap: var(--space-3);
795 margin-bottom: var(--space-5);
796 }
797 .demo-page-tile {
798 background: var(--bg-elevated);
799 border: 1px solid var(--border);
800 border-radius: 14px;
801 padding: var(--space-4) var(--space-4);
802 }
803 .demo-page-tile-title {
804 font-size: 11px;
805 font-weight: 600;
806 text-transform: uppercase;
807 letter-spacing: 0.08em;
808 color: var(--text-faint);
809 margin: 0 0 12px;
810 }
811 .demo-page-list {
812 list-style: none;
813 margin: 0;
814 padding: 0;
815 font-size: 14px;
816 line-height: 1.5;
817 }
818 .demo-page-list li {
819 padding: 6px 0;
820 border-bottom: 1px solid var(--border-subtle);
821 }
822 .demo-page-list li:last-child { border-bottom: 0; }
823 .demo-page-list-small li { font-size: 13px; }
824 .demo-page-list a {
825 color: var(--text-strong);
826 text-decoration: none;
827 font-weight: 500;
828 }
829 .demo-page-list a:hover {
830 color: var(--accent, #8c6dff);
831 text-decoration: underline;
832 }
833 .demo-page-meta { color: var(--text-muted); font-size: 12px; }
834 .demo-page-snippet { color: var(--text-muted); font-style: italic; font-size: 12px; }
835 .demo-page-empty { color: var(--text-muted); font-size: 13px; padding: 6px 0; }
836 .demo-page-bigcount {
837 display: flex;
838 align-items: baseline;
839 gap: 8px;
840 margin-bottom: 12px;
841 }
842 .demo-page-bigcount span:first-child {
843 font-size: 32px;
844 font-weight: 700;
845 color: var(--text-strong);
846 font-family: var(--font-display);
847 letter-spacing: -0.02em;
848 }
849 .demo-page-bigcount-label { color: var(--text-muted); font-size: 12px; }
850
851 /* ─── Demo repos grid ─── */
852 .demo-page-repos-grid {
853 display: grid;
854 grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
855 gap: var(--space-3);
856 }
857 .demo-page-repo-card {
858 display: block;
859 background: var(--bg-secondary);
860 border: 1px solid var(--border-subtle);
861 border-radius: 12px;
862 padding: var(--space-3) var(--space-4);
863 text-decoration: none;
864 color: inherit;
865 transition: border-color 150ms ease, transform 150ms ease;
866 }
867 .demo-page-repo-card:hover {
868 border-color: rgba(140,109,255,0.45);
869 transform: translateY(-1px);
870 text-decoration: none;
871 }
872 .demo-page-repo-name {
873 font-family: var(--font-mono);
874 font-weight: 600;
875 font-size: 13.5px;
876 color: var(--text-strong);
877 margin-bottom: 4px;
878 }
879 .demo-page-repo-tag {
880 font-size: 12.5px;
881 color: var(--text-muted);
882 line-height: 1.45;
883 }
884
885 /* ─── Live feed ─── */
886 .demo-page-feed-list {
887 list-style: none;
888 margin: 0;
889 padding: 0;
890 font-size: 13px;
891 line-height: 1.6;
892 }
893 .demo-page-feed-list li {
894 padding: 6px 0;
895 border-bottom: 1px solid var(--border-subtle);
896 display: flex;
897 align-items: center;
898 gap: 8px;
899 flex-wrap: wrap;
900 }
901 .demo-page-feed-list li:last-child { border-bottom: 0; }
902 .demo-page-feed-list a {
903 color: var(--text-strong);
904 text-decoration: none;
905 font-weight: 500;
906 }
907 .demo-page-feed-list a:hover {
908 color: var(--accent, #8c6dff);
909 text-decoration: underline;
910 }
911 .demo-page-kind {
912 display: inline-flex;
913 align-items: center;
914 padding: 2px 9px;
915 border-radius: 9999px;
916 font-size: 11px;
917 font-weight: 600;
918 letter-spacing: 0.01em;
919 }
920 .demo-page-kind-auto_merge-merged,
921 .demo-page-kind-auto-merge-merged {
922 background: rgba(52,211,153,0.12);
923 color: #34d399;
924 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.28);
925 }
926 .demo-page-kind-ai_build-dispatched,
927 .demo-page-kind-ai-build-dispatched {
928 background: rgba(140,109,255,0.15);
929 color: #b69dff;
930 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.32);
931 }
932 .demo-page-kind-ai_review-posted,
933 .demo-page-kind-ai-review-posted {
934 background: rgba(54,197,214,0.15);
935 color: #5fd3e0;
936 box-shadow: inset 0 0 0 1px rgba(54,197,214,0.32);
937 }
52ad8b1Claude938`;
939
940export default app;