free-coding-models 0.5.1 → 0.5.5

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 (129) hide show
  1. package/README.md +31 -5
  2. package/bin/free-coding-models.js +23 -8
  3. package/changelog/v0.5.4.md +24 -0
  4. package/changelog/v0.5.5.md +15 -0
  5. package/package.json +5 -2
  6. package/sources.js +2 -0
  7. package/src/core/updater.js +174 -10
  8. package/src/tui/app.js +11 -31
  9. package/src/tui/key-handler.js +1 -1
  10. package/src/tui/render-table.js +9 -3
  11. package/src/tui/tui-state.js +6 -0
  12. package/web/assets/providers/aistudio/aistudio-text.svg +1 -0
  13. package/web/assets/providers/aistudio/aistudio.svg +1 -0
  14. package/web/assets/providers/cerebras/cerebras-brand-color.svg +1 -0
  15. package/web/assets/providers/cerebras/cerebras-brand.svg +1 -0
  16. package/web/assets/providers/cerebras/cerebras-color.svg +1 -0
  17. package/web/assets/providers/cerebras/cerebras-text.svg +1 -0
  18. package/web/assets/providers/cerebras/cerebras.svg +1 -0
  19. package/web/assets/providers/cloudflare/cloudflare-color.svg +1 -0
  20. package/web/assets/providers/cloudflare/cloudflare-text.svg +1 -0
  21. package/web/assets/providers/cloudflare/cloudflare.svg +1 -0
  22. package/web/assets/providers/gemini/gemini-color.svg +1 -0
  23. package/web/assets/providers/gemini/gemini-text.svg +1 -0
  24. package/web/assets/providers/gemini/gemini.svg +1 -0
  25. package/web/assets/providers/github/github-text.svg +1 -0
  26. package/web/assets/providers/github/github.svg +1 -0
  27. package/web/assets/providers/groq/groq-text.svg +1 -0
  28. package/web/assets/providers/groq/groq.svg +1 -0
  29. package/web/assets/providers/mistral/mistral-color.svg +1 -0
  30. package/web/assets/providers/mistral/mistral-text.svg +1 -0
  31. package/web/assets/providers/mistral/mistral.svg +1 -0
  32. package/web/assets/providers/nvidia/nvidia-color.svg +1 -0
  33. package/web/assets/providers/nvidia/nvidia-text.svg +1 -0
  34. package/web/assets/providers/nvidia/nvidia.svg +1 -0
  35. package/web/assets/providers/opencode/opencode-text.svg +1 -0
  36. package/web/assets/providers/opencode/opencode.svg +1 -0
  37. package/web/assets/providers/openrouter/openrouter (1).svg +1 -0
  38. package/web/assets/providers/openrouter/openrouter-text.svg +1 -0
  39. package/web/assets/providers/openrouter/openrouter.svg +1 -0
  40. package/web/assets/providers/ovhcloud/ovhcloud.svg +12 -0
  41. package/web/assets/providers/qwen/qwen-color.svg +1 -0
  42. package/web/assets/providers/qwen/qwen-text.svg +1 -0
  43. package/web/assets/providers/qwen/qwen.svg +1 -0
  44. package/web/assets/providers/sambanova/sambanova-color.svg +1 -0
  45. package/web/assets/providers/sambanova/sambanova-text.svg +1 -0
  46. package/web/assets/providers/sambanova/sambanova.svg +1 -0
  47. package/web/assets/providers/scalewaylogo/ScalewayLogo.svg +13 -0
  48. package/web/assets/providers/zai/zai-text.svg +1 -0
  49. package/web/assets/providers/zai/zai.svg +1 -0
  50. package/web/dist/assets/index-uD3faN3G.js +39 -0
  51. package/web/dist/assets/index-uTifVKX1.css +1 -0
  52. package/web/dist/favicon.ico +0 -0
  53. package/web/dist/favicons/apple-touch-icon.png +0 -0
  54. package/web/dist/favicons/browserconfig.xml +12 -0
  55. package/web/dist/favicons/favicon-16x16.png +0 -0
  56. package/web/dist/favicons/favicon-192x192.png +0 -0
  57. package/web/dist/favicons/favicon-32x32.png +0 -0
  58. package/web/dist/favicons/favicon-48x48.png +0 -0
  59. package/web/dist/favicons/favicon-512x512.png +0 -0
  60. package/web/dist/favicons/favicon-96x96.png +0 -0
  61. package/web/dist/favicons/favicon.ico +0 -0
  62. package/web/dist/favicons/mstile-150x150.png +0 -0
  63. package/web/dist/favicons/mstile-310x150.png +0 -0
  64. package/web/dist/favicons/mstile-310x310.png +0 -0
  65. package/web/dist/favicons/mstile-512x512.png +0 -0
  66. package/web/dist/favicons/mstile-70x70.png +0 -0
  67. package/web/dist/favicons/site.webmanifest +25 -0
  68. package/web/dist/index.html +42 -3
  69. package/web/index.html +40 -1
  70. package/web/public/favicon.ico +0 -0
  71. package/web/public/favicons/apple-touch-icon.png +0 -0
  72. package/web/public/favicons/browserconfig.xml +12 -0
  73. package/web/public/favicons/favicon-16x16.png +0 -0
  74. package/web/public/favicons/favicon-192x192.png +0 -0
  75. package/web/public/favicons/favicon-32x32.png +0 -0
  76. package/web/public/favicons/favicon-48x48.png +0 -0
  77. package/web/public/favicons/favicon-512x512.png +0 -0
  78. package/web/public/favicons/favicon-96x96.png +0 -0
  79. package/web/public/favicons/favicon.ico +0 -0
  80. package/web/public/favicons/mstile-150x150.png +0 -0
  81. package/web/public/favicons/mstile-310x150.png +0 -0
  82. package/web/public/favicons/mstile-310x310.png +0 -0
  83. package/web/public/favicons/mstile-512x512.png +0 -0
  84. package/web/public/favicons/mstile-70x70.png +0 -0
  85. package/web/public/favicons/site.webmanifest +25 -0
  86. package/web/server.js +83 -2
  87. package/web/src/App.jsx +210 -84
  88. package/web/src/components/analytics/AnalyticsView.jsx +30 -2
  89. package/web/src/components/analytics/AnalyticsView.module.css +4 -0
  90. package/web/src/components/atoms/AILatencyCell.module.css +9 -1
  91. package/web/src/components/atoms/HealthCell.jsx +113 -28
  92. package/web/src/components/atoms/HealthCell.module.css +43 -2
  93. package/web/src/components/atoms/LastPingCell.module.css +10 -0
  94. package/web/src/components/atoms/NoKeyIcon.jsx +65 -0
  95. package/web/src/components/atoms/NoKeyIcon.module.css +30 -0
  96. package/web/src/components/atoms/ProviderLogo.jsx +188 -0
  97. package/web/src/components/atoms/ProviderLogo.module.css +103 -0
  98. package/web/src/components/atoms/Sparkline.jsx +21 -2
  99. package/web/src/components/atoms/StabilityCell.module.css +5 -0
  100. package/web/src/components/atoms/StatusDot.module.css +7 -0
  101. package/web/src/components/atoms/TPSCell.module.css +7 -1
  102. package/web/src/components/atoms/TierBadge.module.css +11 -0
  103. package/web/src/components/atoms/Toast.module.css +6 -0
  104. package/web/src/components/atoms/VerdictBadge.module.css +12 -0
  105. package/web/src/components/dashboard/DetailPanel.jsx +63 -2
  106. package/web/src/components/dashboard/DetailPanel.module.css +130 -0
  107. package/web/src/components/dashboard/FilterBar.jsx +146 -53
  108. package/web/src/components/dashboard/FilterBar.module.css +138 -58
  109. package/web/src/components/dashboard/ModelTable.jsx +235 -30
  110. package/web/src/components/dashboard/ModelTable.module.css +205 -16
  111. package/web/src/components/layout/Header.jsx +140 -12
  112. package/web/src/components/layout/Header.module.css +191 -27
  113. package/web/src/components/palette/CommandPalette.jsx +146 -0
  114. package/web/src/components/palette/CommandPalette.module.css +121 -0
  115. package/web/src/global.css +67 -15
  116. package/web/src/hooks/useColumnSizing.js +211 -0
  117. package/web/src/hooks/useFavorites.js +150 -0
  118. package/web/src/hooks/useFilter.js +157 -24
  119. package/web/src/hooks/useSocket.js +3 -0
  120. package/web/src/hooks/useTheme.js +72 -16
  121. package/web/src/hooks/useUrlState.js +93 -0
  122. package/web/dist/assets/index-ByGf4Kq-.js +0 -14
  123. package/web/dist/assets/index-Ds7wmHBv.css +0 -1
  124. package/web/src/components/dashboard/StatsBar.jsx +0 -42
  125. package/web/src/components/dashboard/StatsBar.module.css +0 -28
  126. package/web/src/components/layout/Sidebar.jsx +0 -50
  127. package/web/src/components/layout/Sidebar.module.css +0 -86
  128. package/web/src/components/map/MapView.jsx +0 -17
  129. package/web/src/components/map/MapView.module.css +0 -25
