create-nextblock 0.2.77 → 0.8.0

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 (413) hide show
  1. package/bin/create-nextblock.js +740 -459
  2. package/package.json +1 -2
  3. package/scripts/sync-template.js +18 -1
  4. package/templates/nextblock-template/.browserslistrc +11 -0
  5. package/templates/nextblock-template/.swcrc +30 -30
  6. package/templates/nextblock-template/README.md +23 -114
  7. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -28
  8. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +50 -25
  9. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +111 -56
  10. package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -0
  11. package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +118 -0
  12. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -0
  13. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -0
  14. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +48 -28
  15. package/templates/nextblock-template/app/[slug]/page.tsx +63 -6
  16. package/templates/nextblock-template/app/[slug]/page.utils.ts +374 -157
  17. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -0
  18. package/templates/nextblock-template/app/actions/consent.ts +57 -0
  19. package/templates/nextblock-template/app/actions/formActions.ts +130 -11
  20. package/templates/nextblock-template/app/actions/languageActions.ts +31 -30
  21. package/templates/nextblock-template/app/actions/package-actions.ts +183 -0
  22. package/templates/nextblock-template/app/actions/postActions.ts +146 -48
  23. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +21 -0
  24. package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +179 -0
  25. package/templates/nextblock-template/app/actions/visualEditingActions.ts +345 -0
  26. package/templates/nextblock-template/app/actions.ts +67 -12
  27. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -0
  28. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -0
  29. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +965 -0
  30. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -0
  31. package/templates/nextblock-template/app/api/checkout/route.ts +146 -0
  32. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -0
  33. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -0
  34. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3413 -17
  35. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +7830 -0
  36. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +35 -0
  37. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -0
  38. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -0
  39. package/templates/nextblock-template/app/api/draft/disable/route.ts +25 -0
  40. package/templates/nextblock-template/app/api/draft/route.ts +93 -0
  41. package/templates/nextblock-template/app/api/draft/start/route.ts +77 -0
  42. package/templates/nextblock-template/app/api/media/library/route.ts +65 -0
  43. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -0
  44. package/templates/nextblock-template/app/api/media/record/route.ts +160 -0
  45. package/templates/nextblock-template/app/api/search/route.ts +43 -0
  46. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -0
  47. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -0
  48. package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +34 -0
  49. package/templates/nextblock-template/app/api/webhooks/stripe/route.ts +27 -0
  50. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +392 -128
  51. package/templates/nextblock-template/app/article/[slug]/page.tsx +179 -127
  52. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +262 -77
  53. package/templates/nextblock-template/app/auth/callback/route.ts +31 -58
  54. package/templates/nextblock-template/app/cart/page.tsx +7 -0
  55. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -0
  56. package/templates/nextblock-template/app/checkout/page.tsx +52 -0
  57. package/templates/nextblock-template/app/checkout/success/actions.ts +136 -0
  58. package/templates/nextblock-template/app/checkout/success/page.tsx +186 -0
  59. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +163 -33
  60. package/templates/nextblock-template/app/cms/blocks/actions.ts +424 -235
  61. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +212 -151
  62. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +41 -20
  63. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +152 -19
  64. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +25 -17
  65. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +200 -18
  66. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +33 -16
  67. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -0
  68. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +37 -18
  69. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -67
  70. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +108 -31
  71. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -0
  72. package/templates/nextblock-template/app/cms/blocks/editors/FeaturedProductBlockEditor.tsx +31 -0
  73. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +2 -2
  74. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +1 -1
  75. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
  76. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +14 -18
  77. package/templates/nextblock-template/app/cms/blocks/editors/ProductGridBlockEditor.tsx +41 -0
  78. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +318 -118
  79. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +98 -21
  80. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +1 -1
  81. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +27 -9
  82. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +1 -1
  83. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -0
  84. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -0
  85. package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +1507 -0
  86. package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +145 -0
  87. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -0
  88. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +38 -24
  89. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -0
  90. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -0
  91. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -0
  92. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -0
  93. package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -0
  94. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -0
  95. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -0
  96. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -0
  97. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -0
  98. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -0
  99. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -98
  100. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -0
  101. package/templates/nextblock-template/app/cms/dashboard/page.tsx +191 -151
  102. package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +391 -0
  103. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -0
  104. package/templates/nextblock-template/app/cms/layout.tsx +29 -10
  105. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -22
  106. package/templates/nextblock-template/app/cms/media/actions.ts +45 -124
  107. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +1 -1
  108. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  109. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -64
  110. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +227 -158
  111. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -89
  112. package/templates/nextblock-template/app/cms/media/page.tsx +1 -1
  113. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +2 -2
  114. package/templates/nextblock-template/app/cms/orders/[id]/MarkPaidButton.tsx +44 -0
  115. package/templates/nextblock-template/app/cms/orders/[id]/page.tsx +16 -0
  116. package/templates/nextblock-template/app/cms/orders/actions.ts +201 -0
  117. package/templates/nextblock-template/app/cms/orders/page.tsx +20 -0
  118. package/templates/nextblock-template/app/cms/orders/types.ts +20 -0
  119. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +156 -121
  120. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -26
  121. package/templates/nextblock-template/app/cms/pages/actions.ts +54 -38
  122. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +1 -1
  123. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +267 -116
  124. package/templates/nextblock-template/app/cms/pages/page.tsx +25 -18
  125. package/templates/nextblock-template/app/cms/payments/page.tsx +16 -0
  126. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +132 -90
  127. package/templates/nextblock-template/app/cms/posts/actions.ts +71 -72
  128. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +1 -1
  129. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +256 -245
  130. package/templates/nextblock-template/app/cms/posts/new/page.tsx +1 -1
  131. package/templates/nextblock-template/app/cms/posts/page.tsx +20 -13
  132. package/templates/nextblock-template/app/cms/products/ClientNotionEditor.tsx +16 -0
  133. package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +56 -0
  134. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +292 -0
  135. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -0
  136. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -0
  137. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -0
  138. package/templates/nextblock-template/app/cms/products/new/page.tsx +143 -0
  139. package/templates/nextblock-template/app/cms/products/page.tsx +42 -0
  140. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -0
  141. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -0
  142. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -0
  143. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -0
  144. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -0
  145. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +2 -2
  146. package/templates/nextblock-template/app/cms/revisions/actions.ts +285 -285
  147. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -16
  148. package/templates/nextblock-template/app/cms/revisions/utils.ts +8 -3
  149. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -0
  150. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -0
  151. package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -0
  152. package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -0
  153. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -0
  154. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +1 -1
  155. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +2 -2
  156. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +1 -1
  157. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +496 -0
  158. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -0
  159. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +248 -0
  160. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -0
  161. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -0
  162. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -0
  163. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -0
  164. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +203 -44
  165. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -242
  166. package/templates/nextblock-template/app/cms/settings/global-css/actions.ts +65 -0
  167. package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx +46 -0
  168. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +24 -0
  169. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +1 -1
  170. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +2 -2
  171. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +1 -1
  172. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  173. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +82 -6
  174. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -0
  175. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -18
  176. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -16
  177. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -0
  178. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  179. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +120 -82
  180. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -8
  181. package/templates/nextblock-template/app/cms/settings/packages/activation-form.tsx +84 -0
  182. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -0
  183. package/templates/nextblock-template/app/cms/settings/packages/page.tsx +49 -0
  184. package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +53 -0
  185. package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +196 -0
  186. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +26 -0
  187. package/templates/nextblock-template/app/cms/settings/security/actions.ts +251 -0
  188. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +453 -0
  189. package/templates/nextblock-template/app/cms/settings/security/page.tsx +13 -0
  190. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -0
  191. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -0
  192. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -23
  193. package/templates/nextblock-template/app/cms/users/actions.ts +105 -40
  194. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +1 -1
  195. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +65 -152
  196. package/templates/nextblock-template/app/cms/users/page.tsx +15 -10
  197. package/templates/nextblock-template/app/globals.css +9 -0
  198. package/templates/nextblock-template/app/layout.tsx +372 -116
  199. package/templates/nextblock-template/app/lib/seo.test.ts +52 -0
  200. package/templates/nextblock-template/app/lib/seo.ts +279 -0
  201. package/templates/nextblock-template/app/lib/site-settings.ts +87 -0
  202. package/templates/nextblock-template/app/lib/sitemap-utils.ts +224 -39
  203. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -0
  204. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -0
  205. package/templates/nextblock-template/app/lib/ucp/server.ts +1914 -0
  206. package/templates/nextblock-template/app/page.tsx +165 -73
  207. package/templates/nextblock-template/app/product/[slug]/page.tsx +433 -0
  208. package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -0
  209. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -0
  210. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -0
  211. package/templates/nextblock-template/app/profile/account-data.ts +37 -0
  212. package/templates/nextblock-template/app/profile/account-links.ts +22 -0
  213. package/templates/nextblock-template/app/profile/account-types.ts +11 -0
  214. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -0
  215. package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -0
  216. package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -0
  217. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -0
  218. package/templates/nextblock-template/app/profile/page.tsx +51 -0
  219. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -0
  220. package/templates/nextblock-template/app/profile/password/actions.ts +59 -0
  221. package/templates/nextblock-template/app/profile/password/page.tsx +27 -0
  222. package/templates/nextblock-template/app/providers.tsx +55 -17
  223. package/templates/nextblock-template/app/robots.txt/route.ts +11 -1
  224. package/templates/nextblock-template/app/sitemap.ts +128 -0
  225. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -0
  226. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -0
  227. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -0
  228. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -0
  229. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -0
  230. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -0
  231. package/templates/nextblock-template/components/AppShell.tsx +154 -0
  232. package/templates/nextblock-template/components/BlockRenderer.tsx +210 -64
  233. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -0
  234. package/templates/nextblock-template/components/CartTranslator.tsx +210 -0
  235. package/templates/nextblock-template/components/CurrentContentSetter.tsx +25 -0
  236. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -0
  237. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -0
  238. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -0
  239. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -0
  240. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -0
  241. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -0
  242. package/templates/nextblock-template/components/GitHubLoginButton.tsx +36 -0
  243. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -0
  244. package/templates/nextblock-template/components/Header.tsx +49 -41
  245. package/templates/nextblock-template/components/LanguageSwitcher.tsx +55 -32
  246. package/templates/nextblock-template/components/ResponsiveNav.tsx +138 -43
  247. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -8
  248. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -55
  249. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +42 -37
  250. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +6 -2
  251. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -0
  252. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -0
  253. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -84
  254. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +17 -0
  255. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +19 -0
  256. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +262 -8
  257. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +22 -0
  258. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +320 -37
  259. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +11 -8
  260. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +12 -3
  261. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +18 -13
  262. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +90 -0
  263. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +31 -0
  264. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +424 -55
  265. package/templates/nextblock-template/components/blocks/renderers/SectionSlider.tsx +137 -0
  266. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -0
  267. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +37 -22
  268. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +23 -15
  269. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +1 -3
  270. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +1 -3
  271. package/templates/nextblock-template/components/blocks/types.ts +7 -6
  272. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  273. package/templates/nextblock-template/components/form-message.tsx +32 -26
  274. package/templates/nextblock-template/components/header-auth.tsx +69 -17
  275. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +127 -0
  276. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +59 -0
  277. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -0
  278. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -0
  279. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -0
  280. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  281. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -0
  282. package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +1172 -0
  283. package/templates/nextblock-template/context/AuthContext.tsx +23 -90
  284. package/templates/nextblock-template/context/CurrentContentContext.tsx +10 -4
  285. package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
  286. package/templates/nextblock-template/context/language-rest-client.ts +31 -0
  287. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -0
  288. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -0
  289. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -0
  290. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +252 -0
  291. package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +238 -0
  292. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +125 -0
  293. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -0
  294. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -0
  295. package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +104 -0
  296. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -0
  297. package/templates/nextblock-template/docs/README.md +34 -0
  298. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12507 -0
  299. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -14
  300. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -0
  301. package/templates/nextblock-template/index.d.ts +2 -0
  302. package/templates/nextblock-template/lib/ai-block-generation.ts +339 -0
  303. package/templates/nextblock-template/lib/ai-client.ts +247 -0
  304. package/templates/nextblock-template/lib/ai-config.ts +81 -0
  305. package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +125 -0
  306. package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +363 -0
  307. package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +405 -0
  308. package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +1228 -0
  309. package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +5 -0
  310. package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +223 -0
  311. package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +2183 -0
  312. package/templates/nextblock-template/lib/ai-global-agent-tools.ts +4807 -0
  313. package/templates/nextblock-template/lib/ai-key-crypto.test.ts +70 -0
  314. package/templates/nextblock-template/lib/ai-key-crypto.ts +132 -0
  315. package/templates/nextblock-template/lib/ai-model-catalog.test.ts +49 -0
  316. package/templates/nextblock-template/lib/ai-model-catalog.ts +41 -0
  317. package/templates/nextblock-template/lib/ai-model-registry.test.ts +231 -0
  318. package/templates/nextblock-template/lib/ai-model-registry.ts +522 -0
  319. package/templates/nextblock-template/lib/auth/cookies.ts +47 -0
  320. package/templates/nextblock-template/lib/auth/crypto.ts +42 -0
  321. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -0
  322. package/templates/nextblock-template/lib/auth/twoFactor.ts +167 -0
  323. package/templates/nextblock-template/lib/auth-redirects.ts +46 -0
  324. package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +94 -0
  325. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +137 -0
  326. package/templates/nextblock-template/lib/blocks/README.md +13 -670
  327. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +138 -56
  328. package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -0
  329. package/templates/nextblock-template/lib/blocks/ecommerce-block-schemas.ts +31 -0
  330. package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +77 -0
  331. package/templates/nextblock-template/lib/cms-transfer/csv.ts +399 -0
  332. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -0
  333. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -0
  334. package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +199 -0
  335. package/templates/nextblock-template/lib/cortex-widget-registry.ts +88 -0
  336. package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +237 -0
  337. package/templates/nextblock-template/lib/cortex-widget-schema.ts +393 -0
  338. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -0
  339. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -0
  340. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -0
  341. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +68 -0
  342. package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -0
  343. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -0
  344. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -0
  345. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -0
  346. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -0
  347. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -0
  348. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -0
  349. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -0
  350. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -0
  351. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -0
  352. package/templates/nextblock-template/lib/full-backup/server.ts +743 -0
  353. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +45 -0
  354. package/templates/nextblock-template/lib/posts/readTime.ts +60 -0
  355. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -0
  356. package/templates/nextblock-template/lib/privacy/settings.ts +103 -0
  357. package/templates/nextblock-template/lib/privacy/types.ts +67 -0
  358. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -0
  359. package/templates/nextblock-template/lib/promotions/server.ts +741 -0
  360. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -0
  361. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -0
  362. package/templates/nextblock-template/lib/search/server.ts +585 -0
  363. package/templates/nextblock-template/lib/search/types.ts +27 -0
  364. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -0
  365. package/templates/nextblock-template/lib/visual-editing/draft-content.ts +380 -0
  366. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -0
  367. package/templates/nextblock-template/lib/visual-editing/draft-route.ts +82 -0
  368. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -0
  369. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -0
  370. package/templates/nextblock-template/lib/visual-editing/mutations.ts +190 -0
  371. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -0
  372. package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +511 -0
  373. package/templates/nextblock-template/lib/visual-editing/types.ts +122 -0
  374. package/templates/nextblock-template/lib/zod-config.ts +5 -0
  375. package/templates/nextblock-template/next.config.js +190 -66
  376. package/templates/nextblock-template/package.json +34 -30
  377. package/templates/nextblock-template/proxy.ts +435 -253
  378. package/templates/nextblock-template/public/images/NBcover.webp +0 -0
  379. package/templates/nextblock-template/public/images/cap.webp +0 -0
  380. package/templates/nextblock-template/public/images/commerce-plan.webp +0 -0
  381. package/templates/nextblock-template/public/images/commerce-square.webp +0 -0
  382. package/templates/nextblock-template/public/images/commerce-wide.webp +0 -0
  383. package/templates/nextblock-template/public/images/cortex-ai-square.webp +0 -0
  384. package/templates/nextblock-template/public/images/cortex-ai.webp +0 -0
  385. package/templates/nextblock-template/public/images/extensibility.webp +0 -0
  386. package/templates/nextblock-template/public/images/goals.webp +0 -0
  387. package/templates/nextblock-template/public/images/included.webp +0 -0
  388. package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
  389. package/templates/nextblock-template/public/images/pants.webp +0 -0
  390. package/templates/nextblock-template/public/images/t-shirt.webp +0 -0
  391. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -0
  392. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +100 -0
  393. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -0
  394. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -0
  395. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -0
  396. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -0
  397. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -0
  398. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -0
  399. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -0
  400. package/templates/nextblock-template/tailwind.config.js +1 -0
  401. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -0
  402. package/templates/nextblock-template/tools/deploy-supabase.js +69 -71
  403. package/templates/nextblock-template/tsconfig.json +52 -66
  404. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  405. package/templates/nextblock-template/types/jsdom.d.ts +6 -0
  406. package/templates/nextblock-template/app/force-styles.tsx +0 -31
  407. package/templates/nextblock-template/app/sitemap.xml/route.ts +0 -63
  408. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +0 -273
  409. package/templates/nextblock-template/docs/How to Create a Custom Block.md +0 -149
  410. package/templates/nextblock-template/docs/cms-application-overview.md +0 -56
  411. package/templates/nextblock-template/docs/cms-architecture-overview.md +0 -73
  412. package/templates/nextblock-template/docs/files-structure.md +0 -426
  413. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +0 -174
