Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit0224546unknown_key

feat(ui): RepoNav contributors/pulse/traffic tabs + issue detail metadata sidebar

feat(ui): RepoNav contributors/pulse/traffic tabs + issue detail metadata sidebar

- components.tsx: extend RepoNav active union with contributors | pulse | traffic;
  add Contributors and Pulse tabs after Releases (visible to all); add Traffic tab
  gated to owner only (requires currentUser === repoOwner props)
- issues.tsx: add .issue-meta-* CSS namespace with dark-theme sidebar styles;
  query issueLabels+labels and milestone in detail GET handler; render right-side
  metadata sidebar on issue detail page showing colored label pills (with computed
  text contrast), assignees placeholder, and milestone link; sidebar stacks below
  content on mobile (<= 720px)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1Wr1gducAJ51tSHJuJ9tY
Claude committed on June 17, 2026Parent: 5ce80b3
2 files changed+1922022454607f271441a3ba229aa169cc729ddca0f4
2 changed files+192−2
Modifiedsrc/routes/issues.tsx+163−0View fileUnifiedSplit
697697 .bulk-bar-clear { background: none; border: none; color: var(--text-muted); font-size: 12px; cursor: pointer; padding: 4px 8px; }
698698 .bulk-bar-clear:hover { color: var(--text); }
699699
700 /* ─── Issue detail metadata sidebar ─── */
701 .issue-detail-layout {
702 display: flex;
703 align-items: flex-start;
704 gap: 24px;
705 }
706 .issue-detail-main {
707 flex: 1;
708 min-width: 0;
709 }
710 .issue-meta-sidebar {
711 width: 240px;
712 flex-shrink: 0;
713 display: flex;
714 flex-direction: column;
715 gap: 20px;
716 }
717 .issue-meta-section {
718 border-top: 1px solid var(--border);
719 padding-top: 14px;
720 }
721 .issue-meta-section:first-child {
722 border-top: none;
723 padding-top: 0;
724 }
725 .issue-meta-label {
726 font-size: 12px;
727 font-weight: 600;
728 color: var(--text-muted);
729 text-transform: uppercase;
730 letter-spacing: 0.06em;
731 margin-bottom: 8px;
732 }
733 .issue-meta-empty {
734 font-size: 13px;
735 color: var(--text-subtle, var(--text-muted));
736 font-style: italic;
737 }
738 .issue-meta-label-pill {
739 display: inline-flex;
740 align-items: center;
741 padding: 3px 10px;
742 border-radius: 9999px;
743 font-size: 12px;
744 font-weight: 600;
745 line-height: 1.4;
746 margin: 2px 3px 2px 0;
747 border: 1px solid transparent;
748 }
749 .issue-meta-labels {
750 display: flex;
751 flex-wrap: wrap;
752 gap: 4px;
753 }
754 .issue-meta-assignee {
755 display: flex;
756 align-items: center;
757 gap: 8px;
758 font-size: 13px;
759 color: var(--text);
760 }
761 .issue-meta-avatar {
762 width: 24px;
763 height: 24px;
764 border-radius: 9999px;
765 background: rgba(140,109,255,0.25);
766 color: var(--text-strong);
767 font-size: 11px;
768 font-weight: 700;
769 display: inline-flex;
770 align-items: center;
771 justify-content: center;
772 flex-shrink: 0;
773 text-transform: uppercase;
774 }
775 .issue-meta-milestone-link {
776 font-size: 13px;
777 color: var(--text-link, var(--accent));
778 text-decoration: none;
779 display: inline-flex;
780 align-items: center;
781 gap: 5px;
782 }
783 .issue-meta-milestone-link:hover { text-decoration: underline; }
784 @media (max-width: 720px) {
785 .issue-detail-layout { flex-direction: column; }
786 .issue-meta-sidebar { width: 100%; order: 1; }
787 .issue-detail-main { order: 0; }
788 }
789
700790 /* ─── Sort controls (issue list) ─── */
701791 .issues-sort-row {
702792 display: flex;
14401530 (user.id === resolved.owner.id || user.id === issue.authorId);
14411531 const info = c.req.query("info");
14421532
1533 // Labels attached to this issue.
1534 const issueDetailLabels = await db
1535 .select({ id: labels.id, name: labels.name, color: labels.color })
1536 .from(issueLabels)
1537 .innerJoin(labels, eq(issueLabels.labelId, labels.id))
1538 .where(eq(issueLabels.issueId, issue.id));
1539
1540 // Milestone for this issue (if any).
1541 const issueMilestone = issue.milestoneId
1542 ? await db
1543 .select({ id: milestones.id, title: milestones.title })
1544 .from(milestones)
1545 .where(eq(milestones.id, issue.milestoneId))
1546 .limit(1)
1547 .then((rows) => rows[0] ?? null)
1548 : null;
1549
14431550 // Linked PRs — find PRs in this repo whose title or body reference this issue number.
14441551 const issueRefPattern = `%#${issue.number}%`;
14451552 const linkedPrs = await db
15811688 </div>
15821689 </section>
15831690
1691 <div class="issue-detail-layout">
1692 <div class="issue-detail-main">
15841693 <div class="issues-thread">
15851694 {issue.body && (
15861695 <article class="issues-comment">
17101819 </div>
17111820 </form>
17121821 )}
1822 </div>{/* /issue-detail-main */}
1823
1824 {/* Metadata sidebar — Labels, Assignees, Milestone */}
1825 <aside class="issue-meta-sidebar">
1826 {/* Labels */}
1827 <div class="issue-meta-section">
1828 <div class="issue-meta-label">Labels</div>
1829 {issueDetailLabels.length === 0 ? (
1830 <span class="issue-meta-empty">None yet</span>
1831 ) : (
1832 <div class="issue-meta-labels">
1833 {issueDetailLabels.map((lbl) => {
1834 // Compute luminance from hex color to choose dark/light text
1835 const hex = lbl.color.replace("#", "");
1836 const r = parseInt(hex.slice(0, 2), 16) / 255;
1837 const g = parseInt(hex.slice(2, 4), 16) / 255;
1838 const b = parseInt(hex.slice(4, 6), 16) / 255;
1839 const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
1840 const textColor = lum > 0.45 ? "#1a1a1a" : "#ffffff";
1841 return (
1842 <span
1843 class="issue-meta-label-pill"
1844 style={`background:${lbl.color};color:${textColor};border-color:${lbl.color}`}
1845 >
1846 {lbl.name}
1847 </span>
1848 );
1849 })}
1850 </div>
1851 )}
1852 </div>
1853
1854 {/* Assignees — field not yet in schema; placeholder */}
1855 <div class="issue-meta-section">
1856 <div class="issue-meta-label">Assignees</div>
1857 <span class="issue-meta-empty">No one assigned</span>
1858 </div>
1859
1860 {/* Milestone */}
1861 <div class="issue-meta-section">
1862 <div class="issue-meta-label">Milestone</div>
1863 {issueMilestone ? (
1864 <a
1865 href={`/${ownerName}/${repoName}/milestones/${issueMilestone.id}`}
1866 class="issue-meta-milestone-link"
1867 >
1868 &#x25CE; {issueMilestone.title}
1869 </a>
1870 ) : (
1871 <span class="issue-meta-empty">No milestone</span>
1872 )}
1873 </div>
1874 </aside>
1875 </div>{/* /issue-detail-layout */}
17131876 </div>
17141877 {/* Issue keyboard hints bar */}
17151878 <div class="kbd-hints" aria-label="Keyboard shortcuts for this issue">
Modifiedsrc/views/components.tsx+29−2View fileUnifiedSplit
172172 | "debt-map"
173173 | "migrate"
174174 | "deployments"
175 | "nl-search";
176}> = ({ owner, repo, active }) => (
175 | "nl-search"
176 | "contributors"
177 | "pulse"
178 | "traffic";
179 /** Current authenticated user — used for owner-only tab gating. */
180 currentUser?: string | null;
181 /** Repo owner username — used for owner-only tab gating. */
182 repoOwner?: string;
183}> = ({ owner, repo, active, currentUser, repoOwner }) => (
177184 <div class="repo-nav">
178185 <a href={`/${owner}/${repo}`} class={active === "code" ? "active" : ""}>
179186 Code
226233 >
227234 Releases
228235 </a>
236 <a
237 href={`/${owner}/${repo}/contributors`}
238 class={active === "contributors" ? "active" : ""}
239 >
240 Contributors
241 </a>
242 <a
243 href={`/${owner}/${repo}/pulse`}
244 class={active === "pulse" ? "active" : ""}
245 >
246 Pulse
247 </a>
248 {currentUser && repoOwner && currentUser === repoOwner && (
249 <a
250 href={`/${owner}/${repo}/traffic`}
251 class={active === "traffic" ? "active" : ""}
252 >
253 Traffic
254 </a>
255 )}
229256 <a
230257 href={`/${owner}/${repo}/gates`}
231258 class={active === "gates" ? "active" : ""}
232259