@kyro-cms/admin 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +149 -51
  2. package/package.json +52 -5
  3. package/src/collections/auth/index.ts +2 -2
  4. package/src/collections/portfolio/index.ts +343 -0
  5. package/src/components/ActionBar.tsx +153 -16
  6. package/src/components/Admin.tsx +136 -27
  7. package/src/components/ApiExplorer.tsx +325 -0
  8. package/src/components/ApiKeysManager.tsx +563 -0
  9. package/src/components/AuditLogsPage.tsx +664 -0
  10. package/src/components/AutoForm.tsx +1417 -661
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +3 -3
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +199 -57
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +786 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +191 -53
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +149 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/UserManagement.tsx +204 -0
  26. package/src/components/VersionHistoryPanel.tsx +3 -3
  27. package/src/components/WebhookManager.tsx +608 -0
  28. package/src/components/blocks/AccordionBlock.tsx +97 -0
  29. package/src/components/blocks/ArrayBlock.tsx +75 -0
  30. package/src/components/blocks/BlockEditModal.MARKER +12 -0
  31. package/src/components/blocks/BlockEditModal.tsx +774 -0
  32. package/src/components/blocks/ButtonBlock.tsx +165 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +66 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +151 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +64 -0
  38. package/src/components/blocks/HeadingBlock.tsx +81 -0
  39. package/src/components/blocks/HeroBlock.tsx +157 -0
  40. package/src/components/blocks/ImageBlock.tsx +83 -0
  41. package/src/components/blocks/LinkBlock.tsx +71 -0
  42. package/src/components/blocks/ListBlock.tsx +39 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +279 -0
  45. package/src/components/blocks/VStackBlock.tsx +75 -0
  46. package/src/components/blocks/VideoBlock.tsx +45 -0
  47. package/src/components/blocks/index.ts +10 -0
  48. package/src/components/fields/BlocksField.tsx +323 -0
  49. package/src/components/fields/CheckboxField.tsx +15 -9
  50. package/src/components/fields/CodeField.tsx +234 -0
  51. package/src/components/fields/DateField.tsx +38 -11
  52. package/src/components/fields/EditorClient.tsx +271 -0
  53. package/src/components/fields/FileField.tsx +390 -0
  54. package/src/components/fields/HybridContentField.tsx +109 -0
  55. package/src/components/fields/ImageField.tsx +429 -0
  56. package/src/components/fields/JSONField.tsx +361 -0
  57. package/src/components/fields/MarkdownField.tsx +282 -0
  58. package/src/components/fields/NumberField.tsx +42 -12
  59. package/src/components/fields/PortableTextField.tsx +143 -0
  60. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  61. package/src/components/fields/RelationshipField.tsx +231 -59
  62. package/src/components/fields/SelectField.tsx +25 -15
  63. package/src/components/fields/TextField.tsx +45 -14
  64. package/src/components/fields/extensions/blockComponents.tsx +237 -0
  65. package/src/components/fields/extensions/blocksStore.ts +273 -0
  66. package/src/components/fields/index.ts +13 -0
  67. package/src/components/index.ts +1 -2
  68. package/src/components/layout/Header.tsx +2 -2
  69. package/src/components/layout/Layout.tsx +2 -2
  70. package/src/components/ui/Badge.tsx +9 -4
  71. package/src/components/ui/BlockDrawer.tsx +79 -0
  72. package/src/components/ui/Button.tsx +1 -1
  73. package/src/components/ui/CommandPalette.tsx +362 -0
  74. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  75. package/src/components/ui/Dropdown.tsx +1 -1
  76. package/src/components/ui/Modal.tsx +37 -12
  77. package/src/components/ui/PromptModal.tsx +94 -0
  78. package/src/components/ui/SlidePanel.tsx +43 -16
  79. package/src/components/ui/Toast.tsx +80 -14
  80. package/src/env.d.ts +16 -0
  81. package/src/env.ts +20 -0
  82. package/src/index.ts +0 -1
  83. package/src/layouts/AdminLayout.astro +164 -170
  84. package/src/layouts/AuthLayout.astro +50 -0
  85. package/src/lib/MediaService.ts +541 -0
  86. package/src/lib/auth/sqlite-adapter.ts +319 -0
  87. package/src/lib/config.ts +22 -6
  88. package/src/lib/dataStore.ts +132 -74
  89. package/src/lib/db/adapter.ts +54 -0
  90. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  91. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  92. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  93. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  94. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  95. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  96. package/src/lib/db/index.ts +449 -0
  97. package/src/lib/db/mongodb-adapter.ts +207 -0
  98. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  99. package/src/lib/db/schema/mysql-auth.ts +113 -0
  100. package/src/lib/db/schema/mysql-content.ts +20 -0
  101. package/src/lib/db/schema/postgres-auth.ts +116 -0
  102. package/src/lib/db/schema/postgres-content.ts +35 -0
  103. package/src/lib/db/schema/postgres-media.ts +52 -0
  104. package/src/lib/db/schema/postgres-settings.ts +11 -0
  105. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  106. package/src/lib/db/schema/sqlite-content.ts +20 -0
  107. package/src/lib/graphql/index.ts +1 -0
  108. package/src/lib/graphql/schema.ts +443 -0
  109. package/src/lib/rate-limit.ts +267 -0
  110. package/src/lib/storage.ts +374 -0
  111. package/src/lib/store.ts +85 -0
  112. package/src/middleware.ts +116 -28
  113. package/src/pages/[collection]/[id].astro +178 -122
  114. package/src/pages/[collection]/index.astro +24 -156
  115. package/src/pages/admin/api-explorer.astro +98 -0
  116. package/src/pages/admin/graphql-explorer.astro +40 -0
  117. package/src/pages/admin/graphql.astro +97 -0
  118. package/src/pages/admin/index.astro +286 -0
  119. package/src/pages/admin/keys.astro +8 -0
  120. package/src/pages/admin/rest-playground.astro +44 -0
  121. package/src/pages/admin/webhooks.astro +8 -0
  122. package/src/pages/api/[collection]/[id]/publish.ts +44 -0
  123. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  124. package/src/pages/api/[collection]/[id]/versions.ts +36 -0
  125. package/src/pages/api/[collection]/[id].ts +102 -159
  126. package/src/pages/api/[collection]/index.ts +151 -230
  127. package/src/pages/api/auth/[id].ts +48 -69
  128. package/src/pages/api/auth/audit-logs.ts +20 -43
  129. package/src/pages/api/auth/login.ts +159 -45
  130. package/src/pages/api/auth/logout.ts +50 -20
  131. package/src/pages/api/auth/refresh.ts +119 -0
  132. package/src/pages/api/auth/register.ts +110 -40
  133. package/src/pages/api/auth/users.ts +22 -97
  134. package/src/pages/api/collections.ts +59 -0
  135. package/src/pages/api/globals/[slug]/test.ts +172 -0
  136. package/src/pages/api/globals/[slug].ts +42 -0
  137. package/src/pages/api/graphql.ts +90 -0
  138. package/src/pages/api/health.ts +417 -40
  139. package/src/pages/api/keys/[id].ts +26 -0
  140. package/src/pages/api/keys/index.ts +75 -0
  141. package/src/pages/api/media/[id].ts +309 -0
  142. package/src/pages/api/media/folders.ts +609 -0
  143. package/src/pages/api/media/index.ts +146 -0
  144. package/src/pages/api/media/resize.ts +267 -0
  145. package/src/pages/api/search.ts +82 -0
  146. package/src/pages/api/slug-availability.ts +70 -0
  147. package/src/pages/api/storage-config.ts +20 -0
  148. package/src/pages/api/storage-status.ts +206 -0
  149. package/src/pages/api/upload.ts +334 -0
  150. package/src/pages/api/webhooks/index.ts +71 -0
  151. package/src/pages/audit/index.astro +2 -104
  152. package/src/pages/login.astro +82 -0
  153. package/src/pages/media.astro +10 -0
  154. package/src/pages/preview/[collection]/[id].astro +178 -0
  155. package/src/pages/register.astro +102 -0
  156. package/src/pages/roles/index.astro +21 -21
  157. package/src/pages/settings/[slug].astro +162 -0
  158. package/src/pages/settings/index.astro +9 -0
  159. package/src/pages/users/[id].astro +29 -21
  160. package/src/pages/users/index.astro +22 -17
  161. package/src/pages/users/new.astro +18 -17
  162. package/src/styles/main.css +553 -128
  163. package/src/components/layout/Sidebar.tsx +0 -497
  164. package/src/pages/index.astro +0 -225