@@ -0,0 +1,1522 @@
1
+ "use client";
2
+
3
+ import React, { useState, useEffect, useMemo, useTransition } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { toast } from "react-hot-toast";
6
+ import {
7
+ Boxes,
8
+ Plus,
9
+ Trash2,
10
+ Settings,
11
+ Database,
12
+ ImageIcon,
13
+ Type,
14
+ Code,
15
+ ArrowLeft,
16
+ Save,
17
+ PlusCircle,
18
+ Eye,
19
+ ListTree,
20
+ ChevronDown,
21
+ ChevronRight,
22
+ Info,
23
+ FolderOpen,
24
+ Sparkles,
25
+ Layers,
26
+ Loader2,
27
+ GripVertical,
28
+ BookOpen,
29
+ } from "lucide-react";
30
+ import {
31
+ Button,
32
+ Card,
33
+ CardHeader,
34
+ CardTitle,
35
+ CardDescription,
36
+ CardContent,
37
+ CardFooter,
38
+ Badge,
39
+ Input,
40
+ Label,
41
+ Textarea,
42
+ Checkbox,
43
+ ConfirmationDialog,
44
+ Dialog,
45
+ DialogTrigger,
46
+ DialogContent,
47
+ DialogHeader,
48
+ DialogTitle,
49
+ DialogDescription,
50
+ } from "@nextblock-cms/ui";
51
+ import { DynamicLayoutEngine } from "../../../../components/renderers/DynamicLayoutEngine";
52
+ import { ImageR2Picker } from "./ImageR2Picker";
53
+ import { DBRelationSelect } from "./DBRelationSelect";
54
+ import {
55
+ createCustomBlockDefinition,
56
+ updateCustomBlockDefinition,
57
+ } from "../actions";
58
+ import { orderCustomBlockFieldsByLayout } from "@nextblock-cms/utils";
59
+ import type { CustomBlockDefinition, CustomBlockField } from "@nextblock-cms/utils";
60
+
61
+ // Allowed container and field tags
62
+ const CONTAINER_TAGS = ["div", "section", "article", "blockquote", "figure", "figcaption", "h2", "h3", "p", "span"];
63
+ const FIELD_TAGS = ["div", "span", "p", "blockquote", "img", "h2", "h3", "figcaption"];
64
+
65
+ const getFieldIcon = (type: string) => {
66
+ switch (type) {
67
+ case "text":
68
+ return <Type className="h-3 w-3 text-sky-500 shrink-0" />;
69
+ case "rich-text":
70
+ return <Code className="h-3 w-3 text-emerald-500 shrink-0" />;
71
+ case "image_r2":
72
+ return <ImageIcon className="h-3 w-3 text-amber-500 shrink-0" />;
73
+ case "db_relation":
74
+ return <Database className="h-3 w-3 text-violet-500 shrink-0" />;
75
+ default:
76
+ return <Info className="h-3 w-3 text-slate-500 shrink-0" />;
77
+ }
78
+ };
79
+
80
+ interface BlockComposerProps {
81
+ initialData?: CustomBlockDefinition;
82
+ mode: "create" | "edit";
83
+ }
84
+
85
+ interface RelationTableTarget {
86
+ table: string;
87
+ label: string;
88
+ displayColumn: string;
89
+ valueColumn: string;
90
+ valueType: string;
91
+ selectColumns?: string[];
92
+ }
93
+
94
+ export function BlockComposer({ initialData, mode }: BlockComposerProps) {
95
+ const router = useRouter();
96
+ const [isPending, startTransition] = useTransition();
97
+
98
+ // General Block Info
99
+ const [name, setName] = useState(initialData?.name || "");
100
+ const [slug, setSlug] = useState(initialData?.slug || "");
101
+ const [description, setDescription] = useState(initialData?.description || "");
102
+ // Provenance flag: true for newly authored blocks, preserved on edit, set to
103
+ // false automatically by the duplicate action. No longer user-editable.
104
+ const [isOriginal] = useState(initialData?.is_original !== false);
105
+
106
+ // Schema state
107
+ const [fields, setFields] = useState<CustomBlockField[]>(initialData?.fields || []);
108
+ const [layoutSchema, setLayoutSchema] = useState<any>(
109
+ initialData?.layout_schema || {
110
+ type: "container",
111
+ as: "div",
112
+ className: "rounded-xl border bg-card p-6 shadow-sm flex flex-col gap-4",
113
+ children: [],
114
+ }
115
+ );
116
+
117
+ // Active designer view states
118
+ const [activeTab, setActiveTab] = useState<"general" | "fields" | "layout">("general");
119
+ const [relationTables, setRelationTables] = useState<RelationTableTarget[]>([]);
120
+ const [selectedNodePath, setSelectedNodePath] = useState<number[] | null>(null);
121
+
122
+ // Drag and drop state
123
+ const [draggedPath, setDraggedPath] = useState<number[] | null>(null);
124
+ const [dragOverInfo, setDragOverInfo] = useState<{ path: number[]; position: "before" | "after" | "inside" } | null>(null);
125
+
126
+ // Real-time mockup values for preview form
127
+ const [previewValues, setPreviewValues] = useState<Record<string, any>>({});
128
+ const [mockRelationRecords, setMockRelationRecords] = useState<Record<string, any>>({});
129
+
130
+ // Tree helper expanded state
131
+ const [expandedPaths, setExpandedPaths] = useState<Record<string, boolean>>({ "[]": true });
132
+
133
+ // Fetch available table relations on load
134
+ useEffect(() => {
135
+ const fetchRelations = async () => {
136
+ try {
137
+ const res = await fetch("/api/custom-blocks/db-relations?mode=tables");
138
+ if (res.ok) {
139
+ const data = await res.json();
140
+ if (data && data.tables) {
141
+ setRelationTables(data.tables);
142
+ }
143
+ }
144
+ } catch (err) {
145
+ console.error("Error loading relation target tables:", err);
146
+ }
147
+ };
148
+ fetchRelations();
149
+ }, []);
150
+
151
+ // Sync previews whenever fields list changes
152
+ useEffect(() => {
153
+ const freshMock: Record<string, any> = { ...previewValues };
154
+ const freshRelations: Record<string, any> = { ...mockRelationRecords };
155
+
156
+ fields.forEach((f) => {
157
+ if (freshMock[f.key] === undefined) {
158
+ if (f.type === "text") freshMock[f.key] = `Mock text value for ${f.key}`;
159
+ if (f.type === "rich-text") freshMock[f.key] = `<p>Mock <strong>Rich Text</strong> content for ${f.key}</p>`;
160
+ if (f.type === "image_r2") {
161
+ freshMock[f.key] = {
162
+ url: "/images/commerce-square.webp",
163
+ object_key: "images/commerce-square.webp",
164
+ alt: "Sample product image",
165
+ width: 400,
166
+ height: 400,
167
+ };
168
+ }
169
+ if (f.type === "db_relation") {
170
+ freshMock[f.key] = "mock-id-1";
171
+
172
+ const mockRecord: Record<string, any> = {
173
+ id: "mock-id-1",
174
+ title: `Mock ${f.table.charAt(0).toUpperCase() + f.table.slice(1)} Title`,
175
+ name: `Mock ${f.table.charAt(0).toUpperCase() + f.table.slice(1)} Name`,
176
+ full_name: "Mock User Full Name",
177
+ sku: "MOCK-SKU-100",
178
+ short_description: "This is a short descriptive blurb for the preview card layout.",
179
+ file_name: "placeholder-file.png",
180
+ };
181
+
182
+ // Attach realistic image URLs for tables that support images
183
+ if (f.table === "media") {
184
+ mockRecord.object_key = "https://images.unsplash.com/photo-1579546929518-9e396f3cc809?w=400&h=300&fit=crop";
185
+ mockRecord.file_name = "visual-banner.webp";
186
+ } else if (f.table === "profiles") {
187
+ mockRecord.avatar_url = "https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120&h=120&fit=crop";
188
+ mockRecord.full_name = "Clara Dupont";
189
+ } else if (f.table === "products") {
190
+ mockRecord.avatar_url = "https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=400&h=300&fit=crop";
191
+ mockRecord.object_key = "https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=400&h=300&fit=crop";
192
+ mockRecord.title = "Premium Wireless Headphones";
193
+ mockRecord.sku = "HP-WIRELESS-100";
194
+ mockRecord.price = 19900;
195
+ mockRecord.stock = 15;
196
+ mockRecord.short_description = "Active noise cancelling with 40-hour battery life.";
197
+ } else if (f.table === "product_variants") {
198
+ mockRecord.avatar_url = "https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=400&h=300&fit=crop";
199
+ mockRecord.object_key = "https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=400&h=300&fit=crop";
200
+ mockRecord.sku = "MOCK-VAR-RED-L";
201
+ mockRecord.price = 12900;
202
+ mockRecord.stock_quantity = 42;
203
+ } else if (f.table === "posts" || f.table === "pages") {
204
+ mockRecord.feature_image_id = "mock-image-id";
205
+ mockRecord.avatar_url = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=300&fit=crop";
206
+ mockRecord.object_key = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=300&fit=crop";
207
+ }
208
+
209
+ freshRelations[f.key] = {
210
+ record: mockRecord,
211
+ table: f.table,
212
+ value: "mock-id-1",
213
+ };
214
+ }
215
+ }
216
+ });
217
+ setPreviewValues(freshMock);
218
+ setMockRelationRecords(freshRelations);
219
+ }, [fields]);
220
+
221
+ // Handle Slug generation from Name in create mode
222
+ const handleNameChange = (val: string) => {
223
+ setName(val);
224
+ if (mode === "create") {
225
+ setSlug(
226
+ val
227
+ .toLowerCase()
228
+ .replace(/[^a-z0-9]+/g, "-")
229
+ .replace(/(^-|-$)+/g, "")
230
+ );
231
+ }
232
+ };
233
+
234
+ // Pre-generate standard blueprint layout
235
+ const handleGenerateDefaultLayout = () => {
236
+ if (fields.length === 0) {
237
+ toast.error("Please add at least one field first.");
238
+ return;
239
+ }
240
+ const defaultLayout = {
241
+ type: "container",
242
+ as: "div",
243
+ className: "rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-6 flex flex-col gap-4 shadow-sm",
244
+ children: fields.map((f) => {
245
+ if (f.type === "image_r2") {
246
+ return {
247
+ type: "field_render",
248
+ field_key: f.key,
249
+ as: "img",
250
+ className: "h-24 w-24 rounded-full object-cover shadow-sm",
251
+ };
252
+ }
253
+ return {
254
+ type: "field_render",
255
+ field_key: f.key,
256
+ as: f.type === "rich-text" ? "div" : "p",
257
+ className: f.type === "rich-text"
258
+ ? "prose prose-sm max-w-none text-muted-foreground"
259
+ : f.type === "db_relation"
260
+ ? "text-xs font-semibold uppercase tracking-wide bg-muted px-2 py-0.5 rounded text-muted-foreground w-fit"
261
+ : "text-slate-800 dark:text-slate-200 text-sm font-medium",
262
+ emptyFallback: `<p>Empty ${f.label}</p>`,
263
+ };
264
+ }),
265
+ };
266
+ setLayoutSchema(defaultLayout);
267
+ setSelectedNodePath(null);
268
+ toast.success("Generated layout blueprint based on your fields.");
269
+ };
270
+
271
+ // Add a field
272
+ const addField = () => {
273
+ const key = `field_${fields.length + 1}`;
274
+ const newField: CustomBlockField = {
275
+ key,
276
+ label: `Field ${fields.length + 1}`,
277
+ type: "text",
278
+ required: false,
279
+ };
280
+ setFields([...fields, newField]);
281
+ };
282
+
283
+ // Rename every field_render reference in the layout tree from one key to another
284
+ const renameLayoutFieldKey = (node: any, fromKey: string, toKey: string): any => {
285
+ if (!node || typeof node !== "object") return node;
286
+ if (node.type === "field_render") {
287
+ return node.field_key === fromKey ? { ...node, field_key: toKey } : node;
288
+ }
289
+ if (node.type === "container" && Array.isArray(node.children)) {
290
+ return { ...node, children: node.children.map((child: any) => renameLayoutFieldKey(child, fromKey, toKey)) };
291
+ }
292
+ return node;
293
+ };
294
+
295
+ // Migrate a record keyed by field key when that field is renamed
296
+ const migrateKey = <T,>(record: Record<string, T>, fromKey: string, toKey: string): Record<string, T> => {
297
+ if (!(fromKey in record) || fromKey === toKey) return record;
298
+ const next = { ...record };
299
+ next[toKey] = next[fromKey];
300
+ delete next[fromKey];
301
+ return next;
302
+ };
303
+
304
+ // Update field parameters
305
+ const updateField = (index: number, updated: Partial<CustomBlockField>) => {
306
+ const list = [...fields];
307
+ const previousKey = list[index].key;
308
+
309
+ // Auto populate defaults for db_relation
310
+ if (updated.type === "db_relation" && list[index].type !== "db_relation") {
311
+ const defaultTable = relationTables[0]?.table || "pages";
312
+ const targetTable = relationTables.find(t => t.table === defaultTable);
313
+ updated.table = defaultTable;
314
+ updated.display_column = targetTable?.displayColumn || "title";
315
+ updated.value_column = "id";
316
+ updated.multiple = false;
317
+ }
318
+
319
+ list[index] = { ...list[index], ...updated } as CustomBlockField;
320
+ setFields(list);
321
+
322
+ // Keep layout references and preview maps in sync when a field key changes,
323
+ // otherwise the layout would reference an unknown field and saving would fail.
324
+ if (updated.key !== undefined && updated.key !== previousKey) {
325
+ const nextKey = updated.key;
326
+ setLayoutSchema((prev: any) => renameLayoutFieldKey(prev, previousKey, nextKey));
327
+ setPreviewValues((prev) => migrateKey(prev, previousKey, nextKey));
328
+ setMockRelationRecords((prev) => migrateKey(prev, previousKey, nextKey));
329
+ }
330
+ };
331
+
332
+ // Delete a field
333
+ const deleteField = (index: number) => {
334
+ const fieldKey = fields[index].key;
335
+ const list = [...fields];
336
+ list.splice(index, 1);
337
+ setFields(list);
338
+
339
+ // Filter previews
340
+ const freshPreviews = { ...previewValues };
341
+ delete freshPreviews[fieldKey];
342
+ setPreviewValues(freshPreviews);
343
+ };
344
+
345
+ // --- Layout Tree Node Editing Functions ---
346
+ const getLayoutNodeByPath = (root: any, path: number[]): any => {
347
+ let current = root;
348
+ for (const idx of path) {
349
+ if (current?.children && current.children[idx]) {
350
+ current = current.children[idx];
351
+ } else {
352
+ return null;
353
+ }
354
+ }
355
+ return current;
356
+ };
357
+
358
+ const modifyLayoutTree = (
359
+ node: any,
360
+ path: number[],
361
+ action: "update" | "delete" | "insert",
362
+ payload?: any
363
+ ): any => {
364
+ if (path.length === 0) {
365
+ if (action === "update") {
366
+ return { ...node, ...payload };
367
+ }
368
+ if (action === "insert") {
369
+ return {
370
+ ...node,
371
+ children: [...(node.children || []), payload],
372
+ };
373
+ }
374
+ return node;
375
+ }
376
+
377
+ const [head, ...tail] = path;
378
+ if (node.type === "container" && node.children && node.children[head] !== undefined) {
379
+ if (tail.length === 0 && action === "delete") {
380
+ const newChildren = [...node.children];
381
+ newChildren.splice(head, 1);
382
+ return { ...node, children: newChildren };
383
+ }
384
+
385
+ const newChildren = [...node.children];
386
+ newChildren[head] = modifyLayoutTree(node.children[head], tail, action, payload);
387
+ return { ...node, children: newChildren };
388
+ }
389
+ return node;
390
+ };
391
+
392
+ const handleUpdateSelectedNode = (payload: any) => {
393
+ if (!selectedNodePath) return;
394
+ const updated = modifyLayoutTree(layoutSchema, selectedNodePath, "update", payload);
395
+ setLayoutSchema(updated);
396
+ };
397
+
398
+ const handleDeleteNode = (path: number[]) => {
399
+ const updated = modifyLayoutTree(layoutSchema, path, "delete");
400
+ setLayoutSchema(updated);
401
+ setSelectedNodePath(null);
402
+ toast.success("Removed layout node.");
403
+ };
404
+
405
+ const handleInsertNode = (path: number[], type: "container" | "field_render") => {
406
+ const newNode =
407
+ type === "container"
408
+ ? {
409
+ type: "container",
410
+ as: "div",
411
+ className: "flex flex-col gap-2 p-2",
412
+ children: [],
413
+ }
414
+ : {
415
+ type: "field_render",
416
+ field_key: fields[0]?.key || "",
417
+ as: "span",
418
+ className: "text-sm",
419
+ };
420
+
421
+ const updated = modifyLayoutTree(layoutSchema, path, "insert", newNode);
422
+ setLayoutSchema(updated);
423
+
424
+ // Autoexpand the path
425
+ const pathKey = JSON.stringify(path);
426
+ setExpandedPaths({ ...expandedPaths, [pathKey]: true });
427
+ toast.success(`Inserted new ${type === "container" ? "Container" : "Field Render"}.`);
428
+ };
429
+
430
+ // --- Drag & Drop helpers ---
431
+ const isDescendantOrSelf = (parent: number[], child: number[]): boolean => {
432
+ if (child.length < parent.length) return false;
433
+ for (let i = 0; i < parent.length; i++) {
434
+ if (parent[i] !== child[i]) return false;
435
+ }
436
+ return true;
437
+ };
438
+
439
+ const adjustPathAfterRemoval = (source: number[], target: number[]): number[] => {
440
+ let i = 0;
441
+ while (i < source.length && i < target.length) {
442
+ if (source[i] !== target[i]) {
443
+ if (source[i] < target[i]) {
444
+ const adjusted = [...target];
445
+ adjusted[i] = adjusted[i] - 1;
446
+ return adjusted;
447
+ }
448
+ break;
449
+ }
450
+ i++;
451
+ }
452
+ return target;
453
+ };
454
+
455
+ const removeNodeFromTree = (root: any, path: number[]): { newRoot: any; removedNode: any } => {
456
+ const getNode = (curr: any, pathTail: number[]): any => {
457
+ let node = curr;
458
+ for (const idx of pathTail) {
459
+ node = node.children[idx];
460
+ }
461
+ return node;
462
+ };
463
+
464
+ const nodeToRemove = getNode(root, path);
465
+ const clonedNode = JSON.parse(JSON.stringify(nodeToRemove));
466
+
467
+ const remove = (node: any, pathTail: number[]): any => {
468
+ if (pathTail.length === 1) {
469
+ const idx = pathTail[0];
470
+ const newChildren = [...node.children];
471
+ newChildren.splice(idx, 1);
472
+ return { ...node, children: newChildren };
473
+ }
474
+ const [head, ...tail] = pathTail;
475
+ const newChildren = [...node.children];
476
+ newChildren[head] = remove(node.children[head], tail);
477
+ return { ...node, children: newChildren };
478
+ };
479
+
480
+ if (path.length === 0) return { newRoot: null, removedNode: clonedNode };
481
+ return { newRoot: remove(root, path), removedNode: clonedNode };
482
+ };
483
+
484
+ const insertNodeIntoTree = (root: any, path: number[], nodeToInsert: any, position: "before" | "after" | "inside"): any => {
485
+ const insert = (node: any, pathTail: number[]): any => {
486
+ if (pathTail.length === 0) {
487
+ if (position === "inside") {
488
+ return { ...node, children: [...(node.children || []), nodeToInsert] };
489
+ }
490
+ return node;
491
+ }
492
+
493
+ if (pathTail.length === 1) {
494
+ const idx = pathTail[0];
495
+ if (position === "inside") {
496
+ const newChildren = [...node.children];
497
+ const targetNode = newChildren[idx];
498
+ newChildren[idx] = { ...targetNode, children: [...(targetNode.children || []), nodeToInsert] };
499
+ return { ...node, children: newChildren };
500
+ } else {
501
+ const newChildren = [...node.children];
502
+ const insertIdx = position === "before" ? idx : idx + 1;
503
+ newChildren.splice(insertIdx, 0, nodeToInsert);
504
+ return { ...node, children: newChildren };
505
+ }
506
+ }
507
+
508
+ const [head, ...tail] = pathTail;
509
+ const newChildren = [...node.children];
510
+ newChildren[head] = insert(node.children[head], tail);
511
+ return { ...node, children: newChildren };
512
+ };
513
+
514
+ return insert(root, path);
515
+ };
516
+
517
+ const handleMoveNode = (source: number[], target: number[], position: "before" | "after" | "inside") => {
518
+ if (isDescendantOrSelf(source, target)) {
519
+ toast.error("Cannot move a parent node inside or relative to its own descendants.");
520
+ return;
521
+ }
522
+
523
+ const { newRoot, removedNode } = removeNodeFromTree(layoutSchema, source);
524
+ if (!newRoot) return;
525
+
526
+ const adjustedTarget = adjustPathAfterRemoval(source, target);
527
+ const updatedSchema = insertNodeIntoTree(newRoot, adjustedTarget, removedNode, position);
528
+ setLayoutSchema(updatedSchema);
529
+ setSelectedNodePath(null);
530
+ toast.success("Rearranged layout blueprint nodes.");
531
+ };
532
+
533
+ // --- Save / Submit Block ---
534
+ const handleSaveBlock = async () => {
535
+ if (!name.trim()) {
536
+ toast.error("Please provide a block name.");
537
+ return;
538
+ }
539
+ if (!slug.trim()) {
540
+ toast.error("Please provide a block slug.");
541
+ return;
542
+ }
543
+ if (fields.length === 0) {
544
+ toast.error("Block must contain at least one field.");
545
+ return;
546
+ }
547
+
548
+ const payload = {
549
+ name,
550
+ slug,
551
+ description: description || "",
552
+ is_original: isOriginal,
553
+ fields,
554
+ layout_schema: layoutSchema,
555
+ };
556
+
557
+ const toastId = toast.loading(`${mode === "create" ? "Creating" : "Saving"} custom block...`);
558
+ try {
559
+ let res;
560
+ if (mode === "create") {
561
+ res = await createCustomBlockDefinition(payload);
562
+ } else {
563
+ res = await updateCustomBlockDefinition(initialData!.id, payload);
564
+ }
565
+
566
+ if (res.success) {
567
+ toast.success(`Block "${name}" saved successfully!`, { id: toastId });
568
+ startTransition(() => {
569
+ router.push("/cms/custom-blocks");
570
+ router.refresh();
571
+ });
572
+ } else {
573
+ const detail =
574
+ res.issues && res.issues.length > 0
575
+ ? `: ${res.issues.slice(0, 3).join("; ")}`
576
+ : "";
577
+ toast.error(`${res.error || "Failed to save block definition."}${detail}`, {
578
+ id: toastId,
579
+ });
580
+ }
581
+ } catch (err) {
582
+ console.error(err);
583
+ toast.error("An unexpected error occurred while saving.", { id: toastId });
584
+ }
585
+ };
586
+
587
+ // --- Rendering helper for Visual Layout Tree ---
588
+ const renderTreeItem = (node: any, path: number[] = []): React.ReactNode => {
589
+ const isSelected = selectedNodePath && JSON.stringify(selectedNodePath) === JSON.stringify(path);
590
+ const pathKey = JSON.stringify(path);
591
+ const isExpanded = expandedPaths[pathKey] !== false;
592
+
593
+ const toggleExpand = (e: React.MouseEvent) => {
594
+ e.stopPropagation();
595
+ setExpandedPaths({
596
+ ...expandedPaths,
597
+ [pathKey]: !isExpanded,
598
+ });
599
+ };
600
+
601
+ const nodeName =
602
+ node.type === "container"
603
+ ? `Container (${node.as || "div"})`
604
+ : `Render Field: ${node.field_key || "unmapped"}`;
605
+
606
+ return (
607
+ <div key={pathKey} className="ml-4 pl-2 border-l border-slate-100 dark:border-slate-800 space-y-1 mt-1">
608
+ <div
609
+ draggable={true}
610
+ onDragStart={(e) => {
611
+ e.stopPropagation();
612
+ setDraggedPath(path);
613
+ e.dataTransfer.effectAllowed = "move";
614
+ e.dataTransfer.setData("text/plain", JSON.stringify(path));
615
+ }}
616
+ onDragOver={(e) => {
617
+ if (!draggedPath) return;
618
+ e.preventDefault();
619
+ e.stopPropagation();
620
+
621
+ const rect = e.currentTarget.getBoundingClientRect();
622
+ const relativeY = e.clientY - rect.top;
623
+ const height = rect.height;
624
+
625
+ let position: "before" | "after" | "inside";
626
+ if (node.type === "container") {
627
+ if (relativeY < height * 0.25) {
628
+ position = "before";
629
+ } else if (relativeY > height * 0.75) {
630
+ position = "after";
631
+ } else {
632
+ position = "inside";
633
+ }
634
+ } else {
635
+ if (relativeY < height * 0.5) {
636
+ position = "before";
637
+ } else {
638
+ position = "after";
639
+ }
640
+ }
641
+
642
+ if (JSON.stringify(draggedPath) !== JSON.stringify(path)) {
643
+ setDragOverInfo({ path, position });
644
+ }
645
+ }}
646
+ onDragLeave={() => {
647
+ setDragOverInfo(null);
648
+ }}
649
+ onDrop={(e) => {
650
+ e.preventDefault();
651
+ e.stopPropagation();
652
+ if (!draggedPath) return;
653
+
654
+ const rect = e.currentTarget.getBoundingClientRect();
655
+ const relativeY = e.clientY - rect.top;
656
+ const height = rect.height;
657
+
658
+ let position: "before" | "after" | "inside";
659
+ if (node.type === "container") {
660
+ if (relativeY < height * 0.25) {
661
+ position = "before";
662
+ } else if (relativeY > height * 0.75) {
663
+ position = "after";
664
+ } else {
665
+ position = "inside";
666
+ }
667
+ } else {
668
+ if (relativeY < height * 0.5) {
669
+ position = "before";
670
+ } else {
671
+ position = "after";
672
+ }
673
+ }
674
+
675
+ if (JSON.stringify(draggedPath) !== JSON.stringify(path)) {
676
+ handleMoveNode(draggedPath, path, position);
677
+ }
678
+
679
+ setDraggedPath(null);
680
+ setDragOverInfo(null);
681
+ }}
682
+ onDragEnd={() => {
683
+ setDraggedPath(null);
684
+ setDragOverInfo(null);
685
+ }}
686
+ onClick={() => setSelectedNodePath(path)}
687
+ className={`flex items-center justify-between p-2 rounded-lg text-xs font-medium cursor-grab active:cursor-grabbing transition-all group ${
688
+ isSelected
689
+ ? "bg-primary/10 text-primary border border-primary/20"
690
+ : "hover:bg-slate-50 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-300 border border-transparent"
691
+ } ${
692
+ dragOverInfo && JSON.stringify(dragOverInfo.path) === JSON.stringify(path)
693
+ ? dragOverInfo.position === "before"
694
+ ? "border-t-2 border-primary scale-[1.01] bg-primary/5"
695
+ : dragOverInfo.position === "after"
696
+ ? "border-b-2 border-primary scale-[1.01] bg-primary/5"
697
+ : "bg-primary/20 border border-primary/40 scale-[1.02]"
698
+ : ""
699
+ }`}
700
+ >
701
+ <div className="flex items-center gap-2 min-w-0">
702
+ <GripVertical className="h-3 w-3 text-slate-400 dark:text-slate-600 shrink-0 cursor-grab group-hover:text-slate-500" />
703
+ {node.type === "container" ? (
704
+ <button onClick={toggleExpand} className="p-0.5 rounded hover:bg-slate-200 dark:hover:bg-slate-700">
705
+ {isExpanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
706
+ </button>
707
+ ) : (
708
+ <span className="w-3.5" />
709
+ )}
710
+ {node.type === "container" ? (
711
+ <Layers className="h-3.5 w-3.5 text-primary shrink-0" />
712
+ ) : (
713
+ getFieldIcon(fields.find((f) => f.key === node.field_key)?.type || "text")
714
+ )}
715
+ <span className="truncate">{nodeName}</span>
716
+ {node.className && (
717
+ <span className="text-[10px] text-muted-foreground font-mono truncate max-w-[150px]">
718
+ .{node.className.split(" ")[0]}
719
+ </span>
720
+ )}
721
+ </div>
722
+
723
+ <div className="opacity-0 group-hover:opacity-100 flex items-center gap-1 transition-opacity shrink-0">
724
+ {node.type === "container" && (
725
+ <>
726
+ <button
727
+ onClick={(e) => {
728
+ e.stopPropagation();
729
+ handleInsertNode(path, "container");
730
+ }}
731
+ className="p-1 rounded text-slate-500 hover:text-primary hover:bg-slate-100 dark:hover:bg-slate-700"
732
+ title="Add Inner Container"
733
+ >
734
+ <Layers className="h-3.5 w-3.5" />
735
+ </button>
736
+ <button
737
+ onClick={(e) => {
738
+ e.stopPropagation();
739
+ handleInsertNode(path, "field_render");
740
+ }}
741
+ className="p-1 rounded text-slate-500 hover:text-primary hover:bg-slate-100 dark:hover:bg-slate-700"
742
+ title="Add Field Render"
743
+ >
744
+ <Plus className="h-3.5 w-3.5" />
745
+ </button>
746
+ </>
747
+ )}
748
+ {path.length > 0 && (
749
+ <button
750
+ onClick={(e) => {
751
+ e.stopPropagation();
752
+ handleDeleteNode(path);
753
+ }}
754
+ className="p-1 rounded text-slate-500 hover:text-destructive hover:bg-destructive/10"
755
+ title="Remove Node"
756
+ >
757
+ <Trash2 className="h-3.5 w-3.5" />
758
+ </button>
759
+ )}
760
+ </div>
761
+ </div>
762
+
763
+ {node.type === "container" && isExpanded && node.children && (
764
+ <div className="space-y-1">
765
+ {node.children.map((child: any, idx: number) => renderTreeItem(child, [...path, idx]))}
766
+ </div>
767
+ )}
768
+ </div>
769
+ );
770
+ };
771
+
772
+ const selectedNode = selectedNodePath ? getLayoutNodeByPath(layoutSchema, selectedNodePath) : null;
773
+
774
+ // Every field_render node in the layout, in depth-first order, with its path.
775
+ const layoutFieldRefs = useMemo(() => {
776
+ const refs: { path: number[]; key: string }[] = [];
777
+ const walk = (node: any, path: number[]) => {
778
+ if (!node || typeof node !== "object") return;
779
+ if (node.type === "field_render") {
780
+ if (node.field_key) refs.push({ path, key: node.field_key });
781
+ return;
782
+ }
783
+ if (node.type === "container" && Array.isArray(node.children)) {
784
+ node.children.forEach((child: any, idx: number) => walk(child, [...path, idx]));
785
+ }
786
+ };
787
+ walk(layoutSchema, []);
788
+ return refs;
789
+ }, [layoutSchema]);
790
+
791
+ // Preview/property inputs follow the layout order (deduped, each field once),
792
+ // with any fields not yet placed in the layout appended at the end.
793
+ const orderedPreviewFields = useMemo(() => {
794
+ const byKey = new Map(fields.map((field) => [field.key, field]));
795
+ const seen = new Set<string>();
796
+ const ordered: CustomBlockField[] = [];
797
+ for (const ref of layoutFieldRefs) {
798
+ if (seen.has(ref.key)) continue;
799
+ const field = byKey.get(ref.key);
800
+ if (field) {
801
+ ordered.push(field);
802
+ seen.add(ref.key);
803
+ }
804
+ }
805
+ for (const field of fields) {
806
+ if (!seen.has(field.key)) {
807
+ ordered.push(field);
808
+ seen.add(field.key);
809
+ }
810
+ }
811
+ return ordered;
812
+ }, [fields, layoutFieldRefs]);
813
+
814
+ // Field keys already mapped by a different field_render node, so a property is
815
+ // only used once across the layout blueprint.
816
+ const fieldKeysUsedElsewhere = useMemo(() => {
817
+ const selectedKey = selectedNodePath ? JSON.stringify(selectedNodePath) : null;
818
+ const used = new Set<string>();
819
+ for (const ref of layoutFieldRefs) {
820
+ if (selectedKey && JSON.stringify(ref.path) === selectedKey) continue;
821
+ used.add(ref.key);
822
+ }
823
+ return used;
824
+ }, [layoutFieldRefs, selectedNodePath]);
825
+
826
+ // Keep the Properties Schema list itself ordered to match the layout blueprint,
827
+ // so the fields editor mirrors the visual tree order everywhere.
828
+ useEffect(() => {
829
+ setFields((prev) => {
830
+ const ordered = orderCustomBlockFieldsByLayout(prev, layoutSchema);
831
+ const unchanged =
832
+ ordered.length === prev.length && ordered.every((field, index) => field === prev[index]);
833
+ return unchanged ? prev : ordered;
834
+ });
835
+ }, [layoutFieldRefs]);
836
+
837
+ return (
838
+ <div className="w-full flex flex-col gap-6">
839
+ {/* Header section */}
840
+ <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 border-b border-slate-200 dark:border-slate-800 pb-5">
841
+ <div className="flex items-center gap-3">
842
+ <Button variant="ghost" size="icon" onClick={() => router.push("/cms/custom-blocks")} className="h-9 w-9">
843
+ <ArrowLeft className="h-5 w-5" />
844
+ </Button>
845
+ <div>
846
+ <h2 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-slate-50">
847
+ {mode === "create" ? "Build Custom Block" : `Edit block: ${name}`}
848
+ </h2>
849
+ <p className="text-sm text-muted-foreground mt-0.5">
850
+ Specify your properties fields schemas and map visual Tailwind nested layouts.
851
+ </p>
852
+ </div>
853
+ </div>
854
+
855
+ <div className="flex items-center gap-2 shrink-0">
856
+ <Dialog>
857
+ <DialogTrigger asChild>
858
+ <Button size="sm" variant="outline" className="flex items-center gap-1.5 text-slate-600 dark:text-slate-300">
859
+ <BookOpen className="h-4 w-4 text-sky-500" />
860
+ How to Use Guide
861
+ </Button>
862
+ </DialogTrigger>
863
+ <DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-2xl p-6">
864
+ <DialogHeader className="border-b border-slate-100 dark:border-slate-800 pb-4">
865
+ <DialogTitle className="flex items-center gap-2 text-lg font-bold text-slate-900 dark:text-white">
866
+ <Boxes className="h-5 w-5 text-indigo-500" />
867
+ NextBlock Custom Block Creator Guide
868
+ </DialogTitle>
869
+ <DialogDescription className="text-xs text-muted-foreground mt-1">
870
+ A quick step-by-step masterclass on designing, structuring, and visually rendering data-driven user blocks.
871
+ </DialogDescription>
872
+ </DialogHeader>
873
+
874
+ <div className="py-6 space-y-6 text-slate-700 dark:text-slate-300 text-xs leading-relaxed">
875
+ {/* Step 1 */}
876
+ <div className="space-y-2">
877
+ <div className="flex items-center gap-2">
878
+ <Badge className="bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 px-2 py-0.5 text-[10px] font-bold">STEP 1</Badge>
879
+ <h4 className="font-bold text-slate-900 dark:text-white text-sm">Configure Name and Slug Identifiers</h4>
880
+ </div>
881
+ <p className="pl-14 text-slate-600 dark:text-slate-400">
882
+ Choose a descriptive name (e.g. <code className="font-mono bg-muted px-1 py-0.5 rounded text-indigo-400">Product Showcase Card</code>).
883
+ The slug identifier will auto-populate (e.g. <code className="font-mono bg-muted px-1 py-0.5 rounded text-indigo-400">product-showcase-card</code>).
884
+ This slug acts as the database key and cannot be changed once saved.
885
+ </p>
886
+ </div>
887
+
888
+ {/* Step 2 */}
889
+ <div className="space-y-2">
890
+ <div className="flex items-center gap-2">
891
+ <Badge className="bg-sky-500/10 text-sky-400 border border-sky-500/20 px-2 py-0.5 text-[10px] font-bold">STEP 2</Badge>
892
+ <h4 className="font-bold text-slate-900 dark:text-white text-sm">Declare Properties Fields Schema</h4>
893
+ </div>
894
+ <p className="pl-14 text-slate-600 dark:text-slate-400 mb-2">
895
+ Properties define the editable fields editors will populate when adding the block to pages. Supported types:
896
+ </p>
897
+ <ul className="pl-14 list-disc space-y-1.5 text-slate-600 dark:text-slate-400">
898
+ <li><span className="font-bold text-slate-800 dark:text-slate-200">Text:</span> Single line plain string (headings, button labels, links).</li>
899
+ <li><span className="font-bold text-slate-800 dark:text-slate-200">Rich-Text:</span> Full HTML formatting (body text, custom paragraphs, lists).</li>
900
+ <li><span className="font-bold text-slate-800 dark:text-slate-200">R2 Image:</span> Media files uploaded directly to Cloudflare R2 object storage.</li>
901
+ <li><span className="font-bold text-slate-800 dark:text-slate-200">Live DB Relation Link:</span> Reference table rows directly (e.g. link custom pages or products) with dynamic lookup columns.</li>
902
+ </ul>
903
+ </div>
904
+
905
+ {/* Step 3 */}
906
+ <div className="space-y-2">
907
+ <div className="flex items-center gap-2">
908
+ <Badge className="bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-2 py-0.5 text-[10px] font-bold">STEP 3</Badge>
909
+ <h4 className="font-bold text-slate-900 dark:text-white text-sm">Visual Layout Tree & Drag & Drop</h4>
910
+ </div>
911
+ <p className="pl-14 text-slate-600 dark:text-slate-400 mb-2">
912
+ The layout tree outlines how the block is structured inside the DOM. You can nest structural containers and map fields:
913
+ </p>
914
+ <ul className="pl-14 list-disc space-y-1.5 text-slate-600 dark:text-slate-400">
915
+ <li><span className="font-bold text-slate-800 dark:text-slate-200">Containers:</span> Represent tags like <code className="font-mono bg-muted px-1 rounded">&lt;div&gt;</code>, <code className="font-mono bg-muted px-1 rounded">&lt;section&gt;</code>. Style them with Tailwind CSS.</li>
916
+ <li><span className="font-bold text-slate-800 dark:text-slate-200">Field Renders:</span> Maps a field schema value to an HTML tag (e.g. map image to <code className="font-mono bg-muted px-1 rounded">&lt;img&gt;</code>).</li>
917
+ <li><span className="font-bold text-slate-800 dark:text-slate-200">Drag & Drop:</span> Click and drag the <GripVertical className="inline-block h-3.5 w-3.5 mx-0.5 text-slate-400" /> handle of any node in the tree list. Hover over targets to drop:
918
+ <ul className="list-circle pl-6 mt-1 space-y-1">
919
+ <li>Hover near <span className="font-bold text-indigo-400">top</span> of a node to insert <span className="italic">before</span> it.</li>
920
+ <li>Hover near <span className="font-bold text-indigo-400">bottom</span> of a node to insert <span className="italic">after</span> it.</li>
921
+ <li>Hover over the <span className="font-bold text-indigo-400">middle</span> of a container node to nest it <span className="italic">inside</span> as a child.</li>
922
+ </ul>
923
+ </li>
924
+ </ul>
925
+ </div>
926
+
927
+ {/* Quick Styling Presets */}
928
+ <div className="space-y-2 bg-slate-50 dark:bg-slate-950 p-4 border rounded-xl border-slate-100 dark:border-slate-800/80">
929
+ <h5 className="font-bold text-slate-900 dark:text-white flex items-center gap-1">
930
+ <Sparkles className="h-3.5 w-3.5 text-amber-400" />
931
+ Styling Presets Tip
932
+ </h5>
933
+ <p className="text-slate-600 dark:text-slate-400">
934
+ Select a node in the Layout Tree to inspect it. Use the <span className="font-bold text-slate-800 dark:text-slate-200">Quick-Styling utility Presets</span> to apply grid columns, borders, shadow cards, flex columns, and center alignments with one click. Test inputs on the right visualizer to preview live rendering instantly.
935
+ </p>
936
+ </div>
937
+ </div>
938
+ </DialogContent>
939
+ </Dialog>
940
+
941
+ <Button onClick={handleSaveBlock} disabled={isPending} className="shadow-sm">
942
+ {isPending ? (
943
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
944
+ ) : (
945
+ <Save className="h-4 w-4 mr-2" />
946
+ )}
947
+ Save Block Schema
948
+ </Button>
949
+ </div>
950
+ </div>
951
+
952
+ {/* Main Dual-Pane layout */}
953
+ <div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
954
+ {/* Left builder pane (7 columns) */}
955
+ <div className="lg:col-span-7 flex flex-col gap-6">
956
+ <div className="flex border-b border-slate-200 dark:border-slate-800">
957
+ <button
958
+ onClick={() => setActiveTab("general")}
959
+ className={`pb-3 px-4 text-sm font-semibold border-b-2 transition-all flex items-center gap-2 ${
960
+ activeTab === "general"
961
+ ? "border-primary text-primary"
962
+ : "border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
963
+ }`}
964
+ >
965
+ <Settings className="h-4 w-4" /> Config metadata
966
+ </button>
967
+ <button
968
+ onClick={() => setActiveTab("fields")}
969
+ className={`pb-3 px-4 text-sm font-semibold border-b-2 transition-all flex items-center gap-2 ${
970
+ activeTab === "fields"
971
+ ? "border-primary text-primary"
972
+ : "border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
973
+ }`}
974
+ >
975
+ <Database className="h-4 w-4" /> Fields Schema
976
+ </button>
977
+ <button
978
+ onClick={() => setActiveTab("layout")}
979
+ className={`pb-3 px-4 text-sm font-semibold border-b-2 transition-all flex items-center gap-2 ${
980
+ activeTab === "layout"
981
+ ? "border-primary text-primary"
982
+ : "border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
983
+ }`}
984
+ >
985
+ <ListTree className="h-4 w-4" /> Layout Tree
986
+ </button>
987
+ </div>
988
+
989
+ <div className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl p-6 min-h-[450px]">
990
+ {/* Tab 1: General Info */}
991
+ {activeTab === "general" && (
992
+ <div className="space-y-6">
993
+ <div className="space-y-2">
994
+ <Label htmlFor="block-name" className="text-sm font-bold">Block Name</Label>
995
+ <Input
996
+ id="block-name"
997
+ placeholder="e.g. Testimonial Card"
998
+ value={name}
999
+ onChange={(e) => handleNameChange(e.target.value)}
1000
+ />
1001
+ </div>
1002
+ <div className="space-y-2">
1003
+ <Label htmlFor="block-slug" className="text-sm font-bold">Slug Identifier</Label>
1004
+ <Input
1005
+ id="block-slug"
1006
+ placeholder="e.g. testimonial-card"
1007
+ value={slug}
1008
+ onChange={(e) => setSlug(e.target.value)}
1009
+ disabled={mode === "edit"}
1010
+ />
1011
+ <p className="text-xs text-muted-foreground">
1012
+ Unique block identifier. Used in schemas and JSON models. Cannot be changed once created.
1013
+ </p>
1014
+ </div>
1015
+ <div className="space-y-2">
1016
+ <Label htmlFor="block-desc" className="text-sm font-bold">Description</Label>
1017
+ <Textarea
1018
+ id="block-desc"
1019
+ rows={4}
1020
+ placeholder="Describe what this custom block represents or does..."
1021
+ value={description}
1022
+ onChange={(e) => setDescription(e.target.value)}
1023
+ />
1024
+ <p className="text-xs text-muted-foreground">
1025
+ Optional. Admin-only note shown in the blocks library — it does not appear on the front end.
1026
+ </p>
1027
+ </div>
1028
+ </div>
1029
+ )}
1030
+
1031
+ {/* Tab 2: Fields Manager */}
1032
+ {activeTab === "fields" && (
1033
+ <div className="space-y-6">
1034
+ <div className="flex justify-between items-center">
1035
+ <h3 className="text-sm font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider">
1036
+ Properties Schema
1037
+ </h3>
1038
+ <Button onClick={addField} size="sm" variant="outline">
1039
+ <Plus className="mr-1.5 h-4 w-4" /> Add Property Field
1040
+ </Button>
1041
+ </div>
1042
+
1043
+ {fields.length === 0 ? (
1044
+ <div className="text-center py-12 border border-dashed rounded-lg border-slate-200 dark:border-slate-800">
1045
+ <Database className="mx-auto h-10 w-10 text-muted-foreground" />
1046
+ <p className="text-sm text-slate-500 mt-2">No schema fields created.</p>
1047
+ <Button onClick={addField} variant="link" className="mt-2 text-primary font-semibold">
1048
+ Add a field properties selector
1049
+ </Button>
1050
+ </div>
1051
+ ) : (
1052
+ <div className="space-y-2.5">
1053
+ {fields.map((field, idx) => (
1054
+ <div
1055
+ key={idx}
1056
+ className="overflow-hidden rounded-lg border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900/40 shadow-sm"
1057
+ >
1058
+ {/* Identity row */}
1059
+ <div className="grid grid-cols-1 md:grid-cols-12 gap-2.5 items-end p-3">
1060
+ <div className="md:col-span-4 space-y-1">
1061
+ <Label className="text-[10px] uppercase tracking-wide font-semibold text-muted-foreground">Property Key</Label>
1062
+ <Input
1063
+ value={field.key}
1064
+ placeholder="e.g. quote"
1065
+ onChange={(e) => updateField(idx, { key: e.target.value })}
1066
+ className="h-8 font-mono text-xs"
1067
+ />
1068
+ </div>
1069
+ <div className="md:col-span-4 space-y-1">
1070
+ <Label className="text-[10px] uppercase tracking-wide font-semibold text-muted-foreground">Label</Label>
1071
+ <Input
1072
+ value={field.label}
1073
+ placeholder="e.g. Author Name"
1074
+ onChange={(e) => updateField(idx, { label: e.target.value })}
1075
+ className="h-8 text-xs"
1076
+ />
1077
+ </div>
1078
+ <div className="md:col-span-3 space-y-1">
1079
+ <Label className="text-[10px] uppercase tracking-wide font-semibold text-muted-foreground">Type</Label>
1080
+ <select
1081
+ value={field.type}
1082
+ onChange={(e) =>
1083
+ updateField(idx, {
1084
+ type: e.target.value as "text" | "rich-text" | "image_r2" | "db_relation",
1085
+ })
1086
+ }
1087
+ className="w-full rounded-md border border-input bg-background px-2 text-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 h-8"
1088
+ >
1089
+ <option value="text">Text (single-line)</option>
1090
+ <option value="rich-text">Rich-Text (HTML)</option>
1091
+ <option value="image_r2">Cloudflare R2 Image</option>
1092
+ <option value="db_relation">Live DB Relation Link</option>
1093
+ </select>
1094
+ </div>
1095
+ <div className="md:col-span-1 flex justify-end">
1096
+ <Button
1097
+ variant="ghost"
1098
+ size="icon"
1099
+ onClick={() => deleteField(idx)}
1100
+ className="h-8 w-8 text-slate-400 hover:text-destructive hover:bg-destructive/10"
1101
+ title="Remove field"
1102
+ >
1103
+ <Trash2 className="h-4 w-4" />
1104
+ </Button>
1105
+ </div>
1106
+ </div>
1107
+
1108
+ {/* Config + flags strip: context on the left, toggles on the right */}
1109
+ <div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2 border-t border-slate-100 dark:border-slate-800/60 bg-slate-50/70 dark:bg-slate-950/30 px-3 py-2">
1110
+ <div className="flex items-center gap-2 min-w-0">
1111
+ {field.type === "db_relation" ? (
1112
+ <>
1113
+ <Label className="text-[10px] uppercase tracking-wide font-semibold text-muted-foreground shrink-0">
1114
+ Table
1115
+ </Label>
1116
+ <select
1117
+ value={field.table}
1118
+ onChange={(e) => {
1119
+ const table = e.target.value;
1120
+ const spec = relationTables.find((t) => t.table === table);
1121
+ updateField(idx, {
1122
+ table,
1123
+ display_column: spec?.displayColumn || "title",
1124
+ value_column: spec?.valueColumn || "id",
1125
+ });
1126
+ }}
1127
+ className="h-7 rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
1128
+ >
1129
+ {relationTables.map((t) => (
1130
+ <option key={t.table} value={t.table}>
1131
+ {t.label}
1132
+ </option>
1133
+ ))}
1134
+ </select>
1135
+ <span className="hidden lg:inline truncate text-[11px] text-muted-foreground">
1136
+ · choose the column in the Layout Tree
1137
+ </span>
1138
+ </>
1139
+ ) : (
1140
+ <span className="inline-flex items-center gap-1.5 text-[11px] text-muted-foreground">
1141
+ {getFieldIcon(field.type)}
1142
+ {field.type === "text"
1143
+ ? "Single-line text"
1144
+ : field.type === "rich-text"
1145
+ ? "Formatted HTML content"
1146
+ : "Direct image upload (Cloudflare R2)"}
1147
+ </span>
1148
+ )}
1149
+ </div>
1150
+
1151
+ <div className="flex items-center gap-4 shrink-0">
1152
+ {field.type === "db_relation" && (
1153
+ <div className="flex items-center gap-1.5">
1154
+ <Checkbox
1155
+ id={`multiple-${idx}`}
1156
+ checked={field.multiple === true}
1157
+ onCheckedChange={(checked) => updateField(idx, { multiple: checked === true })}
1158
+ />
1159
+ <Label htmlFor={`multiple-${idx}`} className="text-xs font-medium cursor-pointer">
1160
+ Link multiple
1161
+ </Label>
1162
+ </div>
1163
+ )}
1164
+ <div className="flex items-center gap-1.5">
1165
+ <Checkbox
1166
+ id={`required-${idx}`}
1167
+ checked={field.required === true}
1168
+ onCheckedChange={(checked) => updateField(idx, { required: checked === true })}
1169
+ />
1170
+ <Label htmlFor={`required-${idx}`} className="text-xs font-medium cursor-pointer">
1171
+ Required
1172
+ </Label>
1173
+ </div>
1174
+ </div>
1175
+ </div>
1176
+ </div>
1177
+ ))}
1178
+ </div>
1179
+ )}
1180
+ </div>
1181
+ )}
1182
+
1183
+ {/* Tab 3: Visual Layout Tree Editor */}
1184
+ {activeTab === "layout" && (
1185
+ <div className="space-y-6">
1186
+ <div className="flex justify-between items-center border-b border-slate-100 dark:border-slate-800 pb-3">
1187
+ <div>
1188
+ <h3 className="text-sm font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider">
1189
+ Layout Schema Blueprint
1190
+ </h3>
1191
+ <p className="text-[11px] text-muted-foreground mt-0.5">
1192
+ Infinitely nest containers or render mapped schema properties fields.
1193
+ </p>
1194
+ </div>
1195
+ <div className="flex items-center gap-2">
1196
+ <Button onClick={handleGenerateDefaultLayout} size="sm" variant="outline">
1197
+ <Sparkles className="mr-1.5 h-3.5 w-3.5 text-amber-500" />
1198
+ Reset Blueprint Layout
1199
+ </Button>
1200
+ </div>
1201
+ </div>
1202
+
1203
+ <div className="grid grid-cols-1 md:grid-cols-12 gap-6">
1204
+ {/* Left tree layout list (5 columns) */}
1205
+ <div className="md:col-span-6 border rounded-xl border-slate-200 dark:border-slate-800 p-4 bg-slate-50/50 dark:bg-slate-900/30 max-h-[380px] overflow-y-auto">
1206
+ <h4 className="text-xs font-semibold text-slate-500 dark:text-slate-400 mb-2 uppercase tracking-wide">
1207
+ Tree Nodes
1208
+ </h4>
1209
+ {renderTreeItem(layoutSchema, [])}
1210
+ </div>
1211
+
1212
+ {/* Right inspector config (6 columns) */}
1213
+ <div className="md:col-span-6 border border-slate-200 dark:border-slate-800 rounded-xl p-4 bg-white dark:bg-slate-950 space-y-4">
1214
+ <h4 className="text-xs font-bold text-slate-800 dark:text-slate-200 border-b pb-2 uppercase tracking-wide">
1215
+ Node Properties Inspector
1216
+ </h4>
1217
+
1218
+ {!selectedNode ? (
1219
+ <div className="text-center py-10 text-muted-foreground flex flex-col items-center justify-center h-full">
1220
+ <Layers className="h-8 w-8 text-slate-300 dark:text-slate-700 mb-2" />
1221
+ <span className="text-xs italic">Select a layout node on the left to inspect or style.</span>
1222
+ </div>
1223
+ ) : (
1224
+ <div className="space-y-4 text-xs">
1225
+ <div className="flex justify-between items-center bg-slate-50 dark:bg-slate-900 px-3 py-1.5 rounded border">
1226
+ <span className="font-semibold text-slate-700 dark:text-slate-300">
1227
+ Node Type:
1228
+ </span>
1229
+ <Badge variant="secondary" className="uppercase font-mono text-[10px]">
1230
+ {selectedNode.type}
1231
+ </Badge>
1232
+ </div>
1233
+
1234
+ {selectedNode.type === "container" ? (
1235
+ <div className="space-y-3">
1236
+ <div className="space-y-1">
1237
+ <Label className="font-semibold">HTML Element Tag</Label>
1238
+ <select
1239
+ value={selectedNode.as || "div"}
1240
+ onChange={(e) => handleUpdateSelectedNode({ as: e.target.value })}
1241
+ className="w-full rounded-md border h-8 px-2 bg-transparent text-xs"
1242
+ >
1243
+ {CONTAINER_TAGS.map((tag) => (
1244
+ <option key={tag} value={tag}>
1245
+ &lt;{tag}&gt; Element
1246
+ </option>
1247
+ ))}
1248
+ </select>
1249
+ </div>
1250
+ </div>
1251
+ ) : (
1252
+ <div className="space-y-3">
1253
+ <div className="space-y-1">
1254
+ <Label className="font-semibold">Map Property Field</Label>
1255
+ <select
1256
+ value={selectedNode.field_key || ""}
1257
+ onChange={(e) => {
1258
+ const key = e.target.value;
1259
+ const type = fields.find((f) => f.key === key)?.type || "text";
1260
+ handleUpdateSelectedNode({
1261
+ field_key: key,
1262
+ as: type === "image_r2" ? "img" : type === "rich-text" ? "div" : "span",
1263
+ });
1264
+ }}
1265
+ className="w-full rounded-md border h-8 px-2 bg-transparent text-xs"
1266
+ >
1267
+ <option value="" disabled>Select property key...</option>
1268
+ {fields.map((f) => {
1269
+ // Relation fields may be reused by multiple nodes (e.g. image, title,
1270
+ // price); only single-value fields are limited to one placement.
1271
+ const usedElsewhere =
1272
+ fieldKeysUsedElsewhere.has(f.key) && f.type !== "db_relation";
1273
+ return (
1274
+ <option key={f.key} value={f.key} disabled={usedElsewhere}>
1275
+ {f.label} ({f.key}){usedElsewhere ? " — already used" : ""}
1276
+ </option>
1277
+ );
1278
+ })}
1279
+ </select>
1280
+ </div>
1281
+ {(() => {
1282
+ const mappedField = fields.find((f) => f.key === selectedNode.field_key);
1283
+ if (mappedField?.type !== "db_relation") return null;
1284
+ const spec = relationTables.find((t) => t.table === mappedField.table);
1285
+ const columns = spec?.selectColumns || [];
1286
+ return (
1287
+ <div className="space-y-1">
1288
+ <Label className="font-semibold">Relation Column</Label>
1289
+ <select
1290
+ value={selectedNode.column || ""}
1291
+ onChange={(e) => handleUpdateSelectedNode({ column: e.target.value || undefined })}
1292
+ className="w-full rounded-md border h-8 px-2 bg-transparent text-xs"
1293
+ >
1294
+ <option value="">Default ({mappedField.display_column})</option>
1295
+ {columns.map((col) => (
1296
+ <option key={col} value={col}>
1297
+ {col}
1298
+ </option>
1299
+ ))}
1300
+ </select>
1301
+ <p className="text-[10px] text-muted-foreground">
1302
+ Which column of the related record to show. Render as &lt;img&gt; for its image; price columns display as currency.
1303
+ </p>
1304
+ </div>
1305
+ );
1306
+ })()}
1307
+ <div className="space-y-1">
1308
+ <Label className="font-semibold">Render Tag Element</Label>
1309
+ <select
1310
+ value={selectedNode.as || "span"}
1311
+ onChange={(e) => handleUpdateSelectedNode({ as: e.target.value })}
1312
+ className="w-full rounded-md border h-8 px-2 bg-transparent text-xs"
1313
+ >
1314
+ {FIELD_TAGS.map((tag) => (
1315
+ <option key={tag} value={tag}>
1316
+ &lt;{tag}&gt; Render Target
1317
+ </option>
1318
+ ))}
1319
+ </select>
1320
+ </div>
1321
+ <div className="space-y-1">
1322
+ <Label className="font-semibold">Empty Fallback Copy</Label>
1323
+ <Input
1324
+ value={selectedNode.emptyFallback || ""}
1325
+ onChange={(e) => handleUpdateSelectedNode({ emptyFallback: e.target.value })}
1326
+ placeholder="e.g. Quote content goes here..."
1327
+ className="h-8 text-xs"
1328
+ />
1329
+ </div>
1330
+ </div>
1331
+ )}
1332
+
1333
+ <div className="space-y-1.5">
1334
+ <Label className="font-semibold">Tailwind CSS Classes</Label>
1335
+ <Textarea
1336
+ value={selectedNode.className || ""}
1337
+ onChange={(e) => handleUpdateSelectedNode({ className: e.target.value })}
1338
+ placeholder="e.g. flex flex-col gap-4 text-center mt-2 border"
1339
+ className="font-mono text-xs"
1340
+ rows={3}
1341
+ />
1342
+ </div>
1343
+
1344
+ {/* Styling presets */}
1345
+ <div className="space-y-2 pt-2 border-t">
1346
+ <Label className="font-bold text-muted-foreground uppercase text-[9px] block">
1347
+ Quick-Styling utility Presets
1348
+ </Label>
1349
+ <div className="grid grid-cols-2 gap-1.5">
1350
+ <Button
1351
+ variant="outline"
1352
+ size="sm"
1353
+ className="text-[10px] justify-start h-7 px-2"
1354
+ onClick={() => {
1355
+ const current = selectedNode.className || "";
1356
+ const base = current.includes("flex") ? current : `flex flex-col gap-4 ${current}`.trim();
1357
+ handleUpdateSelectedNode({ className: base });
1358
+ }}
1359
+ >
1360
+ Flex Column
1361
+ </Button>
1362
+ <Button
1363
+ variant="outline"
1364
+ size="sm"
1365
+ className="text-[10px] justify-start h-7 px-2"
1366
+ onClick={() => {
1367
+ const current = selectedNode.className || "";
1368
+ const base = current.includes("grid") ? current : `grid gap-6 md:grid-cols-2 ${current}`.trim();
1369
+ handleUpdateSelectedNode({ className: base });
1370
+ }}
1371
+ >
1372
+ Grid (2 Cols)
1373
+ </Button>
1374
+ <Button
1375
+ variant="outline"
1376
+ size="sm"
1377
+ className="text-[10px] justify-start h-7 px-2"
1378
+ onClick={() => {
1379
+ const current = selectedNode.className || "";
1380
+ const base = current.includes("p-") ? current : `p-6 rounded-xl border bg-card shadow-sm ${current}`.trim();
1381
+ handleUpdateSelectedNode({ className: base });
1382
+ }}
1383
+ >
1384
+ Bordered Card
1385
+ </Button>
1386
+ <Button
1387
+ variant="outline"
1388
+ size="sm"
1389
+ className="text-[10px] justify-start h-7 px-2"
1390
+ onClick={() => {
1391
+ const current = selectedNode.className || "";
1392
+ const base = current.includes("items-center") ? current : `items-center justify-center text-center ${current}`.trim();
1393
+ handleUpdateSelectedNode({ className: base });
1394
+ }}
1395
+ >
1396
+ Align Center
1397
+ </Button>
1398
+ </div>
1399
+ </div>
1400
+ </div>
1401
+ )}
1402
+ </div>
1403
+ </div>
1404
+ </div>
1405
+ )}
1406
+ </div>
1407
+ </div>
1408
+
1409
+ {/* Right preview pane (5 columns) */}
1410
+ <div className="lg:col-span-5 space-y-6 lg:sticky lg:top-24">
1411
+ <div className="bg-slate-900 border border-slate-800 rounded-xl p-5 text-slate-100 flex items-center justify-between shadow-md">
1412
+ <div className="flex items-center gap-2">
1413
+ <Eye className="h-4 w-4 text-sky-400" />
1414
+ <h3 className="text-sm font-bold tracking-wider uppercase">
1415
+ Dynamic Layout Editor Preview
1416
+ </h3>
1417
+ </div>
1418
+ <Badge className="bg-emerald-500/15 text-emerald-400 border border-emerald-500/25 font-semibold text-[10px]">
1419
+ Live Playground
1420
+ </Badge>
1421
+ </div>
1422
+
1423
+ {/* Renders layout engine compilation */}
1424
+ <div className="bg-slate-100 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 p-6 rounded-xl min-h-[220px] flex items-center justify-center overflow-x-auto">
1425
+ {fields.length === 0 ? (
1426
+ <span className="text-xs text-muted-foreground italic">Add properties fields to start layout blueprint</span>
1427
+ ) : (
1428
+ <div className="w-full">
1429
+ <DynamicLayoutEngine
1430
+ fields={fields}
1431
+ layoutSchema={layoutSchema}
1432
+ data={{
1433
+ ...previewValues,
1434
+ resolved_relations: mockRelationRecords,
1435
+ }}
1436
+ />
1437
+ </div>
1438
+ )}
1439
+ </div>
1440
+
1441
+ {/* Preview data input values form */}
1442
+ {fields.length > 0 && (
1443
+ <Card className="border-slate-200 dark:border-slate-800 shadow-sm">
1444
+ <CardHeader className="py-3.5 border-b">
1445
+ <CardTitle className="text-xs uppercase font-bold text-slate-500 tracking-wider">
1446
+ Test Mock Values Playground
1447
+ </CardTitle>
1448
+ <CardDescription className="text-[10px]">
1449
+ Fill these custom sandbox inputs to visualize your Tailwind CSS alignment.
1450
+ </CardDescription>
1451
+ </CardHeader>
1452
+ <CardContent className="py-4 space-y-4 max-h-[300px] overflow-y-auto">
1453
+ {orderedPreviewFields.map((f) => {
1454
+ const fieldVal = previewValues[f.key];
1455
+ const fieldId = `preview-${f.key}`;
1456
+
1457
+ return (
1458
+ <div key={f.key} className="space-y-1.5 text-xs">
1459
+ <Label htmlFor={fieldId} className="font-semibold text-slate-700 dark:text-slate-300">
1460
+ {f.label} ({f.key}) {f.required && <span className="text-destructive">*</span>}
1461
+ </Label>
1462
+ {f.type === "rich-text" ? (
1463
+ <Textarea
1464
+ id={fieldId}
1465
+ value={fieldVal || ""}
1466
+ onChange={(e) => setPreviewValues({ ...previewValues, [f.key]: e.target.value })}
1467
+ className="text-xs"
1468
+ rows={2}
1469
+ />
1470
+ ) : f.type === "image_r2" ? (
1471
+ <ImageR2Picker
1472
+ value={fieldVal && typeof fieldVal === "object" && "url" in fieldVal ? fieldVal : null}
1473
+ onChange={(val) => setPreviewValues({ ...previewValues, [f.key]: val })}
1474
+ accept={f.accept}
1475
+ maxBytes={f.max_bytes}
1476
+ />
1477
+ ) : f.type === "db_relation" ? (
1478
+ <DBRelationSelect
1479
+ table={f.table}
1480
+ value={
1481
+ f.multiple
1482
+ ? Array.isArray(fieldVal) ? fieldVal.map(String) : []
1483
+ : fieldVal ? String(fieldVal) : null
1484
+ }
1485
+ onChange={(val, selected) => {
1486
+ setPreviewValues({ ...previewValues, [f.key]: val });
1487
+ if (f.multiple) {
1488
+ setMockRelationRecords({
1489
+ ...mockRelationRecords,
1490
+ [f.key]: selected || [],
1491
+ });
1492
+ } else {
1493
+ setMockRelationRecords({
1494
+ ...mockRelationRecords,
1495
+ [f.key]: selected?.[0] || null,
1496
+ });
1497
+ }
1498
+ }}
1499
+ multiple={f.multiple}
1500
+ displayColumn={f.display_column}
1501
+ valueColumn={f.value_column}
1502
+ filters={f.filters}
1503
+ />
1504
+ ) : (
1505
+ <Input
1506
+ id={fieldId}
1507
+ value={fieldVal || ""}
1508
+ onChange={(e) => setPreviewValues({ ...previewValues, [f.key]: e.target.value })}
1509
+ className="h-8 text-xs"
1510
+ />
1511
+ )}
1512
+ </div>
1513
+ );
1514
+ })}
1515
+ </CardContent>
1516
+ </Card>
1517
+ )}
1518
+ </div>
1519
+ </div>
1520
+ </div>
1521
+ );
1522
+ }