@kyro-cms/admin 0.3.2 → 0.3.5

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,22 +1,15 @@
1
- import React, { useState, useEffect } from "react";
2
- import { apiGet, apiPost, apiDelete } from "../lib/api";
1
+ import React, { useState } from "react";
2
+ import { apiPost } from "../lib/api";
3
+ import { useResourceManager } from "../lib/useResourceManager";
4
+ import { useUIStore } from "../lib/stores";
3
5
  import {
4
- Key,
5
- Plus,
6
- Trash2,
7
- Copy,
8
- CheckCircle2,
9
- Clock,
10
- Shield,
11
- Zap,
12
- AlertTriangle,
13
- X,
14
- Info,
15
- Terminal,
16
- ExternalLink,
17
- Code,
18
- } from "lucide-react";
19
- import { ConfirmModal, Modal, ModalContent, ModalActions } from "./ui/Modal";
6
+ Key, Plus, Trash2, Copy, CheckCircle2, Clock,
7
+ Shield, Zap, AlertTriangle, Info, Terminal,
8
+ Code, RefreshCw,
9
+ } from "./ui/icons";
10
+ import { PageHeader } from "./ui/PageHeader";
11
+ import { Badge } from "./ui/Badge";
12
+ import { Modal, ModalContent, ModalActions } from "./ui/Modal";
20
13
 
