@kyro-cms/admin 0.3.2 → 0.3.4

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 (242) hide show
  1. package/dist/EditorClient-XEUOVAAC.js +466 -0
  2. package/dist/EditorClient-XEUOVAAC.js.map +1 -0
  3. package/dist/EditorClient-YLCGVDXY.cjs +468 -0
  4. package/dist/EditorClient-YLCGVDXY.cjs.map +1 -0
  5. package/dist/chunk-7KPIUCGT.js +384 -0
  6. package/dist/chunk-7KPIUCGT.js.map +1 -0
  7. package/dist/chunk-GOACG6R7.cjs +473 -0
  8. package/dist/chunk-GOACG6R7.cjs.map +1 -0
  9. package/dist/index.cjs +14861 -0
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/index.css +1661 -0
  12. package/dist/index.css.map +1 -0
  13. package/dist/index.d.ts +563 -0
  14. package/dist/index.js +14784 -0
  15. package/dist/index.js.map +1 -0
  16. package/package.json +19 -19
  17. package/src/components/ActionBar.tsx +7 -43
  18. package/src/components/Admin.tsx +138 -277
  19. package/src/components/ApiKeysManager.tsx +428 -419
  20. package/src/components/AuditLogsPage.tsx +35 -39
  21. package/src/components/AuthBridge.tsx +51 -0
  22. package/src/components/AutoForm.tsx +495 -1230
  23. package/src/components/BrandingHub.tsx +18 -19
  24. package/src/components/BulkActionsBar.tsx +1 -1
  25. package/src/components/CreateView.tsx +22 -36
  26. package/src/components/Dashboard.tsx +60 -84
  27. package/src/components/DetailView.tsx +113 -91
  28. package/src/components/DeveloperCenter.tsx +200 -198
  29. package/src/components/FieldRenderer.tsx +206 -0
  30. package/src/components/GraphQLPlayground.tsx +340 -480
  31. package/src/components/ListView.tsx +828 -254
  32. package/src/components/LoginPage.tsx +3 -4
  33. package/src/components/MarketplaceManager.tsx +254 -0
  34. package/src/components/MediaGallery.tsx +856 -1192
  35. package/src/components/PluginsManager.tsx +277 -0
  36. package/src/components/RestPlayground.tsx +398 -560
  37. package/src/components/SessionsManager.tsx +211 -0
  38. package/src/components/Sidebar.astro +179 -151
  39. package/src/components/ThemeProvider.tsx +7 -161
  40. package/src/components/UserManagement.tsx +162 -146
  41. package/src/components/UserMenu.tsx +110 -0
  42. package/src/components/WebhookManager.tsx +305 -367
  43. package/src/components/blocks/AccordionBlock.tsx +4 -4
  44. package/src/components/blocks/ArrayBlock.tsx +3 -3
  45. package/src/components/blocks/BlockEditModal.tsx +8 -8
  46. package/src/components/blocks/BlockWrapper.tsx +61 -0
  47. package/src/components/blocks/ButtonBlock.tsx +4 -4
  48. package/src/components/blocks/ChildBlocksTree.tsx +23 -25
  49. package/src/components/blocks/CodeBlock.tsx +15 -15
  50. package/src/components/blocks/ColumnsBlock.tsx +6 -44
  51. package/src/components/blocks/DividerBlock.tsx +3 -3
  52. package/src/components/blocks/FileBlock.tsx +4 -4
  53. package/src/components/blocks/HeadingBlock.tsx +6 -38
  54. package/src/components/blocks/HeroBlock.tsx +4 -4
  55. package/src/components/blocks/ImageBlock.tsx +4 -4
  56. package/src/components/blocks/LinkBlock.tsx +4 -4
  57. package/src/components/blocks/ListBlock.tsx +3 -3
  58. package/src/components/blocks/ParagraphBlock.tsx +12 -42
  59. package/src/components/blocks/RelationshipBlock.tsx +4 -4
  60. package/src/components/blocks/RichTextBlock.tsx +4 -4
  61. package/src/components/blocks/VStackBlock.tsx +5 -37
  62. package/src/components/blocks/VideoBlock.tsx +4 -4
  63. package/src/components/blocks/types.ts +11 -0
  64. package/src/components/fields/AccordionField.tsx +1 -1
  65. package/src/components/fields/ArrayField.tsx +2 -2
  66. package/src/components/fields/ArrayLayout.tsx +93 -0
  67. package/src/components/fields/BlocksField.tsx +122 -111
  68. package/src/components/fields/ButtonField.tsx +1 -1
  69. package/src/components/fields/CheckboxField.tsx +14 -15
  70. package/src/components/fields/ChildrenField.tsx +2 -2
  71. package/src/components/fields/CodeField.tsx +3 -3
  72. package/src/components/fields/ColumnsField.tsx +2 -2
  73. package/src/components/fields/DateField.tsx +13 -26
  74. package/src/components/fields/EditorClient.tsx +26 -28
  75. package/src/components/fields/FieldLayout.tsx +52 -0
  76. package/src/components/fields/GroupLayout.tsx +35 -0
  77. package/src/components/fields/JSONField.tsx +7 -7
  78. package/src/components/fields/LinkField.tsx +1 -1
  79. package/src/components/fields/MarkdownField.tsx +1 -1
  80. package/src/components/fields/NumberField.tsx +13 -26
  81. package/src/components/fields/PortableTextField.tsx +4 -4
  82. package/src/components/fields/PortableTextRenderer.tsx +1 -1
  83. package/src/components/fields/RelationshipBlockField.tsx +31 -23
  84. package/src/components/fields/RelationshipField.tsx +14 -14
  85. package/src/components/fields/SelectField.tsx +17 -26
  86. package/src/components/fields/TabsLayout.tsx +69 -0
  87. package/src/components/fields/TextField.tsx +85 -38
  88. package/src/components/fields/UploadField.tsx +71 -41
  89. package/src/components/fields/VideoField.tsx +1 -1
  90. package/src/components/fields/extensions/blockComponents.tsx +2 -2
  91. package/src/components/fields/extensions/blocksStore.ts +207 -193
  92. package/src/components/fields/types.ts +22 -0
  93. package/src/components/layout/Layout.tsx +1 -1
  94. package/src/components/ui/ActionMenu.tsx +63 -0
  95. package/src/components/ui/Badge.tsx +59 -5
  96. package/src/components/ui/BlockDrawer.tsx +4 -5
  97. package/src/components/ui/CommandPalette.tsx +58 -36
  98. package/src/components/ui/CommandPaletteWrapper.tsx +18 -17
  99. package/src/components/ui/Dropdown.tsx +18 -16
  100. package/src/components/ui/EmptyState.tsx +25 -0
  101. package/src/components/ui/GlobalModal.tsx +49 -0
  102. package/src/components/ui/IconButton.tsx +44 -0
  103. package/src/components/ui/Modal.tsx +19 -20
  104. package/src/components/ui/PageHeader.tsx +158 -0
  105. package/src/components/ui/Pagination.tsx +61 -0
  106. package/src/components/ui/PromptModal.tsx +1 -1
  107. package/src/components/ui/SearchInput.tsx +57 -0
  108. package/src/components/ui/SeoPreview.tsx +31 -0
  109. package/src/components/ui/SessionModal.tsx +0 -0
  110. package/src/components/ui/SlidePanel.tsx +2 -0
  111. package/src/components/ui/Toast.tsx +65 -122
  112. package/src/components/ui/Toaster.tsx +18 -0
  113. package/src/components/ui/icons.tsx +112 -0
  114. package/src/components/users/UserDetail.tsx +290 -0
  115. package/src/components/users/UserForm.tsx +242 -0
  116. package/src/components/users/UsersList.tsx +338 -0
  117. package/src/env.d.ts +13 -13
  118. package/src/fields/index.ts +2 -1
  119. package/src/global.d.ts +7 -0
  120. package/src/hooks/data.ts +2 -9
  121. package/src/hooks/useAsyncData.ts +36 -0
  122. package/src/hooks/useAutoFormState.ts +527 -0
  123. package/src/hooks/useSelection.ts +49 -0
  124. package/src/hooks/useSession.ts +0 -0
  125. package/src/index.ts +11 -1
  126. package/src/integration.ts +86 -11
  127. package/src/kyro-cms.d.ts +209 -0
  128. package/src/layouts/AdminLayout.astro +128 -11
  129. package/src/layouts/AuthLayout.astro +21 -5
  130. package/src/lib/api.ts +175 -55
  131. package/src/lib/autoform-store.ts +435 -0
  132. package/src/lib/config.ts +82 -34
  133. package/src/lib/createRegistry.ts +29 -0
  134. package/src/lib/default-kyro-config.ts +4 -0
  135. package/src/lib/globals.ts +50 -0
  136. package/src/lib/media-utils.ts +18 -0
  137. package/src/lib/object-utils.ts +77 -0
  138. package/src/lib/paths.ts +61 -0
  139. package/src/lib/stores/index.ts +370 -0
  140. package/src/lib/types.ts +43 -0
  141. package/src/lib/useResourceManager.ts +105 -0
  142. package/src/pages/403.astro +67 -0
  143. package/src/pages/[collection]/[id].astro +14 -180
  144. package/src/pages/[collection]/index.astro +11 -6
  145. package/src/pages/api-explorer.astro +173 -0
  146. package/src/pages/audit/index.astro +2 -0
  147. package/src/pages/auth/login.astro +122 -0
  148. package/src/pages/auth/register.astro +167 -0
  149. package/src/pages/graphql-explorer.astro +59 -0
  150. package/src/pages/{admin/graphql.astro → graphql.astro} +51 -17
  151. package/src/pages/index.astro +577 -0
  152. package/src/pages/index_ALT.astro +3 -0
  153. package/src/pages/keys.astro +11 -0
  154. package/src/pages/marketplace.astro +11 -0
  155. package/src/pages/media.astro +3 -0
  156. package/src/pages/plugins.astro +8 -0
  157. package/src/pages/preview/[collection]/[id].astro +188 -123
  158. package/src/pages/rest-playground.astro +62 -0
  159. package/src/pages/roles/index.astro +183 -76
  160. package/src/pages/sessions.astro +8 -0
  161. package/src/pages/settings/[slug].astro +92 -114
  162. package/src/pages/settings/index.astro +5 -3
  163. package/src/pages/users/[id].astro +25 -154
  164. package/src/pages/users/index.astro +19 -130
  165. package/src/pages/users/new.astro +9 -86
  166. package/src/pages/webhooks.astro +11 -0
  167. package/src/routes.ts +80 -0
  168. package/src/styles/main.css +119 -79
  169. package/src/theme/tokens.ts +1 -0
  170. package/src/vite-env.d.ts +14 -0
  171. package/src/collections/auth/index.ts +0 -155
  172. package/src/collections/portfolio/index.ts +0 -343
  173. package/src/components/ApiExplorer.tsx +0 -325
  174. package/src/components/EnhancedListView.tsx +0 -889
  175. package/src/components/GraphQLExplorer.tsx +0 -675
  176. package/src/components/Icons.tsx +0 -23
  177. package/src/components/StatusBadge.tsx +0 -76
  178. package/src/lib/MediaService.ts +0 -541
  179. package/src/lib/auth/sqlite-adapter.ts +0 -319
  180. package/src/lib/dataStore.ts +0 -226
  181. package/src/lib/db/adapter.ts +0 -54
  182. package/src/lib/db/drizzle-mysql-adapter.ts +0 -194
  183. package/src/lib/db/drizzle-mysql-auth-adapter.ts +0 -327
  184. package/src/lib/db/drizzle-postgres-adapter.ts +0 -202
  185. package/src/lib/db/drizzle-postgres-auth-adapter.ts +0 -304
  186. package/src/lib/db/drizzle-sqlite-adapter.ts +0 -227
  187. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +0 -548
  188. package/src/lib/db/index.ts +0 -449
  189. package/src/lib/db/mongodb-adapter.ts +0 -207
  190. package/src/lib/db/mongodb-auth-adapter.ts +0 -305
  191. package/src/lib/db/schema/mysql-auth.ts +0 -113
  192. package/src/lib/db/schema/mysql-content.ts +0 -20
  193. package/src/lib/db/schema/postgres-auth.ts +0 -116
  194. package/src/lib/db/schema/postgres-content.ts +0 -35
  195. package/src/lib/db/schema/postgres-media.ts +0 -52
  196. package/src/lib/db/schema/postgres-settings.ts +0 -11
  197. package/src/lib/db/schema/sqlite-auth.ts +0 -112
  198. package/src/lib/db/schema/sqlite-content.ts +0 -20
  199. package/src/lib/db/version-adapter.ts +0 -248
  200. package/src/lib/graphql/index.ts +0 -1
  201. package/src/lib/graphql/schema.ts +0 -443
  202. package/src/lib/rate-limit.ts +0 -267
  203. package/src/lib/storage.ts +0 -374
  204. package/src/lib/store.ts +0 -85
  205. package/src/middleware.ts +0 -177
  206. package/src/pages/admin/api-explorer.astro +0 -98
  207. package/src/pages/admin/graphql-explorer.astro +0 -40
  208. package/src/pages/admin/index.astro +0 -286
  209. package/src/pages/admin/keys.astro +0 -8
  210. package/src/pages/admin/rest-playground.astro +0 -44
  211. package/src/pages/admin/webhooks.astro +0 -8
  212. package/src/pages/api/[collection]/[id]/publish.ts +0 -52
  213. package/src/pages/api/[collection]/[id]/unpublish.ts +0 -42
  214. package/src/pages/api/[collection]/[id]/versions.ts +0 -66
  215. package/src/pages/api/[collection]/[id].ts +0 -213
  216. package/src/pages/api/[collection]/index.ts +0 -209
  217. package/src/pages/api/auth/[id].ts +0 -121
  218. package/src/pages/api/auth/audit-logs.ts +0 -57
  219. package/src/pages/api/auth/login.ts +0 -211
  220. package/src/pages/api/auth/logout.ts +0 -66
  221. package/src/pages/api/auth/me.ts +0 -36
  222. package/src/pages/api/auth/refresh.ts +0 -119
  223. package/src/pages/api/auth/register.ts +0 -188
  224. package/src/pages/api/auth/users.ts +0 -97
  225. package/src/pages/api/collections.ts +0 -59
  226. package/src/pages/api/globals/[slug].ts +0 -42
  227. package/src/pages/api/graphql.ts +0 -90
  228. package/src/pages/api/health.ts +0 -426
  229. package/src/pages/api/keys/[id].ts +0 -26
  230. package/src/pages/api/keys/index.ts +0 -75
  231. package/src/pages/api/media/[id].ts +0 -309
  232. package/src/pages/api/media/folders.ts +0 -609
  233. package/src/pages/api/media/index.ts +0 -146
  234. package/src/pages/api/media/resize.ts +0 -267
  235. package/src/pages/api/search.ts +0 -82
  236. package/src/pages/api/slug-availability.ts +0 -70
  237. package/src/pages/api/storage-config.ts +0 -20
  238. package/src/pages/api/storage-status.ts +0 -206
  239. package/src/pages/api/upload.ts +0 -334
  240. package/src/pages/api/webhooks/index.ts +0 -71
  241. package/src/pages/login.astro +0 -82
  242. package/src/pages/register.astro +0 -102
