create-nextblock 0.2.45 → 0.2.47

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 (35) hide show
  1. package/package.json +1 -1
  2. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +45 -27
  3. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +1 -1
  4. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +13 -3
  5. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +11 -4
  6. package/templates/nextblock-template/app/cms/dashboard/actions.ts +98 -0
  7. package/templates/nextblock-template/app/cms/dashboard/page.tsx +76 -153
  8. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +16 -11
  9. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +23 -12
  10. package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +4 -0
  11. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +30 -6
  12. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +17 -11
  13. package/templates/nextblock-template/app/cms/pages/page.tsx +6 -3
  14. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +18 -12
  15. package/templates/nextblock-template/app/cms/posts/page.tsx +8 -5
  16. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +18 -5
  17. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +20 -4
  18. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +33 -7
  19. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +3 -3
  20. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +41 -13
  21. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +15 -13
  22. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +2 -3
  23. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +50 -0
  24. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +14 -2
  25. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +3 -6
  26. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +33 -13
  27. package/templates/nextblock-template/components/BlockRenderer.tsx +14 -1
  28. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +126 -0
  29. package/templates/nextblock-template/docs/How to Create a Custom Block.md +149 -0
  30. package/templates/nextblock-template/hooks/use-hotkeys.ts +27 -0
  31. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +196 -603
  32. package/templates/nextblock-template/next-env.d.ts +1 -1
  33. package/templates/nextblock-template/package.json +1 -1
  34. package/templates/nextblock-template/tsconfig.json +3 -0
  35. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -0
@@ -1,231 +1,151 @@
1
+ import { z } from 'zod';
2
+ import { TestimonialBlockConfig, TestimonialBlockContent } from '../../components/blocks/TestimonialBlock';
3
+
1
4
  /**
2
5
  * Block Registry System
3
6
  *
4
7
  * This module provides the central registry for all block types in the CMS.
5
8
  * It serves as the single source of truth for block definitions, including
6
- * their initial content, editor components, renderer components, and TypeScript
7
- * interface definitions. This eliminates the need to modify utils/supabase/types.ts
8
- * when adding new block types.
9
- */
10
-
11
- /**
12
- * Content interface definitions for all block types
13
- * These provide proper TypeScript support with IDE IntelliSense and compile-time checking
14
- */
15
-
16
- /**
17
- * Content interface for text blocks
18
- * Supports rich HTML content with WYSIWYG editing
19
- */
20
- export interface TextBlockContent {
21
- /** Raw HTML content for the text block */
22
- html_content: string;
23
- }
24
-
25
- /**
26
- * Content interface for heading blocks
27
- * Provides semantic heading structure with configurable hierarchy levels
28
- */
29
- export interface HeadingBlockContent {
30
- /** Heading level (1-6, corresponding to h1-h6 tags) */
31
- level: 1 | 2 | 3 | 4 | 5 | 6;
32
- /** The text content of the heading */
33
- text_content: string;
34
- /** Text alignment of the heading */
35
- textAlign?: 'left' | 'center' | 'right' | 'justify';
36
- /** Color of the heading text, based on theme colors */
37
- textColor?: 'primary' | 'secondary' | 'accent' | 'muted' | 'destructive' | 'background';
38
- }
39
-
40
- /**
41
- * Content interface for image blocks
42
- * Supports images with captions, alt text, and responsive sizing
43
- */
44
- export interface ImageBlockContent {
45
- /** UUID of the media item from the 'media' table */
46
- media_id: string | null;
47
- /** The actual R2 object key (e.g., "uploads/image.png") */
48
- object_key?: string | null;
49
- /** Alternative text for accessibility */
50
- alt_text?: string;
51
- /** Optional caption displayed below the image */
52
- caption?: string;
53
- /** Image width in pixels */
54
- width?: number | null;
55
- /** Image height in pixels */
56
- height?: number | null;
57
- }
58
-
59
- /**
60
- * Content interface for button blocks
61
- * Customizable button/link component with multiple style variants
62
- */
63
- export interface ButtonBlockContent {
64
- /** The text displayed on the button */
65
- text: string;
66
- /** The URL the button links to */
67
- url: string;
68
- /** Visual style variant of the button */
69
- variant?: 'default' | 'outline' | 'secondary' | 'ghost' | 'link';
70
- /** Size of the button */
71
- size?: 'default' | 'sm' | 'lg';
72
- }
73
-
74
- /**
75
- * Content interface for posts grid blocks
76
- * Responsive grid layout for displaying blog posts with pagination
9
+ * their initial content, editor components, renderer components, and Zod schemas.
77
10
  */
