@laststance/claude-plugin-dashboard 0.1.0 → 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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Claude Code Plugin Dashboard
2
2
 
3
- [![npm version](https://badge.fury.io/js/@laststance/claude-plugin-dashboard.svg)](https://www.npmjs.com/package/@laststance/claude-plugin-dashboard)
3
+ [![npm version](https://img.shields.io/npm/v/@laststance/claude-plugin-dashboard)](https://www.npmjs.com/package/@laststance/claude-plugin-dashboard)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
6
  An interactive CLI dashboard to browse, install, and manage [Claude Code](https://claude.ai/code) plugins.
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 { useState, useEffect, useReducer } from 'react';
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: 'discover',
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 'discover':
193
- return getFilteredPlugins(state);
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 (isSearchMode) {
307
- if (key.escape || key.return) {
308
- setIsSearchMode(false);
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
- // Emacs-style navigation (Ctrl+P / Ctrl+N)
328
- if (key.ctrl && input === 'p') {
329
- dispatch({ type: 'MOVE_SELECTION', payload: 'up' });
330
- return;
331
- }
332
- if (key.ctrl && input === 'n') {
333
- dispatch({ type: 'MOVE_SELECTION', payload: 'down' });
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
- // Tab navigation
337
- if (key.leftArrow) {
338
- dispatch({ type: 'PREV_TAB' });
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
- if (key.rightArrow) {
342
- dispatch({ type: 'NEXT_TAB' });
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
- // List navigation
350
- if (key.upArrow) {
351
- dispatch({ type: 'MOVE_SELECTION', payload: 'up' });
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
- if (key.downArrow) {
355
- dispatch({ type: 'MOVE_SELECTION', payload: 'down' });
356
- return;
357
- }
358
- // Enter search mode
359
- if (input === '/' && state.activeTab === 'discover') {
360
- setIsSearchMode(true);
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 or Enter)
364
- if ((input === ' ' || key.return) &&
365
- (state.activeTab === 'discover' || state.activeTab === 'installed')) {
366
- const items = state.activeTab === 'installed'
367
- ? state.plugins.filter((p) => p.isInstalled)
368
- : getFilteredPlugins(state);
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 discover/installed tabs
555
+ // Install (i key) - only on enabled/installed/discover tabs
423
556
  if (input === 'i' &&
424
- (state.activeTab === 'discover' || state.activeTab === 'installed')) {
425
- const items = state.activeTab === 'installed'
426
- ? state.plugins.filter((p) => p.isInstalled)
427
- : getFilteredPlugins(state);
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 discover/installed tabs
577
+ // Uninstall (u key) - only on enabled/installed/discover tabs
441
578
  if (input === 'u' &&
442
- (state.activeTab === 'discover' || state.activeTab === 'installed')) {
443
- const items = state.activeTab === 'installed'
444
- ? state.plugins.filter((p) => p.isInstalled)
445
- : getFilteredPlugins(state);
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
- const installedPlugins = state.plugins.filter((p) => p.isInstalled);
472
- 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 }), _jsx(Text, { dimColor: true, children: "v0.1.0" })] }), _jsx(TabBar, { activeTab: state.activeTab }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [state.activeTab === 'discover' && (_jsx(DiscoverTab, { plugins: filteredPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, sortBy: state.sortBy, sortOrder: state.sortOrder, isSearchMode: isSearchMode })), state.activeTab === 'installed' && (_jsx(InstalledTab, { plugins: installedPlugins, selectedIndex: state.selectedIndex })), state.activeTab === 'marketplaces' && (_jsx(MarketplacesTab, { marketplaces: state.marketplaces, selectedIndex: state.selectedIndex })), state.activeTab === 'errors' && (_jsx(ErrorsTab, { errors: state.errors, selectedIndex: state.selectedIndex }))] }), state.confirmUninstall && state.operationPluginId && (_jsx(ConfirmDialog, { message: `Uninstall ${state.operationPluginId}?` })), state.message && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: state.message }) })), _jsx(KeyHints, { extraHints: state.activeTab === 'discover' || state.activeTab === 'installed'
473
- ? [
474
- { key: 'i', action: 'install' },
475
- { key: 'u', action: 'uninstall' },
476
- ...(state.activeTab === 'discover'
477
- ? [{ key: 's', action: 'sort' }]
478
- : []),
479
- ]
480
- : undefined })] }));
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
  }
package/dist/cli.js CHANGED
@@ -312,5 +312,11 @@ else {
312
312
  console.log('Use "claude-plugin-dashboard help" for non-interactive commands.');
313
313
  process.exit(1);
314
314
  }
315
- render(_jsx(App, {}));
315
+ const instance = render(_jsx(App, {}));
316
+ // Clear screen when app exits (q key or Ctrl+C)
317
+ instance.waitUntilExit().then(() => {
318
+ instance.clear();
319
+ // Clear entire terminal screen and reset cursor to top-left
320
+ process.stdout.write('\x1b[2J\x1b[H');
321
+ });
316
322
  }
@@ -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 {};