@gomjellie/lazyapi 0.0.1 → 0.0.3

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.
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * API Detail 좌측 패널 - 파라미터 입력
3
3
  */
4
- import React, { useState, useEffect, useLayoutEffect, useMemo } from 'react';
4
+ import React, { useState, useEffect, useLayoutEffect, useMemo, useCallback } from 'react';
5
5
  import { Box, Text, useStdout } from 'ink';
6
6
  import SearchInput from '../common/SearchInput.js';
7
7
  import { isReferenceObject, } from '../../types/openapi.js';
8
8
  import { useKeyPress } from '../../hooks/useKeyPress.js';
9
9
  import { formatSchemaFull } from '../../utils/schema-formatter.js';
10
+ import { scaffoldSchema } from '../../utils/schema-scaffolder.js';
10
11
  function getMethodColor(method) {
11
12
  switch (method) {
12
13
  case 'GET':
@@ -23,11 +24,130 @@ function getMethodColor(method) {
23
24
  return 'white';
24
25
  }
25
26
  }
26
- export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex, mode, isFocused, document, commandMode = false, onParameterChange, onExitInsertMode, }) {
27
+ function getStatusColor(status) {
28
+ if (status.startsWith('2'))
29
+ return 'green';
30
+ if (status.startsWith('3'))
31
+ return 'blue';
32
+ if (status.startsWith('4'))
33
+ return 'yellow';
34
+ if (status.startsWith('5'))
35
+ return 'red';
36
+ return 'gray';
37
+ }
38
+ export default function LeftPanel({ endpoint, parameterValues, requestBodyValue, focusedFieldIndex, mode, isFocused, document, commandMode = false, onParameterChange, onExitInsertMode, onRequestBodyEdit, onSetMode, }) {
27
39
  const { stdout } = useStdout();
28
40
  const [localValue, setLocalValue] = useState('');
29
- const [expandedSchemas, setExpandedSchemas] = useState(new Set());
30
- const [schemaScrollOffset, setSchemaScrollOffset] = useState(0);
41
+ const [collapsedSchemas, setCollapsedSchemas] = useState(() => {
42
+ const initial = new Set();
43
+ endpoint.responses?.forEach((res) => {
44
+ initial.add(`res_${res.status}`);
45
+ });
46
+ return initial;
47
+ });
48
+ const [schemaScrollOffsets, setSchemaScrollOffsets] = useState({});
49
+ // 엔드포인트 변경 시 Response Schema들 다시 접기
50
+ useEffect(() => {
51
+ setCollapsedSchemas((prev) => {
52
+ const next = new Set(prev);
53
+ endpoint.responses?.forEach((res) => {
54
+ next.add(`res_${res.status}`);
55
+ });
56
+ return next;
57
+ });
58
+ }, [endpoint.responses]);
59
+ // Helper to calculate dynamic item height
60
+ const getItemHeight = useCallback((item) => {
61
+ let height = 0;
62
+ if (item.type === 'title') {
63
+ // Title height calculation with wrapping estimation
64
+ const panelWidth = Math.floor((stdout?.columns || 80) / 2) - 2; // 50% width - padding
65
+ const titleText = `[${item.endpoint.method}] ${item.endpoint.path}`;
66
+ const titleLines = Math.ceil(titleText.length / panelWidth) || 1;
67
+ height = titleLines;
68
+ if (item.endpoint.summary || item.endpoint.description) {
69
+ const descText = item.endpoint.summary || item.endpoint.description || '';
70
+ const descLines = Math.ceil(descText.length / panelWidth) || 1;
71
+ height += descLines;
72
+ }
73
+ // marginBottom 제거됨
74
+ }
75
+ else if (item.type === 'section') {
76
+ height = 1; // Title only (no margin)
77
+ }
78
+ else if (item.type === 'param') {
79
+ height = 1; // Basic row
80
+ if (item.param.description)
81
+ height += 1;
82
+ const isFieldFocused = item.globalIndex === focusedFieldIndex && isFocused;
83
+ if (isFieldFocused) {
84
+ // Enum hints
85
+ let enumValues = [];
86
+ if (item.param.schema && !isReferenceObject(item.param.schema)) {
87
+ if (item.param.schema.enum) {
88
+ enumValues = item.param.schema.enum;
89
+ }
90
+ else if (item.param.schema.type === 'array' &&
91
+ item.param.schema.items &&
92
+ !isReferenceObject(item.param.schema.items) &&
93
+ item.param.schema.items.enum) {
94
+ enumValues = item.param.schema.items.enum;
95
+ }
96
+ }
97
+ if (enumValues.length > 0)
98
+ height += 2;
99
+ }
100
+ // Schema expansion
101
+ const hasSchema = !!(item.param.schema && isReferenceObject(item.param.schema));
102
+ if (hasSchema) {
103
+ const isExpanded = !collapsedSchemas.has(item.param.name);
104
+ if (!isExpanded) {
105
+ height += 1; // "Press > to view..."
106
+ }
107
+ else {
108
+ // Schema box height calculation (title is now inside the box)
109
+ const lines = formatSchemaFull(item.param.schema, document, 10);
110
+ const maxHeight = Math.floor((stdout?.rows || 24) * 0.3);
111
+ const visibleLinesCount = Math.min(lines.length, maxHeight);
112
+ height += 1 + visibleLinesCount + (lines.length > maxHeight ? 1 : 0) + 2; // +1 for title, +2 for borders
113
+ }
114
+ }
115
+ }
116
+ else if (item.type === 'requestBody') {
117
+ height = 1; // Basic row
118
+ const hasSchema = !!endpoint.requestBody?.schema;
119
+ if (hasSchema) {
120
+ const isExpanded = !collapsedSchemas.has('__requestBody__');
121
+ if (!isExpanded) {
122
+ height += 1;
123
+ }
124
+ else {
125
+ const lines = formatSchemaFull(endpoint.requestBody.schema, document, 10);
126
+ const maxHeight = Math.floor((stdout?.rows || 24) * 0.3);
127
+ const visibleLinesCount = Math.min(lines.length, maxHeight);
128
+ height += 1 + visibleLinesCount + (lines.length > maxHeight ? 1 : 0) + 2; // +1 for title, +2 for borders
129
+ }
130
+ }
131
+ }
132
+ else if (item.type === 'responseItem') {
133
+ height = 1; // Basic row
134
+ const hasSchema = !!item.response.content?.[0]?.schema;
135
+ if (hasSchema) {
136
+ const resKey = `res_${item.response.status}`;
137
+ const isExpanded = !collapsedSchemas.has(resKey);
138
+ if (!isExpanded) {
139
+ height += 1;
140
+ }
141
+ else {
142
+ const lines = formatSchemaFull(item.response.content[0].schema, document, 10);
143
+ const maxHeight = Math.floor((stdout?.rows || 24) * 0.3);
144
+ const visibleLinesCount = Math.min(lines.length, maxHeight);
145
+ height += 1 + visibleLinesCount + (lines.length > maxHeight ? 1 : 0) + 2; // +1 for title, +2 for borders
146
+ }
147
+ }
148
+ }
149
+ return height;
150
+ }, [stdout, focusedFieldIndex, isFocused, collapsedSchemas, document, endpoint]);
31
151
  // Flat Items Memoization
32
152
  const { flatItems, paramIndexToFlatIndex } = useMemo(() => {
33
153
  // 파라미터 분류
@@ -67,44 +187,83 @@ export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex
67
187
  items.push({ type: 'section', text: '▸ Request Body' });
68
188
  indexMap[currentIndex] = items.length;
69
189
  items.push({ type: 'requestBody', endpoint, globalIndex: currentIndex });
190
+ currentIndex++;
191
+ }
192
+ // Responses 섹션 추가
193
+ if (endpoint.responses && endpoint.responses.length > 0) {
194
+ items.push({ type: 'section', text: '▸ Responses' });
195
+ endpoint.responses.forEach((res) => {
196
+ const globalIndex = currentIndex;
197
+ indexMap[globalIndex] = items.length;
198
+ items.push({ type: 'responseItem', response: res, globalIndex });
199
+ currentIndex++;
200
+ });
70
201
  }
71
202
  return { flatItems: items, paramIndexToFlatIndex: indexMap };
72
203
  }, [endpoint]);
73
204
  // 스크롤 로직
74
205
  const totalHeight = (stdout?.rows || 24) - 4;
75
206
  const contentHeight = Math.max(totalHeight - 2, 1);
76
- const visibleItemCount = Math.floor(contentHeight / 2); // 추정치
77
207
  const [scrollOffset, setScrollOffset] = useState(0);
208
+ // Dynamic Scroll Logic based on Item Heights
78
209
  useLayoutEffect(() => {
79
210
  const targetFlatIndex = paramIndexToFlatIndex[focusedFieldIndex];
80
211
  if (targetFlatIndex === undefined)
81
212
  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;
213
+ // 현재 포커스된 아이템의 위치와 높이를 파악해야 함.
214
+ // 하지만 전체 아이템의 높이를 계산하는 것은 비효율적일 수 있으나, 아이템 수가 많지 않으므로 가능함.
215
+ // 여기서는 간단히 scrollOffset을 조정하여 targetItem이 화면 내에 들어오도록 함.
216
+ setScrollOffset((_currentOffset) => {
217
+ // 0. Smart Scroll: 최상단(0)부터 현재 타겟까지 보여줄 있으면 무조건 0 반환
218
+ let heightFromTop = 0;
219
+ for (let i = 0; i <= targetFlatIndex; i++) {
220
+ heightFromTop += getItemHeight(flatItems[i]);
221
+ }
222
+ if (heightFromTop <= contentHeight) {
223
+ return 0;
89
224
  }
90
- // 포커스가 보이는 영역 밖에 있을 때만 스크롤 조정
91
- if (targetFlatIndex < currentOffset) {
92
- // 포커스가 위에 있으면 포커스를 상단에 맞춤
93
- return Math.max(0, targetFlatIndex);
225
+ // 1. 타겟 아이템이 현재 오프셋보다 위에 있으면 타겟을 맨 위로 (즉시 위로 스크롤)
226
+ if (targetFlatIndex < _currentOffset) {
227
+ return targetFlatIndex;
94
228
  }
95
- else if (targetFlatIndex >= currentOffset + visibleItemCount) {
96
- // 포커스가 아래에 있으면 포커스를 하단에 맞춤
97
- return Math.max(0, targetFlatIndex - visibleItemCount + 1);
229
+ // 2. 타겟 아이템이 현재 화면 범위를 넘어 아래에 있는 경우
230
+ // 타겟의 '바닥'이 화면 맨 아래에 닿을 정도까지만 최소한으로 스크롤 내림
231
+ // 이렇게 하면 타겟 위쪽의 컨텐츠가 최대한 많이 남음
232
+ let currentViewHeight = 0;
233
+ for (let i = _currentOffset; i <= targetFlatIndex; i++) {
234
+ currentViewHeight += getItemHeight(flatItems[i]);
235
+ }
236
+ if (currentViewHeight > contentHeight) {
237
+ // 화면을 넘침 -> 타겟이 맨 아래에 오도록 하는 새로운 시작점(S) 계산
238
+ let acc = 0;
239
+ for (let i = targetFlatIndex; i >= 0; i--) {
240
+ const h = getItemHeight(flatItems[i]);
241
+ if (acc + h > contentHeight) {
242
+ // 이 아이템(i)을 넣으면 화면을 넘어가므로 i+1이 최적의 시작점
243
+ return i + 1;
244
+ }
245
+ acc += h;
246
+ }
247
+ return 0;
98
248
  }
99
- return currentOffset;
249
+ // 이미 화면 안에 잘 있으면 현재 오프셋 유지
250
+ return _currentOffset;
100
251
  });
101
- }, [focusedFieldIndex, visibleItemCount, paramIndexToFlatIndex]);
252
+ }, [
253
+ focusedFieldIndex,
254
+ contentHeight,
255
+ paramIndexToFlatIndex,
256
+ flatItems,
257
+ collapsedSchemas,
258
+ isFocused,
259
+ localValue,
260
+ getItemHeight,
261
+ ]);
102
262
  // 포커스 변경시 로컬 값 업데이트
103
263
  useEffect(() => {
104
264
  const flatIndex = paramIndexToFlatIndex[focusedFieldIndex];
105
265
  const item = flatItems[flatIndex];
106
266
  if (item && item.type === 'param') {
107
- // eslint-disable-next-line react-hooks/set-state-in-effect
108
267
  setLocalValue(parameterValues[item.param.name] || '');
109
268
  }
110
269
  }, [focusedFieldIndex, parameterValues, paramIndexToFlatIndex, flatItems]);
@@ -115,22 +274,31 @@ export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex
115
274
  const flatIndex = paramIndexToFlatIndex[focusedFieldIndex];
116
275
  const item = flatItems[flatIndex];
117
276
  if (item?.type === 'requestBody') {
118
- setExpandedSchemas((prev) => {
277
+ setCollapsedSchemas((prev) => {
119
278
  const next = new Set(prev);
120
- next.add('__requestBody__');
279
+ next.delete('__requestBody__');
121
280
  return next;
122
281
  });
123
- setSchemaScrollOffset(0);
282
+ setSchemaScrollOffsets((prev) => ({ ...prev, __requestBody__: 0 }));
124
283
  }
125
284
  else if (item?.type === 'param' &&
126
285
  item.param.schema &&
127
286
  isReferenceObject(item.param.schema)) {
128
- setExpandedSchemas((prev) => {
287
+ setCollapsedSchemas((prev) => {
129
288
  const next = new Set(prev);
130
- next.add(item.param.name);
289
+ next.delete(item.param.name);
131
290
  return next;
132
291
  });
133
- setSchemaScrollOffset(0);
292
+ setSchemaScrollOffsets((prev) => ({ ...prev, [item.param.name]: 0 }));
293
+ }
294
+ else if (item?.type === 'responseItem') {
295
+ const resKey = `res_${item.response.status}`;
296
+ setCollapsedSchemas((prev) => {
297
+ const next = new Set(prev);
298
+ next.delete(resKey);
299
+ return next;
300
+ });
301
+ setSchemaScrollOffsets((prev) => ({ ...prev, [resKey]: 0 }));
134
302
  }
135
303
  };
136
304
  const handleCollapseSchema = () => {
@@ -139,16 +307,24 @@ export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex
139
307
  const flatIndex = paramIndexToFlatIndex[focusedFieldIndex];
140
308
  const item = flatItems[flatIndex];
141
309
  if (item?.type === 'requestBody') {
142
- setExpandedSchemas((prev) => {
310
+ setCollapsedSchemas((prev) => {
143
311
  const next = new Set(prev);
144
- next.delete('__requestBody__');
312
+ next.add('__requestBody__');
145
313
  return next;
146
314
  });
147
315
  }
148
316
  else if (item?.type === 'param') {
149
- setExpandedSchemas((prev) => {
317
+ setCollapsedSchemas((prev) => {
150
318
  const next = new Set(prev);
151
- next.delete(item.param.name);
319
+ next.add(item.param.name);
320
+ return next;
321
+ });
322
+ }
323
+ else if (item?.type === 'responseItem') {
324
+ const resKey = `res_${item.response.status}`;
325
+ setCollapsedSchemas((prev) => {
326
+ const next = new Set(prev);
327
+ next.add(resKey);
152
328
  return next;
153
329
  });
154
330
  }
@@ -159,64 +335,171 @@ export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex
159
335
  const flatIndex = paramIndexToFlatIndex[focusedFieldIndex];
160
336
  const item = flatItems[flatIndex];
161
337
  const maxHeight = Math.floor((stdout?.rows || 24) * 0.3);
162
- if (item?.type === 'requestBody' && expandedSchemas.has('__requestBody__')) {
338
+ if (item?.type === 'requestBody' && !collapsedSchemas.has('__requestBody__')) {
163
339
  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)));
340
+ const lines = formatSchemaFull(endpoint.requestBody.schema, document, 10);
341
+ setSchemaScrollOffsets((prev) => {
342
+ const current = prev.__requestBody__ || 0;
343
+ return {
344
+ ...prev,
345
+ __requestBody__: Math.min(current + 1, Math.max(0, lines.length - maxHeight)),
346
+ };
347
+ });
166
348
  }
167
349
  }
168
- else if (item?.type === 'param' && expandedSchemas.has(item.param.name)) {
350
+ else if (item?.type === 'param' && !collapsedSchemas.has(item.param.name)) {
169
351
  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)));
352
+ const lines = formatSchemaFull(item.param.schema, document, 10);
353
+ setSchemaScrollOffsets((prev) => {
354
+ const current = prev[item.param.name] || 0;
355
+ return {
356
+ ...prev,
357
+ [item.param.name]: Math.min(current + 1, Math.max(0, lines.length - maxHeight)),
358
+ };
359
+ });
360
+ }
361
+ }
362
+ else if (item?.type === 'responseItem') {
363
+ const resKey = `res_${item.response.status}`;
364
+ if (!collapsedSchemas.has(resKey)) {
365
+ const schema = item.response.content?.[0]?.schema;
366
+ if (schema) {
367
+ const lines = formatSchemaFull(schema, document, 10);
368
+ setSchemaScrollOffsets((prev) => {
369
+ const current = prev[resKey] || 0;
370
+ return {
371
+ ...prev,
372
+ [resKey]: Math.min(current + 1, Math.max(0, lines.length - maxHeight)),
373
+ };
374
+ });
375
+ }
172
376
  }
173
377
  }
174
378
  };
175
379
  const handleSchemaScrollUp = () => {
176
- setSchemaScrollOffset((prev) => Math.max(prev - 1, 0));
380
+ if (!isFocused || mode !== 'NORMAL')
381
+ return;
382
+ const flatIndex = paramIndexToFlatIndex[focusedFieldIndex];
383
+ const item = flatItems[flatIndex];
384
+ if (item?.type === 'requestBody') {
385
+ setSchemaScrollOffsets((prev) => {
386
+ const current = prev.__requestBody__ || 0;
387
+ return { ...prev, __requestBody__: Math.max(current - 1, 0) };
388
+ });
389
+ }
390
+ else if (item?.type === 'param') {
391
+ setSchemaScrollOffsets((prev) => {
392
+ const current = prev[item.param.name] || 0;
393
+ return { ...prev, [item.param.name]: Math.max(current - 1, 0) };
394
+ });
395
+ }
396
+ else if (item?.type === 'responseItem') {
397
+ const resKey = `res_${item.response.status}`;
398
+ setSchemaScrollOffsets((prev) => {
399
+ const current = prev[resKey] || 0;
400
+ return { ...prev, [resKey]: Math.max(current - 1, 0) };
401
+ });
402
+ }
403
+ };
404
+ const handleEnterInsertMode = () => {
405
+ if (!isFocused || mode !== 'NORMAL')
406
+ return;
407
+ const flatIndex = paramIndexToFlatIndex[focusedFieldIndex];
408
+ const item = flatItems[flatIndex];
409
+ const isBodyParam = item?.type === 'requestBody' || (item?.type === 'param' && item.param.in === 'body');
410
+ if (isBodyParam) {
411
+ // 1. Request Body인 경우 (OpenAPI 3.0 requestBody OR Swagger 2.0 body param)
412
+ if (onRequestBodyEdit && onSetMode) {
413
+ // 값이 없으면 스키마 기반 생성
414
+ let valueToEdit = requestBodyValue || '';
415
+ const schema = item.type === 'requestBody' ? endpoint.requestBody?.schema : item.param.schema;
416
+ // 빈 문자열이거나 공백만 있는 경우에도 스캐폴딩 적용
417
+ if ((!valueToEdit || valueToEdit.trim() === '') && schema) {
418
+ const scaffolded = scaffoldSchema(schema, document);
419
+ valueToEdit = JSON.stringify(scaffolded, null, 2);
420
+ }
421
+ onRequestBodyEdit(valueToEdit);
422
+ onSetMode('INSERT');
423
+ }
424
+ }
425
+ else if (item?.type === 'param') {
426
+ // 2. 일반 파라미터인 경우
427
+ if (onSetMode) {
428
+ onSetMode('INSERT');
429
+ }
430
+ }
177
431
  };
178
432
  useKeyPress({
179
433
  '>': handleExpandSchema,
180
434
  '<': handleCollapseSchema,
181
435
  j: handleSchemaScrollDown,
182
436
  k: handleSchemaScrollUp,
437
+ i: handleEnterInsertMode,
183
438
  }, mode === 'NORMAL' && isFocused);
184
439
  // Render Helpers
185
- const renderSchema = (ref) => {
186
- const schema = { $ref: ref };
187
- const lines = formatSchemaFull(schema, document, 2);
440
+ const getSchemaName = (schema) => {
441
+ if (!schema)
442
+ return null;
443
+ if (isReferenceObject(schema)) {
444
+ return schema.$ref.split('/').pop() || null;
445
+ }
446
+ return null;
447
+ };
448
+ const renderSchemaBox = (lines, scrollKey) => {
449
+ const offset = schemaScrollOffsets[scrollKey] || 0;
188
450
  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" },
451
+ const visibleLines = lines.slice(offset, offset + maxHeight);
452
+ return (React.createElement(Box, { marginTop: 0, borderStyle: "single", borderColor: "cyan", paddingX: 1, flexDirection: "column" },
191
453
  visibleLines.map((line, idx) => (React.createElement(Text, { key: idx, color: "cyan", dimColor: true }, line))),
192
454
  lines.length > maxHeight && (React.createElement(Text, { color: "gray", dimColor: true },
193
- schemaScrollOffset + 1,
455
+ offset + 1,
194
456
  "-",
195
- Math.min(schemaScrollOffset + maxHeight, lines.length),
196
- " of",
197
- ' ',
457
+ Math.min(offset + maxHeight, lines.length),
458
+ " of ",
198
459
  lines.length,
199
460
  " lines"))));
200
461
  };
462
+ const renderSchema = (ref, paramName) => {
463
+ const schema = { $ref: ref };
464
+ const lines = formatSchemaFull(schema, document, 10);
465
+ return renderSchemaBox(lines, paramName);
466
+ };
201
467
  const renderRequestBodySchema = () => {
202
468
  if (!endpoint.requestBody?.schema)
203
469
  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"))));
470
+ const lines = formatSchemaFull(endpoint.requestBody.schema, document, 10);
471
+ return renderSchemaBox(lines, '__requestBody__');
472
+ };
473
+ const renderResponseSchema = (response) => {
474
+ const schema = response.content?.[0]?.schema;
475
+ if (!schema)
476
+ return null;
477
+ const lines = formatSchemaFull(schema, document, 10);
478
+ const resKey = `res_${response.status}`;
479
+ return renderSchemaBox(lines, resKey);
217
480
  };
218
481
  // Rendering
219
- const visibleItems = flatItems.slice(scrollOffset, scrollOffset + visibleItemCount);
482
+ const getVisibleItemsRange = () => {
483
+ let currentHeight = 0;
484
+ let startIndex = scrollOffset;
485
+ let count = 0;
486
+ // Adjust scrollOffset if focused item is out of view (basic check)
487
+ // This is handled by useLayoutEffect, but we rely on scrollOffset here.
488
+ for (let i = startIndex; i < flatItems.length; i++) {
489
+ const itemHeight = getItemHeight(flatItems[i]);
490
+ if (currentHeight + itemHeight > contentHeight) {
491
+ // If the first item itself is larger than contentHeight, we must show it (clipped)
492
+ if (count === 0) {
493
+ count = 1;
494
+ }
495
+ break;
496
+ }
497
+ currentHeight += itemHeight;
498
+ count++;
499
+ }
500
+ return flatItems.slice(startIndex, startIndex + count);
501
+ };
502
+ const visibleItems = getVisibleItemsRange();
220
503
  const pathParamCount = endpoint.parameters.filter((p) => p.in === 'path').length;
221
504
  const headerParamCount = endpoint.parameters.filter((p) => p.in === 'header').length;
222
505
  const queryParamCount = endpoint.parameters.filter((p) => p.in === 'query').length;
@@ -224,7 +507,7 @@ export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex
224
507
  return (React.createElement(Box, { flexDirection: "column", width: "50%", height: totalHeight, borderStyle: "single", borderColor: isFocused ? 'green' : 'gray', paddingX: 1 }, visibleItems.map((item, idx) => {
225
508
  const key = `item-${scrollOffset + idx}`;
226
509
  if (item.type === 'title') {
227
- return (React.createElement(Box, { key: key, flexDirection: "column", marginBottom: 1 },
510
+ return (React.createElement(Box, { key: key, flexDirection: "column", marginBottom: 0 },
228
511
  React.createElement(Box, null,
229
512
  React.createElement(Text, { bold: true, color: getMethodColor(item.endpoint.method) },
230
513
  "[",
@@ -236,7 +519,7 @@ export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex
236
519
  (item.endpoint.summary || item.endpoint.description) && (React.createElement(Text, { dimColor: true }, item.endpoint.summary || item.endpoint.description))));
237
520
  }
238
521
  if (item.type === 'section') {
239
- return (React.createElement(Box, { key: key, marginTop: 1, marginBottom: 0 },
522
+ return (React.createElement(Box, { key: key, marginTop: 0, marginBottom: 0 },
240
523
  React.createElement(Text, { bold: true, color: "green" }, item.text)));
241
524
  }
242
525
  if (item.type === 'param') {
@@ -257,7 +540,7 @@ export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex
257
540
  const hasSchema = !!(refInfo &&
258
541
  item.param.schema &&
259
542
  isReferenceObject(item.param.schema));
260
- const isExpanded = expandedSchemas.has(item.param.name);
543
+ const isExpanded = !collapsedSchemas.has(item.param.name);
261
544
  // Command Hint Calculation
262
545
  let commandHint = '';
263
546
  if (commandMode) {
@@ -336,27 +619,74 @@ export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex
336
619
  "Schema: ",
337
620
  refInfo,
338
621
  " (Press < to collapse)")),
339
- renderSchema(item.param.schema.$ref)))));
622
+ React.createElement(Box, { marginLeft: 2 }, renderSchema(item.param.schema.$ref, item.param.name))))));
340
623
  }
341
624
  if (item.type === 'requestBody') {
342
625
  const isFieldFocused = item.globalIndex === focusedFieldIndex && isFocused;
343
- const isExpanded = expandedSchemas.has('__requestBody__');
626
+ const isExpanded = !collapsedSchemas.has('__requestBody__');
344
627
  const hasSchema = !!endpoint.requestBody?.schema;
345
- const commandHint = commandMode ? 'rb1' : '';
628
+ const commandHint = commandMode ? 'rqb1' : '';
346
629
  return (React.createElement(Box, { key: key, flexDirection: "column" },
347
630
  React.createElement(Box, { flexDirection: "row", backgroundColor: isFieldFocused ? 'green' : undefined, marginLeft: 1 },
348
- commandMode && (React.createElement(Box, { width: 6 },
631
+ commandMode && (React.createElement(Box, { width: 8 },
349
632
  React.createElement(Text, { color: "yellow" },
350
633
  "[",
351
634
  commandHint,
352
635
  "]"))),
353
636
  React.createElement(Text, { color: isFieldFocused ? 'black' : 'gray' }, item.endpoint.requestBody?.description || 'Request Body Content')),
354
637
  hasSchema && !isExpanded && (React.createElement(Box, { marginLeft: 1, marginTop: 0 },
355
- React.createElement(Text, { color: "yellow", dimColor: true }, "Press > to view schema details"))),
638
+ React.createElement(Text, { color: "yellow", dimColor: true },
639
+ "Press > to view ",
640
+ getSchemaName(endpoint.requestBody?.schema) || 'schema',
641
+ ' ',
642
+ "schema"))),
643
+ hasSchema && isExpanded && (React.createElement(React.Fragment, null,
644
+ React.createElement(Box, { marginLeft: 1, marginTop: 0 },
645
+ React.createElement(Text, { color: "yellow", dimColor: true },
646
+ "Schema: ",
647
+ getSchemaName(endpoint.requestBody?.schema) || 'unknown',
648
+ " (Press < to collapse)")),
649
+ React.createElement(Box, { marginLeft: 1 }, renderRequestBodySchema())))));
650
+ }
651
+ if (item.type === 'responseItem') {
652
+ const isFieldFocused = item.globalIndex === focusedFieldIndex && isFocused;
653
+ const resKey = `res_${item.response.status}`;
654
+ const isExpanded = !collapsedSchemas.has(resKey);
655
+ const hasSchema = !!item.response.content?.[0]?.schema;
656
+ // Responses 시작 위치 계산 (힌트용)
657
+ const responsesStartIndex = pathParamCount +
658
+ queryParamCount +
659
+ headerParamCount +
660
+ endpoint.parameters.filter((p) => p.in === 'body' || p.in === 'formData').length +
661
+ (endpoint.requestBody && endpoint.parameters.filter((p) => p.in === 'body').length === 0
662
+ ? 1
663
+ : 0);
664
+ const rbIndex = item.globalIndex - responsesStartIndex + 1;
665
+ return (React.createElement(Box, { key: key, flexDirection: "column" },
666
+ React.createElement(Box, { flexDirection: "row", backgroundColor: isFieldFocused ? 'green' : undefined, marginLeft: 1 },
667
+ commandMode && (React.createElement(Box, { width: 8 },
668
+ React.createElement(Text, { color: "yellow" },
669
+ "[",
670
+ `rb${rbIndex}`,
671
+ "]"))),
672
+ React.createElement(Text, { color: isFieldFocused ? 'black' : getStatusColor(item.response.status), bold: true }, item.response.status),
673
+ React.createElement(Text, { color: isFieldFocused ? 'black' : 'white' },
674
+ ' ',
675
+ item.response.description || '(No description)')),
676
+ hasSchema && !isExpanded && (React.createElement(Box, { marginLeft: 1, marginTop: 0 },
677
+ React.createElement(Text, { color: "yellow", dimColor: true },
678
+ "Press > to view",
679
+ ' ',
680
+ getSchemaName(item.response.content?.[0]?.schema) || 'schema',
681
+ " schema"))),
356
682
  hasSchema && isExpanded && (React.createElement(React.Fragment, null,
357
683
  React.createElement(Box, { marginLeft: 1, marginTop: 0 },
358
- React.createElement(Text, { color: "yellow", dimColor: true }, "Schema (Press < to collapse)")),
359
- renderRequestBodySchema()))));
684
+ React.createElement(Text, { color: "yellow", dimColor: true },
685
+ "Schema: ",
686
+ getSchemaName(item.response.content?.[0]?.schema) || 'unknown',
687
+ ' ',
688
+ "(Press < to collapse)")),
689
+ React.createElement(Box, { marginLeft: 1 }, renderResponseSchema(item.response))))));
360
690
  }
