@kyro-cms/admin 0.1.5 → 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 +52 -5
- 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 +50 -0
- 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 +116 -28
- 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 +286 -0
- 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 +50 -20
- 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 +82 -0
- package/src/pages/media.astro +10 -0
- package/src/pages/preview/[collection]/[id].astro +178 -0
- package/src/pages/register.astro +102 -0
- 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
- package/src/pages/index.astro +0 -225
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
useBlockById,
|
|
4
|
+
useBlockActions,
|
|
5
|
+
} from "../fields/extensions/blocksStore";
|
|
6
|
+
import { SlidePanel } from "../ui/SlidePanel";
|
|
7
|
+
import { ChildBlocksTree } from "./ChildBlocksTree";
|
|
8
|
+
import { ImageField } from "../fields/ImageField";
|
|
9
|
+
import PortableTextField from "../fields/PortableTextField";
|
|
10
|
+
import { FileField } from "../fields/FileField";
|
|
11
|
+
|
|
12
|
+
interface BlockEditModalProps {
|
|
13
|
+
block: any;
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// @MARKER: BlockEditModal with children - 2026-04-29
|
|
18
|
+
// If you want to revert, check git history for previous version
|
|
19
|
+
|
|
20
|
+
export const BlockEditModal: React.FC<BlockEditModalProps> = ({
|
|
21
|
+
block,
|
|
22
|
+
onClose,
|
|
23
|
+
}) => {
|
|
24
|
+
const blockData = useBlockById(block.id);
|
|
25
|
+
const { updateBlock } = useBlockActions();
|
|
26
|
+
const data = blockData?.data || block.data || {};
|
|
27
|
+
const children = blockData?.children || block.children || [];
|
|
28
|
+
const [collections, setCollections] = useState<string[]>([]);
|
|
29
|
+
const [loadingCollections, setLoadingCollections] = useState(true);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
fetch("/api/collections", { credentials: "include" })
|
|
33
|
+
.then((res) => res.json())
|
|
34
|
+
.then((data) => {
|
|
35
|
+
setCollections(
|
|
36
|
+
(data.collections || []).map((c: any) => c.slug || c.name || c),
|
|
37
|
+
);
|
|
38
|
+
setLoadingCollections(false);
|
|
39
|
+
})
|
|
40
|
+
.catch(() => setLoadingCollections(false));
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const handleChange = (field: string, value: any) => {
|
|
44
|
+
updateBlock(block.id, { data: { ...data, [field]: value } });
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const handleUpdateChildren = (newChildren: any[]) => {
|
|
48
|
+
updateBlock(block.id, { children: newChildren });
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleUpdateColumnChildren = (
|
|
52
|
+
columnIndex: number,
|
|
53
|
+
newChildren: any[],
|
|
54
|
+
) => {
|
|
55
|
+
const columnData = data.columnData || [];
|
|
56
|
+
const newColumnData = [...columnData];
|
|
57
|
+
newColumnData[columnIndex] = {
|
|
58
|
+
...newColumnData[columnIndex],
|
|
59
|
+
children: newChildren,
|
|
60
|
+
};
|
|
61
|
+
updateBlock(block.id, { data: { ...data, columnData: newColumnData } });
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const renderFields = () => {
|
|
65
|
+
switch (block.type) {
|
|
66
|
+
case "heading":
|
|
67
|
+
return (
|
|
68
|
+
<div className="space-y-3">
|
|
69
|
+
<div className="flex items-center gap-2">
|
|
70
|
+
<span className="text-xs font-medium text-[var(--kyro-text-muted)]">
|
|
71
|
+
Level:
|
|
72
|
+
</span>
|
|
73
|
+
<div className="flex items-center gap-1 bg-[var(--kyro-surface-accent)] rounded-lg p-0.5">
|
|
74
|
+
{[1, 2, 3].map((level) => (
|
|
75
|
+
<button
|
|
76
|
+
key={level}
|
|
77
|
+
type="button"
|
|
78
|
+
onClick={() => handleChange("level", level)}
|
|
79
|
+
className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
|
80
|
+
(data.level || 1) === level
|
|
81
|
+
? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-sm"
|
|
82
|
+
: "text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
|
|
83
|
+
}`}
|
|
84
|
+
>
|
|
85
|
+
H{level}
|
|
86
|
+
</button>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<div>
|
|
91
|
+
<input
|
|
92
|
+
type="text"
|
|
93
|
+
value={data.text || ""}
|
|
94
|
+
onChange={(e) => handleChange("text", e.target.value)}
|
|
95
|
+
className="w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
96
|
+
placeholder="Enter heading text..."
|
|
97
|
+
/>
|
|
98
|
+
{data.text && (
|
|
99
|
+
<div
|
|
100
|
+
className="mt-3 p-3 bg-[var(--kyro-surface)] rounded-lg border border-[var(--kyro-border)]"
|
|
101
|
+
style={{
|
|
102
|
+
fontSize:
|
|
103
|
+
data.level === 1
|
|
104
|
+
? "1.75rem"
|
|
105
|
+
: data.level === 2
|
|
106
|
+
? "1.5rem"
|
|
107
|
+
: "1.25rem",
|
|
108
|
+
fontWeight: 700,
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
{data.text}
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
case "paragraph":
|
|
119
|
+
return (
|
|
120
|
+
<div className="space-y-3">
|
|
121
|
+
<PortableTextField
|
|
122
|
+
field={{ name: "paragraph", label: "Content" }}
|
|
123
|
+
value={data.text}
|
|
124
|
+
onChange={(value) => handleChange("text", value)}
|
|
125
|
+
client:only="react"
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
case "image":
|
|
131
|
+
return (
|
|
132
|
+
<div className="space-y-4">
|
|
133
|
+
<ImageField
|
|
134
|
+
field={{ label: "Image", name: "image", maxCount: 1 }}
|
|
135
|
+
value={data.src}
|
|
136
|
+
onChange={(value) => handleChange("src", value)}
|
|
137
|
+
/>
|
|
138
|
+
<div>
|
|
139
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
|
|
140
|
+
Alt Text
|
|
141
|
+
</label>
|
|
142
|
+
<input
|
|
143
|
+
type="text"
|
|
144
|
+
value={data.alt || ""}
|
|
145
|
+
onChange={(e) => handleChange("alt", e.target.value)}
|
|
146
|
+
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
147
|
+
placeholder="Description for accessibility..."
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
case "video":
|
|
154
|
+
return (
|
|
155
|
+
<div className="space-y-4">
|
|
156
|
+
<div>
|
|
157
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
|
|
158
|
+
Video URL
|
|
159
|
+
</label>
|
|
160
|
+
<input
|
|
161
|
+
type="url"
|
|
162
|
+
value={data.src || ""}
|
|
163
|
+
onChange={(e) => handleChange("src", e.target.value)}
|
|
164
|
+
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
165
|
+
placeholder="MP4 or YouTube or Vimeo URL..."
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
case "link":
|
|
172
|
+
return (
|
|
173
|
+
<div className="space-y-4">
|
|
174
|
+
<div>
|
|
175
|
+
<input
|
|
176
|
+
type="text"
|
|
177
|
+
value={data.text || ""}
|
|
178
|
+
onChange={(e) => handleChange("text", e.target.value)}
|
|
179
|
+
className="w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
180
|
+
placeholder="Link text..."
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
<div>
|
|
184
|
+
<input
|
|
185
|
+
type="url"
|
|
186
|
+
value={data.url || ""}
|
|
187
|
+
onChange={(e) => handleChange("url", e.target.value)}
|
|
188
|
+
className="w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
189
|
+
placeholder="https://..."
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
{data.url && data.text && (
|
|
193
|
+
<div className="pt-2">
|
|
194
|
+
<div className="text-xs font-medium text-[var(--kyro-text-muted)] mb-2">
|
|
195
|
+
Preview
|
|
196
|
+
</div>
|
|
197
|
+
<a
|
|
198
|
+
href={data.url}
|
|
199
|
+
target="_blank"
|
|
200
|
+
rel="noopener noreferrer"
|
|
201
|
+
className="inline-flex items-center gap-2 px-4 py-2 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-lg font-medium text-sm hover:opacity-90 transition-opacity"
|
|
202
|
+
>
|
|
203
|
+
{data.text}
|
|
204
|
+
<svg
|
|
205
|
+
className="w-3.5 h-3.5"
|
|
206
|
+
fill="none"
|
|
207
|
+
stroke="currentColor"
|
|
208
|
+
viewBox="0 0 24 24"
|
|
209
|
+
>
|
|
210
|
+
<path
|
|
211
|
+
strokeLinecap="round"
|
|
212
|
+
strokeLinejoin="round"
|
|
213
|
+
strokeWidth="2"
|
|
214
|
+
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
|
215
|
+
/>
|
|
216
|
+
</svg>
|
|
217
|
+
</a>
|
|
218
|
+
</div>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
case "button":
|
|
224
|
+
return (
|
|
225
|
+
<div className="space-y-4">
|
|
226
|
+
<div>
|
|
227
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
|
|
228
|
+
Button Text
|
|
229
|
+
</label>
|
|
230
|
+
<input
|
|
231
|
+
type="text"
|
|
232
|
+
value={data.text || "Button"}
|
|
233
|
+
onChange={(e) => handleChange("text", e.target.value)}
|
|
234
|
+
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
235
|
+
/>
|
|
236
|
+
</div>
|
|
237
|
+
<div>
|
|
238
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
|
|
239
|
+
Link URL
|
|
240
|
+
</label>
|
|
241
|
+
<input
|
|
242
|
+
type="url"
|
|
243
|
+
value={data.url || ""}
|
|
244
|
+
onChange={(e) => handleChange("url", e.target.value)}
|
|
245
|
+
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
246
|
+
placeholder="https://..."
|
|
247
|
+
/>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
case "list":
|
|
253
|
+
const listItems = Array.isArray(data.items) ? data.items : [];
|
|
254
|
+
return (
|
|
255
|
+
<div className="space-y-3">
|
|
256
|
+
<div className="space-y-2">
|
|
257
|
+
{listItems.length === 0 ? (
|
|
258
|
+
<div className="text-center py-6 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-lg">
|
|
259
|
+
No items. Type below to add.
|
|
260
|
+
</div>
|
|
261
|
+
) : (
|
|
262
|
+
<div className="space-y-1">
|
|
263
|
+
{listItems.map((item: any, index: number) => (
|
|
264
|
+
<div key={index} className="flex items-center gap-2 group">
|
|
265
|
+
<span className="text-sm text-[var(--kyro-text-primary)] flex-1">
|
|
266
|
+
{item}
|
|
267
|
+
</span>
|
|
268
|
+
<button
|
|
269
|
+
type="button"
|
|
270
|
+
onClick={() => {
|
|
271
|
+
const newItems = listItems.filter(
|
|
272
|
+
(_: any, i: number) => i !== index,
|
|
273
|
+
);
|
|
274
|
+
handleChange("items", newItems);
|
|
275
|
+
}}
|
|
276
|
+
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-red-50 rounded text-red-500 transition-opacity"
|
|
277
|
+
>
|
|
278
|
+
×
|
|
279
|
+
</button>
|
|
280
|
+
</div>
|
|
281
|
+
))}
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
<input
|
|
285
|
+
type="text"
|
|
286
|
+
onKeyDown={(e) => {
|
|
287
|
+
if (e.key === "Enter") {
|
|
288
|
+
const input = e.target as HTMLInputElement;
|
|
289
|
+
if (input.value.trim()) {
|
|
290
|
+
handleChange("items", [...listItems, input.value.trim()]);
|
|
291
|
+
input.value = "";
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}}
|
|
295
|
+
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
296
|
+
placeholder="Type and press Enter to add..."
|
|
297
|
+
/>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
case "code":
|
|
303
|
+
const languages = [
|
|
304
|
+
{ value: "plaintext", label: "Text", icon: "📄" },
|
|
305
|
+
{ value: "javascript", label: "JS", icon: "🟨" },
|
|
306
|
+
{ value: "typescript", label: "TS", icon: "🔷" },
|
|
307
|
+
{ value: "python", label: "PY", icon: "🐍" },
|
|
308
|
+
{ value: "html", label: "HTML", icon: "🌐" },
|
|
309
|
+
{ value: "css", label: "CSS", icon: "🎨" },
|
|
310
|
+
{ value: "json", label: "JSON", icon: "📋" },
|
|
311
|
+
];
|
|
312
|
+
return (
|
|
313
|
+
<div className="space-y-3">
|
|
314
|
+
<div className="flex items-center gap-2">
|
|
315
|
+
<span className="text-xs font-medium text-[var(--kyro-text-muted)]">
|
|
316
|
+
Language:
|
|
317
|
+
</span>
|
|
318
|
+
<div className="flex items-center gap-1 bg-[var(--kyro-surface-accent)] rounded-lg p-0.5 overflow-x-auto">
|
|
319
|
+
{languages.map((lang) => (
|
|
320
|
+
<button
|
|
321
|
+
key={lang.value}
|
|
322
|
+
type="button"
|
|
323
|
+
onClick={() => handleChange("language", lang.value)}
|
|
324
|
+
className={`px-2 py-1 rounded text-[10px] font-medium whitespace-nowrap transition-all flex items-center gap-1 ${
|
|
325
|
+
(data.language || "plaintext") === lang.value
|
|
326
|
+
? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-sm"
|
|
327
|
+
: "text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
|
|
328
|
+
}`}
|
|
329
|
+
>
|
|
330
|
+
<span className="text-xs">{lang.icon}</span>
|
|
331
|
+
{lang.label}
|
|
332
|
+
</button>
|
|
333
|
+
))}
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
<div>
|
|
337
|
+
<textarea
|
|
338
|
+
value={data.code || ""}
|
|
339
|
+
onChange={(e) => handleChange("code", e.target.value)}
|
|
340
|
+
placeholder="Paste or type your code here..."
|
|
341
|
+
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] min-h-[160px] font-mono text-[var(--kyro-code-text)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
342
|
+
spellCheck={false}
|
|
343
|
+
/>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
case "file":
|
|
349
|
+
return (
|
|
350
|
+
<div className="space-y-4">
|
|
351
|
+
<FileField
|
|
352
|
+
field={{ label: "File", name: "file", maxCount: 1 }}
|
|
353
|
+
value={data.file}
|
|
354
|
+
onChange={(value) => handleChange("file", value)}
|
|
355
|
+
/>
|
|
356
|
+
</div>
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
case "relationship":
|
|
360
|
+
return (
|
|
361
|
+
<div className="space-y-4">
|
|
362
|
+
<div>
|
|
363
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
|
|
364
|
+
Target Collection
|
|
365
|
+
</label>
|
|
366
|
+
{loadingCollections ? (
|
|
367
|
+
<div className="w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-muted)]">
|
|
368
|
+
Loading collections...
|
|
369
|
+
</div>
|
|
370
|
+
) : (
|
|
371
|
+
<select
|
|
372
|
+
value={data.relationTo || ""}
|
|
373
|
+
onChange={(e) => handleChange("relationTo", e.target.value)}
|
|
374
|
+
className="w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
375
|
+
>
|
|
376
|
+
<option value="">Select collection...</option>
|
|
377
|
+
{collections.map((col) => (
|
|
378
|
+
<option key={col} value={col}>
|
|
379
|
+
{col}
|
|
380
|
+
</option>
|
|
381
|
+
))}
|
|
382
|
+
</select>
|
|
383
|
+
)}
|
|
384
|
+
</div>
|
|
385
|
+
<div>
|
|
386
|
+
<label className="flex items-center gap-2 cursor-pointer">
|
|
387
|
+
<input
|
|
388
|
+
type="checkbox"
|
|
389
|
+
checked={data.hasMany || false}
|
|
390
|
+
onChange={(e) => handleChange("hasMany", e.target.checked)}
|
|
391
|
+
className="w-4 h-4 rounded border-[var(--kyro-border)] focus:ring-[var(--kyro-sidebar-active)] focus:ring-offset-0"
|
|
392
|
+
/>
|
|
393
|
+
<span className="text-sm text-[var(--kyro-text-primary)]">
|
|
394
|
+
Allow multiple selections
|
|
395
|
+
</span>
|
|
396
|
+
</label>
|
|
397
|
+
</div>
|
|
398
|
+
<div>
|
|
399
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
|
|
400
|
+
Label Field
|
|
401
|
+
</label>
|
|
402
|
+
<input
|
|
403
|
+
type="text"
|
|
404
|
+
value={data.labelField || "title"}
|
|
405
|
+
onChange={(e) => handleChange("labelField", e.target.value)}
|
|
406
|
+
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
407
|
+
placeholder="title, name, label..."
|
|
408
|
+
/>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
case "hero":
|
|
414
|
+
return (
|
|
415
|
+
<div className="space-y-4">
|
|
416
|
+
<div>
|
|
417
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
|
|
418
|
+
Heading
|
|
419
|
+
</label>
|
|
420
|
+
<input
|
|
421
|
+
type="text"
|
|
422
|
+
value={data.heading || ""}
|
|
423
|
+
onChange={(e) => handleChange("heading", e.target.value)}
|
|
424
|
+
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
425
|
+
placeholder="Hero heading..."
|
|
426
|
+
/>
|
|
427
|
+
</div>
|
|
428
|
+
<div>
|
|
429
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
|
|
430
|
+
Subheading
|
|
431
|
+
</label>
|
|
432
|
+
<textarea
|
|
433
|
+
value={data.subheading || ""}
|
|
434
|
+
onChange={(e) => handleChange("subheading", e.target.value)}
|
|
435
|
+
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] min-h-[80px] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
436
|
+
placeholder="Hero subheading..."
|
|
437
|
+
/>
|
|
438
|
+
</div>
|
|
439
|
+
<div className="grid grid-cols-2 gap-3">
|
|
440
|
+
<div>
|
|
441
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
|
|
442
|
+
Background Image
|
|
443
|
+
</label>
|
|
444
|
+
<ImageField
|
|
445
|
+
field={{
|
|
446
|
+
label: "Background Image",
|
|
447
|
+
name: "bgImage",
|
|
448
|
+
maxCount: 1,
|
|
449
|
+
}}
|
|
450
|
+
value={data.bgImage}
|
|
451
|
+
onChange={(value) => handleChange("bgImage", value)}
|
|
452
|
+
/>
|
|
453
|
+
</div>
|
|
454
|
+
<div>
|
|
455
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
|
|
456
|
+
Video URL
|
|
457
|
+
</label>
|
|
458
|
+
<input
|
|
459
|
+
type="url"
|
|
460
|
+
value={data.videoUrl || ""}
|
|
461
|
+
onChange={(e) => handleChange("videoUrl", e.target.value)}
|
|
462
|
+
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
463
|
+
placeholder="https://youtube.com/..."
|
|
464
|
+
/>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
<div className="grid grid-cols-2 gap-3">
|
|
468
|
+
<div>
|
|
469
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
|
|
470
|
+
CTA Text
|
|
471
|
+
</label>
|
|
472
|
+
<input
|
|
473
|
+
type="text"
|
|
474
|
+
value={data.ctaText || ""}
|
|
475
|
+
onChange={(e) => handleChange("ctaText", e.target.value)}
|
|
476
|
+
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
477
|
+
placeholder="Button text..."
|
|
478
|
+
/>
|
|
479
|
+
</div>
|
|
480
|
+
<div>
|
|
481
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
|
|
482
|
+
CTA URL
|
|
483
|
+
</label>
|
|
484
|
+
<input
|
|
485
|
+
type="url"
|
|
486
|
+
value={data.ctaUrl || ""}
|
|
487
|
+
onChange={(e) => handleChange("ctaUrl", e.target.value)}
|
|
488
|
+
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
489
|
+
placeholder="https://..."
|
|
490
|
+
/>
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
<div className="pt-4 border-t border-[var(--kyro-border)]">
|
|
494
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-2 block">
|
|
495
|
+
Children
|
|
496
|
+
</label>
|
|
497
|
+
<ChildBlocksTree
|
|
498
|
+
blockId={block.id}
|
|
499
|
+
children={children}
|
|
500
|
+
onUpdateChildren={handleUpdateChildren}
|
|
501
|
+
/>
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
case "array":
|
|
507
|
+
return (
|
|
508
|
+
<div className="space-y-4">
|
|
509
|
+
<div className="pt-4 border-t border-[var(--kyro-border)]">
|
|
510
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-2 block">
|
|
511
|
+
Children
|
|
512
|
+
</label>
|
|
513
|
+
<ChildBlocksTree
|
|
514
|
+
blockId={block.id}
|
|
515
|
+
children={children}
|
|
516
|
+
onUpdateChildren={handleUpdateChildren}
|
|
517
|
+
/>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
case "accordion":
|
|
523
|
+
return (
|
|
524
|
+
<div className="space-y-4">
|
|
525
|
+
<div className="space-y-2">
|
|
526
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)]">
|
|
527
|
+
Accordion Items
|
|
528
|
+
</label>
|
|
529
|
+
{(() => {
|
|
530
|
+
const accordionItems = Array.isArray(data.items)
|
|
531
|
+
? data.items
|
|
532
|
+
: [];
|
|
533
|
+
if (accordionItems.length === 0) {
|
|
534
|
+
return (
|
|
535
|
+
<div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-lg">
|
|
536
|
+
No items. Click "Add Item" to create one.
|
|
537
|
+
</div>
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
return (
|
|
541
|
+
<div className="space-y-2">
|
|
542
|
+
{accordionItems.map((item: any, index: number) => (
|
|
543
|
+
<div
|
|
544
|
+
key={index}
|
|
545
|
+
className="p-3 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-surface)] group"
|
|
546
|
+
>
|
|
547
|
+
<div className="flex items-center gap-2 mb-3">
|
|
548
|
+
<div className="flex-1">
|
|
549
|
+
<input
|
|
550
|
+
type="text"
|
|
551
|
+
value={item.title || ""}
|
|
552
|
+
onChange={(e) => {
|
|
553
|
+
const newItems = [...accordionItems];
|
|
554
|
+
newItems[index] = {
|
|
555
|
+
...newItems[index],
|
|
556
|
+
title: e.target.value,
|
|
557
|
+
};
|
|
558
|
+
handleChange("items", newItems);
|
|
559
|
+
}}
|
|
560
|
+
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
561
|
+
placeholder="Item title..."
|
|
562
|
+
/>
|
|
563
|
+
</div>
|
|
564
|
+
<button
|
|
565
|
+
type="button"
|
|
566
|
+
onClick={() => {
|
|
567
|
+
const newItems = accordionItems.filter(
|
|
568
|
+
(_: any, i: number) => i !== index,
|
|
569
|
+
);
|
|
570
|
+
handleChange("items", newItems);
|
|
571
|
+
}}
|
|
572
|
+
className="opacity-0 group-hover:opacity-100 p-1.5 hover:bg-red-50 rounded text-red-500 transition-opacity shrink-0"
|
|
573
|
+
>
|
|
574
|
+
<svg
|
|
575
|
+
className="w-4 h-4"
|
|
576
|
+
fill="none"
|
|
577
|
+
stroke="currentColor"
|
|
578
|
+
viewBox="0 0 24 24"
|
|
579
|
+
>
|
|
580
|
+
<path
|
|
581
|
+
strokeLinecap="round"
|
|
582
|
+
strokeLinejoin="round"
|
|
583
|
+
strokeWidth="2"
|
|
584
|
+
d="M6 18L18 6M6 6l12 12"
|
|
585
|
+
/>
|
|
586
|
+
</svg>
|
|
587
|
+
</button>
|
|
588
|
+
</div>
|
|
589
|
+
<div>
|
|
590
|
+
<label className="text-[10px] font-medium text-[var(--kyro-text-muted)] mb-1 block">
|
|
591
|
+
Content
|
|
592
|
+
</label>
|
|
593
|
+
<textarea
|
|
594
|
+
value={item.content || ""}
|
|
595
|
+
onChange={(e) => {
|
|
596
|
+
const newItems = [...accordionItems];
|
|
597
|
+
newItems[index] = {
|
|
598
|
+
...newItems[index],
|
|
599
|
+
content: e.target.value,
|
|
600
|
+
};
|
|
601
|
+
handleChange("items", newItems);
|
|
602
|
+
}}
|
|
603
|
+
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] min-h-[60px] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
604
|
+
placeholder="Item content..."
|
|
605
|
+
/>
|
|
606
|
+
</div>
|
|
607
|
+
</div>
|
|
608
|
+
))}
|
|
609
|
+
</div>
|
|
610
|
+
);
|
|
611
|
+
})()}
|
|
612
|
+
<button
|
|
613
|
+
type="button"
|
|
614
|
+
onClick={() => {
|
|
615
|
+
const accordionItems = Array.isArray(data.items)
|
|
616
|
+
? data.items
|
|
617
|
+
: [];
|
|
618
|
+
const newItems = [
|
|
619
|
+
...accordionItems,
|
|
620
|
+
{ title: `Item ${accordionItems.length + 1}`, content: "" },
|
|
621
|
+
];
|
|
622
|
+
handleChange("items", newItems);
|
|
623
|
+
}}
|
|
624
|
+
className="mt-2 px-3 py-1.5 text-xs rounded border border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] cursor-pointer hover:border-[var(--kyro-sidebar-active)] transition-colors w-full"
|
|
625
|
+
>
|
|
626
|
+
+ Add Item
|
|
627
|
+
</button>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
case "vstack":
|
|
633
|
+
return (
|
|
634
|
+
<div className="space-y-4">
|
|
635
|
+
<div className="pt-4 border-t border-[var(--kyro-border)]">
|
|
636
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-2 block">
|
|
637
|
+
Children
|
|
638
|
+
</label>
|
|
639
|
+
<ChildBlocksTree
|
|
640
|
+
blockId={block.id}
|
|
641
|
+
children={children}
|
|
642
|
+
onUpdateChildren={handleUpdateChildren}
|
|
643
|
+
/>
|
|
644
|
+
</div>
|
|
645
|
+
</div>
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
case "columns":
|
|
649
|
+
const columns = data.columns || 1;
|
|
650
|
+
const columnData = data.columnData || [];
|
|
651
|
+
return (
|
|
652
|
+
<div className="space-y-4">
|
|
653
|
+
<div className="flex items-center gap-2">
|
|
654
|
+
<span className="text-xs font-medium text-[var(--kyro-text-muted)]">
|
|
655
|
+
Columns:
|
|
656
|
+
</span>
|
|
657
|
+
<div className="flex items-center gap-1">
|
|
658
|
+
<button
|
|
659
|
+
type="button"
|
|
660
|
+
onClick={() =>
|
|
661
|
+
handleChange("columns", Math.max(1, columns - 1))
|
|
662
|
+
}
|
|
663
|
+
disabled={columns <= 1}
|
|
664
|
+
className="w-7 h-7 flex items-center justify-center rounded border border-[var(--kyro-border)] hover:border-[var(--kyro-sidebar-active)] hover:bg-[var(--kyro-surface-accent)] disabled:opacity-30 disabled:cursor-not-allowed text-sm"
|
|
665
|
+
>
|
|
666
|
+
−
|
|
667
|
+
</button>
|
|
668
|
+
<div className="w-8 h-7 flex items-center justify-center bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded text-sm font-medium">
|
|
669
|
+
{columns}
|
|
670
|
+
</div>
|
|
671
|
+
<button
|
|
672
|
+
type="button"
|
|
673
|
+
onClick={() =>
|
|
674
|
+
handleChange("columns", Math.min(6, columns + 1))
|
|
675
|
+
}
|
|
676
|
+
disabled={columns >= 6}
|
|
677
|
+
className="w-7 h-7 flex items-center justify-center rounded border border-[var(--kyro-border)] hover:border-[var(--kyro-sidebar-active)] hover:bg-[var(--kyro-surface-accent)] disabled:opacity-30 disabled:cursor-not-allowed text-sm"
|
|
678
|
+
>
|
|
679
|
+
+
|
|
680
|
+
</button>
|
|
681
|
+
</div>
|
|
682
|
+
<span className="text-[10px] text-[var(--kyro-text-muted)] ml-auto">
|
|
683
|
+
1-6
|
|
684
|
+
</span>
|
|
685
|
+
</div>
|
|
686
|
+
<div className="pt-4 border-t border-[var(--kyro-border)]">
|
|
687
|
+
<div className="flex items-center justify-between mb-4">
|
|
688
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)]">
|
|
689
|
+
Column Children
|
|
690
|
+
</label>
|
|
691
|
+
<div className="flex gap-1">
|
|
692
|
+
{Array.from({ length: columns }, (_, i) => (
|
|
693
|
+
<div
|
|
694
|
+
key={i}
|
|
695
|
+
className={`w-6 h-6 rounded flex items-center justify-center text-[10px] font-medium ${
|
|
696
|
+
i === 0
|
|
697
|
+
? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)]"
|
|
698
|
+
: "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-muted)]"
|
|
699
|
+
}`}
|
|
700
|
+
>
|
|
701
|
+
{i + 1}
|
|
702
|
+
</div>
|
|
703
|
+
))}
|
|
704
|
+
</div>
|
|
705
|
+
</div>
|
|
706
|
+
<div className="overflow-x-auto pb-2 -mx-2 px-2">
|
|
707
|
+
<div
|
|
708
|
+
className={`grid gap-3`}
|
|
709
|
+
style={{
|
|
710
|
+
gridTemplateColumns: `repeat(${columns}, minmax(220px, 1fr))`,
|
|
711
|
+
}}
|
|
712
|
+
>
|
|
713
|
+
{Array.from({ length: columns }, (_, i) => (
|
|
714
|
+
<div
|
|
715
|
+
key={i}
|
|
716
|
+
className="border-2 border-dashed border-[var(--kyro-border)] rounded-lg p-3 bg-[var(--kyro-bg-secondary)]/50 hover:border-[var(--kyro-sidebar-active)]/50 transition-colors"
|
|
717
|
+
>
|
|
718
|
+
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-[var(--kyro-border)]">
|
|
719
|
+
<div className="w-5 h-5 rounded bg-[var(--kyro-sidebar-active)]/10 flex items-center justify-center">
|
|
720
|
+
<span className="text-[10px] font-bold text-[var(--kyro-sidebar-active)]">
|
|
721
|
+
{i + 1}
|
|
722
|
+
</span>
|
|
723
|
+
</div>
|
|
724
|
+
<span className="text-xs font-medium text-[var(--kyro-text-primary)]">
|
|
725
|
+
Column {i + 1}
|
|
726
|
+
</span>
|
|
727
|
+
<span className="text-[10px] text-[var(--kyro-text-muted)] ml-auto">
|
|
728
|
+
{columnData[i]?.children?.length || 0} blocks
|
|
729
|
+
</span>
|
|
730
|
+
</div>
|
|
731
|
+
<ChildBlocksTree
|
|
732
|
+
blockId={`${block.id}-col-${i}`}
|
|
733
|
+
children={columnData[i]?.children || []}
|
|
734
|
+
onUpdateChildren={(newChildren) =>
|
|
735
|
+
handleUpdateColumnChildren(i, newChildren)
|
|
736
|
+
}
|
|
737
|
+
/>
|
|
738
|
+
</div>
|
|
739
|
+
))}
|
|
740
|
+
</div>
|
|
741
|
+
</div>
|
|
742
|
+
</div>
|
|
743
|
+
</div>
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
default:
|
|
747
|
+
return (
|
|
748
|
+
<div className="text-center py-8 text-[var(--kyro-text-muted)]">
|
|
749
|
+
No editor for "{block.type}"
|
|
750
|
+
</div>
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
return (
|
|
756
|
+
<SlidePanel
|
|
757
|
+
open={true}
|
|
758
|
+
onClose={onClose}
|
|
759
|
+
title={`Edit ${block.type}`}
|
|
760
|
+
width="xl"
|
|
761
|
+
>
|
|
762
|
+
<div className="space-y-4">{renderFields()}</div>
|
|
763
|
+
<div className="mt-6 pt-4 border-t border-[var(--kyro-border)]">
|
|
764
|
+
<button
|
|
765
|
+
type="button"
|
|
766
|
+
onClick={onClose}
|
|
767
|
+
className="w-full py-2.5 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-lg font-medium hover:opacity-90 transition-opacity"
|
|
768
|
+
>
|
|
769
|
+
Done
|
|
770
|
+
</button>
|
|
771
|
+
</div>
|
|
772
|
+
</SlidePanel>
|
|
773
|
+
);
|
|
774
|
+
};
|