@@ -42,7 +42,8 @@ export function Surface({
4242 onOpenBlockChange,
4343 onUpdate,
4444 onDuplicate,
45- onDelete
45+ onDelete,
46+ isPaletteDrag = false
4647} : {
4748 blocks : BuilderBlock [ ] ;
4849 workspaceName ?: string ;
@@ -58,10 +59,17 @@ export function Surface({
5859 onUpdate : ( id : string , block : SupportedBlock ) => void ;
5960 onDuplicate : ( id : string ) => void ;
6061 onDelete : ( id : string ) => void ;
62+ /** True while a palette item is being dragged (vs. reordering an existing block). */
63+ isPaletteDrag ?: boolean ;
6164} ) {
6265 const { setNodeRef, isOver } = useDroppable ( { id : SURFACE_DROPPABLE_ID } ) ;
6366 const isDark = previewTheme === 'dark' ;
6467
68+ // Show the end-of-list insertion bar when the cursor is past the last
69+ // block while dragging a palette item. For reorder we rely on
70+ // verticalListSortingStrategy's row shift instead.
71+ const showEndDropZone = isPaletteDrag && isOver && blocks . length > 0 ;
72+
6573 const isModal = previewSurface === 'modal' ;
6674 const blocksList = (
6775 < div
@@ -70,11 +78,15 @@ export function Surface({
7078 'flex min-h-[240px] flex-col py-2 transition-colors' ,
7179 isModal ? 'px-5' : 'px-2' ,
7280 isDark ? 'bg-[#1a1d21]' : 'bg-white' ,
73- isOver && ( isDark ? 'bg-[#2c3036]' : 'bg-[#f0f4ff]' )
81+ isPaletteDrag &&
82+ ( isDark
83+ ? 'bg-[#22262c] outline-2 outline-primary/40 -outline-offset-2 outline-dashed'
84+ : 'bg-[#f5f8ff] outline-2 outline-primary/40 -outline-offset-2 outline-dashed' ) ,
85+ isPaletteDrag && isOver && ( isDark ? 'bg-[#2c3036]' : 'bg-[#eaf0ff]' )
7486 ) }
7587 >
7688 { blocks . length === 0 ? (
77- < EmptyState isDark = { isDark } />
89+ < EmptyState isDark = { isDark } isPaletteDrag = { isPaletteDrag } isOver = { isOver } />
7890 ) : (
7991 < SortableContext items = { blocks . map ( ( b ) => b . id ) } strategy = { verticalListSortingStrategy } >
8092 { blocks . map ( ( block ) => (
@@ -89,8 +101,10 @@ export function Surface({
89101 onUpdate = { onUpdate }
90102 onDuplicate = { onDuplicate }
91103 onDelete = { onDelete }
104+ isPaletteDrag = { isPaletteDrag }
92105 />
93106 ) ) }
107+ { showEndDropZone ? < DropIndicator /> : null }
94108 </ SortableContext >
95109 ) }
96110 </ div >
@@ -313,31 +327,61 @@ function AppHomeFrame({
313327 * Placeholder shown inside the preview surface when the draft has no
314328 * blocks. Uses Slack-style explicit colors keyed off `isDark` so the
315329 * empty state visually matches the rest of the preview chrome and stays
316- * consistent regardless of the host app's light/dark theme.
330+ * consistent regardless of the host app's light/dark theme. While a
331+ * palette item is being dragged the copy switches to a "Drop here" cue
332+ * and the icon container brightens to confirm the empty surface itself
333+ * is the drop target.
317334 * @param props - empty state props
318335 * @param props.isDark - whether the dark Slack canvas is active
336+ * @param props.isPaletteDrag - whether a palette block is currently being dragged
337+ * @param props.isOver - whether the cursor is currently over the surface
319338 * @returns the rendered placeholder
320339 */
321- function EmptyState ( { isDark } : { isDark : boolean } ) {
340+ function EmptyState ( { isDark, isPaletteDrag, isOver } : { isDark : boolean ; isPaletteDrag : boolean ; isOver : boolean } ) {
341+ const active = isPaletteDrag && isOver ;
322342 return (
323343 < div
324344 className = { cn (
325- 'flex flex-1 flex-col items-center justify-center gap-3 px-6 py-10 text-center' ,
326- isDark ? 'text-white/60' : 'text-[#616061]'
345+ 'flex flex-1 flex-col items-center justify-center gap-3 px-6 py-10 text-center transition-colors ' ,
346+ active ? 'text-primary' : isDark ? 'text-white/60' : 'text-[#616061]'
327347 ) }
328348 >
329349 < span
330350 className = { cn (
331- 'flex h-12 w-12 items-center justify-center rounded-full' ,
332- isDark ? 'bg-white/5' : 'bg-[#f3f3f3]'
351+ 'flex h-12 w-12 items-center justify-center rounded-full transition-colors ' ,
352+ active ? 'bg-primary/15 text-primary' : isDark ? 'bg-white/5' : 'bg-[#f3f3f3]'
333353 ) }
334354 >
335355 < LayoutGrid className = "h-6 w-6" />
336356 </ span >
337357 < div className = "flex flex-col gap-0.5" >
338- < p className = { cn ( 'text-sm font-semibold' , isDark ? 'text-white' : 'text-[#1d1c1d]' ) } > Start adding blocks!</ p >
339- < p className = "text-xs" > Drag a block from the left, or click one to add it here.</ p >
358+ < p
359+ className = { cn (
360+ 'text-sm font-semibold transition-colors' ,
361+ active ? 'text-primary' : isDark ? 'text-white' : 'text-[#1d1c1d]'
362+ ) }
363+ >
364+ { active ? 'Drop to add block' : 'Start adding blocks!' }
365+ </ p >
366+ < p className = "text-xs" >
367+ { active ? 'Release to insert it here.' : 'Drag a block from the left, or click one to add it here.' }
368+ </ p >
340369 </ div >
341370 </ div >
342371 ) ;
343372}
373+
374+ /**
375+ * Insertion bar shown at the cursor's drop target while a palette item
376+ * is being dragged. A 2px primary-colored line spanning the row width
377+ * with a small filled caret on the leading edge so the user can see
378+ * exactly where the new block will land.
379+ * @returns the rendered drop indicator
380+ */
381+ function DropIndicator ( ) {
382+ return (
383+ < div aria-hidden = "true" className = "pointer-events-none relative my-1 h-0.5 w-full rounded-full bg-primary" >
384+ < span className = "-left-1 -top-[3px] absolute h-2 w-2 rounded-full bg-primary shadow-[0_0_0_2px_var(--color-background)]" />
385+ </ div >
386+ ) ;
387+ }
0 commit comments