@kyro-cms/admin 0.9.0 → 0.9.2

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 (114) hide show
  1. package/dist/index.cjs +11715 -11292
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +67 -65
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.d.cts +564 -0
  6. package/dist/index.d.ts +11 -10
  7. package/dist/index.js +11326 -10912
  8. package/dist/index.js.map +1 -1
  9. package/package.json +16 -12
  10. package/src/components/ActionBar.tsx +25 -161
  11. package/src/components/Admin.tsx +2 -4
  12. package/src/components/ApiKeysManager.tsx +5 -5
  13. package/src/components/AuditLogsPage.tsx +2 -13
  14. package/src/components/AutoForm.tsx +572 -461
  15. package/src/components/BrandingHub.tsx +7 -4
  16. package/src/components/CreateView.tsx +2 -0
  17. package/src/components/DetailView.tsx +52 -65
  18. package/src/components/DeveloperCenter.tsx +8 -6
  19. package/src/components/FieldRenderer.tsx +94 -19
  20. package/src/components/ListView.tsx +57 -216
  21. package/src/components/MediaGallery.tsx +334 -367
  22. package/src/components/PluginsManager.tsx +197 -70
  23. package/src/components/RestPlayground.tsx +59 -52
  24. package/src/components/SessionsManager.tsx +1 -1
  25. package/src/components/SettingsPage.tsx +22 -0
  26. package/src/components/Sidebar.astro +13 -41
  27. package/src/components/UserManagement.tsx +153 -15
  28. package/src/components/UserMenu.tsx +30 -4
  29. package/src/components/VersionHistoryPanel.tsx +112 -119
  30. package/src/components/WebhookManager.tsx +6 -4
  31. package/src/components/blocks/ArrayBlock.tsx +6 -23
  32. package/src/components/blocks/BlockEditModal.tsx +82 -309
  33. package/src/components/blocks/CardBlock.tsx +35 -0
  34. package/src/components/blocks/ChildBlocksTree.tsx +57 -31
  35. package/src/components/blocks/GenericBlock.tsx +44 -0
  36. package/src/components/blocks/HeadingSubheadingBlock.tsx +32 -0
  37. package/src/components/blocks/HeroBlock.tsx +5 -14
  38. package/src/components/blocks/RichTextBlock.tsx +5 -5
  39. package/src/components/blocks/index.ts +5 -3
  40. package/src/components/fields/AccordionField.tsx +2 -2
  41. package/src/components/fields/ArrayField.tsx +1 -1
  42. package/src/components/fields/ArrayLayout.tsx +120 -29
  43. package/src/components/fields/BlocksField.tsx +433 -55
  44. package/src/components/fields/CardField.tsx +73 -0
  45. package/src/components/fields/CheckboxField.tsx +7 -3
  46. package/src/components/fields/DateField.tsx +4 -1
  47. package/src/components/fields/GroupLayout.tsx +2 -2
  48. package/src/components/fields/HeadingSubheadingField.tsx +43 -0
  49. package/src/components/fields/ListField.tsx +2 -2
  50. package/src/components/fields/NumberField.tsx +4 -1
  51. package/src/components/fields/RelationshipBlockField.tsx +2 -3
  52. package/src/components/fields/RelationshipField.tsx +155 -90
  53. package/src/components/fields/RichTextField.tsx +781 -0
  54. package/src/components/fields/SecretField.tsx +102 -0
  55. package/src/components/fields/SelectField.tsx +19 -6
  56. package/src/components/fields/TabsLayout.tsx +19 -9
  57. package/src/components/fields/TextField.tsx +4 -1
  58. package/src/components/fields/UploadField.tsx +122 -56
  59. package/src/components/fields/extensions/blockComponents.tsx +103 -174
  60. package/src/components/fields/extensions/blocksStore.ts +8 -1
  61. package/src/components/fields/index.ts +4 -2
  62. package/src/components/fix_imports.cjs +23 -0
  63. package/src/components/fix_imports2.cjs +19 -0
  64. package/src/components/replace_svgs.cjs +63 -0
  65. package/src/components/ui/Dropdown.tsx +7 -2
  66. package/src/components/ui/Modal.tsx +24 -27
  67. package/src/components/ui/PageHeader.tsx +5 -5
  68. package/src/components/ui/PromptModal.tsx +2 -10
  69. package/src/components/ui/SlidePanel.tsx +10 -13
  70. package/src/components/ui/SplitButton.tsx +107 -0
  71. package/src/components/ui/Toaster.tsx +0 -1
  72. package/src/components/ui/icons.tsx +110 -109
  73. package/src/components/users/UserDetail.tsx +79 -16
  74. package/src/components/users/UsersList.tsx +8 -85
  75. package/src/hooks/useAutoFormState.ts +187 -196
  76. package/src/hooks/useQueue.ts +60 -0
  77. package/src/integration.ts +148 -46
  78. package/src/kyro-cms.d.ts +7 -2
  79. package/src/layouts/AdminLayout.astro +22 -2
  80. package/src/layouts/AuthLayout.astro +67 -7
  81. package/src/lib/autoform-store.ts +90 -53
  82. package/src/lib/change-source.ts +9 -0
  83. package/src/lib/config.ts +104 -8
  84. package/src/lib/globals.ts +48 -11
  85. package/src/lib/normalize-upload-fields.ts +41 -0
  86. package/src/lib/paths.ts +2 -2
  87. package/src/lib/resolve-field-value.ts +110 -0
  88. package/src/lib/shim/use-sync-external-store-with-selector.js +30 -0
  89. package/src/lib/shim/use-sync-external-store.js +1 -0
  90. package/src/lib/stores/index.ts +1 -0
  91. package/src/lib/useResourceManager.ts +4 -4
  92. package/src/lib/vite-shim-plugin.ts +100 -0
  93. package/src/pages/[collection]/[id].astro +1 -1
  94. package/src/pages/auth/register.astro +5 -2
  95. package/src/pages/preview/[collection]/[id].astro +4 -4
  96. package/src/pages/settings/[slug].astro +2 -2
  97. package/src/styles/main.css +60 -54
  98. package/README.md +0 -46
  99. package/dist/EditorClient-Q23UXR37.cjs +0 -468
  100. package/dist/EditorClient-Q23UXR37.cjs.map +0 -1
  101. package/dist/EditorClient-T5PASFNR.js +0 -466
  102. package/dist/EditorClient-T5PASFNR.js.map +0 -1
  103. package/dist/chunk-3BGDYKTD.cjs +0 -348
  104. package/dist/chunk-3BGDYKTD.cjs.map +0 -1
  105. package/dist/chunk-EEFXLQVT.js +0 -3
  106. package/dist/chunk-EEFXLQVT.js.map +0 -1
  107. package/src/components/blocks/ButtonBlock.tsx +0 -64
  108. package/src/components/blocks/ColumnsBlock.tsx +0 -55
  109. package/src/components/blocks/DividerBlock.tsx +0 -43
  110. package/src/components/blocks/LinkBlock.tsx +0 -65
  111. package/src/components/blocks/VStackBlock.tsx +0 -29
  112. package/src/components/fields/EditorClient.tsx +0 -535
  113. package/src/components/fields/PortableTextField.tsx +0 -155
  114. package/src/components/fields/PortableTextRenderer.tsx +0 -68
