@kyro-cms/admin 0.1.7 → 0.1.9
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/package.json +7 -2
- package/src/components/Admin.tsx +1 -1
- package/src/components/AutoForm.tsx +966 -337
- package/src/components/CreateView.tsx +1 -1
- package/src/components/DetailView.tsx +1 -1
- package/src/components/EnhancedListView.tsx +156 -52
- package/src/components/ListView.tsx +1 -1
- package/src/components/Modal.tsx +65 -8
- package/src/components/Sidebar.astro +2 -2
- package/src/components/ThemeProvider.tsx +8 -2
- package/src/components/blocks/AccordionBlock.tsx +20 -52
- package/src/components/blocks/ArrayBlock.tsx +40 -31
- package/src/components/blocks/BlockEditModal.tsx +170 -581
- package/src/components/blocks/ButtonBlock.tsx +27 -128
- package/src/components/blocks/CodeBlock.tsx +88 -40
- package/src/components/blocks/ColumnsBlock.tsx +27 -85
- package/src/components/blocks/FileBlock.tsx +38 -39
- package/src/components/blocks/HeadingBlock.tsx +9 -31
- package/src/components/blocks/HeroBlock.tsx +42 -100
- package/src/components/blocks/ImageBlock.tsx +6 -7
- package/src/components/blocks/LinkBlock.tsx +27 -33
- package/src/components/blocks/ListBlock.tsx +47 -26
- package/src/components/blocks/RelationshipBlock.tsx +26 -233
- package/src/components/blocks/RichTextBlock.tsx +66 -0
- package/src/components/blocks/VStackBlock.tsx +23 -37
- package/src/components/blocks/VideoBlock.tsx +52 -32
- package/src/components/fields/AccordionField.tsx +213 -0
- package/src/components/fields/ArrayField.tsx +241 -0
- package/src/components/fields/BlocksField.tsx +5 -5
- package/src/components/fields/ButtonField.tsx +53 -0
- package/src/components/fields/CheckboxField.tsx +7 -3
- package/src/components/fields/ChildrenField.tsx +48 -0
- package/src/components/fields/CodeField.tsx +154 -94
- package/src/components/fields/ColumnsField.tsx +137 -0
- package/src/components/fields/DateField.tsx +9 -24
- package/src/components/fields/EditorClient.tsx +426 -160
- package/src/components/fields/HeadingField.tsx +31 -0
- package/src/components/fields/HeroField.tsx +101 -0
- package/src/components/fields/JSONField.tsx +7 -27
- package/src/components/fields/LinkField.tsx +81 -0
- package/src/components/fields/ListField.tsx +74 -0
- package/src/components/fields/MarkdownField.tsx +4 -26
- package/src/components/fields/NumberField.tsx +9 -27
- package/src/components/fields/PortableTextField.tsx +61 -49
- package/src/components/fields/RelationshipBlockField.tsx +233 -0
- package/src/components/fields/RelationshipField.tsx +59 -13
- package/src/components/fields/SelectField.tsx +6 -4
- package/src/components/fields/TextField.tsx +9 -24
- package/src/components/fields/UploadField.tsx +613 -0
- package/src/components/fields/VideoField.tsx +73 -0
- package/src/components/fields/extensions/blockComponents.tsx +11 -1
- package/src/components/fields/extensions/blocksStore.ts +1 -1
- package/src/components/fields/index.ts +12 -1
- package/src/components/layout/Layout.tsx +1 -1
- package/src/lib/api.ts +163 -0
- package/src/lib/config.ts +1 -1
- package/src/lib/dataStore.ts +87 -30
- package/src/lib/date-utils.ts +69 -0
- package/src/lib/db/version-adapter.ts +248 -0
- package/src/lib/i18n.tsx +353 -0
- package/src/lib/slugify.ts +15 -0
- package/src/lib/validation.ts +250 -0
- package/src/pages/api/[collection]/[id]/publish.ts +12 -4
- package/src/pages/api/[collection]/[id]/versions.ts +39 -9
- package/src/pages/api/[collection]/[id].ts +13 -1
- package/src/pages/api/[collection]/index.ts +5 -6
- package/src/styles/main.css +12 -2
- package/src/components/blocks/BlockEditModal.MARKER +0 -12
- package/src/components/fields/FileField.tsx +0 -390
- package/src/components/fields/HybridContentField.tsx +0 -109
- package/src/components/fields/ImageField.tsx +0 -429
|
@@ -1,13 +1,27 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ChevronRight } from "lucide-react";
|
|
2
3
|
import {
|
|
3
4
|
useBlockById,
|
|
4
5
|
useBlockActions,
|
|
5
6
|
} from "../fields/extensions/blocksStore";
|
|
6
7
|
import { SlidePanel } from "../ui/SlidePanel";
|
|
7
8
|
import { ChildBlocksTree } from "./ChildBlocksTree";
|
|
8
|
-
import {
|
|
9
|
+
import { UploadField } from "../fields/UploadField";
|
|
9
10
|
import PortableTextField from "../fields/PortableTextField";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
CodeField,
|
|
13
|
+
LinkField,
|
|
14
|
+
AccordionField,
|
|
15
|
+
ButtonField,
|
|
16
|
+
HeadingField,
|
|
17
|
+
VideoField,
|
|
18
|
+
ListField,
|
|
19
|
+
HeroField,
|
|
20
|
+
ArrayField,
|
|
21
|
+
ChildrenField,
|
|
22
|
+
ColumnsField,
|
|
23
|
+
RelationshipBlockField,
|
|
24
|
+
} from "../fields";
|
|
11
25
|
|
|
12
26
|
interface BlockEditModalProps {
|
|
13
27
|
block: any;
|
|
@@ -25,20 +39,6 @@ export const BlockEditModal: React.FC<BlockEditModalProps> = ({
|
|
|
25
39
|
const { updateBlock } = useBlockActions();
|
|
26
40
|
const data = blockData?.data || block.data || {};
|
|
27
41
|
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
42
|
|
|
43
43
|
const handleChange = (field: string, value: any) => {
|
|
44
44
|
updateBlock(block.id, { data: { ...data, [field]: value } });
|
|
@@ -64,65 +64,33 @@ export const BlockEditModal: React.FC<BlockEditModalProps> = ({
|
|
|
64
64
|
const renderFields = () => {
|
|
65
65
|
switch (block.type) {
|
|
66
66
|
case "heading":
|
|
67
|
+
return (
|
|
68
|
+
<HeadingField
|
|
69
|
+
text={data.text || ""}
|
|
70
|
+
onChange={handleChange}
|
|
71
|
+
compact
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
case "paragraph":
|
|
67
76
|
return (
|
|
68
77
|
<div className="space-y-3">
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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>
|
|
78
|
+
<textarea
|
|
79
|
+
value={data.text || ""}
|
|
80
|
+
onChange={(e) => handleChange("text", e.target.value)}
|
|
81
|
+
className="w-full px-3 py-3 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-primary)] text-sm min-h-[150px] resize-y focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
82
|
+
placeholder="Enter paragraph text..."
|
|
83
|
+
/>
|
|
115
84
|
</div>
|
|
116
85
|
);
|
|
117
86
|
|
|
118
|
-
case "
|
|
87
|
+
case "richtext":
|
|
119
88
|
return (
|
|
120
89
|
<div className="space-y-3">
|
|
121
90
|
<PortableTextField
|
|
122
|
-
field={{ name: "
|
|
123
|
-
value={data.
|
|
124
|
-
onChange={(value) => handleChange("
|
|
125
|
-
client:only="react"
|
|
91
|
+
field={{ name: "richtext", label: "Content" }}
|
|
92
|
+
value={data.content}
|
|
93
|
+
onChange={(value: any) => handleChange("content", value)}
|
|
126
94
|
/>
|
|
127
95
|
</div>
|
|
128
96
|
);
|
|
@@ -130,7 +98,7 @@ export const BlockEditModal: React.FC<BlockEditModalProps> = ({
|
|
|
130
98
|
case "image":
|
|
131
99
|
return (
|
|
132
100
|
<div className="space-y-4">
|
|
133
|
-
<
|
|
101
|
+
<UploadField
|
|
134
102
|
field={{ label: "Image", name: "image", maxCount: 1 }}
|
|
135
103
|
value={data.src}
|
|
136
104
|
onChange={(value) => handleChange("src", value)}
|
|
@@ -152,151 +120,43 @@ export const BlockEditModal: React.FC<BlockEditModalProps> = ({
|
|
|
152
120
|
|
|
153
121
|
case "video":
|
|
154
122
|
return (
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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>
|
|
123
|
+
<VideoField
|
|
124
|
+
src={data.src || ""}
|
|
125
|
+
title={data.title || ""}
|
|
126
|
+
onChange={handleChange}
|
|
127
|
+
compact
|
|
128
|
+
/>
|
|
169
129
|
);
|
|
170
130
|
|
|
171
131
|
case "link":
|
|
172
132
|
return (
|
|
173
|
-
<div className="space-y-
|
|
174
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
)}
|
|
133
|
+
<div className="space-y-3">
|
|
134
|
+
<LinkField
|
|
135
|
+
text={data.text || ""}
|
|
136
|
+
url={data.url || ""}
|
|
137
|
+
onChange={handleChange}
|
|
138
|
+
compact
|
|
139
|
+
/>
|
|
220
140
|
</div>
|
|
221
141
|
);
|
|
222
142
|
|
|
223
143
|
case "button":
|
|
224
144
|
return (
|
|
225
|
-
<
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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>
|
|
145
|
+
<ButtonField
|
|
146
|
+
text={data.text || "Button"}
|
|
147
|
+
url={data.url || ""}
|
|
148
|
+
onChange={handleChange}
|
|
149
|
+
compact
|
|
150
|
+
/>
|
|
250
151
|
);
|
|
251
152
|
|
|
252
153
|
case "list":
|
|
253
|
-
const listItems = Array.isArray(data.items) ? data.items : [];
|
|
254
154
|
return (
|
|
255
|
-
<
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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>
|
|
155
|
+
<ListField
|
|
156
|
+
items={Array.isArray(data.items) ? data.items : []}
|
|
157
|
+
onChange={(items) => handleChange("items", items)}
|
|
158
|
+
compact
|
|
159
|
+
/>
|
|
300
160
|
);
|
|
301
161
|
|
|
302
162
|
case "code":
|
|
@@ -310,189 +170,100 @@ export const BlockEditModal: React.FC<BlockEditModalProps> = ({
|
|
|
310
170
|
{ value: "json", label: "JSON", icon: "📋" },
|
|
311
171
|
];
|
|
312
172
|
return (
|
|
313
|
-
<div className="space-y-
|
|
314
|
-
<
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
173
|
+
<div className="space-y-4">
|
|
174
|
+
<CodeField
|
|
175
|
+
field={{
|
|
176
|
+
type: "code",
|
|
177
|
+
name: "code",
|
|
178
|
+
label: "Snippet",
|
|
179
|
+
language: data.language || "javascript",
|
|
180
|
+
}}
|
|
181
|
+
value={data.code || ""}
|
|
182
|
+
onChange={(val) => handleChange("code", val)}
|
|
183
|
+
/>
|
|
184
|
+
|
|
185
|
+
<div className="grid grid-cols-2 gap-3">
|
|
186
|
+
<div>
|
|
187
|
+
<label className="text-[10px] font-bold uppercase tracking-widest text-[var(--kyro-text-muted)] mb-1.5 block">
|
|
188
|
+
Language
|
|
189
|
+
</label>
|
|
190
|
+
<div className="relative">
|
|
191
|
+
<select
|
|
192
|
+
value={data.language || "javascript"}
|
|
193
|
+
onChange={(e) => handleChange("language", e.target.value)}
|
|
194
|
+
className="w-full pl-3 pr-10 py-2.5 bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)] rounded-xl text-xs font-bold text-[var(--kyro-text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--kyro-primary)]/20 transition-all appearance-none cursor-pointer"
|
|
329
195
|
>
|
|
330
|
-
<
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
196
|
+
<option value="plaintext">Plain Text</option>
|
|
197
|
+
<option value="javascript">JS</option>
|
|
198
|
+
<option value="typescript">TS</option>
|
|
199
|
+
<option value="python">PY</option>
|
|
200
|
+
<option value="html">HTML</option>
|
|
201
|
+
<option value="css">CSS</option>
|
|
202
|
+
<option value="json">JSON</option>
|
|
203
|
+
<option value="rust">Rust</option>
|
|
204
|
+
</select>
|
|
205
|
+
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-[var(--kyro-text-muted)]">
|
|
206
|
+
<ChevronRight className="w-4 h-4 rotate-90" />
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
334
209
|
</div>
|
|
335
210
|
</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
211
|
</div>
|
|
346
212
|
);
|
|
347
213
|
|
|
348
214
|
case "file":
|
|
349
215
|
return (
|
|
350
|
-
<
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
/>
|
|
356
|
-
</div>
|
|
216
|
+
<UploadField
|
|
217
|
+
field={{ label: "File", name: "file", maxCount: 1 }}
|
|
218
|
+
value={data.file}
|
|
219
|
+
onChange={(value) => handleChange("file", value)}
|
|
220
|
+
/>
|
|
357
221
|
);
|
|
358
222
|
|
|
359
223
|
case "relationship":
|
|
360
224
|
return (
|
|
361
|
-
<
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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>
|
|
225
|
+
<RelationshipBlockField
|
|
226
|
+
relationTo={data.relationTo || "pages"}
|
|
227
|
+
hasMany={data.hasMany || false}
|
|
228
|
+
selectedIds={
|
|
229
|
+
Array.isArray(data.selectedIds) ? data.selectedIds : []
|
|
230
|
+
}
|
|
231
|
+
selectedId={data.selectedId}
|
|
232
|
+
labelField={data.labelField || "title"}
|
|
233
|
+
onChange={handleChange}
|
|
234
|
+
/>
|
|
411
235
|
);
|
|
412
236
|
|
|
413
237
|
case "hero":
|
|
414
238
|
return (
|
|
415
|
-
<div className="space-y-
|
|
416
|
-
<
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
239
|
+
<div className="space-y-3">
|
|
240
|
+
<HeroField
|
|
241
|
+
heading={data.heading || ""}
|
|
242
|
+
subheading={data.subheading || ""}
|
|
243
|
+
ctaText={data.ctaText || ""}
|
|
244
|
+
ctaUrl={data.ctaUrl || ""}
|
|
245
|
+
onChange={handleChange}
|
|
246
|
+
compact
|
|
247
|
+
/>
|
|
248
|
+
|
|
249
|
+
<div className="grid grid-cols-2 gap-2">
|
|
250
|
+
<UploadField
|
|
251
|
+
field={{ label: "Background", name: "bgImage", maxCount: 1 }}
|
|
252
|
+
value={data.bgImage}
|
|
253
|
+
onChange={(v) => handleChange("bgImage", v)}
|
|
426
254
|
/>
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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..."
|
|
255
|
+
<input
|
|
256
|
+
type="url"
|
|
257
|
+
value={data.videoUrl || ""}
|
|
258
|
+
onChange={(e) => handleChange("videoUrl", e.target.value)}
|
|
259
|
+
className="w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent font-mono text-xs"
|
|
260
|
+
placeholder="Video URL..."
|
|
437
261
|
/>
|
|
438
262
|
</div>
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
|
263
|
+
|
|
264
|
+
<div className="pt-2 border-t border-[var(--kyro-border)]">
|
|
265
|
+
<label className="text-[10px] font-medium text-[var(--kyro-text-muted)] mb-1.5 block">
|
|
266
|
+
Children ({children.length})
|
|
496
267
|
</label>
|
|
497
268
|
<ChildBlocksTree
|
|
498
269
|
blockId={block.id}
|
|
@@ -505,10 +276,15 @@ export const BlockEditModal: React.FC<BlockEditModalProps> = ({
|
|
|
505
276
|
|
|
506
277
|
case "array":
|
|
507
278
|
return (
|
|
508
|
-
<div className="space-y-
|
|
509
|
-
<
|
|
510
|
-
|
|
511
|
-
|
|
279
|
+
<div className="space-y-3">
|
|
280
|
+
<ArrayField
|
|
281
|
+
items={Array.isArray(data.items) ? data.items : []}
|
|
282
|
+
onChange={(items) => handleChange("items", items)}
|
|
283
|
+
compact
|
|
284
|
+
/>
|
|
285
|
+
<div className="pt-2 border-t border-[var(--kyro-border)]">
|
|
286
|
+
<label className="text-[10px] font-medium text-[var(--kyro-text-muted)] mb-1.5 block">
|
|
287
|
+
Children ({children.length})
|
|
512
288
|
</label>
|
|
513
289
|
<ChildBlocksTree
|
|
514
290
|
blockId={block.id}
|
|
@@ -521,226 +297,39 @@ export const BlockEditModal: React.FC<BlockEditModalProps> = ({
|
|
|
521
297
|
|
|
522
298
|
case "accordion":
|
|
523
299
|
return (
|
|
524
|
-
<
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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>
|
|
300
|
+
<AccordionField
|
|
301
|
+
items={Array.isArray(data.items) ? data.items : []}
|
|
302
|
+
onChange={(items) => handleChange("items", items)}
|
|
303
|
+
compact
|
|
304
|
+
/>
|
|
630
305
|
);
|
|
631
306
|
|
|
632
307
|
case "vstack":
|
|
633
308
|
return (
|
|
634
|
-
<
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
<ChildBlocksTree
|
|
640
|
-
blockId={block.id}
|
|
641
|
-
children={children}
|
|
642
|
-
onUpdateChildren={handleUpdateChildren}
|
|
643
|
-
/>
|
|
644
|
-
</div>
|
|
645
|
-
</div>
|
|
309
|
+
<ChildrenField
|
|
310
|
+
blockId={block.id}
|
|
311
|
+
children={children}
|
|
312
|
+
onUpdateChildren={handleUpdateChildren}
|
|
313
|
+
/>
|
|
646
314
|
);
|
|
647
315
|
|
|
648
316
|
case "columns":
|
|
649
|
-
const columns = data.columns || 1;
|
|
650
|
-
const columnData = data.columnData || [];
|
|
651
317
|
return (
|
|
652
|
-
<
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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>
|
|
318
|
+
<ColumnsField
|
|
319
|
+
columns={data.columns || 2}
|
|
320
|
+
columnData={data.columnData || []}
|
|
321
|
+
onColumnsChange={(c) => {
|
|
322
|
+
const columnData = data.columnData || [];
|
|
323
|
+
const newColumnData = Array.from({ length: c }, (_, i) => ({
|
|
324
|
+
id: i,
|
|
325
|
+
children: columnData[i]?.children || [],
|
|
326
|
+
}));
|
|
327
|
+
updateBlock(block.id, {
|
|
328
|
+
data: { ...data, columns: c, columnData: newColumnData },
|
|
329
|
+
});
|
|
330
|
+
}}
|
|
331
|
+
onUpdateColumnChildren={handleUpdateColumnChildren}
|
|
332
|
+
/>
|
|
744
333
|
);
|
|
745
334
|
|
|
746
335
|
default:
|