@@ -1,50 +1,179 @@
1
+ /**
2
+ * @file web/src/components/layout/Header.module.css
3
+ * @description Layout refactor (M1): the left sidebar is gone, so the header
4
+ * is now the only navigation rail. It hosts the brand mark, primary nav
5
+ * buttons, search, and right-side actions (⌘K, AI Latency, theme, export).
6
+ *
7
+ * 📖 The model table underneath is 100% of the viewport width — the header
8
+ * uses the same width, and nav items can collapse into a kebab at narrow widths.
9
+ */
10
+
1
11
  .header {
2
12
  position: sticky;
3
13
  top: 0;
4
14
  z-index: 100;
5
- height: 60px;
15
+ height: var(--header-h, 60px);
6
16
  display: flex;
7
17
  align-items: center;
8
18
  justify-content: space-between;
9
- padding: 0 24px;
10
- gap: 20px;
19
+ padding: 0 20px;
20
+ gap: 16px;
11
21
  background: var(--color-bg-card);
12
22
  backdrop-filter: blur(20px) saturate(1.5);
13
23
  -webkit-backdrop-filter: blur(20px) saturate(1.5);
14
24
  border-bottom: 1px solid var(--color-border);
25
+ width: 100%;
26
+ }
27
+
28
+ .left {
29
+ display: flex;
30
+ align-items: center;
31
+ gap: 14px;
32
+ flex-shrink: 0;
33
+ }
34
+
35
+ .center {
36
+ flex: 1;
37
+ max-width: 480px;
38
+ min-width: 0;
39
+ }
40
+
41
+ .right {
42
+ display: flex;
43
+ align-items: center;
44
+ gap: 8px;
45
+ flex-shrink: 0;
15
46
  }