@@ -1,4 +1,6 @@
1
1
  import React, { useState, useEffect, useCallback, useRef } from "react";
2
+ import { useUIStore } from "../lib/stores";
3
+ import { apiPath } from "../lib/paths";
2
4
 
3
5
  interface EnvVariable {
4
6
  key: string;
@@ -49,41 +51,38 @@ const STORAGE_KEYS = {
49
51
  folders: "kyro-rest-folders",
50
52
  history: "kyro-rest-history",
51
53
  env: "kyro-rest-env",
52
- activeEnv: "kyro-rest-active-env",
53
54
  };
54
55
 
55
56
  export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
57
+ const [sidebarTab, setSidebarTab] = useState<
58
+ "collections" | "saved" | "history" | "env"
59
+ >("collections");
56
60
  const [folders, setFolders] = useState<RequestFolder[]>([]);
57
61
  const [history, setHistory] = useState<HistoryItem[]>([]);
58
62
  const [envVars, setEnvVars] = useState<EnvVariable[]>([]);
59
- const [activeEnv, setActiveEnv] = useState<string>("default");
60
63
  const [selectedRequest, setSelectedRequest] = useState<SavedRequest | null>(
61
64
  null,
62
65
  );
63
66
  const [currentRequest, setCurrentRequest] = useState<SavedRequest>({
64
- id: "",
65
- name: "New Request",
67
+ id: "new",
68
+ name: "Untitled Request",
66
69
  method: "GET",
67
70
  url: "",
68
- headers: { "Content-Type": "application/json" },
71
+ headers: {},
69
72
  body: "",
70
73
  });
71
- const [response, setResponse] = useState<string>("");
72
- const [responseStatus, setResponseStatus] = useState<number | null>(null);
73
- const [responseTime, setResponseTime] = useState<number | null>(null);
74
+ const [response, setResponse] = useState<Record<string, unknown> | null>(null);
74
75
  const [loading, setLoading] = useState(false);
75
- const [activeTab, setActiveTab] = useState<"headers" | "body" | "params">(
76
- "headers",
76
+ const [error, setError] = useState<string | null>(null);
77
+ const [activeTab, setActiveTab] = useState<"params" | "headers" | "body">(
78
+ "params",
77
79
  );
80
+ const [showFolderModal, setShowFolderModal] = useState(false);
78
81
  const [showSaveModal, setShowSaveModal] = useState(false);
79
- const [showEnvModal, setShowEnvModal] = useState(false);
80
- const [showExportModal, setShowExportModal] = useState(false);
81
- const [saveName, setSaveName] = useState("");
82
- const [saveFolder, setSaveFolder] = useState<string | null>(null);
83
- const [sidebarTab, setSidebarTab] = useState<
84
- "collections" | "saved" | "history" | "env"
85
- >("collections");
86
- const fileInputRef = React.useRef<HTMLInputElement>(null);
82
+ const [newFolderName, setNewFolderName] = useState("");
83
+ const [saveToFolderId, setSaveToFolderId] = useState("");
84
+ const [saveRequestName, setSaveRequestName] = useState("");
85
+ const { confirm, alert } = useUIStore();
87
86
 
88
87
  // Load from localStorage
89
88
  useEffect(() => {
@@ -97,184 +96,160 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
97
96
  if (savedEnv) setEnvVars(JSON.parse(savedEnv));
98
97
  else
99
98
  setEnvVars([
100
- { key: "baseUrl", value: "/api", enabled: true },
99
+ { key: "baseUrl", value: apiPath, enabled: true },
101
100
  { key: "token", value: "", enabled: true },
102
101
  ]);
103
102
 
104
- const savedActiveEnv = localStorage.getItem(STORAGE_KEYS.activeEnv);
105
- if (savedActiveEnv) setActiveEnv(savedActiveEnv);
103
+ setIsMounted(true);
106
104
  }, []);
107
105
 
106
+ const [isMounted, setIsMounted] = useState(false);
107
+
108
108
  // Save to localStorage
109
109
  useEffect(() => {
110
- localStorage.setItem(STORAGE_KEYS.folders, JSON.stringify(folders));
111
- }, [folders]);
110
+ if (isMounted) {
111
+ localStorage.setItem(STORAGE_KEYS.folders, JSON.stringify(folders));
112
+ }
113
+ }, [folders, isMounted]);
112
114
 
