@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,551 @@
1
+ import React, { useState } from "react";
2
+ import { Plus, X, ChevronRight, ChevronDown } from "lucide-react";
3
+ import {
4
+ blockCategories,
5
+ blockIcons,
6
+ getBlockComponent,
7
+ getBlockLabel,
8
+ } from "../fields/extensions/blockComponents";
9
+ import { createNewBlock } from "../fields/extensions/blocksStore";
10
+ import { BlockDrawer } from "../ui/BlockDrawer";
11
+ import { BlockEditModal } from "./BlockEditModal";
12
+
13
+ interface ChildBlocksTreeProps {
14
+ blockId: string;
15
+ children: any[];
16
+ onUpdateChildren: (children: any[]) => void;
17
+ depth?: number;
18
+ maxDepth?: number;
19
+ }
20
+
21
+ const MAX_DEPTH = 6;
22
+
23
+ export const ChildBlocksTree: React.FC<ChildBlocksTreeProps> = ({
24
+ blockId,
25
+ children,
26
+ onUpdateChildren,
27
+ depth = 0,
28
+ maxDepth = MAX_DEPTH,
29
+ }) => {
30
+ const [showAddModal, setShowAddModal] = useState(false);
31
+ const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
32
+ const [editingBlockId, setEditingBlockId] = useState<string | null>(null);
33
+ const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
34
+
35
+ const availableBlocks = blockCategories.flatMap((cat) => cat.blocks);
36
+ const canAddChildren = depth < maxDepth;
37
+ const indentWidth = 16;
38
+
39
+ const handleAddChild = (type: string) => {
40
+ const newChild = createNewBlock(type);
41
+ onUpdateChildren([...children, newChild]);
42
+ setEditingBlockId(newChild.id);
43
+ };
44
+
45
+ const handleRemoveChild = (childId: string) => {
46
+ const filtered = children.filter((c) => c.id !== childId);
47
+ onUpdateChildren(filtered);
48
+ };
49
+
50
+ const handleUpdateChildData = (childId: string, newData: any) => {
51
+ const updated = children.map((child) => {
52
+ if (child.id === childId) {
53
+ return { ...child, data: newData };
54
+ }
55
+ return child;
56
+ });
57
+ onUpdateChildren(updated);
58
+ };
59
+
60
+ const handleUpdateChildChildren = (
61
+ childId: string,
62
+ newGrandchildren: any[],
63
+ ) => {
64
+ const updated = children.map((child) => {
65
+ if (child.id === childId) {
66
+ return { ...child, children: newGrandchildren };
67
+ }
68
+ return child;
69
+ });
70
+ onUpdateChildren(updated);
71
+ };
72
+
73
+ const toggleExpand = (id: string) => {
74
+ setExpandedIds((prev) => {
75
+ const next = new Set(prev);
76
+ if (next.has(id)) {
77
+ next.delete(id);
78
+ } else {
79
+ next.add(id);
80
+ }
81
+ return next;
82
+ });
83
+ };
84
+
85
+ const renderBlock = (child: any) => {
86
+ const hasChildren = child.children && child.children.length > 0;
87
+ const isExpanded = expandedIds.has(child.id);
88
+ const BlockComponent = getBlockComponent(child.type);
89
+ const childHasOwnChildren = hasChildren;
90
+ const isEditing = editingBlockId === child.id;
91
+
92
+ return (
93
+ <div key={child.id} className="relative group">
94
+ <div
95
+ className={`flex items-center group/column gap-2 p-2 bg-[var(--kyro-bg-secondary)] rounded border transition-colors ${
96
+ isEditing
97
+ ? "bg-[var(--kyro-primary)]/10 border-[var(--kyro-primary)]"
98
+ : "border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/50 hover:bg-[var(--kyro-primary)]/5"
99
+ } ${canAddChildren ? "cursor-pointer" : ""}`}
100
+ style={{ marginLeft: depth * indentWidth }}
101
+ onClick={() => {
102
+ if (canAddChildren) {
103
+ setEditingBlockId(isEditing ? null : child.id);
104
+ }
105
+ }}
106
+ >
107
+ {childHasOwnChildren ? (
108
+ <button
109
+ type="button"
110
+ onClick={(e) => {
111
+ e.stopPropagation();
112
+ toggleExpand(child.id);
113
+ }}
114
+ className="p-0.5 hover:bg-[var(--kyro-surface-accent)] rounded"
115
+ >
116
+ {isExpanded ? (
117
+ <ChevronDown className="w-3 h-3 text-[var(--kyro-text-muted)]" />
118
+ ) : (
119
+ <ChevronRight className="w-3 h-3 text-[var(--kyro-text-muted)]" />
120
+ )}
121
+ </button>
122
+ ) : (
123
+ <span className="w-4" />
124
+ )}
125
+
126
+ {blockIcons[child.type] && (
127
+ <span className="text-[var(--kyro-text-secondary)]">
128
+ {blockIcons[child.type]}
129
+ </span>
130
+ )}
131
+
132
+ <span className="text-xs font-medium text-[var(--kyro-text-secondary)] flex-1 truncate">
133
+ {getBlockLabel(child.type)}
134
+ {child.data?.text ? ` - ${child.data.text.slice(0, 30)}` : ""}
135
+ {child.data?.heading ? ` - ${child.data.heading.slice(0, 30)}` : ""}
136
+ </span>
137
+
138
+ {hasChildren && (
139
+ <span className="text-[10px] text-[var(--kyro-text-muted)]">
140
+ ({child.children.length})
141
+ </span>
142
+ )}
143
+
144
+ {confirmDeleteId === child.id ? (
145
+ <div
146
+ className="flex items-center gap-1"
147
+ onClick={(e) => e.stopPropagation()}
148
+ >
149
+ <button
150
+ type="button"
151
+ onClick={() => {
152
+ handleRemoveChild(child.id);
153
+ setConfirmDeleteId(null);
154
+ }}
155
+ className="px-2 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600"
156
+ >
157
+ Remove
158
+ </button>
159
+ <button
160
+ type="button"
161
+ onClick={() => setConfirmDeleteId(null)}
162
+ className="px-2 py-1 text-xs bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] rounded hover:bg-[var(--kyro-border)]"
163
+ >
164
+ Cancel
165
+ </button>
166
+ </div>
167
+ ) : (
168
+ <button
169
+ type="button"
170
+ onClick={(e) => {
171
+ e.stopPropagation();
172
+ setConfirmDeleteId(child.id);
173
+ }}
174
+ className="p-1.5 rounded-md transition-opacity cursor-pointer hover:bg-red-50"
175
+ >
176
+ <X className="w-3.5 h-3.5 text-red-500 invisible group-hover/column:visible" />
177
+ </button>
178
+ )}
179
+ </div>
180
+
181
+ {isEditing && (
182
+ <BlockEditModal
183
+ block={child}
184
+ onClose={() => setEditingBlockId(null)}
185
+ />
186
+ )}
187
+
188
+ {hasChildren && isExpanded && (
189
+ <div className="mt-1">
190
+ <NestedChildBlocks
191
+ parentId={child.id}
192
+ children={child.children}
193
+ onUpdateChildren={(newGrandchildren) =>
194
+ handleUpdateChildChildren(child.id, newGrandchildren)
195
+ }
196
+ depth={depth + 1}
197
+ maxDepth={maxDepth}
198
+ />
199
+ </div>
200
+ )}
201
+ </div>
202
+ );
203
+ };
204
+
205
+ return (
206
+ <div className="space-y-2">
207
+ {children.length > 0 && (
208
+ <div className="space-y-1">{children.map(renderBlock)}</div>
209
+ )}
210
+
211
+ {canAddChildren && (
212
+ <div style={{ marginLeft: depth * indentWidth }}>
213
+ <button
214
+ type="button"
215
+ onClick={() => setShowAddModal(true)}
216
+ className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)] rounded transition-colors"
217
+ >
218
+ <Plus className="w-3 h-3" />
219
+ Add Block
220
+ </button>
221
+
222
+ <BlockDrawer
223
+ open={showAddModal}
224
+ onClose={() => setShowAddModal(false)}
225
+ onSelect={handleAddChild}
226
+ >
227
+ {blockCategories.map((category) => (
228
+ <div key={category.title} className="mb-4">
229
+ <h3 className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase tracking-wide mb-2">
230
+ {category.title}
231
+ </h3>
232
+ <div className="grid grid-cols-3 gap-2">
233
+ {category.blocks.map((block) => (
234
+ <button
235
+ key={block.type}
236
+ type="button"
237
+ onClick={() => {
238
+ handleAddChild(block.type);
239
+ setShowAddModal(false);
240
+ }}
241
+ className="flex flex-col items-center text-center gap-1 p-2 rounded-md border border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/60 hover:bg-[var(--kyro-surface-accent)]/30 transition-all cursor-pointer group"
242
+ >
243
+ <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">
244
+ {blockIcons[block.icon]}
245
+ </div>
246
+ <div className="flex-1 min-w-0">
247
+ <div className="text-xs font-medium uppercase tracking-tight text-[var(--kyro-text-primary)]">
248
+ {block.label}
249
+ </div>
250
+ <div className="text-[10px] text-[var(--kyro-text-muted)] mt-0.5">
251
+ {block.description}
252
+ </div>
253
+ </div>
254
+ </button>
255
+ ))}
256
+ </div>
257
+ </div>
258
+ ))}
259
+ </BlockDrawer>
260
+ </div>
261
+ )}
262
+
263
+ {children.length === 0 && canAddChildren && (
264
+ <div
265
+ className="text-xs text-[var(--kyro-text-muted)] italic py-2"
266
+ style={{ marginLeft: depth * indentWidth }}
267
+ >
268
+ No blocks added. Click "Add Block" to add elements.
269
+ </div>
270
+ )}
271
+
272
+ {depth >= maxDepth && children.length > 0 && (
273
+ <div
274
+ className="text-xs text-[var(--kyro-text-muted)] italic"
275
+ style={{ marginLeft: depth * indentWidth }}
276
+ >
277
+ Maximum nesting level ({maxDepth}) reached
278
+ </div>
279
+ )}
280
+ </div>
281
+ );
282
+ };
283
+
284
+ interface NestedChildBlocksProps {
285
+ parentId: string;
286
+ children: any[];
287
+ onUpdateChildren: (children: any[]) => void;
288
+ depth: number;
289
+ maxDepth: number;
290
+ }
291
+
292
+ const NestedChildBlocks: React.FC<NestedChildBlocksProps> = ({
293
+ parentId,
294
+ children,
295
+ onUpdateChildren,
296
+ depth,
297
+ maxDepth,
298
+ }) => {
299
+ const [showAddModal, setShowAddModal] = useState(false);
300
+ const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
301
+ const [editingBlockId, setEditingBlockId] = useState<string | null>(null);
302
+ const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
303
+
304
+ const availableBlocks = blockCategories.flatMap((cat) => cat.blocks);
305
+ const canAddChildren = depth < maxDepth;
306
+ const indentWidth = 16;
307
+
308
+ const handleAddChild = (type: string) => {
309
+ const newChild = createNewBlock(type);
310
+ onUpdateChildren([...children, newChild]);
311
+ setEditingBlockId(newChild.id);
312
+ };
313
+
314
+ const handleRemoveChild = (childId: string) => {
315
+ const filtered = children.filter((c) => c.id !== childId);
316
+ onUpdateChildren(filtered);
317
+ };
318
+
319
+ const handleUpdateChildData = (childId: string, newData: any) => {
320
+ const updated = children.map((child) => {
321
+ if (child.id === childId) {
322
+ return { ...child, data: newData };
323
+ }
324
+ return child;
325
+ });
326
+ onUpdateChildren(updated);
327
+ };
328
+
329
+ const handleUpdateChildChildren = (
330
+ childId: string,
331
+ newGrandchildren: any[],
332
+ ) => {
333
+ const updated = children.map((child) => {
334
+ if (child.id === childId) {
335
+ return { ...child, children: newGrandchildren };
336
+ }
337
+ return child;
338
+ });
339
+ onUpdateChildren(updated);
340
+ };
341
+
342
+ const toggleExpand = (id: string) => {
343
+ setExpandedIds((prev) => {
344
+ const next = new Set(prev);
345
+ if (next.has(id)) {
346
+ next.delete(id);
347
+ } else {
348
+ next.add(id);
349
+ }
350
+ return next;
351
+ });
352
+ };
353
+
354
+ const renderBlock = (child: any) => {
355
+ const hasChildren = child.children && child.children.length > 0;
356
+ const isExpanded = expandedIds.has(child.id);
357
+ const BlockComponent = getBlockComponent(child.type);
358
+ const childHasOwnChildren = hasChildren;
359
+ const isEditing = editingBlockId === child.id;
360
+
361
+ return (
362
+ <div key={child.id} className="relative group">
363
+ <div
364
+ className={`flex items-center gap-2 p-2 bg-[var(--kyro-bg-secondary)] rounded border transition-colors ${
365
+ isEditing
366
+ ? "bg-[var(--kyro-primary)]/10 border-[var(--kyro-primary)]"
367
+ : "border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/50 hover:bg-[var(--kyro-primary)]/5"
368
+ } ${canAddChildren ? "cursor-pointer" : ""}`}
369
+ style={{ marginLeft: depth * indentWidth }}
370
+ onClick={() => {
371
+ if (canAddChildren) {
372
+ setEditingBlockId(isEditing ? null : child.id);
373
+ }
374
+ }}
375
+ >
376
+ {childHasOwnChildren ? (
377
+ <button
378
+ type="button"
379
+ onClick={(e) => {
380
+ e.stopPropagation();
381
+ toggleExpand(child.id);
382
+ }}
383
+ className="p-0.5 hover:bg-[var(--kyro-surface-accent)] rounded"
384
+ >
385
+ {isExpanded ? (
386
+ <ChevronDown className="w-3 h-3 text-[var(--kyro-text-muted)]" />
387
+ ) : (
388
+ <ChevronRight className="w-3 h-3 text-[var(--kyro-text-muted)]" />
389
+ )}
390
+ </button>
391
+ ) : (
392
+ <span className="w-4" />
393
+ )}
394
+
395
+ {blockIcons[child.type] && (
396
+ <span className="text-[var(--kyro-text-secondary)]">
397
+ {blockIcons[child.type]}
398
+ </span>
399
+ )}
400
+
401
+ <span className="text-xs font-medium text-[var(--kyro-text-secondary)] flex-1 truncate">
402
+ {getBlockLabel(child.type)}
403
+ {child.data?.text ? ` - ${child.data.text.slice(0, 30)}` : ""}
404
+ {child.data?.heading ? ` - ${child.data.heading.slice(0, 30)}` : ""}
405
+ </span>
406
+
407
+ {hasChildren && (
408
+ <span className="text-[10px] text-[var(--kyro-text-muted)]">
409
+ ({child.children.length})
410
+ </span>
411
+ )}
412
+
413
+ {confirmDeleteId === child.id ? (
414
+ <div
415
+ className="flex items-center gap-1"
416
+ onClick={(e) => e.stopPropagation()}
417
+ >
418
+ <button
419
+ type="button"
420
+ onClick={() => {
421
+ handleRemoveChild(child.id);
422
+ setConfirmDeleteId(null);
423
+ }}
424
+ className="px-2 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600"
425
+ >
426
+ Remove
427
+ </button>
428
+ <button
429
+ type="button"
430
+ onClick={() => setConfirmDeleteId(null)}
431
+ className="px-2 py-1 text-xs bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] rounded hover:bg-[var(--kyro-border)]"
432
+ >
433
+ Cancel
434
+ </button>
435
+ </div>
436
+ ) : (
437
+ <button
438
+ type="button"
439
+ onClick={(e) => {
440
+ e.stopPropagation();
441
+ setConfirmDeleteId(child.id);
442
+ }}
443
+ className="p-1.5 rounded-md invisible group-hover:visible transition-opacity cursor-pointer hover:bg-red-50"
444
+ >
445
+ <X className="w-3.5 h-3.5 text-red-500" />
446
+ </button>
447
+ )}
448
+ </div>
449
+
450
+ {isEditing && (
451
+ <BlockEditModal
452
+ block={child}
453
+ onClose={() => setEditingBlockId(null)}
454
+ />
455
+ )}
456
+
457
+ {hasChildren && isExpanded && (
458
+ <div className="mt-1">
459
+ <NestedChildBlocks
460
+ parentId={child.id}
461
+ children={child.children}
462
+ onUpdateChildren={(newGrandchildren) =>
463
+ handleUpdateChildChildren(child.id, newGrandchildren)
464
+ }
465
+ depth={depth + 1}
466
+ maxDepth={maxDepth}
467
+ />
468
+ </div>
469
+ )}
470
+ </div>
471
+ );
472
+ };
473
+
474
+ return (
475
+ <div className="space-y-2">
476
+ {children.length > 0 && (
477
+ <div className="space-y-1">{children.map(renderBlock)}</div>
478
+ )}
479
+
480
+ {canAddChildren && (
481
+ <div style={{ marginLeft: depth * indentWidth }}>
482
+ <button
483
+ type="button"
484
+ onClick={() => setShowAddModal(true)}
485
+ className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)] rounded transition-colors"
486
+ >
487
+ <Plus className="w-3 h-3" />
488
+ Add Block
489
+ </button>
490
+
491
+ <BlockDrawer
492
+ open={showAddModal}
493
+ onClose={() => setShowAddModal(false)}
494
+ onSelect={handleAddChild}
495
+ >
496
+ {blockCategories.map((category) => (
497
+ <div key={category.title} className="mb-4">
498
+ <h3 className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase tracking-wide mb-2">
499
+ {category.title}
500
+ </h3>
501
+ <div className="grid grid-cols-3 gap-2">
502
+ {category.blocks.map((block) => (
503
+ <button
504
+ key={block.type}
505
+ type="button"
506
+ onClick={() => {
507
+ handleAddChild(block.type);
508
+ setShowAddModal(false);
509
+ }}
510
+ className="flex flex-col items-center text-center gap-1 p-2 rounded-md border border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/60 hover:bg-[var(--kyro-surface-accent)]/30 transition-all cursor-pointer group"
511
+ >
512
+ <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">
513
+ {blockIcons[block.icon]}
514
+ </div>
515
+ <div className="flex-1 min-w-0">
516
+ <div className="text-xs font-medium uppercase tracking-tight text-[var(--kyro-text-primary)]">
517
+ {block.label}
518
+ </div>
519
+ <div className="text-[10px] text-[var(--kyro-text-muted)] mt-0.5">
520
+ {block.description}
521
+ </div>
522
+ </div>
523
+ </button>
524
+ ))}
525
+ </div>
526
+ </div>
527
+ ))}
528
+ </BlockDrawer>
529
+ </div>
530
+ )}
531
+
532
+ {children.length === 0 && canAddChildren && (
533
+ <div
534
+ className="text-xs text-[var(--kyro-text-muted)] italic py-2"
535
+ style={{ marginLeft: depth * indentWidth }}
536
+ >
537
+ No blocks added. Click "Add Block" to add elements.
538
+ </div>
539
+ )}
540
+
541
+ {depth >= maxDepth && children.length > 0 && (
542
+ <div
543
+ className="text-xs text-[var(--kyro-text-muted)] italic"
544
+ style={{ marginLeft: depth * indentWidth }}
545
+ >
546
+ Maximum nesting level ({maxDepth}) reached
547
+ </div>
548
+ )}
549
+ </div>
550
+ );
551
+ };
@@ -0,0 +1,114 @@
1
+ import React from "react";
2
+ import {
3
+ useBlockById,
4
+ useBlockActions,
5
+ } from "../fields/extensions/blocksStore";
6
+ import { ChevronRight, X, Code2 } from "lucide-react";
7
+ import { CodeField } from "../fields/CodeField";
8
+
9
+ export const CodeBlock: React.FC<{ block: any; index: number }> = ({
10
+ block,
11
+ index,
12
+ }) => {
13
+ const blockData = useBlockById(block.id);
14
+ const { updateBlock, removeBlock, moveBlock } = useBlockActions();
15
+ const data = blockData?.data ?? block.data ?? {};
16
+
17
+ const handleChange = (field: string, value: any) => {
18
+ updateBlock(block.id, { data: { ...data, [field]: value } });
19
+ };
20
+
21
+ return (
22
+ <div className="group/block relative border border-[var(--kyro-border)] rounded-2xl p-6 mb-6 transition-all duration-300 bg-[var(--kyro-surface)] hover:border-[var(--kyro-primary)]/20">
23
+ <div className="flex items-center justify-between mb-6">
24
+ <div className="flex items-center gap-3">
25
+ <div className="w-9 h-9 rounded-xl bg-[var(--kyro-primary)]/10 flex items-center justify-center text-[var(--kyro-primary)] transition-transform group-hover/block:scale-110">
26
+ <Code2 className="w-5 h-5" />
27
+ </div>
28
+ <div>
29
+ <h4 className="text-sm font-bold tracking-tight text-[var(--kyro-text-primary)]">Code Snippet</h4>
30
+ <p className="text-[10px] font-medium text-[var(--kyro-text-muted)] uppercase tracking-widest">
31
+ Block Editor • {data.language || "javascript"}
32
+ </p>
33
+ </div>
34
+ </div>
35
+
36
+ <div className="flex items-center gap-1.5 opacity-0 group-hover/block:opacity-100 transition-all translate-x-2 group-hover/block:translate-x-0">
37
+ <div className="flex bg-[var(--kyro-surface-accent)]/50 p-1 rounded-xl border border-[var(--kyro-border)]">
38
+ <button
39
+ type="button"
40
+ onClick={() => moveBlock(block.id, "up")}
41
+ className="p-1.5 hover:bg-[var(--kyro-surface)] rounded-lg transition-all text-[var(--kyro-text-muted)] hover:text-[var(--kyro-primary)]"
42
+ title="Move up"
43
+ >
44
+ <ChevronRight className="w-4 h-4 rotate-[-90deg]" />
45
+ </button>
46
+ <div className="w-px h-4 bg-[var(--kyro-border)] mx-1 self-center" />
47
+ <button
48
+ type="button"
49
+ onClick={() => removeBlock(block.id)}
50
+ className="p-1.5 hover:bg-red-500/10 rounded-lg transition-all text-[var(--kyro-text-muted)] hover:text-red-500"
51
+ title="Remove"
52
+ >
53
+ <X className="w-4 h-4" />
54
+ </button>
55
+ </div>
56
+ </div>
57
+ </div>
58
+
59
+ <div className="space-y-6">
60
+ <CodeField
61
+ field={{
62
+ type: "code",
63
+ name: "code",
64
+ label: "Source Code",
65
+ language: data.language || "javascript"
66
+ }}
67
+ value={data.code || ""}
68
+ onChange={(val) => handleChange("code", val)}
69
+ />
70
+
71
+ <div className="flex items-center gap-4 bg-[var(--kyro-surface-accent)]/20 p-4 rounded-xl border border-[var(--kyro-border)]/50">
72
+ <div className="flex-1">
73
+ <label className="text-[10px] font-black uppercase tracking-widest text-[var(--kyro-text-muted)] mb-2 block">
74
+ Syntax Highlighting
75
+ </label>
76
+ <div className="relative">
77
+ <select
78
+ value={data.language || "javascript"}
79
+ onChange={(e) => handleChange("language", e.target.value)}
80
+ className="w-full pl-4 pr-10 py-2.5 bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-xl text-xs font-bold text-[var(--kyro-text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--kyro-primary)]/20 focus:border-[var(--kyro-primary)] transition-all appearance-none cursor-pointer"
81
+ >
82
+ <option value="plaintext">Plain Text</option>
83
+ <option value="javascript">JavaScript</option>
84
+ <option value="typescript">TypeScript</option>
85
+ <option value="python">Python</option>
86
+ <option value="json">JSON</option>
87
+ <option value="html">HTML</option>
88
+ <option value="css">CSS</option>
89
+ <option value="sql">SQL</option>
90
+ <option value="rust">Rust</option>
91
+ <option value="markdown">Markdown</option>
92
+ </select>
93
+ <div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-[var(--kyro-text-muted)]">
94
+ <ChevronRight className="w-4 h-4 rotate-90" />
95
+ </div>
96
+ </div>
97
+ </div>
98
+
99
+ <div className="hidden sm:block w-px h-10 bg-[var(--kyro-border)]" />
100
+
101
+ <div className="hidden sm:flex flex-col justify-center">
102
+ <span className="text-[10px] font-black uppercase tracking-widest text-[var(--kyro-text-muted)] mb-2">
103
+ Status
104
+ </span>
105
+ <div className="flex items-center gap-2 px-3 py-2 bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-xl">
106
+ <div className="w-2 h-2 rounded-full bg-blue-500 animate-pulse" />
107
+ <span className="text-[10px] font-bold text-[var(--kyro-text-primary)] tracking-wide">EDITING</span>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ );
114
+ };