@riverbankcms/sdk 0.129.0 → 0.130.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 (441) hide show
  1. package/README.md +51 -266
  2. package/dist/{PreviewEditorSidebar-B43CJ0g_.mjs → PreviewEditorSidebar-DiEpbOdw.mjs} +313 -239
  3. package/dist/{PreviewEditorUI-DHirVkeM.mjs → PreviewEditorUI-C6MvxnIa.mjs} +3 -3
  4. package/dist/{PreviewQueryProvider-B1kbhaJD.mjs → PreviewQueryProvider-C2rnoPz6.mjs} +1 -1
  5. package/dist/{SdkPreviewModeRuntime-BJumZy1B.mjs → SdkPreviewModeRuntime-DRXA_m5L.mjs} +208 -211
  6. package/dist/{SiteChromeCustomizeContext-D9SlpwEj.mjs → SiteChromeCustomizeContext-BTHdv5_-.mjs} +6222 -6190
  7. package/dist/_dts/ai/src/contracts/commandExposure.d.ts +1 -1
  8. package/dist/_dts/ai/src/contracts/proposalPrimitives.d.ts +52 -0
  9. package/dist/_dts/ai/src/contracts/proposals.d.ts +9438 -33588
  10. package/dist/_dts/ai/src/contracts/siteStyleProposalSchemas.d.ts +32572 -0
  11. package/dist/_dts/api/src/appointmentSetup.d.ts +21 -9
  12. package/dist/_dts/api/src/bookingConfig.d.ts +4 -46
  13. package/dist/_dts/api/src/bookingManagementEndpoints.d.ts +13 -11
  14. package/dist/_dts/api/src/bookingRecords.d.ts +1 -0
  15. package/dist/_dts/api/src/contentRuntime.d.ts +3 -5
  16. package/dist/_dts/api/src/endpoints.d.ts +14 -21
  17. package/dist/_dts/api/src/eventManagementContracts.d.ts +76 -0
  18. package/dist/_dts/api/src/index.d.ts +5 -3
  19. package/dist/_dts/api/src/publicExperience/commerceEndpoints.d.ts +22 -47
  20. package/dist/_dts/api/src/sdk-event-management.d.ts +53 -0
  21. package/dist/_dts/api/src/sdk-event-schedule.d.ts +8 -6
  22. package/dist/_dts/api/src/sdk-event-upsert.d.ts +112 -20
  23. package/dist/_dts/api/src/sdkContracts.d.ts +74 -115
  24. package/dist/_dts/api/src/sdkManagedEventContracts.d.ts +35 -0
  25. package/dist/_dts/api/src/sdkThemeWire.d.ts +4517 -0
  26. package/dist/_dts/api/src/sitePlatformEndpoints.d.ts +18 -29
  27. package/dist/_dts/api/src/siteRuntimeEndpoints.d.ts +23 -37
  28. package/dist/_dts/blocks/src/contracts/index.d.ts +2 -0
  29. package/dist/_dts/blocks/src/contracts/non-block-islands.d.ts +6 -8
  30. package/dist/_dts/blocks/src/contracts/proof-block-islands.d.ts +36 -14
  31. package/dist/_dts/blocks/src/index.d.ts +5 -3
  32. package/dist/_dts/blocks/src/lib/media.d.ts +9 -539
  33. package/dist/_dts/blocks/src/system/blocks/location-map.d.ts +3 -2
  34. package/dist/_dts/blocks/src/system/blocks/media-text.d.ts +135 -90
  35. package/dist/_dts/blocks/src/system/blocks/shop.d.ts +2 -2
  36. package/dist/_dts/blocks/src/system/blocks/shop.shared.d.ts +37 -4
  37. package/dist/_dts/blocks/src/system/blocks/site-footer-nav-groups-layout.d.ts +1 -1
  38. package/dist/_dts/blocks/src/system/blocks/site-footer-runtime.d.ts +3 -0
  39. package/dist/_dts/blocks/src/system/componentFactory.d.ts +5 -5
  40. package/dist/_dts/blocks/src/system/customize/backgrounds.d.ts +8 -6
  41. package/dist/_dts/blocks/src/system/customize/serializeBackgroundOverride.d.ts +3 -0
  42. package/dist/_dts/blocks/src/system/fields/background.d.ts +2 -20
  43. package/dist/_dts/blocks/src/system/fields/backgroundContent.d.ts +17 -0
  44. package/dist/_dts/blocks/src/system/fields/backgroundContentAudit.d.ts +20 -0
  45. package/dist/_dts/blocks/src/system/fields/boxStyles.d.ts +5 -33
  46. package/dist/_dts/blocks/src/system/fragments/builder.d.ts +1 -0
  47. package/dist/_dts/blocks/src/system/fragments/types.d.ts +1 -0
  48. package/dist/_dts/blocks/src/system/helpers/index.d.ts +1 -1
  49. package/dist/_dts/blocks/src/system/helpers/resolveBackground.d.ts +10 -45
  50. package/dist/_dts/blocks/src/system/manifest/fieldValidation/index.d.ts +7 -4
  51. package/dist/_dts/blocks/src/system/manifest/fieldValidation/writeCanonicalization.d.ts +4 -0
  52. package/dist/_dts/blocks/src/system/manifest/schema.d.ts +123 -55
  53. package/dist/_dts/blocks/src/system/registry.d.ts +2 -0
  54. package/dist/_dts/blocks/src/system/runtime/api/types/shop.d.ts +2 -37
  55. package/dist/_dts/blocks/src/system/runtime/components/registry.client.d.ts +61 -24
  56. package/dist/_dts/blocks/src/system/runtime/components/runtime-backed-blocks.d.ts +30 -12
  57. package/dist/_dts/blocks/src/system/runtime/header/enhanceHeaderSection.d.ts +3 -5
  58. package/dist/_dts/blocks/src/system/runtime/header/headerScrollRuntime.d.ts +2 -0
  59. package/dist/_dts/blocks/src/system/runtime/header/runtimeContract.d.ts +22 -0
  60. package/dist/_dts/blocks/src/system/runtime/nodes/commerce/shared/display.d.ts +9 -2
  61. package/dist/_dts/blocks/src/system/runtime/nodes/commerce/shared/view.d.ts +3 -0
  62. package/dist/_dts/blocks/src/system/runtime/nodes/header-section.client.d.ts +2 -0
  63. package/dist/_dts/blocks/src/system/runtime/nodes/header-section.frame.d.ts +21 -0
  64. package/dist/_dts/blocks/src/system/runtime/nodes/header-section.server.d.ts +2 -0
  65. package/dist/_dts/blocks/src/system/runtime/nodes/shop.interactive.d.ts +41 -12
  66. package/dist/_dts/blocks/src/system/runtime/nodes/shop.pass-contract.d.ts +3 -0
  67. package/dist/_dts/blocks/src/system/runtime/nodes/shop.state.d.ts +3 -3
  68. package/dist/_dts/blocks/src/system/transforms/registry/_utils/surfaceForegroundStyles.d.ts +3 -1
  69. package/dist/_dts/blocks/src/system/transforms/registry/background.d.ts +1 -1
  70. package/dist/_dts/blocks/src/system/transforms/registry/boxStyles.d.ts +3 -1
  71. package/dist/_dts/blocks/src/system/transforms/registry/core.d.ts +4 -1
  72. package/dist/_dts/blocks/src/system/transforms/registry/headerRuntime.d.ts +3 -0
  73. package/dist/_dts/blocks/src/system/transforms/registry/index.d.ts +2 -1
  74. package/dist/_dts/blocks/src/system/transforms/registry/layout.d.ts +0 -3
  75. package/dist/_dts/blocks/src/system/transforms/typed.d.ts +41 -43
  76. package/dist/_dts/content-editor/src/FooterComposerOverview.d.ts +4 -2
  77. package/dist/_dts/content-editor/src/FooterComposerPanel.d.ts +2 -17
  78. package/dist/_dts/content-editor/src/FooterNavigationLayoutControl.d.ts +5 -0
  79. package/dist/_dts/content-editor/src/footerComposer.d.ts +2 -1
  80. package/dist/_dts/content-editor/src/footerComposerPanelTypes.d.ts +21 -0
  81. package/dist/_dts/content-editor/src/footerPreviewRuntime.d.ts +2 -1
  82. package/dist/_dts/content-editor/src/useFooterComposerState.d.ts +11 -1
  83. package/dist/_dts/content-editor/src/useFooterNavigationLayout.d.ts +26 -0
  84. package/dist/_dts/content-model/src/templates/defaults.d.ts +7 -0
  85. package/dist/_dts/core/src/booking-date-time.d.ts +2 -2
  86. package/dist/_dts/core/src/booking-time-zone.d.ts +6 -0
  87. package/dist/_dts/core/src/footer-composition-types.d.ts +4 -4
  88. package/dist/_dts/core/src/footer-navigation-layout.d.ts +4 -0
  89. package/dist/_dts/core/src/index.d.ts +1 -0
  90. package/dist/_dts/db/src/generated/supabase/database.types.d.ts +38 -18
  91. package/dist/_dts/media-core/src/index.d.ts +1 -0
  92. package/dist/_dts/preview-next/src/client/preview/FooterSidebarDisclosure.d.ts +8 -0
  93. package/dist/_dts/preview-next/src/client/preview/PageBlockEditorPanel.d.ts +5 -5
  94. package/dist/_dts/preview-next/src/client/preview/PreviewEditorSidebar.d.ts +15 -17
  95. package/dist/_dts/preview-next/src/client/preview/PreviewFooterEditorPanel.d.ts +20 -0
  96. package/dist/_dts/preview-next/src/client/preview/PreviewShell.d.ts +3 -5
  97. package/dist/_dts/preview-next/src/client/preview/PreviewShellLayout.d.ts +3 -5
  98. package/dist/_dts/preview-next/src/client/preview/SiteChromeCustomizeContext.d.ts +4 -4
  99. package/dist/_dts/preview-next/src/client/preview/StyleConfigurator.d.ts +3 -5
  100. package/dist/_dts/preview-next/src/client/preview/StyleConfigurator.state.d.ts +6 -7
  101. package/dist/_dts/preview-next/src/client/preview/siteChromeBrandPreferences.d.ts +7 -7
  102. package/dist/_dts/preview-next/src/client/preview/siteChromeCustomizeTypes.d.ts +4 -4
  103. package/dist/_dts/preview-next/src/client/preview/siteChromeQuickControlHistoryState.d.ts +2 -2
  104. package/dist/_dts/preview-next/src/client/preview/siteChromeSavePlan.d.ts +12 -11
  105. package/dist/_dts/preview-next/src/client/preview/siteChromeSelectionRows.d.ts +53 -0
  106. package/dist/_dts/preview-next/src/client/preview/siteStyleAccess.d.ts +1 -6
  107. package/dist/_dts/preview-next/src/client/preview/siteStylePreviewPlanner.d.ts +6 -15
  108. package/dist/_dts/preview-next/src/client/preview/styleConfiguratorSnapshot.d.ts +4 -6
  109. package/dist/_dts/sdk/src/config/types.d.ts +5 -4
  110. package/dist/_dts/sdk/src/config/validation.d.ts +2 -0
  111. package/dist/_dts/sdk/src/contracts/system-block-content.d.ts +4 -41
  112. package/dist/_dts/sdk/src/next/catch-all-theme-admission.d.ts +23 -0
  113. package/dist/_dts/sdk/src/next/index.d.ts +8 -6
  114. package/dist/_dts/sdk/src/next/resolveTheme.d.ts +1 -2
  115. package/dist/_dts/sdk/src/next/types.d.ts +10 -0
  116. package/dist/_dts/sdk/src/public-api/contracts.d.ts +2 -16
  117. package/dist/_dts/sdk/src/rendering/components/Layout.d.ts +3 -2
  118. package/dist/_dts/sdk/src/rendering/helpers/loadContent.d.ts +5 -3
  119. package/dist/_dts/sdk/src/rendering/helpers/pageRuntimeSupport.d.ts +1 -1
  120. package/dist/_dts/sdk/src/rendering/hooks/useContent.d.ts +4 -2
  121. package/dist/_dts/sdk/src/rendering/index.d.ts +2 -0
  122. package/dist/_dts/sdk/src/rendering/islands/types.d.ts +6 -1
  123. package/dist/_dts/sdk/src/rendering/server.d.ts +2 -0
  124. package/dist/_dts/sdk/src/rendering/themeAdmission.d.ts +11 -0
  125. package/dist/_dts/sdk/src/version.d.ts +1 -1
  126. package/dist/_dts/theme-core/src/blockOverrides.d.ts +1 -1
  127. package/dist/_dts/theme-core/src/boxBackgroundSchema.d.ts +184 -0
  128. package/dist/_dts/theme-core/src/buttons/buttonColourContract.d.ts +96 -0
  129. package/dist/_dts/theme-core/src/buttons/buttonSurfaceContext.d.ts +78 -0
  130. package/dist/_dts/theme-core/src/buttons/index.d.ts +2 -0
  131. package/dist/_dts/theme-core/src/buttons/parseThemeForPersistence.d.ts +2 -2
  132. package/dist/_dts/theme-core/src/buttons/resolveButtonColourStates.d.ts +9 -57
  133. package/dist/_dts/theme-core/src/buttons/resolvedButtonCss.d.ts +6 -3
  134. package/dist/_dts/theme-core/src/buttons/styles/domain.d.ts +8 -7
  135. package/dist/_dts/theme-core/src/buttons/styles/recipes/quietArrow.d.ts +1 -0
  136. package/dist/_dts/theme-core/src/buttons/styles/recipes/staticRecipes.d.ts +11 -0
  137. package/dist/_dts/theme-core/src/buttons/styles/registry.d.ts +12 -0
  138. package/dist/_dts/theme-core/src/buttons/styles/selection.d.ts +5 -0
  139. package/dist/_dts/theme-core/src/buttons/types.d.ts +51 -4
  140. package/dist/_dts/theme-core/src/generated/containerResponsiveThemeCss.d.ts +1 -1
  141. package/dist/_dts/theme-core/src/lib/media.d.ts +583 -10
  142. package/dist/_dts/theme-core/src/mock-themes/all.d.ts +5117 -1433
  143. package/dist/_dts/theme-core/src/palette/index.d.ts +1 -0
  144. package/dist/_dts/theme-core/src/schema.d.ts +5079 -2005
  145. package/dist/_dts/theme-core/src/site-styles/compilationPlan.d.ts +6 -14
  146. package/dist/_dts/theme-core/src/site-styles/curatedSiteStyles.d.ts +29 -102
  147. package/dist/_dts/theme-core/src/site-styles/customizableSurfaces.d.ts +3 -76
  148. package/dist/_dts/theme-core/src/site-styles/designState.d.ts +4 -4
  149. package/dist/_dts/theme-core/src/site-styles/generatedDesignPipeline.d.ts +3 -7
  150. package/dist/_dts/theme-core/src/site-styles/generatedDesignPipelineHelpers.d.ts +17 -0
  151. package/dist/_dts/theme-core/src/site-styles/generatedSchemas.d.ts +2 -2
  152. package/dist/_dts/theme-core/src/site-styles/headerLayoutCatalog.d.ts +281 -0
  153. package/dist/_dts/theme-core/src/site-styles/headerLayoutResolution.d.ts +13 -0
  154. package/dist/_dts/theme-core/src/site-styles/headerLayoutStyles.d.ts +6 -0
  155. package/dist/_dts/theme-core/src/site-styles/headerLayoutTypes.d.ts +142 -0
  156. package/dist/_dts/theme-core/src/site-styles/headerNavStyles.d.ts +6 -0
  157. package/dist/_dts/theme-core/src/site-styles/headerOpeningForeground.d.ts +7 -4
  158. package/dist/_dts/theme-core/src/site-styles/headerOpeningSection.d.ts +2 -2
  159. package/dist/_dts/theme-core/src/site-styles/headerStyleCompiler.d.ts +3 -0
  160. package/dist/_dts/theme-core/src/site-styles/imageSurfaceBackground.d.ts +14 -0
  161. package/dist/_dts/theme-core/src/site-styles/index.d.ts +1 -1
  162. package/dist/_dts/theme-core/src/site-styles/previewCompiler.d.ts +14 -41
  163. package/dist/_dts/theme-core/src/site-styles/runtimeBackgroundResolution.d.ts +23 -0
  164. package/dist/_dts/theme-core/src/site-styles/runtimeCompiler.d.ts +8 -6
  165. package/dist/_dts/theme-core/src/site-styles/siteChromeNavPreferences.d.ts +1 -1
  166. package/dist/client/{CourseRegistrationWizard-xRmA7REV.mjs → CourseRegistrationWizard-Bvtz9G9g.mjs} +5 -5
  167. package/dist/client/{DiscountCodeDisclosure-B9vriL5C.mjs → DiscountCodeDisclosure-Ur-Xx1U_.mjs} +1 -1
  168. package/dist/client/{EventCombined.client-BgVnVTlz.mjs → EventCombined.client-EhscD4U6.mjs} +2 -2
  169. package/dist/client/{EventListing.client-CqdsO7jO.mjs → EventListing.client-Bn26cFTY.mjs} +2 -2
  170. package/dist/client/{LeafletStaticMap-CKPxot9M.mjs → LeafletStaticMap-BUw9Ntzr.mjs} +1 -1
  171. package/dist/client/{StepIndicator-DGCYuBJ1.mjs → StepIndicator-BfbdiVQI.mjs} +2 -2
  172. package/dist/client/{accordion-BfN5Eqd-.mjs → accordion-CjIeof73.mjs} +1 -1
  173. package/dist/client/{bookingForm-DOvCpBFP.mjs → bookingForm-DwEeovf_.mjs} +5 -5
  174. package/dist/client/bookings.mjs +1028 -1038
  175. package/dist/client/{carousel-DPNmiH0f.mjs → carousel-BMrfgOba.mjs} +1 -1
  176. package/dist/client/{cart--egVXlrR.mjs → cart-rXEdxTdb.mjs} +3 -3
  177. package/dist/client/{checkout-DywWtMcq.mjs → checkout-D3JlSzh3.mjs} +3 -3
  178. package/dist/client/client.mjs +18324 -18224
  179. package/dist/client/{courseRegistration-Dk9qFVwt.mjs → courseRegistration-DO5XvMAA.mjs} +2 -2
  180. package/dist/client/{eventCalendar-Bkz2HjlS.mjs → eventCalendar-DO6rTqCn.mjs} +4 -4
  181. package/dist/client/{eventCombined-D4kipYWg.mjs → eventCombined-bJ3i_ovU.mjs} +2 -2
  182. package/dist/client/{eventDetails-Dz_5ix2Q.mjs → eventDetails-CCbrXE7o.mjs} +3 -3
  183. package/dist/client/{eventListing-D0vm-zST.mjs → eventListing-Bdlt9YTm.mjs} +2 -2
  184. package/dist/client/{eventRegistration-Cw1E4Q4p.mjs → eventRegistration-B02lt6iY.mjs} +6 -6
  185. package/dist/client/{form-C2Zf1n3K.mjs → form-DrUBAAox.mjs} +1 -1
  186. package/dist/client/{headerSection-CP05gzdc.mjs → headerSection-MzqvHL6T.mjs} +151 -104
  187. package/dist/client/hooks.mjs +4550 -4211
  188. package/dist/client/{locationMap-Cl3x9PYJ.mjs → locationMap-DG8BxTBm.mjs} +2 -2
  189. package/dist/client/{newsletterForm-DyEJoClm.mjs → newsletterForm-BzO-Td3p.mjs} +1 -1
  190. package/dist/client/{pricing-DGuNR1W4.mjs → pricing-NgK7ozkg.mjs} +1 -1
  191. package/dist/client/{productDetail-Cq-HrGew.mjs → productDetail-CfQ7rhVK.mjs} +3 -3
  192. package/dist/client/{productList-DbB3jwqd.mjs → productList-C23rOLUi.mjs} +3 -3
  193. package/dist/client/{propsDecoder-CiTuhUwY.mjs → propsDecoder-CAZdhwjs.mjs} +2166 -2157
  194. package/dist/client/rendering/client.mjs +3606 -4078
  195. package/dist/client/rendering/islands.mjs +18 -18
  196. package/dist/client/rendering.mjs +15969 -15843
  197. package/dist/client/{scrollToBlock-CShkRqab.mjs → scrollToBlock-DSSqVQr1.mjs} +1 -1
  198. package/dist/client/{shop-Cu4Ib-ZA.mjs → shop-BMToKLK7.mjs} +34 -23
  199. package/dist/client/{shop-commerce-BzpsOdh7.mjs → shop-commerce-DB4JRKmp.mjs} +4 -4
  200. package/dist/client/{shop-commerce-store-DaE7Fhj8.mjs → shop-commerce-store-BOqSIOuc.mjs} +1 -1
  201. package/dist/client/{shopShared-cF0c20vB.mjs → shopShared-B6SK9DZv.mjs} +9 -3
  202. package/dist/client/{useEventFilters-QZ3HMgaJ.mjs → useEventFilters-CULOa4nB.mjs} +2 -2
  203. package/dist/client/{usePolling-BKf0GcE-.mjs → usePolling-BGG_8-jl.mjs} +2 -2
  204. package/dist/preview-next/before-render.mjs +16 -27
  205. package/dist/preview-next/client/runtime.mjs +4 -4
  206. package/dist/preview-next/middleware.mjs +16 -27
  207. package/dist/{previewAuthStore-BesgU5Xu.mjs → previewAuthStore-_GXFRg2W.mjs} +16 -27
  208. package/dist/{sdk-runtime-CfZd5pAm.mjs → sdk-runtime-hhSOMShR.mjs} +2 -2
  209. package/dist/{separator-DkTe68HE.mjs → separator-flRaFc14.mjs} +1 -1
  210. package/dist/server/components.mjs +17802 -17875
  211. package/dist/server/config-validation.mjs +4729 -4416
  212. package/dist/server/config.mjs +4733 -4420
  213. package/dist/server/data.mjs +4533 -4211
  214. package/dist/server/date-time.mjs +22 -13
  215. package/dist/server/index.mjs +17 -28
  216. package/dist/server/manifest.mjs +4966 -0
  217. package/dist/server/next.mjs +17635 -17676
  218. package/dist/server/page-converter.mjs +3759 -4386
  219. package/dist/server/prebuild.mjs +1 -1
  220. package/dist/server/rendering/server.mjs +18960 -19049
  221. package/dist/server/rendering.mjs +17848 -17902
  222. package/dist/server/routing.mjs +4776 -4323
  223. package/dist/server/server.mjs +4609 -4264
  224. package/dist/server/theme-bridge.mjs +1361 -1850
  225. package/dist/server/theme.mjs +736 -605
  226. package/dist/styles/index.css +22 -3
  227. package/package.json +9 -12
  228. package/dist/_dts/api/src/event-presentation.d.ts +0 -14
  229. package/dist/_dts/blocks/src/definitions.d.ts +0 -14
  230. package/dist/_dts/preview-next/src/client/preview/siteChromeLookRows.d.ts +0 -66
  231. package/dist/_dts/sdk/src/cli/canonical-entry-policy.d.ts +0 -53
  232. package/dist/_dts/sdk/src/cli/commands/audit.d.ts +0 -101
  233. package/dist/_dts/sdk/src/cli/commands/block.d.ts +0 -24
  234. package/dist/_dts/sdk/src/cli/commands/compare.d.ts +0 -62
  235. package/dist/_dts/sdk/src/cli/commands/content-type.d.ts +0 -10
  236. package/dist/_dts/sdk/src/cli/commands/content.d.ts +0 -56
  237. package/dist/_dts/sdk/src/cli/commands/delete.d.ts +0 -14
  238. package/dist/_dts/sdk/src/cli/commands/deploy.d.ts +0 -43
  239. package/dist/_dts/sdk/src/cli/commands/drafts.d.ts +0 -18
  240. package/dist/_dts/sdk/src/cli/commands/entry.d.ts +0 -55
  241. package/dist/_dts/sdk/src/cli/commands/env.d.ts +0 -31
  242. package/dist/_dts/sdk/src/cli/commands/event.d.ts +0 -23
  243. package/dist/_dts/sdk/src/cli/commands/eventOccurrenceSchedule.d.ts +0 -12
  244. package/dist/_dts/sdk/src/cli/commands/identifiers.d.ts +0 -10
  245. package/dist/_dts/sdk/src/cli/commands/init-docs.d.ts +0 -13
  246. package/dist/_dts/sdk/src/cli/commands/manifest.d.ts +0 -10
  247. package/dist/_dts/sdk/src/cli/commands/media-validation.d.ts +0 -3
  248. package/dist/_dts/sdk/src/cli/commands/migrate.d.ts +0 -11
  249. package/dist/_dts/sdk/src/cli/commands/navigation.d.ts +0 -12
  250. package/dist/_dts/sdk/src/cli/commands/page.d.ts +0 -14
  251. package/dist/_dts/sdk/src/cli/commands/publish-all.d.ts +0 -50
  252. package/dist/_dts/sdk/src/cli/commands/pull-dated-offering-scope.d.ts +0 -10
  253. package/dist/_dts/sdk/src/cli/commands/pull-dry-run.d.ts +0 -70
  254. package/dist/_dts/sdk/src/cli/commands/pull-scope-diff.d.ts +0 -8
  255. package/dist/_dts/sdk/src/cli/commands/pull.d.ts +0 -43
  256. package/dist/_dts/sdk/src/cli/commands/push/consts.d.ts +0 -3
  257. package/dist/_dts/sdk/src/cli/commands/push/execute/diff.d.ts +0 -19
  258. package/dist/_dts/sdk/src/cli/commands/push/execute/footer.d.ts +0 -42
  259. package/dist/_dts/sdk/src/cli/commands/push/execute/localMediaReporting.d.ts +0 -23
  260. package/dist/_dts/sdk/src/cli/commands/push/execute/media.d.ts +0 -22
  261. package/dist/_dts/sdk/src/cli/commands/push/execute/metadata.d.ts +0 -1
  262. package/dist/_dts/sdk/src/cli/commands/push/execute/reporting.d.ts +0 -7
  263. package/dist/_dts/sdk/src/cli/commands/push/execute/theme.d.ts +0 -63
  264. package/dist/_dts/sdk/src/cli/commands/push/filter.d.ts +0 -15
  265. package/dist/_dts/sdk/src/cli/commands/push/metadata.d.ts +0 -29
  266. package/dist/_dts/sdk/src/cli/commands/push/normalizeLocalContent.d.ts +0 -2
  267. package/dist/_dts/sdk/src/cli/commands/push/options.d.ts +0 -32
  268. package/dist/_dts/sdk/src/cli/commands/push/publishGuidance.d.ts +0 -15
  269. package/dist/_dts/sdk/src/cli/commands/push/pushAllExtras.d.ts +0 -25
  270. package/dist/_dts/sdk/src/cli/commands/push/scopes/commandBackedDatedOfferingScope.d.ts +0 -83
  271. package/dist/_dts/sdk/src/cli/commands/push/scopes/eventCategories.d.ts +0 -4
  272. package/dist/_dts/sdk/src/cli/commands/push/scopes/events.d.ts +0 -4
  273. package/dist/_dts/sdk/src/cli/commands/push/scopes/mediaPrep.d.ts +0 -14
  274. package/dist/_dts/sdk/src/cli/commands/push/scopes/pushScopeGeneric.d.ts +0 -6
  275. package/dist/_dts/sdk/src/cli/commands/push/scopes/types.d.ts +0 -87
  276. package/dist/_dts/sdk/src/cli/commands/push/scopes/venues.d.ts +0 -4
  277. package/dist/_dts/sdk/src/cli/commands/push/stale-resolution.d.ts +0 -48
  278. package/dist/_dts/sdk/src/cli/commands/push/stale.d.ts +0 -17
  279. package/dist/_dts/sdk/src/cli/commands/push/verification.d.ts +0 -56
  280. package/dist/_dts/sdk/src/cli/commands/push/workflowPlan.d.ts +0 -34
  281. package/dist/_dts/sdk/src/cli/commands/push-execute.d.ts +0 -54
  282. package/dist/_dts/sdk/src/cli/commands/push.d.ts +0 -40
  283. package/dist/_dts/sdk/src/cli/commands/setup.d.ts +0 -3
  284. package/dist/_dts/sdk/src/cli/commands/style.d.ts +0 -60
  285. package/dist/_dts/sdk/src/cli/commands/verify.d.ts +0 -11
  286. package/dist/_dts/sdk/src/cli/commands/webhooks.d.ts +0 -44
  287. package/dist/_dts/sdk/src/cli/config-loader.d.ts +0 -47
  288. package/dist/_dts/sdk/src/cli/content/collectionWriters.d.ts +0 -23
  289. package/dist/_dts/sdk/src/cli/content/edit/format.d.ts +0 -2
  290. package/dist/_dts/sdk/src/cli/content/edit/path.d.ts +0 -14
  291. package/dist/_dts/sdk/src/cli/content/edit/planner.d.ts +0 -46
  292. package/dist/_dts/sdk/src/cli/content/entryPaths.d.ts +0 -6
  293. package/dist/_dts/sdk/src/cli/content/footerFile.d.ts +0 -10
  294. package/dist/_dts/sdk/src/cli/content/fs-utils.d.ts +0 -39
  295. package/dist/_dts/sdk/src/cli/content/legacyEventScheduleShapeError.d.ts +0 -5
  296. package/dist/_dts/sdk/src/cli/content/localEventSchedule.d.ts +0 -17
  297. package/dist/_dts/sdk/src/cli/content/media-manifest.d.ts +0 -13
  298. package/dist/_dts/sdk/src/cli/content/metadataAfterPush.d.ts +0 -25
  299. package/dist/_dts/sdk/src/cli/content/metadataStore.d.ts +0 -29
  300. package/dist/_dts/sdk/src/cli/content/reader.d.ts +0 -488
  301. package/dist/_dts/sdk/src/cli/content/writer.d.ts +0 -142
  302. package/dist/_dts/sdk/src/cli/env-scope.d.ts +0 -2
  303. package/dist/_dts/sdk/src/cli/env.d.ts +0 -71
  304. package/dist/_dts/sdk/src/cli/errors.d.ts +0 -62
  305. package/dist/_dts/sdk/src/cli/helpers/ai-runtime.d.ts +0 -2
  306. package/dist/_dts/sdk/src/cli/helpers.d.ts +0 -399
  307. package/dist/_dts/sdk/src/cli/index.d.ts +0 -11
  308. package/dist/_dts/sdk/src/cli/init-docs/constants.d.ts +0 -16
  309. package/dist/_dts/sdk/src/cli/init-docs/index.d.ts +0 -12
  310. package/dist/_dts/sdk/src/cli/init-docs/templates.d.ts +0 -18
  311. package/dist/_dts/sdk/src/cli/init-docs/zod-to-markdown.d.ts +0 -28
  312. package/dist/_dts/sdk/src/cli/load-config.d.ts +0 -14
  313. package/dist/_dts/sdk/src/cli/media/identifiers.d.ts +0 -18
  314. package/dist/_dts/sdk/src/cli/media/local-media.d.ts +0 -14
  315. package/dist/_dts/sdk/src/cli/media/local-sync.d.ts +0 -91
  316. package/dist/_dts/sdk/src/cli/media/portable.d.ts +0 -11
  317. package/dist/_dts/sdk/src/cli/media/rich-text.d.ts +0 -12
  318. package/dist/_dts/sdk/src/cli/media/value-utils.d.ts +0 -15
  319. package/dist/_dts/sdk/src/cli/media/write-guard.d.ts +0 -52
  320. package/dist/_dts/sdk/src/cli/merge-remote/entryLocal.d.ts +0 -3
  321. package/dist/_dts/sdk/src/cli/merge-remote/entryMerge.d.ts +0 -78
  322. package/dist/_dts/sdk/src/cli/merge-remote/entryMergePlan.d.ts +0 -45
  323. package/dist/_dts/sdk/src/cli/merge-remote/entryMergePush.d.ts +0 -24
  324. package/dist/_dts/sdk/src/cli/merge-remote/entryMergeReport.d.ts +0 -18
  325. package/dist/_dts/sdk/src/cli/merge-remote/entryRemote.d.ts +0 -9
  326. package/dist/_dts/sdk/src/cli/merge-remote/entrySnapshots.d.ts +0 -48
  327. package/dist/_dts/sdk/src/cli/merge-remote/mergeFieldChanges.d.ts +0 -22
  328. package/dist/_dts/sdk/src/cli/merge-remote/pageLocal.d.ts +0 -3
  329. package/dist/_dts/sdk/src/cli/merge-remote/pageMerge.d.ts +0 -78
  330. package/dist/_dts/sdk/src/cli/merge-remote/pageMergePlan.d.ts +0 -36
  331. package/dist/_dts/sdk/src/cli/merge-remote/pageMergePush.d.ts +0 -23
  332. package/dist/_dts/sdk/src/cli/merge-remote/pageMergeReport.d.ts +0 -17
  333. package/dist/_dts/sdk/src/cli/merge-remote/pageSnapshots.d.ts +0 -46
  334. package/dist/_dts/sdk/src/cli/merge-remote/sharedBaseSnapshots.d.ts +0 -32
  335. package/dist/_dts/sdk/src/cli/merge-remote/sharedMergePlan.d.ts +0 -27
  336. package/dist/_dts/sdk/src/cli/merge-remote/sharedMergeReport.d.ts +0 -63
  337. package/dist/_dts/sdk/src/cli/migrations/entries.d.ts +0 -28
  338. package/dist/_dts/sdk/src/cli/migrations/events.d.ts +0 -73
  339. package/dist/_dts/sdk/src/cli/navigationIdentity.d.ts +0 -11
  340. package/dist/_dts/sdk/src/cli/output.d.ts +0 -133
  341. package/dist/_dts/sdk/src/cli/program.d.ts +0 -29
  342. package/dist/_dts/sdk/src/cli/push-config.d.ts +0 -49
  343. package/dist/_dts/sdk/src/cli/setup/setupPlan.d.ts +0 -51
  344. package/dist/_dts/sdk/src/cli/site-commands/commandKeys.d.ts +0 -13
  345. package/dist/_dts/sdk/src/cli/site-commands/commandRuntime.d.ts +0 -170
  346. package/dist/_dts/sdk/src/cli/site-commands/commandRuntimeCompat.d.ts +0 -21
  347. package/dist/_dts/sdk/src/cli/site-commands/commandSurfaceDispatch.d.ts +0 -32
  348. package/dist/_dts/sdk/src/cli/site-commands/datedOfferingCommands.d.ts +0 -53
  349. package/dist/_dts/sdk/src/cli/site-commands/entryCommands.d.ts +0 -65
  350. package/dist/_dts/sdk/src/cli/site-commands/eventScheduleCompiler.d.ts +0 -24
  351. package/dist/_dts/sdk/src/cli/site-commands/footerCommands.d.ts +0 -68
  352. package/dist/_dts/sdk/src/cli/site-commands/formCommands.d.ts +0 -53
  353. package/dist/_dts/sdk/src/cli/site-commands/index.d.ts +0 -8
  354. package/dist/_dts/sdk/src/cli/site-commands/localBlockManifestResolver.d.ts +0 -31
  355. package/dist/_dts/sdk/src/cli/site-commands/navigationCommands.d.ts +0 -57
  356. package/dist/_dts/sdk/src/cli/site-commands/oneOffCommands.d.ts +0 -82
  357. package/dist/_dts/sdk/src/cli/site-commands/pageCommands.d.ts +0 -84
  358. package/dist/_dts/sdk/src/cli/site-commands/pushExecution.d.ts +0 -39
  359. package/dist/_dts/sdk/src/cli/site-commands/pushExecutionPlan.d.ts +0 -8
  360. package/dist/_dts/sdk/src/cli/site-commands/pushExecutionTypes.d.ts +0 -97
  361. package/dist/_dts/sdk/src/cli/site-commands/residualSettingsTrimming.d.ts +0 -15
  362. package/dist/_dts/sdk/src/cli/sync/diff.d.ts +0 -222
  363. package/dist/_dts/sdk/src/cli/sync/executor.d.ts +0 -84
  364. package/dist/_dts/sdk/src/cli/sync/field-diff.d.ts +0 -39
  365. package/dist/_dts/sdk/src/cli/sync/index.d.ts +0 -10
  366. package/dist/_dts/sdk/src/cli/sync/mapper.d.ts +0 -41
  367. package/dist/_dts/sdk/src/cli/sync/media-sync.d.ts +0 -15
  368. package/dist/_dts/sdk/src/cli/sync/media.d.ts +0 -158
  369. package/dist/_dts/sdk/src/cli/sync/syncResultAggregation.d.ts +0 -4
  370. package/dist/_dts/sdk/src/cli/sync/validation.d.ts +0 -68
  371. package/dist/_dts/sdk/src/cli/utils/checksum.d.ts +0 -1
  372. package/dist/_dts/sdk/src/client/management/blocks.d.ts +0 -19
  373. package/dist/_dts/sdk/src/client/management/catalog.d.ts +0 -10
  374. package/dist/_dts/sdk/src/client/management/entries.d.ts +0 -6
  375. package/dist/_dts/sdk/src/client/management/eventCategories.d.ts +0 -6
  376. package/dist/_dts/sdk/src/client/management/events.d.ts +0 -7
  377. package/dist/_dts/sdk/src/client/management/footer.d.ts +0 -9
  378. package/dist/_dts/sdk/src/client/management/forms.d.ts +0 -6
  379. package/dist/_dts/sdk/src/client/management/http.d.ts +0 -79
  380. package/dist/_dts/sdk/src/client/management/identifiers.d.ts +0 -11
  381. package/dist/_dts/sdk/src/client/management/index.d.ts +0 -41
  382. package/dist/_dts/sdk/src/client/management/media.d.ts +0 -72
  383. package/dist/_dts/sdk/src/client/management/navigation.d.ts +0 -6
  384. package/dist/_dts/sdk/src/client/management/pages.d.ts +0 -6
  385. package/dist/_dts/sdk/src/client/management/pull.d.ts +0 -6
  386. package/dist/_dts/sdk/src/client/management/settings-branding.d.ts +0 -32
  387. package/dist/_dts/sdk/src/client/management/settings.d.ts +0 -6
  388. package/dist/_dts/sdk/src/client/management/theme.d.ts +0 -8
  389. package/dist/_dts/sdk/src/client/management/types.d.ts +0 -1069
  390. package/dist/_dts/sdk/src/client/management/venues.d.ts +0 -6
  391. package/dist/_dts/sdk/src/client/management/webhooks.d.ts +0 -3
  392. package/dist/_dts/sdk/src/test/env.d.ts +0 -4
  393. package/dist/_dts/site-commands/src/adapter.d.ts +0 -23
  394. package/dist/_dts/site-commands/src/appointmentCommandSchemas.d.ts +0 -100
  395. package/dist/_dts/site-commands/src/capabilityGaps.d.ts +0 -8
  396. package/dist/_dts/site-commands/src/commandContract.d.ts +0 -1277
  397. package/dist/_dts/site-commands/src/commandDomain.d.ts +0 -20
  398. package/dist/_dts/site-commands/src/commandIdentifiers.d.ts +0 -7
  399. package/dist/_dts/site-commands/src/commandValidation.d.ts +0 -10
  400. package/dist/_dts/site-commands/src/commands.d.ts +0 -1983
  401. package/dist/_dts/site-commands/src/domain.d.ts +0 -104
  402. package/dist/_dts/site-commands/src/eventCommandContent.d.ts +0 -11
  403. package/dist/_dts/site-commands/src/exposure.d.ts +0 -41
  404. package/dist/_dts/site-commands/src/guards.d.ts +0 -1
  405. package/dist/_dts/site-commands/src/index.d.ts +0 -18
  406. package/dist/_dts/site-commands/src/metadata.d.ts +0 -555
  407. package/dist/_dts/site-commands/src/pagePaths.d.ts +0 -6
  408. package/dist/_dts/site-commands/src/planner.d.ts +0 -58
  409. package/dist/_dts/site-commands/src/refContributions.d.ts +0 -11
  410. package/dist/_dts/site-commands/src/refs.d.ts +0 -90
  411. package/dist/_dts/site-commands/src/siteStyleCommandSchema.d.ts +0 -405
  412. package/dist/_dts/site-commands/src/siteStyleSelectionSchemaBuilder.d.ts +0 -46
  413. package/dist/_dts/site-commands/src/stableJson.d.ts +0 -5
  414. package/dist/_dts/site-commands/src/staticExecutionGaps.d.ts +0 -10
  415. package/dist/_dts/site-commands/src/venueCommandMapping.d.ts +0 -21
  416. package/dist/_dts/theme-core/src/data.d.ts +0 -12
  417. package/dist/_dts/theme-core/src/site-styles/headerLooks.d.ts +0 -841
  418. package/dist/_dts/theme-core/src/site-styles/headerStyleParts.d.ts +0 -4
  419. package/dist/cli/index.mjs +0 -162988
  420. package/dist/cli/init-docs/content/agents-section.md +0 -67
  421. package/dist/cli/init-docs/content/cli-reference.md +0 -1238
  422. package/dist/cli/init-docs/content/content-management.md +0 -792
  423. package/dist/cli/init-docs/content/context-brand.md +0 -125
  424. package/dist/cli/init-docs/content/context-brief.md +0 -77
  425. package/dist/cli/init-docs/content/context-knowledge.md +0 -111
  426. package/dist/cli/init-docs/content/getting-started.md +0 -171
  427. package/dist/cli/init-docs/content/site-workflows-readme.md +0 -96
  428. package/dist/cli/init-docs/content/workflow-add-block.md +0 -299
  429. package/dist/cli/init-docs/content/workflow-agent-safe-sync.md +0 -83
  430. package/dist/cli/init-docs/content/workflow-block-extensions.md +0 -370
  431. package/dist/cli/init-docs/content/workflow-cmsify-page.md +0 -357
  432. package/dist/cli/init-docs/content/workflow-content-types.md +0 -330
  433. package/dist/cli/init-docs/content/workflow-create-page.md +0 -212
  434. package/dist/cli/init-docs/content/workflow-custom-block.md +0 -470
  435. package/dist/cli/init-docs/content/workflow-editor-workflows.md +0 -130
  436. package/dist/cli/init-docs/content/workflow-isr-revalidation.md +0 -158
  437. package/dist/cli/init-docs/content/workflow-preview-mode.md +0 -252
  438. package/dist/cli/init-docs/content/workflow-publish.md +0 -285
  439. package/dist/cli/init-docs/content/workflow-remote-setup.md +0 -51
  440. package/dist/cli/init-docs/content/workflow-templates.md +0 -381
  441. /package/dist/_dts/{sdk/src/cli/utils/mime.d.ts → media-core/src/mimeType.d.ts} +0 -0