@@ -1,23 +1,18 @@
1
1
  import React, { useState, useEffect } from "react";
2
- import { apiGet, apiPatch, apiDelete } from "../lib/api";
2
+ import { apiGet, apiPost, apiPatch, apiDelete } from "../lib/api";
3
3
  import {
4
4
  Users,
5
5
  UserPlus,
6
6
  Shield,
7
7
  Lock,
8
8
  Unlock,
9
- MoreVertical,
10
- Mail,
11
9
  Clock,
12
10
  Search,
13
- Filter,
14
11
  Trash2,
15
- Edit2,
16
- ChevronRight,
17
- ShieldCheck,
18
- ShieldAlert
12
+ AlertTriangle,
19
13
  } from "./ui/icons";
20
14
  import { useUIStore, toast } from "../lib/stores";
15
+ import { Modal, ModalContent, ModalActions } from "./ui/Modal";
21
16
  import { Badge } from "./ui/Badge";
22
17
  import { PageHeader } from "./ui/PageHeader";
23
18
 
@@ -26,6 +21,7 @@ interface User {
26
21
  email: string;
27
22
  name?: string;
28
23
  role: string;
24
+ avatar?: string;
29
25
  locked?: boolean;
30
26
  lastLogin?: string;
31
27
  tenantId?: string;
@@ -36,6 +32,10 @@ export function UserManagement() {
36
32
  const [users, setUsers] = useState<User[]>([]);
37
33
  const [loading, setLoading] = useState(true);
38
34
  const [searchQuery, setSearchQuery] = useState("");
35
+ const [showCreateModal, setShowCreateModal] = useState(false);
36
+ const [createForm, setCreateForm] = useState({ name: "", email: "", password: "", role: "customer" });
37
+ const [createError, setCreateError] = useState("");
38
+ const [creating, setCreating] = useState(false);
39
39
  const { confirm, alert } = useUIStore();
40
40
 
41
41
  useEffect(() => {
@@ -71,6 +71,7 @@ export function UserManagement() {
71
71
  toast.success(isLocking ? `Account locked: ${user.email}` : `Account restored: ${user.email}`);
72
72
  } catch (error) {
73
73
  console.error("Failed to toggle user lock:", error);
74
+ toast.error("Failed to update account status");
74
75
  }
75
76
  },
76
77
  });
@@ -89,11 +90,41 @@ export function UserManagement() {
89
90
  toast.success(`Identity purged: ${user.email}`);
90
91
  } catch (error) {
91
92
  console.error("Failed to delete user:", error);
93
+ toast.error("Failed to delete user");
92
94
  }
93
95
  },
