free-coding-models 0.5.0 → 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.
Files changed (145) hide show
  1. package/README.md +40 -6
  2. package/bin/free-coding-models.js +10 -0
  3. package/changelog/v0.5.1.md +24 -0
  4. package/changelog/v0.5.4.md +24 -0
  5. package/package.json +11 -3
  6. package/sources.js +2 -0
  7. package/src/core/router-daemon.js +166 -1
  8. package/src/core/utils.js +2 -0
  9. package/src/tui/cli-help.js +2 -0
  10. package/src/tui/key-handler.js +1 -1
  11. package/src/tui/render-table.js +1 -1
  12. package/web/README.md +8 -5
  13. package/web/assets/providers/aistudio/aistudio-text.svg +1 -0
  14. package/web/assets/providers/aistudio/aistudio.svg +1 -0
  15. package/web/assets/providers/cerebras/cerebras-brand-color.svg +1 -0
  16. package/web/assets/providers/cerebras/cerebras-brand.svg +1 -0
  17. package/web/assets/providers/cerebras/cerebras-color.svg +1 -0
  18. package/web/assets/providers/cerebras/cerebras-text.svg +1 -0
  19. package/web/assets/providers/cerebras/cerebras.svg +1 -0
  20. package/web/assets/providers/cloudflare/cloudflare-color.svg +1 -0
  21. package/web/assets/providers/cloudflare/cloudflare-text.svg +1 -0
  22. package/web/assets/providers/cloudflare/cloudflare.svg +1 -0
  23. package/web/assets/providers/gemini/gemini-color.svg +1 -0
  24. package/web/assets/providers/gemini/gemini-text.svg +1 -0
  25. package/web/assets/providers/gemini/gemini.svg +1 -0
  26. package/web/assets/providers/github/github-text.svg +1 -0
  27. package/web/assets/providers/github/github.svg +1 -0
  28. package/web/assets/providers/groq/groq-text.svg +1 -0
  29. package/web/assets/providers/groq/groq.svg +1 -0
  30. package/web/assets/providers/mistral/mistral-color.svg +1 -0
  31. package/web/assets/providers/mistral/mistral-text.svg +1 -0
  32. package/web/assets/providers/mistral/mistral.svg +1 -0
  33. package/web/assets/providers/nvidia/nvidia-color.svg +1 -0
  34. package/web/assets/providers/nvidia/nvidia-text.svg +1 -0
  35. package/web/assets/providers/nvidia/nvidia.svg +1 -0
  36. package/web/assets/providers/opencode/opencode-text.svg +1 -0
  37. package/web/assets/providers/opencode/opencode.svg +1 -0
  38. package/web/assets/providers/openrouter/openrouter (1).svg +1 -0
  39. package/web/assets/providers/openrouter/openrouter-text.svg +1 -0
  40. package/web/assets/providers/openrouter/openrouter.svg +1 -0
  41. package/web/assets/providers/ovhcloud/ovhcloud.svg +12 -0
  42. package/web/assets/providers/qwen/qwen-color.svg +1 -0
  43. package/web/assets/providers/qwen/qwen-text.svg +1 -0
  44. package/web/assets/providers/qwen/qwen.svg +1 -0
  45. package/web/assets/providers/sambanova/sambanova-color.svg +1 -0
  46. package/web/assets/providers/sambanova/sambanova-text.svg +1 -0
  47. package/web/assets/providers/sambanova/sambanova.svg +1 -0
  48. package/web/assets/providers/scalewaylogo/ScalewayLogo.svg +13 -0
  49. package/web/assets/providers/zai/zai-text.svg +1 -0
  50. package/web/assets/providers/zai/zai.svg +1 -0
  51. package/web/dist/assets/index-BoWmUveV.js +39 -0
  52. package/web/dist/assets/index-BrpHevg4.css +1 -0
  53. package/web/dist/favicon.ico +0 -0
  54. package/web/dist/favicons/apple-touch-icon.png +0 -0
  55. package/web/dist/favicons/browserconfig.xml +12 -0
  56. package/web/dist/favicons/favicon-16x16.png +0 -0
  57. package/web/dist/favicons/favicon-192x192.png +0 -0
  58. package/web/dist/favicons/favicon-32x32.png +0 -0
  59. package/web/dist/favicons/favicon-48x48.png +0 -0
  60. package/web/dist/favicons/favicon-512x512.png +0 -0
  61. package/web/dist/favicons/favicon-96x96.png +0 -0
  62. package/web/dist/favicons/favicon.ico +0 -0
  63. package/web/dist/favicons/mstile-150x150.png +0 -0
  64. package/web/dist/favicons/mstile-310x150.png +0 -0
  65. package/web/dist/favicons/mstile-310x310.png +0 -0
  66. package/web/dist/favicons/mstile-512x512.png +0 -0
  67. package/web/dist/favicons/mstile-70x70.png +0 -0
  68. package/web/dist/favicons/site.webmanifest +25 -0
  69. package/web/dist/index.html +42 -6
  70. package/web/index.html +40 -4
  71. package/web/package.json +11 -0
  72. package/web/public/favicon.ico +0 -0
  73. package/web/public/favicons/apple-touch-icon.png +0 -0
  74. package/web/public/favicons/browserconfig.xml +12 -0
  75. package/web/public/favicons/favicon-16x16.png +0 -0
  76. package/web/public/favicons/favicon-192x192.png +0 -0
  77. package/web/public/favicons/favicon-32x32.png +0 -0
  78. package/web/public/favicons/favicon-48x48.png +0 -0
  79. package/web/public/favicons/favicon-512x512.png +0 -0
  80. package/web/public/favicons/favicon-96x96.png +0 -0
  81. package/web/public/favicons/favicon.ico +0 -0
  82. package/web/public/favicons/mstile-150x150.png +0 -0
  83. package/web/public/favicons/mstile-310x150.png +0 -0
  84. package/web/public/favicons/mstile-310x310.png +0 -0
  85. package/web/public/favicons/mstile-512x512.png +0 -0
  86. package/web/public/favicons/mstile-70x70.png +0 -0
  87. package/web/public/favicons/site.webmanifest +25 -0
  88. package/web/server.js +676 -211
  89. package/web/src/App.jsx +233 -71
  90. package/web/src/components/analytics/AnalyticsView.jsx +40 -6
  91. package/web/src/components/analytics/AnalyticsView.module.css +4 -0
  92. package/web/src/components/atoms/AILatencyCell.jsx +38 -0
  93. package/web/src/components/atoms/AILatencyCell.module.css +51 -0
  94. package/web/src/components/atoms/HealthCell.jsx +138 -0
  95. package/web/src/components/atoms/HealthCell.module.css +56 -0
  96. package/web/src/components/atoms/LastPingCell.jsx +35 -0
  97. package/web/src/components/atoms/LastPingCell.module.css +45 -0
  98. package/web/src/components/atoms/MoodCell.jsx +25 -0
  99. package/web/src/components/atoms/MoodCell.module.css +6 -0
  100. package/web/src/components/atoms/NoKeyIcon.jsx +65 -0
  101. package/web/src/components/atoms/NoKeyIcon.module.css +30 -0
  102. package/web/src/components/atoms/ProviderLogo.jsx +188 -0
  103. package/web/src/components/atoms/ProviderLogo.module.css +103 -0
  104. package/web/src/components/atoms/RankCell.jsx +9 -0
  105. package/web/src/components/atoms/RankCell.module.css +9 -0
  106. package/web/src/components/atoms/Sparkline.jsx +21 -2
  107. package/web/src/components/atoms/StabilityCell.module.css +5 -0
  108. package/web/src/components/atoms/StatusDot.module.css +7 -0
  109. package/web/src/components/atoms/TPSCell.jsx +36 -0
  110. package/web/src/components/atoms/TPSCell.module.css +44 -0
  111. package/web/src/components/atoms/TierBadge.module.css +11 -0
  112. package/web/src/components/atoms/Toast.module.css +6 -0
  113. package/web/src/components/atoms/VerdictBadge.jsx +30 -7
  114. package/web/src/components/atoms/VerdictBadge.module.css +36 -15
  115. package/web/src/components/dashboard/DetailPanel.jsx +63 -2
  116. package/web/src/components/dashboard/DetailPanel.module.css +130 -0
  117. package/web/src/components/dashboard/ExportModal.jsx +9 -4
  118. package/web/src/components/dashboard/FilterBar.jsx +228 -33
  119. package/web/src/components/dashboard/FilterBar.module.css +187 -22
  120. package/web/src/components/dashboard/ModelTable.jsx +500 -54
  121. package/web/src/components/dashboard/ModelTable.module.css +318 -31
  122. package/web/src/components/layout/Footer.jsx +1 -1
  123. package/web/src/components/layout/Header.jsx +171 -9
  124. package/web/src/components/layout/Header.module.css +223 -25
  125. package/web/src/components/palette/CommandPalette.jsx +146 -0
  126. package/web/src/components/palette/CommandPalette.module.css +121 -0
  127. package/web/src/components/settings/SettingsView.jsx +24 -6
  128. package/web/src/components/settings/SettingsView.module.css +0 -1
  129. package/web/src/global.css +118 -84
  130. package/web/src/hooks/useColumnSizing.js +211 -0
  131. package/web/src/hooks/useFavorites.js +150 -0
  132. package/web/src/hooks/useFilter.js +262 -37
  133. package/web/src/hooks/useSSE.js +33 -9
  134. package/web/src/hooks/useSocket.js +200 -0
  135. package/web/src/hooks/useTheme.js +72 -16
  136. package/web/src/hooks/useUrlState.js +93 -0
  137. package/web/vite.config.js +41 -0
  138. package/web/dist/assets/index-CGN-0_A0.css +0 -1
  139. package/web/dist/assets/index-CvMUM9Jr.js +0 -11
  140. package/web/src/components/dashboard/StatsBar.jsx +0 -40
  141. package/web/src/components/dashboard/StatsBar.module.css +0 -28
  142. package/web/src/components/layout/Sidebar.jsx +0 -42
  143. package/web/src/components/layout/Sidebar.module.css +0 -76
  144. package/web/src/components/map/MapView.jsx +0 -17
  145. package/web/src/components/map/MapView.module.css +0 -25