78
- export interface PostsGridBlockContent {
79
- /** Number of posts to display per page */
80
- postsPerPage: number;
81
- /** Number of columns in the grid layout */
82
- columns: number;
83
- /** Whether to show pagination controls */
84
- showPagination: boolean;
85
- /** Optional title displayed above the posts grid */
86
- title?: string;
87
- }
88
-
89
- export interface VideoEmbedBlockContent {
90
- /** The video URL (YouTube, Vimeo, etc.) */
91
- url: string;
92
- /** Optional title for the video */
93
- title?: string;
94
- /** Whether the video should autoplay */
95
- autoplay?: boolean;
96
- /** Whether to show video controls */
97
- controls?: boolean;
98
- }
99
-
100
- /**
101
- * Content interface for section blocks
102
- * Provides flexible column layouts with responsive breakpoints and background options
103
- */
104
- export interface Gradient {
105
- type: 'linear' | 'radial';
106
- direction?: string;
107
- stops: Array<{ color: string; position: number }>;
108
- }
109
-
110
- export interface SectionBlockContent {
111
- /** Container width type */
112
- container_type: 'full-width' | 'container' | 'container-sm' | 'container-lg' | 'container-xl';
113
- /** Background configuration */
114
- background: {
115
- type: 'none' | 'theme' | 'solid' | 'gradient' | 'image';
116
- theme?: 'primary' | 'secondary' | 'muted' | 'accent' | 'destructive';
117
- solid_color?: string;
118
- min_height?: string;
119
- gradient?: Gradient;
120
- image?: {
121
- media_id: string;
122
- object_key: string;
123
- alt_text?: string;
124
- width?: number;
125
- height?: number;
126
- blur_data_url?: string;
127
- size: 'cover' | 'contain';
128
- position: 'center' | 'top' | 'bottom' | 'left' | 'right';
129
- quality?: number | null;
130
- overlay?: {
131
- type: 'gradient';
132
- gradient: Gradient;
133
- };
134
- };
135
- };
136
- /** Responsive column configuration */
137
- responsive_columns: {
138
- mobile: 1 | 2;
139
- tablet: 1 | 2 | 3;
140
- desktop: 1 | 2 | 3 | 4;
141
- };
142
- /** Gap between columns */
143
- column_gap: 'none' | 'sm' | 'md' | 'lg' | 'xl';
144
- /** Section padding */
145
- padding: {
146
- top: 'none' | 'sm' | 'md' | 'lg' | 'xl';
147
- bottom: 'none' | 'sm' | 'md' | 'lg' | 'xl';
148
- };
149
- /** Vertical alignment of columns */
150
- vertical_alignment?: 'start' | 'center' | 'end' | 'stretch';
151
- /** Array of blocks within columns - 2D array where each index represents a column */
152
- column_blocks: Array<Array<{
153
- block_type: BlockType;
154
- content: Record<string, any>;
155
- temp_id?: string; // For client-side management before save
156
- }>>;
157
- }
158
-
159
- /**
160
- * Content interface for hero blocks
161
- * A specialized version of the section block for page headers
162
- */
163
- export type HeroBlockContent = SectionBlockContent;
164
-
165
- /**
166
- * Represents a single option for select, radio, or checkbox group fields.
167
- */
168
- export interface FormFieldOption {
169
- label: string;
170
- value: string;
171
- }
172
-
173
- /**
174
- * Represents a single field within the form block.
175
- */
176
- export interface FormField {
177
- temp_id: string; // For client-side keying and reordering
178
- field_type: 'text' | 'email' | 'textarea' | 'select' | 'radio' | 'checkbox';
179
- label: string;
180
- placeholder?: string;
181
- is_required: boolean;
182
- options?: FormFieldOption[];
183
- }
184
-
185
- /**
186
- * Content interface for the main form block.
187
- */
188
- export interface FormBlockContent {
189
- /** The email address where form submissions will be sent. */
190
- recipient_email: string;
191
- /** The text to display on the submit button. */
192
- submit_button_text: string;
193
- /** The message to show after a successful submission. */
194
- success_message: string;
195
- /** An array of form field configurations. */
196
- fields: FormField[];
197
- }
198
11
 
199
12
  /**
200
13
  * Available block types - defined here as the source of truth
201
14
  */
202
- export const availableBlockTypes = ["text", "heading", "image", "button", "posts_grid", "video_embed", "section", "hero", "form"] as const;
15
+ export const availableBlockTypes = ["text", "heading", "image", "button", "posts_grid", "video_embed", "section", "hero", "form", "testimonial"] as const;
203
16
  export type BlockType = (typeof availableBlockTypes)[number];
204
17
 
