@raystack/chronicle 0.7.4 → 0.9.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.
Files changed (50) hide show
  1. package/dist/cli/index.js +14 -2
  2. package/package.json +3 -4
  3. package/src/components/api/api-code-snippet.module.css +23 -0
  4. package/src/components/api/api-code-snippet.tsx +64 -0
  5. package/src/components/api/api-field-list.module.css +76 -0
  6. package/src/components/api/api-field-list.tsx +91 -0
  7. package/src/components/api/api-overview.module.css +65 -0
  8. package/src/components/api/api-overview.tsx +216 -0
  9. package/src/components/api/api-response-panel.module.css +62 -0
  10. package/src/components/api/api-response-panel.tsx +54 -0
  11. package/src/components/api/index.ts +5 -6
  12. package/src/components/api/json-editor.tsx +8 -8
  13. package/src/components/api/method-badge.tsx +2 -2
  14. package/src/components/api/playground-dialog.module.css +342 -0
  15. package/src/components/api/playground-dialog.tsx +583 -0
  16. package/src/components/ui/search.module.css +27 -5
  17. package/src/components/ui/search.tsx +28 -19
  18. package/src/lib/api-routes.ts +37 -8
  19. package/src/lib/openapi.ts +26 -0
  20. package/src/lib/page-context.tsx +1 -1
  21. package/src/lib/schema.ts +45 -3
  22. package/src/lib/source.ts +79 -13
  23. package/src/lib/use-api-operation.ts +15 -0
  24. package/src/pages/ApiLayout.module.css +1 -0
  25. package/src/pages/ApiPage.tsx +7 -38
  26. package/src/pages/DocsPage.tsx +40 -1
  27. package/src/server/api/apis-proxy.ts +8 -1
  28. package/src/server/api/ready.ts +15 -0
  29. package/src/server/api/search.ts +159 -85
  30. package/src/server/entry-server.tsx +1 -1
  31. package/src/server/routes/[...slug].md.ts +1 -0
  32. package/src/server/routes/apis/[...slug].md.ts +181 -0
  33. package/src/server/vite-config.ts +11 -0
  34. package/src/themes/default/Layout.module.css +53 -0
  35. package/src/themes/default/Layout.tsx +162 -11
  36. package/src/themes/default/Page.module.css +4 -0
  37. package/src/themes/default/Page.tsx +6 -1
  38. package/src/types/config.ts +2 -1
  39. package/src/components/api/code-snippets.module.css +0 -7
  40. package/src/components/api/code-snippets.tsx +0 -76
  41. package/src/components/api/endpoint-page.module.css +0 -58
  42. package/src/components/api/endpoint-page.tsx +0 -283
  43. package/src/components/api/field-row.module.css +0 -126
  44. package/src/components/api/field-row.tsx +0 -204
  45. package/src/components/api/field-section.module.css +0 -24
  46. package/src/components/api/field-section.tsx +0 -100
  47. package/src/components/api/key-value-editor.module.css +0 -13
  48. package/src/components/api/key-value-editor.tsx +0 -62
  49. package/src/components/api/response-panel.module.css +0 -8
  50. package/src/components/api/response-panel.tsx +0 -44
