@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,435 @@
1
+ import { create } from "zustand";
2
+ import { persist, createJSONStorage } from "zustand/middleware";
3
+ import type { StateStorage } from "zustand/middleware";
4
+ import { createStorage } from "unstorage";
5
+ import indexedbDriver from "unstorage/drivers/indexedb";
6
+ import type { Version } from "@kyro-cms/core/client";
7
+
8
+ let storageInstance: ReturnType<typeof createStorage> | null = null;
9
+ let storageReady = false;
10
+
11
+ const getStorage = async () => {
12
+ if (storageInstance && storageReady) {
13
+ return storageInstance;
14
+ }
15
+
16
+ storageInstance = createStorage({
17
+ driver: indexedbDriver({
18
+ dbName: "kyro-autosave",
19
+ storeName: "autosave",
20
+ }),
21
+ });
22
+
23
+ storageReady = true;
24
+ return storageInstance;
25
+ };
26
+
27
+ const createAutoFormStorage = (): StateStorage => {
28
+ if (typeof window === "undefined") {
29
+ return {
30
+ getItem: async () => null,
31
+ setItem: async () => {},
32
+ removeItem: async () => {},
33
+ };
34
+ }
35
+
36
+ return {
37
+ getItem: async (name: string): Promise<string | null> => {
38
+ try {
39
+ const store = await getStorage();
40
+ const value = await store.getItem<string>(name);
41
+ return value ?? null;
42
+ } catch (e) {
43
+ console.error("Storage getItem error:", e);
44
+ return localStorage.getItem(name);
45
+ }
46
+ },
47
+ setItem: async (name: string, value: string): Promise<void> => {
48
+ try {
49
+ const store = await getStorage();
50
+ await store.setItem(name, value);
51
+ } catch (e) {
52
+ console.error("Storage setItem error:", e);
53
+ localStorage.setItem(name, value);
54
+ }
55
+ },
56
+ removeItem: async (name: string): Promise<void> => {
57
+ try {
58
+ const store = await getStorage();
59
+ await store.removeItem(name);
60
+ } catch (e) {
61
+ console.error("Storage removeItem error:", e);
62
+ localStorage.removeItem(name);
63
+ }
64
+ },
65
+ };
66
+ };
67
+
68
+ interface AutoFormStore {
69
+ // In-memory document state
70
+ formData: Record<string, unknown>;
71
+ lastSavedData: Record<string, unknown>;
72
+ sidebarCollapsed: boolean;
73
+ draftCache: Record<string, BrowserDraftCacheEntry>;
74
+
75
+ // UI State (not persisted)
76
+ activeTab: number;
77
+ isSlugLocked: boolean;
78
+ view: "edit" | "version" | "api";
79
+ isDropdownOpen: boolean;
80
+ versions: Version[];
81
+ loadingVersions: boolean;
82
+ showPreview: boolean;
83
+ isMenuOpen: boolean;
84
+ hasUnsavedChanges: boolean;
85
+ loadingFields: Record<string, boolean>;
86
+ compareMode: boolean;
87
+ compareSelected: string[];
88
+ compareDiffs: VersionDiff[];
89
+ loadingDiffs: boolean;
90
+ isAutoSaving: boolean;
91
+ autoSaveStatus: "idle" | "saving" | "success" | "error" | "conflict";
92
+
93
+ // Auto-save
94
+ lastAutoSaveTime: number;
95
+ autoSaveSkip: boolean;
96
+ autoSaveTimer: NodeJS.Timeout | null;
97
+
98
+ // Actions - Field Updates
99
+ setField: (field: string, value: unknown) => void;
100
+ setFormData: (data: Record<string, unknown>) => void;
101
+ setLastSavedData: (data: Record<string, unknown>) => void;
102
+ setNestedField: (path: string, value: unknown) => void;
103
+
104
+ // Actions - UI State
105
+ setActiveTab: (tab: number) => void;
106
+ setIsSlugLocked: (locked: boolean | ((prev: boolean) => boolean)) => void;
107
+ setView: (view: "edit" | "version" | "api") => void;
108
+ setIsDropdownOpen: (open: boolean | ((prev: boolean) => boolean)) => void;
109
+ setVersions: (versions: Version[]) => void;
110
+ setLoadingVersions: (loading: boolean) => void;
111
+ setShowPreview: (show: boolean | ((prev: boolean) => boolean)) => void;
112
+ setIsMenuOpen: (open: boolean | ((prev: boolean) => boolean)) => void;
113
+ setHasUnsavedChanges: (hasChanges: boolean) => void;
114
+ setLoadingFields: (fields: Record<string, boolean> | ((prev: Record<string, boolean>) => Record<string, boolean>)) => void;
115
+ updateLoadingField: (field: string, loading: boolean) => void;
116
+ setCompareMode: (mode: boolean) => void;
117
+ setCompareSelected: (selected: string[] | ((prev: string[]) => string[])) => void;
118
+ setCompareDiffs: (diffs: VersionDiff[]) => void;
119
+ setLoadingDiffs: (loading: boolean) => void;
120
+ setIsAutoSaving: (saving: boolean) => void;
121
+ setAutoSaveStatus: (status: "idle" | "saving" | "success" | "error" | "conflict") => void;
122
+ setSidebarCollapsed: (collapsed: boolean) => void;
123
+
124
+ // Auto-save actions
125
+ setAutoSaveSkip: (skip: boolean) => void;
126
+ setLastAutoSaveTime: (time: number) => void;
127
+ startAutoSaveTimer: (callback: () => void, delay: number) => void;
128
+ clearAutoSaveTimer: () => void;
129
+
130
+ // Actions - Data Management
131
+ markSaved: () => void;
132
+ resetForm: () => void;
133
+ loadDocument: (
134
+ data: Record<string, unknown>,
135
+ lastSaved?: Record<string, unknown>,
136
+ ) => void;
137
+
138
+ updateTabData: (tabName: string, newTabData: Record<string, unknown>) => void;
139
+ getField: (field: string) => unknown;
140
+ getNestedField: (path: string) => unknown;
141
+ getHasChanges: () => boolean;
142
+ setDraftCache: (documentKey: string, draft: BrowserDraftCacheEntry) => void;
143
+ getDraftCache: (documentKey: string) => BrowserDraftCacheEntry | null;
144
+ clearDraftCache: (documentKey: string) => void;
145
+ }
146
+
147
+ export interface BrowserDraftCacheEntry {
148
+ data: Record<string, unknown>;
149
+ baseUpdatedAt?: string | null;
150
+ draftUpdatedAt: string;
151
+ lastSyncedAt?: string | null;
152
+ }
153
+
154
+ export const useAutoFormStore = create<AutoFormStore>()(
155
+ persist(
156
+ (set, get) => ({
157
+ // Initial persisted state
158
+ formData: {},
159
+ lastSavedData: {},
160
+ sidebarCollapsed: false,
161
+ draftCache: {},
162
+
163
+ // Initial UI state
164
+ activeTab: 0,
165
+ isSlugLocked: true,
166
+ view: "edit",
167
+ isDropdownOpen: false,
168
+ versions: [],
169
+ loadingVersions: false,
170
+ showPreview: false,
171
+ isMenuOpen: false,
172
+ hasUnsavedChanges: false,
173
+ loadingFields: {},
174
+ compareMode: false,
175
+ compareSelected: [],
176
+ compareDiffs: [],
177
+ loadingDiffs: false,
178
+ isAutoSaving: false,
179
+ autoSaveStatus: "idle",
180
+
181
+ // Auto-save state
182
+ lastAutoSaveTime: 0,
183
+ autoSaveSkip: false,
184
+ autoSaveTimer: null,
185
+
186
+ // Field update actions
187
+ setField: (field: string, value: unknown) => {
188
+ if (field === "blocks") {
189
+ console.log("autoform setField blocks: value=", value);
190
+ }
191
+ set((state) => ({
192
+ formData: {
193
+ ...state.formData,
194
+ [field]: value,
195
+ },
196
+ }));
197
+ },
198
+
199
+ setFormData: (data: Record<string, unknown>) => {
200
+ set({ formData: data });
201
+ },
202
+
203
+ setNestedField: (path: string, value: unknown) => {
204
+ set((state) => {
205
+ const keys = path.split(".");
206
+ const newFormData = { ...state.formData };
207
+ let current: Record<string, unknown> = newFormData;
208
+
209
+ for (let i = 0; i < keys.length - 1; i++) {
210
+ if (current[keys[i]] === undefined) {
211
+ current[keys[i]] = {};
212
+ }
213
+ current[keys[i]] = { ...current[keys[i]] };
214
+ current = current[keys[i]];
215
+ }
216
+
217
+ current[keys[keys.length - 1]] = value;
218
+
219
+ return { formData: newFormData };
220
+ });
221
+ },
222
+
223
+ // UI state actions
224
+ setActiveTab: (tab: number) => set({ activeTab: tab }),
225
+ setIsSlugLocked: (locked) =>
226
+ set((state) => ({
227
+ isSlugLocked:
228
+ typeof locked === "function" ? locked(state.isSlugLocked) : locked,
229
+ })),
230
+ setView: (view) => set({ view }),
231
+ setIsDropdownOpen: (open) =>
232
+ set((state) => ({
233
+ isDropdownOpen:
234
+ typeof open === "function" ? open(state.isDropdownOpen) : open,
235
+ })),
236
+ setVersions: (versions: Version[]) => set({ versions }),
237
+ setLoadingVersions: (loading: boolean) =>
238
+ set({ loadingVersions: loading }),
239
+ setShowPreview: (show) =>
240
+ set((state) => ({
241
+ showPreview:
242
+ typeof show === "function" ? show(state.showPreview) : show,
243
+ })),
244
+ setIsMenuOpen: (open) =>
245
+ set((state) => ({
246
+ isMenuOpen: typeof open === "function" ? open(state.isMenuOpen) : open,
247
+ })),
248
+ setHasUnsavedChanges: (hasChanges: boolean) =>
249
+ set({ hasUnsavedChanges: hasChanges }),
250
+ setLoadingFields: (fields) =>
251
+ set((state) => ({
252
+ loadingFields:
253
+ typeof fields === "function" ? fields(state.loadingFields) : fields,
254
+ })),
255
+ updateLoadingField: (field, loading) =>
256
+ set((state) => ({
257
+ loadingFields: { ...state.loadingFields, [field]: loading },
258
+ })),
259
+ setCompareMode: (mode: boolean) => set({ compareMode: mode }),
260
+ setCompareSelected: (selected) =>
261
+ set((state) => ({
262
+ compareSelected:
263
+ typeof selected === "function"
264
+ ? selected(state.compareSelected)
265
+ : selected,
266
+ })),
267
+ setCompareDiffs: (diffs: VersionDiff[]) => set({ compareDiffs: diffs }),
268
+ setLoadingDiffs: (loading: boolean) => set({ loadingDiffs: loading }),
269
+ setIsAutoSaving: (saving: boolean) => set({ isAutoSaving: saving }),
270
+ setAutoSaveStatus: (status) => set({ autoSaveStatus: status }),
271
+ setSidebarCollapsed: (collapsed: boolean) =>
272
+ set({ sidebarCollapsed: collapsed }),
273
+
274
+ // Auto-save actions
275
+ setAutoSaveSkip: (skip: boolean) => set({ autoSaveSkip: skip }),
276
+ setLastAutoSaveTime: (time: number) => set({ lastAutoSaveTime: time }),
277
+
278
+ startAutoSaveTimer: (callback: () => void, delay: number) => {
279
+ const { autoSaveTimer } = get();
280
+ if (autoSaveTimer) {
281
+ clearTimeout(autoSaveTimer);
282
+ }
283
+ const timer = setTimeout(callback, delay);
284
+ set({ autoSaveTimer: timer as any });
285
+ },
286
+
287
+ clearAutoSaveTimer: () => {
288
+ const { autoSaveTimer } = get();
289
+ if (autoSaveTimer) {
290
+ clearTimeout(autoSaveTimer);
291
+ set({ autoSaveTimer: null });
292
+ }
293
+ },
294
+
295
+ // Data management
296
+ markSaved: () => {
297
+ const { formData } = get();
298
+ set({ lastSavedData: formData, hasUnsavedChanges: false });
299
+ },
300
+
301
+ setLastSavedData: (data: Record<string, unknown>) => {
302
+ set({ lastSavedData: data });
303
+ },
304
+
305
+ resetForm: () => {
306
+ set({
307
+ formData: {},
308
+ lastSavedData: {},
309
+ hasUnsavedChanges: false,
310
+ activeTab: 0,
311
+ });
312
+ },
313
+
314
+ loadDocument: (
315
+ data: Record<string, unknown>,
316
+ lastSaved?: Record<string, unknown>,
317
+ ) => {
318
+ set({
319
+ formData: data,
320
+ lastSavedData: lastSaved || data,
321
+ hasUnsavedChanges: false,
322
+ });
323
+ },
324
+
325
+ updateTabData: (tabName, newTabData) => {
326
+ const { formData } = get();
327
+ const tabData = formData[tabName] || {};
328
+
329
+ let updatedTab;
330
+ if (Array.isArray(newTabData)) {
331
+ updatedTab = { ...tabData, content: newTabData };
332
+ } else if (typeof newTabData === "object" && newTabData !== null) {
333
+ updatedTab = { ...tabData, ...newTabData };
334
+ } else {
335
+ updatedTab = newTabData;
336
+ }
337
+
338
+ set((state) => ({
339
+ formData: {
340
+ ...state.formData,
341
+ [tabName]: updatedTab,
342
+ },
343
+ }));
344
+ },
345
+
346
+ // Computed values
347
+ getField: (field: string) => {
348
+ return get().formData[field];
349
+ },
350
+
351
+ getNestedField: (path: string) => {
352
+ const keys = path.split(".");
353
+ let current: unknown = get().formData;
354
+
355
+ for (const key of keys) {
356
+ if (current === undefined || current === null) return undefined;
357
+ current = (current as Record<string, unknown>)[key];
358
+ }
359
+
360
+ return current;
361
+ },
362
+
363
+ getHasChanges: () => {
364
+ const { formData, lastSavedData } = get();
365
+ return JSON.stringify(formData) !== JSON.stringify(lastSavedData);
366
+ },
367
+
368
+ setDraftCache: (documentKey, draft) =>
369
+ set((state) => ({
370
+ draftCache: {
371
+ ...state.draftCache,
372
+ [documentKey]: draft,
373
+ },
374
+ })),
375
+
376
+ getDraftCache: (documentKey) => get().draftCache[documentKey] || null,
377
+
378
+ clearDraftCache: (documentKey) =>
379
+ set((state) => {
380
+ const next = { ...state.draftCache };
381
+ delete next[documentKey];
382
+ return { draftCache: next };
383
+ }),
384
+ }),
385
+ {
386
+ name: "kyro-autoform-storage",
387
+ storage: createJSONStorage(() => createAutoFormStorage()),
388
+ partialize: (state) => ({
389
+ sidebarCollapsed: state.sidebarCollapsed,
390
+ draftCache: state.draftCache,
391
+ }),
392
+ },
393
+ ),
394
+ );
395
+
396
+ // Helper hook to get field value with tab support
397
+ export function useAutoFormField(
398
+ fieldName: string,
399
+ tabData?: Record<string, unknown>,
400
+ ) {
401
+ const formData = useAutoFormStore((s) => s.formData);
402
+ const setField = useAutoFormStore((s) => s.setField);
403
+ const setNestedField = useAutoFormStore((s) => s.setNestedField);
404
+
405
+ // If tabData is provided, look inside it first (for fields inside tabs)
406
+ if (tabData !== undefined) {
407
+ return {
408
+ value: tabData[fieldName],
409
+ onChange: (value: unknown) => {
410
+ return value;
411
+ },
412
+ };
413
+ }
414
+
415
+ return {
416
+ value: formData[fieldName],
417
+ onChange: (value: unknown) => setField(fieldName, value),
418
+ };
419
+ }
420
+
421
+ // Helper to get formData for tabs
422
+ export function useAutoFormTabData(tabName: string) {
423
+ const formData = useAutoFormStore((s) => s.formData);
424
+ const setNestedField = useAutoFormStore((s) => s.setNestedField);
425
+ const setFormData = useAutoFormStore((s) => s.setFormData);
426
+
427
+ const tabData = formData[tabName] || {};
428
+ const updateTabData = useAutoFormStore((s) => s.updateTabData);
429
+
430
+ const onTabDataChange = (newTabData: Record<string, unknown>) => {
431
+ updateTabData(tabName, newTabData);
432
+ };
433
+
434
+ return { tabData, onTabDataChange };
435
+ }
package/src/lib/config.ts CHANGED
@@ -1,31 +1,72 @@
1
1
  import type { CollectionConfig, GlobalConfig } from "@kyro-cms/core/client";
