@kyro-cms/admin 0.3.2 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/dist/EditorClient-XEUOVAAC.js +466 -0
  2. package/dist/EditorClient-XEUOVAAC.js.map +1 -0
  3. package/dist/EditorClient-YLCGVDXY.cjs +468 -0
  4. package/dist/EditorClient-YLCGVDXY.cjs.map +1 -0
  5. package/dist/chunk-7KPIUCGT.js +384 -0
  6. package/dist/chunk-7KPIUCGT.js.map +1 -0
  7. package/dist/chunk-GOACG6R7.cjs +473 -0
  8. package/dist/chunk-GOACG6R7.cjs.map +1 -0
  9. package/dist/index.cjs +14861 -0
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/index.css +1661 -0
  12. package/dist/index.css.map +1 -0
  13. package/dist/index.d.ts +563 -0
  14. package/dist/index.js +14784 -0
  15. package/dist/index.js.map +1 -0
  16. package/package.json +19 -19
  17. package/src/components/ActionBar.tsx +7 -43
  18. package/src/components/Admin.tsx +138 -277
  19. package/src/components/ApiKeysManager.tsx +428 -419
  20. package/src/components/AuditLogsPage.tsx +35 -39
  21. package/src/components/AuthBridge.tsx +51 -0
  22. package/src/components/AutoForm.tsx +495 -1230
  23. package/src/components/BrandingHub.tsx +18 -19
  24. package/src/components/BulkActionsBar.tsx +1 -1
  25. package/src/components/CreateView.tsx +22 -36
  26. package/src/components/Dashboard.tsx +60 -84
  27. package/src/components/DetailView.tsx +113 -91
  28. package/src/components/DeveloperCenter.tsx +200 -198
  29. package/src/components/FieldRenderer.tsx +206 -0
  30. package/src/components/GraphQLPlayground.tsx +340 -480
  31. package/src/components/ListView.tsx +828 -254
  32. package/src/components/LoginPage.tsx +3 -4
  33. package/src/components/MarketplaceManager.tsx +254 -0
  34. package/src/components/MediaGallery.tsx +856 -1192
  35. package/src/components/PluginsManager.tsx +277 -0
  36. package/src/components/RestPlayground.tsx +398 -560
  37. package/src/components/SessionsManager.tsx +211 -0
  38. package/src/components/Sidebar.astro +179 -151
  39. package/src/components/ThemeProvider.tsx +7 -161
  40. package/src/components/UserManagement.tsx +162 -146
  41. package/src/components/UserMenu.tsx +110 -0
  42. package/src/components/WebhookManager.tsx +305 -367
  43. package/src/components/blocks/AccordionBlock.tsx +4 -4
  44. package/src/components/blocks/ArrayBlock.tsx +3 -3
  45. package/src/components/blocks/BlockEditModal.tsx +8 -8
  46. package/src/components/blocks/BlockWrapper.tsx +61 -0
  47. package/src/components/blocks/ButtonBlock.tsx +4 -4
  48. package/src/components/blocks/ChildBlocksTree.tsx +23 -25
  49. package/src/components/blocks/CodeBlock.tsx +15 -15
  50. package/src/components/blocks/ColumnsBlock.tsx +6 -44
  51. package/src/components/blocks/DividerBlock.tsx +3 -3
  52. package/src/components/blocks/FileBlock.tsx +4 -4
  53. package/src/components/blocks/HeadingBlock.tsx +6 -38
  54. package/src/components/blocks/HeroBlock.tsx +4 -4
  55. package/src/components/blocks/ImageBlock.tsx +4 -4
  56. package/src/components/blocks/LinkBlock.tsx +4 -4
  57. package/src/components/blocks/ListBlock.tsx +3 -3
  58. package/src/components/blocks/ParagraphBlock.tsx +12 -42
  59. package/src/components/blocks/RelationshipBlock.tsx +4 -4
  60. package/src/components/blocks/RichTextBlock.tsx +4 -4
  61. package/src/components/blocks/VStackBlock.tsx +5 -37
  62. package/src/components/blocks/VideoBlock.tsx +4 -4
  63. package/src/components/blocks/types.ts +11 -0
  64. package/src/components/fields/AccordionField.tsx +1 -1
  65. package/src/components/fields/ArrayField.tsx +2 -2
  66. package/src/components/fields/ArrayLayout.tsx +93 -0
  67. package/src/components/fields/BlocksField.tsx +122 -111
  68. package/src/components/fields/ButtonField.tsx +1 -1
  69. package/src/components/fields/CheckboxField.tsx +14 -15
  70. package/src/components/fields/ChildrenField.tsx +2 -2
  71. package/src/components/fields/CodeField.tsx +3 -3
  72. package/src/components/fields/ColumnsField.tsx +2 -2
  73. package/src/components/fields/DateField.tsx +13 -26
  74. package/src/components/fields/EditorClient.tsx +26 -28
  75. package/src/components/fields/FieldLayout.tsx +52 -0
  76. package/src/components/fields/GroupLayout.tsx +35 -0
  77. package/src/components/fields/JSONField.tsx +7 -7
  78. package/src/components/fields/LinkField.tsx +1 -1
  79. package/src/components/fields/MarkdownField.tsx +1 -1
  80. package/src/components/fields/NumberField.tsx +13 -26
  81. package/src/components/fields/PortableTextField.tsx +4 -4
  82. package/src/components/fields/PortableTextRenderer.tsx +1 -1
  83. package/src/components/fields/RelationshipBlockField.tsx +31 -23
  84. package/src/components/fields/RelationshipField.tsx +14 -14
  85. package/src/components/fields/SelectField.tsx +17 -26
  86. package/src/components/fields/TabsLayout.tsx +69 -0
  87. package/src/components/fields/TextField.tsx +85 -38
  88. package/src/components/fields/UploadField.tsx +71 -41
  89. package/src/components/fields/VideoField.tsx +1 -1
  90. package/src/components/fields/extensions/blockComponents.tsx +2 -2
  91. package/src/components/fields/extensions/blocksStore.ts +207 -193
  92. package/src/components/fields/types.ts +22 -0
  93. package/src/components/layout/Layout.tsx +1 -1
  94. package/src/components/ui/ActionMenu.tsx +63 -0
  95. package/src/components/ui/Badge.tsx +59 -5
  96. package/src/components/ui/BlockDrawer.tsx +4 -5
  97. package/src/components/ui/CommandPalette.tsx +58 -36
  98. package/src/components/ui/CommandPaletteWrapper.tsx +18 -17
  99. package/src/components/ui/Dropdown.tsx +18 -16
  100. package/src/components/ui/EmptyState.tsx +25 -0
  101. package/src/components/ui/GlobalModal.tsx +49 -0
  102. package/src/components/ui/IconButton.tsx +44 -0
  103. package/src/components/ui/Modal.tsx +19 -20
  104. package/src/components/ui/PageHeader.tsx +158 -0
  105. package/src/components/ui/Pagination.tsx +61 -0
  106. package/src/components/ui/PromptModal.tsx +1 -1
  107. package/src/components/ui/SearchInput.tsx +57 -0
  108. package/src/components/ui/SeoPreview.tsx +31 -0
  109. package/src/components/ui/SessionModal.tsx +0 -0
  110. package/src/components/ui/SlidePanel.tsx +2 -0
  111. package/src/components/ui/Toast.tsx +65 -122
  112. package/src/components/ui/Toaster.tsx +18 -0
  113. package/src/components/ui/icons.tsx +112 -0
  114. package/src/components/users/UserDetail.tsx +290 -0
  115. package/src/components/users/UserForm.tsx +242 -0
  116. package/src/components/users/UsersList.tsx +338 -0
  117. package/src/env.d.ts +13 -13
  118. package/src/fields/index.ts +2 -1
  119. package/src/global.d.ts +7 -0
  120. package/src/hooks/data.ts +2 -9
  121. package/src/hooks/useAsyncData.ts +36 -0
  122. package/src/hooks/useAutoFormState.ts +527 -0
  123. package/src/hooks/useSelection.ts +49 -0
  124. package/src/hooks/useSession.ts +0 -0
  125. package/src/index.ts +11 -1
  126. package/src/integration.ts +86 -11
  127. package/src/kyro-cms.d.ts +209 -0
  128. package/src/layouts/AdminLayout.astro +128 -11
  129. package/src/layouts/AuthLayout.astro +21 -5
  130. package/src/lib/api.ts +175 -55
  131. package/src/lib/autoform-store.ts +435 -0
  132. package/src/lib/config.ts +82 -34
  133. package/src/lib/createRegistry.ts +29 -0
  134. package/src/lib/default-kyro-config.ts +4 -0
  135. package/src/lib/globals.ts +50 -0
  136. package/src/lib/media-utils.ts +18 -0
  137. package/src/lib/object-utils.ts +77 -0
  138. package/src/lib/paths.ts +61 -0
  139. package/src/lib/stores/index.ts +370 -0
  140. package/src/lib/types.ts +43 -0
  141. package/src/lib/useResourceManager.ts +105 -0
  142. package/src/pages/403.astro +67 -0
  143. package/src/pages/[collection]/[id].astro +14 -180
  144. package/src/pages/[collection]/index.astro +11 -6
  145. package/src/pages/api-explorer.astro +173 -0
  146. package/src/pages/audit/index.astro +2 -0
  147. package/src/pages/auth/login.astro +122 -0
  148. package/src/pages/auth/register.astro +167 -0
  149. package/src/pages/graphql-explorer.astro +59 -0
  150. package/src/pages/{admin/graphql.astro → graphql.astro} +51 -17
  151. package/src/pages/index.astro +577 -0
  152. package/src/pages/index_ALT.astro +3 -0
  153. package/src/pages/keys.astro +11 -0
  154. package/src/pages/marketplace.astro +11 -0
  155. package/src/pages/media.astro +3 -0
  156. package/src/pages/plugins.astro +8 -0
  157. package/src/pages/preview/[collection]/[id].astro +188 -123
  158. package/src/pages/rest-playground.astro +62 -0
  159. package/src/pages/roles/index.astro +183 -76
  160. package/src/pages/sessions.astro +8 -0
  161. package/src/pages/settings/[slug].astro +92 -114
  162. package/src/pages/settings/index.astro +5 -3
  163. package/src/pages/users/[id].astro +25 -154
  164. package/src/pages/users/index.astro +19 -130
  165. package/src/pages/users/new.astro +9 -86
  166. package/src/pages/webhooks.astro +11 -0
  167. package/src/routes.ts +80 -0
  168. package/src/styles/main.css +119 -79
  169. package/src/theme/tokens.ts +1 -0
  170. package/src/vite-env.d.ts +14 -0
  171. package/src/collections/auth/index.ts +0 -155
  172. package/src/collections/portfolio/index.ts +0 -343
  173. package/src/components/ApiExplorer.tsx +0 -325
  174. package/src/components/EnhancedListView.tsx +0 -889
  175. package/src/components/GraphQLExplorer.tsx +0 -675
  176. package/src/components/Icons.tsx +0 -23
  177. package/src/components/StatusBadge.tsx +0 -76
  178. package/src/lib/MediaService.ts +0 -541
  179. package/src/lib/auth/sqlite-adapter.ts +0 -319
  180. package/src/lib/dataStore.ts +0 -226
  181. package/src/lib/db/adapter.ts +0 -54
  182. package/src/lib/db/drizzle-mysql-adapter.ts +0 -194
  183. package/src/lib/db/drizzle-mysql-auth-adapter.ts +0 -327
  184. package/src/lib/db/drizzle-postgres-adapter.ts +0 -202
  185. package/src/lib/db/drizzle-postgres-auth-adapter.ts +0 -304
  186. package/src/lib/db/drizzle-sqlite-adapter.ts +0 -227
  187. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +0 -548
  188. package/src/lib/db/index.ts +0 -449
  189. package/src/lib/db/mongodb-adapter.ts +0 -207
  190. package/src/lib/db/mongodb-auth-adapter.ts +0 -305
  191. package/src/lib/db/schema/mysql-auth.ts +0 -113
  192. package/src/lib/db/schema/mysql-content.ts +0 -20
  193. package/src/lib/db/schema/postgres-auth.ts +0 -116
  194. package/src/lib/db/schema/postgres-content.ts +0 -35
  195. package/src/lib/db/schema/postgres-media.ts +0 -52
  196. package/src/lib/db/schema/postgres-settings.ts +0 -11
  197. package/src/lib/db/schema/sqlite-auth.ts +0 -112
  198. package/src/lib/db/schema/sqlite-content.ts +0 -20
  199. package/src/lib/db/version-adapter.ts +0 -248
  200. package/src/lib/graphql/index.ts +0 -1
  201. package/src/lib/graphql/schema.ts +0 -443
  202. package/src/lib/rate-limit.ts +0 -267
  203. package/src/lib/storage.ts +0 -374
  204. package/src/lib/store.ts +0 -85
  205. package/src/middleware.ts +0 -177
  206. package/src/pages/admin/api-explorer.astro +0 -98
  207. package/src/pages/admin/graphql-explorer.astro +0 -40
  208. package/src/pages/admin/index.astro +0 -286
  209. package/src/pages/admin/keys.astro +0 -8
  210. package/src/pages/admin/rest-playground.astro +0 -44
  211. package/src/pages/admin/webhooks.astro +0 -8
  212. package/src/pages/api/[collection]/[id]/publish.ts +0 -52
  213. package/src/pages/api/[collection]/[id]/unpublish.ts +0 -42
  214. package/src/pages/api/[collection]/[id]/versions.ts +0 -66
  215. package/src/pages/api/[collection]/[id].ts +0 -213
  216. package/src/pages/api/[collection]/index.ts +0 -209
  217. package/src/pages/api/auth/[id].ts +0 -121
  218. package/src/pages/api/auth/audit-logs.ts +0 -57
  219. package/src/pages/api/auth/login.ts +0 -211
  220. package/src/pages/api/auth/logout.ts +0 -66
  221. package/src/pages/api/auth/me.ts +0 -36
  222. package/src/pages/api/auth/refresh.ts +0 -119
  223. package/src/pages/api/auth/register.ts +0 -188
  224. package/src/pages/api/auth/users.ts +0 -97
  225. package/src/pages/api/collections.ts +0 -59
  226. package/src/pages/api/globals/[slug].ts +0 -42
  227. package/src/pages/api/graphql.ts +0 -90
  228. package/src/pages/api/health.ts +0 -426
  229. package/src/pages/api/keys/[id].ts +0 -26
  230. package/src/pages/api/keys/index.ts +0 -75
  231. package/src/pages/api/media/[id].ts +0 -309
  232. package/src/pages/api/media/folders.ts +0 -609
  233. package/src/pages/api/media/index.ts +0 -146
  234. package/src/pages/api/media/resize.ts +0 -267
  235. package/src/pages/api/search.ts +0 -82
  236. package/src/pages/api/slug-availability.ts +0 -70
  237. package/src/pages/api/storage-config.ts +0 -20
  238. package/src/pages/api/storage-status.ts +0 -206
  239. package/src/pages/api/upload.ts +0 -334
  240. package/src/pages/api/webhooks/index.ts +0 -71
  241. package/src/pages/login.astro +0 -82
  242. package/src/pages/register.astro +0 -102