16
- .left { display: flex; align-items: center; gap: 12px; }
17
- .center { flex: 1; max-width: 480px; }
18
- .right { display: flex; align-items: center; gap: 10px; }
19
47
 
20
- .logo { display: flex; align-items: center; gap: 8px; }
48
+ /* ─── Brand ─── */
49
+ .logo { display: flex; align-items: center; gap: 6px; }
21
50
  .logoIcon {
22
51
  color: var(--color-brand);
23
52
  font-family: var(--font-mono);
24
- font-size: 22px;
53
+ font-size: 20px;
25
54
  font-weight: 900;
26
55
  display: flex;
27
56
  align-items: center;
28
57
  }
29
58
  .logoText {
30
- font-size: 16px; font-weight: 700;
59
+ font-size: 14px; font-weight: 700;
31
60
  color: var(--color-text);
32
61
  display: flex;
33
62
  align-items: center;
34
63
  }
35
- .logoTextHighlight {
36
- color: var(--color-brand);
37
- }
64
+ .logoTextHighlight { color: var(--color-brand); }
38
65
  .version {
39
- font-size: 11px; font-weight: 500; font-family: var(--font-mono);
66
+ font-size: 10px; font-weight: 500; font-family: var(--font-mono);
40
67
  color: var(--color-text-dim); background: var(--color-accent-dim);
41
- padding: 2px 8px; border-radius: 999px; border: 1px solid var(--color-border);
68
+ padding: 2px 6px; border-radius: 999px; border: 1px solid var(--color-border);
69
+ }
70
+
71
+ /* ─── Primary nav (replaces the sidebar) ─── */
72
+ .nav {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 2px;
76
+ margin-left: 8px;
77
+ padding-left: 12px;
78
+ border-left: 1px solid var(--color-border);
79
+ height: 36px;
80
+ }
81
+
82
+ .navBtn {
83
+ display: inline-flex;
84
+ align-items: center;
85
+ gap: 5px;
86
+ height: 30px;
87
+ padding: 0 10px;
88
+ font-size: 12px;
89
+ font-weight: 600;
90
+ font-family: var(--font-sans);
91
+ color: var(--color-text-muted);
92
+ background: transparent;
93
+ border: 1px solid transparent;
94
+ border-radius: 6px;
95
+ cursor: pointer;
96
+ transition: background 150ms, color 150ms, border-color 150ms;
97
+ white-space: nowrap;
98
+ }
99
+ .navBtn:hover {
100
+ background: var(--color-bg-hover);
101
+ color: var(--color-text);
102
+ }
103
+ .navBtnActive {
104
+ background: var(--color-accent-dim);
105
+ color: var(--color-accent);
106
+ border-color: var(--color-border);
107
+ font-weight: 700;
108
+ }
109
+ .navBtnActive:hover {
110
+ background: var(--color-accent-dim);
111
+ color: var(--color-accent);
42
112
  }
43
113
 
