@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
@@ -0,0 +1,266 @@
1
+ /**
2
+ * API 상세 화면
3
+ */
4
+ import React from 'react';
5
+ import { Box, Text, useStdout } from 'ink';
6
+ import { useAppSelector, useAppDispatch } from '../../store/hooks.js';
7
+ import { setMode, detailSetFocus, detailSetPanel, detailSetRequestTab, detailSetResponseTab, detailSetResponse, detailSetParam, } from '../../store/appSlice.js';
8
+ import { useNavigation } from '../../hooks/useNavigation.js';
9
+ import { useKeyPress } from '../../hooks/useKeyPress.js';
10
+ import { useApiClient } from '../../hooks/useApiClient.js';
11
+ import StatusBar from '../common/StatusBar.js';
12
+ import KeybindingFooter from '../common/KeybindingFooter.js';
13
+ import LeftPanel from './LeftPanel.js';
14
+ import RightPanel from './RightPanel.js';
15
+ import { getServerUrl } from '../../types/openapi.js';
16
+ import { setCommandInput } from '../../store/appSlice.js';
17
+ export default function DetailView() {
18
+ const detail = useAppSelector((state) => state.app.detail);
19
+ const document = useAppSelector((state) => state.app.document);
20
+ const mode = useAppSelector((state) => state.app.mode);
21
+ const commandInput = useAppSelector((state) => state.app.commandInput);
22
+ const dispatch = useAppDispatch();
23
+ const { goBack } = useNavigation();
24
+ const { executeRequest, loading } = useApiClient();
25
+ const { stdout } = useStdout();
26
+ // detail이 없는 경우를 대비한 기본값
27
+ const endpoint = detail?.endpoint;
28
+ const activePanel = detail?.activePanel || 'left';
29
+ const activeRequestTab = detail?.activeRequestTab || 'headers';
30
+ const activeResponseTab = detail?.activeResponseTab || 'body';
31
+ // 파라미터 필드 개수 계산
32
+ const pathParams = endpoint?.parameters.filter((p) => p.in === 'path') || [];
33
+ const queryParams = endpoint?.parameters.filter((p) => p.in === 'query') || [];
34
+ const headerParams = endpoint?.parameters.filter((p) => p.in === 'header') || [];
35
+ const bodyParams = endpoint?.parameters.filter((p) => p.in === 'body') || [];
36
+ const formDataParams = endpoint?.parameters.filter((p) => p.in === 'formData') || [];
37
+ const totalFields = pathParams.length +
38
+ queryParams.length +
39
+ headerParams.length +
40
+ bodyParams.length +
41
+ formDataParams.length;
42
+ const handleExecuteRequest = async () => {
43
+ if (!detail || !document)
44
+ return;
45
+ const baseURL = getServerUrl(document);
46
+ if (!baseURL)
47
+ return;
48
+ const response = await executeRequest(detail.endpoint, detail.parameterValues, detail.headers, detail.requestBody, baseURL);
49
+ if (response) {
50
+ // Detail 페이지에서 바로 결과 표시 (Result 페이지로 이동하지 않음)
51
+ dispatch(detailSetResponse(response));
52
+ // Response Body 탭으로 자동 전환
53
+ dispatch(detailSetResponseTab('body'));
54
+ // Response 패널로 포커스 이동
55
+ dispatch(detailSetPanel('response'));
56
+ }
57
+ };
58
+ const handleCommandSubmit = (cmd) => {
59
+ // 1. 종료 명령어 처리
60
+ if (cmd === 'q' || cmd === 'quit' || cmd === 'exit') {
61
+ process.exit(0);
62
+ }
63
+ // 2. 접두사 있는 명령어 처리 (pp1, qp1, h1, rb1, rs1, rq1...)
64
+ const match = cmd.match(/^(pp|qp|h|rb|rs|rq)(\d+)$/);
65
+ if (match) {
66
+ const type = match[1];
67
+ const index = parseInt(match[2], 10);
68
+ const targetIndex = index - 1;
69
+ if (type === 'pp') {
70
+ // Path Parameters
71
+ if (targetIndex >= 0 && targetIndex < pathParams.length) {
72
+ dispatch(detailSetFocus(targetIndex));
73
+ dispatch(detailSetPanel('left'));
74
+ }
75
+ }
76
+ else if (type === 'qp') {
77
+ // Query Parameters
78
+ const queryStartIndex = pathParams.length;
79
+ if (targetIndex >= 0 && targetIndex < queryParams.length) {
80
+ dispatch(detailSetFocus(queryStartIndex + targetIndex));
81
+ dispatch(detailSetPanel('left'));
82
+ }
83
+ }
84
+ else if (type === 'h') {
85
+ // Headers
86
+ const headerStartIndex = pathParams.length + queryParams.length;
87
+ if (targetIndex >= 0 && targetIndex < headerParams.length) {
88
+ dispatch(detailSetFocus(headerStartIndex + targetIndex));
89
+ dispatch(detailSetPanel('left'));
90
+ }
91
+ }
92
+ else if (type === 'rq') {
93
+ // Request Tabs
94
+ const tabs = ['headers', 'body', 'query'];
95
+ if (targetIndex >= 0 && targetIndex < tabs.length) {
96
+ dispatch(detailSetRequestTab(tabs[targetIndex]));
97
+ dispatch(detailSetPanel('request'));
98
+ }
99
+ }
100
+ else if (type === 'rb') {
101
+ // Request Body
102
+ const bodyStartIndex = pathParams.length + queryParams.length + headerParams.length;
103
+ // 만약 bodyParams가 있으면 그쪽으로, 아니면 requestBody(Raw)로
104
+ if (bodyParams.length > 0) {
105
+ if (targetIndex >= 0 && targetIndex < bodyParams.length) {
106
+ dispatch(detailSetFocus(bodyStartIndex + targetIndex));
107
+ dispatch(detailSetPanel('left'));
108
+ }
109
+ }
110
+ else if (endpoint?.requestBody) {
111
+ // Form Data가 있으면 그 뒤
112
+ const rawBodyIndex = bodyStartIndex + formDataParams.length;
113
+ if (targetIndex === 0) { // rb1 only for raw body
114
+ dispatch(detailSetFocus(rawBodyIndex));
115
+ dispatch(detailSetPanel('left'));
116
+ }
117
+ }
118
+ }
119
+ else if (type === 'rs') {
120
+ // Response Tabs
121
+ const tabs = [
122
+ 'body',
123
+ 'headers',
124
+ 'cookies',
125
+ 'curl',
126
+ ];
127
+ if (targetIndex >= 0 && targetIndex < tabs.length) {
128
+ dispatch(detailSetResponseTab(tabs[targetIndex]));
129
+ dispatch(detailSetPanel('response'));
130
+ }
131
+ }
132
+ dispatch(setMode('NORMAL'));
133
+ return;
134
+ }
135
+ // 2. 숫자만 있는 경우 (기존 동작 호환: 현재 포커스된 영역의 아이템 선택)
136
+ const index = parseInt(cmd, 10);
137
+ if (!isNaN(index)) {
138
+ dispatch(setMode('NORMAL'));
139
+ const targetIndex = index - 1;
140
+ if (activePanel === 'left') {
141
+ if (targetIndex >= 0 && targetIndex < totalFields) {
142
+ dispatch(detailSetFocus(targetIndex));
143
+ }
144
+ }
145
+ else if (activePanel === 'request') {
146
+ const tabs = ['headers', 'body', 'query'];
147
+ if (targetIndex >= 0 && targetIndex < tabs.length) {
148
+ dispatch(detailSetRequestTab(tabs[targetIndex]));
149
+ }
150
+ }
151
+ else if (activePanel === 'response') {
152
+ const tabs = [
153
+ 'body',
154
+ 'headers',
155
+ 'cookies',
156
+ 'curl',
157
+ ];
158
+ if (targetIndex >= 0 && targetIndex < tabs.length) {
159
+ dispatch(detailSetResponseTab(tabs[targetIndex]));
160
+ }
161
+ }
162
+ return;
163
+ }
164
+ dispatch(setMode('NORMAL'));
165
+ };
166
+ // 키보드 입력 처리 (NORMAL 모드)
167
+ useKeyPress({
168
+ j: () => {
169
+ if (activePanel === 'left' && totalFields > 0) {
170
+ // 다음 필드로
171
+ const nextIndex = Math.min(detail.focusedFieldIndex + 1, totalFields - 1);
172
+ dispatch(detailSetFocus(nextIndex));
173
+ }
174
+ },
175
+ k: () => {
176
+ if (activePanel === 'left' && totalFields > 0) {
177
+ // 이전 필드로
178
+ const prevIndex = Math.max(detail.focusedFieldIndex - 1, 0);
179
+ dispatch(detailSetFocus(prevIndex));
180
+ }
181
+ },
182
+ h: () => {
183
+ // 왼쪽 패널로 (어디서든)
184
+ dispatch(detailSetPanel('left'));
185
+ },
186
+ l: () => {
187
+ // 오른쪽(Request) 패널로
188
+ if (activePanel === 'left') {
189
+ dispatch(detailSetPanel('request'));
190
+ }
191
+ },
192
+ onTab: () => {
193
+ // 패널 순환: Left -> Request -> Response -> Left
194
+ if (activePanel === 'left') {
195
+ dispatch(detailSetPanel('request'));
196
+ }
197
+ else if (activePanel === 'request') {
198
+ dispatch(detailSetPanel('response'));
199
+ }
200
+ else {
201
+ dispatch(detailSetPanel('left'));
202
+ }
203
+ },
204
+ i: () => {
205
+ // INSERT 모드로 전환 (왼쪽 패널에서만)
206
+ if (activePanel === 'left') {
207
+ dispatch(setMode('INSERT'));
208
+ }
209
+ },
210
+ u: () => {
211
+ // 뒤로 가기
212
+ goBack();
213
+ },
214
+ onReturn: () => {
215
+ // 요청 실행
216
+ handleExecuteRequest();
217
+ },
218
+ ':': () => {
219
+ dispatch(setMode('COMMAND'));
220
+ },
221
+ }, mode === 'NORMAL');
222
+ // Command Mode Handler
223
+ useKeyPress({
224
+ onEscape: () => {
225
+ dispatch(setMode('NORMAL'));
226
+ dispatch(setCommandInput(''));
227
+ }
228
+ }, mode === 'COMMAND');
229
+ // INSERT 모드 키보드 입력 처리
230
+ useKeyPress({
231
+ onEscape: () => {
232
+ dispatch(setMode('NORMAL'));
233
+ },
234
+ }, mode === 'INSERT');
235
+ const keyBindings = mode === 'NORMAL'
236
+ ? [
237
+ { key: 'j/k', description: activePanel === 'left' ? 'Navigate' : 'Scroll' },
238
+ { key: 'h/l', description: 'Panel' },
239
+ { key: ':', description: 'Command' },
240
+ { key: 'Tab', description: 'Cycle Panel' },
241
+ { key: 'i', description: 'Insert' },
242
+ { key: 'Enter', description: 'Execute' },
243
+ { key: 'u', description: 'Back' },
244
+ ]
245
+ : [
246
+ { key: 'Tab', description: 'Autocomplete' },
247
+ { key: 'Type', description: 'Edit Value' },
248
+ { key: 'Esc', description: 'Normal Mode' },
249
+ ];
250
+ const baseURL = document ? getServerUrl(document) || '' : '';
251
+ const terminalHeight = stdout?.rows || 24;
252
+ // detail 또는 endpoint가 없는 경우 에러 표시
253
+ if (!detail || !endpoint) {
254
+ return (React.createElement(Box, { flexDirection: "column", height: terminalHeight },
255
+ React.createElement(StatusBar, { title: "API DETAIL", currentPath: "", mode: mode, status: "ERROR" }),
256
+ React.createElement(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center" },
257
+ React.createElement(Text, { color: "red" }, "\uC0C1\uC138 \uC815\uBCF4\uB97C \uD45C\uC2DC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.")),
258
+ React.createElement(KeybindingFooter, { bindings: [], mode: mode })));
259
+ }
260
+ return (React.createElement(Box, { flexDirection: "column", height: terminalHeight },
261
+ React.createElement(StatusBar, { title: "API DETAIL", currentPath: endpoint.path, mode: mode, status: loading ? 'LOADING...' : 'READY' }),
262
+ React.createElement(Box, { flexGrow: 1, overflow: "hidden" },
263
+ React.createElement(LeftPanel, { endpoint: endpoint, parameterValues: detail.parameterValues, focusedFieldIndex: detail.focusedFieldIndex, mode: mode, isFocused: activePanel === 'left', document: document, commandMode: mode === 'COMMAND', onParameterChange: (key, value) => dispatch(detailSetParam({ key, value })), onExitInsertMode: () => dispatch(setMode('NORMAL')) }),
264
+ React.createElement(RightPanel, { activeRequestTab: activeRequestTab, activeResponseTab: activeResponseTab, activePanel: activePanel, response: detail.response, endpoint: endpoint, parameterValues: detail.parameterValues, requestBody: detail.requestBody, requestHeaders: detail.headers, baseURL: baseURL, mode: mode, isLoading: loading, commandMode: mode === 'COMMAND' })),
265
+ React.createElement(KeybindingFooter, { bindings: keyBindings, mode: mode, commandInput: commandInput, onCommandChange: (value) => dispatch(setCommandInput(value)), onCommandSubmit: handleCommandSubmit })));
266
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * API Detail 좌측 패널 - 파라미터 입력
3
+ */
4
+ import React from 'react';
5
+ import { Endpoint, OpenAPIDocument } from '../../types/openapi.js';
6
+ import { AppMode } from '../../types/app-state.js';
7
+ interface LeftPanelProps {
8
+ endpoint: Endpoint;
9
+ parameterValues: Record<string, string>;
10
+ focusedFieldIndex: number;
11
+ mode: AppMode;
12
+ isFocused: boolean;
13
+ document: OpenAPIDocument;
14
+ commandMode?: boolean;
15
+ onParameterChange: (key: string, value: string) => void;
16
+ onExitInsertMode?: () => void;
17
+ }
18
+ export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex, mode, isFocused, document, commandMode, onParameterChange, onExitInsertMode, }: LeftPanelProps): React.JSX.Element;
19
+ export {};
@@ -0,0 +1,363 @@
1
+ /**
2
+ * API Detail 좌측 패널 - 파라미터 입력
3
+ */
4
+ import React, { useState, useEffect, useLayoutEffect, useMemo } from 'react';
5
+ import { Box, Text, useStdout } from 'ink';
6
+ import SearchInput from '../common/SearchInput.js';
7
+ import { isReferenceObject, } from '../../types/openapi.js';
8
+ import { useKeyPress } from '../../hooks/useKeyPress.js';
9
+ import { formatSchemaFull } from '../../utils/schema-formatter.js';
10
+ function getMethodColor(method) {
11
+ switch (method) {
12
+ case 'GET':
13
+ return 'blue';
14
+ case 'POST':
15
+ return 'green';
16
+ case 'PUT':
17
+ return 'yellow';
18
+ case 'DELETE':
19
+ return 'red';
20
+ case 'PATCH':
21
+ return 'magenta';
22
+ default:
23
+ return 'white';
24
+ }
25
+ }
26
+ export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex, mode, isFocused, document, commandMode = false, onParameterChange, onExitInsertMode, }) {
27
+ const { stdout } = useStdout();
28
+ const [localValue, setLocalValue] = useState('');
29
+ const [expandedSchemas, setExpandedSchemas] = useState(new Set());
30
+ const [schemaScrollOffset, setSchemaScrollOffset] = useState(0);
31
+ // Flat Items Memoization
32
+ const { flatItems, paramIndexToFlatIndex } = useMemo(() => {
33
+ // 파라미터 분류
34
+ const pathParams = endpoint.parameters.filter((p) => p.in === 'path');
35
+ const queryParams = endpoint.parameters.filter((p) => p.in === 'query');
36
+ const headerParams = endpoint.parameters.filter((p) => p.in === 'header');
37
+ const bodyParams = endpoint.parameters.filter((p) => p.in === 'body');
38
+ const formDataParams = endpoint.parameters.filter((p) => p.in === 'formData');
39
+ const hasRequestBody = endpoint.requestBody && bodyParams.length === 0;
40
+ const items = [];
41
+ const indexMap = [];
42
+ // 1. Title
43
+ items.push({ type: 'title', endpoint });
44
+ // Helper
45
+ const addParams = (params, sectionTitle, startIndex) => {
46
+ if (params.length > 0) {
47
+ items.push({ type: 'section', text: sectionTitle });
48
+ params.forEach((p, i) => {
49
+ const globalIndex = startIndex + i;
50
+ indexMap[globalIndex] = items.length;
51
+ items.push({ type: 'param', param: p, globalIndex });
52
+ });
53
+ }
54
+ };
55
+ let currentIndex = 0;
56
+ addParams(pathParams, '▸ Path Parameters', currentIndex);
57
+ currentIndex += pathParams.length;
58
+ addParams(queryParams, '▸ Query Parameters', currentIndex);
59
+ currentIndex += queryParams.length;
60
+ addParams(headerParams, '▸ Headers', currentIndex);
61
+ currentIndex += headerParams.length;
62
+ addParams(bodyParams, '▸ Request Body', currentIndex);
63
+ currentIndex += bodyParams.length;
64
+ addParams(formDataParams, '▸ Form Data', currentIndex);
65
+ currentIndex += formDataParams.length;
66
+ if (hasRequestBody) {
67
+ items.push({ type: 'section', text: '▸ Request Body' });
68
+ indexMap[currentIndex] = items.length;
69
+ items.push({ type: 'requestBody', endpoint, globalIndex: currentIndex });
70
+ }
71
+ return { flatItems: items, paramIndexToFlatIndex: indexMap };
72
+ }, [endpoint]);
73
+ // 스크롤 로직
74
+ const totalHeight = (stdout?.rows || 24) - 4;
75
+ const contentHeight = Math.max(totalHeight - 2, 1);
76
+ const visibleItemCount = Math.floor(contentHeight / 2); // 추정치
77
+ const [scrollOffset, setScrollOffset] = useState(0);
78
+ useLayoutEffect(() => {
79
+ const targetFlatIndex = paramIndexToFlatIndex[focusedFieldIndex];
80
+ if (targetFlatIndex === undefined)
81
+ return;
82
+ // 스크롤 위치 조정은 DOM 업데이트 직후에 실행되어야 하므로 useLayoutEffect 사용
83
+ // 이는 화면 흔들림을 방지하기 위한 필수적인 작업입니다
84
+ // eslint-disable-next-line react-hooks/set-state-in-effect
85
+ setScrollOffset((currentOffset) => {
86
+ // 현재 보이는 영역 내에 있으면 스크롤 변경하지 않음 (화면 흔들림 방지)
87
+ if (targetFlatIndex >= currentOffset && targetFlatIndex < currentOffset + visibleItemCount) {
88
+ return currentOffset;
89
+ }
90
+ // 포커스가 보이는 영역 밖에 있을 때만 스크롤 조정
91
+ if (targetFlatIndex < currentOffset) {
92
+ // 포커스가 위에 있으면 포커스를 상단에 맞춤
93
+ return Math.max(0, targetFlatIndex);
94
+ }
95
+ else if (targetFlatIndex >= currentOffset + visibleItemCount) {
96
+ // 포커스가 아래에 있으면 포커스를 하단에 맞춤
97
+ return Math.max(0, targetFlatIndex - visibleItemCount + 1);
98
+ }
99
+ return currentOffset;
100
+ });
101
+ }, [focusedFieldIndex, visibleItemCount, paramIndexToFlatIndex]);
102
+ // 포커스 변경시 로컬 값 업데이트
103
+ useEffect(() => {
104
+ const flatIndex = paramIndexToFlatIndex[focusedFieldIndex];
105
+ const item = flatItems[flatIndex];
106
+ if (item && item.type === 'param') {
107
+ // eslint-disable-next-line react-hooks/set-state-in-effect
108
+ setLocalValue(parameterValues[item.param.name] || '');
109
+ }
110
+ }, [focusedFieldIndex, parameterValues, paramIndexToFlatIndex, flatItems]);
111
+ // Handlers
112
+ const handleExpandSchema = () => {
113
+ if (!isFocused || mode !== 'NORMAL')
114
+ return;
115
+ const flatIndex = paramIndexToFlatIndex[focusedFieldIndex];
116
+ const item = flatItems[flatIndex];
117
+ if (item?.type === 'requestBody') {
118
+ setExpandedSchemas((prev) => {
119
+ const next = new Set(prev);
120
+ next.add('__requestBody__');
121
+ return next;
122
+ });
123
+ setSchemaScrollOffset(0);
124
+ }
125
+ else if (item?.type === 'param' &&
126
+ item.param.schema &&
127
+ isReferenceObject(item.param.schema)) {
128
+ setExpandedSchemas((prev) => {
129
+ const next = new Set(prev);
130
+ next.add(item.param.name);
131
+ return next;
132
+ });
133
+ setSchemaScrollOffset(0);
134
+ }
135
+ };
136
+ const handleCollapseSchema = () => {
137
+ if (!isFocused || mode !== 'NORMAL')
138
+ return;
139
+ const flatIndex = paramIndexToFlatIndex[focusedFieldIndex];
140
+ const item = flatItems[flatIndex];
141
+ if (item?.type === 'requestBody') {
142
+ setExpandedSchemas((prev) => {
143
+ const next = new Set(prev);
144
+ next.delete('__requestBody__');
145
+ return next;
146
+ });
147
+ }
148
+ else if (item?.type === 'param') {
149
+ setExpandedSchemas((prev) => {
150
+ const next = new Set(prev);
151
+ next.delete(item.param.name);
152
+ return next;
153
+ });
154
+ }
155
+ };
156
+ const handleSchemaScrollDown = () => {
157
+ if (!isFocused || mode !== 'NORMAL')
158
+ return;
159
+ const flatIndex = paramIndexToFlatIndex[focusedFieldIndex];
160
+ const item = flatItems[flatIndex];
161
+ const maxHeight = Math.floor((stdout?.rows || 24) * 0.3);
162
+ if (item?.type === 'requestBody' && expandedSchemas.has('__requestBody__')) {
163
+ if (endpoint.requestBody?.schema) {
164
+ const lines = formatSchemaFull(endpoint.requestBody.schema, document, 3);
165
+ setSchemaScrollOffset((prev) => Math.min(prev + 1, Math.max(0, lines.length - maxHeight)));
166
+ }
167
+ }
168
+ else if (item?.type === 'param' && expandedSchemas.has(item.param.name)) {
169
+ if (item.param.schema && isReferenceObject(item.param.schema)) {
170
+ const lines = formatSchemaFull(item.param.schema, document, 2);
171
+ setSchemaScrollOffset((prev) => Math.min(prev + 1, Math.max(0, lines.length - maxHeight)));
172
+ }
173
+ }
174
+ };
175
+ const handleSchemaScrollUp = () => {
176
+ setSchemaScrollOffset((prev) => Math.max(prev - 1, 0));
177
+ };
178
+ useKeyPress({
179
+ '>': handleExpandSchema,
180
+ '<': handleCollapseSchema,
181
+ j: handleSchemaScrollDown,
182
+ k: handleSchemaScrollUp,
183
+ }, mode === 'NORMAL' && isFocused);
184
+ // Render Helpers
185
+ const renderSchema = (ref) => {
186
+ const schema = { $ref: ref };
187
+ const lines = formatSchemaFull(schema, document, 2);
188
+ const maxHeight = Math.floor((stdout?.rows || 24) * 0.3);
189
+ const visibleLines = lines.slice(schemaScrollOffset, schemaScrollOffset + maxHeight);
190
+ return (React.createElement(Box, { marginTop: 1, borderStyle: "single", borderColor: "cyan", paddingX: 1, flexDirection: "column" },
191
+ visibleLines.map((line, idx) => (React.createElement(Text, { key: idx, color: "cyan", dimColor: true }, line))),
192
+ lines.length > maxHeight && (React.createElement(Text, { color: "gray", dimColor: true },
193
+ schemaScrollOffset + 1,
194
+ "-",
195
+ Math.min(schemaScrollOffset + maxHeight, lines.length),
196
+ " of",
197
+ ' ',
198
+ lines.length,
199
+ " lines"))));
200
+ };
201
+ const renderRequestBodySchema = () => {
202
+ if (!endpoint.requestBody?.schema)
203
+ return null;
204
+ const lines = formatSchemaFull(endpoint.requestBody.schema, document, 3);
205
+ const maxHeight = Math.floor((stdout?.rows || 24) * 0.3);
206
+ const visibleLines = lines.slice(schemaScrollOffset, schemaScrollOffset + maxHeight);
207
+ return (React.createElement(Box, { marginTop: 1, borderStyle: "single", borderColor: "cyan", paddingX: 1, flexDirection: "column" },
208
+ visibleLines.map((line, idx) => (React.createElement(Text, { key: idx, color: "cyan", dimColor: true }, line))),
209
+ lines.length > maxHeight && (React.createElement(Text, { color: "gray", dimColor: true },
210
+ schemaScrollOffset + 1,
211
+ "-",
212
+ Math.min(schemaScrollOffset + maxHeight, lines.length),
213
+ " of",
214
+ ' ',
215
+ lines.length,
216
+ " lines"))));
217
+ };
218
+ // Rendering
219
+ const visibleItems = flatItems.slice(scrollOffset, scrollOffset + visibleItemCount);
220
+ const pathParamCount = endpoint.parameters.filter((p) => p.in === 'path').length;
221
+ const headerParamCount = endpoint.parameters.filter((p) => p.in === 'header').length;
222
+ const queryParamCount = endpoint.parameters.filter((p) => p.in === 'query').length;
223
+ const bodyStartIndex = pathParamCount + queryParamCount + headerParamCount;
224
+ return (React.createElement(Box, { flexDirection: "column", width: "50%", height: totalHeight, borderStyle: "single", borderColor: isFocused ? 'green' : 'gray', paddingX: 1 }, visibleItems.map((item, idx) => {
225
+ const key = `item-${scrollOffset + idx}`;
226
+ if (item.type === 'title') {
227
+ return (React.createElement(Box, { key: key, flexDirection: "column", marginBottom: 1 },
228
+ React.createElement(Box, null,
229
+ React.createElement(Text, { bold: true, color: getMethodColor(item.endpoint.method) },
230
+ "[",
231
+ item.endpoint.method,
232
+ "]"),
233
+ React.createElement(Text, { bold: true, color: "green" },
234
+ ' ',
235
+ item.endpoint.path)),
236
+ (item.endpoint.summary || item.endpoint.description) && (React.createElement(Text, { dimColor: true }, item.endpoint.summary || item.endpoint.description))));
237
+ }
238
+ if (item.type === 'section') {
239
+ return (React.createElement(Box, { key: key, marginTop: 1, marginBottom: 0 },
240
+ React.createElement(Text, { bold: true, color: "green" }, item.text)));
241
+ }
242
+ if (item.type === 'param') {
243
+ const isFieldFocused = item.globalIndex === focusedFieldIndex && isFocused;
244
+ const value = parameterValues[item.param.name] || '';
245
+ let typeInfo = '';
246
+ let refInfo = '';
247
+ if (item.param.schema) {
248
+ if (isReferenceObject(item.param.schema)) {
249
+ refInfo = item.param.schema.$ref.split('/').pop() || item.param.schema.$ref;
250
+ typeInfo = 'object';
251
+ }
252
+ else {
253
+ const schemaType = item.param.schema.type;
254
+ typeInfo = Array.isArray(schemaType) ? schemaType.join(' | ') : schemaType || '';
255
+ }
256
+ }
257
+ const hasSchema = !!(refInfo &&
258
+ item.param.schema &&
259
+ isReferenceObject(item.param.schema));
260
+ const isExpanded = expandedSchemas.has(item.param.name);
261
+ // Command Hint Calculation
262
+ let commandHint = '';
263
+ if (commandMode) {
264
+ if (item.param.in === 'path') {
265
+ const ppIndex = item.globalIndex + 1;
266
+ commandHint = `pp${ppIndex}`;
267
+ }
268
+ else if (item.param.in === 'query') {
269
+ const qpIndex = item.globalIndex - pathParamCount + 1;
270
+ commandHint = `qp${qpIndex}`;
271
+ }
272
+ else if (item.param.in === 'header') {
273
+ const hIndex = item.globalIndex - (pathParamCount + queryParamCount) + 1;
274
+ commandHint = `h${hIndex}`;
275
+ }
276
+ else if (item.param.in === 'body') {
277
+ const rbIndex = item.globalIndex - bodyStartIndex + 1;
278
+ commandHint = `rb${rbIndex}`;
279
+ }
280
+ }
281
+ return (React.createElement(Box, { key: key, flexDirection: "column" },
282
+ React.createElement(Box, { marginLeft: 1, flexDirection: "row" },
283
+ React.createElement(Box, { flexDirection: "row" },
284
+ commandMode && (React.createElement(Box, { width: 6 },
285
+ React.createElement(Text, { color: "yellow" },
286
+ "[",
287
+ commandHint,
288
+ "]"))),
289
+ React.createElement(Text, { color: isFieldFocused ? 'green' : 'white', bold: isFieldFocused }, item.param.name),
290
+ React.createElement(Text, { color: "gray" },
291
+ " (",
292
+ item.param.in,
293
+ ")"),
294
+ item.param.required && React.createElement(Text, { color: "red" }, " *"),
295
+ typeInfo && React.createElement(Text, { color: "cyan" },
296
+ " : ",
297
+ typeInfo)),
298
+ React.createElement(Box, { marginLeft: 2, flexGrow: 1 }, mode === 'INSERT' && isFieldFocused ? (React.createElement(SearchInput, { value: localValue, onChange: (val) => {
299
+ setLocalValue(val);
300
+ onParameterChange(item.param.name, val);
301
+ }, suggestions: [], placeholder: "", focus: true, onSubmit: onExitInsertMode })) : (React.createElement(Text, { color: value ? 'white' : 'gray', backgroundColor: isFieldFocused ? 'green' : undefined, bold: isFieldFocused && !value }, value || (isFieldFocused ? ' (empty) ' : '(empty)'))))),
302
+ item.param.description && (React.createElement(Box, { marginLeft: 2, marginTop: 0 },
303
+ React.createElement(Text, { color: "gray", dimColor: true }, item.param.description))),
304
+ (() => {
305
+ if (!isFieldFocused)
306
+ return null;
307
+ let enumValues = [];
308
+ if (item.param.schema && !isReferenceObject(item.param.schema)) {
309
+ if (item.param.schema.enum) {
310
+ enumValues = item.param.schema.enum;
311
+ }
312
+ else if (item.param.schema.type === 'array' &&
313
+ item.param.schema.items &&
314
+ !isReferenceObject(item.param.schema.items) &&
315
+ item.param.schema.items.enum) {
316
+ enumValues = item.param.schema.items.enum;
317
+ }
318
+ }
319
+ if (enumValues.length > 0) {
320
+ return (React.createElement(Box, { marginLeft: 2, marginTop: 0, flexDirection: "column" },
321
+ React.createElement(Text, { color: "yellow", dimColor: true },
322
+ "Available values: ",
323
+ enumValues.join(', ')),
324
+ React.createElement(Text, { color: "gray", dimColor: true }, "(Tab to autocomplete)")));
325
+ }
326
+ return null;
327
+ })(),
328
+ hasSchema && !isExpanded && (React.createElement(Box, { marginLeft: 2, marginTop: 0 },
329
+ React.createElement(Text, { color: "yellow", dimColor: true },
330
+ "Press > to view ",
331
+ refInfo,
332
+ " schema"))),
333
+ hasSchema && isExpanded && isReferenceObject(item.param.schema) && (React.createElement(React.Fragment, null,
334
+ React.createElement(Box, { marginLeft: 2, marginTop: 0 },
335
+ React.createElement(Text, { color: "yellow", dimColor: true },
336
+ "Schema: ",
337
+ refInfo,
338
+ " (Press < to collapse)")),
339
+ renderSchema(item.param.schema.$ref)))));
340
+ }
341
+ if (item.type === 'requestBody') {
342
+ const isFieldFocused = item.globalIndex === focusedFieldIndex && isFocused;
343
+ const isExpanded = expandedSchemas.has('__requestBody__');
344
+ const hasSchema = !!endpoint.requestBody?.schema;
345
+ const commandHint = commandMode ? 'rb1' : '';
346
+ return (React.createElement(Box, { key: key, flexDirection: "column" },
347
+ React.createElement(Box, { flexDirection: "row", backgroundColor: isFieldFocused ? 'green' : undefined, marginLeft: 1 },
348
+ commandMode && (React.createElement(Box, { width: 6 },
349
+ React.createElement(Text, { color: "yellow" },
350
+ "[",
351
+ commandHint,
352
+ "]"))),
353
+ React.createElement(Text, { color: isFieldFocused ? 'black' : 'gray' }, item.endpoint.requestBody?.description || 'Request Body Content')),
354
+ hasSchema && !isExpanded && (React.createElement(Box, { marginLeft: 1, marginTop: 0 },
355
+ React.createElement(Text, { color: "yellow", dimColor: true }, "Press > to view schema details"))),
356
+ hasSchema && isExpanded && (React.createElement(React.Fragment, null,
357
+ React.createElement(Box, { marginLeft: 1, marginTop: 0 },
358
+ React.createElement(Text, { color: "yellow", dimColor: true }, "Schema (Press < to collapse)")),
359
+ renderRequestBodySchema()))));
360
+ }
361
+ return null;
362
+ })));
363
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 파라미터 입력 폼 컴포넌트
3
+ */
4
+ import React from 'react';
5
+ import { Endpoint } from '../../types/openapi.js';
6
+ import { AppMode } from '../../types/app-state.js';
7
+ interface ParameterFormProps {
8
+ endpoint: Endpoint;
9
+ parameterValues: Record<string, string>;
10
+ requestBody: string;
11
+ focusedFieldIndex: number;
12
+ mode: AppMode;
13
+ onParameterChange: (key: string, value: string) => void;
14
+ onBodyChange: (body: string) => void;
15
+ }
16
+ export default function ParameterForm({ endpoint, parameterValues, requestBody, focusedFieldIndex, mode, onParameterChange, onBodyChange, }: ParameterFormProps): React.JSX.Element;
17
+ export {};