@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.
@@ -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 ? 'cyan' : '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));
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 ? 'cyan' : '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));
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: "Q " }), query ? _jsx(Text, { children: query }) : _jsx(Text, { dimColor: true, children: placeholder }), isActive && _jsx(Text, { color: "cyan", children: "\u258C" })] }));
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 = 'discover' | 'installed' | 'marketplaces' | 'errors';
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" onTabChange={setActiveTab} />
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: 'discover', label: 'Discover' },
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" onTabChange={setActiveTab} />
29
+ * <TabBar activeTab="discover" isFocused={true} />
18
30
  */
19
- export default function TabBar({ activeTab }) {
20
- return (_jsx(Box, { gap: 2, marginBottom: 1, children: TABS.map((tab) => {
21
- const isActive = tab.id === activeTab;
22
- return (_jsx(Box, { children: isActive ? (_jsx(Text, { bold: true, color: "cyan", backgroundColor: "#333333", children: ` ${tab.label} ` })) : (_jsx(Text, { color: "gray", children: ` ${tab.label} ` })) }, tab.id));
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
- isSearchMode?: boolean;
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, isSearchMode, }: DiscoverTabProps): import("react/jsx-runtime").JSX.Element;
27
+ export default function DiscoverTab({ plugins, selectedIndex, searchQuery, sortBy, sortOrder, focusZone, }: DiscoverTabProps): import("react/jsx-runtime").JSX.Element;
26
28
  export {};
@@ -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, isSearchMode = false, }) {
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: isSearchMode, placeholder: "Type to search..." }) }), _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(PluginList, { plugins: plugins, selectedIndex: selectedIndex, visibleCount: 12 }) }), _jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(PluginDetail, { plugin: selectedPlugin }) })] })] }));
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
- // Filter to installed plugins only
16
- const installedPlugins = plugins.filter((p) => p.isInstalled);
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 = installedPlugins.filter((p) => p.isEnabled).length;
20
- const disabledCount = installedPlugins.length - enabledCount;
21
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Installed plugins (", installedPlugins.length > 0
22
- ? `${selectedIndex + 1}/${installedPlugins.length}`
23
- : '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"] })] })] }), _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: installedPlugins.length === 0 ? (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "No plugins installed" }), _jsxs(Text, { dimColor: true, children: ["Use the Discover tab or", ' ', _jsx(Text, { color: "white", children: "/plugin install" }), " in Claude Code"] })] })) : (_jsx(PluginList, { plugins: installedPlugins, selectedIndex: selectedIndex, visibleCount: 12 })) }), _jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(PluginDetail, { plugin: selectedPlugin }) })] })] }));
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"] })] }), _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: "No marketplaces found" }), _jsxs(Text, { dimColor: true, children: ["Add marketplaces with", ' ', _jsx(Text, { color: "white", children: "/plugin add-marketplace" })] })] })) : (_jsx(MarketplaceList, { marketplaces: marketplaces, selectedIndex: selectedIndex })) }), _jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(MarketplaceDetail, { marketplace: selectedMarketplace }) })] })] }));
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
  }
@@ -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: 'discover' | 'installed' | 'marketplaces' | 'errors';
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
  };