114
+ /* 📖 "Coming in M2" badge — small inline label that shows the menu structure
115
+ 📖 is honest even before those milestones land. */
116
+ .comingBadge {
117
+ font-size: 9px;
118
+ font-weight: 700;
119
+ font-family: var(--font-mono);
120
+ color: var(--color-text-dim);
121
+ background: var(--color-surface);
122
+ border: 1px solid var(--color-border);
123
+ padding: 1px 4px;
124
+ border-radius: 3px;
125
+ margin-left: 2px;
126
+ line-height: 1;
127
+ text-transform: uppercase;
128
+ letter-spacing: 0.3px;
129
+ }
130
+
131
+ /* ─── Overflow menu (kebab) ─── */
132
+ .menuWrap { position: relative; }
133
+ .menuTrigger {
134
+ width: 30px;
135
+ padding: 0;
136
+ justify-content: center;
137
+ }
138
+ .menuPopover {
139
+ position: absolute;
140
+ top: calc(100% + 6px);
141
+ right: 0;
142
+ min-width: 200px;
143
+ background: var(--color-bg-elevated);
144
+ border: 1px solid var(--color-border);
145
+ border-radius: 8px;
146
+ padding: 4px;
147
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.32);
148
+ z-index: 200;
149
+ display: flex;
150
+ flex-direction: column;
151
+ gap: 1px;
152
+ }
153
+ .menuItem {
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 8px;
157
+ width: 100%;
158
+ padding: 7px 10px;
159
+ font-size: 12px;
160
+ font-weight: 500;
161
+ color: var(--color-text);
162
+ background: transparent;
163
+ border: none;
164
+ border-radius: 5px;
165
+ cursor: pointer;
166
+ text-align: left;
167
+ transition: background 100ms;
168
+ }
169
+ .menuItem:hover { background: var(--color-bg-hover); }
170
+ .menuItem > span:first-of-type { flex: 1; }
171
+
172
+ /* ─── Search ─── */
44
173
  .searchBar {
45
174
  display: flex; align-items: center; gap: 8px;
46
175
  background: var(--color-surface); border: 1px solid var(--color-border);
47
- border-radius: 10px; padding: 0 12px; height: 36px;
176
+ border-radius: 8px; padding: 0 12px; height: 34px;
48
177
  transition: border-color 150ms, box-shadow 150ms;
49
178
  }
