free-coding-models 0.5.31 → 0.5.34

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 (37) hide show
  1. package/LICENSE +43 -21
  2. package/README.md +2 -1
  3. package/bin/free-coding-models.js +22 -2
  4. package/changelog/v0.5.32.md +4 -0
  5. package/changelog/v0.5.33.md +4 -0
  6. package/changelog/v0.5.34.md +4 -0
  7. package/package.json +3 -2
  8. package/sources.js +12 -52
  9. package/src/core/router-daemon.js +64 -50
  10. package/src/core/schema-normalizer.js +172 -0
  11. package/src/core/tool-bootstrap.js +11 -0
  12. package/src/core/tool-launchers.js +37 -2
  13. package/src/core/tool-metadata.js +3 -0
  14. package/web/dist/assets/index-r8niexgJ.css +1 -0
  15. package/web/dist/assets/index-zzUvtumw.js +40 -0
  16. package/web/dist/index.html +2 -2
  17. package/web/src/App.jsx +93 -105
  18. package/web/src/components/changelog/ChangelogView.module.css +13 -14
  19. package/web/src/components/dashboard/FilterBar.jsx +17 -2
  20. package/web/src/components/dashboard/FilterBar.module.css +68 -21
  21. package/web/src/components/dashboard/ModelTable.jsx +118 -42
  22. package/web/src/components/dashboard/ModelTable.module.css +82 -27
  23. package/web/src/components/help/HelpView.module.css +13 -14
  24. package/web/src/components/install/InstallEndpointsView.module.css +13 -13
  25. package/web/src/components/installed/InstalledModelsView.module.css +13 -13
  26. package/web/src/components/layout/Header.jsx +28 -28
  27. package/web/src/components/layout/Header.module.css +56 -30
  28. package/web/src/components/palette/CommandPalette.jsx +14 -14
  29. package/web/src/components/playground/PlaygroundView.module.css +13 -14
  30. package/web/src/components/recommend/RecommendView.module.css +13 -11
  31. package/web/src/components/router/RouterView.module.css +13 -13
  32. package/web/src/components/tools/ToolPicker.module.css +27 -14
  33. package/web/src/global.css +58 -31
  34. package/web/src/hooks/urlState.constants.js +1 -1
  35. package/web/src/hooks/useUrlState.js +19 -3
  36. package/web/dist/assets/index-CsFt3qt5.css +0 -1
  37. package/web/dist/assets/index-D2n0DqkV.js +0 -40
@@ -46,24 +46,24 @@
46
46
  }
47
47
 
48
48
  /* ─── Brand ─── */