205
- /**
206
- * Property definition for content schema
207
- */
208
- export interface ContentPropertyDefinition {
209
- /** The TypeScript type of the property */
210
- type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'union';
211
- /** Whether this property is required */
212
- required?: boolean;
213
- /** Human-readable description of the property */
214
- description?: string;
215
- /** Default value for the property */
216
- default?: any;
217
- /** For union types, the possible values */
218
- unionValues?: readonly string[];
219
- /** For array types, the type of array elements */
220
- arrayElementType?: string;
221
- /** Additional constraints or validation info */
222
- constraints?: {
223
- min?: number;
224
- max?: number;
225
- pattern?: string;
226
- enum?: readonly any[];
227
- };
228
- }
18
+ // --- Zod Schemas & Inferred Types ---
19
+
20
+ export const TextBlockSchema = z.object({
21
+ html_content: z.string().describe('Raw HTML content for the text block'),
22
+ });
23
+ export type TextBlockContent = z.infer<typeof TextBlockSchema>;
24
+
25
+ export const HeadingBlockSchema = z.object({
26
+ level: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5), z.literal(6)]).describe('Heading level (1-6)'),
27
+ text_content: z.string().describe('The text content of the heading'),
28
+ textAlign: z.enum(['left', 'center', 'right', 'justify']).optional().describe('Text alignment'),
29
+ textColor: z.enum(['primary', 'secondary', 'accent', 'muted', 'destructive', 'background']).optional().describe('Color of the heading text'),
30
+ });
31
+ export type HeadingBlockContent = z.infer<typeof HeadingBlockSchema>;
32
+
33
+ export const ImageBlockSchema = z.object({
34
+ media_id: z.string().nullable().describe('UUID of the media item'),
35
+ object_key: z.string().nullable().optional().describe('The actual R2 object key'),
36
+ alt_text: z.string().optional().describe('Alternative text'),
37
+ caption: z.string().optional().describe('Optional caption'),
38
+ width: z.number().nullable().optional().describe('Image width'),
39
+ height: z.number().nullable().optional().describe('Image height'),
40
+ });
41
+ export type ImageBlockContent = z.infer<typeof ImageBlockSchema>;
42
+
43
+ export const ButtonBlockSchema = z.object({
44
+ text: z.string().describe('The text displayed on the button'),
45
+ url: z.string().describe('The URL the button links to'),
46
+ variant: z.enum(['default', 'outline', 'secondary', 'ghost', 'link']).optional().describe('Visual style variant'),
47
+ size: z.enum(['default', 'sm', 'lg']).optional().describe('Size of the button'),
48
+ });
49
+ export type ButtonBlockContent = z.infer<typeof ButtonBlockSchema>;
50
+
51
+ export const PostsGridBlockSchema = z.object({
52
+ postsPerPage: z.number().min(1).max(50).describe('Number of posts per page'),
53
+ columns: z.number().min(1).max(6).describe('Number of columns'),
54
+ showPagination: z.boolean().describe('Whether to show pagination'),
55
+ title: z.string().optional().describe('Optional title'),
56
+ });
57
+ export type PostsGridBlockContent = z.infer<typeof PostsGridBlockSchema>;
58
+
59
+ export const VideoEmbedBlockSchema = z.object({
60
+ url: z.string().describe('The video URL'),
61
+ title: z.string().optional().describe('Optional title'),
62
+ autoplay: z.boolean().optional().describe('Autoplay'),
63
+ controls: z.boolean().optional().describe('Show controls'),
64
+ });
65
+ export type VideoEmbedBlockContent = z.infer<typeof VideoEmbedBlockSchema>;
66
+
67
+ // Section helpers
68
+ const GradientSchema = z.object({
69
+ type: z.enum(['linear', 'radial']),
70
+ direction: z.string().optional(),
71
+ stops: z.array(z.object({ color: z.string(), position: z.number() })),
72
+ });
73
+ export type Gradient = z.infer<typeof GradientSchema>;
74
+
75
+ const BackgroundSchema = z.object({
76
+ type: z.enum(['none', 'theme', 'solid', 'gradient', 'image']),
77
+ theme: z.enum(['primary', 'secondary', 'muted', 'accent', 'destructive']).optional(),
78
+ solid_color: z.string().optional(),
79
+ min_height: z.string().optional(),
80
+ gradient: GradientSchema.optional(),
81
+ image: z.object({
82
+ media_id: z.string(),
83
+ object_key: z.string(),
84
+ alt_text: z.string().optional(),
85
+ width: z.number().optional(),
86
+ height: z.number().optional(),
87
+ blur_data_url: z.string().optional(),
88
+ size: z.enum(['cover', 'contain']),
89
+ position: z.enum(['center', 'top', 'bottom', 'left', 'right']),
90
+ quality: z.number().nullable().optional(),
91
+ overlay: z.object({
92
+ type: z.literal('gradient'),
93
+ gradient: GradientSchema,
94
+ }).optional(),
95
+ }).optional(),
96
+ });
97
+
98
+ const BlockInColumnSchema = z.object({
99
+ block_type: z.enum(availableBlockTypes),
100
+ content: z.record(z.any()),
101
+ temp_id: z.string().optional(),
102
+ });
103
+
104
+ export const SectionBlockSchema = z.object({
105
+ container_type: z.enum(['full-width', 'container', 'container-sm', 'container-lg', 'container-xl']),
106
+ background: BackgroundSchema,
107
+ responsive_columns: z.object({
108
+ mobile: z.union([z.literal(1), z.literal(2)]),
109
+ tablet: z.union([z.literal(1), z.literal(2), z.literal(3)]),
110
+ desktop: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]),
111
+ }),
112
+ column_gap: z.enum(['none', 'sm', 'md', 'lg', 'xl']),
113
+ padding: z.object({
114
+ top: z.enum(['none', 'sm', 'md', 'lg', 'xl']),
115
+ bottom: z.enum(['none', 'sm', 'md', 'lg', 'xl']),
116
+ }),
117
+ vertical_alignment: z.enum(['start', 'center', 'end', 'stretch']).optional(),
118
+ column_blocks: z.array(z.array(BlockInColumnSchema)),
119
+ });
120
+ export type SectionBlockContent = z.infer<typeof SectionBlockSchema>;
121
+
122
+ export const HeroBlockSchema = SectionBlockSchema;
123
+ export type HeroBlockContent = z.infer<typeof HeroBlockSchema>;
124
+
125
+ // Form helpers
126
+ export const FormFieldOptionSchema = z.object({
127
+ label: z.string(),
128
+ value: z.string(),
129
+ });
130
+ export type FormFieldOption = z.infer<typeof FormFieldOptionSchema>;
131
+
132
+ export const FormFieldSchema = z.object({
133
+ temp_id: z.string(),
134
+ field_type: z.enum(['text', 'email', 'textarea', 'select', 'radio', 'checkbox']),
135
+ label: z.string(),
136
+ placeholder: z.string().optional(),
137
+ is_required: z.boolean(),
138
+ options: z.array(FormFieldOptionSchema).optional(),
139
+ });
140
+ export type FormField = z.infer<typeof FormFieldSchema>;
141
+
142
+ export const FormBlockSchema = z.object({
143
+ recipient_email: z.string().email(),
144
+ submit_button_text: z.string(),
145
+ success_message: z.string(),
146
+ fields: z.array(FormFieldSchema),
147
+ });
148
+ export type FormBlockContent = z.infer<typeof FormBlockSchema>;
229
149
 