@@ -5,6 +5,8 @@ import type {
5
5
  Field,
6
6
  Block,
7
7
  } from "@kyro-cms/core/client";
8
+
9
+ type View = "edit" | "version" | "api";
8
10
  import { UploadField } from "./fields/UploadField";
9
11
  import { CodeField } from "./fields";
10
12
  import NumberField from "./fields/NumberField";
@@ -12,18 +14,31 @@ import CheckboxField from "./fields/CheckboxField";
12
14
  import SelectField from "./fields/SelectField";
13
15
  import DateField from "./fields/DateField";
14
16
  import { MarkdownField } from "./fields/MarkdownField";
17
+ import TextField from "./fields/TextField";
15
18
  import { globals, collections } from "../lib/config";
16
19
  import { slugifyText } from "../lib/slugify";
20
+ import { resolveUrl, apiDelete, fetchWithAuth } from "../lib/api";
21
+ import { useAutoFormStore } from "../lib/autoform-store";
22
+ import { useAutoFormState } from "../hooks/useAutoFormState";
23
+ import { useUIStore } from "../lib/stores";
24
+
25
+ import { adminPath as ADMIN_BASE, apiPath as API_BASE } from "../lib/paths";
17
26
 
18
27
  import { BlocksField } from "./fields/BlocksField";
19
28
  import PortableTextField from "./fields/PortableTextField";
20
29
  import { ConfirmModal, Modal as UIModal } from "./ui/Modal";
30
+ import { ListField } from "./fields/ListField";
31
+ import { RelationshipBlockField } from "./fields/RelationshipBlockField";
32
+ import { FieldRenderer } from "./FieldRenderer";
33
+ import { TabsLayout } from "./fields/TabsLayout";
34
+ import { GroupLayout } from "./fields/GroupLayout";
35
+ import { ArrayLayout } from "./fields/ArrayLayout";
21
36
 