94
96
  });
95
97
  };
96
98
 
99
+ const handleCreateUser = async () => {
100
+ if (!createForm.email.trim() || !createForm.password.trim()) {
101
+ setCreateError("Email and password are required");
102
+ return;
103
+ }
104
+ setCreating(true);
105
+ setCreateError("");
106
+ try {
107
+ await apiPost("/api/users", {
108
+ name: createForm.name.trim() || undefined,
109
+ email: createForm.email.trim(),
110
+ password: createForm.password,
111
+ role: createForm.role,
112
+ });
113
+ setShowCreateModal(false);
114
+ setCreateForm({ name: "", email: "", password: "", role: "customer" });
115
+ toast.success("User created successfully");
116
+ loadUsers();
117
+ } catch (err) {
118
+ const message = err instanceof Error ? err.message : "Failed to create user";
119
+ setCreateError(message);
120
+ toast.error(message);
121
+ } finally {
122
+ setCreating(false);
123
+ }
124
+ };
125
+
126
+ const roleOptions = ["super_admin", "admin", "editor", "author", "customer", "guest"];
127
+
97
128
  const filteredUsers = users.filter(
98
129
  (u) =>
99
130
  u.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -110,7 +141,9 @@ export function UserManagement() {
110
141
  action={{
111
142
  label: "New User",
112
143
  onClick: () => {
113
- // New user logic
144
+ setCreateForm({ name: "", email: "", password: "", role: "customer" });
145
+ setCreateError("");
146
+ setShowCreateModal(true);
114
147
  },
115
148
  icon: UserPlus,
116
149
  }}
@@ -165,9 +198,7 @@ export function UserManagement() {
165
198
  <tr key={user.id} className={`hover:bg-[var(--kyro-surface-accent)]/50 transition-colors group ${user.locked ? "opacity-50 grayscale" : ""}`}>
166
199
  <td className="px-6 py-3.5">
167
200
  <div className="flex items-center gap-3">
168
- <div className="w-8 h-8 rounded-lg bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] flex items-center justify-center text-xs font-bold text-[var(--kyro-primary)] group-hover:scale-105 transition-transform">
169
- {user.name ? user.name[0] : user.email[0].toUpperCase()}
170
- </div>
201
+ <AvatarCell user={user} />
171
202
  <div className="min-w-0">
172
203
  <div className="flex items-center gap-2">
173
204
  <div className="text-xs font-bold text-[var(--kyro-text-primary)] truncate">{user.name || user.email.split("@")[0]}</div>
@@ -214,9 +245,6 @@ export function UserManagement() {
214
245
  >
215
246
  <Trash2 className="w-3.5 h-3.5" />
216
247
  </button>
217
- <button className="p-1.5 rounded-lg border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface)] transition-all">
218
- <MoreVertical className="w-3.5 h-3.5" />
219
- </button>
220
248
  </div>
221
249
  </td>
222
250
  </tr>
@@ -225,6 +253,116 @@ export function UserManagement() {
225
253
  </tbody>
226
254
  </table>
227
255
  </div>
256
+
257
+ {/* Create User Modal */}
258
+ <Modal
259
+ open={showCreateModal}
260
+ onClose={() => setShowCreateModal(false)}
261
+ title="Create User"
262
+ size="lg"
263
+ >
264
+ <ModalContent>
265
+ <div className="space-y-6">
266
+ <div>
267
+ <label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)] uppercase tracking-wider">Name (optional)</label>
268
+ <input
269
+ type="text"
270
+ value={createForm.name}
271
+ onChange={(e) => setCreateForm({ ...createForm, name: e.target.value })}
272
+ placeholder="John Doe"
273
+ 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)]"
274
+ />
275
+ </div>
276
+ <div>
277
+ <label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)] uppercase tracking-wider">Email Address</label>
278
+ <input
279
+ type="email"
280
+ value={createForm.email}
281
+ onChange={(e) => setCreateForm({ ...createForm, email: e.target.value })}
282
+ placeholder="user@example.com"
283
+ required
284
+ 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)]"
285
+ />
286
+ </div>
287
+ <div>
288
+ <label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)] uppercase tracking-wider">Password</label>
289
+ <input
290
+ type="password"
291
+ value={createForm.password}
292
+ onChange={(e) => setCreateForm({ ...createForm, password: e.target.value })}
293
+ placeholder="Minimum 12 characters"
294
+ required
295
+ minLength={12}
296
+ 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)]"
297
+ />
298
+ </div>
299
+ <div>
300
+ <label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)] uppercase tracking-wider">Role</label>
301
+ <select
302
+ value={createForm.role}
303
+ onChange={(e) => setCreateForm({ ...createForm, role: e.target.value })}
304
+ 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)]"
305
+ >
306
+ {roleOptions.map((r) => (
307
+ <option key={r} value={r}>{r}</option>
308
+ ))}
309
+ </select>
310
+ </div>
311
+ {createError && (
312
+ <div className="p-3 bg-red-500/10 border border-red-500/20 rounded-xl flex items-center gap-2 text-red-500 text-xs font-bold">
313
+ <AlertTriangle className="w-4 h-4" />
314
+ {createError}
315
+ </div>
316
+ )}
317
+ </div>
318
+ </ModalContent>
319
+ <ModalActions>
320
+ <button
321
+ type="button"
322
+ onClick={() => setShowCreateModal(false)}
323
+ className="px-6 py-2.5 rounded-xl font-bold text-sm border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] transition-colors"
324
+ >
325
+ Cancel
326
+ </button>
327
+ <button
328
+ type="button"
329
+ onClick={handleCreateUser}
330
+ disabled={creating}
331
+ className="kyro-btn kyro-btn-primary px-6 py-2.5 rounded-xl font-bold text-sm hover:opacity-90 transition-all shadow-lg shadow-[var(--kyro-primary)]/10 disabled:opacity-50"
332
+ >
333
+ {creating ? "Creating..." : "Create User"}
334
+ </button>
335
+ </ModalActions>
336
+ </Modal>
337
+
338
+
339
+ </div>
340
+ );
341
+ }
342
+
343
+ function AvatarCell({ user }: { user: User }) {
344
+ const [url, setUrl] = useState<string | null>(null);
345
+
346
+ useEffect(() => {
347
+ const avatar = user.avatar;
348
+ if (typeof avatar === "string" && /^[0-9a-f-]+$/i.test(avatar)) {
349
+ apiGet<any>(`/api/media/${avatar}`)
350
+ .then((media) => setUrl(media?.thumbnailUrl || media?.url || null))
351
+ .catch(() => setUrl(null));
352
+ }
353
+ }, [user.avatar]);
354
+
355
+ if (url) {
356
+ return (
357
+ <div className="w-8 h-8 rounded-lg overflow-hidden border border-[var(--kyro-border)] flex-shrink-0">
358
+ <img src={url} alt="" className="w-full h-full object-cover" />
359
+ </div>
360
+ );
361
+ }
362
+
363
+ return (
364
+ <div className="w-8 h-8 rounded-lg bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] flex items-center justify-center text-xs font-bold text-[var(--kyro-primary)] flex-shrink-0">
365
+ {user.name ? user.name[0] : user.email[0].toUpperCase()}
228
366
  </div>
229
367
  );
230
368
  }