49
- .logo { display: flex; align-items: center; gap: 6px; }
50
- .logoIcon {
49
+ .logo { display: flex; align-items: center; gap: 8px; }
50
+ .logoIconSvg {
51
51
  color: var(--color-brand);
52
- font-family: var(--font-mono);
53
- font-size: 20px;
54
- font-weight: 900;
55
52
  display: flex;
56
53
  align-items: center;
54
+ flex-shrink: 0;
57
55
  }
58
56
  .logoText {
59
- font-size: 14px; font-weight: 700;
57
+ font-size: 16px; font-weight: 750;
58
+ font-family: var(--font-mono);
60
59
  color: var(--color-text);
61
60
  display: flex;
62
61
  align-items: center;
62
+ letter-spacing: -0.2px;
63
63
  }
64
64
  .logoTextHighlight { color: var(--color-brand); }
65
65
  .version {
66
- font-size: 10px; font-weight: 500; font-family: var(--font-mono);
66
+ font-size: 12px; font-weight: 500; font-family: var(--font-mono);
67
67
  color: var(--color-text-muted); background: var(--color-surface);
68
68
  padding: 2px 6px; border-radius: 999px; border: 1px solid var(--color-border);
69
69
  }
@@ -72,7 +72,7 @@
72
72
  .nav {
73
73
  display: flex;
74
74
  align-items: center;
75
- gap: 2px;
75
+ gap: 8px;
76
76
  margin-left: 8px;
77
77
  padding-left: 12px;
78
78
  border-left: 1px solid var(--color-border);
@@ -82,39 +82,44 @@
82
82
  .navBtn {
83
83
  display: inline-flex;
84
84
  align-items: center;
85
- gap: 5px;
85
+ gap: 6px;
86
86
  height: 30px;
87
- padding: 0 10px;
88
- font-size: 12px;
87
+ padding: 0 12px;
88
+ font-size: 13px;
89
89
  font-weight: 600;
90
- font-family: var(--font-sans);
90
+ font-family: var(--font-mono); /* Monospace font looks extremely high-tech */
91
+ text-transform: uppercase;
92
+ letter-spacing: 0.5px;
91
93
  color: var(--color-text-muted);
92
- background: transparent;
93
- border: 1px solid transparent;
94
+ background: var(--color-surface);
95
+ border: 1px solid var(--color-border);
94
96
  border-radius: 6px;
95
97
  cursor: pointer;
96
- transition: background 150ms, color 150ms, border-color 150ms;
98
+ transition: all 150ms var(--ease-out);
97
99
  white-space: nowrap;
98
100
  }
99
101
  .navBtn:hover {
100
102
  background: var(--color-bg-hover);
101
103
  color: var(--color-text);
104
+ border-color: var(--color-border-hover);
102
105
  }
103
106
  .navBtnActive {
104
- background: var(--color-accent-dim);
105
- color: var(--color-accent);
106
- border-color: var(--color-border);
107
- font-weight: 700;
107
+ background: var(--color-accent);
108
+ color: var(--color-bg);
109
+ border-color: var(--color-accent);
110
+ font-weight: 800;
111
+ box-shadow: 0 0 12px var(--color-accent-glow);
108
112
  }
109
113
  .navBtnActive:hover {
110
- background: var(--color-accent-dim);
111
- color: var(--color-accent);
114
+ background: var(--color-accent-hover);
115
+ color: var(--color-bg);
116
+ border-color: var(--color-accent-hover);
112
117
  }
113
118
 
114
119
  /* 📖 "Coming in M2" badge — small inline label that shows the menu structure
115
120
  📖 is honest even before those milestones land. */
116
121
  .comingBadge {
117
- font-size: 9px;
122
+ font-size: 10px;
118
123
  font-weight: 700;
119
124
  font-family: var(--font-mono);
120
125
  color: var(--color-text-dim);
@@ -156,7 +161,7 @@
156
161
  gap: 8px;
157
162
  width: 100%;
158
163
  padding: 7px 10px;
159
- font-size: 12px;
164
+ font-size: 14px;
160
165
  font-weight: 500;
161
166
  color: var(--color-text);
162
167
  background: transparent;
@@ -167,6 +172,11 @@
167
172
  transition: background 100ms;
168
173
  }
169
174
  .menuItem:hover { background: var(--color-bg-hover); }
175
+ .menuItemActive {
176
+ background: var(--color-accent-dim) !important;
177
+ color: var(--color-accent) !important;
178
+ font-weight: 700;
179
+ }
170
180
  .menuItem > span:first-of-type { flex: 1; }
171
181
 
172
182
  /* ─── Search ─── */
@@ -180,10 +190,10 @@
180
190
  border-color: var(--color-accent);
181
191
  box-shadow: 0 0 0 3px var(--color-accent-glow);
182
192
  }
183
- .searchIcon { font-size: 14px; flex-shrink: 0; }
193
+ .searchIcon { font-size: 16px; flex-shrink: 0; }
184
194
  .searchInput {
185
195
  flex: 1; background: none; border: none; outline: none;
186
- color: var(--color-text); font-size: 13px; font-family: var(--font-sans);
196
+ color: var(--color-text); font-size: 15px; font-family: var(--font-sans);
187
197
  min-width: 0;
188
198
  }
189
199
  .searchInput::placeholder { color: var(--color-text-dim); }
@@ -193,7 +203,7 @@
193
203
  display: inline-flex; align-items: center; justify-content: center;
194
204
  padding: 6px 8px; border: 1px solid var(--color-border); border-radius: 6px;
195
205
  background: var(--color-surface); color: var(--color-text);
196
- cursor: pointer; font-size: 16px; transition: all 150ms;
206
+ cursor: pointer; font-size: 18px; transition: all 150ms;
197
207
  }