21
14
  interface ApiKeyItem {
22
15
  id: string;
@@ -26,81 +19,85 @@ interface ApiKeyItem {
26
19
  permissions?: string[];
27
20
  lastUsed?: string;
28
21
  createdAt: string;
22
+ expiresAt?: string;
29
23
  }
30
24
 
25
+ interface PermissionsOption {
26
+ label: string;
27
+ value: string;
28
+ }
29
+
30
+ const ALL_PERMISSIONS: PermissionsOption[] = [
31
+ { label: "All (*)", value: "*" },
32
+ { label: "Posts — Read", value: "posts:read" },
33
+ { label: "Posts — Create", value: "posts:create" },
34
+ { label: "Posts — Update", value: "posts:update" },
35
+ { label: "Posts — Delete", value: "posts:delete" },
36
+ { label: "Pages — Read", value: "pages:read" },
37
+ { label: "Pages — Create", value: "pages:create" },
38
+ { label: "Pages — Update", value: "pages:update" },
39
+ { label: "Pages — Delete", value: "pages:delete" },
40
+ { label: "Users — Read", value: "users:read" },
41
+ { label: "Users — Admin", value: "users:admin" },
42
+ { label: "Media — Read", value: "media:read" },
43
+ { label: "Media — Upload", value: "media:upload" },
44
+ { label: "API Keys — Read", value: "apikeys:read" },
45
+ { label: "API Keys — Admin", value: "apikeys:admin" },
46
+ ];
47
+
31
48
  export function ApiKeysManager() {
32
- const [keys, setKeys] = useState<ApiKeyItem[]>([]);
33
- const [loading, setLoading] = useState(false);
49
+ const { items: keys, loading, create, update, remove, isCreateModalOpen, setIsCreateModalOpen } =
50
+ useResourceManager<ApiKeyItem>(
51
+ React.useMemo(() => ({
52
+ endpoint: "/api/keys",
53
+ transformLoad: (data) => (data as ApiKeyItem[]).map((k) => ({
54
+ ...k,
55
+ keyPrefix: k.keyPrefix || k.key?.substring(0, 8) || "",
56
+ })),
57
+ }), [])
58
+ );
59
+
34
60
  const [newKey, setNewKey] = useState<ApiKeyItem | null>(null);
35
- const [showCreateModal, setShowCreateModal] = useState(false);
36
- const [showDeleteModal, setShowDeleteModal] = useState(false);
37
61
  const [showHelpModal, setShowHelpModal] = useState(false);
38
- const [showAlertModal, setShowAlertModal] = useState(false);
39
- const [alertMessage, setAlertMessage] = useState("");
40
- const [deleteKeyId, setDeleteKeyId] = useState<string | null>(null);
41
62
  const [newKeyName, setNewKeyName] = useState("");
63
+ const [newKeyPermissions, setNewKeyPermissions] = useState<string[]>(["*"]);
64
+ const [newKeyExpires, setNewKeyExpires] = useState("");
42
65
  const [copiedId, setCopiedId] = useState<string | null>(null);
43
66
  const [createError, setCreateError] = useState("");
44
-
45
- const loadKeys = async () => {
46
- setLoading(true);
47
- try {
48
- const data = await apiGet("/api/keys");
49
- setKeys(
50
- data.map((k: any) => ({
51
- ...k,
52
- key: k.key,
53
- keyPrefix: k.keyPrefix || k.key?.substring(0, 8) || "",
54
- })),
55
- );
56
- } catch (e) {
57
- console.error(e);
58
- } finally {
59
- setLoading(false);
60
- }
61
- };
62
-
63
- useEffect(() => {
64
- loadKeys();
65
- }, []);
67
+ const [rotatingId, setRotatingId] = useState<string | null>(null);
68
+ const { alert, confirm: kyroConfirm } = useUIStore();
66
69
 
67
70
  const handleCreateKey = async () => {
68
- if (!newKeyName.trim()) {
69
- setCreateError("Please enter a name for the API key");
70
- return;
71
- }
72
-
71
+ if (!newKeyName.trim()) { setCreateError("Name is required"); return; }
73
72
  try {
74
- const created = await apiPost("/api/keys", { name: newKeyName });
73
+ const data: Record<string, unknown> = { name: newKeyName, permissions: newKeyPermissions };
74
+ if (newKeyExpires) data.expiresAt = new Date(newKeyExpires).toISOString();
75
+ const created = await create(data);
75
76
  setNewKey(created);
76
- setShowCreateModal(false);
77
77
  setNewKeyName("");
78
+ setNewKeyPermissions(["*"]);
79
+ setNewKeyExpires("");
78
80
  setCreateError("");
79
- loadKeys();
80
- } catch (e) {
81
- console.error(e);
82
- setCreateError("Failed to create API key");
83
- }
84
- };
85
-
86
- const handleDeleteKey = async (id: string) => {
87
- setDeleteKeyId(id);
88
- setShowDeleteModal(true);
81
+ } catch { setCreateError("Failed to create API key"); }
89
82
  };
90
83
 
91
- const confirmDeleteKey = async () => {
92
- if (!deleteKeyId) return;
93
-
94
- try {
95
- await apiDelete(`/api/keys/${deleteKeyId}`);
96
- loadKeys();
97
- } catch (e) {
98
- console.error(e);
99
- setAlertMessage("Failed to delete API key");
100
- setShowAlertModal(true);
101
- }
102
- setShowDeleteModal(false);
103
- setDeleteKeyId(null);
84
+ const handleRotateKey = (key: ApiKeyItem) => {
85
+ kyroConfirm({
86
+ title: "Rotate API Key",
87
+ message: `Are you sure you want to regenerate the key "${key.name}"? The current key will stop working immediately and this action cannot be undone.`,
88
+ variant: "danger",
89
+ onConfirm: async () => {
90
+ setRotatingId(key.id);
91
+ try {
92
+ const rotated = await apiPost<ApiKeyItem>(`/api/keys/${key.id}/rotate`);
93
+ setNewKey(rotated);
94
+ } catch {
95
+ alert({ title: "Error", message: "Failed to rotate key. Please try again." });
96
+ } finally {
97
+ setRotatingId(null);
98
+ }
99
+ },
100
+ });
104
101
  };
105
102
 
106
103
  const copyToClipboard = (key: string, id: string) => {
@@ -110,314 +107,396 @@ export function ApiKeysManager() {
110
107
  };
111
108
 
112
109
  return (
113
- <div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 px-8 pb-32">
114
- {/* Header */}
115
- <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 pt-4">
116
- <div>
110
+ <div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-32">
111
+
112
+ <PageHeader
113
+ title="API Keys"
114
+ description="Programmatic tokens for secure infrastructure integration."
115
+ icon={Key}
116
+ actions={
117
117
  <div className="flex items-center gap-3">
118
- <h1 className="text-4xl font-black tracking-tighter text-[var(--kyro-text-primary)]">
119
- API <span className="text-[var(--kyro-primary)]">Keys</span>
120
- </h1>
121
118
  <button
122
119
  type="button"
123
120
  onClick={() => setShowHelpModal(true)}
124
- className="p-2 rounded-lg text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)] transition-all"
125
- title="Learn how API keys work"
121
+ className="px-4 py-2 bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] rounded-xl font-bold text-xs border border-[var(--kyro-border)] hover:bg-[var(--kyro-surface)] transition-all flex items-center gap-2"
126
122
  >
127
- <Info className="w-5 h-5" />
123
+ <Info className="w-4 h-4" />
124
+ <span>Integration Guide</span>
125
+ </button>
126
+ <button
127
+ type="button"
128
+ onClick={() => { setNewKeyName(""); setCreateError(""); setIsCreateModalOpen(true); }}
129
+ className="px-6 py-2.5 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-sm shadow-xl shadow-[var(--kyro-primary)]/10 hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center gap-2"
130
+ >
131
+ <Plus className="w-4 h-4" />
132
+ <span>Create Access Key</span>
128
133
  </button>
129
134
  </div>
130
- <p className="text-[var(--kyro-text-secondary)] mt-1 font-medium opacity-70">
131
- Secure tokens for authenticating API requests.
132
- </p>
133
- </div>
134
- <button
135
- type="button"
136
- onClick={() => {
137
- setNewKeyName("");
138
- setCreateError("");
139
- setShowCreateModal(true);
140
- }}
141
- 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:opacity-90 active:scale-95 transition-all"
142
- >
143
- <Plus className="w-4 h-4" />
144
- Create API Key
145
- </button>
146
- </div>
135
+ }
136
+ />
147
137
 
148
- {/* How to Use Section */}
149
- <div className="grid md:grid-cols-2 gap-4">
150
- {/* Quick Start */}
151
- <div className="surface-tile p-6">
152
- <div className="flex items-center gap-2 mb-3">
153
- <Terminal className="w-5 h-5 text-[var(--kyro-primary)]" />
154
- <h3 className="font-bold">Quick Start</h3>
155
- </div>
156
- <p className="text-sm text-[var(--kyro-text-secondary)] opacity-70 mb-3">
157
- Include your API key in requests:
158
- </p>
159
- <div className="space-y-2">
160
- <div className="bg-[var(--kyro-bg-secondary)] rounded-lg p-3 font-mono text-xs border border-[var(--kyro-border)]">
161
- <div className="text-[var(--kyro-text-muted)] mb-2">
162
- Example request:
163
- </div>
164
- <div className="text-[var(--kyro-primary)]">curl -X GET \</div>
165
- <div className="text-[var(--kyro-text-secondary)]">
166
- {" "}
167
- https://yoursite.com/api/posts \
138
+ <div className="grid md:grid-cols-2 gap-6">
139
+ {/* Terminal Card 1 */}
140
+ <div className="group relative overflow-hidden bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)] rounded-[2rem] p-1 hover:border-[var(--kyro-primary)]/30 transition-all duration-500">
141
+ <div className="p-6 bg-[var(--kyro-surface)] rounded-[1.8rem] h-full">
142
+ <div className="flex items-center justify-between mb-6">
143
+ <div className="flex items-center gap-3">
144
+ <div className="p-2 bg-blue-500/10 rounded-lg">
145
+ <Terminal className="w-4 h-4 text-blue-500" />
146
+ </div>
147
+ <h3 className="text-xs font-bold uppercase tracking-widest opacity-60">Shell / CURL</h3>
168
148
  </div>
169
- <div className="text-[var(--kyro-text-secondary)]">
170
- {" "}
171
- -H "Authorization: ApiKey kyro_xxx"
149
+ <div className="flex gap-1.5">
150
+ <div className="w-2.5 h-2.5 rounded-full bg-red-500/20" />
151
+ <div className="w-2.5 h-2.5 rounded-full bg-amber-500/20" />
152
+ <div className="w-2.5 h-2.5 rounded-full bg-green-500/20" />
172
153
  </div>
173
154
  </div>
174
- <button
175
- type="button"
176
- onClick={() => {
177
- navigator.clipboard.writeText(
178
- 'curl -X GET https://yoursite.com/api/posts -H "Authorization: ApiKey YOUR_KEY"',
179
- );
180
- }}
181
- className="text-xs text-[var(--kyro-primary)] hover:underline flex items-center gap-1"
182
- >
183
- <Copy className="w-3 h-3" /> Copy curl example
184
- </button>
155
+
156
+ <div className="relative group/code">
157
+ <pre className="p-5 bg-[var(--kyro-bg)] rounded-2xl border border-[var(--kyro-border)] font-mono text-[10px] leading-relaxed overflow-x-auto">
158
+ <div className="flex gap-4">
159
+ <span className="opacity-20 select-none w-4">1</span>
160
+ <span><span className="text-[var(--kyro-primary)]">curl</span> -X GET \</span>
161
+ </div>
162
+ <div className="flex gap-4">
163
+ <span className="opacity-20 select-none w-4">2</span>
164
+ <span> https://api.yoursite.com/v1/posts \</span>
165
+ </div>
166
+ <div className="flex gap-4">
167
+ <span className="opacity-20 select-none w-4">3</span>
168
+ <span> -H <span className="text-green-500">"Authorization: ApiKey kyro_xxx"</span></span>
169
+ </div>
170
+ </pre>
171
+ <button
172
+ type="button"
173
+ onClick={() => navigator.clipboard.writeText('curl -X GET https://api.yoursite.com/v1/posts -H "Authorization: ApiKey YOUR_KEY"')}
174
+ className="absolute top-3 right-3 p-2 bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-lg opacity-0 group-hover/code:opacity-100 transition-opacity hover:text-[var(--kyro-primary)] shadow-sm"
175
+ >
176
+ <Copy className="w-3.5 h-3.5" />
177
+ </button>
178
+ </div>
185
179
  </div>
186
180
  </div>
187
181
 
188
- {/* JavaScript/Fetch */}
189
- <div className="surface-tile p-6">
190
- <div className="flex items-center gap-2 mb-3">
191
- <Code className="w-5 h-5 text-[var(--kyro-primary)]" />
192
- <h3 className="font-bold">JavaScript / Fetch</h3>
193
- </div>
194
- <div className="bg-[var(--kyro-bg-secondary)] rounded-lg p-3 font-mono text-xs border border-[var(--kyro-border)]">
195
- <div className="text-[var(--kyro-text-muted)] mb-2">
196
- Example code:
197
- </div>
198
- <div>
199
- <span className="text-[var(--kyro-primary)]">await fetch</span>(
200
- <span className="text-green-600">'https://api'</span>, {"{"}
182
+ {/* Terminal Card 2 */}
183
+ <div className="group relative overflow-hidden bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)] rounded-[2rem] p-1 hover:border-[var(--kyro-primary)]/30 transition-all duration-500">
184
+ <div className="p-6 bg-[var(--kyro-surface)] rounded-[1.8rem] h-full">
185
+ <div className="flex items-center justify-between mb-6">
186
+ <div className="flex items-center gap-3">
187
+ <div className="p-2 bg-indigo-500/10 rounded-lg">
188
+ <Code className="w-4 h-4 text-indigo-500" />
189
+ </div>
190
+ <h3 className="text-xs font-bold uppercase tracking-widest opacity-60">JavaScript SDK</h3>
191
+ </div>
192
+ <div className="flex gap-1.5">
193
+ <div className="w-2.5 h-2.5 rounded-full bg-red-500/20" />
194
+ <div className="w-2.5 h-2.5 rounded-full bg-amber-500/20" />
195
+ <div className="w-2.5 h-2.5 rounded-full bg-green-500/20" />
196
+ </div>
201
197
  </div>
202
- <div> headers: {"{"}</div>
203
- <div>
204
- {" "}
205
- <span className="text-green-600">'Authorization'</span>:{" "}
206
- <span className="text-yellow-700">'ApiKey kyro_xxx'</span>,
198
+
199
+ <div className="relative group/code">
200
+ <pre className="p-5 bg-[var(--kyro-bg)] rounded-2xl border border-[var(--kyro-border)] font-mono text-[10px] leading-relaxed overflow-x-auto">
201
+ <div className="flex gap-4">
202
+ <span className="opacity-20 select-none w-4">1</span>
203
+ <span><span className="text-indigo-400">const</span> res = <span className="text-indigo-400">await</span> <span className="text-blue-400">fetch</span>(url, {"{"}</span>
204
+ </div>
205
+ <div className="flex gap-4">
206
+ <span className="opacity-20 select-none w-4">2</span>
207
+ <span> headers: {"{"} </span>
208
+ </div>
209
+ <div className="flex gap-4">
210
+ <span className="opacity-20 select-none w-4">3</span>
211
+ <span> <span className="text-green-500">'Authorization'</span>: <span className="text-green-500">'ApiKey xxx'</span></span>
212
+ </div>
213
+ <div className="flex gap-4">
214
+ <span className="opacity-20 select-none w-4">4</span>
215
+ <span> {"}"}</span>
216
+ </div>
217
+ <div className="flex gap-4">
218
+ <span className="opacity-20 select-none w-4">5</span>
219
+ <span>{"}"});</span>
220
+ </div>
221
+ </pre>
222
+ <button
223
+ type="button"
224
+ onClick={() => navigator.clipboard.writeText('await fetch(url, { headers: { "Authorization": "ApiKey YOUR_KEY" } })')}
225
+ className="absolute top-3 right-3 p-2 bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-lg opacity-0 group-hover/code:opacity-100 transition-opacity hover:text-[var(--kyro-primary)] shadow-sm"
226
+ >
227
+ <Copy className="w-3.5 h-3.5" />
228
+ </button>
207
229
  </div>
208
- <div> {"}"}</div>
209
- <div>{"}"})</div>
210
230
  </div>
211
231
  </div>
212
232
  </div>
213
233
 
214
- {/* Best Practices */}
215
- <div className="surface-tile p-4 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)]">
216
- <div className="flex items-start gap-3">
217
- <Shield className="w-5 h-5 text-amber-600 flex-shrink-0" />
218
- <div>
219
- <h4 className="font-bold text-[var(--kyro-text-primary)] mb-2">
220
- Best Practices for API Keys
221
- </h4>
222
- <ul className="text-sm text-[var(--kyro-text-secondary)] grid md:grid-cols-2 gap-2">
223
- <li className="flex items-center gap-2">
224
- <span className="text-amber-600">•</span> Never commit to
225
- version control
226
- </li>
227
- <li className="flex items-center gap-2">
228
- <span className="text-amber-600">•</span> Store in environment
229
- variables
230
- </li>
231
- <li className="flex items-center gap-2">
232
- <span className="text-amber-600">•</span> Use separate keys per
233
- app
234
- </li>
235
- <li className="flex items-center gap-2">
236
- <span className="text-amber-600">•</span> Revoke unused keys
237
- </li>
238
- </ul>
234
+ <div className="flex surface-tile flex-col gap-8">
235
+ {/* Warning/Best Practices Banner */}
236
+ <div className="relative overflow-hidden p-6 rounded-3xl border border-[var(--kyro-border)] bg-gradient-to-br from-[var(--kyro-surface-accent)] to-transparent group">
237
+ <div className="absolute top-0 right-0 p-8 opacity-5 group-hover:opacity-10 transition-opacity">
238
+ <Shield className="w-32 h-32 rotate-12" />
239
239
  </div>
240
- </div>
241
- </div>
242
240
 
243
- {/* New Key Alert */}
244
- {newKey && (
245
- <div className="surface-tile p-8 bg-green-500/10 border border-green-500/20">
246
- <div className="flex items-start gap-4">
247
- <div className="p-3 bg-green-500/20 rounded-2xl">
248
- <CheckCircle2 className="w-6 h-6 text-green-500" />
241
+ <div className="flex items-start gap-4 relative z-10">
242
+ <div className="p-3 bg-amber-500/10 rounded-2xl">
243
+ <Shield className="w-6 h-6 text-amber-500" />
249
244
  </div>
250
- <div className="flex-1">
251
- <h3 className="text-xl font-black text-green-600 mb-2">
252
- API Key Created Successfully
253
- </h3>
254
- <p className="text-sm text-[var(--kyro-text-secondary)] mb-4">
255
- Copy this key now.{" "}
256
- <span className="text-red-500 font-bold">
257
- This is the only time it will be shown.
258
- </span>
245
+ <div>
246
+ <h4 className="text-lg font-bold text-[var(--kyro-text-primary)] mb-1">Security Best Practices</h4>
247
+ <p className="text-sm text-[var(--kyro-text-secondary)] opacity-60 mb-4 max-w-2xl">
248
+ API keys grant full access to your resources. Treat them with the same level of security as your passwords.
259
249
  </p>
260
- <div className="flex items-center gap-3">
261
- <code className="flex-1 p-4 bg-[var(--kyro-bg)] border border-green-500/30 rounded-xl font-mono text-sm break-all">
262
- {newKey.key}
263
- </code>
250
+ <div className="grid sm:grid-cols-2 gap-x-8 gap-y-2">
251
+ {[
252
+ "Never commit keys to version control",
253
+ "Rotate keys every 90 days",
254
+ "Use specific permissions (least privilege)",
255
+ "Store keys in secure environment variables"
256
+ ].map((text, i) => (
257
+ <div key={i} className="flex items-center gap-2 text-xs font-medium text-[var(--kyro-text-secondary)]">
258
+ <div className="w-1.5 h-1.5 rounded-full bg-amber-500/50" />
259
+ {text}
260
+ </div>
261
+ ))}
262
+ </div>
263
+ </div>
264
+ </div>
265
+ </div>
266
+
267
+ {/* Success Banner for New Keys */}
268
+ {newKey && (
269
+ <div className="relative overflow-hidden p-8 rounded-3xl border border-green-500/30 bg-green-500/5 backdrop-blur-xl animate-in fade-in slide-in-from-top-4 duration-500">
270
+ <div className="flex items-start gap-6">
271
+ <div className="p-4 bg-green-500/10 rounded-2xl">
272
+ <CheckCircle2 className="w-8 h-8 text-green-500" />
273
+ </div>
274
+ <div className="flex-1">
275
+ <div className="flex items-center justify-between mb-2">
276
+ <h3 className="text-xl font-bold text-green-500">API Key Generated</h3>
277
+ <Badge variant="success" className="text-[10px] font-bold">New</Badge>
278
+ </div>
279
+ <p className="text-sm text-[var(--kyro-text-secondary)] mb-6">
280
+ This is the <span className="text-green-500 font-bold uppercase tracking-tight">only time</span> the full key will be shown. Please store it securely.
281
+ </p>
282
+
283
+ <div className="flex flex-col sm:flex-row gap-3">
284
+ <div className="flex-1 flex items-center gap-3 p-4 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-2xl font-mono text-sm break-all group relative">
285
+ <span className="opacity-80 select-all">{newKey.key}</span>
286
+ </div>
287
+ <button
288
+ type="button"
289
+ onClick={() => copyToClipboard(newKey.key!, newKey.id)}
290
+ className="flex items-center justify-center gap-2 px-8 py-4 bg-[var(--kyro-primary)] text-white rounded-2xl font-bold hover:scale-[1.02] active:scale-[0.98] transition-all shadow-lg shadow-[var(--kyro-primary)]/20"
291
+ >
292
+ {copiedId === newKey.id ? <CheckCircle2 className="w-5 h-5" /> : <Copy className="w-5 h-5" />}
293
+ <span>{copiedId === newKey.id ? "Copied" : "Copy Key"}</span>
294
+ </button>
295
+ </div>
296
+
264
297
  <button
265
298
  type="button"
266
- onClick={() => copyToClipboard(newKey.key!, newKey.id)}
267
- className="p-4 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl hover:opacity-90 flex-shrink-0"
268
- title="Copy to clipboard"
299
+ onClick={() => setNewKey(null)}
300
+ className="mt-6 text-xs font-bold uppercase tracking-widest text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-colors"
269
301
  >
270
- {copiedId === newKey.id ? (
271
- <CheckCircle2 className="w-5 h-5" />
272
- ) : (
273
- <Copy className="w-5 h-5" />
274
- )}
302
+ Dismiss Message
275
303
  </button>
276
304
  </div>
277
305
  </div>
278
306
  </div>
279
- <button
280
- type="button"
281
- onClick={() => setNewKey(null)}
282
- className="mt-6 text-sm font-bold text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
283
- >
284
- I've copied my key - dismiss this message
285
- </button>
286
- </div>
287
- )}
288
-
289
- {/* Keys List */}
290
- <section className="space-y-6">
291
- <div className="flex items-center gap-2 px-2">
292
- <Key className="w-4 h-4 text-[var(--kyro-primary)] opacity-40" />
293
- <span className="text-[10px] font-black uppercase tracking-widest opacity-40">
294
- Your API Keys
295
- </span>
296
- </div>
307
+ )}
297
308
 
298
- {loading ? (
299
- <div className="surface-tile p-8 text-center">Loading...</div>
300
- ) : keys.length === 0 ? (
301
- <div className="surface-tile p-12 text-center border-2 border-dashed border-[var(--kyro-border)]">
302
- <div className="w-16 h-16 mx-auto mb-6 bg-[var(--kyro-surface-accent)] rounded-2xl flex items-center justify-center">
303
- <Shield className="w-8 h-8 text-[var(--kyro-text-secondary)] opacity-20" />
309
+ <section className="space-y-6">
310
+ <div className="flex items-center justify-between px-2">
311
+ <div className="flex items-center gap-2">
312
+ <div className="w-1 h-4 bg-[var(--kyro-primary)] rounded-full" />
313
+ <h2 className="text-sm font-medium tracking-[0.2em] opacity-40">Active Credentials</h2>
314
+ </div>
315
+ <div className="text-[10px] font-bold opacity-40">
316
+ {keys.length} KEY{keys.length !== 1 && "S"}
304
317
  </div>
305
- <h3 className="text-lg font-black mb-2">No API Keys Yet</h3>
306
- <p className="text-sm text-[var(--kyro-text-secondary)] opacity-60 mb-6">
307
- Create your first API key to authenticate with the API.
308
- </p>
309
- <button
310
- type="button"
311
- onClick={() => setShowCreateModal(true)}
312
- className="px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-full font-black text-sm hover:opacity-90 transition-all"
313
- >
314
- <Plus className="w-4 h-4 inline mr-2" />
315
- Create Your First Key
316
- </button>
317
318
  </div>
318
- ) : (
319
- <div className="grid gap-4">
320
- {keys.map((key) => (
321
- <div
322
- key={key.id}
323
- className="surface-tile p-6 group hover:border-[var(--kyro-primary)] transition-all"
319
+
320
+ {loading ? (
321
+ <div className="flex items-center justify-center p-20 surface-tile rounded-3xl opacity-50 italic">
322
+ Synchronizing with vault...
323
+ </div>
324
+ ) : keys.length === 0 ? (
325
+ <div className="p-16 text-center rounded-[3rem] border-2 border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)]/30">
326
+ <div className="w-20 h-20 mx-auto mb-6 bg-gradient-to-tr from-[var(--kyro-surface)] to-[var(--kyro-surface-accent)] rounded-3xl flex items-center justify-center shadow-xl border border-[var(--kyro-border)]">
327
+ <Key className="w-10 h-10 text-[var(--kyro-primary)]" />
328
+ </div>
329
+ <h3 className="text-2xl font-bold mb-3">Initialize Your Access</h3>
330
+ <p className="text-sm text-[var(--kyro-text-secondary)] opacity-60 mb-8 max-w-sm mx-auto">
331
+ No API credentials found. Generate a key to begin interacting with the Kyro CMS programmatic interface.
332
+ </p>
333
+ <button
334
+ type="button"
335
+ onClick={() => setIsCreateModalOpen(true)}
336
+ className="inline-flex items-center gap-3 px-8 py-4 bg-[var(--kyro-primary)] text-white rounded-2xl font-bold hover:scale-[1.05] transition-all shadow-xl shadow-[var(--kyro-primary)]/10"
324
337
  >
325
- <div className="flex items-start justify-between gap-6">
326
- <div className="flex-1">
327
- <div className="flex items-center gap-3 mb-3">
328
- <h3 className="text-lg font-black">{key.name}</h3>
329
- <span className="px-2 py-0.5 bg-green-500/10 text-green-500 rounded-full text-[8px] font-black uppercase">
330
- Active
331
- </span>
332
- </div>
333
- <div className="flex items-center gap-2 text-sm font-mono text-[var(--kyro-text-secondary)] opacity-50 mb-4">
334
- <Key className="w-3 h-3" />
335
- <span>{key.keyPrefix}••••••••</span>
336
- <span className="text-[10px] opacity-40">
337
- (prefix shown)
338
- </span>
339
- </div>
340
- <div className="flex items-center gap-6 text-[10px] font-black uppercase opacity-40">
341
- <div className="flex items-center gap-2">
342
- <Clock className="w-3 h-3" />
343
- {key.lastUsed
344
- ? `Last used: ${new Date(key.lastUsed).toLocaleDateString()}`
345
- : "Never used"}
338
+ <Plus className="w-5 h-5" />
339
+ Generate API Key
340
+ </button>
341
+ </div>
342
+ ) : (
343
+ <div className="grid gap-4">
344
+ {keys.map((key) => (
345
+ <div
346
+ key={key.id}
347
+ className="group relative overflow-hidden bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-3xl p-6 hover:border-[var(--kyro-primary)]/50 transition-all duration-300"
348
+ >
349
+ <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 relative z-10">
350
+ <div className="flex-1">
351
+ <div className="flex items-center gap-3 mb-4">
352
+ <div className="p-2.5 bg-[var(--kyro-surface-accent)] rounded-xl group-hover:bg-[var(--kyro-primary)]/10 transition-colors">
353
+ <Key className="w-5 h-5 text-[var(--kyro-text-secondary)] group-hover:text-[var(--kyro-primary)] transition-colors" />
354
+ </div>
355
+ <div>
356
+ <h3 className="text-lg font-bold group-hover:text-[var(--kyro-primary)] transition-colors">{key.name}</h3>
357
+ <div className="flex items-center gap-2 mt-0.5">
358
+ <span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
359
+ <span className="text-[10px] font-bold uppercase tracking-wider text-green-500">Live</span>
360
+ </div>
361
+ </div>
346
362
  </div>
347
- <div className="flex items-center gap-2">
348
- <Zap className="w-3 h-3" />
349
- Created: {new Date(key.createdAt).toLocaleDateString()}
363
+
364
+ <div className="grid sm:grid-cols-3 gap-6 pt-2">
365
+ <div className="space-y-1">
366
+ <span className="text-[9px] font-bold uppercase tracking-widest opacity-30">Key Snippet</span>
367
+ <div className="font-mono text-sm opacity-60 tracking-tighter">
368
+ {key.keyPrefix}<span className="opacity-20">••••••••••••••••</span>
369
+ </div>
370
+ </div>
371
+ <div className="space-y-1 border-l border-[var(--kyro-border)] pl-6">
372
+ <span className="text-[9px] font-bold uppercase tracking-widest opacity-30">Usage Context</span>
373
+ <div className="flex flex-wrap gap-1">
374
+ {key.permissions?.includes("*") ? (
375
+ <Badge variant="warning" className="text-[8px] font-bold uppercase px-1.5">Full Access</Badge>
376
+ ) : (
377
+ key.permissions?.slice(0, 2).map((p) => (
378
+ <Badge key={p} variant="outline" className="text-[8px] font-bold px-1.5 opacity-60">{p}</Badge>
379
+ ))
380
+ )}
381
+ {key.permissions && key.permissions.length > 2 && (
382
+ <span className="text-[8px] font-bold opacity-30">+{key.permissions.length - 2} more</span>
383
+ )}
384
+ </div>
385
+ </div>
386
+ <div className="space-y-1 border-l border-[var(--kyro-border)] pl-6">
387
+ <span className="text-[9px] font-bold uppercase tracking-widest opacity-30">Last Activity</span>
388
+ <div className="text-[10px] font-bold opacity-60 flex items-center gap-1.5">
389
+ <Clock className="w-3 h-3" />
390
+ {key.lastUsed ? new Date(key.lastUsed).toLocaleDateString() : "Never used"}
391
+ </div>
392
+ </div>
350
393
  </div>
351
394
  </div>
352
- </div>
353
- <div className="flex items-center gap-2">
354
- <button
355
- type="button"
356
- onClick={() =>
357
- copyToClipboard(
358
- key.key || `${key.keyPrefix}...`,
359
- key.id,
360
- )
361
- }
362
- className="p-3 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-xl hover:bg-[var(--kyro-surface)] flex items-center gap-2"
363
- >
364
- {copiedId === key.id ? (
365
- <CheckCircle2 className="w-4 h-4 text-green-500" />
366
- ) : (
367
- <Copy className="w-4 h-4" />
368
- )}
369
- <span className="text-xs font-medium">
370
- {copiedId === key.id ? "Copied!" : "Copy"}
371
- </span>
372
- </button>
373
- <button
374
- type="button"
375
- onClick={() => handleDeleteKey(key.id)}
376
- className="p-3 text-red-500 bg-red-500/10 rounded-xl hover:bg-red-500/20"
377
- title="Delete API key"
378
- >
379
- <Trash2 className="w-4 h-4" />
380
- </button>
395
+
396
+ <div className="flex items-center gap-2 lg:bg-[var(--kyro-surface-accent)]/50 lg:p-2 lg:rounded-2xl lg:opacity-0 group-hover:opacity-100 transition-all duration-300 lg:translate-x-4 group-hover:translate-x-0">
397
+ <button
398
+ type="button"
399
+ onClick={() => copyToClipboard(key.key || `${key.keyPrefix}...`, key.id)}
400
+ className="flex-1 lg:flex-none p-3 bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-xl hover:border-[var(--kyro-primary)] transition-all flex items-center justify-center gap-2"
401
+ title="Copy key"
402
+ >
403
+ {copiedId === key.id ? <CheckCircle2 className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
404
+ <span className="text-xs font-bold">{copiedId === key.id ? "Copied" : "Copy"}</span>
405
+ </button>
406
+
407
+ <div className="w-px h-6 bg-[var(--kyro-border)] mx-1 hidden lg:block" />
408
+
409
+ <button
410
+ type="button"
411
+ onClick={() => handleRotateKey(key)}
412
+ disabled={rotatingId === key.id}
413
+ className="p-3 bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-xl hover:border-[var(--kyro-primary)] transition-all group/rotate"
414
+ title="Rotate Key"
415
+ >
416
+ <RefreshCw className={`w-4 h-4 text-[var(--kyro-text-secondary)] group-hover/rotate:text-[var(--kyro-primary)] ${rotatingId === key.id ? "animate-spin" : ""}`} />
417
+ </button>
418
+
419
+ <button
420
+ type="button"
421
+ onClick={() => remove(key.id, "API Key")}
422
+ className="p-3 bg-red-500/5 border border-red-500/10 rounded-xl hover:bg-red-500/10 hover:border-red-500/30 transition-all group/delete"
423
+ title="Revoke Access"
424
+ >
425
+ <Trash2 className="w-4 h-4 text-red-500/50 group-hover/delete:text-red-500" />
426
+ </button>
427
+ </div>
381
428
  </div>
382
429
  </div>
383
- </div>
384
- ))}
385
- </div>
386
- )}
387
- </section>
430
+ ))}
431
+ </div>
432
+ )}
433
+ </section>
434
+ </div>
388
435
 
389
436
  {/* Create Modal */}
390
- <Modal
391
- open={showCreateModal}
392
- onClose={() => setShowCreateModal(false)}
393
- title="Create New API Key"
394
- >
437
+ <Modal size="lg" open={isCreateModalOpen} onClose={() => setIsCreateModalOpen(false)} title="Create New API Key">
395
438
  <ModalContent>
396
- <p className="text-sm text-[var(--kyro-text-secondary)] mb-4">
397
- Give your API key a descriptive name to identify its purpose. You
398
- can create multiple keys for different use cases.
399
- </p>
400
- <input
401
- type="text"
402
- value={newKeyName}
403
- onChange={(e) => {
404
- setNewKeyName(e.target.value);
405
- setCreateError("");
406
- }}
407
- placeholder="e.g., Production App, Staging, Mobile App"
408
- 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)]"
409
- onKeyDown={(e) => e.key === "Enter" && handleCreateKey()}
410
- />
411
- {createError && (
412
- <p className="mt-2 text-sm text-red-500">{createError}</p>
413
- )}
414
- <div className="mt-4 p-4 bg-amber-500/10 border border-amber-500/20 rounded-xl flex items-start gap-3">
415
- <AlertTriangle className="w-5 h-5 text-amber-500 flex-shrink-0 mt-0.5" />
439
+ <div className="space-y-5">
440
+ <div>
441
+ <label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)]">Name</label>
442
+ <input
443
+ type="text"
444
+ value={newKeyName}
445
+ onChange={(e) => { setNewKeyName(e.target.value); setCreateError(""); }}
446
+ placeholder="e.g., Production App, Staging, Mobile App"
447
+ 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)]"
448
+ onKeyDown={(e) => e.key === "Enter" && handleCreateKey()}
449
+ />
450
+ {createError && <p className="mt-1.5 text-xs text-red-500">{createError}</p>}
451
+ </div>
452
+
416
453
  <div>
