claudeup 3.14.0 → 3.15.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 (29) hide show
  1. package/package.json +1 -1
  2. package/src/ui/adapters/skillsAdapter.js +105 -0
  3. package/src/ui/adapters/skillsAdapter.ts +159 -0
  4. package/src/ui/components/primitives/ActionHints.js +13 -0
  5. package/src/ui/components/primitives/ActionHints.tsx +41 -0
  6. package/src/ui/components/primitives/DetailSection.js +7 -0
  7. package/src/ui/components/primitives/DetailSection.tsx +22 -0
  8. package/src/ui/components/primitives/KeyValueLine.js +8 -0
  9. package/src/ui/components/primitives/KeyValueLine.tsx +19 -0
  10. package/src/ui/components/primitives/ListCategoryRow.js +8 -0
  11. package/src/ui/components/primitives/ListCategoryRow.tsx +38 -0
  12. package/src/ui/components/primitives/MetaText.js +8 -0
  13. package/src/ui/components/primitives/MetaText.tsx +14 -0
  14. package/src/ui/components/primitives/ScopeDetail.js +32 -0
  15. package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
  16. package/src/ui/components/primitives/ScopeSquares.js +11 -0
  17. package/src/ui/components/primitives/ScopeSquares.tsx +33 -0
  18. package/src/ui/components/primitives/SelectableRow.js +5 -0
  19. package/src/ui/components/primitives/SelectableRow.tsx +24 -0
  20. package/src/ui/components/primitives/index.js +8 -0
  21. package/src/ui/components/primitives/index.ts +9 -0
  22. package/src/ui/registry.js +1 -0
  23. package/src/ui/registry.ts +27 -0
  24. package/src/ui/renderers/skillRenderers.js +82 -0
  25. package/src/ui/renderers/skillRenderers.tsx +209 -0
  26. package/src/ui/screens/SkillsScreen.js +41 -190
  27. package/src/ui/screens/SkillsScreen.tsx +436 -796
  28. package/src/ui/theme.js +47 -0
  29. package/src/ui/theme.ts +53 -0
