@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
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+ import type { Field } from "@kyro-cms/core/client";
3
+
4
+ interface FieldLayoutProps {
5
+ field: Field;
6
+ error?: string;
7
+ children: React.ReactNode;
8
+ hideLabel?: boolean;
9
+ }
10
+
11
+ export default function FieldLayout({
12
+ field,
13
+ error,
14
+ children,
15
+ hideLabel = false,
16
+ }: FieldLayoutProps) {
17
+ return (
18
+ <div className="space-y-2.5 w-full group">
19
+ {field.label && !hideLabel && (
20
+ <div className="flex justify-between items-end mb-1">
21
+ <label className="block text-xs font-bold tracking-widest text-[var(--kyro-text-secondary)] opacity-50 group-focus-within:opacity-100 group-focus-within:text-[var(--kyro-primary)] transition-all duration-300">
22
+ {field.label}
23
+ {field.required && (
24
+ <span className="text-[var(--kyro-error)] ml-1">*</span>
25
+ )}
26
+ </label>
27
+ </div>
28
+ )}
29
+
30
+ <div className="relative transform transition-transform duration-200 focus-within:scale-[1.002]">
31
+ {children}
32
+ </div>
33
+
34
+ {(field.admin?.description || error) && (
35
+ <div className="flex flex-col gap-1.5 px-1">
36
+ {field.admin?.description && !error && (
37
+ <p className="text-[11px] leading-relaxed text-[var(--kyro-text-muted)] font-medium opacity-60 italic">
38
+ {field.admin.description}
39
+ </p>
40
+ )}
41
+ {error && (
42
+ <p className="text-[11px] leading-relaxed text-[var(--kyro-error)] font-bold flex items-center gap-2 animate-in fade-in slide-in-from-top-1 duration-300">
43
+ <span className="w-1.5 h-1.5 rounded-full bg-[var(--kyro-error)] shadow-[0_0_8px_var(--kyro-error)]" />
44
+ {error}
45
+ </p>
46
+ )}
47
+ </div>
48
+ )}
49
+ </div>
50
+
51
+ );
52
+ }
@@ -0,0 +1,35 @@
1
+ import React from "react";
2
+ import type { Field } from "@kyro-cms/core/client";
3
+
4
+ interface GroupLayoutProps {
5
+ field: Field;
6
+ value: Record<string, unknown> | null;
7
+ onChange: (value: Record<string, unknown>) => void;
8
+ renderField: (
9
+ field: Field,
10
+ parentData: Record<string, unknown>,
11
+ onChange: (value: unknown) => void,
12
+ ) => React.ReactNode;
13
+ }
14
+
15
+ export function GroupLayout({
16
+ field,
17
+ value,
18
+ onChange,
19
+ renderField,
20
+ }: GroupLayoutProps) {
21
+ const groupData = value || {};
22
+
23
+ return (
24
+ <div className="kyro-form-group border border-[var(--kyro-border)] rounded-[var(--kyro-radius-lg)] p-6 bg-[var(--kyro-surface-accent)]/30">
25
+ <h3 className="text-sm font-bold tracking-widest text-[var(--kyro-text-primary)] mb-6 border-b border-[var(--kyro-border)] pb-2 inline-block">
26
+ {field.label || field.name}
27
+ </h3>
28
+ <div className="space-y-6">
29
+ {(field as Field & { fields?: Field[] }).fields.map((f: Field) =>
30
+ renderField(f, groupData, onChange),
31
+ )}
32
+ </div>
33
+ </div>
34
+ );
35
+ }
@@ -3,15 +3,15 @@ import type { JSONField as JSONFieldType } from "@kyro-cms/core/client";
3
3
 
4
4
  interface JSONFieldProps {
5
5
  field: JSONFieldType;
6
- value?: any;
7
- onChange?: (value: any) => void;
6
+ value?: unknown;
7
+ onChange?: (value: unknown) => void;
8
8
  error?: string;
9
9
  disabled?: boolean;
10
10
  }
11
11
 
