@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.
Files changed (39) hide show
  1. package/package.json +16 -16
  2. package/src/api/config-handler.ts +76 -0
  3. package/src/api/handler.ts +4 -4
  4. package/src/api/router.ts +17 -0
  5. package/src/hooks/index.ts +1 -0
  6. package/src/hooks/useCategories.ts +76 -0
  7. package/src/index.tsx +8 -27
  8. package/src/init.tsx +0 -9
  9. package/src/lib/config-storage.ts +65 -0
  10. package/src/lib/layouts/blocks/ColumnsBlock.tsx +177 -13
  11. package/src/lib/layouts/blocks/ColumnsBlock.tsx.tmp +81 -0
  12. package/src/lib/layouts/registerLayoutBlocks.ts +6 -1
  13. package/src/lib/mappers/apiMapper.ts +53 -22
  14. package/src/registry/BlockRegistry.ts +1 -4
  15. package/src/state/EditorContext.tsx +39 -33
  16. package/src/state/types.ts +1 -1
  17. package/src/types/index.ts +2 -0
  18. package/src/types/post.ts +4 -0
  19. package/src/views/CanvasEditor/BlockWrapper.tsx +87 -24
  20. package/src/views/CanvasEditor/CanvasEditorView.tsx +214 -794
  21. package/src/views/CanvasEditor/EditorBody.tsx +317 -127
  22. package/src/views/CanvasEditor/EditorHeader.tsx +106 -17
  23. package/src/views/CanvasEditor/LayoutContainer.tsx +208 -380
  24. package/src/views/CanvasEditor/components/EditorCanvas.tsx +160 -0
  25. package/src/views/CanvasEditor/components/EditorLibrary.tsx +122 -0
  26. package/src/views/CanvasEditor/components/EditorSidebar.tsx +181 -0
  27. package/src/views/CanvasEditor/components/ErrorBanner.tsx +31 -0
  28. package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +260 -49
  29. package/src/views/CanvasEditor/components/index.ts +11 -0
  30. package/src/views/CanvasEditor/hooks/index.ts +10 -0
  31. package/src/views/CanvasEditor/hooks/useHeroBlock.ts +103 -0
  32. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +142 -0
  33. package/src/views/CanvasEditor/hooks/usePostLoader.ts +39 -0
  34. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +55 -0
  35. package/src/views/CanvasEditor/hooks/useUnsavedChanges.ts +339 -0
  36. package/src/views/PostManager/PostCards.tsx +18 -13
  37. package/src/views/PostManager/PostFilters.tsx +15 -0
  38. package/src/views/PostManager/PostManagerView.tsx +21 -15
  39. 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
- // Debug: Log darkMode value
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 containerRef = React.useRef<HTMLDivElement>(null);
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
- // Listen for clear-drop-indicator events from nested containers
50
- React.useEffect(() => {
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
- // Global drag end handler - clear all indicators when drag ends
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 using mouse position
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
- // Use mouse Y position relative to container for accurate positioning
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
- let finalTop = relativeTop;
126
- if (element) {
127
- const elementRect = element.getBoundingClientRect();
128
- const elementTop = elementRect.top - containerRect.top;
129
- const elementCenter = elementTop + elementRect.height / 2;
130
-
131
- // If mouse is in upper half of element, show above; otherwise show below
132
- if (relativeTop < elementCenter) {
133
- finalTop = elementTop;
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
- finalTop = elementTop + elementRect.height;
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 for body drop zone (at the end)
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
- // Position at the bottom of the container
261
- const relativeTop = containerRect.height;
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: relativeTop,
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 end of root level
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: blocks.length,
450
+ targetIndex,
305
451
  });
306
452
 
307
453
  if (currentIndex !== -1) {
308
- // Already in root, move to end
309
- console.log('[EditorBody] Moving within root to end:', { blockId, targetIndex: blocks.length - 1 });
310
- onBlockMove(blockId, blocks.length - 1);
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 the end
313
- console.log('[EditorBody] Moving nested block to root end:', { blockId, targetIndex: blocks.length });
314
- onBlockMove(blockId, blocks.length, undefined);
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 to end
318
- console.log('[EditorBody] Adding new block to end:', { blockType, index: blocks.length });
319
- onBlockAdd(blockType, blocks.length);
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
- {/* Enhanced Drop Indicator - Visual drop zone */}
349
- {dropIndicatorPosition && (
350
- <div
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 className="flex flex-col p-8">
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
- /* Empty State - Drop Zone */
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
+ }