@kyro-cms/admin 0.10.19 → 0.11.0
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/dist/index.cjs +20 -20
- package/dist/index.js +20 -20
- package/package.json +2 -2
- package/src/components/fields/ArrayField.tsx +319 -171
- package/src/components/fields/ArrayLayout.tsx +319 -104
- package/src/components/fields/BlocksField.tsx +33 -25
- package/src/components/fields/ListField.tsx +141 -31
- package/src/components/fields/RelationshipField.tsx +139 -2
- package/src/lib/normalize-upload-fields.ts +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kyro-cms/admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22"
|
|
6
6
|
},
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"vitest": "^4.1.4"
|
|
121
121
|
},
|
|
122
122
|
"peerDependencies": {
|
|
123
|
-
"@kyro-cms/core": "^0.
|
|
123
|
+
"@kyro-cms/core": "^0.11.0",
|
|
124
124
|
"react": "^19.0.0",
|
|
125
125
|
"react-dom": "^19.0.0"
|
|
126
126
|
},
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Plus, ChevronDown, ChevronUp, X } from "../ui/icons";
|
|
2
|
+
import { Plus, ChevronDown, ChevronUp, X, GripVertical } from "../ui/icons";
|
|
3
|
+
import {
|
|
4
|
+
DndContext,
|
|
5
|
+
closestCenter,
|
|
6
|
+
PointerSensor,
|
|
7
|
+
useSensor,
|
|
8
|
+
useSensors,
|
|
9
|
+
type DragEndEvent,
|
|
10
|
+
} from "@dnd-kit/core";
|
|
11
|
+
import {
|
|
12
|
+
SortableContext,
|
|
13
|
+
useSortable,
|
|
14
|
+
verticalListSortingStrategy,
|
|
15
|
+
} from "@dnd-kit/sortable";
|
|
16
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
3
17
|
|
|
4
18
|
interface ArrayFieldItem {
|
|
19
|
+
id?: string;
|
|
5
20
|
[key: string]: unknown;
|
|
6
21
|
}
|
|
7
22
|
|
|
@@ -12,6 +27,219 @@ interface ArrayFieldProps {
|
|
|
12
27
|
compact?: boolean;
|
|
13
28
|
}
|
|
14
29
|
|
|
30
|
+
interface SortableArrayFieldItemProps {
|
|
31
|
+
id: string;
|
|
32
|
+
index: number;
|
|
33
|
+
isOpen: boolean;
|
|
34
|
+
setOpenIndex: (index: number | null) => void;
|
|
35
|
+
item: Record<string, unknown>;
|
|
36
|
+
labelField: string;
|
|
37
|
+
itemKeys: string[];
|
|
38
|
+
inputClass: string;
|
|
39
|
+
handleItemChange: (index: number, field: string, value: string) => void;
|
|
40
|
+
handleRemove: (index: number) => void;
|
|
41
|
+
compact: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function SortableArrayFieldItem({
|
|
45
|
+
id,
|
|
46
|
+
index,
|
|
47
|
+
isOpen,
|
|
48
|
+
setOpenIndex,
|
|
49
|
+
item,
|
|
50
|
+
labelField,
|
|
51
|
+
itemKeys,
|
|
52
|
+
inputClass,
|
|
53
|
+
handleItemChange,
|
|
54
|
+
handleRemove,
|
|
55
|
+
compact,
|
|
56
|
+
}: SortableArrayFieldItemProps) {
|
|
57
|
+
const {
|
|
58
|
+
attributes,
|
|
59
|
+
listeners,
|
|
60
|
+
setNodeRef,
|
|
61
|
+
transform,
|
|
62
|
+
transition,
|
|
63
|
+
isDragging,
|
|
64
|
+
} = useSortable({ id });
|
|
65
|
+
|
|
66
|
+
const style = {
|
|
67
|
+
transform: CSS.Transform.toString(transform),
|
|
68
|
+
transition,
|
|
69
|
+
zIndex: isDragging ? 10 : 1,
|
|
70
|
+
opacity: isDragging ? 0.8 : 1,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const itemLabel =
|
|
74
|
+
String(item[labelField] || item.title || item.name || `Item ${index + 1}`);
|
|
75
|
+
|
|
76
|
+
if (compact) {
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
ref={setNodeRef}
|
|
80
|
+
style={style}
|
|
81
|
+
className={`border border-[var(--kyro-border)] rounded-lg overflow-hidden group bg-[var(--kyro-surface)] ${
|
|
82
|
+
isDragging ? "border-[var(--kyro-primary)] shadow-md" : ""
|
|
83
|
+
}`}
|
|
84
|
+
>
|
|
85
|
+
<div 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">
|
|
86
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
87
|
+
<div
|
|
88
|
+
{...attributes}
|
|
89
|
+
{...listeners}
|
|
90
|
+
className="p-0.5 cursor-grab active:cursor-grabbing text-[var(--kyro-text-muted)] hover:bg-[var(--kyro-border)] rounded flex-shrink-0"
|
|
91
|
+
onClick={(e) => e.stopPropagation()}
|
|
92
|
+
>
|
|
93
|
+
<GripVertical className="w-3.5 h-3.5" />
|
|
94
|
+
</div>
|
|
95
|
+
<span
|
|
96
|
+
onClick={() => setOpenIndex(isOpen ? null : index)}
|
|
97
|
+
className="text-sm font-medium text-[var(--kyro-text-primary)] truncate cursor-pointer flex-1 py-1 text-left"
|
|
98
|
+
>
|
|
99
|
+
{itemLabel}
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
<div className="flex items-center gap-1">
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
onClick={(e) => {
|
|
106
|
+
e.stopPropagation();
|
|
107
|
+
handleRemove(index);
|
|
108
|
+
}}
|
|
109
|
+
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-[var(--kyro-danger-bg)] rounded text-[var(--kyro-error)] transition-opacity"
|
|
110
|
+
title="Remove"
|
|
111
|
+
>
|
|
112
|
+
<X className="w-3.5 h-3.5" />
|
|
113
|
+
</button>
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
onClick={() => setOpenIndex(isOpen ? null : index)}
|
|
117
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
118
|
+
>
|
|
119
|
+
{isOpen ? (
|
|
120
|
+
<ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
121
|
+
) : (
|
|
122
|
+
<ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
123
|
+
)}
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
{isOpen && (
|
|
128
|
+
<div className="p-2.5 bg-[var(--kyro-surface)] space-y-2">
|
|
129
|
+
{itemKeys.length > 0 ? (
|
|
130
|
+
itemKeys.map((key) => (
|
|
131
|
+
<input
|
|
132
|
+
key={key}
|
|
133
|
+
type="text"
|
|
134
|
+
value={String(item[key] || "")}
|
|
135
|
+
onChange={(e) =>
|
|
136
|
+
handleItemChange(index, key, e.target.value)
|
|
137
|
+
}
|
|
138
|
+
onClick={(e) => e.stopPropagation()}
|
|
139
|
+
className={inputClass}
|
|
140
|
+
placeholder={key}
|
|
141
|
+
/>
|
|
142
|
+
))
|
|
143
|
+
) : (
|
|
144
|
+
<input
|
|
145
|
+
type="text"
|
|
146
|
+
value={String(item.value || "")}
|
|
147
|
+
onChange={(e) =>
|
|
148
|
+
handleItemChange(index, "value", e.target.value)
|
|
149
|
+
}
|
|
150
|
+
onClick={(e) => e.stopPropagation()}
|
|
151
|
+
className={inputClass}
|
|
152
|
+
placeholder="Value..."
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div
|
|
163
|
+
ref={setNodeRef}
|
|
164
|
+
style={style}
|
|
165
|
+
className={`border border-[var(--kyro-border)] rounded-lg overflow-hidden group bg-[var(--kyro-surface)] ${
|
|
166
|
+
isDragging ? "border-[var(--kyro-primary)] shadow-md" : ""
|
|
167
|
+
}`}
|
|
168
|
+
>
|
|
169
|
+
<div className="w-full flex items-center justify-between p-3 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-sidebar-active)]/10 transition-colors">
|
|
170
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
171
|
+
<div
|
|
172
|
+
{...attributes}
|
|
173
|
+
{...listeners}
|
|
174
|
+
className="p-1 cursor-grab active:cursor-grabbing text-[var(--kyro-text-muted)] hover:bg-[var(--kyro-border)] rounded flex-shrink-0"
|
|
175
|
+
onClick={(e) => e.stopPropagation()}
|
|
176
|
+
>
|
|
177
|
+
<GripVertical className="w-4 h-4" />
|
|
178
|
+
</div>
|
|
179
|
+
<span
|
|
180
|
+
onClick={() => setOpenIndex(isOpen ? null : index)}
|
|
181
|
+
className="text-sm font-medium text-[var(--kyro-text-primary)] truncate cursor-pointer flex-1 py-1 text-left"
|
|
182
|
+
>
|
|
183
|
+
{itemLabel}
|
|
184
|
+
</span>
|
|
185
|
+
</div>
|
|
186
|
+
<div className="flex items-center gap-1">
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
onClick={(e) => {
|
|
190
|
+
e.stopPropagation();
|
|
191
|
+
handleRemove(index);
|
|
192
|
+
}}
|
|
193
|
+
className="opacity-0 group-hover:opacity-100 p-1.5 hover:bg-[var(--kyro-danger-bg)] rounded text-[var(--kyro-error)] transition-opacity"
|
|
194
|
+
title="Remove"
|
|
195
|
+
>
|
|
196
|
+
<X className="w-4 h-4" />
|
|
197
|
+
</button>
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
onClick={() => setOpenIndex(isOpen ? null : index)}
|
|
201
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
202
|
+
>
|
|
203
|
+
{isOpen ? (
|
|
204
|
+
<ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
205
|
+
) : (
|
|
206
|
+
<ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
207
|
+
)}
|
|
208
|
+
</button>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
{isOpen && (
|
|
212
|
+
<div className="p-3 bg-[var(--kyro-surface)] space-y-2">
|
|
213
|
+
{itemKeys.length > 0 ? (
|
|
214
|
+
itemKeys.map((key) => (
|
|
215
|
+
<input
|
|
216
|
+
key={key}
|
|
217
|
+
type="text"
|
|
218
|
+
value={String(item[key] || "")}
|
|
219
|
+
onChange={(e) =>
|
|
220
|
+
handleItemChange(index, key, e.target.value)
|
|
221
|
+
}
|
|
222
|
+
className={inputClass}
|
|
223
|
+
placeholder={key}
|
|
224
|
+
/>
|
|
225
|
+
))
|
|
226
|
+
) : (
|
|
227
|
+
<input
|
|
228
|
+
type="text"
|
|
229
|
+
value={String(item.value || "")}
|
|
230
|
+
onChange={(e) =>
|
|
231
|
+
handleItemChange(index, "value", e.target.value)
|
|
232
|
+
}
|
|
233
|
+
className={inputClass}
|
|
234
|
+
placeholder="Value..."
|
|
235
|
+
/>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
15
243
|
export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
16
244
|
items = [],
|
|
17
245
|
labelField = "title",
|
|
@@ -20,6 +248,32 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
20
248
|
}) => {
|
|
21
249
|
const [openIndex, setOpenIndex] = React.useState<number | null>(0);
|
|
22
250
|
|
|
251
|
+
// Sync stable IDs and heal bad data
|
|
252
|
+
React.useEffect(() => {
|
|
253
|
+
let needsUpdate = false;
|
|
254
|
+
const updated = items
|
|
255
|
+
.filter((item) => {
|
|
256
|
+
if (typeof item !== "object" || item === null) {
|
|
257
|
+
needsUpdate = true;
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
})
|
|
262
|
+
.map((item) => {
|
|
263
|
+
if (!item.id && !item._key) {
|
|
264
|
+
needsUpdate = true;
|
|
265
|
+
return {
|
|
266
|
+
...item,
|
|
267
|
+
id: Math.random().toString(36).substr(2, 9),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return item;
|
|
271
|
+
});
|
|
272
|
+
if (needsUpdate) {
|
|
273
|
+
onChange(updated);
|
|
274
|
+
}
|
|
275
|
+
}, [items, onChange]);
|
|
276
|
+
|
|
23
277
|
const handleItemChange = (index: number, field: string, value: string) => {
|
|
24
278
|
const newItems = [...items];
|
|
25
279
|
newItems[index] = { ...newItems[index], [field]: value };
|
|
@@ -35,7 +289,9 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
35
289
|
};
|
|
36
290
|
|
|
37
291
|
const handleAdd = () => {
|
|
292
|
+
const newId = Math.random().toString(36).substr(2, 9);
|
|
38
293
|
const newItem: ArrayFieldItem = {
|
|
294
|
+
id: newId,
|
|
39
295
|
[labelField]: `Item ${items.length + 1}`,
|
|
40
296
|
};
|
|
41
297
|
onChange([...items, newItem]);
|
|
@@ -51,88 +307,71 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
51
307
|
? Object.keys(items[0]).filter((k) => k !== "id" && k !== "_key")
|
|
52
308
|
: [];
|
|
53
309
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
310
|
+
const sensors = useSensors(
|
|
311
|
+
useSensor(PointerSensor, {
|
|
312
|
+
activationConstraint: {
|
|
313
|
+
distance: 3,
|
|
314
|
+
},
|
|
315
|
+
})
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const handleDragEnd = (event: DragEndEvent) => {
|
|
319
|
+
const { active, over } = event;
|
|
320
|
+
if (!over || active.id === over.id) return;
|
|
321
|
+
|
|
322
|
+
const oldIndex = items.findIndex((item) => (item.id || item._key) === active.id);
|
|
323
|
+
const newIndex = items.findIndex((item) => (item.id || item._key) === over.id);
|
|
324
|
+
|
|
325
|
+
if (oldIndex !== -1 && newIndex !== -1) {
|
|
326
|
+
const newItems = [...items];
|
|
327
|
+
const [movedItem] = newItems.splice(oldIndex, 1);
|
|
328
|
+
newItems.splice(newIndex, 0, movedItem);
|
|
329
|
+
onChange(newItems);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const itemIds = React.useMemo(() => {
|
|
334
|
+
return items.map((item) => item?.id || item?._key || "");
|
|
335
|
+
}, [items]);
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<DndContext
|
|
339
|
+
sensors={sensors}
|
|
340
|
+
collisionDetection={closestCenter}
|
|
341
|
+
onDragEnd={handleDragEnd}
|
|
342
|
+
>
|
|
343
|
+
<div className={compact ? "space-y-1.5" : "space-y-2"}>
|
|
57
344
|
{items.length === 0 ? (
|
|
58
|
-
<div className=
|
|
345
|
+
<div className={compact
|
|
346
|
+
? "text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-md"
|
|
347
|
+
: "text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-lg"
|
|
348
|
+
}>
|
|
59
349
|
No items. Click "Add Item" to create one.
|
|
60
350
|
</div>
|
|
61
351
|
) : (
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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>
|
|
352
|
+
<SortableContext
|
|
353
|
+
items={itemIds}
|
|
354
|
+
strategy={verticalListSortingStrategy}
|
|
355
|
+
>
|
|
356
|
+
<div className={compact ? "space-y-1" : "space-y-2"}>
|
|
357
|
+
{items.map((item, index) => (
|
|
358
|
+
<SortableArrayFieldItem
|
|
359
|
+
key={item.id || item._key || index}
|
|
360
|
+
id={item.id || item._key || `idx-${index}`}
|
|
361
|
+
index={index}
|
|
362
|
+
isOpen={openIndex === index}
|
|
363
|
+
setOpenIndex={setOpenIndex}
|
|
364
|
+
item={item as Record<string, unknown>}
|
|
365
|
+
labelField={labelField}
|
|
366
|
+
itemKeys={itemKeys}
|
|
367
|
+
inputClass={inputClass}
|
|
368
|
+
handleItemChange={handleItemChange}
|
|
369
|
+
handleRemove={handleRemove}
|
|
370
|
+
compact={compact}
|
|
371
|
+
/>
|
|
372
|
+
))}
|
|
373
|
+
</div>
|
|
374
|
+
</SortableContext>
|
|
136
375
|
)}
|
|
137
376
|
<button
|
|
138
377
|
type="button"
|
|
@@ -143,98 +382,7 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
143
382
|
Add Item
|
|
144
383
|
</button>
|
|
145
384
|
</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>
|
|
385
|
+
</DndContext>
|
|
238
386
|
);
|
|
239
387
|
};
|
|
240
388
|
|