@@ -0,0 +1,583 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback, useMemo } from 'react'
4
+ import type { OpenAPIV3 } from 'openapi-types'
5
+ import { Dialog, Button, Badge, IconButton, InputField, CopyButton, Select, Menu } from '@raystack/apsara'
6
+ import { Cross2Icon, ChevronDownIcon, ChevronUpIcon, PlayIcon, PlusIcon } from '@radix-ui/react-icons'
7
+ import { CounterClockwiseClockIcon, CodeIcon } from '@radix-ui/react-icons'
8
+ import { MethodBadge } from '@/components/api/method-badge'
9
+ import { flattenSchema, generateExampleJson, toKind, type SchemaField } from '@/lib/schema'
10
+ import { generateCurl } from '@/lib/snippet-generators'
11
+ import { JsonEditor } from '@/components/api/json-editor'
12
+ import styles from './playground-dialog.module.css'
13
+
14
+ type AuthScheme = {
15
+ name: string
16
+ type: 'apiKey' | 'bearer' | 'basic' | 'none'
17
+ headerName: string
18
+ placeholder: string
19
+ }
20
+
21
+ function getAuthSchemes(
22
+ document: OpenAPIV3.Document,
23
+ auth?: { type: string; header: string; placeholder?: string }
24
+ ): AuthScheme[] {
25
+ const schemes: AuthScheme[] = [{ name: 'None', type: 'none', headerName: '', placeholder: '' }]
26
+ const securitySchemes = (document.components?.securitySchemes ?? {}) as Record<string, OpenAPIV3.SecuritySchemeObject>
27
+
28
+ for (const [name, scheme] of Object.entries(securitySchemes)) {
29
+ if (scheme.type === 'apiKey' && 'name' in scheme && 'in' in scheme && scheme.in === 'header') {
30
+ schemes.push({ name: `API Key (${scheme.name ?? name})`, type: 'apiKey', headerName: scheme.name ?? name, placeholder: 'Enter API key' })
31
+ } else if (scheme.type === 'http' && 'scheme' in scheme) {
32
+ if (scheme.scheme === 'bearer') {
33
+ schemes.push({ name: 'Bearer Token', type: 'bearer', headerName: 'Authorization', placeholder: 'Enter bearer token' })
34
+ } else if (scheme.scheme === 'basic') {
35
+ schemes.push({ name: 'Basic Auth', type: 'basic', headerName: 'Authorization', placeholder: '' })
36
+ }
37
+ }
38
+ }
39
+
40
+ if (auth && !schemes.some((s) => s.headerName === auth.header && s.type !== 'none')) {
41
+ schemes.push({ name: `API Key (${auth.header})`, type: 'apiKey', headerName: auth.header, placeholder: auth.placeholder ?? 'Enter API key' })
42
+ }
43
+
44
+ return schemes
45
+ }
46
+
47
+ interface PlaygroundDialogProps {
48
+ open: boolean
49
+ onOpenChange: (open: boolean) => void
50
+ method: string
51
+ path: string
52
+ operation: OpenAPIV3.OperationObject
53
+ serverUrl: string
54
+ specName: string
55
+ auth?: { type: string; header: string; placeholder?: string }
56
+ document: OpenAPIV3.Document
57
+ }
58
+
59
+ export function PlaygroundDialog({
60
+ open, onOpenChange, method, path, operation, serverUrl, specName, auth, document,
61
+ }: PlaygroundDialogProps) {
62
+ const params = (operation.parameters ?? []) as OpenAPIV3.ParameterObject[]
63
+ const body = getRequestBody(operation.requestBody as OpenAPIV3.RequestBodyObject | undefined)
64
+
65
+ const headerFields = paramsToFields(params.filter((p) => p.in === 'header'))
66
+ const pathFields = paramsToFields(params.filter((p) => p.in === 'path'))
67
+ const queryFields = paramsToFields(params.filter((p) => p.in === 'query'))
68
+
69
+ const authSchemes = useMemo(() => getAuthSchemes(document, auth), [document, auth])
70
+ const defaultScheme = authSchemes.find((s) => s.type !== 'none') ?? authSchemes[0]
71
+
72
+ const [selectedScheme, setSelectedScheme] = useState(defaultScheme.name)
73
+ const [authToken, setAuthToken] = useState('')
74
+ const [basicUser, setBasicUser] = useState('')
75
+ const [basicPass, setBasicPass] = useState('')
76
+ const [headerValues, setHeaderValues] = useState<Record<string, string>>({})
77
+ const [pathValues, setPathValues] = useState<Record<string, string>>({})
78
+ const [queryValues, setQueryValues] = useState<Record<string, string>>({})
79
+ const [jsonMode, setJsonMode] = useState(false)
80
+ const [bodyValues, setBodyValues] = useState<Record<string, unknown>>(() => {
81
+ if (!body) return {}
82
+ const init: Record<string, unknown> = {}
83
+ for (const f of body.fields) {
84
+ if (f.kind === 'array') init[f.name] = []
85
+ else if (f.kind === 'object' || f.children) init[f.name] = {}
86
+ else init[f.name] = ''
87
+ }
88
+ return init
89
+ })
90
+ const [bodyJsonStr, setBodyJsonStr] = useState(() => body ? body.jsonExample : '{}')
91
+
92
+ const [responseData, setResponseData] = useState<{
93
+ status: number; statusText: string; body: unknown; headers?: Record<string, string>; time: number
94
+ } | null>(null)
95
+ const [responseView, setResponseView] = useState<'body' | 'headers'>('body')
96
+ const [loading, setLoading] = useState(false)
97
+ const [collapsed, setCollapsed] = useState<Record<string, boolean>>({})
98
+
99
+ const toggleCollapse = (section: string) => {
100
+ setCollapsed((prev) => ({ ...prev, [section]: !prev[section] }))
101
+ }
102
+
103
+ const currentScheme = authSchemes.find((s) => s.name === selectedScheme) ?? authSchemes[0]
104
+
105
+ const getAuthHeaders = useCallback((): Record<string, string> => {
106
+ const headers: Record<string, string> = {}
107
+ if (currentScheme.type === 'apiKey' && authToken) {
108
+ headers[currentScheme.headerName] = authToken
109
+ } else if (currentScheme.type === 'bearer' && authToken) {
110
+ headers['Authorization'] = `Bearer ${authToken}`
111
+ } else if (currentScheme.type === 'basic' && (basicUser || basicPass)) {
112
+ headers['Authorization'] = `Basic ${btoa(`${basicUser}:${basicPass}`)}`
113
+ }
114
+ return headers
115
+ }, [currentScheme, authToken, basicUser, basicPass])
116
+
117
+ const handleReset = () => {
118
+ setSelectedScheme(defaultScheme.name)
119
+ setAuthToken('')
120
+ setBasicUser('')
121
+ setBasicPass('')
122
+ setHeaderValues({})
123
+ setPathValues({})
124
+ setQueryValues({})
125
+ setBodyValues(() => {
126
+ if (!body) return {}
127
+ const init: Record<string, unknown> = {}
128
+ for (const f of body.fields) {
129
+ if (f.kind === 'array') init[f.name] = []
130
+ else if (f.kind === 'object' || f.children) init[f.name] = {}
131
+ else init[f.name] = ''
132
+ }
133
+ return init
134
+ })
135
+ setResponseData(null)
136
+ }
137
+
138
+ const handleSend = useCallback(async () => {
139
+ setLoading(true)
140
+ setResponseData(null)
141
+ const startTime = performance.now()
142
+
143
+ let resolvedPath = path
144
+ for (const [key, value] of Object.entries(pathValues)) {
145
+ if (value) resolvedPath = resolvedPath.replace(`{${key}}`, encodeURIComponent(value))
146
+ }
147
+
148
+ const queryEntries = Object.entries(queryValues).filter(([, v]) => v)
149
+ const queryString = queryEntries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&')
150
+ const fullPath = queryString ? `${resolvedPath}?${queryString}` : resolvedPath
151
+
152
+ const reqHeaders: Record<string, string> = { ...getAuthHeaders() }
153
+ for (const [key, value] of Object.entries(headerValues)) {
154
+ if (value) reqHeaders[key] = value
155
+ }
156
+
157
+ let reqBody: unknown = undefined
158
+ if (body) {
159
+ reqHeaders['Content-Type'] = body.contentType ?? 'application/json'
160
+ if (jsonMode) {
161
+ try {
162
+ reqBody = JSON.parse(bodyJsonStr)
163
+ } catch {
164
+ setResponseData({ status: 0, statusText: 'Error', body: 'Invalid JSON in request body', time: 0 })
165
+ setLoading(false)
166
+ return
167
+ }
168
+ } else {
169
+ reqBody = bodyValues
170
+ }
171
+ }
172
+
173
+ try {
174
+ const res = await fetch('/api/apis-proxy', {
175
+ method: 'POST',
176
+ headers: { 'Content-Type': 'application/json' },
177
+ body: JSON.stringify({ specName, method, path: fullPath, headers: reqHeaders, body: reqBody }),
178
+ })
179
+ const data = await res.json()
180
+ const elapsed = Math.round(performance.now() - startTime)
181
+ if (data.status !== undefined) {
182
+ setResponseData({ ...data, time: elapsed })
183
+ } else {
184
+ setResponseData({ status: res.status, statusText: res.statusText, body: data.error ?? data, time: elapsed })
185
+ }
186
+ } catch {
187
+ setResponseData({ status: 0, statusText: 'Error', body: 'Failed to send request', time: 0 })
188
+ } finally {
189
+ setLoading(false)
190
+ }
191
+ }, [specName, method, path, pathValues, queryValues, getAuthHeaders, headerValues, bodyValues, body])
192
+
193
+ const responseJson = responseData
194
+ ? (typeof responseData.body === 'string' ? responseData.body : JSON.stringify(responseData.body, null, 2))
195
+ : ''
196
+
197
+ const responseLines = responseJson ? responseJson.split('\n') : []
198
+
199
+ const curlSnippet = useMemo(() => {
200
+ const headers: Record<string, string> = { ...getAuthHeaders(), ...headerValues }
201
+ if (body) headers['Content-Type'] = body.contentType ?? 'application/json'
202
+ const bodyStr = body ? (jsonMode ? bodyJsonStr : JSON.stringify(bodyValues)) : undefined
203
+ return generateCurl({ method, url: serverUrl + path, headers, body: bodyStr })
204
+ }, [method, path, serverUrl, getAuthHeaders, headerValues, bodyValues, bodyJsonStr, jsonMode, body])
205
+
206
+
207
+ return (
208
+ <Dialog open={open} onOpenChange={onOpenChange}>
209
+ <Dialog.Content className={styles.dialog} showCloseButton={false}>
210
+ {/* Action Nav */}
211
+ <div className={styles.actionNav}>
212
+ <span className={styles.actionNavTitle}>{operation.summary ?? `${method} ${path}`}</span>
213
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--rs-space-3)' }}>
214
+ <IconButton size={2} onClick={handleReset} aria-label="Reset">
215
+ <CounterClockwiseClockIcon />
216
+ </IconButton>
217
+ <IconButton size={2} onClick={() => onOpenChange(false)} aria-label="Close">
218
+ <Cross2Icon />
219
+ </IconButton>
220
+ </div>
221
+ </div>
222
+
223
+ {/* Split Panel */}
224
+ <div className={styles.splitPanel}>
225
+ {/* Left Panel */}
226
+ <div className={styles.leftPanel}>
227
+ <div className={styles.panelHeader}>
228
+ <span className={styles.panelTitle}>Test request</span>
229
+ </div>
230
+
231
+ {/* Fields */}
232
+ <div className={styles.fieldsScroll}>
233
+ {/* Auth Section */}
234
+ {(
235
+ <>
236
+ <div className={styles.sectionHeader}>
237
+ <span className={styles.sectionLabel}>Authorization</span>
238
+ <IconButton size={2} onClick={() => toggleCollapse('auth')}>
239
+ {collapsed.auth ? <ChevronDownIcon /> : <ChevronUpIcon />}
240
+ </IconButton>
241
+ </div>
242
+ {!collapsed.auth && (
243
+ <>
244
+ {authSchemes.length > 2 && (
245
+ <div className={styles.fieldRow}>
246
+ <span className={styles.fieldLabel}>Type</span>
247
+ <div className={styles.fieldInput}>
248
+ <Select value={selectedScheme} onValueChange={setSelectedScheme}>
249
+ <Select.Trigger size="small">
250
+ <Select.Value />
251
+ </Select.Trigger>
252
+ <Select.Content>
253
+ {authSchemes.map((s) => (
254
+ <Select.Item key={s.name} value={s.name}>{s.name}</Select.Item>
255
+ ))}
256
+ </Select.Content>
257
+ </Select>
258
+ </div>
259
+ </div>
260
+ )}
261
+ {currentScheme.type === 'basic' ? (
262
+ <>
263
+ <div className={styles.fieldRow}>
264
+ <span className={styles.fieldLabel}>Username</span>
265
+ <div className={styles.fieldInput}>
266
+ <InputField size="small" placeholder="Enter username" value={basicUser} onChange={(e) => setBasicUser(e.target.value)} />
267
+ </div>
268
+ </div>
269
+ <div className={styles.fieldRow}>
270
+ <span className={styles.fieldLabel}>Password</span>
271
+ <div className={styles.fieldInput}>
272
+ <InputField size="small" type="password" placeholder="Enter password" value={basicPass} onChange={(e) => setBasicPass(e.target.value)} />
273
+ </div>
274
+ </div>
275
+ </>
276
+ ) : currentScheme.type !== 'none' ? (
277
+ <div className={styles.fieldRow}>
278
+ <span className={styles.fieldLabel}>{currentScheme.headerName}</span>
279
+ <div className={styles.fieldInput}>
280
+ <InputField size="small" placeholder={currentScheme.placeholder} value={authToken} onChange={(e) => setAuthToken(e.target.value)} />
281
+ </div>
282
+ </div>
283
+ ) : null}
284
+ {headerFields.filter((f) => f.name !== currentScheme.headerName).map((f) => (
285
+ <div key={f.name} className={styles.fieldRow}>
286
+ <span className={styles.fieldLabel}>{f.name}</span>
287
+ <div className={styles.fieldInput}>
288
+ <InputField size="small" placeholder="Enter value" value={headerValues[f.name] ?? ''} onChange={(e) => setHeaderValues({ ...headerValues, [f.name]: e.target.value })} />
289
+ </div>
290
+ </div>
291
+ ))}
292
+ </>
293
+ )}
294
+ </>
295
+ )}
296
+
297
+ {/* Path Params */}
298
+ {pathFields.length > 0 && (
299
+ <>
300
+ <div className={styles.sectionHeader}>
301
+ <span className={styles.sectionLabel}>Path Parameters</span>
302
+ <IconButton size={2} onClick={() => toggleCollapse('path')}>
303
+ {collapsed.path ? <ChevronDownIcon /> : <ChevronUpIcon />}
304
+ </IconButton>
305
+ </div>
306
+ {!collapsed.path && pathFields.map((f) => (
307
+ <div key={f.name} className={styles.fieldRow}>
308
+ <span className={styles.fieldLabel}>{f.name}</span>
309
+ <div className={styles.fieldInput}>
310
+ <InputField
311
+ size="small"
312
+ placeholder="Enter value"
313
+ value={pathValues[f.name] ?? ''}
314
+ onChange={(e) => setPathValues({ ...pathValues, [f.name]: e.target.value })}
315
+ />
316
+ </div>
317
+ </div>
318
+ ))}
319
+ </>
320
+ )}
321
+
322
+ {/* Query Params */}
323
+ {queryFields.length > 0 && (
324
+ <>
325
+ <div className={styles.sectionHeader}>
326
+ <span className={styles.sectionLabel}>Query Parameters</span>
327
+ <IconButton size={2} onClick={() => toggleCollapse('query')}>
328
+ {collapsed.query ? <ChevronDownIcon /> : <ChevronUpIcon />}
329
+ </IconButton>
330
+ </div>
331
+ {!collapsed.query && queryFields.map((f) => (
332
+ <div key={f.name} className={styles.fieldRow}>
333
+ <span className={styles.fieldLabel}>{f.name}</span>
334
+ <div className={styles.fieldInput}>
335
+ <InputField
336
+ size="small"
337
+ placeholder={f.description ?? 'Enter value'}
338
+ value={queryValues[f.name] ?? ''}
339
+ onChange={(e) => setQueryValues({ ...queryValues, [f.name]: e.target.value })}
340
+ />
341
+ </div>
342
+ </div>
343
+ ))}
344
+ </>
345
+ )}
346
+
347
+ {/* Body Section */}
348
+ {body && (
349
+ <>
350
+ <div className={styles.sectionHeader}>
351
+ <span className={styles.sectionLabel}>Body</span>
352
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--rs-space-3)' }}>
353
+ <IconButton size={2} onClick={() => {
354
+ if (!jsonMode) {
355
+ setBodyJsonStr(JSON.stringify(bodyValues, null, 2))
356
+ } else {
357
+ try { setBodyValues(JSON.parse(bodyJsonStr)) } catch { /* ignore */ }
358
+ }
359
+ setJsonMode(!jsonMode)
360
+ }}>
361
+ <CodeIcon />
362
+ </IconButton>
363
+ <IconButton size={2} onClick={() => toggleCollapse('body')}>
364
+ {collapsed.body ? <ChevronDownIcon /> : <ChevronUpIcon />}
365
+ </IconButton>
366
+ </div>
367
+ </div>
368
+ {!collapsed.body && (
369
+ jsonMode ? (
370
+ <div className={styles.jsonEditorWrap}>
371
+ <JsonEditor
372
+ value={bodyJsonStr}
373
+ onChange={(val) => setBodyJsonStr(val)}
374
+ />
375
+ </div>
376
+ ) : (
377
+ body.fields.map((f) => (
378
+ <BodyFieldRow
379
+ key={f.name}
380
+ field={f}
381
+ value={bodyValues[f.name]}
382
+ onChange={(val) => setBodyValues({ ...bodyValues, [f.name]: val })}
383
+ />
384
+ ))
385
+ )
386
+ )}
387
+ </>
388
+ )}
389
+ </div>
390
+ </div>
391
+
392
+ {/* Right Panel */}
393
+ <div className={styles.rightPanel}>
394
+ <div className={styles.responseHeader}>
395
+ <span className={styles.panelTitle}>Response</span>
396
+ {responseData && (
397
+ <Menu>
398
+ <Menu.Trigger render={<Button variant="text" color="neutral" size="small" trailingIcon={<ChevronDownIcon />} />}>
399
+ {responseView === 'body' ? 'Body' : 'Headers'}
400
+ </Menu.Trigger>
401
+ <Menu.Content>
402
+ <Menu.Item onClick={() => setResponseView('body')}>Body</Menu.Item>
403
+ <Menu.Item onClick={() => setResponseView('headers')}>Headers</Menu.Item>
404
+ </Menu.Content>
405
+ </Menu>
406
+ )}
407
+ </div>
408
+
409
+ {responseData ? (
410
+ <>
411
+ <div className={styles.statusBar}>
412
+ <span className={styles.statusText}>
413
+ Status : <span className={styles.statusValue}>{responseData.status}</span>
414
+ </span>
415
+ <div className={styles.statusSeparator} />
416
+ <span className={styles.statusText}>
417
+ Time : <span className={styles.statusValue}>{responseData.time} ms</span>
418
+ </span>
419
+ </div>
420
+ {responseView === 'body' ? (
421
+ <div className={styles.codeArea}>
422
+ <div className={styles.lineNumbers}>
423
+ {responseLines.map((_, i) => (
424
+ <div key={i}>{i + 1}</div>
425
+ ))}
426
+ </div>
427
+ <pre className={styles.responseCode}>{responseJson}</pre>
428
+ </div>
429
+ ) : (
430
+ <div className={styles.headersArea}>
431
+ {responseData.headers ? (
432
+ Object.entries(responseData.headers).map(([k, v]) => (
433
+ <div key={k} className={styles.headerRow}>
434
+ <span className={styles.headerKey}>{k}</span>
435
+ <span className={styles.headerValue}>{v}</span>
436
+ </div>
437
+ ))
438
+ ) : (
439
+ <div className={styles.emptyResponse}>No headers available</div>
440
+ )}
441
+ </div>
442
+ )}
443
+ </>
444
+ ) : (
445
+ <div className={styles.emptyResponse}>
446
+ {loading ? 'Sending...' : 'Send a request to see the response'}
447
+ </div>
448
+ )}
449
+ </div>
450
+ </div>
451
+
452
+ {/* Bottom Bar */}
453
+ <div className={styles.bottomBar}>
454
+ <div className={styles.pathBar}>
455
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
456
+ <MethodBadge method={method} size="micro" />
457
+ <span className={styles.pathText}>{path}</span>
458
+ </div>
459
+ <CopyButton text={curlSnippet} size={2} />
460
+ </div>
461
+ <Button
462
+ variant="solid"
463
+ color="accent"
464
+ size="small"
465
+ trailingIcon={<PlayIcon />}
466
+ onClick={handleSend}
467
+ disabled={loading}
468
+ >
469
+ {loading ? 'Sending...' : 'Send'}
470
+ </Button>
471
+ </div>
472
+ </Dialog.Content>
473
+ </Dialog>
474
+ )
475
+ }
476
+
477
+ function BodyFieldRow({ field, value, onChange }: {
478
+ field: SchemaField
479
+ value: unknown
480
+ onChange: (val: unknown) => void
481
+ }) {
482
+ const hasChildren = field.children && field.children.length > 0
483
+
484
+ if (field.kind === 'array' && !hasChildren) {
485
+ const items = Array.isArray(value) ? value as string[] : []
486
+ return (
487
+ <div className={styles.arrayField}>
488
+ <div className={styles.fieldRow}>
489
+ <span className={styles.fieldLabel}>{field.name} {field.required && <Badge variant="danger" size="micro">required</Badge>}</span>
490
+ <IconButton size={2} onClick={() => onChange([...items, ''])} aria-label="Add item">
491
+ <PlusIcon />
492
+ </IconButton>
493
+ </div>
494
+ {items.map((item, i) => (
495
+ <div key={i} className={styles.arrayItemRow}>
496
+ <div className={styles.fieldInput}>
497
+ <InputField
498
+ size="small"
499
+ placeholder={`${field.name}[${i}]`}
500
+ value={String(item)}
501
+ onChange={(e) => {
502
+ const updated = [...items]
503
+ updated[i] = e.target.value
504
+ onChange(updated)
505
+ }}
506
+ />
507
+ </div>
508
+ <IconButton size={2} onClick={() => onChange(items.filter((_, j) => j !== i))} aria-label="Remove item">
509
+ <Cross2Icon />
510
+ </IconButton>
511
+ </div>
512
+ ))}
513
+ </div>
514
+ )
515
+ }
516
+
517
+ if (hasChildren) {
518
+ const objValue = (typeof value === 'object' && value !== null ? value : {}) as Record<string, unknown>
519
+ return (
520
+ <div className={styles.arrayField}>
521
+ <div className={styles.sectionHeader}>
522
+ <span className={styles.sectionLabel}>{field.name}</span>
523
+ </div>
524
+ <div className={styles.nestedFields}>
525
+ {field.children!.map((child) => (
526
+ <BodyFieldRow
527
+ key={child.name}
528
+ field={child}
529
+ value={objValue[child.name]}
530
+ onChange={(val) => onChange({ ...objValue, [child.name]: val })}
531
+ />
532
+ ))}
533
+ </div>
534
+ </div>
535
+ )
536
+ }
537
+
538
+ return (
539
+ <div className={styles.fieldRow}>
540
+ <span className={styles.fieldLabel}>{field.name} {field.required && <Badge variant="danger" size="micro">required</Badge>}</span>
541
+ <div className={styles.fieldInput}>
542
+ <InputField
543
+ size="small"
544
+ placeholder={field.description ?? 'Enter value'}
545
+ value={String(value ?? '')}
546
+ onChange={(e) => onChange(e.target.value)}
547
+ />
548
+ </div>
549
+ </div>
550
+ )
551
+ }
552
+
553
+ function paramsToFields(params: OpenAPIV3.ParameterObject[]): SchemaField[] {
554
+ return params.map((p) => {
555
+ const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject
556
+ return {
557
+ name: p.name,
558
+ type: schema.type ? String(schema.type) : 'string',
559
+ kind: toKind(schema.type),
560
+ required: p.required ?? false,
561
+ description: p.description,
562
+ }
563
+ })
564
+ }
565
+
566
+ interface RequestBody {
567
+ contentType: string
568
+ fields: SchemaField[]
569
+ jsonExample: string
570
+ }
571
+
572
+ function getRequestBody(body: OpenAPIV3.RequestBodyObject | undefined): RequestBody | null {
573
+ if (!body?.content) return null
574
+ const contentType = Object.keys(body.content)[0]
575
+ if (!contentType) return null
576
+ const schema = body.content[contentType]?.schema as OpenAPIV3.SchemaObject | undefined
577
+ if (!schema) return null
578
+ return {
579
+ contentType,
580
+ fields: flattenSchema(schema),
581
+ jsonExample: JSON.stringify(generateExampleJson(schema), null, 2),
582
+ }
583
+ }
@@ -13,6 +13,7 @@
13
13
 
