@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.
@@ -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
- return (
65
- <div className="kyro-form-field">
66
- <label className="kyro-form-label">{field.label || field.name}</label>
67
- {isRelationship ? (
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
- ) : compact ? (
85
- <div className="kyro-form-array kyro-form-array--compact border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)]/30 rounded-md overflow-hidden">
86
- {(items as Record<string, unknown>[]).map((item, index) => (
87
- <div
88
- key={index}
89
- 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"
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
- <span className="text-[10px] font-bold text-[var(--kyro-text-muted)] pt-2 min-w-[18px] text-center">
92
- {index + 1}
93
- </span>
94
- <div className="flex-1 flex items-start gap-1.5 min-w-0">
95
- {(field as Field & { fields?: Field[] }).fields.map((f: Field) => {
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
- <div key={f.name} className="flex-1 min-w-0">
98
- {renderField(f, item, (newItem) => {
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
- </div>
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
- <button
108
- type="button"
109
- disabled={disabled}
110
- onClick={() => onChange(items.filter((_: unknown, i: number) => i !== index))}
111
- 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"
112
- title="Remove"
113
- >
114
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
115
- <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
116
- </svg>
117
- </button>
118
- </div>
119
- ))}
120
- <button
121
- type="button"
122
- 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"
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
- onClick={(e) => { e.stopPropagation(); setNameDraft((block.name as string) || ""); setEditingName(true); }}
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-grab active:cursor-grabbing ${isEditing
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
- onClick={(e) => {
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="md:hidden p-1 hover:bg-[var(--kyro-surface-accent)] rounded text-[var(--kyro-text-secondary)] transition-colors"
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
- delay: 200,
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 - add as top-level block
573
- handleAddBlock(blockType);
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
  }