2
+ import projectConfig from "kyro:config";
2
3
  import {
3
4
  blogCollections,
4
5
  ecommerceCollections,
5
6
  minimalCollections,
6
7
  kitchenSinkCollections,
7
8
  mediaCollections,
9
+ authCollections,
8
10
  allSettingsGlobals,
9
11
  coreSettingsGlobals,
10
12
  ecommerceSettingsGlobals,
11
- } from "@kyro-cms/core";
12
- import { authCollections } from "../collections/auth";
13
- import { portfolioCollections } from "../collections/portfolio";
13
+ } from "@kyro-cms/core/templates";
14
14
 
15
- export type AdminTemplate =
16
- | "minimal"
17
- | "blog"
18
- | "ecommerce"
19
- | "kitchen-sink"
20
- | "portfolio";
15
+ type ConfigCollectionInput =
16
+ | CollectionConfig[]
17
+ | Record<string, CollectionConfig>
18
+ | undefined;
19
+ type ConfigGlobalInput =
20
+ | GlobalConfig[]
21
+ | Record<string, GlobalConfig>
22
+ | undefined;
23
+
24
+ export type AdminTemplate = "minimal" | "blog" | "ecommerce" | "kitchen-sink";
25
+
26
+ export function toArray<T>(input: T[] | Record<string, T> | undefined): T[] {
27
+ if (!input) return [];
28
+ return Array.isArray(input) ? input : Object.values(input);
29
+ }
30
+
31
+ export function toCollectionMap(
32
+ collections: CollectionConfig[],
33
+ ): Record<string, CollectionConfig> {
34
+ return collections.reduce(
35
+ (acc, c) => {
36
+ if (c.slug) acc[c.slug] = c;
37
+ return acc;
38
+ },
39
+ {} as Record<string, CollectionConfig>,
40
+ );
41
+ }
42
+
43
+ export function toGlobalMap(globals: GlobalConfig[]): Record<string, GlobalConfig> {
44
+ return globals.reduce(
45
+ (acc, g) => {
46
+ if (g.slug) acc[g.slug] = g;
47
+ return acc;
48
+ },
49
+ {} as Record<string, GlobalConfig>,
50
+ );
51
+ }
52
+
53
+ function addMissingCollections(
54
+ target: CollectionConfig[],
55
+ collections: CollectionConfig[],
56
+ ) {
57
+ const existing = new Set(target.map((collection) => collection.slug));
58
+ for (const collection of collections) {
59
+ if (!existing.has(collection.slug)) {
60
+ target.push(collection);
61
+ existing.add(collection.slug);
62
+ }
63
+ }
64
+ }
21
65
 