@@ -0,0 +1,4966 @@
1
+ import { z } from "zod";
2
+ import * as fs from "fs/promises";
3
+ import * as path from "path";
4
+ //#region ../blocks/src/system/node/validation-constants.ts
5
+ /**
6
+ * Constants for validating declarative node layouts.
7
+ *
8
+ * These whitelists define which node types and transform IDs
9
+ * are allowed in custom block layouts.
10
+ */
11
+ /**
12
+ * Valid node types for declarative layouts.
13
+ *
14
+ * This whitelist defines which component types can be used in
15
+ * SdkCustomBlock.layout definitions. Node types not in this list
16
+ * will be rejected during validation.
17
+ */
18
+ const ALLOWED_NODE_TYPES = [
19
+ "div",
20
+ "figure",
21
+ "section",
22
+ "headerSection",
23
+ "stack",
24
+ "inline",
25
+ "grid",
26
+ "text",
27
+ "richText",
28
+ "media",
29
+ "videoEmbed",
30
+ "icon",
31
+ "feature-card-icon",
32
+ "svg",
33
+ "button",
34
+ "link",
35
+ "carousel",
36
+ "accordion",
37
+ "accordionItem",
38
+ "form",
39
+ "booking-form",
40
+ "weekly-slot-availability",
41
+ "event-registration",
42
+ "event-spotlight",
43
+ "event-listing",
44
+ "event-calendar",
45
+ "offerings-preview",
46
+ "event-combined",
47
+ "shop",
48
+ "product-list",
49
+ "product-detail",
50
+ "cart",
51
+ "checkout",
52
+ "gifting"
53
+ ];
54
+ /**
55
+ * Valid transform IDs for declarative layouts.
56
+ *
57
+ * This whitelist defines which transforms can be referenced in
58
+ * $bind.transforms within custom block layouts. Transform IDs
59
+ * not in this list will be rejected during validation.
60
+ */
61
+ const ALLOWED_TRANSFORM_IDS = [
62
+ "string.uppercase",
63
+ "value.fallback",
64
+ "date.formatShort",
65
+ "array.length",
66
+ "value.jsonArray",
67
+ "value.csv",
68
+ "array.marqueeLoop",
69
+ "value.mapToken",
70
+ "layout.maxWidthClass",
71
+ "layout.headerRootClass",
72
+ "layout.footerRootClass",
73
+ "layout.headerRootStyle",
74
+ "layout.headerSectionBackground",
75
+ "layout.classicNavMode",
76
+ "layout.footerRootStyle",
77
+ "ui.headerNavLinkClass",
78
+ "ui.headingClassFromAlignment",
79
+ "ui.bodyClassFromAlignment",
80
+ "ui.stackAlignFromAlignment",
81
+ "ui.sectionStateTone",
82
+ "ui.heroContentWrapperClass",
83
+ "ui.heroCtaJustify",
84
+ "ui.heroStackJustifyFromVerticalAlignment",
85
+ "ui.heroGridClassFromVerticalAlignment",
86
+ "ui.conditionalClass",
87
+ "ui.twoColumnContainerClass",
88
+ "ui.twoColumnFlexBasis",
89
+ "ui.imageGalleryFilterClass",
90
+ "ui.imageGalleryTileClass",
91
+ "ui.imageGalleryTileStyle",
92
+ "ui.imageGalleryMarqueeTileStyle",
93
+ "ui.imageGalleryMarqueeDuration",
94
+ "ui.imageGalleryMarqueeDirection",
95
+ "ui.imageFragmentClass",
96
+ "ui.imageFragmentSize",
97
+ "media.fromUrl",
98
+ "background.resolveStyle",
99
+ "background.resolveColorsStyle",
100
+ "background.resolveFrameClassName",
101
+ "background.resolveImageStyle",
102
+ "background.resolveImageClassName",
103
+ "background.resolveOverlayStyle",
104
+ "links.resolve",
105
+ "links.label",
106
+ "content.interpolateRichTextTokens",
107
+ "boxStyles.spacing",
108
+ "boxStyles.resolveRootClass",
109
+ "boxStyles.resolveRootStyle",
110
+ "boxStyles.semanticGap",
111
+ "boxStyles.semanticGapValue",
112
+ "boxStyles.cardVariantClass",
113
+ "containerStyles.resolveBoxClassName"
114
+ ];
115
+ new Set(ALLOWED_NODE_TYPES);
116
+ new Set(ALLOWED_TRANSFORM_IDS);
117
+ //#endregion
118
+ //#region ../blocks/src/system/manifest/richTextSchema.ts
119
+ /**
120
+ * Shared Rich Text Schema
121
+ *
122
+ * This module provides a single source of truth for TipTap rich text validation
123
+ * across both the block-form editor and the block rendering system.
124
+ *
125
+ * Rich-text fields use one canonical root shape:
126
+ * `{ type: 'doc', content: [...] }`.
127
+ */
128
+ /**
129
+ * TipTap document node schema (recursive structure)
130
+ *
131
+ * Supports the full TipTap document structure including:
132
+ * - type: Node type (e.g., 'doc', 'paragraph', 'text')
133
+ * - content: Nested array of child nodes
134
+ * - text: Text content for text nodes
135
+ * - attrs: Node attributes (e.g., alignment, color)
136
+ * - marks: Text formatting marks (e.g., bold, italic)
137
+ */
138
+ const TipTapMarkSchema = z.object({
139
+ type: z.string().min(1),
140
+ attrs: z.record(z.string(), z.unknown()).optional()
141
+ }).strict();
142
+ const TipTapNodeSchema = z.lazy(() => z.object({
143
+ type: z.string().min(1),
144
+ content: z.array(TipTapNodeSchema).optional(),
145
+ text: z.string().optional(),
146
+ attrs: z.record(z.string(), z.unknown()).optional(),
147
+ marks: z.array(TipTapMarkSchema).optional()
148
+ }).strict().superRefine((node, ctx) => {
149
+ if (node.type === "doc") ctx.addIssue({
150
+ code: "custom",
151
+ path: ["type"],
152
+ message: "Document nodes are only valid at the rich-text root"
153
+ });
154
+ if (node.type === "text") {
155
+ if (node.text === void 0 || node.text.length === 0) ctx.addIssue({
156
+ code: "custom",
157
+ path: ["text"],
158
+ message: "Text nodes require a non-empty text value"
159
+ });
160
+ if (node.content !== void 0) ctx.addIssue({
161
+ code: "custom",
162
+ path: ["content"],
163
+ message: "Text nodes cannot contain child nodes"
164
+ });
165
+ return;
166
+ }
167
+ if (node.text !== void 0) ctx.addIssue({
168
+ code: "custom",
169
+ path: ["text"],
170
+ message: "Only text nodes can contain a text value"
171
+ });
172
+ }));
173
+ /**
174
+ * Canonical schema for a TipTap rich-text document root.
175
+ *
176
+ * Stricter than {@link TipTapNodeSchema}: `type` is literally `'doc'` and
177
+ * `content` is required (possibly empty). This is the shape every producer
178
+ * (dummy content, editor output, migrations) must emit, and the shape every
179
+ * non-legacy consumer can rely on.
180
+ *
181
+ */
182
+ const RichTextDocSchema = z.object({
183
+ type: z.literal("doc"),
184
+ content: z.array(TipTapNodeSchema)
185
+ }).strict();
186
+ function createHardBreakNode() {
187
+ return { type: "hardBreak" };
188
+ }
189
+ /**
190
+ * Parse a small markdown subset into TipTap inline nodes.
191
+ *
192
+ * This intentionally supports only bold (`**text**`) because the SDK/content
193
+ * import paths use it for lightweight default content, not full markdown.
194
+ */
195
+ function parseRichTextInlineMarkdown(text) {
196
+ const out = [];
197
+ const pushText = (value, marks) => {
198
+ if (!value) return;
199
+ out.push(marks && marks.length > 0 ? {
200
+ type: "text",
201
+ text: value,
202
+ marks
203
+ } : {
204
+ type: "text",
205
+ text: value
206
+ });
207
+ };
208
+ let rest = text;
209
+ while (rest.length > 0) {
210
+ const start = rest.indexOf("**");
211
+ if (start === -1) {
212
+ pushText(rest);
213
+ break;
214
+ }
215
+ const end = rest.indexOf("**", start + 2);
216
+ if (end === -1) {
217
+ pushText(rest);
218
+ break;
219
+ }
220
+ pushText(rest.slice(0, start));
221
+ pushText(rest.slice(start + 2, end), [{ type: "bold" }]);
222
+ rest = rest.slice(end + 2);
223
+ }
224
+ return out;
225
+ }
226
+ function parseRichTextParagraphInlineMarkdown(text) {
227
+ return text.split("\n").flatMap((line, index) => {
228
+ const lineNodes = parseRichTextInlineMarkdown(line.trim());
229
+ return index === 0 ? lineNodes : [createHardBreakNode(), ...lineNodes];
230
+ });
231
+ }
232
+ /**
233
+ * Convert a plain string with a tiny markdown subset to canonical TipTap JSON.
234
+ *
235
+ * Double newlines create semantic paragraphs. Single newlines remain inside the
236
+ * current paragraph as TipTap `hardBreak` nodes for line-oriented content such
237
+ * as addresses, opening hours, and contact details.
238
+ */
239
+ function createRichTextDocFromMarkdown(markdown) {
240
+ const normalized = markdown.replace(/\r\n/g, "\n").trim();
241
+ if (!normalized) return {
242
+ type: "doc",
243
+ content: []
244
+ };
245
+ return {
246
+ type: "doc",
247
+ content: normalized.split(/\n{2,}/).map((paragraph) => paragraph.trim()).filter(Boolean).map((paragraph) => {
248
+ const inline = parseRichTextParagraphInlineMarkdown(paragraph);
249
+ return inline.length > 0 ? {
250
+ type: "paragraph",
251
+ content: inline
252
+ } : { type: "paragraph" };
253
+ })
254
+ };
255
+ }
256
+ //#endregion
257
+ //#region src/manifest/debug.ts
258
+ /**
259
+ * Create a debug writer that outputs to a timestamped subdirectory.
260
+ *
261
+ * Each invocation creates a unique directory like:
262
+ * `{debugDir}/manifest-20260126-083345/`
263
+ *
264
+ * This avoids the dangerous pattern of clearing arbitrary directories.
265
+ */
266
+ function createDebugWriter(debugDir) {
267
+ if (!debugDir) return {
268
+ outputDir: void 0,
269
+ writeText: async () => {},
270
+ writeJson: async () => {}
271
+ };
272
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
273
+ const runDir = path.join(debugDir, `manifest-${timestamp}`);
274
+ let dirCreated = false;
275
+ async function ensureDir() {
276
+ if (!dirCreated) {
277
+ await fs.mkdir(runDir, { recursive: true });
278
+ dirCreated = true;
279
+ }
280
+ }
281
+ return {
282
+ outputDir: runDir,
283
+ async writeText(filename, content) {
284
+ try {
285
+ await ensureDir();
286
+ await fs.writeFile(path.join(runDir, filename), content, "utf-8");
287
+ } catch {}
288
+ },
289
+ async writeJson(filename, value) {
290
+ try {
291
+ await ensureDir();
292
+ await fs.writeFile(path.join(runDir, filename), JSON.stringify(value, null, 2), "utf-8");
293
+ } catch {}
294
+ }
295
+ };
296
+ }
297
+ //#endregion
298
+ //#region src/manifest/prompt/theme-summary.ts
299
+ /**
300
+ * Font weight name to numeric value mapping.
301
+ */
302
+ const WEIGHT_MAP = {
303
+ thin: "100",
304
+ extralight: "200",
305
+ light: "300",
306
+ regular: "400",
307
+ normal: "400",
308
+ medium: "500",
309
+ semibold: "600",
310
+ bold: "700",
311
+ extrabold: "800",
312
+ black: "900"
313
+ };
314
+ function asRecord(value) {
315
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
316
+ return value;
317
+ }
318
+ /**
319
+ * Extract unique typography weights from theme configuration.
320
+ *
321
+ * @param theme - The theme to extract weights from
322
+ * @returns Array of numeric weight strings (e.g., ['400', '600', '700'])
323
+ */
324
+ function extractTypographyWeights(theme) {
325
+ const typography = asRecord(asRecord(theme)?.typography);
326
+ const weights = /* @__PURE__ */ new Set();
327
+ const bodyWeight = (() => {
328
+ const body = asRecord(typography?.body);
329
+ return typeof body?.weight === "string" ? body.weight : null;
330
+ })();
331
+ if (bodyWeight) weights.add(WEIGHT_MAP[bodyWeight] || bodyWeight);
332
+ const headingWeight = (() => {
333
+ const def = asRecord(asRecord(typography?.headings)?.default);
334
+ return typeof def?.weight === "string" ? def.weight : null;
335
+ })();
336
+ if (headingWeight) weights.add(WEIGHT_MAP[headingWeight] || headingWeight);
337
+ for (const level of [
338
+ "h1",
339
+ "h2",
340
+ "h3",
341
+ "h4",
342
+ "h5",
343
+ "h6"
344
+ ]) {
345
+ const levelWeight = (() => {
346
+ const levelRec = asRecord(asRecord(typography?.headings)?.[level]);
347
+ return typeof levelRec?.weight === "string" ? levelRec.weight : null;
348
+ })();
349
+ if (levelWeight) weights.add(WEIGHT_MAP[levelWeight] || levelWeight);
350
+ }
351
+ const result = Array.from(weights).sort();
352
+ return result.length > 0 ? result : [
353
+ "400",
354
+ "600",
355
+ "700"
356
+ ];
357
+ }
358
+ /**
359
+ * Map corner style to actual CSS value for LLM understanding.
360
+ */
361
+ function getCornerRadius(corners) {
362
+ switch (corners) {
363
+ case "square": return "0px";
364
+ case "soft": return "4px";
365
+ case "rounded": return "8px";
366
+ case "pill": return "9999px";
367
+ default:
368
+ console.warn("Unknown corner style, defaulting to soft (4px)");
369
+ return "4px";
370
+ }
371
+ }
372
+ /**
373
+ * Infer usage hint for a color based on its name.
374
+ *
375
+ * @param name - The color name from the theme palette
376
+ * @returns A human-readable usage description
377
+ */
378
+ function inferColorUsage(name) {
379
+ const lower = name.toLowerCase();
380
+ if (lower.includes("primary")) return "buttons, links, primary accents";
381
+ if (lower.includes("secondary")) return "secondary buttons, subtle accents";
382
+ if (lower.includes("accent")) return "highlights, focus states, special emphasis";
383
+ if (lower.includes("background") || lower.includes("bg")) return "page/section backgrounds";
384
+ if (lower.includes("surface")) return "card backgrounds, elevated surfaces";
385
+ if (lower.includes("text") || lower.includes("foreground")) return "body text, headings";
386
+ if (lower.includes("muted")) return "subtle text, borders, disabled states";
387
+ if (lower.includes("error") || lower.includes("danger") || lower.includes("red")) return "error states, destructive actions";
388
+ if (lower.includes("success") || lower.includes("green")) return "success states, positive feedback";
389
+ if (lower.includes("warning") || lower.includes("yellow") || lower.includes("orange")) return "warning states, cautions";
390
+ if (lower.includes("info") || lower.includes("blue")) return "informational messages";
391
+ return "general styling";
392
+ }
393
+ /**
394
+ * Summarize theme for LLM context.
395
+ *
396
+ * Extracts the most relevant theme information for the LLM to understand
397
+ * available tokens without overwhelming the context window.
398
+ *
399
+ * @example
400
+ * ```typescript
401
+ * const summary = summarizeThemeForLLM(theme);
402
+ * // Use in system prompt to show available tokens
403
+ * ```
404
+ */
405
+ function summarizeThemeForLLM(theme) {
406
+ const themeRecord = asRecord(theme);
407
+ const paletteRec = asRecord(themeRecord?.palette);
408
+ const palette = (Array.isArray(paletteRec?.colors) ? paletteRec.colors : []).map((color) => asRecord(color)).filter((color) => Boolean(color)).filter((color) => typeof color.name === "string" && typeof color.hex === "string").map((color) => {
409
+ const token = String(color.name);
410
+ return {
411
+ token,
412
+ hex: String(color.hex),
413
+ usage: inferColorUsage(token)
414
+ };
415
+ });
416
+ const weights = extractTypographyWeights(theme);
417
+ const typographyRec = asRecord(themeRecord?.typography);
418
+ const bodyRec = asRecord(typographyRec?.body);
419
+ const headingsDefaultRec = asRecord(asRecord(typographyRec?.headings)?.default);
420
+ const bodyFamily = typeof bodyRec?.family === "string" ? bodyRec.family : "system-ui";
421
+ const headingFamily = typeof headingsDefaultRec?.family === "string" ? headingsDefaultRec.family : bodyFamily;
422
+ const spacing = typeof themeRecord?.space === "string" ? themeRecord.space : "standard";
423
+ const cornersValue = typeof themeRecord?.corners === "string" ? themeRecord.corners : "soft";
424
+ return {
425
+ palette,
426
+ typography: {
427
+ bodyFamily,
428
+ headingFamily,
429
+ weights
430
+ },
431
+ spacing,
432
+ corners: `${cornersValue} (${getCornerRadius(cornersValue)})`
433
+ };
434
+ }
435
+ /**
436
+ * Format theme summary as a string for inclusion in prompts.
437
+ */
438
+ function formatThemeSummaryForPrompt(summary) {
439
+ return [
440
+ "## Available Theme Tokens",
441
+ "",
442
+ "### Color Palette",
443
+ ...summary.palette.map((c) => `- **${c.token}**: ${c.hex} (${c.usage})`),
444
+ "",
445
+ "### Typography",
446
+ `- Body font: ${summary.typography.bodyFamily}`,
447
+ `- Heading font: ${summary.typography.headingFamily}`,
448
+ `- Available weights: ${summary.typography.weights.join(", ")}`,
449
+ "",
450
+ "### Spacing & Corners",
451
+ `- Spacing preset: ${summary.spacing}`,
452
+ `- Corner style: ${summary.corners}`
453
+ ].join("\n");
454
+ }
455
+ //#endregion
456
+ //#region src/manifest/validation/layout-utils.ts
457
+ /**
458
+ * Collect node information from a layout tree.
459
+ * Returns an array of NodeInfo objects with path, type, and binding information.
460
+ */
461
+ function collectLayoutNodeInfos(node, path = "layout", out = []) {
462
+ if (!node || typeof node !== "object") return out;
463
+ const n = node;
464
+ const type = typeof n.type === "string" ? n.type : "unknown";
465
+ let bindFrom = null;
466
+ if (n.$bind && typeof n.$bind === "object") {
467
+ const b = n.$bind;
468
+ if (typeof b.from === "string") bindFrom = b.from;
469
+ }
470
+ out.push({
471
+ path,
472
+ type,
473
+ bindFrom
474
+ });
475
+ if (Array.isArray(n.children)) for (let i = 0; i < n.children.length; i++) collectLayoutNodeInfos(n.children[i], `${path}.children[${i}]`, out);
476
+ return out;
477
+ }
478
+ /**
479
+ * Collect all classNames from a layout tree.
480
+ * Returns a Set of all className tokens found in node props.
481
+ */
482
+ function collectLayoutClassNames(node, out = /* @__PURE__ */ new Set()) {
483
+ if (!node || typeof node !== "object") return out;
484
+ const n = node;
485
+ if (n.props && typeof n.props === "object") {
486
+ const className = n.props.className;
487
+ if (typeof className === "string") for (const token of className.split(/\s+/).filter(Boolean)) out.add(token);
488
+ }
489
+ if (Array.isArray(n.children)) for (const child of n.children) collectLayoutClassNames(child, out);
490
+ return out;
491
+ }
492
+ //#endregion
493
+ //#region src/zod/titleSourceSchema.ts
494
+ const titleSourceValueSchema = z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]);
495
+ //#endregion
496
+ //#region src/manifest/validation/output-schema.ts
497
+ /**
498
+ * Zod Schema for LLM Output Validation
499
+ *
500
+ * Validates the raw output from the LLM before further processing.
501
+ * This schema is used with getAiResponse() for structured output.
502
+ *
503
+ * IMPORTANT: OpenAI structured output requires all schema fields to have explicit types.
504
+ * We cannot use z.any() - it must be replaced with explicit union types.
505
+ */
506
+ const blockCategoryEnum = z.enum([
507
+ "marketing",
508
+ "content",
509
+ "blog",
510
+ "media",
511
+ "layout",
512
+ "interactive"
513
+ ]);
514
+ /**
515
+ * Color object (used in palette.colors)
516
+ */
517
+ const llmColorValue = z.object({
518
+ name: z.string(),
519
+ hex: z.string()
520
+ });
521
+ /**
522
+ * Theme patch primitive values.
523
+ */
524
+ const llmThemePrimitive = z.union([
525
+ z.string(),
526
+ z.number(),
527
+ z.boolean(),
528
+ z.null()
529
+ ]);
530
+ /**
531
+ * Theme patch value - explicit union of allowed types.
532
+ * Covers: primitives, color objects, arrays of colors, and arrays of primitives.
533
+ */
534
+ const llmThemePatchValue = z.union([
535
+ llmThemePrimitive,
536
+ llmColorValue,
537
+ z.array(llmColorValue),
538
+ z.array(llmThemePrimitive)
539
+ ]);
540
+ /**
541
+ * LLM-compatible theme patch operation schema.
542
+ * Uses discriminated union for the 3 operation types.
543
+ */
544
+ const llmThemePatchOpSchema = z.discriminatedUnion("op", [
545
+ z.object({
546
+ op: z.literal("add"),
547
+ path: z.string().min(1),
548
+ value: llmThemePatchValue
549
+ }),
550
+ z.object({
551
+ op: z.literal("replace"),
552
+ path: z.string().min(1),
553
+ value: llmThemePatchValue
554
+ }),
555
+ z.object({
556
+ op: z.literal("remove"),
557
+ path: z.string().min(1)
558
+ })
559
+ ]);
560
+ /**
561
+ * CSS declaration - property/value pair.
562
+ * Using explicit object instead of z.record() for OpenAI compatibility.
563
+ */
564
+ const llmCssDeclaration = z.object({
565
+ property: z.string(),
566
+ value: z.string()
567
+ });
568
+ /**
569
+ * LLM-compatible custom CSS rule schema.
570
+ * Uses array of declarations instead of record for OpenAI compatibility.
571
+ */
572
+ const llmCustomCssRuleSchema = z.object({
573
+ id: z.string().min(1),
574
+ selector: z.string().min(1),
575
+ declarations: z.array(llmCssDeclaration)
576
+ });
577
+ /**
578
+ * LLM-compatible nested rule schema for @media/@supports rules.
579
+ * Uses array of declarations instead of record for OpenAI compatibility.
580
+ */
581
+ const llmNestedRuleSchema = z.object({
582
+ selector: z.string().min(1),
583
+ declarations: z.array(llmCssDeclaration)
584
+ });
585
+ /**
586
+ * LLM-compatible @media at-rule schema.
587
+ */
588
+ const llmMediaAtRuleSchema = z.object({
589
+ id: z.string().min(1),
590
+ type: z.literal("media"),
591
+ query: z.string().min(1),
592
+ rules: z.array(llmNestedRuleSchema)
593
+ });
594
+ /**
595
+ * LLM-compatible at-rule schema.
596
+ * For now, only supports @media rules from LLM output.
597
+ * Other at-rules (@keyframes, @font-face, @supports) can be added manually if needed.
598
+ */
599
+ const llmAtRuleSchema = llmMediaAtRuleSchema;
600
+ /**
601
+ * Convert array-of-declarations format to record format.
602
+ * LLM outputs: [{ property: "fontSize", value: "3rem" }]
603
+ * Runtime expects: { fontSize: "3rem" }
604
+ */
605
+ function normalizeDeclarationsArrayToRecord(declarations) {
606
+ const record = {};
607
+ for (const decl of declarations) record[decl.property] = decl.value;
608
+ return record;
609
+ }
610
+ /**
611
+ * Normalize LLM CSS rule output to runtime format.
612
+ */
613
+ function normalizeLlmCssRule(rule) {
614
+ return {
615
+ id: rule.id,
616
+ selector: rule.selector,
617
+ declarations: normalizeDeclarationsArrayToRecord(rule.declarations)
618
+ };
619
+ }
620
+ /**
621
+ * Normalize LLM nested rule output to runtime format.
622
+ */
623
+ function normalizeLlmNestedRule(rule) {
624
+ return {
625
+ selector: rule.selector,
626
+ declarations: normalizeDeclarationsArrayToRecord(rule.declarations)
627
+ };
628
+ }
629
+ /**
630
+ * Normalize LLM @media at-rule output to runtime format.
631
+ */
632
+ function normalizeLlmMediaAtRule(atRule) {
633
+ return {
634
+ id: atRule.id,
635
+ type: "media",
636
+ query: atRule.query,
637
+ rules: atRule.rules.map(normalizeLlmNestedRule)
638
+ };
639
+ }
640
+ /**
641
+ * Primitive JSON value types for defaultValue fields.
642
+ * These are the base types that can appear in arrays or objects.
643
+ */
644
+ const jsonPrimitiveSchema = z.union([
645
+ z.string(),
646
+ z.number(),
647
+ z.boolean(),
648
+ z.null()
649
+ ]);
650
+ /**
651
+ * JSON-compatible value type for defaultValue fields.
652
+ * OpenAI requires explicit types and doesn't support 'propertyNames' (from z.record).
653
+ * For LLM-generated defaults, we only need primitives and arrays of primitives.
654
+ * Complex object defaults can be added manually after generation if needed.
655
+ */
656
+ const jsonValueSchema = z.union([jsonPrimitiveSchema, z.array(jsonPrimitiveSchema)]);
657
+ /**
658
+ * Base field properties common to all field types.
659
+ * Note: OpenAI structured output requires all properties in 'required' array.
660
+ * Optional fields use nullable types instead of .optional().
661
+ * UI hints removed as they're not critical for LLM generation.
662
+ */
663
+ const llmBaseFieldSchema = z.object({
664
+ id: z.string().min(1),
665
+ label: z.string().min(1),
666
+ description: z.string().nullable(),
667
+ required: z.boolean(),
668
+ defaultValue: jsonValueSchema.nullable()
669
+ });
670
+ /**
671
+ * Text field schema.
672
+ */
673
+ const llmTextFieldSchema = llmBaseFieldSchema.extend({
674
+ type: z.literal("text"),
675
+ multiline: z.boolean(),
676
+ maxLength: z.number().int().positive().nullable()
677
+ });
678
+ /**
679
+ * Rich text field schema.
680
+ */
681
+ const llmRichTextFieldSchema = llmBaseFieldSchema.extend({ type: z.literal("richText") });
682
+ /**
683
+ * Media field schema.
684
+ */
685
+ const llmMediaFieldSchema = llmBaseFieldSchema.extend({
686
+ type: z.literal("media"),
687
+ mediaKinds: z.array(z.enum([
688
+ "image",
689
+ "video",
690
+ "audio"
691
+ ])).nullable()
692
+ });
693
+ /**
694
+ * File field schema.
695
+ */
696
+ const llmFileFieldSchema = llmBaseFieldSchema.extend({
697
+ type: z.literal("file"),
698
+ fileKinds: z.array(z.enum([
699
+ "document",
700
+ "spreadsheet",
701
+ "archive",
702
+ "audio"
703
+ ])).nullable()
704
+ });
705
+ /**
706
+ * Boolean field schema.
707
+ */
708
+ const llmBooleanFieldSchema = llmBaseFieldSchema.extend({ type: z.literal("boolean") });
709
+ /**
710
+ * Link field schema.
711
+ */
712
+ const llmLinkFieldSchema = llmBaseFieldSchema.extend({ type: z.literal("link") });
713
+ /**
714
+ * Select field schema.
715
+ */
716
+ const llmSelectFieldSchema = llmBaseFieldSchema.extend({
717
+ type: z.literal("select"),
718
+ options: z.array(z.object({
719
+ value: z.string(),
720
+ label: z.string()
721
+ })),
722
+ multiple: z.boolean().nullable()
723
+ });
724
+ /**
725
+ * Number field schema.
726
+ */
727
+ const llmNumberFieldSchema = llmBaseFieldSchema.extend({ type: z.literal("number") });
728
+ /**
729
+ * Schema for nested fields in groups/repeaters.
730
+ * Uses a permissive object schema that OpenAI accepts (has explicit type key).
731
+ * All properties are required (nullable for optional ones) per OpenAI's requirements.
732
+ * The actual field structure is validated at runtime.
733
+ */
734
+ const llmNestedFieldSchema = z.object({
735
+ id: z.string(),
736
+ type: z.string(),
737
+ label: z.string(),
738
+ required: z.boolean().nullable(),
739
+ multiline: z.boolean().nullable(),
740
+ defaultValue: jsonValueSchema.nullable()
741
+ }).passthrough();
742
+ const llmNestedFieldsSchema = z.array(llmNestedFieldSchema);
743
+ /**
744
+ * Group field schema.
745
+ */
746
+ const llmGroupFieldSchema = llmBaseFieldSchema.extend({
747
+ type: z.literal("group"),
748
+ schema: z.object({ fields: llmNestedFieldsSchema })
749
+ });
750
+ /**
751
+ * Repeater field schema.
752
+ */
753
+ const llmRepeaterFieldSchema = llmBaseFieldSchema.extend({
754
+ type: z.literal("repeater"),
755
+ itemLabel: z.string(),
756
+ itemLabelSource: z.string().nullable(),
757
+ minItems: z.number().int().min(0),
758
+ maxItems: z.number().int().positive().nullable(),
759
+ allowConversion: z.boolean(),
760
+ schema: z.object({ fields: llmNestedFieldsSchema })
761
+ });
762
+ /**
763
+ * Combined field schema for LLM output.
764
+ * Supports: text, richText, media, file, boolean, link, select, number, group, repeater.
765
+ */
766
+ const llmFieldSchema = z.union([
767
+ llmTextFieldSchema,
768
+ llmRichTextFieldSchema,
769
+ llmMediaFieldSchema,
770
+ llmFileFieldSchema,
771
+ llmBooleanFieldSchema,
772
+ llmLinkFieldSchema,
773
+ llmSelectFieldSchema,
774
+ llmNumberFieldSchema,
775
+ llmGroupFieldSchema,
776
+ llmRepeaterFieldSchema
777
+ ]);
778
+ /**
779
+ * Simplified binding schema for LLM output.
780
+ * Note: Transform options removed as OpenAI doesn't support z.record() (emits propertyNames).
781
+ * All properties required (nullable for optional ones) per OpenAI's requirements.
782
+ */
783
+ const llmBindingSchema = z.object({
784
+ from: z.string(),
785
+ fallback: z.string().nullable(),
786
+ transforms: z.array(z.object({ id: z.string() })).nullable()
787
+ });
788
+ /**
789
+ * Simplified repeat schema for LLM output.
790
+ */
791
+ const llmRepeatSchema = z.object({
792
+ collection: z.object({ from: z.string() }),
793
+ itemName: z.string()
794
+ });
795
+ /**
796
+ * Props schema - uses passthrough to accept any props structure.
797
+ * OpenAI doesn't support z.record() (emits propertyNames), so we use
798
+ * a permissive object. Actual props are validated at runtime.
799
+ */
800
+ const llmPropsSchema = z.object({}).passthrough().nullable();
801
+ /**
802
+ * Simplified node schema for LLM output.
803
+ * All properties required (nullable for optional ones) per OpenAI's requirements.
804
+ *
805
+ * IMPORTANT:
806
+ * OpenAI structured output doesn't support truly-recursive JSON schemas.
807
+ * However, giving children a "type-only" schema causes the model to omit
808
+ * nested node details entirely (no props/$bind/etc), which then fails our
809
+ * runtime validation. To avoid both problems, we use a depth-limited schema
810
+ * that still fully specifies child node shape.
811
+ */
812
+ function buildLlmNodeSchema(maxDepth) {
813
+ const nodeBase = z.object({
814
+ type: z.string(),
815
+ props: llmPropsSchema,
816
+ $bind: llmBindingSchema.nullable(),
817
+ $repeat: llmRepeatSchema.nullable(),
818
+ $when: z.object({ condition: z.object({ from: z.string() }) }).nullable()
819
+ });
820
+ let schema = nodeBase.extend({ children: z.null() });
821
+ for (let i = 0; i < maxDepth; i++) schema = nodeBase.extend({ children: z.array(schema).nullable() });
822
+ return schema;
823
+ }
824
+ const llmNodeSchema = buildLlmNodeSchema(6);
825
+ /**
826
+ * LLM output schema for custom block definitions.
827
+ *
828
+ * Uses simplified LLM-compatible schemas (no z.any()) for OpenAI structured output.
829
+ * All properties required (nullable for optional ones) per OpenAI's requirements.
830
+ * Full validation happens after parsing via validateLayout().
831
+ *
832
+ * Note: Explicit type annotation required due to recursive schema complexity.
833
+ */
834
+ const llmCustomBlockSchema = z.object({
835
+ id: z.string().min(8).regex(/^custom\.[a-z][a-z0-9-]*$/, { message: "Block ID must start with 'custom.' followed by lowercase letters, numbers, or hyphens" }),
836
+ title: z.string().min(1, "Title is required"),
837
+ titleSource: titleSourceValueSchema.nullable(),
838
+ description: z.string().nullable(),
839
+ category: blockCategoryEnum,
840
+ icon: z.string().nullable(),
841
+ tags: z.array(z.string()).nullable(),
842
+ fields: llmFieldSchema.array().min(1, "Custom blocks must have at least one field"),
843
+ layout: llmNodeSchema
844
+ });
845
+ /**
846
+ * Schema for the complete LLM output.
847
+ *
848
+ * This is used with getAiResponse({ kind: 'json', schema }) for
849
+ * structured output generation.
850
+ * All properties required (nullable for optional ones) per OpenAI's requirements.
851
+ *
852
+ * Note: Explicit type annotation required due to complex nested types.
853
+ */
854
+ const htmlToManifestOutputSchema = z.object({
855
+ blocks: z.array(llmCustomBlockSchema).min(1, "At least one block is required").max(10, "Maximum 10 blocks per conversion"),
856
+ themePatch: z.array(llmThemePatchOpSchema).nullable(),
857
+ customCssRules: z.array(llmCustomCssRuleSchema).nullable(),
858
+ customCssAtRules: z.array(llmMediaAtRuleSchema).nullable(),
859
+ /**
860
+ * Explanation of fixes made when retrying after validation errors.
861
+ * Should explain what changes were made and why.
862
+ */
863
+ fixRationale: z.string().nullable()
864
+ });
865
+ /**
866
+ * Collect validation errors from a parsed output.
867
+ *
868
+ * Goes beyond Zod schema validation to check:
869
+ * - Node types are whitelisted
870
+ * - Transform IDs are whitelisted
871
+ * - $bind references match defined fields
872
+ * - Content nodes have bindings (text, richText, media, button, link)
873
+ * - All defined fields are used in the layout
874
+ * - Redundant link/text nodes in repeaters
875
+ */
876
+ function collectValidationErrors(output) {
877
+ const errors = [];
878
+ const blockIds = output.blocks.map((b) => b.id);
879
+ const singleBlockId = blockIds.length === 1 ? blockIds[0] ?? null : null;
880
+ for (const block of output.blocks) {
881
+ const layoutErrors = validateLayoutDeep(block.layout, block.id, block.fields);
882
+ errors.push(...layoutErrors);
883
+ const fieldInfo = collectFieldInfo(block.fields);
884
+ const used = collectUsedFieldRefs(block.layout);
885
+ const unusedTopLevelFields = Array.from(fieldInfo.topLevelFieldIds).filter((id) => !used.topLevel.has(id));
886
+ if (unusedTopLevelFields.length > 0) errors.push({
887
+ path: `blocks["${block.id}"].fields`,
888
+ message: `Block "${block.id}": Fields defined but never used in layout: ${unusedTopLevelFields.join(", ")}`,
889
+ suggestion: `Every field should be bound to a layout node. Add $bind: { from: "content.${unusedTopLevelFields[0] ?? "fieldId"}" } to an appropriate node, or remove unused fields.`
890
+ });
891
+ for (const [repeaterFieldId, nestedFieldIds] of fieldInfo.repeaterNestedFieldIds.entries()) {
892
+ if (!used.topLevel.has(repeaterFieldId)) continue;
893
+ const usedNested = used.nestedByRepeater.get(repeaterFieldId) ?? /* @__PURE__ */ new Set();
894
+ const missingNested = Array.from(nestedFieldIds).filter((id) => !usedNested.has(id));
895
+ if (missingNested.length === 0) continue;
896
+ const itemName = used.itemNameByRepeater.get(repeaterFieldId) ?? "item";
897
+ errors.push({
898
+ path: `blocks["${block.id}"].fields`,
899
+ message: `Block "${block.id}": Repeater field "${repeaterFieldId}": nested fields defined but never used in repeated layout: ${missingNested.join(", ")}`,
900
+ suggestion: `Inside the $repeat subtree for content.${repeaterFieldId} (itemName "${itemName}"), add nodes with $bind.from like "${itemName}.${missingNested[0] ?? "fieldId"}".`
901
+ });
902
+ }
903
+ const missingBindings = findMissingBindings(block.layout, block.id);
904
+ errors.push(...missingBindings);
905
+ errors.push(...validateLinkFieldDefaultValues(block));
906
+ errors.push(...findMissingLinkHrefs(block.layout, block.id, block.fields));
907
+ errors.push(...detectRedundantLinkLabels(block.layout, block.id, block.fields));
908
+ }
909
+ if (output.customCssRules && output.customCssRules.length > 0) errors.push(...validateCustomCssRules(output.customCssRules, singleBlockId, output.blocks));
910
+ return errors;
911
+ }
912
+ function validateLinkFieldDefaultValues(block) {
913
+ const errors = [];
914
+ for (const field of block.fields) {
915
+ if (field.type !== "link") continue;
916
+ const dv = field.defaultValue ?? null;
917
+ const isRequired = field.required === true;
918
+ if (dv === null) {
919
+ if (isRequired) errors.push({
920
+ path: `blocks["${block.id}"].fields["${field.id}"].defaultValue`,
921
+ message: `Block "${block.id}": required link field "${field.id}" is missing defaultValue`,
922
+ suggestion: `Set defaultValue to an object like { "href": "/path-or-#anchor", "label": "Link Text" }.`
923
+ });
924
+ continue;
925
+ }
926
+ if (typeof dv !== "object" || Array.isArray(dv)) {
927
+ errors.push({
928
+ path: `blocks["${block.id}"].fields["${field.id}"].defaultValue`,
929
+ message: `Block "${block.id}": link field "${field.id}" defaultValue must be an object (got ${typeof dv})`,
930
+ suggestion: `Use { "href": "/path-or-#anchor", "label": "Link Text" }.`
931
+ });
932
+ continue;
933
+ }
934
+ const record = dv;
935
+ const href = typeof record.href === "string" ? record.href.trim() : "";
936
+ const label = typeof record.label === "string" ? record.label.trim() : "";
937
+ const title = typeof record.title === "string" ? record.title.trim() : "";
938
+ const identifier = typeof record.identifier === "string" ? record.identifier.trim() : "";
939
+ const routeId = typeof record.routeId === "string" ? record.routeId.trim() : "";
940
+ const hasDestination = href.length > 0 || identifier.length > 0 || routeId.length > 0;
941
+ const hasLabel = label.length > 0 || title.length > 0 || identifier.length > 0 || href.length > 0;
942
+ if (!hasDestination || !hasLabel) errors.push({
943
+ path: `blocks["${block.id}"].fields["${field.id}"].defaultValue`,
944
+ message: `Block "${block.id}": link field "${field.id}" defaultValue is missing destination/label (got ${JSON.stringify(dv)})`,
945
+ suggestion: `Use { "href": "/path-or-#anchor", "label": "Link Text" }.`
946
+ });
947
+ }
948
+ return errors;
949
+ }
950
+ function getNestedFields(field) {
951
+ if (!field || typeof field !== "object") return null;
952
+ if (!field.schema || typeof field.schema !== "object") return null;
953
+ const fields = field.schema.fields;
954
+ return Array.isArray(fields) ? fields : null;
955
+ }
956
+ function resolveFieldTypeForFrom(allFields, from, repeatStack) {
957
+ if (!from) return null;
958
+ if (from.startsWith("content.")) {
959
+ const [topId, ...rest] = from.replace("content.", "").split(".").filter(Boolean);
960
+ if (!topId) return null;
961
+ let current = allFields.find((f) => f.id === topId);
962
+ if (!current) return null;
963
+ for (const seg of rest) {
964
+ const nested = getNestedFields(current);
965
+ if (!nested) return null;
966
+ current = nested.find((f) => f.id === seg);
967
+ if (!current) return null;
968
+ }
969
+ return current.type ?? null;
970
+ }
971
+ for (let i = repeatStack.length - 1; i >= 0; i--) {
972
+ const ctx = repeatStack[i];
973
+ if (!ctx) continue;
974
+ const prefix = `${ctx.itemName}.`;
975
+ if (!from.startsWith(prefix)) continue;
976
+ const [nestedId, ...rest] = from.slice(prefix.length).split(".").filter(Boolean);
977
+ if (!nestedId) return null;
978
+ let current = ctx.fields.find((f) => f.id === nestedId);
979
+ if (!current) return null;
980
+ for (const seg of rest) {
981
+ const nested = getNestedFields(current);
982
+ if (!nested) return null;
983
+ current = nested.find((f) => f.id === seg);
984
+ if (!current) return null;
985
+ }
986
+ return current.type ?? null;
987
+ }
988
+ return null;
989
+ }
990
+ function bindingHasTransform(bind, id) {
991
+ if (!bind) return false;
992
+ const transforms = bind.transforms;
993
+ if (!Array.isArray(transforms)) return false;
994
+ return transforms.some((t) => t && typeof t === "object" && t.id === id);
995
+ }
996
+ function extractNestedHrefBindFromProps(props) {
997
+ if (!props || typeof props !== "object") return null;
998
+ const href = props.href;
999
+ if (!href || typeof href !== "object") return null;
1000
+ if (!("$bind" in href)) return null;
1001
+ const bind = href.$bind;
1002
+ return bind && typeof bind === "object" ? bind : null;
1003
+ }
1004
+ function findMissingLinkHrefs(node, blockId, fields, path = "layout", repeatStack = []) {
1005
+ const errors = [];
1006
+ if (!node || typeof node !== "object") return errors;
1007
+ const n = node;
1008
+ const nodeType = typeof n.type === "string" ? n.type : null;
1009
+ let nextRepeatStack = repeatStack;
1010
+ if (n.$repeat && typeof n.$repeat === "object") {
1011
+ const repeat = n.$repeat;
1012
+ const itemName = typeof repeat.itemName === "string" ? repeat.itemName : null;
1013
+ const collection = repeat.collection && typeof repeat.collection === "object" ? repeat.collection : null;
1014
+ const from = collection && typeof collection.from === "string" ? collection.from : null;
1015
+ if (itemName && from && from.startsWith("content.")) {
1016
+ const repeaterFieldId = from.replace("content.", "").split(".")[0] ?? "";
1017
+ const repeaterField = fields.find((f) => f.id === repeaterFieldId) ?? null;
1018
+ const nested = repeaterField ? getNestedFields(repeaterField) ?? [] : [];
1019
+ nextRepeatStack = [...repeatStack, {
1020
+ itemName,
1021
+ fields: nested
1022
+ }];
1023
+ }
1024
+ }
1025
+ if (nodeType === "link" || nodeType === "button") {
1026
+ const bind = n.$bind && typeof n.$bind === "object" ? n.$bind : null;
1027
+ const bindFrom = bind && typeof bind.from === "string" ? bind.from : null;
1028
+ const bindFieldType = bindFrom ? resolveFieldTypeForFrom(fields, bindFrom, nextRepeatStack) : null;
1029
+ const hrefBind = extractNestedHrefBindFromProps(n.props);
1030
+ const hrefFrom = hrefBind && typeof hrefBind.from === "string" ? hrefBind.from : null;
1031
+ const hrefFieldType = hrefFrom ? resolveFieldTypeForFrom(fields, hrefFrom, nextRepeatStack) : null;
1032
+ if (bindFieldType === "link" || hrefFieldType === "link" || bindingHasTransform(bind, "links.label")) {
1033
+ if (!hrefBind || !hrefFrom) errors.push({
1034
+ path,
1035
+ message: `Block "${blockId}": ${nodeType} node at ${path} is bound to a link field but is missing props.href binding`,
1036
+ suggestion: `Add props.href: { "$bind": { "from": "${bindFrom ?? "content.yourLinkField"}", "fallback": null, "transforms": [{ "id": "links.resolve" }] } }`
1037
+ });
1038
+ else if (!bindingHasTransform(hrefBind, "links.resolve")) errors.push({
1039
+ path,
1040
+ message: `Block "${blockId}": ${nodeType} node at ${path} has props.href binding but is missing transforms: [{id:"links.resolve"}]`,
1041
+ suggestion: `Set props.href.$bind.transforms to [{ "id": "links.resolve" }].`
1042
+ });
1043
+ if (bindFrom && bindFieldType === "link" && !bindingHasTransform(bind, "links.label")) errors.push({
1044
+ path,
1045
+ message: `Block "${blockId}": ${nodeType} node at ${path} binds label from a link field but is missing links.label transform`,
1046
+ suggestion: `Set node.$bind.transforms to [{ "id": "links.label" }] so the label renders as text.`
1047
+ });
1048
+ }
1049
+ }
1050
+ if ("children" in n && Array.isArray(n.children)) for (let i = 0; i < n.children.length; i++) errors.push(...findMissingLinkHrefs(n.children[i], blockId, fields, `${path}.children[${i}]`, nextRepeatStack));
1051
+ return errors;
1052
+ }
1053
+ /**
1054
+ * Detect redundant link label patterns in repeaters.
1055
+ *
1056
+ * A common LLM mistake: when iterating over a repeater with link fields,
1057
+ * the LLM creates both:
1058
+ * 1. A link/button node with links.label transform (correct - displays the label)
1059
+ * 2. A separate text node binding to item.label (redundant - duplicates the label)
1060
+ *
1061
+ * This validation detects when a container has sibling nodes where:
1062
+ * - One is a link/button with links.label transform
1063
+ * - Another is a text node binding to a label-like field from the same item
1064
+ */
1065
+ function detectRedundantLinkLabels(node, blockId, fields, path = "layout", repeatStack = []) {
1066
+ const errors = [];
1067
+ if (!node || typeof node !== "object") return errors;
1068
+ const n = node;
1069
+ let nextRepeatStack = repeatStack;
1070
+ if (n.$repeat && typeof n.$repeat === "object") {
1071
+ const repeat = n.$repeat;
1072
+ const itemName = typeof repeat.itemName === "string" ? repeat.itemName : null;
1073
+ const collection = repeat.collection && typeof repeat.collection === "object" ? repeat.collection : null;
1074
+ const from = collection && typeof collection.from === "string" ? collection.from : null;
1075
+ if (itemName && from && from.startsWith("content.")) {
1076
+ const repeaterFieldId = from.replace("content.", "").split(".")[0] ?? "";
1077
+ const repeaterField = fields.find((f) => f.id === repeaterFieldId) ?? null;
1078
+ const nested = repeaterField ? getNestedFields(repeaterField) ?? [] : [];
1079
+ nextRepeatStack = [...repeatStack, {
1080
+ itemName,
1081
+ fields: nested
1082
+ }];
1083
+ }
1084
+ }
1085
+ if (Array.isArray(n.children) && n.children.length > 1) {
1086
+ const children = n.children;
1087
+ const redundancies = findRedundantLinkTextSiblings(children, nextRepeatStack, path);
1088
+ errors.push(...redundancies.map((r) => ({
1089
+ path: r.path,
1090
+ message: `Block "${blockId}": ${r.message}`,
1091
+ suggestion: r.suggestion
1092
+ })));
1093
+ }
1094
+ if (Array.isArray(n.children)) for (let i = 0; i < n.children.length; i++) errors.push(...detectRedundantLinkLabels(n.children[i], blockId, fields, `${path}.children[${i}]`, nextRepeatStack));
1095
+ return errors;
1096
+ }
1097
+ /**
1098
+ * Find redundant text nodes that duplicate link labels in a set of sibling nodes.
1099
+ */
1100
+ function findRedundantLinkTextSiblings(children, repeatStack, parentPath) {
1101
+ const results = [];
1102
+ const linkNodesWithLabel = [];
1103
+ for (let i = 0; i < children.length; i++) {
1104
+ const child = children[i];
1105
+ if (!child) continue;
1106
+ const nodeType = child.type;
1107
+ if (nodeType !== "link" && nodeType !== "button") continue;
1108
+ const bind = child.$bind;
1109
+ if (!bind) continue;
1110
+ if (!bindingHasTransform(bind, "links.label")) continue;
1111
+ const bindFrom = typeof bind.from === "string" ? bind.from : "";
1112
+ if (bindFrom) linkNodesWithLabel.push({
1113
+ index: i,
1114
+ bindFrom
1115
+ });
1116
+ }
1117
+ if (linkNodesWithLabel.length === 0) return results;
1118
+ for (let i = 0; i < children.length; i++) {
1119
+ const child = children[i];
1120
+ if (!child) continue;
1121
+ if (child.type !== "text") continue;
1122
+ const bind = child.$bind;
1123
+ if (!bind) continue;
1124
+ const textBindFrom = typeof bind.from === "string" ? bind.from : "";
1125
+ if (!textBindFrom) continue;
1126
+ for (const linkNode of linkNodesWithLabel) {
1127
+ const redundancy = checkRedundantLabelBinding(linkNode.bindFrom, textBindFrom, repeatStack);
1128
+ if (redundancy) results.push({
1129
+ path: `${parentPath}.children[${i}]`,
1130
+ message: `Redundant text node binding to "${textBindFrom}" - the link node at children[${linkNode.index}] already displays the label via links.label transform`,
1131
+ suggestion: `Remove this text node. The link/button node with links.label transform already displays "${redundancy.labelField}" as its visible text.`
1132
+ });
1133
+ }
1134
+ }
1135
+ return results;
1136
+ }
1137
+ /**
1138
+ * Check if a text binding is redundant given a link binding with links.label.
1139
+ * Returns info about the redundant label field, or null if not redundant.
1140
+ */
1141
+ function checkRedundantLabelBinding(linkBindFrom, textBindFrom, repeatStack) {
1142
+ for (let i = repeatStack.length - 1; i >= 0; i--) {
1143
+ const ctx = repeatStack[i];
1144
+ if (!ctx) continue;
1145
+ const prefix = `${ctx.itemName}.`;
1146
+ if (!linkBindFrom.startsWith(prefix) || !textBindFrom.startsWith(prefix)) continue;
1147
+ const linkFieldId = linkBindFrom.slice(prefix.length).split(".")[0] ?? "";
1148
+ const textFieldId = textBindFrom.slice(prefix.length).split(".")[0] ?? "";
1149
+ const linkField = ctx.fields.find((f) => f.id === linkFieldId);
1150
+ if (!linkField || linkField.type !== "link") continue;
1151
+ if ([
1152
+ "label",
1153
+ "text",
1154
+ "title",
1155
+ "name"
1156
+ ].includes(textFieldId.toLowerCase()) || textFieldId.toLowerCase() === `${linkFieldId}label` || textFieldId.toLowerCase() === `${linkFieldId}text`) return { labelField: textFieldId };
1157
+ }
1158
+ return null;
1159
+ }
1160
+ const DISALLOWED_SELECTOR_TOKENS = [
1161
+ "@media",
1162
+ "@supports",
1163
+ "@keyframes",
1164
+ "@font-face",
1165
+ "@import"
1166
+ ];
1167
+ function validateCustomCssRules(rules, singleBlockId, blocks) {
1168
+ const errors = [];
1169
+ const layoutClassNamesByBlockId = /* @__PURE__ */ new Map();
1170
+ const layoutNodeInfosByBlockId = /* @__PURE__ */ new Map();
1171
+ for (const block of blocks) {
1172
+ layoutClassNamesByBlockId.set(block.id, collectLayoutClassNames(block.layout));
1173
+ layoutNodeInfosByBlockId.set(block.id, collectLayoutNodeInfos(block.layout));
1174
+ }
1175
+ const missingLayoutHooksByBlockId = /* @__PURE__ */ new Map();
1176
+ for (let i = 0; i < rules.length; i++) {
1177
+ const rule = rules[i];
1178
+ if (!rule) continue;
1179
+ const selector = rule.selector.trim();
1180
+ const rulePath = `customCssRules[${i}]`;
1181
+ if (!selector) {
1182
+ errors.push({
1183
+ path: `${rulePath}.selector`,
1184
+ message: "customCssRules: selector cannot be empty",
1185
+ suggestion: "Provide a concrete selector scoped to the block (e.g., `[data-block=\"custom.my-block\"] .my-class`)."
1186
+ });
1187
+ continue;
1188
+ }
1189
+ if (selector.startsWith("@") || selector.includes("{") || selector.includes("}")) {
1190
+ errors.push({
1191
+ path: `${rulePath}.selector`,
1192
+ message: `customCssRules: selector "${rule.selector}" looks like an at-rule. At-rules are not supported in customCssRules selectors.`,
1193
+ suggestion: "Remove @media/@supports wrappers and emit only base styles, or add non-responsive styles only."
1194
+ });
1195
+ continue;
1196
+ }
1197
+ for (const token of DISALLOWED_SELECTOR_TOKENS) if (selector.toLowerCase().includes(token)) {
1198
+ errors.push({
1199
+ path: `${rulePath}.selector`,
1200
+ message: `customCssRules: selector "${rule.selector}" contains "${token}". At-rules are not supported here.`,
1201
+ suggestion: "Remove responsive/at-rule CSS and keep only base selectors."
1202
+ });
1203
+ break;
1204
+ }
1205
+ if (selector.includes(".cms-node-")) errors.push({
1206
+ path: `${rulePath}.selector`,
1207
+ message: `customCssRules: selector "${rule.selector}" relies on internal ".cms-node-*" classes.`,
1208
+ suggestion: "Do not target internal renderer classes. Add your own className to the relevant node props and target that class instead."
1209
+ });
1210
+ const targetBlockId = extractBlockIdFromSelector(selector) ?? singleBlockId;
1211
+ if (!targetBlockId) {
1212
+ errors.push({
1213
+ path: `${rulePath}.selector`,
1214
+ message: "customCssRules: selector must include a [data-block=\"custom.*\"] scope when multiple blocks are present.",
1215
+ suggestion: "Prefix selectors with `[data-block=\"custom.some-block-id\"]`."
1216
+ });
1217
+ continue;
1218
+ }
1219
+ const expectedScope = `[data-block="${targetBlockId}"]`;
1220
+ if (!selector.includes(expectedScope)) errors.push({
1221
+ path: `${rulePath}.selector`,
1222
+ message: `customCssRules: selector must be scoped to ${expectedScope} for safety.`,
1223
+ suggestion: `Prefix the selector with ${expectedScope} (e.g., \`${expectedScope} .my-class\`).`
1224
+ });
1225
+ const classNames = layoutClassNamesByBlockId.get(targetBlockId) ?? /* @__PURE__ */ new Set();
1226
+ const selectorClasses = extractSelectorClasses$1(selector);
1227
+ for (const className of selectorClasses) if (!classNames.has(className)) {
1228
+ const existing = missingLayoutHooksByBlockId.get(targetBlockId) ?? /* @__PURE__ */ new Set();
1229
+ existing.add(className);
1230
+ missingLayoutHooksByBlockId.set(targetBlockId, existing);
1231
+ }
1232
+ }
1233
+ for (const [blockId, missingClassNames] of missingLayoutHooksByBlockId.entries()) {
1234
+ const missing = Array.from(missingClassNames).sort();
1235
+ if (missing.length === 0) continue;
1236
+ const attachSuggestions = buildClassHookAttachSuggestions(blockId, missing, layoutNodeInfosByBlockId.get(blockId) ?? []);
1237
+ const attachSuggestionsLine = attachSuggestions ? `\n\nSuggested attachments:\n${attachSuggestions}` : "";
1238
+ errors.push({
1239
+ path: "customCssRules",
1240
+ message: `customCssRules: selectors reference className hooks not present in the layout for block "${blockId}": ${missing.map((c) => `"${c}"`).join(", ")}`,
1241
+ suggestion: `Add these classes to one or more nodes via props.className. Tip: prefer kebab-case hooks and attach them to the exact nodes you want to style (grid/headline/body/etc).${attachSuggestionsLine}`
1242
+ });
1243
+ }
1244
+ return errors;
1245
+ }
1246
+ function buildClassHookAttachSuggestions(blockId, missingClassNames, nodes) {
1247
+ if (missingClassNames.length === 0) return null;
1248
+ const slug = blockId.replace(/^custom\./, "");
1249
+ const lines = [];
1250
+ const rootPath = "layout";
1251
+ const firstGrid = nodes.find((n) => n.type === "grid")?.path ?? null;
1252
+ const firstStack = nodes.find((n) => n.type === "stack")?.path ?? null;
1253
+ const headlineNode = nodes.find((n) => (n.bindFrom ?? "").includes("headline"))?.path ?? null;
1254
+ const highlightNode = nodes.find((n) => (n.bindFrom ?? "").includes("highlight"))?.path ?? null;
1255
+ const paragraphNode = nodes.find((n) => (n.bindFrom ?? "").includes("paragraph"))?.path ?? null;
1256
+ for (const className of missingClassNames) {
1257
+ const lower = className.toLowerCase();
1258
+ if (className === slug || className === `split-${slug}` || lower.includes(slug)) {
1259
+ lines.push(`- ${className} → add to ${rootPath}.props.className`);
1260
+ continue;
1261
+ }
1262
+ if (lower.includes("grid") && firstGrid) {
1263
+ lines.push(`- ${className} → add to ${firstGrid}.props.className`);
1264
+ continue;
1265
+ }
1266
+ if ((lower.includes("headline") || lower.includes("title")) && (headlineNode ?? firstStack)) {
1267
+ lines.push(`- ${className} → add to ${headlineNode ?? firstStack ?? rootPath}.props.className`);
1268
+ continue;
1269
+ }
1270
+ if (lower.includes("highlight") && highlightNode) {
1271
+ lines.push(`- ${className} → add to ${highlightNode}.props.className`);
1272
+ continue;
1273
+ }
1274
+ if ((lower.includes("body") || lower.includes("copy") || lower.includes("paragraph")) && (paragraphNode ?? firstStack)) {
1275
+ lines.push(`- ${className} → add to ${paragraphNode ?? firstStack ?? rootPath}.props.className`);
1276
+ continue;
1277
+ }
1278
+ lines.push(`- ${className} → add to ${rootPath}.props.className (fallback)`);
1279
+ }
1280
+ return lines.slice(0, 8).join("\n");
1281
+ }
1282
+ /**
1283
+ * Extract class names from a CSS selector.
1284
+ * This version handles scoped selectors by stripping [data-block=...] attributes
1285
+ * and skipping internal .cms-node-* classes.
1286
+ */
1287
+ function extractSelectorClasses$1(selector) {
1288
+ const withoutDataBlockAttrs = selector.replace(/\[data-block=(["']).*?\1\]/g, "");
1289
+ const classes = /* @__PURE__ */ new Set();
1290
+ const re = /\.([_a-zA-Z][-_a-zA-Z0-9]*)/g;
1291
+ let match;
1292
+ while ((match = re.exec(withoutDataBlockAttrs)) !== null) {
1293
+ const className = match[1];
1294
+ if (!className) continue;
1295
+ if (className.startsWith("cms-node-")) continue;
1296
+ classes.add(className);
1297
+ }
1298
+ return classes;
1299
+ }
1300
+ function extractBlockIdFromSelector(selector) {
1301
+ return selector.match(/\[data-block=(["'])(custom\.[^"']+)\1\]/)?.[2] ?? null;
1302
+ }
1303
+ /**
1304
+ * Validate that a node type is in the allowed list.
1305
+ */
1306
+ function validateNodeType(nodeType, path, ctx) {
1307
+ if (!ALLOWED_NODE_TYPES.includes(nodeType)) return {
1308
+ path: `${path}.type`,
1309
+ message: `Block "${ctx.blockId}": Unknown node type "${nodeType}" at ${path}`,
1310
+ suggestion: `ALLOWED: ${ALLOWED_NODE_TYPES.slice(0, 10).join(", ")}... FIX: Replace "${nodeType}" with a valid node type.`
1311
+ };
1312
+ return null;
1313
+ }
1314
+ /**
1315
+ * Validate transforms in a $bind object.
1316
+ */
1317
+ function validateTransforms(transforms, path, ctx) {
1318
+ const errors = [];
1319
+ for (let i = 0; i < transforms.length; i++) {
1320
+ const transform = transforms[i];
1321
+ if (transform.id && typeof transform.id === "string") {
1322
+ if (!ALLOWED_TRANSFORM_IDS.includes(transform.id)) errors.push({
1323
+ path: `${path}.transforms[${i}].id`,
1324
+ message: `Block "${ctx.blockId}": Unknown transform "${transform.id}"`,
1325
+ suggestion: `ALLOWED: ${ALLOWED_TRANSFORM_IDS.slice(0, 8).join(", ")}... FIX: Replace with a valid transform ID or remove.`
1326
+ });
1327
+ }
1328
+ }
1329
+ return errors;
1330
+ }
1331
+ /**
1332
+ * Validate a single $bind object for field references and transforms.
1333
+ */
1334
+ function validateBinding(bind, path, ctx) {
1335
+ const errors = [];
1336
+ if ("from" in bind && typeof bind.from === "string") {
1337
+ const bindPath = bind.from;
1338
+ if (bindPath.startsWith("content.")) {
1339
+ const fieldId = bindPath.replace("content.", "").split(".")[0] ?? "";
1340
+ if (fieldId && !ctx.fieldIds.has(fieldId)) errors.push({
1341
+ path: `${path}.from`,
1342
+ message: `Block "${ctx.blockId}": $bind references unknown field "${fieldId}"`,
1343
+ suggestion: `DEFINED FIELDS: ${Array.from(ctx.fieldIds).join(", ")}. FIX: Add field { id: '${fieldId}', type: '...' } or use existing field.`
1344
+ });
1345
+ }
1346
+ }
1347
+ if ("transforms" in bind && Array.isArray(bind.transforms)) errors.push(...validateTransforms(bind.transforms, path, ctx));
1348
+ return errors;
1349
+ }
1350
+ /**
1351
+ * Deep validation of layout tree for allowed node types, transforms, and bindings.
1352
+ */
1353
+ function validateLayoutDeep(node, blockId, fields, path = "layout") {
1354
+ return validateLayoutNode(node, path, {
1355
+ blockId,
1356
+ fieldIds: collectFieldIds(fields)
1357
+ });
1358
+ }
1359
+ /**
1360
+ * Recursively validate a layout node and its children.
1361
+ */
1362
+ function validateLayoutNode(node, path, ctx) {
1363
+ const errors = [];
1364
+ if (!node || typeof node !== "object") return errors;
1365
+ const n = node;
1366
+ if ("type" in n && typeof n.type === "string") {
1367
+ const typeError = validateNodeType(n.type, path, ctx);
1368
+ if (typeError) errors.push(typeError);
1369
+ }
1370
+ if ("$bind" in n && n.$bind && typeof n.$bind === "object") {
1371
+ const bindErrors = validateBinding(n.$bind, `${path}.$bind`, ctx);
1372
+ errors.push(...bindErrors);
1373
+ }
1374
+ if ("props" in n && n.props && typeof n.props === "object") {
1375
+ const propsErrors = validatePropsBindings(n.props, `${path}.props`, ctx);
1376
+ errors.push(...propsErrors);
1377
+ }
1378
+ if ("children" in n && Array.isArray(n.children)) for (let i = 0; i < n.children.length; i++) {
1379
+ const childErrors = validateLayoutNode(n.children[i], `${path}.children[${i}]`, ctx);
1380
+ errors.push(...childErrors);
1381
+ }
1382
+ return errors;
1383
+ }
1384
+ /**
1385
+ * Validate bindings in props objects.
1386
+ */
1387
+ function validatePropsBindings(props, path, ctx) {
1388
+ const errors = [];
1389
+ for (const [key, value] of Object.entries(props)) if (value && typeof value === "object" && "$bind" in value) {
1390
+ const bind = value.$bind;
1391
+ const bindErrors = validateBinding(bind, `${path}.${key}.$bind`, ctx);
1392
+ errors.push(...bindErrors);
1393
+ } else if (value && typeof value === "object" && !Array.isArray(value)) {
1394
+ const nestedErrors = validatePropsBindings(value, `${path}.${key}`, ctx);
1395
+ errors.push(...nestedErrors);
1396
+ }
1397
+ return errors;
1398
+ }
1399
+ /**
1400
+ * Collect all field IDs from a fields array, including nested fields.
1401
+ */
1402
+ function collectFieldIds(fields) {
1403
+ const ids = /* @__PURE__ */ new Set();
1404
+ function collect(fieldList) {
1405
+ for (const field of fieldList) {
1406
+ ids.add(field.id);
1407
+ if (field.schema && typeof field.schema === "object" && "fields" in field.schema) {
1408
+ const nestedFields = field.schema.fields;
1409
+ if (isFieldIdRecordArray(nestedFields)) collect(nestedFields);
1410
+ }
1411
+ if (isRecordArray(field.tabs)) {
1412
+ for (const tab of field.tabs) if (isFieldIdRecordArray(tab.fields)) collect(tab.fields);
1413
+ }
1414
+ if (field.itemTypes && typeof field.itemTypes === "object") {
1415
+ for (const itemType of Object.values(field.itemTypes)) if (itemType && typeof itemType === "object" && "fields" in itemType) collect(itemType.fields);
1416
+ }
1417
+ }
1418
+ }
1419
+ collect(fields);
1420
+ return ids;
1421
+ }
1422
+ function isRecordArray(value) {
1423
+ return Array.isArray(value);
1424
+ }
1425
+ function isFieldIdRecordArray(value) {
1426
+ return Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null && typeof item.id === "string");
1427
+ }
1428
+ /**
1429
+ * Content node types that typically require bindings to display field data.
1430
+ */
1431
+ const CONTENT_NODE_TYPES = [
1432
+ "text",
1433
+ "richText",
1434
+ "media",
1435
+ "button",
1436
+ "link"
1437
+ ];
1438
+ /**
1439
+ * Collect field metadata for "unused field" validation.
1440
+ *
1441
+ * IMPORTANT:
1442
+ * - Only top-level fields can be referenced via `content.fieldId`.
1443
+ * - Nested fields (e.g. repeater item schema fields) are referenced via the repeat itemName
1444
+ * (e.g. `item.text`) and must be validated within the $repeat subtree.
1445
+ */
1446
+ function collectFieldInfo(fields) {
1447
+ const topLevelFieldIds = /* @__PURE__ */ new Set();
1448
+ const repeaterNestedFieldIds = /* @__PURE__ */ new Map();
1449
+ for (const field of fields) {
1450
+ topLevelFieldIds.add(field.id);
1451
+ if (field.type === "repeater" && field.schema && typeof field.schema === "object" && "fields" in field.schema) {
1452
+ const nestedFields = field.schema.fields ?? [];
1453
+ repeaterNestedFieldIds.set(field.id, new Set(nestedFields.map((f) => f.id)));
1454
+ }
1455
+ }
1456
+ return {
1457
+ topLevelFieldIds,
1458
+ repeaterNestedFieldIds
1459
+ };
1460
+ }
1461
+ function collectUsedFieldRefs(node) {
1462
+ const topLevel = /* @__PURE__ */ new Set();
1463
+ const nestedByRepeater = /* @__PURE__ */ new Map();
1464
+ const itemNameByRepeater = /* @__PURE__ */ new Map();
1465
+ function addNested(repeaterFieldId, nestedFieldId) {
1466
+ const used = nestedByRepeater.get(repeaterFieldId) ?? /* @__PURE__ */ new Set();
1467
+ used.add(nestedFieldId);
1468
+ nestedByRepeater.set(repeaterFieldId, used);
1469
+ }
1470
+ function recordBindFrom(from, repeatStack) {
1471
+ if (from.startsWith("content.")) {
1472
+ const fieldId = from.replace("content.", "").split(".")[0] ?? "";
1473
+ if (fieldId) topLevel.add(fieldId);
1474
+ return;
1475
+ }
1476
+ for (let i = repeatStack.length - 1; i >= 0; i--) {
1477
+ const ctx = repeatStack[i];
1478
+ if (!ctx) continue;
1479
+ if (from.startsWith(`${ctx.itemName}.`)) {
1480
+ const nestedFieldId = from.replace(`${ctx.itemName}.`, "").split(".")[0] ?? "";
1481
+ if (nestedFieldId) addNested(ctx.repeaterFieldId, nestedFieldId);
1482
+ return;
1483
+ }
1484
+ }
1485
+ }
1486
+ function visitProps(props, repeatStack) {
1487
+ for (const value of Object.values(props)) if (value && typeof value === "object" && "$bind" in value) {
1488
+ const bind = value.$bind;
1489
+ if (bind && typeof bind.from === "string") recordBindFrom(bind.from, repeatStack);
1490
+ } else if (value && typeof value === "object" && !Array.isArray(value)) visitProps(value, repeatStack);
1491
+ }
1492
+ function visit(n, repeatStack) {
1493
+ if (!n || typeof n !== "object") return;
1494
+ const nodeObj = n;
1495
+ let nextRepeatStack = repeatStack;
1496
+ if (nodeObj.$repeat && typeof nodeObj.$repeat === "object") {
1497
+ const repeat = nodeObj.$repeat;
1498
+ const itemName = typeof repeat.itemName === "string" ? repeat.itemName : null;
1499
+ const collection = repeat.collection && typeof repeat.collection === "object" ? repeat.collection : null;
1500
+ const from = collection && typeof collection.from === "string" ? collection.from : null;
1501
+ if (itemName && from && from.startsWith("content.")) {
1502
+ const repeaterFieldId = from.replace("content.", "").split(".")[0] ?? "";
1503
+ if (repeaterFieldId) {
1504
+ topLevel.add(repeaterFieldId);
1505
+ itemNameByRepeater.set(repeaterFieldId, itemName);
1506
+ nextRepeatStack = [...repeatStack, {
1507
+ repeaterFieldId,
1508
+ itemName
1509
+ }];
1510
+ }
1511
+ }
1512
+ }
1513
+ if (nodeObj.$bind && typeof nodeObj.$bind === "object") {
1514
+ const bind = nodeObj.$bind;
1515
+ if (typeof bind.from === "string") recordBindFrom(bind.from, nextRepeatStack);
1516
+ }
1517
+ if (nodeObj.props && typeof nodeObj.props === "object") visitProps(nodeObj.props, nextRepeatStack);
1518
+ if (Array.isArray(nodeObj.children)) for (const child of nodeObj.children) visit(child, nextRepeatStack);
1519
+ }
1520
+ visit(node, []);
1521
+ return {
1522
+ topLevel,
1523
+ nestedByRepeater,
1524
+ itemNameByRepeater
1525
+ };
1526
+ }
1527
+ /**
1528
+ * Find content nodes that should have bindings but don't.
1529
+ * Returns validation errors for text, richText, media, button, link nodes without $bind.
1530
+ */
1531
+ function findMissingBindings(node, blockId, path = "layout") {
1532
+ const errors = [];
1533
+ if (!node || typeof node !== "object") return errors;
1534
+ const n = node;
1535
+ const nodeType = n.type;
1536
+ if ("$repeat" in n && n.$repeat && typeof n.$repeat === "object") {
1537
+ if (nodeType && CONTENT_NODE_TYPES.includes(nodeType)) errors.push({
1538
+ path,
1539
+ message: `Block "${blockId}": ${nodeType} node at ${path} uses $repeat, but content nodes cannot be repeat containers`,
1540
+ suggestion: `Move $repeat to a container node (grid/stack/div) and put a ${nodeType} child inside with $bind.from like "item.field".`
1541
+ });
1542
+ if (!Array.isArray(n.children) || n.children.length === 0) errors.push({
1543
+ path,
1544
+ message: `Block "${blockId}": node at ${path} uses $repeat but has no children to repeat`,
1545
+ suggestion: "Add one or more child nodes under children[] that bind to the repeat item (e.g., $bind.from: \"item.text\")."
1546
+ });
1547
+ }
1548
+ if (nodeType && CONTENT_NODE_TYPES.includes(nodeType)) {
1549
+ const hasBind = "$bind" in n && n.$bind && typeof n.$bind === "object" && "from" in n.$bind;
1550
+ if (Array.isArray(n.children) && n.children.length > 0) errors.push({
1551
+ path,
1552
+ message: `Block "${blockId}": ${nodeType} node at ${path} has children, but content nodes must have children: null`,
1553
+ suggestion: `Replace this ${nodeType} with a container node (div/stack/inline) and put the ${nodeType} nodes inside it.`
1554
+ });
1555
+ if (!hasBind) errors.push({
1556
+ path: `${path}`,
1557
+ message: `Block "${blockId}": ${nodeType} node at ${path} has no $bind - it won't display any content`,
1558
+ suggestion: `Add $bind: { from: "content.fieldName" } to connect this ${nodeType} to a field. Example: { "type": "${nodeType}", "$bind": { "from": "content.headline" } }`
1559
+ });
1560
+ }
1561
+ if ("children" in n && Array.isArray(n.children)) for (let i = 0; i < n.children.length; i++) {
1562
+ const childErrors = findMissingBindings(n.children[i], blockId, `${path}.children[${i}]`);
1563
+ errors.push(...childErrors);
1564
+ }
1565
+ return errors;
1566
+ }
1567
+ /**
1568
+ * Calculate the maximum depth of a layout tree.
1569
+ * Returns 0 for a leaf node, increments for each level of nesting.
1570
+ */
1571
+ function getMaxDepth(node, currentDepth = 0) {
1572
+ if (!node || typeof node !== "object") return currentDepth;
1573
+ const n = node;
1574
+ if (!n.children || !Array.isArray(n.children)) return currentDepth;
1575
+ if (n.children.length === 0) return currentDepth;
1576
+ return Math.max(...n.children.map((child) => getMaxDepth(child, currentDepth + 1)));
1577
+ }
1578
+ /**
1579
+ * Container node types that might be truncated if they have null children at max depth.
1580
+ */
1581
+ const CONTAINER_NODE_TYPES = [
1582
+ "section",
1583
+ "stack",
1584
+ "grid",
1585
+ "div",
1586
+ "inline"
1587
+ ];
1588
+ /**
1589
+ * Detect if layout was likely truncated at depth limit.
1590
+ * Returns paths to container nodes at max depth that have children: null.
1591
+ *
1592
+ * This helps identify when the LLM wanted to create deeper structures but
1593
+ * was constrained by the schema depth limit.
1594
+ */
1595
+ function detectPotentialTruncation(node, maxAllowedDepth, currentDepth = 0, path = "layout") {
1596
+ if (!node || typeof node !== "object") return [];
1597
+ const n = node;
1598
+ const truncatedPaths = [];
1599
+ if (currentDepth === maxAllowedDepth && n.children === null) {
1600
+ if (CONTAINER_NODE_TYPES.includes(n.type || "")) truncatedPaths.push(`${path} (${n.type ?? "node"} at depth ${currentDepth})`);
1601
+ }
1602
+ if (n.children && Array.isArray(n.children)) n.children.forEach((child, i) => {
1603
+ truncatedPaths.push(...detectPotentialTruncation(child, maxAllowedDepth, currentDepth + 1, `${path}.children[${i}]`));
1604
+ });
1605
+ return truncatedPaths;
1606
+ }
1607
+ /**
1608
+ * Format validation errors as actionable feedback for LLM self-correction.
1609
+ */
1610
+ function formatValidationFeedback(errors, output) {
1611
+ const lines = [];
1612
+ if (output && output.blocks[0]?.layout) {
1613
+ const layout = output.blocks[0].layout;
1614
+ const maxDepth = getMaxDepth(layout);
1615
+ const truncatedPaths = detectPotentialTruncation(layout, 6);
1616
+ if (truncatedPaths.length > 0) {
1617
+ lines.push("### DEPTH LIMIT WARNING");
1618
+ lines.push("");
1619
+ lines.push(`The layout reached the maximum depth of 6 levels.`);
1620
+ lines.push("These container nodes at the depth limit have no children:");
1621
+ for (const path of truncatedPaths) lines.push(` ⚠️ ${path}`);
1622
+ lines.push("");
1623
+ lines.push("**FIX**: Flatten your layout structure or use $repeat for lists.");
1624
+ lines.push("- Combine wrapper nodes where possible (e.g., section > stack > div → section > stack)");
1625
+ lines.push("- Use $repeat instead of manual nesting for repeated items");
1626
+ lines.push(`- Current max depth: ${maxDepth}, limit: 6`);
1627
+ lines.push("");
1628
+ }
1629
+ }
1630
+ if (errors.length === 0) return lines.length > 0 ? lines.join("\n") : "";
1631
+ lines.push("## CRITICAL: Your output failed validation. You MUST fix these errors.");
1632
+ lines.push("");
1633
+ lines.push("Your previous output had the following problems:");
1634
+ lines.push("");
1635
+ const missingBindErrors = errors.filter((e) => e.message.includes("no $bind"));
1636
+ const unusedFieldErrors = errors.filter((e) => e.message.includes("never used"));
1637
+ const otherErrors = errors.filter((e) => !e.message.includes("no $bind") && !e.message.includes("never used"));
1638
+ if (missingBindErrors.length > 0) {
1639
+ lines.push("### MISSING $bind ERRORS (nodes that display nothing)");
1640
+ lines.push("");
1641
+ for (const error of missingBindErrors) lines.push(`❌ ${error.message}`);
1642
+ lines.push("");
1643
+ lines.push("**FIX**: Every text, richText, media, button, and link node MUST have $bind.");
1644
+ lines.push("");
1645
+ lines.push("WRONG (displays nothing):");
1646
+ lines.push("```json");
1647
+ lines.push("{ \"type\": \"text\", \"props\": { \"as\": \"h1\" }, \"children\": null, \"$bind\": null }");
1648
+ lines.push("```");
1649
+ lines.push("");
1650
+ lines.push("CORRECT (bound to field):");
1651
+ lines.push("```json");
1652
+ lines.push("{ \"type\": \"text\", \"props\": { \"as\": \"h1\" }, \"children\": null, \"$bind\": { \"from\": \"content.headline\", \"fallback\": null, \"transforms\": null } }");
1653
+ lines.push("```");
1654
+ lines.push("");
1655
+ }
1656
+ if (unusedFieldErrors.length > 0) {
1657
+ lines.push("### UNUSED FIELD ERRORS (fields defined but never bound)");
1658
+ lines.push("");
1659
+ for (const error of unusedFieldErrors) lines.push(`❌ ${error.message}`);
1660
+ lines.push("");
1661
+ const fieldNames = unusedFieldErrors.flatMap((e) => {
1662
+ const captured = e.message.match(/never used in layout: (.+)$/)?.[1];
1663
+ return captured ? captured.split(", ") : [];
1664
+ });
1665
+ if (fieldNames.length > 0) {
1666
+ lines.push("**CHECKLIST - Add a binding for EACH of these fields:**");
1667
+ for (const field of fieldNames) lines.push(`- [ ] ${field} → text node with $bind: { from: "content.${field}" }`);
1668
+ lines.push("");
1669
+ }
1670
+ lines.push("**COMMON MISTAKE**: Creating container nodes with `children: null` instead of actual bindings.");
1671
+ lines.push("");
1672
+ lines.push("This is WRONG (container with no content inside):");
1673
+ lines.push("```json");
1674
+ lines.push("{ \"type\": \"inline\", \"children\": null }");
1675
+ lines.push("```");
1676
+ lines.push("");
1677
+ lines.push("This is CORRECT (container with bound text node):");
1678
+ lines.push("```json");
1679
+ lines.push("{ \"type\": \"inline\", \"children\": [");
1680
+ lines.push(" { \"type\": \"text\", \"$bind\": { \"from\": \"content.fieldName\", \"fallback\": null, \"transforms\": null }, \"props\": null, \"$repeat\": null, \"$when\": null, \"children\": null }");
1681
+ lines.push("] }");
1682
+ lines.push("```");
1683
+ lines.push("");
1684
+ lines.push("**FIX**: Every field you define MUST be used in a $bind somewhere in the layout.");
1685
+ lines.push("If you have a field \"highlight\", you MUST have a node with $bind: { from: \"content.highlight\" }");
1686
+ lines.push("");
1687
+ }
1688
+ if (otherErrors.length > 0) {
1689
+ lines.push("### OTHER ERRORS");
1690
+ lines.push("");
1691
+ for (const error of otherErrors) {
1692
+ lines.push(`❌ ${error.message}`);
1693
+ if (error.suggestion) lines.push(` FIX: ${error.suggestion}`);
1694
+ }
1695
+ lines.push("");
1696
+ }
1697
+ lines.push("---");
1698
+ lines.push("Re-generate the COMPLETE output with these errors fixed.");
1699
+ lines.push("Do NOT just acknowledge the errors - output the corrected JSON.");
1700
+ return lines.join("\n");
1701
+ }
1702
+ //#endregion
1703
+ //#region src/manifest/passes/pass1-fields.ts
1704
+ /**
1705
+ * Pass 1: Field Extraction
1706
+ *
1707
+ * Shared schema and prompt for extracting editable content fields from HTML.
1708
+ * Used by both two-pass and three-pass converters.
1709
+ */
1710
+ const simpleFieldTypeEnum = z.enum([
1711
+ "text",
1712
+ "richText",
1713
+ "media",
1714
+ "link",
1715
+ "boolean",
1716
+ "select",
1717
+ "number"
1718
+ ]);
1719
+ z.enum([
1720
+ "text",
1721
+ "richText",
1722
+ "media",
1723
+ "link",
1724
+ "boolean",
1725
+ "select",
1726
+ "number",
1727
+ "repeater",
1728
+ "group"
1729
+ ]);
1730
+ const linkDefaultValue = z.object({ label: z.string().describe("The visible link text") });
1731
+ const mediaDefaultValue = z.object({
1732
+ src: z.string().describe("Image/video source URL"),
1733
+ alt: z.string().describe("Alt text for accessibility (empty string if none)")
1734
+ });
1735
+ /**
1736
+ * Schema for nested fields inside repeaters/groups.
1737
+ * Uses simple types only (no nested repeaters) to avoid deep recursion.
1738
+ */
1739
+ const nestedFieldSchema = z.object({
1740
+ id: z.string().min(1).describe("camelCase field ID"),
1741
+ type: simpleFieldTypeEnum,
1742
+ label: z.string().min(1),
1743
+ required: z.boolean().nullable()
1744
+ });
1745
+ /**
1746
+ * Simple field schema (text, richText, media, link, boolean, select, number).
1747
+ */
1748
+ const simpleFieldSchema = z.object({
1749
+ id: z.string().min(1).describe("camelCase field ID (e.g., headline, ctaButton)"),
1750
+ type: simpleFieldTypeEnum,
1751
+ label: z.string().min(1),
1752
+ required: z.boolean(),
1753
+ multiline: z.boolean().nullable().describe("For text fields: true if multiple lines"),
1754
+ defaultValue: z.union([
1755
+ z.string(),
1756
+ z.number(),
1757
+ z.boolean(),
1758
+ linkDefaultValue,
1759
+ mediaDefaultValue,
1760
+ z.null()
1761
+ ]).describe("Default content extracted from HTML"),
1762
+ htmlContext: z.string().describe("Brief description of where this appears in the HTML")
1763
+ });
1764
+ /**
1765
+ * Repeater field schema - for lists of similar items that editors can add/remove/reorder.
1766
+ */
1767
+ const repeaterFieldSchema = z.object({
1768
+ id: z.string().min(1).describe("camelCase field ID (e.g., socialLinks, features)"),
1769
+ type: z.literal("repeater"),
1770
+ label: z.string().min(1),
1771
+ required: z.boolean(),
1772
+ itemLabel: z.string().min(1).describe("Singular label for each item (e.g., \"Link\", \"Feature\")"),
1773
+ itemFields: z.array(nestedFieldSchema).min(1).describe("Fields for each item in the repeater"),
1774
+ htmlContext: z.string().describe("Brief description of where this appears in the HTML")
1775
+ });
1776
+ /**
1777
+ * Group field schema - for organizing related fields together (not repeating).
1778
+ */
1779
+ const groupFieldSchema = z.object({
1780
+ id: z.string().min(1).describe("camelCase field ID (e.g., contactInfo, seoSettings)"),
1781
+ type: z.literal("group"),
1782
+ label: z.string().min(1),
1783
+ required: z.boolean(),
1784
+ groupFields: z.array(nestedFieldSchema).min(1).describe("Fields within the group"),
1785
+ htmlContext: z.string().describe("Brief description of where this appears in the HTML")
1786
+ });
1787
+ /**
1788
+ * Combined field schema supporting all field types.
1789
+ * Uses discriminated union on 'type' field.
1790
+ */
1791
+ const extractedFieldSchema = z.union([
1792
+ simpleFieldSchema,
1793
+ repeaterFieldSchema,
1794
+ groupFieldSchema
1795
+ ]);
1796
+ /**
1797
+ * Schema for extracted content entry data.
1798
+ * Used when the LLM decides to use content types.
1799
+ */
1800
+ const extractedEntrySchema = z.object({
1801
+ identifier: z.string().regex(/^[a-z0-9-]+$/).describe("Unique slug-safe identifier (e.g., wine-tasting-evening)"),
1802
+ title: z.string().min(1).describe("Entry title"),
1803
+ data: z.object({}).passthrough().describe("Entry data matching the content type schema")
1804
+ });
1805
+ /**
1806
+ * Schema for a new content type definition.
1807
+ * Used when no existing content type fits.
1808
+ */
1809
+ const newContentTypeSchema = z.object({
1810
+ key: z.string().regex(/^[a-z][a-z0-9-]*$/).describe("Content type key (e.g., testimonial, team-member)"),
1811
+ name: z.string().min(1).describe("Human-readable name"),
1812
+ description: z.string().nullable().describe("Description of this content type"),
1813
+ fields: z.array(z.object({
1814
+ id: z.string().min(1),
1815
+ type: z.string().min(1),
1816
+ label: z.string().min(1),
1817
+ required: z.boolean().nullable(),
1818
+ description: z.string().nullable()
1819
+ })).min(1),
1820
+ isRouted: z.boolean().describe("Whether entries have their own pages")
1821
+ });
1822
+ /**
1823
+ * Discriminated union for content type decision.
1824
+ * The LLM chooses how to handle list-like content.
1825
+ */
1826
+ const contentTypeApproachSchema = z.discriminatedUnion("approach", [
1827
+ z.object({
1828
+ approach: z.literal("existingContentType"),
1829
+ contentTypeKey: z.string().min(1).describe("Key of the existing content type to use"),
1830
+ entries: z.array(extractedEntrySchema).min(1).describe("Extracted entries"),
1831
+ rationale: z.string().describe("Why this content type fits")
1832
+ }),
1833
+ z.object({
1834
+ approach: z.literal("newContentType"),
1835
+ contentType: newContentTypeSchema,
1836
+ entries: z.array(extractedEntrySchema).min(1).describe("Extracted entries"),
1837
+ rationale: z.string().describe("Why a new content type is needed")
1838
+ }),
1839
+ z.object({
1840
+ approach: z.literal("repeater"),
1841
+ rationale: z.string().describe("Why repeater is preferred over content type")
1842
+ }),
1843
+ z.object({
1844
+ approach: z.literal("none"),
1845
+ rationale: z.string().describe("Why this is not list data")
1846
+ })
1847
+ ]);
1848
+ const pass1OutputSchema = z.object({
1849
+ blockId: z.string().regex(/^custom\.[a-z][a-z0-9-]*$/).describe("Block ID like custom.hero-section"),
1850
+ blockTitle: z.string().min(1),
1851
+ blockDescription: z.string().nullable(),
1852
+ category: z.enum([
1853
+ "marketing",
1854
+ "content",
1855
+ "blog",
1856
+ "media",
1857
+ "layout",
1858
+ "interactive"
1859
+ ]),
1860
+ fields: z.array(extractedFieldSchema).min(1),
1861
+ /**
1862
+ * Content type decision for list-like content.
1863
+ * When content types are available, the LLM may decide to use content entries
1864
+ * instead of repeater fields. This is null when no content types were provided
1865
+ * or when the content is not list-like.
1866
+ */
1867
+ contentTypeApproach: contentTypeApproachSchema.nullable()
1868
+ });
1869
+ function buildContentTypePromptSection(contentTypes) {
1870
+ return `
1871
+
1872
+ ## CONTENT TYPE CONSIDERATION
1873
+
1874
+ If this section displays a LIST of similar items (testimonials, events, team members, etc.),
1875
+ you must decide how to handle the data. Choose ONE of these approaches:
1876
+
1877
+ ### Option A: Use existing content type (PREFERRED when schema matches)
1878
+
1879
+ Available content types:
1880
+ ${contentTypes.map((ct) => {
1881
+ const fieldList = ct.fields.map((f) => `${f.id} (${f.type})`).join(", ");
1882
+ return `- **${ct.key}**: ${ct.name}
1883
+ Fields: ${fieldList}
1884
+ Routable: ${ct.isRouted ? "Yes (has own pages)" : "No"}`;
1885
+ }).join("\n")}
1886
+
1887
+ If using a content type:
1888
+ 1. Add a "contentType" field with the type key as defaultValue
1889
+ 2. Extract entries matching the content type's schema
1890
+ 3. Set contentTypeApproach with approach: "existingContentType"
1891
+
1892
+ ### Option B: Create new content type (when no existing type fits)
1893
+
1894
+ Define a new content type with:
1895
+ - key: slug-safe identifier
1896
+ - name: human-readable name
1897
+ - fields: schema fields matching the data
1898
+ - isRouted: true if entries should have their own pages
1899
+
1900
+ ### Option C: Use repeater field (for page-specific lists)
1901
+
1902
+ Use when items are specific to this page and won't be reused elsewhere.
1903
+ Example: FAQ items, feature bullet points, pricing tiers.
1904
+ Set contentTypeApproach with approach: "repeater"
1905
+
1906
+ ### Option D: Not list data (standard fields)
1907
+
1908
+ If this is not list-like content, set contentTypeApproach with approach: "none"
1909
+
1910
+ ## GUIDANCE
1911
+
1912
+ - Testimonials, team bios → content types (reusable across site)
1913
+ - Events, blog posts → content types (have own pages)
1914
+ - FAQ items, feature bullets → repeaters (page-specific)
1915
+ - Single items → plain fields`;
1916
+ }
1917
+ const PASS1_SYSTEM_PROMPT = `You are an expert at analyzing HTML/JSX markup to extract editable content fields for a CMS.
1918
+
1919
+ Your task: Analyze the HTML/JSX markup and identify ALL pieces of content that should be editable by a user.
1920
+
1921
+ ## FIELD ECONOMY PRINCIPLES
1922
+
1923
+ **Prefer fewer, smarter fields. CMS editors should not face 25-field forms.**
1924
+
1925
+ 1. **Use richText for formatted lists and multi-line content:**
1926
+ - Opening hours → single richText with formatted list (not 6 separate text fields)
1927
+ - Address with line breaks → single richText with hard line breaks / Shift+Enter between lines (not address1, address2, city, postcode)
1928
+ - Feature lists, specs, ingredients → richText with bullet list
1929
+
1930
+ RichText supports: paragraphs, hard line breaks, bold, italic, bullet lists, numbered lists, links.
1931
+ The CMS editor can format content naturally without rigid field structure.
1932
+ For line-oriented content like addresses, opening hours, and contact details, keep one logical paragraph and use hard line breaks between lines. In the CMS editor, authors create those line breaks with Shift+Enter; Enter creates a new paragraph.
1933
+
1934
+ 2. **Use repeaters only when items need individual editing/reordering:**
1935
+ - Testimonials with avatar + name + quote → repeater (each item is distinct)
1936
+ - Team members with photo + bio → repeater
1937
+ - Social links where user adds/removes platforms → repeater
1938
+
1939
+ 3. **Anti-patterns to AVOID:**
1940
+ ❌ hoursMonFriDay, hoursMonFriTime, hoursSatDay... (6+ fields for hours table)
1941
+ ❌ socialInstagram, socialFacebook, socialTwitter (proliferating similar fields)
1942
+ ❌ address1, address2, city, state, zip (5 fields for one address)
1943
+
1944
+ ✅ openingHours: single richText field with formatted content / hard line breaks
1945
+ ✅ address: single richText field with hard line breaks (Shift+Enter in the editor)
1946
+ ✅ socialLinks: repeater with { platform, url } IF user needs to add/remove links
1947
+
1948
+ ## Field Type Rules
1949
+
1950
+ ### Simple Fields
1951
+ - **text**: Short text (<200 chars), headings, labels, button text
1952
+ - Set multiline: true for paragraphs or text with line breaks
1953
+ - Set multiline: false for single-line text
1954
+ - **richText**: Formatted content with HTML tags (p, strong, em, ul, etc.)
1955
+ - **media**: Images (<img>) or videos (<video>)
1956
+ - defaultValue: { "src": "image-url", "alt": "description" }
1957
+ - **link**: Anchors (<a>) or buttons that navigate
1958
+ - defaultValue: { "label": "visible text" }
1959
+ - Only extract the visible text - link destinations are configured in the CMS
1960
+
1961
+ ### Container Fields
1962
+
1963
+ - **repeater**: List of similar items that editors can add/remove/reorder
1964
+ - Use when: Multiple similar items (social links, features, team members)
1965
+ - Requires: itemLabel (singular name for item), itemFields (array of nested fields)
1966
+ - Example: Social links → repeater with itemFields: [{ id: "platform", type: "text" }, { id: "url", type: "link" }]
1967
+
1968
+ - **group**: Related fields grouped together (NOT repeating)
1969
+ - Use when: Logically related fields that should appear together in the CMS
1970
+ - Requires: groupFields (array of nested fields)
1971
+ - Example: Contact info → group with groupFields: [{ id: "phone", type: "text" }, { id: "email", type: "text" }]
1972
+
1973
+ ### When to use each type:
1974
+ - Single value → simple field (text, richText, media, link)
1975
+ - Formatted list where structure is static → richText (opening hours, address; use hard line breaks / Shift+Enter for line-oriented text)
1976
+ - List where items can be added/removed/reordered → repeater
1977
+ - Related fields that belong together → group
1978
+
1979
+ ## Output Requirements
1980
+
1981
+ 1. Give each field a camelCase id (e.g., headline, ctaButton, heroImage)
1982
+ 2. Extract the actual content as defaultValue
1983
+ 3. Describe where each field appears in htmlContext
1984
+ 4. Set required: true for primary content, false for optional
1985
+
1986
+ ## Examples
1987
+
1988
+ ### Example 1: Simple Fields
1989
+
1990
+ Input HTML:
1991
+ \`\`\`html
1992
+ <section>
1993
+ <h1>Welcome to Our Site</h1>
1994
+ <p>We build amazing products.</p>
1995
+ <a href="/start">Get Started</a>
1996
+ </section>
1997
+ \`\`\`
1998
+
1999
+ Output:
2000
+ \`\`\`json
2001
+ {
2002
+ "blockId": "custom.hero",
2003
+ "blockTitle": "Hero Section",
2004
+ "blockDescription": "Simple hero with headline, description, and CTA",
2005
+ "category": "marketing",
2006
+ "fields": [
2007
+ { "id": "headline", "type": "text", "label": "Headline", "required": true, "multiline": false, "defaultValue": "Welcome to Our Site", "htmlContext": "h1 main heading" },
2008
+ { "id": "description", "type": "text", "label": "Description", "required": true, "multiline": false, "defaultValue": "We build amazing products.", "htmlContext": "p below heading" },
2009
+ { "id": "cta", "type": "link", "label": "Call to Action", "required": false, "multiline": null, "defaultValue": { "label": "Get Started" }, "htmlContext": "a button at bottom" }
2010
+ ],
2011
+ "contentTypeApproach": { "approach": "none", "rationale": "Single hero section, not list data" }
2012
+ }
2013
+ \`\`\`
2014
+
2015
+ ### Example 2: Repeater Field
2016
+
2017
+ Input HTML with repeating social links:
2018
+ \`\`\`html
2019
+ <footer>
2020
+ <div class="social-links">
2021
+ <a href="https://instagram.com/..."><svg>Instagram</svg></a>
2022
+ <a href="https://facebook.com/..."><svg>Facebook</svg></a>
2023
+ <a href="https://twitter.com/..."><svg>Twitter</svg></a>
2024
+ </div>
2025
+ </footer>
2026
+ \`\`\`
2027
+
2028
+ Output with repeater:
2029
+ \`\`\`json
2030
+ {
2031
+ "blockId": "custom.footer",
2032
+ "blockTitle": "Footer",
2033
+ "blockDescription": "Footer with social links",
2034
+ "category": "layout",
2035
+ "fields": [
2036
+ {
2037
+ "id": "socialLinks",
2038
+ "type": "repeater",
2039
+ "label": "Social Links",
2040
+ "required": false,
2041
+ "itemLabel": "Link",
2042
+ "itemFields": [
2043
+ { "id": "platform", "type": "text", "label": "Platform", "required": true },
2044
+ { "id": "url", "type": "link", "label": "URL", "required": true }
2045
+ ],
2046
+ "htmlContext": "Row of social media icon links"
2047
+ }
2048
+ ],
2049
+ "contentTypeApproach": { "approach": "repeater", "rationale": "Social links are page-specific and editors need to add/remove platforms" }
2050
+ }
2051
+ \`\`\`
2052
+ `;
2053
+ //#endregion
2054
+ //#region src/manifest/passes/ai-helpers.ts
2055
+ /**
2056
+ * Make a structured JSON request to the AI model.
2057
+ *
2058
+ * @param config - Model configuration (model, system prompt, temperature)
2059
+ * @param userMessage - The user message to send
2060
+ * @param schema - Zod schema for response validation
2061
+ * @returns Parsed response matching the schema
2062
+ */
2063
+ async function getJsonFromAi(ai, config, userMessage, schema) {
2064
+ const response = await ai.getAiResponse(config, (_ctx) => [{
2065
+ role: "user",
2066
+ content: userMessage
2067
+ }], {
2068
+ kind: "json",
2069
+ schema
2070
+ });
2071
+ if (response.kind !== "json") throw new Error("Unexpected response kind");
2072
+ return response.data;
2073
+ }
2074
+ //#endregion
2075
+ //#region src/manifest/passes/pass3-styling.ts
2076
+ /**
2077
+ * Pass 3 Styling Module
2078
+ *
2079
+ * Two-step styling pass:
2080
+ * - Pass 3a: LLM outputs className patches (which nodes get which classes)
2081
+ * - Pass 3b: LLM outputs CSS rules (targeting those classes)
2082
+ *
2083
+ * Programmatic transformations between passes ensure coordination:
2084
+ * - Patches are applied deterministically
2085
+ * - Selectors are scoped programmatically
2086
+ */
2087
+ const pass3aOutputSchema = z.object({
2088
+ classNamePatches: z.array(z.object({
2089
+ nodePath: z.string(),
2090
+ className: z.string().min(1)
2091
+ })),
2092
+ rationale: z.string()
2093
+ });
2094
+ const pass3bOutputSchema = z.object({
2095
+ customCssRules: z.array(llmCustomCssRuleSchema).nullable(),
2096
+ customCssAtRules: z.array(llmMediaAtRuleSchema).nullable(),
2097
+ rationale: z.string()
2098
+ });
2099
+ /**
2100
+ * Get a node at a specific path in the layout tree.
2101
+ * Path format: "" for root, or "children[0].children[1]" etc.
2102
+ *
2103
+ * STRICT PARSING: Rejects invalid path formats to prevent silent failures.
2104
+ */
2105
+ function getNodeAtPath(layout, path) {
2106
+ if (path === "" || path === "root" || path === "layout") return layout;
2107
+ let normalizedPath = path;
2108
+ if (normalizedPath.startsWith("layout.")) normalizedPath = normalizedPath.slice(7);
2109
+ if (!/^(children\[\d+\]\.)*children\[\d+\]$/.test(normalizedPath)) return null;
2110
+ const parts = normalizedPath.match(/children\[(\d+)\]/g) || [];
2111
+ let current = layout;
2112
+ for (const part of parts) {
2113
+ const indexMatch = part.match(/\d+/);
2114
+ if (!indexMatch) return null;
2115
+ const index = parseInt(indexMatch[0], 10);
2116
+ if (!current.children || !current.children[index]) return null;
2117
+ current = current.children[index];
2118
+ }
2119
+ return current;
2120
+ }
2121
+ /**
2122
+ * Apply className patches to a layout.
2123
+ * Uses APPEND semantics - preserves existing classNames.
2124
+ */
2125
+ function applyClassNamePatches(layout, patches) {
2126
+ const patched = structuredClone(layout);
2127
+ for (const patch of patches) {
2128
+ const node = getNodeAtPath(patched, patch.nodePath);
2129
+ if (!node) throw new Error(`Invalid nodePath: ${patch.nodePath}`);
2130
+ node.props = node.props || {};
2131
+ const existing = node.props.className;
2132
+ if (existing) {
2133
+ if (!existing.split(/\s+/).includes(patch.className)) node.props.className = `${existing} ${patch.className}`;
2134
+ } else node.props.className = patch.className;
2135
+ }
2136
+ return patched;
2137
+ }
2138
+ /**
2139
+ * Generate node path inventory for the LLM prompt.
2140
+ */
2141
+ function buildNodeInventory(layout) {
2142
+ return collectLayoutNodeInfos(layout).map((info) => {
2143
+ const pathDisplay = info.path === "layout" ? "\"\"" : `"${info.path.replace("layout.", "")}"`;
2144
+ const bindInfo = info.bindFrom ? ` -> binds to ${info.bindFrom}` : "";
2145
+ return `- ${pathDisplay} (${info.type})${bindInfo}`;
2146
+ }).join("\n");
2147
+ }
2148
+ /**
2149
+ * Validate Pass 3a output - className patches.
2150
+ */
2151
+ function validatePass3aOutput(output, layout) {
2152
+ const errors = [];
2153
+ const seenPaths = /* @__PURE__ */ new Set();
2154
+ for (const patch of output.classNamePatches) {
2155
+ if (seenPaths.has(patch.nodePath)) {}
2156
+ seenPaths.add(patch.nodePath);
2157
+ if (!getNodeAtPath(layout, patch.nodePath)) errors.push(`Invalid nodePath "${patch.nodePath}": node does not exist in layout`);
2158
+ if (!/^[a-zA-Z_-][a-zA-Z0-9_-]*$/.test(patch.className)) errors.push(`Invalid className "${patch.className}": must be a valid CSS class name (no spaces, start with letter/underscore/hyphen)`);
2159
+ }
2160
+ return errors;
2161
+ }
2162
+ /**
2163
+ * Extract class names from a CSS selector.
2164
+ *
2165
+ * This is a simplified version used for validating LLM output BEFORE scoping.
2166
+ * At this stage, selectors are simple like ".hero-title" without [data-block=...].
2167
+ *
2168
+ * NOTE: A different implementation exists in output-schema.ts for final validation
2169
+ * of scoped selectors (strips [data-block=...] and skips .cms-node-* classes).
2170
+ */
2171
+ function extractSelectorClasses(selector) {
2172
+ const classes = /* @__PURE__ */ new Set();
2173
+ const re = /\.([_a-zA-Z][-_a-zA-Z0-9]*)/g;
2174
+ let match;
2175
+ while ((match = re.exec(selector)) !== null) {
2176
+ const className = match[1];
2177
+ if (className) classes.add(className);
2178
+ }
2179
+ return classes;
2180
+ }
2181
+ /**
2182
+ * Validate Pass 3b output BEFORE scoping.
2183
+ * Ensures selectors are class-based and reference only available classNames.
2184
+ */
2185
+ function validatePass3bOutput(output, availableClassNames) {
2186
+ const errors = [];
2187
+ const checkSelector = (selector, ruleId) => {
2188
+ if (selector.includes("[data-block=")) errors.push(`Rule "${ruleId}": Selector should not include [data-block=...]. We add scoping programmatically.`);
2189
+ if (selector.includes("@media") || selector.includes("@supports")) errors.push(`Rule "${ruleId}": Selector contains @media/@supports. Use customCssAtRules for responsive styles.`);
2190
+ const selectorClasses = extractSelectorClasses(selector);
2191
+ for (const cls of selectorClasses) if (!availableClassNames.has(cls)) errors.push(`Rule "${ruleId}": Selector references unknown class ".${cls}". Available: ${Array.from(availableClassNames).join(", ")}`);
2192
+ };
2193
+ for (const rule of output.customCssRules ?? []) checkSelector(rule.selector, rule.id);
2194
+ for (const atRule of output.customCssAtRules ?? []) for (const nestedRule of atRule.rules) checkSelector(nestedRule.selector, atRule.id);
2195
+ return errors;
2196
+ }
2197
+ /**
2198
+ * Scope a single selector with the block prefix.
2199
+ * Handles comma-separated selector lists safely.
2200
+ *
2201
+ * IMPORTANT: ".a, .b" must become "[data-block=...] .a, [data-block=...] .b"
2202
+ * NOT "[data-block=...] .a, .b" (which would leak .b globally)
2203
+ */
2204
+ function scopeSelector(prefix, selector) {
2205
+ return selector.split(",").map((part) => `${prefix} ${part.trim()}`).join(", ");
2206
+ }
2207
+ /**
2208
+ * Scope all selectors in CSS rules.
2209
+ * Operates on LLM-format rules (before normalization).
2210
+ */
2211
+ function scopeSelectorsInPlace(prefix, rules) {
2212
+ return rules.map((rule) => ({
2213
+ ...rule,
2214
+ selector: scopeSelector(prefix, rule.selector)
2215
+ }));
2216
+ }
2217
+ /**
2218
+ * Scope and normalize CSS rules from Pass 3b output.
2219
+ * Pipeline: scope selectors -> normalize declarations
2220
+ */
2221
+ function scopeAndNormalizeRules(blockId, output) {
2222
+ const prefix = `[data-block="${blockId}"]`;
2223
+ const scopedRules = output.customCssRules ? scopeSelectorsInPlace(prefix, output.customCssRules) : null;
2224
+ const scopedAtRules = output.customCssAtRules ? output.customCssAtRules.map((atRule) => ({
2225
+ ...atRule,
2226
+ rules: scopeSelectorsInPlace(prefix, atRule.rules)
2227
+ })) : null;
2228
+ return {
2229
+ customCssRules: scopedRules?.map(normalizeLlmCssRule) ?? null,
2230
+ customCssAtRules: scopedAtRules?.map(normalizeLlmMediaAtRule) ?? null
2231
+ };
2232
+ }
2233
+ function buildPass3aSystemPrompt() {
2234
+ return `You are an expert at styling CMS block layouts.
2235
+
2236
+ ## YOUR TASK
2237
+
2238
+ Analyze the layout and determine which nodes need CSS class names for styling.
2239
+ You will output className patches - a list of which nodes get which class names.
2240
+
2241
+ ## RULES
2242
+
2243
+ 1. Assign classNames to nodes that need custom styling
2244
+ 2. Use descriptive, semantic class names (e.g., "hero-title", "cta-button", "feature-grid")
2245
+ 3. Use nodePath EXACTLY as shown in the available paths list
2246
+ 4. The root node is referenced as "" (empty string)
2247
+ 5. You can assign multiple patches to different nodes
2248
+ 6. Use kebab-case for class names (e.g., "hero-title" not "heroTitle")
2249
+
2250
+ ## OUTPUT FORMAT
2251
+
2252
+ Return:
2253
+ - classNamePatches: Array of { nodePath, className } objects
2254
+ - rationale: Explain your styling decisions
2255
+
2256
+ ## EXAMPLE
2257
+
2258
+ Available paths:
2259
+ - "" (section)
2260
+ - "children[0]" (text) -> binds to content.headline
2261
+ - "children[1]" (text) -> binds to content.body
2262
+
2263
+ Output:
2264
+ \`\`\`json
2265
+ {
2266
+ "classNamePatches": [
2267
+ { "nodePath": "", "className": "hero-section" },
2268
+ { "nodePath": "children[0]", "className": "hero-title" },
2269
+ { "nodePath": "children[1]", "className": "hero-body" }
2270
+ ],
2271
+ "rationale": "Added semantic class names to enable styling. The root section gets 'hero-section' for container styles, the headline gets 'hero-title' for typography, and the body gets 'hero-body' for paragraph styling."
2272
+ }
2273
+ \`\`\`
2274
+ `;
2275
+ }
2276
+ /**
2277
+ * Build the user message for Pass 3a (className patches).
2278
+ */
2279
+ function buildPass3aUserMessage(layout, html, nodeInventory) {
2280
+ return `Analyze this layout and determine which nodes need CSS class names for styling.
2281
+
2282
+ ## LAYOUT TREE
2283
+
2284
+ \`\`\`json
2285
+ ${JSON.stringify(layout, null, 2)}
2286
+ \`\`\`
2287
+
2288
+ ## AVAILABLE NODE PATHS
2289
+
2290
+ Choose from these paths (generated from the layout):
2291
+ ${nodeInventory}
2292
+
2293
+ ## ORIGINAL HTML (for style reference)
2294
+
2295
+ \`\`\`html
2296
+ ${html}
2297
+ \`\`\`
2298
+
2299
+ Return className patches for nodes that need styling.
2300
+ `;
2301
+ }
2302
+ function buildPass3bSystemPrompt() {
2303
+ return `You are an expert at writing CSS for CMS blocks.
2304
+
2305
+ ## YOUR TASK
2306
+
2307
+ Write CSS rules for the styled layout nodes.
2308
+
2309
+ ## RULES
2310
+
2311
+ 1. Write selectors using ONLY the classNames listed in "AVAILABLE CLASSNAMES"
2312
+ 2. Use simple selectors: \`.hero-title\`, not \`[data-block="..."] .hero-title\`
2313
+ 3. Do NOT include [data-block=...] in selectors - we add that programmatically
2314
+ 4. Use customCssRules for base styles
2315
+ 5. Use customCssAtRules for responsive (@media) styles
2316
+ 6. Match the visual styling from the original HTML
2317
+
2318
+ ## COLOR TOKENS (IMPORTANT)
2319
+
2320
+ - Prefer **literal hex** values from the provided theme summary.
2321
+ - If you use CSS variables from the app, use the **resolved color variables**:
2322
+ - ✅ \`var(--color-primary)\`, \`var(--color-accent)\`, \`var(--color-background)\`, \`var(--color-foreground)\`, etc.
2323
+ - ❌ Do NOT use \`var(--primary)\` / \`var(--accent)\` directly (those are HSL components).
2324
+ - Do NOT invent variables like \`--white\`, \`--text\`, \`--theme-text\`, \`--theme-background\`.
2325
+
2326
+ ## DECLARATION FORMAT
2327
+
2328
+ Use array format with camelCase properties:
2329
+ \`\`\`json
2330
+ {
2331
+ "declarations": [
2332
+ { "property": "fontSize", "value": "3rem" },
2333
+ { "property": "fontWeight", "value": "bold" }
2334
+ ]
2335
+ }
2336
+ \`\`\`
2337
+
2338
+ ## EXAMPLE
2339
+
2340
+ Available classNames: hero-section, hero-title, hero-body
2341
+
2342
+ Output:
2343
+ \`\`\`json
2344
+ {
2345
+ "customCssRules": [
2346
+ {
2347
+ "id": "hero-title-base",
2348
+ "selector": ".hero-title",
2349
+ "declarations": [
2350
+ { "property": "fontSize", "value": "2rem" },
2351
+ { "property": "fontWeight", "value": "bold" }
2352
+ ]
2353
+ }
2354
+ ],
2355
+ "customCssAtRules": [
2356
+ {
2357
+ "id": "hero-title-lg",
2358
+ "type": "media",
2359
+ "query": "(min-width: 1024px)",
2360
+ "rules": [{
2361
+ "selector": ".hero-title",
2362
+ "declarations": [{ "property": "fontSize", "value": "3rem" }]
2363
+ }]
2364
+ }
2365
+ ],
2366
+ "rationale": "Base font size of 2rem scales to 3rem on large screens."
2367
+ }
2368
+ \`\`\`
2369
+ `;
2370
+ }
2371
+ /**
2372
+ * Build the user message for Pass 3b (CSS rules).
2373
+ */
2374
+ function buildPass3bUserMessage(patchedLayout, html, availableClassNames, themeSummary) {
2375
+ return `Write CSS rules for the styled layout nodes.
2376
+
2377
+ ## LAYOUT WITH CLASSNAMES
2378
+
2379
+ \`\`\`json
2380
+ ${JSON.stringify(patchedLayout, null, 2)}
2381
+ \`\`\`
2382
+
2383
+ ## AVAILABLE CLASSNAMES
2384
+
2385
+ These classNames are assigned to nodes in the layout:
2386
+ ${availableClassNames.map((c) => `- ${c}`).join("\n")}
2387
+
2388
+ ## ORIGINAL HTML (for style reference)
2389
+
2390
+ \`\`\`html
2391
+ ${html}
2392
+ \`\`\`
2393
+
2394
+ ## THEME COLORS
2395
+
2396
+ ${themeSummary}
2397
+
2398
+ Write CSS rules targeting ONLY the available classNames listed above.
2399
+ Do NOT include [data-block=...] in selectors - we add that automatically.
2400
+ `;
2401
+ }
2402
+ //#endregion
2403
+ //#region src/manifest/constants.ts
2404
+ /**
2405
+ * Manifest Generator Constants
2406
+ *
2407
+ * Configuration constants for the HTML-to-manifest converter.
2408
+ */
2409
+ /**
2410
+ * Default model for manifest generation.
2411
+ * Can be overridden via the RIVERBANK_MANIFEST_MODEL environment variable.
2412
+ */
2413
+ const DEFAULT_MANIFEST_MODEL = process.env.RIVERBANK_MANIFEST_MODEL || "gpt-5-mini";
2414
+ //#endregion
2415
+ //#region src/manifest/three-pass-converter.ts
2416
+ /**
2417
+ * Three-Pass HTML-to-Manifest Converter
2418
+ *
2419
+ * Pass 1: Extract fields from HTML (identify editable content)
2420
+ * Pass 2: Generate layout structure using the extracted fields (no styling)
2421
+ * Pass 3: Add styling via two sub-passes:
2422
+ * - Pass 3a: LLM outputs className patches (which nodes get which classes)
2423
+ * - Pass 3b: LLM outputs CSS rules (targeting those classes)
2424
+ *
2425
+ * This approach ensures CSS rules reference classNames that actually exist
2426
+ * in the layout by applying patches programmatically between passes.
2427
+ */
2428
+ /**
2429
+ * Convert a plain string (or simple markdown) to TipTap doc format.
2430
+ *
2431
+ * Double newlines create semantic paragraphs. Single newlines remain inside the
2432
+ * current paragraph as TipTap hardBreak nodes for line-oriented content such as
2433
+ * addresses and opening hours.
2434
+ */
2435
+ function stringToTipTapDoc(text) {
2436
+ return createRichTextDocFromMarkdown(text);
2437
+ }
2438
+ /**
2439
+ * Coerce a richText defaultValue to TipTap format.
2440
+ * Handles:
2441
+ * - Plain strings → TipTap doc with paragraphs
2442
+ * - Already valid TipTap docs → pass through
2443
+ */
2444
+ function coerceRichTextDefaultValue(value) {
2445
+ if (value === null || value === void 0) return null;
2446
+ if (typeof value === "string") return stringToTipTapDoc(value);
2447
+ const parsed = RichTextDocSchema.safeParse(value);
2448
+ return parsed.success ? parsed.data : null;
2449
+ }
2450
+ /**
2451
+ * Check if a Pass 1 field is a repeater field.
2452
+ */
2453
+ function isRepeaterField(field) {
2454
+ return field.type === "repeater" && "itemFields" in field;
2455
+ }
2456
+ /**
2457
+ * Check if a Pass 1 field is a group field.
2458
+ */
2459
+ function isGroupField(field) {
2460
+ return field.type === "group" && "groupFields" in field;
2461
+ }
2462
+ /**
2463
+ * Map a Pass 1 field to a simplified field format for HtmlToManifestOutput.
2464
+ * This produces FieldDefinition-compatible output.
2465
+ */
2466
+ function mapPass1FieldToSimpleOutput(field) {
2467
+ if (isRepeaterField(field)) return {
2468
+ id: field.id,
2469
+ type: "repeater",
2470
+ label: field.label,
2471
+ required: field.required,
2472
+ itemLabel: field.itemLabel,
2473
+ minItems: 0,
2474
+ maxItems: void 0,
2475
+ schema: { fields: field.itemFields.map((nf) => ({
2476
+ id: nf.id,
2477
+ type: nf.type,
2478
+ label: nf.label,
2479
+ required: nf.required ?? false
2480
+ })) }
2481
+ };
2482
+ if (isGroupField(field)) return {
2483
+ id: field.id,
2484
+ type: "group",
2485
+ label: field.label,
2486
+ required: field.required,
2487
+ schema: { fields: field.groupFields.map((nf) => ({
2488
+ id: nf.id,
2489
+ type: nf.type,
2490
+ label: nf.label,
2491
+ required: nf.required ?? false
2492
+ })) }
2493
+ };
2494
+ const simpleField = field;
2495
+ let defaultValue = simpleField.defaultValue ?? void 0;
2496
+ if (field.type === "richText" && defaultValue !== void 0) defaultValue = coerceRichTextDefaultValue(defaultValue) ?? void 0;
2497
+ return {
2498
+ id: field.id,
2499
+ type: field.type,
2500
+ label: field.label,
2501
+ required: field.required,
2502
+ defaultValue,
2503
+ multiline: simpleField.multiline ?? void 0
2504
+ };
2505
+ }
2506
+ /**
2507
+ * Map a Pass 1 field to the output schema format.
2508
+ * Handles simple fields, repeaters, and groups.
2509
+ */
2510
+ function mapPass1FieldToOutputField(field) {
2511
+ if (isRepeaterField(field)) return {
2512
+ id: field.id,
2513
+ type: "repeater",
2514
+ label: field.label,
2515
+ description: null,
2516
+ required: field.required,
2517
+ defaultValue: null,
2518
+ itemLabel: field.itemLabel,
2519
+ itemLabelSource: null,
2520
+ minItems: 0,
2521
+ maxItems: null,
2522
+ allowConversion: false,
2523
+ schema: { fields: field.itemFields.map((nf) => ({
2524
+ id: nf.id,
2525
+ type: nf.type,
2526
+ label: nf.label,
2527
+ required: nf.required,
2528
+ multiline: null,
2529
+ defaultValue: null
2530
+ })) }
2531
+ };
2532
+ if (isGroupField(field)) return {
2533
+ id: field.id,
2534
+ type: "group",
2535
+ label: field.label,
2536
+ description: null,
2537
+ required: field.required,
2538
+ defaultValue: null,
2539
+ schema: { fields: field.groupFields.map((nf) => ({
2540
+ id: nf.id,
2541
+ type: nf.type,
2542
+ label: nf.label,
2543
+ required: nf.required,
2544
+ multiline: null,
2545
+ defaultValue: null
2546
+ })) }
2547
+ };
2548
+ const simpleField = field;
2549
+ let defaultValue = simpleField.defaultValue ?? null;
2550
+ if (field.type === "richText" && defaultValue !== null) defaultValue = coerceRichTextDefaultValue(defaultValue) ?? null;
2551
+ return {
2552
+ id: field.id,
2553
+ type: field.type,
2554
+ label: field.label,
2555
+ description: null,
2556
+ required: field.required,
2557
+ defaultValue,
2558
+ multiline: simpleField.multiline ?? false,
2559
+ maxLength: null
2560
+ };
2561
+ }
2562
+ const pass2OutputSchema = z.object({
2563
+ layout: llmNodeSchema,
2564
+ rationale: z.string().describe("Explain which node binds to which field")
2565
+ });
2566
+ function buildPass2SystemPrompt(fields, contentTypeApproach) {
2567
+ const fieldList = fields.map((f) => {
2568
+ if (isRepeaterField(f)) {
2569
+ const itemFieldsList = f.itemFields.map((nf) => `${nf.id} (${nf.type})`).join(", ");
2570
+ return `- ${f.id} (repeater): ${f.label}\n Item fields: ${itemFieldsList}`;
2571
+ }
2572
+ if (isGroupField(f)) {
2573
+ const groupFieldsList = f.groupFields.map((nf) => `${nf.id} (${nf.type})`).join(", ");
2574
+ return `- ${f.id} (group): ${f.label}\n Group fields: ${groupFieldsList}`;
2575
+ }
2576
+ return `- ${f.id} (${f.type}): ${f.label}`;
2577
+ }).join("\n");
2578
+ const hasRepeaterFields = fields.some(isRepeaterField);
2579
+ let contentTypeSection = "";
2580
+ if (contentTypeApproach?.approach === "existingContentType" || contentTypeApproach?.approach === "newContentType") contentTypeSection = `
2581
+
2582
+ ## DATA BINDING FOR CONTENT ENTRIES
2583
+
2584
+ This block loads "${contentTypeApproach.approach === "existingContentType" ? contentTypeApproach.contentTypeKey : contentTypeApproach.contentType.key}" entries via dataLoaders. Use the $repeat pattern to iterate:
2585
+
2586
+ \`\`\`json
2587
+ {
2588
+ "type": "grid",
2589
+ "$repeat": { "collection": { "from": "data.entries" }, "itemName": "entry" },
2590
+ "children": [
2591
+ { "type": "text", "$bind": { "from": "entry.content.name", "fallback": null, "transforms": null }, "props": null, "children": null, "$repeat": null },
2592
+ { "type": "richText", "$bind": { "from": "entry.content.body", "fallback": null, "transforms": null }, "props": null, "children": null, "$repeat": null }
2593
+ ],
2594
+ "props": null,
2595
+ "$bind": null
2596
+ }
2597
+ \`\`\`
2598
+
2599
+ Available entry fields:
2600
+ ${contentTypeApproach.approach === "newContentType" ? contentTypeApproach.contentType.fields.map((f) => `- entry.content.${f.id} (${f.type})`).join("\n") : "(see content type schema)"}
2601
+
2602
+ Also available on each entry:
2603
+ - entry.title - Entry title
2604
+ - entry.slug - URL slug (if routable)
2605
+ - entry.identifier - Unique identifier
2606
+
2607
+ CRITICAL: The $repeat node MUST be a container (grid/stack/div), and its children bind to entry.content.* fields.`;
2608
+ return `You are an expert at creating layout structures for a CMS block.
2609
+
2610
+ ## YOUR TASK
2611
+
2612
+ Create a layout tree that DISPLAYS the following fields. Each field MUST appear in the layout.
2613
+ DO NOT add any styling - no className props, no custom CSS. Just structure and $bind.
2614
+
2615
+ ## FIELDS TO DISPLAY (${fields.length} total)
2616
+
2617
+ ${fieldList}
2618
+
2619
+ ## CRITICAL RULE: Every field must be bound into the layout
2620
+
2621
+ The layout MUST contain one binding per field. Most fields bind via a node's $bind.
2622
+ Link fields ALSO need a bound href prop so they render as clickable links.
2623
+
2624
+ | Field Type | Node Type | Binding Pattern |
2625
+ |------------|-----------|-----------------|
2626
+ | text | "text" | node.$bind.from: "content.{fieldId}" |
2627
+ | richText | "richText" | node.$bind.from: "content.{fieldId}" |
2628
+ | media | "media" | node.$bind.from: "content.{fieldId}" |
2629
+ | link | "button" or "link" | node.$bind.from: "content.{fieldId}" with transforms: [{id:"links.label"}] AND props.href: { "$bind": { from: "content.{fieldId}", transforms: [{id:"links.resolve"}] } } |
2630
+ | repeater | container + children | Use $repeat on a container, with child nodes that bind to item.{itemFieldId} |${hasRepeaterFields ? `
2631
+
2632
+ ## REPEATER FIELD BINDING
2633
+
2634
+ For repeater fields, use $repeat to iterate over items. The container gets $repeat, and children bind to item fields:
2635
+
2636
+ \`\`\`json
2637
+ {
2638
+ "type": "inline",
2639
+ "props": null,
2640
+ "$repeat": { "collection": { "from": "content.socialLinks" }, "itemName": "item" },
2641
+ "$bind": null,
2642
+ "$when": null,
2643
+ "children": [
2644
+ {
2645
+ "type": "link",
2646
+ "props": { "href": { "$bind": { "from": "item.url", "fallback": null, "transforms": [{ "id": "links.resolve" }] } } },
2647
+ "$bind": { "from": "item.platform", "fallback": null, "transforms": null },
2648
+ "$repeat": null,
2649
+ "$when": null,
2650
+ "children": null
2651
+ }
2652
+ ]
2653
+ }
2654
+ \`\`\`
2655
+
2656
+ CRITICAL for repeaters:
2657
+ 1. The $repeat node MUST be a container (inline, div, grid, stack)
2658
+ 2. The $repeat node MUST have children: [...] (NOT children: null)
2659
+ 3. Child nodes bind to item.{fieldId} (not content.{fieldId})
2660
+
2661
+ ## IMPORTANT: Link fields already display their label
2662
+
2663
+ When binding a link field to a link/button node with the \`links.label\` transform,
2664
+ DO NOT create a separate text node to display the label. The link node already
2665
+ displays the label via the transform.
2666
+
2667
+ ❌ WRONG (redundant - displays label twice):
2668
+ \`\`\`json
2669
+ {
2670
+ "type": "inline",
2671
+ "$repeat": { "collection": { "from": "content.navLinks" }, "itemName": "item" },
2672
+ "children": [
2673
+ { "type": "link", "$bind": { "from": "item.url", "transforms": [{"id": "links.label"}] }, "props": {"href": {...}} },
2674
+ { "type": "text", "$bind": { "from": "item.label" } } // ← WRONG! Redundant
2675
+ ]
2676
+ }
2677
+ \`\`\`
2678
+
2679
+ ✅ CORRECT (link node displays label via transform):
2680
+ \`\`\`json
2681
+ {
2682
+ "type": "inline",
2683
+ "$repeat": { "collection": { "from": "content.navLinks" }, "itemName": "item" },
2684
+ "children": [
2685
+ { "type": "link", "$bind": { "from": "item.url", "transforms": [{"id": "links.label"}] }, "props": {"href": {...}} }
2686
+ ]
2687
+ }
2688
+ \`\`\`` : ""}
2689
+
2690
+ ## ❌ WRONG - This layout is BROKEN (no $bind = nothing displays):
2691
+
2692
+ \`\`\`json
2693
+ {
2694
+ "type": "section",
2695
+ "children": [
2696
+ { "type": "div" },
2697
+ { "type": "div" }
2698
+ ]
2699
+ }
2700
+ \`\`\`
2701
+
2702
+ ## ✅ CORRECT - Every field has a node with $bind:
2703
+
2704
+ Given fields: headline (text), description (text), ctaButton (link)
2705
+
2706
+ \`\`\`json
2707
+ {
2708
+ "type": "section",
2709
+ "props": null,
2710
+ "children": [
2711
+ {
2712
+ "type": "stack",
2713
+ "props": { "gap": "lg" },
2714
+ "children": [
2715
+ {
2716
+ "type": "text",
2717
+ "props": { "as": "h1" },
2718
+ "children": null,
2719
+ "$bind": { "from": "content.headline", "fallback": null, "transforms": null },
2720
+ "$repeat": null
2721
+ },
2722
+ {
2723
+ "type": "text",
2724
+ "props": { "as": "p" },
2725
+ "children": null,
2726
+ "$bind": { "from": "content.description", "fallback": null, "transforms": null },
2727
+ "$repeat": null
2728
+ },
2729
+ {
2730
+ "type": "button",
2731
+ "props": {
2732
+ "variant": "primary",
2733
+ "href": { "$bind": { "from": "content.ctaButton", "fallback": null, "transforms": [{ "id": "links.resolve" }] } }
2734
+ },
2735
+ "children": null,
2736
+ "$bind": { "from": "content.ctaButton", "fallback": null, "transforms": [{ "id": "links.label" }] },
2737
+ "$repeat": null
2738
+ }
2739
+ ],
2740
+ "$bind": null,
2741
+ "$repeat": null
2742
+ }
2743
+ ],
2744
+ "$bind": null,
2745
+ "$repeat": null
2746
+ }
2747
+ \`\`\`
2748
+
2749
+ ## Node Types
2750
+
2751
+ - **Container nodes** (organize children, $bind: null): section, stack, inline, grid, div
2752
+ - **Content nodes** (display data, MUST have $bind): text, richText, media, button, link
2753
+
2754
+ Valid types: ${ALLOWED_NODE_TYPES.slice(0, 15).join(", ")}...
2755
+
2756
+ ## LAYOUT DEPTH LIMIT
2757
+
2758
+ The layout can nest UP TO 6 levels deep. Count from root:
2759
+
2760
+ ✅ VALID (within limit):
2761
+ section (root)
2762
+ └─ stack (depth 1)
2763
+ └─ grid (depth 2)
2764
+ └─ div (depth 3)
2765
+ └─ inline (depth 4)
2766
+ └─ text (depth 5) ← OK at depth 5
2767
+
2768
+ ❌ INVALID (exceeds limit - will be truncated):
2769
+ section (root)
2770
+ └─ stack → div → grid → stack → inline → div → text ← TOO DEEP!
2771
+
2772
+ **If you need deeply nested content**:
2773
+ - Use $repeat instead of manual nesting for lists
2774
+ - Flatten wrapper divs that don't add semantic value
2775
+ - Combine related nodes (e.g., inline + text can often just be text)
2776
+
2777
+ In your rationale, note the depth of complex paths:
2778
+ - "headline → text at depth 2 (section > stack > text)"
2779
+ - "links → repeater at depth 3, items at depth 4"
2780
+
2781
+ ## IMPORTANT: Do NOT output raw HTML tag node types
2782
+
2783
+ Do not invent node types like "main", "nav", "header", "footer", etc.
2784
+ Use the allowed node types only (e.g. "section", "div", "headerSection", "footerSection").
2785
+
2786
+ ## Output Requirements
2787
+
2788
+ In your rationale, you MUST list each field and confirm how it is bound:
2789
+ - headline → text node at children[0].children[0]
2790
+ - description → text node at children[0].children[1]
2791
+ - ctaButton → button node at children[0].children[2] (label via links.label, href via props.href + links.resolve)
2792
+
2793
+ If a field has no corresponding $bind node, YOUR OUTPUT IS INVALID.
2794
+ ${contentTypeSection}`;
2795
+ }
2796
+ async function runPass1(html, css, availableContentTypes, ctx) {
2797
+ ctx.onProgress?.("Pass 1: Extracting fields from HTML/JSX...");
2798
+ let systemPrompt = PASS1_SYSTEM_PROMPT;
2799
+ if (availableContentTypes && availableContentTypes.length > 0) systemPrompt = PASS1_SYSTEM_PROMPT + buildContentTypePromptSection(availableContentTypes);
2800
+ const userMessage = `Analyze this HTML/JSX markup and extract all editable content fields:
2801
+
2802
+ \`\`\`html
2803
+ ${html}
2804
+ \`\`\`
2805
+ ${css ? `\nCSS:\n\`\`\`css\n${css}\n\`\`\`` : ""}
2806
+ `;
2807
+ await ctx.debug.writeText("pass1-system-prompt.txt", systemPrompt);
2808
+ await ctx.debug.writeText("pass1-user-message.txt", userMessage);
2809
+ const attempts = [];
2810
+ let lastErrors;
2811
+ for (let attempt = 1; attempt <= ctx.maxRetries; attempt++) {
2812
+ ctx.onProgress?.(`Pass 1 attempt ${attempt}/${ctx.maxRetries}`);
2813
+ try {
2814
+ const attemptUserMessage = lastErrors ? `${userMessage}\n\n⚠️ PREVIOUS ATTEMPT FAILED:\n${lastErrors}\n\nFix these errors and output valid JSON.` : userMessage;
2815
+ const result = await getJsonFromAi(ctx.ai, {
2816
+ model: ctx.model,
2817
+ system: systemPrompt,
2818
+ temperature: ctx.temperature
2819
+ }, attemptUserMessage, pass1OutputSchema);
2820
+ await ctx.debug.writeJson(`pass1-attempt${attempt}-output.json`, result);
2821
+ const fieldErrors = validatePass1FieldDefaults(result);
2822
+ if (fieldErrors.length > 0) {
2823
+ lastErrors = [
2824
+ "## PASS 1 FIELD DEFAULT VALIDATION FAILED",
2825
+ "",
2826
+ ...fieldErrors.map((e) => `- ${e}`),
2827
+ "",
2828
+ "### Link field defaultValue rules",
2829
+ "- For type \"link\", defaultValue MUST be an object with a label key:",
2830
+ " - label: non-empty string (visible link text)",
2831
+ "",
2832
+ "Example:",
2833
+ "{ \"id\": \"cta\", \"type\": \"link\", \"defaultValue\": { \"label\": \"Get Started\" } }"
2834
+ ].join("\n");
2835
+ await ctx.debug.writeText(`pass1-attempt${attempt}-errors.txt`, fieldErrors.join("\n"));
2836
+ attempts.push({
2837
+ attempt,
2838
+ validationErrors: fieldErrors,
2839
+ rawOutput: result
2840
+ });
2841
+ continue;
2842
+ }
2843
+ const transformedResult = transformLinkDefaults(result);
2844
+ await ctx.debug.writeJson("pass1-output.json", transformedResult);
2845
+ ctx.onProgress?.(`Pass 1 complete: Found ${transformedResult.fields.length} fields`);
2846
+ return {
2847
+ success: true,
2848
+ data: transformedResult
2849
+ };
2850
+ } catch (error) {
2851
+ const message = error instanceof Error ? error.message : "Unknown error";
2852
+ attempts.push({
2853
+ attempt,
2854
+ validationErrors: [message]
2855
+ });
2856
+ lastErrors = message;
2857
+ }
2858
+ }
2859
+ return {
2860
+ success: false,
2861
+ error: lastErrors ?? "Pass 1 (field extraction) failed: exhausted retries without a valid result.",
2862
+ attempts
2863
+ };
2864
+ }
2865
+ function validatePass1FieldDefaults(result) {
2866
+ const errors = [];
2867
+ for (const field of result.fields) {
2868
+ if (field.type !== "link") continue;
2869
+ const dv = field.defaultValue;
2870
+ if (!field.required && (dv === null || dv === void 0)) continue;
2871
+ if (!dv || typeof dv !== "object" || Array.isArray(dv)) {
2872
+ errors.push(`Field "${field.id}" (link): defaultValue must be an object like { label: "..." } (got ${typeof dv})`);
2873
+ continue;
2874
+ }
2875
+ const record = dv;
2876
+ const label = typeof record.label === "string" ? record.label.trim() : "";
2877
+ if (label.length === 0) errors.push(`Field "${field.id}" (link): defaultValue must include non-empty label (got label="${label}")`);
2878
+ }
2879
+ return errors;
2880
+ }
2881
+ /**
2882
+ * Transform link field defaultValues from `{ label }` to valid LinkValue shape.
2883
+ *
2884
+ * Pass 1 can only extract the visible link text from HTML. The HTML hrefs
2885
+ * (like `#about`, `#menu`) are meaningless anchors that won't work in the CMS.
2886
+ * Link destinations are configured via the CMS link picker, not extracted from HTML.
2887
+ *
2888
+ * This transforms `{ label: "Our Story" }` to `{ kind: "url", href: "#", label: "Our Story" }`
2889
+ * which satisfies validation (href = hasDestination, label = hasLabel) and works at runtime.
2890
+ */
2891
+ function transformLinkDefaults(pass1Result) {
2892
+ return {
2893
+ ...pass1Result,
2894
+ fields: pass1Result.fields.map((field) => {
2895
+ if (field.type !== "link") return field;
2896
+ const dv = field.defaultValue;
2897
+ if (dv && typeof dv === "object" && ("href" in dv || "kind" in dv)) return field;
2898
+ const label = dv && typeof dv === "object" && "label" in dv ? String(dv.label) : "";
2899
+ return {
2900
+ ...field,
2901
+ defaultValue: {
2902
+ kind: "url",
2903
+ href: "#",
2904
+ label
2905
+ }
2906
+ };
2907
+ })
2908
+ };
2909
+ }
2910
+ /**
2911
+ * Transform layout to add missing props.href bindings for link/button nodes.
2912
+ *
2913
+ * The LLM often forgets to add the props.href binding even when the node's $bind
2914
+ * correctly references a link field. This transform automatically adds it.
2915
+ *
2916
+ * Handles both:
2917
+ * - Top-level link fields: content.fieldId
2918
+ * - Repeater item link fields: item.fieldId (or custom itemName)
2919
+ */
2920
+ function transformMissingLinkHrefs(layout, fields) {
2921
+ const topLevelLinkIds = /* @__PURE__ */ new Set();
2922
+ for (const field of fields) {
2923
+ if (field.type === "link") topLevelLinkIds.add(field.id);
2924
+ if (isGroupField(field)) {
2925
+ for (const nested of field.groupFields) if (nested.type === "link") topLevelLinkIds.add(field.id);
2926
+ }
2927
+ }
2928
+ const repeaterLinkFields = /* @__PURE__ */ new Map();
2929
+ for (const field of fields) if (isRepeaterField(field)) {
2930
+ const linkIds = /* @__PURE__ */ new Set();
2931
+ for (const nested of field.itemFields) if (nested.type === "link") linkIds.add(nested.id);
2932
+ if (linkIds.size > 0) repeaterLinkFields.set(field.id, linkIds);
2933
+ }
2934
+ return transformLayoutNode(layout, topLevelLinkIds, repeaterLinkFields, []);
2935
+ }
2936
+ /**
2937
+ * Check if a binding path resolves to a link field.
2938
+ */
2939
+ function isLinkBinding(bindFrom, topLevelLinkIds, repeaterStack) {
2940
+ if (bindFrom.startsWith("content.")) {
2941
+ const fieldId = bindFrom.replace("content.", "").split(".")[0] ?? "";
2942
+ return topLevelLinkIds.has(fieldId);
2943
+ }
2944
+ for (let i = repeaterStack.length - 1; i >= 0; i--) {
2945
+ const ctx = repeaterStack[i];
2946
+ if (!ctx) continue;
2947
+ const prefix = `${ctx.itemName}.`;
2948
+ if (bindFrom.startsWith(prefix)) {
2949
+ const nestedFieldId = bindFrom.slice(prefix.length).split(".")[0] ?? "";
2950
+ return ctx.linkFieldIds.has(nestedFieldId);
2951
+ }
2952
+ }
2953
+ return false;
2954
+ }
2955
+ function transformLayoutNode(node, topLevelLinkIds, repeaterLinkFields, repeaterStack) {
2956
+ let nextRepeaterStack = repeaterStack;
2957
+ const repeat = node.$repeat;
2958
+ if (repeat?.collection?.from && repeat?.itemName) {
2959
+ const collectionFrom = repeat.collection.from;
2960
+ if (collectionFrom.startsWith("content.")) {
2961
+ const repeaterFieldId = collectionFrom.replace("content.", "").split(".")[0] ?? "";
2962
+ const nestedLinkIds = repeaterLinkFields.get(repeaterFieldId);
2963
+ if (nestedLinkIds && nestedLinkIds.size > 0) nextRepeaterStack = [...repeaterStack, {
2964
+ itemName: repeat.itemName,
2965
+ linkFieldIds: nestedLinkIds
2966
+ }];
2967
+ }
2968
+ }
2969
+ const nodeType = node.type;
2970
+ if (nodeType === "link" || nodeType === "button") {
2971
+ const bind = node.$bind;
2972
+ const bindFrom = typeof bind?.from === "string" ? bind.from : null;
2973
+ if (bindFrom && isLinkBinding(bindFrom, topLevelLinkIds, nextRepeaterStack)) {
2974
+ const props = node.props ?? {};
2975
+ const hrefProp = props.href;
2976
+ if (!(hrefProp && typeof hrefProp === "object" && "$bind" in hrefProp)) {
2977
+ const newProps = {
2978
+ ...props,
2979
+ href: { $bind: {
2980
+ from: bindFrom,
2981
+ fallback: null,
2982
+ transforms: [{ id: "links.resolve" }]
2983
+ } }
2984
+ };
2985
+ let newBind = bind;
2986
+ const transforms = Array.isArray(bind?.transforms) ? bind.transforms : [];
2987
+ if (!transforms.some((t) => t && typeof t === "object" && t.id === "links.label")) newBind = {
2988
+ ...bind,
2989
+ transforms: [...transforms, { id: "links.label" }]
2990
+ };
2991
+ return {
2992
+ ...node,
2993
+ props: newProps,
2994
+ $bind: newBind,
2995
+ children: node.children ? node.children.map((c) => transformLayoutNode(c, topLevelLinkIds, repeaterLinkFields, nextRepeaterStack)) : null
2996
+ };
2997
+ }
2998
+ }
2999
+ }
3000
+ if (node.children && Array.isArray(node.children)) return {
3001
+ ...node,
3002
+ children: node.children.map((c) => transformLayoutNode(c, topLevelLinkIds, repeaterLinkFields, nextRepeaterStack))
3003
+ };
3004
+ return node;
3005
+ }
3006
+ async function runPass2(pass1Result, html, themeSummary, ctx) {
3007
+ ctx.onProgress?.("Pass 2: Generating layout structure...");
3008
+ const systemPrompt = buildPass2SystemPrompt(pass1Result.fields, pass1Result.contentTypeApproach);
3009
+ const baseUserMessage = `Create a layout for this block that displays all the fields.
3010
+ DO NOT add any className props or styling. Just structure and $bind.
3011
+
3012
+ Original markup (HTML/JSX) for reference:
3013
+ \`\`\`html
3014
+ ${html}
3015
+ \`\`\`
3016
+
3017
+ Theme context:
3018
+ ${themeSummary}
3019
+
3020
+ Remember: EVERY field must have a node with $bind pointing to it.
3021
+ `;
3022
+ await ctx.debug.writeText("pass2-system-prompt.txt", systemPrompt);
3023
+ await ctx.debug.writeText("pass2-user-message.txt", baseUserMessage);
3024
+ const attempts = [];
3025
+ let lastErrors;
3026
+ for (let attempt = 1; attempt <= ctx.maxRetries; attempt++) {
3027
+ ctx.onProgress?.(`Pass 2 attempt ${attempt}/${ctx.maxRetries}`);
3028
+ try {
3029
+ const userMessage = lastErrors ? `${baseUserMessage}\n\n⚠️ PREVIOUS ATTEMPT FAILED:\n${lastErrors}\n\nFix these errors.` : baseUserMessage;
3030
+ const pass2Result = await getJsonFromAi(ctx.ai, {
3031
+ model: ctx.model,
3032
+ system: systemPrompt,
3033
+ temperature: ctx.temperature
3034
+ }, userMessage, pass2OutputSchema);
3035
+ await ctx.debug.writeJson(`pass2-attempt${attempt}-output.json`, pass2Result);
3036
+ await ctx.debug.writeText(`pass2-attempt${attempt}-rationale.txt`, pass2Result.rationale);
3037
+ const transformedLayout = transformMissingLinkHrefs(pass2Result.layout, pass1Result.fields);
3038
+ const maxDepth = getMaxDepth(transformedLayout);
3039
+ const potentialTruncations = detectPotentialTruncation(transformedLayout, 6);
3040
+ await ctx.debug.writeJson(`pass2-attempt${attempt}-depth-analysis.json`, {
3041
+ maxDepth,
3042
+ depthLimit: 6,
3043
+ atLimit: maxDepth >= 6,
3044
+ potentialTruncations
3045
+ });
3046
+ const tempOutput = buildValidationOutput(pass1Result, transformedLayout, null, null);
3047
+ const validationErrors = collectValidationErrors(tempOutput);
3048
+ if (validationErrors.length > 0) {
3049
+ lastErrors = formatValidationFeedback(validationErrors, tempOutput);
3050
+ await ctx.debug.writeText(`pass2-attempt${attempt}-errors.txt`, validationErrors.map((e) => e.message).join("\n\n"));
3051
+ attempts.push({
3052
+ attempt,
3053
+ validationErrors: validationErrors.map((e) => e.message),
3054
+ rawOutput: tempOutput
3055
+ });
3056
+ ctx.onProgress?.(`Pass 2 validation errors: ${validationErrors.length}`);
3057
+ continue;
3058
+ }
3059
+ return {
3060
+ success: true,
3061
+ layout: transformedLayout,
3062
+ attempts
3063
+ };
3064
+ } catch (error) {
3065
+ const message = error instanceof Error ? error.message : "Unknown error";
3066
+ attempts.push({
3067
+ attempt,
3068
+ validationErrors: [message]
3069
+ });
3070
+ if (!message.includes("validation")) return {
3071
+ success: false,
3072
+ error: `Pass 2 (layout generation) failed: ${message}`,
3073
+ attempts
3074
+ };
3075
+ lastErrors = message;
3076
+ }
3077
+ }
3078
+ return {
3079
+ success: false,
3080
+ error: `Pass 2 failed after ${ctx.maxRetries} attempts. Last errors: ${lastErrors || "Unknown"}`,
3081
+ attempts
3082
+ };
3083
+ }
3084
+ async function runPass3a(layout, html, ctx) {
3085
+ ctx.onProgress?.("Pass 3a: Generating className patches...");
3086
+ const nodeInventory = buildNodeInventory(layout);
3087
+ const systemPrompt = buildPass3aSystemPrompt();
3088
+ const baseUserMessage = buildPass3aUserMessage(layout, html, nodeInventory);
3089
+ await ctx.debug.writeText("pass3a-system-prompt.txt", systemPrompt);
3090
+ await ctx.debug.writeText("pass3a-user-message.txt", baseUserMessage);
3091
+ let lastErrors;
3092
+ for (let attempt = 1; attempt <= ctx.maxRetries; attempt++) {
3093
+ ctx.onProgress?.(`Pass 3a attempt ${attempt}/${ctx.maxRetries}`);
3094
+ try {
3095
+ const userMessage = lastErrors ? `${baseUserMessage}\n\n⚠️ PREVIOUS ATTEMPT FAILED:\n${lastErrors}\n\nFix these errors.` : baseUserMessage;
3096
+ const result = await getJsonFromAi(ctx.ai, {
3097
+ model: ctx.model,
3098
+ system: systemPrompt,
3099
+ temperature: ctx.temperature
3100
+ }, userMessage, pass3aOutputSchema);
3101
+ await ctx.debug.writeJson(`pass3a-attempt${attempt}-output.json`, result);
3102
+ await ctx.debug.writeText(`pass3a-attempt${attempt}-rationale.txt`, result.rationale);
3103
+ const patchErrors = validatePass3aOutput(result, layout);
3104
+ if (patchErrors.length > 0) {
3105
+ lastErrors = `## PATCH VALIDATION FAILED\n\n${patchErrors.map((e) => `- ${e}`).join("\n")}\n\nFix these errors and try again.`;
3106
+ await ctx.debug.writeText(`pass3a-attempt${attempt}-errors.txt`, patchErrors.join("\n"));
3107
+ ctx.onProgress?.(`Pass 3a validation errors: ${patchErrors.length}`);
3108
+ continue;
3109
+ }
3110
+ return {
3111
+ success: true,
3112
+ patches: result
3113
+ };
3114
+ } catch (error) {
3115
+ const message = error instanceof Error ? error.message : "Unknown error";
3116
+ lastErrors = message;
3117
+ await ctx.debug.writeText(`pass3a-attempt${attempt}-error.txt`, message);
3118
+ ctx.onProgress?.(`Pass 3a error: ${message}`);
3119
+ }
3120
+ }
3121
+ return { success: false };
3122
+ }
3123
+ async function runPass3b(patchedLayout, html, availableClassNames, themeSummary, ctx) {
3124
+ ctx.onProgress?.("Pass 3b: Generating CSS rules...");
3125
+ const systemPrompt = buildPass3bSystemPrompt();
3126
+ const baseUserMessage = buildPass3bUserMessage(patchedLayout, html, availableClassNames, themeSummary);
3127
+ await ctx.debug.writeText("pass3b-system-prompt.txt", systemPrompt);
3128
+ await ctx.debug.writeText("pass3b-user-message.txt", baseUserMessage);
3129
+ let lastErrors;
3130
+ for (let attempt = 1; attempt <= ctx.maxRetries; attempt++) {
3131
+ ctx.onProgress?.(`Pass 3b attempt ${attempt}/${ctx.maxRetries}`);
3132
+ try {
3133
+ const userMessage = lastErrors ? `${baseUserMessage}\n\n⚠️ PREVIOUS ATTEMPT FAILED:\n${lastErrors}\n\nFix these errors.` : baseUserMessage;
3134
+ const result = await getJsonFromAi(ctx.ai, {
3135
+ model: ctx.model,
3136
+ system: systemPrompt,
3137
+ temperature: ctx.temperature
3138
+ }, userMessage, pass3bOutputSchema);
3139
+ await ctx.debug.writeJson(`pass3b-attempt${attempt}-output.json`, result);
3140
+ await ctx.debug.writeText(`pass3b-attempt${attempt}-rationale.txt`, result.rationale);
3141
+ const cssErrors = validatePass3bOutput(result, new Set(availableClassNames));
3142
+ if (cssErrors.length > 0) {
3143
+ lastErrors = `## CSS VALIDATION FAILED\n\n${cssErrors.map((e) => `- ${e}`).join("\n")}\n\nFix these errors and try again. Remember:\n- Use ONLY the classNames listed in AVAILABLE CLASSNAMES\n- Do NOT include [data-block=...] in selectors`;
3144
+ await ctx.debug.writeText(`pass3b-attempt${attempt}-errors.txt`, cssErrors.join("\n"));
3145
+ ctx.onProgress?.(`Pass 3b validation errors: ${cssErrors.length}`);
3146
+ continue;
3147
+ }
3148
+ return {
3149
+ success: true,
3150
+ rules: result
3151
+ };
3152
+ } catch (error) {
3153
+ const message = error instanceof Error ? error.message : "Unknown error";
3154
+ lastErrors = message;
3155
+ await ctx.debug.writeText(`pass3b-attempt${attempt}-error.txt`, message);
3156
+ ctx.onProgress?.(`Pass 3b error: ${message}`);
3157
+ }
3158
+ }
3159
+ return { success: false };
3160
+ }
3161
+ /** Build validation output structure from pass results */
3162
+ function buildValidationOutput(pass1Result, layout, customCssRules, customCssAtRules) {
3163
+ return {
3164
+ blocks: [{
3165
+ id: pass1Result.blockId,
3166
+ title: pass1Result.blockTitle,
3167
+ titleSource: null,
3168
+ description: pass1Result.blockDescription,
3169
+ category: pass1Result.category,
3170
+ icon: "layout",
3171
+ tags: null,
3172
+ fields: pass1Result.fields.map(mapPass1FieldToOutputField),
3173
+ layout
3174
+ }],
3175
+ themePatch: null,
3176
+ customCssRules,
3177
+ customCssAtRules,
3178
+ fixRationale: null
3179
+ };
3180
+ }
3181
+ /**
3182
+ * Build dataLoaders config for a content type.
3183
+ * Used when the LLM decides to create content entries.
3184
+ * @param _contentTypeKey - The content type key (unused, as loader binds dynamically to content.contentType)
3185
+ */
3186
+ function buildDataLoadersForContentType(_contentTypeKey) {
3187
+ return { entries: {
3188
+ endpoint: "listPublishedEntries",
3189
+ params: {
3190
+ type: { $bind: { from: "content.contentType" } },
3191
+ siteId: { $bind: { from: "$root.siteId" } },
3192
+ stage: { $bind: {
3193
+ from: "$root.previewStage",
3194
+ fallback: "published"
3195
+ } }
3196
+ }
3197
+ } };
3198
+ }
3199
+ /**
3200
+ * Extract entries and content types from the content type approach.
3201
+ */
3202
+ function extractEntriesFromApproach(approach) {
3203
+ if (!approach) return {
3204
+ entries: void 0,
3205
+ contentTypes: void 0,
3206
+ contentTypeKey: void 0
3207
+ };
3208
+ if (approach.approach === "existingContentType") return {
3209
+ entries: approach.entries.map((e) => ({
3210
+ identifier: e.identifier,
3211
+ contentType: approach.contentTypeKey,
3212
+ title: e.title,
3213
+ data: e.data,
3214
+ status: "draft"
3215
+ })),
3216
+ contentTypes: void 0,
3217
+ contentTypeKey: approach.contentTypeKey
3218
+ };
3219
+ if (approach.approach === "newContentType") return {
3220
+ entries: approach.entries.map((e) => ({
3221
+ identifier: e.identifier,
3222
+ contentType: approach.contentType.key,
3223
+ title: e.title,
3224
+ data: e.data,
3225
+ status: "draft"
3226
+ })),
3227
+ contentTypes: [{
3228
+ key: approach.contentType.key,
3229
+ name: approach.contentType.name,
3230
+ description: approach.contentType.description ?? void 0,
3231
+ fields: approach.contentType.fields.map((f) => ({
3232
+ id: f.id,
3233
+ type: f.type,
3234
+ label: f.label,
3235
+ required: f.required ?? void 0,
3236
+ description: f.description ?? void 0
3237
+ })),
3238
+ isRouted: approach.contentType.isRouted
3239
+ }],
3240
+ contentTypeKey: approach.contentType.key
3241
+ };
3242
+ return {
3243
+ entries: void 0,
3244
+ contentTypes: void 0,
3245
+ contentTypeKey: void 0
3246
+ };
3247
+ }
3248
+ /** Build final output structure */
3249
+ function buildFinalOutput(pass1Result, layout, scopedRules) {
3250
+ const normalizedLayout = normalizeLlmLayoutToRuntimeNode(layout);
3251
+ const { entries, contentTypes, contentTypeKey } = extractEntriesFromApproach(pass1Result.contentTypeApproach);
3252
+ const block = {
3253
+ id: pass1Result.blockId,
3254
+ title: pass1Result.blockTitle,
3255
+ description: pass1Result.blockDescription ?? void 0,
3256
+ category: pass1Result.category,
3257
+ icon: "layout",
3258
+ fields: pass1Result.fields.map(mapPass1FieldToSimpleOutput),
3259
+ layout: normalizedLayout
3260
+ };
3261
+ if (contentTypeKey) Object.assign(block, { dataLoaders: buildDataLoadersForContentType(contentTypeKey) });
3262
+ return {
3263
+ blocks: [block],
3264
+ customCssRules: scopedRules?.customCssRules ?? void 0,
3265
+ customCssAtRules: scopedRules?.customCssAtRules ?? void 0,
3266
+ entries,
3267
+ contentTypes
3268
+ };
3269
+ }
3270
+ function normalizeLlmBindingToRuntimeBinding(binding) {
3271
+ const normalized = { from: binding.from };
3272
+ if (binding.fallback !== null && binding.fallback !== void 0) normalized.fallback = binding.fallback;
3273
+ if (Array.isArray(binding.transforms)) normalized.transforms = binding.transforms.map((t) => ({ id: t.id }));
3274
+ return normalized;
3275
+ }
3276
+ /**
3277
+ * Convert the converter's LLM-friendly node shape (nullable keys)
3278
+ * into the runtime NodeDefinition shape used by sdkCustomBlockSchema / PageRenderer.
3279
+ */
3280
+ function normalizeLlmLayoutToRuntimeNode(raw) {
3281
+ const node = raw;
3282
+ const normalized = { type: node.type };
3283
+ if (node.props && Object.keys(node.props).length > 0) normalized.props = node.props;
3284
+ if (Array.isArray(node.children) && node.children.length > 0) normalized.children = node.children.map((child) => normalizeLlmLayoutToRuntimeNode(child));
3285
+ if (node.$bind) normalized.$bind = normalizeLlmBindingToRuntimeBinding(node.$bind);
3286
+ if (node.$repeat) normalized.$repeat = {
3287
+ collection: { from: node.$repeat.collection.from },
3288
+ itemName: node.$repeat.itemName
3289
+ };
3290
+ if (node.$when) normalized.$when = { when: { from: node.$when.condition.from } };
3291
+ return normalized;
3292
+ }
3293
+ async function convertHtmlToManifest(input, options) {
3294
+ const { ai, maxRetries = 3, model = DEFAULT_MANIFEST_MODEL, temperature = .2, onProgress, debugDir } = options;
3295
+ const { html, css, theme, availableContentTypes } = input;
3296
+ const debug = createDebugWriter(debugDir);
3297
+ const ctx = {
3298
+ ai,
3299
+ model,
3300
+ temperature,
3301
+ maxRetries,
3302
+ debug,
3303
+ onProgress
3304
+ };
3305
+ const themeSummary = formatThemeSummaryForPrompt(summarizeThemeForLLM(theme));
3306
+ const pass1 = await runPass1(html, css, availableContentTypes, ctx);
3307
+ if (!pass1.success) return {
3308
+ success: false,
3309
+ error: pass1.error,
3310
+ attempts: pass1.attempts
3311
+ };
3312
+ const pass1Result = pass1.data;
3313
+ const pass2 = await runPass2(pass1Result, html, themeSummary, ctx);
3314
+ if (!pass2.success) return {
3315
+ success: false,
3316
+ error: pass2.error,
3317
+ attempts: pass2.attempts
3318
+ };
3319
+ const pass2Layout = pass2.layout;
3320
+ const pass3a = await runPass3a(pass2Layout, html, ctx);
3321
+ if (!pass3a.success || pass3a.patches.classNamePatches.length === 0) {
3322
+ onProgress?.("Pass 3a failed or produced no patches, returning unstyled layout");
3323
+ await debug.writeText("pass3-fallback.txt", "Pass 3a (className patches) failed or produced no patches. Returning unstyled layout from Pass 2.");
3324
+ const finalOutput = buildFinalOutput(pass1Result, pass2Layout, null);
3325
+ await debug.writeJson("final-output.json", finalOutput);
3326
+ return {
3327
+ success: true,
3328
+ output: finalOutput,
3329
+ attempts: pass2.attempts.length + 1
3330
+ };
3331
+ }
3332
+ onProgress?.("Applying className patches to layout...");
3333
+ let patchedLayout;
3334
+ try {
3335
+ patchedLayout = applyClassNamePatches(pass2Layout, pass3a.patches.classNamePatches);
3336
+ await debug.writeJson("pass3-patched-layout.json", patchedLayout);
3337
+ } catch (error) {
3338
+ const message = error instanceof Error ? error.message : "Unknown error";
3339
+ onProgress?.(`Failed to apply patches: ${message}`);
3340
+ await debug.writeText("pass3-fallback.txt", `Failed to apply patches: ${message}`);
3341
+ const finalOutput = buildFinalOutput(pass1Result, pass2Layout, null);
3342
+ await debug.writeJson("final-output.json", finalOutput);
3343
+ return {
3344
+ success: true,
3345
+ output: finalOutput,
3346
+ attempts: pass2.attempts.length + 1
3347
+ };
3348
+ }
3349
+ const availableClassNames = Array.from(collectLayoutClassNames(patchedLayout));
3350
+ const pass3b = await runPass3b(patchedLayout, html, availableClassNames, themeSummary, ctx);
3351
+ let scopedRules = null;
3352
+ if (pass3b.success) {
3353
+ scopedRules = scopeAndNormalizeRules(pass1Result.blockId, pass3b.rules);
3354
+ await debug.writeJson("pass3b-scoped-rules.json", scopedRules);
3355
+ } else {
3356
+ onProgress?.("Pass 3b failed, returning layout with classNames but no CSS rules");
3357
+ await debug.writeText("pass3b-fallback.txt", "Pass 3b (CSS rules) failed. Returning layout with classNames but no CSS rules.");
3358
+ }
3359
+ onProgress?.("Running final validation...");
3360
+ const finalValidationErrors = collectValidationErrors(buildValidationOutput(pass1Result, patchedLayout, scopedRules?.customCssRules ?? null, scopedRules?.customCssAtRules ?? null));
3361
+ if (finalValidationErrors.length > 0) {
3362
+ await debug.writeText("final-validation-warnings.txt", finalValidationErrors.map((e) => e.message).join("\n\n"));
3363
+ onProgress?.(`Final validation warnings: ${finalValidationErrors.length}`);
3364
+ }
3365
+ const finalOutput = buildFinalOutput(pass1Result, patchedLayout, scopedRules);
3366
+ await debug.writeJson("final-output.json", finalOutput);
3367
+ onProgress?.("Conversion complete!");
3368
+ return {
3369
+ success: true,
3370
+ output: finalOutput,
3371
+ attempts: pass2.attempts.length + 1
3372
+ };
3373
+ }
3374
+ /**
3375
+ * Validate HTML-to-manifest output structure.
3376
+ *
3377
+ * Can be used to validate output loaded from a file or
3378
+ * generated by another tool.
3379
+ *
3380
+ * @example
3381
+ * ```typescript
3382
+ * const { valid, errors } = validateManifestOutput(jsonData);
3383
+ * if (!valid) {
3384
+ * console.error('Invalid manifest:', errors);
3385
+ * }
3386
+ * ```
3387
+ */
3388
+ function validateManifestOutput(data) {
3389
+ const parseResult = htmlToManifestOutputSchema.safeParse(data);
3390
+ if (!parseResult.success) return {
3391
+ valid: false,
3392
+ errors: parseResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`)
3393
+ };
3394
+ const validationErrors = collectValidationErrors(parseResult.data);
3395
+ if (validationErrors.length > 0) return {
3396
+ valid: false,
3397
+ errors: validationErrors.map((e) => e.message)
3398
+ };
3399
+ return {
3400
+ valid: true,
3401
+ output: parseResult.data
3402
+ };
3403
+ }
3404
+ //#endregion
3405
+ //#region src/manifest/prompt/knowledge-base.ts
3406
+ /**
3407
+ * LLM Knowledge Base for HTML-to-Manifest Conversion
3408
+ *
3409
+ * Contains reference material for the LLM including:
3410
+ * - Allowed node types and their usage
3411
+ * - Transform functions and their purposes
3412
+ * - Field type inference rules
3413
+ * - Styling decision tree
3414
+ * - Block boundary heuristics
3415
+ */
3416
+ /**
3417
+ * Documentation for each allowed node type.
3418
+ */
3419
+ const NODE_TYPE_DOCS = {
3420
+ div: "Generic container for grouping elements",
3421
+ figure: "Container for media with optional caption",
3422
+ section: "Full-width section with optional background and padding",
3423
+ headerSection: "Section styled as a site header",
3424
+ stack: "Vertical flex container (gap between children)",
3425
+ inline: "Horizontal flex container (items in a row)",
3426
+ grid: "CSS grid container",
3427
+ text: "Text element (headings, paragraphs, spans). Use props.as for semantic tag (h1-h6, p, span)",
3428
+ richText: "Rich text content (paragraphs with formatting)",
3429
+ media: "Image or video element. Binds to media field type",
3430
+ videoEmbed: "Embedded video (YouTube, Vimeo)",
3431
+ button: "Button element with variant styling (label comes from node $bind; href comes from props.href)",
3432
+ link: "Link element (label from node $bind; href from props.href)",
3433
+ carousel: "Image/content carousel",
3434
+ accordion: "Accordion container",
3435
+ accordionItem: "Single accordion panel",
3436
+ form: "Contact/submission form",
3437
+ "booking-form": "Booking/appointment form",
3438
+ "event-registration": "Event registration form",
3439
+ "event-spotlight": "Featured event display",
3440
+ "event-listing": "Event listing grid",
3441
+ "event-calendar": "Calendar view of events",
3442
+ "event-combined": "Combined event display",
3443
+ shop: "E-commerce shop component"
3444
+ };
3445
+ /**
3446
+ * Generate node types documentation string for the system prompt.
3447
+ *
3448
+ * Lists all allowed node types grouped by category with descriptions.
3449
+ *
3450
+ * @returns Formatted markdown documentation for node types
3451
+ */
3452
+ function getNodeTypesReference() {
3453
+ return [
3454
+ "## Allowed Node Types",
3455
+ "",
3456
+ "Use these node types in the layout tree. Each node has a `type` property.",
3457
+ "",
3458
+ "### Layout Containers",
3459
+ ...filterNodeDocs([
3460
+ "div",
3461
+ "figure",
3462
+ "section",
3463
+ "headerSection",
3464
+ "stack",
3465
+ "inline",
3466
+ "grid"
3467
+ ]),
3468
+ "",
3469
+ "### Content Nodes",
3470
+ ...filterNodeDocs([
3471
+ "text",
3472
+ "richText",
3473
+ "media",
3474
+ "videoEmbed"
3475
+ ]),
3476
+ "",
3477
+ "### Interactive Nodes",
3478
+ ...filterNodeDocs([
3479
+ "button",
3480
+ "link",
3481
+ "carousel",
3482
+ "accordion",
3483
+ "accordionItem"
3484
+ ]),
3485
+ "",
3486
+ "Full list of allowed types: " + ALLOWED_NODE_TYPES.join(", ")
3487
+ ].join("\n");
3488
+ }
3489
+ function filterNodeDocs(types) {
3490
+ return types.filter((t) => NODE_TYPE_DOCS[t]).map((t) => `- **${t}**: ${NODE_TYPE_DOCS[t] ?? ""}`);
3491
+ }
3492
+ /**
3493
+ * Documentation for commonly used transforms.
3494
+ */
3495
+ const TRANSFORM_DOCS = {
3496
+ "string.uppercase": "Convert string to uppercase",
3497
+ "value.fallback": "Provide fallback value if binding is empty. Options: { fallback: \"default\" }",
3498
+ "date.formatShort": "Format date as short string",
3499
+ "array.length": "Get length of array",
3500
+ "value.jsonArray": "Parse JSON array string",
3501
+ "layout.maxWidthClass": "Get max-width CSS class based on layout setting",
3502
+ "background.resolveStyle": "Resolve background field to CSS styles",
3503
+ "background.resolveClass": "Resolve background field to CSS classes",
3504
+ "links.resolve": "Resolve link objects (LinkValue or {href}) to href strings using the route map",
3505
+ "links.label": "Extract a human-readable label from link objects (label/title/identifier/href)",
3506
+ "media.fromUrl": "Create media object from URL string",
3507
+ "boxStyles.resolveRootClass": "Get root element class from box styles",
3508
+ "boxStyles.resolveRootStyle": "Get root element style from box styles"
3509
+ };
3510
+ /**
3511
+ * Generate transforms documentation string for the system prompt.
3512
+ *
3513
+ * Lists allowed transform IDs with descriptions and example usage.
3514
+ *
3515
+ * @returns Formatted markdown documentation for transforms
3516
+ */
3517
+ function getTransformsReference() {
3518
+ return [
3519
+ "## Available Transforms",
3520
+ "",
3521
+ "Use transforms in $bind to modify values. Example:",
3522
+ "```json",
3523
+ "{",
3524
+ " \"$bind\": {",
3525
+ " \"from\": \"content.title\",",
3526
+ " \"transforms\": [{ \"id\": \"string.uppercase\" }]",
3527
+ " }",
3528
+ "}",
3529
+ "```",
3530
+ "",
3531
+ "### Common Transforms",
3532
+ ...Object.entries(TRANSFORM_DOCS).map(([id, desc]) => `- **${id}**: ${desc}`),
3533
+ "",
3534
+ "Full list of allowed transforms: " + ALLOWED_TRANSFORM_IDS.slice(0, 15).join(", ") + "..."
3535
+ ].join("\n");
3536
+ }
3537
+ const FIELD_TYPE_INFERENCE = `
3538
+ ## Field Type Inference Rules
3539
+
3540
+ When extracting editable content from HTML, choose field types as follows:
3541
+
3542
+ 1. **text** (single-line string):
3543
+ - Short headings, labels, button text (<100 chars, no block elements)
3544
+ - Example: <h1>Welcome</h1> → field type: text
3545
+
3546
+ 2. **richText** (formatted content):
3547
+ - Paragraphs, descriptions, content with <p>, <ul>, <em>, <strong>
3548
+ - Example: <p>Our <strong>mission</strong> is...</p> → field type: richText
3549
+
3550
+ 3. **media** (images/videos):
3551
+ - <img>, <video>, <picture> elements with src attributes
3552
+ - Example: <img src="hero.jpg"> → field type: media
3553
+
3554
+ 4. **link** (navigation/buttons):
3555
+ - <a> elements with href, or buttons that navigate
3556
+ - defaultValue should be an object containing both href and label
3557
+ - Example: <a href="/contact">Contact</a> → field type: link, defaultValue: { href: "/contact", label: "Contact" }
3558
+
3559
+ 5. **repeater** (repeated content):
3560
+ - 3+ similar sibling elements (cards, testimonials, features)
3561
+ - Example: Multiple <div class="card">...</div> → repeater with schema
3562
+
3563
+ 6. **group** (related fields):
3564
+ - Logically related fields (e.g., author name + avatar)
3565
+ - Use to organize complex content
3566
+
3567
+ 7. **boolean** (toggles):
3568
+ - Show/hide states (isVisible, showButton)
3569
+
3570
+ 8. **select** (predefined options):
3571
+ - When content appears to be one of N options
3572
+ - Define options array with value/label
3573
+ `;
3574
+ const STYLING_DECISION_TREE = `
3575
+ ## Styling Decision Tree
3576
+
3577
+ When converting styles from HTML/Tailwind to the manifest, follow this priority:
3578
+
3579
+ 1. **Theme Token Match** (highest priority):
3580
+ Does a theme token exist for this exact style?
3581
+ → YES: Use token reference in props (e.g., background: 'primary')
3582
+
3583
+ Common token mappings:
3584
+ - bg-blue-600 → background: 'primary' (if primary is blue)
3585
+ - text-gray-900 → color: 'foreground'
3586
+ - rounded-lg → uses theme.corners setting
3587
+
3588
+ 2. **Close Token Match**:
3589
+ Is the style close to an existing token?
3590
+ → YES: Use the token (minor variations are acceptable)
3591
+
3592
+ 3. **Site-wide Addition** (themePatch):
3593
+ Is this a one-off value that should apply site-wide?
3594
+ → YES: Add to themePatch array
3595
+
3596
+ Example: New accent color for the whole site
3597
+ { "op": "add", "path": "/palette/colors/-", "value": { "name": "coral", "hex": "#ff6b6b" } }
3598
+
3599
+ 4. **Block-specific CSS** (customCssRules):
3600
+ Is this specific to this block only?
3601
+ → YES: Add to customCssRules
3602
+
3603
+ Example: Custom animation for hero section
3604
+ 1) Add a className to a node you want to target:
3605
+ { "type": "text", "props": { "className": "hero-title" }, "children": null, "$bind": { "from": "content.title", "fallback": null, "transforms": null }, "$repeat": null, "$when": null }
3606
+
3607
+ 2) Scope CSS to the block and target that class:
3608
+ {
3609
+ "id": "hero-pulse",
3610
+ "selector": "[data-block=\"custom.hero\"] .hero-title",
3611
+ "declarations": [{ "property": "animation", "value": "pulse 2s infinite" }]
3612
+ }
3613
+
3614
+ 5. **Field Parameterization**:
3615
+ Can the style be controlled by the user?
3616
+ → YES: Add a field and use $bind with transform
3617
+
3618
+ 6. **Inline Styles** (last resort):
3619
+ None of the above?
3620
+ → Use inline styles in node props
3621
+
3622
+ Example: props: { style: { marginTop: '2rem' } }
3623
+ `;
3624
+ const BLOCK_BOUNDARY_HEURISTICS = `
3625
+ ## Block Boundary Heuristics
3626
+
3627
+ When converting a page with multiple sections, decide whether to create one block or multiple:
3628
+
3629
+ ### SPLIT into separate blocks when:
3630
+ - **Visual separation**: Full-width background color changes
3631
+ - **Semantic sections**: <section>, <header>, <footer> tags
3632
+ - **Reusable patterns**: Testimonials, pricing cards, feature grids
3633
+ - **Different purposes**: Hero vs features vs CTA
3634
+ - **Major spacing**: py-16+ or 4rem+ vertical padding between sections
3635
+
3636
+ ### KEEP as single block when:
3637
+ - **Tightly coupled**: Heading with its immediate description
3638
+ - **Small content**: <5 editable elements total
3639
+ - **No clear boundaries**: Content flows together naturally
3640
+ - **Context dependency**: Splitting would lose important context
3641
+
3642
+ ### Block ID Naming:
3643
+ - Use descriptive names: custom.hero, custom.features-grid, custom.testimonials
3644
+ - Keep lowercase with hyphens: custom.pricing-cards (not custom.pricingCards)
3645
+ `;
3646
+ const BINDING_SYNTAX = `
3647
+ ## Binding Syntax
3648
+
3649
+ **CRITICAL: $bind connects layout nodes to field data. Without it, nodes display nothing.**
3650
+
3651
+ Connect layout nodes to field content using $bind:
3652
+
3653
+ ### Basic Binding
3654
+ \`\`\`json
3655
+ {
3656
+ "type": "text",
3657
+ "props": { "as": "h1" },
3658
+ "$bind": { "from": "content.headline" }
3659
+ }
3660
+ \`\`\`
3661
+
3662
+ ### Binding with Fallback
3663
+ \`\`\`json
3664
+ {
3665
+ "$bind": {
3666
+ "from": "content.subtitle",
3667
+ "fallback": "Default subtitle text"
3668
+ }
3669
+ }
3670
+ \`\`\`
3671
+
3672
+ ### Binding with Transform
3673
+ \`\`\`json
3674
+ {
3675
+ "$bind": {
3676
+ "from": "content.title",
3677
+ "transforms": [{ "id": "string.uppercase" }]
3678
+ }
3679
+ }
3680
+ \`\`\`
3681
+
3682
+ ### Binding in Props
3683
+ \`\`\`json
3684
+ {
3685
+ "type": "text",
3686
+ "props": {
3687
+ "as": "h1",
3688
+ "className": {
3689
+ "$bind": {
3690
+ "from": "content.alignment",
3691
+ "fallback": null,
3692
+ "transforms": [{ "id": "ui.headingClassFromAlignment" }]
3693
+ }
3694
+ }
3695
+ },
3696
+ "children": null,
3697
+ "$bind": { "from": "content.headline", "fallback": null, "transforms": null },
3698
+ "$repeat": null,
3699
+ "$when": null
3700
+ }
3701
+ \`\`\`
3702
+
3703
+ ### Repeating Content
3704
+ \`\`\`json
3705
+ {
3706
+ "type": "grid",
3707
+ "props": null,
3708
+ "$repeat": {
3709
+ "collection": { "from": "content.features" },
3710
+ "itemName": "feature"
3711
+ },
3712
+ "children": [
3713
+ {
3714
+ "type": "text",
3715
+ "props": null,
3716
+ "children": null,
3717
+ "$bind": { "from": "feature.title", "fallback": null, "transforms": null },
3718
+ "$repeat": null,
3719
+ "$when": null
3720
+ }
3721
+ ],
3722
+ "$bind": null,
3723
+ "$when": null
3724
+ }
3725
+ \`\`\`
3726
+ `;
3727
+ /**
3728
+ * Get the complete knowledge base for the system prompt.
3729
+ *
3730
+ * Combines all reference documentation including node types, transforms,
3731
+ * field inference rules, styling decision tree, and binding syntax.
3732
+ *
3733
+ * @returns Complete knowledge base as a formatted markdown string
3734
+ */
3735
+ function getKnowledgeBase() {
3736
+ return [
3737
+ getNodeTypesReference(),
3738
+ "",
3739
+ getTransformsReference(),
3740
+ "",
3741
+ FIELD_TYPE_INFERENCE,
3742
+ "",
3743
+ STYLING_DECISION_TREE,
3744
+ "",
3745
+ BLOCK_BOUNDARY_HEURISTICS,
3746
+ "",
3747
+ BINDING_SYNTAX
3748
+ ].join("\n");
3749
+ }
3750
+ //#endregion
3751
+ //#region src/manifest/prompt/examples/hero.ts
3752
+ const heroExample = {
3753
+ name: "Hero Section",
3754
+ description: "A classic hero section with centered text and call-to-action button.",
3755
+ html: `<section class="bg-blue-900 py-24 text-center text-white">
3756
+ <div class="container mx-auto px-4">
3757
+ <h1 class="text-5xl font-bold mb-4">Welcome to Our Platform</h1>
3758
+ <p class="text-xl text-blue-100 mb-8 max-w-2xl mx-auto">
3759
+ Build amazing products with our powerful tools and dedicated support.
3760
+ </p>
3761
+ <a href="/get-started" class="inline-block bg-white text-blue-900 px-8 py-3 rounded-lg font-semibold hover:bg-blue-50">
3762
+ Get Started
3763
+ </a>
3764
+ </div>
3765
+ </section>`,
3766
+ output: { blocks: [{
3767
+ id: "custom.hero",
3768
+ title: "Hero Section",
3769
+ description: "Centered hero with headline, description, and CTA",
3770
+ category: "marketing",
3771
+ icon: "layout",
3772
+ tags: [
3773
+ "hero",
3774
+ "landing",
3775
+ "cta"
3776
+ ],
3777
+ fields: [
3778
+ {
3779
+ id: "headline",
3780
+ type: "text",
3781
+ label: "Headline",
3782
+ required: true,
3783
+ multiline: false,
3784
+ defaultValue: "Welcome to Our Platform"
3785
+ },
3786
+ {
3787
+ id: "description",
3788
+ type: "text",
3789
+ label: "Description",
3790
+ required: false,
3791
+ multiline: true,
3792
+ defaultValue: "Build amazing products with our powerful tools and dedicated support."
3793
+ },
3794
+ {
3795
+ id: "cta",
3796
+ type: "link",
3797
+ label: "Call to Action",
3798
+ required: false
3799
+ }
3800
+ ],
3801
+ layout: {
3802
+ type: "section",
3803
+ props: {
3804
+ background: "primary",
3805
+ textColor: "white",
3806
+ className: "text-center py-24"
3807
+ },
3808
+ children: [{
3809
+ type: "stack",
3810
+ props: {
3811
+ align: "center",
3812
+ gap: "lg"
3813
+ },
3814
+ children: [
3815
+ {
3816
+ type: "text",
3817
+ props: {
3818
+ as: "h1",
3819
+ className: "text-5xl font-bold"
3820
+ },
3821
+ children: null,
3822
+ $bind: {
3823
+ from: "content.headline",
3824
+ fallback: null,
3825
+ transforms: null
3826
+ },
3827
+ $repeat: null,
3828
+ $when: null
3829
+ },
3830
+ {
3831
+ type: "text",
3832
+ props: {
3833
+ as: "p",
3834
+ className: "text-xl opacity-80 max-w-2xl"
3835
+ },
3836
+ children: null,
3837
+ $bind: {
3838
+ from: "content.description",
3839
+ fallback: null,
3840
+ transforms: null
3841
+ },
3842
+ $repeat: null,
3843
+ $when: null
3844
+ },
3845
+ {
3846
+ type: "button",
3847
+ props: {
3848
+ variant: "secondary",
3849
+ size: "lg",
3850
+ href: { $bind: {
3851
+ from: "content.cta",
3852
+ fallback: null,
3853
+ transforms: [{ id: "links.resolve" }]
3854
+ } }
3855
+ },
3856
+ children: null,
3857
+ $bind: {
3858
+ from: "content.cta",
3859
+ fallback: null,
3860
+ transforms: [{ id: "links.label" }]
3861
+ },
3862
+ $repeat: null,
3863
+ $when: null
3864
+ }
3865
+ ],
3866
+ $bind: null,
3867
+ $repeat: null,
3868
+ $when: null
3869
+ }],
3870
+ $bind: null,
3871
+ $repeat: null,
3872
+ $when: null
3873
+ }
3874
+ }] },
3875
+ explanation: `
3876
+ KEY POINTS:
3877
+ - EVERY text node has $bind connecting it to a field (headline, description)
3878
+ - CTA button binds label via links.label and href via props.href + links.resolve (cta)
3879
+ - Container nodes (section, stack) do NOT have $bind
3880
+ - All nullable fields are explicitly included (children: null, $repeat: null, $when: null)
3881
+
3882
+ Styling:
3883
+ - bg-blue-900 mapped to 'primary' background token
3884
+ - text-white mapped to textColor: 'white'
3885
+ - Layout uses section > stack for vertical centering`
3886
+ };
3887
+ //#endregion
3888
+ //#region src/manifest/prompt/examples/features-grid.ts
3889
+ const featuresGridExample = {
3890
+ name: "Features Grid",
3891
+ description: "A grid of feature cards with icons, title, and description.",
3892
+ html: `<section class="py-16 bg-gray-50">
3893
+ <div class="container mx-auto px-4">
3894
+ <h2 class="text-3xl font-bold text-center mb-12">Why Choose Us</h2>
3895
+ <div class="grid md:grid-cols-3 gap-8">
3896
+ <div class="bg-white p-6 rounded-lg shadow-sm">
3897
+ <div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mb-4">
3898
+ <svg class="w-6 h-6 text-blue-600"><!-- icon --></svg>
3899
+ </div>
3900
+ <h3 class="text-xl font-semibold mb-2">Fast Performance</h3>
3901
+ <p class="text-gray-600">Lightning-fast load times and optimized delivery.</p>
3902
+ </div>
3903
+ <div class="bg-white p-6 rounded-lg shadow-sm">
3904
+ <div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mb-4">
3905
+ <svg class="w-6 h-6 text-blue-600"><!-- icon --></svg>
3906
+ </div>
3907
+ <h3 class="text-xl font-semibold mb-2">Secure by Default</h3>
3908
+ <p class="text-gray-600">Enterprise-grade security built into every layer.</p>
3909
+ </div>
3910
+ <div class="bg-white p-6 rounded-lg shadow-sm">
3911
+ <div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mb-4">
3912
+ <svg class="w-6 h-6 text-blue-600"><!-- icon --></svg>
3913
+ </div>
3914
+ <h3 class="text-xl font-semibold mb-2">24/7 Support</h3>
3915
+ <p class="text-gray-600">Our team is always here to help you succeed.</p>
3916
+ </div>
3917
+ </div>
3918
+ </div>
3919
+ </section>`,
3920
+ output: { blocks: [{
3921
+ id: "custom.features-grid",
3922
+ title: "Features Grid",
3923
+ description: "Grid of feature cards with icons",
3924
+ category: "marketing",
3925
+ icon: "grid",
3926
+ tags: [
3927
+ "features",
3928
+ "grid",
3929
+ "cards"
3930
+ ],
3931
+ fields: [{
3932
+ id: "sectionTitle",
3933
+ type: "text",
3934
+ label: "Section Title",
3935
+ required: true,
3936
+ multiline: false,
3937
+ defaultValue: "Why Choose Us"
3938
+ }, {
3939
+ id: "features",
3940
+ type: "repeater",
3941
+ label: "Features",
3942
+ required: true,
3943
+ itemLabel: "Feature",
3944
+ minItems: 1,
3945
+ maxItems: 12,
3946
+ allowConversion: false,
3947
+ schema: { fields: [
3948
+ {
3949
+ id: "icon",
3950
+ type: "text",
3951
+ label: "Icon",
3952
+ required: false,
3953
+ multiline: false,
3954
+ ui: { placeholder: "Lucide icon name (e.g., zap, shield, headphones)" }
3955
+ },
3956
+ {
3957
+ id: "title",
3958
+ type: "text",
3959
+ label: "Title",
3960
+ required: true,
3961
+ multiline: false
3962
+ },
3963
+ {
3964
+ id: "description",
3965
+ type: "text",
3966
+ label: "Description",
3967
+ required: false,
3968
+ multiline: true
3969
+ }
3970
+ ] }
3971
+ }],
3972
+ layout: {
3973
+ type: "section",
3974
+ props: {
3975
+ background: "surface",
3976
+ className: "py-16"
3977
+ },
3978
+ children: [{
3979
+ type: "stack",
3980
+ props: { gap: "xl" },
3981
+ children: [{
3982
+ type: "text",
3983
+ props: {
3984
+ as: "h2",
3985
+ className: "text-3xl font-bold text-center"
3986
+ },
3987
+ $bind: { from: "content.sectionTitle" }
3988
+ }, {
3989
+ type: "grid",
3990
+ props: {
3991
+ columns: 3,
3992
+ gap: "lg"
3993
+ },
3994
+ $repeat: {
3995
+ collection: { from: "content.features" },
3996
+ itemName: "feature"
3997
+ },
3998
+ children: [{
3999
+ type: "div",
4000
+ props: { className: "bg-white p-6 rounded-lg shadow-sm" },
4001
+ children: [{
4002
+ type: "stack",
4003
+ props: { gap: "md" },
4004
+ children: [
4005
+ {
4006
+ type: "div",
4007
+ props: { className: "w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center" },
4008
+ children: [{
4009
+ type: "text",
4010
+ props: { className: "text-primary" },
4011
+ $bind: { from: "feature.icon" }
4012
+ }]
4013
+ },
4014
+ {
4015
+ type: "text",
4016
+ props: {
4017
+ as: "h3",
4018
+ className: "text-xl font-semibold"
4019
+ },
4020
+ $bind: { from: "feature.title" }
4021
+ },
4022
+ {
4023
+ type: "text",
4024
+ props: { className: "text-muted" },
4025
+ $bind: { from: "feature.description" }
4026
+ }
4027
+ ]
4028
+ }]
4029
+ }]
4030
+ }]
4031
+ }]
4032
+ }
4033
+ }] },
4034
+ explanation: `
4035
+ - Repeating cards detected (3+ similar elements) → repeater field
4036
+ - Each card has icon, title, description → schema.fields
4037
+ - $repeat used to iterate over features array
4038
+ - Grid layout with columns: 3 for responsive grid
4039
+ - bg-gray-50 mapped to 'surface' token
4040
+ - text-gray-600 mapped to 'muted' token
4041
+ - Icon placeholder uses Lucide icon names`
4042
+ };
4043
+ //#endregion
4044
+ //#region src/manifest/prompt/examples/testimonials.ts
4045
+ const testimonialsExample = {
4046
+ name: "Testimonials",
4047
+ description: "Customer testimonials with quotes, author info, and photos.",
4048
+ html: `<section class="py-20 bg-white">
4049
+ <div class="container mx-auto px-4">
4050
+ <h2 class="text-3xl font-bold text-center mb-4">What Our Customers Say</h2>
4051
+ <p class="text-gray-600 text-center mb-12 max-w-2xl mx-auto">
4052
+ Don't just take our word for it. Here's what our customers have to say.
4053
+ </p>
4054
+ <div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
4055
+ <div class="bg-gray-50 p-8 rounded-xl">
4056
+ <p class="text-gray-700 mb-6 italic">
4057
+ "This platform has completely transformed how we work. The results speak for themselves."
4058
+ </p>
4059
+ <div class="flex items-center gap-4">
4060
+ <img src="/avatars/sarah.jpg" alt="Sarah" class="w-12 h-12 rounded-full">
4061
+ <div>
4062
+ <p class="font-semibold">Sarah Johnson</p>
4063
+ <p class="text-sm text-gray-500">CEO, TechCorp</p>
4064
+ </div>
4065
+ </div>
4066
+ </div>
4067
+ <!-- More testimonials... -->
4068
+ </div>
4069
+ </div>
4070
+ </section>`,
4071
+ output: { blocks: [{
4072
+ id: "custom.testimonials",
4073
+ title: "Testimonials",
4074
+ description: "Customer testimonials with quotes and author info",
4075
+ category: "marketing",
4076
+ icon: "quote",
4077
+ tags: [
4078
+ "testimonials",
4079
+ "reviews",
4080
+ "social-proof"
4081
+ ],
4082
+ fields: [
4083
+ {
4084
+ id: "sectionTitle",
4085
+ type: "text",
4086
+ label: "Section Title",
4087
+ required: true,
4088
+ multiline: false,
4089
+ defaultValue: "What Our Customers Say"
4090
+ },
4091
+ {
4092
+ id: "sectionDescription",
4093
+ type: "text",
4094
+ label: "Section Description",
4095
+ required: false,
4096
+ multiline: true
4097
+ },
4098
+ {
4099
+ id: "testimonials",
4100
+ type: "repeater",
4101
+ label: "Testimonials",
4102
+ required: true,
4103
+ itemLabel: "Testimonial",
4104
+ itemLabelSource: "name",
4105
+ minItems: 1,
4106
+ maxItems: 9,
4107
+ allowConversion: false,
4108
+ schema: { fields: [
4109
+ {
4110
+ id: "quote",
4111
+ type: "text",
4112
+ label: "Quote",
4113
+ required: true,
4114
+ multiline: true
4115
+ },
4116
+ {
4117
+ id: "avatar",
4118
+ type: "media",
4119
+ label: "Photo",
4120
+ required: false,
4121
+ mediaKinds: ["image"]
4122
+ },
4123
+ {
4124
+ id: "name",
4125
+ type: "text",
4126
+ label: "Name",
4127
+ required: true,
4128
+ multiline: false
4129
+ },
4130
+ {
4131
+ id: "role",
4132
+ type: "text",
4133
+ label: "Role / Company",
4134
+ required: false,
4135
+ multiline: false
4136
+ }
4137
+ ] }
4138
+ }
4139
+ ],
4140
+ layout: {
4141
+ type: "section",
4142
+ props: { className: "py-20" },
4143
+ children: [{
4144
+ type: "stack",
4145
+ props: {
4146
+ gap: "xl",
4147
+ align: "center"
4148
+ },
4149
+ children: [{
4150
+ type: "stack",
4151
+ props: {
4152
+ gap: "md",
4153
+ align: "center",
4154
+ className: "max-w-2xl text-center"
4155
+ },
4156
+ children: [{
4157
+ type: "text",
4158
+ props: {
4159
+ as: "h2",
4160
+ className: "text-3xl font-bold"
4161
+ },
4162
+ $bind: { from: "content.sectionTitle" }
4163
+ }, {
4164
+ type: "text",
4165
+ props: { className: "text-muted" },
4166
+ $bind: { from: "content.sectionDescription" }
4167
+ }]
4168
+ }, {
4169
+ type: "grid",
4170
+ props: {
4171
+ columns: 3,
4172
+ gap: "lg"
4173
+ },
4174
+ $repeat: {
4175
+ collection: { from: "content.testimonials" },
4176
+ itemName: "testimonial"
4177
+ },
4178
+ children: [{
4179
+ type: "div",
4180
+ props: { className: "bg-surface p-8 rounded-xl" },
4181
+ children: [{
4182
+ type: "stack",
4183
+ props: { gap: "lg" },
4184
+ children: [{
4185
+ type: "text",
4186
+ props: { className: "italic" },
4187
+ $bind: { from: "testimonial.quote" }
4188
+ }, {
4189
+ type: "inline",
4190
+ props: {
4191
+ gap: "md",
4192
+ align: "center"
4193
+ },
4194
+ children: [{
4195
+ type: "media",
4196
+ props: { className: "w-12 h-12 rounded-full" },
4197
+ $bind: { from: "testimonial.avatar" }
4198
+ }, {
4199
+ type: "stack",
4200
+ props: { gap: "xs" },
4201
+ children: [{
4202
+ type: "text",
4203
+ props: { className: "font-semibold" },
4204
+ $bind: { from: "testimonial.name" }
4205
+ }, {
4206
+ type: "text",
4207
+ props: { className: "text-sm text-muted" },
4208
+ $bind: { from: "testimonial.role" }
4209
+ }]
4210
+ }]
4211
+ }]
4212
+ }]
4213
+ }]
4214
+ }]
4215
+ }]
4216
+ }
4217
+ }] },
4218
+ explanation: `
4219
+ - Testimonials repeater with quote, avatar, name, role fields
4220
+ - itemLabelSource: 'name' shows customer name in editor list
4221
+ - Avatar uses media field with mediaKinds: ['image']
4222
+ - Quote uses multiline text (longer content)
4223
+ - bg-gray-50 cards mapped to 'surface' token
4224
+ - Author info grouped with inline layout
4225
+ - rounded-full for circular avatar styling`
4226
+ };
4227
+ //#endregion
4228
+ //#region src/manifest/prompt/examples/pricing.ts
4229
+ const pricingExample = {
4230
+ name: "Pricing Cards",
4231
+ description: "Pricing tier cards with features list and CTA buttons.",
4232
+ html: `<section class="py-20 bg-gray-100">
4233
+ <div class="container mx-auto px-4">
4234
+ <h2 class="text-3xl font-bold text-center mb-4">Simple, Transparent Pricing</h2>
4235
+ <p class="text-gray-600 text-center mb-12">Choose the plan that works for you</p>
4236
+ <div class="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
4237
+ <div class="bg-white rounded-2xl p-8 shadow-sm">
4238
+ <h3 class="text-xl font-semibold mb-2">Starter</h3>
4239
+ <div class="text-4xl font-bold mb-4">$9<span class="text-lg text-gray-500">/mo</span></div>
4240
+ <ul class="space-y-3 mb-8">
4241
+ <li class="flex items-center gap-2">
4242
+ <svg class="w-5 h-5 text-green-500"><!-- check --></svg>
4243
+ <span>5 projects</span>
4244
+ </li>
4245
+ <li class="flex items-center gap-2">
4246
+ <svg class="w-5 h-5 text-green-500"><!-- check --></svg>
4247
+ <span>Basic analytics</span>
4248
+ </li>
4249
+ </ul>
4250
+ <a href="/signup?plan=starter" class="block w-full text-center bg-gray-900 text-white py-3 rounded-lg">
4251
+ Get Started
4252
+ </a>
4253
+ </div>
4254
+ <!-- More pricing tiers... -->
4255
+ </div>
4256
+ </div>
4257
+ </section>`,
4258
+ output: { blocks: [{
4259
+ id: "custom.pricing",
4260
+ title: "Pricing Cards",
4261
+ description: "Pricing tiers with features and CTA",
4262
+ category: "marketing",
4263
+ icon: "credit-card",
4264
+ tags: [
4265
+ "pricing",
4266
+ "plans",
4267
+ "subscription"
4268
+ ],
4269
+ fields: [
4270
+ {
4271
+ id: "sectionTitle",
4272
+ type: "text",
4273
+ label: "Section Title",
4274
+ required: true,
4275
+ multiline: false,
4276
+ defaultValue: "Simple, Transparent Pricing"
4277
+ },
4278
+ {
4279
+ id: "sectionSubtitle",
4280
+ type: "text",
4281
+ label: "Section Subtitle",
4282
+ required: false,
4283
+ multiline: false
4284
+ },
4285
+ {
4286
+ id: "plans",
4287
+ type: "repeater",
4288
+ label: "Pricing Plans",
4289
+ required: true,
4290
+ itemLabel: "Plan",
4291
+ itemLabelSource: "name",
4292
+ minItems: 1,
4293
+ maxItems: 5,
4294
+ allowConversion: false,
4295
+ schema: { fields: [
4296
+ {
4297
+ id: "name",
4298
+ type: "text",
4299
+ label: "Plan Name",
4300
+ required: true,
4301
+ multiline: false
4302
+ },
4303
+ {
4304
+ id: "price",
4305
+ type: "text",
4306
+ label: "Price",
4307
+ required: true,
4308
+ multiline: false,
4309
+ ui: { placeholder: "e.g., $9" }
4310
+ },
4311
+ {
4312
+ id: "period",
4313
+ type: "text",
4314
+ label: "Period",
4315
+ required: false,
4316
+ multiline: false,
4317
+ defaultValue: "/mo"
4318
+ },
4319
+ {
4320
+ id: "isHighlighted",
4321
+ type: "boolean",
4322
+ label: "Highlight this plan",
4323
+ required: false,
4324
+ defaultValue: false
4325
+ },
4326
+ {
4327
+ id: "features",
4328
+ type: "repeater",
4329
+ label: "Features",
4330
+ required: true,
4331
+ itemLabel: "Feature",
4332
+ minItems: 1,
4333
+ allowConversion: false,
4334
+ schema: { fields: [{
4335
+ id: "text",
4336
+ type: "text",
4337
+ label: "Feature",
4338
+ required: true,
4339
+ multiline: false
4340
+ }] }
4341
+ },
4342
+ {
4343
+ id: "cta",
4344
+ type: "link",
4345
+ label: "Call to Action",
4346
+ required: true
4347
+ },
4348
+ {
4349
+ id: "ctaText",
4350
+ type: "text",
4351
+ label: "Button Text",
4352
+ required: true,
4353
+ multiline: false,
4354
+ defaultValue: "Get Started"
4355
+ }
4356
+ ] }
4357
+ }
4358
+ ],
4359
+ layout: {
4360
+ type: "section",
4361
+ props: {
4362
+ background: "surface",
4363
+ className: "py-20"
4364
+ },
4365
+ children: [{
4366
+ type: "stack",
4367
+ props: {
4368
+ gap: "xl",
4369
+ align: "center"
4370
+ },
4371
+ children: [{
4372
+ type: "stack",
4373
+ props: {
4374
+ gap: "md",
4375
+ align: "center"
4376
+ },
4377
+ children: [{
4378
+ type: "text",
4379
+ props: {
4380
+ as: "h2",
4381
+ className: "text-3xl font-bold"
4382
+ },
4383
+ $bind: { from: "content.sectionTitle" }
4384
+ }, {
4385
+ type: "text",
4386
+ props: { className: "text-muted" },
4387
+ $bind: { from: "content.sectionSubtitle" }
4388
+ }]
4389
+ }, {
4390
+ type: "grid",
4391
+ props: {
4392
+ columns: 3,
4393
+ gap: "lg",
4394
+ className: "max-w-5xl"
4395
+ },
4396
+ $repeat: {
4397
+ collection: { from: "content.plans" },
4398
+ itemName: "plan"
4399
+ },
4400
+ children: [{
4401
+ type: "div",
4402
+ props: { className: "bg-white rounded-2xl p-8 shadow-sm" },
4403
+ children: [{
4404
+ type: "stack",
4405
+ props: { gap: "lg" },
4406
+ children: [
4407
+ {
4408
+ type: "text",
4409
+ props: {
4410
+ as: "h3",
4411
+ className: "text-xl font-semibold"
4412
+ },
4413
+ $bind: { from: "plan.name" }
4414
+ },
4415
+ {
4416
+ type: "inline",
4417
+ props: { align: "baseline" },
4418
+ children: [{
4419
+ type: "text",
4420
+ props: { className: "text-4xl font-bold" },
4421
+ $bind: { from: "plan.price" }
4422
+ }, {
4423
+ type: "text",
4424
+ props: { className: "text-muted" },
4425
+ $bind: { from: "plan.period" }
4426
+ }]
4427
+ },
4428
+ {
4429
+ type: "stack",
4430
+ props: { gap: "sm" },
4431
+ $repeat: {
4432
+ collection: { from: "plan.features" },
4433
+ itemName: "feature"
4434
+ },
4435
+ children: [{
4436
+ type: "inline",
4437
+ props: {
4438
+ gap: "sm",
4439
+ align: "center"
4440
+ },
4441
+ children: [{
4442
+ type: "text",
4443
+ props: { className: "text-green-500" },
4444
+ children: []
4445
+ }, {
4446
+ type: "text",
4447
+ $bind: { from: "feature.text" }
4448
+ }]
4449
+ }]
4450
+ },
4451
+ {
4452
+ type: "button",
4453
+ props: {
4454
+ variant: "primary",
4455
+ className: "w-full",
4456
+ href: { $bind: {
4457
+ from: "plan.cta",
4458
+ transforms: [{ id: "links.resolve" }]
4459
+ } }
4460
+ },
4461
+ $bind: { from: "plan.ctaText" }
4462
+ }
4463
+ ]
4464
+ }]
4465
+ }]
4466
+ }]
4467
+ }]
4468
+ }
4469
+ }] },
4470
+ explanation: `
4471
+ - Nested repeaters: plans > features (each plan has multiple features)
4472
+ - isHighlighted boolean for featured plan styling
4473
+ - Price and period as separate fields for flexible formatting
4474
+ - ctaText separate from cta link for button label customization
4475
+ - itemLabelSource: 'name' shows plan name in editor
4476
+ - Features use inner repeater with simple text field`
4477
+ };
4478
+ //#endregion
4479
+ //#region src/manifest/prompt/examples/cta.ts
4480
+ const ctaExample = {
4481
+ name: "Call to Action",
4482
+ description: "A simple CTA section with headline, description, and action buttons.",
4483
+ html: `<section class="bg-gradient-to-r from-purple-600 to-blue-600 py-16">
4484
+ <div class="container mx-auto px-4 text-center text-white">
4485
+ <h2 class="text-3xl font-bold mb-4">Ready to Get Started?</h2>
4486
+ <p class="text-lg text-white/80 mb-8 max-w-xl mx-auto">
4487
+ Join thousands of happy customers and take your business to the next level.
4488
+ </p>
4489
+ <div class="flex justify-center gap-4">
4490
+ <a href="/signup" class="bg-white text-purple-600 px-6 py-3 rounded-lg font-semibold">
4491
+ Start Free Trial
4492
+ </a>
4493
+ <a href="/contact" class="border border-white text-white px-6 py-3 rounded-lg font-semibold">
4494
+ Contact Sales
4495
+ </a>
4496
+ </div>
4497
+ </div>
4498
+ </section>`,
4499
+ output: {
4500
+ blocks: [{
4501
+ id: "custom.cta",
4502
+ title: "Call to Action",
4503
+ description: "CTA section with headline and buttons",
4504
+ category: "marketing",
4505
+ icon: "megaphone",
4506
+ tags: [
4507
+ "cta",
4508
+ "conversion",
4509
+ "action"
4510
+ ],
4511
+ fields: [
4512
+ {
4513
+ id: "headline",
4514
+ type: "text",
4515
+ label: "Headline",
4516
+ required: true,
4517
+ multiline: false,
4518
+ defaultValue: "Ready to Get Started?"
4519
+ },
4520
+ {
4521
+ id: "description",
4522
+ type: "text",
4523
+ label: "Description",
4524
+ required: false,
4525
+ multiline: true
4526
+ },
4527
+ {
4528
+ id: "primaryButton",
4529
+ type: "group",
4530
+ label: "Primary Button",
4531
+ required: false,
4532
+ schema: { fields: [{
4533
+ id: "text",
4534
+ type: "text",
4535
+ label: "Button Text",
4536
+ required: true,
4537
+ multiline: false,
4538
+ defaultValue: "Start Free Trial"
4539
+ }, {
4540
+ id: "link",
4541
+ type: "link",
4542
+ label: "Link",
4543
+ required: true
4544
+ }] }
4545
+ },
4546
+ {
4547
+ id: "secondaryButton",
4548
+ type: "group",
4549
+ label: "Secondary Button",
4550
+ required: false,
4551
+ schema: { fields: [{
4552
+ id: "text",
4553
+ type: "text",
4554
+ label: "Button Text",
4555
+ required: false,
4556
+ multiline: false,
4557
+ defaultValue: "Contact Sales"
4558
+ }, {
4559
+ id: "link",
4560
+ type: "link",
4561
+ label: "Link",
4562
+ required: false
4563
+ }] }
4564
+ }
4565
+ ],
4566
+ layout: {
4567
+ type: "section",
4568
+ props: {
4569
+ background: "primary",
4570
+ className: "py-16"
4571
+ },
4572
+ children: [{
4573
+ type: "stack",
4574
+ props: {
4575
+ gap: "lg",
4576
+ align: "center",
4577
+ className: "text-center text-white"
4578
+ },
4579
+ children: [
4580
+ {
4581
+ type: "text",
4582
+ props: {
4583
+ as: "h2",
4584
+ className: "text-3xl font-bold"
4585
+ },
4586
+ children: null,
4587
+ $bind: {
4588
+ from: "content.headline",
4589
+ fallback: null,
4590
+ transforms: null
4591
+ },
4592
+ $repeat: null,
4593
+ $when: null
4594
+ },
4595
+ {
4596
+ type: "text",
4597
+ props: { className: "text-lg opacity-80 max-w-xl" },
4598
+ children: null,
4599
+ $bind: {
4600
+ from: "content.description",
4601
+ fallback: null,
4602
+ transforms: null
4603
+ },
4604
+ $repeat: null,
4605
+ $when: null
4606
+ },
4607
+ {
4608
+ type: "inline",
4609
+ props: {
4610
+ gap: "md",
4611
+ justify: "center"
4612
+ },
4613
+ children: [{
4614
+ type: "button",
4615
+ props: {
4616
+ variant: "secondary",
4617
+ size: "lg",
4618
+ href: { $bind: {
4619
+ from: "content.primaryButton.link",
4620
+ fallback: null,
4621
+ transforms: [{ id: "links.resolve" }]
4622
+ } }
4623
+ },
4624
+ children: null,
4625
+ $bind: {
4626
+ from: "content.primaryButton.text",
4627
+ fallback: null,
4628
+ transforms: null
4629
+ },
4630
+ $repeat: null,
4631
+ $when: null
4632
+ }, {
4633
+ type: "button",
4634
+ props: {
4635
+ variant: "outline",
4636
+ size: "lg",
4637
+ href: { $bind: {
4638
+ from: "content.secondaryButton.link",
4639
+ fallback: null,
4640
+ transforms: [{ id: "links.resolve" }]
4641
+ } }
4642
+ },
4643
+ children: null,
4644
+ $bind: {
4645
+ from: "content.secondaryButton.text",
4646
+ fallback: null,
4647
+ transforms: null
4648
+ },
4649
+ $repeat: null,
4650
+ $when: null
4651
+ }],
4652
+ $bind: null,
4653
+ $repeat: null,
4654
+ $when: null
4655
+ }
4656
+ ],
4657
+ $bind: null,
4658
+ $repeat: null,
4659
+ $when: null
4660
+ }],
4661
+ $bind: null,
4662
+ $repeat: null,
4663
+ $when: null
4664
+ }
4665
+ }],
4666
+ customCssRules: [{
4667
+ id: "cta-gradient-bg",
4668
+ selector: "[data-block=\"custom.cta\"]",
4669
+ declarations: [{
4670
+ property: "background",
4671
+ value: "linear-gradient(to right, #9333ea, #2563eb)"
4672
+ }]
4673
+ }]
4674
+ },
4675
+ explanation: `
4676
+ KEY POINTS:
4677
+ - EVERY text node has $bind (headline, description)
4678
+ - Buttons bind label via primaryButton.text / secondaryButton.text and href via props.href + links.resolve
4679
+ - All nullable fields are explicitly included (children: null, $repeat: null, $when: null)
4680
+
4681
+ Structure:
4682
+ - Gradient background uses customCssRules with declarations as array of {property, value}
4683
+ - Two buttons grouped in primaryButton/secondaryButton groups
4684
+ - inline layout for horizontal button arrangement`
4685
+ };
4686
+ //#endregion
4687
+ //#region src/manifest/prompt/examples/index.ts
4688
+ /**
4689
+ * Example Conversions for System Prompt
4690
+ *
4691
+ * These examples teach the LLM how to convert HTML to block manifests.
4692
+ * Each example shows input HTML and expected output JSON.
4693
+ */
4694
+ /**
4695
+ * Format an example for inclusion in the prompt.
4696
+ *
4697
+ * @param example - The conversion example to format
4698
+ * @returns Formatted markdown string for the system prompt
4699
+ */
4700
+ function formatExample(example) {
4701
+ return `
4702
+ ### Example: ${example.name}
4703
+
4704
+ ${example.description}
4705
+
4706
+ **Input HTML:**
4707
+ \`\`\`html
4708
+ ${example.html}
4709
+ \`\`\`
4710
+
4711
+ **Output:**
4712
+ \`\`\`json
4713
+ ${JSON.stringify(example.output, null, 2)}
4714
+ \`\`\`
4715
+
4716
+ **Key decisions:**
4717
+ ${example.explanation}
4718
+ `;
4719
+ }
4720
+ /**
4721
+ * Get all examples formatted for the system prompt.
4722
+ *
4723
+ * @returns Formatted examples section for the system prompt
4724
+ */
4725
+ function getExamples() {
4726
+ return [
4727
+ "## Examples",
4728
+ "",
4729
+ "Study these examples to understand the conversion patterns:",
4730
+ ...[
4731
+ heroExample,
4732
+ featuresGridExample,
4733
+ testimonialsExample,
4734
+ pricingExample,
4735
+ ctaExample
4736
+ ].map(formatExample)
4737
+ ].join("\n");
4738
+ }
4739
+ //#endregion
4740
+ //#region src/manifest/prompt/system.ts
4741
+ const ROLE_DESCRIPTION = `
4742
+ You are an expert HTML-to-manifest conversion specialist. Your task is to convert HTML
4743
+ (typically with Tailwind CSS classes) into declarative block manifests for a CMS.
4744
+
4745
+ You analyze HTML structure and extract:
4746
+ 1. Editable content fields (text, images, links, etc.)
4747
+ 2. Layout structure using the node DSL
4748
+ 3. Styling mapped to theme tokens or custom CSS
4749
+ `;
4750
+ const OUTPUT_FORMAT = `
4751
+ ## Output Format
4752
+
4753
+ You MUST respond with a valid JSON object containing:
4754
+
4755
+ \`\`\`typescript
4756
+ {
4757
+ blocks: Array<{
4758
+ id: string; // Must start with 'custom.' (e.g., 'custom.hero')
4759
+ title: string; // Human-readable name
4760
+ description?: string; // Optional description
4761
+ category: 'marketing' | 'content' | 'blog' | 'media' | 'layout' | 'interactive';
4762
+ icon?: string; // Lucide icon name (e.g., 'layout', 'image')
4763
+ tags?: string[]; // Search tags
4764
+ fields: Array<Field>; // Editable content fields
4765
+ layout: NodeDefinition; // Visual structure
4766
+ }>;
4767
+
4768
+ themePatch?: Array<{ // Optional: Add new theme tokens
4769
+ op: 'add' | 'replace';
4770
+ path: string; // JSON path (e.g., '/palette/colors/-')
4771
+ value: unknown;
4772
+ }>;
4773
+
4774
+ customCssRules?: Array<{ // Optional: Block-specific CSS
4775
+ id: string; // Unique rule ID
4776
+ selector: string; // CSS selector (no :root, html, body, *)
4777
+ declarations: Array<{ property: string; value: string }>;
4778
+ }>;
4779
+ }
4780
+ \`\`\`
4781
+
4782
+ ## Field Types
4783
+
4784
+ Available field types:
4785
+ - **text**: Single-line or multiline text (use multiline: true for paragraphs)
4786
+ - **richText**: Rich text with formatting (bold, italic, links)
4787
+ - **media**: Display media such as images, videos, or audio (specify mediaKinds: ['image'], ['image', 'video'], or ['audio'])
4788
+ - **file**: Downloadable files such as PDFs, documents, spreadsheets, archives, or audio (specify fileKinds: ['document'], ['spreadsheet'], ['archive'], or ['audio'])
4789
+ - **link**: Internal or external links
4790
+ - **boolean**: Toggle switches
4791
+ - **select**: Dropdown with predefined options (must include options array)
4792
+ - **number**: Numeric values
4793
+ - **repeater**: Repeating content (features, testimonials, etc.)
4794
+ - **group**: Grouped related fields
4795
+
4796
+ ## Layout Structure
4797
+
4798
+ The layout is a tree of nodes. Each node has:
4799
+ - **type**: Node type (section, stack, text, media, etc.)
4800
+ - **props**: Optional properties (as, className, style)
4801
+ - **children**: Child nodes (for containers)
4802
+ - **$bind**: Content binding (connects to fields)
4803
+ - **$repeat**: Repeats children for array fields
4804
+ `;
4805
+ const IMPORTANT_RULES = `
4806
+ ## Important Rules
4807
+
4808
+ ### CRITICAL: $bind is REQUIRED for ALL content nodes
4809
+
4810
+ **This is the most important rule.** Without $bind, layout nodes display NOTHING.
4811
+
4812
+ Every node of these types MUST have a $bind:
4813
+ - **text**: MUST have \`$bind: { from: "content.fieldId" }\`
4814
+ - **richText**: MUST have \`$bind: { from: "content.fieldId" }\`
4815
+ - **media**: MUST have \`$bind: { from: "content.fieldId" }\`
4816
+ - **button**: MUST have \`$bind\` for its LABEL (text or link label) and MUST set \`props.href\` for navigation
4817
+ - Label: \`$bind: { from: "content.linkFieldId", transforms: [{ id: "links.label" }] }\`
4818
+ - Href: \`props: { href: { $bind: { from: "content.linkFieldId", transforms: [{ id: "links.resolve" }] } } }\`
4819
+ - **link**: Same as button (label via \`links.label\`, href via \`props.href\` + \`links.resolve\`)
4820
+
4821
+ **WRONG** (empty node that displays nothing):
4822
+ \`\`\`json
4823
+ {
4824
+ "type": "text",
4825
+ "props": { "as": "h1" },
4826
+ "children": null,
4827
+ "$bind": null,
4828
+ "$repeat": null,
4829
+ "$when": null
4830
+ }
4831
+ \`\`\`
4832
+
4833
+ **CORRECT** (bound to field data):
4834
+ \`\`\`json
4835
+ {
4836
+ "type": "text",
4837
+ "props": { "as": "h1" },
4838
+ "children": null,
4839
+ "$bind": { "from": "content.headline", "fallback": null, "transforms": null },
4840
+ "$repeat": null,
4841
+ "$when": null
4842
+ }
4843
+ \`\`\`
4844
+
4845
+ ### Every field MUST be bound in the layout
4846
+
4847
+ If you define a field, you MUST use it with $bind somewhere in the layout (node.$bind or a nested props.$bind). Unused fields are invalid.
4848
+
4849
+ ### Other Rules
4850
+
4851
+ 1. **Block IDs**: Must be 'custom.' + lowercase + hyphens only (e.g., 'custom.hero-section')
4852
+
4853
+ 2. **Field IDs**: Use camelCase (e.g., 'headline', 'ctaButton', 'featureItems')
4854
+
4855
+ 3. **Binding Paths**: Always start with 'content.' (e.g., 'content.headline')
4856
+
4857
+ 4. **Theme Priority**: Use theme tokens when possible before adding customCssRules
4858
+
4859
+ 5. **Custom CSS MUST match the layout**
4860
+ - If a customCssRules selector uses a class (e.g. ".heroTitle"), you MUST add that class to some node via props.className.
4861
+ - Do NOT target internal renderer classes like ".cms-node-*" (they are not guaranteed).
4862
+ - Do NOT put "@media" / at-rules into selector (emit base styles only).
4863
+ - Always scope selectors to the block: prefix with [data-block="custom.your-block-id"].
4864
+
4865
+ 5. **Required Fields**: Every field needs at minimum:
4866
+ - id: Unique identifier
4867
+ - type: Field type
4868
+ - label: Human-readable label
4869
+ - required: Boolean (true if essential content)
4870
+
4871
+ 6. **Layout Requirements**:
4872
+ - Root node is typically 'section' for full-width blocks
4873
+ - Use 'stack' for vertical layouts, 'inline' for horizontal
4874
+ - Every text node MUST have $bind
4875
+ - Include explicit nulls for unused keys (props, children, $bind, $repeat, $when)
4876
+
4877
+ 7. **No Hardcoded Content**: Extract all visible text as fields, bind them with $bind
4878
+
4879
+ 8. **Selector Safety**: customCssRules selectors cannot use :root, html, body, or *
4880
+ `;
4881
+ /**
4882
+ * Build the complete system prompt for HTML-to-manifest conversion.
4883
+ *
4884
+ * Combines role description, output format, theme context, knowledge base,
4885
+ * and examples into a comprehensive system prompt for the LLM.
4886
+ *
4887
+ * @param options - Options including theme and conversion preferences
4888
+ * @returns Complete system prompt string
4889
+ */
4890
+ function buildSystemPrompt(options) {
4891
+ const { theme, includeExamples = true, expectedBlockCount = "auto" } = options;
4892
+ const themeSection = formatThemeSummaryForPrompt(summarizeThemeForLLM(theme));
4893
+ const parts = [
4894
+ ROLE_DESCRIPTION.trim(),
4895
+ "",
4896
+ OUTPUT_FORMAT.trim(),
4897
+ "",
4898
+ IMPORTANT_RULES.trim(),
4899
+ "",
4900
+ themeSection,
4901
+ "",
4902
+ getKnowledgeBase()
4903
+ ];
4904
+ if (expectedBlockCount === "single") {
4905
+ parts.push("");
4906
+ parts.push("**Note**: Convert the HTML into a SINGLE block. Do not split into multiple blocks.");
4907
+ } else if (expectedBlockCount === "multiple") {
4908
+ parts.push("");
4909
+ parts.push("**Note**: This HTML likely contains multiple sections. Split into separate blocks as appropriate.");
4910
+ }
4911
+ if (includeExamples) {
4912
+ parts.push("");
4913
+ parts.push(getExamples());
4914
+ }
4915
+ return parts.join("\n");
4916
+ }
4917
+ /**
4918
+ * Build the user message for the conversion request.
4919
+ *
4920
+ * Formats the HTML and optional CSS for conversion, including any
4921
+ * validation feedback from previous attempts.
4922
+ *
4923
+ * @param options - Message options with HTML content and optional CSS/errors
4924
+ * @returns Formatted user message string
4925
+ */
4926
+ function buildUserMessage(options) {
4927
+ const { html, css, previousErrors, isRetry } = options;
4928
+ const parts = ["Convert the following HTML to a block manifest:"];
4929
+ parts.push("");
4930
+ parts.push("```html");
4931
+ parts.push(html);
4932
+ parts.push("```");
4933
+ if (css) {
4934
+ parts.push("");
4935
+ parts.push("Additional CSS:");
4936
+ parts.push("```css");
4937
+ parts.push(css);
4938
+ parts.push("```");
4939
+ }
4940
+ if (previousErrors) {
4941
+ parts.push("");
4942
+ parts.push("---");
4943
+ parts.push("");
4944
+ parts.push("⚠️ **YOUR PREVIOUS OUTPUT FAILED VALIDATION** ⚠️");
4945
+ parts.push("");
4946
+ parts.push("You must regenerate the output with the errors fixed. Read the errors carefully.");
4947
+ parts.push("");
4948
+ parts.push(previousErrors);
4949
+ }
4950
+ if (isRetry) {
4951
+ parts.push("");
4952
+ parts.push("---");
4953
+ parts.push("");
4954
+ parts.push("**REQUIRED: Explain your fixes in the `fixRationale` field.**");
4955
+ parts.push("");
4956
+ parts.push("In your output, set `fixRationale` to a string explaining:");
4957
+ parts.push("1. What validation errors you found in your previous output");
4958
+ parts.push("2. What specific changes you made to fix each error");
4959
+ parts.push("3. Which fields you bound to which nodes with $bind");
4960
+ parts.push("");
4961
+ parts.push("Example fixRationale: \"Fixed 3 text nodes missing $bind. Added $bind: { from: content.headline } to the h1 text node, $bind: { from: content.description } to the p text node, and $bind: { from: content.highlight } to the span text node. All 3 defined fields are now bound in the layout.\"");
4962
+ }
4963
+ return parts.join("\n");
4964
+ }
4965
+ //#endregion
4966
+ export { buildSystemPrompt, buildUserMessage, collectValidationErrors, convertHtmlToManifest, formatValidationFeedback, htmlToManifestOutputSchema, llmAtRuleSchema, llmCustomBlockSchema, llmCustomCssRuleSchema, llmMediaAtRuleSchema, llmNestedRuleSchema, llmThemePatchOpSchema, normalizeDeclarationsArrayToRecord, normalizeLlmCssRule, normalizeLlmMediaAtRule, summarizeThemeForLLM, validateManifestOutput };