@kyro-cms/admin 0.1.6 → 0.1.8

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 (179) hide show
  1. package/README.md +149 -51
  2. package/package.json +54 -5
  3. package/src/collections/auth/index.ts +2 -2
  4. package/src/collections/portfolio/index.ts +343 -0
  5. package/src/components/ActionBar.tsx +153 -16
  6. package/src/components/Admin.tsx +137 -28
  7. package/src/components/ApiExplorer.tsx +325 -0
  8. package/src/components/ApiKeysManager.tsx +563 -0
  9. package/src/components/AuditLogsPage.tsx +664 -0
  10. package/src/components/AutoForm.tsx +2155 -770
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +4 -4
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +200 -58
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +890 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +192 -54
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +206 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/ThemeProvider.tsx +8 -2
  26. package/src/components/UserManagement.tsx +204 -0
  27. package/src/components/VersionHistoryPanel.tsx +3 -3
  28. package/src/components/WebhookManager.tsx +608 -0
  29. package/src/components/blocks/AccordionBlock.tsx +65 -0
  30. package/src/components/blocks/ArrayBlock.tsx +84 -0
  31. package/src/components/blocks/BlockEditModal.tsx +363 -0
  32. package/src/components/blocks/ButtonBlock.tsx +64 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +114 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +93 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +63 -0
  38. package/src/components/blocks/HeadingBlock.tsx +59 -0
  39. package/src/components/blocks/HeroBlock.tsx +99 -0
  40. package/src/components/blocks/ImageBlock.tsx +82 -0
  41. package/src/components/blocks/LinkBlock.tsx +65 -0
  42. package/src/components/blocks/ListBlock.tsx +60 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +72 -0
  45. package/src/components/blocks/RichTextBlock.tsx +66 -0
  46. package/src/components/blocks/VStackBlock.tsx +61 -0
  47. package/src/components/blocks/VideoBlock.tsx +65 -0
  48. package/src/components/blocks/index.ts +10 -0
  49. package/src/components/fields/AccordionField.tsx +213 -0
  50. package/src/components/fields/ArrayField.tsx +241 -0
  51. package/src/components/fields/BlocksField.tsx +323 -0
  52. package/src/components/fields/ButtonField.tsx +53 -0
  53. package/src/components/fields/CheckboxField.tsx +18 -8
  54. package/src/components/fields/ChildrenField.tsx +48 -0
  55. package/src/components/fields/CodeField.tsx +294 -0
  56. package/src/components/fields/ColumnsField.tsx +137 -0
  57. package/src/components/fields/DateField.tsx +24 -12
  58. package/src/components/fields/EditorClient.tsx +537 -0
  59. package/src/components/fields/HeadingField.tsx +31 -0
  60. package/src/components/fields/HeroField.tsx +101 -0
  61. package/src/components/fields/JSONField.tsx +341 -0
  62. package/src/components/fields/LinkField.tsx +81 -0
  63. package/src/components/fields/ListField.tsx +74 -0
  64. package/src/components/fields/MarkdownField.tsx +260 -0
  65. package/src/components/fields/NumberField.tsx +25 -13
  66. package/src/components/fields/PortableTextField.tsx +155 -0
  67. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  68. package/src/components/fields/RelationshipBlockField.tsx +233 -0
  69. package/src/components/fields/RelationshipField.tsx +278 -60
  70. package/src/components/fields/SelectField.tsx +28 -16
  71. package/src/components/fields/TextField.tsx +31 -15
  72. package/src/components/fields/UploadField.tsx +613 -0
  73. package/src/components/fields/VideoField.tsx +73 -0
  74. package/src/components/fields/extensions/blockComponents.tsx +247 -0
  75. package/src/components/fields/extensions/blocksStore.ts +273 -0
  76. package/src/components/fields/index.ts +24 -0
  77. package/src/components/index.ts +1 -2
  78. package/src/components/layout/Header.tsx +2 -2
  79. package/src/components/layout/Layout.tsx +3 -3
  80. package/src/components/ui/Badge.tsx +9 -4
  81. package/src/components/ui/BlockDrawer.tsx +79 -0
  82. package/src/components/ui/Button.tsx +1 -1
  83. package/src/components/ui/CommandPalette.tsx +362 -0
  84. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  85. package/src/components/ui/Dropdown.tsx +1 -1
  86. package/src/components/ui/Modal.tsx +37 -12
  87. package/src/components/ui/PromptModal.tsx +94 -0
  88. package/src/components/ui/SlidePanel.tsx +43 -16
  89. package/src/components/ui/Toast.tsx +80 -14
  90. package/src/env.d.ts +16 -0
  91. package/src/env.ts +20 -0
  92. package/src/index.ts +0 -1
  93. package/src/layouts/AdminLayout.astro +164 -170
  94. package/src/layouts/AuthLayout.astro +23 -6
  95. package/src/lib/MediaService.ts +541 -0
  96. package/src/lib/api.ts +163 -0
  97. package/src/lib/auth/sqlite-adapter.ts +319 -0
  98. package/src/lib/config.ts +23 -7
  99. package/src/lib/dataStore.ts +188 -73
  100. package/src/lib/date-utils.ts +69 -0
  101. package/src/lib/db/adapter.ts +54 -0
  102. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  103. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  104. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  105. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  106. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  107. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  108. package/src/lib/db/index.ts +449 -0
  109. package/src/lib/db/mongodb-adapter.ts +207 -0
  110. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  111. package/src/lib/db/schema/mysql-auth.ts +113 -0
  112. package/src/lib/db/schema/mysql-content.ts +20 -0
  113. package/src/lib/db/schema/postgres-auth.ts +116 -0
  114. package/src/lib/db/schema/postgres-content.ts +35 -0
  115. package/src/lib/db/schema/postgres-media.ts +52 -0
  116. package/src/lib/db/schema/postgres-settings.ts +11 -0
  117. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  118. package/src/lib/db/schema/sqlite-content.ts +20 -0
  119. package/src/lib/db/version-adapter.ts +248 -0
  120. package/src/lib/graphql/index.ts +1 -0
  121. package/src/lib/graphql/schema.ts +443 -0
  122. package/src/lib/i18n.tsx +353 -0
  123. package/src/lib/rate-limit.ts +267 -0
  124. package/src/lib/slugify.ts +15 -0
  125. package/src/lib/storage.ts +374 -0
  126. package/src/lib/store.ts +85 -0
  127. package/src/lib/validation.ts +250 -0
  128. package/src/middleware.ts +70 -11
  129. package/src/pages/[collection]/[id].astro +178 -122
  130. package/src/pages/[collection]/index.astro +24 -156
  131. package/src/pages/admin/api-explorer.astro +98 -0
  132. package/src/pages/admin/graphql-explorer.astro +40 -0
  133. package/src/pages/admin/graphql.astro +97 -0
  134. package/src/pages/admin/index.astro +200 -139
  135. package/src/pages/admin/keys.astro +8 -0
  136. package/src/pages/admin/rest-playground.astro +44 -0
  137. package/src/pages/admin/webhooks.astro +8 -0
  138. package/src/pages/api/[collection]/[id]/publish.ts +52 -0
  139. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  140. package/src/pages/api/[collection]/[id]/versions.ts +66 -0
  141. package/src/pages/api/[collection]/[id].ts +114 -159
  142. package/src/pages/api/[collection]/index.ts +150 -230
  143. package/src/pages/api/auth/[id].ts +48 -69
  144. package/src/pages/api/auth/audit-logs.ts +20 -43
  145. package/src/pages/api/auth/login.ts +159 -45
  146. package/src/pages/api/auth/logout.ts +42 -24
  147. package/src/pages/api/auth/refresh.ts +119 -0
  148. package/src/pages/api/auth/register.ts +110 -40
  149. package/src/pages/api/auth/users.ts +22 -97
  150. package/src/pages/api/collections.ts +59 -0
  151. package/src/pages/api/globals/[slug]/test.ts +172 -0
  152. package/src/pages/api/globals/[slug].ts +42 -0
  153. package/src/pages/api/graphql.ts +90 -0
  154. package/src/pages/api/health.ts +417 -40
  155. package/src/pages/api/keys/[id].ts +26 -0
  156. package/src/pages/api/keys/index.ts +75 -0
  157. package/src/pages/api/media/[id].ts +309 -0
  158. package/src/pages/api/media/folders.ts +609 -0
  159. package/src/pages/api/media/index.ts +146 -0
  160. package/src/pages/api/media/resize.ts +267 -0
  161. package/src/pages/api/search.ts +82 -0
  162. package/src/pages/api/slug-availability.ts +70 -0
  163. package/src/pages/api/storage-config.ts +20 -0
  164. package/src/pages/api/storage-status.ts +206 -0
  165. package/src/pages/api/upload.ts +334 -0
  166. package/src/pages/api/webhooks/index.ts +71 -0
  167. package/src/pages/audit/index.astro +2 -104
  168. package/src/pages/login.astro +11 -11
  169. package/src/pages/media.astro +10 -0
  170. package/src/pages/preview/[collection]/[id].astro +178 -0
  171. package/src/pages/register.astro +13 -13
  172. package/src/pages/roles/index.astro +21 -21
  173. package/src/pages/settings/[slug].astro +162 -0
  174. package/src/pages/settings/index.astro +9 -0
  175. package/src/pages/users/[id].astro +29 -21
  176. package/src/pages/users/index.astro +22 -17
  177. package/src/pages/users/new.astro +18 -17
  178. package/src/styles/main.css +563 -128
  179. package/src/components/layout/Sidebar.tsx +0 -497
