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
@@ -13,13 +13,14 @@
13
13
  * 📖 Custom widths persist in localStorage via the useColumnSizing hook and survive reloads.
14
14
  * 📖 A "Reset columns" button appears in the toolbar only when the user has custom widths.
15
15
  */
16
- import { useMemo, useEffect, useState, useCallback, Fragment } from 'react'
16
+ import { useMemo, useEffect, useState, useCallback, useRef } from 'react'
17
17
  import {
18
18
  useReactTable,
19
19
  getCoreRowModel,
20
20
  flexRender,
21
21
  createColumnHelper,
22
22
  } from '@tanstack/react-table'
23
+ import { useVirtualizer } from '@tanstack/react-virtual'
23
24
  import { IconStar, IconStarFilled, IconPlayerPlayFilled } from '@tabler/icons-react'
24
25
  import { useColumnSizing } from '../../hooks/useColumnSizing.js'
25
26
 
@@ -176,10 +177,12 @@ function TrendCellRenderer({ row }) {
176
177
  // 📖 the cells always see fresh values.
177
178
  const buildColumns = ({ favorites, onBenchmarkRow, onSelectModel, onLaunch, toolMode }) => [
178
179
  // 📖 Star column — leftmost, mirrors the TUI's `F` key.
180
+ // 📖 First column = bookmarks (TUI `F` key). Widened from 32→104 so the
181
+ // 📖 "BOOKMARKS" header label fits horizontally without clipping.
179
182
  colHelper.display({
180
183
  id: 'fav',
181
- header: '',
182
- size: 32,
184
+ header: 'BOOKMARKS',
185
+ size: 104,
183
186
  enableSorting: false,
184
187
  cell: ({ row }) => {
185
188
  const m = row.original
@@ -351,6 +354,12 @@ export default function ModelTable({
351
354
  // 📖 Expand row state — only one row expanded at a time (accordion)
352
355
  const [expandedRowId, setExpandedRowId] = useState(null)
353
356
 
357
+ // 📖 scrollRef is the internal scroll container — shared by the sticky
358
+ // 📖 <thead> header and the row virtualizer (only visible rows are in the
359
+ // 📖 DOM; off-screen rows stay in React state so live pings/probes keep
360
+ // 📖 updating them and re-render correctly when scrolled into view).
361
+ const scrollRef = useRef(null)
362
+
354
363
  const toggleExpand = useCallback((model) => {
355
364
  const key = `${model.providerKey}/${model.modelId}`
356
365
  setExpandedRowId((prev) => prev === key ? null : key)
@@ -402,6 +411,42 @@ export default function ModelTable({
402
411
 
403
412
  const rows = table.getRowModel().rows
404
413
 
414
+ // 📖 Flatten rows + any expanded detail panel into a single virtualized
415
+ // 📖 list. One <tr> per item keeps TanStack Virtual's dynamic measurement
416
+ // 📖 clean (one measured element per virtual item). The expand item only
417
+ // 📖 exists for the single accordion-open row, so count changes by at most 1.
418
+ const flatRows = useMemo(() => {
419
+ const out = []
420
+ for (const row of rows) {
421
+ const m = row.original
422
+ const key = `${m.providerKey}/${m.modelId}`
423
+ out.push({ type: 'row', row, key })
424
+ if (expandedRowId === key) {
425
+ out.push({ type: 'expand', row, key: `${key}__expand` })
426
+ }
427
+ }
428
+ return out
429
+ }, [rows, expandedRowId])
430
+
431
+ // 📖 Row virtualizer: renders only the ~15-25 rows in the viewport (+overscan)
432
+ // 📖 instead of all 190+. Spacer <tr>s above/below preserve the full scroll
433
+ // 📖 height. This is a RENDER-only optimization — ALL model data (including
434
+ // 📖 live pings/probes for off-screen rows) still lives in React state via
435
+ // 📖 useSocket, so updates continue uninterrupted and show fresh values the
436
+ // 📖 moment a row scrolls back into view. Nothing about probing changes.
437
+ const rowVirtualizer = useVirtualizer({
438
+ count: flatRows.length,
439
+ getScrollElement: () => scrollRef.current,
440
+ estimateSize: (i) => (flatRows[i]?.type === 'expand' ? 260 : 47),
441
+ overscan: 10,
442
+ getItemKey: (i) => flatRows[i].key,
443
+ })
444
+ const virtualItems = rowVirtualizer.getVirtualItems()
445
+ const totalSize = rowVirtualizer.getTotalSize()
446
+ const padTop = virtualItems.length ? Math.max(0, virtualItems[0].start) : 0
447
+ const lastVi = virtualItems.length ? virtualItems[virtualItems.length - 1] : null
448
+ const padBottom = lastVi ? Math.max(0, totalSize - lastVi.end) : 0
449
+
405
450
  // 📖 Total table width = sum of all column sizes. Used to drop the min-width
406
451
  // 📖 override once the user has manually tuned the columns, so horizontal
407
452
  // 📖 scrolling reflects the user's layout rather than the 1200px default.
@@ -433,6 +478,8 @@ export default function ModelTable({
433
478
 
434
479
  return (
435
480
  <div className={styles.container}>
481
+ {/* 📖 resizeToolbar lives OUTSIDE the scroller so it never overlaps the
482
+ 📖 sticky table header (thead th sticks to the top of scrollInner). */}
436
483
  {hasCustomSizing && (
437
484
  <div className={styles.resizeToolbar}>
438
485
  <span className={styles.resizeToolbarHint}>
@@ -448,6 +495,10 @@ export default function ModelTable({
448
495
  </button>
449
496
  </div>
450
497
  )}
498
+ {/* 📖 scrollInner is the actual scroller. It is the scroll element for
499
+ 📖 both the sticky <thead> header and the row virtualizer. Bounded by
500
+ 📖 .dashboardView (viewport height) so the table scrolls internally. */}
501
+ <div className={styles.scrollInner} ref={scrollRef}>
451
502
  <table
452
503
  className={styles.table}
453
504
  style={hasCustomSizing ? { minWidth: `${totalTableWidth}px` } : undefined}
@@ -508,10 +559,45 @@ export default function ModelTable({
508
559
  ))}
509
560
  </thead>
510
561
  <tbody>
511
- {rows.map((row, i) => {
562
+ {/* 📖 Top spacer: represents the scroll height of all virtualized-out
563
+ 📖 rows ABOVE the viewport. aria-hidden + empty cell keeps it
564
+ 📖 invisible while preserving table column layout. */}
565
+ {padTop > 0 && (
566
+ <tr aria-hidden="true" style={{ height: padTop }}>
567
+ <td style={{ height: padTop, padding: 0, border: 0 }} />
568
+ </tr>
569
+ )}
570
+ {virtualItems.map((vi) => {
571
+ const item = flatRows[vi.index]
572
+ // 📖 Expanded detail panel row — its own virtual item so dynamic
573
+ // 📖 measurement stays one-element-per-item (clean heights).
574
+ if (item.type === 'expand') {
575
+ return (
576
+ <tr
577
+ key={item.key}
578
+ className={styles.expandRowWrapper}
579
+ data-index={vi.index}
580
+ ref={rowVirtualizer.measureElement}
581
+ >
582
+ <td colSpan={item.row.getVisibleCells().length} className={styles.expandRowCell}>
583
+ <ExpandedDetailRow
584
+ model={item.row.original}
585
+ favorites={favorites}
586
+ onBenchmark={onBenchmarkRow}
587
+ onLaunch={onLaunch}
588
+ onToast={onToast}
589
+ toolMode={toolMode}
590
+ onSetToolMode={onSetToolMode}
591
+ onCycleToolMode={onCycleToolMode}
592
+ onOpenFallback={onOpenFallback}
593
+ />
594
+ </td>
595
+ </tr>
596
+ )
597
+ }
598
+ const row = item.row
512
599
  const m = row.original
513
- const rowKey = `${m.providerKey}/${m.modelId}`
514
- const isExpanded = expandedRowId === rowKey
600
+ const isExpanded = expandedRowId === item.key
515
601
  const rankIdx = [...top3Ids].indexOf(m.modelId)
516
602
  const rowClasses = []
517
603
  if (rankIdx >= 0) rowClasses.push(styles[`rank${rankIdx + 1}`])
@@ -530,47 +616,37 @@ export default function ModelTable({
530
616
  rowClasses.push(styles.notInSetRow)
531
617
  }
532
618
  return (
533
- <Fragment key={row.id}>
534
- <tr
535
- className={rowClasses.join(' ')}
536
- onClick={() => toggleExpand(m)}
537
- >
538
- {row.getVisibleCells().map(cell => {
539
- const sizePx = `${cell.column.getSize()}px`
540
- return (
541
- <td
542
- key={cell.id}
543
- className={styles.td}
544
- style={{ width: sizePx, minWidth: sizePx, maxWidth: sizePx }}
545
- >
546
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
547
- </td>
548
- )
549
- })}
550
- </tr>
551
- {/* 📖 Expanded detail row — 3-column panel with info, playground, AI latency */}
552
- {isExpanded && (
553
- <tr className={styles.expandRowWrapper}>
554
- <td colSpan={row.getVisibleCells().length} className={styles.expandRowCell}>
555
- <ExpandedDetailRow
556
- model={m}
557
- favorites={favorites}
558
- onBenchmark={onBenchmarkRow}
559
- onLaunch={onLaunch}
560
- onToast={onToast}
561
- toolMode={toolMode}
562
- onSetToolMode={onSetToolMode}
563
- onCycleToolMode={onCycleToolMode}
564
- onOpenFallback={onOpenFallback}
565
- />
619
+ <tr
620
+ key={item.key}
621
+ className={rowClasses.join(' ')}
622
+ onClick={() => toggleExpand(m)}
623
+ data-index={vi.index}
624
+ ref={rowVirtualizer.measureElement}
625
+ >
626
+ {row.getVisibleCells().map(cell => {
627
+ const sizePx = `${cell.column.getSize()}px`
628
+ return (
629
+ <td
630
+ key={cell.id}
631
+ className={styles.td}
632
+ style={{ width: sizePx, minWidth: sizePx, maxWidth: sizePx }}
633
+ >
634
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
566
635
  </td>
567
- </tr>
568
- )}
569
- </Fragment>
636
+ )
637
+ })}
638
+ </tr>
570
639
  )
571
640
  })}
641
+ {/* 📖 Bottom spacer: scroll height of all virtualized-out rows BELOW. */}
642
+ {padBottom > 0 && (
643
+ <tr aria-hidden="true" style={{ height: padBottom }}>
644
+ <td style={{ height: padBottom, padding: 0, border: 0 }} />
645
+ </tr>
646
+ )}
572
647
  </tbody>
573
648
  </table>
649
+ </div>
574
650
  </div>
575
651
  )
576
652
  }
@@ -13,6 +13,24 @@
13
13
  border-right: 0;
14
14
  border-radius: 0;
15
15
  margin: 0;
16
+ flex: 1;
17
+ min-height: 0;
18
+ /* 📖 The container itself doesn't scroll — it is the relative clip box
19
+ 📖 hosting the scrolling table (.scrollInner). overflow:hidden keeps the
20
+ 📖 sticky header + virtualized rows neatly clipped to the table viewport.
21
+ 📖 isolation:isolate creates a clean stacking context so the sticky
22
+ 📖 <thead> header (z-index 10) reliably layers above body cells. */
23
+ overflow: hidden;
24
+ position: relative;
25
+ isolation: isolate;
26
+ display: flex;
27
+ flex-direction: column;
28
+ }
29
+
30
+ /* 📖 The real scroll surface for the table. It is the scroll element for
31
+ 📖 both the sticky <thead> header and the @tanstack/react-virtual row
32
+ 📖 virtualizer (only ~23 of 189 rows live in the DOM at any time). */
33
+ .scrollInner {
16
34
  flex: 1;
17
35
  min-height: 0;
18
36
  overflow: auto;
@@ -24,27 +42,29 @@
24
42
  width: 100%;
25
43
  min-width: 1200px;
26
44
  border-collapse: collapse;
27
- font-size: 12px;
45
+ font-size: 14px;
28
46
  border: 0;
29
47
  }
30
48
 
31
49
  /* ─── Header ─── */
32
50
  .th {
33
- padding: 9px 8px;
34
- font-size: 10px;
35
- font-weight: 700;
51
+ padding: 10px 8px;
52
+ font-size: 12px;
53
+ font-weight: 800;
54
+ font-family: var(--font-mono);
36
55
  text-transform: uppercase;
37
- letter-spacing: 0.5px;
38
- color: var(--color-text-muted);
39
- background: var(--color-bg-elevated);
40
- border-bottom: 1px solid var(--color-border-hover);
41
- border-right: 1px solid var(--color-border);
56
+ letter-spacing: 0.8px;
57
+ color: var(--color-bg);
58
+ background: var(--color-accent);
59
+ border-bottom: 2px solid var(--color-border);
60
+ border-right: 1px solid rgba(0, 0, 0, 0.08);
42
61
  white-space: nowrap;
43
62
  user-select: none;
44
63
  position: sticky;
45
64
  top: 0;
46
65
  z-index: 10;
47
66
  text-align: center;
67
+ transition: all 120ms var(--ease-out);
48
68
  /* 📖 Disable user-select on the whole header so dragging the resize handle
49
69
  📖 never accidentally selects the cell text on rapid drags. */
50
70
  -webkit-user-select: none;
@@ -52,7 +72,7 @@
52
72
  -ms-user-select: none;
53
73
  }
54
74
  .th:last-child { border-right: 0; }
55
- .th:hover { color: var(--color-accent); background: var(--color-bg-active); }
75
+ .th:hover { color: var(--color-bg); background: var(--color-accent-hover); }
56
76
 
57
77
  /* 📖 Inner flex container: header label on the left, resize handle on the right.
58
78
  📖 Letting the label shrink keeps the column tight when the user makes it narrow. */
@@ -120,8 +140,10 @@
120
140
  user-select: none !important;
121
141
  }
122
142
 
123
- /* 📖 Floating toolbar that appears only when the user has custom column widths.
124
- 📖 Sits at the top-right of the table area without pushing rows down. */
143
+ /* 📖 Floating bar shown only when the user has custom column widths.
144
+ 📖 Rendered OUTSIDE the scroller (a normal flex child at the top of the
145
+ 📖 table container) so it stays put without colliding with the sticky
146
+ 📖 <thead>. It shrinks the scroll area; the sticky header sticks below it. */
125
147
  .resizeToolbar {
126
148
  display: flex;
127
149
  align-items: center;
@@ -130,11 +152,9 @@
130
152
  padding: 6px 10px;
131
153
  background: var(--color-bg-elevated);
132
154
  border-bottom: 1px solid var(--color-border);
133
- font-size: 11px;
155
+ font-size: 13px;
134
156
  color: var(--color-text-muted);
135
- position: sticky;
136
- top: 0;
137
- z-index: 11;
157
+ flex-shrink: 0;
138
158
  }
139
159
  .resizeToolbarHint {
140
160
  display: inline-flex;
@@ -154,7 +174,7 @@
154
174
  color: var(--color-text-muted);
155
175
  padding: 3px 10px;
156
176
  border-radius: 4px;
157
- font-size: 11px;
177
+ font-size: 13px;
158
178
  font-weight: 600;
159
179
  cursor: pointer;
160
180
  transition: background 120ms var(--ease-out, ease), color 120ms var(--ease-out, ease), border-color 120ms var(--ease-out, ease);
@@ -176,8 +196,8 @@
176
196
  .th:nth-child(11) { text-align: left; } /* Verdict */
177
197
 
178
198
  /* Sort icons */
179
- .sortIcon { opacity: 0.3; margin-left: 3px; font-size: 9px; }
180
- .sortIconActive { color: var(--color-accent); margin-left: 3px; font-size: 10px; font-weight: 800; }
199
+ .sortIcon { opacity: 0.4; margin-left: 3px; font-size: 9px; color: var(--color-bg); }
200
+ .sortIconActive { color: var(--color-bg); margin-left: 3px; font-size: 10px; font-weight: 900; }
181
201
 
182
202
  /* ─── Data cells ─── */
183
203
  .td {
@@ -191,6 +211,11 @@
191
211
  .td:nth-child(7) { text-align: left; } /* Provider */
192
212
  .td:nth-child(11) { text-align: left; } /* Verdict */
193
213
 
214
+ /* 📖 Center the BOOKMARKS column (star button) so it sits nicely in its
215
+ 📖 wider header. Other narrow numeric columns are centered by default. */
216
+ .td:nth-child(1),
217
+ .th:nth-child(1) { text-align: center; }
218
+
194
219
  /* ─── Row hover ─── */
195
220
  .table tbody tr {
196
221
  cursor: pointer;
@@ -206,7 +231,7 @@
206
231
  /* ─── Shared cell values ─── */
207
232
  .rankNum {
208
233
  font-family: var(--font-mono);
209
- font-size: 11px;
234
+ font-size: 13px;
210
235
  color: var(--color-text-dim);
211
236
  text-align: center;
212
237
  display: block;
@@ -217,7 +242,7 @@
217
242
  .sweMid { color: #3ddc84; }
218
243
  .sweLow { color: var(--color-text-dim); }
219
244
 
220
- .ctx { font-family: var(--font-mono); font-size: 11px; }
245
+ .ctx { font-family: var(--font-mono); font-size: 13px; }
221
246
 
222
247
  .ping { font-family: var(--font-mono); font-weight: 600; }
223
248
  .pingFast { color: #00ff88; }
@@ -232,14 +257,14 @@
232
257
  :global([data-theme="light"]) .pingMedium { color: #b86b00; }
233
258
  :global([data-theme="light"]) .pingSlow { color: #c8143a; }
234
259
 
235
- .uptime { font-family: var(--font-mono); font-size: 11px; }
260
+ .uptime { font-family: var(--font-mono); font-size: 13px; }
236
261
 
237
262
  /* ─── Model cell ─── */
238
263
  .modelCell { display: flex; align-items: center; gap: 6px; overflow: hidden; }
239
264
  .modelMeta { flex: 1; min-width: 0; }
240
265
  .modelHeader { display: flex; align-items: center; gap: 5px; }
241
- .modelName { font-weight: 600; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
242
- .modelId { font-family: var(--font-mono); font-size: 9px; color: var(--color-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
266
+ .modelName { font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
267
+ .modelId { font-family: var(--font-mono); font-size: 11px; color: var(--color-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
243
268
 
244
269
  /* 📖 Provider cell now uses <ProviderLogo /> (icon + wordmark) — see
245
270
  📖 atoms/ProviderLogo.module.css for the layout. The old bordered pill
@@ -368,13 +393,43 @@
368
393
  }
369
394
 
370
395
  /* ─── Expandable row styles ─── */
371
- .expandedRow {
372
- background: var(--color-bg-active) !important;
396
+ .expandedRow td {
397
+ background: var(--color-accent) !important;
398
+ color: var(--color-bg) !important;
399
+ border-bottom: 1px solid rgba(0, 0, 0, 0.15) !important;
400
+ }
401
+ .expandedRow td * {
402
+ color: var(--color-bg) !important;
403
+ }
404
+ .expandedRow :global(.status-dot) {
405
+ border-color: var(--color-bg) !important;
406
+ }
407
+ .expandedRow :global(.provider-wordmark) {
408
+ filter: brightness(0) saturate(100%) !important; /* Forces logos to render dark/black for contrast */
409
+ }
410
+ .expandedRow .rankNum,
411
+ .expandedRow .modelId {
412
+ color: rgba(0, 0, 0, 0.6) !important;
413
+ }
414
+ .expandedRow .sweLow,
415
+ .expandedRow .sweMid,
416
+ .expandedRow .sweHigh {
417
+ color: var(--color-bg) !important;
418
+ }
419
+ .expandedRow .pingFast,
420
+ .expandedRow .pingMedium,
421
+ .expandedRow .pingSlow {
422
+ color: var(--color-bg) !important;
423
+ font-weight: 800;
373
424
  }
374
425
  .expandRowWrapper {
375
426
  border: none;
376
427
  }
377
428
  .expandRowCell {
378
429
  padding: 0 !important;
379
- border-bottom: 2px solid var(--color-accent) !important;
430
+ border-bottom: 3px solid var(--color-accent) !important;
431
+ background: var(--color-bg-elevated) !important;
432
+ }
433
+ .expandRowCell * {
434
+ color: inherit;
380
435
  }
@@ -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(820px, 95vw);
21
- max-height: 90vh;
22
- background: var(--color-bg-elevated);
23
- border: 1px solid var(--color-border-hover);
16
+ width: 100%;
17
+ max-width: 820px;
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 {
@@ -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: 680px;
17
- max-width: 95vw;
18
- max-height: 85vh;
14
+ width: 100%;
15
+ max-width: 680px;
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 {
@@ -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: 640px;
17
- max-width: 95vw;
18
- max-height: 85vh;
14
+ width: 100%;
15
+ max-width: 640px;
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 {
@@ -22,20 +22,18 @@ import styles from './Header.module.css'
22
22
  // 📖 order, icon, and "coming soon" milestone are colocated with the
23
23
  // 📖 rendering code. When a view ships, remove the `comingIn` field.
24
24
  const NAV_ITEMS = [
25
- { id: 'dashboard', label: 'Dashboard', icon: IconLayoutDashboard },
26
- { id: 'settings', label: 'Settings', icon: IconSettings },
27
- { id: 'analytics', label: 'Analytics', icon: IconActivity },
28
- { id: 'recommend', label: 'Recommend', icon: IconSparkles },
29
- { id: 'router', label: 'Router', icon: IconRoute },
30
- { id: 'playground', label: 'Playground', icon: IconMessageChatbot },
25
+ { id: 'dashboard', label: 'Dashboard', icon: IconLayoutDashboard },
26
+ { id: 'router', label: 'Router', icon: IconRoute },
27
+ { id: 'playground', label: 'Playground', icon: IconMessageChatbot },
28
+ { id: 'help', label: 'Help', icon: IconQuestionMark },
29
+ { id: 'install-endpoints', label: 'Install Endpoints', icon: IconPlug },
31
30
  ]
32
31
 
33
- // 📖 Overflow menu items — Help + Changelog shipped in M2; Install Endpoints
34
- // 📖 and Installed Models are still M4.
32
+ // 📖 Overflow menu items
35
33
  const MENU_ITEMS = [
36
- { id: 'help', label: 'Help', icon: IconQuestionMark },
34
+ { id: 'analytics', label: 'Analytics', icon: IconActivity },
35
+ { id: 'recommend', label: 'Recommend', icon: IconSparkles },
37
36
  { id: 'changelog', label: 'Changelog', icon: IconHistory },
38
- { id: 'install-endpoints', label: 'Install Endpoints', icon: IconPlug },
39
37
  { id: 'installed-models', label: 'Installed Models', icon: IconFolders },
40
38
  ]
41
39
 
@@ -90,8 +88,11 @@ export default function Header({
90
88
  return (
91
89
  <header className={styles.header}>
92
90
  <div className={styles.left}>
93
- <div className={styles.logo}>
94
- <span className={styles.logoIcon}>&gt;</span>
91
+ <div className={styles.logo} onClick={() => onNavigate('dashboard')} style={{ cursor: 'pointer' }}>
92
+ <svg className={styles.logoIconSvg} width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
93
+ <path d="M4 5L11 12L4 19" stroke="var(--color-brand)" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
94
+ <path d="M14 5V19M14 5H21M14 11H18" stroke="var(--color-brand)" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
95
+ </svg>
95
96
  <span className={styles.logoText}>
96
97
  <span className={styles.logoTextHighlight}>free</span>
97
98
  <span>-coding-models</span>
@@ -168,7 +169,7 @@ export default function Header({
168
169
  {/* Overflow menu (kebab) — hidden features & occasional flows */}
169
170
  <div className={styles.menuWrap} ref={menuRef}>
170
171
  <button
171
- className={`${styles.navBtn} ${styles.menuTrigger}`}
172
+ className={`${styles.navBtn} ${styles.menuTrigger} ${MENU_ITEMS.some((m) => m.id === currentView) ? styles.navBtnActive : ''}`}
172
173
  onClick={() => setMenuOpen((o) => !o)}
173
174
  title="More features"
174
175
  aria-label="More features"
@@ -181,12 +182,14 @@ export default function Header({
181
182
  <div className={styles.menuPopover} role="menu">
182
183
  {MENU_ITEMS.map((item) => {
183
184
  const Icon = item.icon
185
+ const isActive = currentView === item.id
184
186
  return (
185
187
  <button
186
188
  key={item.id}
187
- className={styles.menuItem}
189
+ className={`${styles.menuItem} ${isActive ? styles.menuItemActive : ''}`}
188
190
  onClick={() => handleMenuClick(item)}
189
191
  role="menuitem"
192
+ aria-current={isActive ? 'page' : undefined}
190
193
  >
191
194
  <Icon size={14} stroke={1.5} />
192
195
  <span>{item.label}</span>
@@ -200,21 +203,18 @@ export default function Header({
200
203
  </nav>
201
204
  </div>
202
205
 
203
- <div className={styles.center}>
204
- <div className={styles.searchBar}>
205
- <span className={styles.searchIcon}><IconSearch size={16} stroke={1.5} /></span>
206
- <input
207
- type="text"
208
- className={styles.searchInput}
209
- placeholder="Search models, providers, tiers..."
210
- value={searchQuery}
211
- onChange={(e) => onSearchChange(e.target.value)}
212
- autoComplete="off"
213
- />
214
- </div>
215
- </div>
216
-
217
206
  <div className={styles.right}>
207
+ <button
208
+ className={`${styles.navBtn} ${currentView === 'settings' ? styles.navBtnActive : ''}`}
209
+ onClick={() => onNavigate('settings')}
210
+ title="Settings"
211
+ aria-current={currentView === 'settings' ? 'page' : undefined}
212
+ style={{ marginRight: '2px' }}
213
+ >
214
+ <IconSettings size={14} stroke={1.5} />
215
+ <span>Settings</span>
216
+ </button>
217
+
218
218
  <ToolPicker
219
219
  toolMode={toolMode}
220
220
  onSetToolMode={onSetToolMode}