create-nextblock 0.2.46 → 0.2.48

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 (32) hide show
  1. package/package.json +1 -1
  2. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +73 -34
  3. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +309 -53
  4. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +175 -25
  5. package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +44 -26
  6. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +74 -16
  7. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +2 -0
  8. package/templates/nextblock-template/app/cms/dashboard/actions.ts +98 -0
  9. package/templates/nextblock-template/app/cms/dashboard/page.tsx +76 -153
  10. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +16 -11
  11. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +23 -12
  12. package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +4 -0
  13. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +30 -6
  14. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +17 -11
  15. package/templates/nextblock-template/app/cms/pages/page.tsx +6 -3
  16. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +18 -12
  17. package/templates/nextblock-template/app/cms/posts/page.tsx +8 -5
  18. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +18 -5
  19. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +20 -4
  20. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +33 -7
  21. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +3 -3
  22. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +41 -13
  23. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +15 -13
  24. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +2 -3
  25. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +50 -0
  26. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +14 -2
  27. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +3 -6
  28. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +33 -13
  29. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +41 -49
  30. package/templates/nextblock-template/hooks/use-hotkeys.ts +27 -0
  31. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +3 -2
  32. package/templates/nextblock-template/package.json +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nextblock",
3
- "version": "0.2.46",
3
+ "version": "0.2.48",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -5,10 +5,8 @@ import { cn } from "@nextblock-cms/utils";
5
5
  import {
6
6
  Dialog,
7
7
  DialogContent,
8
- DialogHeader,
9
8
  DialogTitle,
10
- DialogFooter,
11
- DialogDescription,
9
+ DialogClose,
12
10
  } from "@nextblock-cms/ui";
13
11
  import { Button } from "@nextblock-cms/ui";
14
12
  import { blockRegistry, type BlockType } from "@/lib/blocks/blockRegistry";
@@ -26,6 +24,8 @@ export type BlockEditorProps<T = unknown> = {
26
24
  block: Block<T>;
27
25
  content: T;
28
26
  onChange: (newContent: T) => void;
27
+ className?: string; // Added for editor component styling
28
+ sectionBackground?: import("@/lib/blocks/blockRegistry").SectionBlockContent['background'];
29
29
  };
30
30
 
