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.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/src/data/skill-repos.js +11 -0
  3. package/src/data/skill-repos.ts +12 -0
  4. package/src/services/skills-manager.js +40 -13
  5. package/src/services/skills-manager.ts +38 -16
  6. package/src/ui/adapters/skillsAdapter.js +106 -0
  7. package/src/ui/adapters/skillsAdapter.ts +160 -0
  8. package/src/ui/components/primitives/ActionHints.js +13 -0
  9. package/src/ui/components/primitives/ActionHints.tsx +41 -0
  10. package/src/ui/components/primitives/DetailSection.js +7 -0
  11. package/src/ui/components/primitives/DetailSection.tsx +22 -0
  12. package/src/ui/components/primitives/KeyValueLine.js +8 -0
  13. package/src/ui/components/primitives/KeyValueLine.tsx +19 -0
  14. package/src/ui/components/primitives/ListCategoryRow.js +8 -0
  15. package/src/ui/components/primitives/ListCategoryRow.tsx +38 -0
  16. package/src/ui/components/primitives/MetaText.js +8 -0
  17. package/src/ui/components/primitives/MetaText.tsx +14 -0
  18. package/src/ui/components/primitives/ScopeDetail.js +32 -0
  19. package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
  20. package/src/ui/components/primitives/ScopeSquares.js +11 -0
  21. package/src/ui/components/primitives/ScopeSquares.tsx +33 -0
  22. package/src/ui/components/primitives/SelectableRow.js +5 -0
  23. package/src/ui/components/primitives/SelectableRow.tsx +24 -0
  24. package/src/ui/components/primitives/index.js +8 -0
  25. package/src/ui/components/primitives/index.ts +9 -0
  26. package/src/ui/registry.js +1 -0
  27. package/src/ui/registry.ts +27 -0
  28. package/src/ui/renderers/skillRenderers.js +75 -0
  29. package/src/ui/renderers/skillRenderers.tsx +220 -0
  30. package/src/ui/screens/SkillsScreen.js +46 -195
  31. package/src/ui/screens/SkillsScreen.tsx +436 -796
  32. package/src/ui/theme.js +47 -0
  33. package/src/ui/theme.ts +53 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "3.14.0",
3
+ "version": "3.16.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -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 = [
@@ -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
- // Fetch stars for each unique repo (typically ~7 repos, parallel)
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
- try {
210
- const starResults = await Promise.allSettled(uniqueRepos.map(async (repo) => {
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 (!res.ok)
216
- return;
217
- const data = (await res.json());
218
- if (data.stargazers_count)
219
- repoStars.set(repo, data.stargazers_count);
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
- catch {
223
- // Non-fatal — stars are cosmetic
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
- // Fetch stars for each unique repo (typically ~7 repos, parallel)
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
- try {
279
- const starResults = await Promise.allSettled(
280
- uniqueRepos.map(async (repo) => {
281
- const res = await fetch(`https://api.github.com/repos/${repo}`, {
282
- headers: { Accept: "application/vnd.github+json" },
283
- signal: AbortSignal.timeout(5000),
284
- });
285
- if (!res.ok) return;
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) repoStars.set(repo, data.stargazers_count);
288
- }),
289
- );
290
- } catch {
291
- // Non-fatal — stars are cosmetic
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
+ }