230
150
  /**
231
151
  * Enhanced block definition interface with generic type parameter
@@ -236,21 +156,25 @@ export interface BlockDefinition<T = any> {
236
156
  type: BlockType;
237
157
  /** User-friendly display name for the block */
238
158
  label: string;
239
- /** Optional icon for the block, using lucide-react icon names */
240
- icon?: string;
159
+ /** Optional icon for the block, using lucide-react icon names or component */
160
+ icon?: string | any;
241
161
  /** Default content structure for new blocks of this type */
242
162
  initialContent: T;
243
163
  /** Filename of the editor component (assumed to be in app/cms/blocks/editors/) */
244
- editorComponentFilename: string;
164
+ editorComponentFilename?: string;
245
165
  /** Filename of the renderer component (assumed to be in components/blocks/renderers/) */
246
- rendererComponentFilename: string;
166
+ rendererComponentFilename?: string;
167
+ /** Direct React component for rendering (overrides filename if present) */
168
+ RendererComponent?: React.ComponentType<any>;
169
+ /** Direct React component for editing (overrides filename if present) */
170
+ EditorComponent?: React.ComponentType<any>;
247
171
  /** Optional filename for specific preview components */
248
172
  previewComponentFilename?: string;
249
173
  /**
250
- * Structured schema defining the content properties, types, and constraints.
174
+ * Zod schema defining the content properties, types, and constraints.
251
175
  * Used for validation, documentation, and potential runtime type checking.
252
176
  */
