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
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface KeyValueLineProps {
|
|
4
|
+
label: string;
|
|
5
|
+
value: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Aligned label-value pair for detail panels.
|
|
10
|
+
* Label is padded to 10 chars for consistent alignment.
|
|
11
|
+
*/
|
|
12
|
+
export function KeyValueLine({ label, value }: KeyValueLineProps) {
|
|
13
|
+
return (
|
|
14
|
+
<text>
|
|
15
|
+
<span fg="gray">{label.padEnd(10)}</span>
|
|
16
|
+
{value}
|
|
17
|
+
</text>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
import { SelectableRow } from "./SelectableRow.js";
|
|
4
|
+
export function ListCategoryRow({ title, expanded = false, count, badge, tone = "gray", selected, }) {
|
|
5
|
+
const colors = theme.category[tone];
|
|
6
|
+
const label = `${expanded ? "▼" : "▶"} ${title}${count !== undefined ? ` (${count})` : ""}`;
|
|
7
|
+
return (_jsxs(SelectableRow, { selected: selected, children: [!selected && (_jsx("span", { bg: colors.bg, fg: colors.fg, children: _jsxs("strong", { children: [" ", label, " "] }) })), selected && _jsxs("strong", { children: [" ", label, " "] }), !selected && badge ? (_jsxs("span", { fg: colors.badgeFg, children: [" ", badge] })) : null] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
import { SelectableRow } from "./SelectableRow.js";
|
|
4
|
+
|
|
5
|
+
interface ListCategoryRowProps {
|
|
6
|
+
title: string;
|
|
7
|
+
expanded?: boolean;
|
|
8
|
+
count?: number;
|
|
9
|
+
badge?: string;
|
|
10
|
+
tone?: keyof typeof theme.category;
|
|
11
|
+
selected: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ListCategoryRow({
|
|
15
|
+
title,
|
|
16
|
+
expanded = false,
|
|
17
|
+
count,
|
|
18
|
+
badge,
|
|
19
|
+
tone = "gray",
|
|
20
|
+
selected,
|
|
21
|
+
}: ListCategoryRowProps) {
|
|
22
|
+
const colors = theme.category[tone];
|
|
23
|
+
const label = `${expanded ? "▼" : "▶"} ${title}${count !== undefined ? ` (${count})` : ""}`;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<SelectableRow selected={selected}>
|
|
27
|
+
{!selected && (
|
|
28
|
+
<span bg={colors.bg} fg={colors.fg}>
|
|
29
|
+
<strong> {label} </strong>
|
|
30
|
+
</span>
|
|
31
|
+
)}
|
|
32
|
+
{selected && <strong> {label} </strong>}
|
|
33
|
+
{!selected && badge ? (
|
|
34
|
+
<span fg={colors.badgeFg}> {badge}</span>
|
|
35
|
+
) : null}
|
|
36
|
+
</SelectableRow>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
/**
|
|
4
|
+
* Subdued text for versions, stars, status indicators.
|
|
5
|
+
*/
|
|
6
|
+
export function MetaText({ text, tone = "muted" }) {
|
|
7
|
+
return _jsx("span", { fg: theme.meta[tone], children: text });
|
|
8
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
|
|
4
|
+
interface MetaTextProps {
|
|
5
|
+
text: string;
|
|
6
|
+
tone?: "muted" | "warning" | "success" | "danger";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Subdued text for versions, stars, status indicators.
|
|
11
|
+
*/
|
|
12
|
+
export function MetaText({ text, tone = "muted" }: MetaTextProps) {
|
|
13
|
+
return <span fg={theme.meta[tone]}>{text}</span>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
/**
|
|
4
|
+
* The u/p/l badges used in detail panels.
|
|
5
|
+
* Renders each scope as: [bg=color] key [/bg] ● or ○ Label path
|
|
6
|
+
*/
|
|
7
|
+
export function ScopeDetail({ scopes, paths }) {
|
|
8
|
+
const items = [
|
|
9
|
+
{
|
|
10
|
+
key: "u",
|
|
11
|
+
label: "User",
|
|
12
|
+
color: theme.scopes.user,
|
|
13
|
+
active: scopes.user,
|
|
14
|
+
path: paths?.user,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
key: "p",
|
|
18
|
+
label: "Project",
|
|
19
|
+
color: theme.scopes.project,
|
|
20
|
+
active: scopes.project,
|
|
21
|
+
path: paths?.project,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
key: "l",
|
|
25
|
+
label: "Local",
|
|
26
|
+
color: theme.scopes.local,
|
|
27
|
+
active: scopes.local,
|
|
28
|
+
path: paths?.local,
|
|
29
|
+
},
|
|
30
|
+
].filter((i) => i.path !== undefined || i.active !== undefined);
|
|
31
|
+
return (_jsx("box", { flexDirection: "column", children: items.map((item) => (_jsxs("text", { children: [_jsxs("span", { bg: item.color, fg: "black", children: [" ", item.key, " "] }), _jsx("span", { fg: item.active ? item.color : "gray", children: item.active ? " ● " : " ○ " }), _jsx("span", { fg: item.color, children: item.label }), item.path && _jsxs("span", { fg: "gray", children: [" ", item.path] })] }, item.key))) }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
|
|
4
|
+
interface ScopeDetailScope {
|
|
5
|
+
user?: boolean;
|
|
6
|
+
project?: boolean;
|
|
7
|
+
local?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ScopeDetailPaths {
|
|
11
|
+
user?: string;
|
|
12
|
+
project?: string;
|
|
13
|
+
local?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ScopeDetailProps {
|
|
17
|
+
scopes: ScopeDetailScope;
|
|
18
|
+
paths?: ScopeDetailPaths;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The u/p/l badges used in detail panels.
|
|
23
|
+
* Renders each scope as: [bg=color] key [/bg] ● or ○ Label path
|
|
24
|
+
*/
|
|
25
|
+
export function ScopeDetail({ scopes, paths }: ScopeDetailProps) {
|
|
26
|
+
const items = [
|
|
27
|
+
{
|
|
28
|
+
key: "u",
|
|
29
|
+
label: "User",
|
|
30
|
+
color: theme.scopes.user,
|
|
31
|
+
active: scopes.user,
|
|
32
|
+
path: paths?.user,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: "p",
|
|
36
|
+
label: "Project",
|
|
37
|
+
color: theme.scopes.project,
|
|
38
|
+
active: scopes.project,
|
|
39
|
+
path: paths?.project,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: "l",
|
|
43
|
+
label: "Local",
|
|
44
|
+
color: theme.scopes.local,
|
|
45
|
+
active: scopes.local,
|
|
46
|
+
path: paths?.local,
|
|
47
|
+
},
|
|
48
|
+
].filter((i) => i.path !== undefined || i.active !== undefined);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<box flexDirection="column">
|
|
52
|
+
{items.map((item) => (
|
|
53
|
+
<text key={item.key}>
|
|
54
|
+
<span bg={item.color} fg="black">
|
|
55
|
+
{" "}
|
|
56
|
+
{item.key}{" "}
|
|
57
|
+
</span>
|
|
58
|
+
<span fg={item.active ? item.color : "gray"}>
|
|
59
|
+
{item.active ? " ● " : " ○ "}
|
|
60
|
+
</span>
|
|
61
|
+
<span fg={item.color}>{item.label}</span>
|
|
62
|
+
{item.path && <span fg="gray"> {item.path}</span>}
|
|
63
|
+
</text>
|
|
64
|
+
))}
|
|
65
|
+
</box>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
/**
|
|
4
|
+
* Colored filled/empty squares for scope display in list items.
|
|
5
|
+
* ■ = installed in that scope, □ = not installed.
|
|
6
|
+
*/
|
|
7
|
+
export function ScopeSquares({ user, project, local, selected = false, }) {
|
|
8
|
+
const inactive = selected ? "white" : theme.colors.dim;
|
|
9
|
+
// Returns fragments of <span> — safe to embed inside a <text> parent
|
|
10
|
+
return (_jsxs(_Fragment, { children: [_jsx("span", { fg: user ? theme.scopes.user : inactive, children: "\u25A0" }), _jsx("span", { fg: project ? theme.scopes.project : inactive, children: "\u25A0" }), local !== undefined && (_jsx("span", { fg: local ? theme.scopes.local : inactive, children: "\u25A0" }))] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
|
|
4
|
+
interface ScopeSquaresProps {
|
|
5
|
+
user: boolean;
|
|
6
|
+
project: boolean;
|
|
7
|
+
local?: boolean;
|
|
8
|
+
selected?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Colored filled/empty squares for scope display in list items.
|
|
13
|
+
* ■ = installed in that scope, □ = not installed.
|
|
14
|
+
*/
|
|
15
|
+
export function ScopeSquares({
|
|
16
|
+
user,
|
|
17
|
+
project,
|
|
18
|
+
local,
|
|
19
|
+
selected = false,
|
|
20
|
+
}: ScopeSquaresProps) {
|
|
21
|
+
const inactive = selected ? "white" : theme.colors.dim;
|
|
22
|
+
|
|
23
|
+
// Returns fragments of <span> — safe to embed inside a <text> parent
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<span fg={user ? theme.scopes.user : inactive}>■</span>
|
|
27
|
+
<span fg={project ? theme.scopes.project : inactive}>■</span>
|
|
28
|
+
{local !== undefined && (
|
|
29
|
+
<span fg={local ? theme.scopes.local : inactive}>■</span>
|
|
30
|
+
)}
|
|
31
|
+
</>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
export function SelectableRow({ selected, indent = 0, children, }) {
|
|
4
|
+
return (_jsxs("text", { bg: selected ? theme.selection.bg : undefined, fg: selected ? theme.selection.fg : undefined, children: [" ".repeat(indent), children] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
|
|
4
|
+
interface SelectableRowProps {
|
|
5
|
+
selected: boolean;
|
|
6
|
+
indent?: number;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function SelectableRow({
|
|
11
|
+
selected,
|
|
12
|
+
indent = 0,
|
|
13
|
+
children,
|
|
14
|
+
}: SelectableRowProps) {
|
|
15
|
+
return (
|
|
16
|
+
<text
|
|
17
|
+
bg={selected ? theme.selection.bg : undefined}
|
|
18
|
+
fg={selected ? theme.selection.fg : undefined}
|
|
19
|
+
>
|
|
20
|
+
{" ".repeat(indent)}
|
|
21
|
+
{children}
|
|
22
|
+
</text>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { SelectableRow } from "./SelectableRow.js";
|
|
2
|
+
export { ListCategoryRow } from "./ListCategoryRow.js";
|
|
3
|
+
export { ScopeSquares } from "./ScopeSquares.js";
|
|
4
|
+
export { ScopeDetail } from "./ScopeDetail.js";
|
|
5
|
+
export { ActionHints } from "./ActionHints.js";
|
|
6
|
+
export { MetaText } from "./MetaText.js";
|
|
7
|
+
export { KeyValueLine } from "./KeyValueLine.js";
|
|
8
|
+
export { DetailSection } from "./DetailSection.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { SelectableRow } from "./SelectableRow.js";
|
|
2
|
+
export { ListCategoryRow } from "./ListCategoryRow.js";
|
|
3
|
+
export { ScopeSquares } from "./ScopeSquares.js";
|
|
4
|
+
export { ScopeDetail } from "./ScopeDetail.js";
|
|
5
|
+
export { ActionHints } from "./ActionHints.js";
|
|
6
|
+
export type { Hint } from "./ActionHints.js";
|
|
7
|
+
export { MetaText } from "./MetaText.js";
|
|
8
|
+
export { KeyValueLine } from "./KeyValueLine.js";
|
|
9
|
+
export { DetailSection } from "./DetailSection.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
|
|
3
|
+
export interface RowRenderProps<T> {
|
|
4
|
+
item: T;
|
|
5
|
+
isSelected: boolean;
|
|
6
|
+
width?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DetailRenderProps<T> {
|
|
10
|
+
item: T;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Hint {
|
|
14
|
+
key: string;
|
|
15
|
+
label: string;
|
|
16
|
+
tone?: "default" | "primary" | "danger";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ItemRenderer<T> {
|
|
20
|
+
renderRow: (props: RowRenderProps<T>) => React.ReactNode;
|
|
21
|
+
renderDetail: (props: DetailRenderProps<T>) => React.ReactNode;
|
|
22
|
+
getActionHints?: (item: T) => Hint[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type RendererRegistry<TItem extends { kind: string }> = {
|
|
26
|
+
[K in TItem["kind"]]: ItemRenderer<Extract<TItem, { kind: K }>>;
|
|
27
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { SelectableRow, ListCategoryRow, ScopeSquares, MetaText, KeyValueLine, DetailSection, } from "../components/primitives/index.js";
|
|
3
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
4
|
+
function formatStars(stars) {
|
|
5
|
+
if (!stars)
|
|
6
|
+
return "";
|
|
7
|
+
if (stars >= 1000000)
|
|
8
|
+
return `★ ${(stars / 1000000).toFixed(1)}M`;
|
|
9
|
+
if (stars >= 10000)
|
|
10
|
+
return `★ ${Math.round(stars / 1000)}K`;
|
|
11
|
+
if (stars >= 1000)
|
|
12
|
+
return `★ ${(stars / 1000).toFixed(1)}K`;
|
|
13
|
+
return `★ ${stars}`;
|
|
14
|
+
}
|
|
15
|
+
// ─── Category renderer ────────────────────────────────────────────────────────
|
|
16
|
+
const categoryRenderer = {
|
|
17
|
+
renderRow: ({ item, isSelected }) => {
|
|
18
|
+
const label = `${item.star ?? ""}${item.title}`;
|
|
19
|
+
return (_jsx(ListCategoryRow, { selected: isSelected, title: label, count: item.count, badge: item.badge, tone: item.tone }));
|
|
20
|
+
},
|
|
21
|
+
renderDetail: ({ item }) => {
|
|
22
|
+
const isRec = item.categoryKey === "recommended";
|
|
23
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: isRec ? "green" : "cyan", children: _jsxs("strong", { children: [item.star ?? "", item.title] }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: isRec
|
|
24
|
+
? "Curated skills recommended for most projects"
|
|
25
|
+
: "Popular skills sorted by stars" }) })] }));
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
// ─── Skill renderer ───────────────────────────────────────────────────────────
|
|
29
|
+
const MAX_SKILL_NAME_LEN = 35;
|
|
30
|
+
function truncateName(name) {
|
|
31
|
+
return name.length > MAX_SKILL_NAME_LEN
|
|
32
|
+
? name.slice(0, MAX_SKILL_NAME_LEN - 1) + "\u2026"
|
|
33
|
+
: name;
|
|
34
|
+
}
|
|
35
|
+
const skillRenderer = {
|
|
36
|
+
renderRow: ({ item, isSelected }) => {
|
|
37
|
+
const { skill } = item;
|
|
38
|
+
const hasUser = skill.installedScope === "user";
|
|
39
|
+
const hasProject = skill.installedScope === "project";
|
|
40
|
+
const starsStr = formatStars(skill.stars);
|
|
41
|
+
const displayName = truncateName(skill.name);
|
|
42
|
+
return (_jsxs(SelectableRow, { selected: isSelected, indent: 1, children: [_jsx(ScopeSquares, { user: hasUser, project: hasProject, selected: isSelected }), _jsx("span", { children: " " }), _jsx("span", { fg: isSelected ? "white" : skill.installed ? "white" : "gray", children: displayName }), skill.hasUpdate ? _jsx(MetaText, { text: " \u2B06", tone: "warning" }) : null, starsStr ? _jsx(MetaText, { text: ` ${starsStr}`, tone: "warning" }) : null] }));
|
|
43
|
+
},
|
|
44
|
+
renderDetail: ({ item }) => {
|
|
45
|
+
const { skill } = item;
|
|
46
|
+
const fm = skill.frontmatter;
|
|
47
|
+
const description = fm?.description || skill.description || "Loading...";
|
|
48
|
+
const starsStr = formatStars(skill.stars);
|
|
49
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsxs("text", { fg: "cyan", children: [_jsx("strong", { children: skill.name }), starsStr ? _jsxs("span", { fg: "yellow", children: [" ", starsStr] }) : null] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "white", children: description }) }), fm?.category ? (_jsx(KeyValueLine, { label: "Category", value: _jsx("span", { fg: "cyan", children: fm.category }) })) : null, fm?.author ? (_jsx(KeyValueLine, { label: "Author", value: _jsx("span", { children: fm.author }) })) : null, fm?.version ? (_jsx(KeyValueLine, { label: "Version", value: _jsx("span", { children: fm.version }) })) : null, fm?.tags && fm.tags.length > 0 ? (_jsx(KeyValueLine, { label: "Tags", value: _jsx("span", { children: fm.tags.join(", ") }) })) : null, _jsxs(DetailSection, { children: [_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Source " }), _jsx("span", { fg: "#5c9aff", children: skill.source.repo })] }), _jsxs("text", { children: [_jsx("span", { fg: "gray", children: " " }), _jsx("span", { fg: "gray", children: skill.repoPath })] })] }), _jsxs(DetailSection, { children: [_jsx("text", { children: "─".repeat(24) }), _jsx("text", { children: _jsx("strong", { children: "Scopes:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { bg: "cyan", fg: "black", children: " u " }), _jsx("span", { fg: skill.installedScope === "user" ? "cyan" : "gray", children: skill.installedScope === "user" ? " ● " : " ○ " }), _jsx("span", { fg: "cyan", children: "User" }), _jsx("span", { fg: "gray", children: " ~/.claude/skills/" })] }), _jsxs("text", { children: [_jsx("span", { bg: "green", fg: "black", children: " p " }), _jsx("span", { fg: skill.installedScope === "project" ? "green" : "gray", children: skill.installedScope === "project" ? " ● " : " ○ " }), _jsx("span", { fg: "green", children: "Project" }), _jsx("span", { fg: "gray", children: " .claude/skills/" })] })] })] }), skill.hasUpdate && (_jsx("box", { marginTop: 1, children: _jsx("text", { bg: "yellow", fg: "black", children: " UPDATE AVAILABLE " }) })), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: skill.installed
|
|
50
|
+
? "Press u/p to toggle scope"
|
|
51
|
+
: "Press u/p to install" }) }), _jsx("box", { children: _jsxs("text", { children: [_jsx("span", { bg: "#555555", fg: "white", children: " o " }), _jsx("span", { fg: "gray", children: " Open in browser" })] }) })] }));
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
// ─── Registry ─────────────────────────────────────────────────────────────────
|
|
55
|
+
export const skillRenderers = {
|
|
56
|
+
category: categoryRenderer,
|
|
57
|
+
skill: skillRenderer,
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Dispatch rendering by item kind.
|
|
61
|
+
*/
|
|
62
|
+
export function renderSkillRow(item, _index, isSelected) {
|
|
63
|
+
if (item.kind === "category") {
|
|
64
|
+
return skillRenderers.category.renderRow({ item, isSelected });
|
|
65
|
+
}
|
|
66
|
+
return skillRenderers.skill.renderRow({ item, isSelected });
|
|
67
|
+
}
|
|
68
|
+
export function renderSkillDetail(item) {
|
|
69
|
+
if (!item)
|
|
70
|
+
return _jsx("text", { fg: "gray", children: "Select a skill to see details" });
|
|
71
|
+
if (item.kind === "category") {
|
|
72
|
+
return skillRenderers.category.renderDetail({ item });
|
|
73
|
+
}
|
|
74
|
+
return skillRenderers.skill.renderDetail({ item });
|
|
75
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ItemRenderer } from "../registry.js";
|
|
3
|
+
import type { SkillBrowserItem, SkillCategoryItem, SkillSkillItem } from "../adapters/skillsAdapter.js";
|
|
4
|
+
import {
|
|
5
|
+
SelectableRow,
|
|
6
|
+
ListCategoryRow,
|
|
7
|
+
ScopeSquares,
|
|
8
|
+
ScopeDetail,
|
|
9
|
+
ActionHints,
|
|
10
|
+
MetaText,
|
|
11
|
+
KeyValueLine,
|
|
12
|
+
DetailSection,
|
|
13
|
+
} from "../components/primitives/index.js";
|
|
14
|
+
|
|
15
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function formatStars(stars?: number): string {
|
|
18
|
+
if (!stars) return "";
|
|
19
|
+
if (stars >= 1000000) return `★ ${(stars / 1000000).toFixed(1)}M`;
|
|
20
|
+
if (stars >= 10000) return `★ ${Math.round(stars / 1000)}K`;
|
|
21
|
+
if (stars >= 1000) return `★ ${(stars / 1000).toFixed(1)}K`;
|
|
22
|
+
return `★ ${stars}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─── Category renderer ────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const categoryRenderer: ItemRenderer<SkillCategoryItem> = {
|
|
28
|
+
renderRow: ({ item, isSelected }) => {
|
|
29
|
+
const label = `${item.star ?? ""}${item.title}`;
|
|
30
|
+
return (
|
|
31
|
+
<ListCategoryRow
|
|
32
|
+
selected={isSelected}
|
|
33
|
+
title={label}
|
|
34
|
+
count={item.count}
|
|
35
|
+
badge={item.badge}
|
|
36
|
+
tone={item.tone}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
renderDetail: ({ item }) => {
|
|
42
|
+
const isRec = item.categoryKey === "recommended";
|
|
43
|
+
return (
|
|
44
|
+
<box flexDirection="column">
|
|
45
|
+
<text fg={isRec ? "green" : "cyan"}>
|
|
46
|
+
<strong>
|
|
47
|
+
{item.star ?? ""}
|
|
48
|
+
{item.title}
|
|
49
|
+
</strong>
|
|
50
|
+
</text>
|
|
51
|
+
<box marginTop={1}>
|
|
52
|
+
<text fg="gray">
|
|
53
|
+
{isRec
|
|
54
|
+
? "Curated skills recommended for most projects"
|
|
55
|
+
: "Popular skills sorted by stars"}
|
|
56
|
+
</text>
|
|
57
|
+
</box>
|
|
58
|
+
</box>
|
|
59
|
+
);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ─── Skill renderer ───────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
const MAX_SKILL_NAME_LEN = 35;
|
|
66
|
+
|
|
67
|
+
function truncateName(name: string): string {
|
|
68
|
+
return name.length > MAX_SKILL_NAME_LEN
|
|
69
|
+
? name.slice(0, MAX_SKILL_NAME_LEN - 1) + "\u2026"
|
|
70
|
+
: name;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const skillRenderer: ItemRenderer<SkillSkillItem> = {
|
|
74
|
+
renderRow: ({ item, isSelected }) => {
|
|
75
|
+
const { skill } = item;
|
|
76
|
+
const hasUser = skill.installedScope === "user";
|
|
77
|
+
const hasProject = skill.installedScope === "project";
|
|
78
|
+
const starsStr = formatStars(skill.stars);
|
|
79
|
+
const displayName = truncateName(skill.name);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<SelectableRow selected={isSelected} indent={1}>
|
|
83
|
+
<ScopeSquares user={hasUser} project={hasProject} selected={isSelected} />
|
|
84
|
+
<span> </span>
|
|
85
|
+
<span fg={isSelected ? "white" : skill.installed ? "white" : "gray"}>
|
|
86
|
+
{displayName}
|
|
87
|
+
</span>
|
|
88
|
+
{skill.hasUpdate ? <MetaText text=" ⬆" tone="warning" /> : null}
|
|
89
|
+
{starsStr ? <MetaText text={` ${starsStr}`} tone="warning" /> : null}
|
|
90
|
+
</SelectableRow>
|
|
91
|
+
);
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
renderDetail: ({ item }) => {
|
|
95
|
+
const { skill } = item;
|
|
96
|
+
const fm = skill.frontmatter;
|
|
97
|
+
const description = fm?.description || skill.description || "Loading...";
|
|
98
|
+
const starsStr = formatStars(skill.stars);
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<box flexDirection="column">
|
|
102
|
+
<text fg="cyan">
|
|
103
|
+
<strong>{skill.name}</strong>
|
|
104
|
+
{starsStr ? <span fg="yellow"> {starsStr}</span> : null}
|
|
105
|
+
</text>
|
|
106
|
+
|
|
107
|
+
<box marginTop={1}>
|
|
108
|
+
<text fg="white">{description}</text>
|
|
109
|
+
</box>
|
|
110
|
+
|
|
111
|
+
{fm?.category ? (
|
|
112
|
+
<KeyValueLine
|
|
113
|
+
label="Category"
|
|
114
|
+
value={<span fg="cyan">{fm.category}</span>}
|
|
115
|
+
/>
|
|
116
|
+
) : null}
|
|
117
|
+
{fm?.author ? (
|
|
118
|
+
<KeyValueLine label="Author" value={<span>{fm.author}</span>} />
|
|
119
|
+
) : null}
|
|
120
|
+
{fm?.version ? (
|
|
121
|
+
<KeyValueLine label="Version" value={<span>{fm.version}</span>} />
|
|
122
|
+
) : null}
|
|
123
|
+
{fm?.tags && fm.tags.length > 0 ? (
|
|
124
|
+
<KeyValueLine
|
|
125
|
+
label="Tags"
|
|
126
|
+
value={<span>{(fm.tags as string[]).join(", ")}</span>}
|
|
127
|
+
/>
|
|
128
|
+
) : null}
|
|
129
|
+
|
|
130
|
+
<DetailSection>
|
|
131
|
+
<text>
|
|
132
|
+
<span fg="gray">Source </span>
|
|
133
|
+
<span fg="#5c9aff">{skill.source.repo}</span>
|
|
134
|
+
</text>
|
|
135
|
+
<text>
|
|
136
|
+
<span fg="gray">{" "}</span>
|
|
137
|
+
<span fg="gray">{skill.repoPath}</span>
|
|
138
|
+
</text>
|
|
139
|
+
</DetailSection>
|
|
140
|
+
|
|
141
|
+
<DetailSection>
|
|
142
|
+
<text>{"─".repeat(24)}</text>
|
|
143
|
+
<text><strong>Scopes:</strong></text>
|
|
144
|
+
<box marginTop={1} flexDirection="column">
|
|
145
|
+
<text>
|
|
146
|
+
<span bg="cyan" fg="black"> u </span>
|
|
147
|
+
<span fg={skill.installedScope === "user" ? "cyan" : "gray"}>
|
|
148
|
+
{skill.installedScope === "user" ? " ● " : " ○ "}
|
|
149
|
+
</span>
|
|
150
|
+
<span fg="cyan">User</span>
|
|
151
|
+
<span fg="gray"> ~/.claude/skills/</span>
|
|
152
|
+
</text>
|
|
153
|
+
<text>
|
|
154
|
+
<span bg="green" fg="black"> p </span>
|
|
155
|
+
<span fg={skill.installedScope === "project" ? "green" : "gray"}>
|
|
156
|
+
{skill.installedScope === "project" ? " ● " : " ○ "}
|
|
157
|
+
</span>
|
|
158
|
+
<span fg="green">Project</span>
|
|
159
|
+
<span fg="gray"> .claude/skills/</span>
|
|
160
|
+
</text>
|
|
161
|
+
</box>
|
|
162
|
+
</DetailSection>
|
|
163
|
+
|
|
164
|
+
{skill.hasUpdate && (
|
|
165
|
+
<box marginTop={1}>
|
|
166
|
+
<text bg="yellow" fg="black"> UPDATE AVAILABLE </text>
|
|
167
|
+
</box>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
<box marginTop={1}>
|
|
171
|
+
<text fg="gray">
|
|
172
|
+
{skill.installed
|
|
173
|
+
? "Press u/p to toggle scope"
|
|
174
|
+
: "Press u/p to install"}
|
|
175
|
+
</text>
|
|
176
|
+
</box>
|
|
177
|
+
<box>
|
|
178
|
+
<text>
|
|
179
|
+
<span bg="#555555" fg="white"> o </span>
|
|
180
|
+
<span fg="gray"> Open in browser</span>
|
|
181
|
+
</text>
|
|
182
|
+
</box>
|
|
183
|
+
</box>
|
|
184
|
+
);
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// ─── Registry ─────────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
export const skillRenderers: {
|
|
191
|
+
category: ItemRenderer<SkillCategoryItem>;
|
|
192
|
+
skill: ItemRenderer<SkillSkillItem>;
|
|
193
|
+
} = {
|
|
194
|
+
category: categoryRenderer,
|
|
195
|
+
skill: skillRenderer,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Dispatch rendering by item kind.
|
|
200
|
+
*/
|
|
201
|
+
export function renderSkillRow(
|
|
202
|
+
item: SkillBrowserItem,
|
|
203
|
+
_index: number,
|
|
204
|
+
isSelected: boolean,
|
|
205
|
+
): React.ReactNode {
|
|
206
|
+
if (item.kind === "category") {
|
|
207
|
+
return skillRenderers.category.renderRow({ item, isSelected });
|
|
208
|
+
}
|
|
209
|
+
return skillRenderers.skill.renderRow({ item, isSelected });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function renderSkillDetail(
|
|
213
|
+
item: SkillBrowserItem | undefined,
|
|
214
|
+
): React.ReactNode {
|
|
215
|
+
if (!item) return <text fg="gray">Select a skill to see details</text>;
|
|
216
|
+
if (item.kind === "category") {
|
|
217
|
+
return skillRenderers.category.renderDetail({ item });
|
|
218
|
+
}
|
|
219
|
+
return skillRenderers.skill.renderDetail({ item });
|
|
220
|
+
}
|