claudeup 3.14.0 → 3.16.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/package.json +1 -1
- package/src/data/skill-repos.js +11 -0
- package/src/data/skill-repos.ts +12 -0
- package/src/services/skills-manager.js +40 -13
- package/src/services/skills-manager.ts +38 -16
- package/src/ui/adapters/skillsAdapter.js +106 -0
- package/src/ui/adapters/skillsAdapter.ts +160 -0
- package/src/ui/components/primitives/ActionHints.js +13 -0
- package/src/ui/components/primitives/ActionHints.tsx +41 -0
- package/src/ui/components/primitives/DetailSection.js +7 -0
- package/src/ui/components/primitives/DetailSection.tsx +22 -0
- package/src/ui/components/primitives/KeyValueLine.js +8 -0
- package/src/ui/components/primitives/KeyValueLine.tsx +19 -0
- package/src/ui/components/primitives/ListCategoryRow.js +8 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +38 -0
- package/src/ui/components/primitives/MetaText.js +8 -0
- package/src/ui/components/primitives/MetaText.tsx +14 -0
- package/src/ui/components/primitives/ScopeDetail.js +32 -0
- package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
- package/src/ui/components/primitives/ScopeSquares.js +11 -0
- package/src/ui/components/primitives/ScopeSquares.tsx +33 -0
- package/src/ui/components/primitives/SelectableRow.js +5 -0
- package/src/ui/components/primitives/SelectableRow.tsx +24 -0
- package/src/ui/components/primitives/index.js +8 -0
- package/src/ui/components/primitives/index.ts +9 -0
- package/src/ui/registry.js +1 -0
- package/src/ui/registry.ts +27 -0
- package/src/ui/renderers/skillRenderers.js +75 -0
- package/src/ui/renderers/skillRenderers.tsx +220 -0
- package/src/ui/screens/SkillsScreen.js +46 -195
- package/src/ui/screens/SkillsScreen.tsx +436 -796
- package/src/ui/theme.js +47 -0
- package/src/ui/theme.ts +53 -0
package/package.json
CHANGED
package/src/data/skill-repos.js
CHANGED
|
@@ -5,6 +5,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
5
5
|
skillPath: "skills/find-skills",
|
|
6
6
|
description: "Discover and install new skills from the ecosystem",
|
|
7
7
|
category: "search",
|
|
8
|
+
stars: 12000,
|
|
8
9
|
},
|
|
9
10
|
{
|
|
10
11
|
name: "React Best Practices",
|
|
@@ -12,6 +13,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
12
13
|
skillPath: "skills/react-best-practices",
|
|
13
14
|
description: "Modern React patterns and Vercel deployment guidelines",
|
|
14
15
|
category: "frontend",
|
|
16
|
+
stars: 24000,
|
|
15
17
|
},
|
|
16
18
|
{
|
|
17
19
|
name: "Web Design Guidelines",
|
|
@@ -19,6 +21,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
19
21
|
skillPath: "skills/web-design-guidelines",
|
|
20
22
|
description: "UI/UX design principles and web standards",
|
|
21
23
|
category: "design",
|
|
24
|
+
stars: 24000,
|
|
22
25
|
},
|
|
23
26
|
{
|
|
24
27
|
name: "Remotion Best Practices",
|
|
@@ -26,6 +29,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
26
29
|
skillPath: "skills/remotion-best-practices",
|
|
27
30
|
description: "Programmatic video creation with Remotion",
|
|
28
31
|
category: "media",
|
|
32
|
+
stars: 2400,
|
|
29
33
|
},
|
|
30
34
|
{
|
|
31
35
|
name: "UI/UX Pro Max",
|
|
@@ -33,6 +37,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
33
37
|
skillPath: "skills_en/ui-ux-pro-max",
|
|
34
38
|
description: "50 styles, 21 palettes, 50 font pairings, 9 stacks. Covers React, Next.js, Vue, Svelte, SwiftUI, Flutter, Tailwind, shadcn/ui",
|
|
35
39
|
category: "design",
|
|
40
|
+
stars: 109,
|
|
36
41
|
},
|
|
37
42
|
{
|
|
38
43
|
name: "ElevenLabs TTS",
|
|
@@ -40,6 +45,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
40
45
|
skillPath: "skills/elevenlabs-tts",
|
|
41
46
|
description: "Text-to-speech with ElevenLabs API integration",
|
|
42
47
|
category: "media",
|
|
48
|
+
stars: 206,
|
|
43
49
|
},
|
|
44
50
|
{
|
|
45
51
|
name: "Audit Website",
|
|
@@ -47,6 +53,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
47
53
|
skillPath: "skills/audit-website",
|
|
48
54
|
description: "Security and quality auditing for web applications",
|
|
49
55
|
category: "security",
|
|
56
|
+
stars: 67,
|
|
50
57
|
},
|
|
51
58
|
{
|
|
52
59
|
name: "Systematic Debugging",
|
|
@@ -54,6 +61,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
54
61
|
skillPath: "skills/systematic-debugging",
|
|
55
62
|
description: "Structured debugging methodology with root cause analysis",
|
|
56
63
|
category: "debugging",
|
|
64
|
+
stars: 113000,
|
|
57
65
|
},
|
|
58
66
|
{
|
|
59
67
|
name: "shadcn/ui",
|
|
@@ -61,6 +69,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
61
69
|
skillPath: "packages/shadcn",
|
|
62
70
|
description: "shadcn/ui component library patterns and usage",
|
|
63
71
|
category: "frontend",
|
|
72
|
+
stars: 111000,
|
|
64
73
|
},
|
|
65
74
|
{
|
|
66
75
|
name: "Neon Postgres",
|
|
@@ -68,6 +77,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
68
77
|
skillPath: "skills/neon-postgres",
|
|
69
78
|
description: "Neon serverless Postgres setup and best practices",
|
|
70
79
|
category: "database",
|
|
80
|
+
stars: 42,
|
|
71
81
|
},
|
|
72
82
|
{
|
|
73
83
|
name: "Neon Serverless",
|
|
@@ -75,6 +85,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
75
85
|
skillPath: "skills/neon-serverless",
|
|
76
86
|
description: "Serverless database patterns with Neon",
|
|
77
87
|
category: "database",
|
|
88
|
+
stars: 81,
|
|
78
89
|
},
|
|
79
90
|
];
|
|
80
91
|
export const DEFAULT_SKILL_REPOS = [
|
package/src/data/skill-repos.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface RecommendedSkill {
|
|
|
6
6
|
skillPath: string;
|
|
7
7
|
description: string;
|
|
8
8
|
category: string;
|
|
9
|
+
stars?: number; // fallback when GitHub API is rate-limited
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
@@ -15,6 +16,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
15
16
|
skillPath: "skills/find-skills",
|
|
16
17
|
description: "Discover and install new skills from the ecosystem",
|
|
17
18
|
category: "search",
|
|
19
|
+
stars: 12000,
|
|
18
20
|
},
|
|
19
21
|
{
|
|
20
22
|
name: "React Best Practices",
|
|
@@ -22,6 +24,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
22
24
|
skillPath: "skills/react-best-practices",
|
|
23
25
|
description: "Modern React patterns and Vercel deployment guidelines",
|
|
24
26
|
category: "frontend",
|
|
27
|
+
stars: 24000,
|
|
25
28
|
},
|
|
26
29
|
{
|
|
27
30
|
name: "Web Design Guidelines",
|
|
@@ -29,6 +32,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
29
32
|
skillPath: "skills/web-design-guidelines",
|
|
30
33
|
description: "UI/UX design principles and web standards",
|
|
31
34
|
category: "design",
|
|
35
|
+
stars: 24000,
|
|
32
36
|
},
|
|
33
37
|
{
|
|
34
38
|
name: "Remotion Best Practices",
|
|
@@ -36,6 +40,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
36
40
|
skillPath: "skills/remotion-best-practices",
|
|
37
41
|
description: "Programmatic video creation with Remotion",
|
|
38
42
|
category: "media",
|
|
43
|
+
stars: 2400,
|
|
39
44
|
},
|
|
40
45
|
{
|
|
41
46
|
name: "UI/UX Pro Max",
|
|
@@ -43,6 +48,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
43
48
|
skillPath: "skills_en/ui-ux-pro-max",
|
|
44
49
|
description: "50 styles, 21 palettes, 50 font pairings, 9 stacks. Covers React, Next.js, Vue, Svelte, SwiftUI, Flutter, Tailwind, shadcn/ui",
|
|
45
50
|
category: "design",
|
|
51
|
+
stars: 109,
|
|
46
52
|
},
|
|
47
53
|
{
|
|
48
54
|
name: "ElevenLabs TTS",
|
|
@@ -50,6 +56,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
50
56
|
skillPath: "skills/elevenlabs-tts",
|
|
51
57
|
description: "Text-to-speech with ElevenLabs API integration",
|
|
52
58
|
category: "media",
|
|
59
|
+
stars: 206,
|
|
53
60
|
},
|
|
54
61
|
{
|
|
55
62
|
name: "Audit Website",
|
|
@@ -57,6 +64,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
57
64
|
skillPath: "skills/audit-website",
|
|
58
65
|
description: "Security and quality auditing for web applications",
|
|
59
66
|
category: "security",
|
|
67
|
+
stars: 67,
|
|
60
68
|
},
|
|
61
69
|
{
|
|
62
70
|
name: "Systematic Debugging",
|
|
@@ -64,6 +72,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
64
72
|
skillPath: "skills/systematic-debugging",
|
|
65
73
|
description: "Structured debugging methodology with root cause analysis",
|
|
66
74
|
category: "debugging",
|
|
75
|
+
stars: 113000,
|
|
67
76
|
},
|
|
68
77
|
{
|
|
69
78
|
name: "shadcn/ui",
|
|
@@ -71,6 +80,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
71
80
|
skillPath: "packages/shadcn",
|
|
72
81
|
description: "shadcn/ui component library patterns and usage",
|
|
73
82
|
category: "frontend",
|
|
83
|
+
stars: 111000,
|
|
74
84
|
},
|
|
75
85
|
{
|
|
76
86
|
name: "Neon Postgres",
|
|
@@ -78,6 +88,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
78
88
|
skillPath: "skills/neon-postgres",
|
|
79
89
|
description: "Neon serverless Postgres setup and best practices",
|
|
80
90
|
category: "database",
|
|
91
|
+
stars: 42,
|
|
81
92
|
},
|
|
82
93
|
{
|
|
83
94
|
name: "Neon Serverless",
|
|
@@ -85,6 +96,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
85
96
|
skillPath: "skills/neon-serverless",
|
|
86
97
|
description: "Serverless database patterns with Neon",
|
|
87
98
|
category: "database",
|
|
99
|
+
stars: 81,
|
|
88
100
|
},
|
|
89
101
|
];
|
|
90
102
|
|
|
@@ -202,28 +202,55 @@ export async function fetchAvailableSkills(_repos, projectPath) {
|
|
|
202
202
|
// 2. Fetch popular skills from Firebase API
|
|
203
203
|
const popular = await fetchPopularSkills(30);
|
|
204
204
|
const popularSkills = popular.map((s) => markInstalled({ ...s, isRecommended: false }));
|
|
205
|
-
// 3. Enrich recommended skills with GitHub repo stars
|
|
206
|
-
|
|
205
|
+
// 3. Enrich recommended skills with GitHub repo stars (cached to disk)
|
|
206
|
+
const starsCachePath = path.join(os.homedir(), ".claude", "skill-stars-cache.json");
|
|
207
|
+
let starsCache = {};
|
|
208
|
+
try {
|
|
209
|
+
starsCache = await fs.readJson(starsCachePath);
|
|
210
|
+
}
|
|
211
|
+
catch { /* no cache yet */ }
|
|
207
212
|
const uniqueRepos = [...new Set(recommendedSkills.map((s) => s.source.repo))];
|
|
208
213
|
const repoStars = new Map();
|
|
209
|
-
|
|
210
|
-
|
|
214
|
+
const cacheMaxAge = 24 * 60 * 60 * 1000; // 24 hours
|
|
215
|
+
let cacheUpdated = false;
|
|
216
|
+
for (const repo of uniqueRepos) {
|
|
217
|
+
const cached = starsCache[repo];
|
|
218
|
+
if (cached && Date.now() - new Date(cached.fetchedAt).getTime() < cacheMaxAge) {
|
|
219
|
+
repoStars.set(repo, cached.stars);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
// Try fetching from GitHub (may be rate limited)
|
|
223
|
+
try {
|
|
211
224
|
const res = await fetch(`https://api.github.com/repos/${repo}`, {
|
|
212
225
|
headers: { Accept: "application/vnd.github+json" },
|
|
213
226
|
signal: AbortSignal.timeout(5000),
|
|
214
227
|
});
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
228
|
+
if (res.ok) {
|
|
229
|
+
const data = (await res.json());
|
|
230
|
+
if (data.stargazers_count) {
|
|
231
|
+
repoStars.set(repo, data.stargazers_count);
|
|
232
|
+
starsCache[repo] = { stars: data.stargazers_count, fetchedAt: new Date().toISOString() };
|
|
233
|
+
cacheUpdated = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else if (cached) {
|
|
237
|
+
// Rate limited but have stale cache — use it
|
|
238
|
+
repoStars.set(repo, cached.stars);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
if (cached)
|
|
243
|
+
repoStars.set(repo, cached.stars);
|
|
244
|
+
}
|
|
221
245
|
}
|
|
222
|
-
|
|
223
|
-
|
|
246
|
+
if (cacheUpdated) {
|
|
247
|
+
try {
|
|
248
|
+
await fs.writeJson(starsCachePath, starsCache);
|
|
249
|
+
}
|
|
250
|
+
catch { /* ignore */ }
|
|
224
251
|
}
|
|
225
252
|
for (const rec of recommendedSkills) {
|
|
226
|
-
rec.stars = repoStars.get(rec.source.repo) || undefined;
|
|
253
|
+
rec.stars = repoStars.get(rec.source.repo) || rec.stars || undefined;
|
|
227
254
|
}
|
|
228
255
|
// 4. Combine: recommended first, then popular (dedup by name)
|
|
229
256
|
const seen = new Set(recommendedSkills.map((s) => s.name));
|
|
@@ -271,27 +271,49 @@ export async function fetchAvailableSkills(
|
|
|
271
271
|
const popular = await fetchPopularSkills(30);
|
|
272
272
|
const popularSkills = popular.map((s) => markInstalled({ ...s, isRecommended: false }));
|
|
273
273
|
|
|
274
|
-
// 3. Enrich recommended skills with GitHub repo stars
|
|
275
|
-
|
|
274
|
+
// 3. Enrich recommended skills with GitHub repo stars (cached to disk)
|
|
275
|
+
const starsCachePath = path.join(os.homedir(), ".claude", "skill-stars-cache.json");
|
|
276
|
+
let starsCache: Record<string, { stars: number; fetchedAt: string }> = {};
|
|
277
|
+
try { starsCache = await fs.readJson(starsCachePath); } catch { /* no cache yet */ }
|
|
278
|
+
|
|
276
279
|
const uniqueRepos = [...new Set(recommendedSkills.map((s) => s.source.repo))];
|
|
277
280
|
const repoStars = new Map<string, number>();
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
281
|
+
const cacheMaxAge = 24 * 60 * 60 * 1000; // 24 hours
|
|
282
|
+
let cacheUpdated = false;
|
|
283
|
+
|
|
284
|
+
for (const repo of uniqueRepos) {
|
|
285
|
+
const cached = starsCache[repo];
|
|
286
|
+
if (cached && Date.now() - new Date(cached.fetchedAt).getTime() < cacheMaxAge) {
|
|
287
|
+
repoStars.set(repo, cached.stars);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
// Try fetching from GitHub (may be rate limited)
|
|
291
|
+
try {
|
|
292
|
+
const res = await fetch(`https://api.github.com/repos/${repo}`, {
|
|
293
|
+
headers: { Accept: "application/vnd.github+json" },
|
|
294
|
+
signal: AbortSignal.timeout(5000),
|
|
295
|
+
});
|
|
296
|
+
if (res.ok) {
|
|
286
297
|
const data = (await res.json()) as { stargazers_count?: number };
|
|
287
|
-
if (data.stargazers_count)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
298
|
+
if (data.stargazers_count) {
|
|
299
|
+
repoStars.set(repo, data.stargazers_count);
|
|
300
|
+
starsCache[repo] = { stars: data.stargazers_count, fetchedAt: new Date().toISOString() };
|
|
301
|
+
cacheUpdated = true;
|
|
302
|
+
}
|
|
303
|
+
} else if (cached) {
|
|
304
|
+
// Rate limited but have stale cache — use it
|
|
305
|
+
repoStars.set(repo, cached.stars);
|
|
306
|
+
}
|
|
307
|
+
} catch {
|
|
308
|
+
if (cached) repoStars.set(repo, cached.stars);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (cacheUpdated) {
|
|
312
|
+
try { await fs.writeJson(starsCachePath, starsCache); } catch { /* ignore */ }
|
|
292
313
|
}
|
|
314
|
+
|
|
293
315
|
for (const rec of recommendedSkills) {
|
|
294
|
-
rec.stars = repoStars.get(rec.source.repo) || undefined;
|
|
316
|
+
rec.stars = repoStars.get(rec.source.repo) || rec.stars || undefined;
|
|
295
317
|
}
|
|
296
318
|
|
|
297
319
|
// 4. Combine: recommended first, then popular (dedup by name)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the flat list of items for the SkillsScreen list panel.
|
|
3
|
+
* Extracted from SkillsScreen so it can be tested independently.
|
|
4
|
+
*/
|
|
5
|
+
export function buildSkillBrowserItems({ recommended, popular, installed, searchResults, query, isSearchLoading, }) {
|
|
6
|
+
const lowerQuery = query.toLowerCase();
|
|
7
|
+
const items = [];
|
|
8
|
+
// ── INSTALLED: always shown at top (if any) ──
|
|
9
|
+
const installedFiltered = lowerQuery
|
|
10
|
+
? installed.filter((s) => s.name.toLowerCase().includes(lowerQuery))
|
|
11
|
+
: installed;
|
|
12
|
+
if (installedFiltered.length > 0) {
|
|
13
|
+
items.push({
|
|
14
|
+
id: "cat:installed",
|
|
15
|
+
kind: "category",
|
|
16
|
+
label: `Installed (${installedFiltered.length})`,
|
|
17
|
+
title: "Installed",
|
|
18
|
+
categoryKey: "installed",
|
|
19
|
+
count: installedFiltered.length,
|
|
20
|
+
tone: "purple",
|
|
21
|
+
star: "● ",
|
|
22
|
+
});
|
|
23
|
+
for (const skill of installedFiltered) {
|
|
24
|
+
items.push({
|
|
25
|
+
id: `skill:${skill.id}`,
|
|
26
|
+
kind: "skill",
|
|
27
|
+
label: skill.name,
|
|
28
|
+
skill,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// ── RECOMMENDED: always shown, filtered when searching ──
|
|
33
|
+
const filteredRec = lowerQuery
|
|
34
|
+
? recommended.filter((s) => s.name.toLowerCase().includes(lowerQuery) ||
|
|
35
|
+
(s.description || "").toLowerCase().includes(lowerQuery))
|
|
36
|
+
: recommended;
|
|
37
|
+
items.push({
|
|
38
|
+
id: "cat:recommended",
|
|
39
|
+
kind: "category",
|
|
40
|
+
label: "Recommended",
|
|
41
|
+
title: "Recommended",
|
|
42
|
+
categoryKey: "recommended",
|
|
43
|
+
count: filteredRec.length,
|
|
44
|
+
tone: "green",
|
|
45
|
+
star: "★ ",
|
|
46
|
+
});
|
|
47
|
+
for (const skill of filteredRec) {
|
|
48
|
+
items.push({
|
|
49
|
+
id: `skill:${skill.id}`,
|
|
50
|
+
kind: "skill",
|
|
51
|
+
label: skill.name,
|
|
52
|
+
skill,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// ── SEARCH MODE ──
|
|
56
|
+
if (query.length >= 2) {
|
|
57
|
+
if (!isSearchLoading && searchResults.length > 0) {
|
|
58
|
+
const recNames = new Set(recommended.map((s) => s.name));
|
|
59
|
+
const deduped = searchResults
|
|
60
|
+
.filter((s) => !recNames.has(s.name))
|
|
61
|
+
.sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
|
|
62
|
+
if (deduped.length > 0) {
|
|
63
|
+
items.push({
|
|
64
|
+
id: "cat:search",
|
|
65
|
+
kind: "category",
|
|
66
|
+
label: `Search (${deduped.length})`,
|
|
67
|
+
title: "Search",
|
|
68
|
+
categoryKey: "popular",
|
|
69
|
+
count: deduped.length,
|
|
70
|
+
tone: "teal",
|
|
71
|
+
});
|
|
72
|
+
for (const skill of deduped) {
|
|
73
|
+
items.push({
|
|
74
|
+
id: `skill:${skill.id}`,
|
|
75
|
+
kind: "skill",
|
|
76
|
+
label: skill.name,
|
|
77
|
+
skill,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return items;
|
|
83
|
+
}
|
|
84
|
+
// ── POPULAR (default, no search query) — only skills with meaningful stars ──
|
|
85
|
+
const popularWithStars = popular.filter((s) => (s.stars ?? 0) >= 5);
|
|
86
|
+
if (popularWithStars.length > 0) {
|
|
87
|
+
items.push({
|
|
88
|
+
id: "cat:popular",
|
|
89
|
+
kind: "category",
|
|
90
|
+
label: "Popular",
|
|
91
|
+
title: "Popular",
|
|
92
|
+
categoryKey: "popular",
|
|
93
|
+
count: popularWithStars.length,
|
|
94
|
+
tone: "teal",
|
|
95
|
+
});
|
|
96
|
+
for (const skill of popularWithStars) {
|
|
97
|
+
items.push({
|
|
98
|
+
id: `skill:${skill.id}`,
|
|
99
|
+
kind: "skill",
|
|
100
|
+
label: skill.name,
|
|
101
|
+
skill,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return items;
|
|
106
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { SkillInfo } from "../../types/index.js";
|
|
2
|
+
|
|
3
|
+
// ─── Item types ───────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface SkillCategoryItem {
|
|
6
|
+
id: string;
|
|
7
|
+
kind: "category";
|
|
8
|
+
label: string;
|
|
9
|
+
title: string;
|
|
10
|
+
categoryKey: string;
|
|
11
|
+
count?: number;
|
|
12
|
+
tone: "purple" | "green" | "teal" | "yellow" | "gray" | "red";
|
|
13
|
+
badge?: string;
|
|
14
|
+
star?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SkillSkillItem {
|
|
18
|
+
id: string;
|
|
19
|
+
kind: "skill";
|
|
20
|
+
label: string;
|
|
21
|
+
skill: SkillInfo;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type SkillBrowserItem = SkillCategoryItem | SkillSkillItem;
|
|
25
|
+
|
|
26
|
+
// ─── Adapter ─────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export interface BuildSkillBrowserItemsArgs {
|
|
29
|
+
recommended: SkillInfo[];
|
|
30
|
+
popular: SkillInfo[];
|
|
31
|
+
installed: SkillInfo[];
|
|
32
|
+
searchResults: SkillInfo[];
|
|
33
|
+
query: string;
|
|
34
|
+
isSearchLoading: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Builds the flat list of items for the SkillsScreen list panel.
|
|
39
|
+
* Extracted from SkillsScreen so it can be tested independently.
|
|
40
|
+
*/
|
|
41
|
+
export function buildSkillBrowserItems({
|
|
42
|
+
recommended,
|
|
43
|
+
popular,
|
|
44
|
+
installed,
|
|
45
|
+
searchResults,
|
|
46
|
+
query,
|
|
47
|
+
isSearchLoading,
|
|
48
|
+
}: BuildSkillBrowserItemsArgs): SkillBrowserItem[] {
|
|
49
|
+
const lowerQuery = query.toLowerCase();
|
|
50
|
+
const items: SkillBrowserItem[] = [];
|
|
51
|
+
|
|
52
|
+
// ── INSTALLED: always shown at top (if any) ──
|
|
53
|
+
const installedFiltered = lowerQuery
|
|
54
|
+
? installed.filter((s) => s.name.toLowerCase().includes(lowerQuery))
|
|
55
|
+
: installed;
|
|
56
|
+
|
|
57
|
+
if (installedFiltered.length > 0) {
|
|
58
|
+
items.push({
|
|
59
|
+
id: "cat:installed",
|
|
60
|
+
kind: "category",
|
|
61
|
+
label: `Installed (${installedFiltered.length})`,
|
|
62
|
+
title: "Installed",
|
|
63
|
+
categoryKey: "installed",
|
|
64
|
+
count: installedFiltered.length,
|
|
65
|
+
tone: "purple",
|
|
66
|
+
star: "● ",
|
|
67
|
+
});
|
|
68
|
+
for (const skill of installedFiltered) {
|
|
69
|
+
items.push({
|
|
70
|
+
id: `skill:${skill.id}`,
|
|
71
|
+
kind: "skill",
|
|
72
|
+
label: skill.name,
|
|
73
|
+
skill,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── RECOMMENDED: always shown, filtered when searching ──
|
|
79
|
+
const filteredRec = lowerQuery
|
|
80
|
+
? recommended.filter(
|
|
81
|
+
(s) =>
|
|
82
|
+
s.name.toLowerCase().includes(lowerQuery) ||
|
|
83
|
+
(s.description || "").toLowerCase().includes(lowerQuery),
|
|
84
|
+
)
|
|
85
|
+
: recommended;
|
|
86
|
+
|
|
87
|
+
items.push({
|
|
88
|
+
id: "cat:recommended",
|
|
89
|
+
kind: "category",
|
|
90
|
+
label: "Recommended",
|
|
91
|
+
title: "Recommended",
|
|
92
|
+
categoryKey: "recommended",
|
|
93
|
+
count: filteredRec.length,
|
|
94
|
+
tone: "green",
|
|
95
|
+
star: "★ ",
|
|
96
|
+
});
|
|
97
|
+
for (const skill of filteredRec) {
|
|
98
|
+
items.push({
|
|
99
|
+
id: `skill:${skill.id}`,
|
|
100
|
+
kind: "skill",
|
|
101
|
+
label: skill.name,
|
|
102
|
+
skill,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── SEARCH MODE ──
|
|
107
|
+
if (query.length >= 2) {
|
|
108
|
+
if (!isSearchLoading && searchResults.length > 0) {
|
|
109
|
+
const recNames = new Set(recommended.map((s) => s.name));
|
|
110
|
+
const deduped = searchResults
|
|
111
|
+
.filter((s) => !recNames.has(s.name))
|
|
112
|
+
.sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
|
|
113
|
+
|
|
114
|
+
if (deduped.length > 0) {
|
|
115
|
+
items.push({
|
|
116
|
+
id: "cat:search",
|
|
117
|
+
kind: "category",
|
|
118
|
+
label: `Search (${deduped.length})`,
|
|
119
|
+
title: "Search",
|
|
120
|
+
categoryKey: "popular",
|
|
121
|
+
count: deduped.length,
|
|
122
|
+
tone: "teal",
|
|
123
|
+
});
|
|
124
|
+
for (const skill of deduped) {
|
|
125
|
+
items.push({
|
|
126
|
+
id: `skill:${skill.id}`,
|
|
127
|
+
kind: "skill",
|
|
128
|
+
label: skill.name,
|
|
129
|
+
skill,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return items;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── POPULAR (default, no search query) — only skills with meaningful stars ──
|
|
138
|
+
const popularWithStars = popular.filter((s) => (s.stars ?? 0) >= 5);
|
|
139
|
+
if (popularWithStars.length > 0) {
|
|
140
|
+
items.push({
|
|
141
|
+
id: "cat:popular",
|
|
142
|
+
kind: "category",
|
|
143
|
+
label: "Popular",
|
|
144
|
+
title: "Popular",
|
|
145
|
+
categoryKey: "popular",
|
|
146
|
+
count: popularWithStars.length,
|
|
147
|
+
tone: "teal",
|
|
148
|
+
});
|
|
149
|
+
for (const skill of popularWithStars) {
|
|
150
|
+
items.push({
|
|
151
|
+
id: `skill:${skill.id}`,
|
|
152
|
+
kind: "skill",
|
|
153
|
+
label: skill.name,
|
|
154
|
+
skill,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return items;
|
|
160
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
/**
|
|
4
|
+
* Keyboard shortcut badges for detail panels.
|
|
5
|
+
* Renders: [bg] key [/bg] label
|
|
6
|
+
*/
|
|
7
|
+
export function ActionHints({ hints }) {
|
|
8
|
+
return (_jsx("box", { flexDirection: "column", marginTop: 1, children: hints.map((hint) => (_jsxs("box", { children: [_jsxs("text", { bg: hint.tone === "danger"
|
|
9
|
+
? theme.hints.dangerBg
|
|
10
|
+
: hint.tone === "primary"
|
|
11
|
+
? theme.hints.primaryBg
|
|
12
|
+
: theme.hints.defaultBg, fg: "black", children: [" ", hint.key, " "] }), _jsxs("text", { fg: "gray", children: [" ", hint.label] })] }, `${hint.key}:${hint.label}`))) }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
|
|
4
|
+
export interface Hint {
|
|
5
|
+
key: string;
|
|
6
|
+
label: string;
|
|
7
|
+
tone?: "default" | "primary" | "danger";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ActionHintsProps {
|
|
11
|
+
hints: Hint[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Keyboard shortcut badges for detail panels.
|
|
16
|
+
* Renders: [bg] key [/bg] label
|
|
17
|
+
*/
|
|
18
|
+
export function ActionHints({ hints }: ActionHintsProps) {
|
|
19
|
+
return (
|
|
20
|
+
<box flexDirection="column" marginTop={1}>
|
|
21
|
+
{hints.map((hint) => (
|
|
22
|
+
<box key={`${hint.key}:${hint.label}`}>
|
|
23
|
+
<text
|
|
24
|
+
bg={
|
|
25
|
+
hint.tone === "danger"
|
|
26
|
+
? theme.hints.dangerBg
|
|
27
|
+
: hint.tone === "primary"
|
|
28
|
+
? theme.hints.primaryBg
|
|
29
|
+
: theme.hints.defaultBg
|
|
30
|
+
}
|
|
31
|
+
fg="black"
|
|
32
|
+
>
|
|
33
|
+
{" "}
|
|
34
|
+
{hint.key}{" "}
|
|
35
|
+
</text>
|
|
36
|
+
<text fg="gray"> {hint.label}</text>
|
|
37
|
+
</box>
|
|
38
|
+
))}
|
|
39
|
+
</box>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A labeled section in a detail panel with optional title header.
|
|
4
|
+
*/
|
|
5
|
+
export function DetailSection({ title, children }) {
|
|
6
|
+
return (_jsxs("box", { flexDirection: "column", marginTop: 1, children: [title ? (_jsx("text", { fg: "cyan", children: _jsx("strong", { children: title }) })) : null, children] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface DetailSectionProps {
|
|
4
|
+
title?: string;
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A labeled section in a detail panel with optional title header.
|
|
10
|
+
*/
|
|
11
|
+
export function DetailSection({ title, children }: DetailSectionProps) {
|
|
12
|
+
return (
|
|
13
|
+
<box flexDirection="column" marginTop={1}>
|
|
14
|
+
{title ? (
|
|
15
|
+
<text fg="cyan">
|
|
16
|
+
<strong>{title}</strong>
|
|
17
|
+
</text>
|
|
18
|
+
) : null}
|
|
19
|
+
{children}
|
|
20
|
+
</box>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Aligned label-value pair for detail panels.
|
|
4
|
+
* Label is padded to 10 chars for consistent alignment.
|
|
5
|
+
*/
|
|
6
|
+
export function KeyValueLine({ label, value }) {
|
|
7
|
+
return (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: label.padEnd(10) }), value] }));
|
|
8
|
+
}
|