@@ -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,82 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
+ import { SelectableRow, ListCategoryRow, ScopeSquares, ScopeDetail, ActionHints, 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 skillRenderer = {
30
+ renderRow: ({ item, isSelected }) => {
31
+ const { skill } = item;
32
+ const hasUser = skill.installedScope === "user";
33
+ const hasProject = skill.installedScope === "project";
34
+ const starsStr = formatStars(skill.stars);
35
+ 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: skill.name }), skill.hasUpdate ? _jsx(MetaText, { text: " \u2B06", tone: "warning" }) : null, starsStr ? _jsx(MetaText, { text: ` ${starsStr}`, tone: "warning" }) : null] }));
36
+ },
37
+ renderDetail: ({ item }) => {
38
+ const { skill } = item;
39
+ const fm = skill.frontmatter;
40
+ const description = fm?.description || skill.description || "Loading...";
41
+ const starsStr = formatStars(skill.stars);
42
+ 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 })] })] }), skill.installed && skill.installedScope && (_jsxs(DetailSection, { children: [_jsx("text", { children: "─".repeat(24) }), _jsx(ScopeDetail, { scopes: {
43
+ user: skill.installedScope === "user",
44
+ project: skill.installedScope === "project",
45
+ }, paths: {
46
+ user: "~/.claude/skills/",
47
+ project: ".claude/skills/",
48
+ } })] })), skill.hasUpdate && (_jsx("box", { marginTop: 1, children: _jsxs("text", { bg: "yellow", fg: "black", children: [" ", "UPDATE AVAILABLE", " "] }) })), _jsx(ActionHints, { hints: skill.installed
49
+ ? [
50
+ { key: "d", label: "Uninstall", tone: "danger" },
51
+ { key: "u/p", label: "Reinstall in user/project scope" },
52
+ { key: "o", label: "Open in browser" },
53
+ ]
54
+ : [
55
+ { key: "u", label: "Install in user scope", tone: "primary" },
56
+ { key: "p", label: "Install in project scope", tone: "primary" },
57
+ { key: "o", label: "Open in browser" },
58
+ ] })] }));
59
+ },
60
+ };
61
+ // ─── Registry ─────────────────────────────────────────────────────────────────
62
+ export const skillRenderers = {
63
+ category: categoryRenderer,
64
+ skill: skillRenderer,
65
+ };
66
+ /**
67
+ * Dispatch rendering by item kind.
68
+ */
69
+ export function renderSkillRow(item, _index, isSelected) {
70
+ if (item.kind === "category") {
71
+ return skillRenderers.category.renderRow({ item, isSelected });
72
+ }
73
+ return skillRenderers.skill.renderRow({ item, isSelected });
74
+ }
75
+ export function renderSkillDetail(item) {
76
+ if (!item)
77
+ return _jsx("text", { fg: "gray", children: "Select a skill to see details" });
78
+ if (item.kind === "category") {
79
+ return skillRenderers.category.renderDetail({ item });
80
+ }
81
+ return skillRenderers.skill.renderDetail({ item });
82
+ }
@@ -0,0 +1,209 @@
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 skillRenderer: ItemRenderer<SkillSkillItem> = {
66
+ renderRow: ({ item, isSelected }) => {
67
+ const { skill } = item;
68
+ const hasUser = skill.installedScope === "user";
69
+ const hasProject = skill.installedScope === "project";
70
+ const starsStr = formatStars(skill.stars);
71
+
72
+ return (
73
+ <SelectableRow selected={isSelected} indent={1}>
74
+ <ScopeSquares user={hasUser} project={hasProject} selected={isSelected} />
75
+ <span> </span>
76
+ <span fg={isSelected ? "white" : skill.installed ? "white" : "gray"}>
77
+ {skill.name}
78
+ </span>
79
+ {skill.hasUpdate ? <MetaText text=" ⬆" tone="warning" /> : null}
80
+ {starsStr ? <MetaText text={` ${starsStr}`} tone="warning" /> : null}
81
+ </SelectableRow>
82
+ );
83
+ },
84
+
85
+ renderDetail: ({ item }) => {
86
+ const { skill } = item;
87
+ const fm = skill.frontmatter;
88
+ const description = fm?.description || skill.description || "Loading...";
89
+ const starsStr = formatStars(skill.stars);
90
+
91
+ return (
92
+ <box flexDirection="column">
93
+ <text fg="cyan">
94
+ <strong>{skill.name}</strong>
95
+ {starsStr ? <span fg="yellow"> {starsStr}</span> : null}
96
+ </text>
97
+
98
+ <box marginTop={1}>
99
+ <text fg="white">{description}</text>
100
+ </box>
101
+
102
+ {fm?.category ? (
103
+ <KeyValueLine
104
+ label="Category"
105
+ value={<span fg="cyan">{fm.category}</span>}
106
+ />
107
+ ) : null}
108
+ {fm?.author ? (
109
+ <KeyValueLine label="Author" value={<span>{fm.author}</span>} />
110
+ ) : null}
111
+ {fm?.version ? (
112
+ <KeyValueLine label="Version" value={<span>{fm.version}</span>} />
113
+ ) : null}
114
+ {fm?.tags && fm.tags.length > 0 ? (
115
+ <KeyValueLine
116
+ label="Tags"
117
+ value={<span>{(fm.tags as string[]).join(", ")}</span>}
118
+ />
119
+ ) : null}
120
+
121
+ <DetailSection>
122
+ <text>
123
+ <span fg="gray">Source </span>
124
+ <span fg="#5c9aff">{skill.source.repo}</span>
125
+ </text>
126
+ <text>
127
+ <span fg="gray">{" "}</span>
128
+ <span fg="gray">{skill.repoPath}</span>
129
+ </text>
130
+ </DetailSection>
131
+
132
+ {skill.installed && skill.installedScope && (
133
+ <DetailSection>
134
+ <text>{"─".repeat(24)}</text>
135
+ <ScopeDetail
136
+ scopes={{
137
+ user: skill.installedScope === "user",
138
+ project: skill.installedScope === "project",
139
+ }}
140
+ paths={{
141
+ user: "~/.claude/skills/",
142
+ project: ".claude/skills/",
143
+ }}
144
+ />
145
+ </DetailSection>
146
+ )}
147
+
148
+ {skill.hasUpdate && (
149
+ <box marginTop={1}>
150
+ <text bg="yellow" fg="black">
151
+ {" "}
152
+ UPDATE AVAILABLE{" "}
153
+ </text>
154
+ </box>
155
+ )}
156
+
157
+ <ActionHints
158
+ hints={
159
+ skill.installed
160
+ ? [
161
+ { key: "d", label: "Uninstall", tone: "danger" },
162
+ { key: "u/p", label: "Reinstall in user/project scope" },
163
+ { key: "o", label: "Open in browser" },
164
+ ]
165
+ : [
166
+ { key: "u", label: "Install in user scope", tone: "primary" },
167
+ { key: "p", label: "Install in project scope", tone: "primary" },
168
+ { key: "o", label: "Open in browser" },
169
+ ]
170
+ }
171
+ />
172
+ </box>
173
+ );
174
+ },
175
+ };
176
+
177
+ // ─── Registry ─────────────────────────────────────────────────────────────────
178
+
179
+ export const skillRenderers: {
180
+ category: ItemRenderer<SkillCategoryItem>;
181
+ skill: ItemRenderer<SkillSkillItem>;
182
+ } = {
183
+ category: categoryRenderer,
184
+ skill: skillRenderer,
185
+ };
186
+
187
+ /**
188
+ * Dispatch rendering by item kind.
189
+ */
190
+ export function renderSkillRow(
191
+ item: SkillBrowserItem,
192
+ _index: number,
193
+ isSelected: boolean,
194
+ ): React.ReactNode {
195
+ if (item.kind === "category") {
196
+ return skillRenderers.category.renderRow({ item, isSelected });
197
+ }
198
+ return skillRenderers.skill.renderRow({ item, isSelected });
199
+ }
200
+
201
+ export function renderSkillDetail(
202
+ item: SkillBrowserItem | undefined,
203
+ ): React.ReactNode {
204
+ if (!item) return <text fg="gray">Select a skill to see details</text>;
205
+ if (item.kind === "category") {
206
+ return skillRenderers.category.renderDetail({ item });
207
+ }
208
+ return skillRenderers.skill.renderDetail({ item });
209
+ }