@wildo-ai/saas-website 1.1.4 → 1.1.6

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 (265) hide show
  1. package/dist/esm/astro/blog-post-bridge.d.ts +23 -20
  2. package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -1
  3. package/dist/esm/astro/blog-post-bridge.js +13 -6
  4. package/dist/esm/astro/blog-post-bridge.js.map +1 -1
  5. package/dist/esm/astro/blog-post-head.renderer.d.ts.map +1 -1
  6. package/dist/esm/astro/bridge-runtime.d.ts +55 -26
  7. package/dist/esm/astro/bridge-runtime.d.ts.map +1 -1
  8. package/dist/esm/astro/bridge-runtime.js +21 -7
  9. package/dist/esm/astro/bridge-runtime.js.map +1 -1
  10. package/dist/esm/astro/collect-expected-label-keys.d.ts.map +1 -1
  11. package/dist/esm/astro/headers-renderer.d.ts.map +1 -1
  12. package/dist/esm/astro/i18n-routing.helper.d.ts.map +1 -1
  13. package/dist/esm/astro/label-pack-loader.d.ts +4 -4
  14. package/dist/esm/astro/label-pack-loader.d.ts.map +1 -1
  15. package/dist/esm/astro/label-pack-loader.js +14 -4
  16. package/dist/esm/astro/label-pack-loader.js.map +1 -1
  17. package/dist/esm/astro/llms-txt-server.d.ts.map +1 -1
  18. package/dist/esm/astro/pick-labels-for-page.d.ts.map +1 -1
  19. package/dist/esm/astro/robots-renderer.d.ts +9 -8
  20. package/dist/esm/astro/robots-renderer.d.ts.map +1 -1
  21. package/dist/esm/astro/robots-renderer.js +15 -15
  22. package/dist/esm/astro/robots-renderer.js.map +1 -1
  23. package/dist/esm/astro/sitemap-coverage.d.ts.map +1 -1
  24. package/dist/esm/astro/structured-data.renderer.d.ts.map +1 -1
  25. package/dist/esm/astro/website-page-head.renderer.d.ts +1 -1
  26. package/dist/esm/astro/website-page-head.renderer.d.ts.map +1 -1
  27. package/dist/esm/astro/website-page-head.renderer.js +2 -1
  28. package/dist/esm/astro/website-page-head.renderer.js.map +1 -1
  29. package/dist/esm/astro/website-page-runtime.helper.d.ts.map +1 -1
  30. package/dist/esm/astro/website-site-context.d.ts +5 -2
  31. package/dist/esm/astro/website-site-context.d.ts.map +1 -1
  32. package/dist/esm/astro/website-site-context.js.map +1 -1
  33. package/dist/esm/astro-island.d.ts.map +1 -1
  34. package/dist/esm/astro.d.ts.map +1 -1
  35. package/dist/esm/companion-exports.d.ts +8 -15
  36. package/dist/esm/companion-exports.d.ts.map +1 -1
  37. package/dist/esm/companion-exports.js +8 -15
  38. package/dist/esm/companion-exports.js.map +1 -1
  39. package/dist/esm/components/low-level/WebsiteInternalButton.d.ts.map +1 -1
  40. package/dist/esm/components/low-level/WebsiteInternalLink.d.ts.map +1 -1
  41. package/dist/esm/config/define-website-config.d.ts +3 -2
  42. package/dist/esm/config/define-website-config.d.ts.map +1 -1
  43. package/dist/esm/config/define-website-config.js +10 -10
  44. package/dist/esm/config/define-website-config.js.map +1 -1
  45. package/dist/esm/config/index.d.ts.map +1 -1
  46. package/dist/esm/config/load-website-config.d.ts +6 -0
  47. package/dist/esm/config/load-website-config.d.ts.map +1 -1
  48. package/dist/esm/config/load-website-config.js +20 -10
  49. package/dist/esm/config/load-website-config.js.map +1 -1
  50. package/dist/esm/config/load-website-pricing-catalog.d.ts +13 -0
  51. package/dist/esm/config/load-website-pricing-catalog.d.ts.map +1 -0
  52. package/dist/esm/config/load-website-pricing-catalog.js +34 -0
  53. package/dist/esm/config/load-website-pricing-catalog.js.map +1 -0
  54. package/dist/esm/config/load-website-site-context.d.ts +53 -0
  55. package/dist/esm/config/load-website-site-context.d.ts.map +1 -0
  56. package/dist/esm/config/load-website-site-context.js +98 -0
  57. package/dist/esm/config/load-website-site-context.js.map +1 -0
  58. package/dist/esm/config/wildo-website-config.schemas.d.ts +10 -5
  59. package/dist/esm/config/wildo-website-config.schemas.d.ts.map +1 -1
  60. package/dist/esm/config/wildo-website-config.schemas.js +10 -5
  61. package/dist/esm/config/wildo-website-config.schemas.js.map +1 -1
  62. package/dist/esm/config-loader.d.ts +2 -0
  63. package/dist/esm/config-loader.d.ts.map +1 -1
  64. package/dist/esm/config-loader.js +2 -0
  65. package/dist/esm/config-loader.js.map +1 -1
  66. package/dist/esm/core/anonymous-session/InboundContactForm.d.ts +26 -6
  67. package/dist/esm/core/anonymous-session/InboundContactForm.d.ts.map +1 -1
  68. package/dist/esm/core/anonymous-session/InboundContactForm.js +51 -33
  69. package/dist/esm/core/anonymous-session/InboundContactForm.js.map +1 -1
  70. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +14 -3
  71. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts.map +1 -1
  72. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js +28 -13
  73. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js.map +1 -1
  74. package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts +24 -1
  75. package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts.map +1 -1
  76. package/dist/esm/core/anonymous-session/website-anonymous-session-client.js +46 -3
  77. package/dist/esm/core/anonymous-session/website-anonymous-session-client.js.map +1 -1
  78. package/dist/esm/core/anonymous-session/website-submission-challenge.d.ts +36 -0
  79. package/dist/esm/core/anonymous-session/website-submission-challenge.d.ts.map +1 -0
  80. package/dist/esm/core/anonymous-session/website-submission-challenge.js +69 -0
  81. package/dist/esm/core/anonymous-session/website-submission-challenge.js.map +1 -0
  82. package/dist/esm/core/consent/WebsiteConsentBanner.d.ts +6 -0
  83. package/dist/esm/core/consent/WebsiteConsentBanner.d.ts.map +1 -0
  84. package/dist/esm/core/consent/WebsiteConsentBanner.js +129 -0
  85. package/dist/esm/core/consent/WebsiteConsentBanner.js.map +1 -0
  86. package/dist/esm/core/consent/WebsiteConsentContext.d.ts +85 -0
  87. package/dist/esm/core/consent/WebsiteConsentContext.d.ts.map +1 -0
  88. package/dist/esm/core/consent/WebsiteConsentContext.js +89 -0
  89. package/dist/esm/core/consent/WebsiteConsentContext.js.map +1 -0
  90. package/dist/esm/core/consent/WebsiteConsentGate.d.ts +27 -0
  91. package/dist/esm/core/consent/WebsiteConsentGate.d.ts.map +1 -0
  92. package/dist/esm/core/consent/WebsiteConsentGate.js +9 -0
  93. package/dist/esm/core/consent/WebsiteConsentGate.js.map +1 -0
  94. package/dist/esm/core/consent/useWebsiteConsent.d.ts +10 -0
  95. package/dist/esm/core/consent/useWebsiteConsent.d.ts.map +1 -0
  96. package/dist/esm/core/consent/useWebsiteConsent.js +17 -0
  97. package/dist/esm/core/consent/useWebsiteConsent.js.map +1 -0
  98. package/dist/esm/core/consent/website-consent-storage.d.ts +22 -0
  99. package/dist/esm/core/consent/website-consent-storage.d.ts.map +1 -0
  100. package/dist/esm/core/consent/website-consent-storage.js +102 -0
  101. package/dist/esm/core/consent/website-consent-storage.js.map +1 -0
  102. package/dist/esm/core/contexts/WebsitePageContext.d.ts.map +1 -1
  103. package/dist/esm/core/contexts/WebsitePageContext.js.map +1 -1
  104. package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +33 -6
  105. package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts.map +1 -1
  106. package/dist/esm/core/contexts/WebsiteRuntimeContext.js.map +1 -1
  107. package/dist/esm/core/contexts/WebsiteSectionContext.d.ts.map +1 -1
  108. package/dist/esm/core/contexts/WebsiteSectionContext.js.map +1 -1
  109. package/dist/esm/core/contexts/useWebsitePage.d.ts.map +1 -1
  110. package/dist/esm/core/contexts/useWebsiteRuntime.d.ts +18 -0
  111. package/dist/esm/core/contexts/useWebsiteRuntime.d.ts.map +1 -1
  112. package/dist/esm/core/contexts/useWebsiteRuntime.js +28 -2
  113. package/dist/esm/core/contexts/useWebsiteRuntime.js.map +1 -1
  114. package/dist/esm/core/contexts/useWebsiteSection.d.ts.map +1 -1
  115. package/dist/esm/core/external-providers/WebsiteProviderComponent.d.ts +57 -0
  116. package/dist/esm/core/external-providers/WebsiteProviderComponent.d.ts.map +1 -0
  117. package/dist/esm/core/external-providers/WebsiteProviderComponent.js +56 -0
  118. package/dist/esm/core/external-providers/WebsiteProviderComponent.js.map +1 -0
  119. package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts +1 -2
  120. package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts.map +1 -1
  121. package/dist/esm/core/external-providers/frontend-provider-registry.website.js.map +1 -1
  122. package/dist/esm/core/external-providers/provider-component-registry.website.d.ts +63 -0
  123. package/dist/esm/core/external-providers/provider-component-registry.website.d.ts.map +1 -0
  124. package/dist/esm/core/external-providers/provider-component-registry.website.js +68 -0
  125. package/dist/esm/core/external-providers/provider-component-registry.website.js.map +1 -0
  126. package/dist/esm/core/external-providers/useWebsiteProviderComponent.d.ts +36 -0
  127. package/dist/esm/core/external-providers/useWebsiteProviderComponent.d.ts.map +1 -0
  128. package/dist/esm/core/external-providers/useWebsiteProviderComponent.js +59 -0
  129. package/dist/esm/core/external-providers/useWebsiteProviderComponent.js.map +1 -0
  130. package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts +43 -10
  131. package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts.map +1 -1
  132. package/dist/esm/core/external-providers/useWebsiteProviderScripts.js +92 -29
  133. package/dist/esm/core/external-providers/useWebsiteProviderScripts.js.map +1 -1
  134. package/dist/esm/core/external-providers/website-provider-sdk-activations.d.ts +11 -0
  135. package/dist/esm/core/external-providers/website-provider-sdk-activations.d.ts.map +1 -0
  136. package/dist/esm/core/external-providers/website-provider-sdk-activations.js +14 -0
  137. package/dist/esm/core/external-providers/website-provider-sdk-activations.js.map +1 -0
  138. package/dist/esm/core/factories/define-website-page-manifest.d.ts.map +1 -1
  139. package/dist/esm/core/factories/define-website-section.d.ts +3 -3
  140. package/dist/esm/core/factories/define-website-section.d.ts.map +1 -1
  141. package/dist/esm/core/factories/define-website-section.js +3 -3
  142. package/dist/esm/core/factories/define-website-section.js.map +1 -1
  143. package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts.map +1 -1
  144. package/dist/esm/core/hooks/useWebsiteLabel.d.ts.map +1 -1
  145. package/dist/esm/core/layouts/WebsitePageLayout.d.ts +21 -1
  146. package/dist/esm/core/layouts/WebsitePageLayout.d.ts.map +1 -1
  147. package/dist/esm/core/layouts/WebsitePageLayout.js +39 -8
  148. package/dist/esm/core/layouts/WebsitePageLayout.js.map +1 -1
  149. package/dist/esm/core/layouts/WebsiteSection.d.ts.map +1 -1
  150. package/dist/esm/core/routing/internal-routing.utils.d.ts.map +1 -1
  151. package/dist/esm/index.d.ts +36 -65
  152. package/dist/esm/index.d.ts.map +1 -1
  153. package/dist/esm/index.js +36 -65
  154. package/dist/esm/index.js.map +1 -1
  155. package/dist/esm/mdx/BlogPost.d.ts.map +1 -1
  156. package/dist/esm/mdx/blog-post-collection.config.d.ts.map +1 -1
  157. package/dist/esm/mdx/blog-post-frontmatter-source.d.ts.map +1 -1
  158. package/dist/esm/mdx/index.d.ts.map +1 -1
  159. package/dist/esm/mdx.d.ts.map +1 -1
  160. package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts.map +1 -1
  161. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts +35 -6
  162. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts.map +1 -1
  163. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js +39 -5
  164. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js.map +1 -1
  165. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts +61 -0
  166. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts.map +1 -0
  167. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.js +73 -0
  168. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.js.map +1 -0
  169. package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts.map +1 -1
  170. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.d.ts +32 -0
  171. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.d.ts.map +1 -0
  172. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.js +36 -0
  173. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.js.map +1 -0
  174. package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts.map +1 -1
  175. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts +6 -5
  176. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts.map +1 -1
  177. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js +8 -7
  178. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js.map +1 -1
  179. package/dist/esm/schemas/refs/page-ref.schemas.d.ts.map +1 -1
  180. package/dist/esm/schemas/refs/section-ref.schemas.d.ts.map +1 -1
  181. package/dist/esm/schemas/sections/website-section-category.shared.d.ts.map +1 -1
  182. package/dist/esm/schemas/sections/website-section-category.shared.js.map +1 -1
  183. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts +14 -7
  184. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts.map +1 -1
  185. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js +14 -7
  186. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js.map +1 -1
  187. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.d.ts +57 -0
  188. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.d.ts.map +1 -0
  189. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.js +76 -0
  190. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.js.map +1 -0
  191. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts +23 -0
  192. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts.map +1 -1
  193. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js +20 -2
  194. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js.map +1 -1
  195. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts +7 -5
  196. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts.map +1 -1
  197. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js.map +1 -1
  198. package/dist/esm/schemas/validators/label-pack.validator.d.ts.map +1 -1
  199. package/dist/tsconfig.build.tsbuildinfo +1 -1
  200. package/package.json +6 -29
  201. package/src/__tests__/bundle-isolation.test.ts +19 -22
  202. package/src/astro/__tests__/blog-post-bridge.test.tsx +58 -1
  203. package/src/astro/__tests__/bridge-runtime.test.tsx +44 -1
  204. package/src/astro/__tests__/label-pack-loader.test.ts +29 -1
  205. package/src/astro/blog-post-bridge.tsx +36 -26
  206. package/src/astro/bridge-runtime.tsx +79 -33
  207. package/src/astro/label-pack-loader.ts +12 -4
  208. package/src/astro/robots-renderer.ts +15 -15
  209. package/src/astro/website-page-head.renderer.ts +2 -2
  210. package/src/astro/website-site-context.ts +5 -2
  211. package/src/companion-exports.ts +8 -15
  212. package/src/config/__tests__/website-build-origins.test.ts +52 -0
  213. package/src/config/define-website-config.ts +10 -10
  214. package/src/config/load-website-config.ts +27 -15
  215. package/src/config/load-website-pricing-catalog.ts +52 -0
  216. package/src/config/load-website-site-context.ts +145 -0
  217. package/src/config/wildo-website-config.schemas.ts +10 -5
  218. package/src/config-loader.ts +2 -0
  219. package/src/core/__tests__/WebsitePageLayout.test.tsx +1 -1
  220. package/src/core/__tests__/contexts.test.tsx +20 -1
  221. package/src/core/__tests__/website-consent.test.tsx +312 -0
  222. package/src/core/anonymous-session/InboundContactForm.tsx +100 -45
  223. package/src/core/anonymous-session/__tests__/InboundContactForm.labels.test.tsx +108 -0
  224. package/src/core/anonymous-session/__tests__/InboundContactForm.submission-challenge.test.tsx +139 -0
  225. package/src/core/anonymous-session/__tests__/inbound-contact-form.schema.test.ts +11 -4
  226. package/src/core/anonymous-session/inbound-contact-form.schema.ts +30 -13
  227. package/src/core/anonymous-session/website-anonymous-session-client.ts +52 -3
  228. package/src/core/anonymous-session/website-submission-challenge.ts +99 -0
  229. package/src/core/consent/WebsiteConsentBanner.tsx +223 -0
  230. package/src/core/consent/WebsiteConsentContext.tsx +164 -0
  231. package/src/core/consent/WebsiteConsentGate.tsx +34 -0
  232. package/src/core/consent/useWebsiteConsent.ts +18 -0
  233. package/src/core/consent/website-consent-storage.ts +116 -0
  234. package/src/core/contexts/WebsiteRuntimeContext.tsx +33 -6
  235. package/src/core/contexts/useWebsiteRuntime.ts +33 -2
  236. package/src/core/external-providers/WebsiteProviderComponent.tsx +153 -0
  237. package/src/core/external-providers/__tests__/frontend-provider-registry.website.test.ts +4 -0
  238. package/src/core/external-providers/__tests__/website-provider-component.test.tsx +238 -0
  239. package/src/core/external-providers/__tests__/website-provider-sdks.test.tsx +72 -0
  240. package/src/core/external-providers/frontend-provider-registry.website.ts +1 -2
  241. package/src/core/external-providers/provider-component-registry.website.ts +115 -0
  242. package/src/core/external-providers/useWebsiteProviderComponent.ts +76 -0
  243. package/src/core/external-providers/useWebsiteProviderScripts.ts +114 -40
  244. package/src/core/external-providers/website-provider-sdk-activations.ts +23 -0
  245. package/src/core/factories/define-website-section.ts +3 -3
  246. package/src/core/layouts/WebsitePageLayout.tsx +75 -7
  247. package/src/index.ts +38 -65
  248. package/src/schemas/__tests__/design-token-css-property.test.ts +72 -0
  249. package/src/schemas/__tests__/pricing-offer-structured-data.test.ts +122 -0
  250. package/src/schemas/__tests__/structured-data-reconciliation.test.ts +35 -0
  251. package/src/schemas/design-tokens/website-design-tokens.shared.schemas.ts +45 -5
  252. package/src/schemas/label-keys/__tests__/scaffolded-consent-label-pack.parity.test.ts +83 -0
  253. package/src/schemas/label-keys/website-consent-label-keys.schemas.ts +78 -0
  254. package/src/schemas/label-keys/website-provider-component-label-keys.schemas.ts +40 -0
  255. package/src/schemas/manifests/website-root-config.shared.schemas.ts +10 -9
  256. package/src/schemas/sections/website-section-definition.shared.schemas.ts +14 -7
  257. package/src/schemas/structured-data/pricing-offer-structured-data.shared.ts +144 -0
  258. package/src/schemas/structured-data/structured-data-reconciliation.shared.ts +46 -1
  259. package/src/schemas/structured-data/website-structured-data.shared.schemas.ts +7 -5
  260. package/dist/esm/core/external-providers/provider-scripts.website.d.ts +0 -68
  261. package/dist/esm/core/external-providers/provider-scripts.website.d.ts.map +0 -1
  262. package/dist/esm/core/external-providers/provider-scripts.website.js +0 -99
  263. package/dist/esm/core/external-providers/provider-scripts.website.js.map +0 -1
  264. package/src/core/external-providers/__tests__/provider-scripts.website.test.ts +0 -146
  265. package/src/core/external-providers/provider-scripts.website.ts +0 -126
