@kyro-cms/admin 0.10.20 → 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 -20
- 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
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Field } from "@kyro-cms/core/client";
|
|
3
3
|
import RelationshipField from "./RelationshipField";
|
|
4
|
-
import { ChevronDown, ChevronUp } from "../ui/icons";
|
|
4
|
+
import { ChevronDown, ChevronUp, GripVertical } from "../ui/icons";
|
|
5
|
+
import {
|
|
6
|
+
DndContext,
|
|
7
|
+
closestCenter,
|
|
8
|
+
PointerSensor,
|
|
9
|
+
useSensor,
|
|
10
|
+
useSensors,
|
|
11
|
+
type DragEndEvent,
|
|
12
|
+
} from "@dnd-kit/core";
|
|
13
|
+
import {
|
|
14
|
+
SortableContext,
|
|
15
|
+
useSortable,
|
|
16
|
+
verticalListSortingStrategy,
|
|
17
|
+
} from "@dnd-kit/sortable";
|
|
18
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
5
19
|
|
|
6
20
|
const SIMPLE_TYPES = new Set(["text", "textarea", "number", "checkbox", "select", "radio", "color", "email", "password", "code", "markdown", "upload"]);
|
|
7
21
|
|
|
@@ -23,6 +37,161 @@ function isCompactArray(field: Field): boolean {
|
|
|
23
37
|
return subFields.every((f: Field) => SIMPLE_TYPES.has(f.type));
|
|
24
38
|
}
|
|
25
39
|
|
|
40
|
+
// Sortable item wrapper
|
|
41
|
+
interface SortableArrayItemProps {
|
|
42
|
+
id: string;
|
|
43
|
+
index: number;
|
|
44
|
+
isOpen: boolean;
|
|
45
|
+
setOpenIndex: (index: number | null) => void;
|
|
46
|
+
item: Record<string, unknown>;
|
|
47
|
+
field: Field;
|
|
48
|
+
renderField: any;
|
|
49
|
+
onChangeItem: (newItem: any) => void;
|
|
50
|
+
onRemove: () => void;
|
|
51
|
+
disabled?: boolean;
|
|
52
|
+
compact: boolean;
|
|
53
|
+
getItemLabel: (item: any) => string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function SortableArrayItem({
|
|
57
|
+
id,
|
|
58
|
+
index,
|
|
59
|
+
isOpen,
|
|
60
|
+
setOpenIndex,
|
|
61
|
+
item,
|
|
62
|
+
field,
|
|
63
|
+
renderField,
|
|
64
|
+
onChangeItem,
|
|
65
|
+
onRemove,
|
|
66
|
+
disabled,
|
|
67
|
+
compact,
|
|
68
|
+
getItemLabel,
|
|
69
|
+
}: SortableArrayItemProps) {
|
|
70
|
+
const {
|
|
71
|
+
attributes,
|
|
72
|
+
listeners,
|
|
73
|
+
setNodeRef,
|
|
74
|
+
transform,
|
|
75
|
+
transition,
|
|
76
|
+
isDragging,
|
|
77
|
+
} = useSortable({ id });
|
|
78
|
+
|
|
79
|
+
const style = {
|
|
80
|
+
transform: CSS.Transform.toString(transform),
|
|
81
|
+
transition,
|
|
82
|
+
zIndex: isDragging ? 10 : 1,
|
|
83
|
+
opacity: isDragging ? 0.8 : 1,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (compact) {
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
ref={setNodeRef}
|
|
90
|
+
style={style}
|
|
91
|
+
className={`flex items-start gap-2 px-3 py-1.5 border-b border-[var(--kyro-border)] last:border-b-0 hover:bg-[var(--kyro-sidebar-active)]/5 transition-colors ${
|
|
92
|
+
isDragging ? "bg-[var(--kyro-surface-accent)]/50" : ""
|
|
93
|
+
}`}
|
|
94
|
+
>
|
|
95
|
+
<div
|
|
96
|
+
{...attributes}
|
|
97
|
+
{...listeners}
|
|
98
|
+
className="p-1 cursor-grab active:cursor-grabbing text-[var(--kyro-text-muted)] hover:bg-[var(--kyro-surface-accent)] rounded flex-shrink-0 mt-1"
|
|
99
|
+
>
|
|
100
|
+
<GripVertical className="w-3.5 h-3.5" />
|
|
101
|
+
</div>
|
|
102
|
+
<span className="text-[10px] font-bold text-[var(--kyro-text-muted)] pt-2.5 min-w-[18px] text-center">
|
|
103
|
+
{index + 1}
|
|
104
|
+
</span>
|
|
105
|
+
<div className="flex-1 flex items-start gap-1.5 min-w-0">
|
|
106
|
+
{((field as any).fields || []).map((f: Field) => (
|
|
107
|
+
<div key={f.name} className="flex-1 min-w-0">
|
|
108
|
+
{renderField(f, item, onChangeItem)}
|
|
109
|
+
</div>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
disabled={disabled}
|
|
115
|
+
onClick={onRemove}
|
|
116
|
+
className="text-[var(--kyro-text-muted)] hover:text-[var(--kyro-error)] transition-colors disabled:opacity-30 p-0.5 mt-1.5 flex-shrink-0"
|
|
117
|
+
title="Remove"
|
|
118
|
+
>
|
|
119
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
120
|
+
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
|
121
|
+
</svg>
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
ref={setNodeRef}
|
|
130
|
+
style={style}
|
|
131
|
+
className={`border border-[var(--kyro-border)] rounded-lg overflow-hidden group bg-[var(--kyro-surface)] ${
|
|
132
|
+
isDragging ? "border-[var(--kyro-primary)] shadow-md" : ""
|
|
133
|
+
}`}
|
|
134
|
+
>
|
|
135
|
+
<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">
|
|
136
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
137
|
+
<div
|
|
138
|
+
{...attributes}
|
|
139
|
+
{...listeners}
|
|
140
|
+
className="p-1 cursor-grab active:cursor-grabbing text-[var(--kyro-text-muted)] hover:bg-[var(--kyro-border)] rounded flex-shrink-0"
|
|
141
|
+
>
|
|
142
|
+
<GripVertical className="w-4 h-4" />
|
|
143
|
+
</div>
|
|
144
|
+
<span
|
|
145
|
+
role="button"
|
|
146
|
+
tabIndex={0}
|
|
147
|
+
onClick={() => setOpenIndex(isOpen ? null : index)}
|
|
148
|
+
onKeyDown={(e) => {
|
|
149
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
setOpenIndex(isOpen ? null : index);
|
|
152
|
+
}
|
|
153
|
+
}}
|
|
154
|
+
className="text-xs font-bold tracking-widest text-[var(--kyro-text-muted)] truncate cursor-pointer flex-1 py-1 text-left"
|
|
155
|
+
>
|
|
156
|
+
{getItemLabel(item) || `Item ${index + 1}`}
|
|
157
|
+
</span>
|
|
158
|
+
</div>
|
|
159
|
+
<div className="flex items-center gap-1">
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
disabled={disabled}
|
|
163
|
+
onClick={(e) => {
|
|
164
|
+
e.stopPropagation();
|
|
165
|
+
onRemove();
|
|
166
|
+
}}
|
|
167
|
+
className="text-[11px] font-bold text-[var(--kyro-error)] opacity-0 group-hover:opacity-100 transition-opacity disabled:opacity-30 hover:bg-[var(--kyro-danger-bg)] rounded px-1.5 py-0.5"
|
|
168
|
+
>
|
|
169
|
+
Remove
|
|
170
|
+
</button>
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
onClick={() => setOpenIndex(isOpen ? null : index)}
|
|
174
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
175
|
+
>
|
|
176
|
+
{isOpen ? (
|
|
177
|
+
<ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
178
|
+
) : (
|
|
179
|
+
<ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
180
|
+
)}
|
|
181
|
+
</button>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
{isOpen && (
|
|
185
|
+
<div className="p-4 bg-[var(--kyro-surface)] space-y-4">
|
|
186
|
+
{((field as any).fields || []).map((f: Field) =>
|
|
187
|
+
renderField(f, item, onChangeItem),
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
26
195
|
export function ArrayLayout({
|
|
27
196
|
field,
|
|
28
197
|
value,
|
|
@@ -37,6 +206,32 @@ export function ArrayLayout({
|
|
|
37
206
|
const isRelationship = firstField?.type === "relationship";
|
|
38
207
|
const [openIndex, setOpenIndex] = React.useState<number | null>(0);
|
|
39
208
|
|
|
209
|
+
// Sync stable IDs and heal bad data
|
|
210
|
+
React.useEffect(() => {
|
|
211
|
+
let needsUpdate = false;
|
|
212
|
+
const updated = items
|
|
213
|
+
.filter((item: any) => {
|
|
214
|
+
if (typeof item !== "object" || item === null) {
|
|
215
|
+
needsUpdate = true;
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
})
|
|
220
|
+
.map((item: any) => {
|
|
221
|
+
if (!item.id && !item._key) {
|
|
222
|
+
needsUpdate = true;
|
|
223
|
+
return {
|
|
224
|
+
...item,
|
|
225
|
+
id: Math.random().toString(36).substr(2, 9),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return item;
|
|
229
|
+
});
|
|
230
|
+
if (needsUpdate) {
|
|
231
|
+
onChange(updated);
|
|
232
|
+
}
|
|
233
|
+
}, [value, onChange]);
|
|
234
|
+
|
|
40
235
|
function getItemLabel(item: Record<string, unknown>): string {
|
|
41
236
|
for (const key of ["label", "title", "name"]) {
|
|
42
237
|
const val = item[key];
|
|
@@ -59,12 +254,41 @@ export function ArrayLayout({
|
|
|
59
254
|
}
|
|
60
255
|
return "";
|
|
61
256
|
}
|
|
257
|
+
|
|
62
258
|
const compact = isCompactArray(field);
|
|
63
259
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
{
|
|
260
|
+
// dnd-kit sensors setup
|
|
261
|
+
const sensors = useSensors(
|
|
262
|
+
useSensor(PointerSensor, {
|
|
263
|
+
activationConstraint: {
|
|
264
|
+
distance: 3,
|
|
265
|
+
},
|
|
266
|
+
})
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const handleDragEnd = (event: DragEndEvent) => {
|
|
270
|
+
const { active, over } = event;
|
|
271
|
+
if (!over || active.id === over.id) return;
|
|
272
|
+
|
|
273
|
+
const oldIndex = items.findIndex((item: any) => (item.id || item._key) === active.id);
|
|
274
|
+
const newIndex = items.findIndex((item: any) => (item.id || item._key) === over.id);
|
|
275
|
+
|
|
276
|
+
if (oldIndex !== -1 && newIndex !== -1) {
|
|
277
|
+
const newItems = [...items];
|
|
278
|
+
const [movedItem] = newItems.splice(oldIndex, 1);
|
|
279
|
+
newItems.splice(newIndex, 0, movedItem);
|
|
280
|
+
onChange(newItems);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const itemIds = React.useMemo(() => {
|
|
285
|
+
return items.map((item: any) => item?.id || item?._key || "");
|
|
286
|
+
}, [items]);
|
|
287
|
+
|
|
288
|
+
if (isRelationship) {
|
|
289
|
+
return (
|
|
290
|
+
<div className="kyro-form-field">
|
|
291
|
+
<label className="kyro-form-label">{field.label || field.name}</label>
|
|
68
292
|
<RelationshipField
|
|
69
293
|
field={{
|
|
70
294
|
name: labelField,
|
|
@@ -76,118 +300,109 @@ export function ArrayLayout({
|
|
|
76
300
|
onChange={(newValue) => {
|
|
77
301
|
const newItems = (newValue || []).map((id: string) => ({
|
|
78
302
|
[labelField]: id,
|
|
303
|
+
id: Math.random().toString(36).substr(2, 9),
|
|
79
304
|
}));
|
|
80
305
|
onChange(newItems);
|
|
81
306
|
}}
|
|
82
307
|
disabled={disabled}
|
|
83
308
|
/>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<div className="kyro-form-field">
|
|
315
|
+
<label className="kyro-form-label">{field.label || field.name}</label>
|
|
316
|
+
<DndContext
|
|
317
|
+
sensors={sensors}
|
|
318
|
+
collisionDetection={closestCenter}
|
|
319
|
+
onDragEnd={handleDragEnd}
|
|
320
|
+
>
|
|
321
|
+
{compact ? (
|
|
322
|
+
<div className="kyro-form-array kyro-form-array--compact border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)]/30 rounded-md overflow-hidden">
|
|
323
|
+
<SortableContext
|
|
324
|
+
items={itemIds}
|
|
325
|
+
strategy={verticalListSortingStrategy}
|
|
326
|
+
>
|
|
327
|
+
{(items as Record<string, unknown>[]).map((item, index) => (
|
|
328
|
+
<SortableArrayItem
|
|
329
|
+
key={item.id as string || item._key as string || index}
|
|
330
|
+
id={item.id as string || item._key as string || `idx-${index}`}
|
|
331
|
+
index={index}
|
|
332
|
+
isOpen={false}
|
|
333
|
+
setOpenIndex={() => {}}
|
|
334
|
+
item={item}
|
|
335
|
+
field={field}
|
|
336
|
+
renderField={renderField}
|
|
337
|
+
onChangeItem={(newItem) => {
|
|
338
|
+
const newItems = [...items];
|
|
339
|
+
newItems[index] = newItem;
|
|
340
|
+
onChange(newItems);
|
|
341
|
+
}}
|
|
342
|
+
onRemove={() => onChange(items.filter((_: unknown, i: number) => i !== index))}
|
|
343
|
+
disabled={disabled}
|
|
344
|
+
compact={true}
|
|
345
|
+
getItemLabel={getItemLabel}
|
|
346
|
+
/>
|
|
347
|
+
))}
|
|
348
|
+
</SortableContext>
|
|
349
|
+
<button
|
|
350
|
+
type="button"
|
|
351
|
+
className="w-full py-2 border-2 border-dashed border-[var(--kyro-border)] rounded-none text-xs font-bold text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:border-[var(--kyro-primary)] transition-all disabled:opacity-50"
|
|
352
|
+
disabled={disabled}
|
|
353
|
+
onClick={() => onChange([...items, { id: Math.random().toString(36).substr(2, 9) }])}
|
|
90
354
|
>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
355
|
+
+ Add Item
|
|
356
|
+
</button>
|
|
357
|
+
</div>
|
|
358
|
+
) : (
|
|
359
|
+
<div className="kyro-form-array border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)]/30 rounded-md p-3 space-y-4">
|
|
360
|
+
<SortableContext
|
|
361
|
+
items={itemIds}
|
|
362
|
+
strategy={verticalListSortingStrategy}
|
|
363
|
+
>
|
|
364
|
+
<div className="space-y-2">
|
|
365
|
+
{(items as Record<string, unknown>[]).map((item, index) => {
|
|
366
|
+
const isOpen = openIndex === index;
|
|
96
367
|
return (
|
|
97
|
-
<
|
|
98
|
-
{
|
|
368
|
+
<SortableArrayItem
|
|
369
|
+
key={item.id as string || item._key as string || index}
|
|
370
|
+
id={item.id as string || item._key as string || `idx-${index}`}
|
|
371
|
+
index={index}
|
|
372
|
+
isOpen={isOpen}
|
|
373
|
+
setOpenIndex={setOpenIndex}
|
|
374
|
+
item={item}
|
|
375
|
+
field={field}
|
|
376
|
+
renderField={renderField}
|
|
377
|
+
onChangeItem={(newItem) => {
|
|
99
378
|
const newItems = [...items];
|
|
100
379
|
newItems[index] = newItem;
|
|
101
380
|
onChange(newItems);
|
|
102
|
-
}
|
|
103
|
-
|
|
381
|
+
}}
|
|
382
|
+
onRemove={() => onChange(items.filter((_: unknown, i: number) => i !== index))}
|
|
383
|
+
disabled={disabled}
|
|
384
|
+
compact={false}
|
|
385
|
+
getItemLabel={getItemLabel}
|
|
386
|
+
/>
|
|
104
387
|
);
|
|
105
388
|
})}
|
|
106
389
|
</div>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
disabled={disabled}
|
|
124
|
-
onClick={() => onChange([...items, {}])}
|
|
125
|
-
>
|
|
126
|
-
+ Add Item
|
|
127
|
-
</button>
|
|
128
|
-
</div>
|
|
129
|
-
) : (
|
|
130
|
-
<div className="kyro-form-array border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)]/30 rounded-md p-3 space-y-4">
|
|
131
|
-
{(items as Record<string, unknown>[]).map((item, index) => {
|
|
132
|
-
const isOpen = openIndex === index;
|
|
133
|
-
return (
|
|
134
|
-
<div
|
|
135
|
-
key={index}
|
|
136
|
-
className="border border-[var(--kyro-border)] rounded-lg overflow-hidden group"
|
|
137
|
-
>
|
|
138
|
-
<div
|
|
139
|
-
role="button"
|
|
140
|
-
tabIndex={0}
|
|
141
|
-
onClick={() => setOpenIndex(isOpen ? null : index)}
|
|
142
|
-
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpenIndex(isOpen ? null : index); } }}
|
|
143
|
-
className="w-full flex items-center justify-between p-3 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-sidebar-active)]/10 transition-colors cursor-pointer"
|
|
144
|
-
>
|
|
145
|
-
<span className="text-xs font-bold tracking-widest text-[var(--kyro-text-muted)] truncate">
|
|
146
|
-
{getItemLabel(item) || `Item ${index + 1}`}
|
|
147
|
-
</span>
|
|
148
|
-
<div className="flex items-center gap-1">
|
|
149
|
-
<button
|
|
150
|
-
type="button"
|
|
151
|
-
disabled={disabled}
|
|
152
|
-
onClick={(e) => {
|
|
153
|
-
e.stopPropagation();
|
|
154
|
-
onChange(items.filter((_: unknown, i: number) => i !== index));
|
|
155
|
-
}}
|
|
156
|
-
className="text-[11px] font-bold text-[var(--kyro-error)] opacity-0 group-hover:opacity-100 transition-opacity disabled:opacity-30 hover:bg-[var(--kyro-danger-bg)] rounded px-1.5 py-0.5"
|
|
157
|
-
>
|
|
158
|
-
Remove
|
|
159
|
-
</button>
|
|
160
|
-
{isOpen ? (
|
|
161
|
-
<ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
162
|
-
) : (
|
|
163
|
-
<ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
164
|
-
)}
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
{isOpen && (
|
|
168
|
-
<div className="p-4 bg-[var(--kyro-surface)] space-y-4">
|
|
169
|
-
{(field as Field & { fields?: Field[] }).fields.map((f: Field) =>
|
|
170
|
-
renderField(f, item, (newItem) => {
|
|
171
|
-
const newItems = [...items];
|
|
172
|
-
newItems[index] = newItem;
|
|
173
|
-
onChange(newItems);
|
|
174
|
-
}),
|
|
175
|
-
)}
|
|
176
|
-
</div>
|
|
177
|
-
)}
|
|
178
|
-
</div>
|
|
179
|
-
);
|
|
180
|
-
})}
|
|
181
|
-
<button
|
|
182
|
-
type="button"
|
|
183
|
-
className="w-full py-3 border-2 border-dashed border-[var(--kyro-border)] rounded-lg text-xs font-bold text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:border-[var(--kyro-primary)] transition-all disabled:opacity-50"
|
|
184
|
-
disabled={disabled}
|
|
185
|
-
onClick={() => onChange([...items, {}])}
|
|
186
|
-
>
|
|
187
|
-
+ Add Item
|
|
188
|
-
</button>
|
|
189
|
-
</div>
|
|
190
|
-
)}
|
|
390
|
+
</SortableContext>
|
|
391
|
+
<button
|
|
392
|
+
type="button"
|
|
393
|
+
className="w-full py-3 border-2 border-dashed border-[var(--kyro-border)] rounded-lg text-xs font-bold text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:border-[var(--kyro-primary)] transition-all disabled:opacity-50"
|
|
394
|
+
disabled={disabled}
|
|
395
|
+
onClick={() => {
|
|
396
|
+
const newId = Math.random().toString(36).substr(2, 9);
|
|
397
|
+
onChange([...items, { id: newId }]);
|
|
398
|
+
setOpenIndex(items.length);
|
|
399
|
+
}}
|
|
400
|
+
>
|
|
401
|
+
+ Add Item
|
|
402
|
+
</button>
|
|
403
|
+
</div>
|
|
404
|
+
)}
|
|
405
|
+
</DndContext>
|
|
191
406
|
</div>
|
|
192
407
|
);
|
|
193
408
|
}
|
|
@@ -157,9 +157,7 @@ const SortableBlockComponent = ({
|
|
|
157
157
|
/>
|
|
158
158
|
) : (
|
|
159
159
|
<span
|
|
160
|
-
|
|
161
|
-
className="font-medium text-[var(--kyro-text-secondary)] truncate max-w-[120px] cursor-text hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
162
|
-
title="Click to rename"
|
|
160
|
+
className="font-medium text-[var(--kyro-text-secondary)] truncate max-w-[120px] transition-colors"
|
|
163
161
|
>
|
|
164
162
|
{itemLabel}
|
|
165
163
|
</span>
|
|
@@ -187,6 +185,18 @@ const SortableBlockComponent = ({
|
|
|
187
185
|
</div>
|
|
188
186
|
) : (
|
|
189
187
|
<div className="flex items-center gap-0.5">
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
onClick={(e) => {
|
|
191
|
+
e.stopPropagation();
|
|
192
|
+
setNameDraft((block.name as string) || "");
|
|
193
|
+
setEditingName(true);
|
|
194
|
+
}}
|
|
195
|
+
className="p-0.5 hover:bg-[var(--kyro-surface-accent)] rounded text-[var(--kyro-text-secondary)] transition-colors"
|
|
196
|
+
title="Rename"
|
|
197
|
+
>
|
|
198
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
|
|
199
|
+
</button>
|
|
190
200
|
<button
|
|
191
201
|
type="button"
|
|
192
202
|
onClick={(e) => {
|
|
@@ -227,14 +237,21 @@ const SortableBlockComponent = ({
|
|
|
227
237
|
return (
|
|
228
238
|
<div ref={setNodeRef} style={style} className="relative group mb-2">
|
|
229
239
|
<div
|
|
230
|
-
{...attributes}
|
|
231
|
-
{...listeners}
|
|
232
240
|
onClick={() => setEditingBlockId(block.id as string)}
|
|
233
|
-
className={`flex items-center gap-3 p-3 bg-[var(--kyro-bg-secondary)] rounded-lg border transition-colors cursor-
|
|
241
|
+
className={`flex items-center gap-3 p-3 bg-[var(--kyro-bg-secondary)] rounded-lg border transition-colors cursor-pointer ${isEditing
|
|
234
242
|
? `${(blockTheme[block.type as string] || blockTheme.default).border} bg-[var(--kyro-primary)]/5`
|
|
235
243
|
: "border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/50 hover:bg-[var(--kyro-primary)]/5"
|
|
236
244
|
}`}
|
|
237
245
|
>
|
|
246
|
+
<div
|
|
247
|
+
className="p-1 cursor-grab active:cursor-grabbing text-[var(--kyro-text-muted)] hover:bg-[var(--kyro-surface-accent)] rounded flex-shrink-0"
|
|
248
|
+
{...attributes}
|
|
249
|
+
{...listeners}
|
|
250
|
+
onClick={(e) => e.stopPropagation()}
|
|
251
|
+
>
|
|
252
|
+
<GripVertical className="w-4 h-4" />
|
|
253
|
+
</div>
|
|
254
|
+
|
|
238
255
|
{blockIcons[block.type as string] && (
|
|
239
256
|
<span className="text-[var(--kyro-text-secondary)]">
|
|
240
257
|
{blockIcons[block.type as string]}
|
|
@@ -255,15 +272,7 @@ const SortableBlockComponent = ({
|
|
|
255
272
|
/>
|
|
256
273
|
) : (
|
|
257
274
|
<div
|
|
258
|
-
|
|
259
|
-
if (window.innerWidth >= 768) {
|
|
260
|
-
e.stopPropagation();
|
|
261
|
-
setNameDraft((block.name as string) || "");
|
|
262
|
-
setEditingName(true);
|
|
263
|
-
}
|
|
264
|
-
}}
|
|
265
|
-
className="text-xs font-semibold text-[var(--kyro-text-secondary)] truncate md:cursor-text hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
266
|
-
title="Click to rename"
|
|
275
|
+
className="text-xs font-semibold text-[var(--kyro-text-secondary)] truncate transition-colors"
|
|
267
276
|
>
|
|
268
277
|
{itemLabel}
|
|
269
278
|
{previewSnippet && typeof previewSnippet === "string" && (
|
|
@@ -318,7 +327,7 @@ const SortableBlockComponent = ({
|
|
|
318
327
|
setNameDraft((block.name as string) || "");
|
|
319
328
|
setEditingName(true);
|
|
320
329
|
}}
|
|
321
|
-
className="
|
|
330
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded text-[var(--kyro-text-secondary)] transition-colors"
|
|
322
331
|
title="Rename Block"
|
|
323
332
|
>
|
|
324
333
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
|
|
@@ -537,8 +546,7 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
|
|
|
537
546
|
const sensors = useSensors(
|
|
538
547
|
useSensor(PointerSensor, {
|
|
539
548
|
activationConstraint: {
|
|
540
|
-
|
|
541
|
-
tolerance: 5,
|
|
549
|
+
distance: 3,
|
|
542
550
|
},
|
|
543
551
|
}),
|
|
544
552
|
useSensor(KeyboardSensor),
|
|
@@ -569,8 +577,13 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
|
|
|
569
577
|
});
|
|
570
578
|
}
|
|
571
579
|
} else {
|
|
572
|
-
// Dropped on root level -
|
|
573
|
-
|
|
580
|
+
// Dropped on root level - check if dropped over a specific block to insert at index
|
|
581
|
+
const overIndex = blocks.findIndex((b) => b.id === over.id);
|
|
582
|
+
if (overIndex !== -1) {
|
|
583
|
+
store.getState().addBlock(blockType, overIndex);
|
|
584
|
+
} else {
|
|
585
|
+
handleAddBlock(blockType);
|
|
586
|
+
}
|
|
574
587
|
}
|
|
575
588
|
return;
|
|
576
589
|
}
|