113
115
  useEffect(() => {
114
- localStorage.setItem(
115
- STORAGE_KEYS.history,
116
- JSON.stringify(history.slice(0, 50)),
117
- );
118
- }, [history]);
116
+ if (isMounted) {
117
+ localStorage.setItem(STORAGE_KEYS.history, JSON.stringify(history));
118
+ }
119
+ }, [history, isMounted]);
119
120
 
120
121
  useEffect(() => {
121
- localStorage.setItem(STORAGE_KEYS.env, JSON.stringify(envVars));
122
- }, [envVars]);
122
+ if (isMounted) {
123
+ localStorage.setItem(STORAGE_KEYS.env, JSON.stringify(envVars));
124
+ }
125
+ }, [envVars, isMounted]);
123
126
 
124
- useEffect(() => {
125
- localStorage.setItem(STORAGE_KEYS.activeEnv, activeEnv);
126
- }, [activeEnv]);
127
-
128
- const replaceEnvVars = useCallback(
129
- (text: string): string => {
130
- if (!text) return text;
131
- let result = text;
132
- for (const env of envVars.filter((e) => e.enabled)) {
133
- const pattern = new RegExp(`\\{\\{${env.key}\\}\\}`, "g");
134
- result = result.replace(pattern, env.value);
127
+ const resolveUrl = (url: string) => {
128
+ let resolved = url;
129
+ envVars.forEach((v) => {
130
+ if (v.enabled) {
131
+ resolved = resolved.replace(`{{${v.key}}}`, v.value);
135
132
  }
136
- return result;
137
- },
138
- [envVars],
139
- );
140
-
141
- const executeRequest = async () => {
142
- const url = replaceEnvVars(currentRequest.url);
143
- if (!url) return;
144
-
145
- setLoading(true);
146
- setResponse("");
147
- setResponseStatus(null);
148
- setResponseTime(null);
149
-
150
- const startTime = performance.now();
151
- const headers: Record<string, string> = {};
152
- for (const [key, value] of Object.entries(currentRequest.headers)) {
153
- headers[key] = replaceEnvVars(value);
133
+ });
134
+ // Default base URL if relative
135
+ if (resolved.startsWith("/")) {
136
+ const baseUrl = envVars.find((v) => v.key === "baseUrl" && v.enabled)?.value || apiPath;
137
+ resolved = `${baseUrl}${resolved}`;
154
138
  }
139
+ return resolved;
140
+ };
155
141
 
142
+ const handleSend = async () => {
143
+ setLoading(true);
144
+ setError(null);
145
+ const start = Date.now();
156
146
  try {
157
- const options: RequestInit = {
158
- method: currentRequest.method,
159
- headers,
160
- };
147
+ const url = resolveUrl(currentRequest.url);
148
+ const headers: Record<string, string> = { ...currentRequest.headers };
161
149
 
162
- if (
163
- ["POST", "PUT", "PATCH"].includes(currentRequest.method) &&
164
- currentRequest.body
165
- ) {
166
- options.body = replaceEnvVars(currentRequest.body);
167
- }
168
-
169
- const res = await fetch(url, options);
170
- const endTime = performance.now();
171
-
172
- setResponseStatus(res.status);
173
- setResponseTime(Math.round(endTime - startTime));
150
+ const token = envVars.find(v => v.key === 'token' && v.enabled)?.value;
151
+ if (token) headers['Authorization'] = `Bearer ${token}`;
174
152
 
175
- const contentType = res.headers.get("content-type");
176
- if (contentType?.includes("application/json")) {
177
- const data = await res.json();
178
- setResponse(JSON.stringify(data, null, 2));
179
- } else {
180
- setResponse(await res.text());
181
- }
153
+ const res = await fetch(url, {
154
+ method: currentRequest.method,
155
+ headers: {
156
+ "Content-Type": "application/json",
157
+ ...headers,
158
+ },
159
+ body:
160
+ currentRequest.method !== "GET" && currentRequest.body
161
+ ? currentRequest.body
162
+ : undefined,
163
+ });
164
+
165
+ const duration = Date.now() - start;
166
+ const status = res.status;
167
+ const data = await res.json().catch(() => ({}));
168
+
169
+ setResponse({
170
+ status,
171
+ duration,
172
+ size: JSON.stringify(data).length,
173
+ data,
174
+ });
182
175
 
183
176
  // Add to history
184
- setHistory((prev) =>
185
- [
186
- {
187
- id: Date.now().toString(),
188
- timestamp: Date.now(),
189
- method: currentRequest.method,
190
- url: currentRequest.url,
191
- status: res.status,
192
- duration: Math.round(endTime - startTime),
193
- },
194
- ...prev,
195
- ].slice(0, 50),
196
- );
197
- } catch (error) {
198
- setResponseStatus(0);
199
- setResponse(error instanceof Error ? error.message : "Request failed");
177
+ const historyItem: HistoryItem = {
178
+ id: Date.now().toString(),
179
+ timestamp: Date.now(),
180
+ method: currentRequest.method,
181
+ url: currentRequest.url,
182
+ status,
183
+ duration,
184
+ };
185
+ setHistory((prev) => [historyItem, ...prev].slice(0, 50));
186
+ } catch (err: unknown) {
187
+ const message = err instanceof Error ? err.message : "Unknown error";
188
+ setError(message);
200
189
  } finally {
201
190
  setLoading(false);
202
191
  }
203
192
  };
204
193
 
205
- const saveRequest = () => {
206
- const request: SavedRequest = {
207
- ...currentRequest,
208
- id: currentRequest.id || Date.now().toString(),
209
- name: saveName || currentRequest.name,
210
- folderId: saveFolder || undefined,
211
- };
212
-
213
- if (saveFolder) {
214
- setFolders((prev) =>
215
- prev
216
- .map((f) =>
217
- f.id === saveFolder
218
- ? { ...f, requests: [...f.requests, request] }
219
- : f,
220
- )
221
- .concat(
222
- prev.find((f) => f.id === saveFolder)
223
- ? []
224
- : [{ id: saveFolder, name: saveFolder, requests: [request] }],
225
- ),
226
- );
227
- } else {
228
- setFolders((prev) => [
229
- ...prev,
230
- { id: "default", name: "Uncategorized", requests: [request] },
231
- ]);
232
- }
233
-
234
- setCurrentRequest(request);
235
- setShowSaveModal(false);
236
- setSaveName("");
194
+ const loadRequest = (req: SavedRequest) => {
195
+ setCurrentRequest(req);
196
+ setSelectedRequest(req);
197
+ setResponse(null);
198
+ setError(null);
237
199
  };
238
200
 
239
- const loadRequest = (request: SavedRequest) => {
240
- setSelectedRequest(request);
241
- setCurrentRequest({ ...request });
242
- setResponse("");
243
- setResponseStatus(null);
201
+ const createFolder = () => {
202
+ if (!newFolderName.trim()) return;
203
+ const newFolder: RequestFolder = {
204
+ id: Date.now().toString(),
205
+ name: newFolderName,
206
+ requests: [],
207
+ };
208
+ setFolders((prev) => [...prev, newFolder]);
209
+ setNewFolderName("");
210
+ setShowFolderModal(false);
244
211
  };
245
212
 
246
- const deleteRequest = (requestId: string) => {
247
- setFolders((prev) =>
248
- prev
249
- .map((f) => ({
250
- ...f,
251
- requests: f.requests.filter((r) => r.id !== requestId),
252
- }))
253
- .filter((f) => f.requests.length > 0),
254
- );
255
- if (selectedRequest?.id === requestId) {
256
- setSelectedRequest(null);
257
- }
213
+ const deleteFolder = (id: string) => {
214
+ confirm({
215
+ title: "Delete Folder",
216
+ message: "Are you sure? All requests inside will be deleted.",
217
+ variant: "danger",
218
+ onConfirm: () => {
219
+ setFolders((prev) => prev.filter((f) => f.id !== id));
220
+ }
221
+ });
258
222
  };
259
223
 
260
- const updateHeader = (key: string, value: string) => {
261
- setCurrentRequest((prev) => ({
262
- ...prev,
263
- headers: { ...prev.headers, [key]: value },
264
- }));
265
- };
224
+ const saveRequest = () => {
225
+ if (!saveRequestName.trim() || !saveToFolderId) return;
266
226
 
267
- const removeHeader = (key: string) => {
268
- const newHeaders = { ...currentRequest.headers };
269
- delete newHeaders[key];
270
- setCurrentRequest((prev) => ({ ...prev, headers: newHeaders }));
227
+ const newSavedRequest: SavedRequest = {
228
+ ...currentRequest,
229
+ id: Date.now().toString(),
230
+ name: saveRequestName,
231
+ folderId: saveToFolderId,
232
+ };
233
+
234
+ setFolders((prev) =>
235
+ prev.map((f) =>
236
+ f.id === saveToFolderId
237
+ ? { ...f, requests: [...f.requests, newSavedRequest] }
238
+ : f,
239
+ ),
240
+ );
241
+ setSelectedRequest(newSavedRequest);
242
+ setShowSaveModal(false);
271
243
  };
272
244
 
273
- const addHeader = () => {
274
- setCurrentRequest((prev) => ({
275
- ...prev,
276
- headers: { ...prev.headers, "": "" },
277
- }));
245
+ const deleteRequest = (id: string) => {
246
+ setFolders((prev) =>
247
+ prev.map((f) => ({
248
+ ...f,
249
+ requests: f.requests.filter((r) => r.id !== id),
250
+ })),
251
+ );
252
+ if (selectedRequest?.id === id) setSelectedRequest(null);
278
253
  };
279
254
 
280
255
  const getMethodColor = (method: string) => {
@@ -283,8 +258,6 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
283
258
  return "bg-green-500";
284
259
  case "POST":
285
260
  return "bg-blue-500";
286
- case "PUT":
287
- return "bg-blue-500";
288
261
  case "PATCH":
289
262
  return "bg-yellow-500";
290
263
  case "DELETE":
@@ -352,9 +325,9 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
352
325
  setEnvVars((prev) => [...prev, ...data.envVars]);
353
326
  }
354
327
 
355
- alert("Import successful!");
328
+ alert({ title: "Import Successful", message: "Your playground data has been imported." });
356
329
  } catch (error) {
357
- alert("Failed to import: Invalid JSON file");
330
+ alert({ title: "Import Failed", message: "Invalid JSON file structure." });
358
331
  }
359
332
  };
360
333
  reader.readAsText(file);
@@ -362,21 +335,22 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
362
335
 
363
336
  // Clear all data
364
337
  const clearAllData = () => {
365
- if (
366
- confirm(
367
- "Are you sure you want to clear all saved requests, history, and environment variables?",
368
- )
369
- ) {
370
- setFolders([]);
371
- setHistory([]);
372
- setEnvVars([
373
- { key: "baseUrl", value: "/api", enabled: true },
374
- { key: "token", value: "", enabled: true },
375
- ]);
376
- localStorage.removeItem(STORAGE_KEYS.folders);
377
- localStorage.removeItem(STORAGE_KEYS.history);
378
- localStorage.removeItem(STORAGE_KEYS.env);
379
- }
338
+ confirm({
339
+ title: "Clear All Data?",
340
+ message: "Are you sure you want to clear all saved requests, history, and environment variables? This action cannot be undone.",
341
+ variant: "danger",
342
+ onConfirm: () => {
343
+ setFolders([]);
344
+ setHistory([]);
345
+ setEnvVars([
346
+ { key: "baseUrl", value: apiPath, enabled: true },
347
+ { key: "token", value: "", enabled: true },
348
+ ]);
349
+ localStorage.removeItem(STORAGE_KEYS.folders);
350
+ localStorage.removeItem(STORAGE_KEYS.history);
351
+ localStorage.removeItem(STORAGE_KEYS.env);
352
+ }
353
+ });
380
354
  };
381
355
 
382
356
  return (
@@ -389,11 +363,10 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
389
363
  <button type="button"
390
364
  key={tab}
391
365
  onClick={() => setSidebarTab(tab)}
392
- className={`flex-1 px-2 py-2 text-xs font-bold transition-colors ${
393
- sidebarTab === tab
394
- ? "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] border-b-2 border-pink-500"
395
- : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
396
- }`}
366
+ className={`flex-1 px-2 py-2 text-xs font-bold transition-colors ${sidebarTab === tab
367
+ ? "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] border-b-2 border-pink-500"
368
+ : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
369
+ }`}
397
370
  >
398
371
  {tab.charAt(0).toUpperCase() + tab.slice(1)}
399
372
  </button>
@@ -407,7 +380,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
407
380
  <div className="space-y-3">
408
381
  {collections.map((col) => (
409
382
  <div key={col.slug}>
410
- <h3 className="text-xs font-bold text-[var(--kyro-text-muted)] uppercase mb-2 px-2">
383
+ <h3 className="text-xs font-bold text-[var(--kyro-text-muted)] mb-2 px-2">
411
384
  {col.name}
412
385
  </h3>
413
386
  {[
@@ -438,7 +411,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
438
411
  onClick={() => {
439
412
  setCurrentRequest((prev) => ({
440
413
  ...prev,
441
- method: endpoint.method as any,
414
+ method: endpoint.method as SavedRequest["method"],
442
415
  url: endpoint.path,
443
416
  name: `${col.name} - ${endpoint.name}`,
444
417
  }));
@@ -464,6 +437,23 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
464
437
  {/* Saved */}
465
438
  {sidebarTab === "saved" && (
466
439
  <div className="space-y-3">
440
+ <div className="flex items-center justify-between mb-2">
441
+ <button type="button"
442
+ onClick={() => setShowFolderModal(true)}
443
+ className="text-xs font-bold text-pink-500 hover:underline"
444
+ >
445
+ + New Folder
446
+ </button>
447
+ <div className="flex gap-2">
448
+ <button type="button" onClick={exportData} className="text-xs text-[var(--kyro-text-muted)] hover:text-pink-500" title="Export">
449
+ Export
450
+ </button>
451
+ <label className="text-xs text-[var(--kyro-text-muted)] hover:text-pink-500 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>
467
457
  {folders.length === 0 && (
468
458
  <p className="text-xs text-[var(--kyro-text-muted)] text-center py-4">
469
459
  No saved requests yet
@@ -471,17 +461,21 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
471
461
  )}
472
462
  {folders.map((folder) => (
473
463
  <div key={folder.id}>
474
- <h3 className="text-xs font-bold text-[var(--kyro-text-muted)] uppercase mb-2 px-2">
475
- {folder.name}
476
- </h3>
464
+ <div className="flex items-center justify-between mb-2 px-2">
465
+ <h3 className="text-xs font-bold text-[var(--kyro-text-muted)] ">
466
+ {folder.name}
467
+ </h3>
468
+ <button type="button" onClick={() => deleteFolder(folder.id)} className="text-[var(--kyro-text-muted)] hover:text-red-500">
469
+ ×
470
+ </button>
471
+ </div>
477
472
  {folder.requests.map((req) => (
478
473
  <div
479
474
  key={req.id}
480
- className={`group flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer transition-colors ${
481
- selectedRequest?.id === req.id
482
- ? "bg-[var(--kyro-sidebar-active)]"
483
- : "hover:bg-[var(--kyro-surface-accent)]"
484
- }`}
475
+ className={`group flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer transition-colors ${selectedRequest?.id === req.id
476
+ ? "bg-[var(--kyro-sidebar-active)]"
477
+ : "hover:bg-[var(--kyro-surface-accent)]"
478
+ }`}
485
479
  onClick={() => loadRequest(req)}
486
480
  >
487
481
  <span
@@ -489,7 +483,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
489
483
  >
490
484
  {req.method}
491
485
  </span>
492
- <span className="flex-1 text-xs text-[var(--kyro-text-secondary)] truncate">
486
+ <span className={`flex-1 text-xs truncate ${selectedRequest?.id === req.id ? "text-[var(--kyro-sidebar-text-active)]" : "text-[var(--kyro-text-secondary)]"}`}>
493
487
  {req.name}
494
488
  </span>
495
489
  <button type="button"
@@ -497,451 +491,295 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
497
491
  e.stopPropagation();
498
492
  deleteRequest(req.id);
499
493
  }}
500
- className="opacity-0 group-hover:opacity-100 text-red-500 hover:text-red-600"
494
+ className="opacity-0 group-hover:opacity-100 text-[var(--kyro-text-muted)] hover:text-red-500"
501
495
  >
502
- <svg
503
- className="w-3 h-3"
504
- fill="none"
505
- stroke="currentColor"
506
- viewBox="0 0 24 24"
507
- >
508
- <path
509
- strokeLinecap="round"
510
- strokeLinejoin="round"
511
- strokeWidth="2"
512
- d="M6 18L18 6M6 6l12 12"
513
- />
514
- </svg>
496
+ ×
515
497
  </button>
516
498
  </div>
517
499
  ))}
518
500
  </div>
519
501
  ))}
520
-
521
- {/* Export/Import/Clear Actions */}
522
- <div className="flex gap-2 mt-4 pt-4 border-t border-[var(--kyro-border]">
523
- <button type="button"
524
- onClick={exportData}
525
- className="flex-1 px-3 py-2 text-xs font-bold bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] rounded-lg hover:bg-[var(--kyro-surface)] transition-colors"
526
- title="Export all data"
527
- >
528
- Export
529
- </button>
530
- <button type="button"
531
- onClick={() => fileInputRef.current?.click()}
532
- className="flex-1 px-3 py-2 text-xs font-bold bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] rounded-lg hover:bg-[var(--kyro-surface)] transition-colors"
533
- title="Import data"
534
- >
535
- Import
536
- </button>
537
- <button type="button"
538
- onClick={clearAllData}
539
- className="px-3 py-2 text-xs font-bold bg-red-500/10 text-red-600 rounded-lg hover:bg-red-500/20 transition-colors"
540
- title="Clear all data"
541
- >
542
- <svg
543
- className="w-4 h-4"
544
- fill="none"
545
- stroke="currentColor"
546
- viewBox="0 0 24 24"
547
- >
548
- <path
549
- strokeLinecap="round"
550
- strokeLinejoin="round"
551
- strokeWidth="2"
552
- d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
553
- />
554
- </svg>
555
- </button>
556
- </div>
557
- <input
558
- ref={fileInputRef}
559
- type="file"
560
- accept=".json"
561
- onChange={(e) => {
562
- const file = e.target.files?.[0];
563
- if (file) importData(file);
564
- e.target.value = "";
565
- }}
566
- className="hidden"
567
- />
568
502
  </div>
569
503
  )}
570
504
 
571
505
  {/* History */}
572
506
  {sidebarTab === "history" && (
573
507
  <div className="space-y-1">
574
- {history.length === 0 && (
575
- <p className="text-xs text-[var(--kyro-text-muted)] text-center py-4">
576
- No history yet
577
- </p>
578
- )}
508
+ <div className="flex justify-end mb-2">
509
+ <button type="button" onClick={() => setHistory([])} className="text-[10px] font-bold text-red-500 hover:underline">
510
+ Clear History
511
+ </button>
512
+ </div>
579
513
  {history.map((item) => (
580
- <button type="button"
514
+ <div
581
515
  key={item.id}
516
+ className="p-2 rounded hover:bg-[var(--kyro-surface-accent)] cursor-pointer transition-colors border-b border-[var(--kyro-border)] last:border-0"
582
517
  onClick={() => {
583
518
  setCurrentRequest((prev) => ({
584
519
  ...prev,
585
- method: item.method as any,
520
+ method: item.method as SavedRequest["method"],
586
521
  url: item.url,
587
522
  }));
588
- setResponse("");
523
+ setResponse(null);
589
524
  }}
590
- className="w-full flex items-center gap-2 px-2 py-2 rounded hover:bg-[var(--kyro-surface-accent)] transition-colors"
591
525
  >
592
- <span
593
- className={`${getMethodColor(item.method)} text-white px-1 py-0.5 rounded text-[10px] font-bold`}
594
- >
595
- {item.method}
596
- </span>
597
- <div className="flex-1 min-w-0">
598
- <p className="text-xs text-[var(--kyro-text-secondary)] truncate">
599
- {item.url}
600
- </p>
601
- <p className="text-[10px] text-[var(--kyro-text-muted)]">
602
- {new Date(item.timestamp).toLocaleTimeString()} •{" "}
603
- {item.duration}ms
604
- </p>
526
+ <div className="flex items-center justify-between mb-1">
527
+ <span
528
+ className={`${getMethodColor(item.method)} text-white px-1 py-0.5 rounded text-[10px] font-bold`}
529
+ >
530
+ {item.method}
531
+ </span>
532
+ <span
533
+ className={`${getStatusColor(item.status)} text-white px-1 py-0.5 rounded text-[10px] font-bold`}
534
+ >
535
+ {item.status}
536
+ </span>
605
537
  </div>
606
- <span
607
- className={`${getStatusColor(item.status)} text-white px-1.5 py-0.5 rounded text-[10px] font-bold`}
608
- >
609
- {item.status || "ERR"}
610
- </span>
611
- </button>
538
+ <div className="text-[10px] text-[var(--kyro-text-secondary)] truncate mb-1">
539
+ {item.url}
540
+ </div>
541
+ <div className="text-[9px] text-[var(--kyro-text-muted)] flex justify-between">
542
+ <span>{new Date(item.timestamp).toLocaleTimeString()}</span>
543
+ <span>{item.duration}ms</span>
544
+ </div>
545
+ </div>
612
546
  ))}
613
547
  </div>
614
548
  )}
615
549
 
616
550
  {/* Environment */}
617
551
  {sidebarTab === "env" && (
618
- <div className="space-y-3">
619
- <div className="flex items-center justify-between mb-2">
620
- <span className="text-xs font-bold text-[var(--kyro-text-muted)] uppercase">
621
- Variables
622
- </span>
623
- <button type="button"
624
- onClick={() => {
625
- setEnvVars((prev) => [
626
- ...prev,
627
- { key: "", value: "", enabled: true },
628
- ]);
629
- }}
630
- className="text-xs text-pink-500 hover:text-pink-600"
631
- >
632
- + Add
552
+ <div className="space-y-4">
553
+ <div className="flex justify-between items-center">
554
+ <h3 className="text-xs font-bold text-[var(--kyro-text-muted)] ">Environment</h3>
555
+ <button type="button" onClick={clearAllData} className="text-[10px] text-red-500 font-bold hover:underline">Reset All</button>
556
+ </div>
557
+ <div className="space-y-3">
558
+ {envVars.map((env, i) => (
559
+ <div key={i} className="p-3 bg-[var(--kyro-surface-accent)] rounded-lg border border-[var(--kyro-border)] space-y-2">
560
+ <div className="flex items-center justify-between">
561
+ <span className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)]">{env.key}</span>
562
+ <input type="checkbox" checked={env.enabled} onChange={() => {
563
+ const next = [...envVars];
564
+ next[i].enabled = !next[i].enabled;
565
+ setEnvVars(next);
566
+ }} />
567
+ </div>
568
+ <input
569
+ type="text"
570
+ value={env.value}
571
+ onChange={(e) => {
572
+ const next = [...envVars];
573
+ next[i].value = e.target.value;
574
+ setEnvVars(next);
575
+ }}
576
+ className="w-full bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded px-2 py-1 text-xs"
577
+ />
578
+ </div>
579
+ ))}
580
+ <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">
581
+ + Add Variable
633
582
  </button>
634
583
  </div>
635
- {envVars.map((env, i) => (
636
- <div key={i} className="flex items-center gap-2">
637
- <input
638
- type="checkbox"
639
- checked={env.enabled}
640
- onChange={(e) => {
641
- const newVars = [...envVars];
642
- newVars[i].enabled = e.target.checked;
643
- setEnvVars(newVars);
644
- }}
645
- className="rounded"
646
- />
647
- <input
648
- type="text"
649
- value={env.key}
650
- onChange={(e) => {
651
- const newVars = [...envVars];
652
- newVars[i].key = e.target.value;
653
- setEnvVars(newVars);
654
- }}
655
- placeholder="key"
656
- className="flex-1 px-2 py-1 text-xs bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded"
657
- />
658
- <input
659
- type="text"
660
- value={env.value}
661
- onChange={(e) => {
662
- const newVars = [...envVars];
663
- newVars[i].value = e.target.value;
664
- setEnvVars(newVars);
665
- }}
666
- placeholder="value"
667
- className="flex-1 px-2 py-1 text-xs bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded"
668
- />
669
- <button type="button"
670
- onClick={() =>
671
- setEnvVars((prev) => prev.filter((_, j) => j !== i))
672
- }
673
- className="text-red-500 hover:text-red-600"
674
- >
675
- <svg
676
- className="w-4 h-4"
677
- fill="none"
678
- stroke="currentColor"
679
- viewBox="0 0 24 24"
680
- >
681
- <path
682
- strokeLinecap="round"
683
- strokeLinejoin="round"
684
- strokeWidth="2"
685
- d="M6 18L18 6M6 6l12 12"
686
- />
687
- </svg>
688
- </button>
689
- </div>
690
- ))}
691
- <p className="text-xs text-[var(--kyro-text-muted)]">
692
- Use {"{{variableName}}"} in URLs, headers, and body
693
- </p>
694
584
  </div>
695
585
  )}
696
586
  </div>
697
587
  </div>
698
588
 
699
- {/* Main Content */}
700
- <div className="flex-1 flex flex-col min-w-0">
589
+ {/* Main Area */}
590
+ <div className="flex-1 flex flex-col bg-[var(--kyro-bg)]">
701
591
  {/* URL Bar */}
702
- <div className="flex items-center gap-3 p-4 border-b border-[var(--kyro-border)]">
592
+ <div className="p-4 border-b border-[var(--kyro-border)] flex gap-2">
703
593
  <select
704
594
  value={currentRequest.method}
705
595
  onChange={(e) =>
706
- setCurrentRequest((prev) => ({ ...prev, method: e.target.value }))
596
+ setCurrentRequest({ ...currentRequest, method: e.target.value })
707
597
  }
708
- className={`px-3 py-2 rounded-lg font-bold text-sm ${getMethodColor(currentRequest.method)} text-white`}
598
+ className={`px-3 py-2 rounded-lg font-bold text-white ${getMethodColor(currentRequest.method)}`}
709
599
  >
710
- {["GET", "POST", "PUT", "PATCH", "DELETE"].map((m) => (
711
- <option key={m} value={m}>
712
- {m}
713
- </option>
714
- ))}
600
+ <option value="GET">GET</option>
601
+ <option value="POST">POST</option>
602
+ <option value="PATCH">PATCH</option>
603
+ <option value="DELETE">DELETE</option>
715
604
  </select>
716
- <input
717
- type="text"
718
- value={currentRequest.url}
719
- onChange={(e) =>
720
- setCurrentRequest((prev) => ({ ...prev, url: e.target.value }))
721
- }
722
- placeholder="Enter URL or paste {{variable}}"
723
- className="flex-1 px-4 py-2 bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-lg text-sm font-mono focus:outline-none focus:border-pink-500"
724
- />
605
+ <div className="flex-1 relative">
606
+ <input
607
+ type="text"
608
+ value={currentRequest.url}
609
+ onChange={(e) =>
610
+ setCurrentRequest({ ...currentRequest, url: e.target.value })
611
+ }
612
+ placeholder="Enter request URL..."
613
+ 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"
614
+ />
615
+ </div>
725
616
  <button type="button"
726
- onClick={executeRequest}
727
- disabled={loading || !currentRequest.url}
728
- className="px-6 py-2 bg-pink-500 text-white rounded-lg font-bold text-sm hover:bg-pink-600 disabled:opacity-50 transition-colors"
617
+ onClick={handleSend}
618
+ disabled={loading}
619
+ className="px-6 py-2 bg-pink-500 text-white rounded-lg font-bold hover:bg-pink-600 transition-colors disabled:opacity-50"
729
620
  >
730
- {loading ? "Sending..." : "Send"}
621
+ {loading ? "..." : "Send"}
731
622
  </button>
732
623
  <button type="button"
733
624
  onClick={() => setShowSaveModal(true)}
734
- className="px-4 py-2 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg font-bold text-sm hover:bg-[var(--kyro-surface)] transition-colors"
735
- title="Save request"
736
- >
737
- <svg
738
- className="w-4 h-4"
739
- fill="none"
740
- stroke="currentColor"
741
- viewBox="0 0 24 24"
742
- >
743
- <path
744
- strokeLinecap="round"
745
- strokeLinejoin="round"
746
- strokeWidth="2"
747
- d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"
748
- />
749
- </svg>
750
- </button>
751
- <button type="button"
752
- onClick={duplicateRequest}
753
- className="px-4 py-2 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg font-bold text-sm hover:bg-[var(--kyro-surface)] transition-colors"
754
- title="Duplicate"
625
+ 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)]"
755
626
  >
756
- <svg
757
- className="w-4 h-4"
758
- fill="none"
759
- stroke="currentColor"
760
- viewBox="0 0 24 24"
761
- >
762
- <path
763
- strokeLinecap="round"
764
- strokeLinejoin="round"
765
- strokeWidth="2"
766
- d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
767
- />
768
- </svg>
627
+ Save
769
628
  </button>
770
629
  </div>
771
630
 
772
- {/* Tabs */}
631
+ {/* Request Tabs */}
773
632
  <div className="flex border-b border-[var(--kyro-border)]">
774
- {(["headers", "body", "params"] as const).map((tab) => (
633
+ {(["params", "headers", "body"] as const).map((tab) => (
775
634
  <button type="button"
776
635
  key={tab}
777
636
  onClick={() => setActiveTab(tab)}
778
- className={`px-4 py-2 text-sm font-bold transition-colors ${
779
- activeTab === tab
780
- ? "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] border-b-2 border-pink-500"
781
- : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
782
- }`}
637
+ className={`px-6 py-2 text-xs font-bold tracking-widest ${activeTab === tab
638
+ ? "text-pink-500 border-b-2 border-pink-500"
639
+ : "text-[var(--kyro-text-muted)]"
640
+ }`}
783
641
  >
784
- {tab.charAt(0).toUpperCase() + tab.slice(1)}
785
- {tab === "headers" && (
786
- <span className="ml-2 text-xs text-[var(--kyro-text-muted)]">
787
- ({Object.keys(currentRequest.headers).filter((k) => k).length}
788
- )
789
- </span>
790
- )}
642
+ {tab}
791
643
  </button>
792
644
  ))}
793
645
  </div>
794
646
 
795
- {/* Tab Content */}
796
- <div className="flex-1 overflow-auto p-4">
797
- {activeTab === "headers" && (
798
- <div className="space-y-2">
799
- {Object.entries(currentRequest.headers).map(([key, value], i) => (
800
- <div key={i} className="flex items-center gap-2">
801
- <input
802
- type="text"
803
- value={key}
804
- onChange={(e) => updateHeader(e.target.value, value)}
805
- placeholder="Header name"
806
- className="flex-1 px-3 py-2 bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded text-sm focus:outline-none focus:border-pink-500"
807
- />
808
- <input
809
- type="text"
810
- value={value}
811
- onChange={(e) => updateHeader(key, e.target.value)}
812
- placeholder="Value"
813
- className="flex-1 px-3 py-2 bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded text-sm font-mono focus:outline-none focus:border-pink-500"
814
- />
815
- <button type="button"
816
- onClick={() => removeHeader(key)}
817
- className="p-2 text-red-500 hover:text-red-600"
818
- >
819
- <svg
820
- className="w-4 h-4"
821
- fill="none"
822
- stroke="currentColor"
823
- viewBox="0 0 24 24"
824
- >
825
- <path
826
- strokeLinecap="round"
827
- strokeLinejoin="round"
828
- strokeWidth="2"
829
- d="M6 18L18 6M6 6l12 12"
830
- />
831
- </svg>
832
- </button>
647
+ <div className="flex-1 flex flex-col overflow-hidden">
648
+ <div className="flex-1 p-4 overflow-y-auto">
649
+ {activeTab === "params" && (
650
+ <div className="text-xs text-[var(--kyro-text-muted)]">
651
+ Use query parameters in the URL (e.g. ?limit=10)
652
+ </div>
653
+ )}
654
+ {activeTab === "headers" && (
655
+ <div className="space-y-2">
656
+ <div className="text-xs text-[var(--kyro-text-muted)] mb-4">
657
+ Add custom headers as JSON
833
658
  </div>
834
- ))}
835
- <button type="button"
836
- onClick={addHeader}
837
- className="text-xs text-pink-500 hover:text-pink-600 font-medium"
838
- >
839
- + Add Header
840
- </button>
841
- </div>
842
- )}
843
-
844
- {activeTab === "body" && (
845
- <textarea
846
- value={currentRequest.body}
847
- onChange={(e) =>
848
- setCurrentRequest((prev) => ({ ...prev, body: e.target.value }))
849
- }
850
- placeholder='{"key": "value"}'
851
- className="w-full h-full min-h-[200px] p-4 bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-lg text-sm font-mono resize-none focus:outline-none focus:border-pink-500"
852
- />
853
- )}
854
-
855
- {activeTab === "params" && (
856
- <div className="text-sm text-[var(--kyro-text-secondary)]">
857
- <p>Add query parameters to your URL using:</p>
858
- <code className="block mt-2 p-2 bg-[var(--kyro-surface-accent)] rounded text-xs">
859
- ?param1=value1&param2=value2
860
- </code>
861
- </div>
862
- )}
863
- </div>
659
+ <textarea
660
+ value={JSON.stringify(currentRequest.headers, null, 2)}
661
+ onChange={(e) => {
662
+ try {
663
+ const headers = JSON.parse(e.target.value);
664
+ setCurrentRequest({ ...currentRequest, headers });
665
+ } catch (e) { }
666
+ }}
667
+ 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"
668
+ placeholder='{ "X-Custom-Header": "value" }'
669
+ />
670
+ </div>
671
+ )}
672
+ {activeTab === "body" && (
673
+ <div className="h-full flex flex-col">
674
+ <div className="text-xs text-[var(--kyro-text-muted)] mb-4">
675
+ Request Body (JSON)
676
+ </div>
677
+ <textarea
678
+ value={currentRequest.body}
679
+ onChange={(e) =>
680
+ setCurrentRequest({ ...currentRequest, body: e.target.value })
681
+ }
682
+ 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]"
683
+ placeholder='{ "key": "value" }'
684
+ />
685
+ </div>
686
+ )}
687
+ </div>
864
688
 
865
- {/* Response */}
866
- <div className="border-t border-[var(--kyro-border)]">
867
- <div className="flex items-center justify-between px-4 py-2 bg-[var(--kyro-surface-accent)]">
868
- <span className="text-sm font-bold text-[var(--kyro-text-secondary)]">
869
- Response
870
- </span>
871
- <div className="flex items-center gap-3">
872
- {responseStatus && (
873
- <>
874
- <span
875
- className={`px-2 py-0.5 rounded text-xs font-bold text-white ${getStatusColor(responseStatus)}`}
876
- >
877
- {responseStatus}
878
- </span>
879
- {responseTime && (
880
- <span className="text-xs text-[var(--kyro-text-muted)]">
881
- {responseTime}ms
689
+ {/* Response Pane */}
690
+ <div className="h-1/2 border-t border-[var(--kyro-border)] flex flex-col overflow-hidden bg-[var(--kyro-bg-secondary)]">
691
+ <div className="px-4 py-2 bg-[var(--kyro-surface)] border-b border-[var(--kyro-border)] flex items-center justify-between">
692
+ <span className="text-xs font-bold text-[var(--kyro-text-secondary)] tracking-widest">
693
+ Response
694
+ </span>
695
+ {response && (
696
+ <div className="flex gap-4">
697
+ <span className="text-[10px] font-bold">
698
+ STATUS:{" "}
699
+ <span className={response.status < 400 ? "text-green-500" : "text-red-500"}>
700
+ {response.status}
882
701
  </span>
883
- )}
884
- </>
702
+ </span>
703
+ <span className="text-[10px] font-bold text-[var(--kyro-text-muted)]">
704
+ TIME: {response.duration}ms
705
+ </span>
706
+ <span className="text-[10px] font-bold text-[var(--kyro-text-muted)]">
707
+ SIZE: {response.size}B
708
+ </span>
709
+ </div>
710
+ )}
711
+ </div>
712
+ <div className="flex-1 p-4 overflow-auto">
713
+ {loading ? (
714
+ <div className="flex items-center justify-center h-full">
715
+ <div className="animate-spin w-8 h-8 border-2 border-pink-500 border-t-transparent rounded-full" />
716
+ </div>
717
+ ) : error ? (
718
+ <div className="text-red-500 text-sm font-bold">{error}</div>
719
+ ) : response ? (
720
+ <pre className="text-xs font-mono text-[var(--kyro-text-primary)]">
721
+ {JSON.stringify(response.data, null, 2)}
722
+ </pre>
723
+ ) : (
724
+ <div className="h-full flex items-center justify-center text-[var(--kyro-text-muted)] text-sm italic">
725
+ Send a request to see the response
726
+ </div>
885
727
  )}
886
728
  </div>
887
729
  </div>
888
- <pre className="max-h-64 overflow-auto p-4 text-xs font-mono bg-[var(--kyro-surface)]">
889
- {response || "Send a request to see the response"}
890
- </pre>
891
730
  </div>
892
731
  </div>
893
732
 
894
- {/* Save Modal */}
733
+ {/* Modals */}
734
+ {showFolderModal && (
735
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[100] p-4">
736
+ <div className="surface-tile w-full max-w-md p-6 rounded-2xl shadow-2xl border border-[var(--kyro-border)]">
737
+ <h2 className="text-xl font-bold mb-4">Create Folder</h2>
738
+ <input
739
+ type="text"
740
+ value={newFolderName}
741
+ onChange={(e) => setNewFolderName(e.target.value)}
742
+ placeholder="Folder name..."
743
+ className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2 mb-6"
744
+ />
745
+ <div className="p-4 border-t border-[var(--kyro-border)] flex justify-end gap-2 bg-[var(--kyro-surface-accent)]">
746
+ <button type="button" onClick={() => setShowFolderModal(false)} className="kyro-btn kyro-btn-md kyro-btn-ghost">Cancel</button>
747
+ <button type="button" onClick={createFolder} className="kyro-btn kyro-btn-md kyro-btn-primary bg-pink-500 border-pink-500 text-white hover:bg-pink-600 hover:border-pink-600">Create</button>
748
+ </div>
749
+ </div>
750
+ </div>
751
+ )}
752
+
895
753
  {showSaveModal && (
896
- <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
897
- <div className="bg-[var(--kyro-surface)] rounded-lg p-6 w-96 border border-[var(--kyro-border)]">
898
- <h3 className="text-lg font-bold text-[var(--kyro-text-primary)] mb-4">
899
- Save Request
900
- </h3>
754
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[100] p-4">
755
+ <div className="surface-tile w-full max-w-md p-6 rounded-2xl shadow-2xl border border-[var(--kyro-border)]">
756
+ <h2 className="text-xl font-bold mb-4">Save Request</h2>
901
757
  <div className="space-y-4">
902
758
  <div>
903
- <label className="block text-sm font-bold text-[var(--kyro-text-secondary)] mb-1">
904
- Name
905
- </label>
759
+ <label className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)] block mb-1">Request Name</label>
906
760
  <input
907
761
  type="text"
908
- value={saveName}
909
- onChange={(e) => setSaveName(e.target.value)}
910
- placeholder={currentRequest.name}
911
- className="w-full px-3 py-2 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded text-sm focus:outline-none focus:border-pink-500"
762
+ value={saveRequestName}
763
+ onChange={(e) => setSaveRequestName(e.target.value)}
764
+ placeholder="e.g. List Posts..."
765
+ className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2"
912
766
  />
913
767
  </div>
914
768
  <div>
915
- <label className="block text-sm font-bold text-[var(--kyro-text-secondary)] mb-1">
916
- Folder
917
- </label>
769
+ <label className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)] block mb-1">Folder</label>
918
770
  <select
919
- value={saveFolder || ""}
920
- onChange={(e) => setSaveFolder(e.target.value || null)}
921
- className="w-full px-3 py-2 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded text-sm focus:outline-none focus:border-pink-500"
771
+ value={saveToFolderId}
772
+ onChange={(e) => setSaveToFolderId(e.target.value)}
773
+ className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2"
922
774
  >
923
- <option value="">Uncategorized</option>
924
- {folders.map((f) => (
925
- <option key={f.id} value={f.id}>
926
- {f.name}
927
- </option>
928
- ))}
775
+ <option value="">Select Folder...</option>
776
+ {folders.map(f => <option key={f.id} value={f.id}>{f.name}</option>)}
929
777
  </select>
930
778
  </div>
931
779
  </div>
932
- <div className="flex gap-3 mt-6">
933
- <button type="button"
934
- onClick={() => setShowSaveModal(false)}
935
- className="flex-1 px-4 py-2 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg font-bold text-sm hover:bg-[var(--kyro-surface)]"
936
- >
937
- Cancel
938
- </button>
939
- <button type="button"
940
- onClick={saveRequest}
941
- className="flex-1 px-4 py-2 bg-pink-500 text-white rounded-lg font-bold text-sm hover:bg-pink-600"
942
- >
943
- Save
944
- </button>
780
+ <div className="p-4 border-t border-[var(--kyro-border)] flex justify-end gap-2 bg-[var(--kyro-surface-accent)]">
781
+ <button type="button" onClick={() => setShowSaveModal(false)} className="kyro-btn kyro-btn-md kyro-btn-ghost">Cancel</button>
782
+ <button type="button" onClick={saveRequest} className="kyro-btn kyro-btn-md kyro-btn-primary 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>
945
783
  </div>
946
784
  </div>
947
785
  </div>