create-nextblock 0.15.10 → 0.16.2

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 (266) hide show
  1. package/docker-template/.dockerignore +1 -1
  2. package/libs/db/tsconfig.lib.json +3 -3
  3. package/libs/editor/tsconfig.lib.json +3 -3
  4. package/libs/ui/tsconfig.lib.json +3 -3
  5. package/libs/utils/tsconfig.json +3 -3
  6. package/package.json +50 -50
  7. package/project.json +19 -19
  8. package/templates/nextblock-template/.dockerignore +1 -1
  9. package/templates/nextblock-template/.swcrc +30 -30
  10. package/templates/nextblock-template/AGENTS.md +9 -9
  11. package/templates/nextblock-template/CLAUDE.md +1 -1
  12. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
  13. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
  14. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
  15. package/templates/nextblock-template/app/ToasterProvider.tsx +26 -26
  16. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
  17. package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -280
  18. package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -222
  19. package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -62
  20. package/templates/nextblock-template/app/actions/email.ts +241 -241
  21. package/templates/nextblock-template/app/actions/formActions.ts +245 -245
  22. package/templates/nextblock-template/app/actions/interactions.ts +489 -489
  23. package/templates/nextblock-template/app/actions/threadActions.ts +166 -166
  24. package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -210
  25. package/templates/nextblock-template/app/api/ai/seo/alt-text/route.ts +221 -0
  26. package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -0
  27. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
  28. package/templates/nextblock-template/app/api/checkout/route.ts +162 -162
  29. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +3092 -2900
  30. package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
  31. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  32. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  33. package/templates/nextblock-template/app/api/view/route.ts +114 -114
  34. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  35. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  36. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  37. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  38. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  39. package/templates/nextblock-template/app/checkout/page.tsx +57 -57
  40. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -552
  41. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +27 -0
  42. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
  43. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  44. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
  45. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +180 -3
  46. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +171 -6
  47. package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
  48. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  49. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  50. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +12 -3
  51. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  52. package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
  53. package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
  54. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
  55. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  56. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  57. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  58. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  59. package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
  60. package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
  61. package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
  62. package/templates/nextblock-template/app/cms/layout.tsx +101 -101
  63. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  64. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  65. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +177 -2
  66. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  67. package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
  68. package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
  69. package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
  70. package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
  71. package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
  72. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  73. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +28 -0
  74. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +248 -5
  75. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +33 -0
  76. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +240 -5
  77. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
  78. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  79. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  80. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  81. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  82. package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
  83. package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
  84. package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
  85. package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
  86. package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
  87. package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
  88. package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
  89. package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
  90. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  91. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  92. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
  93. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  94. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  95. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  96. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
  97. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
  98. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
  99. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
  100. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  101. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  102. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  103. package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -0
  104. package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -0
  105. package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -0
  106. package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -0
  107. package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -0
  108. package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -0
  109. package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -0
  110. package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
  111. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  112. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  113. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  114. package/templates/nextblock-template/app/layout.tsx +671 -671
  115. package/templates/nextblock-template/app/lib/seo.ts +319 -319
  116. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  117. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  118. package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
  119. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  120. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  121. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  122. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  123. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  124. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  125. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  126. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  127. package/templates/nextblock-template/app/providers.tsx +96 -96
  128. package/templates/nextblock-template/app/robots.ts +123 -0
  129. package/templates/nextblock-template/app/sitemap.ts +1 -1
  130. package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
  131. package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
  132. package/templates/nextblock-template/app/thread/layout.tsx +15 -15
  133. package/templates/nextblock-template/app/thread/page.tsx +98 -98
  134. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
  135. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
  136. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
  137. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
  138. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
  139. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
  140. package/templates/nextblock-template/components/BlockRenderer.tsx +312 -312
  141. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  142. package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
  143. package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
  144. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  145. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
  146. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
  147. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  148. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  149. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  150. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
  151. package/templates/nextblock-template/components/Header.tsx +38 -38
  152. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  153. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  154. package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
  155. package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
  156. package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
  157. package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
  158. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  159. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  160. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
  161. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
  162. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  163. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
  164. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
  165. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -18
  166. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
  167. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
  168. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
  169. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
  170. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
  171. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
  172. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
  173. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
  174. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
  175. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
  176. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  177. package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
  178. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  179. package/templates/nextblock-template/components/form-message.tsx +32 -32
  180. package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -0
  181. package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -0
  182. package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -0
  183. package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -0
  184. package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -0
  185. package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -0
  186. package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -0
  187. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  188. package/templates/nextblock-template/components/theme-icon.tsx +78 -78
  189. package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
  190. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  191. package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
  192. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  193. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
  194. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
  195. package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
  196. package/templates/nextblock-template/docs/README.md +42 -42
  197. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
  198. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  199. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  200. package/templates/nextblock-template/index.d.ts +7 -7
  201. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  202. package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
  203. package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
  204. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
  205. package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
  206. package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
  207. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
  208. package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
  209. package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
  210. package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
  211. package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
  212. package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
  213. package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -0
  214. package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -0
  215. package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
  216. package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
  217. package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
  218. package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
  219. package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
  220. package/templates/nextblock-template/lib/messages/threads.ts +579 -579
  221. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  222. package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
  223. package/templates/nextblock-template/lib/search/types.ts +27 -27
  224. package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -0
  225. package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -0
  226. package/templates/nextblock-template/lib/seo/block-content.ts +123 -0
  227. package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -0
  228. package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -0
  229. package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -0
  230. package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -0
  231. package/templates/nextblock-template/lib/seo/page-document.ts +412 -0
  232. package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -0
  233. package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -0
  234. package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -0
  235. package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -0
  236. package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -0
  237. package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -0
  238. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +177 -167
  239. package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
  240. package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
  241. package/templates/nextblock-template/lib/site-url.test.ts +89 -89
  242. package/templates/nextblock-template/lib/site-url.ts +102 -102
  243. package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
  244. package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
  245. package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
  246. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  247. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  248. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  249. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  250. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  251. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  252. package/templates/nextblock-template/next-env.d.ts +0 -1
  253. package/templates/nextblock-template/package.json +1 -1
  254. package/templates/nextblock-template/postcss.config.js +6 -6
  255. package/templates/nextblock-template/proxy.ts +240 -20
  256. package/templates/nextblock-template/scripts/backup.js +115 -115
  257. package/templates/nextblock-template/scripts/restore.js +385 -385
  258. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  259. package/templates/nextblock-template/tailwind.config.js +25 -25
  260. package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
  261. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  262. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  263. package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
  264. package/templates/nextblock-template/tools/update.mjs +1303 -1303
  265. package/tsconfig.base.json +3 -3
  266. package/templates/nextblock-template/app/robots.txt/route.ts +0 -32