31
31
  type BlockEditorModalProps = {
@@ -34,6 +34,7 @@ type BlockEditorModalProps = {
34
34
  onClose: () => void;
35
35
  onSave: (updatedContent: unknown) => void;
36
36
  EditorComponent: LazyExoticComponent<ComponentType<BlockEditorProps<unknown>>> | ComponentType<BlockEditorProps<unknown>>;
37
+ sectionBackground?: import("@/lib/blocks/blockRegistry").SectionBlockContent['background'];
37
38
  };
38
39
 
39
40
  export function BlockEditorModal({
@@ -42,8 +43,10 @@ export function BlockEditorModal({
42
43
  onClose,
43
44
  onSave,
44
45
  EditorComponent,
46
+ sectionBackground,
45
47
  }: BlockEditorModalProps) {
46
48
  const [tempContent, setTempContent] = useState(block.content);
49
+ const isValid = true; // Placeholder for future validation logic
47
50
 
48
51
  useEffect(() => {
49
52
  // When the modal is opened with a new block, reset the temp content
@@ -56,42 +59,78 @@ export function BlockEditorModal({
56
59
  onSave(tempContent);
57
60
  };
58
61
 
62
+ const handleContentChange = (newContent: unknown) => {
63
+ setTempContent(newContent);
64
+ // Potentially add validation here and set isValid
65
+ };
66
+
59
67
  const blockInfo = blockRegistry[block.type];
68
+ const displayText = blockInfo?.label || "Block";
60
69
 
61
70
  return (
62
71
  <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
63
- <DialogContent
64
- className={cn(
65
- "w-[90%] max-w-7xl max-h-[90vh]",
66
- {
67
- // For rich text editor, use fixed height and internal scroll so footer stays visible
68
- "h-[90vh] flex flex-col": block.type === "text",
69
- // For other blocks, allow the modal itself to scroll if content exceeds viewport
70
- "overflow-y-auto": block.type !== "text",
71
- }
72
- )}
72
+ <DialogContent
73
+ className="max-w-6xl h-[90vh] flex flex-col p-0 gap-0 overflow-hidden"
74
+ onInteractOutside={(e) => {
75
+ // Prevent closing when interacting with Tiptap bubbles outside the dialog portal (rare but possible)
76
+ }}
73
77
  >
74
- <DialogHeader>
75
- <DialogTitle>Editing {blockInfo?.label || "Block"}</DialogTitle>
76
- <DialogDescription>
77
- Make changes to your block here. Click save when you&apos;re done.
78
- </DialogDescription>
79
- </DialogHeader>
80
- <div className="py-4 flex-grow flex flex-col min-h-0">
81
- <Suspense fallback={<div className="flex justify-center items-center h-32">Loading editor...</div>}>
82
- <EditorComponent
83
- block={block}
84
- content={tempContent}
85
- onChange={setTempContent}
86
- />
87
- </Suspense>
88
- </div>
89
- <DialogFooter>
90
- <Button variant="outline" onClick={onClose}>
91
- Cancel
92
- </Button>
93
- <Button onClick={handleSave}>Save</Button>
94
- </DialogFooter>
78
+ {/* Header */}
79
+ <div className="flex items-center justify-between p-4 border-b bg-background/95 backdrop-blur z-10">
80
+ <div className="flex items-center gap-2">
81
+ <DialogTitle className="text-lg font-semibold">Edit {displayText}</DialogTitle>
82
+ </div>
83
+ <div className="flex items-center gap-2">
84
+ <DialogClose asChild>
85
+ <Button variant="ghost" size="sm">Cancel</Button>
86
+ </DialogClose>
87
+ <Button onClick={handleSave} disabled={!isValid} size="sm">
88
+ Save (CMD+S)
89
+ </Button>
90
+ </div>
91
+ </div>
92
+
93
+ {/* Editor Area with Contextual Background */}
94
+ <div
95
+ className={cn(
96
+ "flex-1 overflow-y-auto p-6",
97
+ // Conditional Background Logic:
98
+ // Only apply specific section background to 'text' and 'heading' blocks to allow "Live Preview" of copy.
99
+ // For complex blocks like Forms, Buttons, etc., keep a neutral background to ensure input field contrast.
100
+ (block.type === 'text' || block.type === 'heading') ? (
101
+ // If no specific background, use white/dark default
102
+ (!sectionBackground || sectionBackground.type === 'none') && "bg-muted/10"
103
+ ) : "bg-muted/10", // Default for non-text blocks
104
+
105
+ // Apply theme classes if present (ONLY for text/heading)
106
+ (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'theme' && sectionBackground.theme === 'primary' && 'bg-primary text-primary-foreground',
107
+ (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'theme' && sectionBackground.theme === 'secondary' && 'bg-secondary text-secondary-foreground',
108
+ (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'theme' && sectionBackground.theme === 'muted' && 'bg-muted text-muted-foreground',
109
+
110
+ // Dark mode prose invert if dark background (approximate check for solid color)
111
+ (block.type === 'text' || block.type === 'heading') && (sectionBackground?.type === 'solid' && sectionBackground.solid_color && ['#000', '#111', '#0f172a', 'black'].some(c => sectionBackground.solid_color?.includes(c))) && "[&_.prose]:prose-invert"
112
+ )}
113
+ style={{
114
+ // Only apply custom color/gradient styles for text/heading
115
+ backgroundColor: (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'solid' ? sectionBackground.solid_color : undefined,
116
+ backgroundImage: (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'gradient' && sectionBackground.gradient ?
117
+ `${sectionBackground.gradient.type}-gradient(${sectionBackground.gradient.direction}, ${sectionBackground.gradient.stops.map(s => `${s.color} ${s.position}%`).join(', ')})`
118
+ : undefined
119
+ }}
120
+ >
121
+ <div className="max-w-6xl mx-auto">
122
+ <Suspense fallback={<div className="flex justify-center items-center h-32">Loading editor...</div>}>
123
+ <EditorComponent
124
+ block={block}
125
+ content={tempContent}
126
+ onChange={handleContentChange}
127
+ className="bg-transparent border-none shadow-none focus-within:ring-0 min-h-[60vh]" // Make editor transparent
128
+ sectionBackground={sectionBackground} // Pass down if editor supports it
129
+ />
130
+ </Suspense>
131
+ </div>
132
+ </div>
133
+
95
134
  </DialogContent>
96
135
  </Dialog>
97
136
  );
@@ -1,10 +1,9 @@
1
- // app/cms/blocks/components/ColumnEditor.tsx
2
1
  "use client";
3
2
 
4
3
  import React, { useState, lazy } from 'react';
5
4
  import { cn } from '@nextblock-cms/utils';
6
5
  import { Button } from '@nextblock-cms/ui';
7
- import { PlusCircle, Trash2, Edit2, GripVertical } from "lucide-react";
6
+ import { PlusCircle, Trash2, Edit2, GripVertical, Image as ImageIcon } from "lucide-react";
8
7
  import type { SectionBlockContent } from '@/lib/blocks/blockRegistry';
9
8
  import { availableBlockTypes, getBlockDefinition, getInitialContent, BlockType } from '@/lib/blocks/blockRegistry';
10
9
  import { useDroppable } from "@dnd-kit/core";
@@ -14,7 +13,6 @@ import { BlockEditorModal } from './BlockEditorModal';
14
13
  import { ConfirmationDialog } from '@nextblock-cms/ui';
15
14
  import BlockTypeSelector from './BlockTypeSelector';
16
15
 
17
-
18
16
  type ColumnBlock = SectionBlockContent['column_blocks'][0][0];
19
17
 
20
18
  // Sortable block item component for column blocks
@@ -26,9 +24,10 @@ interface SortableColumnBlockProps {
26
24
  onDelete: () => void;
27
25
  blockType: 'section' | 'hero';
28
26
  onClick: (e: React.MouseEvent<HTMLDivElement>) => void;
27
+ sectionBackground?: SectionBlockContent['background'];
29
28
  }
30
29
 
31
- function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, blockType, onClick }: SortableColumnBlockProps) {
30
+ function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, blockType, onClick, sectionBackground }: SortableColumnBlockProps) {
32
31
  const {
33
32
  attributes,
34
33
  listeners,
@@ -53,8 +52,215 @@ function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, bloc
53
52
  opacity: isDragging ? 0.5 : 1,
54
53
  };
55
54
 
56
- const blockDefinition = getBlockDefinition(block.block_type);
57
- const blockLabel = blockDefinition?.label || block.block_type;
55
+ const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
56
+
57
+ // Helper to check for dark background
58
+ const isDarkBackground = React.useMemo(() => {
59
+ if (!sectionBackground) return false;
60
+
61
+ // Theme checks
62
+ if (sectionBackground.type === 'theme') {
63
+ return ['primary', 'secondary', 'destructive', 'accent', 'dark'].includes(sectionBackground.theme || '');
64
+ }
65
+
66
+ // Image & Gradient - Assume Dark (safe default for overlays)
67
+ if (sectionBackground.type === 'image' || sectionBackground.type === 'gradient') {
68
+ return true;
69
+ }
70
+
71
+ // Solid Color checks
72
+ if (sectionBackground.type === 'solid' && sectionBackground.solid_color) {
73
+ const str = sectionBackground.solid_color.trim();
74
+ let r = 0, g = 0, b = 0;
75
+
76
+ if (str.startsWith('#')) {
77
+ const hex = str.replace('#', '');
78
+ if (hex.length === 3) {
79
+ r = parseInt(hex[0] + hex[0], 16);
80
+ g = parseInt(hex[1] + hex[1], 16);
81
+ b = parseInt(hex[2] + hex[2], 16);
82
+ } else if (hex.length === 6) {
83
+ r = parseInt(hex.substring(0, 2), 16);
84
+ g = parseInt(hex.substring(2, 4), 16);
85
+ b = parseInt(hex.substring(4, 6), 16);
86
+ } else {
87
+ return ['black', 'navy', 'darkblue', 'darkgray'].includes(str.toLowerCase());
88
+ }
89
+ } else if (str.startsWith('rgb')) {
90
+ const matches = str.match(/\d+/g);
91
+ if (matches && matches.length >= 3) {
92
+ r = parseInt(matches[0]);
93
+ g = parseInt(matches[1]);
94
+ b = parseInt(matches[2]);
95
+ } else {
96
+ return false;
97
+ }
98
+ } else {
99
+ return ['black', 'navy', 'darkblue', 'darkgray'].includes(str.toLowerCase());
100
+ }
101
+
102
+ // Calculate luminance (YIQ)
103
+ const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
104
+ return yiq < 128; // Strict threshold for white text
105
+ }
106
+ return false;
107
+ }, [sectionBackground]);
108
+
109
+ // Helper to render content preview
110
+ const renderContentPreview = () => {
111
+ switch (block.block_type) {
112
+ case 'text': {
113
+ // Basic detection of alignment from HTML content to mirror frontend
114
+ const isCentered = block.content.html_content?.includes('text-align: center') || block.content.html_content?.includes('class="text-center"');
115
+ const isRight = block.content.html_content?.includes('text-align: right') || block.content.html_content?.includes('class="text-right"');
116
+ const alignmentClass = isCentered ? 'text-center' : isRight ? 'text-right' : 'text-left';
117
+
118
+ return (
119
+ <div className="flex gap-3">
120
+ <div className={cn("text-xs w-full", alignmentClass)}>
121
+ {block.content.html_content ? (
122
+ <div
123
+ dangerouslySetInnerHTML={{ __html: block.content.html_content }}
124
+ className={cn(
125
+ "prose prose-xs max-w-none [&>p]:my-0 [&>h1]:my-0 [&>h2]:my-0 [&>h3]:my-0",
126
+ isDarkBackground ? "prose-invert text-white/90 drop-shadow-sm" : "dark:prose-invert",
127
+ // Ensure the inner prose content also respects the alignment if not explicitly set on children
128
+ isCentered && "[&_*]:text-center",
129
+ isRight && "[&_*]:text-right"
130
+ )}
131
+ />
132
+ ) : <span className={cn("text-muted-foreground", isDarkBackground && "text-white/50")}>Empty text block</span>}
133
+ </div>
134
+ </div>
135
+ );
136
+ }
137
+ case 'heading': {
138
+ const content = block.content as any;
139
+ const level = content.level || 1;
140
+ const headingAlign = content.textAlign || 'left';
141
+ const textColor = content.textColor || 'foreground';
142
+
143
+ const sizeClasses: Record<number, string> = {
144
+ 1: "text-4xl font-extrabold",
145
+ 2: "text-3xl font-bold",
146
+ 3: "text-2xl font-semibold",
147
+ 4: "text-xl font-semibold",
148
+ 5: "text-lg font-semibold",
149
+ 6: "text-base font-semibold",
150
+ };
151
+
152
+ const colorClasses: Record<string, string> = {
153
+ primary: "text-primary",
154
+ secondary: "text-secondary",
155
+ accent: "text-accent",
156
+ destructive: "text-destructive",
157
+ muted: "text-muted-foreground",
158
+ background: "text-background",
159
+ foreground: "text-foreground"
160
+ };
161
+
162
+ // Override for dark background if color is basic
163
+ let appliedColorClass = colorClasses[textColor] || "text-foreground";
164
+ if (isDarkBackground) {
165
+ if (textColor === 'foreground') appliedColorClass = 'text-white/90';
166
+ if (textColor === 'muted') appliedColorClass = 'text-white/70';
167
+ }
168
+
169
+ return (
170
+ <div className="flex gap-3">
171
+ <div className={cn(
172
+ "w-full leading-tight",
173
+ sizeClasses[level] || sizeClasses[1],
174
+ appliedColorClass,
175
+ headingAlign === 'center' && 'text-center',
176
+ headingAlign === 'right' && 'text-right'
177
+ )}>
178
+ {content.text_content || <span className={cn("text-muted-foreground font-normal text-sm italic", isDarkBackground && "text-white/50")}>Empty heading</span>}
179
+ </div>
180
+ </div>
181
+ );
182
+ }
183
+ case 'image': {
184
+ const imageUrl = block.content.object_key ? `${R2_BASE_URL}/${block.content.object_key}` : block.content.src;
185
+ return (
186
+ <div className="flex gap-3">
187
+ <div className="flex-shrink-0 h-10 w-10 bg-muted/20 rounded overflow-hidden flex items-center justify-center border border-white/10">
188
+ {imageUrl ? (
189
+ /* eslint-disable-next-line @next/next/no-img-element */
190
+ <img src={imageUrl} alt={block.content.alt_text || 'Block image'} className="h-full w-full object-cover" />
191
+ ) : (
192
+ <ImageIcon className={cn("h-5 w-5", isDarkBackground ? "text-white/50" : "text-muted-foreground")} />
193
+ )}
194
+ </div>
195
+ <div className="flex flex-col justify-center">
196
+ <span className="text-xs font-medium truncate max-w-[150px]">{block.content.alt_text || 'No description'}</span>
197
+ <span className={cn("text-[10px] truncate max-w-[150px]", isDarkBackground ? "text-white/50" : "text-muted-foreground")}>{imageUrl ? 'Image set' : 'No image selected'}</span>
198
+ </div>
199
+ </div>
200
+ );
201
+ }
202
+ case 'button': {
203
+ const content = block.content as any;
204
+ return (
205
+ <div className={cn("flex gap-3",
206
+ content.position === 'center' ? 'justify-center' :
207
+ content.position === 'right' ? 'justify-end' : 'justify-start'
208
+ )}>
209
+ <Button
210
+ variant={content.variant || 'default'}
211
+ size={content.size || 'default'}
212
+ className={cn("pointer-events-none", block.content.variant === 'outline' && "text-foreground")}
213
+ tabIndex={-1}
214
+ >
215
+ {content.text || 'Button'}
216
+ </Button>
217
+ </div>
218
+ );
219
+ }
220
+ case 'video_embed':
221
+ return (
222
+ <div className="flex gap-3 items-center">
223
+ <div className="text-xs truncate max-w-[200px]">
224
+ {block.content.title || block.content.url || 'No Video URL'}
225
+ </div>
226
+ </div>
227
+ );
228
+ case 'posts_grid':
229
+ return (
230
+ <div className="flex gap-3 items-center">
231
+ <div className={cn("text-xs", isDarkBackground ? "text-white/70" : "text-muted-foreground")}>
232
+ Posts Grid: {block.content.columns || 3} cols, {block.content.postsPerPage || 12} items
233
+ </div>
234
+ </div>
235
+ );
236
+ case 'testimonial':
237
+ return (
238
+ <div className="flex flex-col gap-2">
239
+ <div className={cn("italic text-xs line-clamp-2", isDarkBackground ? "text-white/80" : "text-muted-foreground")}>
240
+ "{block.content.quote || 'No quote'}"
241
+ </div>
242
+ <div className="flex items-center gap-2">
243
+ {block.content.image_url ? (
244
+ /* eslint-disable-next-line @next/next/no-img-element */
245
+ <img src={block.content.image_url} alt={block.content.author_name} className="w-5 h-5 rounded-full object-cover border border-white/10" />
246
+ ) : (
247
+ <div className={cn("w-5 h-5 rounded-full flex items-center justify-center text-[9px] font-bold border border-white/10", isDarkBackground ? "bg-white/20 text-white" : "bg-muted text-foreground")}>
248
+ {(block.content.author_name || 'A').charAt(0)}
249
+ </div>
250
+ )}
251
+ <div className="flex flex-col leading-none">
252
+ <span className={cn("text-[10px] font-semibold", isDarkBackground ? "text-white" : "text-foreground")}>{block.content.author_name || 'Author Name'}</span>
253
+ {block.content.author_title && <span className={cn("text-[9px]", isDarkBackground ? "text-white/60" : "text-muted-foreground")}>{block.content.author_title}</span>}
254
+ </div>
255
+ </div>
256
+ </div>
257
+ );
258
+ default:
259
+ // For fallback blocks, we might still want the label if we can't render a preview
260
+ // But user asked to remove it. Let's show a generic "configured" message or similar.
261
+ return <div className={cn("text-xs", isDarkBackground ? "text-white/50" : "text-muted-foreground")}>Unknown Block Type</div>;
262
+ }
263
+ };
58
264
 
59
265
  return (
60
266
  <div
@@ -64,45 +270,27 @@ function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, bloc
64
270
  {...attributes}
65
271
  {...listeners}
66
272
  className={cn(
67
- "group relative p-2 border border-gray-200 dark:border-gray-700 rounded bg-white dark:bg-gray-900 shadow-sm",
68
- "cursor-pointer hover:border-primary"
273
+ "group relative p-3 border border-transparent hover:border-dashed hover:border-primary/50 rounded-lg bg-transparent transition-all",
274
+ "cursor-pointer",
275
+ isDarkBackground ? "text-white border-white/20 hover:border-white/50" : "text-foreground border-transparent"
69
276
  )}
70
277
  >
71
- <div className="flex items-center justify-between mb-1">
72
- <div className="flex items-center gap-2">
73
- <GripVertical className="h-3 w-3 text-gray-400" />
74
- <span className="text-xs font-medium text-gray-600 dark:text-gray-300 capitalize">
75
- {blockLabel}
76
- </span>
77
- </div>
78
- <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
79
- <Button variant="ghost" size="sm" onClick={onEdit} className="h-6 w-6 p-0" title="Edit block">
80
- <Edit2 className="h-3 w-3" />
278
+ {/* Absolute positioning for actions */}
279
+ <div className="absolute top-2 right-2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-10 p-1 py-2 hover:bg-gray-500 rounded-lg">
280
+ <div className="cursor-grab active:cursor-grabbing p-1">
281
+ <GripVertical className={cn("h-3 w-3", isDarkBackground ? "text-white drop-shadow-md" : "text-foreground/70")} />
282
+ </div>
283
+ <Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); onEdit(); }} className="h-6 w-6 p-0 hover:bg-transparent" title="Edit block">
284
+ <Edit2 className={cn("h-3 w-3", isDarkBackground ? "text-white drop-shadow-md" : "text-foreground/70")} />
81
285
  </Button>
82
- <Button variant="ghost" size="sm" onClick={onDelete} className="h-6 w-6 p-0 text-red-600 hover:text-red-700" title="Delete block">
286
+ <Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); onDelete(); }} className="h-6 w-6 p-0 text-red-600 hover:text-red-700 hover:bg-transparent" title="Delete block">
83
287
  <Trash2 className="h-3 w-3" />
84
288
  </Button>
85
- </div>
86
289
  </div>
87
- <div className="text-xs text-gray-500 dark:text-gray-400">
88
- {block.block_type === 'text' && (
89
- <div dangerouslySetInnerHTML={{ __html: (block.content.html_content || 'Empty text').substring(0, 50) + (block.content.html_content && block.content.html_content.length > 50 ? '...' : '') }} />
90
- )}
91
- {block.block_type === 'heading' && (
92
- <div>H{block.content.level || 1}: {(block.content.text_content || 'Empty heading').substring(0, 30) + (block.content.text_content && block.content.text_content.length > 30 ? '...' : '')}</div>
93
- )}
94
- {block.block_type === 'image' && (
95
- <div>Image: {block.content.alt_text || block.content.media_id ? 'Image selected' : 'No image selected'}</div>
96
- )}
97
- {block.block_type === 'button' && (
98
- <div>Button: {block.content.text || 'No text'} → {block.content.url || '#'}</div>
99
- )}
100
- {block.block_type === 'video_embed' && (
101
- <div>Video: {block.content.title || block.content.url || 'No URL set'}</div>
102
- )}
103
- {block.block_type === 'posts_grid' && (
104
- <div>Posts Grid: {block.content.columns || 3} cols, {block.content.postsPerPage || 12} posts</div>
105
- )}
290
+
291
+ {/* Live Preview Area - No headers, just content */}
292
+ <div className={cn("min-h-[20px]", isDarkBackground ? "text-white drop-shadow-sm" : "text-foreground")}>
293
+ {renderContentPreview()}
106
294
  </div>
107
295
  </div>
108
296
  );
@@ -114,11 +302,12 @@ export interface ColumnEditorProps {
114
302
  blocks: ColumnBlock[];
115
303
  onBlocksChange: (newBlocks: ColumnBlock[]) => void;
116
304
  blockType: 'section' | 'hero';
305
+ sectionBackground?: SectionBlockContent['background'];
117
306
  }
118
307
 
119
308
  type EditingBlock = ColumnBlock & { index: number };
120
309
 
121
- export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, blockType }: ColumnEditorProps) {
310
+ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, blockType, sectionBackground }: ColumnEditorProps) {
122
311
  const [editingBlock, setEditingBlock] = useState<EditingBlock | null>(null);
123
312
  const [isBlockSelectorOpen, setIsBlockSelectorOpen] = useState(false);
124
313
  const [LazyEditor, setLazyEditor] = useState<React.LazyExoticComponent<React.ComponentType<any>> | React.ComponentType<any> | null>(null);
@@ -129,6 +318,57 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
129
318
  id: `${blockType}-column-droppable-${columnIndex}`,
130
319
  });