361
691
  return null;
362
692
  })));
@@ -3,13 +3,14 @@
3
3
  */
4
4
  import React from 'react';
5
5
  import { ApiResponse, AppMode } from '../../types/app-state.js';
6
- import { Endpoint } from '../../types/openapi.js';
6
+ import { Endpoint, OpenAPIDocument } from '../../types/openapi.js';
7
7
  interface RightPanelProps {
8
8
  activeRequestTab: 'headers' | 'body' | 'query';
9
9
  activeResponseTab: 'body' | 'headers' | 'cookies' | 'curl';
10
10
  activePanel: 'left' | 'request' | 'response';
11
11
  response: ApiResponse | null;
12
12
  endpoint: Endpoint;
13
+ document?: OpenAPIDocument;
13
14
  parameterValues: Record<string, string>;
14
15
  requestBody: string;
15
16
  requestHeaders: Record<string, string>;
@@ -18,5 +19,5 @@ interface RightPanelProps {
18
19
  isLoading: boolean;
19
20
  commandMode?: boolean;
20
21
  }
21
- export default function RightPanel({ activeRequestTab, activeResponseTab, activePanel, response, endpoint, parameterValues, requestBody, requestHeaders, baseURL, mode, isLoading, commandMode, }: RightPanelProps): React.JSX.Element;
22
+ export default function RightPanel({ activeRequestTab, activeResponseTab, activePanel, response, endpoint, document, parameterValues, requestBody, requestHeaders, baseURL, mode, isLoading, commandMode, }: RightPanelProps): React.JSX.Element;
22
23
  export {};