50
179
  .searchBar:focus-within {
@@ -55,14 +184,11 @@
55
184
  .searchInput {
56
185
  flex: 1; background: none; border: none; outline: none;
57
186
  color: var(--color-text); font-size: 13px; font-family: var(--font-sans);
187
+ min-width: 0;
58
188
  }
59
189
  .searchInput::placeholder { color: var(--color-text-dim); }
60
- .kbd {
61
- font-size: 10px; font-family: var(--font-mono); color: var(--color-text-dim);
62
- background: var(--color-bg); border: 1px solid var(--color-border);
63
- padding: 2px 6px; border-radius: 4px; flex-shrink: 0;
64
- }
65
190
 
191
+ /* ─── Right-side buttons ─── */
66
192
  .iconBtn {
67
193
  display: inline-flex; align-items: center; justify-content: center;
68
194
  padding: 6px 8px; border: 1px solid var(--color-border); border-radius: 6px;
@@ -70,18 +196,28 @@
70
196
  cursor: pointer; font-size: 16px; transition: all 150ms;
71
197
  }
72
198
  .iconBtn:hover { background: var(--color-bg-hover); border-color: var(--color-border-hover); }
73
- .primaryBtn {
199
+
200
+ /* 📖 ⌘K button — the only global keyboard shortcut in the Web UI.
201
+ 📖 Tinted accent so users can spot it instantly. */
202
+ .cmdkBtn {
74
203
  display: inline-flex; align-items: center; gap: 6px;
75
- padding: 6px 14px; font-size: 13px; font-weight: 600;
76
- border: 1px solid var(--color-accent); border-radius: 6px;
77
- background: var(--color-accent); color: var(--color-bg); cursor: pointer;
78
- font-family: var(--font-sans); transition: all 150ms;
204
+ padding: 0 10px; height: 30px;
205
+ border: 1px solid var(--color-border); border-radius: 6px;
206
+ background: var(--color-surface); color: var(--color-text);
207
+ cursor: pointer; font-size: 12px; font-weight: 600;
208
+ font-family: var(--font-mono);
209
+ transition: all 150ms;
210
+ }
211
+ .cmdkBtn:hover {
212
+ border-color: var(--color-accent);
213
+ background: var(--color-bg-hover);
79
214
  }
80
- .primaryBtn:hover { background: var(--color-accent-hover); }
215
+ .cmdkLabel { font-size: 11px; letter-spacing: 0.5px; }
81
216
 
217
+ /* ─── AI Latency benchmark button (reused from before) ─── */
82
218
  .benchmarkBtn {
83
219
  display: inline-flex; align-items: center; gap: 6px;
84
- padding: 6px 14px; font-size: 12px; font-weight: 700;
220
+ padding: 0 12px; height: 30px; font-size: 12px; font-weight: 700;
85
221
  border: 1px solid var(--color-border); border-radius: 6px;
86
222
  background: var(--color-surface); color: var(--color-text);
87
223
  cursor: pointer; font-family: var(--font-mono); transition: all 150ms;
@@ -102,6 +238,34 @@
102
238
  border-radius: 50%; animation: spin 0.8s linear infinite; flex-shrink: 0;
103
239
  }
104
240
 
241
+ /* 📖 Light theme: deep green for the benchmark button outline/hover and
242
+ 📖 the spinner ring so they stay visible on the white header bar. */
243
+ :global([data-theme="light"]) .benchmarkBtn:hover {
244
+ border-color: #008f4d; color: #008f4d; background: rgba(0, 143, 77, 0.06);
245
+ }
246
+ :global([data-theme="light"]) .benchmarkActive {
247
+ border-color: #008f4d !important; color: #008f4d !important;
248
+ background: rgba(0, 143, 77, 0.10) !important;
249
+ }
250
+ :global([data-theme="light"]) .spinner {
251
+ border-color: rgba(0, 0, 0, 0.1);
252
+ border-top-color: #008f4d;
253
+ }
254
+
255
+ /* 📖 Responsive — at narrow widths the nav collapses into the kebab menu.
256
+ 📖 We never reintroduce a left sidebar. The table keeps 100% width. */
257
+ @media (max-width: 1024px) {
258
+ .navBtn > span:not(.comingBadge) { display: none; }
259
+ .navBtn { padding: 0 8px; }
260
+ .center { max-width: 320px; }
261
+ }
105
262
  @media (max-width: 768px) {
106
263
  .center { display: none; }
264
+ .logoText { display: none; }
265
+ .cmdkLabel { display: none; }
266
+ .benchmarkBtn > span:not(.benchmarkRunning) { display: none; }
267
+ }
268
+ @media (max-width: 560px) {
269
+ .version { display: none; }
270
+ .navBtn { padding: 0 6px; }
107
271
  }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * @file web/src/components/palette/CommandPalette.jsx
3
+ * @description ⌘K / Ctrl+P command palette — M1 placeholder, M2 ships the full version.
4
+ * 📖 M1 keeps the modal opening, the ⌘K wiring, and a small launcher that exposes
5
+ * 📖 the most-used actions (cycle theme, reset view, navigate views, change ping mode).
6
+ * 📖 M2 will replace this with the TUI-aligned palette that consumes
7
+ * 📖 `src/tui/command-palette.js` via `getCommandPaletteSnapshot()` for full parity.
8
+ *
9
+ * @functions
10
+ * → CommandPalette — main modal component
11
+ */
12
+ import { useEffect, useMemo, useRef, useState } from 'react'
13
+ import { IconSearch, IconCommand, IconBolt, IconArrowRight, IconArrowsExchange } from '@tabler/icons-react'
14
+ import styles from './CommandPalette.module.css'
15
+
16
+ const PING_MODE_CYCLE = ['speed', 'normal', 'slow', 'forced']
17
+
18
+ export default function CommandPalette({
19
+ onClose, onNavigate, onCycleTheme, onResetView,
20
+ onSetPingMode, onToast, onExport,
21
+ currentView, theme, pingMode,
22
+ }) {
23
+ const [query, setQuery] = useState('')
24
+ const [cursor, setCursor] = useState(0)
25
+ const inputRef = useRef(null)
26
+ const listRef = useRef(null)
27
+
28
+ // 📖 Static command list for M1 — these are the universal "no matter what view
29
+ // 📖 I'm in, this is what I want" actions. M2 will replace this with the TUI
30
+ // 📖 registry import.
31
+ const commands = useMemo(() => {
32
+ const items = [
33
+ { id: 'view.dashboard', label: 'Go to Dashboard', icon: IconArrowRight, run: () => onNavigate('dashboard') },
34
+ { id: 'view.settings', label: 'Go to Settings', icon: IconArrowRight, run: () => onNavigate('settings') },
35
+ { id: 'view.analytics', label: 'Go to Analytics', icon: IconArrowRight, run: () => onNavigate('analytics') },
36
+ { id: 'view.recommend', label: 'Open Smart Recommend (coming in M3)', icon: IconArrowRight, disabled: true },
37
+ { id: 'view.router', label: 'Open Router Dashboard (coming in M4)', icon: IconArrowRight, disabled: true },
38
+ { id: 'view.help', label: 'Open Help (coming in M2)', icon: IconArrowRight, disabled: true },
39
+ { id: 'view.changelog', label: 'Open Changelog (coming in M2)', icon: IconArrowRight, disabled: true },
40
+ { id: 'action.cycle-theme', label: `Cycle theme (current: ${theme})`, icon: IconArrowsExchange, run: onCycleTheme },
41
+ { id: 'action.reset-view', label: 'Reset view (filters + sort)', icon: IconArrowsExchange, run: onResetView },
42
+ { id: 'action.ping.speed', label: 'Ping mode → Speed (2s)', icon: IconBolt, run: () => onSetPingMode('speed') },
43
+ { id: 'action.ping.normal', label: 'Ping mode → Normal (10s)', icon: IconBolt, run: () => onSetPingMode('normal') },
44
+ { id: 'action.ping.slow', label: 'Ping mode → Slow (30s)', icon: IconBolt, run: () => onSetPingMode('slow') },
45
+ { id: 'action.ping.forced', label: 'Ping mode → Forced (4s)', icon: IconBolt, run: () => onSetPingMode('forced') },
46
+ { id: 'action.export', label: 'Export models…', icon: IconArrowRight, run: onExport },
47
+ ]
48
+ return items
49
+ }, [onNavigate, onCycleTheme, onResetView, onSetPingMode, onExport, theme])
50
+
51
+ // 📖 Fuzzy filter: substring match (case-insensitive). Good enough for M1's small set.
52
+ const filtered = useMemo(() => {
53
+ const q = query.trim().toLowerCase()
54
+ if (!q) return commands
55
+ return commands.filter((cmd) => cmd.label.toLowerCase().includes(q))
56
+ }, [commands, query])
57
+
58
+ // 📖 Focus the search input on open.
59
+ useEffect(() => {
60
+ inputRef.current?.focus()
61
+ }, [])
62
+
63
+ // 📖 Keyboard handler — Esc, arrows, Enter.
64
+ useEffect(() => {
65
+ const onKey = (e) => {
66
+ if (e.key === 'Escape') { e.preventDefault(); onClose(); return }
67
+ if (e.key === 'ArrowDown') {
68
+ e.preventDefault()
69
+ setCursor((c) => Math.min(filtered.length - 1, c + 1))
70
+ return
71
+ }
72
+ if (e.key === 'ArrowUp') {
73
+ e.preventDefault()
74
+ setCursor((c) => Math.max(0, c - 1))
75
+ return
76
+ }
77
+ if (e.key === 'Enter') {
78
+ e.preventDefault()
79
+ const item = filtered[cursor]
80
+ if (item && !item.disabled) {
81
+ item.run?.()
82
+ onClose()
83
+ } else if (item?.disabled) {
84
+ onToast?.(`${item.label} is not available yet.`, 'info')
85
+ }
86
+ }
87
+ }
88
+ document.addEventListener('keydown', onKey)
89
+ return () => document.removeEventListener('keydown', onKey)
90
+ }, [filtered, cursor, onClose, onToast])
91
+
92
+ // 📖 Reset cursor when query changes.
93
+ useEffect(() => { setCursor(0) }, [query])
94
+
95
+ return (
96
+ <div className={styles.backdrop} onClick={onClose}>
97
+ <div className={styles.modal} onClick={(e) => e.stopPropagation()}>
98
+ <div className={styles.searchRow}>
99
+ <IconSearch size={16} stroke={1.5} className={styles.searchIcon} />
100
+ <input
101
+ ref={inputRef}
102
+ className={styles.input}
103
+ placeholder="Type a command…"
104
+ value={query}
105
+ onChange={(e) => setQuery(e.target.value)}
106
+ autoComplete="off"
107
+ />
108
+ <span className={styles.kbd}><IconCommand size={10} stroke={1.5} />K</span>
109
+ </div>
110
+
111
+ <ul className={styles.list} ref={listRef} role="listbox">
112
+ {filtered.length === 0 && (
113
+ <li className={styles.empty}>No matching command.</li>
114
+ )}
115
+ {filtered.map((cmd, idx) => {
116
+ const Icon = cmd.icon
117
+ return (
118
+ <li
119
+ key={cmd.id}
120
+ className={`${styles.item} ${idx === cursor ? styles.itemActive : ''} ${cmd.disabled ? styles.itemDisabled : ''}`}
121
+ onClick={() => {
122
+ if (cmd.disabled) { onToast?.(`${cmd.label} is not available yet.`, 'info'); return }
123
+ cmd.run?.()
124
+ onClose()
125
+ }}
126
+ onMouseEnter={() => setCursor(idx)}
127
+ role="option"
128
+ aria-selected={idx === cursor}
129
+ >
130
+ <Icon size={14} stroke={1.5} className={styles.itemIcon} />
131
+ <span className={styles.itemLabel}>{cmd.label}</span>
132
+ {idx === cursor && <span className={styles.itemEnter}>↵</span>}
133
+ </li>
134
+ )
135
+ })}
136
+ </ul>
137
+
138
+ <div className={styles.footer}>
139
+ <span><kbd>↑↓</kbd> navigate</span>
140
+ <span><kbd>↵</kbd> select</span>
141
+ <span><kbd>Esc</kbd> close</span>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ )
146
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * @file web/src/components/palette/CommandPalette.module.css
3
+ * @description Styles for the ⌘K command palette modal — M1 placeholder version.
4
+ */
5
+
6
+ .backdrop {
7
+ position: fixed;
8
+ inset: 0;
9
+ background: rgba(0, 0, 0, 0.55);
10
+ backdrop-filter: blur(4px);
11
+ -webkit-backdrop-filter: blur(4px);
12
+ z-index: 500;
13
+ display: flex;
14
+ align-items: flex-start;
15
+ justify-content: center;
16
+ padding-top: 12vh;
17
+ }
18
+
19
+ .modal {
20
+ width: min(560px, 92vw);
21
+ background: var(--color-bg-elevated);
22
+ border: 1px solid var(--color-border-hover);
23
+ border-radius: 14px;
24
+ box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
25
+ display: flex;
26
+ flex-direction: column;
27
+ max-height: 70vh;
28
+ overflow: hidden;
29
+ }
30
+
31
+ .searchRow {
32
+ display: flex;
33
+ align-items: center;
34
+ gap: 10px;
35
+ padding: 14px 16px;
36
+ border-bottom: 1px solid var(--color-border);
37
+ }
38
+ .searchIcon { color: var(--color-text-dim); flex-shrink: 0; }
39
+ .input {
40
+ flex: 1;
41
+ background: none;
42
+ border: none;
43
+ outline: none;
44
+ color: var(--color-text);
45
+ font-size: 15px;
46
+ font-family: var(--font-sans);
47
+ }
48
+ .input::placeholder { color: var(--color-text-dim); }
49
+ .kbd {
50
+ font-size: 10px;
51
+ font-family: var(--font-mono);
52
+ color: var(--color-text-dim);
53
+ background: var(--color-surface);
54
+ border: 1px solid var(--color-border);
55
+ padding: 2px 6px;
56
+ border-radius: 4px;
57
+ display: inline-flex;
58
+ align-items: center;
59
+ gap: 2px;
60
+ }
61
+
62
+ .list {
63
+ list-style: none;
64
+ margin: 0;
65
+ padding: 6px;
66
+ overflow-y: auto;
67
+ flex: 1;
68
+ }
69
+
70
+ .item {
71
+ display: flex;
72
+ align-items: center;
73
+ gap: 10px;
74
+ padding: 9px 12px;
75
+ border-radius: 7px;
76
+ cursor: pointer;
77
+ font-size: 13px;
78
+ color: var(--color-text);
79
+ transition: background 80ms;
80
+ user-select: none;
81
+ }
82
+ .itemActive { background: var(--color-accent-dim); }
83
+ .itemDisabled {
84
+ cursor: not-allowed;
85
+ opacity: 0.5;
86
+ }
87
+ .itemDisabled:hover { background: transparent; }
88
+ .itemIcon { color: var(--color-text-muted); flex-shrink: 0; }
89
+ .itemLabel { flex: 1; }
90
+ .itemEnter {
91
+ font-size: 11px;
92
+ color: var(--color-text-dim);
93
+ font-family: var(--font-mono);
94
+ }
95
+
96
+ .empty {
97
+ padding: 20px;
98
+ text-align: center;
99
+ color: var(--color-text-dim);
100
+ font-size: 13px;
101
+ }
102
+
103
+ .footer {
104
+ display: flex;
105
+ gap: 16px;
106
+ padding: 8px 16px;
107
+ border-top: 1px solid var(--color-border);
108
+ font-size: 11px;
109
+ color: var(--color-text-dim);
110
+ background: var(--color-bg-card);
111
+ }
112
+ .footer kbd {
113
+ display: inline-block;
114
+ font-family: var(--font-mono);
115
+ background: var(--color-surface);
116
+ border: 1px solid var(--color-border);
117
+ padding: 1px 5px;
118
+ border-radius: 3px;
119
+ margin-right: 3px;
120
+ font-size: 10px;
121
+ }
@@ -59,8 +59,6 @@
59
59
  --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
60
60
 
61
61
  --header-h: 60px;
62
- --sidebar-w: 64px;
63
- --sidebar-w-expanded: 200px;
64
62
  --radius-sm: 4px;
65
63
  --radius-md: 6px;
66
64
  --radius-lg: 8px;
@@ -94,10 +92,42 @@
94
92
  --color-accent-hover: #333333;
95
93
  --color-accent-glow: rgba(0, 0, 0, 0.05);
96
94
  --color-accent-dim: rgba(0, 0, 0, 0.02);
97
- --color-danger-dim: rgba(255, 68, 68, 0.08);
98
- --color-warning-dim: rgba(255, 170, 0, 0.08);
99
- --color-success-dim: rgba(0, 255, 136, 0.08);
100
- --color-info-dim: rgba(6, 182, 212, 0.08);
95
+
96
+ /* 📖 Bright neon colors that look great on black become unreadable on white.
97
+ 📖 We swap them for darker, AA-contrast equivalents in light mode. The
98
+ 📖 hex values are picked to stay visually similar to the dark variant
99
+ 📖 (still "green" / "cyan" / "gold") while passing WCAG AA on #fff. */
100
+ --color-success: #008f4d; /* was #00ff88 (neon) */
101
+ --color-warning: #b86b00; /* was #ffaa00 (neon amber) */
102
+ --color-danger: #c8143a; /* was #ff4444 (neon red) */
103
+ --color-info: #0e7490; /* was #06b6d4 (neon cyan) */
104
+ --color-danger-dim: rgba(200, 20, 58, 0.08);
105
+ --color-warning-dim: rgba(184, 107, 0, 0.08);
106
+ --color-success-dim: rgba(0, 143, 77, 0.08);
107
+ --color-info-dim: rgba(14, 116, 144, 0.08);
108
+
109
+ /* 📖 Tier + verdict palette: shift toward darker, more saturated variants
110
+ 📖 so badges stay legible on white. The original "neon" feel is dropped
111
+ 📖 in favor of "saturated-but-deep" colors that pass contrast. */
112
+ --tier-splus: #b08800; /* was #ffd700 (gold) */
113
+ --tier-s: #c45a00; /* was #ff8c00 (dark orange) */
114
+ --tier-aplus: #0073a8; /* was #00c8ff (neon cyan) */
115
+ --tier-a: #1f7a3e; /* was #3ddc84 (mid green) */
116
+ --tier-aminus: #2f7a2f; /* was #7ecf7e (light green) */
117
+ --tier-bplus: #5a5a8a; /* was #a8a8c8 (lavender) */
118
+ --tier-b: #555566; /* was #808098 (gray-blue) */
119
+ --tier-c: #6a6a7a; /* was #606078 (slate) */
120
+
121
+ --verdict-perfect: #008f4d; /* was #00ff88 */
122
+ --verdict-normal: #4a7a00; /* was #76b900 */
123
+ --verdict-slow: #b86b00; /* was #ffaa00 */
124
+ --verdict-spiky: #c45000; /* was #ff6600 */
125
+ --verdict-veryslow: #c8143a; /* was #ff4444 */
126
+ --verdict-overloaded: #b8101f; /* was #ff2222 */
127
+ --verdict-unstable: #990000; /* was #cc0000 */
128
+ --verdict-notactive: #6a6a7a; /* was #555570 */
129
+ --verdict-pending: #6a6a8a; /* was #444460 */
130
+
101
131
  --shadow-sm: none;
102
132
  --shadow-md: none;
103
133
  --shadow-lg: none;
@@ -121,6 +151,7 @@ body {
121
151
  overflow-x: hidden;
122
152
  position: relative;
123
153
  display: flex;
154
+ flex-direction: column;
124
155
  }
125
156
 
126
157
  a { color: var(--color-accent); text-decoration: none; }
@@ -145,21 +176,45 @@ code { font-family: var(--font-mono); font-size: 12px; background: var(--color-s
145
176
  }
146
177
 
147
178
  /* ─── App Layout ─── */
179
+ .app-shell {
180
+ flex: 1;
181
+ display: flex;
182
+ flex-direction: column;
183
+ min-height: 100vh;
184
+ width: 100%;
185
+ position: relative;
186
+ z-index: 1;
187
+ }
188
+
148
189
  .app-content {
149
190
  flex: 1;
150
- margin-left: var(--sidebar-w);
151
191
  position: relative;
152
192
  z-index: 1;
153
- min-height: 100vh;
193
+ min-height: calc(100vh - var(--header-h));
154
194
  display: flex;
155
195
  flex-direction: column;
156
- transition: margin-left var(--transition-medium);
157
196
  }
158
197
 
159
198
  .view {
160
199
  display: flex;
161
200
  flex-direction: column;
162
201
  flex: 1;
202
+ min-height: 0;
203
+ }
204
+
205
+ .update-warning-banner {
206
+ position: sticky;
207
+ top: var(--header-h, 60px);
208
+ z-index: 90;
209
+ padding: 10px 18px;
210
+ background: var(--color-danger);
211
+ color: #fff;
212
+ border-bottom: 1px solid rgba(255, 255, 255, 0.22);
213
+ font-weight: 800;
214
+ font-size: 13px;
215
+ letter-spacing: 0.01em;
216
+ text-align: center;
217
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22);
163
218
  }
164
219
 
165
220
  /* ─── Scrollbar ─── */
@@ -171,26 +226,23 @@ code { font-family: var(--font-mono); font-size: 12px; background: var(--color-s
171
226
 
172
227
  /* ─── Responsive ─── */
173
228
  @media (max-width: 768px) {
174
- .app-content { margin-left: 0; }
229
+ /* 📖 No sidebar to collapse on small viewports — the header itself collapses
230
+ 📖 (see Header.module.css). The model table keeps its 100% width. */
175
231
  }
176
232
 
177
233
  /* ─── Full-page horizontal/vertical scroll layout ─── */
178
234
  .app-content {
179
235
  flex: 1;
180
- margin-left: var(--sidebar-w);
181
236
  position: relative;
182
237
  z-index: 1;
183
- min-height: 100vh;
238
+ min-height: calc(100vh - var(--header-h));
184
239
  display: flex;
185
240
  flex-direction: column;
186
- overflow: auto; /* allow scrolling in both directions */
187
- transition: margin-left var(--transition-medium);
188
241
  }
189
242
 
190
243
  .view {
191
244
  display: flex;
192
245
  flex-direction: column;
193
246
  flex: 1;
194
- overflow: hidden; /* let inner components scroll */
195
247
  min-height: 0;
196
248
  }