@gomjellie/lazyapi 0.0.1

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.
Files changed (63) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +162 -0
  3. package/dist/App.d.ts +9 -0
  4. package/dist/App.js +65 -0
  5. package/dist/cli.d.ts +5 -0
  6. package/dist/cli.js +39 -0
  7. package/dist/components/common/ErrorMessage.d.ts +10 -0
  8. package/dist/components/common/ErrorMessage.js +17 -0
  9. package/dist/components/common/KeybindingFooter.d.ts +18 -0
  10. package/dist/components/common/KeybindingFooter.js +20 -0
  11. package/dist/components/common/LoadingSpinner.d.ts +9 -0
  12. package/dist/components/common/LoadingSpinner.js +15 -0
  13. package/dist/components/common/SearchInput.d.ts +14 -0
  14. package/dist/components/common/SearchInput.js +72 -0
  15. package/dist/components/common/StatusBar.d.ts +14 -0
  16. package/dist/components/common/StatusBar.js +36 -0
  17. package/dist/components/detail-view/DetailView.d.ts +5 -0
  18. package/dist/components/detail-view/DetailView.js +266 -0
  19. package/dist/components/detail-view/LeftPanel.d.ts +19 -0
  20. package/dist/components/detail-view/LeftPanel.js +363 -0
  21. package/dist/components/detail-view/ParameterForm.d.ts +17 -0
  22. package/dist/components/detail-view/ParameterForm.js +77 -0
  23. package/dist/components/detail-view/RightPanel.d.ts +22 -0
  24. package/dist/components/detail-view/RightPanel.js +251 -0
  25. package/dist/components/list-view/EndpointList.d.ts +12 -0
  26. package/dist/components/list-view/EndpointList.js +72 -0
  27. package/dist/components/list-view/Explorer.d.ts +13 -0
  28. package/dist/components/list-view/Explorer.js +83 -0
  29. package/dist/components/list-view/FilterBar.d.ts +12 -0
  30. package/dist/components/list-view/FilterBar.js +33 -0
  31. package/dist/components/list-view/ListView.d.ts +5 -0
  32. package/dist/components/list-view/ListView.js +304 -0
  33. package/dist/components/list-view/SearchBar.d.ts +11 -0
  34. package/dist/components/list-view/SearchBar.js +14 -0
  35. package/dist/hooks/useApiClient.d.ts +11 -0
  36. package/dist/hooks/useApiClient.js +34 -0
  37. package/dist/hooks/useKeyPress.d.ts +26 -0
  38. package/dist/hooks/useKeyPress.js +57 -0
  39. package/dist/hooks/useNavigation.d.ts +10 -0
  40. package/dist/hooks/useNavigation.js +33 -0
  41. package/dist/services/http-client.d.ts +21 -0
  42. package/dist/services/http-client.js +173 -0
  43. package/dist/services/openapi-parser.d.ts +47 -0
  44. package/dist/services/openapi-parser.js +318 -0
  45. package/dist/store/appSlice.d.ts +14 -0
  46. package/dist/store/appSlice.js +144 -0
  47. package/dist/store/hooks.d.ts +9 -0
  48. package/dist/store/hooks.js +7 -0
  49. package/dist/store/index.d.ts +12 -0
  50. package/dist/store/index.js +12 -0
  51. package/dist/types/app-state.d.ts +126 -0
  52. package/dist/types/app-state.js +4 -0
  53. package/dist/types/openapi.d.ts +86 -0
  54. package/dist/types/openapi.js +115 -0
  55. package/dist/utils/clipboard.d.ts +11 -0
  56. package/dist/utils/clipboard.js +26 -0
  57. package/dist/utils/formatters.d.ts +35 -0
  58. package/dist/utils/formatters.js +93 -0
  59. package/dist/utils/schema-formatter.d.ts +21 -0
  60. package/dist/utils/schema-formatter.js +301 -0
  61. package/dist/utils/validators.d.ts +16 -0
  62. package/dist/utils/validators.js +158 -0
  63. package/package.json +88 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 gomjellie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # lazyapi
