@barodoc/theme-docs 6.0.0 → 7.0.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.
@@ -8,6 +8,7 @@ interface ParamDef {
8
8
  required?: boolean;
9
9
  defaultValue?: string;
10
10
  description?: string;
11
+ enum?: string[];
11
12
  }
12
13
 
13
14
  interface ApiPlaygroundProps {
@@ -19,14 +20,223 @@ interface ApiPlaygroundProps {
19
20
  className?: string;
20
21
  }
21
22
 
22
- const methodColors: Record<string, string> = {
23
- GET: "bg-green-100 text-green-800 dark:bg-green-950 dark:text-green-300",
24
- POST: "bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300",
25
- PUT: "bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300",
26
- PATCH: "bg-orange-100 text-orange-800 dark:bg-orange-950 dark:text-orange-300",
27
- DELETE: "bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300",
23
+ interface HistoryEntry {
24
+ id: number;
25
+ method: string;
26
+ url: string;
27
+ status: number;
28
+ statusText: string;
29
+ time: number;
30
+ size: number;
31
+ body: string;
32
+ headers: [string, string][];
33
+ timestamp: number;
34
+ }
35
+
36
+ const METHOD_COLORS: Record<string, string> = {
37
+ GET: "bd-method-get",
38
+ POST: "bd-method-post",
39
+ PUT: "bd-method-put",
40
+ PATCH: "bd-method-patch",
41
+ DELETE: "bd-method-delete",
28
42
  };
29
43
 
44
+ type SnippetLang = "curl" | "javascript" | "python" | "node";
45
+ type ResponseTab = "body" | "headers" | "code";
46
+
47
+ const SNIPPET_LABELS: Record<SnippetLang, string> = {
48
+ curl: "cURL",
49
+ javascript: "JavaScript",
50
+ python: "Python",
51
+ node: "Node.js",
52
+ };
53
+
54
+ const DB_NAME = "barodoc-api-playground";
55
+ const DB_VERSION = 1;
56
+ const STORE_NAME = "history";
57
+ const MAX_HISTORY = 5;
58
+
59
+ function openDb(): Promise<IDBDatabase> {
60
+ return new Promise((resolve, reject) => {
61
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
62
+ req.onupgradeneeded = () => {
63
+ const db = req.result;
64
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
65
+ db.createObjectStore(STORE_NAME, { keyPath: "id", autoIncrement: true });
66
+ }
67
+ };
68
+ req.onsuccess = () => resolve(req.result);
69
+ req.onerror = () => reject(req.error);
70
+ });
71
+ }
72
+
73
+ async function loadHistory(): Promise<HistoryEntry[]> {
74
+ try {
75
+ const db = await openDb();
76
+ return new Promise((resolve) => {
77
+ const tx = db.transaction(STORE_NAME, "readonly");
78
+ const store = tx.objectStore(STORE_NAME);
79
+ const req = store.getAll();
80
+ req.onsuccess = () => {
81
+ const items = (req.result as HistoryEntry[]).sort((a, b) => b.timestamp - a.timestamp);
82
+ resolve(items.slice(0, MAX_HISTORY));
83
+ };
84
+ req.onerror = () => resolve([]);
85
+ });
86
+ } catch {
87
+ return [];
88
+ }
89
+ }
90
+
91
+ async function saveHistory(entry: Omit<HistoryEntry, "id">): Promise<void> {
92
+ try {
93
+ const db = await openDb();
94
+ const tx = db.transaction(STORE_NAME, "readwrite");
95
+ const store = tx.objectStore(STORE_NAME);
96
+ store.add(entry);
97
+
98
+ const allReq = store.getAll();
99
+ allReq.onsuccess = () => {
100
+ const items = (allReq.result as HistoryEntry[]).sort((a, b) => b.timestamp - a.timestamp);
101
+ const toDelete = items.slice(MAX_HISTORY);
102
+ for (const item of toDelete) {
103
+ store.delete(item.id);
104
+ }
105
+ };
106
+ } catch {
107
+ // IndexedDB unavailable — silent fail
108
+ }
109
+ }
110
+
111
+ function generateSnippet(
112
+ lang: SnippetLang,
113
+ method: string,
114
+ url: string,
115
+ headers: Record<string, string>,
116
+ body?: string,
117
+ ): string {
118
+ switch (lang) {
119
+ case "curl": {
120
+ const parts = [`curl -X ${method} '${url}'`];
121
+ for (const [k, v] of Object.entries(headers)) {
122
+ parts.push(` -H '${k}: ${v}'`);
123
+ }
124
+ if (body) parts.push(` -d '${body}'`);
125
+ return parts.join(" \\\n");
126
+ }
127
+ case "javascript": {
128
+ const opts: string[] = [` method: "${method}"`];
129
+ if (Object.keys(headers).length > 0) {
130
+ opts.push(` headers: ${JSON.stringify(headers, null, 4).replace(/\n/g, "\n ")}`);
131
+ }
132
+ if (body) opts.push(` body: JSON.stringify(${body})`);
133
+ return `const response = await fetch("${url}", {\n${opts.join(",\n")}\n});\nconst data = await response.json();\nconsole.log(data);`;
134
+ }
135
+ case "python": {
136
+ const lines = ["import requests", ""];
137
+ if (body) {
138
+ lines.push(`response = requests.${method.toLowerCase()}(`);
139
+ lines.push(` "${url}",`);
140
+ if (Object.keys(headers).length > 0) {
141
+ lines.push(` headers=${pythonDict(headers)},`);
142
+ }
143
+ lines.push(` json=${body}`);
144
+ lines.push(")");
145
+ } else {
146
+ lines.push(`response = requests.${method.toLowerCase()}(`);
147
+ lines.push(` "${url}"${Object.keys(headers).length > 0 ? "," : ""}`);
148
+ if (Object.keys(headers).length > 0) {
149
+ lines.push(` headers=${pythonDict(headers)}`);
150
+ }
151
+ lines.push(")");
152
+ }
153
+ lines.push("print(response.json())");
154
+ return lines.join("\n");
155
+ }
156
+ case "node": {
157
+ const lines = ['const axios = require("axios");', ""];
158
+ const config: string[] = [];
159
+ if (Object.keys(headers).length > 0) {
160
+ config.push(` headers: ${JSON.stringify(headers, null, 4).replace(/\n/g, "\n ")}`);
161
+ }
162
+ if (body) config.push(` data: ${body}`);
163
+ lines.push(`const { data } = await axios({`);
164
+ lines.push(` method: "${method}",`);
165
+ lines.push(` url: "${url}",`);
166
+ if (config.length > 0) lines.push(config.join(",\n") + ",");
167
+ lines.push("});");
168
+ lines.push("console.log(data);");
169
+ return lines.join("\n");
170
+ }
171
+ }
172
+ }
173
+
174
+ function pythonDict(obj: Record<string, string>): string {
175
+ const entries = Object.entries(obj).map(([k, v]) => `"${k}": "${v}"`);
176
+ if (entries.length <= 2) return `{${entries.join(", ")}}`;
177
+ return `{\n ${entries.join(",\n ")}\n }`;
178
+ }
179
+
180
+ function highlightJson(text: string): React.ReactNode[] {
181
+ const withValues = text.replace(
182
+ /("(?:[^"\\]|\\.)*")(\s*:)?|(\b(?:true|false|null)\b)|(\b-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)/g,
183
+ (match, str, colon, bool, num) => {
184
+ if (str && colon) return `<k>${str}</k>${colon}`;
185
+ if (str) return `<s>${str}</s>`;
186
+ if (bool) return `<b>${bool}</b>`;
187
+ if (num) return `<n>${num}</n>`;
188
+ return match;
189
+ },
190
+ );
191
+
192
+ const parts = withValues.split(/(<[ksnb]>.*?<\/[ksnb]>)/g);
193
+ return parts.map((part, i) => {
194
+ const keyM = part.match(/^<k>(.*)<\/k>$/);
195
+ if (keyM) return <span key={i} className="bd-json-key">{keyM[1]}</span>;
196
+ const strM = part.match(/^<s>(.*)<\/s>$/);
197
+ if (strM) return <span key={i} className="bd-json-str">{strM[1]}</span>;
198
+ const boolM = part.match(/^<b>(.*)<\/b>$/);
199
+ if (boolM) return <span key={i} className="bd-json-bool">{boolM[1]}</span>;
200
+ const numM = part.match(/^<n>(.*)<\/n>$/);
201
+ if (numM) return <span key={i} className="bd-json-num">{numM[1]}</span>;
202
+ return <React.Fragment key={i}>{part}</React.Fragment>;
203
+ });
204
+ }
205
+
206
+ function formatSize(bytes: number): string {
207
+ if (bytes < 1024) return `${bytes} B`;
208
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
209
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
210
+ }
211
+
212
+ function CopyButton({ text }: { text: string }) {
213
+ const [copied, setCopied] = React.useState(false);
214
+
215
+ return (
216
+ <button
217
+ className="bd-pg-copy"
218
+ onClick={() => {
219
+ navigator.clipboard.writeText(text).then(() => {
220
+ setCopied(true);
221
+ setTimeout(() => setCopied(false), 2000);
222
+ });
223
+ }}
224
+ title="Copy"
225
+ >
226
+ {copied ? (
227
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
228
+ <polyline points="20 6 9 17 4 12" />
229
+ </svg>
230
+ ) : (
231
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
232
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
233
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
234
+ </svg>
235
+ )}
236
+ </button>
237
+ );
238
+ }
239
+
30
240
  export function ApiPlayground({
31
241
  method = "GET",
32
242
  url,
@@ -37,25 +247,60 @@ export function ApiPlayground({
37
247
  }: ApiPlaygroundProps) {
38
248
  const [paramValues, setParamValues] = React.useState<Record<string, string>>(() => {
39
249
  const vals: Record<string, string> = {};
40
- for (const p of params) {
41
- vals[p.name] = p.defaultValue || "";
42
- }
250
+ for (const p of params) vals[p.name] = p.defaultValue || "";
43
251
  return vals;
44
252
  });
45
253
  const [bodyValue, setBodyValue] = React.useState(initialBody || "");
254
+ const [bodyError, setBodyError] = React.useState<string | null>(null);
46
255
  const [response, setResponse] = React.useState<{
47
256
  status: number;
48
257
  statusText: string;
49
258
  body: string;
259
+ headers: [string, string][];
50
260
  time: number;
261
+ size: number;
51
262
  } | null>(null);
52
263
  const [loading, setLoading] = React.useState(false);
53
264
  const [error, setError] = React.useState<string | null>(null);
265
+ const [responseTab, setResponseTab] = React.useState<ResponseTab>("body");
266
+ const [snippetLang, setSnippetLang] = React.useState<SnippetLang>("curl");
267
+ const [showAuth, setShowAuth] = React.useState(false);
268
+ const [authType, setAuthType] = React.useState<"bearer" | "apikey" | "basic">("bearer");
269
+ const [bearerToken, setBearerToken] = React.useState(() =>
270
+ typeof window !== "undefined" ? localStorage.getItem("bd-api-bearer") || "" : "",
271
+ );
272
+ const [apiKeyName, setApiKeyName] = React.useState(() =>
273
+ typeof window !== "undefined" ? localStorage.getItem("bd-api-keyname") || "X-API-Key" : "X-API-Key",
274
+ );
275
+ const [apiKeyValue, setApiKeyValue] = React.useState(() =>
276
+ typeof window !== "undefined" ? localStorage.getItem("bd-api-keyvalue") || "" : "",
277
+ );
278
+ const [basicUser, setBasicUser] = React.useState(() =>
279
+ typeof window !== "undefined" ? localStorage.getItem("bd-api-basic-user") || "" : "",
280
+ );
281
+ const [basicPass, setBasicPass] = React.useState(() =>
282
+ typeof window !== "undefined" ? localStorage.getItem("bd-api-basic-pass") || "" : "",
283
+ );
284
+ const [history, setHistory] = React.useState<HistoryEntry[]>([]);
285
+ const [showHistory, setShowHistory] = React.useState(false);
286
+ const [historyView, setHistoryView] = React.useState<HistoryEntry | null>(null);
287
+
288
+ React.useEffect(() => {
289
+ if (typeof window === "undefined") return;
290
+ localStorage.setItem("bd-api-bearer", bearerToken);
291
+ localStorage.setItem("bd-api-keyname", apiKeyName);
292
+ localStorage.setItem("bd-api-keyvalue", apiKeyValue);
293
+ localStorage.setItem("bd-api-basic-user", basicUser);
294
+ localStorage.setItem("bd-api-basic-pass", basicPass);
295
+ }, [bearerToken, apiKeyName, apiKeyValue, basicUser, basicPass]);
296
+
297
+ React.useEffect(() => {
298
+ loadHistory().then(setHistory);
299
+ }, []);
54
300
 
55
301
  function buildUrl(): string {
56
302
  let finalUrl = url;
57
303
  const queryParams = new URLSearchParams();
58
-
59
304
  for (const p of params) {
60
305
  const val = paramValues[p.name] || "";
61
306
  if (p.in === "path") {
@@ -64,31 +309,75 @@ export function ApiPlayground({
64
309
  queryParams.set(p.name, val);
65
310
  }
66
311
  }
67
-
68
312
  const qs = queryParams.toString();
69
313
  return qs ? `${finalUrl}?${qs}` : finalUrl;
70
314
  }
71
315
 
72
- async function sendRequest() {
73
- setLoading(true);
74
- setError(null);
75
- setResponse(null);
76
-
77
- const start = performance.now();
78
- const reqUrl = buildUrl();
316
+ function buildLiveUrl(): string {
317
+ let finalUrl = url;
318
+ for (const p of params) {
319
+ const val = paramValues[p.name];
320
+ if (p.in === "path") {
321
+ finalUrl = val
322
+ ? finalUrl.replace(`{${p.name}}`, val)
323
+ : finalUrl;
324
+ }
325
+ }
326
+ const queryParts: string[] = [];
327
+ for (const p of params) {
328
+ if (p.in === "query" && paramValues[p.name]) {
329
+ queryParts.push(`${p.name}=${paramValues[p.name]}`);
330
+ }
331
+ }
332
+ return queryParts.length > 0 ? `${finalUrl}?${queryParts.join("&")}` : finalUrl;
333
+ }
79
334
 
80
- const headers: Record<string, string> = { ...initialHeaders };
335
+ function getRequestHeaders(): Record<string, string> {
336
+ const h: Record<string, string> = { ...initialHeaders };
81
337
  for (const p of params) {
82
338
  if (p.in === "header" && paramValues[p.name]) {
83
- headers[p.name] = paramValues[p.name];
339
+ h[p.name] = paramValues[p.name];
84
340
  }
85
341
  }
342
+ if (bearerToken && authType === "bearer") {
343
+ h["Authorization"] = `Bearer ${bearerToken}`;
344
+ }
345
+ if (apiKeyValue && authType === "apikey") {
346
+ h[apiKeyName] = apiKeyValue;
347
+ }
348
+ if (basicUser && authType === "basic") {
349
+ h["Authorization"] = `Basic ${btoa(`${basicUser}:${basicPass}`)}`;
350
+ }
351
+ const hasBody = ["POST", "PUT", "PATCH"].includes(method) && bodyValue;
352
+ if (hasBody && !h["Content-Type"]) {
353
+ h["Content-Type"] = "application/json";
354
+ }
355
+ return h;
356
+ }
86
357
 
358
+ async function sendRequest() {
87
359
  const hasBody = ["POST", "PUT", "PATCH"].includes(method) && bodyValue;
88
- if (hasBody && !headers["Content-Type"]) {
89
- headers["Content-Type"] = "application/json";
360
+
361
+ if (hasBody) {
362
+ try {
363
+ JSON.parse(bodyValue);
364
+ setBodyError(null);
365
+ } catch (e) {
366
+ setBodyError(e instanceof Error ? e.message : "Invalid JSON");
367
+ return;
368
+ }
90
369
  }
91
370
 
371
+ setLoading(true);
372
+ setError(null);
373
+ setResponse(null);
374
+ setHistoryView(null);
375
+ setResponseTab("body");
376
+
377
+ const start = performance.now();
378
+ const reqUrl = buildUrl();
379
+ const headers = getRequestHeaders();
380
+
92
381
  try {
93
382
  const res = await fetch(reqUrl, {
94
383
  method,
@@ -97,15 +386,36 @@ export function ApiPlayground({
97
386
  });
98
387
 
99
388
  const elapsed = Math.round(performance.now() - start);
389
+ const rawText = await res.text();
390
+ const size = new Blob([rawText]).size;
100
391
  let text: string;
101
392
  try {
102
- const json = await res.json();
393
+ const json = JSON.parse(rawText);
103
394
  text = JSON.stringify(json, null, 2);
104
395
  } catch {
105
- text = await res.text();
396
+ text = rawText;
106
397
  }
107
398
 
108
- setResponse({ status: res.status, statusText: res.statusText, body: text, time: elapsed });
399
+ const resHeaders: [string, string][] = [];
400
+ res.headers.forEach((v, k) => resHeaders.push([k, v]));
401
+
402
+ const resObj = { status: res.status, statusText: res.statusText, body: text, headers: resHeaders, time: elapsed, size };
403
+ setResponse(resObj);
404
+
405
+ const historyEntry: Omit<HistoryEntry, "id"> = {
406
+ method,
407
+ url: reqUrl,
408
+ status: res.status,
409
+ statusText: res.statusText,
410
+ time: elapsed,
411
+ size,
412
+ body: text,
413
+ headers: resHeaders,
414
+ timestamp: Date.now(),
415
+ };
416
+ await saveHistory(historyEntry);
417
+ const updated = await loadHistory();
418
+ setHistory(updated);
109
419
  } catch (err) {
110
420
  setError(err instanceof Error ? err.message : "Request failed");
111
421
  } finally {
@@ -113,88 +423,367 @@ export function ApiPlayground({
113
423
  }
114
424
  }
115
425
 
426
+ const liveUrl = buildLiveUrl();
427
+ const reqHeaders = getRequestHeaders();
428
+ const hasBody = ["POST", "PUT", "PATCH"].includes(method);
429
+ const currentSnippet = generateSnippet(
430
+ snippetLang,
431
+ method,
432
+ liveUrl,
433
+ reqHeaders,
434
+ hasBody && bodyValue ? bodyValue : undefined,
435
+ );
436
+
437
+ const displayResponse = historyView || response;
438
+
116
439
  return (
117
440
  <div className={cn("bd-playground", className)}>
118
- {/* Header */}
441
+ {/* Header with method + live URL + send */}
119
442
  <div className="bd-playground-header">
120
- <span className={cn("bd-playground-method", methodColors[method])}>
443
+ <span className={cn("bd-playground-method", METHOD_COLORS[method])}>
121
444
  {method}
122
445
  </span>
123
- <code className="bd-playground-url">{url}</code>
124
- <button
125
- className="bd-playground-send"
126
- onClick={sendRequest}
127
- disabled={loading}
128
- >
129
- {loading ? "Sending..." : "Send"}
446
+ <code className="bd-playground-url">{liveUrl}</code>
447
+ <button className="bd-playground-send" onClick={sendRequest} disabled={loading}>
448
+ {loading ? (
449
+ <>
450
+ <svg className="bd-pg-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
451
+ <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
452
+ </svg>
453
+ Sending...
454
+ </>
455
+ ) : (
456
+ <>
457
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
458
+ <path d="M5 12h14M12 5l7 7-7 7" />
459
+ </svg>
460
+ Send
461
+ </>
462
+ )}
463
+ </button>
464
+ </div>
465
+
466
+ {/* Auth toggle */}
467
+ <div className="bd-pg-auth-bar">
468
+ <button className="bd-pg-auth-toggle" onClick={() => setShowAuth(!showAuth)}>
469
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
470
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
471
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
472
+ </svg>
473
+ Authentication
474
+ <svg
475
+ width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"
476
+ style={{ transform: showAuth ? "rotate(180deg)" : "none", transition: "transform 200ms" }}
477
+ >
478
+ <polyline points="6 9 12 15 18 9" />
479
+ </svg>
130
480
  </button>
131
481
  </div>
132
482
 
483
+ {showAuth && (
484
+ <div className="bd-pg-auth-panel">
485
+ <div className="bd-pg-auth-type">
486
+ <button
487
+ className={cn("bd-pg-auth-type-btn", authType === "bearer" && "active")}
488
+ onClick={() => setAuthType("bearer")}
489
+ >
490
+ Bearer Token
491
+ </button>
492
+ <button
493
+ className={cn("bd-pg-auth-type-btn", authType === "apikey" && "active")}
494
+ onClick={() => setAuthType("apikey")}
495
+ >
496
+ API Key
497
+ </button>
498
+ <button
499
+ className={cn("bd-pg-auth-type-btn", authType === "basic" && "active")}
500
+ onClick={() => setAuthType("basic")}
501
+ >
502
+ Basic Auth
503
+ </button>
504
+ </div>
505
+ {authType === "bearer" && (
506
+ <div className="bd-playground-param">
507
+ <label className="bd-playground-label">Token</label>
508
+ <input
509
+ type="text"
510
+ className="bd-playground-input"
511
+ value={bearerToken}
512
+ placeholder="Enter Bearer token..."
513
+ onChange={(e) => setBearerToken(e.target.value)}
514
+ />
515
+ </div>
516
+ )}
517
+ {authType === "apikey" && (
518
+ <>
519
+ <div className="bd-playground-param">
520
+ <label className="bd-playground-label">Header Name</label>
521
+ <input
522
+ type="text"
523
+ className="bd-playground-input"
524
+ value={apiKeyName}
525
+ placeholder="X-API-Key"
526
+ onChange={(e) => setApiKeyName(e.target.value)}
527
+ />
528
+ </div>
529
+ <div className="bd-playground-param">
530
+ <label className="bd-playground-label">Key Value</label>
531
+ <input
532
+ type="text"
533
+ className="bd-playground-input"
534
+ value={apiKeyValue}
535
+ placeholder="Enter API key..."
536
+ onChange={(e) => setApiKeyValue(e.target.value)}
537
+ />
538
+ </div>
539
+ </>
540
+ )}
541
+ {authType === "basic" && (
542
+ <>
543
+ <div className="bd-playground-param">
544
+ <label className="bd-playground-label">Username</label>
545
+ <input
546
+ type="text"
547
+ className="bd-playground-input"
548
+ value={basicUser}
549
+ placeholder="Username"
550
+ onChange={(e) => setBasicUser(e.target.value)}
551
+ />
552
+ </div>
553
+ <div className="bd-playground-param">
554
+ <label className="bd-playground-label">Password</label>
555
+ <input
556
+ type="password"
557
+ className="bd-playground-input"
558
+ value={basicPass}
559
+ placeholder="Password"
560
+ onChange={(e) => setBasicPass(e.target.value)}
561
+ />
562
+ </div>
563
+ </>
564
+ )}
565
+ </div>
566
+ )}
567
+
133
568
  {/* Parameters */}
134
569
  {params.length > 0 && (
135
570
  <div className="bd-playground-params">
136
571
  {params.map((p) => (
137
572
  <div key={p.name} className="bd-playground-param">
138
573
  <label className="bd-playground-label">
139
- <span>{p.name}</span>
574
+ <span className="bd-pg-param-name">{p.name}</span>
140
575
  <span className="bd-playground-param-meta">
141
576
  {p.in}
142
577
  {p.required && <span className="bd-playground-required">*</span>}
143
578
  </span>
144
579
  </label>
145
- <input
146
- type="text"
147
- className="bd-playground-input"
148
- value={paramValues[p.name] || ""}
149
- placeholder={p.description || p.type || ""}
150
- onChange={(e) =>
151
- setParamValues((prev) => ({ ...prev, [p.name]: e.target.value }))
152
- }
153
- />
580
+ {p.enum && p.enum.length > 0 ? (
581
+ <select
582
+ className="bd-playground-input bd-playground-select"
583
+ value={paramValues[p.name] || ""}
584
+ onChange={(e) =>
585
+ setParamValues((prev) => ({ ...prev, [p.name]: e.target.value }))
586
+ }
587
+ >
588
+ <option value="">Select {p.name}...</option>
589
+ {p.enum.map((val) => (
590
+ <option key={val} value={val}>{val}</option>
591
+ ))}
592
+ </select>
593
+ ) : (
594
+ <input
595
+ type="text"
596
+ className="bd-playground-input"
597
+ value={paramValues[p.name] || ""}
598
+ placeholder={p.description || p.type || ""}
599
+ onChange={(e) =>
600
+ setParamValues((prev) => ({ ...prev, [p.name]: e.target.value }))
601
+ }
602
+ />
603
+ )}
154
604
  </div>
155
605
  ))}
156
606
  </div>
157
607
  )}
158
608
 
159
609
  {/* Body */}
160
- {["POST", "PUT", "PATCH"].includes(method) && (
610
+ {hasBody && (
161
611
  <div className="bd-playground-body-section">
162
612
  <label className="bd-playground-label">Request Body</label>
163
613
  <textarea
164
- className="bd-playground-textarea"
614
+ className={cn("bd-playground-textarea", bodyError && "bd-playground-textarea-error")}
165
615
  value={bodyValue}
166
- onChange={(e) => setBodyValue(e.target.value)}
616
+ onChange={(e) => {
617
+ setBodyValue(e.target.value);
618
+ if (bodyError) setBodyError(null);
619
+ }}
167
620
  rows={6}
168
621
  placeholder='{ "key": "value" }'
169
622
  />
623
+ {bodyError && (
624
+ <div className="bd-pg-body-error">
625
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
626
+ <circle cx="12" cy="12" r="10" />
627
+ <line x1="15" y1="9" x2="9" y2="15" />
628
+ <line x1="9" y1="9" x2="15" y2="15" />
629
+ </svg>
630
+ Invalid JSON: {bodyError}
631
+ </div>
632
+ )}
170
633
  </div>
171
634
  )}
172
635
 
636
+ {/* Code Snippets */}
637
+ <div className="bd-pg-code-section">
638
+ <div className="bd-pg-code-tabs">
639
+ {(Object.keys(SNIPPET_LABELS) as SnippetLang[]).map((lang) => (
640
+ <button
641
+ key={lang}
642
+ className={cn("bd-pg-code-tab", snippetLang === lang && "active")}
643
+ onClick={() => setSnippetLang(lang)}
644
+ >
645
+ {SNIPPET_LABELS[lang]}
646
+ </button>
647
+ ))}
648
+ <CopyButton text={currentSnippet} />
649
+ </div>
650
+ <pre className="bd-pg-code-pre">
651
+ <code>{currentSnippet}</code>
652
+ </pre>
653
+ </div>
654
+
173
655
  {/* Response */}
174
- {(response || error) && (
656
+ {(displayResponse || error) && (
175
657
  <div className="bd-playground-response">
176
658
  {error ? (
177
659
  <div className="bd-playground-error">{error}</div>
178
- ) : response ? (
660
+ ) : displayResponse ? (
179
661
  <>
180
662
  <div className="bd-playground-response-header">
181
- <span
182
- className={cn(
183
- "bd-playground-status",
184
- response.status < 300 ? "bd-status-ok" : "bd-status-err"
185
- )}
186
- >
187
- {response.status} {response.statusText}
188
- </span>
189
- <span className="bd-playground-time">{response.time}ms</span>
663
+ <div className="bd-pg-response-meta">
664
+ <span
665
+ className={cn(
666
+ "bd-playground-status",
667
+ displayResponse.status < 300 ? "bd-status-ok" : displayResponse.status < 500 ? "bd-status-warn" : "bd-status-err",
668
+ )}
669
+ >
670
+ {displayResponse.status}
671
+ </span>
672
+ <span className="bd-pg-status-text">{displayResponse.statusText}</span>
673
+ <span className="bd-playground-time">{displayResponse.time}ms</span>
674
+ <span className="bd-playground-size">{formatSize(displayResponse.size)}</span>
675
+ </div>
676
+ <div className="bd-pg-response-tabs">
677
+ <button
678
+ className={cn("bd-pg-resp-tab", responseTab === "body" && "active")}
679
+ onClick={() => setResponseTab("body")}
680
+ >
681
+ Body
682
+ </button>
683
+ <button
684
+ className={cn("bd-pg-resp-tab", responseTab === "headers" && "active")}
685
+ onClick={() => setResponseTab("headers")}
686
+ >
687
+ Headers
688
+ <span className="bd-pg-badge">{displayResponse.headers.length}</span>
689
+ </button>
690
+ </div>
190
691
  </div>
191
- <pre className="bd-playground-pre">
192
- <code>{response.body}</code>
193
- </pre>
692
+
693
+ {historyView && (
694
+ <div className="bd-pg-history-banner">
695
+ Viewing history entry
696
+ <button className="bd-pg-history-dismiss" onClick={() => setHistoryView(null)}>
697
+ Back to current
698
+ </button>
699
+ </div>
700
+ )}
701
+
702
+ {responseTab === "body" && (
703
+ <div className="bd-pg-response-body">
704
+ <CopyButton text={displayResponse.body} />
705
+ <pre className="bd-playground-pre">
706
+ <code>{highlightJson(displayResponse.body)}</code>
707
+ </pre>
708
+ </div>
709
+ )}
710
+
711
+ {responseTab === "headers" && (
712
+ <div className="bd-pg-headers-table">
713
+ <table>
714
+ <thead>
715
+ <tr>
716
+ <th>Header</th>
717
+ <th>Value</th>
718
+ </tr>
719
+ </thead>
720
+ <tbody>
721
+ {displayResponse.headers.map(([k, v]) => (
722
+ <tr key={k}>
723
+ <td><code>{k}</code></td>
724
+ <td>{v}</td>
725
+ </tr>
726
+ ))}
727
+ </tbody>
728
+ </table>
729
+ </div>
730
+ )}
194
731
  </>
195
732
  ) : null}
196
733
  </div>
197
734
  )}
735
+
736
+ {/* History */}
737
+ {history.length > 0 && (
738
+ <div className="bd-pg-history-section">
739
+ <button
740
+ className="bd-pg-history-toggle"
741
+ onClick={() => setShowHistory(!showHistory)}
742
+ >
743
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
744
+ <circle cx="12" cy="12" r="10" />
745
+ <polyline points="12 6 12 12 16 14" />
746
+ </svg>
747
+ History
748
+ <span className="bd-pg-badge">{history.length}</span>
749
+ <svg
750
+ width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"
751
+ style={{ transform: showHistory ? "rotate(180deg)" : "none", transition: "transform 200ms", marginLeft: "auto" }}
752
+ >
753
+ <polyline points="6 9 12 15 18 9" />
754
+ </svg>
755
+ </button>
756
+ {showHistory && (
757
+ <div className="bd-pg-history-list">
758
+ {history.map((entry) => (
759
+ <button
760
+ key={entry.id}
761
+ className={cn("bd-pg-history-item", historyView?.id === entry.id && "active")}
762
+ onClick={() => {
763
+ setHistoryView(entry);
764
+ setResponseTab("body");
765
+ setError(null);
766
+ }}
767
+ >
768
+ <span className={cn("bd-pg-history-method", METHOD_COLORS[entry.method])}>
769
+ {entry.method}
770
+ </span>
771
+ <span className="bd-pg-history-url">{entry.url.replace(/^https?:\/\/[^/]+/, "")}</span>
772
+ <span
773
+ className={cn(
774
+ "bd-pg-history-status",
775
+ entry.status < 300 ? "bd-status-ok" : entry.status < 500 ? "bd-status-warn" : "bd-status-err",
776
+ )}
777
+ >
778
+ {entry.status}
779
+ </span>
780
+ <span className="bd-pg-history-time">{entry.time}ms</span>
781
+ </button>
782
+ ))}
783
+ </div>
784
+ )}
785
+ </div>
786
+ )}
198
787
  </div>
199
788
  );
200
789
  }