253
- contentSchema: Record<string, ContentPropertyDefinition>;
177
+ schema: z.ZodType<T>;
254
178
  /**
255
179
  * JSDoc-style comments providing additional context about the block type,
256
180
  * its use cases, and any special considerations.
@@ -279,14 +203,7 @@ export const blockRegistry: Record<BlockType, BlockDefinition> = {
279
203
  initialContent: { html_content: "" } as TextBlockContent,
280
204
  editorComponentFilename: "TextBlockEditor.tsx",
281
205
  rendererComponentFilename: "TextBlockRenderer.tsx",
282
- contentSchema: {
283
- html_content: {
284
- type: 'string',
285
- required: true,
286
- description: 'Rich text content for the text block',
287
- default: '',
288
- },
289
- },
206
+ schema: TextBlockSchema,
290
207
  documentation: {
291
208
  description: 'A rich text block that supports HTML content with WYSIWYG editing',
292
209
  examples: [
@@ -313,46 +230,7 @@ export const blockRegistry: Record<BlockType, BlockDefinition> = {
313
230
  initialContent: { level: 1, text_content: "New Heading", textAlign: 'left', textColor: undefined } as HeadingBlockContent,
314
231
  editorComponentFilename: "HeadingBlockEditor.tsx",
315
232
  rendererComponentFilename: "HeadingBlockRenderer.tsx",
316
- contentSchema: {
317
- level: {
318
- type: 'union',
319
- required: true,
320
- description: 'Heading level (1-6, corresponding to h1-h6 tags)',
321
- default: 1,
322
- unionValues: ['1', '2', '3', '4', '5', '6'] as const,
323
- constraints: {
324
- min: 1,
325
- max: 6,
326
- enum: [1, 2, 3, 4, 5, 6] as const,
327
- },
328
- },
329
- text_content: {
330
- type: 'string',
331
- required: true,
332
- description: 'The text content of the heading',
333
- default: 'New Heading',
334
- },
335
- textAlign: {
336
- type: 'union',
337
- required: false,
338
- description: 'Text alignment of the heading',
339
- default: 'left',
340
- unionValues: ['left', 'center', 'right', 'justify'] as const,
341
- constraints: {
342
- enum: ['left', 'center', 'right', 'justify'] as const,
343
- },
344
- },
345
- textColor: {
346
- type: 'union',
347
- required: false,
348
- description: 'Color of the heading text, based on theme colors',
349
- default: undefined, // Or a specific default like 'primary' if desired
350
- unionValues: ['primary', 'secondary', 'accent', 'muted', 'destructive'] as const,
351
- constraints: {
352
- enum: ['primary', 'secondary', 'accent', 'muted', 'destructive'] as const,
353
- },
354
- },
355
- },
233
+ schema: HeadingBlockSchema,
356
234
  documentation: {
357
235
  description: 'A semantic heading block with configurable hierarchy levels',
358
236
  examples: [
@@ -379,44 +257,7 @@ export const blockRegistry: Record<BlockType, BlockDefinition> = {
379
257
  initialContent: { media_id: null, alt_text: "", caption: "" } as ImageBlockContent,
380
258
  editorComponentFilename: "ImageBlockEditor.tsx",
381
259
  rendererComponentFilename: "ImageBlockRenderer.tsx",
382
- contentSchema: {
383
- media_id: {
384
- type: 'string',
385
- required: false,
386
- description: 'UUID of the media item from the media table',
387
- default: null,
388
- },
389
- object_key: {
390
- type: 'string',
391
- required: false,
392
- description: 'The actual R2 object key (e.g., "uploads/image.png")',
393
- default: null,
394
- },
395
- alt_text: {
396
- type: 'string',
397
- required: false,
398
- description: 'Alternative text for accessibility',
399
- default: '',
400
- },
401
- caption: {
402
- type: 'string',
403
- required: false,
404
- description: 'Optional caption displayed below the image',
405
- default: '',
406
- },
407
- width: {
408
- type: 'number',
409
- required: false,
410
- description: 'Image width in pixels',
411
- default: null,
412
- },
413
- height: {
414
- type: 'number',
415
- required: false,
416
- description: 'Image height in pixels',
417
- default: null,
418
- },
419
- },
260
+ schema: ImageBlockSchema,
420
261
  documentation: {
421
262
  description: 'An image block with support for captions, alt text, and responsive sizing',
422
263
  examples: [
@@ -443,40 +284,7 @@ export const blockRegistry: Record<BlockType, BlockDefinition> = {
443
284
  initialContent: { text: "Click Me", url: "#", variant: "default", size: "default" } as ButtonBlockContent,
444
285
  editorComponentFilename: "ButtonBlockEditor.tsx",
445
286
  rendererComponentFilename: "ButtonBlockRenderer.tsx",
446
- contentSchema: {
447
- text: {
448
- type: 'string',
449
- required: true,
450
- description: 'The text displayed on the button',
451
- default: 'Click Me',
452
- },
453
- url: {
454
- type: 'string',
455
- required: true,
456
- description: 'The URL the button links to',
457
- default: '#',
458
- },
459
- variant: {
460
- type: 'union',
461
- required: false,
462
- description: 'Visual style variant of the button',
463
- default: 'default',
464
- unionValues: ['default', 'outline', 'secondary', 'ghost', 'link'] as const,
465
- constraints: {
466
- enum: ['default', 'outline', 'secondary', 'ghost', 'link'] as const,
467
- },
468
- },
469
- size: {
470
- type: 'union',
471
- required: false,
472
- description: 'Size of the button',
473
- default: 'default',
474
- unionValues: ['default', 'sm', 'lg'] as const,
475
- constraints: {
476
- enum: ['default', 'sm', 'lg'] as const,
477
- },
478
- },
479
- },
287
+ schema: ButtonBlockSchema,
480
288
  documentation: {
481
289
  description: 'A customizable button/link component with multiple style variants',
482
290
  examples: [
@@ -504,40 +312,7 @@ export const blockRegistry: Record<BlockType, BlockDefinition> = {
504
312
  initialContent: { postsPerPage: 12, columns: 3, showPagination: true, title: "Recent Posts" } as PostsGridBlockContent,
505
313
  editorComponentFilename: "PostsGridBlockEditor.tsx",
506
314
  rendererComponentFilename: "PostsGridBlockRenderer.tsx",
507
- contentSchema: {
508
- postsPerPage: {
509
- type: 'number',
510
- required: true,
511
- description: 'Number of posts to display per page',
512
- default: 12,
513
- constraints: {
514
- min: 1,
515
- max: 50,
516
- },
517
- },
518
- columns: {
519
- type: 'number',
520
- required: true,
521
- description: 'Number of columns in the grid layout',
522
- default: 3,
523
- constraints: {
524
- min: 1,
525
- max: 6,
526
- },
527
- },
528
- showPagination: {
529
- type: 'boolean',
530
- required: true,
531
- description: 'Whether to show pagination controls',
532
- default: true,
533
- },
534
- title: {
535
- type: 'string',
536
- required: false,
537
- description: 'Optional title displayed above the posts grid',
538
- default: 'Recent Posts',
539
- },
540
- },
315
+ schema: PostsGridBlockSchema,
541
316
  documentation: {
542
317
  description: 'A responsive grid layout for displaying blog posts with pagination',
543
318
  examples: [
@@ -569,32 +344,7 @@ export const blockRegistry: Record<BlockType, BlockDefinition> = {
569
344
  } as VideoEmbedBlockContent,
570
345
  editorComponentFilename: "VideoEmbedBlockEditor.tsx",
571
346
  rendererComponentFilename: "VideoEmbedBlockRenderer.tsx",
572
- contentSchema: {
573
- url: {
574
- type: 'string',
575
- required: true,
576
- description: 'The video URL (YouTube, Vimeo, etc.)',
577
- default: '',
578
- },
579
- title: {
580
- type: 'string',
581
- required: false,
582
- description: 'Optional title for the video',
583
- default: '',
584
- },
585
- autoplay: {
586
- type: 'boolean',
587
- required: false,
588
- description: 'Whether the video should autoplay',
589
- default: false,
590
- },
591
- controls: {
592
- type: 'boolean',
593
- required: false,
594
- description: 'Whether to show video controls',
595
- default: true,
596
- },
597
- },
347
+ schema: VideoEmbedBlockSchema,
598
348
  documentation: {
599
349
  description: 'Embeds videos from popular platforms with customizable playback options',
600
350
  examples: [
@@ -633,63 +383,7 @@ export const blockRegistry: Record<BlockType, BlockDefinition> = {
633
383
  } as SectionBlockContent,
634
384
  editorComponentFilename: "SectionBlockEditor.tsx",
635
385
  rendererComponentFilename: "SectionBlockRenderer.tsx",
636
- contentSchema: {
637
- container_type: {
638
- type: 'union',
639
- required: true,
640
- description: 'Container width type',
641
- default: 'container',
642
- unionValues: ['full-width', 'container', 'container-sm', 'container-lg', 'container-xl'] as const,
643
- constraints: {
644
- enum: ['full-width', 'container', 'container-sm', 'container-lg', 'container-xl'] as const,
645
- },
646
- },
647
- background: {
648
- type: 'object',
649
- required: true,
650
- description: 'Background configuration',
651
- default: { type: 'none' },
652
- },
653
- responsive_columns: {
654
- type: 'object',
655
- required: true,
656
- description: 'Responsive column configuration',
657
- default: { mobile: 1, tablet: 2, desktop: 3 },
658
- },
659
- column_gap: {
660
- type: 'union',
661
- required: true,
662
- description: 'Gap between columns',
663
- default: 'md',
664
- unionValues: ['none', 'sm', 'md', 'lg', 'xl'] as const,
665
- constraints: {
666
- enum: ['none', 'sm', 'md', 'lg', 'xl'] as const,
667
- },
668
- },
669
- padding: {
670
- type: 'object',
671
- required: true,
672
- description: 'Section padding configuration',
673
- default: { top: 'md', bottom: 'md' },
674
- },
675
- vertical_alignment: {
676
- type: 'union',
677
- required: false,
678
- description: 'Vertical alignment of columns',
679
- default: 'start',
680
- unionValues: ['start', 'center', 'end', 'stretch'] as const,
681
- constraints: {
682
- enum: ['start', 'center', 'end', 'stretch'] as const,
683
- },
684
- },
685
- column_blocks: {
686
- type: 'array',
687
- required: true,
688
- description: 'Array of blocks within columns',
689
- default: [],
690
- arrayElementType: 'object',
691
- },
692
- },
386
+ schema: SectionBlockSchema,
693
387
  documentation: {
694
388
  description: 'A flexible section layout with responsive columns and background options',
695
389
  examples: [
@@ -711,7 +405,8 @@ export const blockRegistry: Record<BlockType, BlockDefinition> = {
711
405
  ],
712
406
  },
713
407
  },
714
- hero: {
408
+
409
+ hero: {
715
410
  type: "hero",
716
411
  label: "Hero Section",
717
412
  icon: "LayoutTemplate",
@@ -732,16 +427,14 @@ hero: {
732
427
  } as HeroBlockContent,
733
428
  editorComponentFilename: "SectionBlockEditor.tsx", // Reusing section editor
734
429
  rendererComponentFilename: "HeroBlockRenderer.tsx", // Specific renderer for hero
735
- contentSchema: {
736
- // The content schema is inherited from SectionBlockContent, so we don't need to redefine it here.
737
- // We could add specific validation for the hero block if needed in the future.
738
- },
430
+ schema: HeroBlockSchema,
739
431
  documentation: {
740
432
  description: 'A specialized hero section for the top of a page, with prioritized images and pre-populated content.',
741
433
  useCases: ['Main page hero/banner', 'Introductory section with a strong call to action'],
742
434
  notes: ['This block reuses the Section editor but has a different renderer for optimized image loading.'],
743
435
  },
744
436
  },
437
+
745
438
  form: {
746
439
  type: "form",
747
440
  label: "Form",
@@ -754,33 +447,7 @@ hero: {
754
447
  } as FormBlockContent,
755
448
  editorComponentFilename: "FormBlockEditor.tsx",
756
449
  rendererComponentFilename: "FormBlockRenderer.tsx",
757
- contentSchema: {
758
- recipient_email: {
759
- type: 'string',
760
- required: true,
761
- description: 'The email address where form submissions will be sent.',
762
- default: 'your-email@example.com',
763
- },
764
- submit_button_text: {
765
- type: 'string',
766
- required: true,
767
- description: 'The text to display on the submit button.',
768
- default: 'Submit',
769
- },
770
- success_message: {
771
- type: 'string',
772
- required: true,
773
- description: 'The message shown to the user after successful submission.',
774
- default: 'Thank you for your submission!',
775
- },
776
- fields: {
777
- type: 'array',
778
- required: true,
779
- description: 'The fields that make up the form.',
780
- default: [],
781
- arrayElementType: 'object',
782
- },
783
- },
450
+ schema: FormBlockSchema,
784
451
  documentation: {
785
452
  description: 'Creates an interactive form that can be submitted to a specified email address.',
786
453
  useCases: [
@@ -794,8 +461,20 @@ hero: {
794
461
  ],
795
462
  },
796
463
  },
464
+
465
+ "testimonial": {
466
+ ...TestimonialBlockConfig,
467
+ // Adapt SDK config to BlockDefinition requirements
468
+ editorComponentFilename: 'TestimonialBlockEditor', // Placeholder, not used if EditorComponent is present
469
+ rendererComponentFilename: 'TestimonialBlockRenderer', // Placeholder
470
+ documentation: {
471
+ description: 'Display a user testimonial with a quote, author, and optional image.',
472
+ useCases: ['Social proof', 'Customer reviews'],
473
+ }
474
+ } as BlockDefinition<TestimonialBlockContent>,
797
475
  };
798
476
 
477
+
799
478
  /**
800
479
  * Get the block definition for a specific block type
801
480
  *
@@ -837,13 +516,13 @@ export function isValidBlockType(blockType: string): blockType is BlockType {
837
516
  }
838
517
 
839
518
  /**
840
- * Get the content schema for a specific block type
519
+ * Get the Zod schema for a specific block type
841
520
  *
842
521
  * @param blockType - The type of block to get the schema for
843
- * @returns The content schema object or undefined if not found
522
+ * @returns The Zod schema object or undefined if not found
844
523
  */