14
14
  .list {
15
15
  max-height: 400px;
16
+ gap: var(--rs-space-3);
16
17
  }
17
18
 
18
19
  .list :global([cmdk-group-heading]) {
@@ -24,13 +25,14 @@
24
25
  }
25
26
 
26
27
  .item {
27
- height: 32px;
28
+ min-height: 40px;
28
29
  padding: var(--rs-space-3);
29
30
  gap: var(--rs-space-3);
30
31
  border-radius: var(--rs-radius-2);
31
32
  cursor: pointer;
32
33
  }
33
34
 
35
+
34
36
  .item[data-selected="true"] {
35
37
  background: var(--rs-color-background-base-primary-hover);
36
38
  }
@@ -43,8 +45,9 @@
43
45
 
44
46
  .resultText {
45
47
  display: flex;
46
- align-items: center;
47
- gap: 8px;
48
+ flex-direction: column;
49
+ gap: 2px;
50
+ min-width: 0;
48
51
  }
49
52
 
50
53
  .headingText {
@@ -68,16 +71,35 @@
68
71
  }
69
72
 
70
73
  .icon {
71
- width: 18px;
72
- height: 18px;
74
+ width: 48px;
75
+ height: 24px;
73
76
  color: var(--rs-color-foreground-base-secondary);
74
77
  flex-shrink: 0;
75
78
  }
76
79
 
80
+ .itemContent :global([class*="badge-module"]) {
81
+ min-width: 48px;
82
+ justify-content: center;
83
+ }
84
+
77
85
  .item[data-selected="true"] .icon {
78
86
  color: var(--rs-color-foreground-accent-primary-hover);
79
87
  }
80
88
 
89
+ .snippetText {
90
+ font-size: var(--rs-font-size-mini);
91
+ line-height: var(--rs-line-height-mini);
92
+ color: var(--rs-color-foreground-base-tertiary);
93
+ overflow: hidden;
94
+ text-overflow: ellipsis;
95
+ white-space: nowrap;
96
+ }
97
+
98
+ .matchHighlight {
99
+ color: var(--rs-color-foreground-accent-primary);
100
+ font-weight: var(--rs-font-weight-medium);
101
+ }
102
+
81
103
  .pageText :global(mark),
82
104
  .headingText :global(mark) {
83
105
  background: transparent;