@@ -1,761 +1,761 @@
1
- import { z } from '../zod-config';
2
- import { TestimonialBlockConfig, TestimonialBlockContent } from '../../components/blocks/TestimonialBlock';
3
- import { ProductGridBlockSchema, ProductGridBlockContent, FeaturedProductBlockSchema, FeaturedProductBlockContent, CartBlockSchema, CartBlockContent, CheckoutBlockSchema, CheckoutBlockContent, ProductDetailsBlockSchema, ProductDetailsBlockContent } from './ecommerce-block-schemas';
4
- import { availableBlockTypes, type BlockType } from './blockTypes';
5
- import { isCustomCssColor, TEXT_COLOR_TOKENS } from './blockColors';
6
- export { availableBlockTypes, type BlockType } from './blockTypes';
7
-
8
- /**
9
- * Block Registry System
10
- *
11
- * This module provides the central registry for all block types in the CMS.
12
- * It serves as the single source of truth for block definitions, including
13
- * their initial content, editor components, renderer components, and Zod schemas.
14
- */
15
-
16
- // --- Zod Schemas & Inferred Types ---
17
-
18
- export const TextBlockSchema = z.object({
19
- html_content: z.string().describe('Raw HTML content for the text block'),
20
- });
21
- export type TextBlockContent = z.infer<typeof TextBlockSchema>;
22
-
23
- /**
24
- * A text colour is either a theme token (keeps following the theme and dark
25
- * mode) or a literal CSS colour from the colour picker. Existing rows only ever
26
- * hold tokens, so widening the union is backward compatible.
27
- * See `./blockColors` for the token -> class mapping used at render time.
28
- */
29
- export const TextColorSchema = z
30
- .union([
31
- z.enum(TEXT_COLOR_TOKENS),
32
- z
33
- .string()
34
- .refine(isCustomCssColor, 'Must be a hex, rgb(a), hsl(a) or named CSS colour'),
35
- ])
36
- .describe(
37
- `A theme token (${TEXT_COLOR_TOKENS.join(', ')}) or a custom CSS colour such as "#FF8800", "rgba(255,136,0,0.8)" or "hsl(28, 100%, 50%)"`,
38
- );
39
-
40
- export const HeadingBlockSchema = z.object({
41
- 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)'),
42
- text_content: z.string().describe('The text content of the heading'),
43
- textAlign: z.enum(['left', 'center', 'right', 'justify']).optional().describe('Text alignment'),
44
- textColor: TextColorSchema.optional().describe('Color of the heading text'),
45
- });
46
- export type HeadingBlockContent = z.infer<typeof HeadingBlockSchema>;
47
-
48
- // Stock-photo attribution (required for Unsplash: photographer + Unsplash must be
49
- // credited and linked). Optional; set when an external_url is a stock photo. Field
50
- // names match the search_stock_photos result so the agent can copy them verbatim.
51
- // All fields are nullable + optional: the search_stock_photos result carries
52
- // explicit nulls for missing values (e.g. Pexels has no downloadLocation, and
53
- // utmSource is null until the Unsplash app name is set), and the agent copies the
54
- // object verbatim — so the schema must accept null, not just undefined.
55
- export const ImageAttributionSchema = z.object({
56
- provider: z.string().nullable().optional().describe('e.g. "unsplash" or "pexels"'),
57
- photographer: z.string().nullable().optional(),
58
- photographerUrl: z.string().nullable().optional(),
59
- sourceUrl: z.string().nullable().optional().describe('Link to the photo on the provider'),
60
- downloadLocation: z.string().nullable().optional().describe('Unsplash download-trigger endpoint'),
61
- utmSource: z.string().nullable().optional().describe('Unsplash app name for attribution utm_source'),
62
- });
63
- export type ImageAttribution = z.infer<typeof ImageAttributionSchema>;
64
-
65
- export const ImageBlockSchema = z.object({
66
- media_id: z.string().nullable().optional().describe('UUID of the media item'),
67
- object_key: z.string().nullable().optional().describe('The actual R2 object key'),
68
- external_url: z
69
- .string()
70
- .nullable()
71
- .optional()
72
- .describe('Direct external image URL (e.g. a stock photo). Rendered as-is; media_id/object_key are not required when this is set.'),
73
- attribution: ImageAttributionSchema.optional().describe('Stock-photo credit (required for Unsplash).'),
74
- alt_text: z.string().optional().describe('Alternative text'),
75
- caption: z.string().optional().describe('Optional caption'),
76
- width: z.number().nullable().optional().describe('Image width'),
77
- height: z.number().nullable().optional().describe('Image height'),
78
- blur_data_url: z.string().nullable().optional().describe('Base64 blur placeholder'),
79
- });
80
- export type ImageBlockContent = z.infer<typeof ImageBlockSchema>;
81
-
82
- export const ButtonBlockSchema = z.object({
83
- text: z.string().describe('The text displayed on the button'),
84
- url: z.string().describe('The URL the button links to'),
85
- variant: z.enum(['default', 'outline', 'secondary', 'ghost', 'link']).optional().describe('Visual style variant'),
86
- size: z.enum(['default', 'sm', 'lg', 'full']).optional().describe('Size of the button'),
87
- position: z.enum(['left', 'center', 'right']).optional().describe('Button alignment'),
88
- });
89
- export type ButtonBlockContent = z.infer<typeof ButtonBlockSchema>;
90
-
91
- export const PostsGridBlockSchema = z.object({
92
- postsPerPage: z.number().min(1).max(50).describe('Number of posts per page'),
93
- columns: z.number().min(1).max(6).describe('Number of columns'),
94
- showPagination: z.boolean().describe('Whether to show pagination'),
95
- title: z.string().optional().describe('Optional title'),
96
- });
97
- export type PostsGridBlockContent = z.infer<typeof PostsGridBlockSchema>;
98
-
99
- export const VideoEmbedBlockSchema = z.object({
100
- url: z.string().describe('The video URL'),
101
- title: z.string().optional().describe('Optional title'),
102
- autoplay: z.boolean().optional().describe('Autoplay'),
103
- controls: z.boolean().optional().describe('Show controls'),
104
- });
105
- export type VideoEmbedBlockContent = z.infer<typeof VideoEmbedBlockSchema>;
106
-
107
- // Section helpers
108
- const GradientSchema = z.object({
109
- type: z.enum(['linear', 'radial']),
110
- direction: z.string().optional(),
111
- stops: z.array(z.object({ color: z.string(), position: z.number() })),
112
- });
113
- export type Gradient = z.infer<typeof GradientSchema>;
114
-
115
- const BackgroundSchema = z.object({
116
- type: z.enum(['none', 'theme', 'solid', 'gradient', 'image']),
117
- theme: z.enum(['primary', 'secondary', 'muted', 'accent', 'destructive']).optional(),
118
- solid_color: z.string().optional(),
119
- min_height: z.string().optional(),
120
- gradient: GradientSchema.optional(),
121
- image: z.object({
122
- media_id: z.string().optional(),
123
- object_key: z.string().optional(),
124
- external_url: z.string().optional(),
125
- attribution: ImageAttributionSchema.optional(),
126
- alt_text: z.string().optional(),
127
- width: z.number().optional(),
128
- height: z.number().optional(),
129
- blur_data_url: z.string().optional(),
130
- size: z.enum(['cover', 'contain']),
131
- position: z.enum(['center', 'top', 'bottom', 'left', 'right']),
132
- quality: z.number().nullable().optional(),
133
- overlay: z.object({
134
- type: z.literal('gradient'),
135
- gradient: GradientSchema,
136
- }).optional(),
137
- }).optional(),
138
- });
139
-
140
- const BlockInColumnSchema = z.object({
141
- block_type: z.enum(availableBlockTypes),
142
- content: z.record(z.string(), z.any()),
143
- temp_id: z.string().optional(),
144
- });
145
-
146
- const SlideSchema = z.object({
147
- background: BackgroundSchema,
148
- column_blocks: z.array(z.array(BlockInColumnSchema)),
149
- });
150
-
151
- export const SectionBlockSchema = z.object({
152
- container_type: z.enum(['full-width', 'container', 'container-sm', 'container-lg', 'container-xl']),
153
- background: BackgroundSchema,
154
- responsive_columns: z.object({
155
- mobile: z.union([z.literal(1), z.literal(2)]),
156
- tablet: z.union([z.literal(1), z.literal(2), z.literal(3)]),
157
- desktop: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]),
158
- }),
159
- column_gap: z.enum(['none', 'sm', 'md', 'lg', 'xl']),
160
- padding: z.object({
161
- top: z.enum(['none', 'sm', 'md', 'lg', 'xl']),
162
- bottom: z.enum(['none', 'sm', 'md', 'lg', 'xl']),
163
- }),
164
- vertical_alignment: z.enum(['start', 'center', 'end', 'stretch']).optional(),
165
- column_blocks: z.array(z.array(BlockInColumnSchema)),
166
- is_hero: z.boolean().optional(),
167
- slider: z.boolean().optional(),
168
- autoplay: z.boolean().optional(),
169
- timeframe: z.number().optional(),
170
- slides: z.array(SlideSchema).optional(),
171
- });
172
- export type SectionBlockContent = z.infer<typeof SectionBlockSchema>;
173
-
174
-
175
-
176
- // Form helpers
177
- export const FormFieldOptionSchema = z.object({
178
- label: z.string(),
179
- value: z.string(),
180
- });
181
- export type FormFieldOption = z.infer<typeof FormFieldOptionSchema>;
182
-
183
- export const FormFieldSchema = z.object({
184
- temp_id: z.string(),
185
- field_type: z.enum(['text', 'email', 'textarea', 'select', 'radio', 'checkbox']),
186
- label: z.string(),
187
- placeholder: z.string().optional(),
188
- is_required: z.boolean(),
189
- options: z.array(FormFieldOptionSchema).optional(),
190
- });
191
- export type FormField = z.infer<typeof FormFieldSchema>;
192
-
193
- export const FormBlockSchema = z.object({
194
- // Opaque handle into `form_endpoints`, which holds the destination address and the
195
- // field manifest server-side. Safe to serialize: it grants nothing.
196
- form_key: z.string().optional(),
197
- // Legacy only. Migration 27 moved every stored address into form_endpoints and
198
- // deleted this key; it stays optional so an un-migrated import still validates,
199
- // and the renderer strips it before the content reaches the browser.
200
- recipient_email: z.string().email().optional(),
201
- submit_button_text: z.string(),
202
- success_message: z.string(),
203
- fields: z.array(FormFieldSchema),
204
- botProtectionProvider: z.enum(['none', 'turnstile', 'recaptcha']).optional().default('none'),
205
- botProtectionSiteKey: z.string().optional(),
206
- });
207
- export type FormBlockContent = z.infer<typeof FormBlockSchema>;
208
-
209
- /**
210
- * Enhanced block definition interface with generic type parameter
211
- * Links the TypeScript interface to the block definition for better type safety
212
- */
213
- export interface BlockDefinition<T = any> {
214
- /** The unique identifier for the block type */
215
- type: BlockType;
216
- /** User-friendly display name for the block */
217
- label: string;
218
- /** Optional icon for the block, using lucide-react icon names or component */
219
- icon?: string | any;
220
- /** Default content structure for new blocks of this type */
221
- initialContent: T;
222
- /** Filename of the editor component (assumed to be in app/cms/blocks/editors/) */
223
- editorComponentFilename?: string;
224
- /** Filename of the renderer component (assumed to be in components/blocks/renderers/) */
225
- rendererComponentFilename?: string;
226
- /** Direct React component for rendering (overrides filename if present) */
227
- RendererComponent?: React.ComponentType<any>;
228
- /** Direct React component for editing (overrides filename if present) */
229
- EditorComponent?: React.ComponentType<any>;
230
- /** Optional filename for specific preview components */
231
- previewComponentFilename?: string;
232
- /**
233
- * Zod schema defining the content properties, types, and constraints.
234
- * Used for validation, documentation, and potential runtime type checking.
235
- */
236
- schema: z.ZodType<T>;
237
- /**
238
- * JSDoc-style comments providing additional context about the block type,
239
- * its use cases, and any special considerations.
240
- */
241
- documentation?: {
242
- description?: string;
243
- examples?: string[];
244
- useCases?: string[];
245
- notes?: string[];
246
- };
247
- }
248
-
249
- /**
250
- * Central registry of all available block types and their configurations
251
- *
252
- * This registry contains the complete definition for each block type,
253
- * including their initial content values and structured content schemas.
254
- * This serves as the single source of truth for all block-related information,
255
- * eliminating the need to modify utils/supabase/types.ts when adding new block types.
256
- */
257
- export const blockRegistry: Record<BlockType, BlockDefinition> = {
258
- text: {
259
- type: "text",
260
- label: "Rich Text Block",
261
- icon: "FileText",
262
- initialContent: { html_content: "" } as TextBlockContent,
263
- editorComponentFilename: "TextBlockEditor.tsx",
264
- rendererComponentFilename: "TextBlockRenderer.tsx",
265
- schema: TextBlockSchema,
266
- documentation: {
267
- description: 'A rich text block that supports HTML content with WYSIWYG editing',
268
- examples: [
269
- '<p>Simple paragraph text</p>',
270
- '<h2>Heading with <strong>bold text</strong></h2>',
271
- '<ul><li>List item 1</li><li>List item 2</li></ul>',
272
- ],
273
- useCases: [
274
- 'Article content and body text',
275
- 'Rich formatted content with links and styling',
276
- 'Lists, quotes, and other structured text',
277
- ],
278
- notes: [
279
- 'Content is sanitized before rendering to prevent XSS attacks',
280
- 'Supports most HTML tags commonly used in content',
281
- ],
282
- },
283
- },
284
-
285
- heading: {
286
- type: "heading",
287
- label: "Heading",
288
- icon: "Heading",
289
- initialContent: { level: 1, text_content: "New Heading", textAlign: 'left', textColor: undefined } as HeadingBlockContent,
290
- editorComponentFilename: "HeadingBlockEditor.tsx",
291
- rendererComponentFilename: "HeadingBlockRenderer.tsx",
292
- schema: HeadingBlockSchema,
293
- documentation: {
294
- description: 'A semantic heading block with configurable hierarchy levels',
295
- examples: [
296
- '{ level: 1, text_content: "Main Page Title" }',
297
- '{ level: 2, text_content: "Section Heading" }',
298
- '{ level: 3, text_content: "Subsection Title" }',
299
- ],
300
- useCases: [
301
- 'Page and section titles',
302
- 'Content hierarchy and structure',
303
- 'SEO-friendly heading organization',
304
- ],
305
- notes: [
306
- 'Choose heading levels based on content hierarchy, not visual appearance',
307
- 'Avoid skipping heading levels (e.g., h1 to h3 without h2)',
308
- ],
309
- },
310
- },
311
-
312
- image: {
313
- type: "image",
314
- label: "Image",
315
- icon: "Image",
316
- initialContent: { media_id: null, alt_text: "", caption: "" } as ImageBlockContent,
317
- editorComponentFilename: "ImageBlockEditor.tsx",
318
- rendererComponentFilename: "ImageBlockRenderer.tsx",
319
- schema: ImageBlockSchema,
320
- documentation: {
321
- description: 'An image block with support for captions, alt text, and responsive sizing',
322
- examples: [
323
- '{ media_id: "uuid-123", alt_text: "Product photo", caption: "Our latest product" }',
324
- '{ media_id: "uuid-456", alt_text: "Team photo", width: 800, height: 600 }',
325
- ],
326
- useCases: [
327
- 'Article illustrations and photos',
328
- 'Product images and galleries',
329
- 'Decorative and informational graphics',
330
- ],
331
- notes: [
332
- 'Always provide alt_text for accessibility compliance',
333
- 'Images are automatically optimized and served from CDN',
334
- 'Dimensions are used for layout optimization and preventing content shifts',
335
- ],
336
- },
337
- },
338
-
339
- button: {
340
- type: "button",
341
- label: "Button",
342
- icon: "SquareMousePointer",
343
- initialContent: { text: "Click Me", url: "#", variant: "default", size: "default", position: "left" } as ButtonBlockContent,
344
- editorComponentFilename: "ButtonBlockEditor.tsx",
345
- rendererComponentFilename: "ButtonBlockRenderer.tsx",
346
- schema: ButtonBlockSchema,
347
- documentation: {
348
- description: 'A customizable button/link component with multiple style variants',
349
- examples: [
350
- '{ text: "Learn More", url: "/about", variant: "default", size: "lg" }',
351
- '{ text: "Contact Us", url: "/contact", variant: "outline" }',
352
- '{ text: "Download", url: "/files/doc.pdf", variant: "secondary" }',
353
- ],
354
- useCases: [
355
- 'Call-to-action buttons',
356
- 'Navigation links with button styling',
357
- 'Download and external links',
358
- ],
359
- notes: [
360
- 'External URLs automatically open in new tabs',
361
- 'Button styles follow the design system theme',
362
- 'Use appropriate variants based on button importance and context',
363
- ],
364
- },
365
- },
366
-
367
- posts_grid: {
368
- type: "posts_grid",
369
- label: "Posts Grid",
370
- icon: "LayoutGrid",
371
- initialContent: { postsPerPage: 12, columns: 3, showPagination: true, title: "Recent Posts" } as PostsGridBlockContent,
372
- editorComponentFilename: "PostsGridBlockEditor.tsx",
373
- rendererComponentFilename: "PostsGridBlockRenderer.tsx",
374
- schema: PostsGridBlockSchema,
375
- documentation: {
376
- description: 'A responsive grid layout for displaying blog posts with pagination',
377
- examples: [
378
- '{ postsPerPage: 6, columns: 2, showPagination: true, title: "Latest News" }',
379
- '{ postsPerPage: 9, columns: 3, showPagination: false, title: "Featured Articles" }',
380
- ],
381
- useCases: [
382
- 'Blog post listings and archives',
383
- 'Featured content sections',
384
- 'News and article showcases',
385
- ],
386
- notes: [
387
- 'Grid automatically adapts to smaller screens',
388
- 'Posts are filtered by current language',
389
- 'Pagination improves performance for large post collections',
390
- ],
391
- },
392
- },
393
-
394
- video_embed: {
395
- type: "video_embed",
396
- label: "Video Embed",
397
- icon: "SquarePlay",
398
- initialContent: {
399
- url: "",
400
- title: "",
401
- autoplay: false,
402
- controls: true
403
- } as VideoEmbedBlockContent,
404
- editorComponentFilename: "VideoEmbedBlockEditor.tsx",
405
- rendererComponentFilename: "VideoEmbedBlockRenderer.tsx",
406
- schema: VideoEmbedBlockSchema,
407
- documentation: {
408
- description: 'Embeds videos from popular platforms with customizable playback options',
409
- examples: [
410
- '{ url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", title: "Rick Roll", controls: true }',
411
- '{ url: "https://vimeo.com/123456789", autoplay: false, controls: true }',
412
- ],
413
- useCases: [
414
- 'Tutorial and educational videos',
415
- 'Product demonstrations',
416
- 'Marketing and promotional content',
417
- ],
418
- notes: [
419
- 'Supports YouTube, Vimeo, and other major video platforms',
420
- 'Autoplay may be restricted by browser policies',
421
- 'Videos are responsive and adapt to container width',
422
- ],
423
- },
424
- },
425
-
426
- section: {
427
- type: "section",
428
- label: "Section Layout",
429
- icon: "Columns3",
430
- initialContent: {
431
- container_type: "container",
432
- background: { type: "none" },
433
- responsive_columns: { mobile: 1, tablet: 2, desktop: 3 },
434
- column_gap: "md",
435
- vertical_alignment: "start",
436
- padding: { top: "md", bottom: "md" },
437
- column_blocks: [
438
- [{ block_type: "text", content: { html_content: "<p>Column 1</p>" } }],
439
- [{ block_type: "text", content: { html_content: "<p>Column 2</p>" } }],
440
- [{ block_type: "text", content: { html_content: "<p>Column 3</p>" } }]
441
- ],
442
- is_hero: false,
443
- slider: false,
444
- autoplay: false,
445
- timeframe: 5,
446
- slides: []
447
- } as SectionBlockContent,
448
- editorComponentFilename: "SectionBlockEditor.tsx",
449
- rendererComponentFilename: "SectionBlockRenderer.tsx",
450
- schema: SectionBlockSchema,
451
- documentation: {
452
- description: 'A flexible section layout with responsive columns and background options',
453
- examples: [
454
- '{ container_type: "container", responsive_columns: { mobile: 1, tablet: 2, desktop: 3 } }',
455
- '{ background: { type: "gradient" }, column_blocks: [...] }',
456
- '{ container_type: "full-width", background: { type: "image" } }',
457
- ],
458
- useCases: [
459
- 'Feature sections with multiple content blocks',
460
- 'Comparison layouts and product showcases',
461
- 'Hero sections with structured content',
462
- 'Multi-column article layouts',
463
- ],
464
- notes: [
465
- 'Blocks within sections can be edited inline',
466
- 'Supports full drag-and-drop between columns and sections',
467
- 'Background images are managed through existing media system',
468
- 'Responsive breakpoints follow Tailwind CSS conventions',
469
- ],
470
- },
471
- },
472
-
473
-
474
- form: {
475
- type: "form",
476
- label: "Form",
477
- icon: "NotebookPen",
478
- initialContent: {
479
- submit_button_text: "Submit",
480
- success_message: "Thank you for your submission!",
481
- fields: [],
482
- botProtectionProvider: "none",
483
- botProtectionSiteKey: "",
484
- } as FormBlockContent,
485
- editorComponentFilename: "FormBlockEditor.tsx",
486
- rendererComponentFilename: "FormBlockRenderer.tsx",
487
- schema: FormBlockSchema,
488
- documentation: {
489
- description: 'Creates an interactive form that can be submitted to a specified email address.',
490
- useCases: [
491
- 'Contact forms',
492
- 'Lead generation forms',
493
- 'Simple surveys',
494
- ],
495
- notes: [
496
- 'Submissions are stored as message threads and appear in CMS → Messages.',
497
- 'The destination address lives server-side in form_endpoints, keyed by form_key — it is never sent to the browser.',
498
- 'Email notification is best-effort: a stored submission is the record even when SMTP is unconfigured.',
499
- ],
500
- },
501
- },
502
-
503
- "testimonial": {
504
- ...TestimonialBlockConfig,
505
- // Adapt SDK config to BlockDefinition requirements
506
- editorComponentFilename: 'TestimonialBlockEditor', // Placeholder, not used if EditorComponent is present
507
- rendererComponentFilename: 'TestimonialBlockRenderer', // Placeholder
508
- documentation: {
509
- description: 'Display a user testimonial with a quote, author, and optional image.',
510
- useCases: ['Social proof', 'Customer reviews'],
511
- }
512
- } as BlockDefinition<TestimonialBlockContent>,
513
-
514
- "product_grid": {
515
- type: "product_grid",
516
- label: "Product Grid",
517
- icon: "ShoppingBag", // Lucide icon
518
- initialContent: { type: 'latest', limit: 6, showPagination: false } as ProductGridBlockContent,
519
- editorComponentFilename: "ProductGridBlockEditor.tsx", // Assuming standard naming
520
- rendererComponentFilename: "ProductGridBlockRenderer.tsx", // Assuming mapping to ProductGridBlock.tsx handled by renderer map not shown here, or we need to ensure the renderer map uses the file we created. The prompt implies creating the file in `lib/blocks` serves as the component. I will assume the system maps it.
521
- schema: ProductGridBlockSchema,
522
- documentation: {
523
- description: 'Displays a grid of products: the latest ones, everything in one or more categories, or a hand-picked list.',
524
- useCases: ['Homepage featured products', 'Category pages', 'Curated collections']
525
- }
526
- },
527
-
528
- "featured_product": {
529
- type: "featured_product",
530
- label: "Featured Product",
531
- icon: "Star",
532
- initialContent: { productId: '', showBackground: false } as FeaturedProductBlockContent,
533
- editorComponentFilename: "FeaturedProductBlockEditor.tsx",
534
- rendererComponentFilename: "FeaturedProductBlockRenderer.tsx",
535
- schema: FeaturedProductBlockSchema,
536
- documentation: {
537
- description: 'Highlights a specific product with a detailed view.',
538
- useCases: ['Product spotlight', 'Special offers']
539
- }
540
- },
541
-
542
- "cart": {
543
- type: "cart",
544
- label: "Cart",
545
- icon: "ShoppingCart",
546
- initialContent: {} as CartBlockContent,
547
- editorComponentFilename: "CartBlockEditor.tsx",
548
- rendererComponentFilename: "CartBlockRenderer.tsx",
549
- schema: CartBlockSchema,
550
- documentation: {
551
- description: 'Displays the shopping cart.',
552
- useCases: ['Cart page']
553
- }
554
- },
555
-
556
- "checkout": {
557
- type: "checkout",
558
- label: "Checkout",
559
- icon: "CreditCard",
560
- initialContent: {} as CheckoutBlockContent,
561
- editorComponentFilename: "CheckoutBlockEditor.tsx",
562
- rendererComponentFilename: "CheckoutBlockRenderer.tsx",
563
- schema: CheckoutBlockSchema,
564
- documentation: {
565
- description: 'Displays the checkout form.',
566
- useCases: ['Checkout page']
567
- }
568
- },
569
-
570
- "product_details": {
571
- type: "product_details",
572
- label: "Product Details",
573
- icon: "Tag",
574
- initialContent: {} as ProductDetailsBlockContent,
575
- editorComponentFilename: "ProductDetailsBlockEditor.tsx",
576
- rendererComponentFilename: "ProductDetailsBlockRenderer.tsx",
577
- schema: ProductDetailsBlockSchema,
578
- documentation: {
579
- description: 'Displays product details (Title, Price, Add to Cart) using context.',
580
- useCases: ['Product Template Page']
581
- }
582
- },
583
- };
584
-
585
-
586
- /**
587
- * Get the block definition for a specific block type
588
- *
589
- * @param blockType - The type of block to get the definition for
590
- * @returns The block definition or undefined if not found
591
- */
592
- export function getBlockDefinition(blockType: string): BlockDefinition | undefined {
593
- if (blockType in blockRegistry) {
594
- return blockRegistry[blockType as BlockType];
595
- }
596
- return undefined;
597
- }
598
-
599
- /**
600
- * Whether a block type has anything for an author to configure.
601
- *
602
- * `cart`, `checkout` and `product_details` are context-driven: they declare an
603
- * empty schema and ship no editor file at all, so opening an editor for them
604
- * would show an empty dialog (and fail on the missing module). Used to decide
605
- * whether adding a block should jump straight into its editor.
606
- *
607
- * @param blockType - The block type or custom-block slug
608
- * @returns true when the block exposes editable fields
609
- */
610
- export function blockHasEditableContent(blockType: string): boolean {
611
- const definition = getBlockDefinition(blockType);
612
- // Custom blocks are not in the registry; they always render a field editor.
613
- if (!definition) return true;
614
- const shape = (definition.schema as unknown as { shape?: Record<string, unknown> })?.shape;
615
- if (!shape) return true;
616
- return Object.keys(shape).length > 0;
617
- }
618
-
619
- /**
620
- * Get the initial content for a specific block type
621
- *
622
- * @param blockType - The type of block to get initial content for
623
- * @returns The initial content object or undefined if block type not found
624
- */
625
- export function getInitialContent(blockType: string): object | undefined {
626
- if (blockType in blockRegistry) {
627
- return blockRegistry[blockType as BlockType]?.initialContent;
628
- }
629
- return {};
630
- }
631
-
632
- /**
633
- * Get the label for a specific block type
634
- *
635
- * @param blockType - The type of block to get the label for
636
- * @returns The user-friendly label or undefined if block type not found
637
- */
638
- export function getBlockLabel(blockType: string): string | undefined {
639
- if (blockType in blockRegistry) {
640
- return blockRegistry[blockType as BlockType]?.label;
641
- }
642
- return blockType
643
- .split(/[-_]+/)
644
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
645
- .join(" ");
646
- }
647
-
648
- /**
649
- * Check if a block type is valid/registered
650
- *
651
- * @param blockType - The block type to validate
652
- * @returns True if the block type exists in the registry
653
- */
654
- export function isValidBlockType(blockType: string): boolean {
655
- return blockType in blockRegistry || /^[a-z0-9_-]+$/.test(blockType);
656
- }
657
-
658
- /**
659
- * Get the Zod schema for a specific block type
660
- *
661
- * @param blockType - The type of block to get the schema for
662
- * @returns The Zod schema object or undefined if not found
663
- */
664
- export function getBlockSchema(blockType: string): z.ZodType<any> | undefined {
665
- if (blockType in blockRegistry) {
666
- return blockRegistry[blockType as BlockType]?.schema;
667
- }
668
- return z.record(z.string(), z.any());
669
- }
670
-
671
- /**
672
- * Get documentation for a specific block type
673
- *
674
- * @param blockType - The type of block to get documentation for
675
- * @returns The documentation object or undefined if not found
676
- */
677
- export function getBlockDocumentation(blockType: string): BlockDefinition['documentation'] | undefined {
678
- if (blockType in blockRegistry) {
679
- return blockRegistry[blockType as BlockType]?.documentation;
680
- }
681
- return {
682
- description: "Custom user-defined block layout component",
683
- };
684
- }
685
-
686
- /**
687
- * Generate a union type for all block content types
688
- * This creates a discriminated union based on block type
689
- *
690
- * @returns A TypeScript union type for all block content
691
- */
692
- export type AllBlockContent =
693
- | ({ type: "text" } & TextBlockContent)
694
- | ({ type: "heading" } & HeadingBlockContent)
695
- | ({ type: "image" } & ImageBlockContent)
696
- | ({ type: "button" } & ButtonBlockContent)
697
- | ({ type: "posts_grid" } & PostsGridBlockContent)
698
- | ({ type: "section" } & SectionBlockContent)
699
-
700
- | ({ type: "video_embed" } & VideoEmbedBlockContent)
701
- | ({ type: "form" } & FormBlockContent)
702
- | ({ type: "testimonial" } & TestimonialBlockContent)
703
- | ({ type: "product_grid" } & ProductGridBlockContent)
704
- | ({ type: "featured_product" } & FeaturedProductBlockContent)
705
- | ({ type: "cart" } & CartBlockContent)
706
- | ({ type: "checkout" } & CheckoutBlockContent)
707
- | ({ type: "product_details" } & ProductDetailsBlockContent);
708
-
709
- /**
710
- * Validate block content against its schema
711
- * Performs runtime validation based on the Zod schema definitions
712
- *
713
- * @param blockType - The type of block to validate
714
- * @param content - The content to validate
715
- * @returns An object with validation results
716
- */
717
- export function validateBlockContent(
718
- blockType: BlockType,
719
- content: Record<string, any>
720
- ): {
721
- isValid: boolean;
722
- errors: string[];
723
- warnings: string[];
724
- } {
725
- const schema = getBlockSchema(blockType);
726
- if (!schema) {
727
- return { isValid: false, errors: ['Block type not found in registry'], warnings: [] };
728
- }
729
-
730
- const result = schema.safeParse(content);
731
-
732
- if (result.success) {
733
- return { isValid: true, errors: [], warnings: [] };
734
- } else {
735
- // Format Zod errors
736
- const errors = result.error.issues.map(e => {
737
- const path = e.path.join('.');
738
- return path ? `${path}: ${e.message}` : e.message;
739
- });
740
- return { isValid: false, errors, warnings: [] };
741
- }
742
- }
743
-
744
- /**
745
- * Generate default content for a block type based on its schema
746
- * This is more comprehensive than initialContent as it includes all properties with defaults
747
- *
748
- * @param blockType - The type of block
749
- * @returns Complete default content object
750
- */
751
- export function generateDefaultContent(blockType: BlockType): Record<string, any> {
752
- // For Zod, initialContent is the best source of defaults as defined in the registry.
753
- // Zod schemas don't easily expose default values without parsing.
754
- // We return initialContent which is required to be valid against the schema.
755
- return getInitialContent(blockType) as Record<string, any> || {};
756
- }
757
-
758
-
759
-
760
-
761
-
1
+ import { z } from '../zod-config';
2
+ import { TestimonialBlockConfig, TestimonialBlockContent } from '../../components/blocks/TestimonialBlock';
3
+ import { ProductGridBlockSchema, ProductGridBlockContent, FeaturedProductBlockSchema, FeaturedProductBlockContent, CartBlockSchema, CartBlockContent, CheckoutBlockSchema, CheckoutBlockContent, ProductDetailsBlockSchema, ProductDetailsBlockContent } from './ecommerce-block-schemas';
4
+ import { availableBlockTypes, type BlockType } from './blockTypes';
5
+ import { isCustomCssColor, TEXT_COLOR_TOKENS } from './blockColors';
6
+ export { availableBlockTypes, type BlockType } from './blockTypes';
7
+
8
+ /**
9
+ * Block Registry System
10
+ *
11
+ * This module provides the central registry for all block types in the CMS.
12
+ * It serves as the single source of truth for block definitions, including
13
+ * their initial content, editor components, renderer components, and Zod schemas.
14
+ */
15
+
16
+ // --- Zod Schemas & Inferred Types ---
17
+
18
+ export const TextBlockSchema = z.object({
19
+ html_content: z.string().describe('Raw HTML content for the text block'),
20
+ });
21
+ export type TextBlockContent = z.infer<typeof TextBlockSchema>;
22
+
23
+ /**
24
+ * A text colour is either a theme token (keeps following the theme and dark
25
+ * mode) or a literal CSS colour from the colour picker. Existing rows only ever
26
+ * hold tokens, so widening the union is backward compatible.
27
+ * See `./blockColors` for the token -> class mapping used at render time.
28
+ */
29
+ export const TextColorSchema = z
30
+ .union([
31
+ z.enum(TEXT_COLOR_TOKENS),
32
+ z
33
+ .string()
34
+ .refine(isCustomCssColor, 'Must be a hex, rgb(a), hsl(a) or named CSS colour'),
35
+ ])
36
+ .describe(
37
+ `A theme token (${TEXT_COLOR_TOKENS.join(', ')}) or a custom CSS colour such as "#FF8800", "rgba(255,136,0,0.8)" or "hsl(28, 100%, 50%)"`,
38
+ );
39
+
40
+ export const HeadingBlockSchema = z.object({
41
+ 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)'),
42
+ text_content: z.string().describe('The text content of the heading'),
43
+ textAlign: z.enum(['left', 'center', 'right', 'justify']).optional().describe('Text alignment'),
44
+ textColor: TextColorSchema.optional().describe('Color of the heading text'),
45
+ });
46
+ export type HeadingBlockContent = z.infer<typeof HeadingBlockSchema>;
47
+
48
+ // Stock-photo attribution (required for Unsplash: photographer + Unsplash must be
49
+ // credited and linked). Optional; set when an external_url is a stock photo. Field
50
+ // names match the search_stock_photos result so the agent can copy them verbatim.
51
+ // All fields are nullable + optional: the search_stock_photos result carries
52
+ // explicit nulls for missing values (e.g. Pexels has no downloadLocation, and
53
+ // utmSource is null until the Unsplash app name is set), and the agent copies the
54
+ // object verbatim — so the schema must accept null, not just undefined.
55
+ export const ImageAttributionSchema = z.object({
56
+ provider: z.string().nullable().optional().describe('e.g. "unsplash" or "pexels"'),
57
+ photographer: z.string().nullable().optional(),
58
+ photographerUrl: z.string().nullable().optional(),
59
+ sourceUrl: z.string().nullable().optional().describe('Link to the photo on the provider'),
60
+ downloadLocation: z.string().nullable().optional().describe('Unsplash download-trigger endpoint'),
61
+ utmSource: z.string().nullable().optional().describe('Unsplash app name for attribution utm_source'),
62
+ });
63
+ export type ImageAttribution = z.infer<typeof ImageAttributionSchema>;
64
+
65
+ export const ImageBlockSchema = z.object({
66
+ media_id: z.string().nullable().optional().describe('UUID of the media item'),
67
+ object_key: z.string().nullable().optional().describe('The actual R2 object key'),
68
+ external_url: z
69
+ .string()
70
+ .nullable()
71
+ .optional()
72
+ .describe('Direct external image URL (e.g. a stock photo). Rendered as-is; media_id/object_key are not required when this is set.'),
73
+ attribution: ImageAttributionSchema.optional().describe('Stock-photo credit (required for Unsplash).'),
74
+ alt_text: z.string().optional().describe('Alternative text'),
75
+ caption: z.string().optional().describe('Optional caption'),
76
+ width: z.number().nullable().optional().describe('Image width'),
77
+ height: z.number().nullable().optional().describe('Image height'),
78
+ blur_data_url: z.string().nullable().optional().describe('Base64 blur placeholder'),
79
+ });
80
+ export type ImageBlockContent = z.infer<typeof ImageBlockSchema>;
81
+
82
+ export const ButtonBlockSchema = z.object({
83
+ text: z.string().describe('The text displayed on the button'),
84
+ url: z.string().describe('The URL the button links to'),
85
+ variant: z.enum(['default', 'outline', 'secondary', 'ghost', 'link']).optional().describe('Visual style variant'),
86
+ size: z.enum(['default', 'sm', 'lg', 'full']).optional().describe('Size of the button'),
87
+ position: z.enum(['left', 'center', 'right']).optional().describe('Button alignment'),
88
+ });
89
+ export type ButtonBlockContent = z.infer<typeof ButtonBlockSchema>;
90
+
91
+ export const PostsGridBlockSchema = z.object({
92
+ postsPerPage: z.number().min(1).max(50).describe('Number of posts per page'),
93
+ columns: z.number().min(1).max(6).describe('Number of columns'),
94
+ showPagination: z.boolean().describe('Whether to show pagination'),
95
+ title: z.string().optional().describe('Optional title'),
96
+ });
97
+ export type PostsGridBlockContent = z.infer<typeof PostsGridBlockSchema>;
98
+
99
+ export const VideoEmbedBlockSchema = z.object({
100
+ url: z.string().describe('The video URL'),
101
+ title: z.string().optional().describe('Optional title'),
102
+ autoplay: z.boolean().optional().describe('Autoplay'),
103
+ controls: z.boolean().optional().describe('Show controls'),
104
+ });
105
+ export type VideoEmbedBlockContent = z.infer<typeof VideoEmbedBlockSchema>;
106
+
107
+ // Section helpers
108
+ const GradientSchema = z.object({
109
+ type: z.enum(['linear', 'radial']),
110
+ direction: z.string().optional(),
111
+ stops: z.array(z.object({ color: z.string(), position: z.number() })),
112
+ });
113
+ export type Gradient = z.infer<typeof GradientSchema>;
114
+
115
+ const BackgroundSchema = z.object({
116
+ type: z.enum(['none', 'theme', 'solid', 'gradient', 'image']),
117
+ theme: z.enum(['primary', 'secondary', 'muted', 'accent', 'destructive']).optional(),
118
+ solid_color: z.string().optional(),
119
+ min_height: z.string().optional(),
120
+ gradient: GradientSchema.optional(),
121
+ image: z.object({
122
+ media_id: z.string().optional(),
123
+ object_key: z.string().optional(),
124
+ external_url: z.string().optional(),
125
+ attribution: ImageAttributionSchema.optional(),
126
+ alt_text: z.string().optional(),
127
+ width: z.number().optional(),
128
+ height: z.number().optional(),
129
+ blur_data_url: z.string().optional(),
130
+ size: z.enum(['cover', 'contain']),
131
+ position: z.enum(['center', 'top', 'bottom', 'left', 'right']),
132
+ quality: z.number().nullable().optional(),
133
+ overlay: z.object({
134
+ type: z.literal('gradient'),
135
+ gradient: GradientSchema,
136
+ }).optional(),
137
+ }).optional(),
138
+ });
139
+
140
+ const BlockInColumnSchema = z.object({
141
+ block_type: z.enum(availableBlockTypes),
142
+ content: z.record(z.string(), z.any()),
143
+ temp_id: z.string().optional(),
144
+ });
145
+
146
+ const SlideSchema = z.object({
147
+ background: BackgroundSchema,
148
+ column_blocks: z.array(z.array(BlockInColumnSchema)),
149
+ });
150
+
151
+ export const SectionBlockSchema = z.object({
152
+ container_type: z.enum(['full-width', 'container', 'container-sm', 'container-lg', 'container-xl']),
153
+ background: BackgroundSchema,
154
+ responsive_columns: z.object({
155
+ mobile: z.union([z.literal(1), z.literal(2)]),
156
+ tablet: z.union([z.literal(1), z.literal(2), z.literal(3)]),
157
+ desktop: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]),
158
+ }),
159
+ column_gap: z.enum(['none', 'sm', 'md', 'lg', 'xl']),
160
+ padding: z.object({
161
+ top: z.enum(['none', 'sm', 'md', 'lg', 'xl']),
162
+ bottom: z.enum(['none', 'sm', 'md', 'lg', 'xl']),
163
+ }),
164
+ vertical_alignment: z.enum(['start', 'center', 'end', 'stretch']).optional(),
165
+ column_blocks: z.array(z.array(BlockInColumnSchema)),
166
+ is_hero: z.boolean().optional(),
167
+ slider: z.boolean().optional(),
168
+ autoplay: z.boolean().optional(),
169
+ timeframe: z.number().optional(),
170
+ slides: z.array(SlideSchema).optional(),
171
+ });
172
+ export type SectionBlockContent = z.infer<typeof SectionBlockSchema>;
173
+
174
+
175
+
176
+ // Form helpers
177
+ export const FormFieldOptionSchema = z.object({
178
+ label: z.string(),
179
+ value: z.string(),
180
+ });
181
+ export type FormFieldOption = z.infer<typeof FormFieldOptionSchema>;
182
+
183
+ export const FormFieldSchema = z.object({
184
+ temp_id: z.string(),
185
+ field_type: z.enum(['text', 'email', 'textarea', 'select', 'radio', 'checkbox']),
186
+ label: z.string(),
187
+ placeholder: z.string().optional(),
188
+ is_required: z.boolean(),
189
+ options: z.array(FormFieldOptionSchema).optional(),
190
+ });
191
+ export type FormField = z.infer<typeof FormFieldSchema>;
192
+
193
+ export const FormBlockSchema = z.object({
194
+ // Opaque handle into `form_endpoints`, which holds the destination address and the
195
+ // field manifest server-side. Safe to serialize: it grants nothing.
196
+ form_key: z.string().optional(),
197
+ // Legacy only. Migration 27 moved every stored address into form_endpoints and
198
+ // deleted this key; it stays optional so an un-migrated import still validates,
199
+ // and the renderer strips it before the content reaches the browser.
200
+ recipient_email: z.string().email().optional(),
201
+ submit_button_text: z.string(),
202
+ success_message: z.string(),
203
+ fields: z.array(FormFieldSchema),
204
+ botProtectionProvider: z.enum(['none', 'turnstile', 'recaptcha']).optional().default('none'),
205
+ botProtectionSiteKey: z.string().optional(),
206
+ });
207
+ export type FormBlockContent = z.infer<typeof FormBlockSchema>;
208
+
209
+ /**
210
+ * Enhanced block definition interface with generic type parameter
211
+ * Links the TypeScript interface to the block definition for better type safety
212
+ */
213
+ export interface BlockDefinition<T = any> {
214
+ /** The unique identifier for the block type */
215
+ type: BlockType;
216
+ /** User-friendly display name for the block */
217
+ label: string;
218
+ /** Optional icon for the block, using lucide-react icon names or component */
219
+ icon?: string | any;
220
+ /** Default content structure for new blocks of this type */
221
+ initialContent: T;
222
+ /** Filename of the editor component (assumed to be in app/cms/blocks/editors/) */
223
+ editorComponentFilename?: string;
224
+ /** Filename of the renderer component (assumed to be in components/blocks/renderers/) */
225
+ rendererComponentFilename?: string;
226
+ /** Direct React component for rendering (overrides filename if present) */
227
+ RendererComponent?: React.ComponentType<any>;
228
+ /** Direct React component for editing (overrides filename if present) */
229
+ EditorComponent?: React.ComponentType<any>;
230
+ /** Optional filename for specific preview components */
231
+ previewComponentFilename?: string;
232
+ /**
233
+ * Zod schema defining the content properties, types, and constraints.
234
+ * Used for validation, documentation, and potential runtime type checking.
235
+ */
236
+ schema: z.ZodType<T>;
237
+ /**
238
+ * JSDoc-style comments providing additional context about the block type,
239
+ * its use cases, and any special considerations.
240
+ */
241
+ documentation?: {
242
+ description?: string;
243
+ examples?: string[];
244
+ useCases?: string[];
245
+ notes?: string[];
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Central registry of all available block types and their configurations
251
+ *
252
+ * This registry contains the complete definition for each block type,
253
+ * including their initial content values and structured content schemas.
254
+ * This serves as the single source of truth for all block-related information,
255
+ * eliminating the need to modify utils/supabase/types.ts when adding new block types.
256
+ */
257
+ export const blockRegistry: Record<BlockType, BlockDefinition> = {
258
+ text: {
259
+ type: "text",
260
+ label: "Rich Text Block",
261
+ icon: "FileText",
262
+ initialContent: { html_content: "" } as TextBlockContent,
263
+ editorComponentFilename: "TextBlockEditor.tsx",
264
+ rendererComponentFilename: "TextBlockRenderer.tsx",
265
+ schema: TextBlockSchema,
266
+ documentation: {
267
+ description: 'A rich text block that supports HTML content with WYSIWYG editing',
268
+ examples: [
269
+ '<p>Simple paragraph text</p>',
270
+ '<h2>Heading with <strong>bold text</strong></h2>',
271
+ '<ul><li>List item 1</li><li>List item 2</li></ul>',
272
+ ],
273
+ useCases: [
274
+ 'Article content and body text',
275
+ 'Rich formatted content with links and styling',
276
+ 'Lists, quotes, and other structured text',
277
+ ],
278
+ notes: [
279
+ 'Content is sanitized before rendering to prevent XSS attacks',
280
+ 'Supports most HTML tags commonly used in content',
281
+ ],
282
+ },
283
+ },
284
+
285
+ heading: {
286
+ type: "heading",
287
+ label: "Heading",
288
+ icon: "Heading",
289
+ initialContent: { level: 1, text_content: "New Heading", textAlign: 'left', textColor: undefined } as HeadingBlockContent,
290
+ editorComponentFilename: "HeadingBlockEditor.tsx",
291
+ rendererComponentFilename: "HeadingBlockRenderer.tsx",
292
+ schema: HeadingBlockSchema,
293
+ documentation: {
294
+ description: 'A semantic heading block with configurable hierarchy levels',
295
+ examples: [
296
+ '{ level: 1, text_content: "Main Page Title" }',
297
+ '{ level: 2, text_content: "Section Heading" }',
298
+ '{ level: 3, text_content: "Subsection Title" }',
299
+ ],
300
+ useCases: [
301
+ 'Page and section titles',
302
+ 'Content hierarchy and structure',
303
+ 'SEO-friendly heading organization',
304
+ ],
305
+ notes: [
306
+ 'Choose heading levels based on content hierarchy, not visual appearance',
307
+ 'Avoid skipping heading levels (e.g., h1 to h3 without h2)',
308
+ ],
309
+ },
310
+ },
311
+
312
+ image: {
313
+ type: "image",
314
+ label: "Image",
315
+ icon: "Image",
316
+ initialContent: { media_id: null, alt_text: "", caption: "" } as ImageBlockContent,
317
+ editorComponentFilename: "ImageBlockEditor.tsx",
318
+ rendererComponentFilename: "ImageBlockRenderer.tsx",
319
+ schema: ImageBlockSchema,
320
+ documentation: {
321
+ description: 'An image block with support for captions, alt text, and responsive sizing',
322
+ examples: [
323
+ '{ media_id: "uuid-123", alt_text: "Product photo", caption: "Our latest product" }',
324
+ '{ media_id: "uuid-456", alt_text: "Team photo", width: 800, height: 600 }',
325
+ ],
326
+ useCases: [
327
+ 'Article illustrations and photos',
328
+ 'Product images and galleries',
329
+ 'Decorative and informational graphics',
330
+ ],
331
+ notes: [
332
+ 'Always provide alt_text for accessibility compliance',
333
+ 'Images are automatically optimized and served from CDN',
334
+ 'Dimensions are used for layout optimization and preventing content shifts',
335
+ ],
336
+ },
337
+ },
338
+
339
+ button: {
340
+ type: "button",
341
+ label: "Button",
342
+ icon: "SquareMousePointer",
343
+ initialContent: { text: "Click Me", url: "#", variant: "default", size: "default", position: "left" } as ButtonBlockContent,
344
+ editorComponentFilename: "ButtonBlockEditor.tsx",
345
+ rendererComponentFilename: "ButtonBlockRenderer.tsx",
346
+ schema: ButtonBlockSchema,
347
+ documentation: {
348
+ description: 'A customizable button/link component with multiple style variants',
349
+ examples: [
350
+ '{ text: "Learn More", url: "/about", variant: "default", size: "lg" }',
351
+ '{ text: "Contact Us", url: "/contact", variant: "outline" }',
352
+ '{ text: "Download", url: "/files/doc.pdf", variant: "secondary" }',
353
+ ],
354
+ useCases: [
355
+ 'Call-to-action buttons',
356
+ 'Navigation links with button styling',
357
+ 'Download and external links',
358
+ ],
359
+ notes: [
360
+ 'External URLs automatically open in new tabs',
361
+ 'Button styles follow the design system theme',
362
+ 'Use appropriate variants based on button importance and context',
363
+ ],
364
+ },
365
+ },
366
+
367
+ posts_grid: {
368
+ type: "posts_grid",
369
+ label: "Posts Grid",
370
+ icon: "LayoutGrid",
371
+ initialContent: { postsPerPage: 12, columns: 3, showPagination: true, title: "Recent Posts" } as PostsGridBlockContent,
372
+ editorComponentFilename: "PostsGridBlockEditor.tsx",
373
+ rendererComponentFilename: "PostsGridBlockRenderer.tsx",
374
+ schema: PostsGridBlockSchema,
375
+ documentation: {
376
+ description: 'A responsive grid layout for displaying blog posts with pagination',
377
+ examples: [
378
+ '{ postsPerPage: 6, columns: 2, showPagination: true, title: "Latest News" }',
379
+ '{ postsPerPage: 9, columns: 3, showPagination: false, title: "Featured Articles" }',
380
+ ],
381
+ useCases: [
382
+ 'Blog post listings and archives',
383
+ 'Featured content sections',
384
+ 'News and article showcases',
385
+ ],
386
+ notes: [
387
+ 'Grid automatically adapts to smaller screens',
388
+ 'Posts are filtered by current language',
389
+ 'Pagination improves performance for large post collections',
390
+ ],
391
+ },
392
+ },
393
+
394
+ video_embed: {
395
+ type: "video_embed",
396
+ label: "Video Embed",
397
+ icon: "SquarePlay",
398
+ initialContent: {
399
+ url: "",
400
+ title: "",
401
+ autoplay: false,
402
+ controls: true
403
+ } as VideoEmbedBlockContent,
404
+ editorComponentFilename: "VideoEmbedBlockEditor.tsx",
405
+ rendererComponentFilename: "VideoEmbedBlockRenderer.tsx",
406
+ schema: VideoEmbedBlockSchema,
407
+ documentation: {
408
+ description: 'Embeds videos from popular platforms with customizable playback options',
409
+ examples: [
410
+ '{ url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", title: "Rick Roll", controls: true }',
411
+ '{ url: "https://vimeo.com/123456789", autoplay: false, controls: true }',
412
+ ],
413
+ useCases: [
414
+ 'Tutorial and educational videos',
415
+ 'Product demonstrations',
416
+ 'Marketing and promotional content',
417
+ ],
418
+ notes: [
419
+ 'Supports YouTube, Vimeo, and other major video platforms',
420
+ 'Autoplay may be restricted by browser policies',
421
+ 'Videos are responsive and adapt to container width',
422
+ ],
423
+ },
424
+ },
425
+
426
+ section: {
427
+ type: "section",
428
+ label: "Section Layout",
429
+ icon: "Columns3",
430
+ initialContent: {
431
+ container_type: "container",
432
+ background: { type: "none" },
433
+ responsive_columns: { mobile: 1, tablet: 2, desktop: 3 },
434
+ column_gap: "md",
435
+ vertical_alignment: "start",
436
+ padding: { top: "md", bottom: "md" },
437
+ column_blocks: [
438
+ [{ block_type: "text", content: { html_content: "<p>Column 1</p>" } }],
439
+ [{ block_type: "text", content: { html_content: "<p>Column 2</p>" } }],
440
+ [{ block_type: "text", content: { html_content: "<p>Column 3</p>" } }]
441
+ ],
442
+ is_hero: false,
443
+ slider: false,
444
+ autoplay: false,
445
+ timeframe: 5,
446
+ slides: []
447
+ } as SectionBlockContent,
448
+ editorComponentFilename: "SectionBlockEditor.tsx",
449
+ rendererComponentFilename: "SectionBlockRenderer.tsx",
450
+ schema: SectionBlockSchema,
451
+ documentation: {
452
+ description: 'A flexible section layout with responsive columns and background options',
453
+ examples: [
454
+ '{ container_type: "container", responsive_columns: { mobile: 1, tablet: 2, desktop: 3 } }',
455
+ '{ background: { type: "gradient" }, column_blocks: [...] }',
456
+ '{ container_type: "full-width", background: { type: "image" } }',
457
+ ],
458
+ useCases: [
459
+ 'Feature sections with multiple content blocks',
460
+ 'Comparison layouts and product showcases',
461
+ 'Hero sections with structured content',
462
+ 'Multi-column article layouts',
463
+ ],
464
+ notes: [
465
+ 'Blocks within sections can be edited inline',
466
+ 'Supports full drag-and-drop between columns and sections',
467
+ 'Background images are managed through existing media system',
468
+ 'Responsive breakpoints follow Tailwind CSS conventions',
469
+ ],
470
+ },
471
+ },
472
+
473
+
474
+ form: {
475
+ type: "form",
476
+ label: "Form",
477
+ icon: "NotebookPen",
478
+ initialContent: {
479
+ submit_button_text: "Submit",
480
+ success_message: "Thank you for your submission!",
481
+ fields: [],
482
+ botProtectionProvider: "none",
483
+ botProtectionSiteKey: "",
484
+ } as FormBlockContent,
485
+ editorComponentFilename: "FormBlockEditor.tsx",
486
+ rendererComponentFilename: "FormBlockRenderer.tsx",
487
+ schema: FormBlockSchema,
488
+ documentation: {
489
+ description: 'Creates an interactive form that can be submitted to a specified email address.',
490
+ useCases: [
491
+ 'Contact forms',
492
+ 'Lead generation forms',
493
+ 'Simple surveys',
494
+ ],
495
+ notes: [
496
+ 'Submissions are stored as message threads and appear in CMS → Messages.',
497
+ 'The destination address lives server-side in form_endpoints, keyed by form_key — it is never sent to the browser.',
498
+ 'Email notification is best-effort: a stored submission is the record even when SMTP is unconfigured.',
499
+ ],
500
+ },
501
+ },
502
+
503
+ "testimonial": {
504
+ ...TestimonialBlockConfig,
505
+ // Adapt SDK config to BlockDefinition requirements
506
+ editorComponentFilename: 'TestimonialBlockEditor', // Placeholder, not used if EditorComponent is present
507
+ rendererComponentFilename: 'TestimonialBlockRenderer', // Placeholder
508
+ documentation: {
509
+ description: 'Display a user testimonial with a quote, author, and optional image.',
510
+ useCases: ['Social proof', 'Customer reviews'],
511
+ }
512
+ } as BlockDefinition<TestimonialBlockContent>,
513
+
514
+ "product_grid": {
515
+ type: "product_grid",
516
+ label: "Product Grid",
517
+ icon: "ShoppingBag", // Lucide icon
518
+ initialContent: { type: 'latest', limit: 6, showPagination: false } as ProductGridBlockContent,
519
+ editorComponentFilename: "ProductGridBlockEditor.tsx", // Assuming standard naming
520
+ rendererComponentFilename: "ProductGridBlockRenderer.tsx", // Assuming mapping to ProductGridBlock.tsx handled by renderer map not shown here, or we need to ensure the renderer map uses the file we created. The prompt implies creating the file in `lib/blocks` serves as the component. I will assume the system maps it.
521
+ schema: ProductGridBlockSchema,
522
+ documentation: {
523
+ description: 'Displays a grid of products: the latest ones, everything in one or more categories, or a hand-picked list.',
524
+ useCases: ['Homepage featured products', 'Category pages', 'Curated collections']
525
+ }
526
+ },
527
+
528
+ "featured_product": {
529
+ type: "featured_product",
530
+ label: "Featured Product",
531
+ icon: "Star",
532
+ initialContent: { productId: '', showBackground: false } as FeaturedProductBlockContent,
533
+ editorComponentFilename: "FeaturedProductBlockEditor.tsx",
534
+ rendererComponentFilename: "FeaturedProductBlockRenderer.tsx",
535
+ schema: FeaturedProductBlockSchema,
536
+ documentation: {
537
+ description: 'Highlights a specific product with a detailed view.',
538
+ useCases: ['Product spotlight', 'Special offers']
539
+ }
540
+ },
541
+
542
+ "cart": {
543
+ type: "cart",
544
+ label: "Cart",
545
+ icon: "ShoppingCart",
546
+ initialContent: {} as CartBlockContent,
547
+ editorComponentFilename: "CartBlockEditor.tsx",
548
+ rendererComponentFilename: "CartBlockRenderer.tsx",
549
+ schema: CartBlockSchema,
550
+ documentation: {
551
+ description: 'Displays the shopping cart.',
552
+ useCases: ['Cart page']
553
+ }
554
+ },
555
+
556
+ "checkout": {
557
+ type: "checkout",
558
+ label: "Checkout",
559
+ icon: "CreditCard",
560
+ initialContent: {} as CheckoutBlockContent,
561
+ editorComponentFilename: "CheckoutBlockEditor.tsx",
562
+ rendererComponentFilename: "CheckoutBlockRenderer.tsx",
563
+ schema: CheckoutBlockSchema,
564
+ documentation: {
565
+ description: 'Displays the checkout form.',
566
+ useCases: ['Checkout page']
567
+ }
568
+ },
569
+
570
+ "product_details": {
571
+ type: "product_details",
572
+ label: "Product Details",
573
+ icon: "Tag",
574
+ initialContent: {} as ProductDetailsBlockContent,
575
+ editorComponentFilename: "ProductDetailsBlockEditor.tsx",
576
+ rendererComponentFilename: "ProductDetailsBlockRenderer.tsx",
577
+ schema: ProductDetailsBlockSchema,
578
+ documentation: {
579
+ description: 'Displays product details (Title, Price, Add to Cart) using context.',
580
+ useCases: ['Product Template Page']
581
+ }
582
+ },
583
+ };
584
+
585
+
586
+ /**
587
+ * Get the block definition for a specific block type
588
+ *
589
+ * @param blockType - The type of block to get the definition for
590
+ * @returns The block definition or undefined if not found
591
+ */
592
+ export function getBlockDefinition(blockType: string): BlockDefinition | undefined {
593
+ if (blockType in blockRegistry) {
594
+ return blockRegistry[blockType as BlockType];
595
+ }
596
+ return undefined;
597
+ }
598
+
599
+ /**
600
+ * Whether a block type has anything for an author to configure.
601
+ *
602
+ * `cart`, `checkout` and `product_details` are context-driven: they declare an
603
+ * empty schema and ship no editor file at all, so opening an editor for them
604
+ * would show an empty dialog (and fail on the missing module). Used to decide
605
+ * whether adding a block should jump straight into its editor.
606
+ *
607
+ * @param blockType - The block type or custom-block slug
608
+ * @returns true when the block exposes editable fields
609
+ */
610
+ export function blockHasEditableContent(blockType: string): boolean {
611
+ const definition = getBlockDefinition(blockType);
612
+ // Custom blocks are not in the registry; they always render a field editor.
613
+ if (!definition) return true;
614
+ const shape = (definition.schema as unknown as { shape?: Record<string, unknown> })?.shape;
615
+ if (!shape) return true;
616
+ return Object.keys(shape).length > 0;
617
+ }
618
+
619
+ /**
620
+ * Get the initial content for a specific block type
621
+ *
622
+ * @param blockType - The type of block to get initial content for
623
+ * @returns The initial content object or undefined if block type not found
624
+ */
625
+ export function getInitialContent(blockType: string): object | undefined {
626
+ if (blockType in blockRegistry) {
627
+ return blockRegistry[blockType as BlockType]?.initialContent;
628
+ }
629
+ return {};
630
+ }
631
+
632
+ /**
633
+ * Get the label for a specific block type
634
+ *
635
+ * @param blockType - The type of block to get the label for
636
+ * @returns The user-friendly label or undefined if block type not found
637
+ */
638
+ export function getBlockLabel(blockType: string): string | undefined {
639
+ if (blockType in blockRegistry) {
640
+ return blockRegistry[blockType as BlockType]?.label;
641
+ }
642
+ return blockType
643
+ .split(/[-_]+/)
644
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
645
+ .join(" ");
646
+ }
647
+
648
+ /**
649
+ * Check if a block type is valid/registered
650
+ *
651
+ * @param blockType - The block type to validate
652
+ * @returns True if the block type exists in the registry
653
+ */
654
+ export function isValidBlockType(blockType: string): boolean {
655
+ return blockType in blockRegistry || /^[a-z0-9_-]+$/.test(blockType);
656
+ }
657
+
658
+ /**
659
+ * Get the Zod schema for a specific block type
660
+ *
661
+ * @param blockType - The type of block to get the schema for
662
+ * @returns The Zod schema object or undefined if not found
663
+ */
664
+ export function getBlockSchema(blockType: string): z.ZodType<any> | undefined {
665
+ if (blockType in blockRegistry) {
666
+ return blockRegistry[blockType as BlockType]?.schema;
667
+ }
668
+ return z.record(z.string(), z.any());
669
+ }
670
+
671
+ /**
672
+ * Get documentation for a specific block type
673
+ *
674
+ * @param blockType - The type of block to get documentation for
675
+ * @returns The documentation object or undefined if not found
676
+ */
677
+ export function getBlockDocumentation(blockType: string): BlockDefinition['documentation'] | undefined {
678
+ if (blockType in blockRegistry) {
679
+ return blockRegistry[blockType as BlockType]?.documentation;
680
+ }
681
+ return {
682
+ description: "Custom user-defined block layout component",
683
+ };
684
+ }
685
+
686
+ /**
687
+ * Generate a union type for all block content types
688
+ * This creates a discriminated union based on block type
689
+ *
690
+ * @returns A TypeScript union type for all block content
691
+ */
692
+ export type AllBlockContent =
693
+ | ({ type: "text" } & TextBlockContent)
694
+ | ({ type: "heading" } & HeadingBlockContent)
695
+ | ({ type: "image" } & ImageBlockContent)
696
+ | ({ type: "button" } & ButtonBlockContent)
697
+ | ({ type: "posts_grid" } & PostsGridBlockContent)
698
+ | ({ type: "section" } & SectionBlockContent)
699
+
700
+ | ({ type: "video_embed" } & VideoEmbedBlockContent)
701
+ | ({ type: "form" } & FormBlockContent)
702
+ | ({ type: "testimonial" } & TestimonialBlockContent)
703
+ | ({ type: "product_grid" } & ProductGridBlockContent)
704
+ | ({ type: "featured_product" } & FeaturedProductBlockContent)
705
+ | ({ type: "cart" } & CartBlockContent)
706
+ | ({ type: "checkout" } & CheckoutBlockContent)
707
+ | ({ type: "product_details" } & ProductDetailsBlockContent);
708
+
709
+ /**
710
+ * Validate block content against its schema
711
+ * Performs runtime validation based on the Zod schema definitions
712
+ *
713
+ * @param blockType - The type of block to validate
714
+ * @param content - The content to validate
715
+ * @returns An object with validation results
716
+ */
717
+ export function validateBlockContent(
718
+ blockType: BlockType,
719
+ content: Record<string, any>
720
+ ): {
721
+ isValid: boolean;
722
+ errors: string[];
723
+ warnings: string[];
724
+ } {
725
+ const schema = getBlockSchema(blockType);
726
+ if (!schema) {
727
+ return { isValid: false, errors: ['Block type not found in registry'], warnings: [] };
728
+ }
729
+
730
+ const result = schema.safeParse(content);
731
+
732
+ if (result.success) {
733
+ return { isValid: true, errors: [], warnings: [] };
734
+ } else {
735
+ // Format Zod errors
736
+ const errors = result.error.issues.map(e => {
737
+ const path = e.path.join('.');
738
+ return path ? `${path}: ${e.message}` : e.message;
739
+ });
740
+ return { isValid: false, errors, warnings: [] };
741
+ }
742
+ }
743
+
744
+ /**
745
+ * Generate default content for a block type based on its schema
746
+ * This is more comprehensive than initialContent as it includes all properties with defaults
747
+ *
748
+ * @param blockType - The type of block
749
+ * @returns Complete default content object
750
+ */
751
+ export function generateDefaultContent(blockType: BlockType): Record<string, any> {
752
+ // For Zod, initialContent is the best source of defaults as defined in the registry.
753
+ // Zod schemas don't easily expose default values without parsing.
754
+ // We return initialContent which is required to be valid against the schema.
755
+ return getInitialContent(blockType) as Record<string, any> || {};
756
+ }
757
+
758
+
759
+
760
+
761
+