@kyro-cms/admin 0.9.5 → 0.9.6

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.
@@ -2,6 +2,15 @@ import React, { useState, useEffect, useCallback, useRef } from "react";
2
2
  import { useUIStore, toast } from "../lib/stores";
3
3
  import { apiPath } from "../lib/paths";
4
4
  import { Modal } from "./ui/Modal";
5
+ import {
6
+ Copy,
7
+ Download,
8
+ Check,
9
+ Play,
10
+ Plus,
11
+ X,
12
+ ChevronRight,
13
+ } from "./ui/icons";
5
14
 
6
15
  interface EnvVariable {
7
16
  key: string;
@@ -54,16 +63,26 @@ const STORAGE_KEYS = {
54
63
  env: "kyro-rest-env",
55
64
  };
56
65
 
66
+ const METHOD_COLORS: Record<string, string> = {
67
+ GET: "#22c55e",
68
+ POST: "#3b82f6",
69
+ PATCH: "#eab308",
70
+ DELETE: "#ef4444",
71
+ };
72
+
73
+ const METHOD_BG: Record<string, string> = {
74
+ GET: "rgba(34,197,94,0.12)",
75
+ POST: "rgba(59,130,246,0.12)",
76
+ PATCH: "rgba(234,179,8,0.12)",
77
+ DELETE: "rgba(239,68,68,0.12)",
78
+ };
79
+
57
80
  export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
58
- const [sidebarTab, setSidebarTab] = useState<
59
- "collections" | "saved" | "history" | "env"
60
- >("collections");
81
+ const [sidebarTab, setSidebarTab] = useState<"collections" | "saved" | "history" | "env">("collections");
61
82
  const [folders, setFolders] = useState<RequestFolder[]>([]);
62
83
  const [history, setHistory] = useState<HistoryItem[]>([]);
63
84
  const [envVars, setEnvVars] = useState<EnvVariable[]>([]);
64
- const [selectedRequest, setSelectedRequest] = useState<SavedRequest | null>(
65
- null,
66
- );
85
+ const [selectedRequest, setSelectedRequest] = useState<SavedRequest | null>(null);
67
86
  const [currentRequest, setCurrentRequest] = useState<SavedRequest>({
68
87
  id: "new",
69
88
  name: "Untitled Request",
@@ -75,64 +94,52 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
75
94
  const [response, setResponse] = useState<Record<string, unknown> | null>(null);
76
95
  const [loading, setLoading] = useState(false);
77
96
  const [error, setError] = useState<string | null>(null);
78
- const [activeTab, setActiveTab] = useState<"params" | "headers" | "body">(
79
- "params",
80
- );
97
+ const [activeEditorTab, setActiveEditorTab] = useState<"params" | "headers" | "body">("params");
81
98
  const [showFolderModal, setShowFolderModal] = useState(false);
82
99
  const [showSaveModal, setShowSaveModal] = useState(false);
83
100
  const [newFolderName, setNewFolderName] = useState("");
84
101
  const [saveToFolderId, setSaveToFolderId] = useState("");
85
102
  const [saveRequestName, setSaveRequestName] = useState("");
103
+ const [copied, setCopied] = useState(false);
104
+ const [showSidebar, setShowSidebar] = useState(true);
105
+ const [splitPos, setSplitPos] = useState(50);
106
+ const [isDragging, setIsDragging] = useState(false);
107
+ const containerRef = useRef<HTMLDivElement>(null);
108
+ const inputRef = useRef<HTMLInputElement>(null);
86
109
  const { confirm } = useUIStore();
110
+ const [isMounted, setIsMounted] = useState(false);
87
111
 
88
- // Load from localStorage
89
112
  useEffect(() => {
90
113
  const savedFolders = localStorage.getItem(STORAGE_KEYS.folders);
91
114
  if (savedFolders) setFolders(JSON.parse(savedFolders));
92
-
93
115
  const savedHistory = localStorage.getItem(STORAGE_KEYS.history);
94
116
  if (savedHistory) setHistory(JSON.parse(savedHistory));
95
-
96
117
  const savedEnv = localStorage.getItem(STORAGE_KEYS.env);
97
118
  if (savedEnv) setEnvVars(JSON.parse(savedEnv));
98
- else
99
- setEnvVars([
100
- { key: "baseUrl", value: apiPath, enabled: true },
101
- { key: "token", value: "", enabled: true },
102
- ]);
103
-
119
+ else setEnvVars([
120
+ { key: "baseUrl", value: apiPath, enabled: true },
121
+ { key: "token", value: "", enabled: true },
122
+ ]);
104
123
  setIsMounted(true);
105
124
  }, []);
106
125
 
107
- const [isMounted, setIsMounted] = useState(false);
108
-
109
- // Save to localStorage
110
126
  useEffect(() => {
111
- if (isMounted) {
112
- localStorage.setItem(STORAGE_KEYS.folders, JSON.stringify(folders));
113
- }
127
+ if (isMounted) localStorage.setItem(STORAGE_KEYS.folders, JSON.stringify(folders));
114
128
  }, [folders, isMounted]);
115
129
 
116
130
  useEffect(() => {
117
- if (isMounted) {
118
- localStorage.setItem(STORAGE_KEYS.history, JSON.stringify(history));
119
- }
131
+ if (isMounted) localStorage.setItem(STORAGE_KEYS.history, JSON.stringify(history));
120
132
  }, [history, isMounted]);
121
133
 
122
134
  useEffect(() => {
123
- if (isMounted) {
124
- localStorage.setItem(STORAGE_KEYS.env, JSON.stringify(envVars));
125
- }
135
+ if (isMounted) localStorage.setItem(STORAGE_KEYS.env, JSON.stringify(envVars));
126
136
  }, [envVars, isMounted]);
127
137
 
128
138
  const resolveUrl = (url: string) => {
129
139
  let resolved = url;
130
140
  envVars.forEach((v) => {
131
- if (v.enabled) {
132
- resolved = resolved.replace(`{{${v.key}}}`, v.value);
133
- }
141
+ if (v.enabled) resolved = resolved.replace(`{{${v.key}}}`, v.value);
134
142
  });
135
- // Default base URL if relative
136
143
  if (resolved.startsWith("/")) {
137
144
  const baseUrl = envVars.find((v) => v.key === "baseUrl" && v.enabled)?.value || apiPath;
138
145
  resolved = `${baseUrl}${resolved}`;
@@ -140,57 +147,53 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
140
147
  return resolved;
141
148
  };
142
149
 
143
- const handleSend = async () => {
150
+ const handleSend = useCallback(async () => {
144
151
  setLoading(true);
145
152
  setError(null);
146
153
  const start = Date.now();
147
154
  try {
148
155
  const url = resolveUrl(currentRequest.url);
149
156
  const headers: Record<string, string> = { ...currentRequest.headers };
150
-
151
157
  const token = envVars.find(v => v.key === 'token' && v.enabled)?.value;
152
158
  if (token) headers['Authorization'] = `Bearer ${token}`;
153
159
 
154
160
  const res = await fetch(url, {
155
161
  method: currentRequest.method,
156
- headers: {
157
- "Content-Type": "application/json",
158
- ...headers,
159
- },
160
- body:
161
- currentRequest.method !== "GET" && currentRequest.body
162
- ? currentRequest.body
163
- : undefined,
162
+ headers: { "Content-Type": "application/json", ...headers },
163
+ body: currentRequest.method !== "GET" && currentRequest.body ? currentRequest.body : undefined,
164
164
  });
165
165
 
166
166
  const duration = Date.now() - start;
167
167
  const status = res.status;
168
168
  const data = await res.json().catch(() => ({}));
169
169
 
170
- setResponse({
171
- status,
172
- duration,
173
- size: JSON.stringify(data).length,
174
- data,
175
- });
170
+ setResponse({ status, duration, size: JSON.stringify(data).length, data });
176
171
 
177
- // Add to history
178
- const historyItem: HistoryItem = {
172
+ setHistory((prev) => [{
179
173
  id: Date.now().toString(),
180
174
  timestamp: Date.now(),
181
175
  method: currentRequest.method,
182
176
  url: currentRequest.url,
183
177
  status,
184
178
  duration,
185
- };
186
- setHistory((prev) => [historyItem, ...prev].slice(0, 50));
187
- } catch (err: unknown) {
188
- const message = err instanceof Error ? err.message : "Unknown error";
189
- setError(message);
179
+ }, ...prev].slice(0, 50));
180
+ } catch (err: unknown) {
181
+ setError(err instanceof Error ? err.message : "Unknown error");
190
182
  } finally {
191
183
  setLoading(false);
192
184
  }
193
- };
185
+ }, [currentRequest, envVars]);
186
+
187
+ useEffect(() => {
188
+ const handler = (e: KeyboardEvent) => {
189
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
190
+ e.preventDefault();
191
+ handleSend();
192
+ }
193
+ };
194
+ window.addEventListener("keydown", handler);
195
+ return () => window.removeEventListener("keydown", handler);
196
+ }, [handleSend]);
194
197
 
195
198
  const loadRequest = (req: SavedRequest) => {
196
199
  setCurrentRequest(req);
@@ -201,12 +204,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
201
204
 
202
205
  const createFolder = () => {
203
206
  if (!newFolderName.trim()) return;
204
- const newFolder: RequestFolder = {
205
- id: Date.now().toString(),
206
- name: newFolderName,
207
- requests: [],
208
- };
209
- setFolders((prev) => [...prev, newFolder]);
207
+ setFolders((prev) => [...prev, { id: Date.now().toString(), name: newFolderName, requests: [] }]);
210
208
  setNewFolderName("");
211
209
  setShowFolderModal(false);
212
210
  };
@@ -216,514 +214,460 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
216
214
  title: "Delete Folder",
217
215
  message: "Are you sure? All requests inside will be deleted.",
218
216
  variant: "danger",
219
- onConfirm: () => {
220
- setFolders((prev) => prev.filter((f) => f.id !== id));
221
- }
217
+ onConfirm: () => setFolders((prev) => prev.filter((f) => f.id !== id)),
222
218
  });
223
219
  };
224
220
 
225
221
  const saveRequest = () => {
226
222
  if (!saveRequestName.trim() || !saveToFolderId) return;
227
-
228
223
  const newSavedRequest: SavedRequest = {
229
- ...currentRequest,
230
- id: Date.now().toString(),
231
- name: saveRequestName,
232
- folderId: saveToFolderId,
224
+ ...currentRequest, id: Date.now().toString(), name: saveRequestName, folderId: saveToFolderId,
233
225
  };
234
-
235
- setFolders((prev) =>
236
- prev.map((f) =>
237
- f.id === saveToFolderId
238
- ? { ...f, requests: [...f.requests, newSavedRequest] }
239
- : f,
240
- ),
241
- );
226
+ setFolders((prev) => prev.map((f) => f.id === saveToFolderId ? { ...f, requests: [...f.requests, newSavedRequest] } : f));
242
227
  setSelectedRequest(newSavedRequest);
243
228
  setShowSaveModal(false);
244
229
  };
245
230
 
246
231
  const deleteRequest = (id: string) => {
247
- setFolders((prev) =>
248
- prev.map((f) => ({
249
- ...f,
250
- requests: f.requests.filter((r) => r.id !== id),
251
- })),
252
- );
232
+ setFolders((prev) => prev.map((f) => ({ ...f, requests: f.requests.filter((r) => r.id !== id) })));
253
233
  if (selectedRequest?.id === id) setSelectedRequest(null);
254
234
  };
255
235
 
256
- const getMethodColor = (method: string) => {
257
- switch (method) {
258
- case "GET":
259
- return "bg-green-500";
260
- case "POST":
261
- return "bg-blue-500";
262
- case "PATCH":
263
- return "bg-yellow-500";
264
- case "DELETE":
265
- return "bg-red-500";
266
- default:
267
- return "bg-gray-500";
268
- }
269
- };
270
-
271
- const getStatusColor = (status: number) => {
272
- if (status === 0) return "bg-red-500";
273
- if (status < 300) return "bg-green-500";
274
- if (status < 400) return "bg-blue-500";
275
- if (status < 500) return "bg-yellow-500";
276
- return "bg-red-500";
277
- };
278
-
279
236
  const duplicateRequest = () => {
280
- const duplicate: SavedRequest = {
281
- ...currentRequest,
282
- id: Date.now().toString(),
283
- name: `${currentRequest.name} (copy)`,
284
- };
285
- setCurrentRequest(duplicate);
237
+ setCurrentRequest({ ...currentRequest, id: Date.now().toString(), name: `${currentRequest.name} (copy)` });
286
238
  setSelectedRequest(null);
287
239
  };
288
240
 
289
- // Export all data
290
241
  const exportData = () => {
291
- const data = {
292
- version: "1.0",
293
- exportedAt: new Date().toISOString(),
294
- folders,
295
- history: history.slice(0, 50),
296
- envVars,
297
- };
298
-
299
- const blob = new Blob([JSON.stringify(data, null, 2)], {
300
- type: "application/json",
301
- });
302
- const url = URL.createObjectURL(blob);
242
+ const blob = new Blob([JSON.stringify({ version: "1.0", exportedAt: new Date().toISOString(), folders, history: history.slice(0, 50), envVars }, null, 2)], { type: "application/json" });
243
+ const blobUrl = URL.createObjectURL(blob);
303
244
  const a = document.createElement("a");
304
- a.href = url;
245
+ a.href = blobUrl;
305
246
  a.download = `kyro-rest-playground-${Date.now()}.json`;
306
- document.body.appendChild(a);
307
247
  a.click();
308
- document.body.removeChild(a);
309
- URL.revokeObjectURL(url);
248
+ URL.revokeObjectURL(blobUrl);
310
249
  };
311
250
 
312
- // Import data
313
251
  const importData = (file: File) => {
314
252
  const reader = new FileReader();
315
253
  reader.onload = (e) => {
316
254
  try {
317
255
  const data = JSON.parse(e.target?.result as string);
318
-
319
- if (data.folders) {
320
- setFolders((prev) => [...prev, ...data.folders]);
321
- }
322
- if (data.history) {
323
- setHistory((prev) => [...data.history, ...prev].slice(0, 50));
324
- }
325
- if (data.envVars) {
326
- setEnvVars((prev) => [...prev, ...data.envVars]);
327
- }
328
-
256
+ if (data.folders) setFolders((prev) => [...prev, ...data.folders]);
257
+ if (data.history) setHistory((prev) => [...data.history, ...prev].slice(0, 50));
258
+ if (data.envVars) setEnvVars((prev) => [...prev, ...data.envVars]);
329
259
  toast.success("Your playground data has been imported.");
330
- } catch (err) {
331
- toast.error("Invalid JSON file structure.");
332
- }
260
+ } catch { toast.error("Invalid JSON file structure."); }
333
261
  };
334
262
  reader.readAsText(file);
335
263
  };
