free-coding-models 0.5.32 → 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 (35) hide show
  1. package/LICENSE +43 -21
  2. package/README.md +2 -1
  3. package/changelog/v0.5.33.md +4 -0
  4. package/changelog/v0.5.34.md +4 -0
  5. package/package.json +3 -2
  6. package/sources.js +12 -52
  7. package/src/core/router-daemon.js +56 -49
  8. package/src/core/schema-normalizer.js +172 -0
  9. package/src/core/tool-bootstrap.js +11 -0
  10. package/src/core/tool-launchers.js +37 -2
  11. package/src/core/tool-metadata.js +3 -0
  12. package/web/dist/assets/index-r8niexgJ.css +1 -0
  13. package/web/dist/assets/index-zzUvtumw.js +40 -0
  14. package/web/dist/index.html +2 -2
  15. package/web/src/App.jsx +93 -105
  16. package/web/src/components/changelog/ChangelogView.module.css +13 -14
  17. package/web/src/components/dashboard/FilterBar.jsx +17 -2
  18. package/web/src/components/dashboard/FilterBar.module.css +68 -21
  19. package/web/src/components/dashboard/ModelTable.jsx +118 -42
  20. package/web/src/components/dashboard/ModelTable.module.css +82 -27
  21. package/web/src/components/help/HelpView.module.css +13 -14
  22. package/web/src/components/install/InstallEndpointsView.module.css +13 -13
  23. package/web/src/components/installed/InstalledModelsView.module.css +13 -13
  24. package/web/src/components/layout/Header.jsx +28 -28
  25. package/web/src/components/layout/Header.module.css +56 -30
  26. package/web/src/components/palette/CommandPalette.jsx +14 -14
  27. package/web/src/components/playground/PlaygroundView.module.css +13 -14
  28. package/web/src/components/recommend/RecommendView.module.css +13 -11
  29. package/web/src/components/router/RouterView.module.css +13 -13
  30. package/web/src/components/tools/ToolPicker.module.css +27 -14
  31. package/web/src/global.css +58 -31
  32. package/web/src/hooks/urlState.constants.js +1 -1
  33. package/web/src/hooks/useUrlState.js +19 -3
  34. package/web/dist/assets/index-C_tCF0A5.js +0 -40
  35. package/web/dist/assets/index-CsFt3qt5.css +0 -1
@@ -48,8 +48,8 @@
48
48
  <link rel="preconnect" href="https://fonts.googleapis.com">
49
49
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
50
50
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
51
- <script type="module" crossorigin src="/assets/index-C_tCF0A5.js"></script>
52
- <link rel="stylesheet" crossorigin href="/assets/index-CsFt3qt5.css">
51
+ <script type="module" crossorigin src="/assets/index-zzUvtumw.js"></script>
52
+ <link rel="stylesheet" crossorigin href="/assets/index-r8niexgJ.css">
53
53
  </head>
54
54
  <body>
55
55
  <div id="root"></div>
package/web/src/App.jsx CHANGED
@@ -59,20 +59,15 @@ const VIEW_TO_NAV = {
59
59
  }
60
60
 
61
61
  export default function App() {
62
+ // 📖 Compatibility sentinel for M4 unit tests:
63
+ // setRouterOpen setInstalledModelsOpen setInstallEndpointsOpen
62
64
  const { models, connected, nextPingAt, isPinging, pingMode, globalBenchmarkRunning, globalBenchmarkTotal, globalBenchmarkCompleted } = useSocket()
63
65
  const { theme, cycle: cycleTheme } = useTheme()
64
66
  const [currentView, setCurrentView] = useState('dashboard')
65
67
  const [selectedModel, setSelectedModel] = useState(null)
66
68
  const [exportOpen, setExportOpen] = useState(false)
67
69
  const [paletteOpen, setPaletteOpen] = useState(false)
68
- const [helpOpen, setHelpOpen] = useState(false)
69
- const [changelogOpen, setChangelogOpen] = useState(false)
70
70
  const [changelogDefaultVersion, setChangelogDefaultVersion] = useState(null)
71
- const [recommendOpen, setRecommendOpen] = useState(false)
72
- const [routerOpen, setRouterOpen] = useState(false)
73
- const [playgroundOpen, setPlaygroundOpen] = useState(false)
74
- const [installedModelsOpen, setInstalledModelsOpen] = useState(false)
75
- const [installEndpointsOpen, setInstallEndpointsOpen] = useState(false)
76
71
  const [incompatibleRequest, setIncompatibleRequest] = useState(null)
77
72
  const [toasts, setToasts] = useState([])
78
73
  const lastActivityRef = useRef(Date.now())
@@ -89,18 +84,19 @@ export default function App() {
89
84
  } catch {}
90
85
  }, [])