22
66
  export function getAdminConfig(template: AdminTemplate = "blog") {
23
67
  const collections: CollectionConfig[] = [];
24
68
  const globals: GlobalConfig[] = [];
25
69
 
26
- collections.push(...Object.values(mediaCollections));
27
- collections.push(...Object.values(authCollections));
28
-
29
70
  switch (template) {
30
71
  case "minimal":
31
72
  collections.push(...Object.values(minimalCollections));
@@ -48,37 +89,44 @@ export function getAdminConfig(template: AdminTemplate = "blog") {
48
89
  );
49
90
  globals.push(...allSettingsGlobals);
50
91
  break;
51
- case "portfolio":
52
- collections.push(...Object.values(blogCollections));
53
- collections.push(...Object.values(portfolioCollections));
54
- globals.push(...coreSettingsGlobals);
55
- break;
56
92
  }
57
93
 
58
- const collectionsMap = collections.reduce(
59
- (acc, c) => {
60
- if (c.slug) acc[c.slug] = c;
61
- return acc;
62
- },
63
- {} as Record<string, CollectionConfig>,
64
- );
94
+ addMissingCollections(collections, Object.values(mediaCollections));
95
+ addMissingCollections(collections, Object.values(authCollections));
65
96
 
66
- const globalsMap = globals.reduce(
67
- (acc, g) => {
68
- if (g.slug) acc[g.slug] = g;
69
- return acc;
70
- },
71
- {} as Record<string, GlobalConfig>,
72
- );
97
+ return {
98
+ collections: toCollectionMap(collections),
99
+ globals: toGlobalMap(globals),
100
+ };
101
+ }
102
+
103
+ function createProjectAdminConfig(config: {
104
+ collections?: ConfigCollectionInput;
105
+ globals?: ConfigGlobalInput;
106
+ }) {
107
+ const projectCollections = toArray(config.collections);
108
+ const projectGlobals = toArray(config.globals);
109
+
110
+ if (projectCollections.length === 0 && projectGlobals.length === 0) {
111
+ return getAdminConfig("kitchen-sink");
112
+ }
113
+
114
+ const collections: CollectionConfig[] = [];
115
+ addMissingCollections(collections, Object.values(mediaCollections));
116
+ addMissingCollections(collections, Object.values(authCollections));
117
+ addMissingCollections(collections, projectCollections);
73
118
 
74
- return { collections: collectionsMap, globals: globalsMap };
119
+ return {
120
+ collections: toCollectionMap(collections),
121
+ globals: toGlobalMap(projectGlobals),
122
+ };
75
123
  }