@@ -0,0 +1,403 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import {
3
+ Terminal,
4
+ Key,
5
+ PlayCircle,
6
+ Copy,
7
+ RefreshCcw,
8
+ Trash2,
9
+ ExternalLink,
10
+ ChevronRight,
11
+ Code2,
12
+ Lock,
13
+ Eye,
14
+ EyeOff,
15
+ } from "lucide-react";
16
+ import CodeMirror from "@uiw/react-codemirror";
17
+ import { json } from "@codemirror/lang-json";
18
+ import { aura } from "@uiw/codemirror-theme-aura";
19
+ import { Modal, ModalContent, ModalActions } from "./ui/Modal";
20
+
21
+ interface ApiKey {
22
+ id: string;
23
+ name: string;
24
+ key: string;
25
+ lastUsed?: string;
26
+ createdAt: string;
27
+ }
28
+
29
+ export function DeveloperCenter({ collections }: { collections: any }) {
30
+ const [keys, setKeys] = useState<ApiKey[]>([]);
31
+ const [loading, setLoading] = useState(false);
32
+ const [showKey, setShowKey] = useState<string | null>(null);
33
+ const [testEndpoint, setTestEndpoint] = useState("");
34
+ const [playgroundResult, setPlaygroundResult] = useState<unknown>(null);
35
+ const [exploring, setExploring] = useState(false);
36
+
37
+ const [showCreateModal, setShowCreateModal] = useState(false);
38
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
39
+ const [deleteKeyId, setDeleteKeyId] = useState<string | null>(null);
40
+ const [newKeyName, setNewKeyName] = useState("");
41
+
42
+ const loadKeys = async () => {
43
+ try {
44
+ const res = await fetch("/api/keys");
45
+ if (res.ok) {
46
+ const data = await res.json();
47
+ setKeys(data);
48
+ }
49
+ } catch (e) {
50
+ console.error(e);
51
+ }
52
+ };
53
+
54
+ useEffect(() => {
55
+ loadKeys();
56
+ }, []);
57
+
58
+ const handleGenerateKey = async () => {
59
+ setNewKeyName("");
60
+ setShowCreateModal(true);
61
+ };
62
+
63
+ const confirmGenerateKey = async () => {
64
+ if (!newKeyName.trim()) return;
65
+ try {
66
+ const res = await fetch("/api/keys", {
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify({ name: newKeyName }),
70
+ });
71
+ if (res.ok) {
72
+ loadKeys();
73
+ }
74
+ } catch (e) {
75
+ console.error(e);
76
+ }
77
+ setShowCreateModal(false);
78
+ setNewKeyName("");
79
+ };
80
+
81
+ const handleRevokeKey = async (id: string) => {
82
+ setDeleteKeyId(id);
83
+ setShowDeleteModal(true);
84
+ };
85
+
86
+ const confirmRevokeKey = async () => {
87
+ if (!deleteKeyId) return;
88
+ try {
89
+ const res = await fetch(`/api/keys/${deleteKeyId}`, { method: "DELETE" });
90
+ if (res.ok) {
91
+ loadKeys();
92
+ }
93
+ } catch (e) {
94
+ console.error(e);
95
+ }
96
+ setShowDeleteModal(false);
97
+ setDeleteKeyId(null);
98
+ };
99
+
100
+ const handleRunTest = async () => {
101
+ if (!testEndpoint) return;
102
+ setExploring(true);
103
+ try {
104
+ const response = await fetch(`/api/${testEndpoint}`);
105
+ const data = await response.json();
106
+ setPlaygroundResult(data);
107
+ } catch (e) {
108
+ setPlaygroundResult({
109
+ error: "Failed to fetch. Ensure the endpoint exists.",
110
+ });
111
+ } finally {
112
+ setExploring(false);
113
+ }
114
+ };
115
+
116
+ return (
117
+ <div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 px-8 pb-32">
118
+ {/* Header */}
119
+ <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 pt-4">
120
+ <div>
121
+ <h1 className="text-4xl font-black tracking-tighter text-[var(--kyro-text-primary)]">
122
+ Developer <span className="text-[var(--kyro-primary)]">Center</span>
123
+ </h1>
124
+ <p className="text-[var(--kyro-text-secondary)] mt-1 font-medium opacity-60">
125
+ Provision access keys and explore the headless API ecosystem.
126
+ </p>
127
+ </div>
128
+ <div className="flex items-center gap-3">
129
+ <button type="button"
130
+ onClick={handleGenerateKey}
131
+ className="flex items-center gap-2 px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-full font-black text-sm shadow-xl hover:shadow-[var(--kyro-primary)] active:scale-95 transition-all"
132
+ >
133
+ <Key className="w-4 h-4" />
134
+ Generate New Key
135
+ </button>
136
+ </div>
137
+ </div>
138
+
139
+ <div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
140
+ {/* API Key List */}
141
+ <section className="xl:col-span-2 space-y-6">
142
+ <div className="flex items-center justify-between px-2">
143
+ <div className="flex items-center gap-2">
144
+ <Lock className="w-4 h-4 text-[var(--kyro-primary)]" />
145
+ <span className="text-[10px] font-black uppercase tracking-widest opacity-40">
146
+ Access Credentials
147
+ </span>
148
+ </div>
149
+ </div>
150
+
151
+ <div className="space-y-4">
152
+ {keys.map((key) => (
153
+ <div
154
+ key={key.id}
155
+ className="surface-tile p-6 group transition-all duration-300 hover:border-[var(--kyro-primary)] hover:shadow-xl"
156
+ >
157
+ <div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
158
+ <div className="flex-1 min-w-0">
159
+ <h3 className="text-lg font-black tracking-tight mb-1">
160
+ {key.name}
161
+ </h3>
162
+ <div className="flex items-center gap-3">
163
+ <div className="flex-1 bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)] rounded-xl px-4 py-2 flex items-center justify-between group/key overflow-hidden">
164
+ <code className="text-xs font-mono opacity-80 truncate mr-4">
165
+ {showKey === key.id
166
+ ? key.key
167
+ : "••••••••••••••••••••••••••••••••"}
168
+ </code>
169
+ <div className="flex items-center gap-1 shrink-0">
170
+ <button type="button"
171
+ onClick={() =>
172
+ setShowKey(showKey === key.id ? null : key.id)
173
+ }
174
+ className="p-1.5 hover:bg-[var(--kyro-surface)] rounded-md transition-all text-[var(--kyro-text-secondary)]"
175
+ >
176
+ {showKey === key.id ? (
177
+ <EyeOff className="w-3.5 h-3.5" />
178
+ ) : (
179
+ <Eye className="w-3.5 h-3.5" />
180
+ )}
181
+ </button>
182
+ <button type="button" className="p-1.5 hover:bg-[var(--kyro-surface)] rounded-md transition-all text-[var(--kyro-text-secondary)]">
183
+ <Copy className="w-3.5 h-3.5" />
184
+ </button>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ <div className="flex items-center gap-4 mt-3 text-[10px] font-black uppercase tracking-widest opacity-30">
189
+ <span>
190
+ Created {new Date(key.createdAt).toLocaleDateString()}
191
+ </span>
192
+ <span>
193
+ Last used{" "}
194
+ {key.lastUsed
195
+ ? new Date(key.lastUsed).toLocaleDateString()
196
+ : "Never"}
197
+ </span>
198
+ </div>
199
+ </div>
200
+ <div className="flex items-center gap-3">
201
+ <button type="button"
202
+ onClick={() => handleRevokeKey(key.id)}
203
+ className="p-3 bg-red-500/5 text-red-500 rounded-xl hover:bg-red-500/10 transition-all border border-transparent hover:border-red-500/20"
204
+ title="Revoke Key"
205
+ >
206
+ <Trash2 className="w-4 h-4" />
207
+ </button>
208
+ </div>
209
+ </div>
210
+ </div>
211
+ ))}
212
+ </div>
213
+ </section>
214
+
215
+ {/* Sidebar Stats/Info */}
216
+ <section className="space-y-6">
217
+ <div className="surface-tile p-8 bg-[var(--kyro-primary)] group overflow-hidden relative">
218
+ <div className="relative z-10">
219
+ <Terminal className="w-8 h-8 text-white mb-4 opacity-40" />
220
+ <h3 className="text-white text-xl font-black tracking-tight mb-2">
221
+ Endpoint Hub
222
+ </h3>
223
+ <p className="text-white/70 text-sm font-medium leading-relaxed mb-6">
224
+ All content is delivered via our optimized REST API. Use your
225
+ keys to authorize requests.
226
+ </p>
227
+ <button type="button" className="w-full py-3 bg-white text-[var(--kyro-primary)] rounded-xl font-black text-xs uppercase tracking-widest hover:shadow-2xl transition-all flex items-center justify-center gap-2">
228
+ Review API Docs
229
+ <ExternalLink className="w-3.5 h-3.5" />
230
+ </button>
231
+ </div>
232
+ {/* Decorative glow */}
233
+ <div className="absolute -bottom-10 -right-10 w-40 h-40 bg-white opacity-10 blur-3xl group-hover:scale-150 transition-transform duration-700" />
234
+ </div>
235
+
236
+ <div className="surface-tile p-8">
237
+ <h4 className="text-[10px] font-black uppercase tracking-widest opacity-40 mb-6">
238
+ Base URLs
239
+ </h4>
240
+ <div className="space-y-4">
241
+ <div>
242
+ <p className="text-xs font-bold mb-2">
243
+ Content Delivery (Production)
244
+ </p>
245
+ <div className="bg-[var(--kyro-bg-secondary)] p-3 rounded-xl border border-[var(--kyro-border)] flex items-center justify-between group">
246
+ <code className="text-[10px] font-mono opacity-60">
247
+ https://api.kyro.io/v1
248
+ </code>
249
+ <Copy className="w-3.5 h-3.5 opacity-0 group-hover:opacity-40 cursor-pointer transition-opacity" />
250
+ </div>
251
+ </div>
252
+ <div>
253
+ <p className="text-xs font-bold mb-2">
254
+ Editor Snapshots (Preview)
255
+ </p>
256
+ <div className="bg-[var(--kyro-bg-secondary)] p-3 rounded-xl border border-[var(--kyro-border)] flex items-center justify-between group">
257
+ <code className="text-[10px] font-mono opacity-60">
258
+ https://preview.kyro.io/v1
259
+ </code>
260
+ <Copy className="w-3.5 h-3.5 opacity-0 group-hover:opacity-40 cursor-pointer transition-opacity" />
261
+ </div>
262
+ </div>
263
+ </div>
264
+ </div>
265
+ </section>
266
+
267
+ {/* Playground Explorer */}
268
+ <section className="xl:col-span-3 surface-tile p-8 space-y-8">
269
+ <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6">
270
+ <div>
271
+ <h2 className="text-2xl font-black tracking-tighter flex items-center gap-3">
272
+ <PlayCircle className="w-6 h-6 text-[var(--kyro-primary)]" />
273
+ API{" "}
274
+ <span className="text-[var(--kyro-primary)]">Playground</span>
275
+ </h2>
276
+ <p className="text-[var(--kyro-text-secondary)] text-sm font-medium opacity-60">
277
+ Test your endpoints and analyze response payloads in real-time.
278
+ </p>
279
+ </div>
280
+ <div className="flex items-center gap-3 flex-1 lg:max-w-xl">
281
+ <div className="relative flex-1 group">
282
+ <span className="absolute left-4 top-1/2 -translate-y-1/2 text-[10px] font-black opacity-40 mt-0.5">
283
+ GET /api/
284
+ </span>
285
+ <input
286
+ type="text"
287
+ value={testEndpoint}
288
+ onChange={(e) => setTestEndpoint(e.target.value)}
289
+ placeholder="collection-slug"
290
+ className="w-full pl-20 pr-4 py-3 bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)] rounded-2xl focus:outline-none focus:ring-2 focus:ring-[var(--kyro-primary)] transition-all font-mono text-xs font-bold"
291
+ />
292
+ </div>
293
+ <button type="button"
294
+ onClick={handleRunTest}
295
+ disabled={exploring || !testEndpoint}
296
+ className="px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-2xl font-black text-xs uppercase tracking-widest shadow-xl disabled:opacity-50 disabled:cursor-not-allowed hover:shadow-[var(--kyro-primary)] transition-all flex items-center gap-2 shrink-0"
297
+ >
298
+ {exploring ? (
299
+ <RefreshCcw className="w-3.5 h-3.5 animate-spin" />
300
+ ) : (
301
+ <ChevronRight className="w-4 h-4" />
302
+ )}
303
+ Fire
304
+ </button>
305
+ </div>
306
+ </div>
307
+
308
+ {playgroundResult ? (
309
+ <div className="animate-in fade-in slide-in-from-top-4 duration-500">
310
+ <div className="flex items-center gap-2 mb-4">
311
+ <Code2 className="w-4 h-4 opacity-40" />
312
+ <span className="text-[10px] font-black uppercase tracking-widest opacity-40">
313
+ JSON Response Payload
314
+ </span>
315
+ </div>
316
+ <div className="rounded-3xl overflow-hidden border border-[var(--kyro-border)] shadow-2xl">
317
+ <CodeMirror
318
+ value={JSON.stringify(playgroundResult, null, 2)}
319
+ height="350px"
320
+ theme={aura}
321
+ extensions={[json()]}
322
+ editable={false}
323
+ className="text-sm"
324
+ />
325
+ </div>
326
+ </div>
327
+ ) : (
328
+ <div className="h-64 rounded-3xl border-2 border-dashed border-[var(--kyro-border)] flex flex-col items-center justify-center opacity-30 select-none">
329
+ <Code2 className="w-12 h-12 mb-4" />
330
+ <p className="font-bold text-sm">
331
+ Enter an endpoint above to begin testing.
332
+ </p>
333
+ </div>
334
+ )}
335
+ </section>
336
+ </div>
337
+
338
+ {/* Create Modal */}
339
+ <Modal
340
+ open={showCreateModal}
341
+ onClose={() => setShowCreateModal(false)}
342
+ title="Create API Key"
343
+ >
344
+ <ModalContent>
345
+ <p className="text-sm text-[var(--kyro-text-secondary)] mb-4">
346
+ Give your API key a name to identify its purpose.
347
+ </p>
348
+ <input
349
+ type="text"
350
+ value={newKeyName}
351
+ onChange={(e) => setNewKeyName(e.target.value)}
352
+ placeholder="e.g., Production API"
353
+ className="w-full px-4 py-3 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
354
+ onKeyDown={(e) => e.key === "Enter" && confirmGenerateKey()}
355
+ />
356
+ </ModalContent>
357
+ <ModalActions>
358
+ <button type="button"
359
+ onClick={() => setShowCreateModal(false)}
360
+ className="px-4 py-2 rounded-lg font-medium text-sm border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)] transition-colors"
361
+ >
362
+ Cancel
363
+ </button>
364
+ <button type="button"
365
+ onClick={confirmGenerateKey}
366
+ className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90 transition-colors"
367
+ >
368
+ Create
369
+ </button>
370
+ </ModalActions>
371
+ </Modal>
372
+
373
+ {/* Delete Confirmation Modal */}
374
+ <Modal
375
+ open={showDeleteModal}
376
+ onClose={() => setShowDeleteModal(false)}
377
+ title="Revoke API Key"
378
+ variant="danger"
379
+ >
380
+ <ModalContent>
381
+ <p className="text-sm text-[var(--kyro-text-secondary)]">
382
+ Are you sure you want to revoke this key? Any integrations using it
383
+ will stop working.
384
+ </p>
385
+ </ModalContent>
386
+ <ModalActions>
387
+ <button type="button"
388
+ onClick={() => setShowDeleteModal(false)}
389
+ className="px-4 py-2 rounded-lg font-medium text-sm border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)] transition-colors"
390
+ >
391
+ Cancel
392
+ </button>
393
+ <button type="button"
394
+ onClick={confirmRevokeKey}
395
+ className="px-4 py-2 rounded-lg font-medium text-sm bg-red-500 text-white hover:bg-red-600 transition-colors"
396
+ >
397
+ Revoke
398
+ </button>
399
+ </ModalActions>
400
+ </Modal>
401
+ </div>
402
+ );
403
+ }