@kyro-cms/admin 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -3
- 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
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ChevronDown, ChevronUp, Plus, X } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface AccordionItem {
|
|
5
|
+
title: string;
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface AccordionFieldProps {
|
|
10
|
+
items?: AccordionItem[];
|
|
11
|
+
onChange: (items: AccordionItem[]) => void;
|
|
12
|
+
compact?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const AccordionField: React.FC<AccordionFieldProps> = ({
|
|
16
|
+
items = [],
|
|
17
|
+
onChange,
|
|
18
|
+
compact = false,
|
|
19
|
+
}) => {
|
|
20
|
+
const [openIndex, setOpenIndex] = React.useState<number | null>(0);
|
|
21
|
+
|
|
22
|
+
const handleTitleChange = (index: number, value: string) => {
|
|
23
|
+
const newItems = [...items];
|
|
24
|
+
newItems[index] = { ...newItems[index], title: value };
|
|
25
|
+
onChange(newItems);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const handleContentChange = (index: number, value: string) => {
|
|
29
|
+
const newItems = [...items];
|
|
30
|
+
newItems[index] = { ...newItems[index], content: value };
|
|
31
|
+
onChange(newItems);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const handleRemove = (index: number) => {
|
|
35
|
+
const newItems = items.filter((_, i) => i !== index);
|
|
36
|
+
onChange(newItems);
|
|
37
|
+
if (openIndex === index) setOpenIndex(null);
|
|
38
|
+
else if (openIndex !== null && openIndex > index)
|
|
39
|
+
setOpenIndex(openIndex - 1);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const handleAdd = () => {
|
|
43
|
+
onChange([...items, { title: `Item ${items.length + 1}`, content: "" }]);
|
|
44
|
+
setOpenIndex(items.length);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const baseInputClass =
|
|
48
|
+
"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";
|
|
49
|
+
const smallInputClass =
|
|
50
|
+
"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";
|
|
51
|
+
|
|
52
|
+
if (compact) {
|
|
53
|
+
return (
|
|
54
|
+
<div className="space-y-2">
|
|
55
|
+
{items.length === 0 ? (
|
|
56
|
+
<div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-lg">
|
|
57
|
+
No items. Click "Add Item" to create one.
|
|
58
|
+
</div>
|
|
59
|
+
) : (
|
|
60
|
+
<div className="space-y-1.5">
|
|
61
|
+
{items.map((item: AccordionItem, index: number) => {
|
|
62
|
+
const isOpen = openIndex === index;
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
key={index}
|
|
66
|
+
className="border border-[var(--kyro-border)] rounded-lg overflow-hidden group"
|
|
67
|
+
>
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
onClick={() => setOpenIndex(isOpen ? null : index)}
|
|
71
|
+
className="w-full flex items-center justify-between p-2.5 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-sidebar-active)]/10 transition-colors"
|
|
72
|
+
>
|
|
73
|
+
<span className="text-sm font-medium text-[var(--kyro-text-primary)] truncate">
|
|
74
|
+
{item.title || `Item ${index + 1}`}
|
|
75
|
+
</span>
|
|
76
|
+
<div className="flex items-center gap-1">
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
onClick={(e) => {
|
|
80
|
+
e.stopPropagation();
|
|
81
|
+
handleRemove(index);
|
|
82
|
+
}}
|
|
83
|
+
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-[var(--kyro-danger-bg)] rounded text-[var(--kyro-error)] transition-opacity"
|
|
84
|
+
title="Remove"
|
|
85
|
+
>
|
|
86
|
+
<X className="w-3.5 h-3.5" />
|
|
87
|
+
</button>
|
|
88
|
+
{isOpen ? (
|
|
89
|
+
<ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
90
|
+
) : (
|
|
91
|
+
<ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
</button>
|
|
95
|
+
{isOpen && (
|
|
96
|
+
<div className="p-2.5 bg-[var(--kyro-surface)] space-y-2">
|
|
97
|
+
<input
|
|
98
|
+
type="text"
|
|
99
|
+
value={item.title || ""}
|
|
100
|
+
onChange={(e) =>
|
|
101
|
+
handleTitleChange(index, e.target.value)
|
|
102
|
+
}
|
|
103
|
+
onClick={(e) => e.stopPropagation()}
|
|
104
|
+
className={smallInputClass}
|
|
105
|
+
placeholder="Item title..."
|
|
106
|
+
/>
|
|
107
|
+
<textarea
|
|
108
|
+
value={item.content || ""}
|
|
109
|
+
onChange={(e) =>
|
|
110
|
+
handleContentChange(index, e.target.value)
|
|
111
|
+
}
|
|
112
|
+
onClick={(e) => e.stopPropagation()}
|
|
113
|
+
className={`${smallInputClass} min-h-[60px] resize-none`}
|
|
114
|
+
placeholder="Item content..."
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
})}
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
onClick={handleAdd}
|
|
126
|
+
className="flex items-center justify-center gap-1.5 w-full px-3 py-2 text-xs font-medium rounded-lg border border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:border-[var(--kyro-sidebar-active)] hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
127
|
+
>
|
|
128
|
+
<Plus className="w-3.5 h-3.5" />
|
|
129
|
+
Add Item
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className="space-y-2">
|
|
137
|
+
{items.length === 0 ? (
|
|
138
|
+
<div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-lg">
|
|
139
|
+
No items. Click "Add Item" to create one.
|
|
140
|
+
</div>
|
|
141
|
+
) : (
|
|
142
|
+
<div className="space-y-2">
|
|
143
|
+
{items.map((item: AccordionItem, index: number) => {
|
|
144
|
+
const isOpen = openIndex === index;
|
|
145
|
+
return (
|
|
146
|
+
<div
|
|
147
|
+
key={index}
|
|
148
|
+
className="border border-[var(--kyro-border)] rounded-lg overflow-hidden group"
|
|
149
|
+
>
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
onClick={() => setOpenIndex(isOpen ? null : index)}
|
|
153
|
+
className="w-full flex items-center justify-between p-3 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-sidebar-active)]/10 transition-colors"
|
|
154
|
+
>
|
|
155
|
+
<span className="text-sm font-medium text-[var(--kyro-text-primary)] truncate">
|
|
156
|
+
{item.title || `Item ${index + 1}`}
|
|
157
|
+
</span>
|
|
158
|
+
<div className="flex items-center gap-1">
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
onClick={(e) => {
|
|
162
|
+
e.stopPropagation();
|
|
163
|
+
handleRemove(index);
|
|
164
|
+
}}
|
|
165
|
+
className="opacity-0 group-hover:opacity-100 p-1.5 hover:bg-[var(--kyro-danger-bg)] rounded text-[var(--kyro-error)] transition-opacity"
|
|
166
|
+
title="Remove"
|
|
167
|
+
>
|
|
168
|
+
<X className="w-4 h-4" />
|
|
169
|
+
</button>
|
|
170
|
+
{isOpen ? (
|
|
171
|
+
<ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
172
|
+
) : (
|
|
173
|
+
<ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
</button>
|
|
177
|
+
{isOpen && (
|
|
178
|
+
<div className="p-3 bg-[var(--kyro-surface)] space-y-2">
|
|
179
|
+
<input
|
|
180
|
+
type="text"
|
|
181
|
+
value={item.title || ""}
|
|
182
|
+
onChange={(e) => handleTitleChange(index, e.target.value)}
|
|
183
|
+
className={baseInputClass}
|
|
184
|
+
placeholder="Item title..."
|
|
185
|
+
/>
|
|
186
|
+
<textarea
|
|
187
|
+
value={item.content || ""}
|
|
188
|
+
onChange={(e) =>
|
|
189
|
+
handleContentChange(index, e.target.value)
|
|
190
|
+
}
|
|
191
|
+
className={`${baseInputClass} min-h-[60px] resize-none`}
|
|
192
|
+
placeholder="Item content..."
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
})}
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
onClick={handleAdd}
|
|
204
|
+
className="flex items-center justify-center gap-1.5 w-full px-3 py-2 text-xs font-medium rounded-lg border border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:border-[var(--kyro-sidebar-active)] hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
205
|
+
>
|
|
206
|
+
<Plus className="w-3.5 h-3.5" />
|
|
207
|
+
Add Item
|
|
208
|
+
</button>
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export default AccordionField;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Plus, ChevronDown, ChevronUp, X } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface ArrayFieldItem {
|
|
5
|
+
[key: string]: any;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface ArrayFieldProps {
|
|
9
|
+
items?: ArrayFieldItem[];
|
|
10
|
+
labelField?: string;
|
|
11
|
+
onChange: (items: ArrayFieldItem[]) => void;
|
|
12
|
+
compact?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
16
|
+
items = [],
|
|
17
|
+
labelField = "title",
|
|
18
|
+
onChange,
|
|
19
|
+
compact = false,
|
|
20
|
+
}) => {
|
|
21
|
+
const [openIndex, setOpenIndex] = React.useState<number | null>(0);
|
|
22
|
+
|
|
23
|
+
const handleItemChange = (index: number, field: string, value: string) => {
|
|
24
|
+
const newItems = [...items];
|
|
25
|
+
newItems[index] = { ...newItems[index], [field]: value };
|
|
26
|
+
onChange(newItems);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const handleRemove = (index: number) => {
|
|
30
|
+
const newItems = items.filter((_, i) => i !== index);
|
|
31
|
+
onChange(newItems);
|
|
32
|
+
if (openIndex === index) setOpenIndex(null);
|
|
33
|
+
else if (openIndex !== null && openIndex > index)
|
|
34
|
+
setOpenIndex(openIndex - 1);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleAdd = () => {
|
|
38
|
+
const newItem: ArrayFieldItem = {
|
|
39
|
+
[labelField]: `Item ${items.length + 1}`,
|
|
40
|
+
};
|
|
41
|
+
onChange([...items, newItem]);
|
|
42
|
+
setOpenIndex(items.length);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const inputClass = compact
|
|
46
|
+
? "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"
|
|
47
|
+
: "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";
|
|
48
|
+
|
|
49
|
+
const itemKeys =
|
|
50
|
+
items.length > 0
|
|
51
|
+
? Object.keys(items[0]).filter((k) => k !== "id" && k !== "_key")
|
|
52
|
+
: [];
|
|
53
|
+
|
|
54
|
+
if (compact) {
|
|
55
|
+
return (
|
|
56
|
+
<div className="space-y-1.5">
|
|
57
|
+
{items.length === 0 ? (
|
|
58
|
+
<div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-lg">
|
|
59
|
+
No items. Click "Add Item" to create one.
|
|
60
|
+
</div>
|
|
61
|
+
) : (
|
|
62
|
+
<div className="space-y-1">
|
|
63
|
+
{items.map((item, index) => {
|
|
64
|
+
const isOpen = openIndex === index;
|
|
65
|
+
const itemLabel =
|
|
66
|
+
item[labelField] ||
|
|
67
|
+
item.title ||
|
|
68
|
+
item.name ||
|
|
69
|
+
`Item ${index + 1}`;
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
key={index}
|
|
73
|
+
className="border border-[var(--kyro-border)] rounded-lg overflow-hidden group"
|
|
74
|
+
>
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
onClick={() => setOpenIndex(isOpen ? null : index)}
|
|
78
|
+
className="w-full flex items-center justify-between p-2.5 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-sidebar-active)]/10 transition-colors"
|
|
79
|
+
>
|
|
80
|
+
<span className="text-sm font-medium text-[var(--kyro-text-primary)] truncate">
|
|
81
|
+
{itemLabel}
|
|
82
|
+
</span>
|
|
83
|
+
<div className="flex items-center gap-1">
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={(e) => {
|
|
87
|
+
e.stopPropagation();
|
|
88
|
+
handleRemove(index);
|
|
89
|
+
}}
|
|
90
|
+
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-[var(--kyro-danger-bg)] rounded text-[var(--kyro-error)] transition-opacity"
|
|
91
|
+
title="Remove"
|
|
92
|
+
>
|
|
93
|
+
<X className="w-3.5 h-3.5" />
|
|
94
|
+
</button>
|
|
95
|
+
{isOpen ? (
|
|
96
|
+
<ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
97
|
+
) : (
|
|
98
|
+
<ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
</button>
|
|
102
|
+
{isOpen && (
|
|
103
|
+
<div className="p-2.5 bg-[var(--kyro-surface)] space-y-2">
|
|
104
|
+
{itemKeys.length > 0 ? (
|
|
105
|
+
itemKeys.map((key) => (
|
|
106
|
+
<input
|
|
107
|
+
key={key}
|
|
108
|
+
type="text"
|
|
109
|
+
value={item[key] || ""}
|
|
110
|
+
onChange={(e) =>
|
|
111
|
+
handleItemChange(index, key, e.target.value)
|
|
112
|
+
}
|
|
113
|
+
onClick={(e) => e.stopPropagation()}
|
|
114
|
+
className={inputClass}
|
|
115
|
+
placeholder={key}
|
|
116
|
+
/>
|
|
117
|
+
))
|
|
118
|
+
) : (
|
|
119
|
+
<input
|
|
120
|
+
type="text"
|
|
121
|
+
value={item.value || ""}
|
|
122
|
+
onChange={(e) =>
|
|
123
|
+
handleItemChange(index, "value", e.target.value)
|
|
124
|
+
}
|
|
125
|
+
onClick={(e) => e.stopPropagation()}
|
|
126
|
+
className={inputClass}
|
|
127
|
+
placeholder="Value..."
|
|
128
|
+
/>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
})}
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
onClick={handleAdd}
|
|
140
|
+
className="flex items-center justify-center gap-1.5 w-full px-3 py-2 text-xs font-medium rounded-lg border border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:border-[var(--kyro-sidebar-active)] hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
141
|
+
>
|
|
142
|
+
<Plus className="w-3.5 h-3.5" />
|
|
143
|
+
Add Item
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div className="space-y-2">
|
|
151
|
+
{items.length === 0 ? (
|
|
152
|
+
<div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-lg">
|
|
153
|
+
No items. Click "Add Item" to create one.
|
|
154
|
+
</div>
|
|
155
|
+
) : (
|
|
156
|
+
<div className="space-y-2">
|
|
157
|
+
{items.map((item, index) => {
|
|
158
|
+
const isOpen = openIndex === index;
|
|
159
|
+
const itemLabel =
|
|
160
|
+
item[labelField] ||
|
|
161
|
+
item.title ||
|
|
162
|
+
item.name ||
|
|
163
|
+
`Item ${index + 1}`;
|
|
164
|
+
return (
|
|
165
|
+
<div
|
|
166
|
+
key={index}
|
|
167
|
+
className="border border-[var(--kyro-border)] rounded-lg overflow-hidden group"
|
|
168
|
+
>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
onClick={() => setOpenIndex(isOpen ? null : index)}
|
|
172
|
+
className="w-full flex items-center justify-between p-3 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-sidebar-active)]/10 transition-colors"
|
|
173
|
+
>
|
|
174
|
+
<span className="text-sm font-medium text-[var(--kyro-text-primary)] truncate">
|
|
175
|
+
{itemLabel}
|
|
176
|
+
</span>
|
|
177
|
+
<div className="flex items-center gap-1">
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
onClick={(e) => {
|
|
181
|
+
e.stopPropagation();
|
|
182
|
+
handleRemove(index);
|
|
183
|
+
}}
|
|
184
|
+
className="opacity-0 group-hover:opacity-100 p-1.5 hover:bg-[var(--kyro-danger-bg)] rounded text-[var(--kyro-error)] transition-opacity"
|
|
185
|
+
title="Remove"
|
|
186
|
+
>
|
|
187
|
+
<X className="w-4 h-4" />
|
|
188
|
+
</button>
|
|
189
|
+
{isOpen ? (
|
|
190
|
+
<ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
191
|
+
) : (
|
|
192
|
+
<ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
</button>
|
|
196
|
+
{isOpen && (
|
|
197
|
+
<div className="p-3 bg-[var(--kyro-surface)] space-y-2">
|
|
198
|
+
{itemKeys.length > 0 ? (
|
|
199
|
+
itemKeys.map((key) => (
|
|
200
|
+
<input
|
|
201
|
+
key={key}
|
|
202
|
+
type="text"
|
|
203
|
+
value={item[key] || ""}
|
|
204
|
+
onChange={(e) =>
|
|
205
|
+
handleItemChange(index, key, e.target.value)
|
|
206
|
+
}
|
|
207
|
+
className={inputClass}
|
|
208
|
+
placeholder={key}
|
|
209
|
+
/>
|
|
210
|
+
))
|
|
211
|
+
) : (
|
|
212
|
+
<input
|
|
213
|
+
type="text"
|
|
214
|
+
value={item.value || ""}
|
|
215
|
+
onChange={(e) =>
|
|
216
|
+
handleItemChange(index, "value", e.target.value)
|
|
217
|
+
}
|
|
218
|
+
className={inputClass}
|
|
219
|
+
placeholder="Value..."
|
|
220
|
+
/>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
})}
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
onClick={handleAdd}
|
|
232
|
+
className="flex items-center justify-center gap-1.5 w-full px-3 py-2 text-xs font-medium rounded-lg border border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:border-[var(--kyro-sidebar-active)] hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
233
|
+
>
|
|
234
|
+
<Plus className="w-3.5 h-3.5" />
|
|
235
|
+
Add Item
|
|
236
|
+
</button>
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export default ArrayField;
|
|
@@ -107,7 +107,7 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
|
|
|
107
107
|
setOnBlocksChange(onBlocksChange);
|
|
108
108
|
}
|
|
109
109
|
return () => {
|
|
110
|
-
setOnBlocksChange(() => {
|
|
110
|
+
setOnBlocksChange(() => {});
|
|
111
111
|
};
|
|
112
112
|
}, [onBlocksChange, setOnBlocksChange]);
|
|
113
113
|
|
|
@@ -148,7 +148,7 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
|
|
|
148
148
|
// Determine left border style based on document status
|
|
149
149
|
const getBorderClass = () => {
|
|
150
150
|
if (justSaved) {
|
|
151
|
-
return "border-l-[3px] border-
|
|
151
|
+
return "border-l-[3px] border-[var(--kyro-success)]";
|
|
152
152
|
}
|
|
153
153
|
if (
|
|
154
154
|
documentStatus === "draft" ||
|
|
@@ -226,9 +226,9 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
|
|
|
226
226
|
// Render active drag overlay
|
|
227
227
|
const activeBlock = activeDrag
|
|
228
228
|
? blockCategories
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
229
|
+
.flatMap((cat) => cat.blocks)
|
|
230
|
+
.find((b) => `drawer-${b.type}` === activeDrag.id) ||
|
|
231
|
+
blocks.find((b) => b.id === activeDrag.id)
|
|
232
232
|
: null;
|
|
233
233
|
|
|
234
234
|
const activeBlockLabel = activeBlock
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ExternalLink } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface ButtonFieldProps {
|
|
5
|
+
text?: string;
|
|
6
|
+
url?: string;
|
|
7
|
+
onChange: (field: string, value: string) => void;
|
|
8
|
+
compact?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ButtonField: React.FC<ButtonFieldProps> = ({
|
|
12
|
+
text = "Button",
|
|
13
|
+
url = "",
|
|
14
|
+
onChange,
|
|
15
|
+
compact = false,
|
|
16
|
+
}) => {
|
|
17
|
+
const inputClass = compact
|
|
18
|
+
? "flex-1 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"
|
|
19
|
+
: "flex-1 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";
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex items-center gap-2">
|
|
23
|
+
<input
|
|
24
|
+
type="text"
|
|
25
|
+
value={text}
|
|
26
|
+
onChange={(e) => onChange("text", e.target.value)}
|
|
27
|
+
className={inputClass}
|
|
28
|
+
placeholder="Button text..."
|
|
29
|
+
/>
|
|
30
|
+
<span className="text-[var(--kyro-text-muted)] text-xs">→</span>
|
|
31
|
+
<input
|
|
32
|
+
type="url"
|
|
33
|
+
value={url}
|
|
34
|
+
onChange={(e) => onChange("url", e.target.value)}
|
|
35
|
+
className={`${inputClass} font-mono text-xs`}
|
|
36
|
+
placeholder="https://..."
|
|
37
|
+
/>
|
|
38
|
+
{text && url && (
|
|
39
|
+
<a
|
|
40
|
+
href={url}
|
|
41
|
+
target="_blank"
|
|
42
|
+
rel="noopener noreferrer"
|
|
43
|
+
className={`shrink-0 ${compact ? "p-1.5" : "p-2"} rounded text-[var(--kyro-text-muted)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)] transition-colors`}
|
|
44
|
+
title={url}
|
|
45
|
+
>
|
|
46
|
+
<ExternalLink className={compact ? "w-3.5 h-3.5" : "w-4 h-4"} />
|
|
47
|
+
</a>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default ButtonField;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckboxField as CheckboxFieldType } from "@kyro-cms/core";
|
|
1
|
+
import type { CheckboxField as CheckboxFieldType } from "@kyro-cms/core/client";
|
|
2
2
|
|
|
3
3
|
interface CheckboxFieldComponentProps {
|
|
4
4
|
field: CheckboxFieldType;
|
|
@@ -29,7 +29,9 @@ export default function CheckboxField({
|
|
|
29
29
|
/>
|
|
30
30
|
<span className="text-sm font-medium text-[var(--kyro-text-primary)]">
|
|
31
31
|
{field.label || field.name}
|
|
32
|
-
{field.required &&
|
|
32
|
+
{field.required && (
|
|
33
|
+
<span className="text-[var(--kyro-error)] ml-1">*</span>
|
|
34
|
+
)}
|
|
33
35
|
</span>
|
|
34
36
|
</label>
|
|
35
37
|
{field.admin?.description && !error && (
|
|
@@ -37,7 +39,9 @@ export default function CheckboxField({
|
|
|
37
39
|
{field.admin.description}
|
|
38
40
|
</p>
|
|
39
41
|
)}
|
|
40
|
-
{error &&
|
|
42
|
+
{error && (
|
|
43
|
+
<p className="text-xs text-[var(--kyro-error)] ml-6">{error}</p>
|
|
44
|
+
)}
|
|
41
45
|
</div>
|
|
42
46
|
);
|
|
43
47
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ChildBlocksTree } from "../blocks/ChildBlocksTree";
|
|
3
|
+
|
|
4
|
+
interface ChildrenFieldProps {
|
|
5
|
+
blockId: string;
|
|
6
|
+
children: any[];
|
|
7
|
+
onUpdateChildren: (newChildren: any[]) => void;
|
|
8
|
+
label?: string;
|
|
9
|
+
compact?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ChildrenField: React.FC<ChildrenFieldProps> = ({
|
|
13
|
+
blockId,
|
|
14
|
+
children,
|
|
15
|
+
onUpdateChildren,
|
|
16
|
+
label,
|
|
17
|
+
compact = false,
|
|
18
|
+
}) => {
|
|
19
|
+
if (compact) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="pt-2 border-t border-[var(--kyro-border)]">
|
|
22
|
+
<label className="text-[10px] font-medium text-[var(--kyro-text-muted)] mb-1.5 block">
|
|
23
|
+
{label || `Children (${children.length})`}
|
|
24
|
+
</label>
|
|
25
|
+
<ChildBlocksTree
|
|
26
|
+
blockId={blockId}
|
|
27
|
+
children={children}
|
|
28
|
+
onUpdateChildren={onUpdateChildren}
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="pt-4 border-t border-[var(--kyro-border)]">
|
|
36
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-2 block">
|
|
37
|
+
{label || `Children (${children.length})`}
|
|
38
|
+
</label>
|
|
39
|
+
<ChildBlocksTree
|
|
40
|
+
blockId={blockId}
|
|
41
|
+
children={children}
|
|
42
|
+
onUpdateChildren={onUpdateChildren}
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default ChildrenField;
|