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