417
- <p className="text-xs font-bold text-amber-600 mb-1">Important</p>
418
- <p className="text-xs text-[var(--kyro-text-secondary)]">
419
- The API key will be shown only once after creation. Copy it
420
- immediately and store it securely.
454
+ <label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)]">Permissions</label>
455
+ <div className="space-y-1 max-h-48 overflow-y-auto">
456
+ {ALL_PERMISSIONS.map((opt) => (
457
+ <label key={opt.value} className="flex items-center gap-3 p-2 rounded-lg hover:bg-[var(--kyro-surface-accent)] cursor-pointer">
458
+ <input
459
+ type="checkbox"
460
+ checked={newKeyPermissions.includes(opt.value)}
461
+ onChange={(e) => {
462
+ if (opt.value === "*") {
463
+ setNewKeyPermissions(e.target.checked ? ["*"] : []);
464
+ } else {
465
+ setNewKeyPermissions(
466
+ e.target.checked
467
+ ? [...newKeyPermissions.filter((p) => p !== "*"), opt.value]
468
+ : newKeyPermissions.filter((p) => p !== opt.value)
469
+ );
470
+ }
471
+ }}
472
+ className="accent-[var(--kyro-primary)]"
473
+ />
474
+ <span className="text-sm font-mono">{opt.label}</span>
475
+ </label>
476
+ ))}
477
+ </div>
478
+ <p className="mt-1 text-[10px] text-[var(--kyro-text-muted)]">
479
+ Select specific permissions or choose "*" for full access.
480
+ </p>
481
+ </div>
482
+
483
+ <div>
484
+ <label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)]">Expires (optional)</label>
485
+ <input
486
+ type="datetime-local"
487
+ value={newKeyExpires}
488
+ onChange={(e) => setNewKeyExpires(e.target.value)}
489
+ 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)]"
490
+ />
491
+ <p className="mt-1 text-[10px] text-[var(--kyro-text-muted)]">
492
+ Leave empty for no expiration. Time is in UTC.
493
+ </p>
494
+ </div>
495
+
496
+ <div className="p-3 bg-amber-500/10 border border-amber-500/20 rounded-xl flex items-start gap-2.5">
497
+ <AlertTriangle className="w-4 h-4 text-amber-500 flex-shrink-0 mt-0.5" />
498
+ <p className="text-[11px] text-amber-600 font-medium">
499
+ The key will be shown only once after creation — copy it immediately.
421
500
  </p>
