free-coding-models 0.5.1 → 0.5.4
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 +31 -5
- package/changelog/v0.5.4.md +24 -0
- package/package.json +5 -2
- package/sources.js +2 -0
- package/src/tui/key-handler.js +1 -1
- package/web/assets/providers/aistudio/aistudio-text.svg +1 -0
- package/web/assets/providers/aistudio/aistudio.svg +1 -0
- package/web/assets/providers/cerebras/cerebras-brand-color.svg +1 -0
- package/web/assets/providers/cerebras/cerebras-brand.svg +1 -0
- package/web/assets/providers/cerebras/cerebras-color.svg +1 -0
- package/web/assets/providers/cerebras/cerebras-text.svg +1 -0
- package/web/assets/providers/cerebras/cerebras.svg +1 -0
- package/web/assets/providers/cloudflare/cloudflare-color.svg +1 -0
- package/web/assets/providers/cloudflare/cloudflare-text.svg +1 -0
- package/web/assets/providers/cloudflare/cloudflare.svg +1 -0
- package/web/assets/providers/gemini/gemini-color.svg +1 -0
- package/web/assets/providers/gemini/gemini-text.svg +1 -0
- package/web/assets/providers/gemini/gemini.svg +1 -0
- package/web/assets/providers/github/github-text.svg +1 -0
- package/web/assets/providers/github/github.svg +1 -0
- package/web/assets/providers/groq/groq-text.svg +1 -0
- package/web/assets/providers/groq/groq.svg +1 -0
- package/web/assets/providers/mistral/mistral-color.svg +1 -0
- package/web/assets/providers/mistral/mistral-text.svg +1 -0
- package/web/assets/providers/mistral/mistral.svg +1 -0
- package/web/assets/providers/nvidia/nvidia-color.svg +1 -0
- package/web/assets/providers/nvidia/nvidia-text.svg +1 -0
- package/web/assets/providers/nvidia/nvidia.svg +1 -0
- package/web/assets/providers/opencode/opencode-text.svg +1 -0
- package/web/assets/providers/opencode/opencode.svg +1 -0
- package/web/assets/providers/openrouter/openrouter (1).svg +1 -0
- package/web/assets/providers/openrouter/openrouter-text.svg +1 -0
- package/web/assets/providers/openrouter/openrouter.svg +1 -0
- package/web/assets/providers/ovhcloud/ovhcloud.svg +12 -0
- package/web/assets/providers/qwen/qwen-color.svg +1 -0
- package/web/assets/providers/qwen/qwen-text.svg +1 -0
- package/web/assets/providers/qwen/qwen.svg +1 -0
- package/web/assets/providers/sambanova/sambanova-color.svg +1 -0
- package/web/assets/providers/sambanova/sambanova-text.svg +1 -0
- package/web/assets/providers/sambanova/sambanova.svg +1 -0
- package/web/assets/providers/scalewaylogo/ScalewayLogo.svg +13 -0
- package/web/assets/providers/zai/zai-text.svg +1 -0
- package/web/assets/providers/zai/zai.svg +1 -0
- package/web/dist/assets/index-BoWmUveV.js +39 -0
- package/web/dist/assets/index-BrpHevg4.css +1 -0
- package/web/dist/favicon.ico +0 -0
- package/web/dist/favicons/apple-touch-icon.png +0 -0
- package/web/dist/favicons/browserconfig.xml +12 -0
- package/web/dist/favicons/favicon-16x16.png +0 -0
- package/web/dist/favicons/favicon-192x192.png +0 -0
- package/web/dist/favicons/favicon-32x32.png +0 -0
- package/web/dist/favicons/favicon-48x48.png +0 -0
- package/web/dist/favicons/favicon-512x512.png +0 -0
- package/web/dist/favicons/favicon-96x96.png +0 -0
- package/web/dist/favicons/favicon.ico +0 -0
- package/web/dist/favicons/mstile-150x150.png +0 -0
- package/web/dist/favicons/mstile-310x150.png +0 -0
- package/web/dist/favicons/mstile-310x310.png +0 -0
- package/web/dist/favicons/mstile-512x512.png +0 -0
- package/web/dist/favicons/mstile-70x70.png +0 -0
- package/web/dist/favicons/site.webmanifest +25 -0
- package/web/dist/index.html +42 -3
- package/web/index.html +40 -1
- package/web/public/favicon.ico +0 -0
- package/web/public/favicons/apple-touch-icon.png +0 -0
- package/web/public/favicons/browserconfig.xml +12 -0
- package/web/public/favicons/favicon-16x16.png +0 -0
- package/web/public/favicons/favicon-192x192.png +0 -0
- package/web/public/favicons/favicon-32x32.png +0 -0
- package/web/public/favicons/favicon-48x48.png +0 -0
- package/web/public/favicons/favicon-512x512.png +0 -0
- package/web/public/favicons/favicon-96x96.png +0 -0
- package/web/public/favicons/favicon.ico +0 -0
- package/web/public/favicons/mstile-150x150.png +0 -0
- package/web/public/favicons/mstile-310x150.png +0 -0
- package/web/public/favicons/mstile-310x310.png +0 -0
- package/web/public/favicons/mstile-512x512.png +0 -0
- package/web/public/favicons/mstile-70x70.png +0 -0
- package/web/public/favicons/site.webmanifest +25 -0
- package/web/server.js +71 -1
- package/web/src/App.jsx +209 -89
- package/web/src/components/analytics/AnalyticsView.jsx +30 -2
- package/web/src/components/analytics/AnalyticsView.module.css +4 -0
- package/web/src/components/atoms/AILatencyCell.module.css +9 -1
- package/web/src/components/atoms/HealthCell.jsx +113 -28
- package/web/src/components/atoms/HealthCell.module.css +43 -2
- package/web/src/components/atoms/LastPingCell.module.css +10 -0
- package/web/src/components/atoms/NoKeyIcon.jsx +65 -0
- package/web/src/components/atoms/NoKeyIcon.module.css +30 -0
- package/web/src/components/atoms/ProviderLogo.jsx +188 -0
- package/web/src/components/atoms/ProviderLogo.module.css +103 -0
- package/web/src/components/atoms/Sparkline.jsx +21 -2
- package/web/src/components/atoms/StabilityCell.module.css +5 -0
- package/web/src/components/atoms/StatusDot.module.css +7 -0
- package/web/src/components/atoms/TPSCell.module.css +7 -1
- package/web/src/components/atoms/TierBadge.module.css +11 -0
- package/web/src/components/atoms/Toast.module.css +6 -0
- package/web/src/components/atoms/VerdictBadge.module.css +12 -0
- package/web/src/components/dashboard/DetailPanel.jsx +63 -2
- package/web/src/components/dashboard/DetailPanel.module.css +130 -0
- package/web/src/components/dashboard/FilterBar.jsx +146 -53
- package/web/src/components/dashboard/FilterBar.module.css +138 -58
- package/web/src/components/dashboard/ModelTable.jsx +235 -30
- package/web/src/components/dashboard/ModelTable.module.css +205 -16
- package/web/src/components/layout/Header.jsx +140 -12
- package/web/src/components/layout/Header.module.css +191 -27
- package/web/src/components/palette/CommandPalette.jsx +146 -0
- package/web/src/components/palette/CommandPalette.module.css +121 -0
- package/web/src/global.css +52 -15
- package/web/src/hooks/useColumnSizing.js +211 -0
- package/web/src/hooks/useFavorites.js +150 -0
- package/web/src/hooks/useFilter.js +157 -24
- package/web/src/hooks/useTheme.js +72 -16
- package/web/src/hooks/useUrlState.js +93 -0
- package/web/dist/assets/index-ByGf4Kq-.js +0 -14
- package/web/dist/assets/index-Ds7wmHBv.css +0 -1
- package/web/src/components/dashboard/StatsBar.jsx +0 -42
- package/web/src/components/dashboard/StatsBar.module.css +0 -28
- package/web/src/components/layout/Sidebar.jsx +0 -50
- package/web/src/components/layout/Sidebar.module.css +0 -86
- package/web/src/components/map/MapView.jsx +0 -17
- package/web/src/components/map/MapView.module.css +0 -25
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file web/src/components/dashboard/ModelTable.module.css
|
|
3
3
|
* @description Styles for ModelTable with TanStack Table — full horizontal scroll.
|
|
4
|
+
* 📖 Seamless layout: the container is flush with the page edges (no margin,
|
|
5
|
+
* 📖 no left/right border) so the table runs full-bleed below the sticky
|
|
6
|
+
* 📖 header + sticky FilterBar. Only the bottom border remains.
|
|
4
7
|
*/
|
|
5
8
|
.container {
|
|
6
9
|
background: var(--color-bg-card);
|
|
7
|
-
border:
|
|
8
|
-
border-
|
|
9
|
-
|
|
10
|
+
border-top: 0;
|
|
11
|
+
border-bottom: 1px solid var(--color-border);
|
|
12
|
+
border-left: 0;
|
|
13
|
+
border-right: 0;
|
|
14
|
+
border-radius: 0;
|
|
15
|
+
margin: 0;
|
|
10
16
|
flex: 1;
|
|
11
17
|
min-height: 0;
|
|
12
18
|
overflow: auto;
|
|
@@ -19,7 +25,7 @@
|
|
|
19
25
|
min-width: 1200px;
|
|
20
26
|
border-collapse: collapse;
|
|
21
27
|
font-size: 12px;
|
|
22
|
-
border:
|
|
28
|
+
border: 0;
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
/* ─── Header ─── */
|
|
@@ -39,10 +45,131 @@
|
|
|
39
45
|
top: 0;
|
|
40
46
|
z-index: 10;
|
|
41
47
|
text-align: center;
|
|
48
|
+
/* 📖 Disable user-select on the whole header so dragging the resize handle
|
|
49
|
+
📖 never accidentally selects the cell text on rapid drags. */
|
|
50
|
+
-webkit-user-select: none;
|
|
51
|
+
-moz-user-select: none;
|
|
52
|
+
-ms-user-select: none;
|
|
42
53
|
}
|
|
43
54
|
.th:last-child { border-right: 0; }
|
|
44
55
|
.th:hover { color: var(--color-accent); background: var(--color-bg-active); }
|
|
45
56
|
|
|
57
|
+
/* 📖 Inner flex container: header label on the left, resize handle on the right.
|
|
58
|
+
📖 Letting the label shrink keeps the column tight when the user makes it narrow. */
|
|
59
|
+
.thInner {
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
justify-content: space-between;
|
|
63
|
+
gap: 4px;
|
|
64
|
+
width: 100%;
|
|
65
|
+
min-width: 0;
|
|
66
|
+
}
|
|
67
|
+
.thLabel {
|
|
68
|
+
display: inline-flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
gap: 4px;
|
|
71
|
+
min-width: 0;
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
text-overflow: ellipsis;
|
|
74
|
+
white-space: nowrap;
|
|
75
|
+
flex: 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* 📖 Resize handle: a 6px-wide grab zone on the right edge of every header.
|
|
79
|
+
📖 The visible bar is 2px wide and centered in the grab zone so users get
|
|
80
|
+
📖 generous hit-target without it looking like a hard line. */
|
|
81
|
+
.resizeHandle {
|
|
82
|
+
flex-shrink: 0;
|
|
83
|
+
width: 6px;
|
|
84
|
+
height: 22px;
|
|
85
|
+
margin-left: 2px;
|
|
86
|
+
display: inline-flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
justify-content: center;
|
|
89
|
+
cursor: col-resize;
|
|
90
|
+
position: relative;
|
|
91
|
+
touch-action: none;
|
|
92
|
+
/* 📖 Stop the click from also triggering the header's sort handler. */
|
|
93
|
+
user-select: none;
|
|
94
|
+
-webkit-user-select: none;
|
|
95
|
+
}
|
|
96
|
+
.resizeHandle::after {
|
|
97
|
+
content: '';
|
|
98
|
+
display: block;
|
|
99
|
+
width: 2px;
|
|
100
|
+
height: 14px;
|
|
101
|
+
background: transparent;
|
|
102
|
+
border-radius: 1px;
|
|
103
|
+
transition: background 120ms var(--ease-out, ease);
|
|
104
|
+
}
|
|
105
|
+
.resizeHandle:hover::after,
|
|
106
|
+
.resizeHandleActive::after {
|
|
107
|
+
background: var(--color-accent);
|
|
108
|
+
}
|
|
109
|
+
.resizeHandleActive {
|
|
110
|
+
cursor: col-resize;
|
|
111
|
+
}
|
|
112
|
+
/* 📖 While a column is actively resizing, highlight its entire header and freeze
|
|
113
|
+
📖 the cursor on the whole document so dragging off the handle still works. */
|
|
114
|
+
.th:has(.resizeHandleActive) {
|
|
115
|
+
background: var(--color-bg-active);
|
|
116
|
+
color: var(--color-accent);
|
|
117
|
+
}
|
|
118
|
+
:global(.col-resizing) {
|
|
119
|
+
cursor: col-resize !important;
|
|
120
|
+
user-select: none !important;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* 📖 Floating toolbar that appears only when the user has custom column widths.
|
|
124
|
+
📖 Sits at the top-right of the table area without pushing rows down. */
|
|
125
|
+
.resizeToolbar {
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
justify-content: space-between;
|
|
129
|
+
gap: 8px;
|
|
130
|
+
padding: 6px 10px;
|
|
131
|
+
background: var(--color-bg-elevated);
|
|
132
|
+
border-bottom: 1px solid var(--color-border);
|
|
133
|
+
font-size: 11px;
|
|
134
|
+
color: var(--color-text-muted);
|
|
135
|
+
position: sticky;
|
|
136
|
+
top: 0;
|
|
137
|
+
z-index: 11;
|
|
138
|
+
}
|
|
139
|
+
.resizeToolbarHint {
|
|
140
|
+
display: inline-flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
gap: 4px;
|
|
143
|
+
letter-spacing: 0.2px;
|
|
144
|
+
}
|
|
145
|
+
.resizeToolbarHint strong {
|
|
146
|
+
color: var(--color-text);
|
|
147
|
+
font-weight: 700;
|
|
148
|
+
font-family: var(--font-mono);
|
|
149
|
+
}
|
|
150
|
+
.resizeResetButton {
|
|
151
|
+
appearance: none;
|
|
152
|
+
background: transparent;
|
|
153
|
+
border: 1px solid var(--color-border-hover);
|
|
154
|
+
color: var(--color-text-muted);
|
|
155
|
+
padding: 3px 10px;
|
|
156
|
+
border-radius: 4px;
|
|
157
|
+
font-size: 11px;
|
|
158
|
+
font-weight: 600;
|
|
159
|
+
cursor: pointer;
|
|
160
|
+
transition: background 120ms var(--ease-out, ease), color 120ms var(--ease-out, ease), border-color 120ms var(--ease-out, ease);
|
|
161
|
+
white-space: nowrap;
|
|
162
|
+
}
|
|
163
|
+
.resizeResetButton:hover {
|
|
164
|
+
background: var(--color-bg-active);
|
|
165
|
+
color: var(--color-accent);
|
|
166
|
+
border-color: var(--color-accent);
|
|
167
|
+
}
|
|
168
|
+
.resizeResetButton:focus-visible {
|
|
169
|
+
outline: 2px solid var(--color-accent);
|
|
170
|
+
outline-offset: 1px;
|
|
171
|
+
}
|
|
172
|
+
|
|
46
173
|
/* Center certain columns */
|
|
47
174
|
.th:nth-child(6) { text-align: left; } /* Model */
|
|
48
175
|
.th:nth-child(7) { text-align: left; } /* Provider */
|
|
@@ -98,25 +225,25 @@
|
|
|
98
225
|
.pingSlow { color: #ff4444; }
|
|
99
226
|
.pingNone { color: var(--color-text-dim); }
|
|
100
227
|
|
|
228
|
+
/* 📖 Light theme: darker SWE + ping colors. */
|
|
229
|
+
:global([data-theme="light"]) .sweHigh { color: #7a5e00; }
|
|
230
|
+
:global([data-theme="light"]) .sweMid { color: #1f7a3e; }
|
|
231
|
+
:global([data-theme="light"]) .pingFast { color: #008f4d; }
|
|
232
|
+
:global([data-theme="light"]) .pingMedium { color: #b86b00; }
|
|
233
|
+
:global([data-theme="light"]) .pingSlow { color: #c8143a; }
|
|
234
|
+
|
|
101
235
|
.uptime { font-family: var(--font-mono); font-size: 11px; }
|
|
102
236
|
|
|
103
237
|
/* ─── Model cell ─── */
|
|
104
238
|
.modelCell { display: flex; align-items: center; gap: 6px; overflow: hidden; }
|
|
105
239
|
.modelMeta { flex: 1; min-width: 0; }
|
|
106
|
-
.modelHeader { display: flex; align-items: center; gap:
|
|
240
|
+
.modelHeader { display: flex; align-items: center; gap: 5px; }
|
|
107
241
|
.modelName { font-weight: 600; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
108
242
|
.modelId { font-family: var(--font-mono); font-size: 9px; color: var(--color-text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
109
|
-
.noKey {
|
|
110
|
-
font-size: 9px; font-weight: 600; padding: 1px 5px; border-radius: 3px; flex-shrink: 0;
|
|
111
|
-
background: rgba(255,170,0,0.12); color: #ffaa00; border: 1px solid rgba(255,170,0,0.2);
|
|
112
|
-
}
|
|
113
243
|
|
|
114
|
-
/*
|
|
115
|
-
.
|
|
116
|
-
|
|
117
|
-
background: var(--color-accent-dim); color: var(--color-text-muted);
|
|
118
|
-
border: 1px solid var(--color-border); white-space: nowrap;
|
|
119
|
-
}
|
|
244
|
+
/* 📖 Provider cell now uses <ProviderLogo /> (icon + wordmark) — see
|
|
245
|
+
📖 atoms/ProviderLogo.module.css for the layout. The old bordered pill
|
|
246
|
+
📖 was replaced; no extra styling is needed here. */
|
|
120
247
|
|
|
121
248
|
/* ─── Benchmarking row highlight ── */
|
|
122
249
|
@keyframes benchRowPulse {
|
|
@@ -141,4 +268,66 @@
|
|
|
141
268
|
border-color: #d8d8d8;
|
|
142
269
|
}
|
|
143
270
|
|
|
144
|
-
.empty { text-align: center; padding: 60px 0; color: var(--color-text-muted); }
|
|
271
|
+
.empty { text-align: center; padding: 60px 0; color: var(--color-text-muted); }
|
|
272
|
+
|
|
273
|
+
/* ─── M1: favorites star button (TUI `F` key) ─── */
|
|
274
|
+
.favBtn {
|
|
275
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
276
|
+
width: 24px; height: 24px;
|
|
277
|
+
background: transparent;
|
|
278
|
+
border: none;
|
|
279
|
+
color: var(--color-text-dim);
|
|
280
|
+
cursor: pointer;
|
|
281
|
+
border-radius: 4px;
|
|
282
|
+
transition: color 120ms, background 120ms, transform 120ms;
|
|
283
|
+
}
|
|
284
|
+
.favBtn:hover {
|
|
285
|
+
color: #ffd700;
|
|
286
|
+
background: rgba(255, 215, 0, 0.10);
|
|
287
|
+
transform: scale(1.1);
|
|
288
|
+
}
|
|
289
|
+
.favBtnActive {
|
|
290
|
+
color: #ffd700;
|
|
291
|
+
}
|
|
292
|
+
.favBtnActive:hover {
|
|
293
|
+
color: #ffae00;
|
|
294
|
+
background: rgba(255, 174, 0, 0.14);
|
|
295
|
+
}
|
|
296
|
+
:global([data-theme="light"]) .favBtn { color: #999; }
|
|
297
|
+
:global([data-theme="light"]) .favBtn:hover { color: #b08800; background: rgba(176, 136, 0, 0.10); }
|
|
298
|
+
:global([data-theme="light"]) .favBtnActive { color: #b08800; }
|
|
299
|
+
|
|
300
|
+
/* ─── M1: clickable AI Lat. cell (TUI `Ctrl+A`) ─── */
|
|
301
|
+
.aiLatencyWrap {
|
|
302
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 4px;
|
|
303
|
+
cursor: pointer;
|
|
304
|
+
padding: 2px 4px;
|
|
305
|
+
border-radius: 4px;
|
|
306
|
+
position: relative;
|
|
307
|
+
transition: background 100ms;
|
|
308
|
+
}
|
|
309
|
+
.aiLatencyWrap:hover {
|
|
310
|
+
background: rgba(180, 0, 255, 0.08);
|
|
311
|
+
}
|
|
312
|
+
.aiLatencyWrap.aiLatencyRunning { cursor: progress; }
|
|
313
|
+
.aiLatencyWrap.aiLatencyRunning:hover { background: transparent; }
|
|
314
|
+
.aiLatencyPlay {
|
|
315
|
+
opacity: 0;
|
|
316
|
+
color: #b400ff;
|
|
317
|
+
transition: opacity 100ms;
|
|
318
|
+
}
|
|
319
|
+
.aiLatencyWrap:hover .aiLatencyPlay { opacity: 1; }
|
|
320
|
+
.aiLatencyRunning .aiLatencyPlay { display: none; }
|
|
321
|
+
|
|
322
|
+
/* ─── M1: dark-red row for tool-incompatible models ─── */
|
|
323
|
+
.incompatible td {
|
|
324
|
+
background: rgba(255, 0, 0, 0.06) !important;
|
|
325
|
+
color: var(--color-text-muted) !important;
|
|
326
|
+
}
|
|
327
|
+
.incompatible:hover td {
|
|
328
|
+
background: rgba(255, 0, 0, 0.10) !important;
|
|
329
|
+
}
|
|
330
|
+
:global([data-theme="light"]) .incompatible td {
|
|
331
|
+
background: rgba(200, 20, 58, 0.06) !important;
|
|
332
|
+
color: #888 !important;
|
|
333
|
+
}
|
|
@@ -1,16 +1,83 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file web/src/components/layout/Header.jsx
|
|
3
|
-
* @description Top header bar
|
|
4
|
-
* 📖
|
|
3
|
+
* @description Top header bar — global navigation + search + actions.
|
|
4
|
+
* 📖 Layout refactor (M1): no more left sidebar. All navigation lives here.
|
|
5
|
+
* 📖 - Always-visible nav buttons: Dashboard, Settings, Analytics, Recommend, Router
|
|
6
|
+
* 📖 - Overflow menu (kebab): Help, Changelog, Install Endpoints, Installed Models
|
|
7
|
+
* 📖 - Right side: ⌘K (command palette), AI Latency, theme, export
|
|
8
|
+
* 📖 Each unimplemented feature shows a friendly "Coming in M2/M3/M4" toast so
|
|
9
|
+
* 📖 the menu structure is honest and complete from day one.
|
|
5
10
|
*/
|
|
6
|
-
import {
|
|
11
|
+
import { useEffect, useRef, useState } from 'react'
|
|
12
|
+
import {
|
|
13
|
+
IconBolt, IconSearch, IconDownload, IconSettings, IconMoon, IconSun,
|
|
14
|
+
IconPlayerPlay, IconCommand, IconLayoutDashboard, IconActivity,
|
|
15
|
+
IconSparkles, IconRoute, IconDots, IconQuestionMark, IconHistory,
|
|
16
|
+
IconPlug, IconFolders,
|
|
17
|
+
} from '@tabler/icons-react'
|
|
7
18
|
import styles from './Header.module.css'
|
|
8
19
|
|
|
20
|
+
// 📖 Top-level nav items — always visible as buttons. Inlined here so the
|
|
21
|
+
// 📖 order, icon, and "coming soon" milestone are colocated with the
|
|
22
|
+
// 📖 rendering code. When a view ships, remove the `comingIn` field.
|
|
23
|
+
const NAV_ITEMS = [
|
|
24
|
+
{ id: 'dashboard', label: 'Dashboard', icon: IconLayoutDashboard },
|
|
25
|
+
{ id: 'settings', label: 'Settings', icon: IconSettings },
|
|
26
|
+
{ id: 'analytics', label: 'Analytics', icon: IconActivity },
|
|
27
|
+
{ id: 'recommend', label: 'Recommend', icon: IconSparkles, comingIn: 'M3' },
|
|
28
|
+
{ id: 'router', label: 'Router', icon: IconRoute, comingIn: 'M4' },
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
// 📖 Overflow menu items — Help, Changelog, Install Endpoints, Installed Models.
|
|
32
|
+
const MENU_ITEMS = [
|
|
33
|
+
{ id: 'help', label: 'Help', icon: IconQuestionMark, comingIn: 'M2' },
|
|
34
|
+
{ id: 'changelog', label: 'Changelog', icon: IconHistory, comingIn: 'M2' },
|
|
35
|
+
{ id: 'install-endpoints', label: 'Install Endpoints', icon: IconPlug, comingIn: 'M4' },
|
|
36
|
+
{ id: 'installed-models', label: 'Installed Models', icon: IconFolders, comingIn: 'M4' },
|
|
37
|
+
]
|
|
38
|
+
|
|
9
39
|
export default function Header({
|
|
10
40
|
searchQuery, onSearchChange,
|
|
11
|
-
|
|
12
|
-
|
|
41
|
+
currentView, onNavigate,
|
|
42
|
+
onToggleTheme, onOpenExport, onOpenCommandPalette,
|
|
43
|
+
onBenchmark, benchmarkRunning, benchmarkTotal, benchmarkCompleted,
|
|
44
|
+
modelsCount, theme, onToast,
|
|
13
45
|
}) {
|
|
46
|
+
const [menuOpen, setMenuOpen] = useState(false)
|
|
47
|
+
const menuRef = useRef(null)
|
|
48
|
+
|
|
49
|
+
// 📖 Close the overflow menu on outside click or Esc.
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!menuOpen) return
|
|
52
|
+
const onClick = (e) => {
|
|
53
|
+
if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false)
|
|
54
|
+
}
|
|
55
|
+
const onKey = (e) => { if (e.key === 'Escape') setMenuOpen(false) }
|
|
56
|
+
document.addEventListener('mousedown', onClick)
|
|
57
|
+
document.addEventListener('keydown', onKey)
|
|
58
|
+
return () => {
|
|
59
|
+
document.removeEventListener('mousedown', onClick)
|
|
60
|
+
document.removeEventListener('keydown', onKey)
|
|
61
|
+
}
|
|
62
|
+
}, [menuOpen])
|
|
63
|
+
|
|
64
|
+
const handleNavClick = (item) => {
|
|
65
|
+
if (item.comingIn) {
|
|
66
|
+
onToast?.(`${item.label} arrives in milestone ${item.comingIn}.`, 'info')
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
onNavigate(item.id)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const handleMenuClick = (item) => {
|
|
73
|
+
setMenuOpen(false)
|
|
74
|
+
if (item.comingIn) {
|
|
75
|
+
onToast?.(`${item.label} arrives in milestone ${item.comingIn}.`, 'info')
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
onNavigate(item.id)
|
|
79
|
+
}
|
|
80
|
+
|
|
14
81
|
return (
|
|
15
82
|
<header className={styles.header}>
|
|
16
83
|
<div className={styles.left}>
|
|
@@ -23,7 +90,61 @@ export default function Header({
|
|
|
23
90
|
</span>
|
|
24
91
|
</div>
|
|
25
92
|
<span className={styles.version}>v{__APP_VERSION__}</span>
|
|
93
|
+
|
|
94
|
+
{/* Always-visible primary nav (replaces the old left sidebar) */}
|
|
95
|
+
<nav className={styles.nav} aria-label="Primary">
|
|
96
|
+
{NAV_ITEMS.map((item) => {
|
|
97
|
+
const Icon = item.icon
|
|
98
|
+
const isActive = currentView === item.id
|
|
99
|
+
return (
|
|
100
|
+
<button
|
|
101
|
+
key={item.id}
|
|
102
|
+
className={`${styles.navBtn} ${isActive ? styles.navBtnActive : ''}`}
|
|
103
|
+
onClick={() => handleNavClick(item)}
|
|
104
|
+
title={item.comingIn ? `${item.label} — coming in ${item.comingIn}` : item.label}
|
|
105
|
+
aria-current={isActive ? 'page' : undefined}
|
|
106
|
+
>
|
|
107
|
+
<Icon size={14} stroke={1.5} />
|
|
108
|
+
<span>{item.label}</span>
|
|
109
|
+
{item.comingIn && <span className={styles.comingBadge}>{item.comingIn}</span>}
|
|
110
|
+
</button>
|
|
111
|
+
)
|
|
112
|
+
})}
|
|
113
|
+
|
|
114
|
+
{/* Overflow menu (kebab) — hidden features & occasional flows */}
|
|
115
|
+
<div className={styles.menuWrap} ref={menuRef}>
|
|
116
|
+
<button
|
|
117
|
+
className={`${styles.navBtn} ${styles.menuTrigger}`}
|
|
118
|
+
onClick={() => setMenuOpen((o) => !o)}
|
|
119
|
+
title="More features"
|
|
120
|
+
aria-haspopup="true"
|
|
121
|
+
aria-expanded={menuOpen}
|
|
122
|
+
>
|
|
123
|
+
<IconDots size={16} stroke={1.5} />
|
|
124
|
+
</button>
|
|
125
|
+
{menuOpen && (
|
|
126
|
+
<div className={styles.menuPopover} role="menu">
|
|
127
|
+
{MENU_ITEMS.map((item) => {
|
|
128
|
+
const Icon = item.icon
|
|
129
|
+
return (
|
|
130
|
+
<button
|
|
131
|
+
key={item.id}
|
|
132
|
+
className={styles.menuItem}
|
|
133
|
+
onClick={() => handleMenuClick(item)}
|
|
134
|
+
role="menuitem"
|
|
135
|
+
>
|
|
136
|
+
<Icon size={14} stroke={1.5} />
|
|
137
|
+
<span>{item.label}</span>
|
|
138
|
+
{item.comingIn && <span className={styles.comingBadge}>{item.comingIn}</span>}
|
|
139
|
+
</button>
|
|
140
|
+
)
|
|
141
|
+
})}
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
</nav>
|
|
26
146
|
</div>
|
|
147
|
+
|
|
27
148
|
<div className={styles.center}>
|
|
28
149
|
<div className={styles.searchBar}>
|
|
29
150
|
<span className={styles.searchIcon}><IconSearch size={16} stroke={1.5} /></span>
|
|
@@ -35,11 +156,21 @@ export default function Header({
|
|
|
35
156
|
onChange={(e) => onSearchChange(e.target.value)}
|
|
36
157
|
autoComplete="off"
|
|
37
158
|
/>
|
|
38
|
-
<kbd className={styles.kbd}>Ctrl+K</kbd>
|
|
39
159
|
</div>
|
|
40
160
|
</div>
|
|
161
|
+
|
|
41
162
|
<div className={styles.right}>
|
|
42
|
-
{/*
|
|
163
|
+
{/* ⌘K — the only global keyboard shortcut, opens the command palette */}
|
|
164
|
+
<button
|
|
165
|
+
className={styles.cmdkBtn}
|
|
166
|
+
onClick={onOpenCommandPalette}
|
|
167
|
+
title="Command palette (⌘K / Ctrl+P)"
|
|
168
|
+
aria-label="Open command palette"
|
|
169
|
+
>
|
|
170
|
+
<IconCommand size={14} stroke={1.5} />
|
|
171
|
+
<span className={styles.cmdkLabel}>⌘K</span>
|
|
172
|
+
</button>
|
|
173
|
+
|
|
43
174
|
<button
|
|
44
175
|
className={`${styles.benchmarkBtn} ${benchmarkRunning ? styles.benchmarkActive : ''}`}
|
|
45
176
|
onClick={onBenchmark}
|
|
@@ -57,16 +188,13 @@ export default function Header({
|
|
|
57
188
|
)}
|
|
58
189
|
</button>
|
|
59
190
|
|
|
60
|
-
<button className={styles.iconBtn} onClick={onToggleTheme} title=
|
|
191
|
+
<button className={styles.iconBtn} onClick={onToggleTheme} title={`Theme: ${theme} (click to cycle auto / dark / light)`}>
|
|
61
192
|
{theme === 'light' ? <IconMoon size={16} stroke={1.5} /> : <IconSun size={16} stroke={1.5} />}
|
|
62
193
|
</button>
|
|
63
194
|
<button className={styles.iconBtn} onClick={onOpenExport} title="Export Data">
|
|
64
195
|
<IconDownload size={16} stroke={1.5} />
|
|
65
196
|
</button>
|
|
66
|
-
<button className={styles.primaryBtn} onClick={onOpenSettings}>
|
|
67
|
-
<IconSettings size={16} stroke={1.5} /> Settings
|
|
68
|
-
</button>
|
|
69
197
|
</div>
|
|
70
198
|
</header>
|
|
71
199
|
)
|
|
72
|
-
}
|
|
200
|
+
}
|