@laststance/claude-plugin-dashboard 0.2.2 → 0.3.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/README.md +17 -1
- package/dist/app.d.ts +7 -1
- package/dist/app.js +231 -84
- package/dist/cli.js +58 -67
- package/dist/components/ComponentBadges.d.ts +3 -3
- package/dist/components/ComponentBadges.js +12 -11
- package/dist/components/ComponentList.d.ts +53 -0
- package/dist/components/ComponentList.js +193 -0
- package/dist/components/KeyHints.js +25 -28
- package/dist/components/MarketplaceActionMenu.d.ts +41 -0
- package/dist/components/MarketplaceActionMenu.js +68 -0
- package/dist/components/MarketplaceDetail.d.ts +10 -3
- package/dist/components/MarketplaceDetail.js +10 -4
- package/dist/components/PluginDetail.d.ts +3 -0
- package/dist/components/PluginDetail.js +28 -4
- package/dist/components/PluginList.js +19 -7
- package/dist/services/componentService.d.ts +12 -1
- package/dist/services/componentService.js +238 -0
- package/dist/services/marketplaceActionsService.d.ts +17 -0
- package/dist/services/marketplaceActionsService.js +18 -0
- package/dist/services/pluginService.js +78 -2
- package/dist/tabs/DiscoverTab.js +1 -1
- package/dist/tabs/EnabledTab.js +2 -2
- package/dist/tabs/InstalledTab.js +2 -2
- package/dist/tabs/MarketplacesTab.d.ts +15 -2
- package/dist/tabs/MarketplacesTab.js +13 -4
- package/dist/types/index.d.ts +110 -1
- package/package.json +5 -3
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* ComponentBadges component
|
|
4
|
-
* Displays plugin component type badges with
|
|
5
|
-
*
|
|
4
|
+
* Displays plugin component type badges with readable text labels and counts
|
|
5
|
+
* Labels: Skills, Slash, Agents, Hooks, MCP, LSP
|
|
6
6
|
*/
|
|
7
7
|
import { Box, Text } from 'ink';
|
|
8
8
|
/**
|
|
9
|
-
* Badge configurations with
|
|
9
|
+
* Badge configurations with readable text labels and colors
|
|
10
10
|
*/
|
|
11
11
|
const BADGE_CONFIGS = [
|
|
12
|
-
{ label: '
|
|
13
|
-
{ label: '
|
|
14
|
-
{ label: '
|
|
15
|
-
{ label: '
|
|
16
|
-
{ label: '
|
|
17
|
-
{ label: '
|
|
12
|
+
{ label: 'Skills', color: 'magenta', key: 'skills' },
|
|
13
|
+
{ label: 'Slash', color: 'cyan', key: 'commands' },
|
|
14
|
+
{ label: 'Agents', color: 'blue', key: 'agents' },
|
|
15
|
+
{ label: 'Hooks', color: 'yellow', key: 'hooks', isBoolean: true },
|
|
16
|
+
{ label: 'MCP', color: 'green', key: 'mcpServers' },
|
|
17
|
+
{ label: 'LSP', color: 'blueBright', key: 'lspServers' },
|
|
18
18
|
];
|
|
19
19
|
/**
|
|
20
20
|
* Displays component type badges for a plugin
|
|
@@ -23,7 +23,7 @@ const BADGE_CONFIGS = [
|
|
|
23
23
|
* @returns Badges component or null if no components
|
|
24
24
|
* @example
|
|
25
25
|
* <ComponentBadges components={{ skills: 5, commands: 2 }} />
|
|
26
|
-
* // Renders:
|
|
26
|
+
* // Renders: Skills:5 Slash:2
|
|
27
27
|
*/
|
|
28
28
|
export default function ComponentBadges({ components, }) {
|
|
29
29
|
if (!components) {
|
|
@@ -43,9 +43,10 @@ export default function ComponentBadges({ components, }) {
|
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
45
|
* Single badge component
|
|
46
|
+
* Renders as "Label:count" without brackets for better readability
|
|
46
47
|
*/
|
|
47
48
|
function Badge({ label, count, color, }) {
|
|
48
|
-
return (_jsxs(Text, { children: [_jsx(Text, {
|
|
49
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: color, bold: true, children: label }), count !== undefined && _jsxs(Text, { dimColor: true, children: [":", count] })] }));
|
|
49
50
|
}
|
|
50
51
|
/**
|
|
51
52
|
* Get a human-readable description of component types
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ComponentList component
|
|
3
|
+
* Displays detailed plugin components in a collapsible list format
|
|
4
|
+
* Shows component names when available, falls back to counts
|
|
5
|
+
*
|
|
6
|
+
* Data Source Architecture:
|
|
7
|
+
* - Installed plugins: Names + descriptions from file system scan
|
|
8
|
+
* - Not installed: Names only from marketplace JSON (if available)
|
|
9
|
+
* - Fallback: Count-only display from PluginComponents
|
|
10
|
+
*/
|
|
11
|
+
import type { PluginComponents, PluginComponentsDetailed } from '../types/index.js';
|
|
12
|
+
/**
|
|
13
|
+
* Props for ComponentList
|
|
14
|
+
*/
|
|
15
|
+
export interface ComponentListProps {
|
|
16
|
+
/** Component counts (backward compat fallback) */
|
|
17
|
+
components?: PluginComponents;
|
|
18
|
+
/** Detailed component info with names */
|
|
19
|
+
componentsDetailed?: PluginComponentsDetailed;
|
|
20
|
+
/** Maximum visible items per category (default: 3) */
|
|
21
|
+
maxItems?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Displays component details in a collapsible list
|
|
25
|
+
* Shows names when available, falls back to counts
|
|
26
|
+
* @param props - ComponentListProps
|
|
27
|
+
* @returns React node or null if no components
|
|
28
|
+
* @example
|
|
29
|
+
* <ComponentList
|
|
30
|
+
* componentsDetailed={{ skills: [{ name: 'xlsx', type: 'skill' }] }}
|
|
31
|
+
* maxItems={3}
|
|
32
|
+
* />
|
|
33
|
+
*/
|
|
34
|
+
export default function ComponentList({ components, componentsDetailed, maxItems, }: ComponentListProps): React.ReactNode;
|
|
35
|
+
/**
|
|
36
|
+
* Check if PluginComponentsDetailed has any data
|
|
37
|
+
* @param detailed - Detailed components object
|
|
38
|
+
* @returns true if any category has items
|
|
39
|
+
* @example
|
|
40
|
+
* hasAnyDetailedComponents({ skills: [{ name: 'xlsx', type: 'skill' }] }) // => true
|
|
41
|
+
* hasAnyDetailedComponents({}) // => false
|
|
42
|
+
*/
|
|
43
|
+
export declare function hasAnyDetailedComponents(detailed: PluginComponentsDetailed): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Check if PluginComponents has any count data
|
|
46
|
+
* @param components - Components counts object
|
|
47
|
+
* @returns true if any category has count > 0
|
|
48
|
+
* @example
|
|
49
|
+
* hasAnyCountComponents({ skills: 5 }) // => true
|
|
50
|
+
* hasAnyCountComponents({ hooks: true }) // => true
|
|
51
|
+
* hasAnyCountComponents({}) // => false
|
|
52
|
+
*/
|
|
53
|
+
export declare function hasAnyCountComponents(components: PluginComponents): boolean;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ComponentList component
|
|
4
|
+
* Displays detailed plugin components in a collapsible list format
|
|
5
|
+
* Shows component names when available, falls back to counts
|
|
6
|
+
*
|
|
7
|
+
* Data Source Architecture:
|
|
8
|
+
* - Installed plugins: Names + descriptions from file system scan
|
|
9
|
+
* - Not installed: Names only from marketplace JSON (if available)
|
|
10
|
+
* - Fallback: Count-only display from PluginComponents
|
|
11
|
+
*/
|
|
12
|
+
import { Box, Text } from 'ink';
|
|
13
|
+
/**
|
|
14
|
+
* Category configurations with display settings
|
|
15
|
+
*/
|
|
16
|
+
const CATEGORY_CONFIGS = [
|
|
17
|
+
{
|
|
18
|
+
label: 'Skills',
|
|
19
|
+
color: 'magenta',
|
|
20
|
+
detailedKey: 'skills',
|
|
21
|
+
countKey: 'skills',
|
|
22
|
+
type: 'skill',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
label: 'Slash',
|
|
26
|
+
color: 'cyan',
|
|
27
|
+
detailedKey: 'commands',
|
|
28
|
+
countKey: 'commands',
|
|
29
|
+
type: 'command',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: 'Agents',
|
|
33
|
+
color: 'blue',
|
|
34
|
+
detailedKey: 'agents',
|
|
35
|
+
countKey: 'agents',
|
|
36
|
+
type: 'agent',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: 'MCP',
|
|
40
|
+
color: 'green',
|
|
41
|
+
detailedKey: 'mcpServers',
|
|
42
|
+
countKey: 'mcpServers',
|
|
43
|
+
type: 'mcp',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: 'LSP',
|
|
47
|
+
color: 'blueBright',
|
|
48
|
+
detailedKey: 'lspServers',
|
|
49
|
+
countKey: 'lspServers',
|
|
50
|
+
type: 'lsp',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: 'Hooks',
|
|
54
|
+
color: 'yellow',
|
|
55
|
+
detailedKey: 'hooks',
|
|
56
|
+
countKey: 'hooks',
|
|
57
|
+
type: 'hook',
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
/**
|
|
61
|
+
* Default maximum visible items per category
|
|
62
|
+
*/
|
|
63
|
+
const DEFAULT_MAX_ITEMS = 3;
|
|
64
|
+
/**
|
|
65
|
+
* Displays component details in a collapsible list
|
|
66
|
+
* Shows names when available, falls back to counts
|
|
67
|
+
* @param props - ComponentListProps
|
|
68
|
+
* @returns React node or null if no components
|
|
69
|
+
* @example
|
|
70
|
+
* <ComponentList
|
|
71
|
+
* componentsDetailed={{ skills: [{ name: 'xlsx', type: 'skill' }] }}
|
|
72
|
+
* maxItems={3}
|
|
73
|
+
* />
|
|
74
|
+
*/
|
|
75
|
+
export default function ComponentList({ components, componentsDetailed, maxItems = DEFAULT_MAX_ITEMS, }) {
|
|
76
|
+
// No data at all
|
|
77
|
+
if (!components && !componentsDetailed) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
// Check if we have any components to display
|
|
81
|
+
const hasDetailedData = componentsDetailed && hasAnyDetailedComponents(componentsDetailed);
|
|
82
|
+
const hasCountData = components && hasAnyCountComponents(components);
|
|
83
|
+
if (!hasDetailedData && !hasCountData) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 0, children: _jsx(Text, { color: "cyan", bold: true, children: "\u2500\u2500 Components \u2500\u2500" }) }), CATEGORY_CONFIGS.map((config) => {
|
|
87
|
+
const detailedItems = componentsDetailed?.[config.detailedKey];
|
|
88
|
+
const count = components?.[config.countKey];
|
|
89
|
+
// Skip if no data for this category
|
|
90
|
+
if (!detailedItems?.length && !count) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
// Prefer detailed data, fall back to count
|
|
94
|
+
if (detailedItems && detailedItems.length > 0) {
|
|
95
|
+
// Type guard: detailedItems could be ComponentInfo[] or string[]
|
|
96
|
+
if (isComponentInfoArray(detailedItems)) {
|
|
97
|
+
return (_jsx(CategorySection, { label: config.label, color: config.color, items: detailedItems, maxItems: maxItems }, config.detailedKey));
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// String array (mcpServers, lspServers, hooks)
|
|
101
|
+
return (_jsx(CategorySectionSimple, { label: config.label, color: config.color, items: detailedItems, maxItems: maxItems }, config.detailedKey));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Count-only fallback (number) or boolean hooks fallback
|
|
105
|
+
if (typeof count === 'number' && count > 0) {
|
|
106
|
+
return (_jsx(CountOnlySection, { label: config.label, color: config.color, count: count }, config.countKey));
|
|
107
|
+
}
|
|
108
|
+
// Boolean hooks fallback (hooks: true without detailed info)
|
|
109
|
+
if (config.countKey === 'hooks' && count === true) {
|
|
110
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: config.color, bold: true, children: config.label }), _jsx(Text, { dimColor: true, children: " (configured)" })] }, config.countKey));
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
})] }));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Single category section with collapsible ComponentInfo items
|
|
117
|
+
* Shows first N items, then "+M more..." for overflow
|
|
118
|
+
* @returns React node for the category section
|
|
119
|
+
*/
|
|
120
|
+
function CategorySection({ label, color, items, maxItems, }) {
|
|
121
|
+
const visibleItems = items.slice(0, maxItems);
|
|
122
|
+
const remainingCount = items.length - maxItems;
|
|
123
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: color, bold: true, children: label }), _jsxs(Text, { dimColor: true, children: [" (", items.length, ")"] })] }), visibleItems.map((item) => (_jsxs(Text, { children: [' ', "\u2022 ", item.name, item.description && (_jsxs(Text, { dimColor: true, children: [" - ", truncate(item.description, 30)] }))] }, item.name))), remainingCount > 0 && (_jsxs(Text, { dimColor: true, children: [' ', "\u2514\u2500 +", remainingCount, " more..."] }))] }));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Category section for simple string arrays (mcpServers, lspServers, hooks)
|
|
127
|
+
* @returns React node for the category section
|
|
128
|
+
*/
|
|
129
|
+
function CategorySectionSimple({ label, color, items, maxItems, }) {
|
|
130
|
+
const visibleItems = items.slice(0, maxItems);
|
|
131
|
+
const remainingCount = items.length - maxItems;
|
|
132
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: color, bold: true, children: label }), _jsxs(Text, { dimColor: true, children: [" (", items.length, ")"] })] }), visibleItems.map((item) => (_jsxs(Text, { children: [' ', "\u2022 ", item] }, item))), remainingCount > 0 && (_jsxs(Text, { dimColor: true, children: [' ', "\u2514\u2500 +", remainingCount, " more..."] }))] }));
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Category section showing only count (fallback when no detailed info)
|
|
136
|
+
* @returns React node for the count-only section
|
|
137
|
+
*/
|
|
138
|
+
function CountOnlySection({ label, color, count, }) {
|
|
139
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: color, bold: true, children: label }), _jsxs(Text, { dimColor: true, children: [": ", count] })] }));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Type guard to check if array is ComponentInfo[]
|
|
143
|
+
* @param arr - Array to check
|
|
144
|
+
* @returns true if array contains ComponentInfo objects
|
|
145
|
+
*/
|
|
146
|
+
function isComponentInfoArray(arr) {
|
|
147
|
+
return arr.length > 0 && typeof arr[0] === 'object' && 'name' in arr[0];
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Check if PluginComponentsDetailed has any data
|
|
151
|
+
* @param detailed - Detailed components object
|
|
152
|
+
* @returns true if any category has items
|
|
153
|
+
* @example
|
|
154
|
+
* hasAnyDetailedComponents({ skills: [{ name: 'xlsx', type: 'skill' }] }) // => true
|
|
155
|
+
* hasAnyDetailedComponents({}) // => false
|
|
156
|
+
*/
|
|
157
|
+
export function hasAnyDetailedComponents(detailed) {
|
|
158
|
+
return ((detailed.skills?.length ?? 0) > 0 ||
|
|
159
|
+
(detailed.commands?.length ?? 0) > 0 ||
|
|
160
|
+
(detailed.agents?.length ?? 0) > 0 ||
|
|
161
|
+
(detailed.hooks?.length ?? 0) > 0 ||
|
|
162
|
+
(detailed.mcpServers?.length ?? 0) > 0 ||
|
|
163
|
+
(detailed.lspServers?.length ?? 0) > 0);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Check if PluginComponents has any count data
|
|
167
|
+
* @param components - Components counts object
|
|
168
|
+
* @returns true if any category has count > 0
|
|
169
|
+
* @example
|
|
170
|
+
* hasAnyCountComponents({ skills: 5 }) // => true
|
|
171
|
+
* hasAnyCountComponents({ hooks: true }) // => true
|
|
172
|
+
* hasAnyCountComponents({}) // => false
|
|
173
|
+
*/
|
|
174
|
+
export function hasAnyCountComponents(components) {
|
|
175
|
+
return ((components.skills ?? 0) > 0 ||
|
|
176
|
+
(components.commands ?? 0) > 0 ||
|
|
177
|
+
(components.agents ?? 0) > 0 ||
|
|
178
|
+
components.hooks === true ||
|
|
179
|
+
(components.mcpServers ?? 0) > 0 ||
|
|
180
|
+
(components.lspServers ?? 0) > 0);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Truncate string to max length with ellipsis
|
|
184
|
+
* @param str - String to truncate
|
|
185
|
+
* @param maxLength - Maximum length
|
|
186
|
+
* @returns Truncated string
|
|
187
|
+
*/
|
|
188
|
+
function truncate(str, maxLength) {
|
|
189
|
+
if (str.length <= maxLength) {
|
|
190
|
+
return str;
|
|
191
|
+
}
|
|
192
|
+
return str.slice(0, maxLength - 3) + '...';
|
|
193
|
+
}
|
|
@@ -4,40 +4,37 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
* Displays keyboard shortcuts footer at the bottom of the dashboard
|
|
5
5
|
*/
|
|
6
6
|
import { Box, Text } from 'ink';
|
|
7
|
+
import { match } from 'ts-pattern';
|
|
7
8
|
/**
|
|
8
9
|
* Get base hints based on current focus zone
|
|
9
10
|
* @param focusZone - Current focus zone
|
|
10
11
|
* @returns Array of hint objects
|
|
11
12
|
*/
|
|
12
13
|
function getBaseHints(focusZone) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{ key: 'h', action: 'help' },
|
|
38
|
-
{ key: 'q or ^C', action: 'quit' },
|
|
39
|
-
];
|
|
40
|
-
}
|
|
14
|
+
return match(focusZone)
|
|
15
|
+
.with('tabbar', () => [
|
|
16
|
+
{ key: '←/→', action: 'switch tabs' },
|
|
17
|
+
{ key: '↓', action: 'search/list' },
|
|
18
|
+
{ key: 'Tab', action: 'next tab' },
|
|
19
|
+
{ key: 'h', action: 'help' },
|
|
20
|
+
{ key: 'q or ^C', action: 'quit' },
|
|
21
|
+
])
|
|
22
|
+
.with('search', () => [
|
|
23
|
+
{ key: '↑', action: 'tabs' },
|
|
24
|
+
{ key: '↓/Enter', action: 'list' },
|
|
25
|
+
{ key: 'ESC', action: 'clear/exit' },
|
|
26
|
+
{ key: 'h', action: 'help' },
|
|
27
|
+
{ key: 'q or ^C', action: 'quit' },
|
|
28
|
+
])
|
|
29
|
+
.with('list', () => [
|
|
30
|
+
{ key: '↑/↓', action: 'navigate' },
|
|
31
|
+
{ key: '↑(top)', action: 'search' },
|
|
32
|
+
{ key: 'Space', action: 'toggle' },
|
|
33
|
+
{ key: 'Tab', action: 'next tab' },
|
|
34
|
+
{ key: 'h', action: 'help' },
|
|
35
|
+
{ key: 'q or ^C', action: 'quit' },
|
|
36
|
+
])
|
|
37
|
+
.exhaustive();
|
|
41
38
|
}
|
|
42
39
|
/**
|
|
43
40
|
* Displays keyboard shortcut hints in the footer
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action menu component for marketplace operations
|
|
3
|
+
* Displays selectable list of actions: Browse, Update, Auto-update toggle, Remove
|
|
4
|
+
*/
|
|
5
|
+
import type { Marketplace } from '../types/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Available marketplace actions
|
|
8
|
+
*/
|
|
9
|
+
export type MarketplaceAction = 'browse' | 'update' | 'autoUpdate' | 'remove';
|
|
10
|
+
/**
|
|
11
|
+
* Props for MarketplaceActionMenu component
|
|
12
|
+
*/
|
|
13
|
+
export interface MarketplaceActionMenuProps {
|
|
14
|
+
/** Currently selected marketplace */
|
|
15
|
+
marketplace: Marketplace;
|
|
16
|
+
/** Index of selected action in menu */
|
|
17
|
+
selectedIndex: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* MarketplaceActionMenu - Selectable action list for marketplace operations
|
|
21
|
+
* @param props - Component props
|
|
22
|
+
* @returns Action menu UI with keyboard navigation support
|
|
23
|
+
* @example
|
|
24
|
+
* <MarketplaceActionMenu
|
|
25
|
+
* marketplace={selectedMarketplace}
|
|
26
|
+
* selectedIndex={0}
|
|
27
|
+
* />
|
|
28
|
+
*/
|
|
29
|
+
export default function MarketplaceActionMenu({ marketplace, selectedIndex, }: MarketplaceActionMenuProps): import("react/jsx-runtime").JSX.Element;
|
|
30
|
+
/**
|
|
31
|
+
* Get action at specified index
|
|
32
|
+
* @param marketplace - The marketplace
|
|
33
|
+
* @param index - Action index
|
|
34
|
+
* @returns The action id at the index
|
|
35
|
+
*/
|
|
36
|
+
export declare function getActionAtIndex(marketplace: Marketplace, index: number): MarketplaceAction;
|
|
37
|
+
/**
|
|
38
|
+
* Get total number of actions available
|
|
39
|
+
* @returns Number of actions in menu
|
|
40
|
+
*/
|
|
41
|
+
export declare function getActionCount(): number;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Action menu component for marketplace operations
|
|
4
|
+
* Displays selectable list of actions: Browse, Update, Auto-update toggle, Remove
|
|
5
|
+
*/
|
|
6
|
+
import { Box, Text } from 'ink';
|
|
7
|
+
/**
|
|
8
|
+
* Get action items for the marketplace
|
|
9
|
+
* @param marketplace - The marketplace to get actions for
|
|
10
|
+
* @returns Array of action items with dynamic labels
|
|
11
|
+
*/
|
|
12
|
+
function getActionItems(marketplace) {
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
id: 'browse',
|
|
16
|
+
label: `Browse plugins (${marketplace.pluginCount || 0})`,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'update',
|
|
20
|
+
label: 'Update marketplace',
|
|
21
|
+
description: `last updated ${marketplace.lastUpdated}`,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'autoUpdate',
|
|
25
|
+
label: marketplace.autoUpdate
|
|
26
|
+
? 'Disable auto-update'
|
|
27
|
+
: 'Enable auto-update',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'remove',
|
|
31
|
+
label: 'Remove marketplace',
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* MarketplaceActionMenu - Selectable action list for marketplace operations
|
|
37
|
+
* @param props - Component props
|
|
38
|
+
* @returns Action menu UI with keyboard navigation support
|
|
39
|
+
* @example
|
|
40
|
+
* <MarketplaceActionMenu
|
|
41
|
+
* marketplace={selectedMarketplace}
|
|
42
|
+
* selectedIndex={0}
|
|
43
|
+
* />
|
|
44
|
+
*/
|
|
45
|
+
export default function MarketplaceActionMenu({ marketplace, selectedIndex, }) {
|
|
46
|
+
const actions = getActionItems(marketplace);
|
|
47
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [actions.map((action, index) => {
|
|
48
|
+
const isSelected = index === selectedIndex;
|
|
49
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: isSelected ? 'cyan' : 'white', children: isSelected ? '❯' : ' ' }), _jsx(Text, { color: isSelected ? 'cyan' : 'white', bold: isSelected, children: action.label }), action.description && _jsxs(Text, { dimColor: true, children: ["(", action.description, ")"] })] }, action.id));
|
|
50
|
+
}), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter to select \u00B7 escape to go back" }) })] }));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get action at specified index
|
|
54
|
+
* @param marketplace - The marketplace
|
|
55
|
+
* @param index - Action index
|
|
56
|
+
* @returns The action id at the index
|
|
57
|
+
*/
|
|
58
|
+
export function getActionAtIndex(marketplace, index) {
|
|
59
|
+
const actions = getActionItems(marketplace);
|
|
60
|
+
return actions[index]?.id ?? 'browse';
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get total number of actions available
|
|
64
|
+
* @returns Number of actions in menu
|
|
65
|
+
*/
|
|
66
|
+
export function getActionCount() {
|
|
67
|
+
return 4; // browse, update, autoUpdate, remove
|
|
68
|
+
}
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MarketplaceDetail component
|
|
3
|
-
* Right panel showing marketplace information
|
|
3
|
+
* Right panel showing marketplace information and action menu
|
|
4
4
|
*/
|
|
5
5
|
import type { Marketplace } from '../types/index.js';
|
|
6
6
|
interface MarketplaceDetailProps {
|
|
7
7
|
marketplace: Marketplace | null;
|
|
8
|
+
showActionMenu?: boolean;
|
|
9
|
+
actionMenuSelectedIndex?: number;
|
|
8
10
|
}
|
|
9
11
|
/**
|
|
10
12
|
* Displays detailed information about a selected marketplace
|
|
13
|
+
* Shows action menu when showActionMenu is true
|
|
11
14
|
* @example
|
|
12
|
-
* <MarketplaceDetail
|
|
15
|
+
* <MarketplaceDetail
|
|
16
|
+
* marketplace={selectedMarketplace}
|
|
17
|
+
* showActionMenu={state.showMarketplaceActionMenu}
|
|
18
|
+
* actionMenuSelectedIndex={state.actionMenuSelectedIndex}
|
|
19
|
+
* />
|
|
13
20
|
*/
|
|
14
|
-
export default function MarketplaceDetail({ marketplace, }: MarketplaceDetailProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export default function MarketplaceDetail({ marketplace, showActionMenu, actionMenuSelectedIndex, }: MarketplaceDetailProps): import("react/jsx-runtime").JSX.Element;
|
|
15
22
|
export {};
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* MarketplaceDetail component
|
|
4
|
-
* Right panel showing marketplace information
|
|
4
|
+
* Right panel showing marketplace information and action menu
|
|
5
5
|
*/
|
|
6
6
|
import { Box, Text } from 'ink';
|
|
7
|
+
import MarketplaceActionMenu from './MarketplaceActionMenu.js';
|
|
7
8
|
/**
|
|
8
9
|
* Displays detailed information about a selected marketplace
|
|
10
|
+
* Shows action menu when showActionMenu is true
|
|
9
11
|
* @example
|
|
10
|
-
* <MarketplaceDetail
|
|
12
|
+
* <MarketplaceDetail
|
|
13
|
+
* marketplace={selectedMarketplace}
|
|
14
|
+
* showActionMenu={state.showMarketplaceActionMenu}
|
|
15
|
+
* actionMenuSelectedIndex={state.actionMenuSelectedIndex}
|
|
16
|
+
* />
|
|
11
17
|
*/
|
|
12
|
-
export default function MarketplaceDetail({ marketplace, }) {
|
|
18
|
+
export default function MarketplaceDetail({ marketplace, showActionMenu = false, actionMenuSelectedIndex = 0, }) {
|
|
13
19
|
if (!marketplace) {
|
|
14
20
|
return (_jsx(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "gray", children: _jsx(Text, { dimColor: true, children: "Select a marketplace to view details" }) }));
|
|
15
21
|
}
|
|
16
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: marketplace.name || marketplace.id }) }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(DetailRow, { label: "ID", value: marketplace.id }), _jsx(DetailRow, { label: "Plugins", value: `${marketplace.pluginCount || 0}` }), _jsx(DetailRow, { label: "Source", value: marketplace.source.source }), marketplace.source.url && (_jsx(DetailRow, { label: "URL", value: marketplace.source.url })), marketplace.source.repo && (_jsx(DetailRow, { label: "Repo", value: marketplace.source.repo })), _jsx(DetailRow, { label: "Last Updated", value: formatDate(marketplace.lastUpdated) })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "Install Location:" }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: marketplace.installLocation })] })] }));
|
|
22
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: marketplace.name || marketplace.id }) }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(DetailRow, { label: "ID", value: marketplace.id }), _jsx(DetailRow, { label: "Plugins", value: `${marketplace.pluginCount || 0}` }), _jsx(DetailRow, { label: "Source", value: marketplace.source.source }), marketplace.source.url && (_jsx(DetailRow, { label: "URL", value: marketplace.source.url })), marketplace.source.repo && (_jsx(DetailRow, { label: "Repo", value: marketplace.source.repo })), _jsx(DetailRow, { label: "Last Updated", value: formatDate(marketplace.lastUpdated) }), _jsx(DetailRow, { label: "Auto-update", value: marketplace.autoUpdate ? 'Enabled' : 'Disabled' })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "Install Location:" }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: marketplace.installLocation })] }), showActionMenu ? (_jsx(MarketplaceActionMenu, { marketplace: marketplace, selectedIndex: actionMenuSelectedIndex })) : (_jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: _jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, color: "white", children: "Enter" }), ' ', "actions |", ' ', _jsx(Text, { bold: true, color: "white", children: "a" }), ' ', "add |", ' ', _jsx(Text, { bold: true, color: "white", children: "u" }), ' ', "update"] }) }))] }));
|
|
17
23
|
}
|
|
18
24
|
/**
|
|
19
25
|
* Single detail row with label and value
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PluginDetail component
|
|
3
3
|
* Right panel showing detailed plugin information
|
|
4
|
+
*
|
|
5
|
+
* Uses fixed height to prevent layout jumping when switching between plugins
|
|
6
|
+
* with different amounts of content (e.g., varying component counts).
|
|
4
7
|
*/
|
|
5
8
|
import type { Plugin } from '../types/index.js';
|
|
6
9
|
interface PluginDetailProps {
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* PluginDetail component
|
|
4
4
|
* Right panel showing detailed plugin information
|
|
5
|
+
*
|
|
6
|
+
* Uses fixed height to prevent layout jumping when switching between plugins
|
|
7
|
+
* with different amounts of content (e.g., varying component counts).
|
|
5
8
|
*/
|
|
6
9
|
import { Box, Text } from 'ink';
|
|
7
10
|
import StatusIcon from './StatusIcon.js';
|
|
8
11
|
import ComponentBadges from './ComponentBadges.js';
|
|
12
|
+
import ComponentList, { hasAnyCountComponents, hasAnyDetailedComponents, } from './ComponentList.js';
|
|
13
|
+
/**
|
|
14
|
+
* Fixed height for PluginDetail panel (in terminal lines)
|
|
15
|
+
* Matches PluginList's visibleCount * 2 + 2 = 26 lines
|
|
16
|
+
* This ensures both panels have consistent height regardless of content
|
|
17
|
+
*/
|
|
18
|
+
const DETAIL_PANEL_HEIGHT = 26;
|
|
9
19
|
/**
|
|
10
20
|
* Displays detailed information about a selected plugin
|
|
11
21
|
* @example
|
|
@@ -13,15 +23,29 @@ import ComponentBadges from './ComponentBadges.js';
|
|
|
13
23
|
*/
|
|
14
24
|
export default function PluginDetail({ plugin }) {
|
|
15
25
|
if (!plugin) {
|
|
16
|
-
return (_jsx(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "gray", children: _jsx(Text, { dimColor: true, children: "Select a plugin to view details" }) }));
|
|
26
|
+
return (_jsx(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "gray", height: DETAIL_PANEL_HEIGHT, children: _jsx(Text, { dimColor: true, children: "Select a plugin to view details" }) }));
|
|
17
27
|
}
|
|
18
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan", children: [_jsxs(Box, { marginBottom: 1, gap: 1, children: [_jsx(StatusIcon, { isInstalled: plugin.isInstalled, isEnabled: plugin.isEnabled }), _jsx(Text, { bold: true, color: "cyan", children: plugin.name })] }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { wrap: "wrap", children: plugin.description || 'No description available' }) }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(DetailRow, { label: "Marketplace", value: plugin.marketplace }), _jsx(DetailRow, { label: "Version", value: plugin.version }), _jsx(DetailRow, { label: "Installs", value: formatCount(plugin.installCount) }),
|
|
28
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan", height: DETAIL_PANEL_HEIGHT, overflow: "hidden", children: [_jsxs(Box, { marginBottom: 1, gap: 1, children: [_jsx(StatusIcon, { isInstalled: plugin.isInstalled, isEnabled: plugin.isEnabled }), _jsx(Text, { bold: true, color: "cyan", children: plugin.name })] }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { wrap: "wrap", children: plugin.description || 'No description available' }) }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(DetailRow, { label: "Marketplace", value: plugin.marketplace }), _jsx(DetailRow, { label: "Version", value: plugin.version || '-' }), _jsx(DetailRow, { label: "Installs", value: formatCount(plugin.installCount) }), _jsx(DetailRow, { label: "Category", value: plugin.category || '-' }), _jsx(DetailRow, { label: "Author", value: plugin.author?.name || '-' }), _jsx(DetailRow, { label: "Homepage", value: plugin.homepage || '-' }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "gray", children: 'Components:'.padEnd(12) }), plugin.components ? (_jsx(ComponentBadges, { components: plugin.components })) : (_jsx(Text, { color: "gray", children: "-" }))] })] }), ((plugin.components && hasAnyCountComponents(plugin.components)) ||
|
|
29
|
+
(plugin.componentsDetailed &&
|
|
30
|
+
hasAnyDetailedComponents(plugin.componentsDetailed))) && (_jsxs(_Fragment, { children: [_jsx(Box, { marginY: 1, children: _jsx(Text, { dimColor: true, children: '─'.repeat(36) }) }), _jsx(ComponentList, { components: plugin.components, componentsDetailed: plugin.componentsDetailed, maxItems: 3 })] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Status:" }), plugin.isInstalled ? (plugin.isEnabled ? (_jsx(Text, { color: "green", bold: true, wrap: "truncate", children: "Enabled" })) : (_jsx(Text, { color: "yellow", bold: true, wrap: "truncate", children: "Disabled" }))) : (_jsx(Text, { color: "gray", wrap: "truncate", children: "Not Installed" }))] }), plugin.installedAt && (_jsxs(Text, { dimColor: true, wrap: "truncate", children: ["Installed: ", formatDate(plugin.installedAt)] }))] }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "column", children: plugin.isInstalled ? (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, color: "white", children: "Space" }), ' ', plugin.isEnabled ? 'disable' : 'enable', " |", ' ', _jsx(Text, { bold: true, color: "white", children: "u" }), ' ', "uninstall"] }) })) : (_jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, color: "white", children: "i" }), ' ', "install"] })) })] }));
|
|
19
31
|
}
|
|
20
32
|
/**
|
|
21
33
|
* Single detail row with label and value
|
|
34
|
+
* Pads both label and value to fixed widths to prevent ghost text artifacts
|
|
35
|
+
* when content changes between selections
|
|
36
|
+
* @param label - Label text (e.g., "Marketplace", "Version")
|
|
37
|
+
* @param value - Value to display
|
|
38
|
+
* @returns Detail row with fixed-width padding to overwrite previous content
|
|
22
39
|
*/
|
|
23
40
|
function DetailRow({ label, value }) {
|
|
24
|
-
|
|
41
|
+
// Fixed widths to ensure consistent line length and prevent ghost text
|
|
42
|
+
const LABEL_WIDTH = 12;
|
|
43
|
+
const VALUE_WIDTH = 40;
|
|
44
|
+
const paddedLabel = `${label}:`.padEnd(LABEL_WIDTH);
|
|
45
|
+
// Truncate long values, pad short values with spaces to overwrite old content
|
|
46
|
+
const truncatedValue = value.length > VALUE_WIDTH ? value.slice(0, VALUE_WIDTH - 1) + '…' : value;
|
|
47
|
+
const paddedValue = truncatedValue.padEnd(VALUE_WIDTH);
|
|
48
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: "gray", children: paddedLabel }), _jsx(Text, { children: paddedValue })] }));
|
|
25
49
|
}
|
|
26
50
|
/**
|
|
27
51
|
* Format large numbers with K/M suffix
|