198
208
  .iconBtn:hover { background: var(--color-bg-hover); border-color: var(--color-border-hover); }
199
209
 
@@ -204,7 +214,7 @@
204
214
  padding: 0 10px; height: 30px;
205
215
  border: 1px solid var(--color-border); border-radius: 6px;
206
216
  background: var(--color-surface); color: var(--color-text);
207
- cursor: pointer; font-size: 12px; font-weight: 600;
217
+ cursor: pointer; font-size: 14px; font-weight: 600;
208
218
  font-family: var(--font-mono);
209
219
  transition: all 150ms;
210
220
  }
@@ -212,12 +222,12 @@
212
222
  border-color: var(--color-accent);
213
223
  background: var(--color-bg-hover);
214
224
  }
215
- .cmdkLabel { font-size: 11px; letter-spacing: 0.5px; }
225
+ .cmdkLabel { font-size: 13px; letter-spacing: 0.5px; }
216
226
 
217
227
  /* ─── AI Latency benchmark button (reused from before) ─── */
218
228
  .benchmarkBtn {
219
229
  display: inline-flex; align-items: center; gap: 6px;
220
- padding: 0 12px; height: 30px; font-size: 12px; font-weight: 700;
230
+ padding: 0 12px; height: 30px; font-size: 14px; font-weight: 700;
221
231
  border: 1px solid var(--color-border); border-radius: 6px;
222
232
  background: var(--color-surface); color: var(--color-text);
223
233
  cursor: pointer; font-family: var(--font-mono); transition: all 150ms;
@@ -307,7 +317,7 @@
307
317
  align-items: center;
308
318
  gap: 10px;
309
319
  padding: 10px 12px;
310
- font-size: 13px;
320
+ font-size: 15px;
311
321
  font-weight: 600;
312
322
  color: var(--color-text-muted);
313
323
  background: transparent;
@@ -329,3 +339,19 @@
329
339
  background: var(--color-border);
330
340
  margin: 6px 0;
331
341
  }
342
+
343
+ /* ─── Keyboard Focus & Micro-interaction Active States ─── */
344
+ .navBtn:focus-visible,
345
+ .iconBtn:focus-visible,
346
+ .cmdkBtn:focus-visible,
347
+ .benchmarkBtn:focus-visible {
348
+ outline: 2px solid var(--color-accent);
349
+ outline-offset: 2px;
350
+ }
351
+
352
+ .navBtn:active,
353
+ .iconBtn:active,
354
+ .cmdkBtn:active,
355
+ .benchmarkBtn:active {
356
+ transform: scale(0.96);
357
+ }
@@ -37,14 +37,14 @@ const PING_MODE_CYCLE = ['speed', 'normal', 'slow', 'forced']
37
37
  // 📖 has a few commands the TUI doesn't). They are appended to the TUI registry
38
38
  // 📖 after a `Web` separator so the two surfaces stay easy to compare.
