groove-dev 0.13.0 → 0.14.0
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/docs/SKILLS-API-SPEC.md +277 -0
- package/node_modules/@groove-dev/daemon/skills-registry.json +273 -21
- package/node_modules/@groove-dev/daemon/src/api.js +2 -2
- package/node_modules/@groove-dev/daemon/src/skills.js +34 -6
- package/node_modules/@groove-dev/gui/dist/assets/index-C-zD4i2v.js +74 -0
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/src/views/SkillsMarketplace.jsx +724 -181
- package/package.json +1 -1
- package/packages/daemon/skills-registry.json +273 -21
- package/packages/daemon/src/api.js +2 -2
- package/packages/daemon/src/skills.js +34 -6
- package/packages/gui/dist/assets/index-C-zD4i2v.js +74 -0
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/src/views/SkillsMarketplace.jsx +724 -181
- package/node_modules/@groove-dev/gui/dist/assets/index-C4fB8_Qg.js +0 -74
- package/packages/gui/dist/assets/index-C4fB8_Qg.js +0 -74
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// GROOVE GUI — Skills Marketplace
|
|
1
|
+
// GROOVE GUI — Skills Marketplace (App Store)
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
|
-
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
5
5
|
|
|
6
6
|
const CATEGORY_LABELS = {
|
|
7
7
|
all: 'All Skills',
|
|
@@ -13,22 +13,328 @@ const CATEGORY_LABELS = {
|
|
|
13
13
|
specialized: 'Specialized',
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
const CATEGORY_ICONS = {
|
|
17
|
+
design: '\u2728',
|
|
18
|
+
quality: '\u2714',
|
|
19
|
+
devtools: '\u2699',
|
|
20
|
+
workflow: '\u21BB',
|
|
21
|
+
security: '\u26E8',
|
|
22
|
+
specialized: '\u2606',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const SORT_OPTIONS = [
|
|
26
|
+
{ id: 'popular', label: 'Popular' },
|
|
27
|
+
{ id: 'rating', label: 'Top Rated' },
|
|
28
|
+
{ id: 'newest', label: 'Newest' },
|
|
29
|
+
{ id: 'name', label: 'A\u2013Z' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function formatDownloads(n) {
|
|
33
|
+
if (!n) return '0';
|
|
34
|
+
if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
|
|
35
|
+
return String(n);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderStars(rating) {
|
|
39
|
+
if (!rating) return null;
|
|
40
|
+
const full = Math.floor(rating);
|
|
41
|
+
const half = rating - full >= 0.3;
|
|
42
|
+
const stars = [];
|
|
43
|
+
for (let i = 0; i < 5; i++) {
|
|
44
|
+
if (i < full) stars.push('\u2605');
|
|
45
|
+
else if (i === full && half) stars.push('\u2606');
|
|
46
|
+
else stars.push('\u2606');
|
|
47
|
+
}
|
|
48
|
+
return stars.join('');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sortSkills(skills, sortBy) {
|
|
52
|
+
const sorted = [...skills];
|
|
53
|
+
switch (sortBy) {
|
|
54
|
+
case 'popular': return sorted.sort((a, b) => (b.downloads || 0) - (a.downloads || 0));
|
|
55
|
+
case 'rating': return sorted.sort((a, b) => (b.rating || 0) - (a.rating || 0));
|
|
56
|
+
case 'name': return sorted.sort((a, b) => a.name.localeCompare(b.name));
|
|
57
|
+
case 'newest': return sorted.reverse();
|
|
58
|
+
default: return sorted;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Skill Detail Modal ──────────────────────────────────────────────
|
|
63
|
+
function SkillDetailModal({ skill, content, installing, onInstall, onUninstall, onClose }) {
|
|
64
|
+
if (!skill) return null;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div style={modal.overlay} onClick={onClose}>
|
|
68
|
+
<div style={modal.container} onClick={(e) => e.stopPropagation()}>
|
|
69
|
+
{/* Close */}
|
|
70
|
+
<button onClick={onClose} style={modal.closeBtn}>×</button>
|
|
71
|
+
|
|
72
|
+
{/* Header */}
|
|
73
|
+
<div style={modal.header}>
|
|
74
|
+
<div style={{
|
|
75
|
+
...modal.icon,
|
|
76
|
+
background: skill.installed
|
|
77
|
+
? 'var(--green)'
|
|
78
|
+
: skill.price > 0 ? 'var(--purple)' : 'var(--accent)',
|
|
79
|
+
}}>
|
|
80
|
+
{skill.icon || skill.name.charAt(0)}
|
|
81
|
+
</div>
|
|
82
|
+
<div style={modal.headerInfo}>
|
|
83
|
+
<div style={modal.name}>{skill.name}</div>
|
|
84
|
+
<div style={modal.author}>
|
|
85
|
+
by {skill.author}
|
|
86
|
+
{skill.source === 'claude-official' && (
|
|
87
|
+
<span style={modal.verifiedBadge} title="Official">✓</span>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
<div style={modal.meta}>
|
|
91
|
+
{skill.rating > 0 && (
|
|
92
|
+
<span style={modal.metaItem}>
|
|
93
|
+
<span style={{ color: 'var(--amber)' }}>{renderStars(skill.rating)}</span>
|
|
94
|
+
{' '}{skill.rating} ({skill.ratingCount || 0})
|
|
95
|
+
</span>
|
|
96
|
+
)}
|
|
97
|
+
{skill.downloads > 0 && (
|
|
98
|
+
<span style={modal.metaItem}>
|
|
99
|
+
{'\u2913'} {formatDownloads(skill.downloads)}
|
|
100
|
+
</span>
|
|
101
|
+
)}
|
|
102
|
+
<span style={modal.metaItem}>
|
|
103
|
+
{CATEGORY_ICONS[skill.category] || ''} {CATEGORY_LABELS[skill.category] || skill.category}
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
<div style={modal.headerAction}>
|
|
108
|
+
{skill.installed ? (
|
|
109
|
+
<button
|
|
110
|
+
onClick={() => onUninstall(skill.id)}
|
|
111
|
+
disabled={installing === skill.id}
|
|
112
|
+
style={modal.uninstallBtn}
|
|
113
|
+
>
|
|
114
|
+
{installing === skill.id ? 'Removing...' : 'Uninstall'}
|
|
115
|
+
</button>
|
|
116
|
+
) : (
|
|
117
|
+
<button
|
|
118
|
+
onClick={() => onInstall(skill.id)}
|
|
119
|
+
disabled={installing === skill.id}
|
|
120
|
+
style={skill.price > 0 ? modal.buyBtn : modal.installBtn}
|
|
121
|
+
>
|
|
122
|
+
{installing === skill.id
|
|
123
|
+
? 'Installing...'
|
|
124
|
+
: skill.price > 0
|
|
125
|
+
? `$${skill.price.toFixed(2)}`
|
|
126
|
+
: 'Install'}
|
|
127
|
+
</button>
|
|
128
|
+
)}
|
|
129
|
+
{skill.price === 0 && <div style={modal.freeLabel}>Free</div>}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{/* Description */}
|
|
134
|
+
<div style={modal.section}>
|
|
135
|
+
<div style={modal.sectionTitle}>About</div>
|
|
136
|
+
<div style={modal.description}>{skill.description}</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{/* Tags */}
|
|
140
|
+
<div style={modal.section}>
|
|
141
|
+
<div style={modal.sectionTitle}>Tags</div>
|
|
142
|
+
<div style={modal.tagRow}>
|
|
143
|
+
{(skill.tags || []).map((t) => (
|
|
144
|
+
<span key={t} style={modal.tag}>{t}</span>
|
|
145
|
+
))}
|
|
146
|
+
{(skill.roles || []).map((r) => (
|
|
147
|
+
<span key={r} style={modal.roleTag}>{r}</span>
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{/* Author Profile */}
|
|
153
|
+
{skill.authorProfile && (
|
|
154
|
+
<div style={modal.section}>
|
|
155
|
+
<div style={modal.sectionTitle}>Developer</div>
|
|
156
|
+
<div style={modal.authorCard}>
|
|
157
|
+
<div style={modal.authorAvatar}>
|
|
158
|
+
{skill.authorProfile.avatar
|
|
159
|
+
? <img src={skill.authorProfile.avatar} alt="" style={{ width: '100%', height: '100%', borderRadius: 6 }} />
|
|
160
|
+
: skill.author.charAt(0)
|
|
161
|
+
}
|
|
162
|
+
</div>
|
|
163
|
+
<div style={modal.authorDetails}>
|
|
164
|
+
<div style={modal.authorName}>{skill.author}</div>
|
|
165
|
+
<div style={modal.authorLinks}>
|
|
166
|
+
{skill.authorProfile.website && (
|
|
167
|
+
<a href={skill.authorProfile.website} target="_blank" rel="noopener noreferrer" style={modal.authorLink}>
|
|
168
|
+
Website
|
|
169
|
+
</a>
|
|
170
|
+
)}
|
|
171
|
+
{skill.authorProfile.github && (
|
|
172
|
+
<a href={`https://github.com/${skill.authorProfile.github}`} target="_blank" rel="noopener noreferrer" style={modal.authorLink}>
|
|
173
|
+
GitHub
|
|
174
|
+
</a>
|
|
175
|
+
)}
|
|
176
|
+
{skill.authorProfile.twitter && (
|
|
177
|
+
<a href={`https://x.com/${skill.authorProfile.twitter}`} target="_blank" rel="noopener noreferrer" style={modal.authorLink}>
|
|
178
|
+
@{skill.authorProfile.twitter}
|
|
179
|
+
</a>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{/* Content Preview */}
|
|
188
|
+
{content && (
|
|
189
|
+
<div style={modal.section}>
|
|
190
|
+
<div style={modal.sectionTitle}>Skill Instructions</div>
|
|
191
|
+
<pre style={modal.contentPre}>
|
|
192
|
+
{content.replace(/^---[\s\S]*?---\n/, '').trim().slice(0, 2000)}
|
|
193
|
+
{content.length > 2000 ? '\n...' : ''}
|
|
194
|
+
</pre>
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Featured Banner ─────────────────────────────────────────────────
|
|
203
|
+
function FeaturedBanner({ skills, onSelect }) {
|
|
204
|
+
const featured = skills.filter((s) => s.featured).slice(0, 3);
|
|
205
|
+
if (featured.length === 0) return null;
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<div style={styles.featuredSection}>
|
|
209
|
+
<div style={styles.featuredLabel}>Featured</div>
|
|
210
|
+
<div style={styles.featuredRow}>
|
|
211
|
+
{featured.map((skill) => (
|
|
212
|
+
<div
|
|
213
|
+
key={skill.id}
|
|
214
|
+
onClick={() => onSelect(skill)}
|
|
215
|
+
style={styles.featuredCard}
|
|
216
|
+
onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'var(--accent)'; e.currentTarget.style.transform = 'translateY(-1px)'; }}
|
|
217
|
+
onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.transform = 'none'; }}
|
|
218
|
+
>
|
|
219
|
+
<div style={styles.featuredGradient}>
|
|
220
|
+
<div style={{
|
|
221
|
+
...styles.featuredIcon,
|
|
222
|
+
background: skill.installed ? 'var(--green)' : 'var(--accent)',
|
|
223
|
+
}}>
|
|
224
|
+
{skill.icon || skill.name.charAt(0)}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
<div style={styles.featuredInfo}>
|
|
228
|
+
<div style={styles.featuredName}>{skill.name}</div>
|
|
229
|
+
<div style={styles.featuredAuthor}>{skill.author}</div>
|
|
230
|
+
<div style={styles.featuredDesc}>{skill.description}</div>
|
|
231
|
+
<div style={styles.featuredMeta}>
|
|
232
|
+
{skill.rating > 0 && (
|
|
233
|
+
<span style={{ color: 'var(--amber)', fontSize: 10 }}>
|
|
234
|
+
{renderStars(skill.rating)} {skill.rating}
|
|
235
|
+
</span>
|
|
236
|
+
)}
|
|
237
|
+
<span style={{ color: 'var(--text-muted)', fontSize: 10 }}>
|
|
238
|
+
{'\u2913'} {formatDownloads(skill.downloads)}
|
|
239
|
+
</span>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
{skill.installed && (
|
|
243
|
+
<div style={styles.featuredBadge}>installed</div>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
))}
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── Skill Card ──────────────────────────────────────────────────────
|
|
253
|
+
function SkillCard({ skill, onSelect, hovered, onHover }) {
|
|
254
|
+
return (
|
|
255
|
+
<div
|
|
256
|
+
onClick={() => onSelect(skill)}
|
|
257
|
+
onMouseEnter={() => onHover(skill.id)}
|
|
258
|
+
onMouseLeave={() => onHover(null)}
|
|
259
|
+
style={{
|
|
260
|
+
...styles.card,
|
|
261
|
+
borderColor: skill.installed
|
|
262
|
+
? 'var(--green)'
|
|
263
|
+
: hovered ? 'var(--accent)' : 'var(--border)',
|
|
264
|
+
background: hovered ? 'var(--bg-hover)' : 'var(--bg-surface)',
|
|
265
|
+
transform: hovered ? 'translateY(-1px)' : 'none',
|
|
266
|
+
}}
|
|
267
|
+
>
|
|
268
|
+
{/* Top row: icon + info + badges */}
|
|
269
|
+
<div style={styles.cardTop}>
|
|
270
|
+
<div style={{
|
|
271
|
+
...styles.cardIcon,
|
|
272
|
+
background: skill.installed
|
|
273
|
+
? 'var(--green)'
|
|
274
|
+
: skill.price > 0 ? 'var(--purple)' : 'var(--accent)',
|
|
275
|
+
}}>
|
|
276
|
+
{skill.icon || skill.name.charAt(0)}
|
|
277
|
+
</div>
|
|
278
|
+
<div style={styles.cardInfo}>
|
|
279
|
+
<div style={styles.cardName}>
|
|
280
|
+
{skill.name}
|
|
281
|
+
{skill.source === 'claude-official' && (
|
|
282
|
+
<span style={styles.verifiedDot} title="Official">{'\u2713'}</span>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
<div style={styles.cardAuthor}>{skill.author}</div>
|
|
286
|
+
</div>
|
|
287
|
+
{skill.installed && (
|
|
288
|
+
<div style={styles.installedBadge}>installed</div>
|
|
289
|
+
)}
|
|
290
|
+
{skill.price > 0 && !skill.installed && (
|
|
291
|
+
<div style={styles.priceBadge}>${skill.price.toFixed(2)}</div>
|
|
292
|
+
)}
|
|
293
|
+
{skill.price === 0 && !skill.installed && (
|
|
294
|
+
<div style={styles.freeBadge}>Free</div>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
{/* Description */}
|
|
299
|
+
<div style={styles.cardDesc}>{skill.description}</div>
|
|
300
|
+
|
|
301
|
+
{/* Bottom: rating + downloads + category */}
|
|
302
|
+
<div style={styles.cardBottom}>
|
|
303
|
+
<div style={styles.cardStats}>
|
|
304
|
+
{skill.rating > 0 && (
|
|
305
|
+
<span style={styles.cardRating}>
|
|
306
|
+
<span style={{ color: 'var(--amber)' }}>{'\u2605'}</span> {skill.rating}
|
|
307
|
+
</span>
|
|
308
|
+
)}
|
|
309
|
+
{skill.downloads > 0 && (
|
|
310
|
+
<span style={styles.cardDownloads}>
|
|
311
|
+
{'\u2913'} {formatDownloads(skill.downloads)}
|
|
312
|
+
</span>
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
<span style={styles.catTag}>
|
|
316
|
+
{CATEGORY_ICONS[skill.category] || ''} {CATEGORY_LABELS[skill.category] || skill.category}
|
|
317
|
+
</span>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── Main Marketplace ────────────────────────────────────────────────
|
|
16
324
|
export default function SkillsMarketplace() {
|
|
17
325
|
const [skills, setSkills] = useState([]);
|
|
18
326
|
const [categories, setCategories] = useState([]);
|
|
19
327
|
const [search, setSearch] = useState('');
|
|
20
328
|
const [category, setCategory] = useState('all');
|
|
329
|
+
const [sortBy, setSortBy] = useState('popular');
|
|
330
|
+
const [tab, setTab] = useState('browse'); // browse | installed
|
|
21
331
|
const [loading, setLoading] = useState(true);
|
|
22
332
|
const [installing, setInstalling] = useState(null);
|
|
23
|
-
const [
|
|
333
|
+
const [selectedSkill, setSelectedSkill] = useState(null);
|
|
24
334
|
const [skillContent, setSkillContent] = useState(null);
|
|
25
335
|
const [hovered, setHovered] = useState(null);
|
|
26
336
|
|
|
27
|
-
|
|
28
|
-
fetchSkills();
|
|
29
|
-
}, [search, category]);
|
|
30
|
-
|
|
31
|
-
async function fetchSkills() {
|
|
337
|
+
const fetchSkills = useCallback(async () => {
|
|
32
338
|
setLoading(true);
|
|
33
339
|
try {
|
|
34
340
|
const params = new URLSearchParams();
|
|
@@ -40,13 +346,23 @@ export default function SkillsMarketplace() {
|
|
|
40
346
|
setCategories(data.categories || []);
|
|
41
347
|
} catch { /* ignore */ }
|
|
42
348
|
setLoading(false);
|
|
43
|
-
}
|
|
349
|
+
}, [search, category]);
|
|
350
|
+
|
|
351
|
+
useEffect(() => { fetchSkills(); }, [fetchSkills]);
|
|
44
352
|
|
|
45
353
|
async function handleInstall(id) {
|
|
46
354
|
setInstalling(id);
|
|
47
355
|
try {
|
|
48
356
|
await fetch(`/api/skills/${id}/install`, { method: 'POST' });
|
|
49
357
|
await fetchSkills();
|
|
358
|
+
// Update selected skill state
|
|
359
|
+
setSkills((prev) => {
|
|
360
|
+
const updated = prev.find((s) => s.id === id);
|
|
361
|
+
if (updated && selectedSkill?.id === id) {
|
|
362
|
+
setSelectedSkill({ ...updated, installed: true });
|
|
363
|
+
}
|
|
364
|
+
return prev;
|
|
365
|
+
});
|
|
50
366
|
} catch { /* ignore */ }
|
|
51
367
|
setInstalling(null);
|
|
52
368
|
}
|
|
@@ -55,20 +371,17 @@ export default function SkillsMarketplace() {
|
|
|
55
371
|
setInstalling(id);
|
|
56
372
|
try {
|
|
57
373
|
await fetch(`/api/skills/${id}`, { method: 'DELETE' });
|
|
58
|
-
setExpandedSkill(null);
|
|
59
|
-
setSkillContent(null);
|
|
60
374
|
await fetchSkills();
|
|
375
|
+
if (selectedSkill?.id === id) {
|
|
376
|
+
setSelectedSkill((prev) => prev ? { ...prev, installed: false } : null);
|
|
377
|
+
setSkillContent(null);
|
|
378
|
+
}
|
|
61
379
|
} catch { /* ignore */ }
|
|
62
380
|
setInstalling(null);
|
|
63
381
|
}
|
|
64
382
|
|
|
65
|
-
async function
|
|
66
|
-
|
|
67
|
-
setExpandedSkill(null);
|
|
68
|
-
setSkillContent(null);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
setExpandedSkill(skill.id);
|
|
383
|
+
async function handleSelect(skill) {
|
|
384
|
+
setSelectedSkill(skill);
|
|
72
385
|
setSkillContent(null);
|
|
73
386
|
if (skill.installed) {
|
|
74
387
|
try {
|
|
@@ -79,176 +392,224 @@ export default function SkillsMarketplace() {
|
|
|
79
392
|
}
|
|
80
393
|
}
|
|
81
394
|
|
|
82
|
-
const
|
|
395
|
+
const installedSkills = skills.filter((s) => s.installed);
|
|
396
|
+
const displaySkills = tab === 'installed'
|
|
397
|
+
? sortSkills(installedSkills, sortBy)
|
|
398
|
+
: sortSkills(skills, sortBy);
|
|
83
399
|
|
|
84
400
|
return (
|
|
85
401
|
<div style={styles.root}>
|
|
86
|
-
{/* Header */}
|
|
87
|
-
<div style={styles.
|
|
88
|
-
<div>
|
|
89
|
-
<div style={styles.title}>Skills
|
|
90
|
-
<div style={styles.
|
|
91
|
-
|
|
402
|
+
{/* Header bar */}
|
|
403
|
+
<div style={styles.headerBar}>
|
|
404
|
+
<div style={styles.headerLeft}>
|
|
405
|
+
<div style={styles.title}>Skills Store</div>
|
|
406
|
+
<div style={styles.headerTabs}>
|
|
407
|
+
<button
|
|
408
|
+
onClick={() => setTab('browse')}
|
|
409
|
+
style={{
|
|
410
|
+
...styles.headerTab,
|
|
411
|
+
color: tab === 'browse' ? 'var(--text-bright)' : 'var(--text-dim)',
|
|
412
|
+
borderBottom: tab === 'browse' ? '2px solid var(--accent)' : '2px solid transparent',
|
|
413
|
+
}}
|
|
414
|
+
>
|
|
415
|
+
Browse
|
|
416
|
+
</button>
|
|
417
|
+
<button
|
|
418
|
+
onClick={() => setTab('installed')}
|
|
419
|
+
style={{
|
|
420
|
+
...styles.headerTab,
|
|
421
|
+
color: tab === 'installed' ? 'var(--text-bright)' : 'var(--text-dim)',
|
|
422
|
+
borderBottom: tab === 'installed' ? '2px solid var(--green)' : '2px solid transparent',
|
|
423
|
+
}}
|
|
424
|
+
>
|
|
425
|
+
My Skills ({installedSkills.length})
|
|
426
|
+
</button>
|
|
92
427
|
</div>
|
|
93
428
|
</div>
|
|
429
|
+
<div style={styles.headerRight}>
|
|
430
|
+
<span style={styles.headerCount}>
|
|
431
|
+
{skills.length} skills
|
|
432
|
+
</span>
|
|
433
|
+
</div>
|
|
94
434
|
</div>
|
|
95
435
|
|
|
96
|
-
{/* Search +
|
|
436
|
+
{/* Search + Filters */}
|
|
97
437
|
<div style={styles.toolbar}>
|
|
98
|
-
<
|
|
99
|
-
style={styles.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
438
|
+
<div style={styles.searchRow}>
|
|
439
|
+
<div style={styles.searchWrap}>
|
|
440
|
+
<span style={styles.searchIcon}>{'\u2315'}</span>
|
|
441
|
+
<input
|
|
442
|
+
style={styles.search}
|
|
443
|
+
placeholder="Search skills..."
|
|
444
|
+
value={search}
|
|
445
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
446
|
+
/>
|
|
447
|
+
</div>
|
|
448
|
+
<div style={styles.sortWrap}>
|
|
449
|
+
{SORT_OPTIONS.map((opt) => (
|
|
450
|
+
<button
|
|
451
|
+
key={opt.id}
|
|
452
|
+
onClick={() => setSortBy(opt.id)}
|
|
453
|
+
style={{
|
|
454
|
+
...styles.sortBtn,
|
|
455
|
+
color: sortBy === opt.id ? 'var(--text-bright)' : 'var(--text-muted)',
|
|
456
|
+
background: sortBy === opt.id ? 'var(--bg-active)' : 'transparent',
|
|
457
|
+
}}
|
|
458
|
+
>
|
|
459
|
+
{opt.label}
|
|
460
|
+
</button>
|
|
461
|
+
))}
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
{tab === 'browse' && (
|
|
465
|
+
<div style={styles.catRow}>
|
|
115
466
|
<button
|
|
116
|
-
|
|
117
|
-
onClick={() => setCategory(cat.id)}
|
|
467
|
+
onClick={() => setCategory('all')}
|
|
118
468
|
style={{
|
|
119
469
|
...styles.catBtn,
|
|
120
|
-
...(category ===
|
|
470
|
+
...(category === 'all' ? styles.catBtnActive : {}),
|
|
121
471
|
}}
|
|
122
472
|
>
|
|
123
|
-
|
|
473
|
+
All
|
|
124
474
|
</button>
|
|
125
|
-
|
|
126
|
-
|
|
475
|
+
{categories.map((cat) => (
|
|
476
|
+
<button
|
|
477
|
+
key={cat.id}
|
|
478
|
+
onClick={() => setCategory(cat.id)}
|
|
479
|
+
style={{
|
|
480
|
+
...styles.catBtn,
|
|
481
|
+
...(category === cat.id ? styles.catBtnActive : {}),
|
|
482
|
+
}}
|
|
483
|
+
>
|
|
484
|
+
{CATEGORY_ICONS[cat.id] || ''} {CATEGORY_LABELS[cat.id] || cat.id} ({cat.count})
|
|
485
|
+
</button>
|
|
486
|
+
))}
|
|
487
|
+
</div>
|
|
488
|
+
)}
|
|
127
489
|
</div>
|
|
128
490
|
|
|
129
|
-
{/*
|
|
130
|
-
<div style={styles.
|
|
491
|
+
{/* Scrollable content */}
|
|
492
|
+
<div style={styles.scrollArea}>
|
|
493
|
+
{/* Featured banner — only on browse tab, no search, all category */}
|
|
494
|
+
{tab === 'browse' && !search && category === 'all' && (
|
|
495
|
+
<FeaturedBanner skills={skills} onSelect={handleSelect} />
|
|
496
|
+
)}
|
|
497
|
+
|
|
498
|
+
{/* Grid */}
|
|
131
499
|
{loading && skills.length === 0 && (
|
|
132
500
|
<div style={styles.empty}>Loading skills...</div>
|
|
133
501
|
)}
|
|
134
502
|
|
|
135
|
-
{!loading &&
|
|
136
|
-
<div style={styles.empty}>
|
|
503
|
+
{!loading && displaySkills.length === 0 && (
|
|
504
|
+
<div style={styles.empty}>
|
|
505
|
+
{tab === 'installed'
|
|
506
|
+
? 'No skills installed yet. Browse the store to find skills.'
|
|
507
|
+
: 'No skills match your search.'}
|
|
508
|
+
</div>
|
|
137
509
|
)}
|
|
138
510
|
|
|
139
|
-
{
|
|
140
|
-
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
>
|
|
151
|
-
{/* Card header */}
|
|
152
|
-
<div style={styles.cardTop}>
|
|
153
|
-
<div style={{
|
|
154
|
-
...styles.cardIcon,
|
|
155
|
-
background: skill.installed ? 'var(--green)' : 'var(--accent)',
|
|
156
|
-
}}>
|
|
157
|
-
{skill.icon || skill.name.charAt(0)}
|
|
158
|
-
</div>
|
|
159
|
-
<div style={styles.cardInfo}>
|
|
160
|
-
<div style={styles.cardName}>{skill.name}</div>
|
|
161
|
-
<div style={styles.cardAuthor}>{skill.author}</div>
|
|
162
|
-
</div>
|
|
163
|
-
{skill.installed && (
|
|
164
|
-
<div style={styles.installedBadge}>installed</div>
|
|
165
|
-
)}
|
|
166
|
-
</div>
|
|
167
|
-
|
|
168
|
-
{/* Description */}
|
|
169
|
-
<div style={styles.cardDesc}>{skill.description}</div>
|
|
170
|
-
|
|
171
|
-
{/* Tags */}
|
|
172
|
-
<div style={styles.tagRow}>
|
|
173
|
-
<span style={styles.catTag}>{CATEGORY_LABELS[skill.category] || skill.category}</span>
|
|
174
|
-
{skill.roles.slice(0, 3).map((r) => (
|
|
175
|
-
<span key={r} style={styles.roleTag}>{r}</span>
|
|
176
|
-
))}
|
|
177
|
-
</div>
|
|
178
|
-
</div>
|
|
179
|
-
|
|
180
|
-
{/* Expanded detail */}
|
|
181
|
-
{expandedSkill === skill.id && (
|
|
182
|
-
<div style={styles.detail}>
|
|
183
|
-
{skillContent && (
|
|
184
|
-
<div style={styles.contentPreview}>
|
|
185
|
-
<div style={styles.contentLabel}>SKILL INSTRUCTIONS</div>
|
|
186
|
-
<pre style={styles.contentPre}>
|
|
187
|
-
{skillContent.replace(/^---[\s\S]*?---\n/, '').trim().slice(0, 800)}
|
|
188
|
-
{skillContent.length > 800 ? '\n...' : ''}
|
|
189
|
-
</pre>
|
|
190
|
-
</div>
|
|
191
|
-
)}
|
|
192
|
-
<div style={styles.detailActions}>
|
|
193
|
-
{skill.installed ? (
|
|
194
|
-
<button
|
|
195
|
-
onClick={(e) => { e.stopPropagation(); handleUninstall(skill.id); }}
|
|
196
|
-
disabled={installing === skill.id}
|
|
197
|
-
style={styles.uninstallBtn}
|
|
198
|
-
>
|
|
199
|
-
{installing === skill.id ? 'Removing...' : 'Uninstall'}
|
|
200
|
-
</button>
|
|
201
|
-
) : (
|
|
202
|
-
<button
|
|
203
|
-
onClick={(e) => { e.stopPropagation(); handleInstall(skill.id); }}
|
|
204
|
-
disabled={installing === skill.id}
|
|
205
|
-
style={styles.installBtn}
|
|
206
|
-
>
|
|
207
|
-
{installing === skill.id ? 'Installing...' : 'Install'}
|
|
208
|
-
</button>
|
|
209
|
-
)}
|
|
210
|
-
</div>
|
|
211
|
-
</div>
|
|
212
|
-
)}
|
|
213
|
-
</div>
|
|
214
|
-
))}
|
|
511
|
+
<div style={styles.grid}>
|
|
512
|
+
{displaySkills.map((skill) => (
|
|
513
|
+
<SkillCard
|
|
514
|
+
key={skill.id}
|
|
515
|
+
skill={skill}
|
|
516
|
+
onSelect={handleSelect}
|
|
517
|
+
hovered={hovered === skill.id}
|
|
518
|
+
onHover={setHovered}
|
|
519
|
+
/>
|
|
520
|
+
))}
|
|
521
|
+
</div>
|
|
215
522
|
</div>
|
|
523
|
+
|
|
524
|
+
{/* Detail Modal */}
|
|
525
|
+
<SkillDetailModal
|
|
526
|
+
skill={selectedSkill}
|
|
527
|
+
content={skillContent}
|
|
528
|
+
installing={installing}
|
|
529
|
+
onInstall={handleInstall}
|
|
530
|
+
onUninstall={handleUninstall}
|
|
531
|
+
onClose={() => { setSelectedSkill(null); setSkillContent(null); }}
|
|
532
|
+
/>
|
|
216
533
|
</div>
|
|
217
534
|
);
|
|
218
535
|
}
|
|
219
536
|
|
|
537
|
+
// ── Styles ──────────────────────────────────────────────────────────
|
|
538
|
+
|
|
220
539
|
const styles = {
|
|
221
540
|
root: {
|
|
222
541
|
height: '100%', display: 'flex', flexDirection: 'column',
|
|
223
542
|
overflow: 'hidden',
|
|
224
543
|
},
|
|
225
|
-
|
|
226
|
-
|
|
544
|
+
|
|
545
|
+
// Header bar
|
|
546
|
+
headerBar: {
|
|
547
|
+
padding: '12px 20px 0',
|
|
548
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
227
549
|
flexShrink: 0,
|
|
228
550
|
},
|
|
551
|
+
headerLeft: {
|
|
552
|
+
display: 'flex', alignItems: 'center', gap: 20,
|
|
553
|
+
},
|
|
229
554
|
title: {
|
|
230
|
-
fontSize:
|
|
555
|
+
fontSize: 15, fontWeight: 700, color: 'var(--text-bright)',
|
|
556
|
+
letterSpacing: 0.3,
|
|
231
557
|
},
|
|
232
|
-
|
|
233
|
-
|
|
558
|
+
headerTabs: {
|
|
559
|
+
display: 'flex', gap: 0,
|
|
560
|
+
},
|
|
561
|
+
headerTab: {
|
|
562
|
+
padding: '6px 14px',
|
|
563
|
+
background: 'none', border: 'none',
|
|
564
|
+
fontSize: 12, fontWeight: 500,
|
|
565
|
+
fontFamily: 'var(--font)', cursor: 'pointer',
|
|
566
|
+
transition: 'color 0.1s',
|
|
567
|
+
},
|
|
568
|
+
headerRight: {
|
|
569
|
+
display: 'flex', alignItems: 'center', gap: 10,
|
|
570
|
+
},
|
|
571
|
+
headerCount: {
|
|
572
|
+
fontSize: 11, color: 'var(--text-muted)',
|
|
234
573
|
},
|
|
574
|
+
|
|
575
|
+
// Toolbar
|
|
235
576
|
toolbar: {
|
|
236
|
-
padding: '
|
|
577
|
+
padding: '10px 20px',
|
|
237
578
|
flexShrink: 0,
|
|
238
579
|
},
|
|
580
|
+
searchRow: {
|
|
581
|
+
display: 'flex', gap: 10, alignItems: 'center',
|
|
582
|
+
},
|
|
583
|
+
searchWrap: {
|
|
584
|
+
flex: 1, position: 'relative',
|
|
585
|
+
},
|
|
586
|
+
searchIcon: {
|
|
587
|
+
position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
|
|
588
|
+
color: 'var(--text-muted)', fontSize: 13, pointerEvents: 'none',
|
|
589
|
+
},
|
|
239
590
|
search: {
|
|
240
|
-
width: '100%', padding: '
|
|
591
|
+
width: '100%', padding: '7px 12px 7px 30px',
|
|
241
592
|
background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
242
|
-
borderRadius:
|
|
593
|
+
borderRadius: 6, color: 'var(--text-primary)', fontSize: 12,
|
|
243
594
|
fontFamily: 'var(--font)', outline: 'none',
|
|
244
595
|
},
|
|
596
|
+
sortWrap: {
|
|
597
|
+
display: 'flex', gap: 2,
|
|
598
|
+
},
|
|
599
|
+
sortBtn: {
|
|
600
|
+
padding: '5px 10px',
|
|
601
|
+
background: 'transparent', border: '1px solid transparent',
|
|
602
|
+
borderRadius: 4, fontSize: 10, fontWeight: 500,
|
|
603
|
+
fontFamily: 'var(--font)', cursor: 'pointer',
|
|
604
|
+
transition: 'all 0.1s',
|
|
605
|
+
},
|
|
245
606
|
catRow: {
|
|
246
607
|
display: 'flex', gap: 4, marginTop: 8, flexWrap: 'wrap',
|
|
247
608
|
},
|
|
248
609
|
catBtn: {
|
|
249
|
-
padding: '
|
|
610
|
+
padding: '4px 12px',
|
|
250
611
|
background: 'transparent', border: '1px solid var(--border)',
|
|
251
|
-
borderRadius:
|
|
612
|
+
borderRadius: 14, color: 'var(--text-dim)', fontSize: 10,
|
|
252
613
|
fontFamily: 'var(--font)', cursor: 'pointer',
|
|
253
614
|
transition: 'all 0.1s',
|
|
254
615
|
},
|
|
@@ -256,27 +617,87 @@ const styles = {
|
|
|
256
617
|
borderColor: 'var(--accent)', color: 'var(--accent)',
|
|
257
618
|
background: 'rgba(51, 175, 188, 0.08)',
|
|
258
619
|
},
|
|
259
|
-
|
|
620
|
+
|
|
621
|
+
// Scroll area
|
|
622
|
+
scrollArea: {
|
|
260
623
|
flex: 1, overflowY: 'auto', padding: '0 20px 20px',
|
|
261
|
-
|
|
624
|
+
},
|
|
625
|
+
|
|
626
|
+
// Featured
|
|
627
|
+
featuredSection: {
|
|
628
|
+
marginBottom: 16,
|
|
629
|
+
},
|
|
630
|
+
featuredLabel: {
|
|
631
|
+
fontSize: 11, fontWeight: 700, color: 'var(--text-dim)',
|
|
632
|
+
textTransform: 'uppercase', letterSpacing: 1, marginBottom: 8,
|
|
633
|
+
},
|
|
634
|
+
featuredRow: {
|
|
635
|
+
display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 8,
|
|
636
|
+
},
|
|
637
|
+
featuredCard: {
|
|
638
|
+
padding: '14px',
|
|
639
|
+
background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
640
|
+
borderRadius: 8, cursor: 'pointer',
|
|
641
|
+
transition: 'border-color 0.15s, transform 0.15s',
|
|
642
|
+
position: 'relative', overflow: 'hidden',
|
|
643
|
+
},
|
|
644
|
+
featuredGradient: {
|
|
645
|
+
display: 'flex', alignItems: 'center', marginBottom: 10,
|
|
646
|
+
},
|
|
647
|
+
featuredIcon: {
|
|
648
|
+
width: 40, height: 40, borderRadius: 10,
|
|
649
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
650
|
+
fontSize: 16, fontWeight: 700, color: 'var(--bg-base)',
|
|
651
|
+
},
|
|
652
|
+
featuredInfo: {
|
|
653
|
+
flex: 1,
|
|
654
|
+
},
|
|
655
|
+
featuredName: {
|
|
656
|
+
fontSize: 13, fontWeight: 700, color: 'var(--text-bright)',
|
|
657
|
+
},
|
|
658
|
+
featuredAuthor: {
|
|
659
|
+
fontSize: 10, color: 'var(--text-muted)', marginTop: 1,
|
|
660
|
+
},
|
|
661
|
+
featuredDesc: {
|
|
662
|
+
fontSize: 10, color: 'var(--text-dim)', marginTop: 6,
|
|
663
|
+
lineHeight: 1.45, display: '-webkit-box',
|
|
664
|
+
WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden',
|
|
665
|
+
},
|
|
666
|
+
featuredMeta: {
|
|
667
|
+
display: 'flex', gap: 10, marginTop: 8,
|
|
668
|
+
},
|
|
669
|
+
featuredBadge: {
|
|
670
|
+
position: 'absolute', top: 8, right: 8,
|
|
671
|
+
fontSize: 8, fontWeight: 600, color: 'var(--green)',
|
|
672
|
+
border: '1px solid var(--green)', borderRadius: 3,
|
|
673
|
+
padding: '1px 5px', textTransform: 'uppercase', letterSpacing: 0.5,
|
|
674
|
+
},
|
|
675
|
+
|
|
676
|
+
// 3-wide grid
|
|
677
|
+
grid: {
|
|
678
|
+
display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 8,
|
|
262
679
|
},
|
|
263
680
|
empty: {
|
|
264
|
-
padding: '
|
|
681
|
+
padding: '60px 0', textAlign: 'center',
|
|
265
682
|
color: 'var(--text-dim)', fontSize: 12,
|
|
683
|
+
gridColumn: '1 / -1',
|
|
266
684
|
},
|
|
685
|
+
|
|
686
|
+
// Skill Card
|
|
267
687
|
card: {
|
|
268
|
-
padding: '
|
|
688
|
+
padding: '14px',
|
|
269
689
|
background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
270
|
-
borderRadius:
|
|
271
|
-
transition: 'border-color 0.
|
|
690
|
+
borderRadius: 8, cursor: 'pointer',
|
|
691
|
+
transition: 'border-color 0.12s, background 0.12s, transform 0.12s',
|
|
692
|
+
display: 'flex', flexDirection: 'column',
|
|
272
693
|
},
|
|
273
694
|
cardTop: {
|
|
274
695
|
display: 'flex', alignItems: 'center', gap: 10,
|
|
275
696
|
},
|
|
276
697
|
cardIcon: {
|
|
277
|
-
width:
|
|
698
|
+
width: 36, height: 36, borderRadius: 8,
|
|
278
699
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
279
|
-
fontSize:
|
|
700
|
+
fontSize: 14, fontWeight: 700, color: 'var(--bg-base)',
|
|
280
701
|
flexShrink: 0,
|
|
281
702
|
},
|
|
282
703
|
cardInfo: {
|
|
@@ -284,67 +705,189 @@ const styles = {
|
|
|
284
705
|
},
|
|
285
706
|
cardName: {
|
|
286
707
|
fontSize: 12, fontWeight: 600, color: 'var(--text-bright)',
|
|
708
|
+
display: 'flex', alignItems: 'center', gap: 4,
|
|
709
|
+
},
|
|
710
|
+
verifiedDot: {
|
|
711
|
+
fontSize: 9, color: 'var(--accent)', fontWeight: 700,
|
|
287
712
|
},
|
|
288
713
|
cardAuthor: {
|
|
289
|
-
fontSize: 10, color: 'var(--text-muted)',
|
|
714
|
+
fontSize: 10, color: 'var(--text-muted)', marginTop: 1,
|
|
290
715
|
},
|
|
291
716
|
installedBadge: {
|
|
292
|
-
fontSize:
|
|
717
|
+
fontSize: 8, fontWeight: 600, color: 'var(--green)',
|
|
293
718
|
border: '1px solid var(--green)', borderRadius: 3,
|
|
294
719
|
padding: '1px 6px', textTransform: 'uppercase', letterSpacing: 0.5,
|
|
295
720
|
flexShrink: 0,
|
|
296
721
|
},
|
|
722
|
+
priceBadge: {
|
|
723
|
+
fontSize: 10, fontWeight: 700, color: 'var(--purple)',
|
|
724
|
+
border: '1px solid var(--purple)', borderRadius: 4,
|
|
725
|
+
padding: '2px 8px', flexShrink: 0,
|
|
726
|
+
},
|
|
727
|
+
freeBadge: {
|
|
728
|
+
fontSize: 9, fontWeight: 500, color: 'var(--text-muted)',
|
|
729
|
+
flexShrink: 0,
|
|
730
|
+
},
|
|
297
731
|
cardDesc: {
|
|
298
|
-
fontSize: 11, color: 'var(--text-dim)', marginTop:
|
|
299
|
-
lineHeight: 1.
|
|
732
|
+
fontSize: 11, color: 'var(--text-dim)', marginTop: 10,
|
|
733
|
+
lineHeight: 1.5, flex: 1,
|
|
300
734
|
display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical',
|
|
301
735
|
overflow: 'hidden',
|
|
302
736
|
},
|
|
303
|
-
|
|
304
|
-
display: 'flex',
|
|
737
|
+
cardBottom: {
|
|
738
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
739
|
+
marginTop: 10, paddingTop: 8,
|
|
740
|
+
borderTop: '1px solid var(--border)',
|
|
741
|
+
},
|
|
742
|
+
cardStats: {
|
|
743
|
+
display: 'flex', gap: 10,
|
|
744
|
+
},
|
|
745
|
+
cardRating: {
|
|
746
|
+
fontSize: 10, color: 'var(--text-dim)',
|
|
747
|
+
},
|
|
748
|
+
cardDownloads: {
|
|
749
|
+
fontSize: 10, color: 'var(--text-muted)',
|
|
305
750
|
},
|
|
306
751
|
catTag: {
|
|
307
|
-
fontSize: 9, padding: '
|
|
752
|
+
fontSize: 9, padding: '2px 8px', borderRadius: 4,
|
|
308
753
|
background: 'var(--bg-active)', color: 'var(--text-dim)',
|
|
309
|
-
fontWeight:
|
|
754
|
+
fontWeight: 500,
|
|
310
755
|
},
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
// ── Modal styles ────────────────────────────────────────────────────
|
|
759
|
+
const modal = {
|
|
760
|
+
overlay: {
|
|
761
|
+
position: 'fixed', inset: 0,
|
|
762
|
+
background: 'rgba(0, 0, 0, 0.6)',
|
|
763
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
764
|
+
zIndex: 1000,
|
|
765
|
+
backdropFilter: 'blur(2px)',
|
|
314
766
|
},
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
767
|
+
container: {
|
|
768
|
+
width: '90%', maxWidth: 560, maxHeight: '85vh',
|
|
769
|
+
background: 'var(--bg-chrome)', border: '1px solid var(--border)',
|
|
770
|
+
borderRadius: 10, overflowY: 'auto',
|
|
771
|
+
padding: '24px', position: 'relative',
|
|
320
772
|
},
|
|
321
|
-
|
|
322
|
-
|
|
773
|
+
closeBtn: {
|
|
774
|
+
position: 'absolute', top: 12, right: 14,
|
|
775
|
+
background: 'none', border: 'none',
|
|
776
|
+
color: 'var(--text-dim)', fontSize: 20,
|
|
777
|
+
cursor: 'pointer', fontFamily: 'var(--font)',
|
|
778
|
+
lineHeight: 1, padding: '2px 6px',
|
|
323
779
|
},
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
780
|
+
header: {
|
|
781
|
+
display: 'flex', gap: 14, alignItems: 'flex-start',
|
|
782
|
+
marginBottom: 20,
|
|
327
783
|
},
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
784
|
+
icon: {
|
|
785
|
+
width: 52, height: 52, borderRadius: 12,
|
|
786
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
787
|
+
fontSize: 20, fontWeight: 700, color: 'var(--bg-base)',
|
|
788
|
+
flexShrink: 0,
|
|
789
|
+
},
|
|
790
|
+
headerInfo: {
|
|
791
|
+
flex: 1, minWidth: 0,
|
|
792
|
+
},
|
|
793
|
+
name: {
|
|
794
|
+
fontSize: 16, fontWeight: 700, color: 'var(--text-bright)',
|
|
795
|
+
},
|
|
796
|
+
author: {
|
|
797
|
+
fontSize: 11, color: 'var(--text-dim)', marginTop: 2,
|
|
798
|
+
display: 'flex', alignItems: 'center', gap: 4,
|
|
334
799
|
},
|
|
335
|
-
|
|
336
|
-
display: 'flex',
|
|
800
|
+
verifiedBadge: {
|
|
801
|
+
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
802
|
+
width: 14, height: 14, borderRadius: '50%',
|
|
803
|
+
background: 'var(--accent)', color: 'var(--bg-base)',
|
|
804
|
+
fontSize: 8, fontWeight: 700, flexShrink: 0,
|
|
805
|
+
},
|
|
806
|
+
meta: {
|
|
807
|
+
display: 'flex', gap: 12, marginTop: 6,
|
|
808
|
+
fontSize: 11, color: 'var(--text-dim)',
|
|
809
|
+
},
|
|
810
|
+
metaItem: {
|
|
811
|
+
display: 'flex', alignItems: 'center', gap: 3,
|
|
812
|
+
},
|
|
813
|
+
headerAction: {
|
|
814
|
+
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4,
|
|
815
|
+
flexShrink: 0,
|
|
337
816
|
},
|
|
338
817
|
installBtn: {
|
|
339
|
-
padding: '
|
|
340
|
-
background: 'var(--accent)', border: '
|
|
341
|
-
borderRadius:
|
|
818
|
+
padding: '8px 28px',
|
|
819
|
+
background: 'var(--accent)', border: 'none',
|
|
820
|
+
borderRadius: 6, color: 'var(--bg-base)', fontSize: 12, fontWeight: 700,
|
|
821
|
+
fontFamily: 'var(--font)', cursor: 'pointer',
|
|
822
|
+
transition: 'opacity 0.1s',
|
|
823
|
+
},
|
|
824
|
+
buyBtn: {
|
|
825
|
+
padding: '8px 28px',
|
|
826
|
+
background: 'var(--purple)', border: 'none',
|
|
827
|
+
borderRadius: 6, color: '#fff', fontSize: 12, fontWeight: 700,
|
|
342
828
|
fontFamily: 'var(--font)', cursor: 'pointer',
|
|
829
|
+
transition: 'opacity 0.1s',
|
|
343
830
|
},
|
|
344
831
|
uninstallBtn: {
|
|
345
|
-
padding: '
|
|
832
|
+
padding: '8px 20px',
|
|
346
833
|
background: 'transparent', border: '1px solid var(--red)',
|
|
347
|
-
borderRadius:
|
|
834
|
+
borderRadius: 6, color: 'var(--red)', fontSize: 12, fontWeight: 600,
|
|
348
835
|
fontFamily: 'var(--font)', cursor: 'pointer',
|
|
349
836
|
},
|
|
837
|
+
freeLabel: {
|
|
838
|
+
fontSize: 9, color: 'var(--text-muted)', fontWeight: 500,
|
|
839
|
+
},
|
|
840
|
+
section: {
|
|
841
|
+
marginBottom: 16,
|
|
842
|
+
},
|
|
843
|
+
sectionTitle: {
|
|
844
|
+
fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
|
|
845
|
+
textTransform: 'uppercase', letterSpacing: 0.8, marginBottom: 8,
|
|
846
|
+
},
|
|
847
|
+
description: {
|
|
848
|
+
fontSize: 12, color: 'var(--text-primary)', lineHeight: 1.6,
|
|
849
|
+
},
|
|
850
|
+
tagRow: {
|
|
851
|
+
display: 'flex', gap: 5, flexWrap: 'wrap',
|
|
852
|
+
},
|
|
853
|
+
tag: {
|
|
854
|
+
fontSize: 10, padding: '2px 8px', borderRadius: 4,
|
|
855
|
+
background: 'var(--bg-active)', color: 'var(--text-dim)',
|
|
856
|
+
},
|
|
857
|
+
roleTag: {
|
|
858
|
+
fontSize: 10, padding: '2px 8px', borderRadius: 4,
|
|
859
|
+
background: 'rgba(51, 175, 188, 0.1)', color: 'var(--accent)',
|
|
860
|
+
},
|
|
861
|
+
authorCard: {
|
|
862
|
+
display: 'flex', gap: 12, alignItems: 'center',
|
|
863
|
+
padding: '10px 12px',
|
|
864
|
+
background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
865
|
+
borderRadius: 6,
|
|
866
|
+
},
|
|
867
|
+
authorAvatar: {
|
|
868
|
+
width: 36, height: 36, borderRadius: 6,
|
|
869
|
+
background: 'var(--accent)',
|
|
870
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
871
|
+
fontSize: 14, fontWeight: 700, color: 'var(--bg-base)',
|
|
872
|
+
flexShrink: 0,
|
|
873
|
+
},
|
|
874
|
+
authorDetails: {
|
|
875
|
+
flex: 1,
|
|
876
|
+
},
|
|
877
|
+
authorName: {
|
|
878
|
+
fontSize: 12, fontWeight: 600, color: 'var(--text-bright)',
|
|
879
|
+
},
|
|
880
|
+
authorLinks: {
|
|
881
|
+
display: 'flex', gap: 10, marginTop: 4,
|
|
882
|
+
},
|
|
883
|
+
authorLink: {
|
|
884
|
+
fontSize: 10, color: 'var(--accent)', textDecoration: 'none',
|
|
885
|
+
},
|
|
886
|
+
contentPre: {
|
|
887
|
+
fontSize: 10, color: 'var(--text-dim)', lineHeight: 1.6,
|
|
888
|
+
fontFamily: 'var(--font)', whiteSpace: 'pre-wrap', wordBreak: 'break-word',
|
|
889
|
+
maxHeight: 300, overflowY: 'auto',
|
|
890
|
+
padding: '10px 12px', background: 'var(--bg-surface)',
|
|
891
|
+
border: '1px solid var(--border)', borderRadius: 6,
|
|
892
|
+
},
|
|
350
893
|
};
|