@@ -1,46 +1,333 @@
1
+ /**
2
+ * @file web/src/components/dashboard/ModelTable.module.css
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.
7
+ */
1
8
  .container {
2
9
  background: var(--color-bg-card);
3
- backdrop-filter: blur(12px) saturate(1.5);
4
- -webkit-backdrop-filter: blur(12px) saturate(1.5);
5
- border: 1px solid var(--color-border);
6
- border-radius: 16px;
7
- overflow: hidden;
8
- margin: 16px 0;
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;
16
+ flex: 1;
17
+ min-height: 0;
18
+ overflow: auto;
19
+ display: flex;
20
+ flex-direction: column;
21
+ }
22
+
23
+ .table {
24
+ width: 100%;
25
+ min-width: 1200px;
26
+ border-collapse: collapse;
27
+ font-size: 12px;
28
+ border: 0;
9
29
  }
10
- .table { width: 100%; border-collapse: collapse; font-size: 13px; }
30
+
31
+ /* ─── Header ─── */
11
32
  .th {
12
- padding: 12px 14px; font-size: 11px; font-weight: 700; text-transform: uppercase;
13
- letter-spacing: 0.8px; color: var(--color-text-muted); background: var(--color-bg-elevated);
14
- border-bottom: 1px solid var(--color-border); white-space: nowrap; user-select: none; cursor: default;
33
+ padding: 9px 8px;
34
+ font-size: 10px;
35
+ font-weight: 700;
36
+ text-transform: uppercase;
37
+ letter-spacing: 0.5px;
38
+ color: var(--color-text-muted);
39
+ background: var(--color-bg-elevated);
40
+ border-bottom: 1px solid var(--color-border-hover);
41
+ border-right: 1px solid var(--color-border);
42
+ white-space: nowrap;
43
+ user-select: none;
44
+ position: sticky;
45
+ top: 0;
46
+ z-index: 10;
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;
53
+ }
54
+ .th:last-child { border-right: 0; }
55
+ .th:hover { color: var(--color-accent); background: var(--color-bg-active); }
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
+
173
+ /* Center certain columns */
174
+ .th:nth-child(6) { text-align: left; } /* Model */
175
+ .th:nth-child(7) { text-align: left; } /* Provider */
176
+ .th:nth-child(11) { text-align: left; } /* Verdict */
177
+
178
+ /* Sort icons */
179
+ .sortIcon { opacity: 0.3; margin-left: 3px; font-size: 9px; }
180
+ .sortIconActive { color: var(--color-accent); margin-left: 3px; font-size: 10px; font-weight: 800; }
181
+
182
+ /* ─── Data cells ─── */
183
+ .td {
184
+ padding: 7px 8px;
185
+ border-bottom: 1px solid var(--color-border);
186
+ border-right: 1px solid var(--color-border);
187
+ white-space: nowrap;
188
+ }
189
+ .td:last-child { border-right: 0; }
190
+ .td:nth-child(6) { text-align: left; } /* Model */
191
+ .td:nth-child(7) { text-align: left; } /* Provider */
192
+ .td:nth-child(11) { text-align: left; } /* Verdict */
193
+
194
+ /* ─── Row hover ─── */
195
+ .table tbody tr {
196
+ cursor: pointer;
197
+ transition: background 100ms;
15
198
  }