@@ -0,0 +1,241 @@
1
+ import React from "react";
2
+ import { Plus, ChevronDown, ChevronUp, X } from "lucide-react";
3
+
4
+ interface ArrayFieldItem {
5
+ [key: string]: any;
6
+ }
7
+
8
+ interface ArrayFieldProps {
9
+ items?: ArrayFieldItem[];
10
+ labelField?: string;
11
+ onChange: (items: ArrayFieldItem[]) => void;
12
+ compact?: boolean;
13
+ }
14
+
15
+ export const ArrayField: React.FC<ArrayFieldProps> = ({
16
+ items = [],
17
+ labelField = "title",
18
+ onChange,
19
+ compact = false,
20
+ }) => {
21
+ const [openIndex, setOpenIndex] = React.useState<number | null>(0);
22
+
23
+ const handleItemChange = (index: number, field: string, value: string) => {
24
+ const newItems = [...items];
25
+ newItems[index] = { ...newItems[index], [field]: value };
26
+ onChange(newItems);
27
+ };
28
+
29
+ const handleRemove = (index: number) => {
30
+ const newItems = items.filter((_, i) => i !== index);
31
+ onChange(newItems);
32
+ if (openIndex === index) setOpenIndex(null);
33
+ else if (openIndex !== null && openIndex > index)
34
+ setOpenIndex(openIndex - 1);
35
+ };
36
+
37
+ const handleAdd = () => {
38
+ const newItem: ArrayFieldItem = {
39
+ [labelField]: `Item ${items.length + 1}`,
40
+ };
41
+ onChange([...items, newItem]);
42
+ setOpenIndex(items.length);
43
+ };
44
+
45
+ const inputClass = compact
46
+ ? "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"
47
+ : "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";
48
+
49
+ const itemKeys =
50
+ items.length > 0
51
+ ? Object.keys(items[0]).filter((k) => k !== "id" && k !== "_key")
52
+ : [];
53
+
54
+ if (compact) {
55
+ return (
56
+ <div className="space-y-1.5">
57
+ {items.length === 0 ? (
58
+ <div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-lg">
59
+ No items. Click "Add Item" to create one.
60
+ </div>
61
+ ) : (
62
+ <div className="space-y-1">
63
+ {items.map((item, index) => {
64
+ const isOpen = openIndex === index;
65
+ const itemLabel =
66
+ item[labelField] ||
67
+ item.title ||
68
+ item.name ||
69
+ `Item ${index + 1}`;
70
+ return (
71
+ <div
72
+ key={index}
73
+ className="border border-[var(--kyro-border)] rounded-lg overflow-hidden group"
74
+ >
75
+ <button
76
+ type="button"
77
+ onClick={() => setOpenIndex(isOpen ? null : index)}
78
+ className="w-full flex items-center justify-between p-2.5 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-sidebar-active)]/10 transition-colors"
79
+ >
80
+ <span className="text-sm font-medium text-[var(--kyro-text-primary)] truncate">
81
+ {itemLabel}
82
+ </span>
83
+ <div className="flex items-center gap-1">
84
+ <button
85
+ type="button"
86
+ onClick={(e) => {
87
+ e.stopPropagation();
88
+ handleRemove(index);
89
+ }}
90
+ className="opacity-0 group-hover:opacity-100 p-1 hover:bg-[var(--kyro-danger-bg)] rounded text-[var(--kyro-error)] transition-opacity"
91
+ title="Remove"
92
+ >
93
+ <X className="w-3.5 h-3.5" />
94
+ </button>
95
+ {isOpen ? (
96
+ <ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
97
+ ) : (
98
+ <ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
99
+ )}
100
+ </div>
101
+ </button>
102
+ {isOpen && (
103
+ <div className="p-2.5 bg-[var(--kyro-surface)] space-y-2">
104
+ {itemKeys.length > 0 ? (
105
+ itemKeys.map((key) => (
106
+ <input
107
+ key={key}
108
+ type="text"
109
+ value={item[key] || ""}
110
+ onChange={(e) =>
111
+ handleItemChange(index, key, e.target.value)
112
+ }
113
+ onClick={(e) => e.stopPropagation()}
114
+ className={inputClass}
115
+ placeholder={key}
116
+ />
117
+ ))
118
+ ) : (
119
+ <input
120
+ type="text"
121
+ value={item.value || ""}
122
+ onChange={(e) =>
123
+ handleItemChange(index, "value", e.target.value)
124
+ }
125
+ onClick={(e) => e.stopPropagation()}
126
+ className={inputClass}
127
+ placeholder="Value..."
128
+ />
129
+ )}
130
+ </div>
131
+ )}
132
+ </div>
133
+ );
134
+ })}
135
+ </div>
136
+ )}
137
+ <button
138
+ type="button"
139
+ onClick={handleAdd}
140
+ className="flex items-center justify-center gap-1.5 w-full px-3 py-2 text-xs font-medium rounded-lg border border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:border-[var(--kyro-sidebar-active)] hover:text-[var(--kyro-text-primary)] transition-colors"
141
+ >
142
+ <Plus className="w-3.5 h-3.5" />
143
+ Add Item
144
+ </button>
145
+ </div>
146
+ );
147
+ }
148
+
149
+ return (
150
+ <div className="space-y-2">
151
+ {items.length === 0 ? (
152
+ <div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-lg">
153
+ No items. Click "Add Item" to create one.
154
+ </div>
155
+ ) : (
156
+ <div className="space-y-2">
157
+ {items.map((item, index) => {
158
+ const isOpen = openIndex === index;
159
+ const itemLabel =
160
+ item[labelField] ||
161
+ item.title ||
162
+ item.name ||
163
+ `Item ${index + 1}`;
164
+ return (
165
+ <div
166
+ key={index}
167
+ className="border border-[var(--kyro-border)] rounded-lg overflow-hidden group"
168
+ >
169
+ <button
170
+ type="button"
171
+ onClick={() => setOpenIndex(isOpen ? null : index)}
172
+ className="w-full flex items-center justify-between p-3 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-sidebar-active)]/10 transition-colors"
173
+ >
174
+ <span className="text-sm font-medium text-[var(--kyro-text-primary)] truncate">
175
+ {itemLabel}
176
+ </span>
177
+ <div className="flex items-center gap-1">
178
+ <button
179
+ type="button"
180
+ onClick={(e) => {
181
+ e.stopPropagation();
182
+ handleRemove(index);
183
+ }}
184
+ className="opacity-0 group-hover:opacity-100 p-1.5 hover:bg-[var(--kyro-danger-bg)] rounded text-[var(--kyro-error)] transition-opacity"
185
+ title="Remove"
186
+ >
187
+ <X className="w-4 h-4" />
188
+ </button>
189
+ {isOpen ? (
190
+ <ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
191
+ ) : (
192
+ <ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
193
+ )}
194
+ </div>
195
+ </button>
196
+ {isOpen && (
197
+ <div className="p-3 bg-[var(--kyro-surface)] space-y-2">
198
+ {itemKeys.length > 0 ? (
199
+ itemKeys.map((key) => (
200
+ <input
201
+ key={key}
202
+ type="text"
203
+ value={item[key] || ""}
204
+ onChange={(e) =>
205
+ handleItemChange(index, key, e.target.value)
206
+ }
207
+ className={inputClass}
208
+ placeholder={key}
209
+ />
210
+ ))
211
+ ) : (
212
+ <input
213
+ type="text"
214
+ value={item.value || ""}
215
+ onChange={(e) =>
216
+ handleItemChange(index, "value", e.target.value)
217
+ }
218
+ className={inputClass}
219
+ placeholder="Value..."
220
+ />
221
+ )}
222
+ </div>
223
+ )}
224
+ </div>
225
+ );
226
+ })}
227
+ </div>
228
+ )}
229
+ <button
230
+ type="button"
231
+ onClick={handleAdd}
232
+ className="flex items-center justify-center gap-1.5 w-full px-3 py-2 text-xs font-medium rounded-lg border border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:border-[var(--kyro-sidebar-active)] hover:text-[var(--kyro-text-primary)] transition-colors"
233
+ >
234
+ <Plus className="w-3.5 h-3.5" />
235
+ Add Item
236
+ </button>
237
+ </div>
238
+ );
239
+ };
240
+
241
+ export default ArrayField;
@@ -0,0 +1,323 @@
1
+ import React, { useState, useEffect, useCallback, useRef } from "react";
2
+ import { useBlocksStore, createNewBlock } from "./extensions/blocksStore";
3
+ import { BlockDrawer, DraggableBlockType } from "../ui/BlockDrawer";
4
+ import { Plus, Box } from "lucide-react";
5
+ import {
6
+ BLOCK_COMPONENTS,
7
+ getBlockComponent,
8
+ blockCategories,
9
+ blockIcons,
10
+ } from "./extensions/blockComponents";
11
+ import {
12
+ DndContext,
13
+ closestCenter,
14
+ PointerSensor,
15
+ useSensor,
16
+ useSensors,
17
+ KeyboardSensor,
18
+ useDraggable,
19
+ DragOverlay,
20
+ } from "@dnd-kit/core";
21
+ import type { DragEndEvent, DragStartEvent, Active } from "@dnd-kit/core";
22
+ import {
23
+ SortableContext,
24
+ useSortable,
25
+ verticalListSortingStrategy,
26
+ } from "@dnd-kit/sortable";
27
+ import { CSS } from "@dnd-kit/utilities";
28
+
29
+ interface BlocksFieldProps {
30
+ field: any;
31
+ value: any;
32
+ onChange?: (value: any) => void;
33
+ onBlocksChange?: () => void;
34
+ error?: string;
35
+ disabled?: boolean;
36
+ documentStatus?: "draft" | "published" | "scheduled" | "archived";
37
+ justSaved?: boolean;
38
+ }
39
+
40
+ import { GripVertical } from "lucide-react";
41
+
42
+ // Sortable block wrapper for drag-and-drop
43
+ const SortableBlockComponent = ({
44
+ block,
45
+ index,
46
+ }: {
47
+ block: any;
48
+ index: number;
49
+ }) => {
50
+ const {
51
+ attributes,
52
+ listeners,
53
+ setNodeRef,
54
+ transform,
55
+ transition,
56
+ isDragging,
57
+ } = useSortable({ id: block.id });
58
+
59
+ const style = {
60
+ transform: CSS.Transform.toString(transform),
61
+ transition,
62
+ zIndex: isDragging ? 10 : 1,
63
+ opacity: isDragging ? 0.8 : 1,
64
+ };
65
+
66
+ const Component = getBlockComponent(block.type);
67
+
68
+ return (
69
+ <div ref={setNodeRef} style={style} className="relative group pl-8">
70
+ <div
71
+ className="absolute left-0 top-1/2 -translate-y-1/2 p-1.5 cursor-grab active:cursor-grabbing text-[var(--kyro-text-muted)] opacity-0 group-hover:opacity-100 transition-opacity hover:bg-[var(--kyro-surface-accent)] rounded"
72
+ {...attributes}
73
+ {...listeners}
74
+ >
75
+ <GripVertical className="w-4 h-4" />
76
+ </div>
77
+ {Component ? (
78
+ <Component block={block} index={index} />
79
+ ) : (
80
+ <div className="p-4 border border-[var(--kyro-border)] rounded-md">
81
+ Unknown block: {block.type}
82
+ </div>
83
+ )}
84
+ </div>
85
+ );
86
+ };
87
+ // Memoize per-block to minimize re-renders when unrelated blocks change
88
+ const SortableBlock = React.memo(SortableBlockComponent);
89
+
90
+ export const BlocksField: React.FC<BlocksFieldProps> = ({
91
+ field,
92
+ value,
93
+ onChange,
94
+ onBlocksChange,
95
+ error,
96
+ disabled,
97
+ documentStatus,
98
+ justSaved,
99
+ }) => {
100
+ const [isDrawerOpen, setIsDrawerOpen] = useState(false);
101
+ const { blocks, setBlocks, addBlock, setOnBlocksChange } = useBlocksStore();
102
+ const [activeDrag, setActiveDrag] = useState<Active | null>(null);
103
+
104
+ // Register blocks change callback
105
+ useEffect(() => {
106
+ if (onBlocksChange) {
107
+ setOnBlocksChange(onBlocksChange);
108
+ }
109
+ return () => {
110
+ setOnBlocksChange(() => {});
111
+ };
112
+ }, [onBlocksChange, setOnBlocksChange]);
113
+
114
+ // Sync external value changes (e.g., auto-save restore) to store
115
+ useEffect(() => {
116
+ const currentIds = blocks.map((b) => b.id).join(",");
117
+ const valueIds = (value || []).map((b: any) => b.id).join(",");
118
+
119
+ // Only update if the IDs don't match (external change)
120
+ if (valueIds !== currentIds) {
121
+ if (value && Array.isArray(value) && value.length > 0) {
122
+ setBlocks(value);
123
+ } else if (!value || (Array.isArray(value) && value.length === 0)) {
124
+ setBlocks([]);
125
+ }
126
+ }
127
+ }, [value]);
128
+
129
+ // Debounced sync of store changes back to parent form to reduce re-renders
130
+ const onChangeTimer = useRef<number | null>(null);
131
+ useEffect(() => {
132
+ if (!onChange) return;
133
+ if (onChangeTimer.current) {
134
+ window.clearTimeout(onChangeTimer.current);
135
+ onChangeTimer.current = null;
136
+ }
137
+ onChangeTimer.current = window.setTimeout(() => {
138
+ onChange(blocks);
139
+ }, 250);
140
+ return () => {
141
+ if (onChangeTimer.current) {
142
+ window.clearTimeout(onChangeTimer.current);
143
+ onChangeTimer.current = null;
144
+ }
145
+ };
146
+ }, [blocks, onChange]);
147
+
148
+ // Determine left border style based on document status
149
+ const getBorderClass = () => {
150
+ if (justSaved) {
151
+ return "border-l-[3px] border-[var(--kyro-success)]";
152
+ }
153
+ if (
154
+ documentStatus === "draft" ||
155
+ documentStatus === "scheduled" ||
156
+ documentStatus === "archived"
157
+ ) {
158
+ return "border-l-[3px] border-amber-500";
159
+ }
160
+ return "";
161
+ };
162
+
163
+ const handleAddBlock = useCallback(
164
+ (blockType: string) => {
165
+ addBlock(blockType);
166
+ },
167
+ [addBlock],
168
+ );
169
+
170
+ // Set up dnd-kit sensors
171
+ const sensors = useSensors(
172
+ useSensor(PointerSensor, {
173
+ activationConstraint: {
174
+ distance: 8,
175
+ },
176
+ }),
177
+ useSensor(KeyboardSensor),
178
+ );
179
+
180
+ const handleDragStart = (event: DragStartEvent) => {
181
+ setActiveDrag(event.active);
182
+ };
183
+
184
+ const handleDragEnd = (event: DragEndEvent) => {
185
+ const { active, over } = event;
186
+ setActiveDrag(null);
187
+
188
+ if (!over) return;
189
+
190
+ // Case 1: Dragged from drawer
191
+ if (active.id.toString().startsWith("drawer-")) {
192
+ const blockType = active.id.toString().replace("drawer-", "");
193
+
194
+ // Check if dropped on a container
195
+ if (over.id.toString().startsWith("container-")) {
196
+ const containerId = over.id.toString().replace("container-", "");
197
+ const container = blocks.find((b) => b.id === containerId);
198
+ if (container) {
199
+ const { updateBlock } = useBlocksStore.getState();
200
+ const newBlock = createNewBlock(blockType);
201
+ updateBlock(containerId, {
202
+ children: [...(container.children || []), newBlock],
203
+ });
204
+ }
205
+ } else {
206
+ // Dropped on root level - add as top-level block
207
+ addBlock(blockType);
208
+ }
209
+ return;
210
+ }
211
+
212
+ // Case 2: Reordering existing blocks
213
+ if (active.id !== over.id) {
214
+ const oldIndex = blocks.findIndex((b) => b.id === active.id);
215
+ const newIndex = blocks.findIndex((b) => b.id === over.id);
216
+
217
+ if (oldIndex !== -1 && newIndex !== -1) {
218
+ const newBlocks = [...blocks];
219
+ const [movedBlock] = newBlocks.splice(oldIndex, 1);
220
+ newBlocks.splice(newIndex, 0, movedBlock);
221
+ setBlocks(newBlocks);
222
+ }
223
+ }
224
+ };
225
+
226
+ // Render active drag overlay
227
+ const activeBlock = activeDrag
228
+ ? blockCategories
229
+ .flatMap((cat) => cat.blocks)
230
+ .find((b) => `drawer-${b.type}` === activeDrag.id) ||
231
+ blocks.find((b) => b.id === activeDrag.id)
232
+ : null;
233
+
234
+ const activeBlockLabel = activeBlock
235
+ ? "label" in activeBlock
236
+ ? (activeBlock as any).label
237
+ : activeBlock.type
238
+ : "Block";
239
+
240
+ const borderClass = getBorderClass();
241
+
242
+ return (
243
+ <div className={`kyro-form-field ${borderClass}`}>
244
+ <DndContext
245
+ sensors={sensors}
246
+ collisionDetection={closestCenter}
247
+ onDragStart={handleDragStart}
248
+ onDragEnd={handleDragEnd}
249
+ >
250
+ {/* Block Builder Toolbar */}
251
+ <div className="flex items-center justify-between mb-2">
252
+ <label className="kyro-form-label">{field.label || field.name}</label>
253
+ <button
254
+ type="button"
255
+ onClick={() => setIsDrawerOpen(true)}
256
+ disabled={disabled}
257
+ className="flex items-center gap-2 px-3 py-2 text-sm text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)]/30 rounded-md transition-colors disabled:opacity-50"
258
+ >
259
+ <Plus className="w-4 h-4" />
260
+ Add Block
261
+ </button>
262
+ <BlockDrawer
263
+ open={isDrawerOpen}
264
+ onClose={() => setIsDrawerOpen(false)}
265
+ onSelect={handleAddBlock}
266
+ >
267
+ <div className="space-y-4">
268
+ {blockCategories.map((category) => (
269
+ <div key={category.title}>
270
+ <h3 className="text-xs font-semibold text-[var(--kyro-text-muted)] mb-2 uppercase tracking-wider">
271
+ {category.title}
272
+ </h3>
273
+ <div className="grid grid-cols-3 gap-2">
274
+ {category.blocks.map((block) => (
275
+ <DraggableBlockType
276
+ key={block.type}
277
+ block={block}
278
+ onSelect={handleAddBlock}
279
+ >
280
+ <div className="w-6 h-6 flex items-center justify-center rounded group-hover:bg-[var(--kyro-primary)]/10 group-hover:text-[var(--kyro-primary)] transition-all duration-300">
281
+ <span className="text-[var(--kyro-text-muted)]">
282
+ {blockIcons[
283
+ block.icon as keyof typeof blockIcons
284
+ ] || <Box className="w-4 h-4" />}
285
+ </span>
286
+ </div>
287
+ </DraggableBlockType>
288
+ ))}
289
+ </div>
290
+ </div>
291
+ ))}
292
+ </div>
293
+ </BlockDrawer>
294
+ </div>
295
+
296
+ {/* Block List with Drag-and-Drop */}
297
+ <SortableContext
298
+ items={blocks.map((b) => b.id)}
299
+ strategy={verticalListSortingStrategy}
300
+ >
301
+ <div className="space-y-4">
302
+ {blocks.map((block, index) => (
303
+ <SortableBlock key={block.id} block={block} index={index} />
304
+ ))}
305
+ {blocks.length === 0 && (
306
+ <div className="text-center py-12 text-[var(--kyro-text-muted)] border-2 border-dashed border-[var(--kyro-border)] rounded-lg">
307
+ Click the button above to add your first block
308
+ </div>
309
+ )}
310
+ </div>
311
+ </SortableContext>
312
+ <DragOverlay>
313
+ {activeDrag && activeBlock && (
314
+ <div className="bg-[var(--kyro-surface)] border border-[var(--kyro-primary)] rounded-md p-3 shadow-lg">
315
+ {(activeBlock as any).label || activeBlock.type || "Block"}
316
+ </div>
317
+ )}
318
+ </DragOverlay>
319
+ </DndContext>
320
+ {error && <p className="kyro-form-error">{error}</p>}
321
+ </div>
322
+ );
323
+ };
@@ -0,0 +1,53 @@
1
+ import React from "react";
2
+ import { ExternalLink } from "lucide-react";
3
+
4
+ interface ButtonFieldProps {
5
+ text?: string;
6
+ url?: string;
7
+ onChange: (field: string, value: string) => void;
8
+ compact?: boolean;
9
+ }
10
+
11
+ export const ButtonField: React.FC<ButtonFieldProps> = ({
12
+ text = "Button",
13
+ url = "",
14
+ onChange,
15
+ compact = false,
16
+ }) => {
17
+ const inputClass = compact
18
+ ? "flex-1 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"
19
+ : "flex-1 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";
20
+
21
+ return (
22
+ <div className="flex items-center gap-2">
23
+ <input
24
+ type="text"
25
+ value={text}
26
+ onChange={(e) => onChange("text", e.target.value)}
27
+ className={inputClass}
28
+ placeholder="Button text..."
29
+ />
30
+ <span className="text-[var(--kyro-text-muted)] text-xs">→</span>
31
+ <input
32
+ type="url"
33
+ value={url}
34
+ onChange={(e) => onChange("url", e.target.value)}
35
+ className={`${inputClass} font-mono text-xs`}
36
+ placeholder="https://..."
37
+ />
38
+ {text && url && (
39
+ <a
40
+ href={url}
41
+ target="_blank"
42
+ rel="noopener noreferrer"
43
+ className={`shrink-0 ${compact ? "p-1.5" : "p-2"} rounded text-[var(--kyro-text-muted)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)] transition-colors`}
44
+ title={url}
45
+ >
46
+ <ExternalLink className={compact ? "w-3.5 h-3.5" : "w-4 h-4"} />
47
+ </a>
48
+ )}
49
+ </div>
50
+ );
51
+ };
52
+
53
+ export default ButtonField;
@@ -1,4 +1,4 @@
1
- import type { CheckboxField as CheckboxFieldType } from '@kyro-cms/core';
1
+ import type { CheckboxField as CheckboxFieldType } from "@kyro-cms/core/client";
2
2
 
