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
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/installed/InstalledModelsView.jsx
|
|
3
|
+
* @description Installed Models modal — scans all tool configs, shows models, soft-delete.
|
|
4
|
+
* 📖 M4: Full TUI parity for installed models management.
|
|
5
|
+
*/
|
|
6
|
+
import { useInstalledModels } from '../../hooks/useInstalledModels.js'
|
|
7
|
+
import { IconFolders, IconTrash, IconRefresh } from '@tabler/icons-react'
|
|
8
|
+
import styles from './InstalledModelsView.module.css'
|
|
9
|
+
|
|
10
|
+
export default function InstalledModelsView({ onClose, onToast }) {
|
|
11
|
+
const { results, loading, refresh, disableModel } = useInstalledModels()
|
|
12
|
+
|
|
13
|
+
const handleDisable = async (toolMode, modelId) => {
|
|
14
|
+
try {
|
|
15
|
+
const result = await disableModel(toolMode, modelId)
|
|
16
|
+
if (result.success) {
|
|
17
|
+
onToast?.(`Disabled ${modelId} in ${toolMode}.`, 'success')
|
|
18
|
+
} else {
|
|
19
|
+
onToast?.(`Failed: ${result.error || 'unknown error'}`, 'error')
|
|
20
|
+
}
|
|
21
|
+
} catch (err) {
|
|
22
|
+
onToast?.(`Error: ${err.message}`, 'error')
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const totalModels = results.reduce((sum, r) => sum + r.models.length, 0)
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className={styles.overlay} onClick={(e) => e.target === e.currentTarget && onClose()}>
|
|
30
|
+
<div className={styles.modal}>
|
|
31
|
+
<div className={styles.header}>
|
|
32
|
+
<h2 className={styles.title}>
|
|
33
|
+
<IconFolders size={20} stroke={1.5} />
|
|
34
|
+
Installed Models
|
|
35
|
+
{!loading && <span className={styles.count}>{totalModels}</span>}
|
|
36
|
+
</h2>
|
|
37
|
+
<div className={styles.headerActions}>
|
|
38
|
+
<button className={styles.refreshBtn} onClick={refresh} title="Refresh">
|
|
39
|
+
<IconRefresh size={14} />
|
|
40
|
+
</button>
|
|
41
|
+
<button className={styles.closeBtn} onClick={onClose} aria-label="Close">✕</button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div className={styles.body}>
|
|
46
|
+
{loading && <div className={styles.empty}>Scanning tool configs…</div>}
|
|
47
|
+
|
|
48
|
+
{!loading && totalModels === 0 && (
|
|
49
|
+
<div className={styles.empty}>
|
|
50
|
+
No models found in tool configs.
|
|
51
|
+
<br />
|
|
52
|
+
<span className={styles.hint}>Use "Install Endpoints" to configure models for your tools.</span>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{!loading && results.map((tool) => (
|
|
57
|
+
<div key={tool.toolMode} className={styles.toolGroup}>
|
|
58
|
+
<div className={styles.toolHeader}>
|
|
59
|
+
<span className={styles.toolEmoji}>{tool.toolEmoji}</span>
|
|
60
|
+
<span className={styles.toolLabel}>{tool.toolLabel}</span>
|
|
61
|
+
<span className={styles.toolCount}>{tool.models.length} model{tool.models.length !== 1 ? 's' : ''}</span>
|
|
62
|
+
{!tool.isValid && <span className={styles.toolMissing}>Not installed</span>}
|
|
63
|
+
</div>
|
|
64
|
+
{tool.models.length > 0 && (
|
|
65
|
+
<div className={styles.modelList}>
|
|
66
|
+
{tool.models.map((model) => (
|
|
67
|
+
<div key={`${tool.toolMode}-${model.modelId}`} className={styles.modelRow}>
|
|
68
|
+
<span className={styles.modelIcon}>{model.isExternal ? '🔗' : '✅'}</span>
|
|
69
|
+
<span className={styles.modelName}>{model.label}</span>
|
|
70
|
+
{model.tier && model.tier !== '-' && <span className={styles.modelTier}>{model.tier}</span>}
|
|
71
|
+
<span className={styles.modelId} title={model.modelId}>{model.modelId}</span>
|
|
72
|
+
<button
|
|
73
|
+
className={styles.disableBtn}
|
|
74
|
+
onClick={() => handleDisable(tool.toolMode, model.modelId)}
|
|
75
|
+
title="Remove model from config (backup saved)"
|
|
76
|
+
>
|
|
77
|
+
<IconTrash size={12} />
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
.overlay {
|
|
2
|
+
position: fixed;
|
|
3
|
+
inset: 0;
|
|
4
|
+
z-index: 200;
|
|
5
|
+
background: rgba(0, 0, 0, 0.6);
|
|
6
|
+
display: flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
animation: fadeIn 0.15s ease;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.modal {
|
|
13
|
+
background: var(--bg-primary, #0f0f17);
|
|
14
|
+
border: 1px solid var(--border, #1e1e2e);
|
|
15
|
+
border-radius: 12px;
|
|
16
|
+
width: 640px;
|
|
17
|
+
max-width: 95vw;
|
|
18
|
+
max-height: 85vh;
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.header {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: space-between;
|
|
28
|
+
padding: 16px 20px;
|
|
29
|
+
border-bottom: 1px solid var(--border, #1e1e2e);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.title {
|
|
33
|
+
margin: 0;
|
|
34
|
+
font-size: 16px;
|
|
35
|
+
font-weight: 600;
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: 8px;
|
|
39
|
+
color: var(--text-primary, #e0e0e0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.count {
|
|
43
|
+
font-size: 12px;
|
|
44
|
+
background: var(--bg-secondary, #14141f);
|
|
45
|
+
border: 1px solid var(--border, #1e1e2e);
|
|
46
|
+
border-radius: 10px;
|
|
47
|
+
padding: 2px 8px;
|
|
48
|
+
color: var(--text-muted, #888);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.headerActions {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: 6px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.refreshBtn {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
width: 28px;
|
|
62
|
+
height: 28px;
|
|
63
|
+
background: var(--bg-secondary, #14141f);
|
|
64
|
+
border: 1px solid var(--border, #1e1e2e);
|
|
65
|
+
border-radius: 6px;
|
|
66
|
+
color: var(--text-muted, #888);
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
&:hover { color: var(--text-primary, #e0e0e0); }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.closeBtn {
|
|
72
|
+
background: none;
|
|
73
|
+
border: none;
|
|
74
|
+
color: var(--text-muted, #888);
|
|
75
|
+
font-size: 18px;
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
padding: 4px 8px;
|
|
78
|
+
border-radius: 6px;
|
|
79
|
+
&:hover { background: var(--bg-hover, #1a1a2e); color: var(--text-primary, #e0e0e0); }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.body {
|
|
83
|
+
padding: 16px 20px;
|
|
84
|
+
overflow-y: auto;
|
|
85
|
+
flex: 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.empty {
|
|
89
|
+
color: var(--text-muted, #666);
|
|
90
|
+
font-size: 13px;
|
|
91
|
+
padding: 30px 0;
|
|
92
|
+
text-align: center;
|
|
93
|
+
line-height: 1.6;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.hint {
|
|
97
|
+
font-size: 12px;
|
|
98
|
+
color: var(--text-muted, #555);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Tool Groups */
|
|
102
|
+
.toolGroup {
|
|
103
|
+
margin-bottom: 16px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.toolHeader {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
gap: 8px;
|
|
110
|
+
margin-bottom: 6px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.toolEmoji {
|
|
114
|
+
font-size: 16px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.toolLabel {
|
|
118
|
+
font-size: 14px;
|
|
119
|
+
font-weight: 600;
|
|
120
|
+
color: var(--text-primary, #e0e0e0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.toolCount {
|
|
124
|
+
font-size: 11px;
|
|
125
|
+
color: var(--text-muted, #888);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.toolMissing {
|
|
129
|
+
font-size: 10px;
|
|
130
|
+
padding: 2px 6px;
|
|
131
|
+
background: rgba(255, 140, 0, 0.15);
|
|
132
|
+
color: #ff8c00;
|
|
133
|
+
border-radius: 4px;
|
|
134
|
+
text-transform: uppercase;
|
|
135
|
+
letter-spacing: 0.3px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* Model Rows */
|
|
139
|
+
.modelList {
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-direction: column;
|
|
142
|
+
gap: 3px;
|
|
143
|
+
padding-left: 24px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.modelRow {
|
|
147
|
+
display: flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
gap: 8px;
|
|
150
|
+
padding: 6px 8px;
|
|
151
|
+
border-radius: 6px;
|
|
152
|
+
font-size: 12px;
|
|
153
|
+
background: var(--bg-secondary, #14141f);
|
|
154
|
+
&:hover { background: var(--bg-hover, #1a1a2e); }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.modelIcon {
|
|
158
|
+
font-size: 12px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.modelName {
|
|
162
|
+
color: var(--text-primary, #e0e0e0);
|
|
163
|
+
min-width: 120px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.modelTier {
|
|
167
|
+
font-size: 10px;
|
|
168
|
+
font-weight: 700;
|
|
169
|
+
padding: 1px 5px;
|
|
170
|
+
background: rgba(0, 200, 255, 0.1);
|
|
171
|
+
color: #00c8ff;
|
|
172
|
+
border-radius: 3px;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.modelId {
|
|
176
|
+
flex: 1;
|
|
177
|
+
color: var(--text-muted, #888);
|
|
178
|
+
font-family: 'SF Mono', monospace;
|
|
179
|
+
overflow: hidden;
|
|
180
|
+
text-overflow: ellipsis;
|
|
181
|
+
white-space: nowrap;
|
|
182
|
+
font-size: 11px;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.disableBtn {
|
|
186
|
+
background: none;
|
|
187
|
+
border: none;
|
|
188
|
+
color: var(--text-muted, #666);
|
|
189
|
+
cursor: pointer;
|
|
190
|
+
padding: 4px;
|
|
191
|
+
border-radius: 4px;
|
|
192
|
+
display: flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
&:hover { color: #dc3545; background: rgba(220, 53, 69, 0.1); }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@keyframes fadeIn {
|
|
198
|
+
from { opacity: 0; }
|
|
199
|
+
to { opacity: 1; }
|
|
200
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/launch/IncompatibleFallbackModal.jsx
|
|
3
|
+
* @description Web equivalent of the TUI incompatible-fallback overlay. Lets the
|
|
4
|
+
* user switch to a compatible endpoint target or install a similar model endpoint.
|
|
5
|
+
*
|
|
6
|
+
* @functions IncompatibleFallbackModal → switch-tool and similar-model picker
|
|
7
|
+
* @exports IncompatibleFallbackModal
|
|
8
|
+
*/
|
|
9
|
+
import { IconAlertTriangle, IconPlugConnected, IconTool, IconX } from '@tabler/icons-react'
|
|
10
|
+
import {
|
|
11
|
+
findSimilarCompatibleModels,
|
|
12
|
+
getCompatibleTools,
|
|
13
|
+
getToolMeta,
|
|
14
|
+
} from '../../../../src/core/tool-metadata.js'
|
|
15
|
+
import styles from './LaunchModal.module.css'
|
|
16
|
+
|
|
17
|
+
export default function IncompatibleFallbackModal({ request, models = [], onClose, onSwitchToolLaunch, onLaunchSimilar }) {
|
|
18
|
+
if (!request?.model) return null
|
|
19
|
+
const { model, toolMode } = request
|
|
20
|
+
const activeMeta = getToolMeta(toolMode)
|
|
21
|
+
const compatibleTools = getCompatibleTools(model.providerKey)
|
|
22
|
+
const similar = findSimilarCompatibleModels(model.sweScore, toolMode, models, 3)
|
|
23
|
+
.filter((candidate) => candidate.modelId !== model.modelId || candidate.providerKey !== model.providerKey)
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className={styles.backdrop} onClick={onClose}>
|
|
27
|
+
<div className={styles.modalWide} onClick={(event) => event.stopPropagation()}>
|
|
28
|
+
<div className={styles.header}>
|
|
29
|
+
<div>
|
|
30
|
+
<p className={styles.eyebrow}>Incompatible model</p>
|
|
31
|
+
<h2><IconAlertTriangle size={18} /> {model.label} cannot be installed in {activeMeta.emoji} {activeMeta.label}</h2>
|
|
32
|
+
</div>
|
|
33
|
+
<button className={styles.close} onClick={onClose} aria-label="Close"><IconX size={18} /></button>
|
|
34
|
+
</div>
|
|
35
|
+
<div className={styles.grid}>
|
|
36
|
+
<section className={styles.card}>
|
|
37
|
+
<h3><IconTool size={15} /> Switch tool</h3>
|
|
38
|
+
<p className={styles.muted}>This provider can be configured in these tools. Pick one and FCM installs the endpoint.</p>
|
|
39
|
+
<div className={styles.stack}>
|
|
40
|
+
{compatibleTools.map((mode) => {
|
|
41
|
+
const meta = getToolMeta(mode)
|
|
42
|
+
return (
|
|
43
|
+
<button key={mode} className={styles.option} onClick={() => onSwitchToolLaunch?.(mode, model)}>
|
|
44
|
+
<span>{meta.emoji}</span>
|
|
45
|
+
<strong>{meta.label}</strong>
|
|
46
|
+
</button>
|
|
47
|
+
)
|
|
48
|
+
})}
|
|
49
|
+
</div>
|
|
50
|
+
</section>
|
|
51
|
+
<section className={styles.card}>
|
|
52
|
+
<h3><IconPlugConnected size={15} /> Similar models</h3>
|
|
53
|
+
<p className={styles.muted}>Or keep {activeMeta.label} and install a compatible model close to the same SWE score.</p>
|
|
54
|
+
<div className={styles.stack}>
|
|
55
|
+
{similar.length === 0 && <p className={styles.muted}>No close compatible alternative found yet.</p>}
|
|
56
|
+
{similar.map((candidate) => (
|
|
57
|
+
<button key={`${candidate.providerKey}/${candidate.modelId}`} className={styles.option} onClick={() => onLaunchSimilar?.(candidate)}>
|
|
58
|
+
<span>{candidate.tier}</span>
|
|
59
|
+
<strong>{candidate.label}</strong>
|
|
60
|
+
<small>{candidate.providerKey} · {candidate.sweScore}</small>
|
|
61
|
+
</button>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
</section>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/launch/LaunchButton.jsx
|
|
3
|
+
* @description Reusable M3 endpoint install action for table rows, DetailPanel, and Recommend results.
|
|
4
|
+
* @functions LaunchButton → renders a plug button that installs the selected model endpoint
|
|
5
|
+
* @exports LaunchButton
|
|
6
|
+
*/
|
|
7
|
+
import { IconPlugConnected } from '@tabler/icons-react'
|
|
8
|
+
import { getToolMeta } from '../../../../src/core/tool-metadata.js'
|
|
9
|
+
import styles from './LaunchButton.module.css'
|
|
10
|
+
|
|
11
|
+
export default function LaunchButton({ model, toolMode = 'opencode', onLaunch, variant = 'default', disabled = false }) {
|
|
12
|
+
const meta = getToolMeta(toolMode)
|
|
13
|
+
const label = variant === 'icon' ? 'Install endpoint' : `Install endpoint in ${meta.emoji} ${meta.label}`
|
|
14
|
+
return (
|
|
15
|
+
<button
|
|
16
|
+
type="button"
|
|
17
|
+
className={`${styles.button} ${styles[variant] || ''}`}
|
|
18
|
+
disabled={disabled || !model}
|
|
19
|
+
onClick={(event) => {
|
|
20
|
+
event.stopPropagation()
|
|
21
|
+
if (model) onLaunch?.(model)
|
|
22
|
+
}}
|
|
23
|
+
title={model ? `Install ${model.label} endpoint in ${meta.label}` : 'Select a model first'}
|
|
24
|
+
aria-label={model ? `Install ${model.label} endpoint in ${meta.label}` : 'Install selected model endpoint'}
|
|
25
|
+
>
|
|
26
|
+
<IconPlugConnected size={variant === 'icon' ? 13 : 14} stroke={1.8} />
|
|
27
|
+
{variant !== 'icon' && <span>{label}</span>}
|
|
28
|
+
</button>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** @file web/src/components/launch/LaunchButton.module.css */
|
|
2
|
+
.button {
|
|
3
|
+
display: inline-flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
justify-content: center;
|
|
6
|
+
gap: 7px;
|
|
7
|
+
padding: 8px 12px;
|
|
8
|
+
border: 1px solid rgba(61, 220, 132, 0.45);
|
|
9
|
+
border-radius: 6px;
|
|
10
|
+
background: rgba(61, 220, 132, 0.12);
|
|
11
|
+
color: #3ddc84;
|
|
12
|
+
cursor: pointer;
|
|
13
|
+
font-size: 12px;
|
|
14
|
+
font-weight: 800;
|
|
15
|
+
font-family: var(--font-mono);
|
|
16
|
+
transition: transform 120ms, background 120ms, border-color 120ms;
|
|
17
|
+
}
|
|
18
|
+
.button:hover:not(:disabled) {
|
|
19
|
+
transform: translateY(-1px);
|
|
20
|
+
background: rgba(61, 220, 132, 0.20);
|
|
21
|
+
border-color: rgba(61, 220, 132, 0.75);
|
|
22
|
+
}
|
|
23
|
+
.button:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
24
|
+
.icon {
|
|
25
|
+
width: 26px;
|
|
26
|
+
height: 24px;
|
|
27
|
+
padding: 0;
|
|
28
|
+
opacity: 0.72;
|
|
29
|
+
}
|
|
30
|
+
.icon:hover:not(:disabled) { opacity: 1; }
|
|
31
|
+
:global([data-theme="light"]) .button {
|
|
32
|
+
color: #087d42;
|
|
33
|
+
border-color: rgba(8, 125, 66, 0.35);
|
|
34
|
+
background: rgba(8, 125, 66, 0.08);
|
|
35
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/** @file web/src/components/launch/LaunchModal.module.css */
|
|
2
|
+
.backdrop {
|
|
3
|
+
position: fixed;
|
|
4
|
+
inset: 0;
|
|
5
|
+
z-index: 500;
|
|
6
|
+
display: flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
padding: 24px;
|
|
10
|
+
background: rgba(0, 0, 0, 0.62);
|
|
11
|
+
backdrop-filter: blur(4px);
|
|
12
|
+
}
|
|
13
|
+
.modal,
|
|
14
|
+
.modalWide {
|
|
15
|
+
width: min(560px, 96vw);
|
|
16
|
+
max-height: min(760px, 92vh);
|
|
17
|
+
overflow: auto;
|
|
18
|
+
border: 1px solid var(--color-border);
|
|
19
|
+
border-radius: 14px;
|
|
20
|
+
background: var(--color-bg-elevated);
|
|
21
|
+
color: var(--color-text);
|
|
22
|
+
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.45);
|
|
23
|
+
}
|
|
24
|
+
.modalWide { width: min(920px, 96vw); }
|
|
25
|
+
.header {
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: flex-start;
|
|
28
|
+
justify-content: space-between;
|
|
29
|
+
gap: 18px;
|
|
30
|
+
padding: 20px 22px;
|
|
31
|
+
border-bottom: 1px solid var(--color-border);
|
|
32
|
+
}
|
|
33
|
+
.header h2 {
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 8px;
|
|
37
|
+
margin: 4px 0 0;
|
|
38
|
+
font-size: 18px;
|
|
39
|
+
}
|
|
40
|
+
.eyebrow {
|
|
41
|
+
margin: 0;
|
|
42
|
+
color: var(--color-accent);
|
|
43
|
+
font-size: 11px;
|
|
44
|
+
font-weight: 900;
|
|
45
|
+
text-transform: uppercase;
|
|
46
|
+
letter-spacing: .9px;
|
|
47
|
+
}
|
|
48
|
+
.close {
|
|
49
|
+
display: inline-flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
width: 30px;
|
|
53
|
+
height: 30px;
|
|
54
|
+
border: 1px solid var(--color-border);
|
|
55
|
+
border-radius: 7px;
|
|
56
|
+
background: var(--color-surface);
|
|
57
|
+
color: var(--color-text-muted);
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
}
|
|
60
|
+
.close:hover { color: var(--color-text); background: var(--color-bg-hover); }
|
|
61
|
+
.body { padding: 20px 22px; display: flex; flex-direction: column; gap: 12px; }
|
|
62
|
+
.command {
|
|
63
|
+
display: block;
|
|
64
|
+
padding: 12px;
|
|
65
|
+
border: 1px solid var(--color-border);
|
|
66
|
+
border-radius: 8px;
|
|
67
|
+
background: var(--color-surface);
|
|
68
|
+
color: var(--color-accent);
|
|
69
|
+
white-space: pre-wrap;
|
|
70
|
+
word-break: break-word;
|
|
71
|
+
}
|
|
72
|
+
.note,
|
|
73
|
+
.muted { color: var(--color-text-muted); font-size: 13px; line-height: 1.45; }
|
|
74
|
+
.docs { display: inline-flex; align-items: center; gap: 5px; color: var(--color-accent); font-weight: 700; text-decoration: none; }
|
|
75
|
+
.error { color: #ff5a7a; font-weight: 700; }
|
|
76
|
+
.footer {
|
|
77
|
+
display: flex;
|
|
78
|
+
justify-content: flex-end;
|
|
79
|
+
gap: 10px;
|
|
80
|
+
padding: 16px 22px 20px;
|
|
81
|
+
border-top: 1px solid var(--color-border);
|
|
82
|
+
}
|
|
83
|
+
.primary,
|
|
84
|
+
.secondary,
|
|
85
|
+
.option {
|
|
86
|
+
border: 1px solid var(--color-border);
|
|
87
|
+
border-radius: 8px;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
font-weight: 800;
|
|
90
|
+
}
|
|
91
|
+
.primary {
|
|
92
|
+
padding: 9px 14px;
|
|
93
|
+
background: var(--color-accent);
|
|
94
|
+
color: var(--color-bg);
|
|
95
|
+
border-color: var(--color-accent);
|
|
96
|
+
}
|
|
97
|
+
.primary:disabled { opacity: .55; cursor: not-allowed; }
|
|
98
|
+
.secondary {
|
|
99
|
+
padding: 9px 14px;
|
|
100
|
+
background: var(--color-surface);
|
|
101
|
+
color: var(--color-text);
|
|
102
|
+
}
|
|
103
|
+
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; padding: 18px; }
|
|
104
|
+
.card {
|
|
105
|
+
padding: 16px;
|
|
106
|
+
border: 1px solid var(--color-border);
|
|
107
|
+
border-radius: 12px;
|
|
108
|
+
background: var(--color-surface);
|
|
109
|
+
}
|
|
110
|
+
.card h3 { display: flex; align-items: center; gap: 7px; margin: 0 0 6px; font-size: 14px; }
|
|
111
|
+
.stack { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
|
|
112
|
+
.option {
|
|
113
|
+
width: 100%;
|
|
114
|
+
display: grid;
|
|
115
|
+
grid-template-columns: auto 1fr auto;
|
|
116
|
+
align-items: center;
|
|
117
|
+
gap: 10px;
|
|
118
|
+
padding: 10px;
|
|
119
|
+
background: var(--color-bg-card);
|
|
120
|
+
color: var(--color-text);
|
|
121
|
+
text-align: left;
|
|
122
|
+
}
|
|
123
|
+
.option:hover { border-color: var(--color-accent); background: var(--color-bg-hover); }
|
|
124
|
+
.option small { color: var(--color-text-dim); font-family: var(--font-mono); font-size: 10px; }
|
|
125
|
+
@media (max-width: 720px) { .grid { grid-template-columns: 1fr; } }
|