@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
|
@@ -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) => {
|
|
@@ -225,15 +235,7 @@ const SortableBlockComponent = ({
|
|
|
225
235
|
}
|
|
226
236
|
|
|
227
237
|
return (
|
|
228
|
-
<div ref={setNodeRef} style={style} className="relative group
|
|
229
|
-
<div
|
|
230
|
-
className="hidden md:absolute left-0 top-1/2 -translate-y-1/2 p-1.5 cursor-grab active:cursor-grabbing text-[var(--kyro-text-muted)] opacity-0 group-hover:opacity-100 transition-opacity hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
231
|
-
{...attributes}
|
|
232
|
-
{...listeners}
|
|
233
|
-
>
|
|
234
|
-
<GripVertical className="w-4 h-4" />
|
|
235
|
-
</div>
|
|
236
|
-
|
|
238
|
+
<div ref={setNodeRef} style={style} className="relative group mb-2">
|
|
237
239
|
<div
|
|
238
240
|
onClick={() => setEditingBlockId(block.id as string)}
|
|
239
241
|
className={`flex items-center gap-3 p-3 bg-[var(--kyro-bg-secondary)] rounded-lg border transition-colors cursor-pointer ${isEditing
|
|
@@ -241,6 +243,15 @@ const SortableBlockComponent = ({
|
|
|
241
243
|
: "border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/50 hover:bg-[var(--kyro-primary)]/5"
|
|
242
244
|
}`}
|
|
243
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
|
+
|
|
244
255
|
{blockIcons[block.type as string] && (
|
|
245
256
|
<span className="text-[var(--kyro-text-secondary)]">
|
|
246
257
|
{blockIcons[block.type as string]}
|
|
@@ -261,15 +272,7 @@ const SortableBlockComponent = ({
|
|
|
261
272
|
/>
|
|
262
273
|
) : (
|
|
263
274
|
<div
|
|
264
|
-
|
|
265
|
-
if (window.innerWidth >= 768) {
|
|
266
|
-
e.stopPropagation();
|
|
267
|
-
setNameDraft((block.name as string) || "");
|
|
268
|
-
setEditingName(true);
|
|
269
|
-
}
|
|
270
|
-
}}
|
|
271
|
-
className="text-xs font-semibold text-[var(--kyro-text-secondary)] truncate md:cursor-text hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
272
|
-
title="Click to rename"
|
|
275
|
+
className="text-xs font-semibold text-[var(--kyro-text-secondary)] truncate transition-colors"
|
|
273
276
|
>
|
|
274
277
|
{itemLabel}
|
|
275
278
|
{previewSnippet && typeof previewSnippet === "string" && (
|
|
@@ -324,7 +327,7 @@ const SortableBlockComponent = ({
|
|
|
324
327
|
setNameDraft((block.name as string) || "");
|
|
325
328
|
setEditingName(true);
|
|
326
329
|
}}
|
|
327
|
-
className="
|
|
330
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded text-[var(--kyro-text-secondary)] transition-colors"
|
|
328
331
|
title="Rename Block"
|
|
329
332
|
>
|
|
330
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>
|
|
@@ -543,7 +546,7 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
|
|
|
543
546
|
const sensors = useSensors(
|
|
544
547
|
useSensor(PointerSensor, {
|
|
545
548
|
activationConstraint: {
|
|
546
|
-
distance:
|
|
549
|
+
distance: 3,
|
|
547
550
|
},
|
|
548
551
|
}),
|
|
549
552
|
useSensor(KeyboardSensor),
|
|
@@ -574,8 +577,13 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
|
|
|
574
577
|
});
|
|
575
578
|
}
|
|
576
579
|
} else {
|
|
577
|
-
// Dropped on root level -
|
|
578
|
-
|
|
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
|
+
}
|
|
579
587
|
}
|
|
580
588
|
return;
|
|
581
589
|
}
|