@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.
- package/README.md +149 -51
- package/package.json +53 -6
- package/src/collections/auth/index.ts +2 -2
- package/src/collections/portfolio/index.ts +343 -0
- package/src/components/ActionBar.tsx +153 -16
- package/src/components/Admin.tsx +136 -27
- package/src/components/ApiExplorer.tsx +325 -0
- package/src/components/ApiKeysManager.tsx +563 -0
- package/src/components/AuditLogsPage.tsx +664 -0
- package/src/components/AutoForm.tsx +1417 -661
- package/src/components/BrandingHub.tsx +267 -0
- package/src/components/BulkActionsBar.tsx +3 -3
- package/src/components/CreateView.tsx +3 -3
- package/src/components/Dashboard.tsx +393 -0
- package/src/components/DetailView.tsx +199 -57
- package/src/components/DeveloperCenter.tsx +403 -0
- package/src/components/EnhancedListView.tsx +786 -0
- package/src/components/GraphQLExplorer.tsx +675 -0
- package/src/components/GraphQLPlayground.tsx +627 -0
- package/src/components/ListView.tsx +191 -53
- package/src/components/MediaGallery.tsx +1569 -0
- package/src/components/Modal.tsx +149 -0
- package/src/components/RestPlayground.tsx +951 -0
- package/src/components/Sidebar.astro +237 -0
- package/src/components/UserManagement.tsx +204 -0
- package/src/components/VersionHistoryPanel.tsx +3 -3
- package/src/components/WebhookManager.tsx +608 -0
- package/src/components/blocks/AccordionBlock.tsx +97 -0
- package/src/components/blocks/ArrayBlock.tsx +75 -0
- package/src/components/blocks/BlockEditModal.MARKER +12 -0
- package/src/components/blocks/BlockEditModal.tsx +774 -0
- package/src/components/blocks/ButtonBlock.tsx +165 -0
- package/src/components/blocks/ChildBlocksTree.tsx +551 -0
- package/src/components/blocks/CodeBlock.tsx +66 -0
- package/src/components/blocks/ColumnsBlock.tsx +151 -0
- package/src/components/blocks/DividerBlock.tsx +43 -0
- package/src/components/blocks/FileBlock.tsx +64 -0
- package/src/components/blocks/HeadingBlock.tsx +81 -0
- package/src/components/blocks/HeroBlock.tsx +157 -0
- package/src/components/blocks/ImageBlock.tsx +83 -0
- package/src/components/blocks/LinkBlock.tsx +71 -0
- package/src/components/blocks/ListBlock.tsx +39 -0
- package/src/components/blocks/ParagraphBlock.tsx +61 -0
- package/src/components/blocks/RelationshipBlock.tsx +279 -0
- package/src/components/blocks/VStackBlock.tsx +75 -0
- package/src/components/blocks/VideoBlock.tsx +45 -0
- package/src/components/blocks/index.ts +10 -0
- package/src/components/fields/BlocksField.tsx +323 -0
- package/src/components/fields/CheckboxField.tsx +15 -9
- package/src/components/fields/CodeField.tsx +234 -0
- package/src/components/fields/DateField.tsx +38 -11
- package/src/components/fields/EditorClient.tsx +271 -0
- package/src/components/fields/FileField.tsx +390 -0
- package/src/components/fields/HybridContentField.tsx +109 -0
- package/src/components/fields/ImageField.tsx +429 -0
- package/src/components/fields/JSONField.tsx +361 -0
- package/src/components/fields/MarkdownField.tsx +282 -0
- package/src/components/fields/NumberField.tsx +42 -12
- package/src/components/fields/PortableTextField.tsx +143 -0
- package/src/components/fields/PortableTextRenderer.tsx +68 -0
- package/src/components/fields/RelationshipField.tsx +231 -59
- package/src/components/fields/SelectField.tsx +25 -15
- package/src/components/fields/TextField.tsx +45 -14
- package/src/components/fields/extensions/blockComponents.tsx +237 -0
- package/src/components/fields/extensions/blocksStore.ts +273 -0
- package/src/components/fields/index.ts +13 -0
- package/src/components/index.ts +1 -2
- package/src/components/layout/Header.tsx +2 -2
- package/src/components/layout/Layout.tsx +2 -2
- package/src/components/ui/Badge.tsx +9 -4
- package/src/components/ui/BlockDrawer.tsx +79 -0
- package/src/components/ui/Button.tsx +1 -1
- package/src/components/ui/CommandPalette.tsx +362 -0
- package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
- package/src/components/ui/Dropdown.tsx +1 -1
- package/src/components/ui/Modal.tsx +37 -12
- package/src/components/ui/PromptModal.tsx +94 -0
- package/src/components/ui/SlidePanel.tsx +43 -16
- package/src/components/ui/Toast.tsx +80 -14
- package/src/env.d.ts +16 -0
- package/src/env.ts +20 -0
- package/src/index.ts +0 -1
- package/src/layouts/AdminLayout.astro +164 -170
- package/src/layouts/AuthLayout.astro +23 -6
- package/src/lib/MediaService.ts +541 -0
- package/src/lib/auth/sqlite-adapter.ts +319 -0
- package/src/lib/config.ts +22 -6
- package/src/lib/dataStore.ts +132 -74
- package/src/lib/db/adapter.ts +54 -0
- package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
- package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
- package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
- package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
- package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
- package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
- package/src/lib/db/index.ts +449 -0
- package/src/lib/db/mongodb-adapter.ts +207 -0
- package/src/lib/db/mongodb-auth-adapter.ts +305 -0
- package/src/lib/db/schema/mysql-auth.ts +113 -0
- package/src/lib/db/schema/mysql-content.ts +20 -0
- package/src/lib/db/schema/postgres-auth.ts +116 -0
- package/src/lib/db/schema/postgres-content.ts +35 -0
- package/src/lib/db/schema/postgres-media.ts +52 -0
- package/src/lib/db/schema/postgres-settings.ts +11 -0
- package/src/lib/db/schema/sqlite-auth.ts +112 -0
- package/src/lib/db/schema/sqlite-content.ts +20 -0
- package/src/lib/graphql/index.ts +1 -0
- package/src/lib/graphql/schema.ts +443 -0
- package/src/lib/rate-limit.ts +267 -0
- package/src/lib/storage.ts +374 -0
- package/src/lib/store.ts +85 -0
- package/src/middleware.ts +70 -11
- package/src/pages/[collection]/[id].astro +178 -122
- package/src/pages/[collection]/index.astro +24 -156
- package/src/pages/admin/api-explorer.astro +98 -0
- package/src/pages/admin/graphql-explorer.astro +40 -0
- package/src/pages/admin/graphql.astro +97 -0
- package/src/pages/admin/index.astro +200 -139
- package/src/pages/admin/keys.astro +8 -0
- package/src/pages/admin/rest-playground.astro +44 -0
- package/src/pages/admin/webhooks.astro +8 -0
- package/src/pages/api/[collection]/[id]/publish.ts +44 -0
- package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
- package/src/pages/api/[collection]/[id]/versions.ts +36 -0
- package/src/pages/api/[collection]/[id].ts +102 -159
- package/src/pages/api/[collection]/index.ts +151 -230
- package/src/pages/api/auth/[id].ts +48 -69
- package/src/pages/api/auth/audit-logs.ts +20 -43
- package/src/pages/api/auth/login.ts +159 -45
- package/src/pages/api/auth/logout.ts +42 -24
- package/src/pages/api/auth/refresh.ts +119 -0
- package/src/pages/api/auth/register.ts +110 -40
- package/src/pages/api/auth/users.ts +22 -97
- package/src/pages/api/collections.ts +59 -0
- package/src/pages/api/globals/[slug]/test.ts +172 -0
- package/src/pages/api/globals/[slug].ts +42 -0
- package/src/pages/api/graphql.ts +90 -0
- package/src/pages/api/health.ts +417 -40
- package/src/pages/api/keys/[id].ts +26 -0
- package/src/pages/api/keys/index.ts +75 -0
- package/src/pages/api/media/[id].ts +309 -0
- package/src/pages/api/media/folders.ts +609 -0
- package/src/pages/api/media/index.ts +146 -0
- package/src/pages/api/media/resize.ts +267 -0
- package/src/pages/api/search.ts +82 -0
- package/src/pages/api/slug-availability.ts +70 -0
- package/src/pages/api/storage-config.ts +20 -0
- package/src/pages/api/storage-status.ts +206 -0
- package/src/pages/api/upload.ts +334 -0
- package/src/pages/api/webhooks/index.ts +71 -0
- package/src/pages/audit/index.astro +2 -104
- package/src/pages/login.astro +11 -11
- package/src/pages/media.astro +10 -0
- package/src/pages/preview/[collection]/[id].astro +178 -0
- package/src/pages/register.astro +13 -13
- package/src/pages/roles/index.astro +21 -21
- package/src/pages/settings/[slug].astro +162 -0
- package/src/pages/settings/index.astro +9 -0
- package/src/pages/users/[id].astro +29 -21
- package/src/pages/users/index.astro +22 -17
- package/src/pages/users/new.astro +18 -17
- package/src/styles/main.css +553 -128
- 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
|
+
};
|