@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.
Files changed (58) hide show
  1. package/README.md +7 -1
  2. package/dist/app.d.ts +7 -1
  3. package/dist/app.js +544 -262
  4. package/dist/cli.js +60 -67
  5. package/dist/components/ComponentBadges.d.ts +0 -9
  6. package/dist/components/ComponentBadges.js +0 -33
  7. package/dist/components/ComponentDetail.d.ts +32 -0
  8. package/dist/components/ComponentDetail.js +106 -0
  9. package/dist/components/ComponentList.d.ts +87 -0
  10. package/dist/components/ComponentList.js +287 -0
  11. package/dist/components/HelpOverlay.js +1 -0
  12. package/dist/components/KeyHints.d.ts +1 -0
  13. package/dist/components/KeyHints.js +33 -29
  14. package/dist/components/MarketplaceActionMenu.d.ts +41 -0
  15. package/dist/components/MarketplaceActionMenu.js +68 -0
  16. package/dist/components/MarketplaceDetail.d.ts +10 -3
  17. package/dist/components/MarketplaceDetail.js +10 -4
  18. package/dist/components/PluginDetail.d.ts +19 -3
  19. package/dist/components/PluginDetail.js +56 -6
  20. package/dist/components/PluginList.js +19 -7
  21. package/dist/services/componentService.d.ts +10 -31
  22. package/dist/services/componentService.js +19 -174
  23. package/dist/services/components/hookService.d.ts +17 -0
  24. package/dist/services/components/hookService.js +45 -0
  25. package/dist/services/components/index.d.ts +41 -0
  26. package/dist/services/components/index.js +126 -0
  27. package/dist/services/components/markdownService.d.ts +39 -0
  28. package/dist/services/components/markdownService.js +147 -0
  29. package/dist/services/components/serverService.d.ts +28 -0
  30. package/dist/services/components/serverService.js +69 -0
  31. package/dist/services/components/skillService.d.ts +48 -0
  32. package/dist/services/components/skillService.js +164 -0
  33. package/dist/services/components/utils.d.ts +23 -0
  34. package/dist/services/components/utils.js +42 -0
  35. package/dist/services/marketplaceActionsService.d.ts +17 -0
  36. package/dist/services/marketplaceActionsService.js +18 -0
  37. package/dist/services/pluginActionsService.d.ts +31 -2
  38. package/dist/services/pluginActionsService.js +65 -6
  39. package/dist/services/pluginService.js +78 -2
  40. package/dist/store/index.d.ts +46 -0
  41. package/dist/store/index.js +47 -0
  42. package/dist/store/slices/marketplaceSlice.d.ts +344 -0
  43. package/dist/store/slices/marketplaceSlice.js +152 -0
  44. package/dist/store/slices/pluginSlice.d.ts +1544 -0
  45. package/dist/store/slices/pluginSlice.js +191 -0
  46. package/dist/store/slices/uiSlice.d.ts +147 -0
  47. package/dist/store/slices/uiSlice.js +126 -0
  48. package/dist/tabs/DiscoverTab.d.ts +8 -2
  49. package/dist/tabs/DiscoverTab.js +2 -2
  50. package/dist/tabs/EnabledTab.d.ts +8 -2
  51. package/dist/tabs/EnabledTab.js +3 -3
  52. package/dist/tabs/ErrorsTab.js +1 -1
  53. package/dist/tabs/InstalledTab.d.ts +8 -2
  54. package/dist/tabs/InstalledTab.js +3 -3
  55. package/dist/tabs/MarketplacesTab.d.ts +15 -2
  56. package/dist/tabs/MarketplacesTab.js +13 -4
  57. package/dist/types/index.d.ts +157 -5
  58. 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
- switch (focusZone) {
14
- case 'tabbar':
15
- return [
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
- case 'search':
23
- return [
24
- { key: '', action: 'tabs' },
25
- { key: '↓/Enter', action: 'list' },
26
- { key: 'ESC', action: 'clear/exit' },
27
- { key: 'h', action: 'help' },
28
- { key: 'q or ^C', action: 'quit' },
29
- ];
30
- case 'list':
31
- default:
32
- return [
33
- { key: '↑/↓', action: 'navigate' },
34
- { key: '↑(top)', action: 'search' },
35
- { key: 'Space', action: 'toggle' },
36
- { key: 'Tab', action: 'next tab' },
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
+ .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, index) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: "white", children: hint.key }), _jsx(Text, { dimColor: true, children: hint.action })] }, index))) }) }));
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 marketplace={selectedMarketplace} />
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 marketplace={selectedMarketplace} />
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: "a" }), ' ', "add |", ' ', _jsx(Text, { bold: true, color: "white", children: "d" }), ' ', "remove |", ' ', _jsx(Text, { bold: true, color: "white", children: "u" }), ' ', "update"] }) })] }));
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 plugin={selectedPlugin} />
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 {};