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.
- package/README.md +31 -5
- package/bin/free-coding-models.js +23 -8
- package/changelog/v0.5.4.md +24 -0
- package/changelog/v0.5.5.md +15 -0
- package/package.json +5 -2
- package/sources.js +2 -0
- package/src/core/updater.js +174 -10
- package/src/tui/app.js +11 -31
- package/src/tui/key-handler.js +1 -1
- package/src/tui/render-table.js +9 -3
- package/src/tui/tui-state.js +6 -0
- 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-uD3faN3G.js +39 -0
- package/web/dist/assets/index-uTifVKX1.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 +83 -2
- package/web/src/App.jsx +210 -84
- 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 +67 -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/useSocket.js +3 -0
- 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,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
|
|
10
|
-
gap:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
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:
|
|
76
|
-
border: 1px solid var(--color-
|
|
77
|
-
background: var(--color-
|
|
78
|
-
font-
|
|
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
|
-
.
|
|
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:
|
|
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
|
+
}
|
package/web/src/global.css
CHANGED
|
@@ -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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
}
|