@jhits/plugin-blog 0.0.5 → 0.0.7
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 +16 -16
- package/src/api/config-handler.ts +76 -0
- package/src/api/handler.ts +4 -4
- package/src/api/router.ts +17 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useCategories.ts +76 -0
- package/src/index.tsx +8 -27
- package/src/init.tsx +0 -9
- package/src/lib/config-storage.ts +65 -0
- package/src/lib/layouts/blocks/ColumnsBlock.tsx +177 -13
- package/src/lib/layouts/blocks/ColumnsBlock.tsx.tmp +81 -0
- package/src/lib/layouts/registerLayoutBlocks.ts +6 -1
- package/src/lib/mappers/apiMapper.ts +53 -22
- package/src/registry/BlockRegistry.ts +1 -4
- package/src/state/EditorContext.tsx +39 -33
- package/src/state/types.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/post.ts +4 -0
- package/src/views/CanvasEditor/BlockWrapper.tsx +87 -24
- package/src/views/CanvasEditor/CanvasEditorView.tsx +214 -794
- package/src/views/CanvasEditor/EditorBody.tsx +317 -127
- package/src/views/CanvasEditor/EditorHeader.tsx +106 -17
- package/src/views/CanvasEditor/LayoutContainer.tsx +208 -380
- package/src/views/CanvasEditor/components/EditorCanvas.tsx +160 -0
- package/src/views/CanvasEditor/components/EditorLibrary.tsx +122 -0
- package/src/views/CanvasEditor/components/EditorSidebar.tsx +181 -0
- package/src/views/CanvasEditor/components/ErrorBanner.tsx +31 -0
- package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +260 -49
- package/src/views/CanvasEditor/components/index.ts +11 -0
- package/src/views/CanvasEditor/hooks/index.ts +10 -0
- package/src/views/CanvasEditor/hooks/useHeroBlock.ts +103 -0
- package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +142 -0
- package/src/views/CanvasEditor/hooks/usePostLoader.ts +39 -0
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +55 -0
- package/src/views/CanvasEditor/hooks/useUnsavedChanges.ts +339 -0
- package/src/views/PostManager/PostCards.tsx +18 -13
- package/src/views/PostManager/PostFilters.tsx +15 -0
- package/src/views/PostManager/PostManagerView.tsx +21 -15
- package/src/views/PostManager/PostTable.tsx +7 -4
|
@@ -35,19 +35,19 @@ export function EditorBody({
|
|
|
35
35
|
darkMode = true,
|
|
36
36
|
backgroundColors,
|
|
37
37
|
}: EditorBodyProps) {
|
|
38
|
-
//
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
console.log('[EditorBody] darkMode:', darkMode);
|
|
41
|
-
}, [darkMode]);
|
|
42
|
-
|
|
38
|
+
// --- State ---
|
|
43
39
|
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
|
44
40
|
const [isDragging, setIsDragging] = useState(false);
|
|
45
41
|
const [draggedBlockId, setDraggedBlockId] = useState<string | null>(null);
|
|
46
42
|
const [dropIndicatorPosition, setDropIndicatorPosition] = useState<{ top: number; left: number; width: number } | null>(null);
|
|
47
|
-
const
|
|
43
|
+
const [calculatedInsertIndex, setCalculatedInsertIndex] = useState<number | null>(null);
|
|
44
|
+
|
|
45
|
+
// --- Refs ---
|
|
46
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
47
|
+
const blockRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
|
48
48
|
|
|
49
|
-
//
|
|
50
|
-
|
|
49
|
+
// --- Cleanup & Event Listeners ---
|
|
50
|
+
useEffect(() => {
|
|
51
51
|
const handleClearIndicator = () => {
|
|
52
52
|
setDropIndicatorPosition(null);
|
|
53
53
|
setDragOverIndex(null);
|
|
@@ -62,13 +62,13 @@ export function EditorBody({
|
|
|
62
62
|
}
|
|
63
63
|
}, []);
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
React.useEffect(() => {
|
|
65
|
+
useEffect(() => {
|
|
67
66
|
const handleGlobalDragEnd = () => {
|
|
68
67
|
setDropIndicatorPosition(null);
|
|
69
68
|
setDragOverIndex(null);
|
|
70
69
|
setIsDragging(false);
|
|
71
70
|
setDraggedBlockId(null);
|
|
71
|
+
setCalculatedInsertIndex(null);
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
document.addEventListener('dragend', handleGlobalDragEnd);
|
|
@@ -76,9 +76,8 @@ export function EditorBody({
|
|
|
76
76
|
document.removeEventListener('dragend', handleGlobalDragEnd);
|
|
77
77
|
};
|
|
78
78
|
}, []);
|
|
79
|
-
// Store refs for all blocks - use callback refs to avoid hook violations
|
|
80
|
-
const blockRefs = React.useRef<Map<string, HTMLDivElement>>(new Map());
|
|
81
79
|
|
|
80
|
+
// --- Helper Functions ---
|
|
82
81
|
const setBlockRef = (blockId: string) => (el: HTMLDivElement | null) => {
|
|
83
82
|
if (el) {
|
|
84
83
|
blockRefs.current.set(blockId, el);
|
|
@@ -87,6 +86,8 @@ export function EditorBody({
|
|
|
87
86
|
}
|
|
88
87
|
};
|
|
89
88
|
|
|
89
|
+
// --- Drag & Drop Logic ---
|
|
90
|
+
|
|
90
91
|
const handleDragStart = (e: React.DragEvent) => {
|
|
91
92
|
// This is called when dragging starts from the container
|
|
92
93
|
// The actual block drag start is handled in BlockWrapper
|
|
@@ -109,37 +110,67 @@ export function EditorBody({
|
|
|
109
110
|
|
|
110
111
|
setDragOverIndex(index);
|
|
111
112
|
|
|
112
|
-
// Calculate position for absolute drop indicator
|
|
113
|
+
// Calculate position for absolute drop indicator - always show between blocks
|
|
113
114
|
const container = containerRef.current;
|
|
114
|
-
if (container) {
|
|
115
|
+
if (container && element) {
|
|
115
116
|
requestAnimationFrame(() => {
|
|
116
117
|
// Double-check refs are still valid inside RAF callback
|
|
117
118
|
if (!containerRef.current) return;
|
|
118
119
|
|
|
119
120
|
const containerRect = containerRef.current.getBoundingClientRect();
|
|
120
|
-
|
|
121
|
+
const elementRect = element.getBoundingClientRect();
|
|
121
122
|
const mouseY = e.clientY;
|
|
122
123
|
const relativeTop = mouseY - containerRect.top;
|
|
124
|
+
|
|
125
|
+
const elementTop = elementRect.top - containerRect.top;
|
|
126
|
+
const elementBottom = elementRect.bottom - containerRect.top;
|
|
127
|
+
const elementCenter = elementTop + elementRect.height / 2;
|
|
128
|
+
|
|
129
|
+
// Calculate width - use container width minus padding (32px on each side)
|
|
130
|
+
const padding = 32;
|
|
131
|
+
const width = containerRect.width - (padding * 2);
|
|
132
|
+
|
|
133
|
+
let finalTop: number;
|
|
123
134
|
|
|
124
135
|
// Determine if we should show above or below the element
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
if (relativeTop < elementCenter) {
|
|
137
|
+
// Show above this block - position between previous block and current block
|
|
138
|
+
if (index === 0) {
|
|
139
|
+
// First block - show at top of container (before first block)
|
|
140
|
+
finalTop = 0;
|
|
141
|
+
} else {
|
|
142
|
+
// Get previous block to find the gap
|
|
143
|
+
const prevBlock = blocks[index - 1];
|
|
144
|
+
const prevBlockEl = blockRefs.current.get(prevBlock.id);
|
|
145
|
+
if (prevBlockEl) {
|
|
146
|
+
const prevBlockRect = prevBlockEl.getBoundingClientRect();
|
|
147
|
+
const prevBlockBottom = prevBlockRect.bottom - containerRect.top;
|
|
148
|
+
// Position in the middle of the gap (mb-6 = 24px margin)
|
|
149
|
+
finalTop = prevBlockBottom + 12; // Half of the 24px gap
|
|
150
|
+
} else {
|
|
151
|
+
finalTop = elementTop;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
// Show below this block - position between current block and next block
|
|
156
|
+
if (index === blocks.length - 1) {
|
|
157
|
+
// Last block - show after it
|
|
158
|
+
finalTop = elementBottom;
|
|
134
159
|
} else {
|
|
135
|
-
|
|
160
|
+
// Get next block to find the gap
|
|
161
|
+
const nextBlock = blocks[index + 1];
|
|
162
|
+
const nextBlockEl = blockRefs.current.get(nextBlock.id);
|
|
163
|
+
if (nextBlockEl) {
|
|
164
|
+
const nextBlockRect = nextBlockEl.getBoundingClientRect();
|
|
165
|
+
const nextBlockTop = nextBlockRect.top - containerRect.top;
|
|
166
|
+
// Position in the middle of the gap
|
|
167
|
+
finalTop = elementBottom + (nextBlockTop - elementBottom) / 2;
|
|
168
|
+
} else {
|
|
169
|
+
finalTop = elementBottom;
|
|
170
|
+
}
|
|
136
171
|
}
|
|
137
172
|
}
|
|
138
173
|
|
|
139
|
-
// Calculate width - use container width minus padding (32px on each side)
|
|
140
|
-
const padding = 32;
|
|
141
|
-
const width = containerRect.width - (padding * 2);
|
|
142
|
-
|
|
143
174
|
setDropIndicatorPosition({
|
|
144
175
|
top: finalTop,
|
|
145
176
|
left: padding,
|
|
@@ -159,6 +190,8 @@ export function EditorBody({
|
|
|
159
190
|
if (!isMovingToNested) {
|
|
160
191
|
setDragOverIndex(null);
|
|
161
192
|
setDropIndicatorPosition(null);
|
|
193
|
+
setCalculatedInsertIndex(null);
|
|
194
|
+
setIsDragging(false);
|
|
162
195
|
}
|
|
163
196
|
}
|
|
164
197
|
};
|
|
@@ -231,6 +264,7 @@ export function EditorBody({
|
|
|
231
264
|
setIsDragging(false);
|
|
232
265
|
setDraggedBlockId(null);
|
|
233
266
|
setDropIndicatorPosition(null);
|
|
267
|
+
setCalculatedInsertIndex(null);
|
|
234
268
|
};
|
|
235
269
|
|
|
236
270
|
const handleBodyDragOver = (e: React.DragEvent) => {
|
|
@@ -244,26 +278,109 @@ export function EditorBody({
|
|
|
244
278
|
// We're over a nested container, clear our indicator
|
|
245
279
|
setDropIndicatorPosition(null);
|
|
246
280
|
setDragOverIndex(null);
|
|
281
|
+
setCalculatedInsertIndex(null);
|
|
247
282
|
setIsDragging(false);
|
|
248
283
|
return;
|
|
249
284
|
}
|
|
250
285
|
|
|
251
286
|
setIsDragging(true);
|
|
252
287
|
|
|
253
|
-
// Calculate drop indicator position
|
|
288
|
+
// Calculate drop indicator position based on mouse Y position relative to blocks
|
|
254
289
|
const container = containerRef.current;
|
|
255
|
-
if (container) {
|
|
290
|
+
if (container && blocks.length > 0) {
|
|
256
291
|
requestAnimationFrame(() => {
|
|
257
292
|
if (!containerRef.current) return;
|
|
258
293
|
|
|
259
294
|
const containerRect = containerRef.current.getBoundingClientRect();
|
|
260
|
-
|
|
261
|
-
const relativeTop = containerRect.
|
|
295
|
+
const mouseY = e.clientY;
|
|
296
|
+
const relativeTop = mouseY - containerRect.top;
|
|
262
297
|
const padding = 32;
|
|
263
298
|
const width = containerRect.width - (padding * 2);
|
|
264
299
|
|
|
300
|
+
// Find the closest insertion point based on block positions - always between blocks
|
|
301
|
+
let insertIndex = blocks.length; // Default to end
|
|
302
|
+
let indicatorTop = containerRect.height; // Default to bottom
|
|
303
|
+
|
|
304
|
+
// Check each block to find where to insert
|
|
305
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
306
|
+
const blockEl = blockRefs.current.get(blocks[i].id);
|
|
307
|
+
if (blockEl) {
|
|
308
|
+
const blockRect = blockEl.getBoundingClientRect();
|
|
309
|
+
const blockTop = blockRect.top - containerRect.top;
|
|
310
|
+
const blockBottom = blockRect.bottom - containerRect.top;
|
|
311
|
+
const blockCenter = blockTop + blockRect.height / 2;
|
|
312
|
+
|
|
313
|
+
// If mouse is above this block's center, insert before it
|
|
314
|
+
if (relativeTop < blockCenter) {
|
|
315
|
+
insertIndex = i;
|
|
316
|
+
if (i === 0) {
|
|
317
|
+
// First block - show at top
|
|
318
|
+
indicatorTop = 0;
|
|
319
|
+
} else {
|
|
320
|
+
// Get previous block to find the gap
|
|
321
|
+
const prevBlock = blocks[i - 1];
|
|
322
|
+
const prevBlockEl = blockRefs.current.get(prevBlock.id);
|
|
323
|
+
if (prevBlockEl) {
|
|
324
|
+
const prevBlockRect = prevBlockEl.getBoundingClientRect();
|
|
325
|
+
const prevBlockBottom = prevBlockRect.bottom - containerRect.top;
|
|
326
|
+
// Position in the middle of the gap
|
|
327
|
+
indicatorTop = prevBlockBottom + (blockTop - prevBlockBottom) / 2;
|
|
328
|
+
} else {
|
|
329
|
+
indicatorTop = blockTop;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
// If mouse is below this block's center, check if we should insert after it
|
|
335
|
+
else if (i === blocks.length - 1 || relativeTop < blockBottom) {
|
|
336
|
+
insertIndex = i + 1;
|
|
337
|
+
if (i === blocks.length - 1) {
|
|
338
|
+
// Last block - show after it
|
|
339
|
+
indicatorTop = blockBottom;
|
|
340
|
+
} else {
|
|
341
|
+
// Get next block to find the gap
|
|
342
|
+
const nextBlock = blocks[i + 1];
|
|
343
|
+
const nextBlockEl = blockRefs.current.get(nextBlock.id);
|
|
344
|
+
if (nextBlockEl) {
|
|
345
|
+
const nextBlockRect = nextBlockEl.getBoundingClientRect();
|
|
346
|
+
const nextBlockTop = nextBlockRect.top - containerRect.top;
|
|
347
|
+
// Position in the middle of the gap
|
|
348
|
+
indicatorTop = blockBottom + (nextBlockTop - blockBottom) / 2;
|
|
349
|
+
} else {
|
|
350
|
+
indicatorTop = blockBottom;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// If we didn't find a position, use the last block's bottom
|
|
359
|
+
if (insertIndex === blocks.length) {
|
|
360
|
+
const lastBlockEl = blockRefs.current.get(blocks[blocks.length - 1].id);
|
|
361
|
+
if (lastBlockEl) {
|
|
362
|
+
const lastBlockRect = lastBlockEl.getBoundingClientRect();
|
|
363
|
+
indicatorTop = lastBlockRect.bottom - containerRect.top;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
setCalculatedInsertIndex(insertIndex);
|
|
265
368
|
setDropIndicatorPosition({
|
|
266
|
-
top:
|
|
369
|
+
top: indicatorTop,
|
|
370
|
+
left: padding,
|
|
371
|
+
width: width,
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
} else if (container) {
|
|
375
|
+
// No blocks, show indicator at top
|
|
376
|
+
requestAnimationFrame(() => {
|
|
377
|
+
if (!containerRef.current) return;
|
|
378
|
+
const containerRect = containerRef.current.getBoundingClientRect();
|
|
379
|
+
const padding = 32;
|
|
380
|
+
const width = containerRect.width - (padding * 2);
|
|
381
|
+
setCalculatedInsertIndex(0);
|
|
382
|
+
setDropIndicatorPosition({
|
|
383
|
+
top: 0,
|
|
267
384
|
left: padding,
|
|
268
385
|
width: width,
|
|
269
386
|
});
|
|
@@ -280,11 +397,40 @@ export function EditorBody({
|
|
|
280
397
|
const blockId = dataTransferBlockId || globalBlockId;
|
|
281
398
|
const blockType = e.dataTransfer.getData('block-type');
|
|
282
399
|
|
|
400
|
+
// Use the calculated insert index if available, otherwise calculate it now
|
|
401
|
+
let targetIndex = calculatedInsertIndex !== null ? calculatedInsertIndex : blocks.length;
|
|
402
|
+
|
|
403
|
+
// If we don't have a calculated index, calculate it based on mouse position
|
|
404
|
+
if (calculatedInsertIndex === null && containerRef.current && blocks.length > 0) {
|
|
405
|
+
const containerRect = containerRef.current.getBoundingClientRect();
|
|
406
|
+
const mouseY = e.clientY;
|
|
407
|
+
const relativeTop = mouseY - containerRect.top;
|
|
408
|
+
|
|
409
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
410
|
+
const blockEl = blockRefs.current.get(blocks[i].id);
|
|
411
|
+
if (blockEl) {
|
|
412
|
+
const blockRect = blockEl.getBoundingClientRect();
|
|
413
|
+
const blockTop = blockRect.top - containerRect.top;
|
|
414
|
+
const blockCenter = blockTop + blockRect.height / 2;
|
|
415
|
+
|
|
416
|
+
if (relativeTop < blockCenter) {
|
|
417
|
+
targetIndex = i;
|
|
418
|
+
break;
|
|
419
|
+
} else if (i === blocks.length - 1) {
|
|
420
|
+
targetIndex = blocks.length;
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
283
427
|
console.log('[EditorBody] Body Drop Event:', {
|
|
284
428
|
dataTransferBlockId,
|
|
285
429
|
globalBlockId,
|
|
286
430
|
resolvedBlockId: blockId,
|
|
287
431
|
blockType,
|
|
432
|
+
calculatedInsertIndex,
|
|
433
|
+
targetIndex,
|
|
288
434
|
currentBlocksCount: blocks.length,
|
|
289
435
|
rootBlockIds: blocks.map(b => b.id),
|
|
290
436
|
});
|
|
@@ -295,28 +441,32 @@ export function EditorBody({
|
|
|
295
441
|
}
|
|
296
442
|
|
|
297
443
|
if (blockId) {
|
|
298
|
-
// Moving existing block to
|
|
444
|
+
// Moving existing block to calculated position
|
|
299
445
|
const currentIndex = blocks.findIndex(b => b.id === blockId);
|
|
300
446
|
console.log('[EditorBody] Body drop - block location:', {
|
|
301
447
|
blockId,
|
|
302
448
|
currentIndex,
|
|
303
449
|
isInRoot: currentIndex !== -1,
|
|
304
|
-
targetIndex
|
|
450
|
+
targetIndex,
|
|
305
451
|
});
|
|
306
452
|
|
|
307
453
|
if (currentIndex !== -1) {
|
|
308
|
-
// Already in root, move to
|
|
309
|
-
|
|
310
|
-
|
|
454
|
+
// Already in root, move to target position
|
|
455
|
+
if (currentIndex !== targetIndex) {
|
|
456
|
+
// Adjust for removal if moving forward
|
|
457
|
+
const adjustedIndex = currentIndex < targetIndex ? targetIndex - 1 : targetIndex;
|
|
458
|
+
console.log('[EditorBody] Moving within root:', { blockId, currentIndex, adjustedIndex });
|
|
459
|
+
onBlockMove(blockId, Math.max(0, adjustedIndex));
|
|
460
|
+
}
|
|
311
461
|
} else {
|
|
312
|
-
// Block is nested - move it to root level at
|
|
313
|
-
console.log('[EditorBody] Moving nested block to root
|
|
314
|
-
onBlockMove(blockId,
|
|
462
|
+
// Block is nested - move it to root level at target position
|
|
463
|
+
console.log('[EditorBody] Moving nested block to root:', { blockId, targetIndex });
|
|
464
|
+
onBlockMove(blockId, targetIndex, undefined);
|
|
315
465
|
}
|
|
316
466
|
} else if (blockType) {
|
|
317
|
-
// Adding new block
|
|
318
|
-
console.log('[EditorBody] Adding new block
|
|
319
|
-
onBlockAdd(blockType,
|
|
467
|
+
// Adding new block at calculated position
|
|
468
|
+
console.log('[EditorBody] Adding new block:', { blockType, index: targetIndex });
|
|
469
|
+
onBlockAdd(blockType, targetIndex);
|
|
320
470
|
} else {
|
|
321
471
|
console.warn('[EditorBody] Body drop with no block ID or type!');
|
|
322
472
|
}
|
|
@@ -325,6 +475,7 @@ export function EditorBody({
|
|
|
325
475
|
setDragOverIndex(null);
|
|
326
476
|
setDraggedBlockId(null);
|
|
327
477
|
setDropIndicatorPosition(null);
|
|
478
|
+
setCalculatedInsertIndex(null);
|
|
328
479
|
};
|
|
329
480
|
|
|
330
481
|
return (
|
|
@@ -343,94 +494,41 @@ export function EditorBody({
|
|
|
343
494
|
setDragOverIndex(null);
|
|
344
495
|
setIsDragging(false);
|
|
345
496
|
setDraggedBlockId(null);
|
|
497
|
+
setCalculatedInsertIndex(null);
|
|
346
498
|
}}
|
|
347
499
|
>
|
|
348
|
-
{/*
|
|
349
|
-
{dropIndicatorPosition && (
|
|
350
|
-
<
|
|
351
|
-
className="absolute z-50 pointer-events-none"
|
|
352
|
-
style={{
|
|
353
|
-
top: `${dropIndicatorPosition.top - 12}px`,
|
|
354
|
-
left: `${dropIndicatorPosition.left}px`,
|
|
355
|
-
width: `${dropIndicatorPosition.width}px`,
|
|
356
|
-
height: '24px',
|
|
357
|
-
}}
|
|
358
|
-
>
|
|
359
|
-
{/* Drop zone background */}
|
|
360
|
-
<div className={`absolute inset-0 rounded-lg border border-dashed backdrop-blur-sm ${darkMode
|
|
361
|
-
? 'bg-primary/10 dark:bg-primary/20 border-primary dark:border-primary/60'
|
|
362
|
-
: 'bg-primary/10 border-primary'
|
|
363
|
-
}`} />
|
|
364
|
-
|
|
365
|
-
{/* Center line indicator */}
|
|
366
|
-
<div className={`absolute top-1/2 left-0 right-0 h-0.5 rounded-full transform -translate-y-1/2 ${darkMode ? 'bg-primary dark:bg-primary/80' : 'bg-primary'
|
|
367
|
-
}`} />
|
|
368
|
-
|
|
369
|
-
{/* Drop icon indicator */}
|
|
370
|
-
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
|
371
|
-
<div className={`w-6 h-6 rounded-full flex items-center justify-center shadow-lg shadow-primary/30 ${darkMode ? 'bg-primary dark:bg-primary/90' : 'bg-primary'
|
|
372
|
-
}`}>
|
|
373
|
-
<div className={`w-2 h-2 rounded-full ${darkMode ? 'bg-white dark:bg-neutral-900' : 'bg-white'
|
|
374
|
-
}`} />
|
|
375
|
-
</div>
|
|
376
|
-
</div>
|
|
377
|
-
</div>
|
|
500
|
+
{/* 1. Visual Indicator Overlay */}
|
|
501
|
+
{dropIndicatorPosition && isDragging && (
|
|
502
|
+
<DropIndicator position={dropIndicatorPosition} darkMode={darkMode} />
|
|
378
503
|
)}
|
|
379
504
|
|
|
380
|
-
<div
|
|
505
|
+
<div
|
|
506
|
+
className="flex flex-col p-8"
|
|
507
|
+
onDragOver={(e) => {
|
|
508
|
+
// Only handle if not over a block or nested container
|
|
509
|
+
const target = e.target as HTMLElement;
|
|
510
|
+
const isOverBlock = target.closest('[data-block-wrapper]');
|
|
511
|
+
const isOverNestedContainer = target.closest('[data-layout-container]') && target.closest('[data-layout-container]') !== containerRef.current;
|
|
512
|
+
|
|
513
|
+
if (!isOverBlock && !isOverNestedContainer) {
|
|
514
|
+
handleBodyDragOver(e);
|
|
515
|
+
}
|
|
516
|
+
}}
|
|
517
|
+
onDrop={(e) => {
|
|
518
|
+
// Only handle if not over a block or nested container
|
|
519
|
+
const target = e.target as HTMLElement;
|
|
520
|
+
const isOverBlock = target.closest('[data-block-wrapper]');
|
|
521
|
+
const isOverNestedContainer = target.closest('[data-layout-container]') && target.closest('[data-layout-container]') !== containerRef.current;
|
|
522
|
+
|
|
523
|
+
if (!isOverBlock && !isOverNestedContainer) {
|
|
524
|
+
handleBodyDrop(e);
|
|
525
|
+
}
|
|
526
|
+
}}
|
|
527
|
+
>
|
|
528
|
+
{/* 2. Content Area */}
|
|
381
529
|
{blocks.length === 0 ? (
|
|
382
|
-
|
|
383
|
-
<div
|
|
384
|
-
className={`flex flex-col items-center justify-center py-20 px-8 rounded-2xl border border-dashed transition-all ${darkMode
|
|
385
|
-
? isDragging
|
|
386
|
-
? 'border-primary/30 bg-primary/5 dark:bg-primary/10'
|
|
387
|
-
: 'border-gray-200/50 dark:border-neutral-700/50 bg-neutral-50/50 dark:bg-neutral-800/30'
|
|
388
|
-
: isDragging
|
|
389
|
-
? 'border-primary/30 bg-primary/5'
|
|
390
|
-
: 'border-gray-200/50 bg-neutral-50/50'
|
|
391
|
-
}`}
|
|
392
|
-
>
|
|
393
|
-
<div
|
|
394
|
-
className={`p-4 rounded-full mb-4 transition-colors ${darkMode
|
|
395
|
-
? isDragging
|
|
396
|
-
? 'bg-primary/10 dark:bg-primary/20'
|
|
397
|
-
: 'bg-neutral-100 dark:bg-neutral-800'
|
|
398
|
-
: isDragging
|
|
399
|
-
? 'bg-primary/10'
|
|
400
|
-
: 'bg-neutral-100'
|
|
401
|
-
}`}
|
|
402
|
-
>
|
|
403
|
-
<Plus
|
|
404
|
-
size={24}
|
|
405
|
-
className={`transition-colors ${darkMode
|
|
406
|
-
? isDragging
|
|
407
|
-
? 'text-primary'
|
|
408
|
-
: 'text-neutral-400 dark:text-neutral-500'
|
|
409
|
-
: isDragging
|
|
410
|
-
? 'text-primary'
|
|
411
|
-
: 'text-neutral-400'
|
|
412
|
-
}`}
|
|
413
|
-
/>
|
|
414
|
-
</div>
|
|
415
|
-
<p
|
|
416
|
-
className={`text-sm font-black uppercase tracking-wider transition-colors ${darkMode
|
|
417
|
-
? isDragging
|
|
418
|
-
? 'text-primary'
|
|
419
|
-
: 'text-neutral-500 dark:text-neutral-400'
|
|
420
|
-
: isDragging
|
|
421
|
-
? 'text-primary'
|
|
422
|
-
: 'text-neutral-500'
|
|
423
|
-
}`}
|
|
424
|
-
>
|
|
425
|
-
{isDragging ? 'Drop Block Here' : 'Drop a component here'}
|
|
426
|
-
</p>
|
|
427
|
-
<p className={`text-xs mt-2 ${darkMode ? 'text-neutral-400 dark:text-neutral-500' : 'text-neutral-400'
|
|
428
|
-
}`}>
|
|
429
|
-
Drag blocks from the library to get started
|
|
430
|
-
</p>
|
|
431
|
-
</div>
|
|
530
|
+
<EmptyState isDragging={isDragging} darkMode={darkMode} />
|
|
432
531
|
) : (
|
|
433
|
-
/* Blocks with visual drop zones between them */
|
|
434
532
|
blocks.map((block, index) => (
|
|
435
533
|
<Fragment key={block.id}>
|
|
436
534
|
{/* Visual Drop Zone - appears between blocks when dragging */}
|
|
@@ -473,3 +571,95 @@ export function EditorBody({
|
|
|
473
571
|
);
|
|
474
572
|
}
|
|
475
573
|
|
|
574
|
+
/**
|
|
575
|
+
* Visual Line that shows where the block will land
|
|
576
|
+
*/
|
|
577
|
+
function DropIndicator({ position, darkMode }: { position: { top: number; left: number; width: number }; darkMode: boolean }) {
|
|
578
|
+
return (
|
|
579
|
+
<div
|
|
580
|
+
className="absolute z-50 pointer-events-none"
|
|
581
|
+
style={{
|
|
582
|
+
top: `${position.top - 12}px`,
|
|
583
|
+
left: `${position.left}px`,
|
|
584
|
+
width: `${position.width}px`,
|
|
585
|
+
height: '24px',
|
|
586
|
+
}}
|
|
587
|
+
>
|
|
588
|
+
{/* Drop zone background */}
|
|
589
|
+
<div className={`absolute inset-0 rounded-lg border border-dashed backdrop-blur-sm ${darkMode
|
|
590
|
+
? 'bg-primary/10 dark:bg-primary/20 border-primary dark:border-primary/60'
|
|
591
|
+
: 'bg-primary/10 border-primary'
|
|
592
|
+
}`} />
|
|
593
|
+
|
|
594
|
+
{/* Center line indicator */}
|
|
595
|
+
<div className={`absolute top-1/2 left-0 right-0 h-0.5 rounded-full transform -translate-y-1/2 ${darkMode ? 'bg-primary dark:bg-primary/80' : 'bg-primary'
|
|
596
|
+
}`} />
|
|
597
|
+
|
|
598
|
+
{/* Drop icon indicator */}
|
|
599
|
+
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
|
600
|
+
<div className={`w-6 h-6 rounded-full flex items-center justify-center shadow-lg shadow-primary/30 ${darkMode ? 'bg-primary dark:bg-primary/90' : 'bg-primary'
|
|
601
|
+
}`}>
|
|
602
|
+
<div className={`w-2 h-2 rounded-full ${darkMode ? 'bg-white dark:bg-neutral-900' : 'bg-white'
|
|
603
|
+
}`} />
|
|
604
|
+
</div>
|
|
605
|
+
</div>
|
|
606
|
+
</div>
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Placeholder when the editor is empty
|
|
612
|
+
*/
|
|
613
|
+
function EmptyState({ isDragging, darkMode }: { isDragging: boolean; darkMode: boolean }) {
|
|
614
|
+
return (
|
|
615
|
+
<div
|
|
616
|
+
className={`flex flex-col items-center justify-center py-20 px-8 rounded-2xl border border-dashed transition-all ${darkMode
|
|
617
|
+
? isDragging
|
|
618
|
+
? 'border-primary/30 bg-primary/5 dark:bg-primary/10'
|
|
619
|
+
: 'border-gray-200/50 dark:border-neutral-700/50 bg-neutral-50/50 dark:bg-neutral-800/30'
|
|
620
|
+
: isDragging
|
|
621
|
+
? 'border-primary/30 bg-primary/5'
|
|
622
|
+
: 'border-gray-200/50 bg-neutral-50/50'
|
|
623
|
+
}`}
|
|
624
|
+
>
|
|
625
|
+
<div
|
|
626
|
+
className={`p-4 rounded-full mb-4 transition-colors ${darkMode
|
|
627
|
+
? isDragging
|
|
628
|
+
? 'bg-primary/10 dark:bg-primary/20'
|
|
629
|
+
: 'bg-neutral-100 dark:bg-neutral-800'
|
|
630
|
+
: isDragging
|
|
631
|
+
? 'bg-primary/10'
|
|
632
|
+
: 'bg-neutral-100'
|
|
633
|
+
}`}
|
|
634
|
+
>
|
|
635
|
+
<Plus
|
|
636
|
+
size={24}
|
|
637
|
+
className={`transition-colors ${darkMode
|
|
638
|
+
? isDragging
|
|
639
|
+
? 'text-primary'
|
|
640
|
+
: 'text-neutral-400 dark:text-neutral-500'
|
|
641
|
+
: isDragging
|
|
642
|
+
? 'text-primary'
|
|
643
|
+
: 'text-neutral-400'
|
|
644
|
+
}`}
|
|
645
|
+
/>
|
|
646
|
+
</div>
|
|
647
|
+
<p
|
|
648
|
+
className={`text-sm font-black uppercase tracking-wider transition-colors ${darkMode
|
|
649
|
+
? isDragging
|
|
650
|
+
? 'text-primary'
|
|
651
|
+
: 'text-neutral-500 dark:text-neutral-400'
|
|
652
|
+
: isDragging
|
|
653
|
+
? 'text-primary'
|
|
654
|
+
: 'text-neutral-500'
|
|
655
|
+
}`}
|
|
656
|
+
>
|
|
657
|
+
{isDragging ? 'Drop Block Here' : 'Drop a component here'}
|
|
658
|
+
</p>
|
|
659
|
+
<p className={`text-xs mt-2 ${darkMode ? 'text-neutral-400 dark:text-neutral-500' : 'text-neutral-400'
|
|
660
|
+
}`}>
|
|
661
|
+
Drag blocks from the library to get started
|
|
662
|
+
</p>
|
|
663
|
+
</div>
|
|
664
|
+
);
|
|
665
|
+
}
|