@@ -12,35 +12,68 @@
12
12
  * does not exist would throw, and starting a vendor SDK there would run a live
13
13
  * analytics client inside a build.
14
14
  *
15
- * SDK activation comes from `@wildo-ai/external-connectors-public/website`, which
16
- * this package now depends on. The script injector beside it does NOT yet, and
17
- * that is a timing artifact rather than a decision: it belongs in the same
18
- * package so the technical doc can use one copy — a second copy of a DOM
19
- * mutation eventually disagrees about idempotence, which is how a vendor tag
20
- * gets loaded twice. Moving it needs that package's `dist` to carry it, and no
21
- * watcher is running in this tree; consolidating it is a one-step follow-up, not
22
- * a design question.
15
+ * Both come from `@wildo-ai/external-connectors-public/website`. The script
16
+ * injector lives there rather than here so the SaaS application, the technical
17
+ * doc and this site run ONE copy of it: a second copy of a DOM mutation
18
+ * eventually disagrees about idempotence, which is how a vendor tag gets loaded
19
+ * twice. It first shipped here alone, and that is how the application came to
20
+ * declare its Drive Picker's Google API script and never inject it.
21
+ *
22
+ * ## Client-side injection, and the alternative that was rejected
23
+ *
24
+ * This is an Astro static site, so the "obvious" home for a third-party tag is
25
+ * the built HTML `<head>`. That was rejected for now: the set of live providers
26
+ * is per SERVICE and per ENVIRONMENT, resolved from the materialized artifact,
27
+ * while the built HTML is one artifact shared across deploys of the same build.
28
+ * Baking the tags in would make the build environment-specific — the same class
29
+ * of coupling the DATA channel exists to avoid. Injecting from the client keeps
30
+ * one build correct everywhere, at the cost of the tag arriving after hydration.
31
+ * Revisit if a provider ever needs to run before first paint.
23
32
  */