12
12
  interface TreeNode {
13
13
  key: string;
14
- value: any;
14
+ value: unknown;
15
15
  type: "object" | "array" | "string" | "number" | "boolean" | "null";
16
16
  path: string[];
17
17
  collapsed?: boolean;
@@ -232,7 +232,7 @@ const TreeView: React.FC<{ data: TreeNode }> = ({ data }) => {
232
232
  if (node.type === "array") {
233
233
  return (
234
234
  <div className="pl-4 border-l border-[var(--kyro-border)]">
235
- {node.value.map((item: any, index: number) => (
235
+ {node.value.map((item: unknown, index: number) => (
236
236
  <TreeNodeView
237
237
  key={index}
238
238
  name={String(index)}
@@ -315,7 +315,7 @@ const TreeView: React.FC<{ data: TreeNode }> = ({ data }) => {
315
315
  );
316
316
  };
317
317
 
318
- const TreeNodeView: React.FC<{ name: string; value: any; path: string[] }> = ({
318
+ const TreeNodeView: React.FC<{ name: string; value: unknown; path: string[] }> = ({
319
319
  name,
320
320
  value,
321
321
  path,
@@ -325,13 +325,13 @@ const TreeNodeView: React.FC<{ name: string; value: any; path: string[] }> = ({
325
325
  return <TreeView data={treeNode} />;
326
326
  };
327
327
 
328
- function getType(value: any): TreeNode["type"] {
328
+ function getType(value: unknown): TreeNode["type"] {
329
329
  if (value === null) return "null";
330
330
  if (Array.isArray(value)) return "array";
331
331
  return typeof value as TreeNode["type"];
332
332
  }
333
333
 
334
- function buildTree(value: any, path: string[]): TreeNode {
334
+ function buildTree(value: unknown, path: string[]): TreeNode {
335
335
  return {
336
336
  key: path[path.length - 1] || "root",
337
337
  value,
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { ExternalLink } from "lucide-react";
2
+ import { ExternalLink } from "../ui/icons";
3
3
 
4
4
  interface LinkFieldProps {
5
5
  text?: string;
@@ -198,7 +198,7 @@ export const MarkdownField: React.FC<MarkdownFieldProps> = ({
198
198
  >
199
199
  {!showPreview ? (
200
200
  <textarea
201
- value={value}
201
+ value={value == null ? "" : value}
202
202
  onChange={handleChange}
203
203
  disabled={disabled}
204
204
  rows={12}
@@ -1,4 +1,5 @@
1
1
  import type { NumberField as NumberFieldType } from "@kyro-cms/core/client";
2
+ import FieldLayout from "./FieldLayout";
2
3
 
3
4
  interface NumberFieldComponentProps {
4
5
  field: NumberFieldType;
@@ -15,42 +16,28 @@ export default function NumberField({
15
16
  error,
16
17
  disabled,
17
18
  }: NumberFieldComponentProps) {
19
+ const isReadOnly = field.admin?.readOnly;
20
+
18
21
  return (
19
- <div className="space-y-1">
20
- {field.label && (
21
- <label className="block text-sm font-medium text-[var(--kyro-text-primary)]">
22
- {field.label}
23
- {field.required && (
24
- <span className="text-[var(--kyro-error)] ml-1">*</span>
25
- )}
26
- </label>
27
- )}
22
+ <FieldLayout
23
+ field={field}
24
+ error={error}
25
+ >
28
26
  <input
29
27
  type="number"
28
+ id={field.name}
30
29
  value={value ?? ""}
31
30
  onChange={(e) => onChange?.(parseFloat(e.target.value) || 0)}
32
31
  placeholder={field.admin?.placeholder}
33
- disabled={disabled || field.admin?.readOnly}
32
+ disabled={disabled || isReadOnly}
34
33
  min={field.min}
35
34
  max={field.max}
36
- step={field.step || field.integer ? 1 : "any"}
35
+ step={field.step || (field.integer ? 1 : "any")}
37
36
  required={field.required}
38
- className={`w-full px-3 py-2 border rounded-md text-sm transition-colors ${
39
- error
40
- ? "border-[var(--kyro-error)] focus:border-[var(--kyro-error)] focus:ring-[var(--kyro-error)]"
41
- : "border-[var(--kyro-border)] focus:border-[var(--kyro-primary)] focus:ring-[var(--kyro-primary)]"
42
- } ${
43
- disabled || field.admin?.readOnly
44
- ? "bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] opacity-50"
45
- : "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"
37
+ className={`kyro-form-input ${
38
+ disabled || isReadOnly ? "opacity-50 cursor-not-allowed" : ""
46
39
  }`}
47
40
  />
48
- {field.admin?.description && !error && (
49
- <p className="text-xs text-[var(--kyro-text-muted)]">
50
- {field.admin.description}
51
- </p>
52
- )}
53
- {error && <p className="text-xs text-[var(--kyro-error)]">{error}</p>}
54
- </div>
41
+ </FieldLayout>
55
42
  );
56
43
  }
@@ -18,15 +18,15 @@ interface PortableTextFieldProps {
18
18
  placeholder?: string;
19
19
  };
20
20
  };
21
- value?: any;
22
- onChange?: (value: any) => void;
21
+ value?: unknown;
22
+ onChange?: (value: unknown) => void;
23
23
  error?: string;
24
24
  disabled?: boolean;
25
25
  }
26
26
 
27
27
  const EditorLazy = lazy(() => import("./EditorClient"));
28
28
 
29
- function toPortableTextArray(value: any): any[] {
29
+ function toPortableTextArray(value: unknown): Record<string, unknown>[] {
30
30
  if (Array.isArray(value)) {
31
31
  if (
32
32
  value.length > 0 &&
@@ -86,7 +86,7 @@ const PortableTextField: React.FC<PortableTextFieldProps> = ({
86
86
  }, [ptValue, isMounted]);
87
87
 
88
88
  const handleChange = useCallback(
89
- (blocks: any[]) => {
89
+ (blocks: Record<string, unknown>[]) => {
90
90
  if (!blocks || !Array.isArray(blocks)) {
91
91
  onChange?.([]);
92
92
  return;
@@ -3,7 +3,7 @@ import { PortableText } from "@portabletext/react";
3
3
  import type { PortableTextComponents } from "@portabletext/react";
4
4
 
5
5
  interface PortableTextRendererProps {
6
- value: any[];
6
+ value: unknown[];
7
7
  className?: string;
8
8
  }
9
9
 
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useEffect } from "react";
2
- import { Search, Loader2, X } from "lucide-react";
2
+ import { Search, Loader2, X } from "../ui/icons";
3
3
  import { apiGet, buildSearchQuery } from "../../lib/api";
4
4
 
5
5
  interface RelationshipBlockFieldProps {
@@ -8,7 +8,7 @@ interface RelationshipBlockFieldProps {
8
8
  selectedIds?: string[];
9
9
  selectedId?: string;
10
10
  labelField?: string;
11
- onChange: (field: string, value: any) => void;
11
+ onChange: (field: string, value: unknown) => void;
12
12
  compact?: boolean;
13
13
  }
14
14
 
@@ -23,7 +23,7 @@ export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
23
23
  }) => {
24
24
  const [isOpen, setIsOpen] = useState(false);
25
25
  const [search, setSearch] = useState("");
26
- const [options, setOptions] = useState<any[]>([]);
26
+ const [options, setOptions] = useState<Record<string, unknown>[]>([]);
27
27
  const [loading, setLoading] = useState(false);
28
28
  const [collections, setCollections] = useState<string[]>([]);
29
29
  const [loadingCollections, setLoadingCollections] = useState(true);
@@ -32,7 +32,7 @@ export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
32
32
  apiGet("/api/collections")
33
33
  .then((data) => {
34
34
  setCollections(
35
- (data.collections || []).map((c: any) => c.slug || c.name || c),
35
+ (data.collections || []).map((c: Record<string, unknown>) => c.slug || c.name || c),
36
36
  );
37
37
  setLoadingCollections(false);
38
38
  })
@@ -55,7 +55,7 @@ export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
55
55
  if (isOpen) fetchOptions(search);
56
56
  }, [isOpen, search, relationTo, labelField]);
57
57
 
58
- const getLabel = (opt: any) => {
58
+ const getLabel = (opt: Record<string, unknown>) => {
59
59
  return (
60
60
  opt?.[labelField] ||
61
61
  opt?.title ||
@@ -70,7 +70,7 @@ export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
70
70
 
71
71
  const activeIds = hasMany ? selectedIds : selectedId ? [selectedId] : [];
72
72
 
73
- const handleSelect = (opt: any) => {
73
+ const handleSelect = (opt: Record<string, unknown>) => {
74
74
  if (hasMany) {
75
75
  if (activeIds.includes(opt.id)) {
76
76
  onChange(
@@ -97,26 +97,34 @@ export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
97
97
  ? "w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
98
98
  : "w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
99
99
 
100
+ // Check if relationTo is a known single collection (not an array)
101
+ const isKnownCollection = relationTo && typeof relationTo === "string" && relationTo.length > 0;
102
+
100
103
  return (
101
104
  <div className={compact ? "space-y-2" : "space-y-4"}>
102
105
  <div className={compact ? "flex items-center gap-2" : "space-y-3"}>
103
- {loadingCollections ? (
104
- <div className={selectClass + " text-[var(--kyro-text-muted)]"}>
105
- Loading...
106
- </div>
107
- ) : (
108
- <select
109
- value={relationTo}
110
- onChange={(e) => onChange("relationTo", e.target.value)}
111
- className={selectClass}
112
- >
113
- <option value="">Select collection...</option>
114
- {collections.map((col) => (
115
- <option key={col} value={col}>
116
- {col}
117
- </option>
118
- ))}
119
- </select>
106
+ {/* Hide collection selector if relationTo is known (single collection) */}
107
+ {!isKnownCollection && (
108
+ <>
109
+ {loadingCollections ? (
110
+ <div className={selectClass + " text-[var(--kyro-text-muted)]"}>
111
+ Loading...
112
+ </div>
113
+ ) : (
114
+ <select
115
+ value={relationTo}
116
+ onChange={(e) => onChange("relationTo", e.target.value)}
117
+ className={selectClass}
118
+ >
119
+ <option value="">Select collection...</option>
120
+ {collections.map((col) => (
121
+ <option key={col} value={col}>
122
+ {col}
123
+ </option>
124
+ ))}
125
+ </select>
126
+ )}
127
+ </>
120
128
  )}
121
129
 
122
130
  {!compact && (
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState, useRef } from "react";
2
- import { Search, X, ChevronDown, Loader2 } from "lucide-react";
2
+ import { Search, X, ChevronDown, Loader2 } from "../ui/icons";
3
3
  import { apiGet, buildSearchQuery } from "../../lib/api";
4
4
 
5
5
  interface RelationshipFieldProps {
@@ -30,7 +30,7 @@ export function RelationshipField({
30
30
  }: RelationshipFieldProps) {
31
31
  const [isOpen, setIsOpen] = useState(false);
32
32
  const [search, setSearch] = useState("");
33
- const [options, setOptions] = useState<any[]>([]);
33
+ const [options, setOptions] = useState<Record<string, unknown>[]>([]);
34
34
  const [loading, setLoading] = useState(false);
35
35
  const containerRef = useRef<HTMLDivElement>(null);
36
36
 
@@ -49,9 +49,9 @@ export function RelationshipField({
49
49
  .then((data) => {
50
50
  setOptions((prev) => {
51
51
  const existingIds = new Set(prev.map((o) => o.id));
52
- const newDocs = (data.docs || []).filter(
53
- (d: any) => !existingIds.has(d.id),
54
- );
52
+ const newDocs = (data.docs || []).filter(
53
+ (d: Record<string, unknown>) => !existingIds.has(d.id as string),
54
+ );
55
55
  return [...prev, ...newDocs];
56
56
  });
57
57
  setLoading(false);
@@ -61,8 +61,8 @@ export function RelationshipField({
61
61
  });
62
62
  };
63
63
 
64
- const fetchSelectedItems = () => {
65
- const items: any[] = isMultiple
64
+ const fetchSelectedItems = () => {
65
+ const items: (string | Record<string, unknown>)[] = isMultiple
66
66
  ? Array.isArray(value)
67
67
  ? value
68
68
  : []
@@ -106,7 +106,7 @@ export function RelationshipField({
106
106
  return () => document.removeEventListener("mousedown", handleClickOutside);
107
107
  }, []);
108
108
 
109
- const getLabel = (opt: any) => {
109
+ const getLabel = (opt: Record<string, unknown>) => {
110
110
  return (
111
111
  opt?.title ||
112
112
  opt?.name ||
@@ -119,11 +119,11 @@ export function RelationshipField({
119
119
  );
120
120
  };
121
121
 
122
- const getValueId = (val: any) => {
122
+ const getValueId = (val: string | Record<string, unknown>) => {
123
123
  return val?.id || val;
124
124
  };
125
125
 
126
- const isSelected = (opt: any) => {
126
+ const isSelected = (opt: Record<string, unknown>) => {
127
127
  const optId = opt.id;
128
128
  if (!value) return false;
129
129
  if (isMultiple && Array.isArray(value)) {
@@ -132,7 +132,7 @@ export function RelationshipField({
132
132
  return getValueId(value) === optId;
133
133
  };
134
134
 
135
- const handleSelect = (opt: any) => {
135
+ const handleSelect = (opt: Record<string, unknown>) => {
136
136
  const optId = opt.id;
137
137
  if (isMultiple) {
138
138
  const current = Array.isArray(value) ? value : [];
@@ -159,7 +159,7 @@ export function RelationshipField({
159
159
  const renderSelectedItems = () => {
160
160
  if (!value) return null;
161
161
 
162
- let items: any[];
162
+ let items: (string | Record<string, unknown>)[];
163
163
  if (isMultiple) {
164
164
  items = Array.isArray(value) ? value : [];
165
165
  } else {
@@ -183,8 +183,8 @@ export function RelationshipField({
183
183
  type="button"
184
184
  onClick={() => {
185
185
  if (isMultiple) {
186
- onChange?.(
187
- items.filter((_: any, i: number) => i !== idx),
186
+ onChange?.(
187
+ (items as (string | Record<string, unknown>)[]).filter((_: unknown, i: number) => i !== idx),
188
188
  );
189
189
  } else {
190
190
  onChange?.(null);
@@ -1,9 +1,10 @@
1
1
  import type { SelectField as SelectFieldType } from "@kyro-cms/core/client";
2
+ import FieldLayout from "./FieldLayout";
2
3
 
3
4
  interface SelectFieldComponentProps {
4
5
  field: SelectFieldType;
5
6
  value?: string | string[];
6
- onChange?: (value: string | string[]) => void;
7
+ onChange?: (value: string | string[] | undefined) => void;
7
8
  error?: string;
8
9
  disabled?: boolean;
9
10
  }
@@ -15,17 +16,15 @@ export default function SelectField({
15
16
  error,
16
17
  disabled,
17
18
  }: SelectFieldComponentProps) {
19
+ const isReadOnly = field.admin?.readOnly;
20
+
18
21
  return (
19
- <div className="space-y-1">
20
- {field.label && (
21
- <label className="block text-sm font-medium text-[var(--kyro-text-primary)]">
22
- {field.label}
23
- {field.required && (
24
- <span className="text-[var(--kyro-error)] ml-1">*</span>
25
- )}
26
- </label>
27
- )}
22
+ <FieldLayout
23
+ field={field}
24
+ error={error}
25
+ >
28
26
  <select
27
+ id={field.name}
29
28
  value={
30
29
  field.hasMany
31
30
  ? Array.isArray(value)
@@ -38,31 +37,23 @@ export default function SelectField({
38
37
  const selected = e.target.value ? e.target.value.split(",") : [];
39
38
  onChange?.(selected);
40
39
  } else {
41
- onChange?.(e.target.value);
40
+ onChange?.(e.target.value || undefined);
42
41
  }
43
42
  }}
44
43
  multiple={field.hasMany}
45
- disabled={disabled || field.admin?.readOnly}
44
+ disabled={disabled || isReadOnly}
46
45
  required={field.required}
47
- className={`w-full px-3 py-2 border rounded-md text-sm transition-colors ${
48
- error
49
- ? "border-[var(--kyro-error)] focus:border-[var(--kyro-error)] focus:ring-[var(--kyro-error)]"
50
- : "border-[var(--kyro-border)] focus:border-[var(--kyro-primary)] focus:ring-[var(--kyro-primary)]"
51
- } ${disabled || field.admin?.readOnly ? "bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-muted)]" : "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"}`}
46
+ className={`kyro-form-input ${
47
+ disabled || isReadOnly ? "opacity-50 cursor-not-allowed" : ""
48
+ }`}
52
49
  >
53
- {!field.required && <option value="">Select...</option>}
54
- {field.options.map((option) => (
50
+ {!field.required && !field.hasMany && <option value="">Select...</option>}
51
+ {field.options?.map((option) => (
55
52
  <option key={option.value} value={option.value}>
56
53
  {option.label}
57
54
  </option>
58
55
  ))}
59
56
  </select>
60
- {field.admin?.description && !error && (
61
- <p className="text-xs text-[var(--kyro-text-secondary)]">
62
- {field.admin.description}
63
- </p>
64
- )}
65
- {error && <p className="text-xs text-[var(--kyro-error)]">{error}</p>}
66
- </div>
57
+ </FieldLayout>
67
58
  );
68
59
  }
@@ -0,0 +1,69 @@
1
+ import React, { useState } from "react";
2
+ import type { Field } from "@kyro-cms/core/client";
3
+ import { SeoPreview } from "../ui/SeoPreview";
4
+
5
+ interface TabsLayoutProps {
6
+ field: Field;
7
+ formData: Record<string, unknown>;
8
+ onTabDataChange: (tabData: Record<string, unknown>) => void;
9
+ renderField: (
10
+ field: Field,
11
+ parentData: Record<string, unknown>,
12
+ onChange: (value: unknown) => void,
13
+ ) => React.ReactNode;
14
+ }
15
+
16
+ export function TabsLayout({
17
+ field,
18
+ formData,
19
+ onTabDataChange,
20
+ renderField,
21
+ }: TabsLayoutProps) {
22
+ const [activeTab, setActiveTab] = useState(0);
23
+
24
+ const fieldTabs = (field as Field & { tabs?: { label: string; fields: Field[] }[] }).tabs || [];
25
+ const currentTab = fieldTabs[activeTab] || fieldTabs[0];
26
+
27
+ // Get nested tab data
28
+ const tabData = formData[field.name as string] || {};
29
+
30
+ return (
31
+ <div className="space-y-8">
32
+ <div className="flex items-center gap-2 border-b border-[var(--kyro-border)] mb-6 overflow-x-auto hide-scrollbar">
33
+ {fieldTabs.map((tab: { label: string }, index: number) => (
34
+ <button
35
+ key={index}
36
+ type="button"
37
+ className={`px-6 py-3 text-xs tracking-widest font-bold transition-all border-b-2 -mb-[1px] whitespace-nowrap ${activeTab === index
38
+ ? "border-[var(--kyro-primary)] text-[var(--kyro-primary)]"
39
+ : "border-transparent text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] opacity-60 hover:opacity-100"
40
+ }`}
41
+ onClick={() => setActiveTab(index)}
42
+ >
43
+ {tab.label}
44
+ </button>
45
+ ))}
46
+ </div>
47
+ <div className="space-y-6">
48
+ {currentTab?.fields.map((f: Field) =>
49
+ renderField(f, tabData, onTabDataChange),
50
+ )}
51
+ </div>
52
+
53
+ {currentTab?.label === "SEO" && (
54
+ <div className="mt-12 pt-8 border-t border-[var(--kyro-border)]">
55
+ <h4 className="text-[10px] font-bold text-[var(--kyro-text-secondary)] tracking-[0.2em] mb-6 opacity-50">
56
+ Live Google Preview
57
+ </h4>
58
+ <SeoPreview
59
+ title={formData.metaTitle || formData.title || "Untitled"}
60
+ description={
61
+ formData.metaDescription || "Please enter a description..."
62
+ }
63
+ slug={formData.slug || "your-slug"}
64
+ />
65
+ </div>
66
+ )}
67
+ </div>
68
+ );
69
+ }