22
37
  interface AutoFormProps {
23
38
  config: CollectionConfig | GlobalConfig;
24
- data?: Record<string, any>;
39
+ data?: Record<string, unknown>;
25
40
  errors?: Record<string, string>;
26
- onChange?: (data: Record<string, any>) => void;
41
+ onChange?: (data: Record<string, unknown>) => void;
27
42
  disabled?: boolean;
28
43
  collectionSlug?: string;
29
44
  globalSlug?: string;
@@ -31,8 +46,8 @@ interface AutoFormProps {
31
46
  layout?: "split" | "single";
32
47
  onActionSuccess?: (message: string) => void;
33
48
  onActionError?: (message: string) => void;
34
- documentStatus?: "draft" | "published" | "scheduled" | "archived";
35
49
  justSaved?: boolean;
50
+ documentStatus?: string;
36
51
  }
37
52
 
38
53
  export function AutoForm({
@@ -47,7 +62,6 @@ export function AutoForm({
47
62
  layout = "split",
48
63
  onActionSuccess,
49
64
  onActionError,
50
- documentStatus,
51
65
  justSaved,
52
66
  }: AutoFormProps) {
53
67
  // Resolve the "live" config to preserve functions (admin.condition) lost during prop serialization
@@ -58,321 +72,117 @@ export function AutoForm({
58
72
  : propConfig;
59
73
  const config = activeConfig || propConfig;
60
74
 
61
- // Helper to extract default values from config recursively
62
- function getDefaults(fields: any[], prefix = ""): Record<string, any> {
63
- const defaults: Record<string, any> = {};
64
- for (const field of fields || []) {
65
- if (field.defaultValue !== undefined) {
66
- const key = prefix + field.name;
67
- defaults[key] = field.defaultValue;
68
- // Also set nested defaults for groups
69
- if (field.type === "group" && field.fields) {
70
- for (const subField of field.fields) {
71
- if (subField.defaultValue !== undefined) {
72
- defaults[prefix + field.name + "." + subField.name] =
73
- subField.defaultValue;
74
- }
75
- }
76
- }
77
- }
78
- if (field.fields && Array.isArray(field.fields)) {
79
- Object.assign(defaults, getDefaults(field.fields, field.name + "."));
80
- }
81
- if (field.tabs) {
82
- for (const tab of field.tabs) {
83
- if (tab.fields) {
84
- Object.assign(defaults, getDefaults(tab.fields, prefix));
85
- }
86
- }
87
- }
88
- }
89
- return defaults;
90
- }
91
-
92
- // Helper to flatten nested object with dot notation keys
93
- function flattenObject(
94
- obj: Record<string, any>,
95
- prefix = "",
96
- ): Record<string, any> {
97
- const result: Record<string, any> = {};
98
- for (const key in obj) {
99
- const newKey = prefix ? `${prefix}.${key}` : key;
100
- const val = obj[key];
101
- if (
102
- val !== null &&
103
- typeof val === "object" &&
104
- !Array.isArray(val) &&
105
- // Only recurse into plain objects, not Dates, Maps, or other class instances
106
- (val.constructor === Object || !val.constructor)
107
- ) {
108
- Object.assign(result, flattenObject(val, newKey));
109
- } else {
110
- result[newKey] = val;
111
- }
112
- }
113
- return result;
114
- }
115
-
116
- // Helper to unflatten dot notation keys back to nested object
117
- function unflattenObject(flat: Record<string, any>): Record<string, any> {
118
- const result: Record<string, any> = {};
119
- for (const key in flat) {
120
- const parts = key.split(".");
121
- let current = result;
122
- for (let i = 0; i < parts.length - 1; i++) {
123
- if (!current[parts[i]]) {
124
- current[parts[i]] = {};
125
- }
126
- current = current[parts[i]];
127
- }
128
- current[parts[parts.length - 1]] = flat[key];
129
- }
130
- return result;
131
- }
132
-
133
- // Merge initial data with defaults from config
134
- const [formData, setFormData] = useState<Record<string, any>>({});
135
-
136
- useEffect(() => {
137
- try {
138
- const configDefaults = config ? getDefaults(config.fields) : {};
139
- const flatInitialData = flattenObject(initialData || {});
140
- const mergedFlatData = { ...configDefaults, ...flatInitialData };
141
- const mergedInitialData = unflattenObject(mergedFlatData);
142
- setFormData(mergedInitialData);
143
- } catch (e) {
144
- console.error("Critical error in AutoForm data initialization:", e);
145
- // Fallback to raw initialData if flattening fails
146
- setFormData(initialData || {});
147
- }
148
- }, [initialData, config]);
149
- const [activeTab, setActiveTab] = useState(0);
150
- const [isSlugLocked, setIsSlugLocked] = useState(true);
151
- const [view, setView] = useState<"edit" | "version" | "api">("edit");
152
- const [isDropdownOpen, setIsDropdownOpen] = useState(false);
153
- const [versions, setVersions] = useState<any[]>([]);
154
- const [loadingVersions, setLoadingVersions] = useState(false);
155
- const [showPreview, setShowPreview] = useState(false);
156
- const [isMenuOpen, setIsMenuOpen] = useState(false);
157
- const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
158
- const [loadingFields, setLoadingFields] = useState<Record<string, boolean>>(
159
- {},
160
- );
161
- const menuRef = useRef<HTMLDivElement>(null);
162
- const [compareMode, setCompareMode] = useState(false);
163
- const [compareSelected, setCompareSelected] = useState<string[]>([]);
164
- const [compareDiffs, setCompareDiffs] = useState<any[]>([]);
165
- const [loadingDiffs, setLoadingDiffs] = useState(false);
166
- const [confirmModal, setConfirmModal] = useState<{
167
- open: boolean;
168
- title: string;
169
- message: string;
170
- onConfirm: () => void;
171
- danger?: boolean;
172
- }>({ open: false, title: "", message: "", onConfirm: () => {} });
173
- const [alertModal, setAlertModal] = useState<{
174
- open: boolean;
175
- title: string;
176
- message: string;
177
- }>({ open: false, title: "", message: "" });
178
- const [lastSavedData, setLastSavedData] = useState<Record<string, any>>({});
179
- const [isAutoSaving, setIsAutoSaving] = useState(false);
180
- const [autoSaveStatus, setAutoSaveStatus] = useState<
181
- "idle" | "saving" | "saved" | "error"
182
- >("idle");
183
- const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(null);
184
- const lastAutoSaveTimeRef = useRef<number>(0);
185
- const autoSaveSkipRef = useRef<boolean>(false);
186
75
 
187
- const disabled = propDisabled;
76
+ const { confirm, alert } = useUIStore();
188
77
 
189
- const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
190
-
191
- useEffect(() => {
192
- const handleToggle = () => {
193
- setSidebarCollapsed((prev) => !prev);
194
- };
195
- window.addEventListener("toggle-sidebar", handleToggle);
196
- return () => window.removeEventListener("toggle-sidebar", handleToggle);
197
- }, []);
198
-
199
- // Track unsaved changes (compare against last saved state)
200
- useEffect(() => {
201
- const isDifferent =
202
- JSON.stringify(formData) !== JSON.stringify(lastSavedData);
203
- setHasUnsavedChanges(isDifferent);
204
- }, [formData, lastSavedData]);
205
-
206
- // Auto-generate slug from configured source field if locked
207
- useEffect(() => {
208
- const slugField = config.fields.find(
209
- (f: any) => f.name === "slug" && f.admin?.autoGenerate,
210
- );
211
- if (!slugField?.admin?.autoGenerate) return;
212
- const sourceField: string = slugField.admin.autoGenerate;
213
- if (isSlugLocked && formData[sourceField]) {
214
- const newSlug = slugifyText(formData[sourceField]);
215
- if (newSlug !== formData.slug) {
216
- setFormData((prev) => ({ ...prev, slug: newSlug }));
217
- }
218
- }
219
- }, [
220
- formData.title,
221
- formData.name,
222
- formData.label,
78
+ const {
79
+ formData,
80
+ lastSavedData,
81
+ hasUnsavedChanges,
82
+ isAutoSaving,
83
+ autoSaveStatus,
84
+ sidebarCollapsed,
85
+ setSidebarCollapsed,
86
+ activeTab,
87
+ setActiveTab,
223
88
  isSlugLocked,
224
- config.fields,
225
- ]);
226
-
227
- // Sync prop changes to local state
228
- useEffect(() => {
229
- if (initialData && Object.keys(initialData).length > 0) {
230
- setFormData(initialData);
231
- setLastSavedData(initialData);
232
- }
233
- }, [initialData]);
234
-
235
- // Auto-save with Strategy 3: 1s debounce, lastSavedData comparison, 15s hard throttle
236
- useEffect(() => {
237
- if (!formData.id || sidebarCollapsed) return;
238
-
239
- if (autoSaveTimerRef.current) {
240
- clearTimeout(autoSaveTimerRef.current);
241
- }
242
-
243
- const now = Date.now();
244
- const timeSinceLastSave = now - lastAutoSaveTimeRef.current;
245
- const hasChanges =
246
- JSON.stringify(formData) !== JSON.stringify(lastSavedData);
247
-
248
- if (!hasChanges) {
249
- setAutoSaveStatus("idle");
250
- return;
251
- }
252
-
253
- if (timeSinceLastSave < 15000 && lastAutoSaveTimeRef.current > 0) {
254
- const remainingTime = Math.max(1000, 15000 - timeSinceLastSave);
255
- autoSaveTimerRef.current = setTimeout(async () => {
256
- await performAutoSave();
257
- }, remainingTime);
258
- } else {
259
- autoSaveTimerRef.current = setTimeout(async () => {
260
- await performAutoSave();
261
- }, 1000);
262
- }
263
-
264
- return () => {
265
- if (autoSaveTimerRef.current) {
266
- clearTimeout(autoSaveTimerRef.current);
267
- }
268
- };
269
- }, [formData]);
89
+ setIsSlugLocked,
90
+ view,
91
+ setView,
92
+ isDropdownOpen,
93
+ setIsDropdownOpen,
94
+ versions,
95
+ loadingVersions,
96
+ showPreview,
97
+ setShowPreview,
98
+ isMenuOpen,
99
+ setIsMenuOpen,
100
+ loadingFields,
101
+ setLoadingFields,
102
+ compareMode,
103
+ setCompareMode,
104
+ compareSelected,
105
+ setCompareSelected,
106
+ compareDiffs,
107
+ setCompareDiffs,
108
+ loadingDiffs,
109
+ setLoadingDiffs,
110
+ setField,
111
+ setFormData,
112
+ markSaved,
113
+ setLastSavedData,
114
+ setAutoSaveStatus,
115
+ fetchVersions,
116
+ saveDocument,
117
+ publishDocument,
118
+ clearDraftArtifacts,
119
+ autoSaveSkipRef,
120
+ lastAutoSaveTimeRef,
121
+ documentStatus,
122
+ hasUnpublishedChanges,
123
+ versionsEnabled,
124
+ } = useAutoFormState({
125
+ config,
126
+ initialData,
127
+ collectionSlug,
128
+ globalSlug,
129
+ onChange,
130
+ onActionSuccess,
131
+ onActionError,
132
+ });
270
133
 
271
- const performAutoSave = async () => {
272
- if (autoSaveSkipRef.current) return;
273
- if (JSON.stringify(formData) === JSON.stringify(lastSavedData)) return;
274
-
275
- setIsAutoSaving(true);
276
- setAutoSaveStatus("saving");
277
-
278
- try {
279
- const { id, createdAt, updatedAt, ...rest } = formData;
280
- const saveData = {
281
- ...rest,
282
- _changeDescription: "Auto-saved",
283
- status: formData.status === "published" ? "draft" : formData.status,
284
- };
285
-
286
- const response = await fetch(`/api/${collectionSlug}/${formData.id}`, {
287
- method: "PATCH",
288
- credentials: "include",
289
- headers: { "Content-Type": "application/json" },
290
- body: JSON.stringify(saveData),
291
- });
292
-
293
- if (response.ok) {
294
- const result = await response.json();
295
- setLastSavedData(result.data || formData);
296
- lastAutoSaveTimeRef.current = Date.now();
297
- setAutoSaveStatus("saved");
298
- fetchVersions();
299
- setTimeout(() => setAutoSaveStatus("idle"), 2000);
300
- } else {
301
- setAutoSaveStatus("error");
302
- setTimeout(() => setAutoSaveStatus("idle"), 3000);
303
- }
304
- } catch (err) {
305
- console.error("Auto-save failed:", err);
306
- setAutoSaveStatus("error");
307
- setTimeout(() => setAutoSaveStatus("idle"), 3000);
308
- } finally {
309
- setIsAutoSaving(false);
310
- }
311
- };
312
-
313
- // Sync to hidden input for Astro form submission
314
- useEffect(() => {
315
- const hiddenInput = document.getElementById(
316
- "form-data",
317
- ) as HTMLInputElement;
318
- if (hiddenInput) {
319
- hiddenInput.value = JSON.stringify(formData);
320
- }
321
- onChange?.(formData);
322
- }, [formData, onChange]);
323
-
324
- useEffect(() => {
325
- if (formData.id) fetchVersions();
326
- }, [formData.id]);
134
+ const menuRef = useRef<HTMLDivElement>(null);
135
+ const disabled = propDisabled;
327
136
 
328
- const fetchVersions = async () => {
329
- setLoadingVersions(true);
330
- try {
331
- const resp = await fetch(
332
- `/api/${collectionSlug}/${formData.id}/versions`,
333
- );
334
- const data = await resp.json();
335
- setVersions(data.docs || []);
336
- } catch (e) {
337
- console.error("Failed to fetch versions:", e);
338
- } finally {
339
- setLoadingVersions(false);
340
- }
341
- };
137
+ const handleRestoreVersion = (versionId: string) => {
138
+ confirm({
139
+ title: "Restore Version",
140
+ message: "Are you sure you want to restore this version? This will overwrite your current changes.",
141
+ onConfirm: async () => {
142
+ try {
143
+ const url = globalSlug
144
+ ? resolveUrl(`/api/globals/${globalSlug}/versions/${versionId}/restore`)
145
+ : resolveUrl(`/api/${collectionSlug}/${formData.id}/versions/${versionId}/restore`);
146
+
147
+ // Try RESTful URL first
148
+ let resp = await fetchWithAuth(url, { method: "POST" });
149
+
150
+ // Fallback to legacy action-based URL for Collections if needed
151
+ if (!resp.ok && collectionSlug) {
152
+ resp = await fetchWithAuth(
153
+ resolveUrl(`/api/${collectionSlug}/${formData.id}/versions`),
154
+ {
155
+ method: "POST",
156
+ headers: { "Content-Type": "application/json" },
157
+ body: JSON.stringify({ versionId, action: "restore" }),
158
+ },
159
+ );
160
+ }
342
161
 
343
- const handleRestoreVersion = async (versionId: string) => {
344
- if (
345
- !confirm(
346
- "Are you sure you want to restore this version? This will overwrite your current changes.",
347
- )
348
- )
349
- return;
350
- try {
351
- const resp = await fetch(
352
- `/api/${collectionSlug}/${formData.id}/versions`,
353
- {
354
- method: "POST",
355
- headers: { "Content-Type": "application/json" },
356
- body: JSON.stringify({ versionId, action: "restore" }),
357
- },
358
- );
359
- const result = await resp.json();
360
- if (result.data) {
361
- setFormData(result.data);
362
- setView("edit");
363
- fetchVersions();
162
+ const result = await resp.json();
163
+ if (result.data) {
164
+ setFormData(result.data);
165
+ useAutoFormStore.getState().loadDocument(result.data, result.data);
166
+ onActionSuccess?.("Version restored successfully");
167
+ fetchVersions();
168
+ setView("edit");
169
+ } else {
170
+ alert({ title: "Error", message: result.error || "Failed to restore version" });
171
+ }
172
+ } catch (err) {
173
+ console.error("Failed to restore version:", err);
174
+ alert({ title: "Error", message: "Failed to restore version" });
175
+ }
364
176
  }
365
- } catch (e) {
366
- console.error("Restore failed:", e);
367
- }
177
+ });
368
178
  };
369
179
 
370
180
  const handleCompareVersions = async () => {
371
181
  if (compareSelected.length !== 2) return;
372
182
  setLoadingDiffs(true);
373
183
  try {
374
- const resp = await fetch(
375
- `/api/${collectionSlug}/${formData.id}/versions?compareA=${compareSelected[0]}&compareB=${compareSelected[1]}`,
184
+ const resp = await fetchWithAuth(
185
+ resolveUrl(`/api/${collectionSlug}/${formData.id}/versions?compareA=${compareSelected[0]}&compareB=${compareSelected[1]}`),
376
186
  );
377
187
  const data = await resp.json();
378
188
  setCompareDiffs(data.diffs || []);
@@ -401,7 +211,7 @@ export function AutoForm({
401
211
  // Cmd/Ctrl + S = Publish
402
212
  if ((e.metaKey || e.ctrlKey) && e.key === "s") {
403
213
  e.preventDefault();
404
- (document.getElementById("btn-save") as any)?.click();
214
+ (document.getElementById("btn-save") as HTMLButtonElement | null)?.click();
405
215
  }
406
216
  // Cmd/Ctrl + P = Toggle Preview
407
217
  if ((e.metaKey || e.ctrlKey) && e.key === "p") {
@@ -422,6 +232,13 @@ export function AutoForm({
422
232
  return () => window.removeEventListener("keydown", handleShortcuts);
423
233
  }, []);
424
234
 
235
+ // Listen for external "View History" trigger from ActionBar
236
+ useEffect(() => {
237
+ const handler = () => setView("version");
238
+ window.addEventListener("kyro:show-version-history", handler);
239
+ return () => window.removeEventListener("kyro:show-version-history", handler);
240
+ }, []);
241
+
425
242
  useEffect(() => {
426
243
  const handleClickOutside = (e: MouseEvent) => {
427
244
  if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
@@ -431,59 +248,59 @@ export function AutoForm({
431
248
  if (isMenuOpen) {
432
249
  document.addEventListener("mousedown", handleClickOutside);
433
250
  return () =>
434
- document.removeEventListener("mousedown", handleClickOutside);
251
+ document.addEventListener("mousedown", handleClickOutside);
435
252
  }
436
253
  }, [isMenuOpen]);
437
254
 
438
255
  const handleCreateNew = () => {
439
256
  if (hasUnsavedChanges) {
440
- setConfirmModal({
441
- open: true,
257
+ confirm({
442
258
  title: "Unsaved Changes",
443
259
  message: "You have unsaved changes. Save before creating new?",
444
260
  onConfirm: async () => {
445
- (document.getElementById("btn-save") as any)?.click();
261
+ (document.getElementById("btn-save") as HTMLButtonElement | null)?.click();
446
262
  await new Promise((r) => setTimeout(r, 1000));
447
- window.location.href = `/${collectionSlug}/new`;
263
+ window.location.href = `${ADMIN_BASE}/${collectionSlug}/new`;
448
264
  },
449
265
  });
450
266
  } else {
451
- window.location.href = `/${collectionSlug}/new`;
267
+ window.location.href = `${ADMIN_BASE}/${collectionSlug}/new`;
452
268
  }
453
269
  };
454
270
 
455
271
  const handleDuplicate = () => {
456
- setConfirmModal({
457
- open: true,
272
+ confirm({
458
273
  title: "Duplicate Document",
459
- message: "Create a duplicate of this document?",
274
+ message: "Are you sure you want to duplicate this document?",
460
275
  onConfirm: async () => {
461
- const { id, createdAt, updatedAt, status, ...rest } = formData;
462
- const duplicateData = {
463
- ...rest,
464
- title: `${rest.title || rest.name || "Untitled"} (Copy)`,
465
- };
466
276
  try {
467
- const response = await fetch(`/api/${collectionSlug}`, {
277
+ const { id, createdAt, updatedAt, ...duplicateData } = formData;
278
+ const response = await fetchWithAuth(`/api/${collectionSlug}`, {
468
279
  method: "POST",
469
- credentials: "include",
470
280
  headers: { "Content-Type": "application/json" },
471
- body: JSON.stringify(duplicateData),
281
+ body: JSON.stringify({
282
+ ...duplicateData,
283
+ title: `${duplicateData.title || duplicateData.name || "Copy"} (Copy)`,
284
+ slug: `${duplicateData.slug || "copy"}-${Date.now()}`,
285
+ status: "draft",
286
+ }),
472
287
  });
288
+
473
289
  if (response.ok) {
474
290
  const result = await response.json();
475
- window.location.href = `/${collectionSlug}/${result.data.id}`;
291
+ onActionSuccess?.("Document duplicated successfully");
292
+ if (result.data?.id) {
293
+ window.location.href = `${ADMIN_BASE}/${collectionSlug}/${result.data.id}`;
294
+ }
476
295
  } else {
477
296
  const error = await response.json();
478
- setAlertModal({
479
- open: true,
297
+ alert({
480
298
  title: "Error",
481
299
  message: error.error || "Failed to duplicate document",
482
300
  });
483
301
  }
484
302
  } catch (err) {
485
- setAlertModal({
486
- open: true,
303
+ alert({
487
304
  title: "Error",
488
305
  message: "Failed to duplicate document",
489
306
  });
@@ -493,61 +310,34 @@ export function AutoForm({
493
310
  };
494
311
 
495
312
  const handleDelete = () => {
496
- setConfirmModal({
497
- open: true,
313
+ confirm({
498
314
  title: "Delete Document",
499
- message: "Delete this document? This cannot be undone.",
500
- danger: true,
501
- onConfirm: () => {
502
- setConfirmModal({
503
- open: true,
504
- title: "Confirm Deletion",
505
- message: "Are you absolutely sure?",
506
- danger: true,
507
- onConfirm: async () => {
508
- try {
509
- const response = await fetch(
510
- `/api/${collectionSlug}/${formData.id}`,
511
- {
512
- method: "DELETE",
513
- credentials: "include",
514
- },
515
- );
516
- if (response.ok) {
517
- window.location.href = `/${collectionSlug}`;
518
- } else {
519
- const error = await response.json();
520
- setAlertModal({
521
- open: true,
522
- title: "Error",
523
- message: error.error || "Failed to delete document",
524
- });
525
- }
526
- } catch (err) {
527
- setAlertModal({
528
- open: true,
529
- title: "Error",
530
- message: "Failed to delete document",
531
- });
532
- }
533
- },
534
- });
315
+ message: "Delete this document? This cannot be undone. Are you absolutely sure?",
316
+ variant: "danger",
317
+ onConfirm: async () => {
318
+ try {
319
+ await apiDelete(`/api/${collectionSlug}/${formData.id}`);
320
+ window.location.href = `${ADMIN_BASE}/${collectionSlug}`;
321
+ } catch (err) {
322
+ alert({
323
+ title: "Error",
324
+ message: (err as Error).message || "Failed to delete document",
325
+ });
326
+ }
535
327
  },
536
328
  });
537
329
  };
538
330
 
539
331
  const handleUnpublish = () => {
540
- setConfirmModal({
541
- open: true,
332
+ confirm({
542
333
  title: "Unpublish Document",
543
334
  message: "Unpublish this document?",
544
335
  onConfirm: async () => {
545
336
  try {
546
- const response = await fetch(
547
- `/api/${collectionSlug}/${formData.id}/unpublish`,
337
+ const response = await fetchWithAuth(
338
+ resolveUrl(`/api/${collectionSlug}/${formData.id}/unpublish`),
548
339
  {
549
340
  method: "POST",
550
- credentials: "include",
551
341
  },
552
342
  );
553
343
  if (response.ok) {
@@ -555,15 +345,13 @@ export function AutoForm({
555
345
  location.reload();
556
346
  } else {
557
347
  const error = await response.json();
558
- setAlertModal({
559
- open: true,
348
+ alert({
560
349
  title: "Error",
561
350
  message: error.error || "Failed to unpublish",
562
351
  });
563
352
  }
564
353
  } catch (err) {
565
- setAlertModal({
566
- open: true,
354
+ alert({
567
355
  title: "Error",
568
356
  message: "Failed to unpublish",
569
357
  });
@@ -572,17 +360,14 @@ export function AutoForm({
572
360
  });
573
361
  };
574
362
 
575
- const handleFieldChange = (fieldName: string, value: any) => {
576
- setFormData((prev) => ({
577
- ...prev,
578
- [fieldName]: value,
579
- }));
363
+ const handleFieldChange = (fieldName: string, value: unknown) => {
364
+ setField(fieldName, value);
580
365
  };
581
366
 
582
367
  const renderField = (
583
368
  field: Field,
584
- parentData?: Record<string, any>,
585
- onParentChange?: (val: any) => void,
369
+ parentData?: Record<string, unknown>,
370
+ onParentChange?: (val: unknown) => void,
586
371
  ): React.ReactNode => {
587
372
  if (field.admin?.hidden) return null;
588
373
 
@@ -606,7 +391,7 @@ export function AutoForm({
606
391
  const value = currentData[field.name!];
607
392
  const error = errors[field.name!];
608
393
 
609
- const onFieldChange = (val: any) => {
394
+ const onFieldChange = (val: unknown) => {
610
395
  if (onParentChange) {
611
396
  onParentChange({ ...currentData, [field.name!]: val });
612
397
  } else {
@@ -614,47 +399,47 @@ export function AutoForm({
614
399
  }
615
400
  };
616
401
 
617
- if (field.type === "row" && "fields" in field) {
402
+ if (field.type === "row" && "fields" in field) {
403
+ const rowFields = (field as Field & { fields?: Field[] }).fields;
618
404
  return (
619
405
  <div
620
406
  key={field.name || `row-${Math.random()}`}
621
407
  className="kyro-form-row flex gap-6 items-end"
622
408
  >
623
- {(field as any).fields.map((f: Field) => {
624
- const fAdmin = f.admin;
625
- const actionUrl = fAdmin?.action;
409
+ {rowFields?.map((f: Field) => {
410
+ const fAdmin = f.admin || {};
411
+ const actionUrl = fAdmin?.action as string | undefined;
626
412
 
627
413
  if (f.type === "button" && actionUrl) {
628
- const siblingEmailField = (field as any).fields?.find(
414
+ const siblingEmailField = rowFields?.find(
629
415
  (ff: Field) => ff.type === "email",
630
416
  );
631
417
  return (
632
418
  <div key={f.name} className="flex-shrink-0">
633
419
  <button
634
420
  type="button"
635
- disabled={disabled}
636
421
  onClick={async () => {
637
- const rowName = field.name;
638
- const emailFieldName = siblingEmailField?.name;
639
- let emailValue = formData[emailFieldName];
640
- if (!emailValue && rowName) {
641
- emailValue = formData[rowName]?.[emailFieldName];
422
+ const rowName = field.name as string | undefined;
423
+ const emailFieldName = siblingEmailField?.name as string | undefined;
424
+ let emailValue = emailFieldName ? formData[emailFieldName] : undefined;
425
+ if (!emailValue && rowName && typeof rowName === "string" && emailFieldName) {
426
+ emailValue = (formData[rowName] as Record<string, unknown>)?.[emailFieldName] as string | undefined;
642
427
  }
643
428
  if (!emailValue) return;
644
429
 
645
430
  setLoadingFields((prev) => ({
646
431
  ...prev,
647
- [f.name!]: true,
432
+ [f.name as string]: true,
648
433
  }));
649
434
  try {
650
- const response = await fetch(actionUrl, {
651
- method: fAdmin.method || "POST",
435
+ const response = await fetchWithAuth(resolveUrl(actionUrl), {
436
+ method: (fAdmin.method as string) || "POST",
652
437
  headers: { "Content-Type": "application/json" },
653
438
  body: JSON.stringify({ email: emailValue }),
654
439
  });
655
- let result;
440
+ let result: { success?: boolean; message?: string; error?: string } = {};
656
441
  try {
657
- result = await response.json();
442
+ result = await response.json() as typeof result;
658
443
  } catch {
659
444
  result = {};
660
445
  }
@@ -668,22 +453,22 @@ export function AutoForm({
668
453
  `Request failed (${response.status})`;
669
454
  onActionError?.(errorMsg);
670
455
  }
671
- } catch (err: any) {
456
+ } catch (err: unknown) {
672
457
  onActionError?.(
673
- err.message || "Error connecting to server",
458
+ err instanceof Error ? err.message : "Error connecting to server",
674
459
  );
675
460
  } finally {
676
461
  setLoadingFields((prev) => ({
677
462
  ...prev,
678
- [f.name!]: false,
463
+ [f.name as string]: false,
679
464
  }));
680
465
  }
681
466
  }}
682
467
  //@ts-ignore
683
- disabled={loadingFields[f.name!] || disabled}
468
+ disabled={loadingFields[f.name as string] || disabled}
684
469
  className="bg-[var(--kyro-primary)] text-white px-4 py-2 rounded-lg font-medium hover:opacity-90 transition-opacity disabled:opacity-50"
685
470
  >
686
- {loadingFields[f.name!] ? "Sending..." : f.label || "Click"}
471
+ {loadingFields[f.name as string] ? "Sending..." : f.label || "Click"}
687
472
  </button>
688
473
  </div>
689
474
  );
@@ -694,7 +479,7 @@ export function AutoForm({
694
479
  key={f.name}
695
480
  className={f.type === "button" ? "flex-shrink-0" : "flex-1"}
696
481
  style={
697
- fAdmin?.width ? { width: fAdmin.width, flex: "none" } : {}
482
+ fAdmin?.width ? { width: fAdmin.width as string, flex: "none" } : {}
698
483
  }
699
484
  >
700
485
  {renderField(f, parentData, onParentChange)}
@@ -706,621 +491,68 @@ export function AutoForm({
706
491
  }
707
492
 
708
493
  switch (field.type) {
709
- case "tabs": {
710
- const fieldTabs = (field as any).tabs;
711
- const currentTab = fieldTabs[activeTab] || fieldTabs[0];
712
-
494
+ case "tabs":
713
495
  return (
714
- <div
496
+ <TabsLayout
715
497
  key={field.name || `tabs-${Math.random()}`}
716
- className="space-y-8"
717
- >
718
- <div className="flex items-center gap-1 border-b border-[var(--kyro-border)] mb-6">
719
- {fieldTabs.map((tab: any, index: number) => (
720
- <button
721
- key={index}
722
- type="button"
723
- className={`px-6 py-3 text-sm font-bold transition-all border-b-2 -mb-[1px] ${
724
- activeTab === index
725
- ? "border-[var(--kyro-text-primary)] text-[var(--kyro-text-primary)]"
726
- : "border-transparent text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
727
- }`}
728
- onClick={() => setActiveTab(index)}
729
- >
730
- {tab.label}
731
- </button>
732
- ))}
733
- </div>
734
- <div className="space-y-6">
735
- {currentTab?.fields.map((f: Field) =>
736
- renderField(f, parentData, onParentChange),
737
- )}
738
- </div>
739
-
740
- {currentTab?.label === "SEO" && (
741
- <div className="mt-12 pt-8 border-t border-[var(--kyro-border)]">
742
- <h4 className="text-xs font-bold text-[var(--kyro-text-secondary)] uppercase tracking-[0.2em] mb-6 opacity-50">
743
- Live Google Preview
744
- </h4>
745
- <SeoPreview
746
- title={formData.metaTitle || formData.title || "Untitled"}
747
- description={
748
- formData.metaDescription || "Please enter a description..."
749
- }
750
- slug={formData.slug || "your-slug"}
751
- />
752
- </div>
753
- )}
754
- </div>
755
- );
756
- }
757
- case "text":
758
- case "email":
759
- const textValue = currentData[field.name!];
760
- const isKeyHidden = String(textValue).startsWith("••");
761
-
762
- return (
763
- <div key={field.name} className="kyro-form-field">
764
- <label className="kyro-form-label flex items-center justify-between">
765
- <div className="flex items-center gap-2">
766
- {field.label || field.name}
767
- {field.required && (
768
- <span className="kyro-form-label-required">*</span>
769
- )}
770
- </div>
771
- {(field.admin?.autoGenerate || field.admin?.readOnly) && (
772
- <button
773
- type="button"
774
- onClick={async (e) => {
775
- e.preventDefault();
776
- e.stopPropagation();
777
-
778
- if (field.admin?.autoGenerate === "key") {
779
- const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
780
- let suffix = "";
781
- for (let i = 0; i < 32; i++) {
782
- suffix +=
783
- chars[Math.floor(Math.random() * chars.length)];
784
- }
785
- onFieldChange(`kyro_${suffix}`);
786
- } else if (field.admin?.autoGenerate) {
787
- onFieldChange(
788
- slugifyText(
789
- formData[field.admin!.autoGenerate as string] || "",
790
- ),
791
- );
792
- } else if (
793
- field.admin?.readOnly &&
794
- textValue &&
795
- !isKeyHidden
796
- ) {
797
- await navigator.clipboard.writeText(String(textValue));
798
- const actualKey = textValue;
799
- onFieldChange(actualKey + "__COPIED__");
800
- setTimeout(
801
- () => onFieldChange("••••••••••••••••••••••••••••••"),
802
- 100,
803
- );
804
- }
805
- }}
806
- className="p-1.5 rounded-lg text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)] transition-all"
807
- title={
808
- field.admin?.autoGenerate === "key"
809
- ? "Generate new key"
810
- : field.admin?.autoGenerate
811
- ? `Generate from ${field.admin.autoGenerate}`
812
- : "Copy to clipboard"
813
- }
814
- >
815
- {field.admin?.autoGenerate === "key" ? (
816
- <svg
817
- width="14"
818
- height="14"
819
- viewBox="0 0 24 24"
820
- fill="none"
821
- stroke="currentColor"
822
- strokeWidth="2"
823
- >
824
- <path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" />
825
- </svg>
826
- ) : field.admin?.autoGenerate ? (
827
- <svg
828
- width="14"
829
- height="14"
830
- viewBox="0 0 24 24"
831
- fill="none"
832
- stroke="currentColor"
833
- strokeWidth="2"
834
- >
835
- <path d="M12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" />
836
- </svg>
837
- ) : (
838
- <svg
839
- width="14"
840
- height="14"
841
- viewBox="0 0 24 24"
842
- fill="none"
843
- stroke="currentColor"
844
- strokeWidth="2"
845
- >
846
- <rect x="8" y="8" width="12" height="12" rx="2" />
847
- <path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" />
848
- </svg>
849
- )}
850
- </button>
851
- )}
852
- </label>
853
- {field.name === "slug" ? (
854
- <div className="flex items-center gap-2">
855
- <div className="relative flex-1">
856
- <input
857
- type="text"
858
- className={`kyro-form-input pr-24 ${isSlugLocked ? "opacity-70 bg-[var(--kyro-bg-secondary)]" : ""}`}
859
- value={value || ""}
860
- onChange={(e) => onFieldChange(e.target.value)}
861
- disabled={isSlugLocked || disabled}
862
- />
863
- <div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
864
- {!isSlugLocked && (
865
- <button
866
- type="button"
867
- onClick={() =>
868
- onFieldChange(
869
- slugifyText(
870
- formData[field.admin?.autoGenerate || "title"] ||
871
- "",
872
- ),
873
- )
874
- }
875
- className="p-1 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)]"
876
- >
877
- <svg
878
- width="12"
879
- height="12"
880
- viewBox="0 0 24 24"
881
- fill="none"
882
- stroke="currentColor"
883
- strokeWidth="2.5"
884
- >
885
- <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
886
- <path d="M21 3v5h-5" />
887
- </svg>
888
- </button>
889
- )}
890
- <button
891
- type="button"
892
- onClick={() => setIsSlugLocked(!isSlugLocked)}
893
- className={`p-1.5 rounded ${isSlugLocked ? "text-[var(--kyro-primary)]" : "text-[var(--kyro-text-secondary)]"}`}
894
- >
895
- {isSlugLocked ? (
896
- <svg
897
- width="12"
898
- height="12"
899
- viewBox="0 0 24 24"
900
- fill="none"
901
- stroke="currentColor"
902
- strokeWidth="2.5"
903
- >
904
- <rect
905
- x="3"
906
- y="11"
907
- width="18"
908
- height="11"
909
- rx="2"
910
- ry="2"
911
- />
912
- <path d="M7 11V7a5 5 0 0 1 10 0v4" />
913
- </svg>
914
- ) : (
915
- <svg
916
- width="12"
917
- height="12"
918
- viewBox="0 0 24 24"
919
- fill="none"
920
- stroke="currentColor"
921
- strokeWidth="2.5"
922
- >
923
- <rect
924
- x="3"
925
- y="11"
926
- width="18"
927
- height="11"
928
- rx="2"
929
- ry="2"
930
- />
931
- <path d="M7 11V7a5 5 0 0 1 9.9-1" />
932
- </svg>
933
- )}
934
- </button>
935
- </div>
936
- </div>
937
- </div>
938
- ) : (
939
- <input
940
- type={(field as any).variant === "url" ? "url" : "text"}
941
- className="kyro-form-input"
942
- value={value || ""}
943
- onChange={(e) => onFieldChange(e.target.value)}
944
- disabled={disabled}
945
- />
946
- )}
947
- {field.name?.toLowerCase().includes("metatitle") && (
948
- <div className="flex items-center justify-between mt-1 text-[10px] font-bold uppercase tracking-wider">
949
- <span
950
- className={
951
- (value?.length || 0) > 60
952
- ? "text-red-500"
953
- : (value?.length || 0) >= 40
954
- ? "text-green-500"
955
- : "text-amber-600"
956
- }
957
- >
958
- {value?.length || 0} / 60 —{" "}
959
- {(value?.length || 0) > 60
960
- ? "Too Long"
961
- : (value?.length || 0) >= 40
962
- ? "Ideal"
963
- : "Short"}
964
- </span>
965
- </div>
966
- )}
967
- {error && <p className="kyro-form-error">{error}</p>}
968
- </div>
969
- );
970
-
971
- case "textarea":
972
- return (
973
- <div key={field.name} className="kyro-form-field">
974
- <label className="kyro-form-label">
975
- {field.label || field.name}
976
- </label>
977
- <textarea
978
- className="kyro-form-input kyro-form-textarea"
979
- value={value || ""}
980
- onChange={(e) => onFieldChange(e.target.value)}
981
- disabled={disabled}
982
- rows={4}
983
- />
984
- {field.name?.toLowerCase().includes("metadescription") && (
985
- <div className="mt-1 text-[10px] font-bold uppercase tracking-wider">
986
- <span
987
- className={
988
- (value?.length || 0) > 160
989
- ? "text-red-500"
990
- : (value?.length || 0) >= 120
991
- ? "text-green-500"
992
- : "text-amber-600"
993
- }
994
- >
995
- {value?.length || 0} / 160 —{" "}
996
- {(value?.length || 0) > 160
997
- ? "Too Long"
998
- : (value?.length || 0) >= 120
999
- ? "Ideal"
1000
- : "Short"}
1001
- </span>
1002
- </div>
1003
- )}
1004
- </div>
1005
- );
1006
-
1007
- case "richtext":
1008
- return (field as any).hasBlocks === false ? (
1009
- <PortableTextField
1010
- key={field.name}
1011
- field={field as any}
1012
- value={value}
1013
- onChange={(newValue: any) => onFieldChange(newValue)}
1014
- disabled={disabled}
1015
- error={error}
1016
- />
1017
- ) : (
1018
- <BlocksField
1019
- key={field.name}
1020
- field={field as any}
1021
- value={value}
1022
- onChange={(newValue: any) => onFieldChange(newValue)}
1023
- disabled={disabled}
1024
- error={error}
498
+ field={field}
499
+ formData={formData}
500
+ onTabDataChange={(newTabData) => {
501
+ const updateTabData = useAutoFormStore.getState().updateTabData;
502
+ updateTabData(field.name as string, newTabData);
503
+ }}
504
+ renderField={renderField}
1025
505
  />
1026
506
  );
1027
507
 
1028
508
  case "group":
1029
- if ("fields" in field) {
1030
- const groupData = value || {};
1031
- return (
1032
- <div key={field.name} className="kyro-form-group">
1033
- <h3 className="kyro-form-group-title">
1034
- {field.label || field.name}
1035
- </h3>
1036
- <div className="kyro-form-group-fields">
1037
- {(field as any).fields.map((f: Field) =>
1038
- renderField(f, groupData, onFieldChange),
1039
- )}
1040
- </div>
1041
- </div>
1042
- );
1043
- }
1044
- return null;
1045
-
1046
- case "array":
1047
- if ("fields" in field) {
1048
- const items = Array.isArray(value) ? value : [];
1049
- const labelField = (field as any).fields?.[0]?.name || "user";
1050
- const isRelationship =
1051
- (field as any).fields?.[0]?.type === "relationship";
1052
- return (
1053
- <div key={field.name} className="kyro-form-field">
1054
- <label className="kyro-form-label">
1055
- {field.label || field.name}
1056
- </label>
1057
- {isRelationship ? (
1058
- <RelationshipField
1059
- field={{
1060
- name: labelField,
1061
- relationTo: (field as any).fields[0].relationTo,
1062
- hasMany: true,
1063
- label: (field as any).fields[0].label,
1064
- }}
1065
- value={items.map((i: any) => i[labelField]).filter(Boolean)}
1066
- onChange={(newValue: any) => {
1067
- const newItems = (newValue || []).map((id: string) => ({
1068
- [labelField]: id,
1069
- }));
1070
- onFieldChange(newItems);
1071
- }}
1072
- disabled={disabled}
1073
- />
1074
- ) : (
1075
- <div className="kyro-form-array">
1076
- {items.map((item: any, index: number) => (
1077
- <div key={index} className="kyro-form-array-item">
1078
- <div className="flex justify-between mb-2">
1079
- <span className="text-xs font-bold opacity-50">
1080
- Item {index + 1}
1081
- </span>
1082
- <button
1083
- type="button"
1084
- className="text-red-500"
1085
- onClick={() =>
1086
- onFieldChange(items.filter((_, i) => i !== index))
1087
- }
1088
- >
1089
- Remove
1090
- </button>
1091
- </div>
1092
- {(field as any).fields.map((f: Field) =>
1093
- renderField(f, item, (newItem) => {
1094
- const newItems = [...items];
1095
- newItems[index] = newItem;
1096
- onFieldChange(newItems);
1097
- }),
1098
- )}
1099
- </div>
1100
- ))}
1101
- <button
1102
- type="button"
1103
- className="kyro-btn kyro-btn-secondary kyro-btn-sm"
1104
- onClick={() => onFieldChange([...items, {}])}
1105
- >
1106
- Add Item
1107
- </button>
1108
- </div>
1109
- )}
1110
- </div>
1111
- );
1112
- }
1113
- return null;
1114
-
1115
- case "blocks":
1116
509
  return (
1117
- <BlocksField
510
+ <GroupLayout
1118
511
  key={field.name}
1119
- field={field as any}
1120
- value={value}
1121
- onChange={(newValue: any) => onFieldChange(newValue)}
1122
- disabled={disabled}
1123
- error={error}
512
+ field={field}
513
+ value={value as Record<string, unknown> | null}
514
+ onChange={onFieldChange}
515
+ renderField={renderField}
1124
516
  />
1125
517
  );
1126
518
 
1127
- case "number":
1128
- return (
1129
- <div key={field.name} className="kyro-form-field">
1130
- <label className="kyro-form-label">
1131
- {field.label || field.name}
1132
- {field.required && (
1133
- <span className="kyro-form-label-required">*</span>
1134
- )}
1135
- </label>
1136
- <NumberField
1137
- field={field as any}
1138
- value={value}
1139
- onChange={(newValue) => onFieldChange(newValue)}
1140
- disabled={disabled}
1141
- error={error}
1142
- />
1143
- </div>
1144
- );
1145
-
1146
- case "checkbox":
1147
- return (
1148
- <div key={field.name} className="kyro-form-field">
1149
- <CheckboxField
1150
- field={field as any}
1151
- value={value}
1152
- onChange={(newValue) => onFieldChange(newValue)}
1153
- disabled={disabled}
1154
- error={error}
1155
- />
1156
- {error && <p className="kyro-form-error">{error}</p>}
1157
- </div>
1158
- );
1159
-
1160
- case "select":
1161
- return (
1162
- <div key={field.name} className="kyro-form-field">
1163
- <label className="kyro-form-label">
1164
- {field.label || field.name}
1165
- {field.required && (
1166
- <span className="kyro-form-label-required">*</span>
1167
- )}
1168
- </label>
1169
- <SelectField
1170
- field={field as any}
1171
- value={value}
1172
- onChange={(newValue) => onFieldChange(newValue)}
1173
- disabled={disabled}
1174
- error={error}
1175
- />
1176
- {error && <p className="kyro-form-error">{error}</p>}
1177
- </div>
1178
- );
1179
-
1180
- case "date":
1181
- return (
1182
- <div key={field.name} className="kyro-form-field">
1183
- <label className="kyro-form-label">
1184
- {field.label || field.name}
1185
- {field.required && (
1186
- <span className="kyro-form-label-required">*</span>
1187
- )}
1188
- </label>
1189
- <DateField
1190
- field={field as any}
1191
- value={value}
1192
- onChange={(newValue) => onFieldChange(newValue)}
1193
- disabled={disabled}
1194
- error={error}
1195
- />
1196
- {error && <p className="kyro-form-error">{error}</p>}
1197
- </div>
1198
- );
1199
-
1200
- case "password":
1201
- return (
1202
- <div key={field.name} className="kyro-form-field">
1203
- <label className="kyro-form-label flex items-center justify-between">
1204
- <div className="flex items-center gap-2">
1205
- {field.label || field.name}
1206
- {field.required && (
1207
- <span className="kyro-form-label-required">*</span>
1208
- )}
1209
- </div>
1210
- </label>
1211
- <input
1212
- type="password"
1213
- className="kyro-form-input"
1214
- value={value || ""}
1215
- onChange={(e) => onFieldChange(e.target.value)}
1216
- disabled={disabled}
1217
- placeholder={
1218
- field.admin?.placeholder || `Enter ${field.label || field.name}`
1219
- }
1220
- />
1221
- {error && <p className="kyro-form-error">{error}</p>}
1222
- </div>
1223
- );
1224
-
1225
- case "radio":
1226
- return (
1227
- <div key={field.name} className="kyro-form-field">
1228
- <label className="kyro-form-label">
1229
- {field.label || field.name}
1230
- {field.required && (
1231
- <span className="kyro-form-label-required">*</span>
1232
- )}
1233
- </label>
1234
- <div className="kyro-form-radio-group">
1235
- {((field as any).options || []).map((opt: any) => (
1236
- <label key={opt.value} className="kyro-form-radio-label">
1237
- <input
1238
- type="radio"
1239
- name={field.name}
1240
- value={opt.value}
1241
- checked={value === opt.value}
1242
- onChange={() => onFieldChange(opt.value)}
1243
- disabled={disabled}
1244
- className="kyro-form-radio"
1245
- />
1246
- <span>{opt.label || opt.value}</span>
1247
- </label>
1248
- ))}
1249
- </div>
1250
- {error && <p className="kyro-form-error">{error}</p>}
1251
- </div>
1252
- );
1253
-
1254
- case "color":
1255
- return (
1256
- <div key={field.name} className="kyro-form-field">
1257
- <label className="kyro-form-label flex items-center gap-2">
1258
- {field.label || field.name}
1259
- {field.required && (
1260
- <span className="kyro-form-label-required">*</span>
1261
- )}
1262
- {value && (
1263
- <span
1264
- className="w-5 h-5 rounded border border-[var(--kyro-border)] shrink-0"
1265
- style={{ backgroundColor: value }}
1266
- />
1267
- )}
1268
- </label>
1269
- <div className="flex items-center gap-3">
1270
- <input
1271
- type="color"
1272
- value={value || "#000000"}
1273
- onChange={(e) => onFieldChange(e.target.value)}
1274
- disabled={disabled}
1275
- className="kyro-form-input h-10 w-14 p-1 cursor-pointer"
1276
- />
1277
- <input
1278
- type="text"
1279
- className="kyro-form-input font-mono uppercase"
1280
- value={value || ""}
1281
- onChange={(e) => onFieldChange(e.target.value)}
1282
- disabled={disabled}
1283
- placeholder="#000000"
1284
- />
1285
- </div>
1286
- {error && <p className="kyro-form-error">{error}</p>}
1287
- </div>
1288
- );
1289
-
1290
- case "markdown":
519
+ case "array":
1291
520
  return (
1292
- <MarkdownField
521
+ <ArrayLayout
1293
522
  key={field.name}
1294
- field={field as any}
1295
- value={value || ""}
1296
- onChange={(val) => onFieldChange(val)}
523
+ field={field}
524
+ value={value as unknown[]}
525
+ onChange={onFieldChange}
526
+ renderField={renderField}
1297
527
  disabled={disabled}
1298
528
  />
1299
529
  );
1300
530
 
531
+
1301
532
  case "button": {
1302
- const isLoading = loadingFields[field.name!];
533
+ const fieldName = field.name as string;
534
+ const isLoading = loadingFields[fieldName];
1303
535
  return (
1304
- <div key={field.name} className="kyro-form-field">
536
+ <div key={fieldName} className="kyro-form-field">
1305
537
  <button
1306
538
  type="button"
1307
539
  disabled={isLoading || disabled}
1308
540
  onClick={async () => {
1309
- const action = field.admin?.action || (field as any).action;
541
+ const action = (field.admin?.action || (field as Record<string, unknown>).action) as string | undefined;
1310
542
  const method =
1311
- field.admin?.method || (field as any).method || "POST";
543
+ (field.admin?.method || (field as Record<string, unknown>).method || "POST") as string;
1312
544
  if (action) {
1313
545
  setLoadingFields((prev) => ({
1314
546
  ...prev,
1315
- [field.name!]: true,
547
+ [fieldName]: true,
1316
548
  }));
1317
549
  try {
1318
- const response = await fetch(action, {
550
+ const response = await fetchWithAuth(action, {
1319
551
  method,
1320
552
  headers: { "Content-Type": "application/json" },
1321
553
  body: JSON.stringify(formData),
1322
554
  });
1323
- const result = await response.json();
555
+ await response.json();
1324
556
  if (response.ok) {
1325
557
  // handle result
1326
558
  } else {
@@ -1331,7 +563,7 @@ export function AutoForm({
1331
563
  } finally {
1332
564
  setLoadingFields((prev) => ({
1333
565
  ...prev,
1334
- [field.name!]: false,
566
+ [fieldName]: false,
1335
567
  }));
1336
568
  }
1337
569
  }
@@ -1366,59 +598,76 @@ export function AutoForm({
1366
598
  );
1367
599
  }
1368
600
 
1369
- case "relationship":
601
+ case "relationship-block":
1370
602
  return (
1371
- <RelationshipField
1372
- key={field.name}
1373
- field={field as any}
1374
- value={value}
1375
- onChange={(newValue) => onFieldChange(newValue)}
1376
- disabled={disabled}
1377
- error={error}
1378
- />
1379
- );
1380
-
1381
- case "code":
1382
- return (
1383
- <CodeField
1384
- key={field.name}
1385
- field={field as any}
1386
- value={value || ""}
1387
- onChange={(newValue) => onFieldChange(newValue)}
1388
- disabled={disabled}
1389
- error={error}
1390
- />
603
+ <div key={field.name} className="kyro-form-field">
604
+ <label className="kyro-form-label">
605
+ {field.label || field.name}
606
+ {field.required && (
607
+ <span className="kyro-form-label-required">*</span>
608
+ )}
609
+ </label>
610
+ <RelationshipBlockField
611
+ relationTo={field.relationTo as string}
612
+ hasMany={field.hasMany as boolean}
613
+ selectedIds={Array.isArray(value) ? value : value ? [value] : []}
614
+ onChange={(_field: string, newValue: unknown) => {
615
+ onFieldChange(newValue);
616
+ }}
617
+ compact
618
+ />
619
+ {field.admin?.description ? (
620
+ <p className="kyro-form-help">{String(field.admin?.description)}</p>
621
+ ) : null}
622
+ </div>
1391
623
  );
1392
624
 
1393
- // @ts-ignore - 'image' is supported but not in the standard union yet
1394
- case "image":
1395
- case "upload":
625
+ default:
1396
626
  return (
1397
- <UploadField
1398
- key={field.name}
1399
- field={field as any}
627
+ <FieldRenderer
628
+ key={field.name || Math.random().toString()}
629
+ field={field}
1400
630
  value={value}
1401
- onChange={(newValue) => onFieldChange(newValue)}
631
+ onChange={onFieldChange}
632
+ error={error}
1402
633
  disabled={disabled}
1403
634
  />
1404
635
  );
1405
-
1406
- default:
1407
- return null;
1408
636
  }
1409
637
  };
1410
638
 
1411
639
  const renderHeader = () => {
1412
- const docTitle = formData.title || formData.name || "Untitled";
1413
- const status = formData.status || "draft";
640
+ const docTitle = String(
641
+ (formData.mainTabs as { title?: string })?.title ||
642
+ formData.title ||
643
+ formData.name ||
644
+ "Untitled",
645
+ );
646
+ // Use _status from the document (set by the new draft/publish system)
647
+ const docStatus = documentStatus ?? formData._status ?? formData.status ?? 'draft';
1414
648
  const isNew = !formData.id;
1415
649
  const lastModified = formData.updatedAt
1416
- ? new Date(formData.updatedAt).toLocaleString()
650
+ ? new Date(formData.updatedAt as string).toLocaleString()
1417
651
  : "Just now";
1418
652
  const createdAt = formData.createdAt
1419
- ? new Date(formData.createdAt).toLocaleString()
653
+ ? new Date(formData.createdAt as string).toLocaleString()
1420
654
  : "Just now";
1421
655
 
656
+ // Status label shown in the header
657
+ const statusLabel = hasUnpublishedChanges
658
+ ? docStatus === 'draft' && !formData._prevStatus
659
+ ? 'Draft'
660
+ : 'Published (unpublished changes)'
661
+ : docStatus === 'published'
662
+ ? 'Published'
663
+ : 'Draft';
664
+
665
+ const statusColor = docStatus === 'published' && !hasUnpublishedChanges
666
+ ? 'bg-[var(--kyro-success)]'
667
+ : hasUnpublishedChanges
668
+ ? 'bg-[var(--kyro-warning)]'
669
+ : 'bg-[var(--kyro-text-muted)]';
670
+
1422
671
  return (
1423
672
  <header className="surface-tile px-8 py-6 flex items-center justify-between sticky top-0 z-50 border-b border-[var(--kyro-border)] mb-8 bg-[var(--kyro-surface)] backdrop-blur-md">
1424
673
  <div className="flex flex-col gap-1">
@@ -1446,9 +695,9 @@ export function AutoForm({
1446
695
  <div className="flex items-center gap-4 text-[11px] font-medium tracking-wide opacity-60 ml-12">
1447
696
  <span className="flex items-center gap-1.5 capitalize">
1448
697
  <span
1449
- className={`h-1.5 w-1.5 rounded-full ${status === "published" && !hasUnsavedChanges ? "bg-[var(--kyro-success)]" : "bg-[var(--kyro-warning)]"}`}
698
+ className={`h-1.5 w-1.5 rounded-full ${statusColor}`}
1450
699
  />
1451
- {hasUnsavedChanges ? "Draft" : status}
700
+ {statusLabel}
1452
701
  </span>
1453
702
  {autoSaveStatus === "saving" && (
1454
703
  <span className="flex items-center gap-1.5 text-[var(--kyro-text-muted)]">
@@ -1471,10 +720,10 @@ export function AutoForm({
1471
720
  d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
1472
721
  />
1473
722
  </svg>
1474
- Saving...
723
+ Saving draft...
1475
724
  </span>
1476
725
  )}
1477
- {autoSaveStatus === "saved" && (
726
+ {autoSaveStatus === "success" && (
1478
727
  <span className="text-[var(--kyro-success)] flex items-center gap-1">
1479
728
  <svg
1480
729
  width="12"
@@ -1486,18 +735,25 @@ export function AutoForm({
1486
735
  >
1487
736
  <path d="M20 6L9 17l-5-5" />
1488
737
  </svg>
1489
- Saved
738
+ Draft saved
1490
739
  </span>
1491
740
  )}
1492
741
  {autoSaveStatus === "error" && (
1493
- <span className="text-[var(--kyro-danger)]">Save failed</span>
742
+ <span className="text-[var(--kyro-danger)]">Draft save failed</span>
743
+ )}
744
+ {autoSaveStatus === "conflict" && (
745
+ <span className="text-[var(--kyro-danger)]">Conflict detected</span>
1494
746
  )}
1495
747
  {hasUnsavedChanges && autoSaveStatus !== "saving" && (
1496
748
  <>
1497
749
  <span className="opacity-30">—</span>
1498
750
  <button
1499
751
  type="button"
1500
- onClick={() => setFormData(lastSavedData)}
752
+ onClick={async () => {
753
+ setFormData(lastSavedData);
754
+ markSaved();
755
+ await clearDraftArtifacts();
756
+ }}
1501
757
  className="text-[var(--kyro-primary)] hover:underline"
1502
758
  >
1503
759
  Revert changes
@@ -1519,8 +775,8 @@ export function AutoForm({
1519
775
  <button
1520
776
  key={v}
1521
777
  type="button"
1522
- onClick={() => setView(v as any)}
1523
- className={`px-5 py-2 text-xs font-black rounded-lg transition-all ${view === v ? "bg-[var(--kyro-surface)] shadow-sm border border-[var(--kyro-border)] text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
778
+ onClick={() => setView(v as View)}
779
+ className={`px-5 py-2 text-xs font-bold rounded-lg transition-all ${view === v ? "bg-[var(--kyro-surface)] shadow-sm border border-[var(--kyro-border)] text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
1524
780
  >
1525
781
  {v.toUpperCase()}
1526
782
  </button>
@@ -1547,7 +803,7 @@ export function AutoForm({
1547
803
  <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3" />
1548
804
  </svg>
1549
805
  {showPreview && (
1550
- <span className="text-[10px] font-black uppercase tracking-widest pr-1">
806
+ <span className="text-[10px] font-bold tracking-widest pr-1">
1551
807
  Active
1552
808
  </span>
1553
809
  )}
@@ -1577,6 +833,7 @@ export function AutoForm({
1577
833
  id="btn-save"
1578
834
  type="button"
1579
835
  onClick={async () => {
836
+ autoSaveSkipRef.current = true;
1580
837
  const hiddenInput = document.getElementById(
1581
838
  "form-data",
1582
839
  ) as HTMLInputElement;
@@ -1593,48 +850,55 @@ export function AutoForm({
1593
850
 
1594
851
  try {
1595
852
  const data = JSON.parse(hiddenInput.value);
1596
- const url = isNew
1597
- ? `/api/${collectionSlug}`
1598
- : `/api/${collectionSlug}/${formData.id}`;
1599
- const method = isNew ? "POST" : "PATCH";
1600
-
1601
- const response = await fetch(url, {
1602
- method,
1603
- credentials: "include",
1604
- headers: { "Content-Type": "application/json" },
1605
- body: JSON.stringify(data),
1606
- });
853
+ const isPost = isNew && !globalSlug;
854
+
855
+ const response = isPost
856
+ ? await fetchWithAuth(`/api/${collectionSlug}`, {
857
+ method: "POST",
858
+ headers: { "Content-Type": "application/json" },
859
+ body: JSON.stringify(data),
860
+ })
861
+ : await saveDocument(data);
1607
862
 
1608
863
  if (response.ok) {
1609
864
  const result = await response.json();
1610
- setLastSavedData(result.data || formData);
865
+ setFormData(result.data || data);
866
+ setLastSavedData(result.data || data);
1611
867
  lastAutoSaveTimeRef.current = Date.now();
1612
- setAutoSaveStatus("saved");
1613
- fetchVersions();
868
+ setAutoSaveStatus("success");
869
+ await clearDraftArtifacts();
870
+ if (versionsEnabled) fetchVersions();
1614
871
  setTimeout(() => setAutoSaveStatus("idle"), 2000);
1615
872
  onActionSuccess?.(
1616
- isNew ? "Document created successfully" : "Changes saved",
873
+ isPost ? "Document created successfully" : "Changes saved",
1617
874
  );
1618
- if (isNew) {
875
+ if (globalSlug) {
876
+ setTimeout(() => {
877
+ window.location.reload();
878
+ }, 1000);
879
+ }
880
+ if (isPost) {
1619
881
  setTimeout(() => {
1620
882
  window.location.href = `/${collectionSlug}`;
1621
883
  }, 800);
1622
884
  }
1623
885
  } else {
1624
886
  const error = await response.json();
1625
- setAlertModal({
1626
- open: true,
1627
- title: "Error",
887
+ if (response.status === 409) {
888
+ setAutoSaveStatus("conflict");
889
+ }
890
+ alert({
891
+ title: response.status === 409 ? "Conflict detected" : "Error",
1628
892
  message: error.error || "Failed to save",
1629
893
  });
1630
894
  }
1631
895
  } catch (err) {
1632
- setAlertModal({
1633
- open: true,
896
+ alert({
1634
897
  title: "Error",
1635
898
  message: "Failed to save document",
1636
899
  });
1637
900
  } finally {
901
+ autoSaveSkipRef.current = false;
1638
902
  if (btn) {
1639
903
  btn.textContent = originalText;
1640
904
  btn.removeAttribute("disabled");
@@ -1643,14 +907,15 @@ export function AutoForm({
1643
907
  }}
1644
908
  className="kyro-btn kyro-btn-primary px-6 py-2.5 text-xs rounded-xl shadow-lg transition-all"
1645
909
  >
1646
- {isNew ? "Create" : hasUnsavedChanges ? "Save Draft" : "Saved"}
910
+ {isNew ? (globalSlug ? "Save" : "Create") : hasUnsavedChanges ? (versionsEnabled ? "Save Draft" : "Save") : "Saved"}
1647
911
  </button>
1648
912
 
1649
- {!isNew && status === "draft" && (
913
+ {!isNew && versionsEnabled && documentStatus === "draft" && (
1650
914
  <button
1651
915
  id="btn-publish"
1652
916
  type="button"
1653
917
  onClick={async () => {
918
+ autoSaveSkipRef.current = true;
1654
919
  const btn = document.getElementById(
1655
920
  "btn-publish",
1656
921
  ) as HTMLButtonElement;
@@ -1661,32 +926,51 @@ export function AutoForm({
1661
926
  }
1662
927
 
1663
928
  try {
1664
- const response = await fetch(
1665
- `/api/${collectionSlug}/${formData.id}/publish`,
1666
- {
1667
- method: "POST",
1668
- credentials: "include",
1669
- },
1670
- );
929
+ if (hasUnsavedChanges) {
930
+ const saveResponse = await saveDocument(formData);
931
+ if (!saveResponse.ok) {
932
+ const saveError = await saveResponse.json().catch(() => ({}));
933
+ if (saveResponse.status === 409) {
934
+ setAutoSaveStatus("conflict");
935
+ }
936
+ alert({
937
+ title:
938
+ saveResponse.status === 409
939
+ ? "Conflict detected"
940
+ : "Error",
941
+ message: saveError.error || "Failed to save latest draft before publishing",
942
+ });
943
+ return;
944
+ }
945
+
946
+ const saveResult = await saveResponse.json();
947
+ setFormData(saveResult.data || formData);
948
+ setLastSavedData(saveResult.data || formData);
949
+ }
950
+
951
+ const response = await publishDocument();
1671
952
 
1672
953
  if (response.ok) {
954
+ await clearDraftArtifacts();
1673
955
  onActionSuccess?.("Published successfully");
1674
956
  location.reload();
1675
957
  } else {
1676
958
  const error = await response.json();
1677
- setAlertModal({
1678
- open: true,
1679
- title: "Error",
959
+ if (response.status === 409) {
960
+ setAutoSaveStatus("conflict");
961
+ }
962
+ alert({
963
+ title: response.status === 409 ? "Conflict detected" : "Error",
1680
964
  message: error.error || "Failed to publish",
1681
965
  });
1682
966
  }
1683
967
  } catch (err) {
1684
- setAlertModal({
1685
- open: true,
968
+ alert({
1686
969
  title: "Error",
1687
970
  message: "Failed to publish",
1688
971
  });
1689
972
  } finally {
973
+ autoSaveSkipRef.current = false;
1690
974
  if (btn) {
1691
975
  btn.textContent = originalText;
1692
976
  btn.removeAttribute("disabled");
@@ -1695,7 +979,7 @@ export function AutoForm({
1695
979
  }}
1696
980
  className="px-6 py-2.5 text-xs font-bold rounded-xl border-2 border-[var(--kyro-border)] text-[var(--kyro-text-primary)] hover:border-[var(--kyro-primary)] hover:bg-[var(--kyro-primary)] hover:text-white transition-all"
1697
981
  >
1698
- Publish
982
+ {formData._prevStatus === 'published' ? 'Publish Changes' : 'Publish'}
1699
983
  </button>
1700
984
  )}
1701
985
 
@@ -1771,7 +1055,7 @@ export function AutoForm({
1771
1055
  </svg>
1772
1056
  Duplicate
1773
1057
  </button>
1774
- {status === "published" && (
1058
+ {formData._status === "published" && (
1775
1059
  <button
1776
1060
  type="button"
1777
1061
  onClick={() => {
@@ -1833,7 +1117,7 @@ export function AutoForm({
1833
1117
  return (
1834
1118
  <div className="w-full space-y-8">
1835
1119
  <div className="surface-tile p-8 space-y-8">
1836
- {config.fields.map((f) => renderField(f))}
1120
+ {config.fields.map((f: Field) => renderField(f))}
1837
1121
  </div>
1838
1122
  </div>
1839
1123
  );
@@ -1842,29 +1126,28 @@ export function AutoForm({
1842
1126
  // Default split layout
1843
1127
  const showRightColumn = !sidebarCollapsed && !showPreview;
1844
1128
  const hasSidebarFields =
1845
- config.fields.some((f) => f.admin?.position === "sidebar") &&
1129
+ config.fields.some((f: Field) => f.admin?.position === "sidebar") &&
1846
1130
  !showPreview;
1847
1131
 
1848
1132
  return (
1849
1133
  <div
1850
- className={`w-full mx-auto grid gap-8 pb-32 transition-all duration-700 ${
1851
- showPreview
1852
- ? "grid-cols-1 lg:grid-cols-2"
1853
- : sidebarCollapsed || !hasSidebarFields
1854
- ? "grid-cols-1"
1855
- : "grid-cols-1 lg:grid-cols-[1fr_380px]"
1856
- }`}
1134
+ className={`w-full mx-auto grid gap-8 pb-32 transition-all duration-700 ${showPreview
1135
+ ? "grid-cols-1 lg:grid-cols-2"
1136
+ : sidebarCollapsed || !hasSidebarFields
1137
+ ? "grid-cols-1"
1138
+ : "grid-cols-1 lg:grid-cols-[1fr_380px]"
1139
+ }`}
1857
1140
  >
1858
1141
  <div className="space-y-8 animate-in fade-in slide-in-from-left-4 duration-500">
1859
1142
  {config.tabs ? (
1860
- renderField({ type: "tabs", tabs: config.tabs } as any)
1143
+ renderField({ type: "tabs", tabs: config.tabs } as Field)
1861
1144
  ) : (
1862
1145
  <div className="surface-tile p-8 space-y-8">
1863
1146
  {config.fields
1864
1147
  .filter(
1865
- (f) => !f.admin?.position || f.admin.position === "main",
1148
+ (f: Field) => !f.admin?.position || f.admin.position === "main",
1866
1149
  )
1867
- .map((f) => renderField(f))}
1150
+ .map((f: Field) => renderField(f))}
1868
1151
  </div>
1869
1152
  )}
1870
1153
  </div>
@@ -1874,7 +1157,7 @@ export function AutoForm({
1874
1157
  <div className="w-full h-full rounded-3xl border border-[var(--kyro-border)] bg-[var(--kyro-bg-secondary)] shadow-2xl overflow-hidden relative group">
1875
1158
  <div className="absolute top-4 left-4 z-10 flex items-center gap-2">
1876
1159
  <div className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
1877
- <span className="text-[10px] font-black uppercase tracking-widest text-white/60">
1160
+ <span className="text-[10px] font-bold tracking-widest text-white/60">
1878
1161
  Live Preview Mode
1879
1162
  </span>
1880
1163
  </div>
@@ -1888,14 +1171,14 @@ export function AutoForm({
1888
1171
  </div>
1889
1172
  ) : sidebarCollapsed ? null : (
1890
1173
  <div className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-500">
1891
- {config.fields.some((f) => f.admin?.position === "sidebar") && (
1174
+ {config.fields.some((f: Field) => f.admin?.position === "sidebar") && (
1892
1175
  <div className="surface-tile p-6 space-y-6">
1893
- <h3 className="text-[10px] font-black uppercase tracking-[0.2em] opacity-40">
1176
+ <h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40">
1894
1177
  Settings
1895
1178
  </h3>
1896
1179
  {config.fields
1897
- .filter((f) => f.admin?.position === "sidebar")
1898
- .map((f) => renderField(f))}
1180
+ .filter((f: Field) => f.admin?.position === "sidebar")
1181
+ .map((f: Field) => renderField(f))}
1899
1182
  </div>
1900
1183
  )}
1901
1184
  </div>
@@ -1924,7 +1207,7 @@ export function AutoForm({
1924
1207
  type="button"
1925
1208
  onClick={handleCompareVersions}
1926
1209
  disabled={loadingDiffs}
1927
- className="px-3 py-1.5 rounded-lg bg-[var(--kyro-primary)] text-white text-[11px] font-bold uppercase tracking-wider hover:opacity-90 disabled:opacity-50"
1210
+ className="px-3 py-1.5 rounded-lg bg-[var(--kyro-primary)] text-white text-[11px] font-bold tracking-wider hover:opacity-90 disabled:opacity-50"
1928
1211
  >
1929
1212
  {loadingDiffs ? "Comparing..." : "Compare"}
1930
1213
  </button>
@@ -1936,11 +1219,10 @@ export function AutoForm({
1936
1219
  setCompareSelected([]);
1937
1220
  setCompareDiffs([]);
1938
1221
  }}
1939
- className={`px-3 py-1.5 rounded-lg text-[11px] font-bold uppercase tracking-wider transition-all ${
1940
- compareMode
1941
- ? "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
1942
- : "border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
1943
- }`}
1222
+ className={`px-3 py-1.5 rounded-lg text-[11px] font-bold tracking-wider transition-all ${compareMode
1223
+ ? "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
1224
+ : "border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
1225
+ }`}
1944
1226
  >
1945
1227
  {compareMode ? "Done" : "Compare"}
1946
1228
  </button>
@@ -1950,7 +1232,7 @@ export function AutoForm({
1950
1232
  {compareDiffs.length > 0 && (
1951
1233
  <div className="border-b border-[var(--kyro-border)]">
1952
1234
  <div className="px-6 py-3 flex items-center justify-between">
1953
- <span className="text-[11px] font-bold text-[var(--kyro-text-primary)] uppercase tracking-wider">
1235
+ <span className="text-[11px] font-bold text-[var(--kyro-text-primary)] tracking-wider">
1954
1236
  {compareDiffs.length} change
1955
1237
  {compareDiffs.length !== 1 ? "s" : ""}
1956
1238
  </span>
@@ -2018,22 +1300,20 @@ export function AutoForm({
2018
1300
  onClick={
2019
1301
  compareMode ? () => toggleCompareSelection(v.id) : undefined
2020
1302
  }
2021
- className={`grid grid-cols-12 gap-3 px-6 py-3 items-center transition-all ${
2022
- compareMode
2023
- ? isSelected
2024
- ? "bg-[var(--kyro-primary)]/5 cursor-pointer"
2025
- : "hover:bg-[var(--kyro-bg-secondary)] cursor-pointer"
2026
- : "hover:bg-[var(--kyro-bg-secondary)]"
2027
- } ${isDraftVersion ? "" : ""}`}
1303
+ className={`grid grid-cols-12 gap-3 px-6 py-3 items-center transition-all ${compareMode
1304
+ ? isSelected
1305
+ ? "bg-[var(--kyro-primary)]/5 cursor-pointer"
1306
+ : "hover:bg-[var(--kyro-bg-secondary)] cursor-pointer"
1307
+ : "hover:bg-[var(--kyro-bg-secondary)]"
1308
+ } ${isDraftVersion ? "" : ""}`}
2028
1309
  >
2029
1310
  <div className="col-span-1 flex items-center gap-2">
2030
1311
  {compareMode ? (
2031
1312
  <div
2032
- className={`w-4 h-4 rounded-full border ${
2033
- isSelected
2034
- ? "border-[var(--kyro-primary)] bg-[var(--kyro-primary)]"
2035
- : "border-[var(--kyro-border)]"
2036
- }`}
1313
+ className={`w-4 h-4 rounded-full border ${isSelected
1314
+ ? "border-[var(--kyro-primary)] bg-[var(--kyro-primary)]"
1315
+ : "border-[var(--kyro-border)]"
1316
+ }`}
2037
1317
  >
2038
1318
  {isSelected && (
2039
1319
  <svg
@@ -2057,13 +1337,13 @@ export function AutoForm({
2057
1337
  <div className="text-[13px] font-medium text-[var(--kyro-text-primary)] truncate flex items-center gap-2">
2058
1338
  {v.changeDescription || "Snapshot"}
2059
1339
  {isAutoSaved && (
2060
- <span className="text-[9px] px-1.5 py-0.5 bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] rounded font-bold uppercase tracking-wider">
1340
+ <span className="text-[9px] px-1.5 py-0.5 bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] rounded font-bold tracking-wider">
2061
1341
  Auto
2062
1342
  </span>
2063
1343
  )}
2064
1344
  </div>
2065
1345
  <div className="text-[11px] text-[var(--kyro-text-muted)]">
2066
- {new Date(v.createdAt).toLocaleString("en-US", {
1346
+ {new Date(v.createdAt as string).toLocaleString("en-US", {
2067
1347
  month: "short",
2068
1348
  day: "numeric",
2069
1349
  hour: "2-digit",
@@ -2074,11 +1354,10 @@ export function AutoForm({
2074
1354
  <div className="col-span-3">
2075
1355
  {v.status && (
2076
1356
  <span
2077
- className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-bold capitalize tracking-wider ${
2078
- v.status === "published"
2079
- ? " text-[var(--kyro-success)]"
2080
- : " text-[var(--kyro-warning)]"
2081
- }`}
1357
+ className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-bold capitalize tracking-wider ${v.status === "published"
1358
+ ? " text-[var(--kyro-success)]"
1359
+ : " text-[var(--kyro-warning)]"
1360
+ }`}
2082
1361
  >
2083
1362
  <span
2084
1363
  className={`w-1.5 h-1.5 rounded-full ${v.status === "published" ? "bg-[var(--kyro-success)]" : "bg-[var(--kyro-warning)]"}`}
@@ -2095,7 +1374,7 @@ export function AutoForm({
2095
1374
  <button
2096
1375
  type="button"
2097
1376
  onClick={() => handleRestoreVersion(v.id)}
2098
- className="px-3 py-1.5 rounded-lg border border-[var(--kyro-border)] text-[11px] font-bold uppercase tracking-wider text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-primary)] hover:text-white hover:border-[var(--kyro-primary)] transition-all active:scale-95"
1377
+ className="px-3 py-1.5 rounded-lg border border-[var(--kyro-border)] text-[11px] font-bold tracking-wider text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-primary)] hover:text-white hover:border-[var(--kyro-primary)] transition-all active:scale-95"
2099
1378
  >
2100
1379
  Restore
2101
1380
  </button>
@@ -2114,7 +1393,7 @@ export function AutoForm({
2114
1393
  <div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4">
2115
1394
  <div className="grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-8">
2116
1395
  <div className="surface-tile p-8 min-w-0">
2117
- <h2 className="text-xl font-black mb-6">Response Payload</h2>
1396
+ <h2 className="text-xl font-bold mb-6">Response Payload</h2>
2118
1397
  <div className="bg-[#0f172a] p-6 rounded-2xl border border-white/5 overflow-x-auto max-h-[800px]">
2119
1398
  <pre className="text-blue-300 text-xs font-mono whitespace-pre-wrap break-all">
2120
1399
  {JSON.stringify(formData, null, 2)}
@@ -2124,11 +1403,11 @@ export function AutoForm({
2124
1403
 
2125
1404
  <div className="space-y-6">
2126
1405
  <div className="surface-tile p-8 space-y-6">
2127
- <h2 className="text-xl font-black mb-6">API Info</h2>
1406
+ <h2 className="text-xl font-bold mb-6">API Info</h2>
2128
1407
 
2129
1408
  <div className="space-y-6">
2130
1409
  <div>
2131
- <label className="text-[10px] font-bold uppercase tracking-[0.1em] text-[var(--kyro-text-secondary)] opacity-50 block mb-2">
1410
+ <label className="text-[10px] font-bold tracking-[0.1em] text-[var(--kyro-text-secondary)] opacity-50 block mb-2">
2132
1411
  Reference Path
2133
1412
  </label>
2134
1413
  <div className="relative group">
@@ -2139,24 +1418,24 @@ export function AutoForm({
2139
1418
  </div>
2140
1419
 
2141
1420
  <div>
2142
- <label className="text-[10px] font-bold uppercase tracking-[0.1em] text-[var(--kyro-text-secondary)] opacity-50 block mb-3">
1421
+ <label className="text-[10px] font-bold tracking-[0.1em] text-[var(--kyro-text-secondary)] opacity-50 block mb-3">
2143
1422
  Methods Allowed
2144
1423
  </label>
2145
1424
  <div className="flex gap-2">
2146
- <span className="px-3 py-1.5 bg-green-500/10 text-green-500 rounded-lg font-black text-[9px] uppercase tracking-wider">
1425
+ <span className="px-3 py-1.5 bg-green-500/10 text-green-500 rounded-lg font-bold text-[9px] tracking-wider">
2147
1426
  GET
2148
1427
  </span>
2149
- <span className="px-3 py-1.5 bg-amber-500/10 text-amber-500 rounded-lg font-black text-[9px] uppercase tracking-wider">
1428
+ <span className="px-3 py-1.5 bg-amber-500/10 text-amber-500 rounded-lg font-bold text-[9px] tracking-wider">
2150
1429
  PATCH
2151
1430
  </span>
2152
- <span className="px-3 py-1.5 bg-red-500/10 text-red-500 rounded-lg font-black text-[9px] uppercase tracking-wider">
1431
+ <span className="px-3 py-1.5 bg-red-500/10 text-red-500 rounded-lg font-bold text-[9px] tracking-wider">
2153
1432
  DELETE
2154
1433
  </span>
2155
1434
  </div>
2156
1435
  </div>
2157
1436
 
2158
1437
  <div className="pt-6 border-t border-[var(--kyro-border)]">
2159
- <label className="text-[10px] font-bold uppercase tracking-[0.1em] text-[var(--kyro-text-secondary)] opacity-50 block mb-4">
1438
+ <label className="text-[10px] font-bold tracking-[0.1em] text-[var(--kyro-text-secondary)] opacity-50 block mb-4">
2160
1439
  Security Policy
2161
1440
  </label>
2162
1441
  <div className="space-y-3">
@@ -2206,7 +1485,7 @@ export function AutoForm({
2206
1485
  </div>
2207
1486
 
2208
1487
  <div className="pt-6 border-t border-[var(--kyro-border)]">
2209
- <label className="text-[10px] font-bold uppercase tracking-[0.1em] text-[var(--kyro-text-secondary)] opacity-50 block mb-2">
1488
+ <label className="text-[10px] font-bold tracking-[0.1em] text-[var(--kyro-text-secondary)] opacity-50 block mb-2">
2210
1489
  Usage Help
2211
1490
  </label>
2212
1491
  <p className="text-[11px] text-[var(--kyro-text-secondary)] leading-relaxed">
@@ -2227,49 +1506,57 @@ export function AutoForm({
2227
1506
  return (
2228
1507
  <div className="flex flex-col h-full">
2229
1508
  {layout !== "single" && renderHeader()}
1509
+ {layout === "single" && (
1510
+ <button
1511
+ id="btn-save"
1512
+ type="button"
1513
+ style={{ width: 0, height: 0, opacity: 0, padding: 0, margin: 0, border: 'none', position: 'absolute' }}
1514
+ onClick={async () => {
1515
+ console.log("[AutoForm] Hidden save button clicked");
1516
+ const hiddenInput = document.getElementById("form-data") as HTMLInputElement;
1517
+ if (!hiddenInput || !hiddenInput.value) {
1518
+ console.error("[AutoForm] #form-data input not found or empty");
1519
+ return;
1520
+ }
1521
+ try {
1522
+ const data = JSON.parse(hiddenInput.value);
1523
+ console.log("[AutoForm] Saving data:", data);
1524
+ const response = await saveDocument(data);
1525
+ if (response.ok) {
1526
+ const result = await response.json();
1527
+ const savedData = result.data || data;
1528
+ setFormData(savedData);
1529
+ setLastSavedData(savedData);
1530
+ onActionSuccess?.("Changes saved");
1531
+ // Trigger a refresh to ensure all global state is updated
1532
+ setTimeout(() => {
1533
+ window.location.reload();
1534
+ }, 1000); // Small delay to let the toast show
1535
+ } else {
1536
+ const errorData = await response.json().catch(() => ({}));
1537
+ console.error("Save global failed:", errorData);
1538
+ onActionError?.(errorData.error || "Save failed");
1539
+ }
1540
+ } catch (e) {
1541
+ console.error("Save error exception:", e);
1542
+ onActionError?.("Save failed: " + (e as Error).message);
1543
+ }
1544
+ }}
1545
+ />
1546
+ )}
2230
1547
  <main className="w-full">
2231
1548
  {view === "edit" && renderEditView()}
2232
1549
  {view === "version" && renderVersionView()}
2233
1550
  {view === "api" && renderApiView()}
2234
1551
  </main>
2235
- <ConfirmModal
2236
- open={confirmModal.open}
2237
- onClose={() => setConfirmModal({ ...confirmModal, open: false })}
2238
- onConfirm={() => {
2239
- confirmModal.onConfirm();
2240
- setConfirmModal({ ...confirmModal, open: false });
2241
- }}
2242
- title={confirmModal.title}
2243
- message={confirmModal.message}
2244
- variant={confirmModal.danger ? "danger" : "default"}
2245
- />
2246
- <UIModal
2247
- open={alertModal.open}
2248
- onClose={() => setAlertModal({ ...alertModal, open: false })}
2249
- title={alertModal.title}
2250
- size="sm"
2251
- footer={
2252
- <button
2253
- type="button"
2254
- onClick={() => setAlertModal({ ...alertModal, open: false })}
2255
- className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90 transition-colors"
2256
- >
2257
- OK
2258
- </button>
2259
- }
2260
- >
2261
- <p className="text-[var(--kyro-text-secondary)]">
2262
- {alertModal.message}
2263
- </p>
2264
- </UIModal>
2265
1552
  </div>
2266
1553
  );
2267
1554
  }
2268
1555
 
2269
1556
  interface RelationshipFieldProps {
2270
- field: any;
2271
- value: any;
2272
- onChange: (value: any) => void;
1557
+ field: Field;
1558
+ value?: unknown;
1559
+ onChange?: (value: unknown) => void;
2273
1560
  disabled?: boolean;
2274
1561
  error?: string;
2275
1562
  }
@@ -2283,7 +1570,7 @@ function RelationshipField({
2283
1570
  }: RelationshipFieldProps) {
2284
1571
  const [isOpen, setIsOpen] = useState(false);
2285
1572
  const [search, setSearch] = useState("");
2286
- const [options, setOptions] = useState<any[]>([]);
1573
+ const [options, setOptions] = useState<unknown[]>([]);
2287
1574
  const [loading, setLoading] = useState(false);
2288
1575
 
2289
1576
  const isMultiple = field.hasMany;
@@ -2293,10 +1580,10 @@ function RelationshipField({
2293
1580
 
2294
1581
  const fetchOptions = () => {
2295
1582
  setLoading(true);
2296
- fetch(`/api/${targetCollection}?limit=50`)
1583
+ fetchWithAuth(`/api/${targetCollection}?limit=50`)
2297
1584
  .then((res) => res.json())
2298
1585
  .then((data) => {
2299
- setOptions(data.docs || []);
1586
+ setOptions((data.docs || []) as unknown[]);
2300
1587
  setLoading(false);
2301
1588
  })
2302
1589
  .catch((err) => {
@@ -2309,38 +1596,43 @@ function RelationshipField({
2309
1596
  fetchOptions();
2310
1597
  }, [targetCollection]);
2311
1598
 
2312
- const getLabel = (opt: any) => {
2313
- if (!opt) return "";
2314
- return (
2315
- opt.title || opt.name || opt.label || opt.filename || opt.slug || opt.id
1599
+ const getLabel = (opt: unknown) => {
1600
+ const o = opt as Record<string, unknown> | undefined;
1601
+ if (!o) return "";
1602
+ return String(
1603
+ o.title || o.name || o.label || o.filename || o.slug || o.id || "",
2316
1604
  );
2317
1605
  };
2318
1606
 
2319
- const findOptionById = (id: string) => {
2320
- return options.find((opt) => opt.id === id);
1607
+ const findOptionById = (id: unknown) => {
1608
+ return (options as Array<Record<string, unknown>>).find(
1609
+ (o) => o.id === id,
1610
+ );
2321
1611
  };
2322
1612
 
2323
1613
  const isSelected = (optId: string) => {
2324
1614
  if (!value) return false;
2325
1615
  if (isMultiple) {
2326
- return (value as any[]).some((v) => (v.id || v) === optId);
1616
+ const arr = Array.isArray(value) ? value : [];
1617
+ return (arr as Array<{ id?: string }>).some((v) => (v.id || v) === optId);
2327
1618
  }
2328
- return (value.id || value) === optId;
1619
+ return ((value as { id?: string })?.id || value) === optId;
2329
1620
  };
2330
1621
 
2331
- const toggleSelection = (opt: any) => {
1622
+ const toggleSelection = (opt: { id?: string }) => {
2332
1623
  if (isMultiple) {
2333
1624
  const current = Array.isArray(value) ? value : [];
2334
- if (isSelected(opt.id)) {
2335
- onChange(current.filter((item) => (item.id || item) !== opt.id));
1625
+ const arr = current as Array<{ id?: string }>;
1626
+ if (isSelected(opt.id as string)) {
1627
+ onChange?.(arr.filter((item) => (item.id || item) !== opt.id));
2336
1628
  } else {
2337
- onChange([...current, opt.id]);
1629
+ onChange?.([...arr, opt.id]);
2338
1630
  }
2339
1631
  } else {
2340
- if (isSelected(opt.id)) {
2341
- onChange(null);
1632
+ if (isSelected(opt.id as string)) {
1633
+ onChange?.(null);
2342
1634
  } else {
2343
- onChange(opt.id);
1635
+ onChange?.(opt.id);
2344
1636
  setIsOpen(false);
2345
1637
  }
2346
1638
  }
@@ -2350,28 +1642,30 @@ function RelationshipField({
2350
1642
  if (!value) return null;
2351
1643
  if (isMultiple && Array.isArray(value)) {
2352
1644
  if (value.length === 0) return "None selected";
2353
- return value
1645
+ const arr = value as Array<{ id?: string }>;
1646
+ return arr
2354
1647
  .map((v) => {
2355
1648
  const id = v.id || v;
2356
1649
  const opt = findOptionById(id);
2357
- return opt ? getLabel(opt) : id;
1650
+ return opt ? getLabel(opt) : String(id);
2358
1651
  })
2359
1652
  .join(", ");
2360
1653
  }
2361
- const id = value.id || value;
1654
+ const id = (value as { id?: string }).id || value;
2362
1655
  const opt = findOptionById(id);
2363
- return opt ? getLabel(opt) : id;
1656
+ return opt ? getLabel(opt) : String(id);
2364
1657
  };
2365
1658
 
2366
- const filteredOptions = search
1659
+ const filteredOptions = (search
2367
1660
  ? (options || []).filter((opt) => {
1661
+ const o = opt as Record<string, unknown>;
2368
1662
  const term = search.toLowerCase();
2369
1663
  const searchableFields = ["title", "name", "label", "filename", "slug"];
2370
1664
  return searchableFields.some(
2371
- (key) => opt[key] && String(opt[key]).toLowerCase().includes(term),
1665
+ (key) => o[key] && String(o[key]).toLowerCase().includes(term),
2372
1666
  );
2373
1667
  })
2374
- : options || [];
1668
+ : options || []) as Array<Record<string, unknown>>;
2375
1669
 
2376
1670
  return (
2377
1671
  <div className="kyro-form-field">
@@ -2405,9 +1699,9 @@ function RelationshipField({
2405
1699
  </div>
2406
1700
  </div>
2407
1701
 
2408
- {field.admin?.description && !error && (
2409
- <p className="kyro-form-help">{field.admin.description}</p>
2410
- )}
1702
+ {(field.admin?.description && !error) ? (
1703
+ <p className="kyro-form-help">{String((field.admin as { description?: string }).description)}</p>
1704
+ ) : null}
2411
1705
  {error && <p className="kyro-form-error">{error}</p>}
2412
1706
 
2413
1707
  {/* Modal */}
@@ -2437,19 +1731,22 @@ function RelationshipField({
2437
1731
  No results found.
2438
1732
  </div>
2439
1733
  ) : (
2440
- filteredOptions.map((opt) => (
2441
- <button
2442
- key={opt.id}
2443
- type="button"
2444
- className={`kyro-relation-modal-item ${isSelected(opt.id) ? "selected" : ""}`}
2445
- onClick={() => toggleSelection(opt)}
2446
- >
2447
- <span>{getLabel(opt)}</span>
2448
- <span className="kyro-relation-modal-item-id">
2449
- {opt.id ? `(${String(opt.id).slice(0, 8)}...)` : ""}
2450
- </span>
2451
- </button>
2452
- ))
1734
+ filteredOptions.map((opt) => {
1735
+ const o = opt as { id?: string };
1736
+ return (
1737
+ <button
1738
+ key={String(o.id)}
1739
+ type="button"
1740
+ className={`kyro-relation-modal-item ${isSelected(String(o.id)) ? "selected" : ""}`}
1741
+ onClick={() => toggleSelection(o)}
1742
+ >
1743
+ <span>{getLabel(opt)}</span>
1744
+ <span className="kyro-relation-modal-item-id">
1745
+ {o.id ? `(${String(o.id).slice(0, 8)}...)` : ""}
1746
+ </span>
1747
+ </button>
1748
+ );
1749
+ })
2453
1750
  )}
2454
1751
  </div>
2455
1752
 
@@ -2474,35 +1771,3 @@ function stripHtml(html: string) {
2474
1771
  if (typeof html !== "string") return "";
2475
1772
  return html.replace(/<[^>]*>?/gm, "").trim();
2476
1773
  }
2477
-
2478
- const SeoPreview = ({
2479
- title,
2480
- description,
2481
- slug,
2482
- }: {
2483
- title: string;
2484
- description: string;
2485
- slug: string;
2486
- }) => (
2487
- <div className="bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-lg p-6 max-w-2xl shadow-sm transition-colors duration-300">
2488
- <div className="flex items-center gap-2 mb-2">
2489
- <div className="w-7 h-7 bg-[var(--kyro-bg-secondary)] rounded-full flex items-center justify-center text-[10px] text-[var(--kyro-text-primary)] font-medium border border-[var(--kyro-border)]">
2490
- K
2491
- </div>
2492
- <div className="flex flex-col">
2493
- <span className="text-sm font-medium text-[var(--kyro-text-primary)] leading-tight">
2494
- kyro-cms.com
2495
- </span>
2496
- <span className="text-[12px] text-[var(--kyro-text-secondary)] leading-tight opacity-80">
2497
- https://kyro-cms.com › posts › {slug}
2498
- </span>
2499
- </div>
2500
- </div>
2501
- <h3 className="text-[20px] text-[#2563eb] dark:text-[#60a5fa] font-medium hover:underline cursor-pointer mb-1 leading-tight transition-colors">
2502
- {title}
2503
- </h3>
2504
- <p className="text-[14px] text-[var(--kyro-text-secondary)] leading-relaxed line-clamp-2">
2505
- {description}
2506
- </p>
2507
- </div>
2508
- );