76
124
 
77
- export const adminConfig = getAdminConfig("minimal");
125
+ export const adminConfig = createProjectAdminConfig(projectConfig);
78
126
  export const collections = adminConfig.collections;
79
127
  export const globals = adminConfig.globals;
80
128
 
81
- export const authCollectionSlugs = ["users", "roles", "audit_logs"];
129
+ export const authCollectionSlugs = ["users", "audit_logs"];
82
130
  export const nonAuthCollections = Object.values(collections).filter(
83
131
  (c) => !authCollectionSlugs.includes(c.slug),
84
132
  );
@@ -0,0 +1,29 @@
1
+ export interface RegistryItem {
2
+ id: string;
3
+ }
4
+
5
+ export function createRegistry<T extends RegistryItem>(validate?: (item: T) => void) {
6
+ const items = new Map<string, T>();
7
+
8
+ return {
9
+ register(item: T) {
10
+ if (items.has(item.id)) {
11
+ console.warn(`Item with id "${item.id}" is already registered. Overwriting.`);
12
+ }
13
+ validate?.(item);
14
+ items.set(item.id, item);
15
+ },
16
+
17
+ unregister(id: string) {
18
+ items.delete(id);
19
+ },
20
+
21
+ get(id: string): T | undefined {
22
+ return items.get(id);
23
+ },
24
+
25
+ getAll(): T[] {
26
+ return Array.from(items.values());
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,4 @@
1
+ export default {
2
+ collections: [],
3
+ globals: [],
4
+ };
@@ -0,0 +1,50 @@
1
+ import projectConfig from "kyro:config";
2
+ import type { BaseAdapter } from "../../../src/registry/types.js";
3
+
4
+ /**
5
+ * Fetches a global document by its slug.
6
+ * Works only in environments where kyro:config is available (Astro server).
7
+ */
8
+ export async function getGlobal(slug: string, options?: { draft?: boolean }) {
9
+ const db = (projectConfig as Record<string, unknown>).adapter as BaseAdapter;
10
+ if (!db) return null;
11
+
12
+ try {
13
+ const doc = await db.findOne({
14
+ collection: `_globals_${slug}`,
15
+ where: {},
16
+ draft: options?.draft ?? false,
17
+ });
18
+
19
+ if (!doc) return null;
20
+
21
+ // Auto-resolve media IDs for common branding fields
22
+ const mediaFields = ["siteLogo", "siteFavicon", "siteOgImage"];
23
+ for (const field of mediaFields) {
24
+ if (typeof doc[field] === "string" && doc[field].length > 0) {
25
+ try {
26
+ const mediaDoc = await db.findByID({
27
+ collection: "media",
28
+ id: doc[field],
29
+ });
30
+ if (mediaDoc) {
31
+ doc[field] = mediaDoc;
32
+ }
33
+ } catch (e) {
34
+ console.warn(`Failed to resolve media for field "${field}":`, e);
35
+ }
36
+ }
37
+ }
38
+
39
+ return doc;
40
+ } catch (e) {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Convenience helper to get the site settings.
47
+ */
48
+ export async function getSiteSettings(options?: { draft?: boolean }) {
49
+ return await getGlobal("site-settings", options);
50
+ }
@@ -0,0 +1,18 @@
1
+ export type FileType = "image" | "video" | "audio" | "document" | "archive" | "code" | "unknown";
2
+
3
+ export function getFileType(mimeType: string, filename?: string): FileType {
4
+ const ext = filename?.split(".").pop()?.toLowerCase();
5
+
6
+ if (mimeType.startsWith("image/")) return "image";
7
+ if (mimeType.startsWith("video/")) return "video";
8
+ if (mimeType.startsWith("audio/")) return "audio";
9
+
10
+ if (["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "csv"].includes(ext || "")) return "document";
11
+ if (["zip", "rar", "tar", "gz", "7z"].includes(ext || "")) return "archive";
12
+ if (["js", "ts", "py", "rb", "java", "c", "cpp", "go", "rs", "css", "html", "json", "xml", "yaml", "toml", "sh"].includes(ext || "")) return "code";
13
+ if (mimeType.startsWith("text/")) return "document";
14
+
15
+ return "unknown";
16
+ }
17
+
18
+ export type MediaIconType = "image" | "video" | "audio" | "document" | "archive" | "unknown";