@kyro-cms/admin 0.1.6 → 0.1.7

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 (163) hide show
  1. package/README.md +149 -51
  2. package/package.json +53 -6
  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 +136 -27
  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 +1417 -661
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +3 -3
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +199 -57
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +786 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +191 -53
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +149 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/UserManagement.tsx +204 -0
  26. package/src/components/VersionHistoryPanel.tsx +3 -3
  27. package/src/components/WebhookManager.tsx +608 -0
  28. package/src/components/blocks/AccordionBlock.tsx +97 -0
  29. package/src/components/blocks/ArrayBlock.tsx +75 -0
  30. package/src/components/blocks/BlockEditModal.MARKER +12 -0
  31. package/src/components/blocks/BlockEditModal.tsx +774 -0
  32. package/src/components/blocks/ButtonBlock.tsx +165 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +66 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +151 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +64 -0
  38. package/src/components/blocks/HeadingBlock.tsx +81 -0
  39. package/src/components/blocks/HeroBlock.tsx +157 -0
  40. package/src/components/blocks/ImageBlock.tsx +83 -0
  41. package/src/components/blocks/LinkBlock.tsx +71 -0
  42. package/src/components/blocks/ListBlock.tsx +39 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +279 -0
  45. package/src/components/blocks/VStackBlock.tsx +75 -0
  46. package/src/components/blocks/VideoBlock.tsx +45 -0
  47. package/src/components/blocks/index.ts +10 -0
  48. package/src/components/fields/BlocksField.tsx +323 -0
  49. package/src/components/fields/CheckboxField.tsx +15 -9
  50. package/src/components/fields/CodeField.tsx +234 -0
  51. package/src/components/fields/DateField.tsx +38 -11
  52. package/src/components/fields/EditorClient.tsx +271 -0
  53. package/src/components/fields/FileField.tsx +390 -0
  54. package/src/components/fields/HybridContentField.tsx +109 -0
  55. package/src/components/fields/ImageField.tsx +429 -0
  56. package/src/components/fields/JSONField.tsx +361 -0
  57. package/src/components/fields/MarkdownField.tsx +282 -0
  58. package/src/components/fields/NumberField.tsx +42 -12
  59. package/src/components/fields/PortableTextField.tsx +143 -0
  60. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  61. package/src/components/fields/RelationshipField.tsx +231 -59
  62. package/src/components/fields/SelectField.tsx +25 -15
  63. package/src/components/fields/TextField.tsx +45 -14
  64. package/src/components/fields/extensions/blockComponents.tsx +237 -0
  65. package/src/components/fields/extensions/blocksStore.ts +273 -0
  66. package/src/components/fields/index.ts +13 -0
  67. package/src/components/index.ts +1 -2
  68. package/src/components/layout/Header.tsx +2 -2
  69. package/src/components/layout/Layout.tsx +2 -2
  70. package/src/components/ui/Badge.tsx +9 -4
  71. package/src/components/ui/BlockDrawer.tsx +79 -0
  72. package/src/components/ui/Button.tsx +1 -1
  73. package/src/components/ui/CommandPalette.tsx +362 -0
  74. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  75. package/src/components/ui/Dropdown.tsx +1 -1
  76. package/src/components/ui/Modal.tsx +37 -12
  77. package/src/components/ui/PromptModal.tsx +94 -0
  78. package/src/components/ui/SlidePanel.tsx +43 -16
  79. package/src/components/ui/Toast.tsx +80 -14
  80. package/src/env.d.ts +16 -0
  81. package/src/env.ts +20 -0
  82. package/src/index.ts +0 -1
  83. package/src/layouts/AdminLayout.astro +164 -170
  84. package/src/layouts/AuthLayout.astro +23 -6
  85. package/src/lib/MediaService.ts +541 -0
  86. package/src/lib/auth/sqlite-adapter.ts +319 -0
  87. package/src/lib/config.ts +22 -6
  88. package/src/lib/dataStore.ts +132 -74
  89. package/src/lib/db/adapter.ts +54 -0
  90. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  91. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  92. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  93. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  94. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  95. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  96. package/src/lib/db/index.ts +449 -0
  97. package/src/lib/db/mongodb-adapter.ts +207 -0
  98. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  99. package/src/lib/db/schema/mysql-auth.ts +113 -0
  100. package/src/lib/db/schema/mysql-content.ts +20 -0
  101. package/src/lib/db/schema/postgres-auth.ts +116 -0
  102. package/src/lib/db/schema/postgres-content.ts +35 -0
  103. package/src/lib/db/schema/postgres-media.ts +52 -0
  104. package/src/lib/db/schema/postgres-settings.ts +11 -0
  105. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  106. package/src/lib/db/schema/sqlite-content.ts +20 -0
  107. package/src/lib/graphql/index.ts +1 -0
  108. package/src/lib/graphql/schema.ts +443 -0
  109. package/src/lib/rate-limit.ts +267 -0
  110. package/src/lib/storage.ts +374 -0
  111. package/src/lib/store.ts +85 -0
  112. package/src/middleware.ts +70 -11
  113. package/src/pages/[collection]/[id].astro +178 -122
  114. package/src/pages/[collection]/index.astro +24 -156
  115. package/src/pages/admin/api-explorer.astro +98 -0
  116. package/src/pages/admin/graphql-explorer.astro +40 -0
  117. package/src/pages/admin/graphql.astro +97 -0
  118. package/src/pages/admin/index.astro +200 -139
  119. package/src/pages/admin/keys.astro +8 -0
  120. package/src/pages/admin/rest-playground.astro +44 -0
  121. package/src/pages/admin/webhooks.astro +8 -0
  122. package/src/pages/api/[collection]/[id]/publish.ts +44 -0
  123. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  124. package/src/pages/api/[collection]/[id]/versions.ts +36 -0
  125. package/src/pages/api/[collection]/[id].ts +102 -159
  126. package/src/pages/api/[collection]/index.ts +151 -230
  127. package/src/pages/api/auth/[id].ts +48 -69
  128. package/src/pages/api/auth/audit-logs.ts +20 -43
  129. package/src/pages/api/auth/login.ts +159 -45
  130. package/src/pages/api/auth/logout.ts +42 -24
  131. package/src/pages/api/auth/refresh.ts +119 -0
  132. package/src/pages/api/auth/register.ts +110 -40
  133. package/src/pages/api/auth/users.ts +22 -97
  134. package/src/pages/api/collections.ts +59 -0
  135. package/src/pages/api/globals/[slug]/test.ts +172 -0
  136. package/src/pages/api/globals/[slug].ts +42 -0
  137. package/src/pages/api/graphql.ts +90 -0
  138. package/src/pages/api/health.ts +417 -40
  139. package/src/pages/api/keys/[id].ts +26 -0
  140. package/src/pages/api/keys/index.ts +75 -0
  141. package/src/pages/api/media/[id].ts +309 -0
  142. package/src/pages/api/media/folders.ts +609 -0
  143. package/src/pages/api/media/index.ts +146 -0
  144. package/src/pages/api/media/resize.ts +267 -0
  145. package/src/pages/api/search.ts +82 -0
  146. package/src/pages/api/slug-availability.ts +70 -0
  147. package/src/pages/api/storage-config.ts +20 -0
  148. package/src/pages/api/storage-status.ts +206 -0
  149. package/src/pages/api/upload.ts +334 -0
  150. package/src/pages/api/webhooks/index.ts +71 -0
  151. package/src/pages/audit/index.astro +2 -104
  152. package/src/pages/login.astro +11 -11
  153. package/src/pages/media.astro +10 -0
  154. package/src/pages/preview/[collection]/[id].astro +178 -0
  155. package/src/pages/register.astro +13 -13
  156. package/src/pages/roles/index.astro +21 -21
  157. package/src/pages/settings/[slug].astro +162 -0
  158. package/src/pages/settings/index.astro +9 -0
  159. package/src/pages/users/[id].astro +29 -21
  160. package/src/pages/users/index.astro +22 -17
  161. package/src/pages/users/new.astro +18 -17
  162. package/src/styles/main.css +553 -128
  163. 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,66 @@
