free-coding-models 0.5.5 → 0.5.8
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 +19 -11
- package/bin/free-coding-models.js +6 -0
- package/changelog/v0.5.5.md +1 -0
- package/changelog/v0.5.6.md +24 -0
- package/changelog/v0.5.7.md +20 -0
- package/package.json +4 -4
- package/src/core/changelog-loader.js +5 -1
- package/src/core/endpoint-installer.js +3 -3
- package/src/core/router-daemon.js +11 -0
- package/src/core/tool-launchers.js +2 -2
- package/web/dist/assets/index-DKmmiDip.css +1 -0
- package/web/dist/assets/index-DzVt42Nf.js +39 -0
- package/web/dist/index.html +2 -2
- package/web/server.js +532 -2
- package/web/src/App.jsx +235 -73
- package/web/src/components/analytics/AnalyticsView.jsx +4 -0
- package/web/src/components/analytics/TokenUsagePanel.jsx +105 -0
- package/web/src/components/analytics/TokenUsagePanel.module.css +126 -0
- package/web/src/components/atoms/TierBadge.module.css +3 -3
- package/web/src/components/changelog/ChangelogView.jsx +135 -0
- package/web/src/components/changelog/ChangelogView.module.css +160 -0
- package/web/src/components/dashboard/DetailPanel.jsx +29 -4
- package/web/src/components/dashboard/DetailPanel.module.css +26 -0
- package/web/src/components/dashboard/FilterBar.jsx +17 -2
- package/web/src/components/dashboard/FilterBar.module.css +17 -0
- package/web/src/components/dashboard/ModelTable.jsx +17 -5
- package/web/src/components/dashboard/ModelTable.module.css +14 -1
- package/web/src/components/help/HelpView.jsx +188 -0
- package/web/src/components/help/HelpView.module.css +157 -0
- package/web/src/components/install/InstallEndpointsView.jsx +278 -0
- package/web/src/components/install/InstallEndpointsView.module.css +351 -0
- package/web/src/components/installed/InstalledModelsView.jsx +89 -0
- package/web/src/components/installed/InstalledModelsView.module.css +200 -0
- package/web/src/components/launch/IncompatibleFallbackModal.jsx +69 -0
- package/web/src/components/launch/LaunchButton.jsx +30 -0
- package/web/src/components/launch/LaunchButton.module.css +35 -0
- package/web/src/components/launch/LaunchModal.module.css +125 -0
- package/web/src/components/layout/Header.jsx +78 -14
- package/web/src/components/layout/Header.module.css +62 -2
- package/web/src/components/palette/CommandPalette.jsx +228 -74
- package/web/src/components/recommend/RecommendView.jsx +121 -0
- package/web/src/components/recommend/RecommendView.module.css +55 -0
- package/web/src/components/router/RouterView.jsx +286 -0
- package/web/src/components/router/RouterView.module.css +357 -0
- package/web/src/components/settings/SettingsView.jsx +281 -8
- package/web/src/components/settings/SettingsView.module.css +174 -0
- package/web/src/components/tools/ToolPicker.jsx +86 -0
- package/web/src/components/tools/ToolPicker.module.css +84 -0
- package/web/src/components/update/UpdateChip.jsx +104 -0
- package/web/src/components/update/UpdateChip.module.css +146 -0
- package/web/src/global.css +23 -0
- package/web/src/hooks/urlState.constants.js +28 -0
- package/web/src/hooks/useChangelog.js +51 -0
- package/web/src/hooks/useInstalledModels.js +42 -0
- package/web/src/hooks/useRecommend.js +67 -0
- package/web/src/hooks/useRouterDashboard.js +133 -0
- package/web/src/hooks/useTokenUsage.js +103 -0
- package/web/src/hooks/useToolMode.js +77 -0
- package/web/src/hooks/useUpdateChecker.js +91 -0
- package/web/src/hooks/useUrlState.js +123 -62
- package/web/src/utils/m3.js +55 -0
- package/web/dist/assets/index-uD3faN3G.js +0 -39
- package/web/dist/assets/index-uTifVKX1.css +0 -1
|
@@ -35,6 +35,7 @@ import AILatencyCell from '../atoms/AILatencyCell.jsx'
|
|
|
35
35
|
import TPSCell from '../atoms/TPSCell.jsx'
|
|
36
36
|
import NoKeyIcon from '../atoms/NoKeyIcon.jsx'
|
|
37
37
|
import ProviderLogo from '../atoms/ProviderLogo.jsx'
|
|
38
|
+
import LaunchButton from '../launch/LaunchButton.jsx'
|
|
38
39
|
|
|
39
40
|
import { pingClass } from '../../utils/format.js'
|
|
40
41
|
import { sweClass } from '../../utils/ranks.js'
|
|
@@ -172,7 +173,7 @@ function TrendCellRenderer({ row }) {
|
|
|
172
173
|
// 📖 Note: `favorites` / `onBenchmarkRow` are read by the cell renderers below
|
|
173
174
|
// 📖 via closure. We rebuild the columns list whenever any of these change so
|
|
174
175
|
// 📖 the cells always see fresh values.
|
|
175
|
-
const buildColumns = ({ favorites, onBenchmarkRow, onSelectModel }) => [
|
|
176
|
+
const buildColumns = ({ favorites, onBenchmarkRow, onSelectModel, onLaunch, toolMode }) => [
|
|
176
177
|
// 📖 Star column — leftmost, mirrors the TUI's `F` key.
|
|
177
178
|
colHelper.display({
|
|
178
179
|
id: 'fav',
|
|
@@ -303,13 +304,24 @@ const buildColumns = ({ favorites, onBenchmarkRow, onSelectModel }) => [
|
|
|
303
304
|
cell: TrendCellRenderer,
|
|
304
305
|
enableSorting: true,
|
|
305
306
|
}),
|
|
307
|
+
colHelper.display({
|
|
308
|
+
id: 'launch',
|
|
309
|
+
header: '🔌',
|
|
310
|
+
size: 42,
|
|
311
|
+
enableSorting: false,
|
|
312
|
+
cell: ({ row }) => (
|
|
313
|
+
<div className={styles.launchCell}>
|
|
314
|
+
<LaunchButton model={row.original} toolMode={toolMode} onLaunch={onLaunch} variant="icon" />
|
|
315
|
+
</div>
|
|
316
|
+
),
|
|
317
|
+
}),
|
|
306
318
|
]
|
|
307
319
|
|
|
308
320
|
// 📖 DEFAULT_COLUMN_SIZING: extracted from the columns above. This is the single
|
|
309
321
|
// 📖 source of truth for both the table layout and the useColumnSizing hook's reset target.
|
|
310
322
|
// 📖 The 'Reset columns' button restores these exact widths.
|
|
311
323
|
// 📖 We build a placeholder columns list (no callbacks needed) to extract sizes.
|
|
312
|
-
const PLACEHOLDER_COLUMNS = buildColumns({ favorites: { isFavorite: () => false, toggle: () => {} }, onBenchmarkRow: null, onSelectModel: null })
|
|
324
|
+
const PLACEHOLDER_COLUMNS = buildColumns({ favorites: { isFavorite: () => false, toggle: () => {} }, onBenchmarkRow: null, onSelectModel: null, onLaunch: null, toolMode: 'opencode' })
|
|
313
325
|
export const DEFAULT_COLUMN_SIZING = PLACEHOLDER_COLUMNS.reduce((acc, c) => {
|
|
314
326
|
if (c.id) acc[c.id] = c.size ?? 80
|
|
315
327
|
return acc
|
|
@@ -330,7 +342,7 @@ function SortIcon({ column }) {
|
|
|
330
342
|
|
|
331
343
|
// ─── Main component ───────────────────────────────────────────────────────────
|
|
332
344
|
export default function ModelTable({
|
|
333
|
-
filtered, onSelectModel, onBenchmarkRow, favorites,
|
|
345
|
+
filtered, onSelectModel, onBenchmarkRow, onLaunch, favorites,
|
|
334
346
|
sortColumn, sortDirection, onSort,
|
|
335
347
|
toolMode = null,
|
|
336
348
|
}) {
|
|
@@ -359,8 +371,8 @@ export default function ModelTable({
|
|
|
359
371
|
// 📖 Build the columns with the current favorites/onBenchmarkRow handlers.
|
|
360
372
|
// 📖 Re-derive when favorites reference changes so the star cells re-render.
|
|
361
373
|
const columns = useMemo(
|
|
362
|
-
() => buildColumns({ favorites, onBenchmarkRow, onSelectModel }),
|
|
363
|
-
[favorites, onBenchmarkRow, onSelectModel]
|
|
374
|
+
() => buildColumns({ favorites, onBenchmarkRow, onSelectModel, onLaunch, toolMode }),
|
|
375
|
+
[favorites, onBenchmarkRow, onSelectModel, onLaunch, toolMode]
|
|
364
376
|
)
|
|
365
377
|
|
|
366
378
|
// 📖 Column sizing state with localStorage persistence (see useColumnSizing.js).
|
|
@@ -239,7 +239,7 @@
|
|
|
239
239
|
.modelMeta { flex: 1; min-width: 0; }
|
|
240
240
|
.modelHeader { display: flex; align-items: center; gap: 5px; }
|
|
241
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-
|
|
242
|
+
.modelId { font-family: var(--font-mono); font-size: 9px; color: var(--color-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
243
243
|
|
|
244
244
|
/* 📖 Provider cell now uses <ProviderLogo /> (icon + wordmark) — see
|
|
245
245
|
📖 atoms/ProviderLogo.module.css for the layout. The old bordered pill
|
|
@@ -330,4 +330,17 @@
|
|
|
330
330
|
:global([data-theme="light"]) .incompatible td {
|
|
331
331
|
background: rgba(200, 20, 58, 0.06) !important;
|
|
332
332
|
color: #888 !important;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* ─── M3: per-row endpoint install plug ─── */
|
|
336
|
+
.launchCell {
|
|
337
|
+
display: flex;
|
|
338
|
+
justify-content: center;
|
|
339
|
+
opacity: 0;
|
|
340
|
+
transition: opacity 120ms;
|
|
341
|
+
}
|
|
342
|
+
.table tbody tr:hover .launchCell,
|
|
343
|
+
.incompatible .launchCell,
|
|
344
|
+
.benchRow .launchCell {
|
|
345
|
+
opacity: 1;
|
|
333
346
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/help/HelpView.jsx
|
|
3
|
+
* @description Help modal — M2 parity with the TUI's `K` / `I` help overlay.
|
|
4
|
+
* 📖 Renders the same content the TUI help overlay does, as JSX, with a live
|
|
5
|
+
* 📖 search bar at the top. The TUI source-of-truth is `src/tui/cli-help.js`
|
|
6
|
+
* 📖 (CLI flags) and `src/tui/overlays.js` `renderHelp` (key bindings) — we
|
|
7
|
+
* 📖 re-render those lists statically here to avoid a TUI-engine import that
|
|
8
|
+
* 📖 would pull chalk into the Web bundle.
|
|
9
|
+
*
|
|
10
|
+
* 📖 The header's overflow menu and the ⌘K palette expose "Help" as a page;
|
|
11
|
+
* 📖 this component is rendered as a full-screen modal inside the Web.
|
|
12
|
+
*
|
|
13
|
+
* @functions
|
|
14
|
+
* → HelpView → main modal component
|
|
15
|
+
*/
|
|
16
|
+
import { useState, useMemo } from 'react'
|
|
17
|
+
import { IconSearch, IconX } from '@tabler/icons-react'
|
|
18
|
+
import styles from './HelpView.module.css'
|
|
19
|
+
|
|
20
|
+
const SECTIONS = [
|
|
21
|
+
{
|
|
22
|
+
id: 'navigation',
|
|
23
|
+
title: '🧭 Navigation',
|
|
24
|
+
items: [
|
|
25
|
+
{ key: '↑ / ↓', desc: 'Navigate rows in the main table (planned — currently mouse-only)' },
|
|
26
|
+
{ key: 'Enter', desc: 'Open the detail panel for the focused row' },
|
|
27
|
+
{ key: 'Esc', desc: 'Close any open modal or detail panel' },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'filters',
|
|
32
|
+
title: '🔍 Filters',
|
|
33
|
+
items: [
|
|
34
|
+
{ key: 'Tier chip', desc: 'Click to cycle: All → S+ → S → A+ → A → A- → B+ → B → C → All' },
|
|
35
|
+
{ key: 'Status chip', desc: 'Click to cycle: All → Up → Down → Pending' },
|
|
36
|
+
{ key: 'Verdict chip', desc: 'Click to cycle: All → Perfect → Normal → Spiky → Slow → Overloaded → Down → Unstable → Pending' },
|
|
37
|
+
{ key: 'Health chip', desc: 'Click to cycle: All → Up → Timeout → Down → Pending → No key → Auth err' },
|
|
38
|
+
{ key: 'Visibility', desc: 'Normal (all) / Configured only (hide no-key) / Usable only (UP + good verdict)' },
|
|
39
|
+
{ key: 'Provider dropdown', desc: 'Filter by provider' },
|
|
40
|
+
{ key: 'Custom text filter', desc: 'Apply via ⌘K palette or `Apply text filter` — clear with the X chip' },
|
|
41
|
+
{ key: 'Reset button', desc: 'Clears every active filter + sort back to defaults' },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'sort',
|
|
46
|
+
title: '📶 Sort',
|
|
47
|
+
items: [
|
|
48
|
+
{ key: 'Click any column header', desc: 'Sort asc → desc → reset (no sort, default order)' },
|
|
49
|
+
{ key: 'Resizable columns', desc: 'Drag the right edge of any header to resize. Double-click resets one column.' },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'favorites',
|
|
54
|
+
title: '⭐ Favorites',
|
|
55
|
+
items: [
|
|
56
|
+
{ key: 'Star button (per row)', desc: 'Click to favorite / unfavorite the model (TUI: F key)' },
|
|
57
|
+
{ key: 'Detail panel', desc: 'Favorite + Up/Down reorder + current priority rank (TUI: Shift+↑/↓)' },
|
|
58
|
+
{ key: 'Pinned mode', desc: 'In the Settings view, toggle "Pinned + always visible" (TUI: Y key)' },
|
|
59
|
+
{ key: 'Shared with TUI', desc: 'Favorites are persisted in the same ~/.free-coding-models.json the TUI uses' },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'benchmark',
|
|
64
|
+
title: '🤖 AI Speed Test (benchmark)',
|
|
65
|
+
items: [
|
|
66
|
+
{ key: 'Header button', desc: 'Run a global AI Speed Test on every visible model' },
|
|
67
|
+
{ key: 'AI Lat. cell', desc: 'Click any AI Lat. cell to benchmark just that model (TUI: Ctrl+A)' },
|
|
68
|
+
{ key: 'Detail panel', desc: 'Dedicated "AI Speed Test" button (TUI: Ctrl+A)' },
|
|
69
|
+
{ key: 'TPS column', desc: 'Tokens per second for the model — appears after a benchmark run' },
|
|
70
|
+
{ key: 'Latency column', desc: 'Real completion latency (not just ping) for the model' },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'tools',
|
|
75
|
+
title: '🧰 Tool mode (M3)',
|
|
76
|
+
items: [
|
|
77
|
+
{ key: 'M3 shipped', desc: 'Endpoint target picker, per-row Install Endpoint button, and incompatible-target fallback modal' },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'palette',
|
|
82
|
+
title: '⚡ Command palette',
|
|
83
|
+
items: [
|
|
84
|
+
{ key: '⌘K / Ctrl+P', desc: 'The Web\'s only global keyboard shortcut — toggles the command palette' },
|
|
85
|
+
{ key: 'Type to search', desc: 'Fuzzy match across all filters, sorts, tools, pages, and theme / ping / reset actions' },
|
|
86
|
+
{ key: '↑ / ↓ + Enter', desc: 'Navigate the results and execute the highlighted command' },
|
|
87
|
+
{ key: 'Esc', desc: 'Close the palette without running anything' },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'theme',
|
|
92
|
+
title: '🌗 Theme',
|
|
93
|
+
items: [
|
|
94
|
+
{ key: 'Theme button (header)', desc: 'Click to cycle: auto → dark → light (TUI: G key)' },
|
|
95
|
+
{ key: 'Auto mode', desc: 'Follows the OS prefers-color-scheme preference and updates live' },
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'ping',
|
|
100
|
+
title: '⚡ Ping mode',
|
|
101
|
+
items: [
|
|
102
|
+
{ key: 'Speed / Normal / Slow / Forced', desc: 'Ping cadence (2s / 10s / 30s / 4s) — TUI: W key' },
|
|
103
|
+
{ key: 'next ping in Xs', desc: 'Live countdown shown in the FilterBar (TUI footer style)' },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'url',
|
|
108
|
+
title: '🔗 URL deep-linking',
|
|
109
|
+
items: [
|
|
110
|
+
{ key: '?tier=S+&sort=verdict&origin=groq&view=dashboard', desc: 'Every filter / sort / view is reflected in the URL — share pre-filtered links' },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'cli',
|
|
115
|
+
title: '⌨️ CLI parity',
|
|
116
|
+
items: [
|
|
117
|
+
{ key: 'Same storage', desc: 'The Web reads + writes the same ~/.free-coding-models.json as the TUI' },
|
|
118
|
+
{ key: 'Same engine', desc: 'All model parsing, ping, and benchmark code is shared with the TUI' },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
export default function HelpView({ onClose }) {
|
|
124
|
+
const [query, setQuery] = useState('')
|
|
125
|
+
|
|
126
|
+
// 📖 Filter every section's items by the live query. Case-insensitive
|
|
127
|
+
// 📖 substring match on the key or description. Empty results hide the
|
|
128
|
+
// 📖 section entirely.
|
|
129
|
+
const filteredSections = useMemo(() => {
|
|
130
|
+
const q = query.trim().toLowerCase()
|
|
131
|
+
if (!q) return SECTIONS
|
|
132
|
+
return SECTIONS.map((section) => ({
|
|
133
|
+
...section,
|
|
134
|
+
items: section.items.filter(
|
|
135
|
+
(item) => item.key.toLowerCase().includes(q) || item.desc.toLowerCase().includes(q),
|
|
136
|
+
),
|
|
137
|
+
})).filter((section) => section.items.length > 0)
|
|
138
|
+
}, [query])
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div className={styles.backdrop} onClick={onClose}>
|
|
142
|
+
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
|
|
143
|
+
<div className={styles.header}>
|
|
144
|
+
<div className={styles.titleRow}>
|
|
145
|
+
<h2 className={styles.title}>❓ Help & Keyboard Shortcuts</h2>
|
|
146
|
+
<button className={styles.closeBtn} onClick={onClose} aria-label="Close help">
|
|
147
|
+
<IconX size={18} stroke={1.5} />
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
<div className={styles.searchBar}>
|
|
151
|
+
<IconSearch size={14} stroke={1.5} className={styles.searchIcon} />
|
|
152
|
+
<input
|
|
153
|
+
className={styles.searchInput}
|
|
154
|
+
placeholder="Search help (filters, hotkeys, TUI parity)…"
|
|
155
|
+
value={query}
|
|
156
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
157
|
+
autoFocus
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
<p className={styles.note}>
|
|
161
|
+
The Web Dashboard is mouse-first. The only global keyboard shortcut is{' '}
|
|
162
|
+
<kbd>⌘K</kbd> / <kbd>Ctrl+P</kbd> for the command palette — everything else
|
|
163
|
+
lives here, in the header menu, or in modals.
|
|
164
|
+
</p>
|
|
165
|
+
</div>
|
|
166
|
+
<div className={styles.body}>
|
|
167
|
+
{filteredSections.length === 0 ? (
|
|
168
|
+
<div className={styles.empty}>No help matches "{query}".</div>
|
|
169
|
+
) : (
|
|
170
|
+
filteredSections.map((section) => (
|
|
171
|
+
<section key={section.id} className={styles.section}>
|
|
172
|
+
<h3 className={styles.sectionTitle}>{section.title}</h3>
|
|
173
|
+
<ul className={styles.itemList}>
|
|
174
|
+
{section.items.map((item) => (
|
|
175
|
+
<li key={item.key + item.desc} className={styles.item}>
|
|
176
|
+
<span className={styles.itemKey}>{item.key}</span>
|
|
177
|
+
<span className={styles.itemDesc}>{item.desc}</span>
|
|
178
|
+
</li>
|
|
179
|
+
))}
|
|
180
|
+
</ul>
|
|
181
|
+
</section>
|
|
182
|
+
))
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/help/HelpView.module.css
|
|
3
|
+
* @description Full-screen help modal styles — TUI parity.
|
|
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: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
padding: 5vh 4vw;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.modal {
|
|
20
|
+
width: min(820px, 95vw);
|
|
21
|
+
max-height: 90vh;
|
|
22
|
+
background: var(--color-bg-elevated);
|
|
23
|
+
border: 1px solid var(--color-border-hover);
|
|
24
|
+
border-radius: 14px;
|
|
25
|
+
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.header {
|
|
32
|
+
padding: 20px 24px 16px;
|
|
33
|
+
border-bottom: 1px solid var(--color-border);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.titleRow {
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
justify-content: space-between;
|
|
40
|
+
gap: 12px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.title {
|
|
44
|
+
font-size: 17px;
|
|
45
|
+
font-weight: 700;
|
|
46
|
+
margin: 0;
|
|
47
|
+
color: var(--color-text);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.closeBtn {
|
|
51
|
+
background: transparent;
|
|
52
|
+
border: none;
|
|
53
|
+
color: var(--color-text-muted);
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
padding: 4px;
|
|
56
|
+
border-radius: 4px;
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
}
|
|
61
|
+
.closeBtn:hover { color: var(--color-text); background: var(--color-bg-hover); }
|
|
62
|
+
|
|
63
|
+
.searchBar {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 8px;
|
|
67
|
+
margin-top: 12px;
|
|
68
|
+
background: var(--color-surface);
|
|
69
|
+
border: 1px solid var(--color-border);
|
|
70
|
+
border-radius: 8px;
|
|
71
|
+
padding: 0 12px;
|
|
72
|
+
height: 34px;
|
|
73
|
+
}
|
|
74
|
+
.searchBar:focus-within {
|
|
75
|
+
border-color: var(--color-accent);
|
|
76
|
+
box-shadow: 0 0 0 3px var(--color-accent-glow);
|
|
77
|
+
}
|
|
78
|
+
.searchIcon { color: var(--color-text-dim); }
|
|
79
|
+
.searchInput {
|
|
80
|
+
flex: 1;
|
|
81
|
+
background: none;
|
|
82
|
+
border: none;
|
|
83
|
+
outline: none;
|
|
84
|
+
color: var(--color-text);
|
|
85
|
+
font-size: 13px;
|
|
86
|
+
font-family: var(--font-sans);
|
|
87
|
+
}
|
|
88
|
+
.searchInput::placeholder { color: var(--color-text-dim); }
|
|
89
|
+
|
|
90
|
+
.note {
|
|
91
|
+
margin: 10px 0 0;
|
|
92
|
+
font-size: 11px;
|
|
93
|
+
color: var(--color-text-muted);
|
|
94
|
+
line-height: 1.5;
|
|
95
|
+
}
|
|
96
|
+
.note kbd {
|
|
97
|
+
display: inline-block;
|
|
98
|
+
font-family: var(--font-mono);
|
|
99
|
+
background: var(--color-surface);
|
|
100
|
+
border: 1px solid var(--color-border);
|
|
101
|
+
padding: 1px 5px;
|
|
102
|
+
border-radius: 3px;
|
|
103
|
+
font-size: 10px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.body {
|
|
107
|
+
padding: 18px 24px 24px;
|
|
108
|
+
overflow-y: auto;
|
|
109
|
+
flex: 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.section { margin-bottom: 18px; }
|
|
113
|
+
.sectionTitle {
|
|
114
|
+
font-size: 12px;
|
|
115
|
+
font-weight: 700;
|
|
116
|
+
text-transform: uppercase;
|
|
117
|
+
letter-spacing: 0.6px;
|
|
118
|
+
color: var(--color-text-muted);
|
|
119
|
+
margin: 0 0 8px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.itemList {
|
|
123
|
+
list-style: none;
|
|
124
|
+
margin: 0;
|
|
125
|
+
padding: 0;
|
|
126
|
+
display: flex;
|
|
127
|
+
flex-direction: column;
|
|
128
|
+
gap: 4px;
|
|
129
|
+
}
|
|
130
|
+
.item {
|
|
131
|
+
display: grid;
|
|
132
|
+
grid-template-columns: minmax(160px, 0.4fr) 1fr;
|
|
133
|
+
gap: 12px;
|
|
134
|
+
align-items: baseline;
|
|
135
|
+
padding: 6px 10px;
|
|
136
|
+
background: var(--color-surface);
|
|
137
|
+
border: 1px solid var(--color-border);
|
|
138
|
+
border-radius: 6px;
|
|
139
|
+
font-size: 12px;
|
|
140
|
+
}
|
|
141
|
+
.itemKey {
|
|
142
|
+
font-weight: 600;
|
|
143
|
+
color: var(--color-text);
|
|
144
|
+
font-family: var(--font-mono);
|
|
145
|
+
font-size: 11px;
|
|
146
|
+
}
|
|
147
|
+
.itemDesc {
|
|
148
|
+
color: var(--color-text-muted);
|
|
149
|
+
line-height: 1.5;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.empty {
|
|
153
|
+
text-align: center;
|
|
154
|
+
padding: 40px 20px;
|
|
155
|
+
color: var(--color-text-dim);
|
|
156
|
+
font-size: 13px;
|
|
157
|
+
}
|