@clidey/dory 0.30.0 → 0.31.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 (74) hide show
  1. package/bin/dist/dory.js +1 -8
  2. package/bin/dist/dory.js.map +1 -1
  3. package/index.html +54 -0
  4. package/package.json +12 -4
  5. package/src/app.css +5 -0
  6. package/src/app.tsx +13 -0
  7. package/src/components/dropdown.tsx +53 -0
  8. package/src/components/hooks.tsx +36 -0
  9. package/src/components/loading.tsx +21 -0
  10. package/src/components/mdx-provider.tsx +13 -0
  11. package/src/components/navigation.tsx +58 -0
  12. package/src/components/notification.tsx +13 -0
  13. package/src/components/routes.tsx +92 -0
  14. package/src/components/search-index.ts +43 -0
  15. package/src/components/search.tsx +448 -0
  16. package/src/components/store.ts +216 -0
  17. package/src/config/mdx.js +12 -0
  18. package/src/config/mdx.ts +12 -0
  19. package/src/embed/loader.ts +200 -0
  20. package/src/embed/types.ts +54 -0
  21. package/src/embed/widget-iframe.ts +302 -0
  22. package/src/main.tsx +22 -0
  23. package/src/mdx/accordion.tsx +41 -0
  24. package/src/mdx/api-playground.tsx +659 -0
  25. package/src/mdx/api.tsx +354 -0
  26. package/src/mdx/async-api.tsx +353 -0
  27. package/src/mdx/callout.tsx +64 -0
  28. package/src/mdx/card.tsx +54 -0
  29. package/src/mdx/code.tsx +289 -0
  30. package/src/mdx/expandable.tsx +36 -0
  31. package/src/mdx/fence.tsx +74 -0
  32. package/src/mdx/icon.tsx +73 -0
  33. package/src/mdx/latex.tsx +26 -0
  34. package/src/mdx/list.tsx +7 -0
  35. package/src/mdx/mdx.tsx +47 -0
  36. package/src/mdx/mermaid.tsx +147 -0
  37. package/src/mdx/open-api.tsx +311 -0
  38. package/src/mdx/processor.ts +174 -0
  39. package/src/mdx/source.tsx +58 -0
  40. package/src/mdx/step.tsx +51 -0
  41. package/src/mdx/table.tsx +20 -0
  42. package/src/mdx/tag.tsx +53 -0
  43. package/src/mdx/text.tsx +99 -0
  44. package/src/mdx/websocket-playground.tsx +824 -0
  45. package/src/plugins/docs-assets.ts +79 -0
  46. package/src/plugins/frontmatter-dev-server.ts +158 -0
  47. package/src/plugins/frontmatter-generator.ts +157 -0
  48. package/src/plugins/html-files-middleware.ts +108 -0
  49. package/src/plugins/html-metadata-injector.ts +76 -0
  50. package/src/plugins/llm-txt-dev-server.ts +183 -0
  51. package/src/plugins/llm-txt-generator.ts +186 -0
  52. package/src/plugins/sanitize.ts +163 -0
  53. package/src/plugins/unified-mdx-plugin.ts +35 -0
  54. package/src/sdk/index.ts +89 -0
  55. package/src/sdk/react.tsx +139 -0
  56. package/src/sdk/vue.ts +107 -0
  57. package/src/theme.scss +72 -0
  58. package/src/types/config.ts +89 -0
  59. package/src/ui/header.tsx +96 -0
  60. package/src/ui/layout.tsx +237 -0
  61. package/src/ui/mobile-navigation.tsx +72 -0
  62. package/src/ui/prev-next-link.tsx +74 -0
  63. package/src/ui/table-of-content.tsx +167 -0
  64. package/src/utils/colors.ts +54 -0
  65. package/src/utils/fonts.ts +23 -0
  66. package/src/utils/functions.ts +12 -0
  67. package/src/vite-env.d.ts +1 -0
  68. package/tsconfig.app.json +33 -0
  69. package/tsconfig.bin.json +25 -0
  70. package/tsconfig.json +7 -0
  71. package/tsconfig.node.json +26 -0
  72. package/vite.config.embed-loader.ts +28 -0
  73. package/vite.config.embed-widget.ts +31 -0
  74. package/vite.config.ts +45 -0
