Commitf6730d0
feat(design): adopt shared PageHeader/Card/Badge/Button across top customer pages
feat(design): adopt shared PageHeader/Card/Badge/Button across top customer pages Second redesign pass (ultracode, 8 agents). Each page swapped its hand-forked hero for the shared PageHeader, unified CTAs to the shared Button, status pills to Badge, and deleted the now-dead forked CSS — presentation only, no route/data/logic changes. Net -793 lines as the per-page stylesheets collapse into the shared primitives. Pages: web, issues, pulls, settings, explore, insights, contributors, compare. Where a hero didn't map cleanly (interactive RepoHeader with star/fork forms, gradient-span titles, linked grade pills) the agent kept the original per the "smaller change" rule. Typecheck clean; suite 3116 pass / 4 pre-existing fails (baseline held). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8 files changed+210−1003f6730d0c5767ded271d5c316dd0ff34a47e8b0ee
8 changed files+210−1003
Modifiedsrc/routes/compare.tsx+47−212View fileUnifiedSplit
@@ -17,7 +17,7 @@
1717
1818import { Hono } from "hono";
1919import { Layout } from "../views/layout";
20import { RepoHeader } from "../views/components";
20import { RepoHeader, PageHeader, Badge, Button, sharedComponentStyles } from "../views/components";
2121import { DiffView } from "../views/diff-view";
2222import { IssueNav } from "./issues";
2323import {
@@ -39,58 +39,6 @@ compare.use("*", softAuth);
3939 * Tokens come from layout.tsx `:root` for light/dark consistency.
4040 * ──────────────────────────────────────────────────────────────────── */
4141const COMPARE_STYLES = `
42 .compare-hero {
43 position: relative;
44 margin: 0 0 var(--space-5);
45 padding: 22px 26px 24px;
46 background: var(--bg-elevated);
47 border: 1px solid var(--border);
48 border-radius: 16px;
49 overflow: hidden;
50 }
51 .compare-hero::before {
52 content: '';
53 position: absolute; top: 0; left: 0; right: 0;
54 height: 2px;
55 background: linear-gradient(90deg, transparent 0%, #5b6ee8 30%, #5f8fa0 70%, transparent 100%);
56 opacity: 0.7;
57 pointer-events: none;
58 }
59 .compare-hero-inner { position: relative; z-index: 1; }
60 .compare-eyebrow {
61 font-size: 12px;
62 font-family: var(--font-mono);
63 color: var(--text-muted);
64 letter-spacing: 0.1em;
65 text-transform: uppercase;
66 font-weight: 600;
67 margin-bottom: 8px;
68 }
69 .compare-eyebrow strong { color: var(--accent); font-weight: 600; }
70 .compare-title {
71 font-family: var(--font-display);
72 font-size: clamp(26px, 3.4vw, 34px);
73 font-weight: 800;
74 letter-spacing: -0.025em;
75 line-height: 1.06;
76 margin: 0 0 8px;
77 color: var(--text-strong);
78 }
79 .compare-title .gradient-text {
80 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
81 -webkit-background-clip: text;
82 background-clip: text;
83 -webkit-text-fill-color: transparent;
84 color: transparent;
85 }
86 .compare-sub {
87 font-size: 14.5px;
88 color: var(--text-muted);
89 line-height: 1.5;
90 margin: 0;
91 max-width: 640px;
92 }
93
9442 /* Branch selector — base ← head */
9543 .compare-branches {
9644 display: flex;
@@ -173,85 +121,15 @@ const COMPARE_STYLES = `
173121 .compare-form-actions {
174122 display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
175123 }
176 .compare-cta {
177 display: inline-flex; align-items: center; gap: 8px;
178 padding: 10px 18px;
179 border-radius: 10px;
180 font-size: 13.5px;
181 font-weight: 600;
182 color: #fff;
183 background: linear-gradient(135deg, #5b6ee8 0%, #6f5be8 60%, #5f8fa0 140%);
184 border: 1px solid rgba(91,110,232,0.55);
185 box-shadow: 0 6px 18px -8px rgba(91,110,232,0.55);
186 text-decoration: none;
187 cursor: pointer;
188 transition: transform 120ms ease, box-shadow 160ms ease, filter 160ms ease;
189 }
190 .compare-cta:hover {
191 transform: translateY(-1px);
192 box-shadow: 0 10px 22px -6px rgba(91,110,232,0.6);
193 color: #fff;
194 }
195 .compare-cta.is-disabled,
196 .compare-cta[aria-disabled="true"] {
197 opacity: 0.55;
198 cursor: not-allowed;
199 filter: grayscale(0.4);
200 transform: none;
201 box-shadow: none;
202 }
203 .compare-cta-secondary {
204 display: inline-flex; align-items: center; gap: 6px;
205 padding: 9px 14px;
206 border-radius: 10px;
207 font-size: 13px;
208 font-weight: 500;
209 color: var(--text);
210 background: var(--bg-secondary);
211 border: 1px solid var(--border);
212 text-decoration: none;
213 transition: border-color 140ms ease, color 140ms ease;
214 }
215 .compare-cta-secondary:hover {
216 border-color: var(--border-strong);
217 color: var(--text-strong);
218 }
219124
220125 /* Summary stats — commits ahead / behind */
221126 .compare-stats {
222127 display: flex;
223128 flex-wrap: wrap;
129 align-items: center;
224130 gap: 8px;
225131 margin: 0 0 var(--space-4);
226132 }
227 .compare-stat-badge {
228 display: inline-flex; align-items: center; gap: 6px;
229 padding: 6px 12px;
230 border-radius: 9999px;
231 font-size: 12.5px;
232 font-weight: 600;
233 font-family: var(--font-mono);
234 font-variant-numeric: tabular-nums;
235 border: 1px solid var(--border);
236 background: var(--bg-secondary);
237 color: var(--text);
238 }
239 .compare-stat-badge .compare-stat-count { color: var(--text-strong); }
240 .compare-stat-badge.is-ahead {
241 color: var(--green);
242 border-color: rgba(52,211,153,0.32);
243 background: rgba(52,211,153,0.10);
244 }
245 .compare-stat-badge.is-behind {
246 color: var(--red);
247 border-color: rgba(248,113,113,0.32);
248 background: rgba(248,113,113,0.10);
249 }
250 .compare-stat-badge.is-files {
251 color: var(--text-link);
252 border-color: rgba(91,110,232,0.32);
253 background: rgba(91,110,232,0.08);
254 }
255133
256134 /* Diff stats bar — +/- counts */
257135 .compare-diffstats {
@@ -382,9 +260,7 @@ const COMPARE_STYLES = `
382260 }
383261
384262 @media (max-width: 720px) {
385 .compare-hero { padding: 18px 18px 20px; }
386263 .compare-form-actions { width: 100%; }
387 .compare-cta, .compare-cta-secondary { flex: 1 1 auto; justify-content: center; }
388264 }
389265`;
390266
@@ -434,22 +310,14 @@ compare.get("/:owner/:repo/compare/:spec?", async (c) => {
434310 <Layout title={`Compare — ${owner}/${repo}`} user={user}>
435311 <RepoHeader owner={owner} repo={repo} />
436312 <IssueNav owner={owner} repo={repo} active="code" />
313 <style dangerouslySetInnerHTML={{ __html: sharedComponentStyles }} />
437314 <style dangerouslySetInnerHTML={{ __html: COMPARE_STYLES }} />
438315
439 <section class="compare-hero">
440 <div class="compare-hero-inner">
441 <div class="compare-eyebrow">
442 Compare changes · <strong>{owner}/{repo}</strong>
443 </div>
444 <h1 class="compare-title">
445 <span class="gradient-text">Compare</span> branches.
446 </h1>
447 <p class="compare-sub">
448 Pick a base and a head branch to see exactly what changed.
449 Open a pull request when the diff looks right.
450 </p>
451 </div>
452 </section>
316 <PageHeader
317 eyebrow={`Compare changes · ${owner}/${repo}`}
318 title="Compare branches."
319 lede="Pick a base and a head branch to see exactly what changed. Open a pull request when the diff looks right."
320 />
453321
454322 <form method="get" action={`/${owner}/${repo}/compare`}>
455323 <div class="compare-branches">
@@ -499,19 +367,16 @@ compare.get("/:owner/:repo/compare/:spec?", async (c) => {
499367 </div>
500368
501369 <div class="compare-form-actions">
502 <button
370 <Button
371 variant="primary"
503372 type="submit"
504 class="compare-cta"
505373 onclick={`this.form.action='/${owner}/${repo}/compare/'+this.form.base.value+'...'+this.form.head.value; return true;`}
506374 >
507375 Compare branches →
508 </button>
509 <a
510 href={`/${owner}/${repo}`}
511 class="compare-cta-secondary"
512 >
376 </Button>
377 <Button variant="secondary" href={`/${owner}/${repo}`}>
513378 Cancel
514 </a>
379 </Button>
515380 </div>
516381 </form>
517382 </Layout>
@@ -580,76 +445,52 @@ compare.get("/:owner/:repo/compare/:spec?", async (c) => {
580445 const isIdentical = base === head;
581446 const hasChanges = commitsBetween.length > 0 || files.length > 0;
582447
448 const cmpTitle = hasChanges
449 ? `${commitsBetween.length} commit${commitsBetween.length !== 1 ? "s" : ""} ahead.`
450 : isIdentical
451 ? "Nothing to compare."
452 : "No changes between branches.";
453 const cmpSuffix = hasChanges
454 ? " — review the diff below, then open a pull request when it looks right."
455 : isIdentical
456 ? " — base and head point to the same ref. Pick different branches to see a diff."
457 : " — these branches diverge but produce no diff. Nothing to merge.";
458 const cmpLede = `${base} ← ${head}${cmpSuffix}`;
459
583460 return c.html(
584461 <Layout title={`${base}...${head} — ${owner}/${repo}`} user={user}>
585462 <RepoHeader owner={owner} repo={repo} />
586463 <IssueNav owner={owner} repo={repo} active="code" />
464 <style dangerouslySetInnerHTML={{ __html: sharedComponentStyles }} />
587465 <style dangerouslySetInnerHTML={{ __html: COMPARE_STYLES }} />
588466
589 <section class="compare-hero">
590 <div class="compare-hero-inner">
591 <div class="compare-eyebrow">
592 Comparing · <strong>{owner}/{repo}</strong>
593 </div>
594 <h1 class="compare-title">
595 {hasChanges ? (
596 <>
597 <span class="gradient-text">
598 {commitsBetween.length} commit
599 {commitsBetween.length !== 1 ? "s" : ""}
600 </span>{" "}
601 ahead.
602 </>
603 ) : isIdentical ? (
604 <>
605 <span class="gradient-text">Nothing</span> to compare.
606 </>
607 ) : (
608 <>
609 <span class="gradient-text">No changes</span> between branches.
610 </>
611 )}
612 </h1>
613 <p class="compare-sub">
614 <span style="font-family:var(--font-mono);color:var(--text)">
615 {base}
616 </span>{" "}
617 <span style="color:var(--text-faint)">←</span>{" "}
618 <span style="font-family:var(--font-mono);color:var(--text)">
619 {head}
620 </span>
621 {hasChanges
622 ? " — review the diff below, then open a pull request when it looks right."
623 : isIdentical
624 ? " — base and head point to the same ref. Pick different branches to see a diff."
625 : " — these branches diverge but produce no diff. Nothing to merge."}
626 </p>
627 </div>
628 </section>
467 <PageHeader
468 eyebrow={`Comparing · ${owner}/${repo}`}
469 title={cmpTitle}
470 lede={cmpLede}
471 />
629472
630473 {/* Summary stats row */}
631474 {hasChanges && (
632475 <div class="compare-stats">
633 <span class="compare-stat-badge is-ahead">
634 <span class="compare-stat-count">+{commitsBetween.length}</span>
635 commit{commitsBetween.length !== 1 ? "s" : ""} ahead
636 </span>
637 <span class="compare-stat-badge is-files">
638 <span class="compare-stat-count">{files.length}</span>
639 file{files.length !== 1 ? "s" : ""} changed
640 </span>
476 <Badge variant="success">
477 +{commitsBetween.length} commit{commitsBetween.length !== 1 ? "s" : ""} ahead
478 </Badge>
479 <Badge variant="info">
480 {files.length} file{files.length !== 1 ? "s" : ""} changed
481 </Badge>
641482 <span class="compare-diffstats">
642483 <span class="compare-add">+{totalAdditions}</span>
643484 <span class="compare-del">−{totalDeletions}</span>
644485 <span class="compare-files-c">lines</span>
645486 </span>
646 <a
487 <Button
488 variant="primary"
647489 href={`/${owner}/${repo}/pulls/new?base=${encodeURIComponent(base)}&head=${encodeURIComponent(head)}`}
648 class="compare-cta"
649490 style="margin-left:auto"
650491 >
651492 Create pull request →
652 </a>
493 </Button>
653494 </div>
654495 )}
655496
@@ -670,19 +511,16 @@ compare.get("/:owner/:repo/compare/:spec?", async (c) => {
670511 class="compare-form-actions"
671512 style="justify-content:center;margin-top:16px"
672513 >
673 <a
674 href={`/${owner}/${repo}/compare`}
675 class="compare-cta-secondary"
676 >
514 <Button variant="secondary" href={`/${owner}/${repo}/compare`}>
677515 ← Pick different branches
678 </a>
679 <span
680 class="compare-cta is-disabled"
681 aria-disabled="true"
516 </Button>
517 <Button
518 variant="primary"
519 disabled
682520 title="Nothing to compare yet"
683521 >
684522 Create pull request →
685 </span>
523 </Button>
686524 </div>
687525 </div>
688526 )}
@@ -691,10 +529,7 @@ compare.get("/:owner/:repo/compare/:spec?", async (c) => {
691529 <>
692530 <div class="compare-section-head">
693531 <h2 class="compare-section-title">Commits in this comparison</h2>
694 <span class="compare-stat-badge">
695 <span class="compare-stat-count">{commitsBetween.length}</span>
696 total
697 </span>
532 <Badge variant="neutral">{commitsBetween.length} total</Badge>
698533 </div>
699534 <div class="compare-commits">
700535 {commitsBetween.map((cm) => (
Modifiedsrc/routes/contributors.tsx+10−87View fileUnifiedSplit
@@ -9,7 +9,7 @@
99
1010import { Hono } from "hono";
1111import { Layout } from "../views/layout";
12import { RepoHeader, RepoNav } from "../views/components";
12import { RepoHeader, RepoNav, PageHeader, Badge, sharedComponentStyles } from "../views/components";
1313import { getRepoPath, repoExists, getDefaultBranch } from "../git/repository";
1414import { softAuth } from "../middleware/auth";
1515import type { AuthEnv } from "../middleware/auth";
@@ -159,20 +159,11 @@ contributors.get("/:owner/:repo/contributors", async (c) => {
159159 <RepoHeader owner={owner} repo={repo} />
160160 <RepoNav owner={owner} repo={repo} active="code" />
161161 <div class="contrib-wrap">
162 <header class="contrib-head">
163 <div class="contrib-eyebrow">
164 <span class="contrib-eyebrow-dot" aria-hidden="true" />
165 Repository · Contributors
166 </div>
167 <h1 class="contrib-title">
168 <span class="contrib-title-grad">Who built this.</span>
169 </h1>
170 <p class="contrib-sub">
171 Everyone with a commit on{" "}
172 <code class="contrib-ref">{ref}</code>, ranked by total commits.
173 Bars below show weekly activity over the last year.
174 </p>
175 </header>
162 <PageHeader
163 eyebrow="Repository · Contributors"
164 title="Who built this."
165 lede={`Everyone with a commit on ${ref}, ranked by total commits. Bars below show weekly activity over the last year.`}
166 />
176167
177168 {/* ─── Stats strip ─── */}
178169 {contribs.length > 0 && (
@@ -276,7 +267,7 @@ contributors.get("/:owner/:repo/contributors", async (c) => {
276267 const handle = handleFromEmail(ctb.email, ctb.name);
277268 const initial = (ctb.name || ctb.email || "?")[0]?.toUpperCase() ?? "?";
278269 const role = idx === 0 ? "Maintainer" : "Contributor";
279 const roleClass = idx === 0 ? "contrib-pill is-maintainer" : "contrib-pill is-contributor";
270 const roleVariant = idx === 0 ? "info" : "neutral";
280271 return (
281272 <div class="contrib-card">
282273 <div class="contrib-rank" aria-hidden="true">
@@ -290,10 +281,7 @@ contributors.get("/:owner/:repo/contributors", async (c) => {
290281 <a href={`/${handle}`} class="contrib-card-name">
291282 {ctb.name}
292283 </a>
293 <span class={roleClass}>
294 <span class="dot" aria-hidden="true" />
295 {role}
296 </span>
284 <Badge variant={roleVariant} dot>{role}</Badge>
297285 </div>
298286 <div class="contrib-card-handle">@{handle}</div>
299287 <div class="contrib-meta-row">
@@ -325,6 +313,7 @@ contributors.get("/:owner/:repo/contributors", async (c) => {
325313 </>
326314 )}
327315 </div>
316 <style dangerouslySetInnerHTML={{ __html: sharedComponentStyles }} />
328317 <style dangerouslySetInnerHTML={{ __html: contribStyles }} />
329318 </Layout>
330319 );
@@ -395,50 +384,7 @@ function IconCommit() {
395384const contribStyles = `
396385 .contrib-wrap { max-width: 1680px; margin: 0 auto; padding: var(--space-5) var(--space-4) var(--space-8); }
397386
398 /* ─── Header strip ─── */
399 .contrib-head { margin-bottom: var(--space-5); }
400 .contrib-eyebrow {
401 display: inline-flex;
402 align-items: center;
403 gap: 8px;
404 text-transform: uppercase;
405 font-family: var(--font-mono);
406 font-size: 11px;
407 letter-spacing: 0.16em;
408 color: var(--text-muted);
409 font-weight: 600;
410 margin-bottom: 10px;
411 }
412 .contrib-eyebrow-dot {
413 width: 8px; height: 8px;
414 border-radius: 9999px;
415 background: linear-gradient(135deg, #5b6ee8, #5f8fa0);
416 box-shadow: 0 0 0 3px rgba(91,110,232,0.18);
417 }
418 .contrib-title {
419 font-family: var(--font-display);
420 font-size: clamp(24px, 3.4vw, 36px);
421 font-weight: 800;
422 letter-spacing: -0.028em;
423 line-height: 1.1;
424 margin: 0 0 6px;
425 color: var(--text-strong);
426 }
427 .contrib-title-grad {
428 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
429 -webkit-background-clip: text;
430 background-clip: text;
431 -webkit-text-fill-color: transparent;
432 color: transparent;
433 }
434 .contrib-sub {
435 margin: 0;
436 font-size: 14px;
437 color: var(--text-muted);
438 line-height: 1.5;
439 max-width: 720px;
440 }
441 .contrib-ref,
387 /* ─── Inline ref chip (activity section) ─── */
442388 .contrib-ref-inline {
443389 font-family: var(--font-mono);
444390 font-size: 12.5px;
@@ -669,29 +615,6 @@ const contribStyles = `
669615 font-weight: 600;
670616 }
671617
672 /* ─── Role pills ─── */
673 .contrib-pill {
674 display: inline-flex;
675 align-items: center;
676 gap: 6px;
677 padding: 3px 9px;
678 border-radius: 9999px;
679 font-size: 11px;
680 font-weight: 600;
681 letter-spacing: 0.02em;
682 }
683 .contrib-pill .dot { width: 6px; height: 6px; border-radius: 9999px; background: currentColor; }
684 .contrib-pill.is-maintainer {
685 background: rgba(91,110,232,0.16);
686 color: #c4b5fd;
687 box-shadow: inset 0 0 0 1px rgba(91,110,232,0.32);
688 }
689 .contrib-pill.is-contributor {
690 background: rgba(148,163,184,0.16);
691 color: #cbd5e1;
692 box-shadow: inset 0 0 0 1px rgba(148,163,184,0.30);
693 }
694
695618 /* ─── Empty state ─── */
696619 .contrib-empty {
697620 position: relative;
Modifiedsrc/routes/explore.tsx+41−205View fileUnifiedSplit
@@ -19,6 +19,7 @@ import { eq, desc, sql, and } from "drizzle-orm";
1919import { db } from "../db";
2020import { repositories, users, repoTopics } from "../db/schema";
2121import { Layout } from "../views/layout";
22import { PageHeader, Badge, Button, sharedComponentStyles } from "../views/components";
2223import { softAuth } from "../middleware/auth";
2324import type { AuthEnv } from "../middleware/auth";
2425
@@ -32,112 +33,11 @@ const ExploreStyle = () => (
3233 __html: `
3334 .explore-wrap { max-width: 1680px; margin: 0 auto; }
3435
35 /* ─── Hero ─── */
36 .explore-hero {
37 position: relative;
38 margin: 4px 0 28px;
39 padding: 40px 36px 36px;
40 background: var(--bg-elevated);
41 border: 1px solid var(--border);
42 border-radius: 18px;
43 overflow: hidden;
44 }
45 .explore-hero::before {
46 content: '';
47 position: absolute;
48 top: 0; left: 0; right: 0;
49 height: 2px;
50 background: linear-gradient(90deg, transparent 0%, #5b6ee8 30%, #5f8fa0 70%, transparent 100%);
51 opacity: 0.75;
52 pointer-events: none;
53 }
54 .explore-hero-bg {
55 position: absolute;
56 inset: -35% -12% auto auto;
57 width: 460px;
58 height: 460px;
59 pointer-events: none;
60 z-index: 0;
61 }
62 .explore-hero-orb {
63 position: absolute;
64 inset: 0;
65 background: radial-gradient(circle, rgba(91,110,232,0.22), rgba(95,143,160,0.10) 45%, transparent 70%);
66 filter: blur(80px);
67 opacity: 0.75;
68 animation: exploreHeroOrb 16s ease-in-out infinite;
69 }
70 .explore-hero-orb-2 {
71 position: absolute;
72 inset: auto auto -25% -15%;
73 width: 320px; height: 320px;
74 background: radial-gradient(circle, rgba(95,143,160,0.16), rgba(91,110,232,0.06) 50%, transparent 75%);
75 filter: blur(70px);
76 opacity: 0.55;
77 pointer-events: none;
78 z-index: 0;
79 animation: exploreHeroOrb2 18s ease-in-out infinite;
80 }
36 /* ─── Hero search (header now rendered via shared <PageHeader>) ─── */
8137 @keyframes exploreHeroOrb {
8238 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.65; }
8339 50% { transform: scale(1.12) translate(-14px, 10px); opacity: 0.92; }
8440 }
85 exploreHeroOrb2 {
86 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.45; }
87 50% { transform: scale(1.08) translate(16px, -8px); opacity: 0.72; }
88 }
89 (prefers-reduced-motion: reduce) {
90 .explore-hero-orb, .explore-hero-orb-2 { animation: none; }
91 }
92 .explore-hero-inner {
93 position: relative;
94 z-index: 1;
95 display: flex;
96 flex-direction: column;
97 gap: 20px;
98 }
99 .explore-hero-text { flex: 1; min-width: 280px; }
100 .explore-hero-eyebrow {
101 display: inline-flex;
102 align-items: center;
103 gap: 8px;
104 font-size: 12px;
105 color: var(--text-muted);
106 margin-bottom: 12px;
107 letter-spacing: 0.10em;
108 text-transform: uppercase;
109 font-weight: 700;
110 }
111 .explore-hero-eyebrow-dot {
112 display: inline-block;
113 width: 6px; height: 6px;
114 border-radius: 9999px;
115 background: linear-gradient(135deg, #5b6ee8, #5f8fa0);
116 box-shadow: 0 0 0 3px rgba(91,110,232,0.18);
117 }
118 .explore-hero-title {
119 font-family: var(--font-display);
120 font-size: clamp(40px, 6.5vw, 72px);
121 font-weight: 800;
122 letter-spacing: -0.032em;
123 line-height: 1.02;
124 margin: 0 0 14px;
125 color: var(--text-strong);
126 }
127 .explore-hero-title-grad {
128 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 45%, #5f8fa0 100%);
129 -webkit-background-clip: text;
130 background-clip: text;
131 -webkit-text-fill-color: transparent;
132 color: transparent;
133 }
134 .explore-hero-sub {
135 font-size: 15.5px;
136 color: var(--text-muted);
137 margin: 0;
138 line-height: 1.55;
139 max-width: 640px;
140 }
14141 .explore-hero-search {
14242 display: flex;
14343 gap: 10px;
@@ -182,7 +82,6 @@ const ExploreStyle = () => (
18282 }
18383 .explore-hero-search .btn { padding: 12px 22px; border-radius: 12px; }
18484 @media (max-width: 720px) {
185 .explore-hero { padding: 28px 22px 26px; }
18685 .explore-hero-search .btn { flex: 1; min-width: 0; }
18786 }
18887
@@ -433,37 +332,6 @@ const ExploreStyle = () => (
433332 gap: 6px;
434333 flex-wrap: wrap;
435334 }
436 .explore-pill {
437 display: inline-flex;
438 align-items: center;
439 gap: 5px;
440 padding: 2px 10px;
441 border-radius: 9999px;
442 font-size: 11.5px;
443 font-weight: 600;
444 line-height: 1.5;
445 letter-spacing: 0.005em;
446 }
447 .explore-pill-private {
448 background: rgba(255,180,94,0.10);
449 color: #ffb45e;
450 border: 1px solid rgba(255,180,94,0.28);
451 }
452 .explore-pill-fork {
453 background: rgba(95,143,160,0.10);
454 color: #5f8fa0;
455 border: 1px solid rgba(95,143,160,0.28);
456 }
457 .explore-pill-archived {
458 background: rgba(255,255,255,0.04);
459 color: var(--text-muted);
460 border: 1px solid var(--border);
461 }
462 .explore-pill-template {
463 background: rgba(91,110,232,0.12);
464 color: var(--text-strong);
465 border: 1px solid rgba(91,110,232,0.30);
466 }
467335
468336 /* ─── Loading skeleton (used when JS later swaps in async fragments) ─── */
469337 .explore-skel-grid {
@@ -655,6 +523,8 @@ const ExploreStyle = () => (
655523 line-height: 1.5;
656524 }
657525 .explore-gh-callout-cta { flex-shrink: 0; white-space: nowrap; }
526
527 ${sharedComponentStyles}
658528 `,
659529 }}
660530 />
@@ -707,27 +577,11 @@ explore.get("/explore", async (c) => {
707577 >
708578 <ExploreStyle />
709579 <div class="explore-wrap">
710 <section class="explore-hero" aria-hidden="true">
711 <div class="explore-hero-bg">
712 <div class="explore-hero-orb" />
713 </div>
714 <div class="explore-hero-orb-2" />
715 <div class="explore-hero-inner">
716 <div class="explore-hero-text">
717 <div class="explore-hero-eyebrow">
718 <span class="explore-hero-eyebrow-dot" />
719 Discover
720 </div>
721 <h1 class="explore-hero-title">
722 What’s{" "}
723 <span class="explore-hero-title-grad">shipping</span>.
724 </h1>
725 <p class="explore-hero-sub">
726 Loading public repositories…
727 </p>
728 </div>
729 </div>
730 </section>
580 <PageHeader
581 eyebrow="Discover"
582 title="What’s shipping."
583 lede="Loading public repositories…"
584 />
731585 <div class="explore-skel-grid" aria-hidden="true">
732586 {Array.from({ length: 9 }).map(() => (
733587 <div class="explore-skel">
@@ -860,29 +714,17 @@ explore.get("/explore", async (c) => {
860714 >
861715 <ExploreStyle />
862716 <div class="explore-wrap">
863 <section class="explore-hero">
864 <div class="explore-hero-bg" aria-hidden="true">
865 <div class="explore-hero-orb" />
866 </div>
867 <div class="explore-hero-orb-2" aria-hidden="true" />
868 <div class="explore-hero-inner">
869 <div class="explore-hero-text">
870 <div class="explore-hero-eyebrow">
871 <span class="explore-hero-eyebrow-dot" aria-hidden="true" />
872 Discover
873 </div>
874 <h1 class="explore-hero-title">
875 What’s{" "}
876 <span class="explore-hero-title-grad">shipping</span>.
877 </h1>
878 <p class="explore-hero-sub">
879 {q
880 ? `Searching public repos for "${q}".`
881 : topic
882 ? `Public repos tagged with #${topic}.`
883 : "Browse public repositories built on Gluecron — AI-reviewed code, gate-checked pushes, and the people shipping them."}
884 </p>
885 </div>
717 <PageHeader
718 eyebrow="Discover"
719 title="What’s shipping."
720 lede={
721 q
722 ? `Searching public repos for "${q}".`
723 : topic
724 ? `Public repos tagged with #${topic}.`
725 : "Browse public repositories built on Gluecron — AI-reviewed code, gate-checked pushes, and the people shipping them."
726 }
727 actions={
886728 <form
887729 method="get"
888730 action="/explore"
@@ -924,8 +766,8 @@ explore.get("/explore", async (c) => {
924766 Search
925767 </button>
926768 </form>
927 </div>
928 </section>
769 }
770 />
929771
930772 {topic && (
931773 <div class="explore-topic-row">
@@ -1069,17 +911,17 @@ explore.get("/explore", async (c) => {
1069911 : "When public repositories are created they'll show up here. Be the first to ship something."}
1070912 </p>
1071913 <div class="explore-empty-cta">
1072 <a href="/explore" class="btn btn-primary">
914 <Button href="/explore" variant="primary">
1073915 Reset filters
1074 </a>
916 </Button>
1075917 {user ? (
1076 <a href="/new" class="btn">
918 <Button href="/new" variant="secondary">
1077919 New repository
1078 </a>
920 </Button>
1079921 ) : (
1080 <a href="/register" class="btn">
922 <Button href="/register" variant="secondary">
1081923 Get started
1082 </a>
924 </Button>
1083925 )}
1084926 </div>
1085927 </div>
@@ -1122,24 +964,18 @@ explore.get("/explore", async (c) => {
1122964 repo.isTemplate) && (
1123965 <div class="explore-card-badges">
1124966 {repo.isPrivate && (
1125 <span class="explore-pill explore-pill-private">
1126 Private
1127 </span>
967 <Badge variant="warn">Private</Badge>
1128968 )}
1129969 {repo.forkedFromId && (
1130 <span class="explore-pill explore-pill-fork">
970 <Badge variant="info">
1131971 <span aria-hidden="true">{"⑂"}</span> Fork
1132 </span>
972 </Badge>
1133973 )}
1134974 {repo.isTemplate && (
1135 <span class="explore-pill explore-pill-template">
1136 Template
1137 </span>
975 <Badge variant="neutral">Template</Badge>
1138976 )}
1139977 {repo.isArchived && (
1140 <span class="explore-pill explore-pill-archived">
1141 Archived
1142 </span>
978 <Badge variant="neutral">Archived</Badge>
1143979 )}
1144980 </div>
1145981 )}
@@ -1202,17 +1038,17 @@ explore.get("/explore", async (c) => {
12021038 search or a topic filter to surface more repositories.
12031039 </p>
12041040 <div class="explore-foot-actions">
1205 <a href="/explore" class="btn">
1041 <Button href="/explore" variant="secondary">
12061042 Reset
1207 </a>
1043 </Button>
12081044 {user ? (
1209 <a href="/new" class="btn btn-primary">
1045 <Button href="/new" variant="primary">
12101046 New repository
1211 </a>
1047 </Button>
12121048 ) : (
1213 <a href="/register" class="btn btn-primary">
1049 <Button href="/register" variant="primary">
12141050 Sign up
1215 </a>
1051 </Button>
12161052 )}
12171053 </div>
12181054 </div>
@@ -1236,9 +1072,9 @@ explore.get("/explore", async (c) => {
12361072 manual setup, no SaaS migration project required.
12371073 </p>
12381074 </div>
1239 <a href="/import" class="btn btn-primary explore-gh-callout-cta">
1075 <Button href="/import" variant="primary" class="explore-gh-callout-cta">
12401076 Migrate from GitHub →
1241 </a>
1077 </Button>
12421078 </div>
12431079 </div>
12441080 </Layout>
Modifiedsrc/routes/insights.tsx+11−18View fileUnifiedSplit
@@ -12,7 +12,7 @@
1212
1313import { Hono } from "hono";
1414import { Layout } from "../views/layout";
15import { RepoHeader, RepoNav } from "../views/components";
15import { RepoHeader, RepoNav, PageHeader, sharedComponentStyles } from "../views/components";
1616import {
1717 getFileTimeline,
1818 getFunctionTimeline,
@@ -550,22 +550,11 @@ insights.get("/:owner/:repo/insights", async (c) => {
550550 <RepoHeader owner={owner} repo={repo} />
551551 <RepoNav owner={owner} repo={repo} active="insights" />
552552 <div class="insights-wrap">
553 <section class="insights-hero">
554 <div class="insights-hero-orb" aria-hidden="true" />
555 <div class="insights-hero-inner">
556 <div class="insights-eyebrow">
557 <span class="insights-eyebrow-dot" aria-hidden="true" />
558 Insights · {owner}/{repo}
559 </div>
560 <h2 class="insights-title">
561 <span class="insights-title-grad">Code intelligence.</span>
562 </h2>
563 <p class="insights-sub">
564 Delivery, quality, code-health, and people signals for this
565 repository — the reports GitHub doesn't ship.
566 </p>
567 </div>
568 </section>
553 <PageHeader
554 eyebrow={`Insights · ${owner}/${repo}`}
555 title="Code intelligence."
556 lede="Delivery, quality, code-health, and people signals for this repository — the reports GitHub doesn't ship."
557 />
569558
570559 {groups.map((g) => (
571560 <>
@@ -578,7 +567,10 @@ insights.get("/:owner/:repo/insights", async (c) => {
578567 {g.cards.map((card) => (
579568 <a
580569 href={card.href}
581 style="display:block;padding:16px 18px;background:var(--bg-elevated);border:1px solid var(--border);border-radius:12px;text-decoration:none;transition:border-color 120ms ease,transform 120ms ease"
570 class="gx-card"
571 data-pad="md"
572 data-interactive=""
573 style="display:block;text-decoration:none"
582574 >
583575 <div style="display:flex;align-items:center;gap:9px;margin-bottom:6px">
584576 <span aria-hidden="true" style="font-size:18px">{card.icon}</span>
@@ -592,6 +584,7 @@ insights.get("/:owner/:repo/insights", async (c) => {
592584 </>
593585 ))}
594586 </div>
587 <style dangerouslySetInnerHTML={{ __html: sharedComponentStyles }} />
595588 <style dangerouslySetInnerHTML={{ __html: styles }} />
596589 </Layout>
597590 );
Modifiedsrc/routes/issues.tsx+44−179View fileUnifiedSplit
@@ -17,7 +17,14 @@ import {
1717 milestones,
1818} from "../db/schema";
1919import { Layout } from "../views/layout";
20import { RepoHeader, RepoNav } from "../views/components";
20import {
21 RepoHeader,
22 RepoNav,
23 PageHeader as GxPageHeader,
24 Button as GxButton,
25 Badge as GxBadge,
26 sharedComponentStyles,
27} from "../views/components";
2128import { PendingCommentsBanner } from "../views/pending-comments-banner";
2229import { ReactionsBar } from "../views/reactions";
2330import { summariseReactions } from "../lib/reactions";
@@ -68,100 +75,6 @@ const issueRoutes = new Hono<AuthEnv>();
6875// other routes/shared views. All design tokens come from :root in layout.tsx.
6976// ---------------------------------------------------------------------------
7077const issuesStyles = `
71 /* Hero card — list page */
72 .issues-hero {
73 position: relative;
74 margin: 4px 0 24px;
75 padding: 28px 32px;
76 background: var(--bg-elevated);
77 border: 1px solid var(--border);
78 border-radius: 16px;
79 overflow: hidden;
80 }
81 .issues-hero::before {
82 content: '';
83 position: absolute;
84 top: 0; left: 0; right: 0;
85 height: 2px;
86 background: linear-gradient(90deg, transparent 0%, #5b6ee8 30%, #5f8fa0 70%, transparent 100%);
87 opacity: 0.7;
88 pointer-events: none;
89 }
90 .issues-hero-bg {
91 position: absolute;
92 inset: -30% -10% auto auto;
93 width: 360px;
94 height: 360px;
95 pointer-events: none;
96 z-index: 0;
97 }
98 .issues-hero-orb {
99 position: absolute;
100 inset: 0;
101 background: radial-gradient(circle, rgba(91,110,232,0.18), rgba(95,143,160,0.09) 45%, transparent 70%);
102 filter: blur(80px);
103 opacity: 0.7;
104 animation: issuesHeroOrb 14s ease-in-out infinite;
105 }
106 issuesHeroOrb {
107 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.6; }
108 50% { transform: scale(1.1) translate(-12px, 8px); opacity: 0.85; }
109 }
110 (prefers-reduced-motion: reduce) {
111 .issues-hero-orb { animation: none; }
112 }
113 .issues-hero-inner {
114 position: relative;
115 z-index: 1;
116 display: flex;
117 justify-content: space-between;
118 align-items: flex-end;
119 gap: 24px;
120 flex-wrap: wrap;
121 }
122 .issues-hero-text { flex: 1; min-width: 280px; }
123 .issues-hero-eyebrow {
124 font-size: 12.5px;
125 color: var(--text-muted);
126 margin-bottom: 8px;
127 letter-spacing: 0.04em;
128 text-transform: uppercase;
129 font-weight: 600;
130 }
131 .issues-hero-eyebrow .issues-hero-repo {
132 color: var(--accent);
133 text-transform: none;
134 letter-spacing: -0.005em;
135 font-weight: 600;
136 }
137 .issues-hero-title {
138 font-family: var(--font-display);
139 font-size: clamp(28px, 4vw, 40px);
140 font-weight: 800;
141 letter-spacing: -0.028em;
142 line-height: 1.05;
143 margin: 0 0 10px;
144 color: var(--text-strong);
145 }
146 .issues-hero-sub {
147 font-size: 15px;
148 color: var(--text-muted);
149 margin: 0;
150 line-height: 1.5;
151 max-width: 580px;
152 }
153 .issues-hero-actions {
154 display: flex;
155 gap: 8px;
156 flex-wrap: wrap;
157 }
158 (max-width: 720px) {
159 .issues-hero { padding: 24px 20px; }
160 .issues-hero-inner { flex-direction: column; align-items: flex-start; }
161 .issues-hero-actions { width: 100%; }
162 .issues-hero-actions .btn { flex: 1; min-width: 0; }
163 }
164
16578 /* Mobile rules — added in the 720px sweep. Kept additive only. */
16679 @media (max-width: 720px) {
16780 .issues-toolbar { flex-direction: column; align-items: stretch; }
@@ -459,27 +372,6 @@ const issuesStyles = `
459372 color: var(--text-muted);
460373 }
461374 .issues-detail-attr strong { color: var(--text); font-weight: 600; }
462 .issues-state-pill {
463 display: inline-flex;
464 align-items: center;
465 gap: 6px;
466 padding: 5px 12px;
467 border-radius: 9999px;
468 font-size: 12.5px;
469 font-weight: 600;
470 line-height: 1.4;
471 letter-spacing: 0.005em;
472 }
473 .issues-state-pill.is-open {
474 background: rgba(52,211,153,0.12);
475 color: var(--green);
476 border: 1px solid rgba(52,211,153,0.35);
477 }
478 .issues-state-pill.is-closed {
479 background: rgba(182,157,255,0.12);
480 color: var(--accent);
481 border: 1px solid rgba(182,157,255,0.35);
482 }
483375 .issues-detail-spacer { flex: 1; }
484376
485377 /* Sub-issues / epics */
@@ -896,6 +788,12 @@ const IssuesStyle = () => (
896788 <style dangerouslySetInnerHTML={{ __html: issuesStyles }} />
897789);
898790
791// Shared design-system primitive styles (PageHeader / Button / Badge).
792// Idempotent — safe to include once per page that uses the gx-* primitives.
793const SharedStyle = () => (
794 <style dangerouslySetInnerHTML={{ __html: sharedComponentStyles }} />
795);
796
899797// Inline empty-state SVG — a softly-tinted speech-bubble icon. No external assets.
900798const IssuesEmptySvg = () => (
901799 <svg
@@ -1161,6 +1059,7 @@ issueRoutes.get("/:owner/:repo/issues", softAuth, requireRepoAccess("read"), asy
11611059 return c.html(
11621060 <Layout title={`Issues — ${ownerName}/${repoName}`} user={user}>
11631061 <IssuesStyle />
1062 <SharedStyle />
11641063 <RepoHeader owner={ownerName} repo={repoName} />
11651064 <IssueNav owner={ownerName} repo={repoName} active="issues" />
11661065 <PendingCommentsBanner
@@ -1168,39 +1067,24 @@ issueRoutes.get("/:owner/:repo/issues", softAuth, requireRepoAccess("read"), asy
11681067 repo={repoName}
11691068 count={pendingCountList}
11701069 />
1171 <section class="issues-hero">
1172 <div class="issues-hero-bg" aria-hidden="true">
1173 <div class="issues-hero-orb" />
1174 </div>
1175 <div class="issues-hero-inner">
1176 <div class="issues-hero-text">
1177 <div class="issues-hero-eyebrow">
1178 Issue tracker \u00B7{" "}
1179 <span class="issues-hero-repo">
1180 {ownerName}/{repoName}
1181 </span>
1182 </div>
1183 <h1 class="issues-hero-title">
1184 Track <span class="gradient-text">what matters</span>.
1185 </h1>
1186 <p class="issues-hero-sub">
1187 {(Number(counts?.open ?? 0) + Number(counts?.closed ?? 0)) === 0
1188 ? "Bugs, ideas, and roadmap items live here. Open the first one and AI Triage will draft a starter classification within seconds."
1189 : `${Number(counts?.open ?? 0)} open \u00B7 ${Number(counts?.closed ?? 0)} closed. AI Triage suggests labels, priority, and possible duplicates the moment an issue is filed.`}
1190 </p>
1191 </div>
1192 <div class="issues-hero-actions">
1070 <GxPageHeader
1071 eyebrow={`Issue tracker \u00B7 ${ownerName}/${repoName}`}
1072 title="Track what matters."
1073 lede={
1074 (Number(counts?.open ?? 0) + Number(counts?.closed ?? 0)) === 0
1075 ? "Bugs, ideas, and roadmap items live here. Open the first one and AI Triage will draft a starter classification within seconds."
1076 : `${Number(counts?.open ?? 0)} open \u00B7 ${Number(counts?.closed ?? 0)} closed. AI Triage suggests labels, priority, and possible duplicates the moment an issue is filed.`
1077 }
1078 actions={
1079 <>
11931080 {user && (
1194 <a
1195 href={`/${ownerName}/${repoName}/issues/new`}
1196 class="btn btn-primary"
1197 >
1081 <GxButton href={`/${ownerName}/${repoName}/issues/new`} variant="primary">
11981082 + New issue
1199 </a>
1083 </GxButton>
12001084 )}
1201 <a
1085 <GxButton
12021086 href={`/${ownerName}/${repoName}/milestones`}
1203 class="btn"
1087 variant="secondary"
12041088 title="View milestones for this repository"
12051089 >
12061090 Milestones
@@ -1209,13 +1093,13 @@ issueRoutes.get("/:owner/:repo/issues", softAuth, requireRepoAccess("read"), asy
12091093 {Number(milestoneCounts?.open ?? 0)}
12101094 </span>
12111095 )}
1212 </a>
1213 <a href={`/${ownerName}/${repoName}`} class="btn">
1096 </GxButton>
1097 <GxButton href={`/${ownerName}/${repoName}`} variant="secondary">
12141098 Back to code
1215 </a>
1216 </div>
1217 </div>
1218 </section>
1099 </GxButton>
1100 </>
1101 }
1102 />
12191103
12201104 <div class="issues-toolbar">
12211105 <div class="issues-filters" role="tablist" aria-label="Issue state filter">
@@ -1409,32 +1293,15 @@ issueRoutes.get(
14091293 return c.html(
14101294 <Layout title={`New issue — ${ownerName}/${repoName}`} user={user}>
14111295 <IssuesStyle />
1296 <SharedStyle />
14121297 <RepoHeader owner={ownerName} repo={repoName} />
14131298 <IssueNav owner={ownerName} repo={repoName} active="issues" />
14141299 <Container maxWidth={800}>
1415 <section class="issues-hero" style="margin-top:4px">
1416 <div class="issues-hero-bg" aria-hidden="true">
1417 <div class="issues-hero-orb" />
1418 </div>
1419 <div class="issues-hero-inner">
1420 <div class="issues-hero-text">
1421 <div class="issues-hero-eyebrow">
1422 New issue ·{" "}
1423 <span class="issues-hero-repo">
1424 {ownerName}/{repoName}
1425 </span>
1426 </div>
1427 <h1 class="issues-hero-title">
1428 File <span class="gradient-text">it cleanly</span>.
1429 </h1>
1430 <p class="issues-hero-sub">
1431 AI Triage will read the body the moment you submit and post
1432 suggested labels, priority, and possible duplicates within
1433 seconds. You stay in control — nothing is applied.
1434 </p>
1435 </div>
1436 </div>
1437 </section>
1300 <GxPageHeader
1301 eyebrow={`New issue · ${ownerName}/${repoName}`}
1302 title="File it cleanly."
1303 lede="AI Triage will read the body the moment you submit and post suggested labels, priority, and possible duplicates within seconds. You stay in control — nothing is applied."
1304 />
14381305 {error && (
14391306 <Alert variant="error">{decodeURIComponent(error)}</Alert>
14401307 )}
@@ -1728,6 +1595,7 @@ issueRoutes.get("/:owner/:repo/issues/:number", softAuth, requireRepoAccess("rea
17281595 user={user}
17291596 >
17301597 <IssuesStyle />
1598 <SharedStyle />
17311599 <RepoHeader owner={ownerName} repo={repoName} />
17321600 <IssueNav owner={ownerName} repo={repoName} active="issues" />
17331601 <PendingCommentsBanner
@@ -1777,15 +1645,12 @@ issueRoutes.get("/:owner/:repo/issues/:number", softAuth, requireRepoAccess("rea
17771645 <span class="issues-detail-number">#{issue.number}</span>
17781646 </h1>
17791647 <div class="issues-detail-attr">
1780 <span
1781 class={`issues-state-pill ${issue.state === "open" ? "is-open" : "is-closed"}`}
1782 title={issue.state === "open" ? "Open" : "Closed"}
1783 >
1648 <GxBadge variant={issue.state === "open" ? "success" : "info"}>
17841649 <span aria-hidden="true">
17851650 {issue.state === "open" ? "\u25CB" : "\u2713"}
17861651 </span>
17871652 {issue.state === "open" ? "Open" : "Closed"}
1788 </span>
1653 </GxBadge>
17891654 <span>
17901655 <strong>{author?.username || "unknown"}</strong> opened this
17911656 issue {formatRelative(issue.createdAt)}
Modifiedsrc/routes/pulls.tsx+31−124View fileUnifiedSplit
@@ -30,7 +30,7 @@ import {
3030 pendingReviewComments,
3131} from "../db/schema";
3232import { Layout } from "../views/layout";
33import { RepoHeader, RepoNav } from "../views/components";
33import { RepoHeader, RepoNav, PageHeader, Button as GxButton, Badge as GxBadge, sharedComponentStyles } from "../views/components";
3434import { PendingCommentsBanner } from "../views/pending-comments-banner";
3535import { DiffView, type InlineDiffComment } from "../views/diff-view";
3636import { ReactionsBar } from "../views/reactions";
@@ -164,83 +164,6 @@ const pulls = new Hono<AuthEnv>();
164164 * `:root` so light/dark stays consistent if/when light mode lands.
165165 * ──────────────────────────────────────────────────────────────────── */
166166const PRS_LIST_STYLES = `
167 .prs-hero {
168 position: relative;
169 margin: 0 0 var(--space-5);
170 padding: 22px 26px 24px;
171 background: var(--bg-elevated);
172 border: 1px solid var(--border);
173 border-radius: 16px;
174 overflow: hidden;
175 }
176 .prs-hero::before {
177 content: '';
178 position: absolute; top: 0; left: 0; right: 0;
179 height: 2px;
180 background: linear-gradient(90deg, transparent 0%, #5b6ee8 30%, #5f8fa0 70%, transparent 100%);
181 opacity: 0.7;
182 pointer-events: none;
183 }
184 .prs-hero-inner {
185 position: relative;
186 display: flex;
187 justify-content: space-between;
188 align-items: flex-end;
189 gap: 20px;
190 flex-wrap: wrap;
191 }
192 .prs-hero-text { flex: 1; min-width: 280px; }
193 .prs-hero-eyebrow {
194 font-size: 12px;
195 color: var(--text-muted);
196 text-transform: uppercase;
197 letter-spacing: 0.08em;
198 font-weight: 600;
199 margin-bottom: 8px;
200 }
201 .prs-hero-title {
202 font-family: var(--font-display);
203 font-size: clamp(26px, 3.4vw, 34px);
204 font-weight: 800;
205 letter-spacing: -0.025em;
206 line-height: 1.06;
207 margin: 0 0 8px;
208 color: var(--text-strong);
209 }
210 .prs-hero-title .gradient-text {
211 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
212 -webkit-background-clip: text;
213 background-clip: text;
214 -webkit-text-fill-color: transparent;
215 color: transparent;
216 }
217 .prs-hero-sub {
218 font-size: 14.5px;
219 color: var(--text-muted);
220 margin: 0;
221 line-height: 1.5;
222 max-width: 620px;
223 }
224 .prs-hero-actions { display: flex; gap: 8px; flex-wrap: wrap; }
225 .prs-cta {
226 display: inline-flex; align-items: center; gap: 6px;
227 padding: 10px 16px;
228 border-radius: 10px;
229 font-size: 13.5px;
230 font-weight: 600;
231 color: #fff;
232 background: linear-gradient(135deg, #5b6ee8 0%, #6f5be8 60%, #5f8fa0 140%);
233 border: 1px solid rgba(91,110,232,0.55);
234 box-shadow: 0 6px 18px -8px rgba(91,110,232,0.55);
235 text-decoration: none;
236 transition: transform 120ms ease, box-shadow 160ms ease;
237 }
238 .prs-cta:hover {
239 transform: translateY(-1px);
240 box-shadow: 0 10px 22px -6px rgba(91,110,232,0.6);
241 color: #fff;
242 }
243
244167 .prs-tabs {
245168 display: flex; flex-wrap: wrap; gap: 6px;
246169 margin: 0 0 18px;
@@ -428,15 +351,11 @@ const PRS_LIST_STYLES = `
428351 .prs-empty-cta { display: inline-flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
429352
430353 @media (max-width: 720px) {
431 .prs-hero-inner { flex-direction: column; align-items: flex-start; }
432 .prs-hero-actions { width: 100%; }
433354 .prs-row-tags { margin-left: 0; }
434355 }
435356
436357 /* Additional mobile rules. Additive only. */
437358 @media (max-width: 720px) {
438 .prs-hero { padding: 18px 18px 20px; }
439 .prs-hero-actions .prs-cta { flex: 1; min-width: 0; justify-content: center; min-height: 44px; }
440359 .prs-tabs { overflow-x: auto; -webkit-overflow-scrolling: touch; flex-wrap: nowrap; }
441360 .prs-tab { min-height: 40px; padding: 9px 14px; white-space: nowrap; }
442361 .prs-row { padding: 12px 14px; gap: 10px; }
@@ -512,20 +431,6 @@ const PRS_DETAIL_STYLES = `
512431 color: var(--text-muted);
513432 font-weight: 400;
514433 }
515 .prs-state-pill {
516 display: inline-flex; align-items: center; gap: 6px;
517 padding: 6px 12px;
518 border-radius: 9999px;
519 font-size: 12.5px;
520 font-weight: 600;
521 line-height: 1;
522 border: 1px solid transparent;
523 }
524 .prs-state-pill.state-open { color: var(--green); background: rgba(52,211,153,0.12); border-color: rgba(52,211,153,0.35); }
525 .prs-state-pill.state-merged { color: var(--accent); background: rgba(91,110,232,0.16); border-color: rgba(91,110,232,0.45); }
526 .prs-state-pill.state-closed { color: var(--red); background: rgba(248,113,113,0.12); border-color: rgba(248,113,113,0.35); }
527 .prs-state-pill.state-draft { color: var(--text-muted); background: rgba(255,255,255,0.05); border-color: var(--border-strong); }
528
529434 .prs-size-badge {
530435 display: inline-flex;
531436 align-items: center;
@@ -2823,36 +2728,29 @@ pulls.get("/:owner/:repo/pulls", softAuth, requireRepoAccess("read"), async (c)
28232728 count={prListPendingCount}
28242729 />
28252730 <style dangerouslySetInnerHTML={{ __html: PRS_LIST_STYLES }} />
2826
2827 <div class="prs-hero">
2828 <div class="prs-hero-inner">
2829 <div class="prs-hero-text">
2830 <div class="prs-hero-eyebrow">Pull requests</div>
2831 <h1 class="prs-hero-title">
2832 Review, <span class="gradient-text">merge with AI</span>.
2833 </h1>
2834 <p class="prs-hero-sub">
2835 {openCount === 0 && allCount === 0
2836 ? "No pull requests yet. Open the first one to start collaborating — AI review runs automatically on every PR."
2837 : `${openCount} open, ${mergedCount} merged, ${closedCount} closed${draftCount > 0 ? ` · ${draftCount} draft${draftCount === 1 ? "" : "s"}` : ""}. AI review, gate checks, and auto-resolve included.`}
2838 </p>
2839 </div>
2840 <div class="prs-hero-actions">
2841 <a
2842 href={`/${ownerName}/${repoName}/pulls/insights`}
2843 class="prs-cta"
2844 style="background:var(--bg-secondary);border-color:var(--border);color:var(--text);box-shadow:none"
2845 >
2731 <style dangerouslySetInnerHTML={{ __html: sharedComponentStyles }} />
2732
2733 <PageHeader
2734 eyebrow="Pull requests"
2735 title="Review, merge with AI."
2736 lede={
2737 openCount === 0 && allCount === 0
2738 ? "No pull requests yet. Open the first one to start collaborating — AI review runs automatically on every PR."
2739 : `${openCount} open, ${mergedCount} merged, ${closedCount} closed${draftCount > 0 ? ` · ${draftCount} draft${draftCount === 1 ? "" : "s"}` : ""}. AI review, gate checks, and auto-resolve included.`
2740 }
2741 actions={
2742 <>
2743 <GxButton href={`/${ownerName}/${repoName}/pulls/insights`} variant="secondary">
28462744 Insights
2847 </a>
2745 </GxButton>
28482746 {user && (
2849 <a href={`/${ownerName}/${repoName}/pulls/new`} class="prs-cta">
2747 <GxButton href={`/${ownerName}/${repoName}/pulls/new`} variant="primary">
28502748 + New pull request
2851 </a>
2749 </GxButton>
28522750 )}
2853 </div>
2854 </div>
2855 </div>
2751 </>
2752 }
2753 />
28562754
28572755 <nav class="prs-tabs" aria-label="Pull request filters">
28582756 {tabPills.map((t) => {
@@ -4422,6 +4320,14 @@ pulls.get("/:owner/:repo/pulls/:number", softAuth, requireRepoAccess("read"), as
44224320 : stateKey === "merged"
44234321 ? "⮌"
44244322 : "✓";
4323 const stateVariant =
4324 stateKey === "open"
4325 ? "success"
4326 : stateKey === "merged"
4327 ? "info"
4328 : stateKey === "closed"
4329 ? "danger"
4330 : "neutral";
44254331 const commentCount = comments.length;
44264332 const aiReviewCount = comments.filter(({ comment }) => comment.isAiReview).length;
44274333 const gatesAllPassed = gateChecks.length > 0 && gateChecks.every((c) => c.passed);
@@ -4464,6 +4370,7 @@ pulls.get("/:owner/:repo/pulls/:number", softAuth, requireRepoAccess("read"), as
44644370 count={prPendingCount}
44654371 />
44664372 <style dangerouslySetInnerHTML={{ __html: PRS_DETAIL_STYLES }} />
4373 <style dangerouslySetInnerHTML={{ __html: sharedComponentStyles }} />
44674374 <div
44684375 id="live-comment-banner"
44694376 class="alert"
@@ -4572,10 +4479,10 @@ pulls.get("/:owner/:repo/pulls/:number", softAuth, requireRepoAccess("read"), as
45724479 </form>
45734480 )}
45744481 <div class="prs-detail-meta">
4575 <span class={`prs-state-pill state-${stateKey}`}>
4482 <GxBadge variant={stateVariant}>
45764483 <span aria-hidden="true">{stateIcon}</span>
45774484 <span>{stateLabel}</span>
4578 </span>
4485 </GxBadge>
45794486 {isAiGeneratedPr(pr.body, pr.headBranch) && (
45804487 <span class="prs-tag is-ai" title="This pull request was opened by an AI agent">⚡ AI-generated</span>
45814488 )}
Modifiedsrc/routes/settings.tsx+19−142View fileUnifiedSplit
@@ -10,7 +10,13 @@ import { getStandupPrefs, setStandupPrefs } from "../lib/ai-standup";
1010import type { AuthEnv } from "../middleware/auth";
1111import { requireAuth } from "../middleware/auth";
1212import { Layout } from "../views/layout";
13import { SettingsNav, settingsNavStyles } from "../views/components";
13import {
14 SettingsNav,
15 settingsNavStyles,
16 PageHeader,
17 Badge,
18 sharedComponentStyles,
19} from "../views/components";
1420import { raw } from "hono/html";
1521import { composeDigest } from "../lib/email-digest";
1622import {
@@ -38,86 +44,6 @@ settings.use("/api/user/*", requireAuth);
3844const settingsStyles = `
3945 .settings-container { max-width: 1200px; margin: 0 auto; }
4046
41 /* ─── Hero ─── */
42 .settings-hero {
43 position: relative;
44 margin-bottom: var(--space-6);
45 padding: var(--space-5) var(--space-6);
46 background: var(--bg-elevated);
47 border: 1px solid var(--border);
48 border-radius: 16px;
49 overflow: hidden;
50 }
51 .settings-hero::before {
52 content: '';
53 position: absolute;
54 top: 0; left: 0; right: 0;
55 height: 2px;
56 background: linear-gradient(90deg, transparent 0%, #5b6ee8 30%, #5f8fa0 70%, transparent 100%);
57 opacity: 0.7;
58 pointer-events: none;
59 }
60 .settings-hero-bg {
61 position: absolute;
62 inset: -20% -10% auto auto;
63 width: 360px; height: 360px;
64 pointer-events: none;
65 z-index: 0;
66 }
67 .settings-hero-orb {
68 position: absolute;
69 inset: 0;
70 background: radial-gradient(circle, rgba(91,110,232,0.18), rgba(95,143,160,0.09) 45%, transparent 70%);
71 filter: blur(80px);
72 opacity: 0.65;
73 animation: settingsHeroOrb 14s ease-in-out infinite;
74 }
75 settingsHeroOrb {
76 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.55; }
77 50% { transform: scale(1.08) translate(-8px, 6px); opacity: 0.78; }
78 }
79 (prefers-reduced-motion: reduce) {
80 .settings-hero-orb { animation: none; }
81 }
82 .settings-hero-inner {
83 position: relative;
84 z-index: 1;
85 max-width: 640px;
86 }
87 .settings-hero-eyebrow {
88 font-size: 13px;
89 color: var(--text-muted);
90 margin-bottom: var(--space-2);
91 letter-spacing: -0.005em;
92 text-transform: none;
93 }
94 .settings-hero-eyebrow .settings-hero-username {
95 color: var(--accent);
96 font-weight: 600;
97 }
98 .settings-hero-title {
99 font-size: clamp(28px, 4vw, 40px);
100 font-family: var(--font-display);
101 font-weight: 800;
102 letter-spacing: -0.028em;
103 line-height: 1.05;
104 margin: 0 0 var(--space-2);
105 color: var(--text-strong);
106 }
107 .settings-hero-title .gradient-text {
108 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
109 -webkit-background-clip: text;
110 background-clip: text;
111 -webkit-text-fill-color: transparent;
112 color: transparent;
113 }
114 .settings-hero-sub {
115 font-size: 15px;
116 color: var(--text-muted);
117 margin: 0;
118 line-height: 1.5;
119 }
120
12147 /* ─── Section cards ─── */
12248 .settings-section {
12349 background: var(--bg-elevated);
@@ -258,35 +184,6 @@ const settingsStyles = `
258184 color: var(--text-muted);
259185 }
260186
261 /* ─── Sleep mode state pill ─── */
262 .settings-sleep-state {
263 display: inline-flex;
264 align-items: center;
265 gap: 8px;
266 padding: 5px 12px;
267 border-radius: 9999px;
268 font-size: 12px;
269 font-weight: 600;
270 letter-spacing: 0.01em;
271 margin-bottom: var(--space-3);
272 }
273 .settings-sleep-state.is-on {
274 background: rgba(91,110,232,0.14);
275 color: var(--accent);
276 box-shadow: inset 0 0 0 1px rgba(91,110,232,0.35);
277 }
278 .settings-sleep-state.is-off {
279 background: rgba(255,255,255,0.04);
280 color: var(--text-muted);
281 box-shadow: inset 0 0 0 1px var(--border);
282 }
283 .settings-sleep-state .dot {
284 width: 7px; height: 7px;
285 border-radius: 9999px;
286 background: currentColor;
287 box-shadow: 0 0 8px currentColor;
288 }
289 .settings-sleep-state.is-off .dot { box-shadow: none; opacity: 0.6; }
290187 .settings-hour-row {
291188 display: flex;
292189 align-items: center;
@@ -511,7 +408,6 @@ const settingsStyles = `
511408
512409 /* ─── Responsive ─── */
513410 @media (max-width: 720px) {
514 .settings-hero { padding: var(--space-4) var(--space-4); }
515411 .settings-section-head,
516412 .settings-section-body,
517413 .settings-section-foot,
@@ -534,29 +430,13 @@ function SettingsHero(props: {
534430 sub: string;
535431}) {
536432 const { username, title, accent, sub } = props;
433 const fullTitle = accent ? `${title} ${accent}` : title;
537434 return (
538 <div class="settings-hero">
539 <div class="settings-hero-bg" aria-hidden="true">
540 <div class="settings-hero-orb" />
541 </div>
542 <div class="settings-hero-inner">
543 <div class="settings-hero-eyebrow">
544 Your account ·{" "}
545 <span class="settings-hero-username">{username}</span>
546 </div>
547 <h1 class="settings-hero-title">
548 {accent ? (
549 <>
550 {title}{" "}
551 <span class="gradient-text">{accent}</span>.
552 </>
553 ) : (
554 <>{title}.</>
555 )}
556 </h1>
557 <p class="settings-hero-sub">{sub}</p>
558 </div>
559 </div>
435 <PageHeader
436 eyebrow={`Your account · ${username}`}
437 title={fullTitle}
438 lede={sub}
439 />
560440 );
561441}
562442
@@ -594,6 +474,7 @@ settings.get("/settings", async (c) => {
594474 <Layout title="Settings" user={user}>
595475 <style dangerouslySetInnerHTML={{ __html: settingsStyles }} />
596476 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
477 <style dangerouslySetInnerHTML={{ __html: sharedComponentStyles }} />
597478 <div class="settings-container">
598479 <SettingsHero
599480 username={user.username}
@@ -838,15 +719,10 @@ settings.get("/settings", async (c) => {
838719 </p>
839720 </div>
840721 <div class="settings-section-body">
841 <div
842 class={
843 "settings-sleep-state " +
844 (user.sleepModeEnabled ? "is-on" : "is-off")
845 }
846 aria-live="polite"
847 >
848 <span class="dot" aria-hidden="true" />
849 {user.sleepModeEnabled ? "Sleep Mode active" : "Sleep Mode off"}
722 <div style="margin-bottom: var(--space-3)" aria-live="polite">
723 <Badge variant={user.sleepModeEnabled ? "info" : "neutral"} dot>
724 {user.sleepModeEnabled ? "Sleep Mode active" : "Sleep Mode off"}
725 </Badge>
850726 </div>
851727 <label class="settings-toggle-row">
852728 <input
@@ -2185,6 +2061,7 @@ settings.get("/settings/keys", async (c) => {
21852061 <Layout title="SSH Keys" user={user}>
21862062 <style dangerouslySetInnerHTML={{ __html: settingsStyles }} />
21872063 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
2064 <style dangerouslySetInnerHTML={{ __html: sharedComponentStyles }} />
21882065 <div class="settings-container">
21892066 <SettingsHero
21902067 username={user.username}
Modifiedsrc/routes/web.tsx+7−36View fileUnifiedSplit
@@ -33,6 +33,8 @@ import {
3333 BranchSwitcher,
3434 HighlightedCode,
3535 PlainCode,
36 Button,
37 sharedComponentStyles,
3638 type RecentPush,
3739} from "../views/components";
3840import { DiffView } from "../views/diff-view";
@@ -3741,39 +3743,8 @@ web.get("/:owner/:repo", async (c) => {
37413743 }
37423744 .empty-option-snippet .ec { color: var(--text-faint); }
37433745 .empty-option-snippet .em { color: var(--accent); }
3744 .empty-option-cta {
3745 display: inline-flex;
3746 align-items: center;
3747 gap: 6px;
3748 padding: 8px 16px;
3749 font-size: 13px;
3750 font-weight: 600;
3751 color: var(--text-strong);
3752 background: var(--bg-secondary);
3753 border: 1px solid var(--border);
3754 border-radius: 9999px;
3755 text-decoration: none;
3756 align-self: flex-start;
3757 transition: border-color 140ms ease, background 140ms ease, color 140ms ease;
3758 }
3759 .empty-option-cta:hover {
3760 border-color: rgba(91,110,232,0.55);
3761 background: rgba(91,110,232,0.08);
3762 color: var(--accent);
3763 text-decoration: none;
3764 }
3765 .empty-option-cta-accent {
3766 background: linear-gradient(135deg, #5b6ee8, #5f8fa0);
3767 border-color: transparent;
3768 color: #fff;
3769 }
3770 .empty-option-cta-accent:hover {
3771 background: linear-gradient(135deg, #7c5df0, #28b4c8);
3772 border-color: transparent;
3773 color: #fff;
3774 box-shadow: 0 6px 18px -6px rgba(91,110,232,0.55);
3775 }
37763746 ` }} />
3747 <style dangerouslySetInnerHTML={{ __html: sharedComponentStyles }} />
37773748 <div class="repo-home-hero">
37783749 <div class="repo-home-hero-orb-wrap" aria-hidden="true">
37793750 <div class="repo-home-hero-orb" />
@@ -3852,9 +3823,9 @@ web.get("/:owner/:repo", async (c) => {
38523823 Mirror an existing GitHub repository here in one click. Gluecron
38533824 syncs the full history and branches.
38543825 </p>
3855 <a href="/import" class="empty-option-cta">
3826 <Button href="/import" variant="secondary" style="align-self:flex-start">
38563827 Import repository
3857 </a>
3828 </Button>
38583829 </div>
38593830 <div class="empty-option-card">
38603831 <div class="empty-option-label">Option C</div>
@@ -3863,9 +3834,9 @@ web.get("/:owner/:repo", async (c) => {
38633834 Let AI write your first feature. Describe what you want to build
38643835 and Gluecron opens a pull request with the code.
38653836 </p>
3866 <a href={`/${owner}/${repo}/specs`} class="empty-option-cta empty-option-cta-accent">
3837 <Button href={`/${owner}/${repo}/specs`} variant="primary" style="align-self:flex-start">
38673838 Let AI write your first feature
3868 </a>
3839 </Button>
38693840 </div>
38703841 </div>
38713842 </div>
38723843