131
320
 
321
+ // Duplicate isDark logic (should ideally be shared utils but inline to avoid import cycles)
322
+ const isDarkBackground = React.useMemo(() => {
323
+ if (!sectionBackground) return false;
324
+
325
+ // Theme checks
326
+ if (sectionBackground.type === 'theme') {
327
+ return ['primary', 'secondary', 'destructive', 'accent', 'dark'].includes(sectionBackground.theme || '');
328
+ }
329
+
330
+ // Image & Gradient - Assume Dark (safe default for overlays)
331
+ if (sectionBackground.type === 'image' || sectionBackground.type === 'gradient') {
332
+ return true;
333
+ }
334
+
335
+ // Solid Color checks
336
+ if (sectionBackground.type === 'solid' && sectionBackground.solid_color) {
337
+ const str = sectionBackground.solid_color.trim();
338
+ let r = 0, g = 0, b = 0;
339
+
340
+ if (str.startsWith('#')) {
341
+ const hex = str.replace('#', '');
342
+ if (hex.length === 3) {
343
+ r = parseInt(hex[0] + hex[0], 16);
344
+ g = parseInt(hex[1] + hex[1], 16);
345
+ b = parseInt(hex[2] + hex[2], 16);
346
+ } else if (hex.length === 6) {
347
+ r = parseInt(hex.substring(0, 2), 16);
348
+ g = parseInt(hex.substring(2, 4), 16);
349
+ b = parseInt(hex.substring(4, 6), 16);
350
+ } else {
351
+ return ['black', 'navy', 'darkblue', 'darkgray'].includes(str.toLowerCase());
352
+ }
353
+ } else if (str.startsWith('rgb')) {
354
+ const matches = str.match(/\d+/g);
355
+ if (matches && matches.length >= 3) {
356
+ r = parseInt(matches[0]);
357
+ g = parseInt(matches[1]);
358
+ b = parseInt(matches[2]);
359
+ } else {
360
+ return false;
361
+ }
362
+ } else {
363
+ return ['black', 'navy', 'darkblue', 'darkgray'].includes(str.toLowerCase());
364
+ }
365
+
366
+ const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
367
+ return yiq < 128;
368
+ }
369
+ return false;
370
+ }, [sectionBackground]);
371
+
132
372
  const handleAddBlock = (selectedBlockType: BlockType) => {
133
373
  if (!selectedBlockType) return;
134
374
  const initialContent = getInitialContent(selectedBlockType);
@@ -146,11 +386,7 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
146
386
  };
