@laststance/claude-plugin-dashboard 0.1.1 → 0.2.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/dist/app.d.ts +24 -0
- package/dist/app.js +254 -67
- package/dist/components/HelpOverlay.d.ts +15 -0
- package/dist/components/HelpOverlay.js +51 -0
- package/dist/components/KeyHints.d.ts +6 -3
- package/dist/components/KeyHints.js +39 -10
- package/dist/components/MarketplaceList.d.ts +4 -2
- package/dist/components/MarketplaceList.js +7 -3
- package/dist/components/PluginList.d.ts +29 -2
- package/dist/components/PluginList.js +26 -5
- package/dist/components/SearchInput.js +1 -1
- package/dist/components/TabBar.d.ts +5 -3
- package/dist/components/TabBar.js +20 -8
- package/dist/services/pluginService.d.ts +10 -0
- package/dist/services/pluginService.js +16 -0
- package/dist/tabs/DiscoverTab.d.ts +5 -3
- package/dist/tabs/DiscoverTab.js +3 -2
- package/dist/tabs/EnabledTab.d.ts +24 -0
- package/dist/tabs/EnabledTab.js +26 -0
- package/dist/tabs/InstalledTab.d.ts +10 -3
- package/dist/tabs/InstalledTab.js +14 -10
- package/dist/tabs/MarketplacesTab.d.ts +10 -3
- package/dist/tabs/MarketplacesTab.js +12 -3
- package/dist/types/index.d.ts +15 -1
- package/package.json +12 -4
package/dist/app.d.ts
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
* Main App component for Claude Code Plugin Dashboard
|
|
3
3
|
* Interactive TUI to browse and manage Claude Code plugins
|
|
4
4
|
*/
|
|
5
|
+
import type { AppState, Action, Plugin, FocusZone } from './types/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Initial application state
|
|
8
|
+
*/
|
|
9
|
+
export declare const initialState: AppState;
|
|
10
|
+
/**
|
|
11
|
+
* Get available focus zones for the current tab
|
|
12
|
+
* Errors tab has no search zone since it doesn't support filtering
|
|
13
|
+
* @param activeTab - The currently active tab
|
|
14
|
+
* @returns Array of available focus zones in navigation order
|
|
15
|
+
*/
|
|
16
|
+
export declare function getAvailableZones(activeTab: AppState['activeTab']): FocusZone[];
|
|
17
|
+
/**
|
|
18
|
+
* State reducer for application state management
|
|
19
|
+
*/
|
|
20
|
+
export declare function appReducer(state: AppState, action: Action): AppState;
|
|
21
|
+
/**
|
|
22
|
+
* Get items array for current tab
|
|
23
|
+
*/
|
|
24
|
+
export declare function getItemsForTab(state: AppState): unknown[];
|
|
25
|
+
/**
|
|
26
|
+
* Get filtered and sorted plugins for discover tab
|
|
27
|
+
*/
|
|
28
|
+
export declare function getFilteredPlugins(state: AppState): Plugin[];
|
|
5
29
|
/**
|
|
6
30
|
* Main App component
|
|
7
31
|
*/
|
package/dist/app.js
CHANGED
|
@@ -3,23 +3,27 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
* Main App component for Claude Code Plugin Dashboard
|
|
4
4
|
* Interactive TUI to browse and manage Claude Code plugins
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { useEffect, useReducer } from 'react';
|
|
7
7
|
import { Box, Text, useInput, useApp } from 'ink';
|
|
8
8
|
import TabBar, { getNextTab } from './components/TabBar.js';
|
|
9
9
|
import KeyHints from './components/KeyHints.js';
|
|
10
10
|
import DiscoverTab from './tabs/DiscoverTab.js';
|
|
11
|
+
import EnabledTab from './tabs/EnabledTab.js';
|
|
11
12
|
import InstalledTab from './tabs/InstalledTab.js';
|
|
12
13
|
import MarketplacesTab from './tabs/MarketplacesTab.js';
|
|
13
14
|
import ErrorsTab from './tabs/ErrorsTab.js';
|
|
14
|
-
import { loadAllPlugins, loadMarketplaces, searchPlugins, sortPlugins, } from './services/pluginService.js';
|
|
15
|
+
import { loadAllPlugins, loadMarketplaces, searchPlugins, searchMarketplaces, sortPlugins, } from './services/pluginService.js';
|
|
15
16
|
import { togglePlugin } from './services/settingsService.js';
|
|
16
17
|
import { installPlugin, uninstallPlugin, } from './services/pluginActionsService.js';
|
|
17
18
|
import ConfirmDialog from './components/ConfirmDialog.js';
|
|
19
|
+
import HelpOverlay from './components/HelpOverlay.js';
|
|
20
|
+
import packageJson from '../package.json' with { type: 'json' };
|
|
18
21
|
/**
|
|
19
22
|
* Initial application state
|
|
20
23
|
*/
|
|
21
|
-
const initialState = {
|
|
22
|
-
activeTab: '
|
|
24
|
+
export const initialState = {
|
|
25
|
+
activeTab: 'enabled',
|
|
26
|
+
focusZone: 'list',
|
|
23
27
|
plugins: [],
|
|
24
28
|
marketplaces: [],
|
|
25
29
|
errors: [],
|
|
@@ -33,16 +37,30 @@ const initialState = {
|
|
|
33
37
|
operation: 'idle',
|
|
34
38
|
operationPluginId: null,
|
|
35
39
|
confirmUninstall: false,
|
|
40
|
+
showHelp: false,
|
|
36
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* Get available focus zones for the current tab
|
|
44
|
+
* Errors tab has no search zone since it doesn't support filtering
|
|
45
|
+
* @param activeTab - The currently active tab
|
|
46
|
+
* @returns Array of available focus zones in navigation order
|
|
47
|
+
*/
|
|
48
|
+
export function getAvailableZones(activeTab) {
|
|
49
|
+
if (activeTab === 'errors') {
|
|
50
|
+
return ['tabbar', 'list'];
|
|
51
|
+
}
|
|
52
|
+
return ['tabbar', 'search', 'list'];
|
|
53
|
+
}
|
|
37
54
|
/**
|
|
38
55
|
* State reducer for application state management
|
|
39
56
|
*/
|
|
40
|
-
function appReducer(state, action) {
|
|
57
|
+
export function appReducer(state, action) {
|
|
41
58
|
switch (action.type) {
|
|
42
59
|
case 'SET_TAB':
|
|
43
60
|
return {
|
|
44
61
|
...state,
|
|
45
62
|
activeTab: action.payload,
|
|
63
|
+
focusZone: 'list',
|
|
46
64
|
selectedIndex: 0,
|
|
47
65
|
searchQuery: '',
|
|
48
66
|
message: null,
|
|
@@ -51,6 +69,7 @@ function appReducer(state, action) {
|
|
|
51
69
|
return {
|
|
52
70
|
...state,
|
|
53
71
|
activeTab: getNextTab(state.activeTab, 'next'),
|
|
72
|
+
focusZone: 'list',
|
|
54
73
|
selectedIndex: 0,
|
|
55
74
|
searchQuery: '',
|
|
56
75
|
message: null,
|
|
@@ -59,10 +78,16 @@ function appReducer(state, action) {
|
|
|
59
78
|
return {
|
|
60
79
|
...state,
|
|
61
80
|
activeTab: getNextTab(state.activeTab, 'prev'),
|
|
81
|
+
focusZone: 'list',
|
|
62
82
|
selectedIndex: 0,
|
|
63
83
|
searchQuery: '',
|
|
64
84
|
message: null,
|
|
65
85
|
};
|
|
86
|
+
case 'SET_FOCUS_ZONE':
|
|
87
|
+
return {
|
|
88
|
+
...state,
|
|
89
|
+
focusZone: action.payload,
|
|
90
|
+
};
|
|
66
91
|
case 'SET_PLUGINS':
|
|
67
92
|
return {
|
|
68
93
|
...state,
|
|
@@ -180,6 +205,11 @@ function appReducer(state, action) {
|
|
|
180
205
|
confirmUninstall: false,
|
|
181
206
|
operationPluginId: null,
|
|
182
207
|
};
|
|
208
|
+
case 'TOGGLE_HELP':
|
|
209
|
+
return {
|
|
210
|
+
...state,
|
|
211
|
+
showHelp: !state.showHelp,
|
|
212
|
+
};
|
|
183
213
|
default:
|
|
184
214
|
return state;
|
|
185
215
|
}
|
|
@@ -187,12 +217,14 @@ function appReducer(state, action) {
|
|
|
187
217
|
/**
|
|
188
218
|
* Get items array for current tab
|
|
189
219
|
*/
|
|
190
|
-
function getItemsForTab(state) {
|
|
220
|
+
export function getItemsForTab(state) {
|
|
191
221
|
switch (state.activeTab) {
|
|
192
|
-
case '
|
|
193
|
-
return
|
|
222
|
+
case 'enabled':
|
|
223
|
+
return state.plugins.filter((p) => p.isInstalled && p.isEnabled);
|
|
194
224
|
case 'installed':
|
|
195
225
|
return state.plugins.filter((p) => p.isInstalled);
|
|
226
|
+
case 'discover':
|
|
227
|
+
return getFilteredPlugins(state);
|
|
196
228
|
case 'marketplaces':
|
|
197
229
|
return state.marketplaces;
|
|
198
230
|
case 'errors':
|
|
@@ -204,7 +236,7 @@ function getItemsForTab(state) {
|
|
|
204
236
|
/**
|
|
205
237
|
* Get filtered and sorted plugins for discover tab
|
|
206
238
|
*/
|
|
207
|
-
function getFilteredPlugins(state) {
|
|
239
|
+
export function getFilteredPlugins(state) {
|
|
208
240
|
let plugins = state.plugins;
|
|
209
241
|
// Apply search filter
|
|
210
242
|
if (state.searchQuery) {
|
|
@@ -220,7 +252,6 @@ function getFilteredPlugins(state) {
|
|
|
220
252
|
export default function App() {
|
|
221
253
|
const { exit } = useApp();
|
|
222
254
|
const [state, dispatch] = useReducer(appReducer, initialState);
|
|
223
|
-
const [isSearchMode, setIsSearchMode] = useState(false);
|
|
224
255
|
// Load data on mount
|
|
225
256
|
useEffect(() => {
|
|
226
257
|
try {
|
|
@@ -288,6 +319,23 @@ export default function App() {
|
|
|
288
319
|
if (state.operation !== 'idle') {
|
|
289
320
|
return;
|
|
290
321
|
}
|
|
322
|
+
// Handle help overlay
|
|
323
|
+
if (state.showHelp) {
|
|
324
|
+
if (input === 'h' || key.escape) {
|
|
325
|
+
dispatch({ type: 'TOGGLE_HELP' });
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
// Exit (q or Ctrl+C) - Global handler, works in all focus zones
|
|
330
|
+
if (input === 'q' || (key.ctrl && input === 'c')) {
|
|
331
|
+
exit();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// Toggle help (h key)
|
|
335
|
+
if (input === 'h') {
|
|
336
|
+
dispatch({ type: 'TOGGLE_HELP' });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
291
339
|
// Handle confirmation dialog
|
|
292
340
|
if (state.confirmUninstall && state.operationPluginId) {
|
|
293
341
|
if (input === 'y' || input === 'Y') {
|
|
@@ -302,10 +350,19 @@ export default function App() {
|
|
|
302
350
|
}
|
|
303
351
|
return;
|
|
304
352
|
}
|
|
305
|
-
// Search mode input
|
|
306
|
-
if (
|
|
307
|
-
|
|
308
|
-
|
|
353
|
+
// Search mode input (when focusZone is 'search')
|
|
354
|
+
if (state.focusZone === 'search') {
|
|
355
|
+
// Up arrow: move focus to tabbar
|
|
356
|
+
if (key.upArrow || (key.ctrl && input === 'p')) {
|
|
357
|
+
dispatch({ type: 'SET_FOCUS_ZONE', payload: 'tabbar' });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// Down arrow, Enter, or Escape: move focus to list
|
|
361
|
+
if (key.escape ||
|
|
362
|
+
key.return ||
|
|
363
|
+
key.downArrow ||
|
|
364
|
+
(key.ctrl && input === 'n')) {
|
|
365
|
+
dispatch({ type: 'SET_FOCUS_ZONE', payload: 'list' });
|
|
309
366
|
return;
|
|
310
367
|
}
|
|
311
368
|
if (key.backspace || key.delete) {
|
|
@@ -324,48 +381,124 @@ export default function App() {
|
|
|
324
381
|
}
|
|
325
382
|
return;
|
|
326
383
|
}
|
|
327
|
-
//
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
384
|
+
// TabBar focus zone navigation
|
|
385
|
+
if (state.focusZone === 'tabbar') {
|
|
386
|
+
// Down arrow: move to search (or list if no search)
|
|
387
|
+
if (key.downArrow || (key.ctrl && input === 'n')) {
|
|
388
|
+
const zones = getAvailableZones(state.activeTab);
|
|
389
|
+
dispatch({
|
|
390
|
+
type: 'SET_FOCUS_ZONE',
|
|
391
|
+
payload: zones.includes('search') ? 'search' : 'list',
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
// Left/Right arrows and Ctrl+B/F: tab switching (only in tabbar)
|
|
396
|
+
// Keep focus on tabbar after navigation
|
|
397
|
+
if (key.leftArrow || (key.ctrl && input === 'b')) {
|
|
398
|
+
dispatch({ type: 'PREV_TAB' });
|
|
399
|
+
dispatch({ type: 'SET_FOCUS_ZONE', payload: 'tabbar' });
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (key.rightArrow || (key.ctrl && input === 'f')) {
|
|
403
|
+
dispatch({ type: 'NEXT_TAB' });
|
|
404
|
+
dispatch({ type: 'SET_FOCUS_ZONE', payload: 'tabbar' });
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
// Tab key: next tab (resets focus to list)
|
|
408
|
+
if (key.tab) {
|
|
409
|
+
dispatch({ type: 'NEXT_TAB' });
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
334
412
|
return;
|
|
335
413
|
}
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
414
|
+
// List focus zone navigation (default zone)
|
|
415
|
+
// Up arrow: move up in list or focus search/tabbar at top
|
|
416
|
+
if (key.upArrow || (key.ctrl && input === 'p')) {
|
|
417
|
+
if (state.selectedIndex === 0) {
|
|
418
|
+
// At top of list: move focus to search (or tabbar if no search)
|
|
419
|
+
const zones = getAvailableZones(state.activeTab);
|
|
420
|
+
dispatch({
|
|
421
|
+
type: 'SET_FOCUS_ZONE',
|
|
422
|
+
payload: zones.includes('search') ? 'search' : 'tabbar',
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
dispatch({ type: 'MOVE_SELECTION', payload: 'up' });
|
|
427
|
+
}
|
|
339
428
|
return;
|
|
340
429
|
}
|
|
341
|
-
|
|
342
|
-
|
|
430
|
+
// Down arrow: move down in list
|
|
431
|
+
if (key.downArrow || (key.ctrl && input === 'n')) {
|
|
432
|
+
dispatch({ type: 'MOVE_SELECTION', payload: 'down' });
|
|
343
433
|
return;
|
|
344
434
|
}
|
|
435
|
+
// Tab key: next tab (from list zone)
|
|
345
436
|
if (key.tab) {
|
|
346
437
|
dispatch({ type: 'NEXT_TAB' });
|
|
347
438
|
return;
|
|
348
439
|
}
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
440
|
+
// Enter search mode (/ key on supported tabs)
|
|
441
|
+
const searchEnabledTabs = [
|
|
442
|
+
'enabled',
|
|
443
|
+
'installed',
|
|
444
|
+
'discover',
|
|
445
|
+
'marketplaces',
|
|
446
|
+
];
|
|
447
|
+
if (input === '/' && searchEnabledTabs.includes(state.activeTab)) {
|
|
448
|
+
dispatch({ type: 'SET_FOCUS_ZONE', payload: 'search' });
|
|
352
449
|
return;
|
|
353
450
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
451
|
+
// Enter key: Install (non-installed) or Toggle (installed)
|
|
452
|
+
if (key.return &&
|
|
453
|
+
(state.activeTab === 'enabled' ||
|
|
454
|
+
state.activeTab === 'installed' ||
|
|
455
|
+
state.activeTab === 'discover')) {
|
|
456
|
+
const items = state.activeTab === 'enabled'
|
|
457
|
+
? state.plugins.filter((p) => p.isInstalled && p.isEnabled)
|
|
458
|
+
: state.activeTab === 'installed'
|
|
459
|
+
? state.plugins.filter((p) => p.isInstalled)
|
|
460
|
+
: getFilteredPlugins(state);
|
|
461
|
+
const selectedPlugin = items[state.selectedIndex];
|
|
462
|
+
if (selectedPlugin) {
|
|
463
|
+
if (!selectedPlugin.isInstalled) {
|
|
464
|
+
// Install non-installed plugin
|
|
465
|
+
handleInstall(selectedPlugin.id);
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
// Toggle installed plugin
|
|
469
|
+
try {
|
|
470
|
+
const newState = togglePlugin(selectedPlugin.id);
|
|
471
|
+
dispatch({
|
|
472
|
+
type: 'TOGGLE_PLUGIN_ENABLED',
|
|
473
|
+
payload: selectedPlugin.id,
|
|
474
|
+
});
|
|
475
|
+
dispatch({
|
|
476
|
+
type: 'SET_MESSAGE',
|
|
477
|
+
payload: newState
|
|
478
|
+
? `✅ ${selectedPlugin.name} enabled`
|
|
479
|
+
: `❌ ${selectedPlugin.name} disabled`,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
dispatch({
|
|
484
|
+
type: 'SET_MESSAGE',
|
|
485
|
+
payload: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
361
490
|
return;
|
|
362
491
|
}
|
|
363
|
-
// Toggle plugin (Space
|
|
364
|
-
if (
|
|
365
|
-
(state.activeTab === '
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
492
|
+
// Toggle plugin (Space only)
|
|
493
|
+
if (input === ' ' &&
|
|
494
|
+
(state.activeTab === 'enabled' ||
|
|
495
|
+
state.activeTab === 'installed' ||
|
|
496
|
+
state.activeTab === 'discover')) {
|
|
497
|
+
const items = state.activeTab === 'enabled'
|
|
498
|
+
? state.plugins.filter((p) => p.isInstalled && p.isEnabled)
|
|
499
|
+
: state.activeTab === 'installed'
|
|
500
|
+
? state.plugins.filter((p) => p.isInstalled)
|
|
501
|
+
: getFilteredPlugins(state);
|
|
369
502
|
const selectedPlugin = items[state.selectedIndex];
|
|
370
503
|
if (selectedPlugin && selectedPlugin.isInstalled) {
|
|
371
504
|
try {
|
|
@@ -419,12 +552,16 @@ export default function App() {
|
|
|
419
552
|
dispatch({ type: 'SET_SEARCH_QUERY', payload: '' });
|
|
420
553
|
return;
|
|
421
554
|
}
|
|
422
|
-
// Install (i key) - only on
|
|
555
|
+
// Install (i key) - only on enabled/installed/discover tabs
|
|
423
556
|
if (input === 'i' &&
|
|
424
|
-
(state.activeTab === '
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
557
|
+
(state.activeTab === 'enabled' ||
|
|
558
|
+
state.activeTab === 'installed' ||
|
|
559
|
+
state.activeTab === 'discover')) {
|
|
560
|
+
const items = state.activeTab === 'enabled'
|
|
561
|
+
? state.plugins.filter((p) => p.isInstalled && p.isEnabled)
|
|
562
|
+
: state.activeTab === 'installed'
|
|
563
|
+
? state.plugins.filter((p) => p.isInstalled)
|
|
564
|
+
: getFilteredPlugins(state);
|
|
428
565
|
const selectedPlugin = items[state.selectedIndex];
|
|
429
566
|
if (selectedPlugin && !selectedPlugin.isInstalled) {
|
|
430
567
|
handleInstall(selectedPlugin.id);
|
|
@@ -437,12 +574,16 @@ export default function App() {
|
|
|
437
574
|
}
|
|
438
575
|
return;
|
|
439
576
|
}
|
|
440
|
-
// Uninstall (u key) - only on
|
|
577
|
+
// Uninstall (u key) - only on enabled/installed/discover tabs
|
|
441
578
|
if (input === 'u' &&
|
|
442
|
-
(state.activeTab === '
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
579
|
+
(state.activeTab === 'enabled' ||
|
|
580
|
+
state.activeTab === 'installed' ||
|
|
581
|
+
state.activeTab === 'discover')) {
|
|
582
|
+
const items = state.activeTab === 'enabled'
|
|
583
|
+
? state.plugins.filter((p) => p.isInstalled && p.isEnabled)
|
|
584
|
+
: state.activeTab === 'installed'
|
|
585
|
+
? state.plugins.filter((p) => p.isInstalled)
|
|
586
|
+
: getFilteredPlugins(state);
|
|
446
587
|
const selectedPlugin = items[state.selectedIndex];
|
|
447
588
|
if (selectedPlugin && selectedPlugin.isInstalled) {
|
|
448
589
|
dispatch({ type: 'SHOW_CONFIRM_UNINSTALL', payload: selectedPlugin.id });
|
|
@@ -452,11 +593,6 @@ export default function App() {
|
|
|
452
593
|
}
|
|
453
594
|
return;
|
|
454
595
|
}
|
|
455
|
-
// Exit (q or Ctrl+C)
|
|
456
|
-
if (input === 'q' || (key.ctrl && input === 'c')) {
|
|
457
|
-
exit();
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
596
|
});
|
|
461
597
|
// Loading state
|
|
462
598
|
if (state.loading) {
|
|
@@ -468,14 +604,65 @@ export default function App() {
|
|
|
468
604
|
}
|
|
469
605
|
// Get filtered data for current tab
|
|
470
606
|
const filteredPlugins = getFilteredPlugins(state);
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
607
|
+
// Apply search filter to enabled plugins
|
|
608
|
+
const enabledPluginsBase = state.plugins.filter((p) => p.isInstalled && p.isEnabled);
|
|
609
|
+
const enabledPlugins = state.searchQuery
|
|
610
|
+
? searchPlugins(state.searchQuery, enabledPluginsBase)
|
|
611
|
+
: enabledPluginsBase;
|
|
612
|
+
// Apply search filter to installed plugins
|
|
613
|
+
const installedPluginsBase = state.plugins.filter((p) => p.isInstalled);
|
|
614
|
+
const installedPlugins = state.searchQuery
|
|
615
|
+
? searchPlugins(state.searchQuery, installedPluginsBase)
|
|
616
|
+
: installedPluginsBase;
|
|
617
|
+
// Apply search filter to marketplaces
|
|
618
|
+
const filteredMarketplaces = state.searchQuery
|
|
619
|
+
? searchMarketplaces(state.searchQuery, state.marketplaces)
|
|
620
|
+
: state.marketplaces;
|
|
621
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsx(Text, { bold: true, color: "magenta", children: "\u26A1 Claude Code Plugin Dashboard" }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { dimColor: true, children: ["v", packageJson.version] })] }), _jsx(TabBar, { activeTab: state.activeTab, isFocused: state.focusZone === 'tabbar' }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [state.activeTab === 'enabled' && (_jsx(EnabledTab, { plugins: enabledPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone })), state.activeTab === 'installed' && (_jsx(InstalledTab, { plugins: installedPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone })), state.activeTab === 'discover' && (_jsx(DiscoverTab, { plugins: filteredPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, sortBy: state.sortBy, sortOrder: state.sortOrder, focusZone: state.focusZone })), state.activeTab === 'marketplaces' && (_jsx(MarketplacesTab, { marketplaces: filteredMarketplaces, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone })), state.activeTab === 'errors' && (_jsx(ErrorsTab, { errors: state.errors, selectedIndex: state.selectedIndex }))] }), state.confirmUninstall && state.operationPluginId && (_jsx(ConfirmDialog, { message: `Uninstall ${state.operationPluginId}?` })), _jsx(HelpOverlay, { isVisible: state.showHelp }), state.message && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: state.message }) })), _jsx(KeyHints, { focusZone: state.focusZone, extraHints: (() => {
|
|
622
|
+
// Search mode - no extra hints (base hints cover it)
|
|
623
|
+
if (state.focusZone === 'search') {
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
626
|
+
// TabBar mode - no extra hints
|
|
627
|
+
if (state.focusZone === 'tabbar') {
|
|
628
|
+
return undefined;
|
|
629
|
+
}
|
|
630
|
+
// List mode - add tab-specific hints
|
|
631
|
+
// Plugin tabs hints (enabled, installed, discover)
|
|
632
|
+
if (state.activeTab === 'enabled' ||
|
|
633
|
+
state.activeTab === 'installed' ||
|
|
634
|
+
state.activeTab === 'discover') {
|
|
635
|
+
const hints = [
|
|
636
|
+
{ key: '/', action: 'search' },
|
|
637
|
+
{ key: 'i', action: 'install' },
|
|
638
|
+
{ key: 'u', action: 'uninstall' },
|
|
639
|
+
];
|
|
640
|
+
// Get selected plugin to determine Enter action
|
|
641
|
+
const items = state.activeTab === 'enabled'
|
|
642
|
+
? enabledPlugins
|
|
643
|
+
: state.activeTab === 'installed'
|
|
644
|
+
? installedPlugins
|
|
645
|
+
: filteredPlugins;
|
|
646
|
+
const selectedPlugin = items[state.selectedIndex];
|
|
647
|
+
// Add contextual Enter hint
|
|
648
|
+
if (selectedPlugin) {
|
|
649
|
+
if (!selectedPlugin.isInstalled) {
|
|
650
|
+
hints.push({ key: 'Enter', action: 'install' });
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
hints.push({ key: 'Enter', action: 'toggle' });
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
// Add sort hint for discover tab
|
|
657
|
+
if (state.activeTab === 'discover') {
|
|
658
|
+
hints.push({ key: 's', action: 'sort' });
|
|
659
|
+
}
|
|
660
|
+
return hints;
|
|
661
|
+
}
|
|
662
|
+
// Marketplaces tab hints
|
|
663
|
+
if (state.activeTab === 'marketplaces') {
|
|
664
|
+
return [{ key: '/', action: 'search' }];
|
|
665
|
+
}
|
|
666
|
+
return undefined;
|
|
667
|
+
})() })] }));
|
|
481
668
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HelpOverlay component
|
|
3
|
+
* Displays a full-screen overlay with all available keyboard shortcuts
|
|
4
|
+
*/
|
|
5
|
+
interface HelpOverlayProps {
|
|
6
|
+
/** Whether the overlay is visible */
|
|
7
|
+
isVisible: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Full-screen help overlay showing all keyboard shortcuts
|
|
11
|
+
* @example
|
|
12
|
+
* <HelpOverlay isVisible={showHelp} />
|
|
13
|
+
*/
|
|
14
|
+
export default function HelpOverlay({ isVisible }: HelpOverlayProps): import("react/jsx-runtime").JSX.Element | null;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* HelpOverlay component
|
|
4
|
+
* Displays a full-screen overlay with all available keyboard shortcuts
|
|
5
|
+
*/
|
|
6
|
+
import { Box, Text } from 'ink';
|
|
7
|
+
const helpSections = [
|
|
8
|
+
{
|
|
9
|
+
title: 'Navigation',
|
|
10
|
+
items: [
|
|
11
|
+
{ key: '←/→, Tab', description: 'Switch tabs' },
|
|
12
|
+
{ key: '↑/↓, ^P/^N', description: 'Navigate list' },
|
|
13
|
+
{ key: '^F/^B', description: 'Switch tabs (Emacs)' },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
title: 'Actions',
|
|
18
|
+
items: [
|
|
19
|
+
{ key: 'i, Enter', description: 'Install / Toggle plugin' },
|
|
20
|
+
{ key: 'u', description: 'Uninstall plugin' },
|
|
21
|
+
{ key: 'Space', description: 'Toggle enable/disable' },
|
|
22
|
+
{ key: 's/S', description: 'Sort options / order' },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
title: 'Search',
|
|
27
|
+
items: [
|
|
28
|
+
{ key: '/', description: 'Enter search mode' },
|
|
29
|
+
{ key: 'Esc, ↓', description: 'Exit search mode' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
title: 'General',
|
|
34
|
+
items: [
|
|
35
|
+
{ key: 'q, ^C', description: 'Quit' },
|
|
36
|
+
{ key: 'h', description: 'Toggle this help' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* Full-screen help overlay showing all keyboard shortcuts
|
|
42
|
+
* @example
|
|
43
|
+
* <HelpOverlay isVisible={showHelp} />
|
|
44
|
+
*/
|
|
45
|
+
export default function HelpOverlay({ isVisible }) {
|
|
46
|
+
if (!isVisible) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const keyWidth = 14;
|
|
50
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: "cyan", paddingX: 2, paddingY: 1, children: [_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Help \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }) }), helpSections.map((section) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: section.title }), section.items.map((item, itemIndex) => (_jsxs(Box, { children: [_jsx(Box, { width: keyWidth, children: _jsx(Text, { color: "green", children: item.key.padEnd(keyWidth - 2) }) }), _jsx(Text, { dimColor: true, children: item.description })] }, itemIndex)))] }, section.title))), _jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press h or Esc to close" }) })] }));
|
|
51
|
+
}
|
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
* KeyHints component
|
|
3
3
|
* Displays keyboard shortcuts footer at the bottom of the dashboard
|
|
4
4
|
*/
|
|
5
|
+
import type { FocusZone } from '../types/index.js';
|
|
5
6
|
interface KeyHintsProps {
|
|
6
7
|
/** Additional context-specific hints */
|
|
7
8
|
extraHints?: Array<{
|
|
8
9
|
key: string;
|
|
9
10
|
action: string;
|
|
10
11
|
}>;
|
|
12
|
+
/** Current focus zone for context-aware hints */
|
|
13
|
+
focusZone?: FocusZone;
|
|
11
14
|
}
|
|
12
15
|
/**
|
|
13
16
|
* Displays keyboard shortcut hints in the footer
|
|
14
17
|
* @example
|
|
15
|
-
* <KeyHints />
|
|
16
|
-
* <KeyHints extraHints={[{ key: 'i', action: 'install' }]} />
|
|
18
|
+
* <KeyHints focusZone="list" />
|
|
19
|
+
* <KeyHints focusZone="search" extraHints={[{ key: 'i', action: 'install' }]} />
|
|
17
20
|
*/
|
|
18
|
-
export default function KeyHints({ extraHints }: KeyHintsProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export default function KeyHints({ extraHints, focusZone, }: KeyHintsProps): import("react/jsx-runtime").JSX.Element;
|
|
19
22
|
export {};
|
|
@@ -4,20 +4,49 @@ 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
|
+
/**
|
|
8
|
+
* Get base hints based on current focus zone
|
|
9
|
+
* @param focusZone - Current focus zone
|
|
10
|
+
* @returns Array of hint objects
|
|
11
|
+
*/
|
|
12
|
+
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
|
+
}
|
|
41
|
+
}
|
|
7
42
|
/**
|
|
8
43
|
* Displays keyboard shortcut hints in the footer
|
|
9
44
|
* @example
|
|
10
|
-
* <KeyHints />
|
|
11
|
-
* <KeyHints extraHints={[{ key: 'i', action: 'install' }]} />
|
|
45
|
+
* <KeyHints focusZone="list" />
|
|
46
|
+
* <KeyHints focusZone="search" extraHints={[{ key: 'i', action: 'install' }]} />
|
|
12
47
|
*/
|
|
13
|
-
export default function KeyHints({ extraHints }) {
|
|
14
|
-
const baseHints =
|
|
15
|
-
{ key: '←/→', action: 'tabs' },
|
|
16
|
-
{ key: '↑/↓', action: 'navigate' },
|
|
17
|
-
{ key: 'Space', action: 'toggle' },
|
|
18
|
-
{ key: '/', action: 'search' },
|
|
19
|
-
{ key: 'q', action: 'quit' },
|
|
20
|
-
];
|
|
48
|
+
export default function KeyHints({ extraHints, focusZone = 'list', }) {
|
|
49
|
+
const baseHints = getBaseHints(focusZone);
|
|
21
50
|
const allHints = extraHints ? [...baseHints, ...extraHints] : baseHints;
|
|
22
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))) }) }));
|
|
23
52
|
}
|
|
@@ -6,11 +6,13 @@ import type { Marketplace } from '../types/index.js';
|
|
|
6
6
|
interface MarketplaceListProps {
|
|
7
7
|
marketplaces: Marketplace[];
|
|
8
8
|
selectedIndex: number;
|
|
9
|
+
/** Whether the list has keyboard focus */
|
|
10
|
+
isFocused?: boolean;
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
13
|
* Displays a list of marketplaces
|
|
12
14
|
* @example
|
|
13
|
-
* <MarketplaceList marketplaces={marketplaces} selectedIndex={0} />
|
|
15
|
+
* <MarketplaceList marketplaces={marketplaces} selectedIndex={0} isFocused={true} />
|
|
14
16
|
*/
|
|
15
|
-
export default function MarketplaceList({ marketplaces, selectedIndex, }: MarketplaceListProps): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export default function MarketplaceList({ marketplaces, selectedIndex, isFocused, }: MarketplaceListProps): import("react/jsx-runtime").JSX.Element;
|
|
16
18
|
export {};
|
|
@@ -7,15 +7,19 @@ import { Box, Text } from 'ink';
|
|
|
7
7
|
/**
|
|
8
8
|
* Displays a list of marketplaces
|
|
9
9
|
* @example
|
|
10
|
-
* <MarketplaceList marketplaces={marketplaces} selectedIndex={0} />
|
|
10
|
+
* <MarketplaceList marketplaces={marketplaces} selectedIndex={0} isFocused={true} />
|
|
11
11
|
*/
|
|
12
|
-
export default function MarketplaceList({ marketplaces, selectedIndex, }) {
|
|
12
|
+
export default function MarketplaceList({ marketplaces, selectedIndex, isFocused = true, }) {
|
|
13
13
|
if (marketplaces.length === 0) {
|
|
14
14
|
return (_jsx(Box, { padding: 1, children: _jsx(Text, { color: "gray", children: "No marketplaces found" }) }));
|
|
15
15
|
}
|
|
16
16
|
return (_jsx(Box, { flexDirection: "column", children: marketplaces.map((marketplace, index) => {
|
|
17
17
|
const isSelected = index === selectedIndex;
|
|
18
|
-
return (_jsxs(Box, { paddingX: 1, children: [_jsx(Box, { width: 2, children: isSelected ? _jsx(Text, { color: "cyan", children: '>' }) : _jsx(Text, { children: " " }) }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: isSelected
|
|
18
|
+
return (_jsxs(Box, { paddingX: 1, children: [_jsx(Box, { width: 2, children: isSelected && isFocused ? (_jsx(Text, { color: "cyan", children: '>' })) : isSelected ? (_jsx(Text, { color: "gray", children: '›' })) : (_jsx(Text, { children: " " })) }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: isSelected && isFocused
|
|
19
|
+
? 'cyan'
|
|
20
|
+
: isSelected
|
|
21
|
+
? 'gray'
|
|
22
|
+
: 'white', children: marketplace.name || marketplace.id }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsxs(Text, { color: "gray", children: [marketplace.pluginCount || 0, " plugins"] })] }), _jsx(Text, { dimColor: true, wrap: "truncate", children: getSourceDisplay(marketplace) })] })] }, marketplace.id));
|
|
19
23
|
}) }));
|
|
20
24
|
}
|
|
21
25
|
/**
|
|
@@ -9,11 +9,38 @@ interface PluginListProps {
|
|
|
9
9
|
selectedIndex: number;
|
|
10
10
|
/** Maximum visible items (for virtual scrolling) */
|
|
11
11
|
visibleCount?: number;
|
|
12
|
+
/** Whether the list has keyboard focus */
|
|
13
|
+
isFocused?: boolean;
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* Scrollable plugin list with selection
|
|
15
17
|
* @example
|
|
16
|
-
* <PluginList plugins={plugins} selectedIndex={0} visibleCount={15} />
|
|
18
|
+
* <PluginList plugins={plugins} selectedIndex={0} visibleCount={15} isFocused={true} />
|
|
17
19
|
*/
|
|
18
|
-
export default function PluginList({ plugins, selectedIndex, visibleCount, }: PluginListProps): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
export default function PluginList({ plugins, selectedIndex, visibleCount, isFocused, }: PluginListProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
/**
|
|
22
|
+
* Truncate text to max length with ellipsis
|
|
23
|
+
* @param text - The text to truncate
|
|
24
|
+
* @param maxLength - Maximum length including ellipsis
|
|
25
|
+
* @returns
|
|
26
|
+
* - Original text if within maxLength
|
|
27
|
+
* - Truncated text with "..." suffix if exceeds maxLength
|
|
28
|
+
* @example
|
|
29
|
+
* truncate('Hello', 10) // => 'Hello'
|
|
30
|
+
* truncate('Hello World', 8) // => 'Hello...'
|
|
31
|
+
*/
|
|
32
|
+
export declare function truncate(text: string, maxLength: number): string;
|
|
33
|
+
/**
|
|
34
|
+
* Format large numbers with K/M suffix
|
|
35
|
+
* @param count - The number to format
|
|
36
|
+
* @returns
|
|
37
|
+
* - "X.XM" for millions (>= 1,000,000)
|
|
38
|
+
* - "X.XK" for thousands (>= 1,000)
|
|
39
|
+
* - String representation for smaller numbers
|
|
40
|
+
* @example
|
|
41
|
+
* formatCount(1500) // => '1.5K'
|
|
42
|
+
* formatCount(1200000) // => '1.2M'
|
|
43
|
+
* formatCount(500) // => '500'
|
|
44
|
+
*/
|
|
45
|
+
export declare function formatCount(count: number): string;
|
|
19
46
|
export {};
|
|
@@ -9,9 +9,9 @@ import StatusIcon from './StatusIcon.js';
|
|
|
9
9
|
/**
|
|
10
10
|
* Scrollable plugin list with selection
|
|
11
11
|
* @example
|
|
12
|
-
* <PluginList plugins={plugins} selectedIndex={0} visibleCount={15} />
|
|
12
|
+
* <PluginList plugins={plugins} selectedIndex={0} visibleCount={15} isFocused={true} />
|
|
13
13
|
*/
|
|
14
|
-
export default function PluginList({ plugins, selectedIndex, visibleCount = 15, }) {
|
|
14
|
+
export default function PluginList({ plugins, selectedIndex, visibleCount = 15, isFocused = true, }) {
|
|
15
15
|
if (plugins.length === 0) {
|
|
16
16
|
return (_jsx(Box, { padding: 1, children: _jsx(Text, { color: "gray", children: "No plugins found" }) }));
|
|
17
17
|
}
|
|
@@ -29,21 +29,42 @@ export default function PluginList({ plugins, selectedIndex, visibleCount = 15,
|
|
|
29
29
|
return (_jsxs(Box, { flexDirection: "column", children: [hasPrevious && _jsxs(Text, { dimColor: true, children: ["\u2191 ", startIndex, " more above"] }), visiblePlugins.map((plugin, index) => {
|
|
30
30
|
const actualIndex = startIndex + index;
|
|
31
31
|
const isSelected = actualIndex === selectedIndex;
|
|
32
|
-
return (_jsxs(Box, { paddingX: 1, children: [_jsx(Box, { width: 2, children: isSelected ? _jsx(Text, { color: "cyan", children: '>' }) : _jsx(Text, { children: " " }) }), _jsx(Box, { width: 2, children: _jsx(StatusIcon, { isInstalled: plugin.isInstalled, isEnabled: plugin.isEnabled }) }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: isSelected
|
|
32
|
+
return (_jsxs(Box, { paddingX: 1, children: [_jsx(Box, { width: 2, children: isSelected && isFocused ? (_jsx(Text, { color: "cyan", children: '>' })) : isSelected ? (_jsx(Text, { color: "gray", children: '›' })) : (_jsx(Text, { children: " " })) }), _jsx(Box, { width: 2, children: _jsx(StatusIcon, { isInstalled: plugin.isInstalled, isEnabled: plugin.isEnabled }) }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: isSelected && isFocused
|
|
33
|
+
? 'cyan'
|
|
34
|
+
: isSelected
|
|
35
|
+
? 'gray'
|
|
36
|
+
: 'white', children: plugin.name }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { color: "gray", children: truncate(plugin.marketplace, 20) }), plugin.installCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "\u00B7" }), _jsxs(Text, { color: "gray", children: [formatCount(plugin.installCount), " installs"] })] }))] }), _jsx(Text, { dimColor: true, wrap: "truncate", children: truncate(plugin.description, 60) })] })] }, plugin.id));
|
|
33
37
|
}), hasMore && (_jsxs(Text, { dimColor: true, children: ["\u2193 ", plugins.length - endIndex, " more below"] }))] }));
|
|
34
38
|
}
|
|
35
39
|
/**
|
|
36
40
|
* Truncate text to max length with ellipsis
|
|
41
|
+
* @param text - The text to truncate
|
|
42
|
+
* @param maxLength - Maximum length including ellipsis
|
|
43
|
+
* @returns
|
|
44
|
+
* - Original text if within maxLength
|
|
45
|
+
* - Truncated text with "..." suffix if exceeds maxLength
|
|
46
|
+
* @example
|
|
47
|
+
* truncate('Hello', 10) // => 'Hello'
|
|
48
|
+
* truncate('Hello World', 8) // => 'Hello...'
|
|
37
49
|
*/
|
|
38
|
-
function truncate(text, maxLength) {
|
|
50
|
+
export function truncate(text, maxLength) {
|
|
39
51
|
if (text.length <= maxLength)
|
|
40
52
|
return text;
|
|
41
53
|
return text.slice(0, maxLength - 3) + '...';
|
|
42
54
|
}
|
|
43
55
|
/**
|
|
44
56
|
* Format large numbers with K/M suffix
|
|
57
|
+
* @param count - The number to format
|
|
58
|
+
* @returns
|
|
59
|
+
* - "X.XM" for millions (>= 1,000,000)
|
|
60
|
+
* - "X.XK" for thousands (>= 1,000)
|
|
61
|
+
* - String representation for smaller numbers
|
|
62
|
+
* @example
|
|
63
|
+
* formatCount(1500) // => '1.5K'
|
|
64
|
+
* formatCount(1200000) // => '1.2M'
|
|
65
|
+
* formatCount(500) // => '500'
|
|
45
66
|
*/
|
|
46
|
-
function formatCount(count) {
|
|
67
|
+
export function formatCount(count) {
|
|
47
68
|
if (count >= 1000000) {
|
|
48
69
|
return `${(count / 1000000).toFixed(1)}M`;
|
|
49
70
|
}
|
|
@@ -10,5 +10,5 @@ import { Box, Text } from 'ink';
|
|
|
10
10
|
* <SearchInput query={searchQuery} isActive={isSearchMode} />
|
|
11
11
|
*/
|
|
12
12
|
export default function SearchInput({ query, isActive = false, placeholder = 'Type to search...', }) {
|
|
13
|
-
return (_jsxs(Box, { borderStyle: isActive ? 'round' : 'single', borderColor: isActive ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(Text, { color: isActive ? 'cyan' : 'gray', children: "
|
|
13
|
+
return (_jsxs(Box, { borderStyle: isActive ? 'round' : 'single', borderColor: isActive ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(Text, { color: isActive ? 'cyan' : 'gray', children: "\uD83D\uDD0D " }), query ? _jsx(Text, { children: query }) : _jsx(Text, { dimColor: true, children: placeholder }), isActive && _jsx(Text, { color: "cyan", children: "\u258C" })] }));
|
|
14
14
|
}
|
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
* Horizontal tab navigation for the dashboard
|
|
4
4
|
* Supports ← → arrow key navigation
|
|
5
5
|
*/
|
|
6
|
-
type Tab = '
|
|
6
|
+
type Tab = 'enabled' | 'installed' | 'discover' | 'marketplaces' | 'errors';
|
|
7
7
|
interface TabBarProps {
|
|
8
8
|
activeTab: Tab;
|
|
9
9
|
onTabChange?: (tab: Tab) => void;
|
|
10
|
+
/** Whether the tab bar has keyboard focus */
|
|
11
|
+
isFocused?: boolean;
|
|
10
12
|
}
|
|
11
13
|
/**
|
|
12
14
|
* Horizontal tab bar component
|
|
13
15
|
* @example
|
|
14
|
-
* <TabBar activeTab="discover"
|
|
16
|
+
* <TabBar activeTab="discover" isFocused={true} />
|
|
15
17
|
*/
|
|
16
|
-
export default function TabBar({ activeTab }: TabBarProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export default function TabBar({ activeTab, isFocused }: TabBarProps): import("react/jsx-runtime").JSX.Element;
|
|
17
19
|
/**
|
|
18
20
|
* Get the next tab in the cycle
|
|
19
21
|
* @param currentTab - Current active tab
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* TabBar component
|
|
4
4
|
* Horizontal tab navigation for the dashboard
|
|
@@ -6,21 +6,33 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
6
6
|
*/
|
|
7
7
|
import { Box, Text } from 'ink';
|
|
8
8
|
const TABS = [
|
|
9
|
-
{ id: '
|
|
9
|
+
{ id: 'enabled', label: 'Enabled' },
|
|
10
10
|
{ id: 'installed', label: 'Installed' },
|
|
11
|
+
{ id: 'discover', label: 'Discover' },
|
|
11
12
|
{ id: 'marketplaces', label: 'Marketplaces' },
|
|
12
13
|
{ id: 'errors', label: 'Errors' },
|
|
13
14
|
];
|
|
15
|
+
/** Color constants for consistent theming */
|
|
16
|
+
const COLORS = {
|
|
17
|
+
/** Background color when tab bar is focused */
|
|
18
|
+
FOCUS_BG: '#1a3a4a',
|
|
19
|
+
/** Background color for active tab (not focused) */
|
|
20
|
+
ACTIVE_BG: '#333333',
|
|
21
|
+
/** Foreground color for active/focused elements */
|
|
22
|
+
ACTIVE_FG: 'cyan',
|
|
23
|
+
/** Foreground color for inactive elements */
|
|
24
|
+
INACTIVE_FG: 'gray',
|
|
25
|
+
};
|
|
14
26
|
/**
|
|
15
27
|
* Horizontal tab bar component
|
|
16
28
|
* @example
|
|
17
|
-
* <TabBar activeTab="discover"
|
|
29
|
+
* <TabBar activeTab="discover" isFocused={true} />
|
|
18
30
|
*/
|
|
19
|
-
export default function TabBar({ activeTab }) {
|
|
20
|
-
return (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
export default function TabBar({ activeTab, isFocused = false }) {
|
|
32
|
+
return (_jsxs(Box, { gap: 2, marginBottom: 1, children: [isFocused && _jsx(Text, { color: COLORS.ACTIVE_FG, children: "\u25B6" }), TABS.map((tab) => {
|
|
33
|
+
const isActive = tab.id === activeTab;
|
|
34
|
+
return (_jsx(Box, { children: isActive ? (_jsx(Text, { bold: true, color: COLORS.ACTIVE_FG, backgroundColor: isFocused ? COLORS.FOCUS_BG : COLORS.ACTIVE_BG, children: isFocused ? `[${tab.label}]` : ` ${tab.label} ` })) : (_jsx(Text, { color: COLORS.INACTIVE_FG, children: ` ${tab.label} ` })) }, tab.id));
|
|
35
|
+
})] }));
|
|
24
36
|
}
|
|
25
37
|
/**
|
|
26
38
|
* Get the next tab in the cycle
|
|
@@ -54,6 +54,16 @@ export declare function searchPlugins(query: string, plugins?: Plugin[]): Plugin
|
|
|
54
54
|
* @returns Sorted plugins array
|
|
55
55
|
*/
|
|
56
56
|
export declare function sortPlugins(plugins: Plugin[], sortBy: 'installs' | 'name' | 'date', order: 'asc' | 'desc'): Plugin[];
|
|
57
|
+
/**
|
|
58
|
+
* Search marketplaces by query
|
|
59
|
+
* Filters marketplaces by name, id, and source URL/repo
|
|
60
|
+
* @param query - Search query
|
|
61
|
+
* @param marketplaces - Marketplaces to search
|
|
62
|
+
* @returns Filtered marketplaces matching the query
|
|
63
|
+
* @example
|
|
64
|
+
* searchMarketplaces('official', marketplaces) // => marketplaces with 'official' in name/id
|
|
65
|
+
*/
|
|
66
|
+
export declare function searchMarketplaces(query: string, marketplaces: Marketplace[]): Marketplace[];
|
|
57
67
|
/**
|
|
58
68
|
* Get plugin statistics
|
|
59
69
|
* @returns Object with various plugin counts
|
|
@@ -172,6 +172,22 @@ export function sortPlugins(plugins, sortBy, order) {
|
|
|
172
172
|
});
|
|
173
173
|
return sorted;
|
|
174
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Search marketplaces by query
|
|
177
|
+
* Filters marketplaces by name, id, and source URL/repo
|
|
178
|
+
* @param query - Search query
|
|
179
|
+
* @param marketplaces - Marketplaces to search
|
|
180
|
+
* @returns Filtered marketplaces matching the query
|
|
181
|
+
* @example
|
|
182
|
+
* searchMarketplaces('official', marketplaces) // => marketplaces with 'official' in name/id
|
|
183
|
+
*/
|
|
184
|
+
export function searchMarketplaces(query, marketplaces) {
|
|
185
|
+
const lowerQuery = query.toLowerCase();
|
|
186
|
+
return marketplaces.filter((m) => m.name.toLowerCase().includes(lowerQuery) ||
|
|
187
|
+
m.id.toLowerCase().includes(lowerQuery) ||
|
|
188
|
+
m.source.url?.toLowerCase().includes(lowerQuery) ||
|
|
189
|
+
m.source.repo?.toLowerCase().includes(lowerQuery));
|
|
190
|
+
}
|
|
175
191
|
/**
|
|
176
192
|
* Get plugin statistics
|
|
177
193
|
* @returns Object with various plugin counts
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
* DiscoverTab component
|
|
3
3
|
* Browse all available plugins from all marketplaces
|
|
4
4
|
*/
|
|
5
|
-
import type { Plugin, AppState } from '../types/index.js';
|
|
5
|
+
import type { Plugin, AppState, FocusZone } from '../types/index.js';
|
|
6
6
|
interface DiscoverTabProps {
|
|
7
7
|
plugins: Plugin[];
|
|
8
8
|
selectedIndex: number;
|
|
9
9
|
searchQuery: string;
|
|
10
10
|
sortBy: AppState['sortBy'];
|
|
11
11
|
sortOrder: AppState['sortOrder'];
|
|
12
|
-
|
|
12
|
+
/** Current focus zone for keyboard navigation */
|
|
13
|
+
focusZone?: FocusZone;
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
16
|
* Discover tab - browse all plugins
|
|
@@ -20,7 +21,8 @@ interface DiscoverTabProps {
|
|
|
20
21
|
* searchQuery={state.searchQuery}
|
|
21
22
|
* sortBy={state.sortBy}
|
|
22
23
|
* sortOrder={state.sortOrder}
|
|
24
|
+
* focusZone="list"
|
|
23
25
|
* />
|
|
24
26
|
*/
|
|
25
|
-
export default function DiscoverTab({ plugins, selectedIndex, searchQuery, sortBy, sortOrder,
|
|
27
|
+
export default function DiscoverTab({ plugins, selectedIndex, searchQuery, sortBy, sortOrder, focusZone, }: DiscoverTabProps): import("react/jsx-runtime").JSX.Element;
|
|
26
28
|
export {};
|
package/dist/tabs/DiscoverTab.js
CHANGED
|
@@ -17,9 +17,10 @@ import SortDropdown from '../components/SortDropdown.js';
|
|
|
17
17
|
* searchQuery={state.searchQuery}
|
|
18
18
|
* sortBy={state.sortBy}
|
|
19
19
|
* sortOrder={state.sortOrder}
|
|
20
|
+
* focusZone="list"
|
|
20
21
|
* />
|
|
21
22
|
*/
|
|
22
|
-
export default function DiscoverTab({ plugins, selectedIndex, searchQuery, sortBy, sortOrder,
|
|
23
|
+
export default function DiscoverTab({ plugins, selectedIndex, searchQuery, sortBy, sortOrder, focusZone = 'list', }) {
|
|
23
24
|
const selectedPlugin = plugins[selectedIndex] ?? null;
|
|
24
|
-
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Discover plugins (", plugins.length > 0 ? `${selectedIndex + 1}/${plugins.length}` : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsx(SortDropdown, { sortBy: sortBy, sortOrder: sortOrder })] }), _jsx(Box, { marginBottom: 1, children: _jsx(SearchInput, { query: searchQuery, isActive:
|
|
25
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Discover plugins (", plugins.length > 0 ? `${selectedIndex + 1}/${plugins.length}` : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsx(SortDropdown, { sortBy: sortBy, sortOrder: sortOrder })] }), _jsx(Box, { marginBottom: 1, children: _jsx(SearchInput, { query: searchQuery, isActive: focusZone === 'search', placeholder: "Type to search..." }) }), _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(PluginList, { plugins: plugins, selectedIndex: selectedIndex, visibleCount: 12, isFocused: focusZone === 'list' }) }), _jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(PluginDetail, { plugin: selectedPlugin }) })] })] }));
|
|
25
26
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EnabledTab component
|
|
3
|
+
* View and manage enabled plugins (installed + enabled)
|
|
4
|
+
*/
|
|
5
|
+
import type { Plugin, FocusZone } from '../types/index.js';
|
|
6
|
+
interface EnabledTabProps {
|
|
7
|
+
plugins: Plugin[];
|
|
8
|
+
selectedIndex: number;
|
|
9
|
+
searchQuery?: string;
|
|
10
|
+
/** Current focus zone for keyboard navigation */
|
|
11
|
+
focusZone?: FocusZone;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Enabled tab - view currently active plugins
|
|
15
|
+
* @param plugins - Filtered enabled plugins (search already applied by parent)
|
|
16
|
+
* @param selectedIndex - Currently selected item index
|
|
17
|
+
* @param searchQuery - Current search query string
|
|
18
|
+
* @param focusZone - Current focus zone for keyboard navigation
|
|
19
|
+
* @returns Enabled tab component
|
|
20
|
+
* @example
|
|
21
|
+
* <EnabledTab plugins={enabledPlugins} selectedIndex={0} searchQuery="" focusZone="list" />
|
|
22
|
+
*/
|
|
23
|
+
export default function EnabledTab({ plugins, selectedIndex, searchQuery, focusZone, }: EnabledTabProps): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* EnabledTab component
|
|
4
|
+
* View and manage enabled plugins (installed + enabled)
|
|
5
|
+
*/
|
|
6
|
+
import { Box, Text } from 'ink';
|
|
7
|
+
import PluginList from '../components/PluginList.js';
|
|
8
|
+
import PluginDetail from '../components/PluginDetail.js';
|
|
9
|
+
import SearchInput from '../components/SearchInput.js';
|
|
10
|
+
/**
|
|
11
|
+
* Enabled tab - view currently active plugins
|
|
12
|
+
* @param plugins - Filtered enabled plugins (search already applied by parent)
|
|
13
|
+
* @param selectedIndex - Currently selected item index
|
|
14
|
+
* @param searchQuery - Current search query string
|
|
15
|
+
* @param focusZone - Current focus zone for keyboard navigation
|
|
16
|
+
* @returns Enabled tab component
|
|
17
|
+
* @example
|
|
18
|
+
* <EnabledTab plugins={enabledPlugins} selectedIndex={0} searchQuery="" focusZone="list" />
|
|
19
|
+
*/
|
|
20
|
+
export default function EnabledTab({ plugins, selectedIndex, searchQuery = '', focusZone = 'list', }) {
|
|
21
|
+
// Plugins are already filtered by parent, use directly
|
|
22
|
+
const selectedPlugin = plugins[selectedIndex] ?? null;
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Enabled plugins (", plugins.length > 0 ? `${selectedIndex + 1}/${plugins.length}` : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { dimColor: true, children: "Currently active in Claude Code" })] }), _jsx(Box, { marginBottom: 1, children: _jsx(SearchInput, { query: searchQuery, isActive: focusZone === 'search', placeholder: "Type to search enabled plugins..." }) }), _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: plugins.length === 0 ? (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: searchQuery ? 'No matching plugins' : 'No enabled plugins' }), _jsx(Text, { dimColor: true, children: searchQuery
|
|
24
|
+
? 'Try a different search term'
|
|
25
|
+
: 'Enable plugins in the Installed tab or use /plugin enable' })] })) : (_jsx(PluginList, { plugins: plugins, selectedIndex: selectedIndex, visibleCount: 12, isFocused: focusZone === 'list' })) }), _jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(PluginDetail, { plugin: selectedPlugin }) })] })] }));
|
|
26
|
+
}
|
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
* InstalledTab component
|
|
3
3
|
* View and manage installed plugins
|
|
4
4
|
*/
|
|
5
|
-
import type { Plugin } from '../types/index.js';
|
|
5
|
+
import type { Plugin, FocusZone } from '../types/index.js';
|
|
6
6
|
interface InstalledTabProps {
|
|
7
7
|
plugins: Plugin[];
|
|
8
8
|
selectedIndex: number;
|
|
9
|
+
searchQuery?: string;
|
|
10
|
+
/** Current focus zone for keyboard navigation */
|
|
11
|
+
focusZone?: FocusZone;
|
|
9
12
|
}
|
|
10
13
|
/**
|
|
11
14
|
* Installed tab - manage installed plugins
|
|
15
|
+
* @param plugins - Filtered installed plugins (search already applied by parent)
|
|
16
|
+
* @param selectedIndex - Currently selected item index
|
|
17
|
+
* @param searchQuery - Current search query string
|
|
18
|
+
* @param focusZone - Current focus zone for keyboard navigation
|
|
12
19
|
* @example
|
|
13
|
-
* <InstalledTab plugins={installedPlugins} selectedIndex={0} />
|
|
20
|
+
* <InstalledTab plugins={installedPlugins} selectedIndex={0} searchQuery="" focusZone="list" />
|
|
14
21
|
*/
|
|
15
|
-
export default function InstalledTab({ plugins, selectedIndex, }: InstalledTabProps): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
export default function InstalledTab({ plugins, selectedIndex, searchQuery, focusZone, }: InstalledTabProps): import("react/jsx-runtime").JSX.Element;
|
|
16
23
|
export {};
|
|
@@ -6,19 +6,23 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
6
6
|
import { Box, Text } from 'ink';
|
|
7
7
|
import PluginList from '../components/PluginList.js';
|
|
8
8
|
import PluginDetail from '../components/PluginDetail.js';
|
|
9
|
+
import SearchInput from '../components/SearchInput.js';
|
|
9
10
|
/**
|
|
10
11
|
* Installed tab - manage installed plugins
|
|
12
|
+
* @param plugins - Filtered installed plugins (search already applied by parent)
|
|
13
|
+
* @param selectedIndex - Currently selected item index
|
|
14
|
+
* @param searchQuery - Current search query string
|
|
15
|
+
* @param focusZone - Current focus zone for keyboard navigation
|
|
11
16
|
* @example
|
|
12
|
-
* <InstalledTab plugins={installedPlugins} selectedIndex={0} />
|
|
17
|
+
* <InstalledTab plugins={installedPlugins} selectedIndex={0} searchQuery="" focusZone="list" />
|
|
13
18
|
*/
|
|
14
|
-
export default function InstalledTab({ plugins, selectedIndex, }) {
|
|
15
|
-
//
|
|
16
|
-
const
|
|
17
|
-
const selectedPlugin = installedPlugins[selectedIndex] ?? null;
|
|
19
|
+
export default function InstalledTab({ plugins, selectedIndex, searchQuery = '', focusZone = 'list', }) {
|
|
20
|
+
// Plugins are already filtered by parent, use directly
|
|
21
|
+
const selectedPlugin = plugins[selectedIndex] ?? null;
|
|
18
22
|
// Count enabled/disabled
|
|
19
|
-
const enabledCount =
|
|
20
|
-
const disabledCount =
|
|
21
|
-
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Installed plugins (",
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
const enabledCount = plugins.filter((p) => p.isEnabled).length;
|
|
24
|
+
const disabledCount = plugins.length - enabledCount;
|
|
25
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Installed plugins (", plugins.length > 0 ? `${selectedIndex + 1}/${plugins.length}` : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { color: "green", children: ["\u25CF ", enabledCount, " enabled"] }), _jsxs(Text, { color: "yellow", children: ["\u25D0 ", disabledCount, " disabled"] })] })] }), _jsx(Box, { marginBottom: 1, children: _jsx(SearchInput, { query: searchQuery, isActive: focusZone === 'search', placeholder: "Type to search installed plugins..." }) }), _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: plugins.length === 0 ? (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: searchQuery ? 'No matching plugins' : 'No plugins installed' }), _jsx(Text, { dimColor: true, children: searchQuery
|
|
26
|
+
? 'Try a different search term'
|
|
27
|
+
: 'Use the Discover tab or /plugin install in Claude Code' })] })) : (_jsx(PluginList, { plugins: plugins, selectedIndex: selectedIndex, visibleCount: 12, isFocused: focusZone === 'list' })) }), _jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(PluginDetail, { plugin: selectedPlugin }) })] })] }));
|
|
24
28
|
}
|
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
* MarketplacesTab component
|
|
3
3
|
* View and manage marketplace sources
|
|
4
4
|
*/
|
|
5
|
-
import type { Marketplace } from '../types/index.js';
|
|
5
|
+
import type { Marketplace, FocusZone } from '../types/index.js';
|
|
6
6
|
interface MarketplacesTabProps {
|
|
7
7
|
marketplaces: Marketplace[];
|
|
8
8
|
selectedIndex: number;
|
|
9
|
+
searchQuery?: string;
|
|
10
|
+
/** Current focus zone for keyboard navigation */
|
|
11
|
+
focusZone?: FocusZone;
|
|
9
12
|
}
|
|
10
13
|
/**
|
|
11
14
|
* Marketplaces tab - manage plugin sources
|
|
15
|
+
* @param marketplaces - Filtered marketplaces (search already applied by parent)
|
|
16
|
+
* @param selectedIndex - Currently selected item index
|
|
17
|
+
* @param searchQuery - Current search query string
|
|
18
|
+
* @param focusZone - Current focus zone for keyboard navigation
|
|
12
19
|
* @example
|
|
13
|
-
* <MarketplacesTab marketplaces={marketplaces} selectedIndex={0} />
|
|
20
|
+
* <MarketplacesTab marketplaces={marketplaces} selectedIndex={0} searchQuery="" focusZone="list" />
|
|
14
21
|
*/
|
|
15
|
-
export default function MarketplacesTab({ marketplaces, selectedIndex, }: MarketplacesTabProps): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
export default function MarketplacesTab({ marketplaces, selectedIndex, searchQuery, focusZone, }: MarketplacesTabProps): import("react/jsx-runtime").JSX.Element;
|
|
16
23
|
export {};
|
|
@@ -6,16 +6,25 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
6
6
|
import { Box, Text } from 'ink';
|
|
7
7
|
import MarketplaceList from '../components/MarketplaceList.js';
|
|
8
8
|
import MarketplaceDetail from '../components/MarketplaceDetail.js';
|
|
9
|
+
import SearchInput from '../components/SearchInput.js';
|
|
9
10
|
/**
|
|
10
11
|
* Marketplaces tab - manage plugin sources
|
|
12
|
+
* @param marketplaces - Filtered marketplaces (search already applied by parent)
|
|
13
|
+
* @param selectedIndex - Currently selected item index
|
|
14
|
+
* @param searchQuery - Current search query string
|
|
15
|
+
* @param focusZone - Current focus zone for keyboard navigation
|
|
11
16
|
* @example
|
|
12
|
-
* <MarketplacesTab marketplaces={marketplaces} selectedIndex={0} />
|
|
17
|
+
* <MarketplacesTab marketplaces={marketplaces} selectedIndex={0} searchQuery="" focusZone="list" />
|
|
13
18
|
*/
|
|
14
|
-
export default function MarketplacesTab({ marketplaces, selectedIndex, }) {
|
|
19
|
+
export default function MarketplacesTab({ marketplaces, selectedIndex, searchQuery = '', focusZone = 'list', }) {
|
|
15
20
|
const selectedMarketplace = marketplaces[selectedIndex] ?? null;
|
|
16
21
|
// Count total plugins across all marketplaces
|
|
17
22
|
const totalPlugins = marketplaces.reduce((sum, m) => sum + (m.pluginCount || 0), 0);
|
|
18
23
|
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Marketplaces (", marketplaces.length > 0
|
|
19
24
|
? `${selectedIndex + 1}/${marketplaces.length}`
|
|
20
|
-
: '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { color: "gray", children: [totalPlugins, " total plugins"] })] }),
|
|
25
|
+
: '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { color: "gray", children: [totalPlugins, " total plugins"] })] }), _jsx(Box, { marginBottom: 1, children: _jsx(SearchInput, { query: searchQuery, isActive: focusZone === 'search', placeholder: "Type to search marketplaces..." }) }), _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: marketplaces.length === 0 ? (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: searchQuery
|
|
26
|
+
? 'No matching marketplaces'
|
|
27
|
+
: 'No marketplaces found' }), _jsx(Text, { dimColor: true, children: searchQuery
|
|
28
|
+
? 'Try a different search term'
|
|
29
|
+
: 'Add marketplaces with /plugin add-marketplace' })] })) : (_jsx(MarketplaceList, { marketplaces: marketplaces, selectedIndex: selectedIndex, isFocused: focusZone === 'list' })) }), _jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(MarketplaceDetail, { marketplace: selectedMarketplace }) })] })] }));
|
|
21
30
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -152,12 +152,19 @@ export interface Settings {
|
|
|
152
152
|
enabledPlugins?: Record<string, boolean>;
|
|
153
153
|
[key: string]: unknown;
|
|
154
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Focus zones for keyboard navigation
|
|
157
|
+
* Defines which UI area currently has keyboard focus
|
|
158
|
+
*/
|
|
159
|
+
export type FocusZone = 'tabbar' | 'search' | 'list';
|
|
155
160
|
/**
|
|
156
161
|
* Application state for useReducer
|
|
157
162
|
*/
|
|
158
163
|
export interface AppState {
|
|
159
164
|
/** Current active tab */
|
|
160
|
-
activeTab: '
|
|
165
|
+
activeTab: 'enabled' | 'installed' | 'discover' | 'marketplaces' | 'errors';
|
|
166
|
+
/** Current focus zone for keyboard navigation */
|
|
167
|
+
focusZone: FocusZone;
|
|
161
168
|
/** All plugins from all marketplaces */
|
|
162
169
|
plugins: Plugin[];
|
|
163
170
|
/** All marketplaces */
|
|
@@ -184,6 +191,8 @@ export interface AppState {
|
|
|
184
191
|
operationPluginId: string | null;
|
|
185
192
|
/** Whether confirmation dialog is showing */
|
|
186
193
|
confirmUninstall: boolean;
|
|
194
|
+
/** Whether help overlay is showing */
|
|
195
|
+
showHelp: boolean;
|
|
187
196
|
}
|
|
188
197
|
/**
|
|
189
198
|
* Action types for useReducer
|
|
@@ -247,4 +256,9 @@ export type Action = {
|
|
|
247
256
|
payload: string;
|
|
248
257
|
} | {
|
|
249
258
|
type: 'HIDE_CONFIRM_UNINSTALL';
|
|
259
|
+
} | {
|
|
260
|
+
type: 'TOGGLE_HELP';
|
|
261
|
+
} | {
|
|
262
|
+
type: 'SET_FOCUS_ZONE';
|
|
263
|
+
payload: FocusZone;
|
|
250
264
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@laststance/claude-plugin-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Interactive CLI dashboard to manage Claude Code plugins",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ryota Murakami <ryota.murakami@laststance.io> (https://github.com/ryota-murakami)",
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
"bin": {
|
|
25
25
|
"claude-plugin-dashboard": "./dist/cli.js"
|
|
26
26
|
},
|
|
27
|
+
"main": "./dist/app.js",
|
|
28
|
+
"types": "./dist/app.d.ts",
|
|
27
29
|
"type": "module",
|
|
28
30
|
"engines": {
|
|
29
31
|
"node": ">=20"
|
|
@@ -42,7 +44,10 @@
|
|
|
42
44
|
"link": "pnpm build && pnpm link --global",
|
|
43
45
|
"prepublishOnly": "pnpm build",
|
|
44
46
|
"prepare": "husky",
|
|
45
|
-
"prettier": "prettier --ignore-unknown --write ."
|
|
47
|
+
"prettier": "prettier --ignore-unknown --write .",
|
|
48
|
+
"test": "vitest",
|
|
49
|
+
"test:run": "vitest run",
|
|
50
|
+
"test:coverage": "vitest run --coverage"
|
|
46
51
|
},
|
|
47
52
|
"dependencies": {
|
|
48
53
|
"ink": "^5.1.0",
|
|
@@ -52,9 +57,12 @@
|
|
|
52
57
|
"@sindresorhus/tsconfig": "^6.0.0",
|
|
53
58
|
"@types/node": "^22.10.2",
|
|
54
59
|
"@types/react": "^18.3.18",
|
|
60
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
55
61
|
"husky": "^9.1.7",
|
|
62
|
+
"ink-testing-library": "^4.0.0",
|
|
56
63
|
"lint-staged": "^16.2.7",
|
|
57
64
|
"prettier": "^3.7.4",
|
|
58
|
-
"typescript": "^5.7.2"
|
|
65
|
+
"typescript": "^5.7.2",
|
|
66
|
+
"vitest": "^4.0.16"
|
|
59
67
|
}
|
|
60
|
-
}
|
|
68
|
+
}
|