3
3
  interface CheckboxFieldComponentProps {
4
4
  field: CheckboxFieldType;
@@ -8,7 +8,13 @@ interface CheckboxFieldComponentProps {
8
8
  disabled?: boolean;
9
9
  }
10
10
 
11
- export default function CheckboxField({ field, value = false, onChange, error, disabled }: CheckboxFieldComponentProps) {
11
+ export default function CheckboxField({
12
+ field,
13
+ value = false,
14
+ onChange,
15
+ error,
16
+ disabled,
17
+ }: CheckboxFieldComponentProps) {
12
18
  return (
13
19
  <div className="space-y-1">
14
20
  <label className="flex items-center gap-2 cursor-pointer">
@@ -17,20 +23,24 @@ export default function CheckboxField({ field, value = false, onChange, error, d
17
23
  checked={value}
18
24
  onChange={(e) => onChange?.(e.target.checked)}
19
25
  disabled={disabled || field.admin?.readOnly}
20
- className={`w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 ${
21
- disabled || field.admin?.readOnly ? 'opacity-50' : ''
26
+ className={`w-4 h-4 rounded border-[var(--kyro-border)] text-[var(--kyro-sidebar-active)] focus:ring-[var(--kyro-sidebar-active)] ${
27
+ disabled || field.admin?.readOnly ? "opacity-50" : ""
22
28
  }`}
23
29
  />
24
- <span className="text-sm font-medium text-gray-700">
30
+ <span className="text-sm font-medium text-[var(--kyro-text-primary)]">
25
31
  {field.label || field.name}
26
- {field.required && <span className="text-red-500 ml-1">*</span>}
32
+ {field.required && (
33
+ <span className="text-[var(--kyro-error)] ml-1">*</span>
34
+ )}
27
35
  </span>
28
36
  </label>
29
37
  {field.admin?.description && !error && (
30
- <p className="text-xs text-gray-500 ml-6">{field.admin.description}</p>
38
+ <p className="text-xs text-[var(--kyro-text-secondary)] ml-6">
39
+ {field.admin.description}
40
+ </p>
31
41
  )}
32
42
  {error && (
33
- <p className="text-xs text-red-600 ml-6">{error}</p>
43
+ <p className="text-xs text-[var(--kyro-error)] ml-6">{error}</p>
34
44
  )}
35
45
  </div>
36
46
  );