24
33
 
25
- import { useEffect } from 'react';
34
+ import { useEffect, useMemo, useRef, useState } from 'react';
26
35
  import {
27
36
  activateFrontendProviderSdks,
28
- type ActivatedFrontendProviderSdks,
29
- } from '@wildo-ai/external-connectors-public/website';
30
- import {
31
- collectDeclaredProviderScripts,
37
+ collectProviderScriptsWithoutComponents,
38
+ consentPurposesRequiredByProvider,
39
+ createFrontendProviderSdkActivationLane,
32
40
  injectProviderScripts,
33
- } from './provider-scripts.website';
34
- import { AppFrontendType } from '@wildo-ai/saas-models/public-runtime';
41
+ } from '@wildo-ai/external-connectors-public/website';
42
+ import { AppFrontendType, type ConsentPurpose } from '@wildo-ai/saas-models/public-runtime';
35
43
  import type { FrontendWebsiteProviderRegistry } from './frontend-provider-registry.website';
44
+ import type { WebsiteProviderComponentRegistry } from './provider-component-registry.website';
45
+ import type { WebsiteProviderSdkActivation } from './website-provider-sdk-activations';
46
+
47
+ /**
48
+ * A stable key for a granted-purpose set, so an effect re-runs when the SET changes and not when a new
49
+ * array with the same members is handed in.
50
+ */
51
+ function grantedKey(grantedConsentPurposes: readonly ConsentPurpose[]): string {
52
+ return [...grantedConsentPurposes].sort().join(',');
53
+ }
36
54
 