@@ -0,0 +1,659 @@
1
+ import { Button, TextArea, useTheme, Input } from '@clidey/ux'
2
+ import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
3
+ import { CheckIcon } from '@heroicons/react/24/outline'
4
+ import classNames from 'classnames'
5
+ import { PencilIcon, PlusIcon, TrashIcon, XIcon } from 'lucide-react'
6
+ import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
7
+ import { Highlight, themes } from 'prism-react-renderer'
8
+ import Dropdown from '../components/dropdown'
9
+ import { Fence } from './fence'
10
+ import { Tag } from './tag'
11
+
12
+ export interface Server {
13
+ url: string;
14
+ description?: string;
15
+ }
16
+
17
+ interface APIPlaygroundProps {
18
+ method: string
19
+ url: string
20
+ title?: string
21
+ description?: string
22
+ parameters?: Array<{
23
+ name: string
24
+ type: string
25
+ required?: boolean
26
+ description?: string
27
+ defaultValue?: string
28
+ }>
29
+ headers?: Record<string, string>
30
+ body?: object
31
+ authType?: 'bearer' | 'apiKey' | 'basic' | 'none'
32
+ servers?: Server[]
33
+ }
34
+
35
+ interface RequestConfig {
36
+ method: string
37
+ url: string
38
+ headers: Record<string, string>
39
+ body?: string
40
+ queryParams: Record<string, string>
41
+ }
42
+
43
+ interface ResponseData {
44
+ status: number
45
+ statusText: string
46
+ headers: Record<string, string>
47
+ data: any
48
+ duration: number
49
+ }
50
+
51
+ export function PlayButton(props: React.ComponentPropsWithoutRef<'svg'>) {
52
+ return (
53
+ <svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
54
+ <path
55
+ fill="currentColor"
56
+ d="M6.3 2.841A1.5 1.5 0 0 0 4 4.11v11.78a1.5 1.5 0 0 0 2.3 1.269l9.344-5.89a1.5 1.5 0 0 0 0-2.538L6.3 2.841Z"
57
+ />
58
+ </svg>
59
+ )
60
+ }
61
+
62
+ function LoadingSpinner(props: React.ComponentPropsWithoutRef<'svg'>) {
63
+ return (
64
+ <svg viewBox="0 0 20 20" aria-hidden="true" className="animate-spin" {...props}>
65
+ <path
66
+ fill="currentColor"
67
+ d="M10 2a8 8 0 0 1 8 8c0 .3-.02.59-.06.88l-1.99-.22A6 6 0 1 0 4 10c0-.34.03-.67.09-1L2.1 8.78A8 8 0 0 1 10 2Z"
68
+ />
69
+ </svg>
70
+ )
71
+ }
72
+
73
+ interface RequestConfig {
74
+ headers: Record<string, string>
75
+ }
76
+
77
+ interface HeadersTabProps {
78
+ requestConfig: RequestConfig
79
+ setRequestConfig: React.Dispatch<React.SetStateAction<RequestConfig>>
80
+ }
81
+
82
+ interface HeaderItem {
83
+ key: string
84
+ value: string
85
+ isEditing: boolean
86
+ isNew?: boolean
87
+ }
88
+
89
+ export const HeadersTab = ({ requestConfig, setRequestConfig }: HeadersTabProps) => {
90
+ const [headersList, setHeadersList] = useState<HeaderItem[]>([])
91
+ const hasInitialized = useRef(false)
92
+
93
+ useEffect(() => {
94
+ if (!hasInitialized.current) {
95
+ const initialHeaders = Object.entries(requestConfig.headers ?? {}).map(([key, value]) => ({
96
+ key,
97
+ value,
98
+ isEditing: false
99
+ }))
100
+ setHeadersList(initialHeaders)
101
+ hasInitialized.current = true
102
+ }
103
+ }, [requestConfig.headers])
104
+
105
+ useEffect(() => {
106
+ const newHeaders: Record<string, string> = {}
107
+ headersList.forEach(h => {
108
+ if (h.key.trim()) newHeaders[h.key] = h.value
109
+ })
110
+ setRequestConfig(prev => ({ ...prev, headers: newHeaders }))
111
+ }, [headersList, setRequestConfig])
112
+
113
+ const updateHeader = (index: number, field: 'key' | 'value', value: string) => {
114
+ setHeadersList(list =>
115
+ list.map((h, i) => (i === index ? { ...h, [field]: value } : h))
116
+ )
117
+ }
118
+
119
+ const saveHeader = (index: number) => {
120
+ setHeadersList(list =>
121
+ list.map((h, i) => (i === index ? { ...h, isEditing: false, isNew: false } : h))
122
+ )
123
+ }
124
+
125
+ const cancelEdit = (index: number) => {
126
+ setHeadersList(list =>
127
+ list.reduce<HeaderItem[]>((acc, h, i) => {
128
+ if (i === index) {
129
+ if (h.isNew) return acc // remove new row if cancelled
130
+ return [...acc, { ...h, isEditing: false }]
131
+ }
132
+ return [...acc, h]
133
+ }, [])
134
+ )
135
+ }
136
+
137
+ const editHeader = (index: number) => {
138
+ setHeadersList(list =>
139
+ list.map((h, i) => (i === index ? { ...h, isEditing: true } : h))
140
+ )
141
+ }
142
+
143
+ const removeHeader = (index: number) => {
144
+ setHeadersList(list => list.filter((_, i) => i !== index))
145
+ }
146
+
147
+ const addEmptyHeaderRow = () => {
148
+ setHeadersList(list => [
149
+ ...list,
150
+ { key: '', value: '', isEditing: true, isNew: true }
151
+ ])
152
+ }
153
+
154
+ return (
155
+ <div className="flex flex-col gap-2 px-2 py-4">
156
+ <div className="flex items-center gap-2 px-2">
157
+ <span className="text-sm font-medium text-zinc-400 w-32">Key</span>
158
+ <span className="text-sm font-medium text-zinc-400 flex-1">Value</span>
159
+ </div>
160
+ {headersList.map((hdr, idx) => (
161
+ <div key={idx} className="flex items-center gap-2">
162
+ {hdr.isEditing ? (
163
+ <>
164
+ <Input
165
+ type="text"
166
+ value={hdr.key}
167
+ onChange={e => updateHeader(idx, 'key', (e.target as HTMLInputElement).value)}
168
+ placeholder="Header name"
169
+ className="w-32 text-sm"
170
+ />
171
+ <Input
172
+ type="text"
173
+ value={hdr.value}
174
+ onChange={e => updateHeader(idx, 'value', (e.target as HTMLInputElement).value)}
175
+ placeholder="Header value"
176
+ className="flex-1 text-sm"
177
+ />
178
+ <Button onClick={() => saveHeader(idx)}>
179
+ <CheckIcon className="w-4 h-4" />
180
+ </Button>
181
+ <Button onClick={() => cancelEdit(idx)}>
182
+ <XIcon className="w-4 h-4" />
183
+ </Button>
184
+ </>
185
+ ) : (
186
+ <>
187
+ <span className="text-sm w-32 truncate">{hdr.key}</span>
188
+ <span className="text-sm flex-1 truncate">{hdr.value}</span>
189
+ <Button onClick={() => editHeader(idx)}>
190
+ <PencilIcon className="w-4 h-4" />
191
+ </Button>
192
+ <Button onClick={() => removeHeader(idx)} className="text-red-400">
193
+ <TrashIcon className="w-4 h-4" />
194
+ </Button>
195
+ </>
196
+ )}
197
+ </div>
198
+ ))}
199
+ <Button onClick={addEmptyHeaderRow} className="text-sm mt-2 w-fit">
200
+ <PlusIcon className="w-4 h-4" /> Add Header
201
+ </Button>
202
+ </div>
203
+ )
204
+ }
205
+
206
+ interface ParamItem {
207
+ name: string;
208
+ value: string;
209
+ isEditing: boolean;
210
+ isNew?: boolean;
211
+ }
212
+
213
+ interface ParamsTabProps {
214
+ queryParams: Record<string, string>;
215
+ setQueryParams: React.Dispatch<React.SetStateAction<RequestConfig>>;
216
+ }
217
+
218
+ export const ParamsTab = ({ queryParams, setQueryParams }: ParamsTabProps) => {
219
+ const [paramsList, setParamsList] = useState<ParamItem[]>([]);
220
+ const hasInitialized = useRef(false);
221
+
222
+ useEffect(() => {
223
+ if (!hasInitialized.current) {
224
+ const initial = Object.entries(queryParams ?? {}).map(([key, value]) => ({
225
+ name: key, value, isEditing: false
226
+ }));
227
+ setParamsList(initial);
228
+ hasInitialized.current = true;
229
+ }
230
+ }, [queryParams]);
231
+
232
+ useEffect(() => {
233
+ const newParams: Record<string, string> = {};
234
+ paramsList.forEach(p => {
235
+ if (p.name.trim()) newParams[p.name] = p.value;
236
+ });
237
+ setQueryParams(prev => ({ ...prev, queryParams: newParams }));
238
+ }, [paramsList, setQueryParams]);
239
+
240
+ const updateParam = (idx: number, field: 'name' | 'value', val: string) =>
241
+ setParamsList(list => list.map((p, i) =>
242
+ i === idx ? { ...p, [field]: val } : p
243
+ ));
244
+
245
+ const saveParam = (idx: number) =>
246
+ setParamsList(list => list.map((p, i) =>
247
+ i === idx ? { ...p, isEditing: false, isNew: false } : p
248
+ ));
249
+
250
+ const cancelEdit = (idx: number) =>
251
+ setParamsList(list =>
252
+ list.reduce<ParamItem[]>((acc, p, i) => {
253
+ if (i === idx) {
254
+ if (p.isNew) return acc;
255
+ return [...acc, { ...p, isEditing: false }];
256
+ }
257
+ return [...acc, p];
258
+ }, [])
259
+ );
260
+
261
+ const editParam = (idx: number) =>
262
+ setParamsList(list =>
263
+ list.map((p, i) => (i === idx ? { ...p, isEditing: true } : p))
264
+ );
265
+
266
+ const removeParam = (idx: number) =>
267
+ setParamsList(list => list.filter((_, i) => i !== idx));
268
+
269
+ const addParamRow = () =>
270
+ setParamsList(list => [
271
+ ...list, { name: '', value: '', isEditing: true, isNew: true }
272
+ ]);
273
+
274
+ return (
275
+ <div className="flex flex-col gap-2 px-2 py-4">
276
+ <div className="flex items-center gap-2">
277
+ <span className="text-sm font-medium text-zinc-400 w-32">Name</span>
278
+ <span className="text-sm font-medium text-zinc-400 flex-1">Value</span>
279
+ </div>
280
+ {paramsList.map((p, i) => (
281
+ <div key={i} className="flex items-center gap-2">
282
+ {p.isEditing ? (
283
+ <>
284
+ <Input value={p.name} onChange={e => updateParam(i, 'name', (e.target as HTMLInputElement).value)} placeholder="Param name" className="w-32 text-sm" />
285
+ <Input value={p.value} onChange={e => updateParam(i, 'value', (e.target as HTMLInputElement).value)} placeholder="Param value" className="flex-1 text-sm" />
286
+ <Button onClick={() => saveParam(i)}>
287
+ <CheckIcon className="w-4 h-4" />
288
+ </Button>
289
+ <Button onClick={() => cancelEdit(i)}>
290
+ <XIcon className="w-4 h-4" />
291
+ </Button>
292
+ </>
293
+ ) : (
294
+ <>
295
+ <span className="w-32 truncate text-sm">{p.name}</span>
296
+ <span className="flex-1 truncate text-sm">{p.value}</span>
297
+ <Button onClick={() => editParam(i)}>
298
+ <PencilIcon className="w-4 h-4" />
299
+ </Button>
300
+ <Button onClick={() => removeParam(i)} className="text-red-400">
301
+ <TrashIcon className="w-4 h-4" />
302
+ </Button>
303
+ </>
304
+ )}
305
+ </div>
306
+ ))}
307
+ <Button onClick={addParamRow} className="mt-2 text-sm self-end">
308
+ <PlusIcon className="w-4 h-4" /> Add Parameter
309
+ </Button>
310
+ </div>
311
+ );
312
+ };
313
+
314
+
315
+ export function APIPlayground({
316
+ method,
317
+ url,
318
+ title,
319
+ description,
320
+ parameters = [],
321
+ headers: defaultHeaders = {},
322
+ body: defaultBody,
323
+ authType = 'none',
324
+ servers = []
325
+ }: APIPlaygroundProps) {
326
+ const [selectedTab, setSelectedTab] = useState(0)
327
+ const [isLoading, setIsLoading] = useState(false)
328
+ const [response, setResponse] = useState<ResponseData | null>(null)
329
+ const [error, setError] = useState<string | null>(null)
330
+ const isDark = useTheme().theme === 'dark';
331
+ const [selectedServer, setSelectedServer] = useState<Server | null>(servers?.[0] || null)
332
+
333
+ // Request configuration state
334
+ const [requestConfig, setRequestConfig] = useState<RequestConfig>({
335
+ method: method.toUpperCase(),
336
+ url: selectedServer?.url + url,
337
+ headers: { 'Content-Type': 'application/json', ...defaultHeaders },
338
+ queryParams: parameters.reduce((acc, param) => {
339
+ acc[param.name] = param.defaultValue || ''
340
+ return acc
341
+ }, {} as Record<string, string>),
342
+ body: defaultBody ? JSON.stringify(defaultBody, null, 2) : ''
343
+ })
344
+
345
+ // Auth state
346
+ const [auth, setAuth] = useState({
347
+ type: authType,
348
+ token: '',
349
+ apiKey: '',
350
+ username: '',
351
+ password: ''
352
+ })
353
+
354
+ const executeRequest = useCallback(async () => {
355
+ setIsLoading(true)
356
+ setError(null)
357
+ setResponse(null)
358
+
359
+ const startTime = Date.now()
360
+
361
+ try {
362
+ // Prepare headers based on auth type
363
+ const headers = { ...requestConfig.headers }
364
+
365
+ switch (auth.type) {
366
+ case 'bearer':
367
+ if (auth.token) {
368
+ headers['Authorization'] = `Bearer ${auth.token}`
369
+ }
370
+ break
371
+ case 'apiKey':
372
+ if (auth.apiKey) {
373
+ headers['X-API-Key'] = auth.apiKey
374
+ }
375
+ break
376
+ case 'basic':
377
+ if (auth.username && auth.password) {
378
+ headers['Authorization'] = `Basic ${btoa(`${auth.username}:${auth.password}`)}`
379
+ }
380
+ break
381
+ }
382
+
383
+ const fetchOptions: RequestInit = {
384
+ method: requestConfig.method,
385
+ headers
386
+ }
387
+
388
+ if (requestConfig.method !== 'GET' && requestConfig.body) {
389
+ fetchOptions.body = JSON.stringify(JSON.parse(requestConfig.body))
390
+ }
391
+
392
+ const response = await fetch(requestConfig.url, fetchOptions)
393
+ const endTime = Date.now()
394
+
395
+ let responseData
396
+ const contentType = response.headers.get('content-type')
397
+
398
+ if (contentType?.includes('application/json')) {
399
+ responseData = await response.json()
400
+ } else {
401
+ responseData = await response.text()
402
+ }
403
+
404
+ const responseHeaders: Record<string, string> = {}
405
+ response.headers.forEach((value, key) => {
406
+ responseHeaders[key] = value
407
+ })
408
+
409
+ setResponse({
410
+ status: response.status,
411
+ statusText: response.statusText,
412
+ headers: responseHeaders,
413
+ data: responseData,
414
+ duration: endTime - startTime
415
+ })
416
+ } catch (err) {
417
+ setError(err instanceof Error ? err.message : 'Request failed')
418
+ } finally {
419
+ setIsLoading(false)
420
+ }
421
+ }, [requestConfig, auth])
422
+
423
+ const tabs = [
424
+ { name: 'Parameters', key: 'params' },
425
+ { name: 'Headers', key: 'headers' },
426
+ { name: 'Body', key: 'body' },
427
+ { name: 'Auth', key: 'auth' }
428
+ ]
429
+
430
+ return (
431
+ <div className="my-6 overflow-hidden rounded-2xl">
432
+ {/* Header */}
433
+ <div className="flex min-h-[calc(--spacing(12)+1px)] flex-wrap items-start gap-x-4 px-4 pt-2 pb-4 justify-between">
434
+ <div className="flex items-center gap-3 pt-3">
435
+ <Tag variant="small">{method.toUpperCase()}</Tag>
436
+ <span className="font-mono text-sm">{title || url}</span>
437
+ </div>
438
+ <Button
439
+ onClick={executeRequest}
440
+ disabled={isLoading}>
441
+ {isLoading ? 'Sending...' : 'Send Request'}
442
+ {isLoading ? <LoadingSpinner className="h-4 w-4" /> : <PlayButton className="h-4 w-4" />}
443
+ </Button>
444
+ </div>
445
+
446
+ {description && (
447
+ <p className="text-sm px-4 py-3">{description}</p>
448
+ )}
449
+
450
+ <div className="flex flex-col gap-4">
451
+ <div className="flex flex-col gap-2">
452
+ <label className="text-sm font-medium">Server</label>
453
+ <Dropdown
454
+ buttonLabel={selectedServer?.description || 'Select Server'}
455
+ items={servers.map(server => ({ label: server.description || server.url, onClick: () => setSelectedServer(server) }))}
456
+ className="w-full"
457
+ />
458
+ </div>
459
+
460
+ {/* Request Configuration */}
461
+ <div className="flex-1">
462
+ <TabGroup selectedIndex={selectedTab} onChange={setSelectedTab}>
463
+ <TabList className="flex">
464
+ {tabs.map((tab) => (
465
+ <Tab
466
+ key={tab.key}
467
+ className={({ selected }) =>
468
+ classNames(
469
+ 'px-4 py-3 text-sm font-medium transition border-b-2',
470
+ selected
471
+ ? 'border-brand-foreground text-brand-foreground'
472
+ : 'border-transparent text-zinc-400 hover:text-zinc-300'
473
+ )
474
+ }
475
+ >
476
+ {tab.name}
477
+ </Tab>
478
+ ))}
479
+ </TabList>
480
+
481
+ <TabPanels className="min-h-[250px]">
482
+ <TabPanel className="p-4">
483
+ <ParamsTab
484
+ queryParams={requestConfig.queryParams}
485
+ setQueryParams={setRequestConfig}
486
+ />
487
+ </TabPanel>
488
+
489
+ <TabPanel className="p-4">
490
+ <HeadersTab
491
+ requestConfig={requestConfig}
492
+ setRequestConfig={setRequestConfig}
493
+ />
494
+ </TabPanel>
495
+
496
+ {/* Body Tab */}
497
+ <TabPanel className="p-4">
498
+ {['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) ? (
499
+ <div className="flex flex-col space-y-2">
500
+ <label className="text-sm font-medium">Request Body (JSON)</label>
501
+ <Highlight
502
+ code={requestConfig.body || ''}
503
+ language="json"
504
+ theme={isDark ? themes.vsDark : themes.vsLight}>
505
+ {({ className, style }) => (
506
+ <TextArea
507
+ value={requestConfig.body}
508
+ onChange={(e) => setRequestConfig(prev => ({ ...prev, body: (e.target as HTMLTextAreaElement).value }))}
509
+ placeholder="Enter JSON body..."
510
+ rows={8}
511
+ className={`w-full text-sm font-mono ${className}`}
512
+ style={style}
513
+ />
514
+ )}
515
+ </Highlight>
516
+ </div>
517
+ ) : (
518
+ <p className="text-sm text-zinc-400">Request body not applicable for {method.toUpperCase()} requests.</p>
519
+ )}
520
+ </TabPanel>
521
+
522
+ {/* Auth Tab */}
523
+ <TabPanel className="p-4 space-y-4">
524
+ <div className="flex flex-col space-y-2">
525
+ <label className="text-sm font-medium">Authentication Type</label>
526
+ <Dropdown
527
+ buttonLabel={auth.type === 'none' ? 'Select Auth Type' : auth.type.charAt(0).toUpperCase() + auth.type.slice(1)}
528
+ items={[
529
+ { label: 'None', onClick: () => setAuth(prev => ({ ...prev, type: 'none' })) },
530
+ { label: 'Bearer Token', onClick: () => setAuth(prev => ({ ...prev, type: 'bearer' })) },
531
+ { label: 'API Key', onClick: () => setAuth(prev => ({ ...prev, type: 'apiKey' })) },
532
+ { label: 'Basic Auth', onClick: () => setAuth(prev => ({ ...prev, type: 'basic' })) }
533
+ ]}
534
+ className="w-full"
535
+ />
536
+ </div>
537
+
538
+ {auth.type === 'bearer' && (
539
+ <div className="space-y-2">
540
+ <label className="text-sm font-medium">Bearer Token</label>
541
+ <Input
542
+ type="password"
543
+ value={auth.token}
544
+ onChange={(e) => setAuth(prev => ({ ...prev, token: (e.target as HTMLInputElement).value }))}
545
+ placeholder="Enter bearer token..."
546
+ className="w-full text-sm"
547
+ />
548
+ </div>
549
+ )}
550
+
551
+ {auth.type === 'apiKey' && (
552
+ <div className="space-y-2">
553
+ <label className="text-sm font-medium">API Key</label>
554
+ <Input
555
+ type="password"
556
+ value={auth.apiKey}
557
+ onChange={(e) => setAuth(prev => ({ ...prev, apiKey: (e.target as HTMLInputElement).value }))}
558
+ placeholder="Enter API key..."
559
+ className="w-full text-sm"
560
+ />
561
+ </div>
562
+ )}
563
+
564
+ {auth.type === 'basic' && (
565
+ <div className="space-y-4">
566
+ <div className="space-y-2">
567
+ <label className="text-sm font-medium">Username</label>
568
+ <Input
569
+ type="text"
570
+ value={auth.username}
571
+ onChange={(e) => setAuth(prev => ({ ...prev, username: (e.target as HTMLInputElement).value }))}
572
+ placeholder="Enter username..."
573
+ className="w-full text-sm"
574
+ />
575
+ </div>
576
+ <div className="space-y-2">
577
+ <label className="text-sm font-medium">Password</label>
578
+ <Input
579
+ type="password"
580
+ value={auth.password}
581
+ onChange={(e) => setAuth(prev => ({ ...prev, password: (e.target as HTMLInputElement).value }))}
582
+ placeholder="Enter password..."
583
+ className="w-full text-sm"
584
+ />
585
+ </div>
586
+ </div>
587
+ )}
588
+ </TabPanel>
589
+ </TabPanels>
590
+ </TabGroup>
591
+ </div>
592
+
593
+ {/* Response */}
594
+ <div className="flex-1">
595
+ <div className="px-4 py-3">
596
+ <h3 className="text-sm font-semibold">Response</h3>
597
+ </div>
598
+
599
+ <div className="p-4">
600
+ {error ? (
601
+ <div className="space-y-4">
602
+ <div className="flex items-center gap-2">
603
+ <span className="inline-flex items-center rounded-full bg-rose-500/10 px-2 py-1 text-xs font-medium text-rose-400 ring-1 ring-rose-500/20">
604
+ Error
605
+ </span>
606
+ </div>
607
+ <pre className="text-sm text-rose-400 bg-rose-500/5 p-3 rounded-lg border border-rose-500/20">
608
+ {error}
609
+ </pre>
610
+ </div>
611
+ ) : response ? (
612
+ <div className="space-y-4">
613
+ <div className="flex items-center gap-4">
614
+ <span className={classNames(
615
+ 'inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset',
616
+ response.status >= 200 && response.status < 300
617
+ ? 'bg-sky-500/10 text-sky-400 ring-sky-500/20'
618
+ : response.status >= 400
619
+ ? 'bg-rose-500/10 text-rose-400 ring-rose-500/20'
620
+ : 'bg-yellow-500/10 text-yellow-400 ring-yellow-500/20'
621
+ )}>
622
+ {response.status} {response.statusText}
623
+ </span>
624
+ <span className="text-xs text-zinc-400">{response.duration}ms</span>
625
+ </div>
626
+
627
+ <div className="space-y-2">
628
+ <h4 className="text-sm font-medium">Response Body</h4>
629
+ <Fence language="json">
630
+ {typeof response.data === 'string'
631
+ ? response.data
632
+ : JSON.stringify(response.data, null, 2)
633
+ }
634
+ </Fence>
635
+ </div>
636
+
637
+ <div className="space-y-2">
638
+ <h4 className="text-sm font-medium">Response Headers</h4>
639
+ <div className="space-y-1">
640
+ {Object.entries(response.headers).map(([key, value]) => (
641
+ <div key={key} className="flex text-xs">
642
+ <span className="text-zinc-400 w-32">{key}:</span>
643
+ <span className="text-zinc-300">{value}</span>
644
+ </div>
645
+ ))}
646
+ </div>
647
+ </div>
648
+ </div>
649
+ ) : (
650
+ <p className="text-sm text-center py-8 text-muted-foreground">
651
+ Click"Send Request" to see the response
652
+ </p>
653
+ )}
654
+ </div>
655
+ </div>
656
+ </div>
657
+ </div>
658
+ )
659
+ }