@kyro-cms/admin 0.3.1 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/dist/EditorClient-XEUOVAAC.js +466 -0
  2. package/dist/EditorClient-XEUOVAAC.js.map +1 -0
  3. package/dist/EditorClient-YLCGVDXY.cjs +468 -0
  4. package/dist/EditorClient-YLCGVDXY.cjs.map +1 -0
  5. package/dist/chunk-7KPIUCGT.js +384 -0
  6. package/dist/chunk-7KPIUCGT.js.map +1 -0
  7. package/dist/chunk-GOACG6R7.cjs +473 -0
  8. package/dist/chunk-GOACG6R7.cjs.map +1 -0
  9. package/dist/index.cjs +14861 -0
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/index.css +1661 -0
  12. package/dist/index.css.map +1 -0
  13. package/dist/index.d.ts +563 -0
  14. package/dist/index.js +14784 -0
  15. package/dist/index.js.map +1 -0
  16. package/package.json +19 -19
  17. package/src/components/ActionBar.tsx +7 -43
  18. package/src/components/Admin.tsx +138 -277
  19. package/src/components/ApiKeysManager.tsx +428 -419
  20. package/src/components/AuditLogsPage.tsx +35 -39
  21. package/src/components/AuthBridge.tsx +51 -0
  22. package/src/components/AutoForm.tsx +495 -1230
  23. package/src/components/BrandingHub.tsx +18 -19
  24. package/src/components/BulkActionsBar.tsx +1 -1
  25. package/src/components/CreateView.tsx +22 -36
  26. package/src/components/Dashboard.tsx +60 -84
  27. package/src/components/DetailView.tsx +113 -91
  28. package/src/components/DeveloperCenter.tsx +200 -198
  29. package/src/components/FieldRenderer.tsx +206 -0
  30. package/src/components/GraphQLPlayground.tsx +340 -480
  31. package/src/components/ListView.tsx +828 -254
  32. package/src/components/LoginPage.tsx +3 -4
  33. package/src/components/MarketplaceManager.tsx +254 -0
  34. package/src/components/MediaGallery.tsx +856 -1192
  35. package/src/components/PluginsManager.tsx +277 -0
  36. package/src/components/RestPlayground.tsx +398 -560
  37. package/src/components/SessionsManager.tsx +211 -0
  38. package/src/components/Sidebar.astro +179 -151
  39. package/src/components/ThemeProvider.tsx +7 -161
  40. package/src/components/UserManagement.tsx +162 -146
  41. package/src/components/UserMenu.tsx +110 -0
  42. package/src/components/WebhookManager.tsx +305 -367
  43. package/src/components/blocks/AccordionBlock.tsx +4 -4
  44. package/src/components/blocks/ArrayBlock.tsx +3 -3
  45. package/src/components/blocks/BlockEditModal.tsx +8 -8
  46. package/src/components/blocks/BlockWrapper.tsx +61 -0
  47. package/src/components/blocks/ButtonBlock.tsx +4 -4
  48. package/src/components/blocks/ChildBlocksTree.tsx +23 -25
  49. package/src/components/blocks/CodeBlock.tsx +15 -15
  50. package/src/components/blocks/ColumnsBlock.tsx +6 -44
  51. package/src/components/blocks/DividerBlock.tsx +3 -3
  52. package/src/components/blocks/FileBlock.tsx +4 -4
  53. package/src/components/blocks/HeadingBlock.tsx +6 -38
  54. package/src/components/blocks/HeroBlock.tsx +4 -4
  55. package/src/components/blocks/ImageBlock.tsx +4 -4
  56. package/src/components/blocks/LinkBlock.tsx +4 -4
  57. package/src/components/blocks/ListBlock.tsx +3 -3
  58. package/src/components/blocks/ParagraphBlock.tsx +12 -42
  59. package/src/components/blocks/RelationshipBlock.tsx +4 -4
  60. package/src/components/blocks/RichTextBlock.tsx +4 -4
  61. package/src/components/blocks/VStackBlock.tsx +5 -37
  62. package/src/components/blocks/VideoBlock.tsx +4 -4
  63. package/src/components/blocks/types.ts +11 -0
  64. package/src/components/fields/AccordionField.tsx +1 -1
  65. package/src/components/fields/ArrayField.tsx +2 -2
  66. package/src/components/fields/ArrayLayout.tsx +93 -0
  67. package/src/components/fields/BlocksField.tsx +122 -111
  68. package/src/components/fields/ButtonField.tsx +1 -1
  69. package/src/components/fields/CheckboxField.tsx +14 -15
  70. package/src/components/fields/ChildrenField.tsx +2 -2
  71. package/src/components/fields/CodeField.tsx +3 -3
  72. package/src/components/fields/ColumnsField.tsx +2 -2
  73. package/src/components/fields/DateField.tsx +13 -26
  74. package/src/components/fields/EditorClient.tsx +26 -28
  75. package/src/components/fields/FieldLayout.tsx +52 -0
  76. package/src/components/fields/GroupLayout.tsx +35 -0
  77. package/src/components/fields/JSONField.tsx +7 -7
  78. package/src/components/fields/LinkField.tsx +1 -1
  79. package/src/components/fields/MarkdownField.tsx +1 -1
  80. package/src/components/fields/NumberField.tsx +13 -26
  81. package/src/components/fields/PortableTextField.tsx +4 -4
  82. package/src/components/fields/PortableTextRenderer.tsx +1 -1
  83. package/src/components/fields/RelationshipBlockField.tsx +31 -23
  84. package/src/components/fields/RelationshipField.tsx +14 -14
  85. package/src/components/fields/SelectField.tsx +17 -26
  86. package/src/components/fields/TabsLayout.tsx +69 -0
  87. package/src/components/fields/TextField.tsx +85 -38
  88. package/src/components/fields/UploadField.tsx +71 -41
  89. package/src/components/fields/VideoField.tsx +1 -1
  90. package/src/components/fields/extensions/blockComponents.tsx +2 -2
  91. package/src/components/fields/extensions/blocksStore.ts +207 -193
  92. package/src/components/fields/types.ts +22 -0
  93. package/src/components/layout/Layout.tsx +1 -1
  94. package/src/components/ui/ActionMenu.tsx +63 -0
  95. package/src/components/ui/Badge.tsx +59 -5
  96. package/src/components/ui/BlockDrawer.tsx +4 -5
  97. package/src/components/ui/CommandPalette.tsx +58 -36
  98. package/src/components/ui/CommandPaletteWrapper.tsx +18 -17
  99. package/src/components/ui/Dropdown.tsx +18 -16
  100. package/src/components/ui/EmptyState.tsx +25 -0
  101. package/src/components/ui/GlobalModal.tsx +49 -0
  102. package/src/components/ui/IconButton.tsx +44 -0
  103. package/src/components/ui/Modal.tsx +19 -20
  104. package/src/components/ui/PageHeader.tsx +158 -0
  105. package/src/components/ui/Pagination.tsx +61 -0
  106. package/src/components/ui/PromptModal.tsx +1 -1
  107. package/src/components/ui/SearchInput.tsx +57 -0
  108. package/src/components/ui/SeoPreview.tsx +31 -0
  109. package/src/components/ui/SessionModal.tsx +0 -0
  110. package/src/components/ui/SlidePanel.tsx +2 -0
  111. package/src/components/ui/Toast.tsx +65 -122
  112. package/src/components/ui/Toaster.tsx +18 -0
  113. package/src/components/ui/icons.tsx +112 -0
  114. package/src/components/users/UserDetail.tsx +290 -0
  115. package/src/components/users/UserForm.tsx +242 -0
  116. package/src/components/users/UsersList.tsx +338 -0
  117. package/src/env.d.ts +13 -13
  118. package/src/fields/index.ts +2 -1
  119. package/src/global.d.ts +7 -0
  120. package/src/hooks/data.ts +2 -9
  121. package/src/hooks/useAsyncData.ts +36 -0
  122. package/src/hooks/useAutoFormState.ts +527 -0
  123. package/src/hooks/useSelection.ts +49 -0
  124. package/src/hooks/useSession.ts +0 -0
  125. package/src/index.ts +11 -1
  126. package/src/integration.ts +86 -11
  127. package/src/kyro-cms.d.ts +209 -0
  128. package/src/layouts/AdminLayout.astro +128 -11
  129. package/src/layouts/AuthLayout.astro +21 -5
  130. package/src/lib/api.ts +175 -55
  131. package/src/lib/autoform-store.ts +435 -0
  132. package/src/lib/config.ts +82 -34
  133. package/src/lib/createRegistry.ts +29 -0
  134. package/src/lib/default-kyro-config.ts +4 -0
  135. package/src/lib/globals.ts +50 -0
  136. package/src/lib/media-utils.ts +18 -0
  137. package/src/lib/object-utils.ts +77 -0
  138. package/src/lib/paths.ts +61 -0
  139. package/src/lib/stores/index.ts +370 -0
  140. package/src/lib/types.ts +43 -0
  141. package/src/lib/useResourceManager.ts +105 -0
  142. package/src/pages/403.astro +67 -0
  143. package/src/pages/[collection]/[id].astro +14 -180
  144. package/src/pages/[collection]/index.astro +11 -6
  145. package/src/pages/api-explorer.astro +173 -0
  146. package/src/pages/audit/index.astro +2 -0
  147. package/src/pages/auth/login.astro +122 -0
  148. package/src/pages/auth/register.astro +167 -0
  149. package/src/pages/graphql-explorer.astro +59 -0
  150. package/src/pages/{admin/graphql.astro → graphql.astro} +51 -17
  151. package/src/pages/index.astro +577 -0
  152. package/src/pages/index_ALT.astro +3 -0
  153. package/src/pages/keys.astro +11 -0
  154. package/src/pages/marketplace.astro +11 -0
  155. package/src/pages/media.astro +3 -0
  156. package/src/pages/plugins.astro +8 -0
  157. package/src/pages/preview/[collection]/[id].astro +188 -123
  158. package/src/pages/rest-playground.astro +62 -0
  159. package/src/pages/roles/index.astro +183 -76
  160. package/src/pages/sessions.astro +8 -0
  161. package/src/pages/settings/[slug].astro +92 -114
  162. package/src/pages/settings/index.astro +5 -3
  163. package/src/pages/users/[id].astro +25 -154
  164. package/src/pages/users/index.astro +19 -130
  165. package/src/pages/users/new.astro +9 -86
  166. package/src/pages/webhooks.astro +11 -0
  167. package/src/routes.ts +80 -0
  168. package/src/styles/main.css +119 -79
  169. package/src/theme/tokens.ts +1 -0
  170. package/src/vite-env.d.ts +14 -0
  171. package/src/collections/auth/index.ts +0 -155
  172. package/src/collections/portfolio/index.ts +0 -343
  173. package/src/components/ApiExplorer.tsx +0 -325
  174. package/src/components/EnhancedListView.tsx +0 -889
  175. package/src/components/GraphQLExplorer.tsx +0 -675
  176. package/src/components/Icons.tsx +0 -23
  177. package/src/components/StatusBadge.tsx +0 -76
  178. package/src/lib/MediaService.ts +0 -541
  179. package/src/lib/auth/sqlite-adapter.ts +0 -319
  180. package/src/lib/dataStore.ts +0 -226
  181. package/src/lib/db/adapter.ts +0 -54
  182. package/src/lib/db/drizzle-mysql-adapter.ts +0 -194
  183. package/src/lib/db/drizzle-mysql-auth-adapter.ts +0 -327
  184. package/src/lib/db/drizzle-postgres-adapter.ts +0 -202
  185. package/src/lib/db/drizzle-postgres-auth-adapter.ts +0 -304
  186. package/src/lib/db/drizzle-sqlite-adapter.ts +0 -227
  187. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +0 -548
  188. package/src/lib/db/index.ts +0 -449
  189. package/src/lib/db/mongodb-adapter.ts +0 -207
  190. package/src/lib/db/mongodb-auth-adapter.ts +0 -305
  191. package/src/lib/db/schema/mysql-auth.ts +0 -113
  192. package/src/lib/db/schema/mysql-content.ts +0 -20
  193. package/src/lib/db/schema/postgres-auth.ts +0 -116
  194. package/src/lib/db/schema/postgres-content.ts +0 -35
  195. package/src/lib/db/schema/postgres-media.ts +0 -52
  196. package/src/lib/db/schema/postgres-settings.ts +0 -11
  197. package/src/lib/db/schema/sqlite-auth.ts +0 -112
  198. package/src/lib/db/schema/sqlite-content.ts +0 -20
  199. package/src/lib/db/version-adapter.ts +0 -248
  200. package/src/lib/graphql/index.ts +0 -1
  201. package/src/lib/graphql/schema.ts +0 -443
  202. package/src/lib/rate-limit.ts +0 -267
  203. package/src/lib/storage.ts +0 -374
  204. package/src/lib/store.ts +0 -85
  205. package/src/middleware.ts +0 -177
  206. package/src/pages/admin/api-explorer.astro +0 -98
  207. package/src/pages/admin/graphql-explorer.astro +0 -40
  208. package/src/pages/admin/index.astro +0 -286
  209. package/src/pages/admin/keys.astro +0 -8
  210. package/src/pages/admin/rest-playground.astro +0 -44
  211. package/src/pages/admin/webhooks.astro +0 -8
  212. package/src/pages/api/[collection]/[id]/publish.ts +0 -52
  213. package/src/pages/api/[collection]/[id]/unpublish.ts +0 -42
  214. package/src/pages/api/[collection]/[id]/versions.ts +0 -66
  215. package/src/pages/api/[collection]/[id].ts +0 -213
  216. package/src/pages/api/[collection]/index.ts +0 -209
  217. package/src/pages/api/auth/[id].ts +0 -121
  218. package/src/pages/api/auth/audit-logs.ts +0 -57
  219. package/src/pages/api/auth/login.ts +0 -211
  220. package/src/pages/api/auth/logout.ts +0 -66
  221. package/src/pages/api/auth/me.ts +0 -36
  222. package/src/pages/api/auth/refresh.ts +0 -119
  223. package/src/pages/api/auth/register.ts +0 -188
  224. package/src/pages/api/auth/users.ts +0 -97
  225. package/src/pages/api/collections.ts +0 -59
  226. package/src/pages/api/globals/[slug].ts +0 -42
  227. package/src/pages/api/graphql.ts +0 -90
  228. package/src/pages/api/health.ts +0 -426
  229. package/src/pages/api/keys/[id].ts +0 -26
  230. package/src/pages/api/keys/index.ts +0 -75
  231. package/src/pages/api/media/[id].ts +0 -309
  232. package/src/pages/api/media/folders.ts +0 -609
  233. package/src/pages/api/media/index.ts +0 -146
  234. package/src/pages/api/media/resize.ts +0 -267
  235. package/src/pages/api/search.ts +0 -82
  236. package/src/pages/api/slug-availability.ts +0 -70
  237. package/src/pages/api/storage-config.ts +0 -20
  238. package/src/pages/api/storage-status.ts +0 -206
  239. package/src/pages/api/upload.ts +0 -334
  240. package/src/pages/api/webhooks/index.ts +0 -71
  241. package/src/pages/login.astro +0 -82
  242. package/src/pages/register.astro +0 -102
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Helper to extract default values from config recursively
3
+ */
4
+ export function getDefaults(fields: any[], prefix = ""): Record<string, any> {
5
+ const defaults: Record<string, any> = {};
6
+ for (const field of fields || []) {
7
+ if (field.defaultValue !== undefined) {
8
+ const key = prefix + field.name;
9
+ defaults[key] = field.defaultValue;
10
+ // Also set nested defaults for groups
11
+ if (field.type === "group" && field.fields) {
12
+ for (const subField of field.fields) {
13
+ if (subField.defaultValue !== undefined) {
14
+ defaults[prefix + field.name + "." + subField.name] =
15
+ subField.defaultValue;
16
+ }
17
+ }
18
+ }
19
+ }
20
+ if (field.fields && Array.isArray(field.fields)) {
21
+ Object.assign(defaults, getDefaults(field.fields, field.name + "."));
22
+ }
23
+ if (field.tabs) {
24
+ for (const tab of field.tabs) {
25
+ if (tab.fields) {
26
+ Object.assign(defaults, getDefaults(tab.fields, prefix));
27
+ }
28
+ }
29
+ }
30
+ }
31
+ return defaults;
32
+ }
33
+
34
+ /**
35
+ * Helper to flatten nested object with dot notation keys
36
+ */
37
+ export function flattenObject(
38
+ obj: Record<string, any>,
39
+ prefix = "",
40
+ ): Record<string, any> {
41
+ const result: Record<string, any> = {};
42
+ for (const key in obj) {
43
+ const newKey = prefix ? `${prefix}.${key}` : key;
44
+ const val = obj[key];
45
+ if (
46
+ val !== null &&
47
+ typeof val === "object" &&
48
+ !Array.isArray(val) &&
49
+ // Only recurse into plain objects, not Dates, Maps, or other class instances
50
+ (val.constructor === Object || !val.constructor)
51
+ ) {
52
+ Object.assign(result, flattenObject(val, newKey));
53
+ } else {
54
+ result[newKey] = val;
55
+ }
56
+ }
57
+ return result;
58
+ }
59
+
60
+ /**
61
+ * Helper to unflatten dot notation keys back to nested object
62
+ */
63
+ export function unflattenObject(flat: Record<string, any>): Record<string, any> {
64
+ const result: Record<string, any> = {};
65
+ for (const key in flat) {
66
+ const parts = key.split(".");
67
+ let current = result;
68
+ for (let i = 0; i < parts.length - 1; i++) {
69
+ if (!current[parts[i]]) {
70
+ current[parts[i]] = {};
71
+ }
72
+ current = current[parts[i]];
73
+ }
74
+ current[parts[parts.length - 1]] = flat[key];
75
+ }
76
+ return result;
77
+ }
@@ -0,0 +1,61 @@
1
+ declare const __KYRO_ADMIN_PATH__: string;
2
+ declare const __KYRO_API_PATH__: string;
3
+
4
+ export const adminPath =
5
+ typeof __KYRO_ADMIN_PATH__ !== "undefined" ? __KYRO_ADMIN_PATH__ : "/admin";
6
+ export const apiPath =
7
+ typeof __KYRO_API_PATH__ !== "undefined" ? __KYRO_API_PATH__ : "/api";
8
+
9
+ export function resolveApi(url: string): string {
10
+ // If URL is already absolute or prefixed with the correct apiPath, return as is
11
+ if (url.startsWith("http") || url.startsWith(apiPath)) return url;
12
+
13
+ // If it starts with the standard "/api/", map it to the configured apiPath
14
+ if (url.startsWith("/api/")) {
15
+ return apiPath + url.slice(4);
16
+ }
17
+
18
+ // Otherwise, prepend the apiPath
19
+ const separator = url.startsWith("/") ? "" : "/";
20
+ return `${apiPath}${separator}${url}`;
21
+ }
22
+
23
+
24
+ export function resolveAdmin(url: string): string {
25
+ // If URL is already absolute or prefixed with the correct adminPath, return as is
26
+ if (url.startsWith("http") || url.startsWith(adminPath)) return url;
27
+
28
+ // If it's a standard "/admin/" path, map it to the configured adminPath
29
+ if (url.startsWith("/admin/")) {
30
+ return adminPath + url.slice(6);
31
+ }
32
+
33
+ // Otherwise, prepend the adminPath if it looks like a relative path
34
+ if (url.startsWith("/")) {
35
+ return adminPath + url;
36
+ }
37
+
38
+ return url;
39
+ }
40
+
41
+
42
+ export function resolveMedia(url: string): string {
43
+ if (!url) return "";
44
+ // Absolute URLs, blob URLs, and data URLs are returned as-is
45
+ if (url.startsWith("http") || url.startsWith("blob:") || url.startsWith("data:")) {
46
+ return url;
47
+ }
48
+
49
+ // For relative paths, ensure they start with a single slash
50
+ // These are relative to the site root (where /uploads/ usually lives)
51
+ return url.startsWith("/") ? url : `/${url}`;
52
+ }
53
+
54
+
55
+ export const paths = {
56
+ admin: adminPath,
57
+ api: apiPath,
58
+ resolveApi,
59
+ resolveAdmin,
60
+ resolveMedia,
61
+ } as const;
@@ -0,0 +1,370 @@
1
+ import { create } from "zustand";
2
+ import { persist } from "zustand/middleware";
3
+ import { apiPath, adminPath } from "../paths";
4
+
5
+ // ============================================================
6
+ // AUTH STORE
7
+ // ============================================================
8
+
9
+ export interface AuthUser {
10
+ id: string;
11
+ email: string;
12
+ role: string;
13
+ tenantId?: string;
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ export interface Permissions {
18
+ collections?: Record<string, { read: boolean; create: boolean; update: boolean; delete: boolean }>;
19
+ globals?: Record<string, { read: boolean; update: boolean }>;
20
+ media?: { read: boolean; create: boolean; update: boolean; delete: boolean };
21
+ users?: { read: boolean; create: boolean; update: boolean; delete: boolean };
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ interface AuthState {
26
+ user: AuthUser | null;
27
+ permissions: Permissions | null;
28
+ isAuthenticated: boolean;
29
+ isLoading: boolean;
30
+ error: string | null;
31
+ setUser: (user: AuthUser | null, permissions?: Permissions | null) => void;
32
+ setLoading: (loading: boolean) => void;
33
+ setError: (error: string | null) => void;
34
+ logout: () => void;
35
+ }
36
+
37
+ export const useAuthStore = create<AuthState>((set) => ({
38
+ user: null,
39
+ permissions: null,
40
+ isAuthenticated: false,
41
+ isLoading: true,
42
+ error: null,
43
+
44
+ setUser: (user, permissions = null) =>
45
+ set({
46
+ user,
47
+ permissions,
48
+ isAuthenticated: !!user,
49
+ isLoading: false,
50
+ error: null,
51
+ }),
52
+
53
+ setLoading: (isLoading) => set({ isLoading }),
54
+
55
+ setError: (error) =>
56
+ set({
57
+ error,
58
+ isLoading: false,
59
+ isAuthenticated: false,
60
+ user: null,
61
+ permissions: null,
62
+ }),
63
+
64
+ logout: () =>
65
+ set({
66
+ user: null,
67
+ permissions: null,
68
+ isAuthenticated: false,
69
+ isLoading: false,
70
+ error: null,
71
+ }),
72
+ }));
73
+
74
+ const API_BASE = apiPath;
75
+ const ADMIN_BASE = adminPath;
76
+
77
+ export async function verifyAuth(): Promise<{
78
+ user: AuthUser | null;
79
+ permissions: Permissions | null;
80
+ }> {
81
+ try {
82
+ const [meRes, accessRes] = await Promise.all([
83
+ fetch(`${API_BASE}/auth/me`, { credentials: "include" }),
84
+ fetch(`${API_BASE}/auth/access`, { credentials: "include" }),
85
+ ]);
86
+
87
+ if (!meRes.ok) {
88
+ return { user: null, permissions: null };
89
+ }
90
+
91
+ const [meData, accessData] = await Promise.all([
92
+ meRes.json(),
93
+ accessRes.ok ? accessRes.json() : Promise.resolve(null),
94
+ ]);
95
+
96
+ return {
97
+ user: meData.user || null,
98
+ permissions: accessData || null,
99
+ };
100
+ } catch {
101
+ return { user: null, permissions: null };
102
+ }
103
+ }
104
+
105
+ export async function doLogout(): Promise<void> {
106
+ try {
107
+ await fetch(`${API_BASE}/auth/logout`, {
108
+ method: "POST",
109
+ credentials: "include",
110
+ });
111
+ } finally {
112
+ useAuthStore.getState().logout();
113
+ }
114
+ }
115
+
116
+ export function redirectToLogin(): void {
117
+ window.location.href = `${ADMIN_BASE}/login`;
118
+ }
119
+
120
+
121
+ // ============================================================
122
+ // TOAST STORE
123
+ // ============================================================
124
+
125
+ export interface Toast {
126
+ id: string;
127
+ type: "success" | "error" | "warning" | "info";
128
+ message: string;
129
+ }
130
+
131
+ interface ToastState {
132
+ toasts: Toast[];
133
+ addToast: (type: Toast["type"], message: string) => void;
134
+ removeToast: (id: string) => void;
135
+ clearToasts: () => void;
136
+ }
137
+
138
+ export const useToastStore = create<ToastState>((set) => ({
139
+ toasts: [],
140
+
141
+ addToast: (type, message) => {
142
+ const id = Math.random().toString(36).substring(7);
143
+ set((state) => ({
144
+ toasts: [...state.toasts, { id, type, message }],
145
+ }));
146
+ },
147
+
148
+ removeToast: (id) =>
149
+ set((state) => ({
150
+ toasts: state.toasts.filter((t) => t.id !== id),
151
+ })),
152
+
153
+ clearToasts: () => set({ toasts: [] }),
154
+ }));
155
+
156
+ export const toast = {
157
+ success: (message: string) =>
158
+ useToastStore.getState().addToast("success", message),
159
+ error: (message: string) =>
160
+ useToastStore.getState().addToast("error", message),
161
+ warning: (message: string) =>
162
+ useToastStore.getState().addToast("warning", message),
163
+ info: (message: string) =>
164
+ useToastStore.getState().addToast("info", message),
165
+ };
166
+
167
+ // ============================================================
168
+ // THEME STORE
169
+ // ============================================================
170
+
171
+ export type ThemeMode = "light" | "dark";
172
+
173
+ interface ThemeState {
174
+ mode: ThemeMode;
175
+ setMode: (mode: ThemeMode) => void;
176
+ toggleMode: () => void;
177
+ }
178
+
179
+ export const useThemeStore = create<ThemeState>()(
180
+ persist(
181
+ (set) => ({
182
+ mode: "light",
183
+
184
+ setMode: (mode) => set({ mode }),
185
+
186
+ toggleMode: () =>
187
+ set((state) => ({ mode: state.mode === "light" ? "dark" : "light" })),
188
+ }),
189
+ {
190
+ name: "kyro-theme",
191
+ },
192
+ ),
193
+ );
194
+
195
+ // ============================================================
196
+ // UI STORE
197
+ // ============================================================
198
+
199
+ export interface ModalConfig {
200
+ title: string;
201
+ message: string;
202
+ confirmLabel?: string;
203
+ cancelLabel?: string;
204
+ onConfirm?: () => void | Promise<void>;
205
+ onCancel?: () => void;
206
+ variant?: "default" | "danger" | "success" | "warning";
207
+ size?: "sm" | "md" | "lg" | "xl";
208
+ }
209
+
210
+ interface UIState {
211
+ sidebarOpen: boolean;
212
+ toggleSidebar: () => void;
213
+ setSidebarOpen: (open: boolean) => void;
214
+
215
+ modal: {
216
+ open: boolean;
217
+ config: ModalConfig | null;
218
+ };
219
+ confirm: (config: ModalConfig) => void;
220
+ alert: (config: Omit<ModalConfig, "onConfirm" | "cancelLabel">) => void;
221
+ closeModal: () => void;
222
+
223
+ activeModal: string | null; // Legacy for specific hardcoded modals
224
+ openModal: (modal: string) => void;
225
+ }
226
+
227
+ export const useUIStore = create<UIState>((set) => ({
228
+ sidebarOpen: true,
229
+ toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
230
+ setSidebarOpen: (open) => set({ sidebarOpen: open }),
231
+
232
+ modal: {
233
+ open: false,
234
+ config: null,
235
+ },
236
+ confirm: (config) => set({
237
+ modal: {
238
+ open: true,
239
+ config: { ...config, variant: config.variant || "default" },
240
+ }
241
+ }),
242
+ alert: (config) => set({
243
+ modal: {
244
+ open: true,
245
+ config: {
246
+ ...config,
247
+ variant: config.variant || "default",
248
+ confirmLabel: config.confirmLabel || "OK",
249
+ onConfirm: () => set((s) => ({ modal: { ...s.modal, open: false } }))
250
+ },
251
+ }
252
+ }),
253
+ closeModal: () => set((state) => ({
254
+ modal: { ...state.modal, open: false },
255
+ activeModal: null
256
+ })),
257
+
258
+ activeModal: null,
259
+ openModal: (modal) => set({ activeModal: modal }),
260
+ }));
261
+
262
+ // ============================================================
263
+ // EDITOR STORE
264
+ // ============================================================
265
+
266
+ interface EditorState {
267
+ editor: unknown;
268
+ setEditor: (editor: unknown) => void;
269
+
270
+ blockDrawerOpen: boolean;
271
+ openBlockDrawer: (options?: { targetColumn?: number }) => void;
272
+ closeBlockDrawer: () => void;
273
+ toggleBlockDrawer: () => void;
274
+
275
+ selectedBlock: string | null;
276
+ setSelectedBlock: (block: string | null) => void;
277
+
278
+ pendingInsert: {
279
+ pos: number | null;
280
+ column: number | null;
281
+ };
282
+ setPendingInsert: (pos: number | null, column?: number) => void;
283
+ clearPendingInsert: () => void;
284
+ }
285
+
286
+ export const useEditorStore = create<EditorState>((set) => ({
287
+ editor: null,
288
+ setEditor: (editor) => set({ editor }),
289
+
290
+ blockDrawerOpen: false,
291
+ openBlockDrawer: (options) =>
292
+ set({
293
+ blockDrawerOpen: true,
294
+ pendingInsert: { pos: null, column: options?.targetColumn ?? null },
295
+ }),
296
+ closeBlockDrawer: () =>
297
+ set({
298
+ blockDrawerOpen: false,
299
+ pendingInsert: { pos: null, column: null },
300
+ }),
301
+ toggleBlockDrawer: () =>
302
+ set((state) => ({ blockDrawerOpen: !state.blockDrawerOpen })),
303
+
304
+ selectedBlock: null,
305
+ setSelectedBlock: (block) => set({ selectedBlock: block }),
306
+
307
+ pendingInsert: { pos: null, column: null },
308
+ setPendingInsert: (pos, column) =>
309
+ set({ pendingInsert: { pos, column: column ?? null } }),
310
+ clearPendingInsert: () => set({ pendingInsert: { pos: null, column: null } }),
311
+ }));
312
+
313
+ // ============================================================
314
+ // DATA STORE
315
+ // ============================================================
316
+
317
+ interface DataCache<T> {
318
+ data: T | null;
319
+ loading: boolean;
320
+ error: string | null;
321
+ lastFetch: number | null;
322
+ }
323
+
324
+ interface DataStore {
325
+ cache: Record<string, DataCache<unknown>>;
326
+ setCache: (key: string, data: unknown) => void;
327
+ setLoading: (key: string, loading: boolean) => void;
328
+ setError: (key: string, error: string) => void;
329
+ getCache: (key: string) => DataCache<unknown> | null;
330
+ invalidateCache: (key?: string) => void;
331
+ }
332
+
333
+ export const useDataStore = create<DataStore>((set, get) => ({
334
+ cache: {},
335
+
336
+ setCache: (key, data) =>
337
+ set((state) => ({
338
+ cache: {
339
+ ...state.cache,
340
+ [key]: { data, loading: false, error: null, lastFetch: Date.now() },
341
+ },
342
+ })),
343
+
344
+ setLoading: (key, loading) =>
345
+ set((state) => ({
346
+ cache: {
347
+ ...state.cache,
348
+ [key]: { ...state.cache[key], loading, error: null },
349
+ },
350
+ })),
351
+
352
+ setError: (key, error) =>
353
+ set((state) => ({
354
+ cache: {
355
+ ...state.cache,
356
+ [key]: { data: null, loading: false, error, lastFetch: null },
357
+ },
358
+ })),
359
+
360
+ getCache: (key) => get().cache[key] || null,
361
+
362
+ invalidateCache: (key) =>
363
+ set((state) => {
364
+ if (key) {
365
+ const { [key]: _, ...rest } = state.cache;
366
+ return { cache: rest };
367
+ }
368
+ return { cache: {} };
369
+ }),
370
+ }));
@@ -0,0 +1,43 @@
1
+ import type { CollectionConfig, GlobalConfig } from "@kyro-cms/core/client";
2
+
3
+ export interface KyroAdminConfig {
4
+ collections?: CollectionConfig[] | Record<string, CollectionConfig>;
5
+ globals?: GlobalConfig[] | Record<string, GlobalConfig>;
6
+ adapter?: unknown;
7
+ name?: string;
8
+ }
9
+
10
+ export interface User {
11
+ id: string;
12
+ email: string;
13
+ name?: string;
14
+ role: string;
15
+ status?: "active" | "locked";
16
+ locked?: boolean;
17
+ lastLogin?: string;
18
+ createdAt?: string;
19
+ updatedAt?: string;
20
+ tenantId?: string;
21
+ emailVerified?: boolean;
22
+ failedLoginAttempts?: number;
23
+ }
24
+
25
+ export interface MediaItem {
26
+ id: string;
27
+ url: string;
28
+ filename: string;
29
+ mimeType: string;
30
+ fileSize: number;
31
+ width?: number;
32
+ height?: number;
33
+ alt?: string;
34
+ title?: string;
35
+ description?: string;
36
+ folder?: string;
37
+ type?: string;
38
+ caption?: string;
39
+ originalName?: string;
40
+ thumbnailUrl?: string;
41
+ createdAt?: string;
42
+ updatedAt?: string;
43
+ }
@@ -0,0 +1,105 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { apiGet, apiPost, apiDelete, apiPatch } from "./api";
3
+ import { useUIStore } from "./stores";
4
+
5
+ interface UseResourceManagerOptions<T> {
6
+ endpoint: string;
7
+ onSuccess?: (action: "load" | "create" | "delete" | "update", data?: unknown) => void;
8
+ onError?: (action: "load" | "create" | "delete" | "update", error: unknown) => void;
9
+ transformLoad?: (data: unknown[]) => T[];
10
+ }
11
+
12
+ export function useResourceManager<T extends { id: string }>(
13
+ options: UseResourceManagerOptions<T>
14
+ ) {
15
+ const [items, setItems] = useState<T[]>([]);
16
+ const [loading, setLoading] = useState(false);
17
+ const [error, setError] = useState<string | null>(null);
18
+ const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
19
+ const { confirm, alert } = useUIStore();
20
+
21
+ const load = useCallback(async () => {
22
+ setLoading(true);
23
+ setError(null);
24
+ try {
25
+ const response = await apiGet<any>(options.endpoint);
26
+ const data = Array.isArray(response) ? response : response.docs || [];
27
+ const transformed = options.transformLoad ? options.transformLoad(data) : data;
28
+ setItems(transformed);
29
+ options.onSuccess?.("load", transformed);
30
+ } catch (e: unknown) {
31
+ const message = e instanceof Error ? e.message : "Failed to load resources";
32
+ setError(message);
33
+ options.onError?.("load", e as Error);
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ }, [options.endpoint, options.transformLoad]);
38
+
39
+ useEffect(() => {
40
+ load();
41
+ }, [load]);
42
+
43
+ const remove = useCallback((id: string, resourceName = "item") => {
44
+ confirm({
45
+ title: `Delete ${resourceName}`,
46
+ message: `Are you sure you want to delete this ${resourceName.toLowerCase()}? This action cannot be undone.`,
47
+ variant: "danger",
48
+ onConfirm: async () => {
49
+ try {
50
+ await apiDelete(`${options.endpoint}/${id}`);
51
+ setItems((prev) => prev.filter((item) => item.id !== id));
52
+ options.onSuccess?.("delete", id);
53
+ } catch (e: unknown) {
54
+ const message = e instanceof Error ? e.message : `Failed to delete ${resourceName}`;
55
+ alert({ title: "Error", message });
56
+ options.onError?.("delete", e as Error);
57
+ }
58
+ },
59
+ });
60
+ }, [options.endpoint, confirm, alert]);
61
+
62
+ const create = useCallback(async (data: unknown) => {
63
+ setError(null);
64
+ try {
65
+ const created = await apiPost<T>(options.endpoint, data);
66
+ setItems((prev) => [created, ...prev]);
67
+ setIsCreateModalOpen(false);
68
+ options.onSuccess?.("create", created);
69
+ return created;
70
+ } catch (e: unknown) {
71
+ const message = e instanceof Error ? e.message : "Failed to create resource";
72
+ setError(message);
73
+ options.onError?.("create", e as Error);
74
+ throw e;
75
+ }
76
+ }, [options.endpoint]);
77
+
78
+ const update = useCallback(async (id: string, data: unknown) => {
79
+ setError(null);
80
+ try {
81
+ const updated = await apiPatch<T>(`${options.endpoint}/${id}`, data);
82
+ setItems((prev) => prev.map((item) => (item.id === id ? updated : item)));
83
+ options.onSuccess?.("update", updated);
84
+ return updated;
85
+ } catch (e: unknown) {
86
+ const message = e instanceof Error ? e.message : "Failed to update resource";
87
+ setError(message);
88
+ options.onError?.("update", e as Error);
89
+ throw e;
90
+ }
91
+ }, [options.endpoint]);
92
+
93
+ return {
94
+ items,
95
+ setItems,
96
+ loading,
97
+ error,
98
+ load,
99
+ remove,
100
+ create,
101
+ update,
102
+ isCreateModalOpen,
103
+ setIsCreateModalOpen,
104
+ };
105
+ }