422
501
  </div>
423
502
  </div>
@@ -425,8 +504,8 @@ export function ApiKeysManager() {
425
504
  <ModalActions>
426
505
  <button
427
506
  type="button"
428
- onClick={() => setShowCreateModal(false)}
429
- 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"
507
+ onClick={() => setIsCreateModalOpen(false)}
508
+ 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)] transition-colors"
430
509
  >
431
510
  Cancel
432
511
  </button>
@@ -440,70 +519,24 @@ export function ApiKeysManager() {
440
519
  </ModalActions>
441
520
  </Modal>
442
521
 
443
- {/* Delete Confirmation Modal */}
444
- <Modal
445
- open={showDeleteModal}
446
- onClose={() => setShowDeleteModal(false)}
447
- title="Delete API Key"
448
- variant="danger"
449
- >
450
- <ModalContent>
451
- <p className="text-sm text-[var(--kyro-text-secondary)] mb-4">
452
- Are you sure you want to delete this API key? This action cannot be
453
- undone.
454
- </p>
455
- <div className="p-4 bg-red-500/10 border border-red-500/20 rounded-xl">
456
- <p className="text-sm font-medium text-red-500">
457
- Any applications or integrations using this key will immediately
458
- lose access.
459
- </p>
460
- </div>
461
- </ModalContent>
462
- <ModalActions>
463
- <button
464
- type="button"
465
- onClick={() => setShowDeleteModal(false)}
466
- 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"
467
- >
468
- Keep Key
469
- </button>
470
- <button
471
- type="button"
472
- onClick={confirmDeleteKey}
473
- className="px-4 py-2 rounded-lg font-medium text-sm bg-red-500 text-white hover:bg-red-600 transition-colors"
474
- >
475
- Delete Permanently
476
- </button>
477
- </ModalActions>
478
- </Modal>
479
-
480
522
  {/* Help Modal */}
481
- <Modal
482
- open={showHelpModal}
483
- onClose={() => setShowHelpModal(false)}
484
- title="How API Keys Work"
485
- >
523
+ <Modal size="lg" open={showHelpModal} onClose={() => setShowHelpModal(false)} title="How API Keys Work">
486
524
  <ModalContent>
487
525
  <div className="space-y-6">
488
526
  <div>
489
527
  <h4 className="font-bold mb-2">What is an API key?</h4>
490
528
  <p className="text-sm text-[var(--kyro-text-secondary)]">
491
- An API key is a unique token that authenticates your requests to
492
- the API. Think of it as a password that's specifically for
493
- programmatic access.
529
+ An API key is a unique token that authenticates your requests to the API. Think of it as a password that's specifically for programmatic access.
494
530
  </p>
495
531
  </div>
496
532
  <div>
497
533
  <h4 className="font-bold mb-2">How to use it</h4>
498
534
  <p className="text-sm text-[var(--kyro-text-secondary)] mb-3">
499
- Add your API key to the Authorization header of your HTTP
500
- requests:
535
+ Add your API key to the Authorization header of your HTTP requests:
501
536
  </p>
502
537
  <div className="bg-[var(--kyro-bg)] rounded-lg p-4 font-mono text-sm space-y-2">
503
538
  <div>
504
- <span className="text-[var(--kyro-text-secondary)]">
505
- Authorization:
506
- </span>{" "}
539
+ <span className="text-[var(--kyro-text-secondary)]">Authorization:</span>{" "}
507
540
  <span className="text-[var(--kyro-primary)]">ApiKey </span>
508
541
  <span className="text-green-500">kyro_xxxxxxxxxxxx</span>
509
542
  </div>
@@ -513,9 +546,7 @@ export function ApiKeysManager() {
513
546
  <h4 className="font-bold mb-2">Best practices</h4>
514
547
  <ul className="text-sm text-[var(--kyro-text-secondary)] space-y-2 list-disc list-inside">
515
548
  <li>Never share your API key publicly</li>
516
- <li>
517
- Store it securely (environment variables, secrets manager)
518
- </li>
549
+ <li>Store it securely (environment variables, secrets manager)</li>
519
550
  <li>Create separate keys for different applications</li>
520
551
  <li>Revoke keys that are no longer in use</li>
521
552
  </ul>
@@ -532,28 +563,6 @@ export function ApiKeysManager() {
532
563
  </button>
533
564
  </ModalActions>
534
565
  </Modal>
535
-
536
- {/* Alert Modal */}
537
- <Modal
538
- open={showAlertModal}
539
- onClose={() => setShowAlertModal(false)}
540
- title="Error"
541
- >
542
- <ModalContent>
543
- <p className="text-sm text-[var(--kyro-text-secondary)]">
544
- {alertMessage}
545
- </p>
546
- </ModalContent>
547
- <ModalActions>
548
- <button
549
- type="button"
550
- onClick={() => setShowAlertModal(false)}
551
- 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"
552
- >
553
- OK
554
- </button>
555
- </ModalActions>
556
- </Modal>
557
566
  </div>
558
567
  );
559
- }
568
+ }