@@ -1,22 +1,41 @@
1
- import React from "react";
1
+ import React, { useState, useEffect } from "react";
2
2
  import { Dropdown, DropdownItem, DropdownSeparator } from "./ui/Dropdown";
3
3
  import { User, Shield, Key, Webhook, Clock, FileText, ExternalLink, HelpCircle, LogOut } from "./ui/icons";
4
+ import { useAuthStore } from "../lib/stores";
5
+ import { apiGet } from "../lib/api";
4
6
 
5
7
  interface UserMenuProps {
6
8
  adminPath: string;
7
9
  }
8
10
 
9
11
  export function UserMenu({ adminPath }: UserMenuProps) {
12
+ const currentUser = useAuthStore((s) => s.user);
13
+ const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
14
+
15
+ useEffect(() => {
16
+ const avatar = currentUser?.avatar;
17
+ if (typeof avatar === "string" && /^[0-9a-f-]+$/i.test(avatar)) {
18
+ apiGet<any>(`/api/media/${avatar}`)
19
+ .then((media) => setAvatarUrl(media?.thumbnailUrl || media?.url || null))
20
+ .catch(() => setAvatarUrl(null));
21
+ } else {
22
+ setAvatarUrl(null);
23
+ }
24
+ }, [currentUser?.avatar]);
10
25
 
11
26
  return (
12
27
  <Dropdown
13
28
  align="right"
14
29
  trigger={
15
30
  <div
16
- className="flex justify-center p-2.5 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface)] rounded-xl transition-all shadow-sm active:scale-95"
31
+ className="flex justify-center p-.5 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface)] rounded-xl transition-all shadow-sm active:scale-95"
17
32
  title="Account"
18
33
  >
19
- <User className="w-4 h-4" strokeWidth={2.5} />
34
+ {avatarUrl ? (
35
+ <img src={avatarUrl} alt="" className="w-8 h-8 rounded-full object-cover" />
36
+ ) : (
37
+ <User className="w-4 h-4" strokeWidth={2.5} />
38
+ )}
20
39
  </div>
21
40
  }
22
41
  >
@@ -28,7 +47,14 @@ export function UserMenu({ adminPath }: UserMenuProps) {
28
47
 
29
48
  <DropdownItem
30
49
  icon={<User className="w-4 h-4" />}
31
- onClick={() => window.location.href = `${adminPath}/settings/account`}
50
+ onClick={() => {
51
+ const id = currentUser?.id;
52
+ if (id) {
53
+ window.location.href = `${adminPath}/users/${id}`;
54
+ } else {
55
+ window.location.href = `${adminPath}/users`;
56
+ }
57
+ }}
32
58
  >
33
59
  Profile Settings
34
60
  </DropdownItem>
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import { SlidePanel } from "./ui/SlidePanel";
3
- import { Button } from "./ui/Button";
4
3
  import { Spinner } from "./ui/Spinner";
4
+ import { History, Eye, GitCompare, Undo2, CheckCircle2, Clock, User } from "lucide-react";
5
5
 
6
6
  interface Version {
7
7
  id: string;
@@ -38,8 +38,15 @@ export function VersionHistoryPanel({
38
38
  onCompare,
39
39
  loading = false,
40
40
  }: VersionHistoryPanelProps) {
41
- const formatDate = (dateStr: string) => {
42
- const date = new Date(dateStr);
41
+ const formatDate = (dateValue: any) => {
42
+ if (!dateValue) return "Unknown date";
43
+ const date = new Date(dateValue);
44
+ if (isNaN(date.getTime())) {
45
+ // Sometimes strings from DB like SQLite might need parsing
46
+ const parsed = Date.parse(dateValue);
47
+ if (!isNaN(parsed)) return new Date(parsed).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", hour: "2-digit", minute: "2-digit" });
48
+ return "Invalid date";
49
+ }
43
50
  return date.toLocaleDateString("en-US", {
44
51
  month: "short",
45
52
  day: "numeric",
@@ -49,10 +56,17 @@ export function VersionHistoryPanel({
49
56
  });
50
57
  };
51
58
 
52
- const formatTimeAgo = (dateStr: string) => {
53
- const date = new Date(dateStr);
59
+ const formatTimeAgo = (dateValue: any) => {
60
+ if (!dateValue) return "Unknown date";
61
+ const date = new Date(dateValue);
62
+ if (isNaN(date.getTime())) return formatDate(dateValue);
63
+
54
64
  const now = new Date();
55
65
  const diffMs = now.getTime() - date.getTime();
66
+
67
+ // If it's in the future (e.g. server clock skew), just say "Just now"
68
+ if (diffMs < 0) return "Just now";
69
+
56
70
  const diffMins = Math.floor(diffMs / 60000);
57
71
  const diffHours = Math.floor(diffMs / 3600000);
58
72
  const diffDays = Math.floor(diffMs / 86400000);
@@ -61,7 +75,7 @@ export function VersionHistoryPanel({
61
75
  if (diffMins < 60) return `${diffMins}m ago`;
62
76
  if (diffHours < 24) return `${diffHours}h ago`;
63
77
  if (diffDays < 7) return `${diffDays}d ago`;
64
- return formatDate(dateStr);
78
+ return formatDate(dateValue);
65
79
  };
66
80
 
67
81
  return (
@@ -76,128 +90,107 @@ export function VersionHistoryPanel({
76
90
  <Spinner />
77
91
  </div>
78
92
  ) : versions.length === 0 ? (
79
- <div className="text-center py-12 text-gray-500">
80
- <svg
81
- className="w-12 h-12 mx-auto mb-4 text-gray-300"
82
- viewBox="0 0 24 24"
83
- fill="none"
84
- stroke="currentColor"
85
- strokeWidth="1.5"
86
- >
87
- <circle cx="12" cy="12" r="10" />
88
- <polyline points="12,6 12,12 16,14" />
89
- </svg>
90
- <p>No version history yet</p>
91
- <p className="text-sm text-gray-400 mt-1">
92
- Versions are created when you save changes
93
- </p>
93
+ <div className="text-center flex flex-col items-center justify-center py-16 text-[var(--kyro-text-muted)]">
94
+ <History className="w-12 h-12 mb-4 opacity-20" />
95
+ <p className="font-medium text-[var(--kyro-text)]">No version history yet</p>
96
+ <p className="text-sm mt-1">Versions are automatically saved as you work.</p>
94
97
  </div>
95
98
  ) : (
96
- <div className="space-y-1">
97
- {versions.map((version) => (
98
- <div
99
- key={version.id}
100
- className={`p-3 rounded-lg border transition-colors ${
101
- version.id === currentVersionId
102
- ? "border-primary bg-primary-light/30"
103
- : "border-gray-100 hover:border-gray-200 hover:bg-gray-50"
104
- }`}
105
- >
106
- <div className="flex items-start justify-between">
107
- <div className="flex-1 min-w-0">
108
- <div className="flex items-center gap-2 mb-1">
109
- <span
110
- className={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium ${
111
- version.status === "published"
112
- ? "bg-green-100 text-green-700"
113
- : "bg-gray-100 text-gray-600"
114
- }`}
115
- >
116
- {version.status === "published" ? "Published" : "Draft"}
117
- </span>
118
- <span className="text-xs text-gray-400">
119
- v{version.version}
120
- </span>
99
+ <div className="space-y-3 px-1 pb-4 pt-1">
100
+ {versions.map((version) => {
101
+ const isCurrent = version.id === currentVersionId;
102
+ return (
103
+ <div
104
+ key={version.id}
105
+ className={`p-4 rounded-xl border transition-all duration-200 group relative overflow-hidden ${
106
+ isCurrent
107
+ ? "border-[var(--kyro-primary)] bg-[var(--kyro-primary)]/5 shadow-sm"
108
+ : "border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/30 hover:bg-[var(--kyro-surface-accent)] hover:shadow-sm bg-[var(--kyro-surface)]"
109
+ }`}
110
+ >
111
+ {isCurrent && (
112
+ <div className="absolute top-0 left-0 w-1 h-full bg-[var(--kyro-primary)] shadow-[0_0_8px_var(--kyro-primary)]" />
113
+ )}
114
+
115
+ <div className="flex items-start justify-between gap-4">
116
+ <div className="flex-1 min-w-0">
117
+ <div className="flex items-center gap-2 mb-2">
118
+ <span
119
+ className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-bold tracking-wide uppercase ${
120
+ version.status === "published"
121
+ ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
122
+ : "bg-zinc-500/10 text-zinc-600 dark:text-zinc-400"
123
+ }`}
124
+ >
125
+ {version.status === "published" && <CheckCircle2 className="w-3 h-3" />}
126
+ {version.status === "published" ? "Published" : "Draft"}
127
+ </span>
128
+ <span className="text-xs font-semibold text-[var(--kyro-text)] px-2 py-0.5 rounded-md bg-[var(--kyro-surface-accent)]">
129
+ v{version.version}
130
+ </span>
131
+ {isCurrent && (
132
+ <span className="text-[10px] font-medium text-[var(--kyro-primary)] flex items-center gap-1">
133
+ <span className="w-1.5 h-1.5 rounded-full bg-[var(--kyro-primary)] animate-pulse" />
134
+ Current
135
+ </span>
136
+ )}
137
+ </div>
138
+
139
+ <div className="flex items-center gap-1.5 text-sm font-medium text-[var(--kyro-text)] truncate mb-1">
140
+ <Clock className="w-3.5 h-3.5 text-[var(--kyro-text-muted)]" />
141
+ {formatTimeAgo(version.createdAt)}
142
+ </div>
143
+
144
+ {version.createdBy && (
145
+ <div className="flex items-center gap-1.5 text-xs text-[var(--kyro-text-muted)] mt-1.5">
146
+ <User className="w-3.5 h-3.5" />
147
+ <span>{version.createdBy.name || version.createdBy.email}</span>
148
+ </div>
149
+ )}
150
+
151
+ {version.changelog && (
152
+ <p className="text-xs text-[var(--kyro-text-secondary)] mt-2 italic border-l-2 border-[var(--kyro-border)] pl-2">
153
+ "{version.changelog}"
154
+ </p>
155
+ )}
121
156
  </div>
122
- <p className="text-sm text-gray-600 truncate">
123
- {formatTimeAgo(version.createdAt)}
124
- </p>
125
- {version.createdBy && (
126
- <p className="text-xs text-gray-400 mt-0.5">
127
- by {version.createdBy.name || version.createdBy.email}
128
- </p>
129
- )}
130
- {version.changelog && (
131
- <p className="text-xs text-gray-500 mt-1 truncate">
132
- {version.changelog}
133
- </p>
134
- )}
135
- </div>
136
- <div className="flex items-center gap-1 ml-2">
137
- <button type="button"
138
- onClick={() => onPreview(version)}
139
- className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded transition-colors"
140
- title="Preview this version"
141
- >
142
- <svg
143
- width="14"
144
- height="14"
145
- viewBox="0 0 24 24"
146
- fill="none"
147
- stroke="currentColor"
148
- strokeWidth="2"
149
- >
150
- <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
151
- <circle cx="12" cy="12" r="3" />
152
- </svg>
153
- </button>
154
- {onCompare && (
157
+
158
+ <div className="flex flex-col sm:flex-row items-center gap-1.5 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity">
155
159
  <button type="button"
156
- onClick={() =>
157
- onCompare(
158
- version,
159
- versions.find((v) => v.id === currentVersionId) ||
160
- version,
161
- )
162
- }
163
- className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded transition-colors"
164
- title="Compare with current"
160
+ onClick={() => onPreview(version)}
161
+ className="p-2 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-primary)]/10 rounded-md transition-colors"
162
+ title="Preview this version"
165
163
  >
166
- <svg
167
- width="14"
168
- height="14"
169
- viewBox="0 0 24 24"
170
- fill="none"
171
- stroke="currentColor"
172
- strokeWidth="2"
173
- >
174
- <path d="M16 3h5v5M8 3H3v5M3 16v5h5M21 16v5h-5M3 12h18" />
175
- </svg>
164
+ <Eye className="w-4 h-4" />
176
165
  </button>
177
- )}
178
- {version.id !== currentVersionId && (
179
- <button type="button"
180
- onClick={() => onRestore(version)}
181
- className="p-1.5 text-gray-400 hover:text-primary hover:bg-primary-light rounded transition-colors"
182
- title="Restore this version"
183
- >
184
- <svg
185
- width="14"
186
- height="14"
187
- viewBox="0 0 24 24"
188
- fill="none"
189
- stroke="currentColor"
190
- strokeWidth="2"
166
+ {onCompare && (
167
+ <button type="button"
168
+ onClick={() =>
169
+ onCompare(
170
+ version,
171
+ versions.find((v) => v.id === currentVersionId) || version,
172
+ )
173
+ }
174
+ className="p-2 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-primary)]/10 rounded-md transition-colors"
175
+ title="Compare with current"
191
176
  >
192
- <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
193
- <path d="M3 3v5h5" />
194
- </svg>
195
- </button>
196
- )}
177
+ <GitCompare className="w-4 h-4" />
178
+ </button>
179
+ )}
180
+ {!isCurrent && (
181
+ <button type="button"
182
+ onClick={() => onRestore(version)}
183
+ className="p-2 text-amber-600 hover:bg-amber-500/10 rounded-md transition-colors"
184
+ title="Restore this version"
185
+ >
186
+ <Undo2 className="w-4 h-4" />
187
+ </button>
188
+ )}
189
+ </div>
197
190
  </div>
198
191
  </div>
199
- </div>
200
- ))}
192
+ );
193
+ })}
201
194
  </div>
202
195
  )}
203
196
  </SlidePanel>
@@ -48,7 +48,7 @@ export function WebhookManager() {
48
48
  endpoint: "/api/webhooks",
49
49
  });
50
50
 
51
- const { alert } = useUIStore();
51
+ const { confirm } = useUIStore();
52
52
  const [showTestModal, setShowTestModal] = useState(false);
53
53
  const [showHelpModal, setShowHelpModal] = useState(false);
54
54
  const [testResult, setTestResult] = useState<{
@@ -76,6 +76,7 @@ export function WebhookManager() {
76
76
  toast.success(`Webhook established: ${formData.name}`);
77
77
  } catch (e) {
78
78
  setCreateError("Failed to create webhook");
79
+ toast.error("Failed to create webhook");
79
80
  }
80
81
  };
81
82
 
@@ -105,6 +106,7 @@ export function WebhookManager() {
105
106
  toast.success(newStatus === "active" ? "Signals resumed" : "Dispatcher paused");
106
107
  } catch (e) {
107
108
  console.error(e);
109
+ toast.error("Failed to toggle webhook status");
108
110
  }
109
111
  };
110
112
 
@@ -214,7 +216,7 @@ export function WebhookManager() {
214
216
  <button
215
217
  type="button"
216
218
  onClick={() => setShowCreateModal(true)}
217
- 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"
219
+ className="kyro-btn kyro-btn-primary inline-flex items-center gap-3 px-8 py-4 rounded-2xl font-bold hover:scale-[1.05] transition-all shadow-xl shadow-[var(--kyro-primary)]/10"
218
220
  >
219
221
  <Plus className="w-5 h-5" />
220
222
  Configure Webhook
@@ -413,7 +415,7 @@ export function WebhookManager() {
413
415
  <button
414
416
  type="button"
415
417
  onClick={handleCreate}
416
- className="px-6 py-2.5 rounded-xl font-bold text-sm bg-[var(--kyro-primary)] text-white hover:opacity-90 transition-all shadow-lg shadow-[var(--kyro-primary)]/10"
418
+ className="kyro-btn kyro-btn-primary px-6 py-2.5 rounded-xl font-bold text-sm hover:opacity-90 transition-all shadow-lg shadow-[var(--kyro-primary)]/10"
417
419
  >
418
420
  Create Webhook
419
421
  </button>
@@ -522,7 +524,7 @@ export function WebhookManager() {
522
524
  <button
523
525
  type="button"
524
526
  onClick={() => setShowHelpModal(false)}
525
- className="w-full py-3 rounded-xl font-bold text-sm bg-[var(--kyro-primary)] text-white hover:opacity-90 transition-all shadow-lg shadow-[var(--kyro-primary)]/20"
527
+ className="kyro-btn kyro-btn-primary w-full py-3 rounded-xl font-bold text-sm hover:opacity-90 transition-all shadow-lg shadow-[var(--kyro-primary)]/20"
526
528
  >
527
529
  I Understand
528
530
  </button>