336
264
 
337
- // Clear all data
338
265
  const clearAllData = () => {
339
266
  confirm({
340
267
  title: "Clear All Data?",
341
- message: "Are you sure you want to clear all saved requests, history, and environment variables? This action cannot be undone.",
268
+ message: "Are you sure? This action cannot be undone.",
342
269
  variant: "danger",
343
270
  onConfirm: () => {
344
271
  setFolders([]);
345
272
  setHistory([]);
346
- setEnvVars([
347
- { key: "baseUrl", value: apiPath, enabled: true },
348
- { key: "token", value: "", enabled: true },
349
- ]);
273
+ setEnvVars([{ key: "baseUrl", value: apiPath, enabled: true }, { key: "token", value: "", enabled: true }]);
350
274
  localStorage.removeItem(STORAGE_KEYS.folders);
351
275
  localStorage.removeItem(STORAGE_KEYS.history);
352
276
  localStorage.removeItem(STORAGE_KEYS.env);
353
- }
277
+ },
354
278
  });
355
279
  };
356
280
 
281
+ const handleCopyResponse = async () => {
282
+ if (response?.data) {
283
+ await navigator.clipboard.writeText(JSON.stringify(response.data, null, 2));
284
+ setCopied(true);
285
+ setTimeout(() => setCopied(false), 2000);
286
+ }
287
+ };
288
+
289
+ const handleDownloadResponse = () => {
290
+ if (!response?.data) return;
291
+ const blob = new Blob([JSON.stringify(response.data, null, 2)], { type: "application/json" });
292
+ const blobUrl = URL.createObjectURL(blob);
293
+ const a = document.createElement("a");
294
+ a.href = blobUrl;
295
+ a.download = "rest-response.json";
296
+ a.click();
297
+ URL.revokeObjectURL(blobUrl);
298
+ };
299
+
300
+ const handlePrettifyBody = () => {
301
+ try {
302
+ const parsed = JSON.parse(currentRequest.body || "{}");
303
+ setCurrentRequest((prev) => ({ ...prev, body: JSON.stringify(parsed, null, 2) }));
304
+ } catch { /* ignore */ }
305
+ };
306
+
307
+ const startDrag = useCallback((e: React.MouseEvent) => {
308
+ e.preventDefault();
309
+ setIsDragging(true);
310
+ }, []);
311
+
312
+ useEffect(() => {
313
+ if (!isDragging) return;
314
+ const onMove = (e: MouseEvent) => {
315
+ if (!containerRef.current) return;
316
+ const rect = containerRef.current.getBoundingClientRect();
317
+ setSplitPos(Math.max(20, Math.min(80, ((e.clientX - rect.left) / rect.width) * 100)));
318
+ };
319
+ const onUp = () => setIsDragging(false);
320
+ window.addEventListener("mousemove", onMove);
321
+ window.addEventListener("mouseup", onUp);
322
+ return () => { window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); };
323
+ }, [isDragging]);
324
+
325
+ const editorPills = [
326
+ { key: "params" as const, label: "Params" },
327
+ { key: "headers" as const, label: "Headers" },
328
+ { key: "body" as const, label: "Body" },
329
+ ];
330
+
331
+ const sidebarPills = [
332
+ { key: "collections" as const, label: "Collections" },
333
+ { key: "saved" as const, label: "Saved" },
334
+ { key: "history" as const, label: "History" },
335
+ { key: "env" as const, label: "Env" },
336
+ ];
337
+
338
+ const methodColor = METHOD_COLORS[currentRequest.method] || "#6b7280";
339
+
357
340
  return (
358
- <div className="h-full flex">
359
- {/* Left Sidebar */}
360
- <div className="w-72 flex-shrink-0 flex flex-col border-r border-[var(--kyro-border)]">
361
- {/* Tabs */}
362
- <div className="flex border-b border-[var(--kyro-border)]">
363
- {(["collections", "saved", "history", "env"] as const).map((tab) => (
364
- <button type="button"
365
- key={tab}
366
- onClick={() => setSidebarTab(tab)}
367
- className={`flex-1 px-2 py-2 text-xs font-bold transition-colors ${sidebarTab === tab
368
- ? "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] border-b-2 border-pink-500"
369
- : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
370
- }`}
371
- >
372
- {tab.charAt(0).toUpperCase() + tab.slice(1)}
373
- </button>
374
- ))}
341
+ <div ref={containerRef} className="h-full flex flex-col bg-[var(--kyro-bg)] overflow-hidden rounded-lg border border-[var(--kyro-border)]">
342
+ {/* Compact top bar */}
343
+ <div className="flex items-center gap-2 px-3 py-2 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)] shrink-0">
344
+ <button
345
+ onClick={() => setShowSidebar(!showSidebar)}
346
+ className="p-1 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] md:hidden"
347
+ title="Toggle sidebar"
348
+ >
349
+ <ChevronRight className={`w-4 h-4 transition-transform ${showSidebar ? "rotate-180" : ""}`} />
350
+ </button>
351
+ <select
352
+ value={currentRequest.method}
353
+ onChange={(e) => setCurrentRequest({ ...currentRequest, method: e.target.value })}
354
+ className="px-2 py-1 text-[10px] font-bold rounded-md border-0 text-white"
355
+ style={{ backgroundColor: methodColor }}
356
+ >
357
+ <option value="GET">GET</option>
358
+ <option value="POST">POST</option>
359
+ <option value="PATCH">PATCH</option>
360
+ <option value="DELETE">DELETE</option>
361
+ </select>
362
+ <input
363
+ ref={inputRef}
364
+ type="text"
365
+ value={currentRequest.url}
366
+ onChange={(e) => setCurrentRequest({ ...currentRequest, url: e.target.value })}
367
+ placeholder="https://api.example.com/endpoint"
368
+ className="flex-1 px-3 py-1.5 text-xs bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-md text-[var(--kyro-text-primary)] placeholder:text-[var(--kyro-text-muted)] focus:outline-none focus:border-[var(--kyro-primary)] font-mono"
369
+ />
370
+ <div className="flex items-center gap-1">
371
+ <button
372
+ onClick={() => setShowSaveModal(true)}
373
+ className="px-2.5 py-1.5 text-[10px] font-semibold rounded-md border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] transition-all"
374
+ >
375
+ Save
376
+ </button>
377
+ <button
378
+ onClick={handleSend}
379
+ disabled={loading || !currentRequest.url}
380
+ className="flex items-center gap-1 px-3 py-1.5 rounded-md bg-[var(--kyro-primary)] text-white text-xs font-semibold hover:opacity-90 disabled:opacity-50 transition-all shadow-sm"
381
+ >
382
+ <Play className="w-3 h-3" />
383
+ {loading ? "..." : "Send"}
384
+ </button>
375
385
  </div>
386
+ </div>
376
387
 
377
- {/* Content */}
378
- <div className="flex-1 overflow-y-auto p-3">
379
- {/* Collections */}
380
- {sidebarTab === "collections" && (
381
- <div className="space-y-3">
382
- {collections.map((col) => (
383
- <div key={col.slug}>
384
- <h3 className="text-xs font-bold text-[var(--kyro-text-muted)] mb-2 px-2">
385
- {col.name}
386
- </h3>
387
- {[
388
- { method: "GET", path: col.endpoints.list, name: "List" },
389
- {
390
- method: "POST",
391
- path: col.endpoints.create,
392
- name: "Create",
393
- },
394
- {
395
- method: "GET",
396
- path: col.endpoints.read,
397
- name: "Get by ID",
398
- },
399
- {
400
- method: "PATCH",
401
- path: col.endpoints.update,
402
- name: "Update",
403
- },
404
- {
405
- method: "DELETE",
406
- path: col.endpoints.delete,
407
- name: "Delete",
408
- },
409
- ].map((endpoint, i) => (
410
- <button type="button"
411
- key={i}
412
- onClick={() => {
413
- setCurrentRequest((prev) => ({
414
- ...prev,
415
- method: endpoint.method as SavedRequest["method"],
416
- url: endpoint.path,
417
- name: `${col.name} - ${endpoint.name}`,
418
- }));
419
- setResponse("");
420
- }}
421
- className="w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs hover:bg-[var(--kyro-surface-accent)] transition-colors"
422
- >
423
- <span
424
- className={`${getMethodColor(endpoint.method)} text-white px-1.5 py-0.5 rounded text-[10px] font-bold`}
425
- >
426
- {endpoint.method}
427
- </span>
428
- <span className="text-[var(--kyro-text-secondary)] truncate">
429
- {endpoint.name}
430
- </span>
431
- </button>
432
- ))}
433
- </div>
388
+ <div className="flex-1 flex overflow-hidden">
389
+ {/* Left sidebar */}
390
+ {showSidebar && (
391
+ <div className="w-60 flex-shrink-0 flex flex-col border-r border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
392
+ <div className="flex border-b border-[var(--kyro-border)]">
393
+ {sidebarPills.map((p) => (
394
+ <button
395
+ key={p.key}
396
+ onClick={() => setSidebarTab(p.key)}
397
+ className={`flex-1 px-1 py-1.5 text-[9px] font-semibold tracking-wider transition-all ${
398
+ sidebarTab === p.key
399
+ ? "text-[var(--kyro-primary)] border-b-2 border-[var(--kyro-primary)] bg-[var(--kyro-surface-accent)]"
400
+ : "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)]"
401
+ }`}
402
+ >
403
+ {p.label}
404
+ </button>
434
405
  ))}
435
406
  </div>
436
- )}
437
407
 
438
- {/* Saved */}
439
- {sidebarTab === "saved" && (
440
- <div className="space-y-3">
441
- <div className="flex items-center justify-between mb-2">
442
- <button type="button"
443
- onClick={() => setShowFolderModal(true)}
444
- className="text-xs font-bold text-pink-500 hover:underline"
445
- >
446
- + New Folder
447
- </button>
448
- <div className="flex gap-2">
449
- <button type="button" onClick={exportData} className="text-xs text-[var(--kyro-text-muted)] hover:text-pink-500" title="Export">
450
- Export
451
- </button>
452
- <label className="text-xs text-[var(--kyro-text-muted)] hover:text-pink-500 cursor-pointer">
453
- Import
454
- <input type="file" className="hidden" accept=".json" onChange={(e) => e.target.files?.[0] && importData(e.target.files[0])} />
455
- </label>
408
+ <div className="flex-1 overflow-y-auto p-2">
409
+ {sidebarTab === "collections" && (
410
+ <div className="space-y-2">
411
+ {collections.map((col) => (
412
+ <div key={col.slug}>
413
+ <h3 className="text-[10px] font-bold text-[var(--kyro-text-muted)] mb-1 px-2">{col.name}</h3>
414
+ {[
415
+ { method: "GET", path: col.endpoints.list, name: "List" },
416
+ { method: "POST", path: col.endpoints.create, name: "Create" },
417
+ { method: "GET", path: col.endpoints.read, name: "Get" },
418
+ { method: "PATCH", path: col.endpoints.update, name: "Update" },
419
+ { method: "DELETE", path: col.endpoints.delete, name: "Delete" },
420
+ ].map((ep, i) => (
421
+ <button
422
+ key={i}
423
+ onClick={() => {
424
+ setCurrentRequest((prev) => ({ ...prev, method: ep.method, url: ep.path, name: `${col.name} - ${ep.name}` }));
425
+ setResponse(null);
426
+ }}
427
+ className="w-full flex items-center gap-2 px-2 py-1 rounded text-[10px] hover:bg-[var(--kyro-surface-accent)] transition-colors"
428
+ >
429
+ <span
430
+ className="text-white px-1 py-0.5 rounded text-[8px] font-bold"
431
+ style={{ backgroundColor: METHOD_COLORS[ep.method] }}
432
+ >
433
+ {ep.method}
434
+ </span>
435
+ <span className="text-[var(--kyro-text-secondary)] truncate">{ep.name}</span>
436
+ </button>
437
+ ))}
438
+ </div>
439
+ ))}
456
440
  </div>
457
- </div>
458
- {folders.length === 0 && (
459
- <p className="text-xs text-[var(--kyro-text-muted)] text-center py-4">
460
- No saved requests yet
461
- </p>
462
441
  )}
463
- {folders.map((folder) => (
464
- <div key={folder.id}>
465
- <div className="flex items-center justify-between mb-2 px-2">
466
- <h3 className="text-xs font-bold text-[var(--kyro-text-muted)] ">
467
- {folder.name}
468
- </h3>
469
- <button type="button" onClick={() => deleteFolder(folder.id)} className="text-[var(--kyro-text-muted)] hover:text-red-500">
470
- ×
442
+
443
+ {sidebarTab === "saved" && (
444
+ <div className="space-y-2">
445
+ <div className="flex items-center justify-between px-2 mb-1">
446
+ <button onClick={() => setShowFolderModal(true)} className="text-[10px] font-semibold text-[var(--kyro-primary)] hover:underline flex items-center gap-1">
447
+ <Plus className="w-3 h-3" /> Folder
471
448
  </button>
449
+ <div className="flex gap-2">
450
+ <button onClick={exportData} className="text-[9px] text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)]">Export</button>
451
+ <label className="text-[9px] text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] cursor-pointer">
452
+ Import
453
+ <input type="file" className="hidden" accept=".json" onChange={(e) => e.target.files?.[0] && importData(e.target.files[0])} />
454
+ </label>
455
+ </div>
456
+ </div>
457
+ {folders.length === 0 && (
458
+ <p className="text-[10px] text-[var(--kyro-text-muted)] text-center py-4">No saved requests yet</p>
459
+ )}
460
+ {folders.map((folder) => (
461
+ <div key={folder.id}>
462
+ <div className="flex items-center justify-between px-2 mb-1">
463
+ <h3 className="text-[10px] font-bold text-[var(--kyro-text-muted)]">{folder.name}</h3>
464
+ <button onClick={() => deleteFolder(folder.id)} className="text-[var(--kyro-text-muted)] hover:text-[var(--kyro-danger)]">
465
+ <X className="w-3 h-3" />
466
+ </button>
467
+ </div>
468
+ {folder.requests.map((req) => (
469
+ <div
470
+ key={req.id}
471
+ className={`flex items-center gap-2 px-2 py-1 rounded cursor-pointer transition-colors ${
472
+ selectedRequest?.id === req.id
473
+ ? "bg-[var(--kyro-primary)] text-white"
474
+ : "hover:bg-[var(--kyro-surface-accent)]"
475
+ }`}
476
+ onClick={() => loadRequest(req)}
477
+ >
478
+ <span
479
+ className="text-white px-1 py-0.5 rounded text-[8px] font-bold"
480
+ style={{ backgroundColor: METHOD_COLORS[req.method] }}
481
+ >
482
+ {req.method}
483
+ </span>
484
+ <span className={`flex-1 text-[10px] truncate ${selectedRequest?.id === req.id ? "text-white" : "text-[var(--kyro-text-secondary)]"}`}>
485
+ {req.name}
486
+ </span>
487
+ <button onClick={(e) => { e.stopPropagation(); deleteRequest(req.id); }}
488
+ className="opacity-0 group-hover:opacity-100 text-[var(--kyro-text-muted)] hover:text-[var(--kyro-danger)]"
489
+ >
490
+ <X className="w-2.5 h-2.5" />
491
+ </button>
492
+ </div>
493
+ ))}
494
+ </div>
495
+ ))}
496
+ </div>
497
+ )}
498
+
499
+ {sidebarTab === "history" && (
500
+ <div className="space-y-1">
501
+ <div className="flex justify-end px-2 mb-1">
502
+ <button onClick={() => setHistory([])} className="text-[9px] font-semibold text-[var(--kyro-danger)] hover:underline">Clear</button>
472
503
  </div>
473
- {folder.requests.map((req) => (
504
+ {history.map((item) => (
474
505
  <div
475
- key={req.id}
476
- className={`group flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer transition-colors ${selectedRequest?.id === req.id
477
- ? "bg-[var(--kyro-sidebar-active)]"
478
- : "hover:bg-[var(--kyro-surface-accent)]"
479
- }`}
480
- onClick={() => loadRequest(req)}
506
+ key={item.id}
507
+ className="p-1.5 rounded hover:bg-[var(--kyro-surface-accent)] cursor-pointer transition-colors border-b border-[var(--kyro-border)] last:border-0"
508
+ onClick={() => { setCurrentRequest((prev) => ({ ...prev, method: item.method, url: item.url })); setResponse(null); }}
481
509
  >
482
- <span
483
- className={`${getMethodColor(req.method)} text-white px-1 py-0.5 rounded text-[10px] font-bold`}
484
- >
485
- {req.method}
486
- </span>
487
- <span className={`flex-1 text-xs truncate ${selectedRequest?.id === req.id ? "text-[var(--kyro-sidebar-text-active)]" : "text-[var(--kyro-text-secondary)]"}`}>
488
- {req.name}
489
- </span>
490
- <button type="button"
491
- onClick={(e) => {
492
- e.stopPropagation();
493
- deleteRequest(req.id);
494
- }}
495
- className="opacity-0 group-hover:opacity-100 text-[var(--kyro-text-muted)] hover:text-red-500"
496
- >
497
- ×
498
- </button>
510
+ <div className="flex items-center justify-between gap-1 mb-0.5">
511
+ <span className="text-white px-1 py-0.5 rounded text-[8px] font-bold" style={{ backgroundColor: METHOD_COLORS[item.method] }}>
512
+ {item.method}
513
+ </span>
514
+ <span
515
+ className="text-white px-1 py-0.5 rounded text-[8px] font-bold"
516
+ style={{ backgroundColor: item.status < 400 ? METHOD_COLORS.GET : METHOD_COLORS.DELETE }}
517
+ >
518
+ {item.status}
519
+ </span>
520
+ </div>
521
+ <div className="text-[9px] text-[var(--kyro-text-secondary)] truncate mb-0.5">{item.url}</div>
522
+ <div className="text-[8px] text-[var(--kyro-text-muted)] flex justify-between">
523
+ <span>{new Date(item.timestamp).toLocaleTimeString()}</span>
524
+ <span>{item.duration}ms</span>
525
+ </div>
499
526
  </div>
500
527
  ))}
501
528
  </div>
502
- ))}
503
- </div>
504
- )}
529
+ )}
505
530
 
506
- {/* History */}
507
- {sidebarTab === "history" && (
508
- <div className="space-y-1">
509
- <div className="flex justify-end mb-2">
510
- <button type="button" onClick={() => setHistory([])} className="text-[10px] font-bold text-red-500 hover:underline">
511
- Clear History
512
- </button>
513
- </div>
514
- {history.map((item) => (
515
- <div
516
- key={item.id}
517
- className="p-2 rounded hover:bg-[var(--kyro-surface-accent)] cursor-pointer transition-colors border-b border-[var(--kyro-border)] last:border-0"
518
- onClick={() => {
519
- setCurrentRequest((prev) => ({
520
- ...prev,
521
- method: item.method as SavedRequest["method"],
522
- url: item.url,
523
- }));
524
- setResponse(null);
525
- }}
526
- >
527
- <div className="flex items-center justify-between mb-1">
528
- <span
529
- className={`${getMethodColor(item.method)} text-white px-1 py-0.5 rounded text-[10px] font-bold`}
530
- >
531
- {item.method}
532
- </span>
533
- <span
534
- className={`${getStatusColor(item.status)} text-white px-1 py-0.5 rounded text-[10px] font-bold`}
535
- >
536
- {item.status}
537
- </span>
538
- </div>
539
- <div className="text-[10px] text-[var(--kyro-text-secondary)] truncate mb-1">
540
- {item.url}
541
- </div>
542
- <div className="text-[9px] text-[var(--kyro-text-muted)] flex justify-between">
543
- <span>{new Date(item.timestamp).toLocaleTimeString()}</span>
544
- <span>{item.duration}ms</span>
531
+ {sidebarTab === "env" && (
532
+ <div className="space-y-3">
533
+ <div className="flex justify-between items-center px-2">
534
+ <h3 className="text-[10px] font-bold text-[var(--kyro-text-muted)]">Variables</h3>
535
+ <button onClick={clearAllData} className="text-[9px] text-[var(--kyro-danger)] font-semibold hover:underline">Reset</button>
545
536
  </div>
537
+ {envVars.map((env, i) => (
538
+ <div key={i} className="p-2 bg-[var(--kyro-surface-accent)] rounded-lg border border-[var(--kyro-border)] space-y-1.5">
539
+ <div className="flex items-center justify-between">
540
+ <span className="text-[9px] font-bold text-[var(--kyro-text-muted)]">{env.key}</span>
541
+ <input type="checkbox" checked={env.enabled} onChange={() => { const n = [...envVars]; n[i].enabled = !n[i].enabled; setEnvVars(n); }} className="accent-[var(--kyro-primary)]" />
542
+ </div>
543
+ <input type="text" value={env.value} onChange={(e) => { const n = [...envVars]; n[i].value = e.target.value; setEnvVars(n); }}
544
+ className="w-full bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded px-2 py-1 text-[10px] font-mono text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
545
+ />
546
+ </div>
547
+ ))}
548
+ <button onClick={() => setEnvVars([...envVars, { key: "", value: "", enabled: true }])}
549
+ className="w-full py-1.5 border-2 border-dashed border-[var(--kyro-border)] rounded-lg text-[10px] text-[var(--kyro-text-muted)] hover:border-[var(--kyro-primary)] hover:text-[var(--kyro-primary)] transition-all"
550
+ >
551
+ + Add Variable
552
+ </button>
546
553
  </div>
547
- ))}
554
+ )}
548
555
  </div>
549
- )}
550
-
551
- {/* Environment */}
552
- {sidebarTab === "env" && (
553
- <div className="space-y-4">
554
- <div className="flex justify-between items-center">
555
- <h3 className="text-xs font-bold text-[var(--kyro-text-muted)] ">Environment</h3>
556
- <button type="button" onClick={clearAllData} className="text-[10px] text-red-500 font-bold hover:underline">Reset All</button>
557
- </div>
558
- <div className="space-y-3">
559
- {envVars.map((env, i) => (
560
- <div key={i} className="p-3 bg-[var(--kyro-surface-accent)] rounded-lg border border-[var(--kyro-border)] space-y-2">
561
- <div className="flex items-center justify-between">
562
- <span className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)]">{env.key}</span>
563
- <input type="checkbox" checked={env.enabled} onChange={() => {
564
- const next = [...envVars];
565
- next[i].enabled = !next[i].enabled;
566
- setEnvVars(next);
567
- }} />
568
- </div>
569
- <input
570
- type="text"
571
- value={env.value}
572
- onChange={(e) => {
573
- const next = [...envVars];
574
- next[i].value = e.target.value;
575
- setEnvVars(next);
576
- }}
577
- className="w-full bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded px-2 py-1 text-xs"
578
- />
579
- </div>
580
- ))}
581
- <button type="button" onClick={() => setEnvVars([...envVars, { key: "", value: "", enabled: true }])} className="w-full py-2 border-2 border-dashed border-[var(--kyro-border)] rounded-lg text-xs text-[var(--kyro-text-muted)] hover:border-pink-500 hover:text-pink-500 transition-all">
582
- + Add Variable
556
+ </div>
557
+ )}
558
+
559
+ {/* Main split area */}
560
+ <div className="flex-1 flex overflow-hidden">
561
+ {/* Left: Editor */}
562
+ <div className="flex flex-col overflow-hidden border-r border-[var(--kyro-border)]" style={{ width: showSidebar ? `${splitPos}%` : "100%" }}>
563
+ {/* Editor pills */}
564
+ <div className="flex gap-0.5 px-3 py-1.5 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
565
+ {editorPills.map((p) => (
566
+ <button
567
+ key={p.key}
568
+ onClick={() => setActiveEditorTab(p.key)}
569
+ className={`px-2.5 py-1 text-[10px] font-semibold rounded-md transition-all ${
570
+ activeEditorTab === p.key
571
+ ? "bg-[var(--kyro-primary)] text-white"
572
+ : "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"
573
+ }`}
574
+ >
575
+ {p.label}
583
576
  </button>
584
- </div>
577
+ ))}
578
+ {activeEditorTab === "body" && (
579
+ <button onClick={handlePrettifyBody} className="ml-auto px-2 py-1 text-[10px] font-semibold rounded-md text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]">
580
+ Prettify
581
+ </button>
582
+ )}
585
583
  </div>
586
- )}
587
- </div>
588
- </div>
589
584
 
590
- {/* Main Area */}
591
- <div className="flex-1 flex flex-col bg-[var(--kyro-bg)]">
592
- {/* URL Bar */}
593
- <div className="p-4 border-b border-[var(--kyro-border)] flex gap-2">
594
- <select
595
- value={currentRequest.method}
596
- onChange={(e) =>
597
- setCurrentRequest({ ...currentRequest, method: e.target.value })
598
- }
599
- className={`px-3 py-2 rounded-lg font-bold text-white ${getMethodColor(currentRequest.method)}`}
600
- >
601
- <option value="GET">GET</option>
602
- <option value="POST">POST</option>
603
- <option value="PATCH">PATCH</option>
604
- <option value="DELETE">DELETE</option>
605
- </select>
606
- <div className="flex-1 relative">
607
- <input
608
- type="text"
609
- value={currentRequest.url}
610
- onChange={(e) =>
611
- setCurrentRequest({ ...currentRequest, url: e.target.value })
612
- }
613
- placeholder="Enter request URL..."
614
- className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2 text-sm focus:outline-none focus:border-pink-500"
615
- />
616
- </div>
617
- <button type="button"
618
- onClick={handleSend}
619
- disabled={loading}
620
- className="px-6 py-2 bg-pink-500 text-white rounded-lg font-bold hover:bg-pink-600 transition-colors disabled:opacity-50"
621
- >
622
- {loading ? "..." : "Send"}
623
- </button>
624
- <button type="button"
625
- onClick={() => setShowSaveModal(true)}
626
- className="px-4 py-2 bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-primary)] rounded-lg font-bold hover:bg-[var(--kyro-surface)] border border-[var(--kyro-border)]"
627
- >
628
- Save
629
- </button>
630
- </div>
631
-
632
- {/* Request Tabs */}
633
- <div className="flex border-b border-[var(--kyro-border)]">
634
- {(["params", "headers", "body"] as const).map((tab) => (
635
- <button type="button"
636
- key={tab}
637
- onClick={() => setActiveTab(tab)}
638
- className={`px-6 py-2 text-xs font-bold tracking-widest ${activeTab === tab
639
- ? "text-pink-500 border-b-2 border-pink-500"
640
- : "text-[var(--kyro-text-muted)]"
641
- }`}
642
- >
643
- {tab}
644
- </button>
645
- ))}
646
- </div>
647
-
648
- <div className="flex-1 flex flex-col overflow-hidden">
649
- <div className="flex-1 p-4 overflow-y-auto">
650
- {activeTab === "params" && (
651
- <div className="text-xs text-[var(--kyro-text-muted)]">
652
- Use query parameters in the URL (e.g. ?limit=10)
653
- </div>
654
- )}
655
- {activeTab === "headers" && (
656
- <div className="space-y-2">
657
- <div className="text-xs text-[var(--kyro-text-muted)] mb-4">
658
- Add custom headers as JSON
585
+ {/* Editor content */}
586
+ <div className="flex-1 overflow-hidden">
587
+ {activeEditorTab === "params" && (
588
+ <div className="flex items-center justify-center h-full text-[11px] text-[var(--kyro-text-muted)]">
589
+ Use query parameters in the URL (e.g. <code className="mx-1 px-1 py-0.5 bg-[var(--kyro-surface-accent)] rounded font-mono">?limit=10</code>)
659
590
  </div>
591
+ )}
592
+ {activeEditorTab === "headers" && (
660
593
  <textarea
661
594
  value={JSON.stringify(currentRequest.headers, null, 2)}
662
- onChange={(e) => {
663
- try {
664
- const headers = JSON.parse(e.target.value);
665
- setCurrentRequest({ ...currentRequest, headers });
666
- } catch (e) { }
667
- }}
668
- className="w-full h-40 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg p-3 font-mono text-xs focus:outline-none focus:border-pink-500"
595
+ onChange={(e) => { try { setCurrentRequest({ ...currentRequest, headers: JSON.parse(e.target.value) }); } catch { /* ignore */ } }}
596
+ className="w-full h-full bg-[var(--kyro-bg)] border-0 p-3 font-mono text-[11px] text-[var(--kyro-text-primary)] resize-none focus:outline-none placeholder:text-[var(--kyro-text-muted)]"
669
597
  placeholder='{ "X-Custom-Header": "value" }'
670
598
  />
671
- </div>
672
- )}
673
- {activeTab === "body" && (
674
- <div className="h-full flex flex-col">
675
- <div className="text-xs text-[var(--kyro-text-muted)] mb-4">
676
- Request Body (JSON)
677
- </div>
599
+ )}
600
+ {activeEditorTab === "body" && (
678
601
  <textarea
679
602
  value={currentRequest.body}
680
- onChange={(e) =>
681
- setCurrentRequest({ ...currentRequest, body: e.target.value })
682
- }
683
- className="flex-1 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg p-3 font-mono text-xs focus:outline-none focus:border-pink-500 min-h-[200px]"
603
+ onChange={(e) => setCurrentRequest({ ...currentRequest, body: e.target.value })}
604
+ className="w-full h-full bg-[var(--kyro-bg)] border-0 p-3 font-mono text-[11px] text-[var(--kyro-text-primary)] resize-none focus:outline-none placeholder:text-[var(--kyro-text-muted)]"
684
605
  placeholder='{ "key": "value" }'
685
606
  />
686
- </div>
687
- )}
607
+ )}
608
+ </div>
609
+
610
+ {/* Status bar */}
611
+ <div className="flex items-center justify-between px-3 py-1 border-t border-[var(--kyro-border)] bg-[var(--kyro-surface)] text-[9px] text-[var(--kyro-text-muted)] font-mono">
612
+ <span>{currentRequest.method} {currentRequest.url || "No URL"}</span>
613
+ <span>{currentRequest.body?.length || 0} chars</span>
614
+ </div>
688
615
  </div>
689
616
 
690
- {/* Response Pane */}
691
- <div className="h-1/2 border-t border-[var(--kyro-border)] flex flex-col overflow-hidden bg-[var(--kyro-bg-secondary)]">
692
- <div className="px-4 py-2 bg-[var(--kyro-surface)] border-b border-[var(--kyro-border)] flex items-center justify-between">
693
- <span className="text-xs font-bold text-[var(--kyro-text-secondary)] tracking-widest">
694
- Response
695
- </span>
617
+ {/* Drag handle */}
618
+ {showSidebar && (
619
+ <div
620
+ className="absolute top-0 bottom-0 z-10 w-1.5 cursor-col-resize group"
621
+ style={{ left: `calc(${splitPos}% - 3px)` }}
622
+ onMouseDown={startDrag}
623
+ >
624
+ <div className="w-0.5 h-full mx-auto bg-transparent group-hover:bg-[var(--kyro-primary)] group-hover:opacity-40 transition-all" />
625
+ </div>
626
+ )}
627
+
628
+ {/* Right: Response */}
629
+ <div className="flex-1 flex flex-col overflow-hidden min-w-0" style={{ width: showSidebar ? `${100 - splitPos}%` : "50%" }}>
630
+ <div className="flex items-center gap-2 px-3 py-1.5 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
631
+ <span className="text-[10px] font-semibold text-[var(--kyro-text-secondary)]">Response</span>
696
632
  {response && (
697
- <div className="flex gap-4">
698
- <span className="text-[10px] font-bold">
699
- STATUS:{" "}
700
- <span className={response.status < 400 ? "text-green-500" : "text-red-500"}>
701
- {response.status}
702
- </span>
633
+ <>
634
+ {response.duration > 0 && (
635
+ <span className="text-[9px] font-mono text-[var(--kyro-text-muted)]">{response.duration}ms</span>
636
+ )}
637
+ <span className={`text-[9px] font-semibold px-1.5 py-0.5 rounded ${
638
+ (response.status as number) < 400 ? "bg-[var(--kyro-success-bg)] text-[var(--kyro-success)]" : "bg-[var(--kyro-danger-bg)] text-[var(--kyro-danger)]"
639
+ }`}>
640
+ {response.status as number}
703
641
  </span>
704
- <span className="text-[10px] font-bold text-[var(--kyro-text-muted)]">
705
- TIME: {response.duration}ms
706
- </span>
707
- <span className="text-[10px] font-bold text-[var(--kyro-text-muted)]">
708
- SIZE: {response.size}B
709
- </span>
710
- </div>
642
+ <span className="text-[9px] font-mono text-[var(--kyro-text-muted)]">{response.size}B</span>
643
+ </>
711
644
  )}
645
+ <div className="ml-auto flex items-center gap-1">
646
+ <button onClick={handleCopyResponse} className="p-1 rounded text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title="Copy response">
647
+ {copied ? <Check className="w-3 h-3 text-[var(--kyro-success)]" /> : <Copy className="w-3 h-3" />}
648
+ </button>
649
+ <button onClick={handleDownloadResponse} className="p-1 rounded text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title="Download response">
650
+ <Download className="w-3 h-3" />
651
+ </button>
652
+ </div>
712
653
  </div>
713
- <div className="flex-1 p-4 overflow-auto">
654
+ <div className="flex-1 overflow-auto bg-[var(--kyro-bg-secondary)]">
714
655
  {loading ? (
715
656
  <div className="flex items-center justify-center h-full">
716
- <div className="animate-spin w-8 h-8 border-2 border-pink-500 border-t-transparent rounded-full" />
657
+ <div className="animate-spin w-6 h-6 border-2 border-[var(--kyro-primary)] border-t-transparent rounded-full" />
717
658
  </div>
718
659
  ) : error ? (
719
- <div className="text-red-500 text-sm font-bold">{error}</div>
660
+ <div className="p-3 m-3 rounded bg-[var(--kyro-danger-bg)] border border-[var(--kyro-danger)]/20 text-[11px] text-[var(--kyro-danger)] font-medium">
661
+ ⚠ {error}
662
+ </div>
720
663
  ) : response ? (
721
- <pre className="text-xs font-mono text-[var(--kyro-text-primary)]">
664
+ <pre className="text-[11px] font-mono text-[var(--kyro-text-primary)] whitespace-pre-wrap p-3">
722
665
  {JSON.stringify(response.data, null, 2)}
723
666
  </pre>
724
667
  ) : (
725
- <div className="h-full flex items-center justify-center text-[var(--kyro-text-muted)] text-sm italic">
726
- Send a request to see the response
668
+ <div className="flex flex-col items-center justify-center h-full opacity-30">
669
+ <Play className="w-10 h-10 mb-2" />
670
+ <p className="text-[11px] font-bold">Send a request</p>
727
671
  </div>
728
672
  )}
729
673
  </div>
@@ -732,58 +676,38 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
732
676
  </div>
733
677
 
734
678
  {/* Modals */}
735
- <Modal
736
- open={showFolderModal}
737
- onClose={() => setShowFolderModal(false)}
738
- title="Create Folder"
739
- size="md"
679
+ <Modal open={showFolderModal} onClose={() => setShowFolderModal(false)} title="Create Folder" size="sm"
740
680
  footer={
741
- <>
742
- <button type="button" onClick={() => setShowFolderModal(false)} className="kyro-btn kyro-btn-md kyro-btn-ghost">Cancel</button>
743
- <button type="button" onClick={createFolder} className="kyro-btn kyro-btn-md bg-pink-500 border-pink-500 text-white hover:bg-pink-600 hover:border-pink-600">Create</button>
744
- </>
681
+ <div className="flex gap-2">
682
+ <button onClick={() => setShowFolderModal(false)} className="kyro-btn kyro-btn-md kyro-btn-ghost">Cancel</button>
683
+ <button onClick={createFolder} className="kyro-btn kyro-btn-md kyro-btn-primary">Create</button>
684
+ </div>
745
685
  }
746
686
  >
747
- <div className="py-2">
748
- <input
749
- type="text"
750
- value={newFolderName}
751
- onChange={(e) => setNewFolderName(e.target.value)}
752
- placeholder="Folder name..."
753
- className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2"
754
- />
755
- </div>
687
+ <input type="text" value={newFolderName} onChange={(e) => setNewFolderName(e.target.value)}
688
+ placeholder="Folder name..." className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-3 py-2 text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
689
+ />
756
690
  </Modal>
757
691
 
758
- <Modal
759
- open={showSaveModal}
760
- onClose={() => setShowSaveModal(false)}
761
- title="Save Request"
762
- size="md"
692
+ <Modal open={showSaveModal} onClose={() => setShowSaveModal(false)} title="Save Request" size="sm"
763
693
  footer={
764
- <>
765
- <button type="button" onClick={() => setShowSaveModal(false)} className="kyro-btn kyro-btn-md kyro-btn-ghost">Cancel</button>
766
- <button type="button" onClick={saveRequest} className="kyro-btn kyro-btn-md bg-pink-500 border-pink-500 text-white hover:bg-pink-600 hover:border-pink-600 disabled:opacity-50 disabled:cursor-not-allowed" disabled={!saveRequestName || !saveToFolderId}>Save</button>
767
- </>
694
+ <div className="flex gap-2">
695
+ <button onClick={() => setShowSaveModal(false)} className="kyro-btn kyro-btn-md kyro-btn-ghost">Cancel</button>
696
+ <button onClick={saveRequest} className="kyro-btn kyro-btn-md kyro-btn-primary" disabled={!saveRequestName || !saveToFolderId}>Save</button>
697
+ </div>
768
698
  }
769
699
  >
770
- <div className="space-y-4 py-2">
700
+ <div className="space-y-3">
771
701
  <div>
772
- <label className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)] block mb-1">Request Name</label>
773
- <input
774
- type="text"
775
- value={saveRequestName}
776
- onChange={(e) => setSaveRequestName(e.target.value)}
777
- placeholder="e.g. List Posts..."
778
- className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2"
702
+ <label className="text-[10px] font-bold text-[var(--kyro-text-muted)] block mb-1">Request Name</label>
703
+ <input type="text" value={saveRequestName} onChange={(e) => setSaveRequestName(e.target.value)}
704
+ placeholder="e.g. List Posts" className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-3 py-2 text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
779
705
  />
780
706
  </div>
781
707
  <div>
782
- <label className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)] block mb-1">Folder</label>
783
- <select
784
- value={saveToFolderId}
785
- onChange={(e) => setSaveToFolderId(e.target.value)}
786
- className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2"
708
+ <label className="text-[10px] font-bold text-[var(--kyro-text-muted)] block mb-1">Folder</label>
709
+ <select value={saveToFolderId} onChange={(e) => setSaveToFolderId(e.target.value)}
710
+ className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-3 py-2 text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
787
711
  >
788
712
  <option value="">Select Folder...</option>
789
713
  {folders.map(f => <option key={f.id} value={f.id}>{f.name}</option>)}