@kyro-cms/admin 0.9.5 → 0.9.7

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