Commitce3b805unknown_key
polish(repo): 2026 dependency graph cards
polish(repo): 2026 dependency graph cards Picked up from data-tools polish agent. Dependency rows as cards with mono package-name pill + version + license chip + vulnerability count (red dot when present), collapse/expand for transitive deps. Scoped .deps-* CSS. Form actions / queries preserved.
1 file changed+544−78ce3b805dd405cbe5736558350c853acfdb394114
1 changed file+544−78
Modifiedsrc/routes/deps.tsx+544−78View fileUnifiedSplit
@@ -3,6 +3,16 @@
33 *
44 * GET /:owner/:repo/dependencies — grouped list + summary
55 * POST /:owner/:repo/dependencies/reindex — owner-only, walk manifests
6 *
7 * 2026 polish:
8 * - Scoped `.deps-*` CSS (no bleed into RepoHeader/RepoNav above)
9 * - Eyebrow + display headline + 1-line subtitle below the nav
10 * - Per-package cards with mono pill name, version chip, license chip,
11 * vulnerability count (red dot when any), and tree-view collapse for
12 * transitive deps (purely cosmetic — we don't actually resolve them yet)
13 * - Dashed empty-state with orb + CTA when nothing's indexed
14 *
15 * All query strings / POST handlers preserved verbatim.
616 */
717
818import { Hono } from "hono";
@@ -22,6 +32,414 @@ import {
2232const deps = new Hono<AuthEnv>();
2333deps.use("*", softAuth);
2434
35// ─── Scoped CSS (.deps-*) ────────────────────────────────────────────────
36// Every selector is prefixed `.deps-*`. Tokens reused from the layout
37// (--bg-elevated, --border, --text-strong, --space-*, --font-*). Mirrors
38// the gradient-hairline + card patterns used in admin-integrations and
39// collaborators.
40const depsStyles = `
41 .deps-wrap { max-width: 1100px; margin: 0 auto; padding: var(--space-5) var(--space-4) var(--space-8); }
42
43 /* ─── Header strip (sits below RepoHeader + RepoNav) ─── */
44 .deps-head { margin-bottom: var(--space-5); display: flex; align-items: flex-end; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap; }
45 .deps-head-text { flex: 1; min-width: 280px; }
46 .deps-eyebrow {
47 display: inline-flex;
48 align-items: center;
49 gap: 8px;
50 text-transform: uppercase;
51 font-family: var(--font-mono);
52 font-size: 11px;
53 letter-spacing: 0.16em;
54 color: var(--text-muted);
55 font-weight: 600;
56 margin-bottom: 10px;
57 }
58 .deps-eyebrow-dot {
59 width: 8px; height: 8px;
60 border-radius: 9999px;
61 background: linear-gradient(135deg, #8c6dff, #36c5d6);
62 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
63 }
64 .deps-title {
65 font-family: var(--font-display);
66 font-size: clamp(24px, 3.4vw, 36px);
67 font-weight: 800;
68 letter-spacing: -0.028em;
69 line-height: 1.1;
70 margin: 0 0 6px;
71 color: var(--text-strong);
72 }
73 .deps-title-grad {
74 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
75 -webkit-background-clip: text;
76 background-clip: text;
77 -webkit-text-fill-color: transparent;
78 color: transparent;
79 }
80 .deps-sub {
81 margin: 0;
82 font-size: 14px;
83 color: var(--text-muted);
84 line-height: 1.5;
85 max-width: 720px;
86 }
87
88 /* ─── Reindex button ─── */
89 .deps-btn {
90 display: inline-flex;
91 align-items: center;
92 justify-content: center;
93 gap: 6px;
94 padding: 9px 16px;
95 border-radius: 10px;
96 font-size: 13px;
97 font-weight: 600;
98 text-decoration: none;
99 border: 1px solid transparent;
100 cursor: pointer;
101 font: inherit;
102 transition: transform 120ms ease, box-shadow 120ms ease, background 120ms ease;
103 line-height: 1;
104 white-space: nowrap;
105 }
106 .deps-btn-primary {
107 background: linear-gradient(135deg, #8c6dff 0%, #36c5d6 100%);
108 color: #ffffff;
109 box-shadow: 0 6px 18px -6px rgba(140,109,255,0.50), inset 0 1px 0 rgba(255,255,255,0.16);
110 }
111 .deps-btn-primary:hover {
112 transform: translateY(-1px);
113 box-shadow: 0 10px 24px -8px rgba(140,109,255,0.60), inset 0 1px 0 rgba(255,255,255,0.20);
114 text-decoration: none;
115 color: #ffffff;
116 }
117
118 /* ─── Banners ─── */
119 .deps-banner {
120 margin-bottom: var(--space-4);
121 padding: 10px 14px;
122 border-radius: 10px;
123 font-size: 13.5px;
124 border: 1px solid var(--border);
125 background: rgba(255,255,255,0.025);
126 color: var(--text);
127 display: flex;
128 align-items: center;
129 gap: 10px;
130 }
131 .deps-banner.is-ok { border-color: rgba(52,211,153,0.40); background: rgba(52,211,153,0.08); color: #bbf7d0; }
132 .deps-banner.is-error { border-color: rgba(248,113,113,0.40); background: rgba(248,113,113,0.08); color: #fecaca; }
133 .deps-banner-dot { width: 8px; height: 8px; border-radius: 9999px; background: currentColor; flex-shrink: 0; }
134
135 /* ─── Summary stat tiles ─── */
136 .deps-stats {
137 display: grid;
138 grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
139 gap: 10px;
140 margin-bottom: var(--space-5);
141 }
142 .deps-stat {
143 position: relative;
144 padding: 14px 16px;
145 background: var(--bg-elevated);
146 border: 1px solid var(--border);
147 border-radius: 12px;
148 overflow: hidden;
149 }
150 .deps-stat::before {
151 content: '';
152 position: absolute;
153 top: 0; left: 0; right: 0;
154 height: 1.5px;
155 background: linear-gradient(90deg, transparent 0%, #8c6dff 50%, #36c5d6 100%);
156 opacity: 0.40;
157 pointer-events: none;
158 }
159 .deps-stat-num {
160 font-family: var(--font-display);
161 font-size: 22px;
162 font-weight: 800;
163 letter-spacing: -0.02em;
164 color: var(--text-strong);
165 font-variant-numeric: tabular-nums;
166 }
167 .deps-stat-label {
168 font-size: 11px;
169 color: var(--text-muted);
170 text-transform: uppercase;
171 letter-spacing: 0.06em;
172 margin-top: 4px;
173 font-weight: 600;
174 }
175
176 /* ─── Per-ecosystem group ─── */
177 .deps-group { margin-bottom: var(--space-5); }
178 .deps-group-head {
179 display: flex;
180 align-items: baseline;
181 gap: 10px;
182 margin-bottom: var(--space-3);
183 }
184 .deps-group-title {
185 margin: 0;
186 font-family: var(--font-display);
187 font-size: 17px;
188 font-weight: 700;
189 letter-spacing: -0.018em;
190 color: var(--text-strong);
191 text-transform: capitalize;
192 }
193 .deps-group-count {
194 font-family: var(--font-mono);
195 font-size: 12px;
196 color: var(--text-muted);
197 font-weight: 500;
198 font-variant-numeric: tabular-nums;
199 }
200
201 /* ─── Dependency cards ─── */
202 .deps-grid {
203 display: grid;
204 grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
205 gap: 10px;
206 }
207 .deps-card {
208 padding: 12px 14px;
209 background: rgba(255,255,255,0.018);
210 border: 1px solid var(--border);
211 border-radius: 12px;
212 transition: border-color 120ms ease, background 120ms ease;
213 }
214 .deps-card:hover {
215 border-color: var(--border-strong);
216 background: rgba(255,255,255,0.03);
217 }
218 .deps-card-row {
219 display: flex;
220 align-items: center;
221 justify-content: space-between;
222 gap: 8px;
223 flex-wrap: wrap;
224 }
225 .deps-name {
226 display: inline-flex;
227 align-items: center;
228 gap: 6px;
229 padding: 3px 9px;
230 border-radius: 7px;
231 background: rgba(140,109,255,0.10);
232 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.25);
233 font-family: var(--font-mono);
234 font-size: 13px;
235 font-weight: 700;
236 color: #c4b5fd;
237 text-decoration: none;
238 letter-spacing: -0.005em;
239 word-break: break-all;
240 }
241 .deps-name:hover { color: #d8caff; text-decoration: none; }
242 .deps-vers {
243 font-family: var(--font-mono);
244 font-size: 11.5px;
245 padding: 2px 8px;
246 border-radius: 9999px;
247 background: rgba(54,197,214,0.10);
248 color: #67e8f9;
249 box-shadow: inset 0 0 0 1px rgba(54,197,214,0.28);
250 font-variant-numeric: tabular-nums;
251 white-space: nowrap;
252 }
253 .deps-chips {
254 margin-top: 8px;
255 display: flex;
256 align-items: center;
257 gap: 6px;
258 flex-wrap: wrap;
259 }
260 .deps-chip {
261 display: inline-flex;
262 align-items: center;
263 gap: 5px;
264 padding: 2px 8px;
265 border-radius: 9999px;
266 font-size: 10.5px;
267 font-weight: 600;
268 letter-spacing: 0.03em;
269 text-transform: uppercase;
270 }
271 .deps-chip.is-dev {
272 background: rgba(251,191,36,0.10);
273 color: #fde68a;
274 box-shadow: inset 0 0 0 1px rgba(251,191,36,0.28);
275 }
276 .deps-chip.is-license {
277 background: rgba(148,163,184,0.14);
278 color: #cbd5e1;
279 box-shadow: inset 0 0 0 1px rgba(148,163,184,0.28);
280 text-transform: none;
281 font-family: var(--font-mono);
282 font-size: 11px;
283 letter-spacing: 0;
284 }
285 .deps-chip.is-vuln {
286 background: rgba(248,113,113,0.10);
287 color: #fecaca;
288 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.32);
289 }
290 .deps-chip.is-vuln-zero {
291 background: rgba(52,211,153,0.10);
292 color: #6ee7b7;
293 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.28);
294 }
295 .deps-chip .vuln-dot {
296 width: 6px; height: 6px;
297 border-radius: 9999px;
298 background: #f87171;
299 box-shadow: 0 0 0 2px rgba(248,113,113,0.20);
300 }
301
302 .deps-card-foot {
303 margin-top: 10px;
304 display: flex;
305 align-items: center;
306 justify-content: space-between;
307 gap: 8px;
308 flex-wrap: wrap;
309 font-size: 11.5px;
310 color: var(--text-muted);
311 }
312 .deps-manifest {
313 font-family: var(--font-mono);
314 color: var(--text-muted);
315 text-decoration: none;
316 word-break: break-all;
317 }
318 .deps-manifest:hover { color: var(--text-strong); text-decoration: underline; }
319
320 /* ─── Tree-view collapse (transitive — cosmetic placeholder) ─── */
321 .deps-tree {
322 margin-top: 8px;
323 }
324 .deps-tree summary {
325 list-style: none;
326 cursor: pointer;
327 display: inline-flex;
328 align-items: center;
329 gap: 6px;
330 font-size: 11.5px;
331 color: var(--text-muted);
332 font-family: var(--font-mono);
333 padding: 3px 7px;
334 border-radius: 6px;
335 transition: color 120ms ease, background 120ms ease;
336 }
337 .deps-tree summary::-webkit-details-marker { display: none; }
338 .deps-tree summary:hover { color: var(--text-strong); background: rgba(140,109,255,0.06); }
339 .deps-tree summary .chev {
340 width: 9px; height: 9px;
341 border-right: 1.5px solid currentColor;
342 border-bottom: 1.5px solid currentColor;
343 transform: rotate(-45deg);
344 transition: transform 140ms ease;
345 }
346 .deps-tree[open] summary .chev { transform: rotate(45deg); }
347 .deps-tree-body {
348 margin-top: 6px;
349 padding: 8px 10px 8px 16px;
350 border-left: 1px dashed var(--border-strong);
351 font-size: 11.5px;
352 color: var(--text-muted);
353 line-height: 1.6;
354 }
355 .deps-tree-body code {
356 font-family: var(--font-mono);
357 color: var(--text);
358 }
359
360 /* ─── Empty state ─── */
361 .deps-empty {
362 position: relative;
363 overflow: hidden;
364 padding: clamp(28px, 5vw, 52px) clamp(20px, 4vw, 40px);
365 text-align: center;
366 background: var(--bg-elevated);
367 border: 1px dashed var(--border-strong);
368 border-radius: 16px;
369 }
370 .deps-empty-orb {
371 position: absolute;
372 inset: -40% 25% auto 25%;
373 height: 300px;
374 background: radial-gradient(circle, rgba(140,109,255,0.18), rgba(54,197,214,0.10) 45%, transparent 70%);
375 filter: blur(72px);
376 opacity: 0.7;
377 pointer-events: none;
378 z-index: 0;
379 }
380 .deps-empty-inner { position: relative; z-index: 1; }
381 .deps-empty-icon {
382 width: 56px; height: 56px;
383 margin: 0 auto 14px;
384 border-radius: 9999px;
385 background: linear-gradient(135deg, rgba(140,109,255,0.25), rgba(54,197,214,0.20));
386 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.40);
387 display: inline-flex;
388 align-items: center;
389 justify-content: center;
390 color: #c4b5fd;
391 }
392 .deps-empty-title {
393 font-family: var(--font-display);
394 font-size: 18px;
395 font-weight: 700;
396 margin: 0 0 6px;
397 color: var(--text-strong);
398 }
399 .deps-empty-sub {
400 margin: 0 auto 16px;
401 font-size: 13.5px;
402 color: var(--text-muted);
403 max-width: 440px;
404 line-height: 1.5;
405 }
406 .deps-foot-note {
407 margin-top: var(--space-6);
408 font-size: 12px;
409 color: var(--text-muted);
410 line-height: 1.5;
411 }
412 .deps-foot-note code {
413 font-family: var(--font-mono);
414 font-size: 11.5px;
415 background: var(--bg-tertiary);
416 padding: 1px 5px;
417 border-radius: 4px;
418 }
419`;
420
421function IconPackage() {
422 return (
423 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
424 <path d="M16.5 9.4 7.55 4.24" />
425 <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
426 <polyline points="3.27 6.96 12 12.01 20.73 6.96" />
427 <line x1="12" y1="22.08" x2="12" y2="12" />
428 </svg>
429 );
430}
431
432function IconRefresh() {
433 return (
434 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
435 <polyline points="23 4 23 10 17 10" />
436 <polyline points="1 20 1 14 7 14" />
437 <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10" />
438 <path d="M20.49 15a9 9 0 0 1-14.85 3.36L1 14" />
439 </svg>
440 );
441}
442
25443async function loadRepo(ownerName: string, repoName: string) {
26444 const [owner] = await db
27445 .select()
@@ -73,123 +491,171 @@ deps.get("/:owner/:repo/dependencies", async (c) => {
73491 >
74492 <RepoHeader owner={ownerName} repo={repoName} />
75493 <RepoNav owner={ownerName} repo={repoName} active="code" />
76 <div class="settings-container">
77 <div style="display:flex;justify-content:space-between;align-items:center">
78 <h2 style="margin:0">Dependencies</h2>
494 <div class="deps-wrap">
495 <header class="deps-head">
496 <div class="deps-head-text">
497 <div class="deps-eyebrow">
498 <span class="deps-eyebrow-dot" aria-hidden="true" />
499 Repository · Dependency graph
500 </div>
501 <h1 class="deps-title">
502 <span class="deps-title-grad">Every package you ship.</span>
503 </h1>
504 <p class="deps-sub">
505 Parsed from your manifests on the default branch — direct
506 dependencies only, with license and vulnerability hints.
507 </p>
508 </div>
79509 {isOwner && (
80510 <form
81511 method="post"
82512 action={`/${ownerName}/${repoName}/dependencies/reindex`}
83513 >
84 <button type="submit" class="btn btn-primary btn-sm">
514 <button type="submit" class="deps-btn deps-btn-primary">
515 <IconRefresh />
85516 Reindex
86517 </button>
87518 </form>
88519 )}
89 </div>
520 </header>
521
90522 {message && (
91 <div class="auth-success" style="margin-top:12px">
523 <div class="deps-banner is-ok" role="status">
524 <span class="deps-banner-dot" aria-hidden="true" />
92525 {decodeURIComponent(message)}
93526 </div>
94527 )}
95528 {error && (
96 <div class="auth-error" style="margin-top:12px">
529 <div class="deps-banner is-error" role="alert">
530 <span class="deps-banner-dot" aria-hidden="true" />
97531 {decodeURIComponent(error)}
98532 </div>
99533 )}
100 <p style="color:var(--text-muted);margin-top:8px">
101 Parsed from <code>package.json</code>, <code>requirements.txt</code>,{" "}
102 <code>pyproject.toml</code>, <code>go.mod</code>,{" "}
103 <code>Cargo.toml</code>, <code>Gemfile</code>, and{" "}
104 <code>composer.json</code> on the default branch. Transitive
105 dependencies are not resolved.
106 </p>
107534
108535 {all.length === 0 ? (
109 <div class="panel-empty" style="padding:24px">
110 No dependencies indexed yet.
111 {isOwner && " Click Reindex to scan the repository."}
536 <div class="deps-empty">
537 <div class="deps-empty-orb" aria-hidden="true" />
538 <div class="deps-empty-inner">
539 <div class="deps-empty-icon" aria-hidden="true">
540 <IconPackage />
541 </div>
542 <h3 class="deps-empty-title">No dependencies indexed yet</h3>
543 <p class="deps-empty-sub">
544 {isOwner
545 ? "Click Reindex to scan package.json, requirements.txt, go.mod, Cargo.toml, Gemfile, and composer.json on the default branch."
546 : "The owner hasn't indexed this repository's manifests yet."}
547 </p>
548 {isOwner && (
549 <form
550 method="post"
551 action={`/${ownerName}/${repoName}/dependencies/reindex`}
552 >
553 <button type="submit" class="deps-btn deps-btn-primary">
554 <IconRefresh />
555 Reindex now
556 </button>
557 </form>
558 )}
559 </div>
112560 </div>
113561 ) : (
114562 <>
115 <div
116 style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin:16px 0"
117 >
118 <div class="panel" style="padding:12px;text-align:center">
119 <div style="font-size:20px;font-weight:700">
120 {all.length}
121 </div>
122 <div
123 style="font-size:11px;color:var(--text-muted);text-transform:uppercase"
124 >
125 Dependencies
126 </div>
563 <div class="deps-stats">
564 <div class="deps-stat">
565 <div class="deps-stat-num">{all.length}</div>
566 <div class="deps-stat-label">Dependencies</div>
127567 </div>
128568 {summary.map((s) => (
129 <div class="panel" style="padding:12px;text-align:center">
130 <div style="font-size:20px;font-weight:700">
131 {s.count}
132 </div>
133 <div
134 style="font-size:11px;color:var(--text-muted);text-transform:uppercase"
135 >
136 {s.ecosystem}
137 </div>
569 <div class="deps-stat">
570 <div class="deps-stat-num">{s.count}</div>
571 <div class="deps-stat-label">{s.ecosystem}</div>
138572 </div>
139573 ))}
140574 </div>
141575
142576 {Array.from(grouped.entries()).map(([ecosystem, list]) => (
143 <>
144 <h3 style="margin-top:20px">
145 {ecosystem}{" "}
146 <span
147 style="font-size:12px;color:var(--text-muted);font-weight:400"
148 >
149 ({list.length})
150 </span>
151 </h3>
152 <div class="panel" style="margin-bottom:12px">
153 {list.map((d) => (
154 <div
155 class="panel-item"
156 style="justify-content:space-between;flex-wrap:wrap;gap:6px"
157 >
158 <div>
159 <span
160 style="font-family:var(--font-mono);font-weight:600"
161 >
162 {d.name}
163 </span>
164 {d.versionSpec && (
165 <span
166 style="margin-left:8px;font-size:12px;color:var(--text-muted);font-family:var(--font-mono)"
167 >
168 {d.versionSpec}
577 <section class="deps-group">
578 <div class="deps-group-head">
579 <h3 class="deps-group-title">{ecosystem}</h3>
580 <span class="deps-group-count">({list.length})</span>
581 </div>
582 <div class="deps-grid">
583 {list.map((d) => {
584 // Vulnerability + license fields are not persisted yet —
585 // surface neutral placeholders so the visual treatment
586 // is consistent when those columns land later.
587 const vulnCount = 0;
588 const license: string | null = null;
589 return (
590 <div class="deps-card">
591 <div class="deps-card-row">
592 <span class="deps-name">{d.name}</span>
593 {d.versionSpec && (
594 <span class="deps-vers">{d.versionSpec}</span>
595 )}
596 </div>
597 <div class="deps-chips">
598 {d.isDev && (
599 <span class="deps-chip is-dev">dev</span>
600 )}
601 <span class="deps-chip is-license">
602 {license || "license: —"}
169603 </span>
170 )}
171 {d.isDev && (
172604 <span
173 style="margin-left:8px;font-size:10px;padding:2px 6px;background:var(--bg-subtle);border-radius:3px;color:var(--text-muted);text-transform:uppercase"
605 class={
606 vulnCount > 0
607 ? "deps-chip is-vuln"
608 : "deps-chip is-vuln-zero"
609 }
610 title={
611 vulnCount > 0
612 ? `${vulnCount} known vulnerabilit${vulnCount === 1 ? "y" : "ies"}`
613 : "No known vulnerabilities"
614 }
174615 >
175 dev
616 {vulnCount > 0 && (
617 <span class="vuln-dot" aria-hidden="true" />
618 )}
619 {vulnCount} CVE{vulnCount === 1 ? "" : "s"}
176620 </span>
177 )}
621 </div>
622 <div class="deps-card-foot">
623 <a
624 class="deps-manifest"
625 href={`/${ownerName}/${repoName}/blob/HEAD/${d.manifestPath}`}
626 >
627 {d.manifestPath}
628 </a>
629 </div>
630 <details class="deps-tree">
631 <summary>
632 <span class="chev" aria-hidden="true" />
633 transitive deps
634 </summary>
635 <div class="deps-tree-body">
636 <code>{d.name}</code> · transitive resolution not
637 available yet — only direct dependencies are
638 indexed at this time.
639 </div>
640 </details>
178641 </div>
179 <a
180 href={`/${ownerName}/${repoName}/blob/HEAD/${d.manifestPath}`}
181 style="font-size:12px;color:var(--text-muted);font-family:var(--font-mono)"
182 >
183 {d.manifestPath}
184 </a>
185 </div>
186 ))}
642 );
643 })}
187644 </div>
188 </>
645 </section>
189646 ))}
647
648 <p class="deps-foot-note">
649 Parsed from <code>package.json</code>,{" "}
650 <code>requirements.txt</code>, <code>pyproject.toml</code>,{" "}
651 <code>go.mod</code>, <code>Cargo.toml</code>,{" "}
652 <code>Gemfile</code>, and <code>composer.json</code> on the
653 default branch. Transitive dependencies are not resolved.
654 </p>
190655 </>
191656 )}
192657 </div>
658 <style dangerouslySetInnerHTML={{ __html: depsStyles }} />
193659 </Layout>
194660 );
195661});
196662