@kyro-cms/admin 0.1.6 → 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 (163) hide show
  1. package/README.md +149 -51
  2. package/package.json +53 -6
  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 +23 -6
  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 +70 -11
  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 +200 -139
  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 +42 -24
  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 +11 -11
  153. package/src/pages/media.astro +10 -0
  154. package/src/pages/preview/[collection]/[id].astro +178 -0
  155. package/src/pages/register.astro +13 -13
  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
@@ -0,0 +1,237 @@
1
+ ---
2
+ import "../styles/main.css";
3
+ import { nonAuthCollections } from "@/lib/config";
4
+
5
+ interface User {
6
+ id: string;
7
+ email: string;
8
+ role: string;
9
+ tenantId?: string;
10
+ }
11
+
12
+ interface NavItem {
13
+ href: string;
14
+ label: string;
15
+ icon: string;
16
+ }
17
+
18
+ interface Props {
19
+ title: string;
20
+ user?: User;
21
+ }
22
+
23
+ const { title, user } = Astro.props;
24
+ const currentPath = Astro.url.pathname;
25
+
26
+ const collectionItems: NavItem[] = nonAuthCollections.map((col) => ({
27
+ href: `/${col.slug}`,
28
+ label: col.label || col.slug,
29
+ icon: "collection",
30
+ }));
31
+
32
+ const navSections: { label: string; items: NavItem[] }[] = [
33
+ {
34
+ label: "Home",
35
+ items: [{ href: "/", label: "Dashboard", icon: "home" }],
36
+ },
37
+ {
38
+ label: "Collections",
39
+ items: collectionItems,
40
+ },
41
+ {
42
+ label: "Settings",
43
+ items: [
44
+ { href: "/settings", label: "General Settings", icon: "settings" },
45
+ { href: "/users", label: "Users", icon: "users" },
46
+ { href: "/roles", label: "Roles", icon: "roles" },
47
+ { href: "/audit", label: "Audit Logs", icon: "audit" },
48
+ ],
49
+ },
50
+ {
51
+ label: "Developer",
52
+ items: [
53
+ { href: "/admin/keys", label: "API Keys", icon: "keys" },
54
+ { href: "/admin/webhooks", label: "Webhooks", icon: "webhooks" },
55
+ ],
56
+ },
57
+ ];
58
+
59
+ const icons: Record<string, string> = {
60
+ home: "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6",
61
+ collection: "M12 12m-5 0a5 5 0 1 1 10 0 5 5 0 1 1-10 0",
62
+ settings:
63
+ "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37M15 12a3 3 0 11-6 0 3 3 0 016 0z",
64
+ users:
65
+ "M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z",
66
+ roles:
67
+ "M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z",
68
+ audit:
69
+ "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z",
70
+ keys: "M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z",
71
+ webhooks:
72
+ "M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1",
73
+ user: "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z",
74
+ logout:
75
+ "M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1",
76
+ sun: "M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.364 17.636l-.707.707M17.636 17.636l-.707-.707M6.364 6.364l-.707-.707M15 12a3 3 0 11-6 0 3 3 0 016 0z",
77
+ moon: "M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z",
78
+ };
79
+
80
+ function isActive(item: NavItem): boolean {
81
+ if (item.href === "/") return title === "Dashboard";
82
+ if (item.href.startsWith("/settings"))
83
+ return currentPath.startsWith("/settings");
84
+ if (item.href === "/users") return title === "Users";
85
+ if (item.href === "/roles") return title === "Roles";
86
+ if (item.href === "/audit") return title === "Audit Logs";
87
+ if (item.href === "/admin/keys") return title === "API Keys";
88
+ if (item.href === "/admin/webhooks") return title === "Webhooks";
89
+ // Collections: match /collection-name and /collection-name/* paths
90
+ if (currentPath === item.href || currentPath.startsWith(item.href + "/"))
91
+ return true;
92
+ return false;
93
+ }
94
+ ---
95
+
96
+ <aside
97
+ class="surface-tile w-[320px] flex flex-col flex-shrink-0 overflow-hidden"
98
+ >
99
+ <div class="px-4 py-8">
100
+ <span
101
+ class="text-3xl font-black tracking-tighter text-[var(--kyro-text-primary)]"
102
+ >KYRO.</span
103
+ >
104
+ </div>
105
+
106
+ <nav class="flex-1 px-4 overflow-y-auto">
107
+ <div class="space-y-4">
108
+ {
109
+ navSections.map((section) => (
110
+ <div class="space-y-1">
111
+ <div class="pt-4 pb-2">
112
+ <p class="px-6 text-[9px] font-black text-[var(--kyro-text-secondary)] uppercase tracking-[0.2em] opacity-40">
113
+ {section.label}
114
+ </p>
115
+ </div>
116
+ {section.items.map((item) => (
117
+ <a
118
+ href={item.href}
119
+ class={`flex items-center gap-4 px-6 py-2 rounded-2xl transition-all font-bold ${
120
+ item.icon === "collection"
121
+ ? currentPath === item.href ||
122
+ currentPath.startsWith(item.href + "/")
123
+ ? "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-primary)]"
124
+ : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"
125
+ : isActive(item)
126
+ ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-lg"
127
+ : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"
128
+ }`}
129
+ >
130
+ <svg
131
+ class="w-5 h-5"
132
+ fill="none"
133
+ stroke="currentColor"
134
+ viewBox="0 0 24 24"
135
+ >
136
+ <path
137
+ stroke-linecap="round"
138
+ stroke-linejoin="round"
139
+ stroke-width="2.5"
140
+ d={icons[item.icon]}
141
+ />
142
+ </svg>
143
+ <span>{item.label}</span>
144
+ </a>
145
+ ))}
146
+ </div>
147
+ ))
148
+ }
149
+ </div>
150
+ </nav>
151
+
152
+ <div class="px-6 py-4 mt-auto">
153
+ <div
154
+ class="flex items-center justify-between p-2 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-2xl"
155
+ >
156
+ <div
157
+ class="flex p-1 bg-[var(--kyro-bg)] rounded-xl border border-[var(--kyro-border)] shadow-inner"
158
+ >
159
+ <button
160
+ id="theme-light-btn"
161
+ class="p-2 rounded-lg transition-all active:scale-95"
162
+ title="Light Mode"
163
+ >
164
+ <svg
165
+ class="w-4 h-4"
166
+ fill="none"
167
+ stroke="currentColor"
168
+ viewBox="0 0 24 24"
169
+ >
170
+ <path
171
+ stroke-linecap="round"
172
+ stroke-linejoin="round"
173
+ stroke-width="2.5"
174
+ d={icons.sun}></path>
175
+ </svg>
176
+ </button>
177
+ <button
178
+ id="theme-dark-btn"
179
+ class="p-2 rounded-lg transition-all active:scale-95"
180
+ title="Dark Mode"
181
+ >
182
+ <svg
183
+ class="w-4 h-4"
184
+ fill="none"
185
+ stroke="currentColor"
186
+ viewBox="0 0 24 24"
187
+ >
188
+ <path
189
+ stroke-linecap="round"
190
+ stroke-linejoin="round"
191
+ stroke-width="2.5"
192
+ d={icons.moon}></path>
193
+ </svg>
194
+ </button>
195
+ </div>
196
+
197
+ <div class="flex items-center gap-2">
198
+ <a
199
+ href={user ? `/users/${user.id}` : "#"}
200
+ class="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"
201
+ title="Account"
202
+ >
203
+ <svg
204
+ class="w-4 h-4"
205
+ fill="none"
206
+ stroke="currentColor"
207
+ viewBox="0 0 24 24"
208
+ >
209
+ <path
210
+ stroke-linecap="round"
211
+ stroke-linejoin="round"
212
+ stroke-width="2.5"
213
+ d={icons.user}></path>
214
+ </svg>
215
+ </a>
216
+ <button
217
+ id="logout-btn"
218
+ class="flex justify-center p-2.5 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-xl transition-all shadow-sm active:scale-95 font-bold"
219
+ title="Logout"
220
+ >
221
+ <svg
222
+ class="w-4 h-4"
223
+ fill="none"
224
+ stroke="currentColor"
225
+ viewBox="0 0 24 24"
226
+ >
227
+ <path
228
+ stroke-linecap="round"
229
+ stroke-linejoin="round"
230
+ stroke-width="2.5"
231
+ d={icons.logout}></path>
232
+ </svg>
233
+ </button>
234
+ </div>
235
+ </div>
236
+ </div>
237
+ </aside>
@@ -0,0 +1,204 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import {
3
+ Users,
4
+ UserPlus,
5
+ Shield,
6
+ Lock,
7
+ Unlock,
8
+ MoreVertical,
9
+ Mail,
10
+ Clock,
11
+ Search,
12
+ Filter,
13
+ } from "lucide-react";
14
+
15
+ interface User {
16
+ id: string;
17
+ email: string;
18
+ name?: string;
19
+ role: string;
20
+ locked?: boolean;
21
+ lastLogin?: string;
22
+ createdAt: string;
23
+ }
24
+
25
+ export function UserManagement() {
26
+ const [users, setUsers] = useState<User[]>([]);
27
+ const [loading, setLoading] = useState(true);
28
+ const [searchQuery, setSearchQuery] = useState("");
29
+
30
+ useEffect(() => {
31
+ loadUsers();
32
+ }, []);
33
+
34
+ const loadUsers = async () => {
35
+ try {
36
+ setLoading(true);
37
+ const response = await fetch("/api/auth/users");
38
+ const result = await response.json();
39
+ setUsers(result.docs || []);
40
+ } catch (error) {
41
+ console.error("Failed to load users:", error);
42
+ } finally {
43
+ setLoading(false);
44
+ }
45
+ };
46
+
47
+ const handleToggleLock = async (user: User) => {
48
+ try {
49
+ const response = await fetch(`/api/auth/${user.id}`, {
50
+ method: "PATCH",
51
+ headers: { "Content-Type": "application/json" },
52
+ body: JSON.stringify({ locked: !user.locked }),
53
+ });
54
+ if (response.ok) {
55
+ setUsers((prev) =>
56
+ prev.map((u) => (u.id === user.id ? { ...u, locked: !u.locked } : u)),
57
+ );
58
+ }
59
+ } catch (error) {
60
+ console.error("Failed to toggle user lock:", error);
61
+ }
62
+ };
63
+
64
+ const filteredUsers = users.filter(
65
+ (u) =>
66
+ u.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
67
+ u.name?.toLowerCase().includes(searchQuery.toLowerCase()),
68
+ );
69
+
70
+ return (
71
+ <div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 px-8 pb-12">
72
+ {/* Header */}
73
+ <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 pt-4">
74
+ <div>
75
+ <h1 className="text-4xl font-black tracking-tighter text-[var(--kyro-text-primary)]">
76
+ Team <span className="text-[var(--kyro-primary)]">Management</span>
77
+ </h1>
78
+ <p className="text-[var(--kyro-text-secondary)] mt-1 font-medium opacity-60">
79
+ Control access and oversee administrative governance.
80
+ </p>
81
+ </div>
82
+ <div className="flex items-center gap-3">
83
+ <button type="button" className="flex items-center gap-2 px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-2xl font-black text-sm shadow-xl active:scale-95 transition-all">
84
+ <UserPlus className="w-4 h-4" />
85
+ Invite Member
86
+ </button>
87
+ </div>
88
+ </div>
89
+
90
+ {/* Tools bar */}
91
+ <div className="flex flex-col md:flex-row gap-4">
92
+ <div className="relative flex-1 group">
93
+ <Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-secondary)] opacity-40 group-focus-within:opacity-100 transition-opacity" />
94
+ <input
95
+ type="text"
96
+ placeholder="Search by name or email..."
97
+ value={searchQuery}
98
+ onChange={(e) => setSearchQuery(e.target.value)}
99
+ className="w-full pl-11 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)] focus:border-[var(--kyro-primary)] transition-all text-sm font-medium"
100
+ />
101
+ </div>
102
+ <div className="flex items-center gap-3 bg-[var(--kyro-bg-secondary)] p-1 rounded-2xl border border-[var(--kyro-border)]">
103
+ <button type="button" className="px-4 py-2 text-[10px] font-black uppercase tracking-widest bg-[var(--kyro-surface)] shadow-sm rounded-xl border border-[var(--kyro-border)]">
104
+ All Users
105
+ </button>
106
+ <button type="button" className="px-4 py-2 text-[10px] font-black uppercase tracking-widest opacity-40 hover:opacity-100 transition-all">
107
+ Admins
108
+ </button>
109
+ <button type="button" className="px-4 py-2 text-[10px] font-black uppercase tracking-widest opacity-40 hover:opacity-100 transition-all">
110
+ Restricted
111
+ </button>
112
+ </div>
113
+ </div>
114
+
115
+ {/* User Grid */}
116
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
117
+ {loading ? (
118
+ Array(6)
119
+ .fill(0)
120
+ .map((_, i) => (
121
+ <div
122
+ key={i}
123
+ className="surface-tile h-48 animate-pulse p-8 bg-[var(--kyro-bg-secondary)]"
124
+ />
125
+ ))
126
+ ) : filteredUsers.length === 0 ? (
127
+ <div className="col-span-full py-20 text-center surface-tile">
128
+ <Users className="w-12 h-12 mx-auto opacity-10 mb-4" />
129
+ <p className="opacity-40 italic">
130
+ No team members found matching your search.
131
+ </p>
132
+ </div>
133
+ ) : (
134
+ filteredUsers.map((user) => (
135
+ <div
136
+ key={user.id}
137
+ className={`surface-tile p-8 group transition-all duration-500 hover:shadow-2xl relative overflow-hidden ${user.locked ? "grayscale opacity-60" : ""}`}
138
+ >
139
+ {/* Status Badge */}
140
+ <div className="absolute top-0 right-0 p-6">
141
+ <span
142
+ className={`inline-flex items-center px-2 py-0.5 rounded-full text-[8px] font-black uppercase tracking-widest ${user.locked ? "bg-red-500/10 text-red-500" : "bg-green-500/10 text-green-500"}`}
143
+ >
144
+ {user.locked ? "Locked" : "Active"}
145
+ </span>
146
+ </div>
147
+
148
+ <div className="flex items-center gap-4 mb-6">
149
+ <div className="w-14 h-14 rounded-2xl bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)] flex items-center justify-center text-xl font-black text-[var(--kyro-primary)] group-hover:scale-110 transition-transform duration-500">
150
+ {user.name ? user.name[0] : user.email[0].toUpperCase()}
151
+ </div>
152
+ <div>
153
+ <h3 className="text-lg font-black tracking-tight">
154
+ {user.name || "Set Name"}
155
+ </h3>
156
+ <div className="flex items-center gap-2 text-[11px] font-bold opacity-40 uppercase tracking-wider">
157
+ <Shield className="w-3 h-3" />
158
+ {user.role}
159
+ </div>
160
+ </div>
161
+ </div>
162
+
163
+ <div className="space-y-4 mb-8">
164
+ <div className="flex items-center gap-3 text-sm font-medium text-[var(--kyro-text-secondary)]">
165
+ <Mail className="w-4 h-4 opacity-40" />
166
+ <span className="truncate">{user.email}</span>
167
+ </div>
168
+ <div className="flex items-center gap-3 text-sm font-medium text-[var(--kyro-text-secondary)]">
169
+ <Clock className="w-4 h-4 opacity-40" />
170
+ <span>
171
+ Last seen{" "}
172
+ {user.lastLogin
173
+ ? new Date(user.lastLogin).toLocaleDateString()
174
+ : "Never"}
175
+ </span>
176
+ </div>
177
+ </div>
178
+
179
+ <div className="flex items-center gap-3">
180
+ <button type="button"
181
+ onClick={() => handleToggleLock(user)}
182
+ className={`flex-1 py-2.5 rounded-xl font-black text-[10px] uppercase tracking-widest transition-all flex items-center justify-center gap-2 ${user.locked ? "bg-green-500/10 text-green-500 hover:bg-green-500/20" : "bg-red-500/10 text-red-500 hover:bg-red-500/20"}`}
183
+ >
184
+ {user.locked ? (
185
+ <Unlock className="w-3 h-3" />
186
+ ) : (
187
+ <Lock className="w-3 h-3" />
188
+ )}
189
+ {user.locked ? "Unlock Account" : "Lock Account"}
190
+ </button>
191
+ <button type="button" className="p-2.5 bg-[var(--kyro-bg-secondary)] rounded-xl border border-[var(--kyro-border)] hover:bg-[var(--kyro-surface)] transition-all">
192
+ <MoreVertical className="w-4 h-4 opacity-40" />
193
+ </button>
194
+ </div>
195
+
196
+ {/* Decorative detail */}
197
+ <div className="absolute -bottom-4 -right-4 w-24 h-24 bg-[var(--kyro-primary)] opacity-0 group-hover:opacity-5 blur-3xl transition-opacity" />
198
+ </div>
199
+ ))
200
+ )}
201
+ </div>
202
+ </div>
203
+ );
204
+ }
@@ -134,7 +134,7 @@ export function VersionHistoryPanel({
134
134
  )}
135
135
  </div>
136
136
  <div className="flex items-center gap-1 ml-2">
137
- <button
137
+ <button type="button"
138
138
  onClick={() => onPreview(version)}
139
139
  className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded transition-colors"
140
140
  title="Preview this version"
@@ -152,7 +152,7 @@ export function VersionHistoryPanel({
152
152
  </svg>
153
153
  </button>
154
154
  {onCompare && (
155
- <button
155
+ <button type="button"
156
156
  onClick={() =>
157
157
  onCompare(
158
158
  version,
@@ -176,7 +176,7 @@ export function VersionHistoryPanel({
176
176
  </button>
177
177
  )}
178
178
  {version.id !== currentVersionId && (
179
- <button
179
+ <button type="button"
180
180
  onClick={() => onRestore(version)}
181
181
  className="p-1.5 text-gray-400 hover:text-primary hover:bg-primary-light rounded transition-colors"
182
182
  title="Restore this version"