@jlcpcb/cli 0.1.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/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@jlcpcb/cli",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "CLI for JLC/EasyEDA component sourcing and KiCad library management",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/l3wi/jlc-cli.git",
10
+ "directory": "packages/jlc-cli"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "bin": {
16
+ "jlc": "./dist/index.js"
17
+ },
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "scripts": {
21
+ "build": "bun build ./src/index.ts --outdir ./dist --target node",
22
+ "dev": "bun --watch ./src/index.ts",
23
+ "typecheck": "tsc --noEmit",
24
+ "test": "bun test",
25
+ "clean": "rm -rf dist"
26
+ },
27
+ "keywords": [
28
+ "jlcpcb",
29
+ "jlc",
30
+ "easyeda",
31
+ "kicad",
32
+ "electronics",
33
+ "components",
34
+ "pcb",
35
+ "cli"
36
+ ],
37
+ "author": "",
38
+ "license": "MIT",
39
+ "dependencies": {
40
+ "@clack/prompts": "^0.9.1",
41
+ "chalk": "^5.3.0",
42
+ "commander": "^12.0.0",
43
+ "ink": "6.6.0",
44
+ "ink-select-input": "6.2.0",
45
+ "@jlcpcb/core": "workspace:*",
46
+ "open": "11.0.0",
47
+ "react-devtools-core": "7.0.1"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^20.0.0",
51
+ "@types/react": "19.2.7"
52
+ }
53
+ }
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import { render, Box, useInput, useApp } from 'ink';
3
+ import { NavigationProvider, useNavigation, useCurrentScreen } from './navigation/NavigationContext.js';
4
+ import { AppStateProvider } from './state/AppStateContext.js';
5
+ import { SearchScreen } from './screens/SearchScreen.js';
6
+ import { InfoScreen } from './screens/InfoScreen.js';
7
+ import { InstallScreen } from './screens/InstallScreen.js';
8
+ import { InstalledScreen } from './screens/InstalledScreen.js';
9
+ import { LibraryScreen } from './screens/LibraryScreen.js';
10
+ import { LibrarySetupScreen } from './screens/LibrarySetupScreen.js';
11
+ import type { ScreenName, ScreenParams } from './navigation/types.js';
12
+
13
+ function ScreenRouter() {
14
+ const { screen } = useCurrentScreen();
15
+
16
+ switch (screen) {
17
+ case 'search':
18
+ return <SearchScreen />;
19
+ case 'info':
20
+ return <InfoScreen />;
21
+ case 'install':
22
+ return <InstallScreen />;
23
+ case 'installed':
24
+ return <InstalledScreen />;
25
+ case 'library':
26
+ return <LibraryScreen />;
27
+ case 'library-setup':
28
+ return <LibrarySetupScreen />;
29
+ default:
30
+ return null;
31
+ }
32
+ }
33
+
34
+ function AppContent() {
35
+ const { exit } = useApp();
36
+ const { pop, currentIndex, history } = useNavigation();
37
+ const { screen } = useCurrentScreen();
38
+
39
+ useInput((input, key) => {
40
+ // Global ESC handler - pops navigation or exits at root
41
+ // Note: Individual screens handle their own keys, this is just for ESC
42
+ if (key.escape) {
43
+ // Don't allow ESC during install (it auto-navigates when done)
44
+ if (screen === 'install') return;
45
+
46
+ const didPop = pop();
47
+ if (!didPop) {
48
+ exit();
49
+ }
50
+ }
51
+
52
+ // 'q' to quit from root screen
53
+ if (input === 'q' && currentIndex === 0) {
54
+ exit();
55
+ }
56
+ });
57
+
58
+ return (
59
+ <Box flexDirection="column" padding={1}>
60
+ <ScreenRouter />
61
+ </Box>
62
+ );
63
+ }
64
+
65
+ interface AppProps<T extends ScreenName> {
66
+ initialScreen: T;
67
+ initialParams: ScreenParams[T];
68
+ }
69
+
70
+ export function App<T extends ScreenName>({ initialScreen, initialParams }: AppProps<T>) {
71
+ return (
72
+ <NavigationProvider initialScreen={initialScreen} initialParams={initialParams}>
73
+ <AppStateProvider>
74
+ <AppContent />
75
+ </AppStateProvider>
76
+ </NavigationProvider>
77
+ );
78
+ }
79
+
80
+ export function renderApp<T extends ScreenName>(
81
+ initialScreen: T,
82
+ initialParams: ScreenParams[T]
83
+ ): void {
84
+ render(<App initialScreen={initialScreen} initialParams={initialParams} />);
85
+ }
@@ -0,0 +1,173 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import type { InstalledComponent } from '@jlcpcb/core';
4
+ import { Divider } from './Divider.js';
5
+
6
+ // Flexible component type that accepts both ComponentSearchResult and ComponentDetails
7
+ export interface DetailViewComponent {
8
+ lcscId: string;
9
+ name?: string;
10
+ manufacturer?: string;
11
+ package?: string;
12
+ stock?: number;
13
+ price?: number;
14
+ libraryType?: 'basic' | 'extended';
15
+ description?: string;
16
+ datasheetPdf?: string;
17
+ datasheet?: string; // ComponentDetails uses this instead of datasheetPdf
18
+ attributes?: Record<string, unknown>;
19
+ }
20
+
21
+ interface DetailViewProps {
22
+ component: DetailViewComponent;
23
+ terminalWidth: number;
24
+ isInstalled?: boolean;
25
+ installedInfo?: InstalledComponent | null;
26
+ statusMessage?: string | null;
27
+ }
28
+
29
+ function formatStock(stock: number): string {
30
+ if (stock < 1000) return String(stock);
31
+ return '>1k';
32
+ }
33
+
34
+ function truncate(str: string, len: number): string {
35
+ if (!str) return '';
36
+ return str.length > len ? str.slice(0, len - 1) + '…' : str;
37
+ }
38
+
39
+ export function DetailView({ component, terminalWidth, isInstalled, installedInfo, statusMessage }: DetailViewProps) {
40
+ const isWide = terminalWidth >= 80;
41
+ const labelWidth = 16;
42
+ // In wide mode, split into two columns with gap; otherwise full width
43
+ const colWidth = isWide ? Math.floor((terminalWidth - 4) / 2) : terminalWidth - 2;
44
+ const valueWidth = colWidth - labelWidth - 1;
45
+ const attrLabelWidth = 20;
46
+ const attrValueWidth = colWidth - attrLabelWidth - 1;
47
+
48
+ const description = (
49
+ <Box flexDirection="column" marginBottom={1} width={terminalWidth - 2}>
50
+ <Text dimColor>Description</Text>
51
+ <Text wrap="wrap">{component.description || 'No description'}</Text>
52
+ </Box>
53
+ );
54
+
55
+ const partInfo = (
56
+ <Box flexDirection="column" width={colWidth}>
57
+ <Text bold underline color="cyan">Part Info</Text>
58
+ <Box marginTop={1} flexDirection="column">
59
+ <Box>
60
+ <Text dimColor>{'Manufacturer'.padEnd(labelWidth)}</Text>
61
+ <Text>{truncate(component.manufacturer || 'N/A', valueWidth)}</Text>
62
+ </Box>
63
+ <Box>
64
+ <Text dimColor>{'MFR.Part #'.padEnd(labelWidth)}</Text>
65
+ <Text bold>{truncate(component.name || 'N/A', valueWidth)}</Text>
66
+ </Box>
67
+ <Box>
68
+ <Text dimColor>{'JLCPCB Part #'.padEnd(labelWidth)}</Text>
69
+ <Text color="cyan" bold>{component.lcscId}</Text>
70
+ </Box>
71
+ <Box>
72
+ <Text dimColor>{'Package'.padEnd(labelWidth)}</Text>
73
+ <Text>{truncate(component.package || 'N/A', valueWidth)}</Text>
74
+ </Box>
75
+ <Box>
76
+ <Text dimColor>{'Stock'.padEnd(labelWidth)}</Text>
77
+ <Text>{component.stock !== undefined ? formatStock(component.stock) : 'N/A'}</Text>
78
+ </Box>
79
+ <Box>
80
+ <Text dimColor>{'Price'.padEnd(labelWidth)}</Text>
81
+ <Text color="green">{component.price ? `$${component.price.toFixed(4)}` : 'N/A'}</Text>
82
+ </Box>
83
+ <Box>
84
+ <Text dimColor>{'Library Type'.padEnd(labelWidth)}</Text>
85
+ <Text color={component.libraryType === 'basic' ? 'green' : 'yellow'}>
86
+ {component.libraryType === 'basic' ? 'Basic' : 'Extended'}
87
+ </Text>
88
+ </Box>
89
+ </Box>
90
+ </Box>
91
+ );
92
+
93
+ const hasAttributes = component.attributes && Object.keys(component.attributes).length > 0;
94
+ const attributes = hasAttributes ? (
95
+ <Box flexDirection="column" marginLeft={isWide ? 2 : 0} marginTop={isWide ? 0 : 1} width={colWidth}>
96
+ <Text bold underline color="cyan">Attributes</Text>
97
+ <Box marginTop={1} flexDirection="column">
98
+ {Object.entries(component.attributes!).slice(0, 10).map(([key, value]) => {
99
+ const keyWidth = Math.max(key.length + 1, attrLabelWidth);
100
+ const remainingWidth = colWidth - keyWidth - 1;
101
+ return (
102
+ <Box key={key}>
103
+ <Text dimColor>{key.padEnd(keyWidth)}</Text>
104
+ <Text>{truncate(String(value), remainingWidth)}</Text>
105
+ </Box>
106
+ );
107
+ })}
108
+ </Box>
109
+ </Box>
110
+ ) : null;
111
+
112
+ const datasheetUrl = component.datasheetPdf || component.datasheet;
113
+ const datasheet = datasheetUrl ? (
114
+ <Box flexDirection="column" marginTop={1} width={terminalWidth - 2}>
115
+ <Text dimColor>Datasheet</Text>
116
+ <Text color="blue" wrap="wrap">{truncate(datasheetUrl, terminalWidth - 4)}</Text>
117
+ </Box>
118
+ ) : null;
119
+
120
+ // Show installation info if installed
121
+ const installInfo = isInstalled && installedInfo ? (
122
+ <Box flexDirection="column" marginTop={1} width={terminalWidth - 2}>
123
+ <Text bold underline color="green">Installation</Text>
124
+ <Box marginTop={1} flexDirection="column">
125
+ <Box>
126
+ <Text dimColor>{'Symbol'.padEnd(labelWidth)}</Text>
127
+ <Text color="cyan">{truncate(installedInfo.symbolRef, terminalWidth - labelWidth - 4)}</Text>
128
+ </Box>
129
+ <Box>
130
+ <Text dimColor>{'Footprint'.padEnd(labelWidth)}</Text>
131
+ <Text color="cyan">{truncate(installedInfo.footprintRef || 'N/A', terminalWidth - labelWidth - 4)}</Text>
132
+ </Box>
133
+ <Box>
134
+ <Text dimColor>{'3D Model'.padEnd(labelWidth)}</Text>
135
+ <Text color={installedInfo.has3dModel ? 'green' : 'yellow'}>
136
+ {installedInfo.has3dModel ? 'Yes' : 'No'}
137
+ </Text>
138
+ </Box>
139
+ </Box>
140
+ </Box>
141
+ ) : null;
142
+
143
+ // Footer text based on installation status
144
+ const footerText = isInstalled
145
+ ? 'S Symbol • F Footprint • M 3D Model • R Regenerate • D Datasheet • Esc Back'
146
+ : 'Enter Install • D Datasheet • Esc Back';
147
+
148
+ return (
149
+ <Box flexDirection="column" width="100%">
150
+ {description}
151
+ <Box flexDirection={isWide ? 'row' : 'column'} width="100%">
152
+ {partInfo}
153
+ {attributes}
154
+ </Box>
155
+ {installInfo}
156
+ {datasheet}
157
+ <Box marginTop={1} flexDirection="column" width="100%">
158
+ <Divider width={terminalWidth} />
159
+ {statusMessage && (
160
+ <Box paddingY={0}>
161
+ <Text color={statusMessage.startsWith('✓') ? 'green' : statusMessage.startsWith('✗') ? 'red' : 'yellow'}>
162
+ {statusMessage}
163
+ </Text>
164
+ </Box>
165
+ )}
166
+ <Box paddingY={0}>
167
+ <Text dimColor>{footerText}</Text>
168
+ </Box>
169
+ <Divider width={terminalWidth} />
170
+ </Box>
171
+ </Box>
172
+ );
173
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { Text } from 'ink';
3
+
4
+ interface DividerProps {
5
+ width: number;
6
+ }
7
+
8
+ export function Divider({ width }: DividerProps) {
9
+ return <Text dimColor>{'─'.repeat(Math.max(width - 2, 10))}</Text>;
10
+ }
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { Divider } from './Divider.js';
4
+
5
+ // Minimal component type for installed view
6
+ export interface InstalledViewComponent {
7
+ lcscId: string;
8
+ name?: string;
9
+ }
10
+
11
+ interface InstalledViewProps {
12
+ component: InstalledViewComponent;
13
+ result: { symbolRef: string; footprintRef: string } | null;
14
+ error: string | null;
15
+ terminalWidth: number;
16
+ }
17
+
18
+ export function InstalledView({
19
+ component,
20
+ result,
21
+ error,
22
+ terminalWidth,
23
+ }: InstalledViewProps) {
24
+ if (error) {
25
+ return (
26
+ <Box flexDirection="column">
27
+ <Text color="red">✗ Installation failed: {error}</Text>
28
+ <Box marginTop={1} flexDirection="column">
29
+ <Divider width={terminalWidth} />
30
+ <Text dimColor>Press any key to go back</Text>
31
+ <Divider width={terminalWidth} />
32
+ </Box>
33
+ </Box>
34
+ );
35
+ }
36
+
37
+ if (!result) {
38
+ return (
39
+ <Box flexDirection="column">
40
+ <Text color="red">✗ Installation failed</Text>
41
+ <Box marginTop={1} flexDirection="column">
42
+ <Divider width={terminalWidth} />
43
+ <Text dimColor>Press any key to go back</Text>
44
+ <Divider width={terminalWidth} />
45
+ </Box>
46
+ </Box>
47
+ );
48
+ }
49
+
50
+ return (
51
+ <Box flexDirection="column">
52
+ <Text color="green" bold>✓ Installed {component.lcscId}</Text>
53
+ <Box marginTop={1} flexDirection="column">
54
+ <Box>
55
+ <Text dimColor>{'Symbol '}</Text>
56
+ <Text color="cyan">{result.symbolRef}</Text>
57
+ </Box>
58
+ <Box>
59
+ <Text dimColor>{'Footprint '}</Text>
60
+ <Text color="cyan">{result.footprintRef}</Text>
61
+ </Box>
62
+ </Box>
63
+ <Box marginTop={1} flexDirection="column">
64
+ <Divider width={terminalWidth} />
65
+ <Text dimColor>Press any key to continue</Text>
66
+ <Divider width={terminalWidth} />
67
+ </Box>
68
+ </Box>
69
+ );
70
+ }
@@ -0,0 +1,93 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import type { ComponentSearchResult } from '@jlcpcb/core';
4
+ import { Divider } from './Divider.js';
5
+
6
+ interface ListViewProps {
7
+ results: ComponentSearchResult[];
8
+ selectedIndex: number;
9
+ isFiltered: boolean;
10
+ terminalWidth: number;
11
+ }
12
+
13
+ function formatStock(stock: number): string {
14
+ if (stock < 1000) return String(stock);
15
+ return '>1k';
16
+ }
17
+
18
+ function truncate(str: string, len: number): string {
19
+ if (!str) return '';
20
+ return str.length > len ? str.slice(0, len - 1) + '…' : str;
21
+ }
22
+
23
+ export function ListView({
24
+ results,
25
+ selectedIndex,
26
+ isFiltered,
27
+ terminalWidth,
28
+ }: ListViewProps) {
29
+ // Calculate column widths based on terminal width
30
+ const minWidth = 80;
31
+ const availableWidth = Math.max(terminalWidth - 4, minWidth);
32
+
33
+ // Fixed columns: MFR.Part(18), Package(10), Stock(6), Price(8), Library(10) + spacing
34
+ const mfrPartWidth = 18;
35
+ const pkgWidth = 10;
36
+ const stockWidth = 6;
37
+ const priceWidth = 8;
38
+ const libraryWidth = 10;
39
+ const fixedWidth = mfrPartWidth + pkgWidth + stockWidth + priceWidth + libraryWidth + 4; // +4 for spacing
40
+
41
+ // Description gets all remaining space
42
+ const descWidth = Math.max(availableWidth - fixedWidth, 15);
43
+
44
+ return (
45
+ <Box flexDirection="column">
46
+ <Box marginBottom={1}>
47
+ <Text bold dimColor>
48
+ {' '}
49
+ {'MFR.Part'.padEnd(mfrPartWidth)}
50
+ {'Description'.padEnd(descWidth)}
51
+ {'Package'.padEnd(pkgWidth)}
52
+ {'Stock'.padStart(stockWidth)}
53
+ {'Price'.padStart(priceWidth)}
54
+ {' Library'}
55
+ </Text>
56
+ </Box>
57
+ {results.map((r, i) => {
58
+ const isSelected = i === selectedIndex;
59
+ const mfrPart = truncate(r.name || '', mfrPartWidth - 1).padEnd(mfrPartWidth);
60
+ const desc = truncate(r.description || '', descWidth - 1).padEnd(descWidth);
61
+ const pkg = truncate(r.package || '', pkgWidth - 1).padEnd(pkgWidth);
62
+ const stock = formatStock(r.stock || 0).padStart(stockWidth);
63
+ const price = r.price ? `$${r.price.toFixed(2)}`.padStart(priceWidth) : ' N/A';
64
+ const library = r.libraryType === 'basic' ? 'Basic' : 'Extended';
65
+ const libraryColor = r.libraryType === 'basic' ? 'green' : 'yellow';
66
+
67
+ return (
68
+ <Box key={r.lcscId}>
69
+ <Text color="cyan">{isSelected ? '▶' : ' '}</Text>
70
+ <Text inverse={isSelected}>
71
+ <Text color="cyan">{mfrPart}</Text>
72
+ <Text dimColor>{desc}</Text>
73
+ {pkg}
74
+ {stock}
75
+ {price}
76
+ {' '}
77
+ <Text color={libraryColor}>{library}</Text>
78
+ </Text>
79
+ </Box>
80
+ );
81
+ })}
82
+ <Box marginTop={1} flexDirection="column">
83
+ <Divider width={terminalWidth} />
84
+ <Box paddingY={0}>
85
+ <Text dimColor>
86
+ ↑/↓ Navigate • Enter View • Tab {isFiltered ? 'All Parts' : 'Basic Only'} • Esc Back
87
+ </Text>
88
+ </Box>
89
+ <Divider width={terminalWidth} />
90
+ </Box>
91
+ </Box>
92
+ );
93
+ }
@@ -0,0 +1,29 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ export interface TerminalSize {
4
+ columns: number;
5
+ rows: number;
6
+ }
7
+
8
+ export function useTerminalSize(): TerminalSize {
9
+ const [size, setSize] = useState<TerminalSize>({
10
+ columns: process.stdout.columns || 80,
11
+ rows: process.stdout.rows || 24,
12
+ });
13
+
14
+ useEffect(() => {
15
+ const handleResize = () => {
16
+ setSize({
17
+ columns: process.stdout.columns || 80,
18
+ rows: process.stdout.rows || 24,
19
+ });
20
+ };
21
+
22
+ process.stdout.on('resize', handleResize);
23
+ return () => {
24
+ process.stdout.off('resize', handleResize);
25
+ };
26
+ }, []);
27
+
28
+ return size;
29
+ }
@@ -0,0 +1,87 @@
1
+ import React, { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
2
+ import type {
3
+ NavigationContextValue,
4
+ ScreenName,
5
+ ScreenParams,
6
+ HistoryEntry,
7
+ } from './types.js';
8
+
9
+ const NavigationContext = createContext<NavigationContextValue | null>(null);
10
+
11
+ interface NavigationProviderProps<T extends ScreenName> {
12
+ initialScreen: T;
13
+ initialParams: ScreenParams[T];
14
+ children: ReactNode;
15
+ }
16
+
17
+ export function NavigationProvider<T extends ScreenName>({
18
+ initialScreen,
19
+ initialParams,
20
+ children,
21
+ }: NavigationProviderProps<T>) {
22
+ const [history, setHistory] = useState<HistoryEntry[]>([
23
+ { screen: initialScreen, params: initialParams },
24
+ ]);
25
+ const [currentIndex, setCurrentIndex] = useState(0);
26
+
27
+ const push = useCallback(
28
+ <S extends ScreenName>(screen: S, params: ScreenParams[S]) => {
29
+ setHistory((prev) => [
30
+ ...prev.slice(0, currentIndex + 1),
31
+ { screen, params } as HistoryEntry,
32
+ ]);
33
+ setCurrentIndex((prev) => prev + 1);
34
+ },
35
+ [currentIndex]
36
+ );
37
+
38
+ const pop = useCallback(() => {
39
+ if (currentIndex > 0) {
40
+ setCurrentIndex((prev) => prev - 1);
41
+ return true;
42
+ }
43
+ return false;
44
+ }, [currentIndex]);
45
+
46
+ const replace = useCallback(
47
+ <S extends ScreenName>(screen: S, params: ScreenParams[S]) => {
48
+ setHistory((prev) => {
49
+ const newHistory = [...prev];
50
+ newHistory[currentIndex] = { screen, params } as HistoryEntry;
51
+ return newHistory;
52
+ });
53
+ },
54
+ [currentIndex]
55
+ );
56
+
57
+ const reset = useCallback(<S extends ScreenName>(screen: S, params: ScreenParams[S]) => {
58
+ setHistory([{ screen, params } as HistoryEntry]);
59
+ setCurrentIndex(0);
60
+ }, []);
61
+
62
+ const value: NavigationContextValue = {
63
+ history,
64
+ currentIndex,
65
+ push,
66
+ pop,
67
+ replace,
68
+ reset,
69
+ };
70
+
71
+ return (
72
+ <NavigationContext.Provider value={value}>{children}</NavigationContext.Provider>
73
+ );
74
+ }
75
+
76
+ export function useNavigation(): NavigationContextValue {
77
+ const context = useContext(NavigationContext);
78
+ if (!context) {
79
+ throw new Error('useNavigation must be used within NavigationProvider');
80
+ }
81
+ return context;
82
+ }
83
+
84
+ export function useCurrentScreen(): HistoryEntry {
85
+ const { history, currentIndex } = useNavigation();
86
+ return history[currentIndex];
87
+ }
@@ -0,0 +1,66 @@
1
+ import type { ComponentSearchResult, ComponentDetails, InstallResult, InstalledComponent, LibraryStatus } from '@jlcpcb/core';
2
+
3
+ export type ScreenName = 'search' | 'info' | 'install' | 'library' | 'library-setup' | 'installed';
4
+
5
+ // Common component type that works across screens
6
+ export type ComponentInfo = ComponentSearchResult | ComponentDetails;
7
+
8
+ export interface SearchParams {
9
+ query: string;
10
+ results: ComponentSearchResult[];
11
+ }
12
+
13
+ export interface InfoParams {
14
+ componentId: string;
15
+ component?: ComponentInfo;
16
+ }
17
+
18
+ export interface InstallParams {
19
+ componentId: string;
20
+ component: ComponentInfo;
21
+ }
22
+
23
+ export interface LibraryParams {
24
+ status?: LibraryStatus;
25
+ installed?: InstalledComponent[];
26
+ }
27
+
28
+ export interface LibrarySetupParams {
29
+ componentId: string;
30
+ component: ComponentInfo;
31
+ }
32
+
33
+ export interface InstalledParams {
34
+ componentId: string;
35
+ component: ComponentInfo;
36
+ result?: InstallResult;
37
+ error?: string;
38
+ }
39
+
40
+ export interface ScreenParams {
41
+ search: SearchParams;
42
+ info: InfoParams;
43
+ install: InstallParams;
44
+ library: LibraryParams;
45
+ 'library-setup': LibrarySetupParams;
46
+ installed: InstalledParams;
47
+ }
48
+
49
+ export interface HistoryEntry<T extends ScreenName = ScreenName> {
50
+ screen: T;
51
+ params: ScreenParams[T];
52
+ }
53
+
54
+ export interface NavigationState {
55
+ history: HistoryEntry[];
56
+ currentIndex: number;
57
+ }
58
+
59
+ export interface NavigationActions {
60
+ push: <T extends ScreenName>(screen: T, params: ScreenParams[T]) => void;
61
+ pop: () => boolean;
62
+ replace: <T extends ScreenName>(screen: T, params: ScreenParams[T]) => void;
63
+ reset: <T extends ScreenName>(screen: T, params: ScreenParams[T]) => void;
64
+ }
65
+
66
+ export type NavigationContextValue = NavigationState & NavigationActions;