@kyro-cms/admin 0.3.1 → 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
@@ -6,11 +6,29 @@ import React, {
6
6
  Suspense,
7
7
  lazy,
8
8
  } from "react";
9
+ import {
10
+ Book,
11
+ Send,
12
+ Trash2,
13
+ Copy,
14
+ RefreshCw,
15
+ Settings,
16
+ Maximize2,
17
+ ChevronRight,
18
+ ChevronDown,
19
+ Search,
20
+ Type,
21
+ Activity,
22
+ Zap,
23
+ Info,
24
+ X
25
+ } from "./ui/icons";
9
26
 
10
27
  interface GraphQLPlaygroundProps {
11
28
  endpoint?: string;
12
29
  initialQuery?: string;
13
30
  initialVariables?: string;
31
+ initialShowDocs?: boolean;
14
32
  }
15
33
 
16
34
  interface QueryTab {
@@ -35,13 +53,13 @@ const DEFAULT_QUERY = `# Welcome to Kyro CMS GraphQL Playground
35
53
 
36
54
  # 1. Introspection - Discover the schema
37
55
  {
38
- _schema {
56
+ __schema {
39
57
  types {
40
58
  name
59
+ kind
41
60
  fields {
42
61
  name
43
- type
44
- required
62
+ type { name kind }
45
63
  }
46
64
  }
47
65
  }
@@ -52,7 +70,6 @@ const DEFAULT_QUERY = `# Welcome to Kyro CMS GraphQL Playground
52
70
  collections {
53
71
  slug
54
72
  name
55
- fields
56
73
  }
57
74
  }
58
75
 
@@ -60,27 +77,47 @@ const DEFAULT_QUERY = `# Welcome to Kyro CMS GraphQL Playground
60
77
  {
61
78
  ping
62
79
  }
80
+ `;
63
81
 