39
39
  function buildWebEntries(deps) {
40
- const { onCycleTheme, onResetView, onSetPingMode, onOpenChangelog, theme, pingMode, modelsCount, onExport } = deps
40
+ const { onCycleTheme, onResetView, onSetPingMode, theme, pingMode, modelsCount, onExport, onNavigate } = deps
41
41
  const web = [
42
- // Pages (open as modals)
43
- { id: 'page.help', section: 'page', label: 'Open Help', keywords: ['help', 'shortcuts', 'reference'], run: () => deps.onOpenHelp?.() },
44
- { id: 'page.changelog', section: 'page', label: 'Open Changelog', keywords: ['changelog', 'release', 'history', 'version'], run: () => onOpenChangelog?.() },
45
- { id: 'page.playground', section: 'page', label: 'Open Playground', keywords: ['playground', 'chat', 'try', 'prompt', 'router'], run: () => deps.onOpenPlayground?.() },
46
- { id: 'page.install-endpoints', section: 'page', label: 'Open Install Endpoints (M4)', keywords: ['install', 'endpoint', 'tool', 'configure'], disabled: true },
47
- { id: 'page.installed-models', section: 'page', label: 'Open Installed Models (M4)', keywords: ['installed', 'models', 'tools'], disabled: true },
42
+ // Pages
43
+ { id: 'page.help', section: 'page', label: 'Open Help', keywords: ['help', 'shortcuts', 'reference'], run: () => onNavigate?.('help') },
44
+ { id: 'page.changelog', section: 'page', label: 'Open Changelog', keywords: ['changelog', 'release', 'history', 'version'], run: () => onNavigate?.('changelog') },
45
+ { id: 'page.playground', section: 'page', label: 'Open Playground', keywords: ['playground', 'chat', 'try', 'prompt', 'router'], run: () => onNavigate?.('playground') },
46
+ { id: 'page.install-endpoints', section: 'page', label: 'Open Install Endpoints', keywords: ['install', 'endpoint', 'tool', 'configure'], run: () => onNavigate?.('install-endpoints') },
47
+ { id: 'page.installed-models', section: 'page', label: 'Open Installed Models', keywords: ['installed', 'models', 'tools'], run: () => onNavigate?.('installed-models') },
48
48
 
49
49
  // Theme
50
50
  { id: 'action.theme.cycle', section: 'action', label: `Cycle theme (current: ${theme})`, keywords: ['theme', 'dark', 'light', 'auto'], run: onCycleTheme },
@@ -92,7 +92,7 @@ export default function CommandPalette({
92
92
  })
93
93
  const webEntries = buildWebEntries({
94
94
  onCycleTheme, onResetView, onSetPingMode,
95
- onOpenChangelog, onOpenHelp, theme, pingMode, modelsCount: models?.length ?? 0, onExport,
95
+ theme, pingMode, modelsCount: models?.length ?? 0, onExport, onNavigate,
96
96
  })
97
97
  // 📖 Update banner entry (only when an update is available) — mirrors
98
98
  // 📖 the TUI palette's "auto-prepended when newer version known" rule.
@@ -177,12 +177,12 @@ export default function CommandPalette({
177
177
  // 📖 ships in M3), we route the user to the right header menu.
178
178
  const id = item.id
179
179
  if (id === 'open-settings') { onNavigate?.('settings'); onClose(); return }
180
- if (id === 'open-help') { onOpenHelp?.(); onClose(); return }
181
- if (id === 'open-changelog') { onOpenChangelog?.(); onClose(); return }
182
- if (id === 'open-recommend') { onToast?.('Recommend arrives in M3', 'info'); onClose(); return }
183
- if (id === 'open-router-dashboard') { onToast?.('Router dashboard arrives in M4', 'info'); onClose(); return }
184
- if (id === 'open-installed-models') { onToast?.('Installed Models arrives in M4', 'info'); onClose(); return }
185
- if (id === 'open-install-endpoints') { onToast?.('Install Endpoints arrives in M4', 'info'); onClose(); return }
180
+ if (id === 'open-help') { onNavigate?.('help'); onClose(); return }
181
+ if (id === 'open-changelog') { onNavigate?.('changelog'); onClose(); return }
182
+ if (id === 'open-recommend') { onNavigate?.('recommend'); onClose(); return }
183
+ if (id === 'open-router-dashboard') { onNavigate?.('router'); onClose(); return }
184
+ if (id === 'open-installed-models') { onNavigate?.('installed-models'); onClose(); return }
185
+ if (id === 'open-install-endpoints') { onNavigate?.('install-endpoints'); onClose(); return }
186
186
  if (id === 'action-update-now') { onRunUpdate?.(); onClose(); return }
187
187
  if (id === 'action-cycle-theme') { onCycleTheme?.(); onClose(); return }
188
188
  if (id === 'action-reset-view') { onResetView?.(); onClose(); return }
@@ -5,27 +5,26 @@
5
5
  */
6
6
 
7
7
  .overlay {
8
- position: fixed;
9
- inset: 0;
10
- z-index: 200;
11
- background: rgba(0, 0, 0, 0.6);
12
8
  display: flex;
13
- align-items: center;
14
- justify-content: center;
15
- animation: fadeIn 0.15s ease;
9
+ flex-direction: column;
10
+ flex: 1;
11
+ min-height: 0;
12
+ width: 100%;
13
+ padding: 24px;
16
14
  }
17
15
 
18
16
  .modal {
19
- background: var(--bg-primary, #0f0f17);
20
- border: 1px solid var(--border, #1e1e2e);
17
+ background: var(--color-bg-card, #080908);
18
+ border: 1px solid var(--color-border, #161a16);
21
19
  border-radius: 12px;
22
- width: 880px;
23
- max-width: 96vw;
24
- max-height: 90vh;
20
+ width: 100%;
21
+ max-width: 1000px;
22
+ margin: 0 auto;
23
+ flex: 1;
25
24
  display: flex;
26
25
  flex-direction: column;
27
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
28
- animation: slideUp 0.18s ease;
26
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
27
+ min-height: 0;
29
28
  }
30
29
 
31
30
  @keyframes fadeIn {
@@ -1,24 +1,26 @@
1
1
  /** @file web/src/components/recommend/RecommendView.module.css */
2
2
  .backdrop {
3
- position: fixed;
4
- inset: 0;
5
- z-index: 480;
6
3
  display: flex;
7
- align-items: center;
8
- justify-content: center;
4
+ flex-direction: column;
5
+ flex: 1;
6
+ min-height: 0;
7
+ width: 100%;
9
8
  padding: 24px;
10
- background: rgba(0, 0, 0, .64);
11
- backdrop-filter: blur(4px);
12
9
  }
13
10
  .modal {
14
- width: min(980px, 96vw);
15
- max-height: min(820px, 94vh);
11
+ width: 100%;
12
+ max-width: 980px;
13
+ margin: 0 auto;
16
14
  overflow: auto;
17
15
  border: 1px solid var(--color-border);
18
16
  border-radius: 16px;
19
- background: var(--color-bg-elevated);
17
+ background: var(--color-bg-card, #080908);
20
18
  color: var(--color-text);
21
- box-shadow: 0 20px 60px rgba(0,0,0,.48);
19
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
20
+ flex: 1;
21
+ display: flex;
22
+ flex-direction: column;
23
+ min-height: 0;
22
24
  }
23
25
  .header,
24
26
  .footer { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 20px 24px; border-bottom: 1px solid var(--color-border); }
@@ -1,24 +1,24 @@
1
1
  .overlay {
2
- position: fixed;
3
- inset: 0;
4
- z-index: 200;
5
- background: rgba(0, 0, 0, 0.6);
6
2
  display: flex;
7
- align-items: center;
8
- justify-content: center;
9
- animation: fadeIn 0.15s ease;
3
+ flex-direction: column;
4
+ flex: 1;
5
+ min-height: 0;
6
+ width: 100%;
7
+ padding: 24px;
10
8
  }
11
9
 
12
10
  .modal {
13
- background: var(--bg-primary, #0f0f17);
14
- border: 1px solid var(--border, #1e1e2e);
11
+ background: var(--color-bg-card, #080908);
12
+ border: 1px solid var(--color-border, #161a16);
15
13
  border-radius: 12px;
16
- width: 720px;
17
- max-width: 95vw;
18
- max-height: 85vh;
14
+ width: 100%;
15
+ max-width: 1000px;
16
+ margin: 0 auto;
17
+ flex: 1;
19
18
  display: flex;
20
19
  flex-direction: column;
21
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
20
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
21
+ min-height: 0;
22
22
  }
23
23
 
24
24
  .header {
@@ -8,33 +8,46 @@
8
8
  align-items: center;
9
9
  gap: 4px;
10
10
  }
11
- .trigger,
12
- .cycle {
11
+ .trigger {
13
12
  height: 30px;
14
13
  display: inline-flex;
15
14
  align-items: center;
16
15
  justify-content: center;
17
- border: 1px solid var(--color-border);
18
- background: var(--color-surface);
19
- color: var(--color-text);
20
- cursor: pointer;
21
- transition: background 150ms, border-color 150ms, color 150ms;
22
- }
23
- .trigger {
24
16
  gap: 6px;
25
- padding: 0 9px;
17
+ padding: 0 12px;
26
18
  border-radius: 6px;
27
- font-size: 12px;
28
- font-weight: 700;
19
+ font-size: 11px;
20
+ font-weight: 800;
21
+ font-family: var(--font-mono);
22
+ text-transform: uppercase;
23
+ letter-spacing: 0.5px;
24
+ background: var(--color-accent);
25
+ color: var(--color-bg);
26
+ border: 1px solid var(--color-accent);
27
+ cursor: pointer;
28
+ transition: all 150ms var(--ease-out);
29
29
  }
30
30
  .cycle {
31
+ height: 30px;
31
32
  width: 30px;
33
+ display: inline-flex;
34
+ align-items: center;
35
+ justify-content: center;
36
+ border: 1px solid var(--color-border);
37
+ background: var(--color-surface);
38
+ color: var(--color-text);
32
39
  border-radius: 6px;
40
+ cursor: pointer;
41
+ transition: all 150ms var(--ease-out);
42
+ }
43
+ .trigger:hover {
44
+ background: var(--color-accent-hover);
45
+ border-color: var(--color-accent-hover);
46
+ color: var(--color-bg);
33
47
  }
34
- .trigger:hover,
35
48
  .cycle:hover {
36
49
  background: var(--color-bg-hover);
37
- border-color: var(--color-accent);
50
+ border-color: var(--color-border-hover);
38
51
  color: var(--color-accent);
39
52
  }
40
53
  .emoji { font-size: 14px; line-height: 1; }
@@ -8,24 +8,24 @@
8
8
 
9
9
  /* ─── CSS Custom Properties (Design Tokens) ─── */
10
10
  :root {
11
- --color-brand: #76b900;
12
- --color-bg: #000000;
13
- --color-bg-elevated: #0a0a0a;
14
- --color-bg-card: #0a0a0a;
15
- --color-bg-hover: #161616;
16
- --color-bg-active: #222222;
17
- --color-surface: #111111;
18
- --color-border: #262626;
19
- --color-border-hover: #404040;
11
+ --color-brand: #c0f20c;
12
+ --color-bg: #050505;
13
+ --color-bg-elevated: #090a09;
14
+ --color-bg-card: #080908;
15
+ --color-bg-hover: #121512;
16
+ --color-bg-active: #181d18;
17
+ --color-surface: #0d100d;
18
+ --color-border: #161a16;
19
+ --color-border-hover: #2e3b2e;
20
20
 
21
21
  --color-text: #ffffff;
22
- --color-text-muted: #888888;
23
- --color-text-dim: #444444;
22
+ --color-text-muted: #8fa08f;
23
+ --color-text-dim: #455245;
24
24
 
25
- --color-accent: #ffffff;
26
- --color-accent-hover: #eaeaea;
27
- --color-accent-glow: rgba(255, 255, 255, 0.1);
28
- --color-accent-dim: rgba(255, 255, 255, 0.05);
25
+ --color-accent: #c0f20c;
26
+ --color-accent-hover: #d2ff2b;
27
+ --color-accent-glow: rgba(192, 242, 12, 0.15);
28
+ --color-accent-dim: rgba(192, 242, 12, 0.08);
29
29
 
30
30
  --color-danger: #ff4444;
31
31
  --color-danger-dim: rgba(255, 68, 68, 0.1);
@@ -160,7 +160,7 @@ textarea:focus-visible {
160
160
  }
161
161
 
162
162
  html {
163
- font-size: 14px;
163
+ font-size: 16px;
164
164
  scroll-behavior: smooth;
165
165
  -webkit-font-smoothing: antialiased;
166
166
  -moz-osx-font-smoothing: grayscale;
@@ -179,23 +179,16 @@ body {
179
179
 
180
180
  a { color: var(--color-accent); text-decoration: none; }
181
181
  a:hover { color: var(--color-accent-hover); }
182
- code { font-family: var(--font-mono); font-size: 12px; background: var(--color-surface); padding: 2px 6px; border-radius: 4px; }
182
+ code { font-family: var(--font-mono); font-size: 14px; background: var(--color-surface); padding: 2px 6px; border-radius: 4px; }
183
183
 
184
184
  /* ─── Background Effects ─── */
185
- .bg-grid {
186
- display: none;
187
- }
188
-
189
- .bg-glow {
190
- display: none;
191
- }
192
-
193
-
194
- [data-theme="light"] .bg-glow { opacity: 0.12; }
195
- [data-theme="light"] .bg-grid {
185
+ [data-theme="light"] .app-shell {
196
186
  background-image:
197
- linear-gradient(rgba(0,0,0,0.04) 1px, transparent 1px),
198
- linear-gradient(90deg, rgba(0,0,0,0.04) 1px, transparent 1px);
187
+ linear-gradient(rgba(0,0,0,0.03) 1px, transparent 1px),
188
+ linear-gradient(90deg, rgba(0,0,0,0.03) 1px, transparent 1px);
189
+ }
190
+ [data-theme="light"] .app-shell::before {
191
+ background: radial-gradient(circle, rgba(0, 143, 77, 0.03) 0%, transparent 70%);
199
192
  }
200
193
 
201
194
  /* ─── App Layout ─── */
@@ -207,6 +200,25 @@ code { font-family: var(--font-mono); font-size: 12px; background: var(--color-s
207
200
  width: 100%;
208
201
  position: relative;
209
202
  z-index: 1;
203
+ background-color: var(--color-bg);
204
+ /* background grid effect */
205
+ background-image:
206
+ linear-gradient(rgba(192, 242, 12, 0.012) 1px, transparent 1px),
207
+ linear-gradient(90deg, rgba(192, 242, 12, 0.012) 1px, transparent 1px);
208
+ background-size: 50px 50px;
209
+ background-position: center top;
210
+ }
211
+
212
+ .app-shell::before {
213
+ content: "";
214
+ position: absolute;
215
+ top: 0;
216
+ left: 25%;
217
+ width: 600px;
218
+ height: 600px;
219
+ background: radial-gradient(circle, rgba(192, 242, 12, 0.03) 0%, transparent 70%);
220
+ z-index: -1;
221
+ pointer-events: none;
210
222
  }
211
223
 
212
224
  .app-content {
@@ -225,6 +237,21 @@ code { font-family: var(--font-mono); font-size: 12px; background: var(--color-s
225
237
  min-height: 0;
226
238
  }
227
239
 
240
+ /* 📖 Dashboard-only: bound the main box to the viewport height (minus the
241
+ 📖 global header) so the model table scrolls INTERNALLY inside .scrollInner
242
+ 📖 instead of growing the whole page. That internal scroll is what lets the
243
+ 📖 green <thead> header be position:sticky and stay pinned while you scroll
244
+ 📖 190+ model rows. Scoped to the dashboard only (main.dashboardView has
245
+ 📖 higher specificity than the shared .view rule, so flex:none + the capped
246
+ 📖 height reliably win). Settings / Analytics / Playground keep their layout. */
247
+ main.dashboardView {
248
+ flex: none;
249
+ height: calc(100vh - var(--header-h));
250
+ height: calc(100dvh - var(--header-h));
251
+ min-height: 0;
252
+ overflow: hidden;
253
+ }
254
+
228
255
  .update-warning-banner {
229
256
  position: sticky;
230
257
  top: var(--header-h, 60px);
@@ -234,7 +261,7 @@ code { font-family: var(--font-mono); font-size: 12px; background: var(--color-s
234
261
  color: #fff;
235
262
  border-bottom: 1px solid rgba(255, 255, 255, 0.22);
236
263
  font-weight: 800;
237
- font-size: 13px;
264
+ font-size: 15px;
238
265
  letter-spacing: 0.01em;
239
266
  text-align: center;
240
267
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22);
@@ -14,7 +14,7 @@ const SORT_VALUES = new Set([
14
14
  'latestPing', 'avg', 'condition', 'verdict', 'stability', 'uptime',
15
15
  'aiLatency', 'tps', 'trend',
16
16
  ])
17
- const VIEW_VALUES = new Set(['dashboard', 'settings', 'analytics', 'recommend', 'router', 'help', 'changelog'])
17
+ const VIEW_VALUES = new Set(['dashboard', 'settings', 'analytics', 'recommend', 'router', 'help', 'changelog', 'playground', 'install-endpoints', 'installed-models'])
18
18
  const DIR_VALUES = new Set(['asc', 'desc'])
19
19
  const TOOL_MODE_VALUES = new Set(TOOL_MODE_ORDER)
20
20
 
@@ -31,7 +31,17 @@ function parseUrlParams() {
31
31
  if (typeof window === 'undefined') return null
32
32
  const params = new URLSearchParams(window.location.search)
33
33
  const out = {}
34
- if (params.has('view') && VALID_VIEWS.has(params.get('view'))) out.view = params.get('view')
34
+
35
+ // 📖 Read pathname as the view slug
36
+ const pathname = window.location.pathname.replace(/^\/|\/$/g, '')
37
+ if (pathname && VALID_VIEWS.has(pathname)) {
38
+ out.view = pathname
39
+ } else if (params.has('view') && VALID_VIEWS.has(params.get('view'))) {
40
+ out.view = params.get('view')
41
+ } else {
42
+ out.view = 'dashboard'
43
+ }
44
+
35
45
  if (params.has('tier') && VALID_TIERS.has(params.get('tier'))) out.tier = params.get('tier')
36
46
  if (params.has('status') && VALID_STATUS.has(params.get('status'))) out.status = params.get('status')
37
47
  if (params.has('provider')) out.provider = params.get('provider')
@@ -71,10 +81,16 @@ export function buildUrlParams(state) {
71
81
  function writeUrl(state) {
72
82
  if (typeof window === 'undefined') return
73
83
  const params = buildUrlParams(state)
84
+
85
+ // 📖 Extract view from the search params so it becomes the pathname slug instead of a search param!
86
+ const view = params.get('view') || 'dashboard'
87
+ params.delete('view')
88
+
74
89
  const search = params.toString()
90
+ const path = view !== 'dashboard' ? `/${view}` : '/'
75
91
  const newUrl = search
76
- ? `${window.location.pathname}?${search}${window.location.hash}`
77
- : `${window.location.pathname}${window.location.hash}`
92
+ ? `${path}?${search}${window.location.hash}`
93
+ : `${path}${window.location.hash}`
78
94
  // 📖 replaceState (not pushState) so back/forward don't fill up with
79
95
  // 📖 every keystroke. The current URL still updates visibly.
80
96
  window.history.replaceState({}, '', newUrl)