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.
- package/package.json +1 -1
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +45 -27
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +1 -1
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +13 -3
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +11 -4
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +98 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +76 -153
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +16 -11
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +23 -12
- package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +4 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +30 -6
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +17 -11
- package/templates/nextblock-template/app/cms/pages/page.tsx +6 -3
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +18 -12
- package/templates/nextblock-template/app/cms/posts/page.tsx +8 -5
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +18 -5
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +20 -4
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +33 -7
- package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +3 -3
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +41 -13
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +15 -13
- package/templates/nextblock-template/app/cms/settings/logos/actions.ts +2 -3
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +50 -0
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +14 -2
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +3 -6
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +33 -13
- package/templates/nextblock-template/components/BlockRenderer.tsx +14 -1
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +126 -0
- package/templates/nextblock-template/docs/How to Create a Custom Block.md +149 -0
- package/templates/nextblock-template/hooks/use-hotkeys.ts +27 -0
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +196 -603
- package/templates/nextblock-template/next-env.d.ts +1 -1
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/tsconfig.json +3 -0
- 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
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
164
|
+
editorComponentFilename?: string;
|
|
245
165
|
/** Filename of the renderer component (assumed to be in components/blocks/renderers/) */
|
|
246
|
-
rendererComponentFilename
|
|
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
|
-
*
|
|
174
|
+
* Zod schema defining the content properties, types, and constraints.
|
|
251
175
|
* Used for validation, documentation, and potential runtime type checking.
|
|
252
176
|
*/
|
|
253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
522
|
+
* @returns The Zod schema object or undefined if not found
|
|
844
523
|
*/
|
|
845
|
-
export function
|
|
846
|
-
return blockRegistry[blockType]?.
|
|
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
|
|
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 =
|
|
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
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
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
|
-
}
|