create-nextblock 0.2.78 → 0.8.1

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 +793 -472
  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 +182 -154
  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 -120
  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,1172 @@
1
+ "use client";
2
+
3
+ import dynamic from "next/dynamic";
4
+ import { usePathname, useRouter } from "next/navigation";
5
+ import React, {
6
+ useCallback,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ type ComponentType,
12
+ } from "react";
13
+ import type { Json } from "@nextblock-cms/db";
14
+ import { Button, Input, Label, Textarea } from "@nextblock-cms/ui";
15
+ import { Loader2, Pencil, Trash2, CloudLightning } from "lucide-react";
16
+ import {
17
+ discardVisualEditingDraft,
18
+ discardVisualEditingProductDraft,
19
+ loadVisualEditingBlockContent,
20
+ loadVisualEditingProductField,
21
+ publishVisualEditingDraft,
22
+ publishVisualEditingProductDraft,
23
+ } from "../../app/actions/visualEditingActions";
24
+ import {
25
+ BlockEditorModal,
26
+ type Block,
27
+ type BlockEditorProps,
28
+ type BlockEditorSaveStatus,
29
+ type EditorSurfaceContext,
30
+ } from "../../app/cms/blocks/components/BlockEditorModal";
31
+ import type {
32
+ NextblockDocumentType,
33
+ NextblockVisualDocumentType,
34
+ NextblockVisualEditInfo,
35
+ VisualEditingBlockRequest,
36
+ VisualEditingProductFieldRequest,
37
+ VisualEditingProductFieldTarget,
38
+ } from "../../lib/visual-editing/types";
39
+ import type { BlockType } from "../../lib/blocks/blockRegistry";
40
+
41
+ type EditorComponent = ComponentType<BlockEditorProps<unknown>>;
42
+ type HoverTarget = {
43
+ element: HTMLElement;
44
+ info: NextblockVisualEditInfo;
45
+ rect: DOMRect;
46
+ };
47
+ type ActiveDocument = {
48
+ parentType: NextblockVisualDocumentType;
49
+ parentId: number | string;
50
+ };
51
+ type ProductFieldVisualEditInfo = NextblockVisualEditInfo & {
52
+ data: Omit<NextblockVisualEditInfo["data"], "parentType" | "target"> & {
53
+ parentType: "product";
54
+ target: VisualEditingProductFieldTarget;
55
+ };
56
+ };
57
+ type ParsedCssColor = {
58
+ r: number;
59
+ g: number;
60
+ b: number;
61
+ a: number;
62
+ };
63
+
64
+ const VISUAL_DRAFT_AUTOSAVE_DELAY_MS = 900;
65
+
66
+ const TextBlockEditor = dynamic(
67
+ () => import("../../app/cms/blocks/editors/TextBlockEditor"),
68
+ { ssr: false }
69
+ ) as EditorComponent;
70
+ const HeadingBlockEditor = dynamic(
71
+ () => import("../../app/cms/blocks/editors/HeadingBlockEditor"),
72
+ { ssr: false }
73
+ ) as EditorComponent;
74
+ const ImageBlockEditor = dynamic(
75
+ () => import("../../app/cms/blocks/editors/ImageBlockEditor"),
76
+ { ssr: false }
77
+ ) as EditorComponent;
78
+ const ButtonBlockEditor = dynamic(
79
+ () => import("../../app/cms/blocks/editors/ButtonBlockEditor"),
80
+ { ssr: false }
81
+ ) as EditorComponent;
82
+ const PostsGridBlockEditor = dynamic(
83
+ () => import("../../app/cms/blocks/editors/PostsGridBlockEditor"),
84
+ { ssr: false }
85
+ ) as EditorComponent;
86
+ const VideoEmbedBlockEditor = dynamic(
87
+ () => import("../../app/cms/blocks/editors/VideoEmbedBlockEditor"),
88
+ { ssr: false }
89
+ ) as EditorComponent;
90
+ const FormBlockEditor = dynamic(
91
+ () => import("../../app/cms/blocks/editors/FormBlockEditor"),
92
+ { ssr: false }
93
+ ) as EditorComponent;
94
+ const ProductGridBlockEditor = dynamic(
95
+ () => import("../../app/cms/blocks/editors/ProductGridBlockEditor"),
96
+ { ssr: false }
97
+ ) as EditorComponent;
98
+
99
+ const FeaturedProductBlockEditor = dynamic(
100
+ () => import("../../app/cms/blocks/editors/FeaturedProductBlockEditor"),
101
+ { ssr: false }
102
+ ) as EditorComponent;
103
+
104
+ // Used for any non-core block type, i.e. user-defined custom blocks. Renders
105
+ // the field-based config form (with a JSON fallback for unknown types).
106
+ const DynamicCustomBlockEditor = dynamic(
107
+ () => import("../../app/cms/blocks/editors/DynamicCustomBlockEditor"),
108
+ { ssr: false }
109
+ ) as EditorComponent;
110
+ const TestimonialBlockEditor = dynamic(
111
+ () =>
112
+ import("../blocks/TestimonialBlock").then(
113
+ (module) => module.TestimonialBlockConfig.EditorComponent as EditorComponent
114
+ ),
115
+ { ssr: false }
116
+ ) as EditorComponent;
117
+ const SectionBlockEditor = dynamic(
118
+ () =>
119
+ import("../../app/cms/blocks/editors/SectionBlockEditor").then((module) => {
120
+ const Editor = module.default;
121
+ return function VisualSectionBlockEditor(props: BlockEditorProps<unknown>) {
122
+ return (
123
+ <Editor
124
+ content={props.content as any}
125
+ onChange={props.onChange as any}
126
+ isConfigPanelOpen={true}
127
+ blockType="section"
128
+ />
129
+ );
130
+ };
131
+ }),
132
+ { ssr: false }
133
+ ) as EditorComponent;
134
+
135
+ const editorComponents: Partial<Record<BlockType, EditorComponent>> = {
136
+ text: TextBlockEditor,
137
+ heading: HeadingBlockEditor,
138
+ image: ImageBlockEditor,
139
+ button: ButtonBlockEditor,
140
+ posts_grid: PostsGridBlockEditor,
141
+ video_embed: VideoEmbedBlockEditor,
142
+ section: SectionBlockEditor,
143
+
144
+ form: FormBlockEditor,
145
+ testimonial: TestimonialBlockEditor,
146
+ product_grid: ProductGridBlockEditor,
147
+ featured_product: FeaturedProductBlockEditor,
148
+ };
149
+
150
+ function parseEditInfo(element: Element | null): NextblockVisualEditInfo | null {
151
+ const raw = element?.getAttribute("data-vercel-edit-info");
152
+ if (!raw) {
153
+ return null;
154
+ }
155
+
156
+ try {
157
+ const parsed = JSON.parse(raw) as NextblockVisualEditInfo;
158
+ const isNextblock =
159
+ parsed?.origin === "nextblock" ||
160
+ parsed?.origin === "https://nextblock-editor" ||
161
+ parsed?.origin === "https://nextblock-editor.com" ||
162
+ parsed?.origin === "https://nextblock.dev" ||
163
+ (parsed && typeof parsed === "object" && parsed.data && "parentType" in parsed.data);
164
+ return isNextblock ? parsed : null;
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+
170
+ function isProductFieldTarget(target: unknown): target is VisualEditingProductFieldTarget {
171
+ return Boolean(
172
+ target &&
173
+ typeof target === "object" &&
174
+ "kind" in target &&
175
+ (target as { kind?: unknown }).kind === "product-field"
176
+ );
177
+ }
178
+
179
+ function isProductFieldInfo(info: NextblockVisualEditInfo): info is ProductFieldVisualEditInfo {
180
+ return info.data.parentType === "product" && isProductFieldTarget(info.data.target);
181
+ }
182
+
183
+ function blockRequestFromInfo(info: NextblockVisualEditInfo): VisualEditingBlockRequest {
184
+ if (info.data.parentType !== "page" && info.data.parentType !== "post" && info.data.parentType !== "product") {
185
+ throw new Error("Invalid block draft document.");
186
+ }
187
+
188
+ if (isProductFieldTarget(info.data.target)) {
189
+ throw new Error("Invalid block draft target.");
190
+ }
191
+
192
+ return {
193
+ parentType: info.data.parentType,
194
+ parentId: info.data.parentType === "product" ? String(info.data.parentId) : Number(info.data.parentId),
195
+ target: info.data.target as VisualEditingBlockRequest["target"],
196
+ };
197
+ }
198
+
199
+ function productFieldRequestFromInfo(info: NextblockVisualEditInfo): VisualEditingProductFieldRequest {
200
+ if (!isProductFieldInfo(info)) {
201
+ throw new Error("Invalid product draft target.");
202
+ }
203
+
204
+ return {
205
+ parentType: "product",
206
+ parentId: String(info.data.parentId),
207
+ target: info.data.target,
208
+ };
209
+ }
210
+
211
+ function documentFromInfo(info: NextblockVisualEditInfo): ActiveDocument {
212
+ return {
213
+ parentType: info.data.parentType,
214
+ parentId: info.data.parentId,
215
+ };
216
+ }
217
+
218
+ function serializeDraftContent(content: unknown) {
219
+ try {
220
+ return JSON.stringify(content ?? null);
221
+ } catch {
222
+ return String(content);
223
+ }
224
+ }
225
+
226
+ function getActionError(result: unknown) {
227
+ if (!result || typeof result !== "object" || !("error" in result)) {
228
+ return "";
229
+ }
230
+
231
+ const error = (result as { error?: unknown }).error;
232
+ return typeof error === "string" ? error : "";
233
+ }
234
+
235
+ async function saveVisualEditingBlockDraftViaApi(
236
+ request: VisualEditingBlockRequest,
237
+ content: Json
238
+ ) {
239
+ const response = await fetch("/api/visual-editing/block-draft", {
240
+ method: "POST",
241
+ headers: { "Content-Type": "application/json" },
242
+ body: JSON.stringify({ request, content }),
243
+ cache: "no-store",
244
+ });
245
+
246
+ let result: unknown = null;
247
+ try {
248
+ result = await response.json();
249
+ } catch {
250
+ // Keep the UI error concise if the route fails before returning JSON.
251
+ }
252
+
253
+ const actionError = getActionError(result);
254
+ if (actionError) {
255
+ return { error: actionError };
256
+ }
257
+
258
+ if (!response.ok) {
259
+ return { error: "Failed to save draft." };
260
+ }
261
+
262
+ return { success: true };
263
+ }
264
+
265
+ async function saveVisualEditingProductDraftViaApi(
266
+ request: VisualEditingProductFieldRequest,
267
+ content: Json
268
+ ) {
269
+ const response = await fetch("/api/visual-editing/product-draft", {
270
+ method: "POST",
271
+ headers: { "Content-Type": "application/json" },
272
+ body: JSON.stringify({ request, content }),
273
+ cache: "no-store",
274
+ });
275
+
276
+ let result: unknown = null;
277
+ try {
278
+ result = await response.json();
279
+ } catch {
280
+ // Keep the UI error concise if the route fails before returning JSON.
281
+ }
282
+
283
+ const actionError = getActionError(result);
284
+ if (actionError) {
285
+ return { error: actionError };
286
+ }
287
+
288
+ if (!response.ok) {
289
+ return { error: "Failed to save product draft." };
290
+ }
291
+
292
+ return { success: true };
293
+ }
294
+
295
+ function createInfoKey(info: NextblockVisualEditInfo) {
296
+ return JSON.stringify({
297
+ parentType: info.data.parentType,
298
+ parentId: info.data.parentId,
299
+ target: info.data.target,
300
+ });
301
+ }
302
+
303
+ function findElementForInfo(info: NextblockVisualEditInfo) {
304
+ const expectedKey = createInfoKey(info);
305
+ const elements = document.querySelectorAll<HTMLElement>("[data-vercel-edit-info]");
306
+
307
+ for (const element of elements) {
308
+ const parsed = parseEditInfo(element);
309
+ if (parsed && createInfoKey(parsed) === expectedKey) {
310
+ return element;
311
+ }
312
+ }
313
+
314
+ return null;
315
+ }
316
+
317
+ function parseCssColor(value: string): ParsedCssColor | null {
318
+ const normalized = value.trim().toLowerCase();
319
+
320
+ if (!normalized || normalized === "transparent") {
321
+ return null;
322
+ }
323
+
324
+ const rgbMatch = normalized.match(/^rgba?\((.+)\)$/);
325
+ if (!rgbMatch) {
326
+ return null;
327
+ }
328
+
329
+ const parts = rgbMatch[1]
330
+ .split(",")
331
+ .map((part) => part.trim())
332
+ .filter(Boolean);
333
+
334
+ if (parts.length < 3) {
335
+ return null;
336
+ }
337
+
338
+ const [r, g, b] = parts.slice(0, 3).map((part) => Number.parseFloat(part));
339
+ const a = parts[3] === undefined ? 1 : Number.parseFloat(parts[3]);
340
+
341
+ if (![r, g, b, a].every(Number.isFinite)) {
342
+ return null;
343
+ }
344
+
345
+ return { r, g, b, a };
346
+ }
347
+
348
+ function isDarkColor(value: string) {
349
+ const color = parseCssColor(value);
350
+ if (!color || color.a <= 0) {
351
+ return false;
352
+ }
353
+
354
+ const r = color.r * color.a + 255 * (1 - color.a);
355
+ const g = color.g * color.a + 255 * (1 - color.a);
356
+ const b = color.b * color.a + 255 * (1 - color.a);
357
+ const yiq = (r * 299 + g * 587 + b * 114) / 1000;
358
+
359
+ return yiq < 140;
360
+ }
361
+
362
+ function isLightColor(value: string) {
363
+ const color = parseCssColor(value);
364
+ if (!color || color.a <= 0) {
365
+ return false;
366
+ }
367
+
368
+ const yiq = (color.r * 299 + color.g * 587 + color.b * 114) / 1000;
369
+ return yiq > 180;
370
+ }
371
+
372
+ function getVisualEditorSurfaceContext(element: HTMLElement | null): EditorSurfaceContext | null {
373
+ if (!element || typeof window === "undefined") {
374
+ return null;
375
+ }
376
+
377
+ let current: HTMLElement | null = element;
378
+
379
+ while (current && current !== document.body && current !== document.documentElement) {
380
+ const computed = window.getComputedStyle(current);
381
+ const backgroundImage = computed.backgroundImage;
382
+ const hasBackgroundImage = Boolean(backgroundImage && backgroundImage !== "none");
383
+ const backgroundColor = parseCssColor(computed.backgroundColor);
384
+ const hasSolidBackground = Boolean(backgroundColor && backgroundColor.a > 0.55);
385
+
386
+ if (hasBackgroundImage || hasSolidBackground) {
387
+ const style: EditorSurfaceContext["style"] = {
388
+ backgroundColor: backgroundColor && backgroundColor.a > 0 ? computed.backgroundColor : undefined,
389
+ backgroundImage: hasBackgroundImage ? backgroundImage : undefined,
390
+ backgroundPosition: computed.backgroundPosition,
391
+ backgroundRepeat: computed.backgroundRepeat,
392
+ backgroundSize: computed.backgroundSize,
393
+ };
394
+
395
+ return {
396
+ isDark: hasBackgroundImage || isDarkColor(computed.backgroundColor),
397
+ style,
398
+ };
399
+ }
400
+
401
+ current = current.parentElement;
402
+ }
403
+
404
+ const textColor = window.getComputedStyle(element).color;
405
+ if (isLightColor(textColor)) {
406
+ return {
407
+ isDark: true,
408
+ style: {
409
+ backgroundColor: "#0f172a",
410
+ },
411
+ };
412
+ }
413
+
414
+ return null;
415
+ }
416
+
417
+ function JsonBlockEditor({ content, onChange }: BlockEditorProps<unknown>) {
418
+ const [value, setValue] = useState(() => JSON.stringify(content ?? {}, null, 2));
419
+ const [error, setError] = useState<string | null>(null);
420
+
421
+ useEffect(() => {
422
+ setValue(JSON.stringify(content ?? {}, null, 2));
423
+ setError(null);
424
+ }, [content]);
425
+
426
+ return (
427
+ <div className="space-y-3 p-3 border-t mt-2">
428
+ <Label htmlFor="nextblock-visual-json-editor">JSON Content</Label>
429
+ <Textarea
430
+ id="nextblock-visual-json-editor"
431
+ value={value}
432
+ onChange={(event) => {
433
+ const nextValue = event.target.value;
434
+ setValue(nextValue);
435
+
436
+ try {
437
+ onChange(JSON.parse(nextValue));
438
+ setError(null);
439
+ } catch (parseError) {
440
+ setError(parseError instanceof Error ? parseError.message : "Invalid JSON");
441
+ }
442
+ }}
443
+ className="min-h-[360px] font-mono text-sm"
444
+ spellCheck={false}
445
+ />
446
+ {error && <p className="text-sm text-destructive">{error}</p>}
447
+ </div>
448
+ );
449
+ }
450
+
451
+ function ProductPlainTextFieldEditor({ block, content, onChange }: BlockEditorProps<unknown>) {
452
+ const value = typeof content === "string" ? content : "";
453
+ const field = typeof block.productField === "string" ? block.productField : "title";
454
+
455
+ if (field === "short_description") {
456
+ return (
457
+ <div className="space-y-2 p-3 border-t mt-2">
458
+ <Label htmlFor="nextblock-visual-product-short-description">Short Description</Label>
459
+ <Textarea
460
+ id="nextblock-visual-product-short-description"
461
+ value={value}
462
+ onChange={(event) => onChange(event.target.value)}
463
+ className="min-h-[140px] text-sm"
464
+ />
465
+ </div>
466
+ );
467
+ }
468
+
469
+ return (
470
+ <div className="space-y-2 p-3 border-t mt-2">
471
+ <Label htmlFor="nextblock-visual-product-title">Product Title</Label>
472
+ <Input
473
+ id="nextblock-visual-product-title"
474
+ value={value}
475
+ onChange={(event) => onChange(event.target.value)}
476
+ className="text-base"
477
+ />
478
+ </div>
479
+ );
480
+ }
481
+
482
+ function getEditorComponent(blockType: string) {
483
+ // Core blocks have dedicated editors; everything else is treated as a custom
484
+ // block and gets the dynamic field-config editor (not the raw JSON editor).
485
+ return editorComponents[blockType as BlockType] ?? DynamicCustomBlockEditor;
486
+ }
487
+
488
+ function getProductFieldEditorComponent(target: VisualEditingProductFieldTarget) {
489
+ return ProductPlainTextFieldEditor;
490
+ }
491
+
492
+ export function NextblockVisualEditing() {
493
+ const router = useRouter();
494
+ const pathname = usePathname();
495
+
496
+ // If we are on a CMS edit or admin page, hide the frontend toolbar entirely to prevent overlap.
497
+ if (pathname?.startsWith("/cms")) {
498
+ return null;
499
+ }
500
+
501
+ const [hoverTarget, setHoverTarget] = useState<HoverTarget | null>(null);
502
+ const [activeInfo, setActiveInfo] = useState<NextblockVisualEditInfo | null>(null);
503
+ const [activeDocument, setActiveDocument] = useState<ActiveDocument | null>(null);
504
+ const [activeBlock, setActiveBlock] = useState<Block | null>(null);
505
+ const [activeSurfaceContext, setActiveSurfaceContext] = useState<EditorSurfaceContext | null>(null);
506
+ const [activeModalTitle, setActiveModalTitle] = useState<string | undefined>(undefined);
507
+ const [isEditorOpen, setIsEditorOpen] = useState(false);
508
+ const [isLoadingBlock, setIsLoadingBlock] = useState(false);
509
+ const [isSavingBlock, setIsSavingBlock] = useState(false);
510
+ const [isPublishing, setIsPublishing] = useState(false);
511
+ const [isDiscarding, setIsDiscarding] = useState(false);
512
+ const [autosaveStatus, setAutosaveStatus] = useState<BlockEditorSaveStatus>("idle");
513
+ const [message, setMessage] = useState<string | null>(null);
514
+ const autosaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
515
+ const activeInfoRef = useRef<NextblockVisualEditInfo | null>(null);
516
+ const activeDocumentRef = useRef<ActiveDocument | null>(null);
517
+ const latestContentRef = useRef<unknown>(null);
518
+ const lastSavedContentRef = useRef<string>("");
519
+ const lastFailedContentRef = useRef<string>("");
520
+ const isAutosaveInFlightRef = useRef(false);
521
+ const activeAutosavePromiseRef = useRef<Promise<boolean> | null>(null);
522
+ const hasQueuedAutosaveRef = useRef(false);
523
+ const hasSavedSinceOpenRef = useRef(false);
524
+ const skipSaveOnBlurRef = useRef(false);
525
+
526
+ useEffect(() => {
527
+ activeInfoRef.current = activeInfo;
528
+ }, [activeInfo]);
529
+
530
+ useEffect(() => {
531
+ activeDocumentRef.current = activeDocument;
532
+ }, [activeDocument]);
533
+
534
+ const clearAutosaveTimer = useCallback(() => {
535
+ if (autosaveTimerRef.current) {
536
+ clearTimeout(autosaveTimerRef.current);
537
+ autosaveTimerRef.current = null;
538
+ }
539
+ }, []);
540
+
541
+ const performAutosave = useCallback(async (): Promise<boolean> => {
542
+ const info = activeInfoRef.current;
543
+ if (!info) {
544
+ return true;
545
+ }
546
+
547
+ clearAutosaveTimer();
548
+
549
+ const nextContent = latestContentRef.current;
550
+ const nextSerialized = serializeDraftContent(nextContent);
551
+ if (nextSerialized === lastSavedContentRef.current) {
552
+ setAutosaveStatus("saved");
553
+ return true;
554
+ }
555
+
556
+ if (nextSerialized === lastFailedContentRef.current && autosaveStatus === "error") {
557
+ return false;
558
+ }
559
+
560
+ if (isAutosaveInFlightRef.current) {
561
+ hasQueuedAutosaveRef.current = true;
562
+ setAutosaveStatus("dirty");
563
+ return activeAutosavePromiseRef.current ?? false;
564
+ }
565
+
566
+ const autosaveOperation = (async () => {
567
+ isAutosaveInFlightRef.current = true;
568
+ hasQueuedAutosaveRef.current = false;
569
+ setIsSavingBlock(true);
570
+ setAutosaveStatus("saving");
571
+ setMessage(null);
572
+
573
+ try {
574
+ const result = isProductFieldInfo(info)
575
+ ? await saveVisualEditingProductDraftViaApi(
576
+ productFieldRequestFromInfo(info),
577
+ nextContent as Json
578
+ )
579
+ : await saveVisualEditingBlockDraftViaApi(
580
+ blockRequestFromInfo(info),
581
+ nextContent as Json
582
+ );
583
+ const actionError = getActionError(result);
584
+
585
+ if (actionError) {
586
+ lastFailedContentRef.current = nextSerialized;
587
+ setAutosaveStatus("error");
588
+ setMessage(actionError);
589
+ return false;
590
+ }
591
+ } catch (error) {
592
+ lastFailedContentRef.current = nextSerialized;
593
+ setAutosaveStatus("error");
594
+ setMessage(error instanceof Error ? error.message : "Failed to save draft.");
595
+ return false;
596
+ } finally {
597
+ isAutosaveInFlightRef.current = false;
598
+ setIsSavingBlock(false);
599
+ }
600
+
601
+ lastSavedContentRef.current = nextSerialized;
602
+ lastFailedContentRef.current = "";
603
+ hasSavedSinceOpenRef.current = true;
604
+ setAutosaveStatus("saved");
605
+ setMessage(null);
606
+
607
+ if (hasQueuedAutosaveRef.current) {
608
+ hasQueuedAutosaveRef.current = false;
609
+ return performAutosave();
610
+ }
611
+
612
+ return true;
613
+ })();
614
+
615
+ activeAutosavePromiseRef.current = autosaveOperation;
616
+
617
+ try {
618
+ return await autosaveOperation;
619
+ } finally {
620
+ if (activeAutosavePromiseRef.current === autosaveOperation) {
621
+ activeAutosavePromiseRef.current = null;
622
+ }
623
+ }
624
+ }, [autosaveStatus, clearAutosaveTimer]);
625
+
626
+ const scheduleAutosave = useCallback(
627
+ (content: unknown) => {
628
+ latestContentRef.current = content;
629
+
630
+ const nextSerialized = serializeDraftContent(content);
631
+ if (nextSerialized === lastSavedContentRef.current) {
632
+ clearAutosaveTimer();
633
+ setAutosaveStatus("saved");
634
+ return;
635
+ }
636
+
637
+ if (nextSerialized === lastFailedContentRef.current && autosaveStatus === "error") {
638
+ return;
639
+ }
640
+
641
+ lastFailedContentRef.current = "";
642
+ setAutosaveStatus("dirty");
643
+
644
+ if (isAutosaveInFlightRef.current) {
645
+ hasQueuedAutosaveRef.current = true;
646
+ return;
647
+ }
648
+
649
+ clearAutosaveTimer();
650
+ autosaveTimerRef.current = setTimeout(() => {
651
+ void performAutosave();
652
+ }, VISUAL_DRAFT_AUTOSAVE_DELAY_MS);
653
+ },
654
+ [autosaveStatus, clearAutosaveTimer, performAutosave]
655
+ );
656
+
657
+ useEffect(() => {
658
+ return () => {
659
+ clearAutosaveTimer();
660
+ };
661
+ }, [clearAutosaveTimer]);
662
+
663
+ const openEditor = useCallback(async (
664
+ info: NextblockVisualEditInfo,
665
+ sourceElement?: HTMLElement | null
666
+ ) => {
667
+ const editorElement = sourceElement ?? findElementForInfo(info);
668
+ const document = documentFromInfo(info);
669
+ activeInfoRef.current = info;
670
+ activeDocumentRef.current = document;
671
+ setActiveInfo(info);
672
+ setActiveDocument(document);
673
+ setActiveBlock(null);
674
+ setActiveSurfaceContext(getVisualEditorSurfaceContext(editorElement));
675
+ setActiveModalTitle(
676
+ isProductFieldInfo(info) ? `Edit ${info.data.target.label}` : undefined
677
+ );
678
+ setIsEditorOpen(true);
679
+ setIsLoadingBlock(true);
680
+ clearAutosaveTimer();
681
+ skipSaveOnBlurRef.current = false;
682
+ latestContentRef.current = null;
683
+ lastSavedContentRef.current = "";
684
+ lastFailedContentRef.current = "";
685
+ isAutosaveInFlightRef.current = false;
686
+ activeAutosavePromiseRef.current = null;
687
+ hasQueuedAutosaveRef.current = false;
688
+ hasSavedSinceOpenRef.current = false;
689
+ setAutosaveStatus("idle");
690
+ setMessage(null);
691
+
692
+ const result = isProductFieldInfo(info)
693
+ ? await loadVisualEditingProductField(productFieldRequestFromInfo(info))
694
+ : await loadVisualEditingBlockContent(blockRequestFromInfo(info));
695
+ setIsLoadingBlock(false);
696
+ const actionError = getActionError(result);
697
+
698
+ if (actionError) {
699
+ setMessage(actionError);
700
+ return;
701
+ }
702
+
703
+ const blockType = isProductFieldInfo(info)
704
+ ? "text"
705
+ : (blockRequestFromInfo(info).target.blockType as BlockType);
706
+ const content = "content" in result ? result.content : null;
707
+ latestContentRef.current = content;
708
+ lastSavedContentRef.current = serializeDraftContent(content);
709
+ setActiveBlock({
710
+ type: blockType,
711
+ content,
712
+ productField: isProductFieldInfo(info) ? info.data.target.field : undefined,
713
+ });
714
+ }, [clearAutosaveTimer]);
715
+
716
+ const flushAutosaveBeforeClose = useCallback(async () => {
717
+ clearAutosaveTimer();
718
+ if (autosaveStatus === "error") {
719
+ return true;
720
+ }
721
+
722
+ const didSave = await performAutosave();
723
+
724
+ if (didSave && hasSavedSinceOpenRef.current) {
725
+ hasSavedSinceOpenRef.current = false;
726
+ router.refresh();
727
+ }
728
+
729
+ return didSave;
730
+ }, [autosaveStatus, clearAutosaveTimer, performAutosave, router]);
731
+
732
+ const startInlineEditing = useCallback(async (
733
+ target: HTMLElement,
734
+ info: NextblockVisualEditInfo
735
+ ) => {
736
+ // Prevent multiple inline edits at the same time
737
+ if (target.contentEditable === "true") {
738
+ return;
739
+ }
740
+
741
+ const document = documentFromInfo(info);
742
+ activeInfoRef.current = info;
743
+ activeDocumentRef.current = document;
744
+ setActiveInfo(info);
745
+ setActiveDocument(document);
746
+ clearAutosaveTimer();
747
+ skipSaveOnBlurRef.current = false;
748
+ latestContentRef.current = null;
749
+ lastSavedContentRef.current = "";
750
+ lastFailedContentRef.current = "";
751
+ isAutosaveInFlightRef.current = false;
752
+ activeAutosavePromiseRef.current = null;
753
+ hasQueuedAutosaveRef.current = false;
754
+ hasSavedSinceOpenRef.current = false;
755
+ setAutosaveStatus("idle");
756
+ setMessage(null);
757
+
758
+ // Add visual loading state
759
+ target.style.opacity = "0.7";
760
+ const result = isProductFieldInfo(info)
761
+ ? await loadVisualEditingProductField(productFieldRequestFromInfo(info))
762
+ : await loadVisualEditingBlockContent(blockRequestFromInfo(info));
763
+ target.style.opacity = "";
764
+
765
+ const actionError = getActionError(result);
766
+ if (actionError) {
767
+ setMessage(actionError);
768
+ return;
769
+ }
770
+
771
+ const content = "content" in result ? result.content : null;
772
+ if (!content || typeof content !== "object") {
773
+ return;
774
+ }
775
+
776
+ latestContentRef.current = content;
777
+ lastSavedContentRef.current = serializeDraftContent(content);
778
+
779
+ const blockType = "blockType" in info.data.target ? info.data.target.blockType : null;
780
+
781
+ // Enable contentEditable
782
+ target.contentEditable = "true";
783
+ target.focus();
784
+
785
+ // Add visual indicators for active editing
786
+ target.style.outline = "2px solid rgb(37 99 235 / 0.72)";
787
+ target.style.outlineOffset = "4px";
788
+
789
+ const handleKeyDown = (event: KeyboardEvent) => {
790
+ if (event.key === "Escape") {
791
+ event.preventDefault();
792
+ target.blur();
793
+ }
794
+ if (blockType === "heading" && event.key === "Enter") {
795
+ event.preventDefault();
796
+ target.blur();
797
+ }
798
+ };
799
+
800
+ const handleInput = () => {
801
+ const updatedContent = { ...content } as any;
802
+ if (blockType === "heading") {
803
+ updatedContent.text_content = target.innerText;
804
+ } else if (blockType === "text") {
805
+ updatedContent.html_content = target.innerHTML;
806
+ }
807
+ scheduleAutosave(updatedContent);
808
+ };
809
+
810
+ const handleBlur = async () => {
811
+ target.contentEditable = "false";
812
+ target.style.outline = "";
813
+ target.style.outlineOffset = "";
814
+
815
+ target.removeEventListener("keydown", handleKeyDown);
816
+ target.removeEventListener("input", handleInput);
817
+ target.removeEventListener("blur", handleBlur);
818
+
819
+ if (skipSaveOnBlurRef.current) {
820
+ clearAutosaveTimer();
821
+ return;
822
+ }
823
+
824
+ // Flush autosave and refresh page
825
+ await flushAutosaveBeforeClose();
826
+ };
827
+
828
+ target.addEventListener("keydown", handleKeyDown);
829
+ target.addEventListener("input", handleInput);
830
+ target.addEventListener("blur", handleBlur);
831
+ }, [clearAutosaveTimer, flushAutosaveBeforeClose, scheduleAutosave]);
832
+
833
+ useEffect(() => {
834
+ const firstEditable = document.querySelector("[data-vercel-edit-info]");
835
+ const firstInfo = parseEditInfo(firstEditable);
836
+ if (firstInfo) {
837
+ const document = documentFromInfo(firstInfo);
838
+ activeDocumentRef.current = document;
839
+ setActiveDocument(document);
840
+ }
841
+
842
+ const handlePointerOver = (event: PointerEvent) => {
843
+ const target = event.target instanceof Element
844
+ ? event.target.closest("[data-vercel-edit-info]")
845
+ : null;
846
+ const info = parseEditInfo(target);
847
+
848
+ if (target instanceof HTMLElement && info) {
849
+ setHoverTarget({
850
+ element: target,
851
+ info,
852
+ rect: target.getBoundingClientRect(),
853
+ });
854
+ }
855
+ };
856
+
857
+ const handleEditTargetClick = (event: MouseEvent) => {
858
+ if (isEditorOpen || event.button !== 0) {
859
+ return;
860
+ }
861
+
862
+ const target = event.target instanceof Element
863
+ ? event.target.closest("[data-vercel-edit-info]")
864
+ : null;
865
+ const info = parseEditInfo(target);
866
+
867
+ if (!(target instanceof HTMLElement) || !info) {
868
+ return;
869
+ }
870
+
871
+ const blockType = "blockType" in info.data.target ? info.data.target.blockType : null;
872
+ const canEditInline = (blockType === "text" || blockType === "heading") && !isProductFieldInfo(info);
873
+
874
+ if (canEditInline) {
875
+ event.preventDefault();
876
+ event.stopPropagation();
877
+ void startInlineEditing(target, info);
878
+ return;
879
+ }
880
+
881
+ // Any other block (custom blocks, images, buttons, etc.): a single click
882
+ // anywhere on the block opens its config editor. The isEditorOpen guard
883
+ // above plus the modal overlay prevent clicks from opening another block.
884
+ event.preventDefault();
885
+ event.stopPropagation();
886
+ void openEditor(info, target);
887
+ };
888
+
889
+ const handleToolbarEdit = (event: Event) => {
890
+ const detail = (event as CustomEvent).detail as
891
+ | {
892
+ element?: HTMLElement;
893
+ target?: string;
894
+ editInfo?: NextblockVisualEditInfo;
895
+ }
896
+ | undefined;
897
+
898
+ const editInfo = detail?.editInfo;
899
+ const isNextblockEditInfo =
900
+ editInfo &&
901
+ typeof editInfo === "object" &&
902
+ editInfo.data &&
903
+ "parentType" in editInfo.data;
904
+
905
+ if (
906
+ editInfo?.origin === "nextblock" ||
907
+ editInfo?.origin === "https://nextblock-editor" ||
908
+ editInfo?.origin === "https://nextblock-editor.com" ||
909
+ editInfo?.origin === "https://nextblock.dev" ||
910
+ isNextblockEditInfo
911
+ ) {
912
+ if (editInfo) {
913
+ void openEditor(editInfo, detail?.element);
914
+ return;
915
+ }
916
+ }
917
+
918
+ const element =
919
+ detail?.element ??
920
+ (detail?.target ? document.querySelector(detail.target) : null) ??
921
+ document.querySelector("[data-vercel-edit-info]");
922
+ const info = parseEditInfo(element);
923
+
924
+ if (info) {
925
+ void openEditor(info, element instanceof HTMLElement ? element : null);
926
+ }
927
+ };
928
+
929
+ document.addEventListener("pointerover", handlePointerOver, true);
930
+ document.addEventListener("click", handleEditTargetClick, true);
931
+ window.addEventListener("edit:open", handleToolbarEdit);
932
+ window.addEventListener("vercel-toolbar:edit", handleToolbarEdit);
933
+ window.addEventListener("vercel-toolbar:visual-edit", handleToolbarEdit);
934
+ window.addEventListener("visual-editing:edit", handleToolbarEdit);
935
+
936
+ return () => {
937
+ document.removeEventListener("pointerover", handlePointerOver, true);
938
+ document.removeEventListener("click", handleEditTargetClick, true);
939
+ window.removeEventListener("edit:open", handleToolbarEdit);
940
+ window.removeEventListener("vercel-toolbar:edit", handleToolbarEdit);
941
+ window.removeEventListener("vercel-toolbar:visual-edit", handleToolbarEdit);
942
+ window.removeEventListener("visual-editing:edit", handleToolbarEdit);
943
+ };
944
+ }, [isEditorOpen, openEditor, startInlineEditing]);
945
+
946
+ const overlayPosition = useMemo(() => {
947
+ if (!hoverTarget) {
948
+ return null;
949
+ }
950
+
951
+ const viewportWidth = typeof window === "undefined" ? 1024 : window.innerWidth;
952
+ const top = Math.max(12, hoverTarget.rect.top + 8);
953
+ const left = Math.min(viewportWidth - 52, Math.max(12, hoverTarget.rect.right - 44));
954
+ return { top, left };
955
+ }, [hoverTarget]);
956
+
957
+ const closeVisualEditor = useCallback(() => {
958
+ clearAutosaveTimer();
959
+ activeInfoRef.current = null;
960
+ setIsEditorOpen(false);
961
+ setActiveInfo(null);
962
+ setActiveBlock(null);
963
+ setActiveSurfaceContext(null);
964
+ setActiveModalTitle(undefined);
965
+ }, [clearAutosaveTimer]);
966
+
967
+ const flushPendingEditorChanges = useCallback(async () => {
968
+ clearAutosaveTimer();
969
+
970
+ if (!activeInfoRef.current) {
971
+ return true;
972
+ }
973
+
974
+ if (autosaveStatus === "error") {
975
+ return false;
976
+ }
977
+
978
+ return performAutosave();
979
+ }, [autosaveStatus, clearAutosaveTimer, performAutosave]);
980
+
981
+ const publishDraft = useCallback(async () => {
982
+ const document = activeDocumentRef.current ?? activeDocument;
983
+ if (!document) {
984
+ return;
985
+ }
986
+
987
+ const didFlush = await flushPendingEditorChanges();
988
+ if (!didFlush) {
989
+ return;
990
+ }
991
+
992
+ setIsPublishing(true);
993
+ setMessage(null);
994
+ const result =
995
+ document.parentType === "product"
996
+ ? await publishVisualEditingProductDraft(String(document.parentId))
997
+ : await publishVisualEditingDraft(
998
+ document.parentType as NextblockDocumentType,
999
+ Number(document.parentId)
1000
+ );
1001
+ setIsPublishing(false);
1002
+ const actionError = getActionError(result);
1003
+
1004
+ if (actionError) {
1005
+ setMessage(actionError);
1006
+ return;
1007
+ }
1008
+
1009
+ setMessage("Draft published.");
1010
+ hasSavedSinceOpenRef.current = false;
1011
+ closeVisualEditor();
1012
+ router.refresh();
1013
+ }, [activeDocument, closeVisualEditor, flushPendingEditorChanges, router]);
1014
+
1015
+ const discardDraft = useCallback(async () => {
1016
+ const document = activeDocumentRef.current ?? activeDocument;
1017
+ if (!document) {
1018
+ return;
1019
+ }
1020
+
1021
+ skipSaveOnBlurRef.current = true;
1022
+ clearAutosaveTimer();
1023
+ setIsDiscarding(true);
1024
+ setMessage(null);
1025
+ const result =
1026
+ document.parentType === "product"
1027
+ ? await discardVisualEditingProductDraft(String(document.parentId))
1028
+ : await discardVisualEditingDraft(
1029
+ document.parentType as NextblockDocumentType,
1030
+ Number(document.parentId)
1031
+ );
1032
+ setIsDiscarding(false);
1033
+ const actionError = getActionError(result);
1034
+
1035
+ if (actionError) {
1036
+ setMessage(actionError);
1037
+ return;
1038
+ }
1039
+
1040
+ setMessage("Draft discarded.");
1041
+ hasSavedSinceOpenRef.current = false;
1042
+ closeVisualEditor();
1043
+ window.location.reload();
1044
+ }, [activeDocument, clearAutosaveTimer, closeVisualEditor]);
1045
+
1046
+ const EditorComponent =
1047
+ activeBlock && activeInfo && isProductFieldInfo(activeInfo)
1048
+ ? getProductFieldEditorComponent(activeInfo.data.target)
1049
+ : activeBlock
1050
+ ? getEditorComponent(activeBlock.type)
1051
+ : JsonBlockEditor;
1052
+
1053
+ const draftActions = (
1054
+ <>
1055
+ <Button
1056
+ type="button"
1057
+ variant="outline"
1058
+ size="sm"
1059
+ className="h-8 rounded-full border-amber-500/20 text-amber-800 dark:text-amber-300 hover:bg-amber-500/10 hover:text-amber-900 dark:hover:text-amber-200 px-3.5 text-xs"
1060
+ onClick={discardDraft}
1061
+ onMouseDown={() => {
1062
+ skipSaveOnBlurRef.current = true;
1063
+ }}
1064
+ disabled={!activeDocument || isPublishing || isDiscarding}
1065
+ >
1066
+ {isDiscarding ? (
1067
+ <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
1068
+ ) : (
1069
+ <Trash2 className="mr-1.5 h-3.5 w-3.5" />
1070
+ )}
1071
+ Discard
1072
+ </Button>
1073
+ <Button
1074
+ type="button"
1075
+ variant="default"
1076
+ size="sm"
1077
+ className="h-8 rounded-full bg-amber-600 hover:bg-amber-500 text-white border-0 shadow-md shadow-amber-600/10 px-4 text-xs font-medium"
1078
+ onClick={publishDraft}
1079
+ disabled={!activeDocument || isPublishing || isDiscarding || isSavingBlock}
1080
+ >
1081
+ {isPublishing ? (
1082
+ <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
1083
+ ) : (
1084
+ <CloudLightning className="mr-1.5 h-3.5 w-3.5" />
1085
+ )}
1086
+ Publish
1087
+ </Button>
1088
+ </>
1089
+ );
1090
+
1091
+ return (
1092
+ <>
1093
+ <style jsx global>{`
1094
+ [data-nextblock-visual-edit] {
1095
+ cursor: pointer;
1096
+ outline-offset: 3px;
1097
+ transition: outline-color 120ms ease, box-shadow 120ms ease;
1098
+ }
1099
+
1100
+ [data-nextblock-visual-edit]:hover {
1101
+ outline: 2px solid rgb(37 99 235 / 0.72);
1102
+ }
1103
+ `}</style>
1104
+
1105
+ {!isEditorOpen && overlayPosition && hoverTarget && (
1106
+ <button
1107
+ type="button"
1108
+ aria-label="Edit block"
1109
+ title="Edit block"
1110
+ className="fixed z-[1000] inline-flex h-9 w-9 items-center justify-center rounded-md border border-border bg-background text-foreground shadow-lg transition hover:bg-muted"
1111
+ style={overlayPosition}
1112
+ onClick={() => void openEditor(hoverTarget.info, hoverTarget.element)}
1113
+ >
1114
+ <Pencil className="h-4 w-4" />
1115
+ </button>
1116
+ )}
1117
+
1118
+ {!isEditorOpen && (
1119
+ <div
1120
+ data-nextblock-draft-toolbar
1121
+ className="fixed bottom-4 left-1/2 z-[1000] flex -translate-x-1/2 items-center gap-3.5 rounded-full border border-amber-500/30 bg-background/95 px-4 py-2 text-sm shadow-xl backdrop-blur-md transition-all duration-300 ease-in-out"
1122
+ >
1123
+ <div className="flex items-center gap-2 px-1">
1124
+ <div className="relative flex h-2.5 w-2.5">
1125
+ {isLoadingBlock || isSavingBlock ? (
1126
+ <Loader2 className="h-3.5 w-3.5 animate-spin text-amber-500 -ml-0.5 -mt-0.5" />
1127
+ ) : (
1128
+ <>
1129
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75"></span>
1130
+ <span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-amber-500"></span>
1131
+ </>
1132
+ )}
1133
+ </div>
1134
+ <span className="font-semibold text-amber-800 dark:text-amber-300 text-[11px] uppercase tracking-wider select-none min-w-[110px]">
1135
+ {isLoadingBlock
1136
+ ? "Loading block..."
1137
+ : isSavingBlock
1138
+ ? "Saving..."
1139
+ : message
1140
+ ? message
1141
+ : "Unpublished Draft"}
1142
+ </span>
1143
+ </div>
1144
+ <div className="h-4 w-[1px] bg-border" />
1145
+ <div className="flex items-center gap-2">
1146
+ {draftActions}
1147
+ </div>
1148
+ </div>
1149
+ )}
1150
+
1151
+ {activeBlock && (
1152
+ <BlockEditorModal
1153
+ block={activeBlock}
1154
+ isOpen={isEditorOpen}
1155
+ onClose={closeVisualEditor}
1156
+ onSave={() => {
1157
+ void performAutosave();
1158
+ }}
1159
+ EditorComponent={EditorComponent}
1160
+ editorSurfaceContext={activeSurfaceContext}
1161
+ titleOverride={activeModalTitle}
1162
+ useContextualSurface={!activeInfo || !isProductFieldInfo(activeInfo)}
1163
+ saveMode="autosave"
1164
+ saveStatus={autosaveStatus}
1165
+ saveStatusText={message && autosaveStatus === "error" ? message : undefined}
1166
+ onAutoChange={scheduleAutosave}
1167
+ onFlushBeforeClose={flushAutosaveBeforeClose}
1168
+ />
1169
+ )}
1170
+ </>
1171
+ );
1172
+ }