64
- # 4. Query posts (if available)
65
- {
66
- posts(page: 1, limit: 10) {
67
- docs {
68
- id
69
- title
70
- slug
71
- status
72
- }
73
- totalDocs
74
- totalPages
75
- page
76
- }
82
+ // --- Docs Types ---
83
+ interface TypeInfo {
84
+ name: string;
85
+ kind: string;
86
+ description?: string;
87
+ fields?: FieldInfo[];
88
+ inputFields?: FieldInfo[];
89
+ enumValues?: { name: string; description?: string; isDeprecated: boolean }[];
90
+ isDeprecated?: boolean;
91
+ }
92
+
93
+ interface FieldInfo {
94
+ name: string;
95
+ description?: string;
96
+ type: { name?: string; kind?: string; ofType?: Record<string, unknown> };
97
+ args: ArgInfo[];
98
+ isDeprecated?: boolean;
99
+ deprecationReason?: string;
100
+ }
101
+
102
+ interface ArgInfo {
103
+ name: string;
104
+ description?: string;
105
+ type: { name?: string; kind?: string; ofType?: Record<string, unknown> };
106
+ defaultValue?: string;
107
+ }
108
+
109
+ interface SchemaInfo {
110
+ queryType: { name: string };
111
+ mutationType?: { name: string };
112
+ subscriptionType?: { name: string };
113
+ types: TypeInfo[];
77
114
  }
78
- `;
79
115
 
80
116
  export function GraphQLPlayground({
81
117
  endpoint = "/api/graphql",
82
118
  initialQuery,
83
119
  initialVariables,
120
+ initialShowDocs = false,
84
121
  }: GraphQLPlaygroundProps) {
85
122
  const [token, setToken] = useState<string>("");
86
123
  const [isConnected, setIsConnected] = useState(false);
@@ -100,91 +137,102 @@ export function GraphQLPlayground({
100
137
  const [activeTab, setActiveTab] = useState<"query" | "variables" | "headers">(
101
138
  "query",
102
139
  );
103
- const [isDark, setIsDark] = useState(false);
104
140
  const [isMounted, setIsMounted] = useState(false);
141
+ const [showDocs, setShowDocs] = useState(initialShowDocs);
142
+ const [schema, setSchema] = useState<SchemaInfo | null>(null);
143
+ const [loadingSchema, setLoadingSchema] = useState(false);
144
+ const [selectedType, setSelectedType] = useState<TypeInfo | null>(null);
105
145
 
106
146
  useEffect(() => {
107
147
  setIsMounted(true);
108
- setIsDark(document.documentElement.classList.contains("dark"));
109
-
110
- const storedToken = localStorage.getItem("graphql_auth_token");
111
- if (storedToken) {
112
- setToken(storedToken);
113
- setIsConnected(true);
114
- }
115
-
116
- const observer = new MutationObserver(() => {
117
- setIsDark(document.documentElement.classList.contains("dark"));
118
- });
119
-
120
- observer.observe(document.documentElement, {
121
- attributes: true,
122
- attributeFilter: ["class"],
123
- });
124
-
125
- return () => observer.disconnect();
126
148
  }, []);
127
149
 
128
- const currentTab = tabs.find((t) => t.id === currentTabId) || tabs[0];
150
+ const currentTab = useMemo(
151
+ () => tabs.find((t) => t.id === currentTabId) || tabs[0],
152
+ [tabs, currentTabId],
153
+ );
129
154
 
130
- const handleTokenSubmit = useCallback(
131
- (e: React.FormEvent) => {
132
- e.preventDefault();
133
- if (token.trim()) {
134
- localStorage.setItem("graphql_auth_token", token.trim());
155
+ const fetchSchema = useCallback(async () => {
156
+ setLoadingSchema(true);
157
+ try {
158
+ const response = await fetch(endpoint, {
159
+ method: "POST",
160
+ headers: { "Content-Type": "application/json" },
161
+ body: JSON.stringify({
162
+ query: `
163
+ {
164
+ __schema {
165
+ queryType { name }
166
+ mutationType { name }
167
+ subscriptionType { name }
168
+ types {
169
+ name
170
+ kind
171
+ description
172
+ fields {
173
+ name
174
+ description
175
+ type { name kind ofType { name kind ofType { name kind } } }
176
+ args {
177
+ name
178
+ description
179
+ type { name kind ofType { name kind } }
180
+ defaultValue
181
+ }
182
+ isDeprecated
183
+ deprecationReason
184
+ }
185
+ inputFields {
186
+ name
187
+ description
188
+ type { name kind ofType { name kind } }
189
+ defaultValue
190
+ }
191
+ enumValues {
192
+ name
193
+ description
194
+ isDeprecated
195
+ }
196
+ }
197
+ }
198
+ }
199
+ `,
200
+ }),
201
+ });
202
+ const data = await response.json();
203
+ if (data.data && data.data.__schema) {
204
+ setSchema(data.data.__schema);
135
205
  setIsConnected(true);
136
- setTabs((prev) =>
137
- prev.map((tab) => ({
138
- ...tab,
139
- headers: JSON.stringify(
140
- { Authorization: `Bearer ${token.trim()}` },
141
- null,
142
- 2,
143
- ),
144
- })),
145
- );
146
206
  }
147
- },
148
- [token],
149
- );
150
-
151
- const clearToken = useCallback(() => {
152
- localStorage.removeItem("graphql_auth_token");
153
- setToken("");
154
- setIsConnected(false);
155
- }, []);
207
+ } catch (err) {
208
+ console.error("Failed to fetch schema", err);
209
+ } finally {
210
+ setLoadingSchema(false);
211
+ }
212
+ }, [endpoint]);
156
213
 
157
- const executeQuery = useCallback(async () => {
158
- if (!currentTab.query.trim()) {
159
- setError("Please enter a query");
160
- return;
214
+ useEffect(() => {
215
+ if (showDocs && !schema) {
216
+ fetchSchema();
161
217
  }
218
+ }, [showDocs, schema, fetchSchema]);
162
219
 
220
+ const handleRun = async () => {
163
221
  setIsLoading(true);
164
222
  setError(null);
165
-
166
223
  try {
167
- let variables: Record<string, any> | undefined;
168
- if (currentTab.variables.trim()) {
169
- try {
170
- variables = JSON.parse(currentTab.variables);
171
- } catch {
172
- setError("Invalid JSON in variables");
173
- setIsLoading(false);
174
- return;
175
- }
176
- }
177
-
178
- let headers: Record<string, string> = {
224
+ const headers: Record<string, string> = {
179
225
  "Content-Type": "application/json",
180
226
  };
181
- if (currentTab.headers.trim()) {
227
+ if (token) headers["Authorization"] = `Bearer ${token}`;
228
+
229
+ // Add custom headers from tab
230
+ if (currentTab.headers) {
182
231
  try {
183
- headers = { ...headers, ...JSON.parse(currentTab.headers) };
184
- } catch {
185
- setError("Invalid JSON in headers");
186
- setIsLoading(false);
187
- return;
232
+ const customHeaders = JSON.parse(currentTab.headers);
233
+ Object.assign(headers, customHeaders);
234
+ } catch (e) {
235
+ console.warn("Invalid custom headers JSON");
188
236
  }
189
237
  }
190
238
 
@@ -193,433 +241,245 @@ export function GraphQLPlayground({
193
241
  headers,
194
242
  body: JSON.stringify({
195
243
  query: currentTab.query,
196
- variables,
244
+ variables: currentTab.variables ? JSON.parse(currentTab.variables) : {},
197
245
  }),
198
246
  });
199
247
 
200
248
  const data = await res.json();
201
249
  setResponse(JSON.stringify(data, null, 2));
202
- } catch (err) {
203
- setError(err instanceof Error ? err.message : "Request failed");
250
+ if (data.errors) setError("Query returned errors");
251
+ } catch (err: unknown) {
252
+ const message = err instanceof Error ? err.message : "Request failed";
253
+ setError(message || "Request failed");
254
+ setResponse(JSON.stringify({ error: message }, null, 2));
204
255
  } finally {
205
256
  setIsLoading(false);
206
257
  }
207
- }, [currentTab, endpoint]);
208
-
209
- const addNewTab = useCallback(() => {
210
- const newTab: QueryTab = {
211
- id: `tab-${Date.now()}`,
212
- name: `Query ${tabs.length + 1}`,
213
- query: "# New Query\n{\n \n}",
214
- variables: "{}",
215
- headers: token
216
- ? JSON.stringify({ Authorization: `Bearer ${token}` }, null, 2)
217
- : "",
218
- };
219
- setTabs((prev) => [...prev, newTab]);
220
- setCurrentTabId(newTab.id);
221
- }, [tabs.length, token]);
222
-
223
- const closeTab = useCallback(
224
- (tabId: string, e: React.MouseEvent) => {
225
- e.stopPropagation();
226
- if (tabs.length <= 1) return;
227
-
228
- const newTabs = tabs.filter((t) => t.id !== tabId);
229
- setTabs(newTabs);
230
- if (currentTabId === tabId) {
231
- setCurrentTabId(newTabs[0].id);
232
- }
233
- },
234
- [tabs, currentTabId],
235
- );
236
-
237
- const updateTab = useCallback(
238
- (field: keyof QueryTab, value: string) => {
239
- setTabs((prev) =>
240
- prev.map((tab) =>
241
- tab.id === currentTabId ? { ...tab, [field]: value } : tab,
242
- ),
243
- );
244
- },
245
- [currentTabId],
246
- );
247
-
248
- // Keyboard shortcut: Ctrl+Enter to run
249
- useEffect(() => {
250
- const handleKeyDown = (e: KeyboardEvent) => {
251
- if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
252
- e.preventDefault();
253
- executeQuery();
254
- }
255
- };
256
-
257
- document.addEventListener("keydown", handleKeyDown);
258
- return () => document.removeEventListener("keydown", handleKeyDown);
259
- }, [executeQuery]);
260
-
261
- const theme = isDark ? aura : githubLight;
262
-
263
- const extensions = useMemo(() => [javascript({ jsx: true })], []);
258
+ };
259
+
260
+ const updateTab = (key: keyof QueryTab, value: string) => {
261
+ setTabs((prev) =>
262
+ prev.map((t) => (t.id === currentTabId ? { ...t, [key]: value } : t)),
263
+ );
264
+ };
265
+
266
+ const extensions = [javascript()];
267
+ const theme = aura;
268
+
269
+ const renderType = (type: Record<string, unknown>): string => {
270
+ if (!type) return "Unknown";
271
+ if (type.name) return type.name;
272
+ if (type.ofType) {
273
+ if (type.kind === "NON_NULL") return `${renderType(type.ofType)}!`;
274
+ if (type.kind === "LIST") return `[${renderType(type.ofType)}]`;
275
+ return renderType(type.ofType);
276
+ }
277
+ return "Unknown";
278
+ };
264
279
 
265
280
  return (
266
- <div className="h-full flex flex-col bg-[var(--kyro-surface)] rounded-lg overflow-hidden border border-[var(--kyro-border)]">
281
+ <div className="h-full flex flex-col bg-[var(--kyro-bg)] overflow-hidden rounded-lg border border-[var(--kyro-border)]">
267
282
  {/* Header */}
268
- <div className="flex items-center justify-between px-4 py-3 bg-[var(--kyro-surface-accent)] border-b border-[var(--kyro-border)]">
283
+ <div className="flex items-center justify-between px-4 py-3 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
269
284
  <div className="flex items-center gap-4">
270
285
  <div className="flex items-center gap-2">
271
- <svg
272
- className="w-5 h-5 text-pink-500"
273
- viewBox="0 0 24 24"
274
- fill="none"
275
- stroke="currentColor"
276
- >
277
- <path
278
- strokeLinecap="round"
279
- strokeLinejoin="round"
280
- strokeWidth="2"
281
- d="M13 10V3L4 14h7v7l9-11h-7z"
282
- />
283
- </svg>
284
- <span className="font-bold text-sm text-[var(--kyro-text-primary)]">
285
- GraphQL
286
- </span>
286
+ <div className="w-8 h-8 rounded-lg bg-pink-500/10 flex items-center justify-center text-pink-500">
287
+ <Zap className="w-4 h-4" />
288
+ </div>
289
+ <span className="font-bold text-sm tracking-tight">GraphQL Playground</span>
287
290
  </div>
288
- <div className="flex items-center gap-1 px-2 py-1 rounded bg-green-500/10">
289
- <div
290
- className={`w-2 h-2 rounded-full ${
291
- isConnected ? "bg-green-500" : "bg-gray-500"
292
- }`}
293
- />
294
- <span className="text-xs text-[var(--kyro-text-secondary)]">
295
- {isConnected ? "Connected" : "Not connected"}
291
+ <div className="h-4 w-px bg-[var(--kyro-border)]" />
292
+ <div className="flex items-center gap-1">
293
+ <div className={`w-2 h-2 rounded-full ${isConnected ? "bg-green-500" : "bg-red-500"} animate-pulse`} />
294
+ <span className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)]">
295
+ {isConnected ? "Connected" : "Disconnected"}
296
296
  </span>
297
297
  </div>
298
298
  </div>
299
-
300
- <div className="flex items-center gap-3">
301
- {isConnected && (
302
- <span className="text-xs text-[var(--kyro-text-muted)] font-mono">
303
- {token.slice(0, 12)}...
304
- </span>
305
- )}
306
- <button type="button"
307
- onClick={clearToken}
308
- className="text-xs text-pink-500 hover:text-pink-600 font-medium"
299
+ <div className="flex items-center gap-2">
300
+ <button
301
+ onClick={() => setShowDocs(!showDocs)}
302
+ className={`kyro-btn kyro-btn-md flex items-center gap-2 ${showDocs
303
+ ? "bg-pink-500 text-white border-pink-500 hover:bg-pink-600 hover:border-pink-600 shadow-[0_0_15px_rgba(236,72,153,0.3)]"
304
+ : "kyro-btn-ghost"
305
+ }`}
309
306
  >
310
- {isConnected ? "Disconnect" : "Connect"}
307
+ <Book className="w-3.5 h-3.5" />
308
+ Documentation
309
+ </button>
310
+ <button
311
+ onClick={handleRun}
312
+ disabled={isLoading}
313
+ className="kyro-btn kyro-btn-md kyro-btn-primary flex items-center gap-2"
314
+ >
315
+ <Send className="w-3.5 h-3.5" />
316
+ {isLoading ? "Running..." : "Run Query"}
311
317
  </button>
312
318
  </div>
313
319
  </div>
314
320
 
315
- {/* Token Input (when not connected) */}
316
- {!isConnected && (
317
- <div className="p-6 border-b border-[var(--kyro-border)]">
318
- <form onSubmit={handleTokenSubmit} className="flex gap-3">
319
- <input
320
- type="text"
321
- value={token}
322
- onChange={(e) => setToken(e.target.value)}
323
- placeholder="Enter auth token (Bearer ...)"
324
- 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"
325
- />
326
- <button
327
- type="submit"
328
- disabled={!token.trim()}
329
- className="px-6 py-2 bg-pink-500 text-white rounded-lg font-bold text-sm hover:bg-pink-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
330
- >
331
- Connect
332
- </button>
333
- </form>
334
- <p className="text-xs text-[var(--kyro-text-muted)] mt-2">
335
- Get your token from the Auth section or login endpoint
336
- </p>
337
- </div>
338
- )}
339
-
340
- {/* Tabs */}
341
- <div className="flex items-center gap-1 px-2 py-2 bg-[var(--kyro-surface-accent)] border-b border-[var(--kyro-border)] overflow-x-auto">
342
- {tabs.map((tab) => (
343
- <div
344
- key={tab.id}
345
- className={`group flex items-center gap-2 px-3 py-1.5 rounded-t cursor-pointer transition-colors ${
346
- currentTabId === tab.id
347
- ? "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"
348
- : "text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface)]"
349
- }`}
350
- onClick={() => setCurrentTabId(tab.id)}
351
- >
352
- <span className="text-xs font-medium">{tab.name}</span>
353
- {tabs.length > 1 && (
354
- <button type="button"
355
- onClick={(e) => closeTab(tab.id, e)}
356
- className="opacity-0 group-hover:opacity-100 text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)]"
321
+ <div className="flex-1 flex overflow-hidden">
322
+ {/* Left: Editor and Schema */}
323
+ <div className={`flex flex-col border-r border-[var(--kyro-border)] transition-all duration-300 ${showDocs ? "w-1/2" : "w-1/2"}`}>
324
+ {/* Tabs for Query/Vars/Headers */}
325
+ <div className="flex border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
326
+ {(["query", "variables", "headers"] as const).map((tab) => (
327
+ <button
328
+ key={tab}
329
+ onClick={() => setActiveTab(tab)}
330
+ className={`px-4 py-2 text-xs font-bold tracking-widest transition-all ${activeTab === tab
331
+ ? "text-pink-500 border-b-2 border-pink-500"
332
+ : "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)]"
333
+ }`}
357
334
  >
358
- <svg
359
- className="w-3 h-3"
360
- fill="none"
361
- stroke="currentColor"
362
- viewBox="0 0 24 24"
363
- >
364
- <path
365
- strokeLinecap="round"
366
- strokeLinejoin="round"
367
- strokeWidth="2"
368
- d="M6 18L18 6M6 6l12 12"
369
- />
370
- </svg>
335
+ {tab}
371
336
  </button>
372
- )}
337
+ ))}
373
338
  </div>
374
- ))}
375
- <button type="button"
376
- onClick={addNewTab}
377
- className="p-1.5 text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface)] rounded"
378
- >
379
- <svg
380
- className="w-4 h-4"
381
- fill="none"
382
- stroke="currentColor"
383
- viewBox="0 0 24 24"
384
- >
385
- <path
386
- strokeLinecap="round"
387
- strokeLinejoin="round"
388
- strokeWidth="2"
389
- d="M12 4v16m8-8H4"
390
- />
391
- </svg>
392
- </button>
393
- </div>
394
339
 
395
- {/* Editor Area */}
396
- <div className="flex-1 flex overflow-hidden">
397
- {/* Left: Query Editor */}
398
- <div className="flex-1 flex flex-col border-r border-[var(--kyro-border)]">
399
- {/* Editor Toolbar */}
400
- <div className="flex items-center gap-2 px-3 py-2 bg-[var(--kyro-surface)] border-b border-[var(--kyro-border)]">
401
- <button type="button"
402
- onClick={() => setActiveTab("query")}
403
- className={`px-3 py-1 text-xs font-bold rounded transition-colors ${
404
- activeTab === "query"
405
- ? "bg-pink-500 text-white"
406
- : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
407
- }`}
408
- >
409
- Query
410
- </button>
411
- <button type="button"
412
- onClick={() => setActiveTab("variables")}
413
- className={`px-3 py-1 text-xs font-bold rounded transition-colors ${
414
- activeTab === "variables"
415
- ? "bg-pink-500 text-white"
416
- : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
417
- }`}
418
- >
419
- Variables
420
- </button>
421
- <button type="button"
422
- onClick={() => setActiveTab("headers")}
423
- className={`px-3 py-1 text-xs font-bold rounded transition-colors ${
424
- activeTab === "headers"
425
- ? "bg-pink-500 text-white"
426
- : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
427
- }`}
428
- >
429
- Headers
430
- </button>
431
-
432
- <div className="flex-1" />
433
-
434
- <button type="button"
435
- onClick={executeQuery}
436
- disabled={isLoading}
437
- className="flex items-center gap-2 px-4 py-1.5 bg-pink-500 text-white rounded font-bold text-xs hover:bg-pink-600 transition-colors disabled:opacity-50"
438
- >
439
- {isLoading ? (
440
- <svg
441
- className="w-4 h-4 animate-spin"
442
- fill="none"
443
- viewBox="0 0 24 24"
444
- >
445
- <circle
446
- className="opacity-25"
447
- cx="12"
448
- cy="12"
449
- r="10"
450
- stroke="currentColor"
451
- strokeWidth="4"
452
- />
453
- <path
454
- className="opacity-75"
455
- fill="currentColor"
456
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
457
- />
458
- </svg>
459
- ) : (
460
- <svg
461
- className="w-4 h-4"
462
- fill="none"
463
- stroke="currentColor"
464
- viewBox="0 0 24 24"
465
- >
466
- <path
467
- strokeLinecap="round"
468
- strokeLinejoin="round"
469
- strokeWidth="2"
470
- d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
471
- />
472
- <path
473
- strokeLinecap="round"
474
- strokeLinejoin="round"
475
- strokeWidth="2"
476
- d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
477
- />
478
- </svg>
340
+ <div className="flex-1 overflow-hidden relative bg-[var(--kyro-bg)]">
341
+ <Suspense fallback={<div className="p-4 text-xs">Loading editor...</div>}>
342
+ {isMounted && (
343
+ <CodeMirrorEditor
344
+ value={activeTab === "query" ? currentTab.query : activeTab === "variables" ? currentTab.variables : currentTab.headers}
345
+ height="100%"
346
+ extensions={extensions}
347
+ theme={theme}
348
+ onChange={(val) => updateTab(activeTab, val)}
349
+ basicSetup={true}
350
+ style={{
351
+ height: "100%",
352
+ fontSize: "13px",
353
+ fontFamily: "'Fira Code', monospace",
354
+ }}
355
+ />
479
356
  )}
480
- Run
481
- <kbd className="ml-1 px-1 py-0.5 bg-white/20 rounded text-[10px]">
482
- ⌘↵
483
- </kbd>
484
- </button>
485
- </div>
486
-
487
- {/* Editor Content */}
488
- <div className="flex-1 overflow-auto">
489
- {activeTab === "query" && (
490
- <Suspense
491
- fallback={
492
- <textarea
493
- value={currentTab.query}
494
- onChange={(e) => updateTab("query", e.target.value)}
495
- className="w-full h-full min-h-[300px] p-4 bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] font-mono text-sm resize-none focus:outline-none"
496
- placeholder="Enter your GraphQL query..."
497
- spellCheck={false}
498
- />
499
- }
500
- >
501
- {isMounted && (
502
- <CodeMirrorEditor
503
- value={currentTab.query}
504
- height="100%"
505
- extensions={extensions}
506
- theme={theme}
507
- onChange={(value) => updateTab("query", value)}
508
- basicSetup={true}
509
- style={{
510
- height: "100%",
511
- minHeight: "300px",
512
- fontSize: "13px",
513
- fontFamily: "'Fira Code', 'JetBrains Mono', monospace",
514
- }}
515
- />
516
- )}
517
- </Suspense>
518
- )}
519
- {activeTab === "variables" && (
520
- <Suspense
521
- fallback={
522
- <textarea
523
- value={currentTab.variables}
524
- onChange={(e) => updateTab("variables", e.target.value)}
525
- className="w-full h-full min-h-[300px] p-4 bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] font-mono text-sm resize-none focus:outline-none"
526
- placeholder='{ "key": "value" }'
527
- spellCheck={false}
528
- />
529
- }
530
- >
531
- {isMounted && (
532
- <CodeMirrorEditor
533
- value={currentTab.variables}
534
- height="100%"
535
- extensions={extensions}
536
- theme={theme}
537
- onChange={(value) => updateTab("variables", value)}
538
- basicSetup={true}
539
- style={{
540
- height: "100%",
541
- minHeight: "300px",
542
- fontSize: "13px",
543
- fontFamily: "'Fira Code', 'JetBrains Mono', monospace",
544
- }}
545
- />
546
- )}
547
- </Suspense>
548
- )}
549
- {activeTab === "headers" && (
550
- <Suspense
551
- fallback={
552
- <textarea
553
- value={currentTab.headers}
554
- onChange={(e) => updateTab("headers", e.target.value)}
555
- className="w-full h-full min-h-[300px] p-4 bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] font-mono text-sm resize-none focus:outline-none"
556
- placeholder='{ "Authorization": "Bearer your-token" }'
557
- spellCheck={false}
558
- />
559
- }
560
- >
561
- {isMounted && (
562
- <CodeMirrorEditor
563
- value={currentTab.headers}
564
- height="100%"
565
- extensions={extensions}
566
- theme={theme}
567
- onChange={(value) => updateTab("headers", value)}
568
- basicSetup={true}
569
- style={{
570
- height: "100%",
571
- minHeight: "300px",
572
- fontSize: "13px",
573
- fontFamily: "'Fira Code', 'JetBrains Mono', monospace",
574
- }}
575
- />
576
- )}
577
- </Suspense>
578
- )}
357
+ </Suspense>
579
358
  </div>
580
359
  </div>
581
360
 
582
- {/* Right: Response */}
583
- <div className="flex-1 flex flex-col">
584
- <div className="flex items-center gap-2 px-3 py-2 bg-[var(--kyro-surface)] border-b border-[var(--kyro-border)]">
585
- <span className="text-xs font-bold text-[var(--kyro-text-secondary)]">
586
- Response
587
- </span>
588
- {error && (
589
- <span className="px-2 py-0.5 bg-red-500/10 text-red-500 text-xs rounded">
590
- Error
591
- </span>
592
- )}
593
- </div>
594
- <div className="flex-1 overflow-auto p-4">
595
- {isLoading ? (
596
- <div className="flex items-center justify-center h-full">
597
- <div className="animate-spin w-8 h-8 border-2 border-pink-500 border-t-transparent rounded-full" />
361
+ {/* Center/Right: Response or Docs */}
362
+ <div className="flex-1 flex flex-col min-w-0">
363
+ {showDocs ? (
364
+ <div className="flex-1 flex flex-col overflow-hidden bg-[var(--kyro-surface)]">
365
+ <div className="flex items-center justify-between px-4 py-2 border-b border-[var(--kyro-border)]">
366
+ <span className="text-xs font-bold tracking-widest text-[var(--kyro-text-secondary)]">Schema Explorer</span>
367
+ <button onClick={() => setShowDocs(false)} className="text-[var(--kyro-text-muted)] hover:text-red-500">
368
+ <X className="w-4 h-4" />
369
+ </button>
598
370
  </div>
599
- ) : response ? (
600
- <pre className="text-sm font-mono text-[var(--kyro-text-primary)] whitespace-pre-wrap">
601
- {response}
602
- </pre>
603
- ) : (
604
- <div className="flex flex-col items-center justify-center h-full text-[var(--kyro-text-muted)]">
605
- <svg
606
- className="w-12 h-12 mb-4 opacity-50"
607
- fill="none"
608
- stroke="currentColor"
609
- viewBox="0 0 24 24"
610
- >
611
- <path
612
- strokeLinecap="round"
613
- strokeLinejoin="round"
614
- strokeWidth="1.5"
615
- d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
616
- />
617
- </svg>
618
- <p className="text-sm">Run a query to see the response</p>
619
- <p className="text-xs mt-1">Press Cmd+Enter to run</p>
371
+ <div className="flex-1 overflow-y-auto p-4 space-y-4">
372
+ {loadingSchema ? (
373
+ <div className="flex items-center justify-center h-40">
374
+ <RefreshCw className="w-6 h-6 animate-spin text-pink-500" />
375
+ </div>
376
+ ) : schema ? (
377
+ <div className="space-y-6">
378
+ {selectedType ? (
379
+ <div className="space-y-4">
380
+ <button
381
+ onClick={() => setSelectedType(null)}
382
+ className="flex items-center gap-1 text-xs text-pink-500 font-bold hover:underline"
383
+ >
384
+ ← Back to types
385
+ </button>
386
+ <div>
387
+ <h3 className="text-lg font-bold text-[var(--kyro-text-primary)]">{selectedType.name}</h3>
388
+ <p className="text-xs text-[var(--kyro-text-muted)] italic">{selectedType.kind}</p>
389
+ {selectedType.description && (
390
+ <p className="mt-2 text-sm text-[var(--kyro-text-secondary)] leading-relaxed">{selectedType.description}</p>
391
+ )}
392
+ </div>
393
+ {selectedType.fields && (
394
+ <div className="space-y-3">
395
+ <h4 className="text-xs font-bold tracking-widest text-[var(--kyro-text-muted)] pt-4">Fields</h4>
396
+ {selectedType.fields.map(f => (
397
+ <div key={f.name} className="p-3 bg-[var(--kyro-surface-accent)] rounded-lg border border-[var(--kyro-border)]">
398
+ <div className="flex items-center justify-between gap-2">
399
+ <span className="font-bold text-sm text-[var(--kyro-text-primary)]">{f.name}</span>
400
+ <span className="text-[10px] font-mono text-pink-500 bg-pink-500/10 px-1.5 py-0.5 rounded">{renderType(f.type)}</span>
401
+ </div>
402
+ {f.description && <p className="text-xs text-[var(--kyro-text-secondary)] mt-1">{f.description}</p>}
403
+ {f.args && f.args.length > 0 && (
404
+ <div className="mt-2 pl-4 border-l-2 border-[var(--kyro-border)] space-y-1">
405
+ {f.args.map(a => (
406
+ <div key={a.name} className="text-[10px]">
407
+ <span className="text-[var(--kyro-text-muted)]">{a.name}:</span> <span className="text-pink-400">{renderType(a.type)}</span>
408
+ </div>
409
+ ))}
410
+ </div>
411
+ )}
412
+ </div>
413
+ ))}
414
+ </div>
415
+ )}
416
+ </div>
417
+ ) : (
418
+ <div className="space-y-4">
419
+ <div className="grid grid-cols-2 gap-2">
420
+ {["Query", "Mutation"].map(t => {
421
+ const found = schema.types.find(type => type.name === t);
422
+ if (!found) return null;
423
+ return (
424
+ <button
425
+ key={t}
426
+ onClick={() => setSelectedType(found)}
427
+ className="flex items-center justify-between p-4 bg-[var(--kyro-surface-accent)] rounded-xl border border-[var(--kyro-border)] hover:border-pink-500 transition-all text-left group"
428
+ >
429
+ <div>
430
+ <span className="text-xs font-bold text-[var(--kyro-text-muted)] block">{t}</span>
431
+ <span className="text-sm font-bold text-[var(--kyro-text-primary)]">Root Operations</span>
432
+ </div>
433
+ <ChevronRight className="w-4 h-4 text-pink-500 group-hover:translate-x-1 transition-transform" />
434
+ </button>
435
+ );
436
+ })}
437
+ </div>
438
+ <div className="space-y-2">
439
+ <h4 className="text-xs font-bold tracking-widest text-[var(--kyro-text-muted)] pt-4">All Types</h4>
440
+ {schema.types.filter(t => !t.name.startsWith("__") && t.kind === "OBJECT").map(t => (
441
+ <button
442
+ key={t.name}
443
+ onClick={() => setSelectedType(t)}
444
+ className="w-full flex items-center justify-between px-4 py-2 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all text-left"
445
+ >
446
+ <span className="text-sm font-medium text-[var(--kyro-text-primary)]">{t.name}</span>
447
+ <ChevronRight className="w-3.5 h-3.5 opacity-30" />
448
+ </button>
449
+ ))}
450
+ </div>
451
+ </div>
452
+ )}
453
+ </div>
454
+ ) : (
455
+ <p className="text-xs text-[var(--kyro-text-muted)]">No schema loaded.</p>
456
+ )}
620
457
  </div>
621
- )}
622
- </div>
458
+ </div>
459
+ ) : (
460
+ <div className="flex-1 flex flex-col overflow-hidden">
461
+ <div className="flex items-center px-4 py-2 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
462
+ <span className="text-xs font-bold tracking-widest text-[var(--kyro-text-secondary)]">Response</span>
463
+ </div>
464
+ <div className="flex-1 overflow-auto p-4 bg-[var(--kyro-bg-secondary)]">
465
+ {isLoading ? (
466
+ <div className="flex flex-col items-center justify-center h-full gap-4">
467
+ <RefreshCw className="w-8 h-8 animate-spin text-pink-500" />
468
+ <span className="text-xs font-bold text-[var(--kyro-text-muted)]">Running Query...</span>
469
+ </div>
470
+ ) : response ? (
471
+ <pre className="text-[13px] font-mono text-[var(--kyro-text-primary)] whitespace-pre-wrap selection:bg-pink-500/20">
472
+ {response}
473
+ </pre>
474
+ ) : (
475
+ <div className="flex flex-col items-center justify-center h-full opacity-30">
476
+ <Activity className="w-16 h-16 mb-4" />
477
+ <p className="text-sm font-bold">Query output will appear here</p>
478
+ </div>
479
+ )}
480
+ </div>
481
+ </div>
482
+ )}
623
483
  </div>
624
484
  </div>
625
485
  </div>