845
- export function getContentSchema(blockType: BlockType): Record<string, ContentPropertyDefinition> | undefined {
846
- return blockRegistry[blockType]?.contentSchema;
524
+ export function getBlockSchema(blockType: BlockType): z.ZodType<any> | undefined {
525
+ return blockRegistry[blockType]?.schema;
847
526
  }
848
527
 
849
528
  /**
@@ -871,11 +550,12 @@ export type AllBlockContent =
871
550
  | ({ type: "section" } & SectionBlockContent)
872
551
  | ({ type: "hero" } & HeroBlockContent)
873
552
  | ({ type: "video_embed" } & VideoEmbedBlockContent)
874
- | ({ type: "form" } & FormBlockContent);
553
+ | ({ type: "form" } & FormBlockContent)
554
+ | ({ type: "testimonial" } & TestimonialBlockContent);
875
555
 
876
556
  /**
877
557
  * Validate block content against its schema
878
- * Performs runtime validation based on the content schema definitions
558
+ * Performs runtime validation based on the Zod schema definitions
879
559
  *
880
560
  * @param blockType - The type of block to validate
881
561
  * @param content - The content to validate
@@ -889,105 +569,23 @@ export function validateBlockContent(
889
569
  errors: string[];
890
570
  warnings: string[];
891
571
  } {
892
- const schema = getContentSchema(blockType);
572
+ const schema = getBlockSchema(blockType);
893
573
  if (!schema) {
894
574
  return { isValid: false, errors: ['Block type not found in registry'], warnings: [] };
895
575
  }
896
576
 
897
- const errors: string[] = [];
898
- const warnings: string[] = [];
899
-
900
- // Check required properties
901
- for (const [propertyName, propertyDef] of Object.entries(schema)) {
902
- if (propertyDef.required && (content[propertyName] === undefined || content[propertyName] === null)) {
903
- errors.push(`Required property '${propertyName}' is missing`);
904
- }
577
+ const result = schema.safeParse(content);
578
+
579
+ if (result.success) {
580
+ return { isValid: true, errors: [], warnings: [] };
581
+ } else {
582
+ // Format Zod errors
583
+ const errors = result.error.errors.map(e => {
584
+ const path = e.path.join('.');
585
+ return path ? `${path}: ${e.message}` : e.message;
586
+ });
587
+ return { isValid: false, errors, warnings: [] };
905
588
  }
906
-
907
- // Check property types and constraints
908
- for (const [propertyName, value] of Object.entries(content)) {
909
- const propertyDef = schema[propertyName];
910
- if (!propertyDef) {
911
- warnings.push(`Property '${propertyName}' is not defined in schema`);
912
- continue;
913
- }
914
-
915
- // Type checking
916
- const actualType = typeof value;
917
- if (propertyDef.type === 'string' && actualType !== 'string') {
918
- errors.push(`Property '${propertyName}' should be a string, got ${actualType}`);
919
- } else if (propertyDef.type === 'number' && actualType !== 'number') {
920
- errors.push(`Property '${propertyName}' should be a number, got ${actualType}`);
921
- } else if (propertyDef.type === 'boolean' && actualType !== 'boolean') {
922
- errors.push(`Property '${propertyName}' should be a boolean, got ${actualType}`);
923
- }
924
-
925
- // Constraint checking
926
- if (propertyDef.constraints) {
927
- const constraints = propertyDef.constraints;
928
-
929
- if (typeof value === 'number') {
930
- if (constraints.min !== undefined && value < constraints.min) {
931
- errors.push(`Property '${propertyName}' should be at least ${constraints.min}, got ${value}`);
932
- }
933
- if (constraints.max !== undefined && value > constraints.max) {
934
- errors.push(`Property '${propertyName}' should be at most ${constraints.max}, got ${value}`);
935
- }
936
- }
937
-
938
- if (constraints.enum && !constraints.enum.includes(value)) {
939
- errors.push(`Property '${propertyName}' should be one of [${constraints.enum.join(', ')}], got ${value}`);
940
- }
941
- }
942
- }
943
-
944
- return {
945
- isValid: errors.length === 0,
946
- errors,
947
- warnings,
948
- };
949
- }
950
-
951
- /**
952
- * Get property information for a specific block type and property
953
- * Useful for building dynamic forms or documentation
954
- *
955
- * @param blockType - The type of block
956
- * @param propertyName - The name of the property
957
- * @returns Property definition or undefined if not found
958
- */
959
- export function getPropertyDefinition(
960
- blockType: BlockType,
961
- propertyName: string
962
- ): ContentPropertyDefinition | undefined {
963
- const schema = getContentSchema(blockType);
964
- return schema?.[propertyName];
965
- }
966
-
967
- /**
968
- * Get all property names for a specific block type
969
- *
970
- * @param blockType - The type of block
971
- * @returns Array of property names
972
- */
973
- export function getPropertyNames(blockType: BlockType): string[] {
974
- const schema = getContentSchema(blockType);
975
- return schema ? Object.keys(schema) : [];
976
- }
977
-
978
- /**
979
- * Get required property names for a specific block type
980
- *
981
- * @param blockType - The type of block
982
- * @returns Array of required property names
983
- */
984
- export function getRequiredProperties(blockType: BlockType): string[] {
985
- const schema = getContentSchema(blockType);
986
- if (!schema) return [];
987
-
988
- return Object.entries(schema)
989
- .filter(([, def]) => def.required)
990
- .map(([name]) => name);
991
589
  }
992
590
 
993
591
  /**
@@ -998,18 +596,13 @@ export function getRequiredProperties(blockType: BlockType): string[] {
998
596
  * @returns Complete default content object
999
597
  */
1000
598
  export function generateDefaultContent(blockType: BlockType): Record<string, any> {
1001
- const schema = getContentSchema(blockType);
1002
- const initialContent = getInitialContent(blockType) || {};
1003
-
1004
- if (!schema) return initialContent;
599
+ // For Zod, initialContent is the best source of defaults as defined in the registry.
600
+ // Zod schemas don't easily expose default values without parsing.
601
+ // We return initialContent which is required to be valid against the schema.
602
+ return getInitialContent(blockType) as Record<string, any> || {};
603
+ }
604
+
605
+
1005
606
 
1006
- const defaultContent: Record<string, any> = { ...initialContent };
1007
607
 
1008
- for (const [propertyName, propertyDef] of Object.entries(schema)) {
1009
- if (defaultContent[propertyName] === undefined && propertyDef.default !== undefined) {
1010
- defaultContent[propertyName] = propertyDef.default;
1011
- }
1012
- }
1013
608
 
1014
- return defaultContent;
1015
- }