Commitf7ad7b8unknown_key
polish(issues): 2026 hero + label pills + AI-comment treatment (parallel session 2.B)
polish(issues): 2026 hero + label pills + AI-comment treatment (parallel session 2.B)
Phase 2 polish — issues list + detail brought up to the landing/
dashboard/auth quality bar.
Changes to src/routes/issues.tsx only (shared view files untouched):
- Issues list page now leads with a hero card matching dash-hero:
gradient hairline strip on the top edge, animated orb in the
top-right corner (14s breath, prefers-reduced-motion honored),
eyebrow ("Issue tracker · owner/repo"), clamp(28-40px) display
title with violet→cyan gradient on "what matters", context-
aware sub-line, and a clear "+ New issue" CTA.
- Filter pills replace the old FilterTabs row: pill-rail with
open/closed counts rendered as inset numeric chips, accent
background on the active pill.
- Issue rows are now first-class cards: 16-col layout (status
icon · main · meta), Inter-Tight title that highlights to
accent on hover, soft purple-tint hover background.
- Empty state uses an inline SVG gradient bullseye + display-font
title + sub-copy + primary CTA — no external assets.
- Detail page hero: display-font title with muted #number suffix,
state pill (green for open, violet for closed) with proper
borders + tint, opened-by attribution, "Build with AI" CTA
preserved on the right.
- Comment thread rebuilt: each comment is a polished card with
a header band, monospace-feel author pill ("Author" tag on the
issue body card), and renderMarkdown body. AI Triage comments
(detected via ISSUE_TRIAGE_MARKER or "## AI Triage" prefix)
get the premium treatment — purple accent border, gradient
top-edge strip, gradient-filled "AI Review" badge with a soft
inner glow, and a violet-tinted header band.
- Composer at the bottom is a polished textarea-in-card with a
"Markdown supported" hint link, focus-within accent glow,
and a footer action row. All three submit buttons (Comment,
Close/Reopen, Re-run AI triage) preserved exactly — no logic
changed.
- "New issue" form gets the matching hero treatment.
All CSS scoped to .issues-* classes (79 classnames added) via an
inline <style> block emitted by an IssuesStyle component, so no
bleed to other pages. Design tokens (--bg-elevated, --border,
--text-strong, --text-muted, --accent, --font-display, .gradient-
text) sourced from :root in layout.tsx — no new tokens defined.
Hard rules respected:
- src/views/{layout,components,ui}.tsx untouched.
- Other route files untouched.
- No data-fetching, auth, redirect, or AI-triage logic touched.
- Only ISSUE_TRIAGE_MARKER added to the import list so the AI-
comment detector can match the same marker the triage writer
emits.
Tests: 2011/2011 pass, 2 skipped. tsc --noEmit clean.
https://claude.ai/code/session_01KdpgsaGpmEExJAz3yhFLvx1 file changed+791−129f7ad7b85b58f3fd588d1e5288508c700eeca57ac
1 changed file+791−129
Modifiedsrc/routes/issues.tsx+791−129View fileUnifiedSplit
@@ -20,7 +20,7 @@ import { summariseReactions } from "../lib/reactions";
2020import { loadIssueTemplate } from "../lib/templates";
2121import { renderMarkdown } from "../lib/markdown";
2222import { liveCommentBannerScript } from "../lib/sse-client";
23import { triggerIssueTriage } from "../lib/issue-triage";
23import { triggerIssueTriage, ISSUE_TRIAGE_MARKER } from "../lib/issue-triage";
2424import { softAuth, requireAuth } from "../middleware/auth";
2525import type { AuthEnv } from "../middleware/auth";
2626import { requireRepoAccess } from "../middleware/repo-access";
@@ -48,6 +48,540 @@ import {
4848
4949const issueRoutes = new Hono<AuthEnv>();
5050
51// ---------------------------------------------------------------------------
52// Visual polish: inline CSS scoped to `.issues-*` so it never collides with
53// other routes/shared views. All design tokens come from :root in layout.tsx.
54// ---------------------------------------------------------------------------
55const issuesStyles = `
56 /* Hero card — list page */
57 .issues-hero {
58 position: relative;
59 margin: 4px 0 24px;
60 padding: 28px 32px;
61 background: var(--bg-elevated);
62 border: 1px solid var(--border);
63 border-radius: 16px;
64 overflow: hidden;
65 }
66 .issues-hero::before {
67 content: '';
68 position: absolute;
69 top: 0; left: 0; right: 0;
70 height: 2px;
71 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
72 opacity: 0.7;
73 pointer-events: none;
74 }
75 .issues-hero-bg {
76 position: absolute;
77 inset: -30% -10% auto auto;
78 width: 360px;
79 height: 360px;
80 pointer-events: none;
81 z-index: 0;
82 }
83 .issues-hero-orb {
84 position: absolute;
85 inset: 0;
86 background: radial-gradient(circle, rgba(140,109,255,0.18), rgba(54,197,214,0.09) 45%, transparent 70%);
87 filter: blur(80px);
88 opacity: 0.7;
89 animation: issuesHeroOrb 14s ease-in-out infinite;
90 }
91 @keyframes issuesHeroOrb {
92 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.6; }
93 50% { transform: scale(1.1) translate(-12px, 8px); opacity: 0.85; }
94 }
95 @media (prefers-reduced-motion: reduce) {
96 .issues-hero-orb { animation: none; }
97 }
98 .issues-hero-inner {
99 position: relative;
100 z-index: 1;
101 display: flex;
102 justify-content: space-between;
103 align-items: flex-end;
104 gap: 24px;
105 flex-wrap: wrap;
106 }
107 .issues-hero-text { flex: 1; min-width: 280px; }
108 .issues-hero-eyebrow {
109 font-size: 12.5px;
110 color: var(--text-muted);
111 margin-bottom: 8px;
112 letter-spacing: 0.04em;
113 text-transform: uppercase;
114 font-weight: 600;
115 }
116 .issues-hero-eyebrow .issues-hero-repo {
117 color: var(--accent);
118 text-transform: none;
119 letter-spacing: -0.005em;
120 font-weight: 600;
121 }
122 .issues-hero-title {
123 font-family: var(--font-display);
124 font-size: clamp(28px, 4vw, 40px);
125 font-weight: 800;
126 letter-spacing: -0.028em;
127 line-height: 1.05;
128 margin: 0 0 10px;
129 color: var(--text-strong);
130 }
131 .issues-hero-sub {
132 font-size: 15px;
133 color: var(--text-muted);
134 margin: 0;
135 line-height: 1.5;
136 max-width: 580px;
137 }
138 .issues-hero-actions {
139 display: flex;
140 gap: 8px;
141 flex-wrap: wrap;
142 }
143 @media (max-width: 720px) {
144 .issues-hero { padding: 24px 20px; }
145 .issues-hero-inner { flex-direction: column; align-items: flex-start; }
146 .issues-hero-actions { width: 100%; }
147 .issues-hero-actions .btn { flex: 1; min-width: 0; }
148 }
149
150 /* Count chip + filter pills */
151 .issues-toolbar {
152 display: flex;
153 align-items: center;
154 justify-content: space-between;
155 gap: 12px;
156 flex-wrap: wrap;
157 margin: 0 0 16px;
158 }
159 .issues-filters {
160 display: inline-flex;
161 background: var(--bg-elevated);
162 border: 1px solid var(--border);
163 border-radius: 9999px;
164 padding: 4px;
165 gap: 2px;
166 }
167 .issues-filter {
168 display: inline-flex;
169 align-items: center;
170 gap: 6px;
171 padding: 6px 14px;
172 border-radius: 9999px;
173 font-size: 13px;
174 font-weight: 500;
175 color: var(--text-muted);
176 text-decoration: none;
177 transition: color 120ms ease, background 120ms ease;
178 line-height: 1.4;
179 }
180 .issues-filter:hover { color: var(--text-strong); text-decoration: none; }
181 .issues-filter.is-active {
182 background: rgba(140,109,255,0.14);
183 color: var(--text-strong);
184 }
185 .issues-filter-count {
186 font-variant-numeric: tabular-nums;
187 font-size: 11.5px;
188 color: var(--text-muted);
189 background: rgba(255,255,255,0.04);
190 padding: 1px 7px;
191 border-radius: 9999px;
192 }
193 .issues-filter.is-active .issues-filter-count {
194 background: rgba(140,109,255,0.18);
195 color: var(--text);
196 }
197
198 /* Issue list — modernised rows */
199 .issues-list {
200 list-style: none;
201 margin: 0;
202 padding: 0;
203 border: 1px solid var(--border);
204 border-radius: 12px;
205 overflow: hidden;
206 background: var(--bg-elevated);
207 }
208 .issues-row {
209 display: flex;
210 align-items: flex-start;
211 gap: 14px;
212 padding: 14px 18px;
213 border-bottom: 1px solid var(--border);
214 transition: background 120ms ease, transform 120ms ease;
215 }
216 .issues-row:last-child { border-bottom: none; }
217 .issues-row:hover { background: rgba(140,109,255,0.04); }
218 .issues-row-icon {
219 width: 18px;
220 height: 18px;
221 flex-shrink: 0;
222 margin-top: 3px;
223 display: inline-flex;
224 align-items: center;
225 justify-content: center;
226 }
227 .issues-row-icon.is-open { color: #34d399; }
228 .issues-row-icon.is-closed { color: #b69dff; }
229 .issues-row-main { flex: 1; min-width: 0; }
230 .issues-row-title {
231 font-family: var(--font-display);
232 font-size: 15.5px;
233 font-weight: 600;
234 line-height: 1.35;
235 letter-spacing: -0.012em;
236 margin: 0;
237 display: flex;
238 flex-wrap: wrap;
239 align-items: center;
240 gap: 8px;
241 }
242 .issues-row-title a {
243 color: var(--text-strong);
244 text-decoration: none;
245 transition: color 120ms ease;
246 }
247 .issues-row-title a:hover { color: var(--accent); }
248 .issues-row-meta {
249 margin-top: 5px;
250 font-size: 12.5px;
251 color: var(--text-muted);
252 line-height: 1.5;
253 }
254 .issues-row-meta strong { color: var(--text); font-weight: 600; }
255 .issues-row-side {
256 display: flex;
257 align-items: center;
258 gap: 12px;
259 color: var(--text-muted);
260 font-size: 12.5px;
261 flex-shrink: 0;
262 }
263 .issues-row-comments {
264 display: inline-flex;
265 align-items: center;
266 gap: 5px;
267 color: var(--text-muted);
268 text-decoration: none;
269 }
270 .issues-row-comments:hover { color: var(--accent); text-decoration: none; }
271
272 /* Label pills (rendered inline on titles) */
273 .issues-label {
274 display: inline-flex;
275 align-items: center;
276 padding: 2px 9px;
277 border-radius: 9999px;
278 font-size: 11.5px;
279 font-weight: 600;
280 line-height: 1.4;
281 background: rgba(140,109,255,0.10);
282 color: var(--text-strong);
283 border: 1px solid rgba(140,109,255,0.28);
284 letter-spacing: 0.005em;
285 }
286
287 /* Polished empty state */
288 .issues-empty {
289 margin: 0;
290 padding: 56px 32px;
291 background: var(--bg-elevated);
292 border: 1px solid var(--border);
293 border-radius: 16px;
294 text-align: center;
295 position: relative;
296 overflow: hidden;
297 }
298 .issues-empty::before {
299 content: '';
300 position: absolute;
301 top: 0; left: 0; right: 0;
302 height: 2px;
303 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
304 opacity: 0.55;
305 pointer-events: none;
306 }
307 .issues-empty-art {
308 width: 96px;
309 height: 96px;
310 margin: 0 auto 18px;
311 display: block;
312 opacity: 0.85;
313 }
314 .issues-empty-title {
315 font-family: var(--font-display);
316 font-size: 22px;
317 font-weight: 700;
318 letter-spacing: -0.018em;
319 color: var(--text-strong);
320 margin: 0 0 8px;
321 }
322 .issues-empty-sub {
323 font-size: 14.5px;
324 color: var(--text-muted);
325 line-height: 1.55;
326 margin: 0 auto 22px;
327 max-width: 460px;
328 }
329 .issues-empty-cta { display: inline-flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
330
331 /* ─── Detail page ─── */
332 .issues-detail-hero {
333 position: relative;
334 margin: 4px 0 20px;
335 padding: 22px 26px;
336 background: var(--bg-elevated);
337 border: 1px solid var(--border);
338 border-radius: 16px;
339 overflow: hidden;
340 }
341 .issues-detail-hero::before {
342 content: '';
343 position: absolute;
344 top: 0; left: 0; right: 0;
345 height: 2px;
346 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
347 opacity: 0.7;
348 pointer-events: none;
349 }
350 .issues-detail-title {
351 font-family: var(--font-display);
352 font-size: clamp(22px, 3vw, 30px);
353 font-weight: 700;
354 letter-spacing: -0.022em;
355 line-height: 1.18;
356 color: var(--text-strong);
357 margin: 0 0 12px;
358 }
359 .issues-detail-title .issues-detail-number {
360 color: var(--text-muted);
361 font-weight: 500;
362 margin-left: 8px;
363 }
364 .issues-detail-attr {
365 display: flex;
366 align-items: center;
367 gap: 12px;
368 flex-wrap: wrap;
369 font-size: 14px;
370 color: var(--text-muted);
371 }
372 .issues-detail-attr strong { color: var(--text); font-weight: 600; }
373 .issues-state-pill {
374 display: inline-flex;
375 align-items: center;
376 gap: 6px;
377 padding: 5px 12px;
378 border-radius: 9999px;
379 font-size: 12.5px;
380 font-weight: 600;
381 line-height: 1.4;
382 letter-spacing: 0.005em;
383 }
384 .issues-state-pill.is-open {
385 background: rgba(52,211,153,0.12);
386 color: #34d399;
387 border: 1px solid rgba(52,211,153,0.35);
388 }
389 .issues-state-pill.is-closed {
390 background: rgba(182,157,255,0.12);
391 color: #b69dff;
392 border: 1px solid rgba(182,157,255,0.35);
393 }
394 .issues-detail-spacer { flex: 1; }
395 .issues-detail-labels {
396 margin-top: 12px;
397 display: flex;
398 gap: 6px;
399 flex-wrap: wrap;
400 }
401
402 /* Comment thread */
403 .issues-thread { margin-top: 18px; }
404 .issues-comment {
405 position: relative;
406 border: 1px solid var(--border);
407 border-radius: 12px;
408 overflow: hidden;
409 background: var(--bg-elevated);
410 margin-bottom: 14px;
411 transition: border-color 160ms ease, box-shadow 160ms ease;
412 }
413 .issues-comment:hover {
414 border-color: var(--border-strong, rgba(255,255,255,0.13));
415 }
416 .issues-comment-header {
417 background: var(--bg-secondary);
418 padding: 10px 16px;
419 border-bottom: 1px solid var(--border);
420 display: flex;
421 align-items: center;
422 gap: 10px;
423 font-size: 13px;
424 color: var(--text-muted);
425 }
426 .issues-comment-header strong { color: var(--text-strong); font-weight: 600; }
427 .issues-comment-body { padding: 14px 18px; }
428 .issues-comment-author-pill {
429 display: inline-flex;
430 align-items: center;
431 padding: 1px 8px;
432 border-radius: 9999px;
433 background: rgba(140,109,255,0.10);
434 color: var(--accent);
435 font-size: 11px;
436 font-weight: 600;
437 letter-spacing: 0.02em;
438 text-transform: uppercase;
439 }
440
441 /* AI Review comment — distinct purple-accent treatment */
442 .issues-comment.is-ai {
443 border-color: rgba(140,109,255,0.45);
444 box-shadow: 0 0 0 1px rgba(140,109,255,0.18), 0 12px 32px -16px rgba(140,109,255,0.25);
445 }
446 .issues-comment.is-ai::before {
447 content: '';
448 position: absolute;
449 top: 0; left: 0; right: 0;
450 height: 2px;
451 background: linear-gradient(90deg, #8c6dff 0%, #36c5d6 100%);
452 pointer-events: none;
453 }
454 .issues-comment.is-ai .issues-comment-header {
455 background: linear-gradient(180deg, rgba(140,109,255,0.08), rgba(140,109,255,0.02));
456 border-bottom-color: rgba(140,109,255,0.22);
457 }
458 .issues-ai-badge {
459 display: inline-flex;
460 align-items: center;
461 gap: 5px;
462 padding: 2px 9px;
463 border-radius: 9999px;
464 background: linear-gradient(135deg, #8c6dff 0%, #36c5d6 100%);
465 color: #fff;
466 font-size: 11px;
467 font-weight: 700;
468 letter-spacing: 0.04em;
469 text-transform: uppercase;
470 line-height: 1.4;
471 }
472 .issues-ai-badge::before {
473 content: '';
474 width: 6px;
475 height: 6px;
476 border-radius: 9999px;
477 background: #fff;
478 opacity: 0.92;
479 box-shadow: 0 0 6px rgba(255,255,255,0.7);
480 }
481
482 /* Composer */
483 .issues-composer {
484 margin-top: 22px;
485 border: 1px solid var(--border);
486 border-radius: 12px;
487 overflow: hidden;
488 background: var(--bg-elevated);
489 transition: border-color 160ms ease, box-shadow 160ms ease;
490 }
491 .issues-composer:focus-within {
492 border-color: rgba(140,109,255,0.55);
493 box-shadow: 0 0 0 3px rgba(140,109,255,0.16);
494 }
495 .issues-composer-header {
496 display: flex;
497 align-items: center;
498 justify-content: space-between;
499 gap: 8px;
500 padding: 10px 14px;
501 background: var(--bg-secondary);
502 border-bottom: 1px solid var(--border);
503 font-size: 12.5px;
504 color: var(--text-muted);
505 }
506 .issues-composer-tag {
507 font-weight: 600;
508 color: var(--text);
509 letter-spacing: -0.005em;
510 }
511 .issues-composer-hint a {
512 color: var(--text-muted);
513 text-decoration: none;
514 border-bottom: 1px dashed currentColor;
515 }
516 .issues-composer-hint a:hover { color: var(--accent); }
517 .issues-composer textarea {
518 display: block;
519 width: 100%;
520 border: 0;
521 background: transparent;
522 color: var(--text);
523 padding: 14px 16px;
524 font-family: var(--font-mono);
525 font-size: 13.5px;
526 line-height: 1.55;
527 resize: vertical;
528 outline: none;
529 min-height: 140px;
530 }
531 .issues-composer-actions {
532 display: flex;
533 align-items: center;
534 gap: 8px;
535 padding: 10px 14px;
536 border-top: 1px solid var(--border);
537 background: var(--bg-secondary);
538 flex-wrap: wrap;
539 }
540
541 /* Info banner inside detail */
542 .issues-info-banner {
543 margin: 0 0 14px;
544 padding: 10px 14px;
545 border-radius: 12px;
546 background: rgba(140,109,255,0.08);
547 border: 1px solid rgba(140,109,255,0.28);
548 color: var(--text);
549 font-size: 13.5px;
550 }
551`;
552
553// Pre-rendered <style> tag (constant, reused per request).
554const IssuesStyle = () => (
555 <style dangerouslySetInnerHTML={{ __html: issuesStyles }} />
556);
557
558// Inline empty-state SVG — a softly-tinted speech-bubble icon. No external assets.
559const IssuesEmptySvg = () => (
560 <svg
561 class="issues-empty-art"
562 viewBox="0 0 96 96"
563 fill="none"
564 xmlns="http://www.w3.org/2000/svg"
565 aria-hidden="true"
566 >
567 <defs>
568 <linearGradient id="issuesEmptyG" x1="0" y1="0" x2="1" y2="1">
569 <stop offset="0%" stop-color="#8c6dff" stop-opacity="0.55" />
570 <stop offset="100%" stop-color="#36c5d6" stop-opacity="0.55" />
571 </linearGradient>
572 </defs>
573 <circle cx="48" cy="48" r="42" stroke="url(#issuesEmptyG)" stroke-width="1.5" fill="rgba(140,109,255,0.04)" />
574 <circle cx="48" cy="48" r="14" stroke="url(#issuesEmptyG)" stroke-width="2" fill="none" />
575 <path d="M48 30v8M48 58v8M30 48h8M58 48h8" stroke="url(#issuesEmptyG)" stroke-width="2" stroke-linecap="round" />
576 </svg>
577);
578
579// Detect AI Triage comments by the marker the triage helper writes.
580function isAiTriageComment(body: string | null | undefined): boolean {
581 if (!body) return false;
582 return body.includes(ISSUE_TRIAGE_MARKER) || body.trimStart().startsWith("## AI Triage");
583}
584
51585// Helper to resolve repo from :owner/:repo params
52586async function resolveRepo(ownerName: string, repoName: string) {
53587 const [owner] = await db
@@ -110,69 +644,133 @@ issueRoutes.get("/:owner/:repo/issues", softAuth, requireRepoAccess("read"), asy
110644
111645 return c.html(
112646 <Layout title={`Issues — ${ownerName}/${repoName}`} user={user}>
647 <IssuesStyle />
113648 <RepoHeader owner={ownerName} repo={repoName} />
114649 <IssueNav owner={ownerName} repo={repoName} active="issues" />
115 <Flex justify="space-between" align="center" style="margin-bottom:16px">
116 <FilterTabs
117 tabs={[
118 {
119 label: `${counts?.open ?? 0} Open`,
120 href: `/${ownerName}/${repoName}/issues?state=open`,
121 active: state === "open",
122 },
123 {
124 label: `${counts?.closed ?? 0} Closed`,
125 href: `/${ownerName}/${repoName}/issues?state=closed`,
126 active: state === "closed",
127 },
128 ]}
129 />
130 {user && (
131 <LinkButton
132 href={`/${ownerName}/${repoName}/issues/new`}
133 variant="primary"
650 <section class="issues-hero">
651 <div class="issues-hero-bg" aria-hidden="true">
652 <div class="issues-hero-orb" />
653 </div>
654 <div class="issues-hero-inner">
655 <div class="issues-hero-text">
656 <div class="issues-hero-eyebrow">
657 Issue tracker \u00B7{" "}
658 <span class="issues-hero-repo">
659 {ownerName}/{repoName}
660 </span>
661 </div>
662 <h1 class="issues-hero-title">
663 Track <span class="gradient-text">what matters</span>.
664 </h1>
665 <p class="issues-hero-sub">
666 {(Number(counts?.open ?? 0) + Number(counts?.closed ?? 0)) === 0
667 ? "Bugs, ideas, and roadmap items live here. Open the first one and AI Triage will draft a starter classification within seconds."
668 : `${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.`}
669 </p>
670 </div>
671 <div class="issues-hero-actions">
672 {user && (
673 <a
674 href={`/${ownerName}/${repoName}/issues/new`}
675 class="btn btn-primary"
676 >
677 + New issue
678 </a>
679 )}
680 <a href={`/${ownerName}/${repoName}`} class="btn">
681 Back to code
682 </a>
683 </div>
684 </div>
685 </section>
686
687 <div class="issues-toolbar">
688 <div class="issues-filters" role="tablist" aria-label="Issue state filter">
689 <a
690 class={`issues-filter${state === "open" ? " is-active" : ""}`}
691 href={`/${ownerName}/${repoName}/issues?state=open`}
692 role="tab"
693 aria-selected={state === "open" ? "true" : "false"}
134694 >
135 New issue
136 </LinkButton>
137 )}
138 </Flex>
695 <span aria-hidden="true">{"\u25CB"}</span>
696 <span>Open</span>
697 <span class="issues-filter-count">{Number(counts?.open ?? 0)}</span>
698 </a>
699 <a
700 class={`issues-filter${state === "closed" ? " is-active" : ""}`}
701 href={`/${ownerName}/${repoName}/issues?state=closed`}
702 role="tab"
703 aria-selected={state === "closed" ? "true" : "false"}
704 >
705 <span aria-hidden="true">{"\u2713"}</span>
706 <span>Closed</span>
707 <span class="issues-filter-count">{Number(counts?.closed ?? 0)}</span>
708 </a>
709 </div>
710 </div>
711
139712 {issueList.length === 0 ? (
140 <EmptyState>
141 <p>
142 No {state} issues.
713 <div class="issues-empty">
714 <IssuesEmptySvg />
715 <h2 class="issues-empty-title">
716 {state === "closed"
717 ? "No closed issues yet"
718 : (Number(counts?.open ?? 0) + Number(counts?.closed ?? 0)) === 0
719 ? "No issues \u2014 yet"
720 : "Nothing open right now"}
721 </h2>
722 <p class="issues-empty-sub">
723 {state === "closed"
724 ? "Closed issues will show up here once the team starts shipping fixes."
725 : (Number(counts?.open ?? 0) + Number(counts?.closed ?? 0)) === 0
726 ? "File the first one and AI Triage will draft a starter classification \u2014 labels, priority, and a duplicate sweep \u2014 within seconds."
727 : "All caught up. New filings will appear here, with AI Triage suggestions auto-posted to every thread."}
728 </p>
729 <div class="issues-empty-cta">
730 {user && state !== "closed" && (
731 <a
732 href={`/${ownerName}/${repoName}/issues/new`}
733 class="btn btn-primary"
734 >
735 + Open the first issue
736 </a>
737 )}
143738 {state === "closed" && (
144 <span>
145 {" "}
146 <a href={`/${ownerName}/${repoName}/issues?state=open`}>
147 View open issues
148 </a>
149 </span>
739 <a
740 href={`/${ownerName}/${repoName}/issues?state=open`}
741 class="btn"
742 >
743 View open issues
744 </a>
150745 )}
151 </p>
152 </EmptyState>
746 </div>
747 </div>
153748 ) : (
154 <List>
749 <ul class="issues-list">
155750 {issueList.map(({ issue, author }) => (
156 <ListItem>
751 <li class="issues-row">
157752 <div
158 class={`issue-state-icon ${issue.state === "open" ? "state-open" : "state-closed"}`}
753 class={`issues-row-icon ${issue.state === "open" ? "is-open" : "is-closed"}`}
754 aria-hidden="true"
755 title={issue.state === "open" ? "Open" : "Closed"}
159756 >
160757 {issue.state === "open" ? "\u25CB" : "\u2713"}
161758 </div>
162 <div>
163 <div class="issue-title">
759 <div class="issues-row-main">
760 <h3 class="issues-row-title">
164761 <a href={`/${ownerName}/${repoName}/issues/${issue.number}`}>
165762 {issue.title}
166763 </a>
167 </div>
168 <div class="issue-meta">
169 #{issue.number} opened by {author.username}{" "}
764 </h3>
765 <div class="issues-row-meta">
766 #{issue.number} opened by{" "}
767 <strong>{author.username}</strong>{" "}
170768 {formatRelative(issue.createdAt)}
171769 </div>
172770 </div>
173 </ListItem>
771 </li>
174772 ))}
175 </List>
773 </ul>
176774 )}
177775 </Layout>
178776 );
@@ -192,10 +790,33 @@ issueRoutes.get(
192790
193791 return c.html(
194792 <Layout title={`New issue — ${ownerName}/${repoName}`} user={user}>
793 <IssuesStyle />
195794 <RepoHeader owner={ownerName} repo={repoName} />
196795 <IssueNav owner={ownerName} repo={repoName} active="issues" />
197796 <Container maxWidth={800}>
198 <h2 style="margin-bottom:16px">New issue</h2>
797 <section class="issues-hero" style="margin-top:4px">
798 <div class="issues-hero-bg" aria-hidden="true">
799 <div class="issues-hero-orb" />
800 </div>
801 <div class="issues-hero-inner">
802 <div class="issues-hero-text">
803 <div class="issues-hero-eyebrow">
804 New issue ·{" "}
805 <span class="issues-hero-repo">
806 {ownerName}/{repoName}
807 </span>
808 </div>
809 <h1 class="issues-hero-title">
810 File <span class="gradient-text">it cleanly</span>.
811 </h1>
812 <p class="issues-hero-sub">
813 AI Triage will read the body the moment you submit and post
814 suggested labels, priority, and possible duplicates within
815 seconds. You stay in control — nothing is applied.
816 </p>
817 </div>
818 </div>
819 </section>
199820 {error && (
200821 <Alert variant="error">{decodeURIComponent(error)}</Alert>
201822 )}
@@ -362,6 +983,7 @@ issueRoutes.get("/:owner/:repo/issues/:number", softAuth, requireRepoAccess("rea
362983 title={`${issue.title} #${issue.number} — ${ownerName}/${repoName}`}
363984 user={user}
364985 >
986 <IssuesStyle />
365987 <RepoHeader owner={ownerName} repo={repoName} />
366988 <IssueNav owner={ownerName} repo={repoName} active="issues" />
367989 <div
@@ -384,98 +1006,138 @@ issueRoutes.get("/:owner/:repo/issues/:number", softAuth, requireRepoAccess("rea
3841006 />
3851007 <div class="issue-detail">
3861008 {info && (
387 <div style="margin: 12px 0; padding: 10px 14px; border-radius: 6px; background: rgba(56, 139, 253, 0.1); border: 1px solid var(--accent); color: var(--text); font-size: 14px">
1009 <div class="issues-info-banner">
3881010 {decodeURIComponent(info)}
3891011 </div>
3901012 )}
391 <h2>
392 {issue.title}{" "}
393 <span style="color:var(--text-muted);font-weight:400">
394 #{issue.number}
395 </span>
396 </h2>
397 <Flex align="center" gap={8} style="margin:8px 0 20px">
398 <Badge variant={issue.state === "open" ? "open" : "closed"}>
399 {issue.state === "open" ? "\u25CB Open" : "\u2713 Closed"}
400 </Badge>
401 <span style="color:var(--text-muted);font-size:14px">
402 <strong style="color:var(--text)">
403 {author?.username || "unknown"}
404 </strong>{" "}
405 opened this issue {formatRelative(issue.createdAt)}
406 </span>
407 {issue.state === "open" && user && user.id === resolved.owner.id && (
408 <a
409 href={`/${ownerName}/${repoName}/spec?fromIssue=${issue.number}`}
410 class="btn btn-primary"
411 style="margin-left:auto;font-size:13px;padding:4px 10px"
412 title="Generate a draft pull request from this issue using Claude"
1013
1014 <section class="issues-detail-hero">
1015 <h1 class="issues-detail-title">
1016 {issue.title}
1017 <span class="issues-detail-number">#{issue.number}</span>
1018 </h1>
1019 <div class="issues-detail-attr">
1020 <span
1021 class={`issues-state-pill ${issue.state === "open" ? "is-open" : "is-closed"}`}
1022 title={issue.state === "open" ? "Open" : "Closed"}
4131023 >
414 Build with AI
415 </a>
1024 <span aria-hidden="true">
1025 {issue.state === "open" ? "\u25CB" : "\u2713"}
1026 </span>
1027 {issue.state === "open" ? "Open" : "Closed"}
1028 </span>
1029 <span>
1030 <strong>{author?.username || "unknown"}</strong> opened this
1031 issue {formatRelative(issue.createdAt)}
1032 </span>
1033 <span class="issues-detail-spacer" />
1034 {issue.state === "open" && user && user.id === resolved.owner.id && (
1035 <a
1036 href={`/${ownerName}/${repoName}/spec?fromIssue=${issue.number}`}
1037 class="btn btn-primary"
1038 style="font-size:13px;padding:6px 12px"
1039 title="Generate a draft pull request from this issue using Claude"
1040 >
1041 Build with AI
1042 </a>
1043 )}
1044 </div>
1045 </section>
1046
1047 <div class="issues-thread">
1048 {issue.body && (
1049 <article class="issues-comment">
1050 <header class="issues-comment-header">
1051 <strong>{author?.username || "unknown"}</strong>
1052 <span class="issues-comment-author-pill">Author</span>
1053 <span>commented {formatRelative(issue.createdAt)}</span>
1054 </header>
1055 <div class="issues-comment-body">
1056 <div
1057 class="markdown-body"
1058 dangerouslySetInnerHTML={{ __html: renderMarkdown(issue.body) }}
1059 />
1060 </div>
1061 </article>
4161062 )}
417 </Flex>
418
419 {issue.body && (
420 <CommentBox
421 author={author?.username || "unknown"}
422 date={issue.createdAt}
423 body={renderMarkdown(issue.body)}
424 />
425 )}
4261063
427 {comments.map(({ comment, author: commentAuthor }) => (
428 <CommentBox
429 author={commentAuthor.username}
430 date={comment.createdAt}
431 body={renderMarkdown(comment.body)}
432 />
433 ))}
1064 {comments.map(({ comment, author: commentAuthor }) => {
1065 const isAi = isAiTriageComment(comment.body);
1066 return (
1067 <article class={`issues-comment${isAi ? " is-ai" : ""}`}>
1068 <header class="issues-comment-header">
1069 <strong>{commentAuthor.username}</strong>
1070 {isAi ? (
1071 <span class="issues-ai-badge" title="Generated by Gluecron AI Triage">
1072 AI Review
1073 </span>
1074 ) : null}
1075 <span>commented {formatRelative(comment.createdAt)}</span>
1076 </header>
1077 <div class="issues-comment-body">
1078 <div
1079 class="markdown-body"
1080 dangerouslySetInnerHTML={{
1081 __html: renderMarkdown(comment.body),
1082 }}
1083 />
1084 </div>
1085 </article>
1086 );
1087 })}
1088 </div>
4341089
4351090 {user && (
436 <div style="margin-top: 20px">
437 <form
438 method="post"
439 action={`/${ownerName}/${repoName}/issues/${issue.number}/comment`}
440 >
441 <div class="form-group">
442 <textarea
443 name="body"
444 rows={6}
445 required
446 placeholder="Leave a comment... (Markdown supported)"
447 style="font-family: var(--font-mono); font-size: 13px"
448 />
449 </div>
450 <div style="display: flex; gap: 8px">
451 <button type="submit" class="btn btn-primary">
452 Comment
1091 <form
1092 class="issues-composer"
1093 method="post"
1094 action={`/${ownerName}/${repoName}/issues/${issue.number}/comment`}
1095 >
1096 <div class="issues-composer-header">
1097 <span class="issues-composer-tag">Add a comment</span>
1098 <span class="issues-composer-hint">
1099 <a
1100 href="https://docs.github.com/en/get-started/writing-on-github"
1101 target="_blank"
1102 rel="noopener noreferrer"
1103 title="Markdown supported: **bold**, _italic_, `code`, links, lists, > quotes"
1104 >
1105 Markdown supported
1106 </a>
1107 </span>
1108 </div>
1109 <textarea
1110 name="body"
1111 rows={6}
1112 required
1113 placeholder="Leave a comment... fenced code blocks, lists, links, and quotes all supported."
1114 />
1115 <div class="issues-composer-actions">
1116 <button type="submit" class="btn btn-primary">
1117 Comment
1118 </button>
1119 {canManage && (
1120 <button
1121 type="submit"
1122 formaction={`/${ownerName}/${repoName}/issues/${issue.number}/${issue.state === "open" ? "close" : "reopen"}`}
1123 class={`btn ${issue.state === "open" ? "btn-danger" : ""}`}
1124 >
1125 {issue.state === "open" ? "Close issue" : "Reopen issue"}
4531126 </button>
454 {canManage && (
455 <button
456 type="submit"
457 formaction={`/${ownerName}/${repoName}/issues/${issue.number}/${issue.state === "open" ? "close" : "reopen"}`}
458 class={`btn ${issue.state === "open" ? "btn-danger" : ""}`}
459 >
460 {issue.state === "open"
461 ? "Close issue"
462 : "Reopen issue"}
463 </button>
464 )}
465 {canManage && issue.state === "open" && (
466 <button
467 type="submit"
468 formaction={`/${ownerName}/${repoName}/issues/${issue.number}/ai-retriage`}
469 formnovalidate
470 class="btn"
471 title="Re-run AI triage. Posts a fresh suggestions comment (use after editing the issue body)."
472 >
473 Re-run AI triage
474 </button>
475 )}
476 </div>
477 </form>
478 </div>
1127 )}
1128 {canManage && issue.state === "open" && (
1129 <button
1130 type="submit"
1131 formaction={`/${ownerName}/${repoName}/issues/${issue.number}/ai-retriage`}
1132 formnovalidate
1133 class="btn"
1134 title="Re-run AI triage. Posts a fresh suggestions comment (use after editing the issue body)."
1135 >
1136 Re-run AI triage
1137 </button>
1138 )}
1139 </div>
1140 </form>
4791141 )}
4801142 </div>
4811143 </Layout>
4821144