nodebench-mcp 2.22.0 → 2.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +366 -280
- package/dist/__tests__/multiHopDogfood.test.d.ts +12 -0
- package/dist/__tests__/multiHopDogfood.test.js +303 -0
- package/dist/__tests__/multiHopDogfood.test.js.map +1 -0
- package/dist/__tests__/presetRealWorldBench.test.js +2 -0
- package/dist/__tests__/presetRealWorldBench.test.js.map +1 -1
- package/dist/__tests__/tools.test.js +158 -6
- package/dist/__tests__/tools.test.js.map +1 -1
- package/dist/__tests__/toolsetGatingEval.test.js +2 -0
- package/dist/__tests__/toolsetGatingEval.test.js.map +1 -1
- package/dist/dashboard/html.d.ts +18 -0
- package/dist/dashboard/html.js +1251 -0
- package/dist/dashboard/html.js.map +1 -0
- package/dist/dashboard/server.d.ts +17 -0
- package/dist/dashboard/server.js +278 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/db.js +38 -0
- package/dist/db.js.map +1 -1
- package/dist/index.js +19 -9
- package/dist/index.js.map +1 -1
- package/dist/tools/prReportTools.d.ts +11 -0
- package/dist/tools/prReportTools.js +911 -0
- package/dist/tools/prReportTools.js.map +1 -0
- package/dist/tools/progressiveDiscoveryTools.js +111 -24
- package/dist/tools/progressiveDiscoveryTools.js.map +1 -1
- package/dist/tools/skillUpdateTools.d.ts +24 -0
- package/dist/tools/skillUpdateTools.js +469 -0
- package/dist/tools/skillUpdateTools.js.map +1 -0
- package/dist/tools/toolRegistry.d.ts +15 -1
- package/dist/tools/toolRegistry.js +315 -11
- package/dist/tools/toolRegistry.js.map +1 -1
- package/dist/tools/uiUxDiveAdvancedTools.js +61 -0
- package/dist/tools/uiUxDiveAdvancedTools.js.map +1 -1
- package/dist/tools/uiUxDiveTools.js +154 -1
- package/dist/tools/uiUxDiveTools.js.map +1 -1
- package/dist/toolsetRegistry.js +4 -0
- package/dist/toolsetRegistry.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,1251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NodeBench MCP — Dashboard HTML v4
|
|
3
|
+
*
|
|
4
|
+
* Single-scroll, zero-tab dashboard. Everything visible at once.
|
|
5
|
+
* Clean, intuitive design: Inter font, clear section hierarchy.
|
|
6
|
+
* Auto-refreshes every 5s with hash-based diffing.
|
|
7
|
+
*
|
|
8
|
+
* v4 improvements:
|
|
9
|
+
* - CSS custom properties for consistent spacing/shadows/gradients
|
|
10
|
+
* - Responsive @media breakpoints for mobile/tablet
|
|
11
|
+
* - Full ARIA accessibility (labels, roles, keyboard nav, focus-visible)
|
|
12
|
+
* - Carousel: IntersectionObserver dot tracking, edge-disabled arrows, keyboard arrows
|
|
13
|
+
* - XSS safety: all user-controlled strings escaped
|
|
14
|
+
* - Smart refresh: skip re-render when data unchanged
|
|
15
|
+
* - Priority-scored category detection
|
|
16
|
+
* - Deferred rendering for collapsed cards (DOM-light pagination)
|
|
17
|
+
*/
|
|
18
|
+
export function getDashboardHtml() {
|
|
19
|
+
return `<!DOCTYPE html>
|
|
20
|
+
<html lang="en" class="dark">
|
|
21
|
+
<head>
|
|
22
|
+
<meta charset="UTF-8">
|
|
23
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
24
|
+
<title>NodeBench UI Dive</title>
|
|
25
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
26
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
|
27
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
28
|
+
<script>
|
|
29
|
+
tailwind.config = {
|
|
30
|
+
darkMode: 'class',
|
|
31
|
+
theme: {
|
|
32
|
+
extend: {
|
|
33
|
+
fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'] },
|
|
34
|
+
colors: {
|
|
35
|
+
surface: { 0: '#09090b', 1: '#111113', 2: '#18181b', 3: '#1f1f23' },
|
|
36
|
+
border: { DEFAULT: '#27272a', subtle: '#1e1e22', focus: '#6366f1' },
|
|
37
|
+
accent: { DEFAULT: '#818cf8', bright: '#a5b4fc', dim: '#4f46e5' },
|
|
38
|
+
ok: '#34d399', warn: '#fbbf24', err: '#f87171',
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
<style>
|
|
45
|
+
/* ── Design Tokens ──────────────────────────────────────── */
|
|
46
|
+
:root {
|
|
47
|
+
--sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 24px; --sp-6: 32px;
|
|
48
|
+
--border-base: #27272a;
|
|
49
|
+
--border-hover: #3f3f46;
|
|
50
|
+
--border-accent: #6366f1;
|
|
51
|
+
--surface-1: #111113;
|
|
52
|
+
--surface-2: #18181b;
|
|
53
|
+
--gradient-accent: linear-gradient(135deg, #1e1b4b, #312e81);
|
|
54
|
+
--shadow-card: 0 0 0 1px rgba(99,102,241,.15), 0 1px 3px rgba(0,0,0,.4);
|
|
55
|
+
--shadow-card-hover: 0 0 0 1px rgba(99,102,241,.35), 0 4px 12px rgba(0,0,0,.5);
|
|
56
|
+
--shadow-lift: 0 8px 30px rgba(99,102,241,.12), 0 2px 8px rgba(0,0,0,.4);
|
|
57
|
+
--radius-sm: 6px; --radius-md: 8px; --radius-lg: 10px; --radius-xl: 12px;
|
|
58
|
+
--transition-fast: .15s ease; --transition-base: .2s ease;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ── Reset & Base ───────────────────────────────────────── */
|
|
62
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
63
|
+
body { font-family: 'Inter', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
|
64
|
+
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
|
|
65
|
+
|
|
66
|
+
/* ── Shared Interactive ─────────────────────────────────── */
|
|
67
|
+
.glass { background: rgba(17,17,19,.72); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); }
|
|
68
|
+
.ring-glow { box-shadow: var(--shadow-card); }
|
|
69
|
+
.ring-glow:hover { box-shadow: var(--shadow-card-hover); }
|
|
70
|
+
|
|
71
|
+
.btn-icon {
|
|
72
|
+
width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
|
|
73
|
+
border-radius: var(--radius-md); border: 1px solid var(--border-base);
|
|
74
|
+
background: transparent; color: #52525b; cursor: pointer;
|
|
75
|
+
transition: all var(--transition-fast);
|
|
76
|
+
}
|
|
77
|
+
.btn-icon:hover { background: var(--surface-2); color: #d4d4d8; border-color: var(--border-hover); }
|
|
78
|
+
.btn-icon.active { background: var(--gradient-accent); border-color: var(--border-accent); color: #c7d2fe; }
|
|
79
|
+
.btn-icon:focus-visible { outline: 2px solid var(--border-accent); outline-offset: 2px; }
|
|
80
|
+
.btn-icon svg { width: 16px; height: 16px; }
|
|
81
|
+
|
|
82
|
+
/* ── Focus-Visible (global) ─────────────────────────────── */
|
|
83
|
+
:focus-visible { outline: 2px solid var(--border-accent); outline-offset: 2px; }
|
|
84
|
+
:focus:not(:focus-visible) { outline: none; }
|
|
85
|
+
|
|
86
|
+
/* ── Animations ─────────────────────────────────────────── */
|
|
87
|
+
@keyframes fadeUp { from { opacity:0; transform: translateY(8px); } to { opacity:1; transform: translateY(0); } }
|
|
88
|
+
.fade-up { animation: fadeUp .35s ease-out both; }
|
|
89
|
+
/* Staggered cascade for lists of cards */
|
|
90
|
+
.fade-up:nth-child(2) { animation-delay: 50ms; }
|
|
91
|
+
.fade-up:nth-child(3) { animation-delay: 100ms; }
|
|
92
|
+
.fade-up:nth-child(n+4) { animation-delay: 150ms; }
|
|
93
|
+
@keyframes pulse2 { 0%,100%{opacity:1} 50%{opacity:.35} }
|
|
94
|
+
.pulse-live { animation: pulse2 2s infinite; }
|
|
95
|
+
|
|
96
|
+
/* ── Skeleton Loading ──────────────────────────────────── */
|
|
97
|
+
.skeleton { border-radius: var(--radius-lg); background: linear-gradient(90deg, var(--surface-2) 25%, #1f1f23 50%, var(--surface-2) 75%); background-size: 200% 100%; animation: skeleton-shimmer 1.5s ease-in-out infinite; }
|
|
98
|
+
@keyframes skeleton-shimmer { 0%, 100% { background-position: 200% 0; } 50% { background-position: 0% 0; } }
|
|
99
|
+
|
|
100
|
+
/* ── Reduced Motion ────────────────────────────────────── */
|
|
101
|
+
@media (prefers-reduced-motion: reduce) {
|
|
102
|
+
*, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; }
|
|
103
|
+
.fade-up { animation: none; opacity: 1; transform: none; }
|
|
104
|
+
.pulse-live { animation: none; }
|
|
105
|
+
.skeleton { animation: none; }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* ── Score Ring ─────────────────────────────────────────── */
|
|
109
|
+
.score-ring { width:64px; height:64px; }
|
|
110
|
+
.score-ring circle { fill:none; stroke-width:5; stroke-linecap:round; }
|
|
111
|
+
.score-ring .bg { stroke: var(--border-base); }
|
|
112
|
+
.score-ring .fg { transition: stroke-dashoffset .6s ease; }
|
|
113
|
+
pre { white-space: pre-wrap; word-break: break-word; tab-size: 2; }
|
|
114
|
+
|
|
115
|
+
/* ── Severity Badges ───────────────────────────────────── */
|
|
116
|
+
.sev-critical { background:#450a0a; color:#fca5a5; }
|
|
117
|
+
.sev-high { background:#451a03; color:#fde68a; }
|
|
118
|
+
.sev-medium { background:#0c1a3d; color:#93c5fd; }
|
|
119
|
+
.sev-low { background:#052e16; color:#86efac; }
|
|
120
|
+
|
|
121
|
+
/* ── File Chips ────────────────────────────────────────── */
|
|
122
|
+
.file-chip {
|
|
123
|
+
display:inline-block; padding:2px 8px; margin:2px 3px 2px 0; border-radius:4px;
|
|
124
|
+
background: var(--surface-2); border:1px solid var(--border-base);
|
|
125
|
+
font-size:11px; font-family:'SF Mono',Monaco,monospace; color:#a1a1aa;
|
|
126
|
+
max-width:260px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* ── Screenshot Grid ───────────────────────────────────── */
|
|
130
|
+
.ss-grid { display:grid; gap: var(--sp-3); }
|
|
131
|
+
.ss-grid.sz-sm { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
|
|
132
|
+
.ss-grid.sz-md { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
|
|
133
|
+
.ss-grid.sz-lg { grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); }
|
|
134
|
+
.ss-card {
|
|
135
|
+
cursor:pointer; transition: transform var(--transition-base), box-shadow var(--transition-base), border-color var(--transition-base);
|
|
136
|
+
border-radius: var(--radius-lg); overflow:hidden; border:1px solid var(--border-base); background: var(--surface-1);
|
|
137
|
+
}
|
|
138
|
+
.ss-card:hover { transform:translateY(-3px); box-shadow: var(--shadow-lift); border-color:#4f46e5; }
|
|
139
|
+
.ss-card:focus-visible { box-shadow: var(--shadow-lift); border-color:#4f46e5; }
|
|
140
|
+
.ss-card img { width:100%; aspect-ratio:16/10; object-fit:cover; display:block; background:linear-gradient(135deg,#18181b 0%,#1f1f23 100%); }
|
|
141
|
+
.ss-meta { padding: var(--sp-2) var(--sp-3); border-top:1px solid #1e1e22; }
|
|
142
|
+
.ss-toolbar {
|
|
143
|
+
display:flex; align-items:center; gap: var(--sp-3); flex-wrap:wrap;
|
|
144
|
+
margin-bottom: var(--sp-3); padding: var(--sp-3) 14px;
|
|
145
|
+
background: var(--surface-1); border-radius: var(--radius-lg); border:1px solid #1e1e22;
|
|
146
|
+
}
|
|
147
|
+
.ss-search {
|
|
148
|
+
background:#09090b; border:1px solid var(--border-base); color:#d4d4d8;
|
|
149
|
+
padding:6px 10px 6px 32px; border-radius: var(--radius-md); font-size:12px;
|
|
150
|
+
width:220px; outline:none; transition: border-color var(--transition-fast);
|
|
151
|
+
}
|
|
152
|
+
.ss-search:focus { border-color: var(--border-accent); box-shadow:0 0 0 3px rgba(99,102,241,.1); }
|
|
153
|
+
.ss-search-wrap { position:relative; display:flex; align-items:center; }
|
|
154
|
+
.ss-search-icon { position:absolute; left:9px; top:50%; transform:translateY(-50%); width:14px; height:14px; color:#52525b; pointer-events:none; }
|
|
155
|
+
|
|
156
|
+
/* ── Category Pills ────────────────────────────────────── */
|
|
157
|
+
.cat-pill {
|
|
158
|
+
display:inline-flex; align-items:center; gap:4px; padding:4px 12px;
|
|
159
|
+
border-radius:999px; font-size:11px; font-weight:500; cursor:pointer;
|
|
160
|
+
transition: all var(--transition-fast); border:1px solid var(--border-base);
|
|
161
|
+
color:#a1a1aa; background:transparent; user-select:none;
|
|
162
|
+
font-family:inherit; line-height:1.4;
|
|
163
|
+
}
|
|
164
|
+
.cat-pill:hover { background: var(--surface-2); border-color: var(--border-hover); }
|
|
165
|
+
.cat-pill[aria-pressed="true"] { background: var(--gradient-accent); border-color: var(--border-accent); color:#c7d2fe; box-shadow:0 0 0 1px rgba(99,102,241,.2); }
|
|
166
|
+
.cat-pill:focus-visible { outline:2px solid var(--border-accent); outline-offset:2px; }
|
|
167
|
+
.cat-pill .cat-count { background: var(--border-base); color:#a1a1aa; padding:1px 6px; border-radius:9px; font-size:10px; margin-left:2px; line-height:1.3; }
|
|
168
|
+
.cat-pill[aria-pressed="true"] .cat-count { background:rgba(99,102,241,.3); color:#e0e7ff; }
|
|
169
|
+
|
|
170
|
+
/* ── Grid Size Toggle ──────────────────────────────────── */
|
|
171
|
+
.sz-btn { width:32px; height:32px; display:flex; align-items:center; justify-content:center; border-radius: var(--radius-md); border:1px solid var(--border-base); background:transparent; color:#52525b; cursor:pointer; transition:all var(--transition-fast); }
|
|
172
|
+
.sz-btn:hover { background: var(--surface-2); color:#d4d4d8; border-color: var(--border-hover); }
|
|
173
|
+
.sz-btn[aria-pressed="true"] { background: var(--gradient-accent); border-color: var(--border-accent); color:#c7d2fe; }
|
|
174
|
+
.sz-btn:focus-visible { outline:2px solid var(--border-accent); outline-offset:2px; }
|
|
175
|
+
.sz-btn svg { width:16px; height:16px; }
|
|
176
|
+
|
|
177
|
+
/* ── Screenshot Groups ─────────────────────────────────── */
|
|
178
|
+
.ss-group-hdr { font-size:12px; font-weight:600; color:#a1a1aa; padding: var(--sp-4) 0 var(--sp-2); margin-bottom: var(--sp-3); display:flex; align-items:center; gap: var(--sp-2); border-bottom:1px solid #1e1e22; }
|
|
179
|
+
.ss-group-hdr .g-count { font-weight:400; color:#52525b; font-size:11px; }
|
|
180
|
+
.ss-show-more {
|
|
181
|
+
display:flex; align-items:center; justify-content:center; gap:6px;
|
|
182
|
+
padding:10px var(--sp-5); border-radius: var(--radius-md); border:1px solid var(--border-base);
|
|
183
|
+
background: var(--surface-1); color:#a1a1aa; font-size:12px; font-weight:500;
|
|
184
|
+
cursor:pointer; transition:all var(--transition-fast); margin-top: var(--sp-3);
|
|
185
|
+
}
|
|
186
|
+
.ss-show-more:hover { border-color: var(--border-accent); color:#c7d2fe; background: var(--gradient-accent); }
|
|
187
|
+
.ss-show-more:focus-visible { outline:2px solid var(--border-accent); outline-offset:2px; }
|
|
188
|
+
|
|
189
|
+
/* ── Section Headers ───────────────────────────────────── */
|
|
190
|
+
.sec-hdr { margin-top: var(--sp-6); margin-bottom: var(--sp-4); display:flex; align-items:center; gap: var(--sp-3); }
|
|
191
|
+
.sec-hdr .sec-icon { width:32px; height:32px; border-radius: var(--radius-md); display:flex; align-items:center; justify-content:center; flex-shrink:0; }
|
|
192
|
+
.sec-hdr .sec-icon svg { width:16px; height:16px; }
|
|
193
|
+
.sec-hdr .sec-text h2 { font-size:14px; font-weight:700; color:#fafafa; letter-spacing:-.01em; }
|
|
194
|
+
.sec-hdr .sec-text .sec-sub { font-size:11px; color:#71717a; margin-top:1px; }
|
|
195
|
+
|
|
196
|
+
/* ── Lightbox ───────────────────────────────────────────── */
|
|
197
|
+
.lightbox { position:fixed; inset:0; z-index:200; background:rgba(0,0,0,.92); display:none; align-items:center; justify-content:center; }
|
|
198
|
+
.lightbox.open { display:flex; }
|
|
199
|
+
.lightbox img { max-width:88vw; max-height:85vh; border-radius: var(--radius-md); box-shadow:0 12px 40px rgba(0,0,0,.6); cursor:default; }
|
|
200
|
+
.lb-chrome { position:absolute; bottom:20px; left:50%; transform:translateX(-50%); display:flex; align-items:center; gap: var(--sp-3); background:rgba(17,17,19,.9); padding: var(--sp-2) var(--sp-4); border-radius: var(--radius-lg); border:1px solid var(--border-base); }
|
|
201
|
+
.lb-label { font-size:12px; color:#a1a1aa; max-width:400px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
202
|
+
.lb-counter { font-size:11px; color:#52525b; white-space:nowrap; }
|
|
203
|
+
.lb-nav {
|
|
204
|
+
width:36px; height:36px; border-radius:50%; border:1px solid var(--border-hover);
|
|
205
|
+
background:rgba(17,17,19,.8); color:#d4d4d8; display:flex; align-items:center;
|
|
206
|
+
justify-content:center; cursor:pointer; font-size:18px; transition:all var(--transition-fast);
|
|
207
|
+
position:absolute; top:50%; z-index:201;
|
|
208
|
+
}
|
|
209
|
+
.lb-nav:hover { background:#4f46e5; border-color: var(--border-accent); color:#fff; }
|
|
210
|
+
.lb-nav:focus-visible { outline:2px solid #a5b4fc; outline-offset:2px; }
|
|
211
|
+
.lb-nav.prev { left:16px; transform:translateY(-50%); }
|
|
212
|
+
.lb-nav.next { right:16px; transform:translateY(-50%); }
|
|
213
|
+
.lb-close {
|
|
214
|
+
position:absolute; top:16px; right:16px; width:36px; height:36px; border-radius:50%;
|
|
215
|
+
border:1px solid var(--border-hover); background:rgba(17,17,19,.8); color:#d4d4d8;
|
|
216
|
+
display:flex; align-items:center; justify-content:center; cursor:pointer; font-size:18px;
|
|
217
|
+
z-index:201; transition:all var(--transition-fast);
|
|
218
|
+
}
|
|
219
|
+
.lb-close:hover { background:#dc2626; border-color:#ef4444; color:#fff; }
|
|
220
|
+
.lb-close:focus-visible { outline:2px solid #ef4444; outline-offset:2px; }
|
|
221
|
+
|
|
222
|
+
/* ── Changelog Carousel ────────────────────────────────── */
|
|
223
|
+
.cl-carousel { position:relative; }
|
|
224
|
+
.cl-track {
|
|
225
|
+
display:flex; gap: var(--sp-4); overflow-x:auto; scroll-snap-type:x mandatory;
|
|
226
|
+
-webkit-overflow-scrolling:touch; padding:4px 0 var(--sp-3); scrollbar-width:none;
|
|
227
|
+
}
|
|
228
|
+
.cl-track::-webkit-scrollbar { display:none; }
|
|
229
|
+
.cl-card {
|
|
230
|
+
flex:0 0 min(100%, 420px); scroll-snap-align:start; background: var(--surface-1);
|
|
231
|
+
border:1px solid var(--border-base); border-radius: var(--radius-xl); padding:20px;
|
|
232
|
+
display:flex; flex-direction:column; gap: var(--sp-3);
|
|
233
|
+
transition: border-color var(--transition-base), box-shadow var(--transition-base);
|
|
234
|
+
}
|
|
235
|
+
.cl-card:hover { border-color: var(--border-hover); box-shadow:0 4px 20px rgba(0,0,0,.3); }
|
|
236
|
+
.cl-card:only-child { flex:0 0 100%; }
|
|
237
|
+
.cl-step { display:inline-flex; align-items:center; justify-content:center; width:24px; height:24px; border-radius:50%; background: var(--gradient-accent); color:#c7d2fe; font-size:11px; font-weight:700; flex-shrink:0; }
|
|
238
|
+
.cl-time { font-size:11px; color:#52525b; }
|
|
239
|
+
.cl-desc { font-size:12px; color:#d4d4d8; line-height:1.6; flex:1; }
|
|
240
|
+
.cl-files { display:flex; flex-wrap:wrap; gap:4px; }
|
|
241
|
+
.cl-files .file-chip { max-width: min(260px, calc(100% - 8px)); }
|
|
242
|
+
.cl-nav-btn {
|
|
243
|
+
position:absolute; top:50%; transform:translateY(-50%); width:32px; height:32px;
|
|
244
|
+
border-radius:50%; border:1px solid var(--border-base); background:rgba(9,9,11,.85);
|
|
245
|
+
backdrop-filter:blur(8px); color:#a1a1aa; display:flex; align-items:center;
|
|
246
|
+
justify-content:center; cursor:pointer; transition:all var(--transition-fast); z-index:5;
|
|
247
|
+
}
|
|
248
|
+
.cl-nav-btn:hover:not(.disabled) { background:#4f46e5; border-color: var(--border-accent); color:#fff; }
|
|
249
|
+
.cl-nav-btn:focus-visible { outline:2px solid var(--border-accent); outline-offset:2px; }
|
|
250
|
+
.cl-nav-btn.disabled { opacity:.25; cursor:default; pointer-events:none; }
|
|
251
|
+
.cl-nav-btn.prev { left:-12px; }
|
|
252
|
+
.cl-nav-btn.next { right:-12px; }
|
|
253
|
+
.cl-nav-btn svg { width:14px; height:14px; }
|
|
254
|
+
.cl-dots { display:flex; justify-content:center; gap:6px; padding-top: var(--sp-2); }
|
|
255
|
+
.cl-dot {
|
|
256
|
+
width:6px; height:6px; border-radius:50%; background: var(--border-base);
|
|
257
|
+
transition: width var(--transition-base), background var(--transition-base), border-radius var(--transition-base);
|
|
258
|
+
cursor:pointer; border:none; padding:0;
|
|
259
|
+
}
|
|
260
|
+
.cl-dot:focus-visible { outline:2px solid var(--border-accent); outline-offset:2px; }
|
|
261
|
+
.cl-dot.active { background:#818cf8; width:18px; border-radius:3px; }
|
|
262
|
+
|
|
263
|
+
/* ── Compare Mode ──────────────────────────────────────── */
|
|
264
|
+
.compare-bar { display:flex; align-items:center; gap: var(--sp-3); padding: var(--sp-2) var(--sp-4); background: var(--surface-1); border-radius: var(--radius-md); margin-bottom: var(--sp-3); }
|
|
265
|
+
.compare-bar select { background: var(--surface-2); border:1px solid var(--border-base); color:#d4d4d8; padding:4px 8px; border-radius: var(--radius-sm); font-size:12px; }
|
|
266
|
+
.compare-grid { display:grid; grid-template-columns:1fr 1fr; gap: var(--sp-3); }
|
|
267
|
+
.compare-grid .compare-col { border:1px solid var(--border-base); border-radius: var(--radius-md); overflow:hidden; }
|
|
268
|
+
.compare-grid .compare-col .ch { padding: var(--sp-2) var(--sp-3); background: var(--surface-2); font-size:11px; color:#a1a1aa; font-weight:600; text-transform:uppercase; letter-spacing:.05em; }
|
|
269
|
+
.compare-grid .compare-col img { width:100%; display:block; }
|
|
270
|
+
.empty-state { text-align:center; padding: var(--sp-6) var(--sp-4); color:#52525b; }
|
|
271
|
+
.empty-state .empty-icon { font-size:28px; margin-bottom: var(--sp-2); opacity:.5; }
|
|
272
|
+
.empty-state .empty-hint { font-size:12px; line-height:1.5; max-width:400px; margin:0 auto; }
|
|
273
|
+
.nav-pill { display:inline-flex; align-items:center; gap:4px; padding:3px 10px; border-radius:999px; font-size:11px; font-weight:500; cursor:pointer; transition:all var(--transition-fast); border:1px solid transparent; }
|
|
274
|
+
.nav-pill:hover { background: var(--surface-2); }
|
|
275
|
+
.nav-pill.active { background:#1e1b4b; border-color:#4f46e5; color:#a5b4fc; }
|
|
276
|
+
|
|
277
|
+
/* ── Responsive ────────────────────────────────────────── */
|
|
278
|
+
@media (max-width: 640px) {
|
|
279
|
+
.ss-grid.sz-md { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
|
|
280
|
+
.ss-grid.sz-lg { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
|
|
281
|
+
.ss-search { width: min(220px, calc(100% - 16px)); }
|
|
282
|
+
.ss-toolbar { padding: var(--sp-2) var(--sp-3); gap: var(--sp-2); }
|
|
283
|
+
.cl-card { flex: 0 0 min(100%, 320px); padding: var(--sp-4); }
|
|
284
|
+
.compare-grid { grid-template-columns: 1fr; }
|
|
285
|
+
.lb-chrome { max-width: calc(100vw - 32px); }
|
|
286
|
+
}
|
|
287
|
+
@media (max-width: 480px) {
|
|
288
|
+
main { padding-left: var(--sp-3) !important; padding-right: var(--sp-3) !important; }
|
|
289
|
+
.sec-hdr { margin-top: var(--sp-5); }
|
|
290
|
+
.ss-grid.sz-sm { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* ── Print Stylesheet ──────────────────────────────────── */
|
|
294
|
+
@media print {
|
|
295
|
+
:root { --surface-1: #f9fafb; --surface-2: #f3f4f6; --border-base: #e5e7eb; }
|
|
296
|
+
html, body { background: #fff; color: #111; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
297
|
+
header, .ss-toolbar, .cat-pill, .sz-btn, .cl-nav-btn, .cl-dots, .lightbox, .ss-show-more, .compare-bar, #compare-btn { display: none !important; }
|
|
298
|
+
main { max-width: 100%; padding: 0 16px; }
|
|
299
|
+
.fade-up, .pulse-live, .skeleton { animation: none; }
|
|
300
|
+
.ring-glow { box-shadow: none; border: 1px solid #d1d5db; }
|
|
301
|
+
.text-white, .text-zinc-200 { color: #111; }
|
|
302
|
+
.text-zinc-300, .text-zinc-400 { color: #4b5563; }
|
|
303
|
+
.text-zinc-500, .text-zinc-600 { color: #6b7280; }
|
|
304
|
+
.text-accent { color: #4f46e5; }
|
|
305
|
+
.text-ok { color: #16a34a; } .text-warn { color: #d97706; } .text-err { color: #dc2626; }
|
|
306
|
+
.bg-surface-0 { background: #fff; } .bg-surface-1, .bg-surface-2 { background: #f3f4f6; }
|
|
307
|
+
.sev-critical { background: #fef2f2; color: #991b1b; border-left: 3px solid #dc2626; }
|
|
308
|
+
.sev-high { background: #fffbeb; color: #92400e; border-left: 3px solid #ea580c; }
|
|
309
|
+
.sev-medium { background: #eff6ff; color: #1e40af; border-left: 3px solid #3b82f6; }
|
|
310
|
+
.sev-low { background: #f0fdf4; color: #166534; border-left: 3px solid #22c55e; }
|
|
311
|
+
.ss-grid { grid-template-columns: repeat(3, 1fr) !important; gap: 8px; }
|
|
312
|
+
.ss-card { page-break-inside: avoid; }
|
|
313
|
+
.sec-hdr { page-break-after: avoid; margin-top: 20px; }
|
|
314
|
+
.glass { background: #fff; backdrop-filter: none; }
|
|
315
|
+
.file-chip { background: #f3f4f6; border-color: #d1d5db; color: #374151; }
|
|
316
|
+
}
|
|
317
|
+
</style>
|
|
318
|
+
</head>
|
|
319
|
+
<body class="bg-surface-0 text-zinc-300 min-h-screen">
|
|
320
|
+
|
|
321
|
+
<!-- Sticky header -->
|
|
322
|
+
<header class="glass border-b border-border sticky top-0 z-50 px-5 h-14 flex items-center justify-between">
|
|
323
|
+
<div class="flex items-center gap-2.5">
|
|
324
|
+
<div class="w-7 h-7 rounded-md bg-gradient-to-br from-accent-dim to-accent flex items-center justify-center text-white text-xs font-bold">N</div>
|
|
325
|
+
<div>
|
|
326
|
+
<span class="text-sm font-semibold text-white tracking-tight" id="hdr-title">UI Dive</span>
|
|
327
|
+
<span class="text-[10px] text-zinc-500 ml-1.5" id="hdr-status"></span>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
<div class="flex items-center gap-2">
|
|
331
|
+
<span class="flex items-center gap-1.5 text-[10px] text-zinc-500"><span class="w-1.5 h-1.5 rounded-full bg-ok pulse-live" aria-hidden="true"></span><span aria-live="polite">Auto-refresh</span></span>
|
|
332
|
+
<label for="session-picker" class="sr-only">Select session</label>
|
|
333
|
+
<select id="session-picker" class="bg-surface-2 border border-border rounded-md px-2.5 py-1 text-xs text-zinc-300 focus:outline-none focus:ring-1 focus:ring-accent max-w-[300px]">
|
|
334
|
+
<option value="">Loading...</option>
|
|
335
|
+
</select>
|
|
336
|
+
<button id="compare-btn" onclick="toggleCompare()" class="text-[11px] px-2.5 py-1 rounded-md border border-border text-zinc-400 hover:text-white hover:border-accent transition-colors" title="Pick two sessions and view their scores and screenshots side-by-side">Compare</button>
|
|
337
|
+
</div>
|
|
338
|
+
</header>
|
|
339
|
+
|
|
340
|
+
<!-- Skip Links (screen-reader + keyboard users) -->
|
|
341
|
+
<nav class="sr-only" aria-label="Skip to section" style="position:fixed;top:56px;left:0;z-index:100">
|
|
342
|
+
<a href="#root" style="background:#4f46e5;color:#fff;padding:8px 16px;display:block" onfocus="this.parentElement.style.position='fixed';this.parentElement.classList.remove('sr-only')" onblur="this.parentElement.classList.add('sr-only')">Skip to main content</a>
|
|
343
|
+
</nav>
|
|
344
|
+
|
|
345
|
+
<!-- All content rendered here -->
|
|
346
|
+
<main id="root" class="max-w-[960px] mx-auto px-5 pt-6 pb-20" role="main">
|
|
347
|
+
<div class="space-y-4" aria-busy="true" aria-label="Loading dashboard">
|
|
348
|
+
<div class="skeleton" style="height:120px"></div>
|
|
349
|
+
<div class="skeleton" style="height:14px;width:180px;margin-top:24px;border-radius:4px"></div>
|
|
350
|
+
<div class="skeleton" style="height:80px"></div>
|
|
351
|
+
<div class="skeleton" style="height:80px"></div>
|
|
352
|
+
</div>
|
|
353
|
+
</main>
|
|
354
|
+
|
|
355
|
+
<script>
|
|
356
|
+
let SID = null;
|
|
357
|
+
let _t = null;
|
|
358
|
+
let _allSessions = [];
|
|
359
|
+
let _compareMode = false;
|
|
360
|
+
let _lastDataHash = '';
|
|
361
|
+
let _failCount = 0;
|
|
362
|
+
let _backoff = 5000;
|
|
363
|
+
// Persistent gallery state (survives auto-refresh re-renders)
|
|
364
|
+
let _activeCat = 'all';
|
|
365
|
+
let _gridSize = 'md';
|
|
366
|
+
let _expandedGroups = new Set();
|
|
367
|
+
let _searchQuery = '';
|
|
368
|
+
const $ = id => document.getElementById(id);
|
|
369
|
+
|
|
370
|
+
async function init() {
|
|
371
|
+
const res = await fetch('/api/sessions');
|
|
372
|
+
_allSessions = await res.json();
|
|
373
|
+
const pk = $('session-picker');
|
|
374
|
+
pk.innerHTML = _allSessions.map(s => {
|
|
375
|
+
const bugs = s.bug_count||0, fixed = s.bugs_resolved||0;
|
|
376
|
+
const tag = bugs===0 ? 'Clean' : fixed>=bugs ? bugs+' fixed' : bugs+' bugs';
|
|
377
|
+
return '<option value="'+esc(s.id)+'">'+esc(s.app_name||'Session')+' '+esc(s.created_at.slice(5,16))+' ['+tag+']</option>';
|
|
378
|
+
}).join('');
|
|
379
|
+
if (_allSessions.length) { SID = _allSessions[0].id; pk.value = SID; }
|
|
380
|
+
pk.onchange = e => { SID = e.target.value; if(!_compareMode) load(); };
|
|
381
|
+
load();
|
|
382
|
+
_t = setInterval(() => { if(!_compareMode) load(); }, 5000);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function toggleCompare() {
|
|
386
|
+
_compareMode = !_compareMode;
|
|
387
|
+
const btn = $('compare-btn');
|
|
388
|
+
btn.textContent = _compareMode ? 'Exit Compare' : 'Compare';
|
|
389
|
+
btn.style.borderColor = _compareMode ? '#6366f1' : '';
|
|
390
|
+
btn.style.color = _compareMode ? '#fff' : '';
|
|
391
|
+
if (_compareMode) {
|
|
392
|
+
renderCompareMode();
|
|
393
|
+
} else {
|
|
394
|
+
_lastDataHash = '';
|
|
395
|
+
load();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function renderCompareMode() {
|
|
400
|
+
const opts = _allSessions.map(s =>
|
|
401
|
+
'<option value="'+esc(s.id)+'">'+esc(s.app_name||'Dive')+' \\u2014 '+esc(s.created_at.slice(5,16))+'</option>'
|
|
402
|
+
).join('');
|
|
403
|
+
const parts = [];
|
|
404
|
+
parts.push('<div class="fade-up">');
|
|
405
|
+
parts.push('<h2 class="text-lg font-bold text-white mb-4">Session Comparison</h2>');
|
|
406
|
+
parts.push('<div class="compare-bar">');
|
|
407
|
+
parts.push('<label for="cmp-left" class="text-[11px] text-zinc-400 font-semibold uppercase">Left</label>');
|
|
408
|
+
parts.push('<select id="cmp-left" class="flex-1" onchange="loadCompare()">'+opts+'</select>');
|
|
409
|
+
parts.push('<span class="text-[11px] text-zinc-400" aria-hidden="true">vs</span>');
|
|
410
|
+
parts.push('<label for="cmp-right" class="text-[11px] text-zinc-400 font-semibold uppercase">Right</label>');
|
|
411
|
+
parts.push('<select id="cmp-right" class="flex-1" onchange="loadCompare()">'+opts+'</select>');
|
|
412
|
+
parts.push('</div>');
|
|
413
|
+
parts.push('<div id="cmp-content"><p class="text-zinc-500 text-sm py-10 text-center">Select two sessions to compare</p></div>');
|
|
414
|
+
parts.push('</div>');
|
|
415
|
+
$('root').innerHTML = parts.join('');
|
|
416
|
+
if (_allSessions.length >= 2) {
|
|
417
|
+
$('cmp-left').value = _allSessions[1].id;
|
|
418
|
+
$('cmp-right').value = _allSessions[0].id;
|
|
419
|
+
loadCompare();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function loadCompare() {
|
|
424
|
+
const leftId = $('cmp-left').value;
|
|
425
|
+
const rightId = $('cmp-right').value;
|
|
426
|
+
if (!leftId || !rightId) return;
|
|
427
|
+
const [lShots, rShots, lOv, rOv] = await Promise.all([
|
|
428
|
+
fetch('/api/session/'+leftId+'/screenshots').then(r=>r.json()),
|
|
429
|
+
fetch('/api/session/'+rightId+'/screenshots').then(r=>r.json()),
|
|
430
|
+
fetch('/api/session/'+leftId+'/overview').then(r=>r.json()),
|
|
431
|
+
fetch('/api/session/'+rightId+'/overview').then(r=>r.json()),
|
|
432
|
+
]);
|
|
433
|
+
const parts = [];
|
|
434
|
+
const lRev = lOv.latestReview, rRev = rOv.latestReview;
|
|
435
|
+
const lScore = lRev?(lRev.score??0):null, rScore = rRev?(rRev.score??0):null;
|
|
436
|
+
const gradeOf = s => s===null?'\\u2014':s>=90?'A':s>=80?'B':s>=70?'C':s>=60?'D':'F';
|
|
437
|
+
parts.push('<div class="compare-grid mb-6">');
|
|
438
|
+
parts.push('<div class="ring-glow rounded-lg p-4 bg-surface-1 text-center"><div class="text-[10px] text-zinc-500 uppercase mb-1">Score</div><div class="text-3xl font-bold '+(lScore>=80?'text-ok':lScore>=60?'text-warn':'text-err')+'">'+gradeOf(lScore)+'</div><div class="text-sm text-zinc-400">'+(lScore??'\\u2014')+'/100</div><div class="text-[10px] text-zinc-600 mt-1">'+lOv.stats.bugs+' bugs · '+lOv.stats.components+' comps</div></div>');
|
|
439
|
+
parts.push('<div class="ring-glow rounded-lg p-4 bg-surface-1 text-center"><div class="text-[10px] text-zinc-500 uppercase mb-1">Score</div><div class="text-3xl font-bold '+(rScore>=80?'text-ok':rScore>=60?'text-warn':'text-err')+'">'+gradeOf(rScore)+'</div><div class="text-sm text-zinc-400">'+(rScore??'\\u2014')+'/100</div><div class="text-[10px] text-zinc-600 mt-1">'+rOv.stats.bugs+' bugs · '+rOv.stats.components+' comps</div></div>');
|
|
440
|
+
parts.push('</div>');
|
|
441
|
+
const routeMap = {};
|
|
442
|
+
lShots.forEach(s => { const r = s.route||s.label; if(!routeMap[r]) routeMap[r]={left:null,right:null}; routeMap[r].left = s; });
|
|
443
|
+
rShots.forEach(s => { const r = s.route||s.label; if(!routeMap[r]) routeMap[r]={left:null,right:null}; routeMap[r].right = s; });
|
|
444
|
+
const routes = Object.keys(routeMap);
|
|
445
|
+
if (routes.length === 0) {
|
|
446
|
+
parts.push('<p class="text-zinc-500 text-sm py-8 text-center">No screenshots to compare. Use dive_snapshot to capture screenshots during dive sessions.</p>');
|
|
447
|
+
} else {
|
|
448
|
+
parts.push('<div class="space-y-4">');
|
|
449
|
+
routes.forEach(r => {
|
|
450
|
+
const pair = routeMap[r];
|
|
451
|
+
parts.push('<div class="fade-up"><div class="text-[11px] text-zinc-400 font-medium mb-1.5 font-mono">'+esc(r)+'</div>');
|
|
452
|
+
parts.push('<div class="compare-grid">');
|
|
453
|
+
if (pair.left) {
|
|
454
|
+
const src = pair.left.base64_thumbnail ? 'data:image/png;base64,'+pair.left.base64_thumbnail : '/api/screenshot/'+encodeURIComponent(pair.left.id)+'/image';
|
|
455
|
+
parts.push('<div class="compare-col"><img src="'+src+'" alt="Left: '+esc(r)+'" loading="lazy" style="cursor:pointer" data-lb-src="'+esc(src)+'" data-lb-label="Left: '+esc(r)+'"></div>');
|
|
456
|
+
} else {
|
|
457
|
+
parts.push('<div class="compare-col" style="display:flex;align-items:center;justify-content:center;min-height:120px;color:#52525b;font-size:11px">No screenshot</div>');
|
|
458
|
+
}
|
|
459
|
+
if (pair.right) {
|
|
460
|
+
const src = pair.right.base64_thumbnail ? 'data:image/png;base64,'+pair.right.base64_thumbnail : '/api/screenshot/'+encodeURIComponent(pair.right.id)+'/image';
|
|
461
|
+
parts.push('<div class="compare-col"><img src="'+src+'" alt="Right: '+esc(r)+'" loading="lazy" style="cursor:pointer" data-lb-src="'+esc(src)+'" data-lb-label="Right: '+esc(r)+'"></div>');
|
|
462
|
+
} else {
|
|
463
|
+
parts.push('<div class="compare-col" style="display:flex;align-items:center;justify-content:center;min-height:120px;color:#52525b;font-size:11px">No screenshot</div>');
|
|
464
|
+
}
|
|
465
|
+
parts.push('</div></div>');
|
|
466
|
+
});
|
|
467
|
+
parts.push('</div>');
|
|
468
|
+
}
|
|
469
|
+
$('cmp-content').innerHTML = parts.join('');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function simpleHash(str) {
|
|
473
|
+
let h = 0;
|
|
474
|
+
for (let i = 0; i < str.length; i++) {
|
|
475
|
+
h = ((h << 5) - h + str.charCodeAt(i)) | 0;
|
|
476
|
+
}
|
|
477
|
+
return h.toString(36);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function load() {
|
|
481
|
+
if (!SID) return;
|
|
482
|
+
try {
|
|
483
|
+
const [ov, bugs, fixes, comps, locs, logs, tests, revs, shots] = await Promise.all([
|
|
484
|
+
fetch('/api/session/'+SID+'/overview').then(r=>r.json()),
|
|
485
|
+
fetch('/api/session/'+SID+'/bugs').then(r=>r.json()),
|
|
486
|
+
fetch('/api/session/'+SID+'/fixes').then(r=>r.json()),
|
|
487
|
+
fetch('/api/session/'+SID+'/components').then(r=>r.json()),
|
|
488
|
+
fetch('/api/session/'+SID+'/code-locations').then(r=>r.json()),
|
|
489
|
+
fetch('/api/session/'+SID+'/changelogs').then(r=>r.json()),
|
|
490
|
+
fetch('/api/session/'+SID+'/tests').then(r=>r.json()),
|
|
491
|
+
fetch('/api/session/'+SID+'/reviews').then(r=>r.json()),
|
|
492
|
+
fetch('/api/session/'+SID+'/screenshots').then(r=>r.json()),
|
|
493
|
+
]);
|
|
494
|
+
// Skip re-render if data unchanged
|
|
495
|
+
const hash = simpleHash(JSON.stringify([ov,bugs,fixes,comps,locs,logs,tests,revs,shots?.length]));
|
|
496
|
+
if (hash === _lastDataHash) return;
|
|
497
|
+
_lastDataHash = hash;
|
|
498
|
+
render(ov, bugs, fixes, comps, locs, logs, tests, revs, shots);
|
|
499
|
+
_failCount = 0;
|
|
500
|
+
_backoff = 5000;
|
|
501
|
+
updateRefreshIndicator('ok');
|
|
502
|
+
} catch(e) {
|
|
503
|
+
_failCount++;
|
|
504
|
+
updateRefreshIndicator('fail');
|
|
505
|
+
if (_failCount <= 1) {
|
|
506
|
+
$('root').innerHTML = '<div style="text-align:center;padding:40px 16px"><div style="font-size:32px;margin-bottom:12px;opacity:.6">⚠</div><div class="text-zinc-300 text-sm font-medium mb-2">Connection error</div><div class="text-zinc-500 text-xs mb-4">'+esc(e.message)+'</div><button onclick="_lastDataHash=\\'\\';load()" class="text-[11px] px-3 py-1.5 rounded-md border border-accent text-accent hover:bg-accent/10" style="cursor:pointer">Retry now</button></div>';
|
|
507
|
+
}
|
|
508
|
+
// Exponential backoff: 5s → 7.5s → 11.25s → ... → max 30s
|
|
509
|
+
_backoff = Math.min(30000, _backoff * 1.5);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function updateRefreshIndicator(status) {
|
|
514
|
+
const dot = document.querySelector('.pulse-live');
|
|
515
|
+
const label = document.querySelector('[aria-live="polite"]');
|
|
516
|
+
if (!dot || !label) return;
|
|
517
|
+
if (status === 'fail') {
|
|
518
|
+
dot.style.background = '#fbbf24';
|
|
519
|
+
label.textContent = 'Retrying in '+Math.round(_backoff/1000)+'s';
|
|
520
|
+
} else {
|
|
521
|
+
dot.style.background = '';
|
|
522
|
+
label.textContent = 'Auto-refresh';
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function render(ov, bugs, fixes, comps, locs, logs, tests, revs, shots) {
|
|
527
|
+
const s = ov.stats, sess = ov.session;
|
|
528
|
+
$('hdr-title').textContent = sess.app_name || 'UI Dive';
|
|
529
|
+
$('hdr-status').textContent = sess.status === 'completed' ? 'Completed' : 'In Progress';
|
|
530
|
+
const ssMap = {};
|
|
531
|
+
(shots||[]).forEach(ss => { ssMap[ss.id] = ss; });
|
|
532
|
+
|
|
533
|
+
const h = [];
|
|
534
|
+
const hasBugs = bugs.length > 0 || fixes.length > 0;
|
|
535
|
+
const hasReview = revs.length > 0;
|
|
536
|
+
|
|
537
|
+
// ── Hero Card ─────────────────────────────────────────────
|
|
538
|
+
const rev = ov.latestReview;
|
|
539
|
+
const score = rev ? (rev.score??0) : null;
|
|
540
|
+
const grade = score!==null ? (score>=90?'A':score>=80?'B':score>=70?'C':score>=60?'D':'F') : '?';
|
|
541
|
+
const gradeClr = score===null?'text-zinc-500':score>=80?'text-ok':score>=60?'text-warn':'text-err';
|
|
542
|
+
const pct = score!==null ? score/100 : 0;
|
|
543
|
+
const circ = 2 * Math.PI * 28;
|
|
544
|
+
const dashOff = circ - (circ * pct);
|
|
545
|
+
|
|
546
|
+
h.push('<div class="fade-up ring-glow rounded-xl bg-surface-1 p-5 mb-6">');
|
|
547
|
+
h.push('<div class="flex items-start gap-5">');
|
|
548
|
+
h.push('<div class="relative shrink-0" title="'+(score!==null?'Quality score: '+score+'/100':'No review yet. Run dive_review to generate a score.')+'">');
|
|
549
|
+
h.push('<svg class="score-ring" viewBox="0 0 64 64" role="img" aria-label="Quality score: '+(score!==null?score+'%':'not rated')+'"><circle class="bg" cx="32" cy="32" r="28"/>');
|
|
550
|
+
if(score!==null) h.push('<circle class="fg" cx="32" cy="32" r="28" stroke="'+(score>=80?'#34d399':score>=60?'#fbbf24':'#f87171')+'" stroke-dasharray="'+circ.toFixed(1)+'" stroke-dashoffset="'+dashOff.toFixed(1)+'" transform="rotate(-90 32 32)"/>');
|
|
551
|
+
h.push('</svg>');
|
|
552
|
+
h.push('<div class="absolute inset-0 flex items-center justify-center"><span class="text-lg font-bold '+gradeClr+'" aria-hidden="true">'+grade+'</span></div>');
|
|
553
|
+
h.push('</div>');
|
|
554
|
+
h.push('<div class="flex-1 min-w-0">');
|
|
555
|
+
h.push('<div class="text-sm font-semibold text-white mb-1">'+esc(sess.app_name||'UI Dive Session')+'</div>');
|
|
556
|
+
h.push('<div class="text-[11px] text-zinc-500 mb-3">'+esc(sess.app_url||'')+(sess.created_at?' · Started '+esc(sess.created_at.slice(0,16)):'')+'</div>');
|
|
557
|
+
h.push('<div class="flex flex-wrap gap-1.5">');
|
|
558
|
+
if(s.bugs>0) h.push('<span class="text-[11px] px-2 py-0.5 rounded-full '+(s.bugsOpen>0?'bg-red-950/50 text-red-300':'bg-emerald-950/50 text-emerald-300')+'">'+s.bugs+' bug'+(s.bugs!==1?'s':'')+((s.bugsResolved>0)?' · '+s.bugsResolved+' fixed':'')+'</span>');
|
|
559
|
+
if(s.fixes>0) h.push('<span class="text-[11px] px-2 py-0.5 rounded-full bg-blue-950/50 text-blue-300">'+s.fixes+' fix'+(s.fixes!==1?'es':'')+'</span>');
|
|
560
|
+
h.push('<span class="text-[11px] px-2 py-0.5 rounded-full bg-zinc-800/60 text-zinc-400">'+s.components+' component'+(s.components!==1?'s':'')+'</span>');
|
|
561
|
+
if(s.codeLocations>0) h.push('<span class="text-[11px] px-2 py-0.5 rounded-full bg-zinc-800/60 text-zinc-400">'+s.codeLocations+' code loc'+(s.codeLocations!==1?'s':'')+'</span>');
|
|
562
|
+
if(s.generatedTests>0) h.push('<span class="text-[11px] px-2 py-0.5 rounded-full bg-violet-950/50 text-violet-300">'+s.generatedTests+' test'+(s.generatedTests!==1?'s':'')+'</span>');
|
|
563
|
+
if(s.codeReviews>0) h.push('<span class="text-[11px] px-2 py-0.5 rounded-full bg-violet-950/50 text-violet-300">'+s.codeReviews+' review'+(s.codeReviews!==1?'s':'')+'</span>');
|
|
564
|
+
h.push('</div>');
|
|
565
|
+
h.push('</div></div>');
|
|
566
|
+
|
|
567
|
+
if (!hasBugs && !hasReview && logs.length===0 && (!shots||shots.length===0)) {
|
|
568
|
+
h.push('<div class="empty-state mt-4 mb-2"><div class="text-zinc-400 text-sm font-medium mb-2">Session is clean</div>');
|
|
569
|
+
h.push('<div class="empty-hint text-zinc-600">No bugs found and no code review yet. Use <code class="text-accent text-[11px]">dive_interact</code> to test interactions, <code class="text-accent text-[11px]">dive_bug</code> to log issues, or <code class="text-accent text-[11px]">dive_review</code> to generate a quality score.</div></div>');
|
|
570
|
+
}
|
|
571
|
+
h.push('</div>');
|
|
572
|
+
|
|
573
|
+
// ── Bugs & Fixes ──────────────────────────────────────────
|
|
574
|
+
if (hasBugs) {
|
|
575
|
+
h.push(sec('Bugs & Fixes', 'Found '+bugs.length+' issue'+(bugs.length!==1?'s':'')+', applied '+fixes.length+' fix'+(fixes.length!==1?'es':'')));
|
|
576
|
+
bugs.forEach(b => {
|
|
577
|
+
const fix = fixes.find(f => f.bug_id === b.id);
|
|
578
|
+
h.push('<div class="ring-glow rounded-lg p-4 mb-2.5 bg-surface-1 fade-up">');
|
|
579
|
+
h.push('<div class="flex items-center gap-2 flex-wrap">' + sevBadge(b.severity) + statusBadge(b.status) +
|
|
580
|
+
'<span class="text-[13px] font-medium text-white leading-snug">'+esc(b.title)+'</span></div>');
|
|
581
|
+
if (b.description) h.push('<p class="text-xs text-zinc-400 mt-2 leading-relaxed">'+esc(b.description)+'</p>');
|
|
582
|
+
if (b.expected||b.actual) {
|
|
583
|
+
h.push('<div class="mt-2.5 grid grid-cols-2 gap-3 text-[11px]">');
|
|
584
|
+
if(b.expected) h.push('<div class="rounded-md bg-surface-0 p-2.5 border border-border"><span class="text-ok font-semibold text-[10px] uppercase tracking-wide">Expected</span><div class="text-zinc-400 mt-1 leading-relaxed">'+esc(b.expected)+'</div></div>');
|
|
585
|
+
if(b.actual) h.push('<div class="rounded-md bg-surface-0 p-2.5 border border-border"><span class="text-err font-semibold text-[10px] uppercase tracking-wide">Actual</span><div class="text-zinc-400 mt-1 leading-relaxed">'+esc(b.actual)+'</div></div>');
|
|
586
|
+
h.push('</div>');
|
|
587
|
+
}
|
|
588
|
+
if (fix) {
|
|
589
|
+
h.push('<div class="mt-3 border-t border-border pt-3">');
|
|
590
|
+
h.push('<div class="flex items-center gap-2 mb-1.5">' +
|
|
591
|
+
(fix.verified?'<span class="text-[10px] px-1.5 py-0.5 rounded bg-ok/10 text-ok font-medium">Verified Fix</span>':
|
|
592
|
+
'<span class="text-[10px] px-1.5 py-0.5 rounded bg-warn/10 text-warn font-medium">Pending Fix</span>') + '</div>');
|
|
593
|
+
h.push('<p class="text-xs text-zinc-400 leading-relaxed">'+esc(fix.fix_description)+'</p>');
|
|
594
|
+
if (fix.files_changed) h.push('<div class="mt-1.5 flex flex-wrap">'+fileChips(fix.files_changed)+'</div>');
|
|
595
|
+
if (fix.verification_notes) h.push('<div class="mt-1.5 text-[11px] text-zinc-500 italic leading-relaxed">'+esc(fix.verification_notes)+'</div>');
|
|
596
|
+
h.push('</div>');
|
|
597
|
+
}
|
|
598
|
+
h.push('</div>');
|
|
599
|
+
});
|
|
600
|
+
const bugIds = new Set(bugs.map(b=>b.id));
|
|
601
|
+
fixes.filter(f=>!bugIds.has(f.bug_id)).forEach(f => {
|
|
602
|
+
h.push('<div class="ring-glow rounded-lg p-4 mb-2.5 bg-surface-1 fade-up">');
|
|
603
|
+
h.push('<div class="flex items-center gap-2">' + sevBadge(f.bug_severity||'medium') +
|
|
604
|
+
(f.verified?'<span class="text-[10px] px-1.5 py-0.5 rounded bg-ok/10 text-ok font-medium">Verified</span>':
|
|
605
|
+
'<span class="text-[10px] px-1.5 py-0.5 rounded bg-warn/10 text-warn font-medium">Pending</span>') +
|
|
606
|
+
'<span class="text-[13px] font-medium text-white">'+esc(f.bug_title||f.bug_id)+'</span></div>');
|
|
607
|
+
h.push('<p class="text-xs text-zinc-400 mt-2">'+esc(f.fix_description)+'</p>');
|
|
608
|
+
if (f.files_changed) h.push('<div class="mt-1.5 flex flex-wrap">'+fileChips(f.files_changed)+'</div>');
|
|
609
|
+
h.push('</div>');
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ── Code Review (only unique findings, not duplicating bugs) ─
|
|
614
|
+
if (hasReview) {
|
|
615
|
+
const r = revs[0];
|
|
616
|
+
const sc = r.score??0;
|
|
617
|
+
const gr = sc>=90?'A':sc>=80?'B':sc>=70?'C':sc>=60?'D':'F';
|
|
618
|
+
let sev = {};
|
|
619
|
+
try { sev = typeof r.severity_counts==='string'?JSON.parse(r.severity_counts):(r.severity_counts||{}); } catch{}
|
|
620
|
+
let findings = [];
|
|
621
|
+
try { findings = typeof r.findings==='string'?JSON.parse(r.findings):(r.findings||[]); } catch{}
|
|
622
|
+
const bugTitles = new Set(bugs.map(b=>(b.title||'').toLowerCase().trim()));
|
|
623
|
+
const uniqueFindings = findings.filter(f => !bugTitles.has((f.title||'').toLowerCase().trim()));
|
|
624
|
+
|
|
625
|
+
h.push(sec('Code Review', sc+'/100 quality score'+(uniqueFindings.length>0?' · '+uniqueFindings.length+' additional finding'+(uniqueFindings.length!==1?'s':''):'')));
|
|
626
|
+
h.push('<div class="ring-glow rounded-lg bg-surface-1 p-5 fade-up">');
|
|
627
|
+
h.push('<div class="flex items-center justify-between">');
|
|
628
|
+
h.push('<div class="flex items-center gap-3"><span class="text-2xl font-bold '+(sc>=80?'text-ok':sc>=60?'text-warn':'text-err')+'">'+gr+'</span>');
|
|
629
|
+
h.push('<div><div class="text-sm font-semibold text-white">'+sc+'/100</div><div class="text-[11px] text-zinc-500">Overall quality</div></div></div>');
|
|
630
|
+
h.push('<div class="flex gap-4 text-center text-[11px]">');
|
|
631
|
+
['critical','high','medium','low'].forEach(sv => {
|
|
632
|
+
const v = sev[sv]||0;
|
|
633
|
+
if(v===0) return;
|
|
634
|
+
const c = {critical:'text-err',high:'text-warn',medium:'text-accent',low:'text-ok'}[sv];
|
|
635
|
+
h.push('<div><div class="text-base font-bold '+c+'">'+v+'</div><div class="text-zinc-500 capitalize">'+sv+'</div></div>');
|
|
636
|
+
});
|
|
637
|
+
if(!sev.critical && !sev.high && !sev.medium && !sev.low) h.push('<div class="text-[11px] text-zinc-500">No findings</div>');
|
|
638
|
+
h.push('</div></div>');
|
|
639
|
+
if (uniqueFindings.length) {
|
|
640
|
+
h.push('<div class="space-y-2 mt-4">');
|
|
641
|
+
uniqueFindings.forEach(f => {
|
|
642
|
+
h.push('<div class="flex items-start gap-2.5 text-xs">');
|
|
643
|
+
h.push(sevBadge(f.severity));
|
|
644
|
+
h.push('<div class="flex-1 min-w-0">');
|
|
645
|
+
h.push('<div class="font-medium text-zinc-200">'+esc(f.title)+'</div>');
|
|
646
|
+
h.push('<div class="text-zinc-500 mt-0.5 leading-relaxed">'+truncWords(esc(f.description),200)+'</div>');
|
|
647
|
+
if (f.codeFile) h.push('<span class="file-chip mt-1">'+esc(shortPath(f.codeFile))+(f.codeLine?' '+esc(f.codeLine):'')+'</span>');
|
|
648
|
+
h.push('</div>');
|
|
649
|
+
h.push(statusBadge(f.status));
|
|
650
|
+
h.push('</div>');
|
|
651
|
+
});
|
|
652
|
+
h.push('</div>');
|
|
653
|
+
}
|
|
654
|
+
h.push('</div>');
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ── Changelog (Carousel) — shown BEFORE screenshots ──────
|
|
658
|
+
if (logs.length) {
|
|
659
|
+
function nearestShot(changeTs) {
|
|
660
|
+
if (!shots || !shots.length || !changeTs) return null;
|
|
661
|
+
let best = null, bestDiff = Infinity;
|
|
662
|
+
const ct = new Date(changeTs).getTime();
|
|
663
|
+
if (isNaN(ct)) return null;
|
|
664
|
+
shots.forEach(ss => {
|
|
665
|
+
const st = new Date(ss.created_at).getTime();
|
|
666
|
+
if (isNaN(st)) return;
|
|
667
|
+
const diff = Math.abs(ct - st);
|
|
668
|
+
if (diff < bestDiff) { bestDiff = diff; best = ss; }
|
|
669
|
+
});
|
|
670
|
+
return best;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
h.push(sec('Changelog', logs.length+' change'+(logs.length!==1?'s':'')+' during this session'));
|
|
674
|
+
h.push('<div class="cl-carousel fade-up" id="cl-carousel" role="region" aria-label="Changelog carousel" tabindex="0">');
|
|
675
|
+
if (logs.length > 1) {
|
|
676
|
+
h.push('<button class="cl-nav-btn prev disabled" id="cl-prev" onclick="clNav(-1)" aria-label="Previous change"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg></button>');
|
|
677
|
+
h.push('<button class="cl-nav-btn next" id="cl-next" onclick="clNav(1)" aria-label="Next change"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg></button>');
|
|
678
|
+
}
|
|
679
|
+
h.push('<div class="cl-track" id="cl-track">');
|
|
680
|
+
logs.forEach((c, idx) => {
|
|
681
|
+
h.push('<div class="cl-card" role="group" aria-label="Change '+(idx+1)+' of '+logs.length+'">');
|
|
682
|
+
h.push('<div class="flex items-center gap-3">');
|
|
683
|
+
h.push('<span class="cl-step" aria-hidden="true">'+(idx+1)+'</span>');
|
|
684
|
+
h.push('<span class="cl-time">'+esc(c.created_at)+'</span>');
|
|
685
|
+
h.push('</div>');
|
|
686
|
+
h.push('<div class="cl-desc">'+truncWords(esc(c.description), 280)+'</div>');
|
|
687
|
+
if (c.files_changed) h.push('<div class="cl-files">'+fileChips(c.files_changed)+'</div>');
|
|
688
|
+
const bef = c.before_screenshot_id ? ssMap[c.before_screenshot_id] : null;
|
|
689
|
+
const aft = c.after_screenshot_id ? ssMap[c.after_screenshot_id] : null;
|
|
690
|
+
if (bef || aft) {
|
|
691
|
+
h.push('<div class="compare-grid mt-1">');
|
|
692
|
+
if (bef) {
|
|
693
|
+
const bSrc = bef.base64_thumbnail ? 'data:image/png;base64,'+bef.base64_thumbnail : '/api/screenshot/'+encodeURIComponent(bef.id)+'/image';
|
|
694
|
+
h.push('<div class="compare-col"><div class="ch">Before</div><img src="'+bSrc+'" alt="Before state" loading="lazy" data-lb-src="'+esc(bSrc)+'" data-lb-label="Before" style="cursor:pointer"></div>');
|
|
695
|
+
}
|
|
696
|
+
if (aft) {
|
|
697
|
+
const aSrc = aft.base64_thumbnail ? 'data:image/png;base64,'+aft.base64_thumbnail : '/api/screenshot/'+encodeURIComponent(aft.id)+'/image';
|
|
698
|
+
h.push('<div class="compare-col"><div class="ch">After</div><img src="'+aSrc+'" alt="After state" loading="lazy" data-lb-src="'+esc(aSrc)+'" data-lb-label="After" style="cursor:pointer"></div>');
|
|
699
|
+
}
|
|
700
|
+
h.push('</div>');
|
|
701
|
+
} else {
|
|
702
|
+
const nearest = nearestShot(c.created_at);
|
|
703
|
+
if (nearest) {
|
|
704
|
+
const nSrc = nearest.base64_thumbnail ? 'data:image/png;base64,'+nearest.base64_thumbnail : '/api/screenshot/'+encodeURIComponent(nearest.id)+'/image';
|
|
705
|
+
h.push('<div class="mt-2 rounded-lg overflow-hidden border border-zinc-800">');
|
|
706
|
+
h.push('<div class="text-[10px] text-zinc-500 px-3 py-1.5 bg-zinc-900/50 flex items-center gap-1.5">');
|
|
707
|
+
h.push('<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0022.5 18.75V5.25A2.25 2.25 0 0020.25 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg>');
|
|
708
|
+
h.push('Nearest capture: '+esc(nearest.label||'screenshot')+'</div>');
|
|
709
|
+
h.push('<img src="'+nSrc+'" alt="'+esc(nearest.label||'Nearest screenshot')+'" loading="lazy" style="width:100%;display:block;max-height:200px;object-fit:cover;cursor:pointer" data-lb-src="'+esc(nSrc)+'" data-lb-label="'+esc(nearest.label||'')+'">');
|
|
710
|
+
h.push('</div>');
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
h.push('</div>');
|
|
714
|
+
});
|
|
715
|
+
h.push('</div>');
|
|
716
|
+
if (logs.length > 1) {
|
|
717
|
+
h.push('<div class="cl-dots" id="cl-dots" role="tablist" aria-label="Changelog navigation">');
|
|
718
|
+
logs.forEach((_, idx) => {
|
|
719
|
+
h.push('<button class="cl-dot'+(idx===0?' active':'')+'" data-idx="'+idx+'" onclick="clGoTo('+idx+')" role="tab" aria-selected="'+(idx===0?'true':'false')+'" aria-label="Go to change '+(idx+1)+'"></button>');
|
|
720
|
+
});
|
|
721
|
+
h.push('</div>');
|
|
722
|
+
}
|
|
723
|
+
h.push('</div>');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// ── Screenshots Gallery (interactive) ────────────────────
|
|
727
|
+
if (shots && shots.length) {
|
|
728
|
+
window._ssAll = shots.map(ss => ({
|
|
729
|
+
src: ss.base64_thumbnail ? 'data:image/png;base64,'+ss.base64_thumbnail : '/api/screenshot/'+encodeURIComponent(ss.id)+'/image',
|
|
730
|
+
label: ss.label||'screenshot',
|
|
731
|
+
route: ss.route||'',
|
|
732
|
+
time: ss.created_at?.slice(5,16)||'',
|
|
733
|
+
}));
|
|
734
|
+
// Raw shot data for deferred card rendering on expand
|
|
735
|
+
window._ssRaw = shots.map((ss, idx) => ({ ...ss, _idx: idx }));
|
|
736
|
+
|
|
737
|
+
// Priority-scored category detection
|
|
738
|
+
const catMap = {};
|
|
739
|
+
shots.forEach((ss, idx) => {
|
|
740
|
+
const lbl = ss.label||'screenshot';
|
|
741
|
+
const cat = classifyScreenshot(lbl);
|
|
742
|
+
if (!catMap[cat]) catMap[cat] = [];
|
|
743
|
+
catMap[cat].push({ ...ss, _idx: idx });
|
|
744
|
+
});
|
|
745
|
+
const MIN_CAT_SIZE = 3;
|
|
746
|
+
const tinyKeys = Object.keys(catMap).filter(k => catMap[k].length < MIN_CAT_SIZE && k !== 'General');
|
|
747
|
+
if (tinyKeys.length > 1) {
|
|
748
|
+
if (!catMap['Other']) catMap['Other'] = [];
|
|
749
|
+
tinyKeys.forEach(k => { catMap['Other'].push(...catMap[k]); delete catMap[k]; });
|
|
750
|
+
}
|
|
751
|
+
const cats = Object.keys(catMap).sort((a,b) => catMap[b].length - catMap[a].length);
|
|
752
|
+
|
|
753
|
+
h.push(sec('Screenshots', shots.length+' captured image'+(shots.length!==1?'s':'')));
|
|
754
|
+
|
|
755
|
+
h.push('<div class="ss-toolbar" role="toolbar" aria-label="Screenshot controls">');
|
|
756
|
+
h.push('<div class="ss-search-wrap"><svg class="ss-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg><label for="ss-search" class="sr-only">Filter screenshots</label><input type="text" class="ss-search" id="ss-search" placeholder="Filter screenshots..." oninput="filterScreenshots()"></div>');
|
|
757
|
+
h.push('<div class="flex-1"></div>');
|
|
758
|
+
h.push('<button class="sz-btn" data-sz="sm" onclick="setGridSize('sm')" aria-label="Compact grid" aria-pressed="false"><svg viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="1" width="6" height="6" rx="1"/><rect x="9" y="1" width="6" height="6" rx="1"/><rect x="1" y="9" width="6" height="6" rx="1"/><rect x="9" y="9" width="6" height="6" rx="1"/></svg></button>');
|
|
759
|
+
h.push('<button class="sz-btn" data-sz="md" onclick="setGridSize('md')" aria-label="Medium grid" aria-pressed="true"><svg viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="1" width="6.5" height="6.5" rx="1.5"/><rect x="8.5" y="1" width="6.5" height="6.5" rx="1.5"/><rect x="1" y="8.5" width="6.5" height="6.5" rx="1.5"/><rect x="8.5" y="8.5" width="6.5" height="6.5" rx="1.5"/></svg></button>');
|
|
760
|
+
h.push('<button class="sz-btn" data-sz="lg" onclick="setGridSize('lg')" aria-label="Large grid" aria-pressed="false"><svg viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="1" width="14" height="6.5" rx="1.5"/><rect x="1" y="8.5" width="14" height="6.5" rx="1.5"/></svg></button>');
|
|
761
|
+
h.push('</div>');
|
|
762
|
+
|
|
763
|
+
h.push('<div class="flex flex-wrap gap-1.5 mb-4" id="ss-cat-bar" role="toolbar" aria-label="Category filter">');
|
|
764
|
+
h.push('<button class="cat-pill" data-cat="all" onclick="filterCat('all')" aria-pressed="true">All<span class="cat-count">'+shots.length+'</span></button>');
|
|
765
|
+
cats.forEach(cat => {
|
|
766
|
+
h.push('<button class="cat-pill" data-cat="'+esc(cat)+'" onclick="filterCat(''+esc(cat)+'')" aria-pressed="false">'+esc(cat)+'<span class="cat-count">'+catMap[cat].length+'</span></button>');
|
|
767
|
+
});
|
|
768
|
+
h.push('</div>');
|
|
769
|
+
|
|
770
|
+
h.push('<div id="ss-gallery">');
|
|
771
|
+
const INITIAL_SHOW = 8;
|
|
772
|
+
cats.forEach(cat => {
|
|
773
|
+
const items = catMap[cat];
|
|
774
|
+
h.push('<div class="ss-group" data-cat="'+esc(cat)+'">');
|
|
775
|
+
h.push('<div class="ss-group-hdr">'+esc(cat)+' <span class="g-count">('+items.length+')</span></div>');
|
|
776
|
+
h.push('<div class="ss-grid sz-'+_gridSize+'">');
|
|
777
|
+
// Render only visible cards; deferred cards rendered on expand
|
|
778
|
+
const visibleCount = Math.min(items.length, INITIAL_SHOW);
|
|
779
|
+
items.slice(0, visibleCount).forEach((ss, i) => {
|
|
780
|
+
h.push(renderSsCard(ss));
|
|
781
|
+
});
|
|
782
|
+
h.push('</div>');
|
|
783
|
+
if (items.length > INITIAL_SHOW) {
|
|
784
|
+
h.push('<button class="ss-show-more" data-cat="'+esc(cat)+'" data-items=\\''+esc(JSON.stringify(items.slice(INITIAL_SHOW).map(x=>x._idx)))+'\\' onclick="toggleGroupExpand(this)">Show '+(items.length - INITIAL_SHOW)+' more</button>');
|
|
785
|
+
}
|
|
786
|
+
h.push('</div>');
|
|
787
|
+
});
|
|
788
|
+
h.push('</div>');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// ── Generated Tests ──────────────────────────────────────
|
|
792
|
+
if (tests.length) {
|
|
793
|
+
h.push(sec('Generated Tests', 'Auto-generated regression tests from findings'));
|
|
794
|
+
tests.forEach(t => {
|
|
795
|
+
h.push('<div class="ring-glow rounded-lg p-4 mb-2 bg-surface-1 fade-up">');
|
|
796
|
+
h.push('<div class="flex items-center gap-2">');
|
|
797
|
+
h.push('<span class="text-[10px] px-1.5 py-0.5 rounded bg-ok/10 text-ok font-medium">'+esc(t.test_framework)+'</span>');
|
|
798
|
+
h.push('<span class="text-xs text-white font-medium">'+esc(t.description||'Regression tests')+'</span></div>');
|
|
799
|
+
if (t.test_file_path) h.push('<span class="file-chip mt-1.5">'+esc(shortPath(t.test_file_path))+'</span>');
|
|
800
|
+
if (t.test_code) {
|
|
801
|
+
h.push('<details class="mt-2"><summary class="text-[11px] text-accent cursor-pointer select-none">View source</summary>');
|
|
802
|
+
h.push('<pre class="mt-1.5 text-[11px] bg-surface-0 rounded-md p-3 text-zinc-400 max-h-64 overflow-auto leading-relaxed border border-border">'+esc(t.test_code)+'</pre></details>');
|
|
803
|
+
}
|
|
804
|
+
h.push('</div>');
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ── Components (grouped by type) ─────────────────────────
|
|
809
|
+
if (comps.length) {
|
|
810
|
+
h.push(sec('Components', s.components+' discovered across the application'));
|
|
811
|
+
const groups = {};
|
|
812
|
+
comps.forEach(c => {
|
|
813
|
+
const t = c.component_type || 'other';
|
|
814
|
+
if(!groups[t]) groups[t] = [];
|
|
815
|
+
groups[t].push(c);
|
|
816
|
+
});
|
|
817
|
+
const typeOrder = ['page','sidebar','header','hero','section','card','list','form','modal','panel','popup','nav','table','other'];
|
|
818
|
+
const sortedTypes = Object.keys(groups).sort((a,b) => {
|
|
819
|
+
const ai = typeOrder.indexOf(a), bi = typeOrder.indexOf(b);
|
|
820
|
+
return (ai===-1?99:ai) - (bi===-1?99:bi);
|
|
821
|
+
});
|
|
822
|
+
sortedTypes.forEach(type => {
|
|
823
|
+
const items = groups[type];
|
|
824
|
+
h.push('<div class="mb-4 fade-up">');
|
|
825
|
+
h.push('<div class="text-[10px] text-zinc-500 uppercase tracking-wider font-semibold mb-1.5">'+esc(type)+'s <span class="text-zinc-600 normal-case font-normal">('+items.length+')</span></div>');
|
|
826
|
+
h.push('<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-1.5">');
|
|
827
|
+
items.forEach(c => {
|
|
828
|
+
const hasBugs = (c.bug_count||0) > 0;
|
|
829
|
+
h.push('<div class="rounded-md px-3 py-2 bg-surface-1 border border-border hover:border-zinc-600 transition-colors'+(hasBugs?' border-l-2 border-l-err':'')+'">');
|
|
830
|
+
h.push('<div class="text-xs font-medium text-zinc-200 truncate" title="'+esc(c.name)+'">'+esc(c.name)+'</div>');
|
|
831
|
+
if (hasBugs) h.push('<div class="text-[10px] text-err mt-0.5">'+c.bug_count+' bug'+(c.bug_count>1?'s':'')+'</div>');
|
|
832
|
+
h.push('</div>');
|
|
833
|
+
});
|
|
834
|
+
h.push('</div></div>');
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// ── Code Locations ───────────────────────────────────────
|
|
839
|
+
if (locs.length) {
|
|
840
|
+
h.push(sec('Code Locations', locs.length+' files traced during this session'));
|
|
841
|
+
h.push('<details class="fade-up"><summary class="text-xs text-accent cursor-pointer select-none mb-2">Show '+locs.length+' traced locations</summary>');
|
|
842
|
+
h.push('<div class="space-y-1">');
|
|
843
|
+
locs.forEach(l => {
|
|
844
|
+
h.push('<div class="flex items-center gap-2 text-[11px] py-1.5 px-2.5 rounded bg-surface-1 border border-border">');
|
|
845
|
+
h.push('<span class="file-chip" style="margin:0" title="'+esc(l.file_path)+'">'+esc(shortPath(l.file_path))+'</span>');
|
|
846
|
+
if (l.line_start) h.push('<span class="text-zinc-600 text-[10px]">L'+l.line_start+(l.line_end&&l.line_end!==l.line_start?'-'+l.line_end:'')+'</span>');
|
|
847
|
+
if (l.search_query) h.push('<span class="text-accent text-[10px] truncate max-w-[140px]" title="'+esc(l.search_query)+'">'+esc(l.search_query)+'</span>');
|
|
848
|
+
h.push('</div>');
|
|
849
|
+
});
|
|
850
|
+
h.push('</div></details>');
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
$('root').innerHTML = h.join('');
|
|
854
|
+
restoreGalleryState();
|
|
855
|
+
initCarousel();
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function renderSsCard(ss) {
|
|
859
|
+
const src = ss.base64_thumbnail ? 'data:image/png;base64,'+ss.base64_thumbnail : '/api/screenshot/'+encodeURIComponent(ss.id)+'/image';
|
|
860
|
+
const lbl = esc(ss.label||'screenshot');
|
|
861
|
+
const rt = ss.route ? ' - '+esc(ss.route) : '';
|
|
862
|
+
return '<div class="ss-card ring-glow fade-up" data-ss-idx="'+ss._idx+'" data-ss-label="'+lbl.toLowerCase()+'" tabindex="0" role="button" aria-label="View screenshot: '+lbl+'">' +
|
|
863
|
+
'<img src="'+src+'" alt="'+lbl+'" loading="lazy" onerror="this.style.display='none';this.nextElementSibling.insertAdjacentHTML('afterbegin','<div style=\\"padding:24px;text-align:center;color:#52525b;font-size:11px\\">Image unavailable</div>')">' +
|
|
864
|
+
'<div class="ss-meta"><div class="text-[11px] text-zinc-300 truncate" title="'+lbl+'">'+lbl+'</div>' +
|
|
865
|
+
'<div class="text-[10px] text-zinc-500">'+esc(ss.created_at?.slice(5,16)||'')+(rt?rt:'')+'</div>' +
|
|
866
|
+
'</div></div>';
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function restoreGalleryState() {
|
|
870
|
+
if (_gridSize !== 'md') setGridSize(_gridSize);
|
|
871
|
+
if (_activeCat !== 'all') filterCat(_activeCat);
|
|
872
|
+
_expandedGroups.forEach(cat => {
|
|
873
|
+
const btn = document.querySelector('.ss-show-more[data-cat="'+cat+'"]');
|
|
874
|
+
if (btn) toggleGroupExpand(btn);
|
|
875
|
+
});
|
|
876
|
+
if (_searchQuery) {
|
|
877
|
+
const el = document.getElementById('ss-search');
|
|
878
|
+
if (el) { el.value = _searchQuery; filterScreenshots(); }
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// ── Slideshow Lightbox ──────────────────────────────────────
|
|
883
|
+
let _lbIdx = 0;
|
|
884
|
+
let _lbVisible = [];
|
|
885
|
+
|
|
886
|
+
function buildLightbox() {
|
|
887
|
+
let lb = document.getElementById('lightbox');
|
|
888
|
+
if (lb) return lb;
|
|
889
|
+
lb = document.createElement('div');
|
|
890
|
+
lb.id = 'lightbox';
|
|
891
|
+
lb.className = 'lightbox';
|
|
892
|
+
lb.setAttribute('role', 'dialog');
|
|
893
|
+
lb.setAttribute('aria-label', 'Screenshot viewer');
|
|
894
|
+
lb.innerHTML =
|
|
895
|
+
'<button class="lb-nav prev" id="lb-prev" aria-label="Previous screenshot">‹</button>' +
|
|
896
|
+
'<img id="lb-img" src="" alt="Screenshot preview">' +
|
|
897
|
+
'<button class="lb-nav next" id="lb-next" aria-label="Next screenshot">›</button>' +
|
|
898
|
+
'<button class="lb-close" id="lb-close" aria-label="Close viewer">×</button>' +
|
|
899
|
+
'<div class="lb-chrome"><span class="lb-label" id="lb-label"></span><span class="lb-counter" id="lb-counter"></span></div>';
|
|
900
|
+
lb.addEventListener('click', e => {
|
|
901
|
+
if (e.target === lb) closeLightbox();
|
|
902
|
+
});
|
|
903
|
+
lb.querySelector('#lb-close').onclick = closeLightbox;
|
|
904
|
+
lb.querySelector('#lb-prev').onclick = e => { e.stopPropagation(); lbNav(-1); };
|
|
905
|
+
lb.querySelector('#lb-next').onclick = e => { e.stopPropagation(); lbNav(1); };
|
|
906
|
+
document.body.appendChild(lb);
|
|
907
|
+
document.addEventListener('keydown', e => {
|
|
908
|
+
const lbEl = document.getElementById('lightbox');
|
|
909
|
+
if (!lbEl || !lbEl.classList.contains('open')) return;
|
|
910
|
+
if (e.key === 'Escape') closeLightbox();
|
|
911
|
+
else if (e.key === 'ArrowLeft') lbNav(-1);
|
|
912
|
+
else if (e.key === 'ArrowRight') lbNav(1);
|
|
913
|
+
});
|
|
914
|
+
return lb;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function openLightbox(idx) {
|
|
918
|
+
const lb = buildLightbox();
|
|
919
|
+
_lbVisible = [];
|
|
920
|
+
document.querySelectorAll('.ss-card:not([style*="display:none"]):not([style*="display: none"])').forEach(card => {
|
|
921
|
+
const i = parseInt(card.dataset.ssIdx);
|
|
922
|
+
if (!isNaN(i)) _lbVisible.push(i);
|
|
923
|
+
});
|
|
924
|
+
if (_lbVisible.length === 0 && window._ssAll) _lbVisible = window._ssAll.map((_, i) => i);
|
|
925
|
+
_lbIdx = _lbVisible.indexOf(idx);
|
|
926
|
+
if (_lbIdx === -1) _lbIdx = 0;
|
|
927
|
+
renderLb();
|
|
928
|
+
lb.classList.add('open');
|
|
929
|
+
lb.querySelector('#lb-close').focus();
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function closeLightbox() {
|
|
933
|
+
const lb = document.getElementById('lightbox');
|
|
934
|
+
if (lb) lb.classList.remove('open');
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function lbNav(dir) {
|
|
938
|
+
_lbIdx = (_lbIdx + dir + _lbVisible.length) % _lbVisible.length;
|
|
939
|
+
renderLb();
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function renderLb() {
|
|
943
|
+
if (!window._ssAll || _lbVisible.length === 0) return;
|
|
944
|
+
const ss = window._ssAll[_lbVisible[_lbIdx]];
|
|
945
|
+
if (!ss) return;
|
|
946
|
+
const img = document.getElementById('lb-img');
|
|
947
|
+
const lbl = document.getElementById('lb-label');
|
|
948
|
+
const ctr = document.getElementById('lb-counter');
|
|
949
|
+
img.src = ss.src;
|
|
950
|
+
img.alt = ss.label + (ss.route ? ' \\u2014 ' + ss.route : '');
|
|
951
|
+
lbl.textContent = ss.label + (ss.route ? ' \\u2014 ' + ss.route : '');
|
|
952
|
+
ctr.textContent = (_lbIdx + 1) + ' / ' + _lbVisible.length;
|
|
953
|
+
document.getElementById('lb-prev').style.display = _lbVisible.length > 1 ? '' : 'none';
|
|
954
|
+
document.getElementById('lb-next').style.display = _lbVisible.length > 1 ? '' : 'none';
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Click delegation for screenshot cards
|
|
958
|
+
document.addEventListener('click', e => {
|
|
959
|
+
const card = e.target.closest('.ss-card[data-ss-idx]');
|
|
960
|
+
if (card) {
|
|
961
|
+
openLightbox(parseInt(card.dataset.ssIdx));
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const el = e.target.closest('[data-lb-src]');
|
|
965
|
+
if (el) {
|
|
966
|
+
const lb = buildLightbox();
|
|
967
|
+
_lbVisible = [];
|
|
968
|
+
window._ssAll = window._ssAll || [{ src: el.dataset.lbSrc, label: el.dataset.lbLabel||'', route: '', time: '' }];
|
|
969
|
+
_lbVisible = [window._ssAll.length - 1];
|
|
970
|
+
_lbIdx = 0;
|
|
971
|
+
document.getElementById('lb-img').src = el.dataset.lbSrc;
|
|
972
|
+
document.getElementById('lb-label').textContent = el.dataset.lbLabel || '';
|
|
973
|
+
document.getElementById('lb-counter').textContent = '';
|
|
974
|
+
document.getElementById('lb-prev').style.display = 'none';
|
|
975
|
+
document.getElementById('lb-next').style.display = 'none';
|
|
976
|
+
lb.classList.add('open');
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
// Enter/Space on screenshot cards + keyboard shortcuts
|
|
981
|
+
document.addEventListener('keydown', e => {
|
|
982
|
+
// Ctrl/Cmd+K: focus search
|
|
983
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
984
|
+
e.preventDefault();
|
|
985
|
+
const search = document.getElementById('ss-search');
|
|
986
|
+
if (search) { search.focus(); search.select(); }
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
990
|
+
const card = e.target.closest('.ss-card[data-ss-idx]');
|
|
991
|
+
if (card) { e.preventDefault(); openLightbox(parseInt(card.dataset.ssIdx)); }
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
// ── Gallery interaction functions ─────────────────────────────
|
|
996
|
+
function filterScreenshots() {
|
|
997
|
+
const q = (document.getElementById('ss-search')?.value || '').toLowerCase().trim();
|
|
998
|
+
_searchQuery = q;
|
|
999
|
+
document.querySelectorAll('.ss-card').forEach(card => {
|
|
1000
|
+
const lbl = card.dataset.ssLabel || '';
|
|
1001
|
+
const match = !q || lbl.includes(q);
|
|
1002
|
+
card.style.display = match ? '' : 'none';
|
|
1003
|
+
});
|
|
1004
|
+
if (q) {
|
|
1005
|
+
document.querySelectorAll('.ss-group').forEach(g => g.style.display = '');
|
|
1006
|
+
document.querySelectorAll('.ss-card[data-collapsed]').forEach(c => {
|
|
1007
|
+
if ((c.dataset.ssLabel||'').includes(q)) c.style.display = '';
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function filterCat(cat) {
|
|
1013
|
+
_activeCat = cat;
|
|
1014
|
+
_searchQuery = '';
|
|
1015
|
+
document.querySelectorAll('#ss-cat-bar .cat-pill').forEach(p => {
|
|
1016
|
+
const isActive = p.dataset.cat === cat;
|
|
1017
|
+
p.setAttribute('aria-pressed', isActive ? 'true' : 'false');
|
|
1018
|
+
});
|
|
1019
|
+
document.querySelectorAll('.ss-group').forEach(g => {
|
|
1020
|
+
g.style.display = (cat === 'all' || g.dataset.cat === cat) ? '' : 'none';
|
|
1021
|
+
});
|
|
1022
|
+
const searchEl = document.getElementById('ss-search');
|
|
1023
|
+
if (searchEl) searchEl.value = '';
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function setGridSize(sz) {
|
|
1027
|
+
_gridSize = sz;
|
|
1028
|
+
document.querySelectorAll('.sz-btn').forEach(b => {
|
|
1029
|
+
const isActive = b.dataset.sz === sz;
|
|
1030
|
+
b.classList.toggle('active', isActive);
|
|
1031
|
+
b.setAttribute('aria-pressed', isActive ? 'true' : 'false');
|
|
1032
|
+
});
|
|
1033
|
+
document.querySelectorAll('.ss-grid').forEach(g => {
|
|
1034
|
+
g.classList.remove('sz-sm', 'sz-md', 'sz-lg');
|
|
1035
|
+
g.classList.add('sz-' + sz);
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function toggleGroupExpand(btn) {
|
|
1040
|
+
const group = btn.closest('.ss-group');
|
|
1041
|
+
if (!group) return;
|
|
1042
|
+
const grid = group.querySelector('.ss-grid');
|
|
1043
|
+
if (!grid) return;
|
|
1044
|
+
|
|
1045
|
+
if (btn.dataset.expanded) {
|
|
1046
|
+
// Collapse: remove cards beyond initial 8
|
|
1047
|
+
const cards = grid.querySelectorAll('.ss-card');
|
|
1048
|
+
let count = 0;
|
|
1049
|
+
cards.forEach(c => {
|
|
1050
|
+
count++;
|
|
1051
|
+
if (count > 8) c.remove();
|
|
1052
|
+
});
|
|
1053
|
+
delete btn.dataset.expanded;
|
|
1054
|
+
_expandedGroups.delete(group.dataset.cat);
|
|
1055
|
+
const hiddenIndices = btn.dataset.items ? JSON.parse(btn.dataset.items) : [];
|
|
1056
|
+
btn.textContent = 'Show ' + hiddenIndices.length + ' more';
|
|
1057
|
+
} else {
|
|
1058
|
+
// Expand: render deferred cards into DOM
|
|
1059
|
+
const hiddenIndices = btn.dataset.items ? JSON.parse(btn.dataset.items) : [];
|
|
1060
|
+
if (window._ssAll && hiddenIndices.length) {
|
|
1061
|
+
const allShots = window._ssRaw || [];
|
|
1062
|
+
hiddenIndices.forEach(idx => {
|
|
1063
|
+
const ss = allShots[idx];
|
|
1064
|
+
if (ss) {
|
|
1065
|
+
grid.insertAdjacentHTML('beforeend', renderSsCard(ss));
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
btn.textContent = 'Show less';
|
|
1070
|
+
btn.dataset.expanded = '1';
|
|
1071
|
+
_expandedGroups.add(group.dataset.cat);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// ── Changelog Carousel ─────────────────────────────────────
|
|
1076
|
+
let _clObserver = null;
|
|
1077
|
+
|
|
1078
|
+
function initCarousel() {
|
|
1079
|
+
const track = document.getElementById('cl-track');
|
|
1080
|
+
if (!track) return;
|
|
1081
|
+
|
|
1082
|
+
// Attach scroll-based arrow + dot updates
|
|
1083
|
+
track.addEventListener('scroll', () => {
|
|
1084
|
+
clUpdateDots();
|
|
1085
|
+
clUpdateArrows();
|
|
1086
|
+
}, { passive: true });
|
|
1087
|
+
|
|
1088
|
+
// Keyboard navigation on carousel focus
|
|
1089
|
+
const carousel = document.getElementById('cl-carousel');
|
|
1090
|
+
if (carousel) {
|
|
1091
|
+
carousel.addEventListener('keydown', e => {
|
|
1092
|
+
if (e.key === 'ArrowLeft') { e.preventDefault(); clNav(-1); }
|
|
1093
|
+
else if (e.key === 'ArrowRight') { e.preventDefault(); clNav(1); }
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// IntersectionObserver for accurate dot tracking
|
|
1098
|
+
const dots = document.getElementById('cl-dots');
|
|
1099
|
+
if (dots && 'IntersectionObserver' in window) {
|
|
1100
|
+
_clObserver = new IntersectionObserver(entries => {
|
|
1101
|
+
entries.forEach(entry => {
|
|
1102
|
+
if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
|
|
1103
|
+
const cards = Array.from(track.querySelectorAll('.cl-card'));
|
|
1104
|
+
const idx = cards.indexOf(entry.target);
|
|
1105
|
+
if (idx >= 0) {
|
|
1106
|
+
dots.querySelectorAll('.cl-dot').forEach((d, i) => {
|
|
1107
|
+
const isActive = i === idx;
|
|
1108
|
+
d.classList.toggle('active', isActive);
|
|
1109
|
+
d.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
}, { root: track, threshold: 0.5 });
|
|
1115
|
+
track.querySelectorAll('.cl-card').forEach(card => _clObserver.observe(card));
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
clUpdateArrows();
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function clNav(dir) {
|
|
1122
|
+
const track = document.getElementById('cl-track');
|
|
1123
|
+
if (!track) return;
|
|
1124
|
+
const card = track.querySelector('.cl-card');
|
|
1125
|
+
if (!card) return;
|
|
1126
|
+
const gap = parseFloat(getComputedStyle(track).gap) || 16;
|
|
1127
|
+
const w = card.offsetWidth + gap;
|
|
1128
|
+
track.scrollBy({ left: dir * w, behavior: 'smooth' });
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function clGoTo(idx) {
|
|
1132
|
+
const track = document.getElementById('cl-track');
|
|
1133
|
+
if (!track) return;
|
|
1134
|
+
const card = track.querySelector('.cl-card');
|
|
1135
|
+
if (!card) return;
|
|
1136
|
+
const gap = parseFloat(getComputedStyle(track).gap) || 16;
|
|
1137
|
+
const w = card.offsetWidth + gap;
|
|
1138
|
+
track.scrollTo({ left: idx * w, behavior: 'smooth' });
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function clUpdateDots() {
|
|
1142
|
+
const track = document.getElementById('cl-track');
|
|
1143
|
+
const dots = document.getElementById('cl-dots');
|
|
1144
|
+
if (!track || !dots) return;
|
|
1145
|
+
// Fallback for browsers without IntersectionObserver
|
|
1146
|
+
if (!_clObserver) {
|
|
1147
|
+
const card = track.querySelector('.cl-card');
|
|
1148
|
+
if (!card) return;
|
|
1149
|
+
const gap = parseFloat(getComputedStyle(track).gap) || 16;
|
|
1150
|
+
const w = card.offsetWidth + gap;
|
|
1151
|
+
const idx = Math.round(track.scrollLeft / w);
|
|
1152
|
+
dots.querySelectorAll('.cl-dot').forEach((d, i) => {
|
|
1153
|
+
const isActive = i === idx;
|
|
1154
|
+
d.classList.toggle('active', isActive);
|
|
1155
|
+
d.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function clUpdateArrows() {
|
|
1161
|
+
const track = document.getElementById('cl-track');
|
|
1162
|
+
const prev = document.getElementById('cl-prev');
|
|
1163
|
+
const next = document.getElementById('cl-next');
|
|
1164
|
+
if (!track || !prev || !next) return;
|
|
1165
|
+
const atStart = track.scrollLeft <= 1;
|
|
1166
|
+
const atEnd = track.scrollLeft + track.clientWidth >= track.scrollWidth - 1;
|
|
1167
|
+
prev.classList.toggle('disabled', atStart);
|
|
1168
|
+
prev.setAttribute('aria-disabled', atStart ? 'true' : 'false');
|
|
1169
|
+
next.classList.toggle('disabled', atEnd);
|
|
1170
|
+
next.setAttribute('aria-disabled', atEnd ? 'true' : 'false');
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// ── Category Classification (priority scoring) ─────────────
|
|
1174
|
+
const CAT_RULES = [
|
|
1175
|
+
{ pattern: /^trace\\s+qa/i, cat: 'Trace QA', priority: 10 },
|
|
1176
|
+
{ pattern: /^trace/i, cat: 'Trace', priority: 9 },
|
|
1177
|
+
{ pattern: /^mcp/i, cat: 'MCP Ledger', priority: 9 },
|
|
1178
|
+
{ pattern: /^benchmarks?/i, cat: 'Benchmarks', priority: 9 },
|
|
1179
|
+
{ pattern: /^page\\s+index/i, cat: 'Page Index', priority: 10 },
|
|
1180
|
+
{ pattern: /^redesign/i, cat: 'Redesigns', priority: 8 },
|
|
1181
|
+
{ pattern: /^final/i, cat: 'Final States', priority: 8 },
|
|
1182
|
+
{ pattern: /landing|home|signin|main\\s+page|navigation/i, cat: 'Navigation', priority: 5 },
|
|
1183
|
+
{ pattern: /fast\\s+agent|agent/i, cat: 'Fast Agent', priority: 5 },
|
|
1184
|
+
];
|
|
1185
|
+
|
|
1186
|
+
function classifyScreenshot(label) {
|
|
1187
|
+
let bestCat = 'General', bestPriority = -1;
|
|
1188
|
+
for (const rule of CAT_RULES) {
|
|
1189
|
+
if (rule.pattern.test(label) && rule.priority > bestPriority) {
|
|
1190
|
+
bestCat = rule.cat;
|
|
1191
|
+
bestPriority = rule.priority;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return bestCat;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// ── Helpers ─────────────────────────────────────────────────
|
|
1198
|
+
const SEC_ICONS = {
|
|
1199
|
+
'Bugs & Fixes': ['#451a03','#fbbf24','<path d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/>'],
|
|
1200
|
+
'Code Review': ['#1e1b4b','#818cf8','<path d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z"/>'],
|
|
1201
|
+
'Screenshots': ['#052e16','#34d399','<path d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0022.5 18.75V5.25A2.25 2.25 0 0020.25 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/>'],
|
|
1202
|
+
'Changelog': ['#172554','#60a5fa','<path d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"/>'],
|
|
1203
|
+
'Generated Tests': ['#14532d','#4ade80','<path d="M4.5 12.75l6 6 9-13.5"/>'],
|
|
1204
|
+
'Components': ['#3b0764','#c084fc','<path d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9"/>'],
|
|
1205
|
+
'Code Locations': ['#1c1917','#a8a29e','<path d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/>'],
|
|
1206
|
+
};
|
|
1207
|
+
function sec(title, subtitle) {
|
|
1208
|
+
const icon = SEC_ICONS[title];
|
|
1209
|
+
const parts = ['<div class="sec-hdr">'];
|
|
1210
|
+
if (icon) {
|
|
1211
|
+
parts.push('<div class="sec-icon" style="background:'+icon[0]+';color:'+icon[1]+'" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">'+icon[2]+'</svg></div>');
|
|
1212
|
+
}
|
|
1213
|
+
parts.push('<div class="sec-text"><h2>'+title+'</h2>');
|
|
1214
|
+
if (subtitle) parts.push('<div class="sec-sub">'+subtitle+'</div>');
|
|
1215
|
+
parts.push('</div></div>');
|
|
1216
|
+
return parts.join('');
|
|
1217
|
+
}
|
|
1218
|
+
function sevBadge(sev) {
|
|
1219
|
+
const c = {critical:'sev-critical',high:'sev-high',medium:'sev-medium',low:'sev-low'}[sev]||'sev-medium';
|
|
1220
|
+
return '<span class="text-[10px] px-1.5 py-0.5 rounded font-medium shrink-0 '+c+'">'+sev+'</span>';
|
|
1221
|
+
}
|
|
1222
|
+
function statusBadge(st) {
|
|
1223
|
+
if(st==='resolved'||st==='fixed') return '<span class="text-[10px] px-1.5 py-0.5 rounded bg-ok/10 text-ok shrink-0">fixed</span>';
|
|
1224
|
+
if(st==='open') return '<span class="text-[10px] px-1.5 py-0.5 rounded bg-err/10 text-err shrink-0">open</span>';
|
|
1225
|
+
return '';
|
|
1226
|
+
}
|
|
1227
|
+
function shortPath(p) {
|
|
1228
|
+
if(!p) return '';
|
|
1229
|
+
const parts = p.replace(/\\\\/g,'/').split('/');
|
|
1230
|
+
return parts.length>3 ? '\\u2026/'+parts.slice(-3).join('/') : p;
|
|
1231
|
+
}
|
|
1232
|
+
function fileChips(raw) {
|
|
1233
|
+
if(!raw) return '';
|
|
1234
|
+
let files = [];
|
|
1235
|
+
try { files = JSON.parse(raw); } catch { files = [raw]; }
|
|
1236
|
+
if(!Array.isArray(files)) files = [String(files)];
|
|
1237
|
+
return files.map(f => '<span class="file-chip" title="'+esc(f)+'">'+esc(shortPath(f))+'</span>').join('');
|
|
1238
|
+
}
|
|
1239
|
+
function truncWords(s, max) {
|
|
1240
|
+
if(!s || s.length<=max) return s||'';
|
|
1241
|
+
const cut = s.lastIndexOf(' ', max);
|
|
1242
|
+
return s.slice(0, cut>0?cut:max) + '\\u2026';
|
|
1243
|
+
}
|
|
1244
|
+
function esc(s) { return s==null?'':String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
1245
|
+
|
|
1246
|
+
init();
|
|
1247
|
+
</script>
|
|
1248
|
+
</body>
|
|
1249
|
+
</html>`;
|
|
1250
|
+
}
|
|
1251
|
+
//# sourceMappingURL=html.js.map
|