55
+ /**
56
+ * Injects the `<script>` tags of the live providers that rest on NO consent purpose and render no component.
57
+ *
58
+ * No consent input, deliberately (#1704): an executed `<script>` cannot be stopped, so a provider that rests on a
59
+ * purpose must ship as a stoppable SDK (started on the grant and stopped on withdrawal by
60
+ * {@link useWebsiteProviderSdks}); a consent-based module declaring `scripts` is refused at projection and skipped by
61
+ * the collector. No grant could change what this injects.
62
+ */
37
63
  export function useWebsiteProviderScripts(
38
64
  registry: FrontendWebsiteProviderRegistry | undefined,
65
+ /**
66
+ * The website's provider-component registry (#1703). A provider with a registered component loads its
67
+ * scripts WITH that component, when a page first shows it — so an address lookup's vendor loader is
68
+ * not downloaded on every page of the site, only on the one with the form. This hook injects the rest.
69
+ */
70
+ componentRegistry?: WebsiteProviderComponentRegistry,
39
71
  ): void {
40
72
  useEffect(() => {
41
73
  if (typeof document === 'undefined') return;
42
74
 
43
- const scripts = collectDeclaredProviderScripts(registry);
75
+ const withComponents = [...(componentRegistry?.providersWithComponents() ?? [])].map((providerRef) => ({ providerRef }));
76
+ const scripts = collectProviderScriptsWithoutComponents(registry?.modules.values() ?? [], withComponents);
44
77
  if (scripts.length === 0) return;
45
78
 
46
79
  injectProviderScripts(scripts, document);
@@ -48,7 +81,7 @@ export function useWebsiteProviderScripts(
48
81
  // work immediately; removing the element undoes neither, so a cleanup that
49
82
  // removed it would only make the next mount inject a SECOND copy of an
50
83
  // already-running script. Idempotence by `src` is the real guard.
51
- }, [registry]);
84
+ }, [registry, componentRegistry]);
52
85
  }
53
86
 
54
87
  /**
@@ -60,33 +93,74 @@ export function useWebsiteProviderScripts(
60
93
  */
