@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 +1 -1
- package/dist/app.d.ts +24 -0
- package/dist/app.js +254 -67
- package/dist/cli.js +7 -1
- 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 +13 -5
|
@@ -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
|
};
|