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.
Files changed (122) hide show
  1. package/README.md +31 -5
  2. package/changelog/v0.5.4.md +24 -0
  3. package/package.json +5 -2
  4. package/sources.js +2 -0
  5. package/src/tui/key-handler.js +1 -1
  6. package/web/assets/providers/aistudio/aistudio-text.svg +1 -0
  7. package/web/assets/providers/aistudio/aistudio.svg +1 -0
  8. package/web/assets/providers/cerebras/cerebras-brand-color.svg +1 -0
  9. package/web/assets/providers/cerebras/cerebras-brand.svg +1 -0
  10. package/web/assets/providers/cerebras/cerebras-color.svg +1 -0
  11. package/web/assets/providers/cerebras/cerebras-text.svg +1 -0
  12. package/web/assets/providers/cerebras/cerebras.svg +1 -0
  13. package/web/assets/providers/cloudflare/cloudflare-color.svg +1 -0
  14. package/web/assets/providers/cloudflare/cloudflare-text.svg +1 -0
  15. package/web/assets/providers/cloudflare/cloudflare.svg +1 -0
  16. package/web/assets/providers/gemini/gemini-color.svg +1 -0
  17. package/web/assets/providers/gemini/gemini-text.svg +1 -0
  18. package/web/assets/providers/gemini/gemini.svg +1 -0
  19. package/web/assets/providers/github/github-text.svg +1 -0
  20. package/web/assets/providers/github/github.svg +1 -0
  21. package/web/assets/providers/groq/groq-text.svg +1 -0
  22. package/web/assets/providers/groq/groq.svg +1 -0
  23. package/web/assets/providers/mistral/mistral-color.svg +1 -0
  24. package/web/assets/providers/mistral/mistral-text.svg +1 -0
  25. package/web/assets/providers/mistral/mistral.svg +1 -0
  26. package/web/assets/providers/nvidia/nvidia-color.svg +1 -0
  27. package/web/assets/providers/nvidia/nvidia-text.svg +1 -0
  28. package/web/assets/providers/nvidia/nvidia.svg +1 -0
  29. package/web/assets/providers/opencode/opencode-text.svg +1 -0
  30. package/web/assets/providers/opencode/opencode.svg +1 -0
  31. package/web/assets/providers/openrouter/openrouter (1).svg +1 -0
  32. package/web/assets/providers/openrouter/openrouter-text.svg +1 -0
  33. package/web/assets/providers/openrouter/openrouter.svg +1 -0
  34. package/web/assets/providers/ovhcloud/ovhcloud.svg +12 -0
  35. package/web/assets/providers/qwen/qwen-color.svg +1 -0
  36. package/web/assets/providers/qwen/qwen-text.svg +1 -0
  37. package/web/assets/providers/qwen/qwen.svg +1 -0
  38. package/web/assets/providers/sambanova/sambanova-color.svg +1 -0
  39. package/web/assets/providers/sambanova/sambanova-text.svg +1 -0
  40. package/web/assets/providers/sambanova/sambanova.svg +1 -0
  41. package/web/assets/providers/scalewaylogo/ScalewayLogo.svg +13 -0
  42. package/web/assets/providers/zai/zai-text.svg +1 -0
  43. package/web/assets/providers/zai/zai.svg +1 -0
  44. package/web/dist/assets/index-BoWmUveV.js +39 -0
  45. package/web/dist/assets/index-BrpHevg4.css +1 -0
  46. package/web/dist/favicon.ico +0 -0
  47. package/web/dist/favicons/apple-touch-icon.png +0 -0
  48. package/web/dist/favicons/browserconfig.xml +12 -0
  49. package/web/dist/favicons/favicon-16x16.png +0 -0
  50. package/web/dist/favicons/favicon-192x192.png +0 -0
  51. package/web/dist/favicons/favicon-32x32.png +0 -0
  52. package/web/dist/favicons/favicon-48x48.png +0 -0
  53. package/web/dist/favicons/favicon-512x512.png +0 -0
  54. package/web/dist/favicons/favicon-96x96.png +0 -0
  55. package/web/dist/favicons/favicon.ico +0 -0
  56. package/web/dist/favicons/mstile-150x150.png +0 -0
  57. package/web/dist/favicons/mstile-310x150.png +0 -0
  58. package/web/dist/favicons/mstile-310x310.png +0 -0
  59. package/web/dist/favicons/mstile-512x512.png +0 -0
  60. package/web/dist/favicons/mstile-70x70.png +0 -0
  61. package/web/dist/favicons/site.webmanifest +25 -0
  62. package/web/dist/index.html +42 -3
  63. package/web/index.html +40 -1
  64. package/web/public/favicon.ico +0 -0
  65. package/web/public/favicons/apple-touch-icon.png +0 -0
  66. package/web/public/favicons/browserconfig.xml +12 -0
  67. package/web/public/favicons/favicon-16x16.png +0 -0
  68. package/web/public/favicons/favicon-192x192.png +0 -0
  69. package/web/public/favicons/favicon-32x32.png +0 -0
  70. package/web/public/favicons/favicon-48x48.png +0 -0
  71. package/web/public/favicons/favicon-512x512.png +0 -0
  72. package/web/public/favicons/favicon-96x96.png +0 -0
  73. package/web/public/favicons/favicon.ico +0 -0
  74. package/web/public/favicons/mstile-150x150.png +0 -0
  75. package/web/public/favicons/mstile-310x150.png +0 -0
  76. package/web/public/favicons/mstile-310x310.png +0 -0
  77. package/web/public/favicons/mstile-512x512.png +0 -0
  78. package/web/public/favicons/mstile-70x70.png +0 -0
  79. package/web/public/favicons/site.webmanifest +25 -0
  80. package/web/server.js +71 -1
  81. package/web/src/App.jsx +209 -89
  82. package/web/src/components/analytics/AnalyticsView.jsx +30 -2
  83. package/web/src/components/analytics/AnalyticsView.module.css +4 -0
  84. package/web/src/components/atoms/AILatencyCell.module.css +9 -1
  85. package/web/src/components/atoms/HealthCell.jsx +113 -28
  86. package/web/src/components/atoms/HealthCell.module.css +43 -2
  87. package/web/src/components/atoms/LastPingCell.module.css +10 -0
  88. package/web/src/components/atoms/NoKeyIcon.jsx +65 -0
  89. package/web/src/components/atoms/NoKeyIcon.module.css +30 -0
  90. package/web/src/components/atoms/ProviderLogo.jsx +188 -0
  91. package/web/src/components/atoms/ProviderLogo.module.css +103 -0
  92. package/web/src/components/atoms/Sparkline.jsx +21 -2
  93. package/web/src/components/atoms/StabilityCell.module.css +5 -0
  94. package/web/src/components/atoms/StatusDot.module.css +7 -0
  95. package/web/src/components/atoms/TPSCell.module.css +7 -1
  96. package/web/src/components/atoms/TierBadge.module.css +11 -0
  97. package/web/src/components/atoms/Toast.module.css +6 -0
  98. package/web/src/components/atoms/VerdictBadge.module.css +12 -0
  99. package/web/src/components/dashboard/DetailPanel.jsx +63 -2
  100. package/web/src/components/dashboard/DetailPanel.module.css +130 -0
  101. package/web/src/components/dashboard/FilterBar.jsx +146 -53
  102. package/web/src/components/dashboard/FilterBar.module.css +138 -58
  103. package/web/src/components/dashboard/ModelTable.jsx +235 -30
  104. package/web/src/components/dashboard/ModelTable.module.css +205 -16
  105. package/web/src/components/layout/Header.jsx +140 -12
  106. package/web/src/components/layout/Header.module.css +191 -27
  107. package/web/src/components/palette/CommandPalette.jsx +146 -0
  108. package/web/src/components/palette/CommandPalette.module.css +121 -0
  109. package/web/src/global.css +52 -15
  110. package/web/src/hooks/useColumnSizing.js +211 -0
  111. package/web/src/hooks/useFavorites.js +150 -0
  112. package/web/src/hooks/useFilter.js +157 -24
  113. package/web/src/hooks/useTheme.js +72 -16
  114. package/web/src/hooks/useUrlState.js +93 -0
  115. package/web/dist/assets/index-ByGf4Kq-.js +0 -14
  116. package/web/dist/assets/index-Ds7wmHBv.css +0 -1
  117. package/web/src/components/dashboard/StatsBar.jsx +0 -42
  118. package/web/src/components/dashboard/StatsBar.module.css +0 -28
  119. package/web/src/components/layout/Sidebar.jsx +0 -50
  120. package/web/src/components/layout/Sidebar.module.css +0 -86
  121. package/web/src/components/map/MapView.jsx +0 -17
  122. 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: 1px solid var(--color-border-hover);
8
- border-radius: var(--radius-lg);
9
- margin: 16px;
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: 1px solid var(--color-border-hover);
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: 4px; }
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
- /* ─── Provider pill ─── */
115
- .providerPill {
116
- display: inline-block; font-size: 10px; font-weight: 500; padding: 2px 7px; border-radius: 999px;
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 with search, AI Latency benchmark button, export, settings, and theme toggle.
4
- * 📖 AI Latency button benchmarks the models currently visible after filters/search.
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 { IconBolt, IconSearch, IconDownload, IconSettings, IconMoon, IconSun, IconPlayerPlay } from '@tabler/icons-react'
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
- onToggleTheme, onOpenSettings, onOpenExport,
12
- onBenchmark, benchmarkRunning, benchmarkTotal, benchmarkCompleted, modelsCount, theme,
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
- {/* AI Latency Benchmark always visible, shows model count when idle */}
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="Toggle theme">
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
+ }