2
+
3
+ ![demo](https://github.com/user-attachments/assets/ed95f068-09da-4226-91fd-430027258b31)
4
+
5
+ <div align="center">
6
+
7
+ A **Vim-style TUI (Terminal User Interface)** tool for exploring Swagger/OpenAPI documents and testing APIs in the terminal
8
+
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
10
+ [![Node Version](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen)](https://nodejs.org)
11
+
12
+ </div>
13
+
14
+ ---
15
+
16
+ ## ✨ Features
17
+
18
+ - 🎹 **Vim-style keyboard navigation** - Familiar keybindings like h, j, k, l
19
+ - 📄 **Swagger 2.0 & OpenAPI 3.x support** - Supports both JSON/YAML formats
20
+ - 🔌 **tRPC support** - Coming soon
21
+ - 🎨 **Terminal-friendly UI** - Beautiful TUI with neon green theme
22
+ - ⚡ **Fast API testing** - Execute APIs directly from the terminal
23
+ - 🔍 **Search & filtering** - Quickly find endpoints
24
+ - 📋 **Clipboard support** - Copy response results to clipboard
25
+
26
+ ## 📦 Installation
27
+
28
+ ```bash
29
+ npm install -g @gomjellie/lazyapi
30
+ ```
31
+
32
+ Or run the development version locally:
33
+
34
+ ```bash
35
+ git clone https://github.com/gomjellie/lazyapi.git
36
+ cd lazyapi
37
+ pnpm install
38
+ pnpm run dev examples/sample-api.json
39
+ ```
40
+
41
+ ## 🚀 Usage
42
+
43
+ ### Basic Usage
44
+
45
+ ```bash
46
+ lazyapi <openapi-file>
47
+ ```
48
+
49
+ ### Examples
50
+
51
+ ```bash
52
+ # JSON file
53
+ lazyapi openapi.json
54
+
55
+ # YAML file
56
+ lazyapi swagger.yaml
57
+
58
+ # URL (coming soon)
59
+ lazyapi https://api.example.com/openapi.json
60
+ ```
61
+
62
+ ## ⌨️ Keyboard Shortcuts
63
+
64
+ ### List View
65
+
66
+ <img width="819" height="351" alt="image" src="https://github.com/user-attachments/assets/03743827-7467-4336-b14f-436fdd56ac78" />
67
+
68
+ | Key | Description |
69
+ | ------------- | -------------------------------------------------------------- |
70
+ | `j` | Move to next item |
71
+ | `k` | Move to previous item |
72
+ | `g` | Move to top of list |
73
+ | `G` | Move to bottom of list |
74
+ | `:m<1-6>` | Select Method (1:ALL, 2:GET, 3:POST, 4:PUT, 5:DELETE, 6:PATCH) |
75
+ | `l` / `Enter` | View details of selected endpoint |
76
+ | `/` | Enter search mode |
77
+ | `:q` | Quit |
78
+
79
+ #### Search Mode
80
+
81
+ | Key | Description |
82
+ | ---------- | ----------------------------------------------------- |
83
+ | Text input | Enter search query (searches path, description, tags) |
84
+ | `Esc` | Cancel search and return to NORMAL mode |
85
+
86
+ ### Detail View
87
+
88
+ <img width="818" height="351" alt="image" src="https://github.com/user-attachments/assets/1bdaff1b-7839-4128-a370-a906fb17692a" />
89
+
90
+ #### NORMAL Mode
91
+
92
+ | Key | Description |
93
+ | ------- | ------------------------------ |
94
+ | `j` | Move to next field |
95
+ | `k` | Move to previous field |
96
+ | `i` | Enter INSERT mode (edit field) |
97
+ | `Enter` | Execute API request |
98
+ | `u` | Return to list view |
99
+
100
+ #### INSERT Mode
101
+
102
+ | Key | Description |
103
+ | ---------- | --------------------- |
104
+ | `Esc` | Switch to NORMAL mode |
105
+ | Text input | Edit field value |
106
+
107
+ ### Result View
108
+
109
+ | Key | Description |
110
+ | --------- | ----------------------------------------- |
111
+ | `j` / `k` | Scroll response content |
112
+ | `Tab` | Switch to next tab (Body/Headers/Cookies) |
113
+ | `y` | Copy Body content to clipboard |
114
+ | `r` | Re-run request |
115
+ | `q` / `u` | Return to detail view |
116
+
117
+ ## 🛠️ Development
118
+
119
+ ### Requirements
120
+
121
+ - Node.js >= 20.0.0
122
+ - pnpm >= 8.0.0
123
+
124
+ ## 📝 Future Plans
125
+
126
+ - [ ] Load OpenAPI documents from URL
127
+ - [ ] Multiple environment support (dev, staging, prod)
128
+ - [ ] Request history storage
129
+ - [ ] Favorites feature
130
+ - [ ] Custom theme support
131
+ - [ ] Plugin system
132
+
133
+ ## 🤝 Contributing
134
+
135
+ Contributions are always welcome! Please open an issue or submit a PR.
136
+
137
+ 1. Fork the Project
138
+ 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
139
+ 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
140
+ 4. Push to the Branch (`git push origin feature/AmazingFeature`)
141
+ 5. Open a Pull Request
142
+
143
+ ## 📄 License
144
+
145
+ MIT License - see the LICENSE file for details.
146
+
147
+ ## 👤 Author
148
+
149
+ **gomjellie**
150
+
151
+ - GitHub: [@gomjellie](https://github.com/gomjellie)
152
+
153
+ ## 🙏 Acknowledgments
154
+
155
+ - [Ink](https://github.com/vadimdemedes/ink) - React-based terminal UI library
156
+ - [swagger-parser](https://github.com/APIDevTools/swagger-parser) - OpenAPI parser
157
+
158
+ ---
159
+
160
+ <div align="center">
161
+ Made with ❤️ by gomjellie
162
+ </div>
package/dist/App.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 메인 애플리케이션 컴포넌트
3
+ */
4
+ import React from 'react';
5
+ interface AppProps {
6
+ filePath: string;
7
+ }
8
+ export default function App({ filePath }: AppProps): React.JSX.Element;
9
+ export {};
package/dist/App.js ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * 메인 애플리케이션 컴포넌트
3
+ */
4
+ import React, { useEffect, useState } from 'react';
5
+ import { Box, Text, useStdout } from 'ink';
6
+ import { Provider } from 'react-redux';
7
+ import { store } from './store/index.js';
8
+ import { useAppSelector, useAppDispatch } from './store/hooks.js';
9
+ import { setDocument, setEndpoints, setLoading, setError } from './store/appSlice.js';
10
+ import { OpenAPIParserService } from './services/openapi-parser.js';
11
+ import ListView from './components/list-view/ListView.js';
12
+ import DetailView from './components/detail-view/DetailView.js';
13
+ import LoadingSpinner from './components/common/LoadingSpinner.js';
14
+ import ErrorMessage from './components/common/ErrorMessage.js';
15
+ function AppContent({ filePath }) {
16
+ const screen = useAppSelector((state) => state.app.screen);
17
+ const loading = useAppSelector((state) => state.app.loading);
18
+ const error = useAppSelector((state) => state.app.error);
19
+ const dispatch = useAppDispatch();
20
+ const [parser] = useState(() => new OpenAPIParserService());
21
+ const { stdout } = useStdout();
22
+ const terminalHeight = stdout?.rows || 24;
23
+ // OpenAPI 문서 로드
24
+ useEffect(() => {
25
+ const loadDocument = async () => {
26
+ dispatch(setLoading(true));
27
+ try {
28
+ const document = await parser.parseFromFile(filePath);
29
+ const endpoints = parser.extractEndpoints(document);
30
+ dispatch(setDocument(document));
31
+ dispatch(setEndpoints(endpoints));
32
+ dispatch(setLoading(false));
33
+ }
34
+ catch (err) {
35
+ const errorMessage = err instanceof Error ? err.message : '알 수 없는 오류가 발생했습니다';
36
+ dispatch(setError(errorMessage));
37
+ }
38
+ };
39
+ loadDocument();
40
+ }, [filePath, parser, dispatch]);
41
+ // 로딩 상태
42
+ if (loading) {
43
+ return (React.createElement(Box, { flexDirection: "column", padding: 1, height: terminalHeight },
44
+ React.createElement(LoadingSpinner, { message: "OpenAPI \uBB38\uC11C\uB97C \uB85C\uB529\uD558\uB294 \uC911..." })));
45
+ }
46
+ // 에러 상태
47
+ if (error) {
48
+ return (React.createElement(Box, { flexDirection: "column", padding: 1, height: terminalHeight },
49
+ React.createElement(ErrorMessage, { message: error })));
50
+ }
51
+ // 화면 렌더링
52
+ switch (screen) {
53
+ case 'LIST':
54
+ return React.createElement(ListView, null);
55
+ case 'DETAIL':
56
+ return React.createElement(DetailView, null);
57
+ default:
58
+ return (React.createElement(Box, null,
59
+ React.createElement(Text, null, "\uC54C \uC218 \uC5C6\uB294 \uD654\uBA74 \uC0C1\uD0DC\uC785\uB2C8\uB2E4.")));
60
+ }
61
+ }
62
+ export default function App({ filePath }) {
63
+ return (React.createElement(Provider, { store: store },
64
+ React.createElement(AppContent, { filePath: filePath })));
65
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI 엔트리 포인트
4
+ */
5
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI 엔트리 포인트
4
+ */
5
+ import React from 'react';
6
+ import { render } from 'ink';
7
+ import path from 'path';
8
+ import App from './App.js';
9
+ // 명령줄 인자 파싱
10
+ const args = process.argv.slice(2);
11
+ if (args.length === 0) {
12
+ console.error('사용법: swagger-cli <file>');
13
+ console.error('');
14
+ console.error('예시:');
15
+ console.error(' swagger-cli openapi.json');
16
+ console.error(' swagger-cli swagger.yaml');
17
+ process.exit(1);
18
+ }
19
+ const filePath = path.resolve(process.cwd(), args[0]);
20
+ // 터미널 클리어하여 깨끗한 화면에서 시작
21
+ console.clear();
22
+ // 앱 렌더링
23
+ const { waitUntilExit } = render(React.createElement(App, { filePath: filePath }), {
24
+ // stdout을 명시적으로 지정하여 출력 위치 고정
25
+ stdout: process.stdout,
26
+ stdin: process.stdin,
27
+ // exitOnCtrlC를 true로 설정하여 Ctrl+C로 종료 가능
28
+ exitOnCtrlC: true,
29
+ // 콘솔 패치를 활성화하여 console.log 등이 화면을 방해하지 않도록
30
+ patchConsole: true,
31
+ });
32
+ waitUntilExit()
33
+ .then(() => {
34
+ process.exit(0);
35
+ })
36
+ .catch((error) => {
37
+ console.error('애플리케이션 오류:', error.message);
38
+ process.exit(1);
39
+ });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 에러 메시지 컴포넌트
3
+ */
4
+ import React from 'react';
5
+ interface ErrorMessageProps {
6
+ message: string;
7
+ details?: string;
8
+ }
9
+ export default function ErrorMessage({ message, details }: ErrorMessageProps): React.JSX.Element;
10
+ export {};
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 에러 메시지 컴포넌트
3
+ */
4
+ import React from 'react';
5
+ import { Box, Text } from 'ink';
6
+ export default function ErrorMessage({ message, details }) {
7
+ return (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", padding: 1, marginY: 1 },
8
+ React.createElement(Box, { marginBottom: details ? 1 : 0 },
9
+ React.createElement(Text, { bold: true, color: "red" },
10
+ "\u2716 \uC624\uB958:",
11
+ ' '),
12
+ React.createElement(Text, { color: "red" }, message)),
13
+ details && (React.createElement(Box, { marginLeft: 2 },
14
+ React.createElement(Text, { dimColor: true }, details))),
15
+ React.createElement(Box, { marginTop: 1 },
16
+ React.createElement(Text, { dimColor: true }, "q \uD0A4\uB97C \uB20C\uB7EC \uC885\uB8CC\uD558\uAC70\uB098 Ctrl+C\uB97C \uB20C\uB7EC\uC8FC\uC138\uC694."))));
17
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 하단 키바인딩 가이드 컴포넌트
3
+ */
4
+ import React from 'react';
5
+ export interface KeyBinding {
6
+ key: string;
7
+ description: string;
8
+ }
9
+ interface KeybindingFooterProps {
10
+ bindings: KeyBinding[];
11
+ mode?: string;
12
+ additionalInfo?: string;
13
+ commandInput?: string;
14
+ onCommandChange?: (value: string) => void;
15
+ onCommandSubmit?: (value: string) => void;
16
+ }
17
+ export default function KeybindingFooter({ bindings, mode, additionalInfo, commandInput, onCommandChange, onCommandSubmit, }: KeybindingFooterProps): React.JSX.Element;
18
+ export {};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * 하단 키바인딩 가이드 컴포넌트
3
+ */
4
+ import React from 'react';
5
+ import { Box, Text } from 'ink';
6
+ import TextInput from 'ink-text-input';
7
+ export default function KeybindingFooter({ bindings, mode, additionalInfo, commandInput = '', onCommandChange, onCommandSubmit, }) {
8
+ return (React.createElement(Box, { borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, borderColor: "green", paddingX: 1, paddingTop: 0 },
9
+ mode && (React.createElement(Box, { marginRight: 2 },
10
+ React.createElement(Text, { bold: true, color: "black", backgroundColor: "green" }, ' ' + mode + ' '))),
11
+ mode === 'COMMAND' && onCommandChange && onCommandSubmit ? (React.createElement(Box, { flexGrow: 1 },
12
+ React.createElement(Text, { color: "yellow" }, ":"),
13
+ React.createElement(TextInput, { value: commandInput, onChange: onCommandChange, onSubmit: onCommandSubmit }))) : (React.createElement(Box, { flexGrow: 1, gap: 3, flexWrap: "wrap" }, bindings.map((binding, index) => (React.createElement(Box, { key: `${binding.key}-${binding.description}-${index}` },
14
+ React.createElement(Text, { bold: true, color: "green" }, binding.key),
15
+ React.createElement(Text, null,
16
+ " ",
17
+ binding.description)))))),
18
+ additionalInfo && (React.createElement(Box, null,
19
+ React.createElement(Text, { dimColor: true }, additionalInfo)))));
20
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 로딩 스피너 컴포넌트
3
+ */
4
+ import React from 'react';
5
+ interface LoadingSpinnerProps {
6
+ message?: string;
7
+ }
8
+ export default function LoadingSpinner({ message }: LoadingSpinnerProps): React.JSX.Element;
9
+ export {};
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 로딩 스피너 컴포넌트
3
+ */
4
+ import React from 'react';
5
+ import { Box, Text } from 'ink';
6
+ import Spinner from 'ink-spinner';
7
+ export default function LoadingSpinner({ message = '로딩 중...' }) {
8
+ return (React.createElement(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", minHeight: 10 },
9
+ React.createElement(Box, { marginBottom: 1 },
10
+ React.createElement(Text, { color: "green" },
11
+ React.createElement(Spinner, { type: "dots" })),
12
+ React.createElement(Text, { color: "green", bold: true },
13
+ ' ',
14
+ message))));
15
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 자동완성 기능이 있는 검색 입력 컴포넌트
3
+ */
4
+ import React from 'react';
5
+ interface SearchInputProps {
6
+ value: string;
7
+ onChange: (value: string) => void;
8
+ suggestions?: string[];
9
+ placeholder?: string;
10
+ focus?: boolean;
11
+ onSubmit?: () => void;
12
+ }
13
+ export default function SearchInput({ value, onChange, suggestions, placeholder, focus, onSubmit, }: SearchInputProps): React.JSX.Element;
14
+ export {};
@@ -0,0 +1,72 @@
1
+ /**
2
+ * 자동완성 기능이 있는 검색 입력 컴포넌트
3
+ */
4
+ import React, { useState, useMemo } from 'react';
5
+ import { Box, Text } from 'ink';
6
+ import { useInput } from 'ink';
7
+ export default function SearchInput({ value, onChange, suggestions = [], placeholder = '', focus = true, onSubmit, }) {
8
+ const [suggestionIndex, setSuggestionIndex] = useState(0);
9
+ const [lastTypedValue, setLastTypedValue] = useState(value);
10
+ // 현재 입력값과 매칭되는 모든 suggestions 찾기
11
+ const matchingSuggestions = useMemo(() => {
12
+ if (!lastTypedValue || suggestions.length === 0) {
13
+ return [];
14
+ }
15
+ return suggestions.filter((suggestion) => suggestion.toLowerCase().startsWith(lastTypedValue.toLowerCase()) &&
16
+ suggestion.toLowerCase() !== lastTypedValue.toLowerCase());
17
+ }, [lastTypedValue, suggestions]);
18
+ // 현재 선택된 suggestion
19
+ const currentSuggestion = useMemo(() => {
20
+ if (matchingSuggestions.length === 0)
21
+ return null;
22
+ return matchingSuggestions[suggestionIndex % matchingSuggestions.length];
23
+ }, [matchingSuggestions, suggestionIndex]);
24
+ // Ghost text 계산
25
+ const ghostText = useMemo(() => {
26
+ if (!currentSuggestion || !lastTypedValue)
27
+ return '';
28
+ return currentSuggestion.slice(lastTypedValue.length);
29
+ }, [currentSuggestion, lastTypedValue]);
30
+ // 키 입력 처리
31
+ useInput((input, key) => {
32
+ if (!focus)
33
+ return;
34
+ if (key.tab) {
35
+ // Tab: ghost text를 자동완성하고 다음 suggestion으로 이동
36
+ if (currentSuggestion && ghostText) {
37
+ onChange(currentSuggestion);
38
+ setLastTypedValue(lastTypedValue); // 타이핑한 부분은 유지
39
+ setSuggestionIndex((prev) => prev + 1);
40
+ }
41
+ }
42
+ else if (key.return) {
43
+ // Enter: 부모에게 알림 (INSERT 모드 종료)
44
+ if (onSubmit) {
45
+ onSubmit();
46
+ }
47
+ }
48
+ else if (key.backspace || key.delete) {
49
+ // Backspace/Delete
50
+ if (value.length > 0) {
51
+ const newValue = value.slice(0, -1);
52
+ onChange(newValue);
53
+ setLastTypedValue(newValue);
54
+ setSuggestionIndex(0);
55
+ }
56
+ }
57
+ else if (key.escape) {
58
+ // Escape: 입력 취소 (부모가 처리)
59
+ }
60
+ else if (!key.ctrl && !key.meta && input) {
61
+ // 일반 문자 입력
62
+ const newValue = value + input;
63
+ onChange(newValue);
64
+ setLastTypedValue(newValue);
65
+ setSuggestionIndex(0);
66
+ }
67
+ }, { isActive: focus });
68
+ return (React.createElement(Box, null,
69
+ React.createElement(Text, { backgroundColor: focus ? 'green' : undefined }, value),
70
+ ghostText && (React.createElement(Text, { dimColor: true, color: "gray" }, ghostText)),
71
+ !value && !ghostText && placeholder && (React.createElement(Text, { dimColor: true, color: "gray" }, placeholder))));
72
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 상단 상태 바 컴포넌트
3
+ */
4
+ import React from 'react';
5
+ import { AppMode } from '../../types/app-state.js';
6
+ interface StatusBarProps {
7
+ title: string;
8
+ version?: string;
9
+ currentPath?: string;
10
+ mode: AppMode;
11
+ status?: string;
12
+ }
13
+ export default function StatusBar({ title, version, currentPath, mode, status }: StatusBarProps): React.JSX.Element;
14
+ export {};
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 상단 상태 바 컴포넌트
3
+ */
4
+ import React from 'react';
5
+ import { Box, Text } from 'ink';
6
+ export default function StatusBar({ title, version, currentPath, mode, status }) {
7
+ const getModeColor = () => {
8
+ switch (mode) {
9
+ case 'INSERT':
10
+ return 'yellow';
11
+ case 'SEARCH':
12
+ return 'cyan';
13
+ default:
14
+ return 'green';
15
+ }
16
+ };
17
+ return (React.createElement(Box, { borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false, borderColor: "green", paddingX: 1, paddingBottom: 0 },
18
+ React.createElement(Box, { flexGrow: 1 },
19
+ React.createElement(Text, { bold: true, color: "green" },
20
+ "\u25A0 ",
21
+ title),
22
+ version && React.createElement(Text, { dimColor: true },
23
+ " v",
24
+ version),
25
+ currentPath && React.createElement(Text, { dimColor: true },
26
+ " | ",
27
+ currentPath)),
28
+ React.createElement(Box, { gap: 2 },
29
+ status && React.createElement(Text, { color: "green" },
30
+ "\u25CF ",
31
+ status),
32
+ React.createElement(Text, { bold: true, color: getModeColor() },
33
+ "[",
34
+ mode,
35
+ "]"))));
36
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * API 상세 화면
3
+ */
4
+ import React from 'react';
5
+ export default function DetailView(): React.JSX.Element;