16
- .th:hover { color: var(--color-accent); }
17
- .table td { padding: 10px 14px; border-bottom: 1px solid var(--color-border); white-space: nowrap; }
18
- .table tbody tr { cursor: pointer; transition: background 150ms; }
19
199
  .table tbody tr:hover { background: var(--color-bg-hover); }
20
- .tdRank { text-align: center; font-family: var(--font-mono); font-weight: 600; color: var(--color-text-dim); }
200
+
201
+ /* ─── Medal rows ─── */
21
202
  .rank1 td:first-child { border-left: 3px solid #ffd700; }
22
203
  .rank2 td:first-child { border-left: 3px solid #c0c0c0; }
23
204
  .rank3 td:first-child { border-left: 3px solid #cd7f32; }
24
- .modelCell { display: flex; flex-direction: column; }
25
- .modelName { font-weight: 600; font-size: 13px; display: flex; align-items: center; gap: 4px; }
26
- .modelId { font-family: var(--font-mono); font-size: 10px; color: var(--color-text-dim); margin-top: 1px; }
27
- .noKey {
28
- font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 3px;
29
- background: rgba(255,170,0,0.12); color: #ffaa00; border: 1px solid rgba(255,170,0,0.2); margin-left: 6px;
30
- }
31
- .providerPill {
32
- display: inline-block; font-size: 11px; font-weight: 500; padding: 2px 8px; border-radius: 999px;
33
- background: var(--color-accent-dim); color: var(--color-text-muted); border: 1px solid var(--color-border);
205
+
206
+ /* ─── Shared cell values ─── */
207
+ .rankNum {
208
+ font-family: var(--font-mono);
209
+ font-size: 11px;
210
+ color: var(--color-text-dim);
211
+ text-align: center;
212
+ display: block;
34
213
  }
214
+
35
215
  .swe { font-family: var(--font-mono); font-weight: 600; }
36
216
  .sweHigh { color: #ffd700; }
37
- .sweMid { color: #3ddc84; }
38
- .sweLow { color: var(--color-text-dim); }
39
- .ctx { font-family: var(--font-mono); font-size: 12px; color: var(--color-text-muted); }
217
+ .sweMid { color: #3ddc84; }
218
+ .sweLow { color: var(--color-text-dim); }
219
+
220
+ .ctx { font-family: var(--font-mono); font-size: 11px; }
221
+
40
222
  .ping { font-family: var(--font-mono); font-weight: 600; }
41
- .pingFast { color: #00ff88; }
223
+ .pingFast { color: #00ff88; }
42
224
  .pingMedium { color: #ffaa00; }
43
- .pingSlow { color: #ff4444; }
44
- .pingNone { color: var(--color-text-dim); }
45
- .uptime { font-family: var(--font-mono); font-size: 12px; }
225
+ .pingSlow { color: #ff4444; }
226
+ .pingNone { color: var(--color-text-dim); }
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
+
235
+ .uptime { font-family: var(--font-mono); font-size: 11px; }
236
+
237
+ /* ─── Model cell ─── */
238
+ .modelCell { display: flex; align-items: center; gap: 6px; overflow: hidden; }
239
+ .modelMeta { flex: 1; min-width: 0; }
240
+ .modelHeader { display: flex; align-items: center; gap: 5px; }
241
+ .modelName { font-weight: 600; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
242
+ .modelId { font-family: var(--font-mono); font-size: 9px; color: var(--color-text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
243
+
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. */
247
+
248
+ /* ─── Benchmarking row highlight ── */
249
+ @keyframes benchRowPulse {
250
+ 0%, 100% { background: rgba(180, 0, 255, 0.04); }
251
+ 50% { background: rgba(180, 0, 255, 0.10); }
252
+ }
253
+ .benchRow {
254
+ animation: benchRowPulse 1.2s ease-in-out infinite;
255
+ }
256
+
257
+ /* ─── Empty state ─── */
258
+ :global([data-theme="light"]) .container {
259
+ border-color: #cfcfcf;
260
+ }
261
+
262
+ :global([data-theme="light"]) .table {
263
+ border-color: #cfcfcf;
264
+ }
265
+
266
+ :global([data-theme="light"]) .th,
267
+ :global([data-theme="light"]) .td {
268
+ border-color: #d8d8d8;
269
+ }
270
+
46
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
+ }
@@ -8,7 +8,7 @@ export default function Footer() {
8
8
  return (
9
9
  <footer className={styles.footer}>
10
10
  <div className={styles.left}>
11
- Made with ❤️ by <a href="https://vavanessa.dev" target="_blank" rel="noopener">Vava-Nessa</a>
11
+ by <a href="https://vavanessa.dev" target="_blank" rel="noopener">Vava-Nessa</a>
12
12
  </div>
13
13
  <div className={styles.right}>
14
14
  <a href="https://github.com/vava-nessa/free-coding-models" target="_blank" rel="noopener">GitHub</a>
@@ -1,22 +1,153 @@
1
1
  /**
2
2
  * @file web/src/components/layout/Header.jsx
3
- * @description Top header bar with search, export button, settings shortcut, and theme toggle.
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.
4
10
  */
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'
5
18
  import styles from './Header.module.css'
6
19
 
7
- export default function Header({ searchQuery, onSearchChange, onToggleTheme, onOpenSettings, onOpenExport }) {
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
+
39
+ export default function Header({
40
+ searchQuery, onSearchChange,
41
+ currentView, onNavigate,
42
+ onToggleTheme, onOpenExport, onOpenCommandPalette,
43
+ onBenchmark, benchmarkRunning, benchmarkTotal, benchmarkCompleted,
44
+ modelsCount, theme, onToast,
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
+
8
81
  return (
9
82
  <header className={styles.header}>
10
83
  <div className={styles.left}>
11
84
  <div className={styles.logo}>
12
- <span className={styles.logoIcon}>⚡</span>
13
- <span className={styles.logoText}>free-coding-models</span>
85
+ <span className={styles.logoIcon}>&gt;</span>
86
+ <span className={styles.logoText}>
87
+ <span className={styles.logoTextHighlight}>free</span>
88
+ <span>-coding-models</span>
89
+ <span className={styles.logoTextHighlight}>_</span>
90
+ </span>
14
91
  </div>
15
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>
16
146
  </div>
147
+
17
148
  <div className={styles.center}>
18
149
  <div className={styles.searchBar}>
19
- <span className={styles.searchIcon}>🔍</span>
150
+ <span className={styles.searchIcon}><IconSearch size={16} stroke={1.5} /></span>
20
151
  <input
21
152
  type="text"
22
153
  className={styles.searchInput}
@@ -25,13 +156,44 @@ export default function Header({ searchQuery, onSearchChange, onToggleTheme, onO
25
156
  onChange={(e) => onSearchChange(e.target.value)}
26
157
  autoComplete="off"
27
158
  />
28
- <kbd className={styles.kbd}>Ctrl+K</kbd>
29
159
  </div>
30
160
  </div>
161
+
31
162
  <div className={styles.right}>
32
- <button className={styles.iconBtn} onClick={onToggleTheme} title="Toggle theme">☽</button>
33
- <button className={styles.iconBtn} onClick={onOpenExport} title="Export Data">↓</button>
34
- <button className={styles.primaryBtn} onClick={onOpenSettings}>⚙ Settings</button>
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
+
174
+ <button
175
+ className={`${styles.benchmarkBtn} ${benchmarkRunning ? styles.benchmarkActive : ''}`}
176
+ onClick={onBenchmark}
177
+ disabled={benchmarkRunning}
178
+ title={benchmarkRunning ? `AI Speed Test running — ${benchmarkCompleted}/${benchmarkTotal}` : `Run AI Latency benchmark on ${modelsCount} visible models`}
179
+ >
180
+ <IconPlayerPlay size={14} stroke={1.5} />
181
+ {benchmarkRunning ? (
182
+ <span className={styles.benchmarkRunning}>
183
+ <span className={styles.spinner} />
184
+ RUN {benchmarkCompleted}/{benchmarkTotal}
185
+ </span>
186
+ ) : (
187
+ <span>AI Latency</span>
188
+ )}
189
+ </button>
190
+
191
+ <button className={styles.iconBtn} onClick={onToggleTheme} title={`Theme: ${theme} (click to cycle auto / dark / light)`}>
192
+ {theme === 'light' ? <IconMoon size={16} stroke={1.5} /> : <IconSun size={16} stroke={1.5} />}
193
+ </button>
194
+ <button className={styles.iconBtn} onClick={onOpenExport} title="Export Data">
195
+ <IconDownload size={16} stroke={1.5} />
196
+ </button>
35
197
  </div>
36
198
  </header>
37
199
  )