1
+ import React from "react";
2
+ import {
3
+ useBlockById,
4
+ useBlockActions,
5
+ } from "../fields/extensions/blocksStore";
6
+ import { ChevronRight, X } from "lucide-react";
7
+
8
+ export const CodeBlock: React.FC<{ block: any; index: number }> = ({
9
+ block,
10
+ index,
11
+ }) => {
12
+ const blockData = useBlockById(block.id);
13
+ const { updateBlock, removeBlock, moveBlock } = useBlockActions();
14
+ const data = blockData?.data ?? block.data ?? {};
15
+
16
+ const handleChange = (field: string, value: any) => {
17
+ updateBlock(block.id, { data: { ...data, [field]: value } });
18
+ };
19
+
20
+ return (
21
+ <div className="block-code border border-[var(--kyro-border)] rounded-md p-4 mb-2 relative group font-mono">
22
+ <div className="flex items-center justify-between mb-1">
23
+ <span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
24
+ Code
25
+ </span>
26
+ <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
27
+ <button type="button"
28
+ onClick={() => moveBlock(block.id, "up")}
29
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
30
+ title="Move up"
31
+ >
32
+ <ChevronRight className="w-3 h-3 rotate-90" />
33
+ </button>
34
+ <button type="button"
35
+ onClick={() => removeBlock(block.id)}
36
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
37
+ title="Remove"
38
+ >
39
+ <X className="w-3 h-3" />
40
+ </button>
41
+ </div>
42
+ </div>
43
+ <div>
44
+ <select
45
+ value={block.data.language || "plaintext"}
46
+ onChange={(e) => handleChange("language", e.target.value)}
47
+ className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] text-sm mb-3"
48
+ >
49
+ <option value="plaintext">Plain Text</option>
50
+ <option value="javascript">JavaScript</option>
51
+ <option value="typescript">TypeScript</option>
52
+ <option value="python">Python</option>
53
+ <option value="json">JSON</option>
54
+ <option value="html">HTML</option>
55
+ <option value="css">CSS</option>
56
+ </select>
57
+ <textarea
58
+ value={block.data.code || ""}
59
+ onChange={(e) => handleChange("code", e.target.value)}
60
+ className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] text-sm min-h-[120px] resize-none"
61
+ placeholder="Enter code..."
62
+ />
63
+ </div>
64
+ </div>
65
+ );
66
+ };