@laststance/claude-plugin-dashboard 0.2.3 → 0.3.2
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 +7 -1
- package/dist/app.d.ts +7 -1
- package/dist/app.js +544 -262
- package/dist/cli.js +60 -67
- package/dist/components/ComponentBadges.d.ts +0 -9
- package/dist/components/ComponentBadges.js +0 -33
- package/dist/components/ComponentDetail.d.ts +32 -0
- package/dist/components/ComponentDetail.js +106 -0
- package/dist/components/ComponentList.d.ts +87 -0
- package/dist/components/ComponentList.js +287 -0
- package/dist/components/HelpOverlay.js +1 -0
- package/dist/components/KeyHints.d.ts +1 -0
- package/dist/components/KeyHints.js +33 -29
- 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 +19 -3
- package/dist/components/PluginDetail.js +56 -6
- package/dist/components/PluginList.js +19 -7
- package/dist/services/componentService.d.ts +10 -31
- package/dist/services/componentService.js +19 -174
- package/dist/services/components/hookService.d.ts +17 -0
- package/dist/services/components/hookService.js +45 -0
- package/dist/services/components/index.d.ts +41 -0
- package/dist/services/components/index.js +126 -0
- package/dist/services/components/markdownService.d.ts +39 -0
- package/dist/services/components/markdownService.js +147 -0
- package/dist/services/components/serverService.d.ts +28 -0
- package/dist/services/components/serverService.js +69 -0
- package/dist/services/components/skillService.d.ts +48 -0
- package/dist/services/components/skillService.js +164 -0
- package/dist/services/components/utils.d.ts +23 -0
- package/dist/services/components/utils.js +42 -0
- package/dist/services/marketplaceActionsService.d.ts +17 -0
- package/dist/services/marketplaceActionsService.js +18 -0
- package/dist/services/pluginActionsService.d.ts +31 -2
- package/dist/services/pluginActionsService.js +65 -6
- package/dist/services/pluginService.js +78 -2
- package/dist/store/index.d.ts +46 -0
- package/dist/store/index.js +47 -0
- package/dist/store/slices/marketplaceSlice.d.ts +344 -0
- package/dist/store/slices/marketplaceSlice.js +152 -0
- package/dist/store/slices/pluginSlice.d.ts +1544 -0
- package/dist/store/slices/pluginSlice.js +191 -0
- package/dist/store/slices/uiSlice.d.ts +147 -0
- package/dist/store/slices/uiSlice.js +126 -0
- package/dist/tabs/DiscoverTab.d.ts +8 -2
- package/dist/tabs/DiscoverTab.js +2 -2
- package/dist/tabs/EnabledTab.d.ts +8 -2
- package/dist/tabs/EnabledTab.js +3 -3
- package/dist/tabs/ErrorsTab.js +1 -1
- package/dist/tabs/InstalledTab.d.ts +8 -2
- package/dist/tabs/InstalledTab.js +3 -3
- package/dist/tabs/MarketplacesTab.d.ts +15 -2
- package/dist/tabs/MarketplacesTab.js +13 -4
- package/dist/types/index.d.ts +157 -5
- package/package.json +10 -3
|
@@ -0,0 +1,287 @@
|
|
|
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
|
+
* Flatten all components from detailed info into a single array for selection
|
|
66
|
+
* @param componentsDetailed - Detailed component info
|
|
67
|
+
* @returns Array of FlatComponentItem for navigation
|
|
68
|
+
* @example
|
|
69
|
+
* flattenComponents({ skills: [{ name: 'xlsx', type: 'skill' }] })
|
|
70
|
+
* // => [{ info: { name: 'xlsx', type: 'skill' }, category: 'Skills', color: 'magenta' }]
|
|
71
|
+
*/
|
|
72
|
+
export function flattenComponents(componentsDetailed) {
|
|
73
|
+
if (!componentsDetailed)
|
|
74
|
+
return [];
|
|
75
|
+
const items = [];
|
|
76
|
+
// Process each category in order
|
|
77
|
+
for (const config of CATEGORY_CONFIGS) {
|
|
78
|
+
const detailedItems = componentsDetailed[config.detailedKey];
|
|
79
|
+
if (!detailedItems?.length)
|
|
80
|
+
continue;
|
|
81
|
+
if (isComponentInfoArray(detailedItems)) {
|
|
82
|
+
for (const info of detailedItems) {
|
|
83
|
+
items.push({ info, category: config.label, color: config.color });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// String array (mcpServers, lspServers, hooks) - convert to ComponentInfo
|
|
88
|
+
for (const name of detailedItems) {
|
|
89
|
+
items.push({
|
|
90
|
+
info: { name, type: config.type },
|
|
91
|
+
category: config.label,
|
|
92
|
+
color: config.color,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return items;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Default visible count for virtual scroll
|
|
101
|
+
*/
|
|
102
|
+
const DEFAULT_VISIBLE_COUNT = 5;
|
|
103
|
+
/**
|
|
104
|
+
* Displays component details in a collapsible list
|
|
105
|
+
* Shows names when available, falls back to counts
|
|
106
|
+
* Supports selection mode when isFocused is true
|
|
107
|
+
* Uses virtual scrolling when focused to prevent layout overflow
|
|
108
|
+
* @param props - ComponentListProps
|
|
109
|
+
* @returns React node or null if no components
|
|
110
|
+
* @example
|
|
111
|
+
* <ComponentList
|
|
112
|
+
* componentsDetailed={{ skills: [{ name: 'xlsx', type: 'skill' }] }}
|
|
113
|
+
* maxItems={3}
|
|
114
|
+
* isFocused={true}
|
|
115
|
+
* selectedIndex={0}
|
|
116
|
+
* visibleCount={5}
|
|
117
|
+
* />
|
|
118
|
+
*/
|
|
119
|
+
export default function ComponentList({ components, componentsDetailed, maxItems = DEFAULT_MAX_ITEMS, isFocused = false, selectedIndex = 0, visibleCount = DEFAULT_VISIBLE_COUNT, }) {
|
|
120
|
+
// No data at all
|
|
121
|
+
if (!components && !componentsDetailed) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
// Check if we have any components to display
|
|
125
|
+
const hasDetailedData = componentsDetailed && hasAnyDetailedComponents(componentsDetailed);
|
|
126
|
+
const hasCountData = components && hasAnyCountComponents(components);
|
|
127
|
+
if (!hasDetailedData && !hasCountData) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
// When focused, use flat virtual scroll approach
|
|
131
|
+
if (isFocused && hasDetailedData) {
|
|
132
|
+
const flatItems = flattenComponents(componentsDetailed);
|
|
133
|
+
return (_jsx(VirtualScrollList, { items: flatItems, selectedIndex: selectedIndex, visibleCount: visibleCount }));
|
|
134
|
+
}
|
|
135
|
+
// Normal mode: show collapsed categories
|
|
136
|
+
// Track current index across all categories
|
|
137
|
+
let currentFlatIndex = 0;
|
|
138
|
+
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) => {
|
|
139
|
+
const detailedItems = componentsDetailed?.[config.detailedKey];
|
|
140
|
+
const count = components?.[config.countKey];
|
|
141
|
+
// Skip if no data for this category
|
|
142
|
+
if (!detailedItems?.length && !count) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
// Prefer detailed data, fall back to count
|
|
146
|
+
if (detailedItems && detailedItems.length > 0) {
|
|
147
|
+
// Type guard: detailedItems could be ComponentInfo[] or string[]
|
|
148
|
+
if (isComponentInfoArray(detailedItems)) {
|
|
149
|
+
const startIndex = currentFlatIndex;
|
|
150
|
+
currentFlatIndex += detailedItems.length;
|
|
151
|
+
return (_jsx(CategorySection, { label: config.label, color: config.color, items: detailedItems, maxItems: maxItems, isFocused: false, selectedIndex: selectedIndex, startIndex: startIndex }, config.detailedKey));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// String array (mcpServers, lspServers, hooks)
|
|
155
|
+
const startIndex = currentFlatIndex;
|
|
156
|
+
currentFlatIndex += detailedItems.length;
|
|
157
|
+
return (_jsx(CategorySectionSimple, { label: config.label, color: config.color, items: detailedItems, maxItems: maxItems, isFocused: false, selectedIndex: selectedIndex, startIndex: startIndex }, config.detailedKey));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Count-only fallback (number) or boolean hooks fallback
|
|
161
|
+
if (typeof count === 'number' && count > 0) {
|
|
162
|
+
return (_jsx(CountOnlySection, { label: config.label, color: config.color, count: count }, config.countKey));
|
|
163
|
+
}
|
|
164
|
+
// Boolean hooks fallback (hooks: true without detailed info)
|
|
165
|
+
if (config.countKey === 'hooks' && count === true) {
|
|
166
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: config.color, bold: true, children: config.label }), _jsx(Text, { dimColor: true, children: " (configured)" })] }, config.countKey));
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
})] }));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Virtual scroll list for focused mode
|
|
173
|
+
* Shows a fixed viewport with scroll indicators
|
|
174
|
+
* @param props - VirtualScrollListProps
|
|
175
|
+
* @returns React node
|
|
176
|
+
*/
|
|
177
|
+
function VirtualScrollList({ items, selectedIndex, visibleCount, }) {
|
|
178
|
+
const totalItems = items.length;
|
|
179
|
+
// Calculate scroll window centered on selection
|
|
180
|
+
const halfVisible = Math.floor(visibleCount / 2);
|
|
181
|
+
let startIndex = Math.max(0, selectedIndex - halfVisible);
|
|
182
|
+
const endIndex = Math.min(totalItems, startIndex + visibleCount);
|
|
183
|
+
// Adjust start if we hit the end
|
|
184
|
+
startIndex = Math.max(0, endIndex - visibleCount);
|
|
185
|
+
const visibleItems = items.slice(startIndex, endIndex);
|
|
186
|
+
const itemsAbove = startIndex;
|
|
187
|
+
const itemsBelow = totalItems - endIndex;
|
|
188
|
+
// Height: 1 header + visibleCount items + 1 indicator (always show space for indicator)
|
|
189
|
+
const listHeight = 1 + visibleCount + 1;
|
|
190
|
+
return (_jsxs(Box, { flexDirection: "column", height: listHeight, children: [_jsxs(Box, { height: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "\u2500\u2500 Components \u2500\u2500" }), _jsx(Text, { dimColor: true, children: " (\u2191\u2193 select, \u2190 back)" })] }), visibleItems.map((item, idx) => {
|
|
191
|
+
const actualIndex = startIndex + idx;
|
|
192
|
+
const isSelected = actualIndex === selectedIndex;
|
|
193
|
+
return (_jsx(Box, { height: 1, children: _jsxs(Text, { inverse: isSelected, children: [isSelected ? ' ▶ ' : ' ', _jsxs(Text, { color: item.color, children: ["[", item.category.charAt(0), "]"] }), ' ', item.info.name] }) }, `${item.category}-${item.info.name}`));
|
|
194
|
+
}), _jsx(Box, { height: 1, children: itemsAbove > 0 && itemsBelow > 0 ? (_jsxs(Text, { dimColor: true, children: [' ', "\u2191", itemsAbove, " more \u00B7 \u2193", itemsBelow, " more"] })) : itemsAbove > 0 ? (_jsxs(Text, { dimColor: true, children: [' ', "\u2191", itemsAbove, " more"] })) : itemsBelow > 0 ? (_jsxs(Text, { dimColor: true, children: [' ', "\u2193", itemsBelow, " more"] })) : (_jsx(Text, { children: " " })) })] }));
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Single category section with collapsible ComponentInfo items
|
|
198
|
+
* Shows first N items, then "+M more..." for overflow
|
|
199
|
+
* Supports selection highlighting when focused
|
|
200
|
+
* @returns React node for the category section
|
|
201
|
+
*/
|
|
202
|
+
function CategorySection({ label, color, items, maxItems, isFocused = false, selectedIndex = 0, startIndex = 0, }) {
|
|
203
|
+
// When focused, show all items to allow selection
|
|
204
|
+
// When not focused, limit to maxItems
|
|
205
|
+
const visibleItems = isFocused ? items : items.slice(0, maxItems);
|
|
206
|
+
const remainingCount = isFocused ? 0 : items.length - maxItems;
|
|
207
|
+
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, index) => {
|
|
208
|
+
const flatIndex = startIndex + index;
|
|
209
|
+
const isSelected = isFocused && flatIndex === selectedIndex;
|
|
210
|
+
return (_jsx(Box, { height: 1, children: _jsxs(Text, { inverse: isSelected, children: [isSelected ? ' ▶ ' : ' ', item.name, item.description && !isSelected && (_jsxs(Text, { dimColor: true, children: [" - ", truncate(item.description, 25)] }))] }) }, item.name));
|
|
211
|
+
}), remainingCount > 0 && (_jsxs(Text, { dimColor: true, children: [' ', "\u2514\u2500 +", remainingCount, " more..."] }))] }));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Category section for simple string arrays (mcpServers, lspServers, hooks)
|
|
215
|
+
* Supports selection highlighting when focused
|
|
216
|
+
* @returns React node for the category section
|
|
217
|
+
*/
|
|
218
|
+
function CategorySectionSimple({ label, color, items, maxItems, isFocused = false, selectedIndex = 0, startIndex = 0, }) {
|
|
219
|
+
// When focused, show all items to allow selection
|
|
220
|
+
const visibleItems = isFocused ? items : items.slice(0, maxItems);
|
|
221
|
+
const remainingCount = isFocused ? 0 : items.length - maxItems;
|
|
222
|
+
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, index) => {
|
|
223
|
+
const flatIndex = startIndex + index;
|
|
224
|
+
const isSelected = isFocused && flatIndex === selectedIndex;
|
|
225
|
+
return (_jsx(Box, { height: 1, children: _jsxs(Text, { inverse: isSelected, children: [isSelected ? ' ▶ ' : ' ', item] }) }, item));
|
|
226
|
+
}), remainingCount > 0 && (_jsxs(Text, { dimColor: true, children: [' ', "\u2514\u2500 +", remainingCount, " more..."] }))] }));
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Category section showing only count (fallback when no detailed info)
|
|
230
|
+
* @returns React node for the count-only section
|
|
231
|
+
*/
|
|
232
|
+
function CountOnlySection({ label, color, count, }) {
|
|
233
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: color, bold: true, children: label }), _jsxs(Text, { dimColor: true, children: [": ", count] })] }));
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Type guard to check if array is ComponentInfo[]
|
|
237
|
+
* @param arr - Array to check
|
|
238
|
+
* @returns true if array contains ComponentInfo objects
|
|
239
|
+
*/
|
|
240
|
+
function isComponentInfoArray(arr) {
|
|
241
|
+
return arr.length > 0 && typeof arr[0] === 'object' && 'name' in arr[0];
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Check if PluginComponentsDetailed has any data
|
|
245
|
+
* @param detailed - Detailed components object
|
|
246
|
+
* @returns true if any category has items
|
|
247
|
+
* @example
|
|
248
|
+
* hasAnyDetailedComponents({ skills: [{ name: 'xlsx', type: 'skill' }] }) // => true
|
|
249
|
+
* hasAnyDetailedComponents({}) // => false
|
|
250
|
+
*/
|
|
251
|
+
export function hasAnyDetailedComponents(detailed) {
|
|
252
|
+
return ((detailed.skills?.length ?? 0) > 0 ||
|
|
253
|
+
(detailed.commands?.length ?? 0) > 0 ||
|
|
254
|
+
(detailed.agents?.length ?? 0) > 0 ||
|
|
255
|
+
(detailed.hooks?.length ?? 0) > 0 ||
|
|
256
|
+
(detailed.mcpServers?.length ?? 0) > 0 ||
|
|
257
|
+
(detailed.lspServers?.length ?? 0) > 0);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Check if PluginComponents has any count data
|
|
261
|
+
* @param components - Components counts object
|
|
262
|
+
* @returns true if any category has count > 0
|
|
263
|
+
* @example
|
|
264
|
+
* hasAnyCountComponents({ skills: 5 }) // => true
|
|
265
|
+
* hasAnyCountComponents({ hooks: true }) // => true
|
|
266
|
+
* hasAnyCountComponents({}) // => false
|
|
267
|
+
*/
|
|
268
|
+
export function hasAnyCountComponents(components) {
|
|
269
|
+
return ((components.skills ?? 0) > 0 ||
|
|
270
|
+
(components.commands ?? 0) > 0 ||
|
|
271
|
+
(components.agents ?? 0) > 0 ||
|
|
272
|
+
components.hooks === true ||
|
|
273
|
+
(components.mcpServers ?? 0) > 0 ||
|
|
274
|
+
(components.lspServers ?? 0) > 0);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Truncate string to max length with ellipsis
|
|
278
|
+
* @param str - String to truncate
|
|
279
|
+
* @param maxLength - Maximum length
|
|
280
|
+
* @returns Truncated string
|
|
281
|
+
*/
|
|
282
|
+
function truncate(str, maxLength) {
|
|
283
|
+
if (str.length <= maxLength) {
|
|
284
|
+
return str;
|
|
285
|
+
}
|
|
286
|
+
return str.slice(0, maxLength - 3) + '...';
|
|
287
|
+
}
|
|
@@ -18,6 +18,7 @@ const helpSections = [
|
|
|
18
18
|
items: [
|
|
19
19
|
{ key: 'i, Enter', description: 'Install / Toggle plugin' },
|
|
20
20
|
{ key: 'u', description: 'Uninstall plugin' },
|
|
21
|
+
{ key: 'U', description: 'Update all installed plugins' },
|
|
21
22
|
{ key: 'Space', description: 'Toggle enable/disable' },
|
|
22
23
|
{ key: 's/S', description: 'Sort options / order' },
|
|
23
24
|
],
|
|
@@ -17,6 +17,7 @@ interface KeyHintsProps {
|
|
|
17
17
|
* @example
|
|
18
18
|
* <KeyHints focusZone="list" />
|
|
19
19
|
* <KeyHints focusZone="search" extraHints={[{ key: 'i', action: 'install' }]} />
|
|
20
|
+
* <KeyHints focusZone="components" />
|
|
20
21
|
*/
|
|
21
22
|
export default function KeyHints({ extraHints, focusZone, }: KeyHintsProps): import("react/jsx-runtime").JSX.Element;
|
|
22
23
|
export {};
|
|
@@ -4,49 +4,53 @@ 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
|
-
|
|
38
|
-
|
|
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
|
+
.with('components', () => [
|
|
38
|
+
{ key: '↑/↓', action: 'select component' },
|
|
39
|
+
{ key: '←', action: 'back to plugin' },
|
|
40
|
+
{ key: 'h', action: 'help' },
|
|
41
|
+
{ key: 'q or ^C', action: 'quit' },
|
|
42
|
+
])
|
|
43
|
+
.exhaustive();
|
|
41
44
|
}
|
|
42
45
|
/**
|
|
43
46
|
* Displays keyboard shortcut hints in the footer
|
|
44
47
|
* @example
|
|
45
48
|
* <KeyHints focusZone="list" />
|
|
46
49
|
* <KeyHints focusZone="search" extraHints={[{ key: 'i', action: 'install' }]} />
|
|
50
|
+
* <KeyHints focusZone="components" />
|
|
47
51
|
*/
|
|
48
52
|
export default function KeyHints({ extraHints, focusZone = 'list', }) {
|
|
49
53
|
const baseHints = getBaseHints(focusZone);
|
|
50
54
|
const allHints = extraHints ? [...baseHints, ...extraHints] : baseHints;
|
|
51
|
-
return (_jsx(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: _jsx(Box, { gap: 2, flexWrap: "wrap", children: allHints.map((hint
|
|
55
|
+
return (_jsx(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: _jsx(Box, { gap: 2, flexWrap: "wrap", children: allHints.map((hint) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: "white", children: hint.key }), _jsx(Text, { dimColor: true, children: hint.action })] }, `${hint.key}-${hint.action}`))) }) }));
|
|
52
56
|
}
|
|
@@ -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 })] }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: _jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, color: "white", children: "
|
|
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,15 +1,31 @@
|
|
|
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).
|
|
7
|
+
*
|
|
8
|
+
* Supports component focus mode for navigating and viewing component details
|
|
4
9
|
*/
|
|
5
|
-
import type { Plugin } from '../types/index.js';
|
|
10
|
+
import type { ComponentDetailedInfo, Plugin } from '../types/index.js';
|
|
6
11
|
interface PluginDetailProps {
|
|
7
12
|
plugin: Plugin | null;
|
|
13
|
+
/** Whether component focus mode is active */
|
|
14
|
+
componentFocusMode?: boolean;
|
|
15
|
+
/** Currently selected component index */
|
|
16
|
+
selectedComponentIndex?: number;
|
|
17
|
+
/** Selected component's detailed info (fetched from service) */
|
|
18
|
+
selectedComponentDetail?: ComponentDetailedInfo | null;
|
|
8
19
|
}
|
|
9
20
|
/**
|
|
10
21
|
* Displays detailed information about a selected plugin
|
|
22
|
+
* Supports component focus mode for drilling into component details
|
|
11
23
|
* @example
|
|
12
|
-
* <PluginDetail
|
|
24
|
+
* <PluginDetail
|
|
25
|
+
* plugin={selectedPlugin}
|
|
26
|
+
* componentFocusMode={true}
|
|
27
|
+
* selectedComponentIndex={0}
|
|
28
|
+
* />
|
|
13
29
|
*/
|
|
14
|
-
export default function PluginDetail({ plugin }: PluginDetailProps): import("react/jsx-runtime").JSX.Element;
|
|
30
|
+
export default function PluginDetail({ plugin, componentFocusMode, selectedComponentIndex, selectedComponentDetail, }: PluginDetailProps): import("react/jsx-runtime").JSX.Element;
|
|
15
31
|
export {};
|