61
94
  export function useWebsiteProviderSdks(
62
95
  registry: FrontendWebsiteProviderRegistry | undefined,
63
- ): void {
96
+ /**
97
+ * The purposes the visitor granted (#534). Providers are activated in TWO groups on purpose: those that
98
+ * rest on no purpose (error monitoring, captcha) are started once and never restarted by a consent
99
+ * change, while the consent-based ones start when their purpose is granted and are DEACTIVATED when it
100
+ * is withdrawn — which is the only way a withdrawal stops a running analytics client.
101
+ */
102
+ grantedConsentPurposes: readonly ConsentPurpose[],
103
+ ): ReadonlyArray<WebsiteProviderSdkActivation> {
104
+ const consentFree = useProviderSdkGroup(registry, false, []);
105
+ const consentBased = useProviderSdkGroup(registry, true, grantedConsentPurposes);
106
+ // The providers RUNNING on this page, both groups, published by the page layout (#1735): a form reads the captcha
107
+ // vendor's handle from here. Emptied the moment a group stops, so nothing holds a handle past a withdrawal.
108
+ return useMemo(() => [...consentFree, ...consentBased], [consentFree, consentBased]);
109
+ }
110
+
111
+ function useProviderSdkGroup(
112
+ registry: FrontendWebsiteProviderRegistry | undefined,
113
+ consentBased: boolean,
114
+ grantedConsentPurposes: readonly ConsentPurpose[],
115
+ ): ReadonlyArray<WebsiteProviderSdkActivation> {
116
+ const [running, setRunning] = useState<ReadonlyArray<WebsiteProviderSdkActivation>>([]);
117
+ // Keyed by the grants this group's providers actually rest on, so changing an unrelated purpose does
118
+ // not stop and restart a running analytics client.
119
+ const relevant = registry === undefined
120
+ ? []
121
+ : [...registry.modules.values()]
122
+ .filter((module) => (consentPurposesRequiredByProvider(module).length > 0) === consentBased)
123
+ .flatMap((module) => consentPurposesRequiredByProvider(module))
124
+ .filter((purpose) => grantedConsentPurposes.includes(purpose));
125
+ const key = grantedKey(relevant);
126
+ // One lane per group for the page's lifetime: a start waits for the previous stop.
127
+ const lane = useRef(createFrontendProviderSdkActivationLane());
64
128
  useEffect(() => {
65
129
  if (registry === undefined) return;
130
+ const modules = [...registry.modules.values()].filter(
131
+ (module) => (consentPurposesRequiredByProvider(module).length > 0) === consentBased,
132
+ );
133
+ if (modules.length === 0) return;
66
134
 
67
- let unmounted = false;
68
- let activated: ActivatedFrontendProviderSdks | undefined;
69
-
70
- void activateFrontendProviderSdks({
71
- surface: AppFrontendType.STATIC_WEBSITE,
72
- modules: registry.modules.values(),
73
- onDiagnostic: (diagnostic) => {
74
- // The website runtime has no logger service, and a provider that cannot
75
- // start is a developer-facing fact that must not be silent.
76
- // eslint-disable-next-line no-console
77
- console.warn(`[wildo-provider-sdk] ${diagnostic.kind}: ${diagnostic.message}`);
135
+ // Through the group's lane, so a start never overlaps the previous stop of the same page-global vendor (#1704): a
136
+ // visitor who grants, withdraws and grants again while the bundle downloads must not have the first run's stop
137
+ // switch off the second run's client. The stop also covers an unmount mid-download.
138
+ const stop = lane.current.start(
139
+ () => activateFrontendProviderSdks({
140
+ surface: AppFrontendType.STATIC_WEBSITE,
141
+ modules,
142
+ grantedConsentPurposes,
143
+ onDiagnostic: (diagnostic) => {
144
+ // The website runtime has no logger service, and a provider that cannot
145
+ // start is a developer-facing fact that must not be silent.
146
+ // eslint-disable-next-line no-console
147
+ console.warn(`[wildo-provider-sdk] ${diagnostic.kind}: ${diagnostic.message}`);
148
+ },
149
+ }),
150
+ (result) => {
151
+ setRunning(modules.flatMap((module) => {
152
+ const activation = result.activations.get(module.metadata.ref);
153
+ return activation === undefined
154
+ ? []
155
+ : [{ providerRef: module.metadata.ref, providerCapabilities: [...module.providerCapabilities], activation }];
156
+ }));
78
157
  },
79
- }).then((result) => {
80
- activated = result;
81
- // Activation is asynchronous and unmount is not: if the surface went away
82
- // while a vendor bundle was still downloading, stop what we just started
83
- // rather than leaving a client nobody can reach.
84
- if (unmounted) void result.deactivateAll();
85
- });
86
-
158
+ );
87
159
  return () => {
88
- unmounted = true;
89
- void activated?.deactivateAll();
160
+ setRunning([]);
161
+ stop();
90
162
  };
91
- }, [registry]);
163
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- keyed by the granted SET, see grantedKey.
164
+ }, [registry, consentBased, key]);
165
+ return running;
92
166
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The provider SDKs RUNNING on this website page (#1735), published by the page layout.
3
+ *
4
+ * A section reaches a running vendor through here rather than through a global — the contact form asks the consented
5
+ * captcha vendor for a token this way. The list is emptied the moment a provider is stopped (a consent withdrawal, a
6
+ * navigation), so a consumer that reads it at the moment it needs a handle never holds one past a withdrawal.
7
+ */
8
+ import { createContext, useContext } from 'react';
9
+ import type { ProviderFrontendSdkActivation } from '@wildo-ai/external-connectors-public/website';
10
+
11
+ /** One running provider: its ref, the capabilities it serves, and its activation (whose `handle` is the vendor's client). */
12
+ export interface WebsiteProviderSdkActivation {
13
+ readonly providerRef: string;
14
+ readonly providerCapabilities: ReadonlyArray<string>;
15
+ readonly activation: ProviderFrontendSdkActivation;
16
+ }
17
+
18
+ export const WebsiteProviderSdkActivationsContext = createContext<ReadonlyArray<WebsiteProviderSdkActivation>>([]);
19
+
20
+ /** The provider SDKs running on this page, in declaration order. Empty outside a page layout and before any start. */
21
+ export function useWebsiteProviderSdkActivations(): ReadonlyArray<WebsiteProviderSdkActivation> {
22
+ return useContext(WebsiteProviderSdkActivationsContext);
23
+ }
@@ -71,9 +71,9 @@ export interface TypedWebsiteSectionDefinition<TProps>
71
71
  * **Why a factory instead of constructing the literal directly**:
72
72
  * - Consumers don't have to remember to parse refs / keys
73
73
  * themselves before assembling the literal.
74
- * - The schema parse provides a single audited validation chokepoint
75
- * so `wildo website audit` (Phase 7) can re-validate definitions
76
- * without re-entering the React graph.
74
+ * - The schema parse provides a single audited validation chokepoint,
75
+ * and the parsed `expectedLabelKeys` are what `wildo website audit`
76
+ * checks every locale pack against, without re-entering the React graph.
77
77
  * - The factory is the documented authoring API the
78
78
  * `website_factory_authoring` skill targets — keeping the
79
79
  * authoring surface narrow lets the framework evolve the
@@ -1,14 +1,20 @@
1
1
  import React, { useMemo, type ReactNode } from 'react';
2
2
  import { BUILTIN_PROVIDER_CAPABILITY } from '@wildo-ai/saas-models/public-runtime';
3
+ import { consentPurposesInUseByProviders } from '@wildo-ai/external-connectors-public/website';
3
4
 
4
5
  import { WebsitePageContextProvider } from '../contexts/WebsitePageContext';
5
6
  import {
6
7
  useWebsiteProviderScripts,
7
8
  useWebsiteProviderSdks,
8
9
  } from '../external-providers/useWebsiteProviderScripts';
10
+ import { WebsiteProviderSdkActivationsContext } from '../external-providers/website-provider-sdk-activations';
9
11
  import type { WebsitePageContextValue } from '../contexts/WebsitePageContext';
10
12
  import { useWebsiteRuntime } from '../contexts/useWebsiteRuntime';
13
+ import { websiteDesignTokenCssVar } from '../../schemas/design-tokens/website-design-tokens.shared.schemas';
11
14
  import { useWebsiteLabelByKey } from '../hooks/useWebsiteLabel';
15
+ import { WebsiteConsentProvider, WebsiteConsentStatus, type WebsiteConsentRecorder } from '../consent/WebsiteConsentContext';
16
+ import { useWebsiteConsent } from '../consent/useWebsiteConsent';
17
+ import { WebsiteConsentBanner, WebsiteConsentSettingsButton } from '../consent/WebsiteConsentBanner';
12
18
  import {
13
19
  WebsitePageRefSchema,
14
20
  type WebsitePageRef,
@@ -96,10 +102,10 @@ const SKIP_LINK_FOCUS_CSS = `
96
102
  overflow: visible !important;
97
103
  clip: auto !important;
98
104
  white-space: normal !important;
99
- background: var(--website-colors-background, #fff) !important;
100
- color: var(--website-colors-foreground, #000) !important;
101
- border: 2px solid var(--website-colors-primary, #000) !important;
102
- border-radius: var(--website-radii-md, 0.375rem) !important;
105
+ background: ${websiteDesignTokenCssVar('colors', 'background', '#fff')} !important;
106
+ color: ${websiteDesignTokenCssVar('colors', 'foreground', '#000')} !important;
107
+ border: 2px solid ${websiteDesignTokenCssVar('colors', 'primary', '#000')} !important;
108
+ border-radius: ${websiteDesignTokenCssVar('radii', 'md', '0.375rem')} !important;
103
109
  z-index: 9999 !important;
104
110
  text-decoration: none !important;
105
111
  }
@@ -133,6 +139,14 @@ const SKIP_LINK_FOCUS_CSS = `
133
139
  * captcha so non-React tooling can detect the configured vendor
134
140
  * without reverse-engineering app code.
135
141
  *
142
+ * 4. **Provides the visitor's consent state and enforces it** (#534) —
143
+ * mounts `WebsiteConsentProvider`, starts a provider SDK or script only
144
+ * once the purpose it rests on is granted (and stops the SDK when it is
145
+ * withdrawn), and renders the consent panel when the site runs a
146
+ * consent-based provider and the visitor has not decided. Here rather
147
+ * than in each bridge because every page — including every blog post —
148
+ * renders through this layout, so no page can be mounted without it.
149
+ *
136
150
  * **Why semantic `<header>` / `<main>` / `<footer>` elements**:
137
151
  * marketing-site SEO scoring penalizes pages that ship a single
138
152
  * unstructured `<div>` tree. Using semantic landmarks costs nothing
@@ -178,6 +192,17 @@ export interface WebsitePageLayoutProps {
178
192
  * vertical-rhythm overrides on long-scroll landing pages.
179
193
  */
180
194
  mainClassName?: string;
195
+ /**
196
+ * OPTIONAL anonymous-session client. When supplied, a visitor's consent decision is ALSO recorded
197
+ * server-side against their session, which is what lets it follow them into the account they go on
198
+ * to create (#1258). Pass the SAME instance the page's capture sections use, so one `uuidRef`
199
+ * carries the decision, the lead and the draft note together.
200
+ *
201
+ * Absent keeps today's behaviour exactly: the decision lives in this browser and nowhere else.
202
+ * The engine cannot construct the client here — it needs the application's own
203
+ * `frontendServiceName` — which is why it arrives as a prop rather than off the runtime context.
204
+ */
205
+ consentRecorder?: WebsiteConsentRecorder;
181
206
  /**
182
207
  * The page's section composition. Typically a sequence of
183
208
  * `<WebsiteSection>` wrappers around concrete section components.
@@ -185,7 +210,21 @@ export interface WebsitePageLayoutProps {
185
210
  children: ReactNode;
186
211
  }
187
212
 
188
- export function WebsitePageLayout({
213
+ export function WebsitePageLayout(props: WebsitePageLayoutProps): ReactNode {
214
+ const runtime = useWebsiteRuntime();
215
+ // Derived from the LIVE providers, so the panel asks about exactly what this site processes.
216
+ const purposesInUse = useMemo(
217
+ () => consentPurposesInUseByProviders(runtime.frontendProviderRegistry?.modules.values()),
218
+ [runtime.frontendProviderRegistry],
219
+ );
220
+ return (
221
+ <WebsiteConsentProvider purposesInUse={purposesInUse} recorder={props.consentRecorder}>
222
+ <WebsitePageLayoutContent {...props} />
223
+ </WebsiteConsentProvider>
224
+ );
225
+ }
226
+
227
+ function WebsitePageLayoutContent({
189
228
  pageRef,
190
229
  header,
191
230
  footer,
@@ -194,6 +233,7 @@ export function WebsitePageLayout({
194
233
  children,
195
234
  }: WebsitePageLayoutProps): ReactNode {
196
235
  const runtime = useWebsiteRuntime();
236
+ const consent = useWebsiteConsent();
197
237
 
198
238
  /**
199
239
  * Defensive parse: callers should pass a `WebsitePageManifest.ref`
@@ -240,8 +280,12 @@ export function WebsitePageLayout({
240
280
  // resolved provider refs and stamped them into the attributes below and
241
281
  // stopped there — a provider could be declared, enabled and hydrated without
242
282
  // anything of it ever running.
243
- useWebsiteProviderScripts(runtime.frontendProviderRegistry);
244
- useWebsiteProviderSdks(runtime.frontendProviderRegistry);
283
+ //
284
+ // Consent-based providers start only once their purpose is granted (#534). Before the stored decision is
285
+ // read, nothing is granted — the static HTML and the first render start nothing optional.
286
+ useWebsiteProviderScripts(runtime.frontendProviderRegistry, runtime.providerComponentRegistry);
287
+ // The running SDKs are published to the page (#1735): the contact form asks the consented captcha vendor for a token.
288
+ const runningProviderSdks = useWebsiteProviderSdks(runtime.frontendProviderRegistry, consent.grantedPurposes);
245
289
 
246
290
  const providerRootAttributes = useMemo<Record<string, string>>(() => {
247
291
  const attrs: Record<string, string> = {};
@@ -289,6 +333,7 @@ export function WebsitePageLayout({
289
333
 
290
334
  return (
291
335
  <WebsitePageContextProvider value={pageContextValue}>
336
+ <WebsiteProviderSdkActivationsContext.Provider value={runningProviderSdks}>
292
337
  <div
293
338
  data-website-page-ref={safePageRef as unknown as string}
294
339
  className={className}
@@ -314,9 +359,32 @@ export function WebsitePageLayout({
314
359
  {children}
315
360
  </main>
316
361
  {resolvedFooter !== null ? <footer>{resolvedFooter}</footer> : null}
362
+ {renderConsentSurface(consent, runtime.consentBannerComponent)}
317
363
  </div>
364
+ </WebsiteProviderSdkActivationsContext.Provider>
318
365
  </WebsitePageContextProvider>
319
366
  );
320
367
  }
321
368
  WebsitePageLayout.displayName = 'WebsitePageLayout';
369
+
370
+ /**
371
+ * The panel while a decision is owed (or re-opened), the reopen button once one is made, and nothing at
372
+ * all on a site whose live providers rest on no consent. Nothing renders before the stored decision is
373
+ * read, so a visitor who already decided never sees the panel flash.
374
+ */
375
+ function renderConsentSurface(
376
+ consent: ReturnType<typeof useWebsiteConsent>,
377
+ BannerOverride: React.ComponentType | undefined,
378
+ ): ReactNode {
379
+ if (!consent.isResolved || consent.purposesInUse.length === 0) return null;
380
+ if (consent.status === WebsiteConsentStatus.UNDECIDED || consent.isSettingsOpen) {
381
+ const Banner = BannerOverride ?? WebsiteConsentBanner;
382
+ return <Banner />;
383
+ }
384
+ return (
385
+ <div data-website-consent-settings="">
386
+ <WebsiteConsentSettingsButton />
387
+ </div>
388
+ );
389
+ }
322
390
  /** @wildo_source:part:end saas.website.page-layout.component */
package/src/index.ts CHANGED
@@ -11,75 +11,33 @@
11
11
  * accompanying SKILLs in `framework-knowledge`.
12
12
  *
13
13
  * @wildo-boundary
14
- * - This barrel MAY re-export from any subdirectory of `src/`.
15
- * - Subdirectories MUST observe their own boundary contracts (see
16
- * each subdirectory's own `index.ts` `@wildo-boundary` block):
14
+ * This barrel re-exports the React-side surface: `schemas/`, `core/`,
15
+ * `components/`. The other entries are separate subpaths with their own
16
+ * audiences — `/astro` (Node, Astro frontmatter), `/astro-island` (the
17
+ * bridge factory), `/config` and `/config-loader` (Node), `/mdx`,
18
+ * `/companion` (React-free). Per directory:
17
19
  * - `schemas/` : NO React, NO Astro, NO `~wildo-frontend-lib/**`
18
- * - `seo/` : NO React, NO Astro
19
- * - `core/` : MAY use React; NO `~wildo-frontend-lib/**` (Q17=B)
20
- * - `astro/` : MAY use Astro; NO React component runtime imports
20
+ * - `core/`, `components/`: MAY use React; NO `~wildo-frontend-lib/**` (Q17=B)
21
+ * - `astro/` : MAY use Astro and Node; NO React component runtime imports
22
+ * - `config/` : Node-side loaders; never reachable from a React-island entry
21
23
  * - `mdx/` : MAY use `@mdx-js/*`
22
- * - `integration/` : MAY use React; MAY import the
23
- * `@wildo-ai/saas-models/public-runtime` SUBPATH,
24
- * the `@wildo-ai/presets-components-models/public-runtime`
25
- * SUBPATH, `@wildo-ai/zod-decorators`,
26
- * plus the SSR / Astro-middleware tier
27
- * (`axios`, `jsonwebtoken`, `socket.io-client`,
28
- * `reflect-metadata`) — D12 revised
29
- * 2026-04-19 (second same-day loosening);
30
- * the ROOT barrels of `@wildo-ai/saas-models`
31
- * and `@wildo-ai/presets-components-models`
32
- * are FORBIDDEN (they boot the decorator
33
- * framework / pull the heavy registry);
34
- * the saas-app DI / backend stack
35
- * (`inversify`, `mongoose`, `express`,
36
- * `pino`, `helmet`, `argon2`, `bcrypt`,
37
- * the `@wildo-ai/saas-frontend-lib` /
38
- * `@wildo-ai/saas-backend-lib` /
39
- * `@wildo-ai/external-connectors-models`
40
- * root + subpath surfaces) is FORBIDDEN;
41
- * MUST NOT import `~wildo-frontend-lib/**`
42
- * (Q17=B / D16)
43
24
  *
44
- * D12 (client-bundle import discipline, all `core/`/`schemas/`/`integration/`
45
- * files): the entire saas-website source tree ships into a browser bundle
46
- * (Astro SSG + React islands). Every shared model atom MUST come from a
47
- * side-effect-free `/public-runtime` subpath:
48
- * - `@wildo-ai/saas-models/public-runtime` is the primary door
49
- * (saas-models-public-runtime.md K-6 — consumers see one path).
50
- * - `@wildo-ai/presets-components-models/public-runtime` is the
51
- * internal seam for symbols not (yet) re-exported by saas-models.
52
- * - `@wildo-ai/zod-decorators` is allowed to import (first revision
53
- * 2026-04-19). The package itself is side-effect-free; only an
54
- * explicit `initZodDecorators()` call mutates `z.ZodType.prototype`,
55
- * which neither saas-website nor the saas-models /public-runtime
56
- * barrel does.
57
- * - `axios`, `jsonwebtoken`, `socket.io-client`, `reflect-metadata` are
58
- * allowed for SSR / Astro-middleware code paths (second same-day
59
- * revision 2026-04-19). The website engine needs them to provide
60
- * middleware utilities (HTTP proxying to the SaaS backend, JWT
61
- * verification of cross-frontend handoff codes, real-time push,
62
- * decorator-metadata reads). They are SSR-tier — discipline
63
- * (and a future strict/loose entry split for React islands vs
64
- * middleware) keeps them out of the React-island bundle.
25
+ * D12 (client-bundle import discipline): everything reachable from a
26
+ * React-island entry ships to the browser, so every shared model atom
27
+ * comes from a side-effect-free `/public-runtime` subpath —
28
+ * `@wildo-ai/saas-models/public-runtime` first,
29
+ * `@wildo-ai/presets-components-models/public-runtime` for symbols it does
30
+ * not re-export yet — and `@wildo-ai/zod-decorators` is allowed because
31
+ * only an explicit `initZodDecorators()` mutates Zod's prototype.
65
32
  *
66
- * The ROOT barrel of `@wildo-ai/saas-models` is forbidden everywhere in
67
- * this package — it calls `initZodDecorators()` at module load and pulls
68
- * `@wildo-ai/external-connectors-models` + the full schema graph into
69
- * the bundle. Same forbid applies to the root barrel of
70
- * `@wildo-ai/presets-components-models`, the saas-app DI runtime
71
- * (`inversify`, `@wildo-ai/saas-frontend-lib`, `@wildo-ai/saas-backend-lib`),
72
- * and pure-backend libs (`mongoose`, `express`, `pino`, `helmet`,
73
- * `argon2`, `bcrypt`). Enforced by `src/__tests__/bundle-isolation.test.ts`.
74
- *
75
- * These boundaries are enforced by review + the bundle-isolation test
76
- * above until Phase 8 promotes them to a programmatic ESLint check
77
- * (cf. `saas-website-slice.md`
78
- * §"DEFERRED to Phase 8 — boundary lint mechanism design").
79
- *
80
- * Phase 1+2 status: schemas + runtime contexts/hooks + layout
81
- * primitives + L1 placeholders + factories are all exported. Phase 5
82
- * adds the Astro adapter; Phase 5.7 the `integration/` layer.
33
+ * The ROOT barrels of `@wildo-ai/saas-models` and
34
+ * `@wildo-ai/presets-components-models` are forbidden everywhere in this
35
+ * package (they boot the decorator framework and pull the full schema
36
+ * graph), as are the saas-app DI runtime (`inversify`,
37
+ * `@wildo-ai/saas-frontend-lib`, `@wildo-ai/saas-backend-lib`) and
38
+ * pure-backend libs (`mongoose`, `express`, `pino`, `helmet`, `argon2`,
39
+ * `bcrypt`). Enforced by `src/__tests__/bundle-isolation.test.ts`, which
40
+ * walks every entry's import graph against an allow-list.
83
41
  *
84
42
  * D18 (L1 boundary, Phase 2): the L1 primitives (`WebsiteHeading`,
85
43
  * `WebsiteText`, `WebsiteButton`, `WebsiteLink`, `WebsiteImage`) are
@@ -95,10 +53,13 @@
95
53
  export * from './schemas/refs/page-ref.schemas';
96
54
  export * from './schemas/refs/section-ref.schemas';
97
55
  export * from './schemas/label-keys/website-label-key.schemas';
56
+ export * from './schemas/label-keys/website-consent-label-keys.schemas';
57
+ export * from './schemas/label-keys/website-provider-component-label-keys.schemas';
98
58
  export * from './schemas/sections/website-section-category.shared';
99
59
  export * from './schemas/sections/website-section-definition.shared.schemas';
100
60
  export * from './schemas/structured-data/website-structured-data.shared.schemas';
101
61
  export * from './schemas/structured-data/structured-data-reconciliation.shared';
62
+ export * from './schemas/structured-data/pricing-offer-structured-data.shared';
102
63
  export * from './schemas/design-tokens/website-design-tokens.shared.schemas';
103
64
  export * from './schemas/manifests/website-page-manifest.shared.schemas';
104
65
  export * from './schemas/manifests/website-root-config.shared.schemas';
@@ -118,9 +79,21 @@ export * from './core/factories/define-website-page-manifest';
118
79
  export * from './core/layouts/WebsitePageLayout';
119
80
  export * from './core/layouts/WebsiteSection';
120
81
 
82
+ export * from './core/consent/website-consent-storage';
83
+ export * from './core/consent/WebsiteConsentContext';
84
+ export * from './core/consent/useWebsiteConsent';
85
+ export * from './core/consent/WebsiteConsentGate';
86
+ export * from './core/consent/WebsiteConsentBanner';
87
+
88
+ export * from './core/external-providers/provider-component-registry.website';
89
+ export * from './core/external-providers/useWebsiteProviderComponent';
90
+ export * from './core/external-providers/WebsiteProviderComponent';
91
+ export * from './core/external-providers/website-provider-sdk-activations';
92
+
121
93
  export * from './core/anonymous-session/website-anonymous-session-client';
122
94
  export * from './core/anonymous-session/inbound-contact-form.schema';
123
95
  export * from './core/anonymous-session/InboundContactForm';
96
+ export * from './core/anonymous-session/website-submission-challenge';
124
97
 
125
98
  export * from './components/low-level/WebsiteHeading';
126
99
  export * from './components/low-level/WebsiteText';
@@ -0,0 +1,72 @@
1
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ import { renderWebsiteDesignTokensCss } from '../../astro/website-page-head.renderer';
7
+ import {
8
+ WebsiteDesignTokensSchema,
9
+ websiteDesignTokenCssProperty,
10
+ websiteDesignTokenCssVar,
11
+ type WebsiteDesignTokens,
12
+ } from '../design-tokens/website-design-tokens.shared.schemas';
13
+
14
+ const TOKENS: WebsiteDesignTokens = WebsiteDesignTokensSchema.parse({
15
+ colors: {
16
+ background: '#fff',
17
+ foreground: '#111',
18
+ primary: '#222',
19
+ primaryForeground: '#fff',
20
+ accent: '#333',
21
+ accentForeground: '#fff',
22
+ surface: '#eee',
23
+ surfaceForeground: '#444',
24
+ border: '#ccc',
25
+ muted: '#ddd',
26
+ mutedForeground: '#555',
27
+ },
28
+ radii: { none: '0', sm: '0.25rem', md: '0.5rem', lg: '0.75rem', xl: '1rem', full: '9999px' },
29
+ });
30
+
31
+ const SOURCE_ROOT = join(__dirname, '..', '..');
32
+
33
+ function sourceFiles(dir: string): string[] {
34
+ return readdirSync(dir).flatMap((entry) => {
35
+ const full = join(dir, entry);
36
+ if (statSync(full).isDirectory()) return entry === '__tests__' ? [] : sourceFiles(full);
37
+ return /\.(ts|tsx)$/.test(entry) ? [full] : [];
38
+ });
39
+ }
40
+
41
+ describe('design-token CSS property naming', () => {
42
+ it('the renderer emits every token under the name the readers reference', () => {
43
+ const css = renderWebsiteDesignTokensCss(TOKENS);
44
+ for (const [group, values] of Object.entries(TOKENS)) {
45
+ for (const token of Object.keys(values)) {
46
+ expect(css).toContain(`${websiteDesignTokenCssProperty(group, token)}:`);
47
+ }
48
+ }
49
+ // The key is verbatim: the reader the consent banner used to be spelled `surface-foreground`.
50
+ expect(css).toContain('--website-colors-surfaceForeground: #444;');
51
+ expect(websiteDesignTokenCssVar('colors', 'surfaceForeground', '#000')).toBe('var(--website-colors-surfaceForeground, #000)');
52
+ expect(websiteDesignTokenCssVar('radii', 'md')).toBe('var(--website-radii-md)');
53
+ });
54
+
55
+ it('no engine source spells a token custom property by hand', () => {
56
+ // Positive control: the walk reaches the component that used to carry the defect.
57
+ const files = sourceFiles(SOURCE_ROOT);
58
+ expect(files.some((file) => file.endsWith('WebsiteConsentBanner.tsx'))).toBe(true);
59
+
60
+ const offenders = files
61
+ .filter((file) => !file.endsWith('website-design-tokens.shared.schemas.ts'))
62
+ .flatMap((file) =>
63
+ readFileSync(file, 'utf8')
64
+ .split('\n')
65
+ .map((line, index) => ({ line, index }))
66
+ // A `var(--website-…)` in code, not in a comment explaining the naming rule.
67
+ .filter(({ line }) => /var\(--website-/.test(line) && !/^\s*(\*|\/\/)/.test(line))
68
+ .map(({ index }) => `${relative(SOURCE_ROOT, file)}:${index + 1}`),
69
+ );
70
+ expect(offenders).toEqual([]);
71
+ });
72
+ });