147
387
 
148
388
  const handleCardClick = (e: React.MouseEvent<HTMLDivElement>, block: ColumnBlock, index: number) => {
149
- // Ignore clicks on buttons to prevent conflicts with drag/delete/edit icons.
150
- if ((e.target as HTMLElement).closest('button')) {
151
- return;
152
- }
153
- // Call the existing function to open the modal for this block.
389
+ if ((e.target as HTMLElement).closest('button')) return;
154
390
  handleStartEdit(block, index);
155
391
  };
156
392
 
@@ -202,20 +438,37 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
202
438
  };
203
439
 
204
440
  return (
205
- <div className="border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800/50 flex flex-col">
206
- <div className="p-3 border-b border-gray-200 dark:border-gray-700">
441
+ <div className={cn(
442
+ "border border-dashed rounded-lg bg-transparent flex flex-col transition-colors",
443
+ isDarkBackground ? "border-white/30" : "border-gray-300 dark:border-gray-700/50"
444
+ )}>
445
+ <div className={cn(
446
+ "p-3 border-b",
447
+ isDarkBackground ? "border-white/20" : "border-gray-200 dark:border-gray-700"
448
+ )}>
207
449
  <div className="flex items-center justify-between">
208
450
  <div className="flex items-center gap-2">
209
- <h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
451
+ <h4 className={cn(
452
+ "text-sm font-medium",
453
+ isDarkBackground ? "text-white" : "text-gray-700 dark:text-gray-300"
454
+ )}>
210
455
  Column {columnIndex + 1}
211
456
  </h4>
212
- <span className="text-xs text-gray-500 bg-gray-200 dark:bg-gray-700 px-2 py-1 rounded">
213
- {blocks.length} block{blocks.length !== 1 ? 's' : ''}
457
+ <span className={cn(
458
+ "text-xs px-2 py-1 rounded",
459
+ isDarkBackground ? "bg-white/20 text-white" : "text-gray-500 bg-gray-200 dark:bg-gray-700"
460
+ )}>
461
+ {blocks.length}
214
462
  </span>
215
463
  </div>
216
464
  </div>
217
465
  <div className="mt-2">
218
- <Button onClick={() => setIsBlockSelectorOpen(true)} size="sm" className="w-full h-8">
466
+ <Button
467
+ onClick={() => setIsBlockSelectorOpen(true)}
468
+ size="sm"
469
+ variant={isDarkBackground ? "secondary" : "default"}
470
+ className="w-full h-8"
471
+ >
219
472
  <PlusCircle className="h-3 w-3 mr-2" />
220
473
  Add Block
221
474
  </Button>
@@ -225,8 +478,8 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
225
478
  {blocks.length === 0 ? (
226
479
  <div
227
480
  ref={setDroppableNodeRef}
228
- className={`h-full flex items-center justify-center text-xs text-gray-500 border-2 border-dashed rounded-lg transition-colors ${
229
- isOver ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' : 'border-gray-300 dark:border-gray-600'
481
+ className={`h-full flex items-center justify-center text-xs text-muted-foreground border-2 border-dashed rounded-lg transition-colors p-4 ${
482
+ isOver ? 'border-primary bg-primary/5' : 'border-gray-300 dark:border-gray-600'
230
483
  }`}
231
484
  >
232
485
  Drag block here
@@ -243,6 +496,7 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
243
496
  onEdit={() => handleStartEdit(block, index)}
244
497
  onDelete={() => handleDeleteBlock(index)}
245
498
  onClick={(e) => handleCardClick(e, block, index)}
499
+ sectionBackground={sectionBackground}
246
500
  />
247
501
  </div>
248
502
  ))}
@@ -262,6 +516,8 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
262
516
  content: editingBlock.content,
263
517
  }}
264
518
  EditorComponent={LazyEditor}
519
+ // Pass the section background to the modal
520
+ sectionBackground={sectionBackground}
265
521
  />
266
522
  )}
267
523
  <ConfirmationDialog