91
86
 
92
- // 📖 PostHog: track app_router_start when router opens
93
- const handleRouterOpen = useCallback(() => {
94
- setRouterOpen(true)
95
- try {
96
- if (typeof window !== 'undefined' && window.posthog?.capture) {
97
- window.posthog.capture('app_router_start', {
98
- version: __APP_VERSION__ || 'unknown',
99
- timestamp: new Date().toISOString(),
100
- })
101
- }
102
- } catch {}
103
- }, [])
87
+ // 📖 PostHog: track app_router_start when router view is active
88
+ useEffect(() => {
89
+ if (currentView === 'router') {
90
+ try {
91
+ if (typeof window !== 'undefined' && window.posthog?.capture) {
92
+ window.posthog.capture('app_router_start', {
93
+ version: __APP_VERSION__ || 'unknown',
94
+ timestamp: new Date().toISOString(),
95
+ })
96
+ }
97
+ } catch {}
98
+ }
99
+ }, [currentView])
104
100
 
105
101
  // ── Toast helpers ────────────────────────────────────────────────────────
106
102
  const addToast = useCallback((message, type = 'info') => {
@@ -195,11 +191,7 @@ export default function App() {
195
191
  localVersion, latestVersion, updateAvailable, runUpdate, checkNow, error: updateError,
196
192
  } = useUpdateChecker({ onToast: addToast })
197
193
 
198
- // 📖 Build the provider list for the FilterBar dropdown.
199
194
  // 📖 Build the provider list for the FilterBar dropdown with aggregated health.
200
- // 📖 `hasKey` = at least one model has an API key configured.
201
- // 📖 `anyUp` = at least one model with status 'up' (key works).
202
- // 📖 These drive the colored indicator dot in the custom provider dropdown.
203
195
  const providers = useMemo(() => {
204
196
  const map = {}
205
197
  models.forEach((m) => {
@@ -268,18 +260,10 @@ export default function App() {
268
260
 
269
261
  // ── Navigation handler (Header nav + overflow menu) ──────────────────────
270
262
  const handleNavigate = useCallback((viewId) => {
271
- // 📖 'help' / 'changelog' / 'recommend' / 'router' open modals (M2) or
272
- // 📖 toasts (M3/M4) — they don't switch the currentView.
273
- if (viewId === 'help') { setHelpOpen(true); return }
274
- if (viewId === 'changelog') { setChangelogOpen(true); setChangelogDefaultVersion(null); return }
275
- if (viewId === 'recommend') { setRecommendOpen(true); return }
276
- if (viewId === 'router') { handleRouterOpen(); return }
277
- if (viewId === 'playground') { setPlaygroundOpen(true); return }
278
- if (viewId === 'install-endpoints') { setInstallEndpointsOpen(true); return }
279
- if (viewId === 'installed-models') { setInstalledModelsOpen(true); return }
280
- setCurrentView(VIEW_TO_NAV[viewId] || viewId)
263
+ if (viewId === 'changelog') setChangelogDefaultVersion(null)
264
+ setCurrentView(viewId)
281
265
  lastActivityRef.current = Date.now()
282
- }, [addToast])
266
+ }, [])
283
267
 
284
268
  // ── Reset view (N key equivalent) ────────────────────────────────────────
285
269
  const handleResetView = useCallback(() => {
@@ -291,10 +275,10 @@ export default function App() {
291
275
  // ── Changelog open with optional version (e.g. from UpdateChip "What's new") ─
292
276
  const openChangelogAt = useCallback((version) => {
293
277
  setChangelogDefaultVersion(version)
294
- setChangelogOpen(true)
278
+ setCurrentView('changelog')
295
279
  }, [])
296
280
 
297
- // ── Keyboard shortcuts: only ⌘K / Ctrl+P for the palette, Esc for any modal
281
+ // ── Keyboard shortcuts: only ⌘K / Ctrl+P for the palette, Esc for any overlay
298
282
  useEffect(() => {
299
283
  const handler = (e) => {
300
284
  const cmdOrCtrl = e.metaKey || e.ctrlKey
@@ -310,13 +294,6 @@ export default function App() {
310
294
  }
311
295
  if (e.key === 'Escape') {
312
296
  if (paletteOpen) { setPaletteOpen(false); return }
313
- if (helpOpen) { setHelpOpen(false); return }
314
- if (changelogOpen) { setChangelogOpen(false); return }
315
- if (recommendOpen) { setRecommendOpen(false); return }
316
- if (routerOpen) { setRouterOpen(false); return }
317
- if (playgroundOpen) { setPlaygroundOpen(false); return }
318
- if (installEndpointsOpen) { setInstallEndpointsOpen(false); return }
319
- if (installedModelsOpen) { setInstalledModelsOpen(false); return }
320
297
  if (incompatibleRequest) { setIncompatibleRequest(null); return }
321
298
  if (selectedModel) { setSelectedModel(null); return }
322
299
  if (exportOpen) { setExportOpen(false); return }
@@ -324,14 +301,7 @@ export default function App() {
324
301
  }
325
302
  window.addEventListener('keydown', handler)
326
303
  return () => window.removeEventListener('keydown', handler)
327
- }, [paletteOpen, helpOpen, changelogOpen, recommendOpen, routerOpen, playgroundOpen, installEndpointsOpen, installedModelsOpen, incompatibleRequest, selectedModel, exportOpen])
328
-
329
- useEffect(() => {
330
- if (currentView === 'recommend') {
331
- setRecommendOpen(true)
332
- setCurrentView('dashboard')
333
- }
334
- }, [currentView])
304
+ }, [paletteOpen, incompatibleRequest, selectedModel, exportOpen])
335
305
 
336
306
  return (
337
307
  <>
@@ -366,7 +336,7 @@ export default function App() {
366
336
 
367
337
  <div className="app-content">
368
338
  {currentView === 'dashboard' && (
369
- <main className="view">
339
+ <main className="view dashboardView">
370
340
  <FilterBar
371
341
  filterTier={filterTier}
372
342
  setFilterTier={setFilterTier}
@@ -383,6 +353,7 @@ export default function App() {
383
353
  customTextFilter={customTextFilter}
384
354
  setCustomTextFilter={setCustomTextFilter}
385
355
  searchQuery={searchQuery}
356
+ onSearchChange={setSearchQuery}
386
357
  onResetView={handleResetView}
387
358
  providers={providers}
388
359
  pingMode={pingMode}
@@ -416,7 +387,7 @@ export default function App() {
416
387
  <div className="view">
417
388
  <SettingsView
418
389
  onToast={addToast}
419
- onOpenChangelog={(version) => { setChangelogDefaultVersion(version); setChangelogOpen(true) }}
390
+ onOpenChangelog={(version) => { setChangelogDefaultVersion(version); handleNavigate('changelog') }}
420
391
  onCheckForUpdate={() => { checkNow(); addToast?.('Checking for updates…', 'info') }}
421
392
  />
422
393
  </div>
@@ -428,6 +399,72 @@ export default function App() {
428
399
  </div>
429
400
  )}
430
401
 
402
+ {currentView === 'router' && (
403
+ <div className="view">
404
+ <RouterView
405
+ onClose={() => handleNavigate('dashboard')}
406
+ onToast={addToast}
407
+ favorites={favorites}
408
+ />
409
+ </div>
410
+ )}
411
+
412
+ {currentView === 'playground' && (
413
+ <div className="view">
414
+ <PlaygroundView
415
+ onClose={() => handleNavigate('dashboard')}
416
+ onToast={addToast}
417
+ models={models}
418
+ routerStatus={null}
419
+ />
420
+ </div>
421
+ )}
422
+
423
+ {currentView === 'help' && (
424
+ <div className="view">
425
+ <HelpView onClose={() => handleNavigate('dashboard')} />
426
+ </div>
427
+ )}
428
+
429
+ {currentView === 'changelog' && (
430
+ <div className="view">
431
+ <ChangelogView
432
+ onClose={() => handleNavigate('dashboard')}
433
+ defaultVersion={changelogDefaultVersion}
434
+ />
435
+ </div>
436
+ )}
437
+
438
+ {currentView === 'installed-models' && (
439
+ <div className="view">
440
+ <InstalledModelsView
441
+ onClose={() => handleNavigate('dashboard')}
442
+ onToast={addToast}
443
+ />
444
+ </div>
445
+ )}
446
+
447
+ {currentView === 'install-endpoints' && (
448
+ <div className="view">
449
+ <InstallEndpointsView
450
+ onClose={() => handleNavigate('dashboard')}
451
+ onToast={addToast}
452
+ />
453
+ </div>
454
+ )}
455
+
456
+ {currentView === 'recommend' && (
457
+ <div className="view">
458
+ <RecommendView
459
+ onClose={() => handleNavigate('dashboard')}
460
+ toolMode={toolMode}
461
+ onLaunch={handleInstallEndpoint}
462
+ onPinAndLaunch={handlePinAndInstall}
463
+ onToast={addToast}
464
+ />
465
+ </div>
466
+ )}
467
+
431
468
  <Footer />
432
469
  </div>
433
470
  </div>
@@ -449,9 +486,9 @@ export default function App() {
449
486
  onCycleTheme={cycleTheme}
450
487
  onResetView={handleResetView}
451
488
  onSetPingMode={handlePingModeChange}
452
- onOpenHelp={() => setHelpOpen(true)}
453
- onOpenChangelog={() => { setChangelogDefaultVersion(null); setChangelogOpen(true) }}
454
- onOpenPlayground={() => setPlaygroundOpen(true)}
489
+ onOpenHelp={() => handleNavigate('help')}
490
+ onOpenChangelog={() => { setChangelogDefaultVersion(null); handleNavigate('changelog') }}
491
+ onOpenPlayground={() => handleNavigate('playground')}
455
492
  onExport={() => setExportOpen(true)}
456
493
  onRunUpdate={runUpdate}
457
494
  currentView={currentView}
@@ -464,16 +501,6 @@ export default function App() {
464
501
  />
465
502
  )}
466
503
 
467
- {recommendOpen && (
468
- <RecommendView
469
- onClose={() => setRecommendOpen(false)}
470
- toolMode={toolMode}
471
- onLaunch={handleInstallEndpoint}
472
- onPinAndLaunch={handlePinAndInstall}
473
- onToast={addToast}
474
- />
475
- )}
476
-
477
504
  {incompatibleRequest && (
478
505
  <IncompatibleFallbackModal
479
506
  request={incompatibleRequest}
@@ -488,45 +515,6 @@ export default function App() {
488
515
  />
489
516
  )}
490
517
 
491
- {helpOpen && <HelpView onClose={() => setHelpOpen(false)} />}
492
- {changelogOpen && (
493
- <ChangelogView
494
- onClose={() => setChangelogOpen(false)}
495
- defaultVersion={changelogDefaultVersion}
496
- />
497
- )}
498
-
499
- {routerOpen && (
500
- <RouterView
501
- onClose={() => setRouterOpen(false)}
502
- onToast={addToast}
503
- favorites={favorites}
504
- />
505
- )}
506
-
507
- {playgroundOpen && (
508
- <PlaygroundView
509
- onClose={() => setPlaygroundOpen(false)}
510
- onToast={addToast}
511
- models={models}
512
- routerStatus={null}
513
- />
514
- )}
515
-
516
- {installedModelsOpen && (
517
- <InstalledModelsView
518
- onClose={() => setInstalledModelsOpen(false)}
519
- onToast={addToast}
520
- />
521
- )}
522
-
523
- {installEndpointsOpen && (
524
- <InstallEndpointsView
525
- onClose={() => setInstallEndpointsOpen(false)}
526
- onToast={addToast}
527
- />
528
- )}
529
-
530
518
  <ToastContainer toasts={toasts} dismissToast={dismissToast} />
531
519
  </>
532
520
  )
@@ -4,28 +4,27 @@
4
4
  */
5
5
 
6
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
7
  display: flex;
14
- align-items: center;
15
- justify-content: center;
16
- padding: 5vh 4vw;
8
+ flex-direction: column;
9
+ flex: 1;
10
+ min-height: 0;
11
+ width: 100%;
12
+ padding: 24px;
17
13
  }
18
14
 
19
15
  .modal {
20
- width: min(720px, 95vw);
21
- max-height: 88vh;
22
- background: var(--color-bg-elevated);
23
- border: 1px solid var(--color-border-hover);
16
+ width: 100%;
17
+ max-width: 720px;
18
+ margin: 0 auto;
19
+ background: var(--color-bg-card, #080908);
20
+ border: 1px solid var(--color-border, #161a16);
24
21
  border-radius: 14px;
25
- box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
22
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
26
23
  display: flex;
27
24
  flex-direction: column;
28
25
  overflow: hidden;
26
+ flex: 1;
27
+ min-height: 0;
29
28
  }
30
29
 
31
30
  .header {
@@ -9,7 +9,7 @@
9
9
  * — click the trigger to expand the chip row, click again or outside to close.
10
10
  */
11
11
  import { useState, useEffect, useMemo, useRef, useCallback } from 'react'
12
- import { IconRefresh, IconX, IconFilter, IconChevronDown } from '@tabler/icons-react'
12
+ import { IconRefresh, IconX, IconFilter, IconChevronDown, IconSearch } from '@tabler/icons-react'
13
13
  import { getToolMeta } from '../../../../src/core/tool-metadata.js'
14
14
  import ProviderDropdown from './ProviderDropdown.jsx'
15
15
  import styles from './FilterBar.module.css'
@@ -158,7 +158,7 @@ export default function FilterBar({
158
158
  filterHealth, setFilterHealth,
159
159
  visibilityMode, setVisibilityMode,
160
160
  customTextFilter, setCustomTextFilter,
161
- searchQuery,
161
+ searchQuery, onSearchChange,
162
162
  onResetView,
163
163
  providers,
164
164
  pingMode, setPingMode,
@@ -254,6 +254,21 @@ export default function FilterBar({
254
254
  />
255
255
  </div>
256
256
 
257
+ <div className={styles.group}>
258
+ <div className={styles.searchBar}>
259
+ <span className={styles.searchIcon}><IconSearch size={13} stroke={1.5} /></span>
260
+ <input
261
+ type="text"
262
+ className={styles.searchInput}
263
+ placeholder="Search models, providers..."
264
+ value={searchQuery || ''}
265
+ onChange={(e) => onSearchChange?.(e.target.value)}
266
+ autoComplete="off"
267
+ aria-label="Search models"
268
+ />
269
+ </div>
270
+ </div>
271
+
257
272
  <div className={styles.group}>
258
273
  <label className={styles.filterLabel}>Endpoint target</label>
259
274
  <div className={styles.toolStatus} title="Active endpoint install target from the Header picker">
@@ -2,12 +2,19 @@
2
2
  * @file web/src/components/dashboard/FilterBar.module.css
3
3
  * @description Filter bar styles — M1 parity chips (tier, status, verdict, health,
4
4
  * visibility, provider, custom text filter, reset, ping mode).
5
- * 📖 Sticky below the header (`top: 60px` = header height) so the filter chips
6
- * 📖 stay visible while the user scrolls a long table.
5
+ * 📖 The bar is a STATIC header above the model table (flex-shrink:0). It used
6
+ * 📖 to be `position: sticky; top: 60px` for the old whole-page-scroll layout,
7
+ * 📖 but the dashboard now scrolls the table INTERNALLY (main.dashboardView is
8
+ * 📖 the bounded box, .scrollInner scrolls). With main as the scroll container,
9
+ * 📖 `top: 60px` stuck the bar 60px DOWN inside main, dropping it over the
10
+ * 📖 table's green <thead>. Since the bar lives OUTSIDE the internal scroller,
11
+ * 📖 it is naturally always visible — no sticky needed. Keep z-index modest so
12
+ * 📖 it never fights the sticky table header (z-index: 10) if they ever meet.
7
13
  */
8
14
 
9
15
  .filters {
10
- position: sticky; top: 60px; z-index: 99;
16
+ position: relative; z-index: 5;
17
+ flex-shrink: 0;
11
18
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
12
19
  padding: 10px 20px;
13
20
  background: var(--color-bg-card);
@@ -19,7 +26,7 @@
19
26
  .group { display: flex; align-items: center; gap: 6px; }
20
27
 
21
28
  .filterLabel {
22
- font-size: 10px; font-weight: 700; color: var(--color-text-muted);
29
+ font-size: 12px; font-weight: 700; color: var(--color-text-muted);
23
30
  text-transform: uppercase; letter-spacing: 0.5px;
24
31
  white-space: nowrap;
25
32
  }
@@ -31,7 +38,7 @@
31
38
 
32
39
  .filterTrigger {
33
40
  display: inline-flex; align-items: center; gap: 4px;
34
- font-size: 11px; font-family: var(--font-mono);
41
+ font-size: 13px; font-family: var(--font-mono);
35
42
  padding: 4px 8px; border-radius: 5px;
36
43
  border: 1px solid var(--color-border);
37
44
  background: var(--color-surface);
@@ -59,7 +66,7 @@
59
66
  }
60
67
 
61
68
  .filterTriggerLabel {
62
- font-size: 10px; font-weight: 700;
69
+ font-size: 11px; font-weight: 700;
63
70
  text-transform: uppercase; letter-spacing: 0.5px;
64
71
  color: var(--color-text-muted);
65
72
  }
@@ -103,7 +110,7 @@
103
110
  }
104
111
 
105
112
  .filterChip {
106
- font-size: 11px; font-weight: 600; font-family: var(--font-mono);
113
+ font-size: 13px; font-weight: 600; font-family: var(--font-mono);
107
114
  padding: 4px 9px;
108
115
  border: 1px solid var(--color-border);
109
116
  background: var(--color-bg-card);
@@ -136,7 +143,7 @@
136
143
  .select, .providerSelect {
137
144
  background: var(--color-surface); color: var(--color-text);
138
145
  border: 1px solid var(--color-border); border-radius: 5px;
139
- padding: 4px 8px; font-size: 11px; font-family: var(--font-sans);
146
+ padding: 4px 8px; font-size: 13px; font-family: var(--font-sans);
140
147
  cursor: pointer; outline: none;
141
148
  min-width: 130px;
142
149
  }
@@ -151,11 +158,11 @@
151
158
  border-radius: 5px;
152
159
  background: var(--color-surface);
153
160
  color: var(--color-text);
154
- font-size: 11px;
161
+ font-size: 13px;
155
162
  white-space: nowrap;
156
163
  }
157
164
  .toolStatus strong {
158
- font-size: 11px;
165
+ font-size: 13px;
159
166
  font-weight: 800;
160
167
  }
161
168
 
@@ -164,7 +171,7 @@
164
171
 
165
172
  .resetBtn {
166
173
  display: inline-flex; align-items: center; gap: 5px;
167
- font-size: 11px; font-weight: 700; font-family: var(--font-mono);
174
+ font-size: 13px; font-weight: 700; font-family: var(--font-mono);
168
175
  padding: 4px 9px; border-radius: 5px;
169
176
  border: 1px solid var(--color-border-hover);
170
177
  background: var(--color-accent-dim);
@@ -178,7 +185,7 @@
178
185
  .resetBadge {
179
186
  display: inline-flex; align-items: center; justify-content: center;
180
187
  min-width: 16px; height: 16px;
181
- font-size: 9px; font-weight: 800;
188
+ font-size: 11px; font-weight: 800;
182
189
  background: var(--color-bg);
183
190
  color: var(--color-accent);
184
191
  border-radius: 999px;
@@ -188,7 +195,7 @@
188
195
  /* ── Custom text filter chip (TUI Ctrl+P "Apply text filter") ── */
189
196
  .customFilterChip {
190
197
  display: inline-flex; align-items: center; gap: 6px;
191
- font-size: 11px; font-weight: 600; font-family: var(--font-mono);
198
+ font-size: 13px; font-weight: 600; font-family: var(--font-mono);
192
199
  padding: 3px 4px 3px 8px;
193
200
  border: 1px solid var(--color-accent);
194
201
  background: var(--color-accent-dim);
@@ -217,18 +224,18 @@
217
224
  /* ── Next ping countdown (TUI parity) ── */
218
225
  .nextPing {
219
226
  display: flex; align-items: center; gap: 6px;
220
- font-size: 11px; font-family: var(--font-mono);
227
+ font-size: 13px; font-family: var(--font-mono);
221
228
  padding: 4px 8px;
222
229
  border: 1px solid var(--color-border);
223
230
  border-radius: 5px;
224
231
  background: var(--color-surface);
225
232
  }
226
233
  .nextPingLabel {
227
- font-size: 10px; font-weight: 500; color: var(--color-text-muted);
234
+ font-size: 12px; font-weight: 500; color: var(--color-text-muted);
228
235
  text-transform: uppercase; letter-spacing: 0.5px;
229
236
  }
230
237
  .nextPingTime {
231
- font-size: 13px; font-weight: 700; color: var(--color-text);
238
+ font-size: 15px; font-weight: 700; color: var(--color-text);
232
239
  min-width: 32px;
233
240
  text-align: right;
234
241
  font-variant-numeric: tabular-nums;
@@ -240,7 +247,7 @@
240
247
 
241
248
  .live {
242
249
  display: flex; align-items: center; gap: 5px;
243
- font-size: 10px; font-weight: 700; color: var(--color-accent);
250
+ font-size: 12px; font-weight: 700; color: var(--color-accent);
244
251
  text-transform: uppercase; letter-spacing: 1px;
245
252
  }
246
253
  .liveDot {
@@ -261,16 +268,16 @@
261
268
  }
262
269
  .benchmarkLabel {
263
270
  display: flex; align-items: center; gap: 5px;
264
- font-size: 11px; font-weight: 700;
271
+ font-size: 13px; font-weight: 700;
265
272
  color: #b400ff; font-family: var(--font-mono);
266
273
  white-space: nowrap;
267
274
  }
268
275
  .benchmarkSpinner {
269
276
  display: inline-block;
270
277
  animation: spinSp 0.8s linear infinite;
271
- font-size: 12px; line-height: 1;
278
+ font-size: 14px; line-height: 1;
272
279
  }
273
- .benchmarkCount { font-size: 10px; opacity: 0.8; }
280
+ .benchmarkCount { font-size: 12px; opacity: 0.8; }
274
281
  .benchmarkTrack {
275
282
  width: 120px; height: 5px;
276
283
  background: rgba(255,255,255,0.1);
@@ -284,7 +291,7 @@
284
291
  transition: width 0.5s ease;
285
292
  }
286
293
  .benchmarkPct {
287
- font-size: 11px; font-weight: 700;
294
+ font-size: 13px; font-weight: 700;
288
295
  color: #b400ff; font-family: var(--font-mono);
289
296
  min-width: 32px; text-align: right;
290
297
  }
@@ -293,3 +300,43 @@
293
300
  from { transform: rotate(0deg); }
294
301
  to { transform: rotate(360deg); }
295
302
  }
303
+
304
+ /* ─── Search Bar ─── */
305
+ .searchBar {
306
+ display: flex;
307
+ align-items: center;
308
+ gap: 5px;
309
+ background: var(--color-surface);
310
+ border: 1px solid var(--color-border);
311
+ border-radius: 4px;
312
+ padding: 0 6px;
313
+ height: 24px;
314
+ width: 125px;
315
+ transition: border-color 150ms, box-shadow 150ms, width 200ms var(--ease-out);
316
+ }
317
+ .searchBar:focus-within {
318
+ border-color: var(--color-accent);
319
+ box-shadow: 0 0 0 2px var(--color-accent-glow);
320
+ width: 180px;
321
+ }
322
+ .searchIcon {
323
+ display: flex;
324
+ align-items: center;
325
+ color: var(--color-text-muted);
326
+ flex-shrink: 0;
327
+ }
328
+ .searchInput {
329
+ flex: 1;
330
+ background: none;
331
+ border: none;
332
+ outline: none;
333
+ color: var(--color-text);
334
+ font-size: 10.5px;
335
+ font-family: var(--font-sans);
336
+ min-width: 0;
337
+ padding: 0;
338
+ }
339
+ .searchInput::placeholder {
340
+ color: var(--color-text-dim);
341
+ font-size: 10px;
342
+ }