@designcrowd/library.brand-page 6.2.11-builder-v2-1 → 6.2.12

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 (579) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-Dg2pOwbr.js → CreatedOnBrandCrowd-de-de-BtVKllNQ.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-BILs_hsB.js → CreatedOnBrandCrowd-en-us-CX5V0v8u.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-VpxQ0N5F.js → CreatedOnBrandCrowd-es-es-CbA02PCL.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-CFHWRHzi.js → CreatedOnBrandCrowd-fr-ca-BQAKruBF.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-CFHWRHzi.js → CreatedOnBrandCrowd-fr-fr-BQAKruBF.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-CZfs45s9.js → CreatedOnBrandCrowd-pt-br-Yk_yzB3s.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-CZfs45s9.js → CreatedOnBrandCrowd-pt-pt-Yk_yzB3s.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-DYMQ6sIF.js → CreatedOnDcom-de-de-DJZQbPSD.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-Dj5sdPid.js → CreatedOnDcom-en-us-BJvNAa4l.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-Cx9yNTro.js → CreatedOnDcom-es-es-CUwB1FRY.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-Bzr0SjGq.js → CreatedOnDcom-fr-ca-CK54U4oh.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-BgqKY35F.js → CreatedOnDcom-fr-fr-DuUNBGrM.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-CXK6qfvL.js → CreatedOnDcom-pt-br-BfODpFNV.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-B8SggNTQ.js → CreatedOnDcom-pt-pt-Cf6u2GCg.js} +1 -1
  15. package/lib/src/brand-page/__fixtures__/production-text-style-resolver.fixture.d.ts +16 -0
  16. package/lib/src/brand-page/__fixtures__/production-text-style-resolver.fixture.js +35 -0
  17. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.d.ts +3 -0
  18. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +19 -13
  19. package/lib/src/brand-page/components/mailing-list/MailingList.mixin.d.ts +3 -1
  20. package/lib/src/brand-page/components/mailing-list/MailingList.mixin.js +17 -2
  21. package/lib/src/brand-page/components/navigation/button-carousel/ButtonCarouselNavigation.mixin.d.ts +11 -3
  22. package/lib/src/brand-page/components/navigation/button-carousel/ButtonCarouselNavigation.mixin.js +35 -1
  23. package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.d.ts +8 -2
  24. package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.js +27 -3
  25. package/lib/src/brand-page/components/page/Page.mixin.d.ts +5 -2
  26. package/lib/src/brand-page/components/page/Page.mixin.js +33 -27
  27. package/lib/src/brand-page/components/page-footer/PageFooter.mixin.d.ts +7 -2
  28. package/lib/src/brand-page/components/page-footer/PageFooter.mixin.js +21 -1
  29. package/lib/src/brand-page/components/payment/Payment.mixin.d.ts +5 -2
  30. package/lib/src/brand-page/components/payment/Payment.mixin.js +22 -2
  31. package/lib/src/brand-page/components/payment/support-me/SupportMe.mixin.d.ts +6 -2
  32. package/lib/src/brand-page/components/payment/support-me/SupportMe.mixin.js +32 -1
  33. package/lib/src/brand-page/components/social-links/SocialLinks.stories.js +0 -3
  34. package/lib/src/brand-page/components/testimonial-section/__fixtures__/data.js +0 -3
  35. package/lib/src/brand-page/components/text/Text.stories.js +0 -17
  36. package/lib/src/brand-page/constants/font.constants.d.ts +62 -0
  37. package/lib/src/brand-page/constants/font.constants.js +35 -0
  38. package/lib/src/brand-page/models/brand-page-context.model.d.ts +2 -0
  39. package/lib/src/brand-page/models/brand-page-context.model.js +7 -0
  40. package/lib/src/brand-page/models/index.d.ts +23 -24
  41. package/lib/src/brand-page/models/index.js +20 -7
  42. package/lib/src/brand-page/models/navigation-styles.model.d.ts +1 -1
  43. package/lib/src/brand-page/models/navigation-styles.model.js +17 -8
  44. package/lib/src/brand-page/models/payment.model.d.ts +11 -6
  45. package/lib/src/brand-page/models/styles.model.d.ts +53 -7
  46. package/lib/src/brand-page/models/styles.model.js +115 -27
  47. package/lib/src/brand-page/utils/color-palette.util.d.ts +43 -23
  48. package/lib/src/brand-page/utils/color-palette.util.js +65 -27
  49. package/lib/src/brand-page/utils/color.util.d.ts +13 -9
  50. package/lib/src/brand-page/utils/color.util.js +56 -15
  51. package/lib/src/brand-page/utils/container-text-color.util.d.ts +46 -13
  52. package/lib/src/brand-page/utils/container-text-color.util.js +321 -41
  53. package/lib/src/brand-page/utils/index.d.ts +1 -0
  54. package/lib/src/brand-page/utils/index.js +1 -0
  55. package/lib/src/brand-page/utils/migration/migration-20.js +6 -2
  56. package/lib/src/brand-page/utils/migration/migration-21.js +34 -6
  57. package/lib/src/brand-page/utils/migration/migration-22.d.ts +99 -0
  58. package/lib/src/brand-page/utils/migration/migration-22.js +909 -0
  59. package/lib/src/brand-page/utils/migration.util.js +17 -0
  60. package/lib/src/brand-page/utils/model.util.d.ts +4 -0
  61. package/lib/src/brand-page/utils/model.util.js +4 -0
  62. package/lib/src/brand-page/utils/personalisation.util.js +9 -1
  63. package/lib/src/brand-page/utils/rte-role-lift.util.d.ts +72 -0
  64. package/lib/src/brand-page/utils/rte-role-lift.util.js +275 -0
  65. package/lib/src/brand-page/utils/style.util.d.ts +5 -1
  66. package/lib/src/brand-page/utils/style.util.js +34 -11
  67. package/lib/src/brand-page/utils/text-role-css.util.d.ts +19 -0
  68. package/lib/src/brand-page/utils/text-role-css.util.js +186 -0
  69. package/lib/src/brand-page/utils/text-role-reconnect.util.d.ts +90 -0
  70. package/lib/src/brand-page/utils/text-role-reconnect.util.js +434 -0
  71. package/lib/src/brand-page/utils/text-style.util.d.ts +53 -23
  72. package/lib/src/brand-page/utils/text-style.util.js +122 -22
  73. package/lib/src/brand-page/utils/view-model.util.d.ts +9 -1
  74. package/lib/src/brand-page/utils/view-model.util.js +124 -111
  75. package/lib/src/brand-page/view-models.d.ts +1 -0
  76. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +4 -1
  77. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +34 -2
  78. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.js +4 -4
  79. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMakerTemplateSettings.mixin.d.ts +4 -1
  80. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.js +0 -10
  81. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/content-settings-items.fixture.js +0 -1
  82. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +44 -138
  83. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.js +0 -28
  84. package/lib/src/brand-page-maker/components/brandpage-settings/ReplaceImageSettingsForBrandPageSettings.mixin.d.ts +4 -1
  85. package/lib/src/brand-page-maker/components/navigation/Navigation.mixin.d.ts +1 -1
  86. package/lib/src/brand-page-maker/components/preview/Preview.mixin.d.ts +7 -0
  87. package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.d.ts +21 -0
  88. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +25 -1
  89. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +25 -1
  90. package/lib/src/brand-page-maker/components/seo-settings/UseExistingDomainModal.mixin.d.ts +21 -0
  91. package/lib/src/brand-page-maker/components/social-share/SocialShare.mixin.d.ts +4 -1
  92. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +25 -1
  93. package/lib/src/brand-page-maker/components/upgrade-to-premium-button/UpgradeToPremiumButton.mixin.d.ts +21 -0
  94. package/lib/src/brand-page-maker/composables/ai-chat/ai-builder-experiment.d.ts +10 -0
  95. package/lib/src/brand-page-maker/composables/ai-chat/ai-builder-experiment.js +11 -0
  96. package/lib/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.d.ts +1 -0
  97. package/lib/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.js +1 -0
  98. package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +14 -0
  99. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/RoleTypographyFill.d.ts +16 -0
  100. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/RoleTypographyFill.js +61 -0
  101. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/RteClipboard.d.ts +11 -0
  102. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/RteClipboard.js +236 -0
  103. package/lib/src/brand-page-maker/composables/rich-text-editor/font.d.ts +2 -1
  104. package/lib/src/brand-page-maker/composables/rich-text-editor/font.js +25 -6
  105. package/lib/src/brand-page-maker/composables/rich-text-editor/formatting.d.ts +17 -4
  106. package/lib/src/brand-page-maker/composables/rich-text-editor/formatting.js +204 -17
  107. package/lib/src/brand-page-maker/composables/rich-text-editor/hyperlink.d.ts +1 -1
  108. package/lib/src/brand-page-maker/composables/rich-text-editor/hyperlink.js +29 -31
  109. package/lib/src/brand-page-maker/composables/rich-text-editor/keepMarksOnEmpty.d.ts +20 -0
  110. package/lib/src/brand-page-maker/composables/rich-text-editor/keepMarksOnEmpty.js +60 -0
  111. package/lib/src/brand-page-maker/composables/rich-text-editor/spacing.d.ts +2 -1
  112. package/lib/src/brand-page-maker/composables/rich-text-editor/spacing.js +22 -6
  113. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +10 -4
  114. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +319 -275
  115. package/lib/src/brand-page-maker/constants/config-settings-constant.js +1 -5
  116. package/lib/src/brand-page-maker/convertors/text-css-style-options.convertor.js +6 -2
  117. package/lib/src/brand-page-maker/convertors/text-size-value.convertor.d.ts +1 -1
  118. package/lib/src/brand-page-maker/convertors/text-size-value.convertor.js +3 -1
  119. package/lib/src/brand-page-maker/default-getters/payment-text-and-border-color.default-getter.js +7 -14
  120. package/lib/src/brand-page-maker/default-getters/text-color.default-getter.js +6 -13
  121. package/lib/src/brand-page-maker/models/content-settings.d.ts +8 -0
  122. package/lib/src/brand-page-maker/models/content-settings.js +112 -81
  123. package/lib/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store-utils.d.ts +2 -2
  124. package/lib/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store-utils.js +5 -2
  125. package/lib/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store.js +4 -3
  126. package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +1257 -117
  127. package/lib/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.d.ts +9 -2
  128. package/lib/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.js +20 -0
  129. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.d.ts +21 -0
  130. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.d.ts +6 -1
  131. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +17 -9
  132. package/lib/src/brand-page-maker/stores/history/history-store.d.ts +59 -5
  133. package/lib/src/brand-page-maker/stores/history/history-store.js +22 -2
  134. package/lib/src/brand-page-maker/utils/apply-color-strategy.js +26 -8
  135. package/lib/src/brand-page-maker/utils/index.d.ts +2 -1
  136. package/lib/src/brand-page-maker/utils/index.js +2 -1
  137. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +117 -15
  138. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +435 -281
  139. package/lib/src/brand-page-maker/utils/rte-authoring.util.d.ts +4 -0
  140. package/lib/src/brand-page-maker/utils/rte-authoring.util.js +101 -0
  141. package/lib/src/brand-page-maker/utils/rte-clipboard.util.d.ts +126 -0
  142. package/lib/src/brand-page-maker/utils/rte-clipboard.util.js +307 -0
  143. package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +13 -3
  144. package/lib/src/{bundled-translations-zfiA5Tsw.js → bundled-translations-yGrvPE5i.js} +17 -17
  145. package/lib/src/{bundled-translations.de-DE-ChsdSyAP.js → bundled-translations.de-DE-L4Znf8Hs.js} +17 -17
  146. package/lib/src/{bundled-translations.es-ES-DflsWQK0.js → bundled-translations.es-ES-D6iticrK.js} +15 -15
  147. package/lib/src/{bundled-translations.fr-CA-fpDRuZ52.js → bundled-translations.fr-CA-BgAVE-9k.js} +25 -25
  148. package/lib/src/{bundled-translations.fr-FR-Bub73Zt-.js → bundled-translations.fr-FR-B8GbyqxQ.js} +17 -17
  149. package/lib/src/{bundled-translations.pt-BR-BlnWE9TB.js → bundled-translations.pt-BR-DjPU3Db7.js} +25 -25
  150. package/lib/src/{bundled-translations.pt-PT-DmzXlnaS.js → bundled-translations.pt-PT-BaepaDpr.js} +23 -23
  151. package/lib/src/bundles/bundled-translations.de-DE.json +21 -2
  152. package/lib/src/bundles/bundled-translations.es-ES.json +21 -2
  153. package/lib/src/bundles/bundled-translations.fr-CA.json +21 -2
  154. package/lib/src/bundles/bundled-translations.fr-FR.json +21 -2
  155. package/lib/src/bundles/bundled-translations.json +21 -2
  156. package/lib/src/bundles/bundled-translations.pt-BR.json +21 -2
  157. package/lib/src/bundles/bundled-translations.pt-PT.json +21 -2
  158. package/lib/src/clients/ai-assistant-client.d.ts +7 -1
  159. package/lib/src/clients/ai-assistant-client.js +7 -15
  160. package/lib/src/composables/useTranslate.d.ts +1 -1
  161. package/lib/src/composables/useTranslate.js +1 -1
  162. package/lib/src/css/library.brand-page-brandCrowd.min.css +284 -188
  163. package/lib/src/css/library.brand-page-brandPage.min.css +284 -188
  164. package/lib/src/css/library.brand-page-designCom.min.css +284 -188
  165. package/lib/src/css/library.brand-page-preview.min.css +284 -188
  166. package/lib/src/index.d.ts +2 -1
  167. package/lib/src/index.mjs +195 -191
  168. package/lib/src/{lib-7gtW85nE.js → lib-kGRlD3_P.js} +39082 -36131
  169. package/lib/src/lib.js +2 -1
  170. package/lib/src/metadata-builder/builders/footer-builder.js +4 -5
  171. package/lib/src/metadata-builder/builders/global-role-propagation.d.ts +58 -0
  172. package/lib/src/metadata-builder/builders/global-role-propagation.js +246 -0
  173. package/lib/src/metadata-builder/builders/global-styles-builder.d.ts +1 -0
  174. package/lib/src/metadata-builder/builders/global-styles-builder.js +116 -45
  175. package/lib/src/metadata-builder/builders/header-builder.js +13 -4
  176. package/lib/src/metadata-builder/builders/section-builders/about-section-builder.js +3 -3
  177. package/lib/src/metadata-builder/builders/section-builders/banner-section-builder.js +3 -3
  178. package/lib/src/metadata-builder/builders/section-builders/booking-section-builder.js +3 -3
  179. package/lib/src/metadata-builder/builders/section-builders/contact-form-builder.js +4 -4
  180. package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +3 -3
  181. package/lib/src/metadata-builder/builders/section-builders/mailing-list-builder.js +2 -2
  182. package/lib/src/metadata-builder/builders/section-builders/mailing-list-section-builder.js +2 -2
  183. package/lib/src/metadata-builder/builders/section-builders/profile-section-builder.js +4 -4
  184. package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +3 -3
  185. package/lib/src/metadata-builder/builders/section-builders/social-links-builder.js +8 -2
  186. package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +2 -2
  187. package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +60 -29
  188. package/lib/src/metadata-builder/builders/section-field-maps.js +1 -0
  189. package/lib/src/metadata-builder/builders/section-field-utils.d.ts +48 -19
  190. package/lib/src/metadata-builder/builders/section-field-utils.js +128 -52
  191. package/lib/src/metadata-builder/builders/section-rules/global-styles.rules.d.ts +4 -2
  192. package/lib/src/metadata-builder/builders/section-rules/global-styles.rules.js +28 -12
  193. package/lib/src/metadata-builder/builders/section-rules/social-links.rules.js +5 -1
  194. package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +1 -1
  195. package/lib/src/metadata-builder/helpers/rte.helpers.js +97 -40
  196. package/lib/src/metadata-builder/metadata-executor.js +23 -1
  197. package/lib/src/metadata-builder/models.d.ts +2 -0
  198. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +18 -19
  199. package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +18 -1
  200. package/lib/src/metadata-builder/sanitizers/rte-schema-check.js +1 -1
  201. package/lib/src/metadata-builder/sanitizers/sanitizer-rules.d.ts +0 -1
  202. package/lib/src/metadata-builder/sanitizers/sanitizer-rules.js +5 -8
  203. package/lib/src/metadata-builder/sanitizers/section-args-validator.d.ts +7 -16
  204. package/lib/src/metadata-builder/sanitizers/section-args-validator.js +95 -22
  205. package/lib/src/shared/color-palette/section-color-matches.js +9 -25
  206. package/lib/src/shared/color-strategies/color-strategies.d.ts +15 -0
  207. package/lib/src/shared/color-strategies/color-strategies.js +31 -0
  208. package/lib/src/shared/color-strategies/rte-color.d.ts +8 -0
  209. package/lib/src/shared/color-strategies/rte-color.js +16 -0
  210. package/lib/src/shared/color-strategies/rte-field-pairs.d.ts +23 -0
  211. package/lib/src/shared/color-strategies/rte-field-pairs.js +28 -0
  212. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +9 -135
  213. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.js +1 -27
  214. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +12 -2
  215. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +7 -0
  216. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +75 -6
  217. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +18 -7
  218. package/lib/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.d.ts +6 -1
  219. package/lib/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.js +0 -7
  220. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +22 -0
  221. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +8 -4
  222. package/lib/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.d.ts +1255 -26
  223. package/lib/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.js +203 -59
  224. package/lib/src/shared/content-settings/store/content-settings-store.d.ts +6 -0
  225. package/lib/src/shared/content-settings/store/content-settings-store.js +105 -8
  226. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +26 -1
  227. package/lib/src/shared/navigation-style-settings/store/index.d.ts +7 -3
  228. package/lib/src/shared/navigation-style-settings/store/index.js +40 -15
  229. package/lib/src/shared/page-settings/components/new-page-modal/NewPageModal.mixin.d.ts +1 -0
  230. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +38 -5
  231. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +91 -13
  232. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.js +10 -2
  233. package/lib/src/shared/page-settings/store/page-settings-store.d.ts +2 -1
  234. package/lib/src/shared/page-settings/store/page-settings-store.js +17 -0
  235. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +32 -1
  236. package/lib/src/shared/rte-extensions/AutoLink.d.ts +17 -0
  237. package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/AutoLink.js +67 -51
  238. package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/MaxLength.js +4 -1
  239. package/lib/src/shared/rte-extensions/TextEffects.d.ts +28 -0
  240. package/lib/src/shared/rte-extensions/TextEffects.js +96 -0
  241. package/lib/src/shared/rte-extensions/ViewDomParser.d.ts +2 -0
  242. package/lib/src/shared/rte-extensions/ViewDomParser.js +21 -0
  243. package/lib/src/shared/rte-extensions.util.d.ts +25 -0
  244. package/lib/src/{brand-page-maker/composables/rich-text-editor/getRteExtensions.js → shared/rte-extensions.util.js} +62 -7
  245. package/lib/src/shared/rte-link.util.d.ts +1 -0
  246. package/lib/src/shared/rte-link.util.js +1 -0
  247. package/lib/src/shared/rte-render.util.d.ts +112 -0
  248. package/lib/src/shared/rte-render.util.js +255 -0
  249. package/lib/src/shared/rte-shape.util.d.ts +6 -0
  250. package/lib/src/shared/rte-shape.util.js +18 -0
  251. package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +23 -1
  252. package/lib/src/shared/side-bar/components/SideBarTabButton.mixin.d.ts +1 -0
  253. package/lib/src/shared/side-bar/store/side-bar-store.d.ts +2 -0
  254. package/lib/src/shared/side-bar/store/side-bar-store.js +9 -0
  255. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +176 -15
  256. package/lib/src/shared/style-settings/components/StyleSettings.mixin.js +7 -1
  257. package/lib/src/shared/style-settings/store/style-settings-store.d.ts +1 -9
  258. package/lib/src/shared/style-settings/store/style-settings-store.js +3 -33
  259. package/lib/src/shared/style-settings/text-style-size-order.d.ts +2 -0
  260. package/lib/src/shared/style-settings/text-style-size-order.js +13 -0
  261. package/lib/src/shared/text-role-typography.util.d.ts +64 -0
  262. package/lib/src/shared/text-role-typography.util.js +183 -0
  263. package/lib/src/utils/minify-brand-page-metadata.d.ts +16 -1
  264. package/lib/src/utils/minify-brand-page-metadata.js +31 -9
  265. package/package.json +1 -1
  266. package/src/brand-page/__fixtures__/migration-22-v21/edited-brandsite-sections.v21.json +982 -0
  267. package/src/brand-page/__fixtures__/migration-22-v21/new-brandcontact-every-section.v21.json +1231 -0
  268. package/src/brand-page/__fixtures__/migration-22-v21/new-brandpage-every-section.v21.json +1231 -0
  269. package/src/brand-page/__fixtures__/migration-22-v21/new-brandsite-every-section.v21.json +1243 -0
  270. package/src/brand-page/__fixtures__/migration-22-v21/real-brandcontact.v21.json +755 -0
  271. package/src/brand-page/__fixtures__/migration-22-v21/real-brandpage-general-link.v21.json +527 -0
  272. package/src/brand-page/__fixtures__/migration-22-v21/real-brandpage-links.v21.json +755 -0
  273. package/src/brand-page/__fixtures__/migration-22-v21/real-brandpage-social-links.v21.json +514 -0
  274. package/src/brand-page/__fixtures__/migration-22-v21/real-brandsite-about.v21.json +459 -0
  275. package/src/brand-page/__fixtures__/migration-22-v21/real-brandsite-footer.v21.json +469 -0
  276. package/src/brand-page/__fixtures__/migration-22-v21/real-brandsite-header-banner.v21.json +481 -0
  277. package/src/brand-page/__fixtures__/migration-22-v21/real-brandsite-sections.v21.json +800 -0
  278. package/src/brand-page/__fixtures__/production-text-style-resolver.fixture.ts +52 -0
  279. package/src/brand-page/components/about-section/AboutSection.vue.test.ts +0 -10
  280. package/src/brand-page/components/contact-form/ContactForm.mixin.test.ts +127 -0
  281. package/src/brand-page/components/contact-form/ContactForm.mixin.ts +31 -14
  282. package/src/brand-page/components/contact-form/ContactForm.vue +26 -12
  283. package/src/brand-page/components/contact-form/ContactForm.vue.test.ts +60 -0
  284. package/src/brand-page/components/mailing-list/MailingList.mixin.test.ts +92 -0
  285. package/src/brand-page/components/mailing-list/MailingList.mixin.ts +28 -3
  286. package/src/brand-page/components/mailing-list/MailingList.vue +9 -4
  287. package/src/brand-page/components/mailing-list/MailingList.vue.test.ts +63 -0
  288. package/src/brand-page/components/navigation/button-carousel/ButtonCarouselNavigation.mixin.ts +51 -1
  289. package/src/brand-page/components/navigation/button-carousel/ButtonCarouselNavigation.test.ts +114 -0
  290. package/src/brand-page/components/navigation/button-carousel/ButtonCarouselNavigation.vue +8 -2
  291. package/src/brand-page/components/navigation/navigation-carousel/NavigationCarousel.test.ts +182 -0
  292. package/src/brand-page/components/navigation/navigation-carousel/NavigationCarousel.vue +58 -12
  293. package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.ts +40 -3
  294. package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue +15 -7
  295. package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue.test.ts +133 -3
  296. package/src/brand-page/components/page/Page.mixin.ts +41 -34
  297. package/src/brand-page/components/page/Page.vue +17 -6
  298. package/src/brand-page/components/page/Page.vue.test.ts +91 -1
  299. package/src/brand-page/components/page-footer/PageFooter.mixin.ts +32 -2
  300. package/src/brand-page/components/page-footer/PageFooter.test.ts +79 -0
  301. package/src/brand-page/components/page-footer/PageFooter.vue +12 -4
  302. package/src/brand-page/components/payment/Payment.mixin.ts +34 -2
  303. package/src/brand-page/components/payment/Payment.test.ts +188 -0
  304. package/src/brand-page/components/payment/Payment.vue +12 -5
  305. package/src/brand-page/components/payment/RequestPaymentStripe.vue +1 -0
  306. package/src/brand-page/components/payment/support-me/SupportMe.mixin.ts +49 -2
  307. package/src/brand-page/components/payment/support-me/SupportMe.test.ts +100 -0
  308. package/src/brand-page/components/payment/support-me/SupportMe.vue +7 -1
  309. package/src/brand-page/components/payment/support-me/SupportMe.vue.test.ts +66 -0
  310. package/src/brand-page/components/save-contact/SaveContactWidget.test.ts +100 -0
  311. package/src/brand-page/components/save-contact/SaveContactWidget.vue +6 -3
  312. package/src/brand-page/components/shop/PaymentModal.vue +2 -6
  313. package/src/brand-page/components/shop/PaymentShippingAddress.vue +6 -0
  314. package/src/brand-page/components/shop/Shop.vue +15 -2
  315. package/src/brand-page/components/shop/Shop.vue.test.ts +74 -2
  316. package/src/brand-page/components/social-links/SocialLinks.stories.ts +0 -3
  317. package/src/brand-page/components/social-links/SocialLinks.vue.test.ts +0 -3
  318. package/src/brand-page/components/testimonial-section/__fixtures__/data.ts +0 -3
  319. package/src/brand-page/components/text/Text.stories.ts +0 -17
  320. package/src/brand-page/components/text-wrapper/TextWrapper.parity.test.ts +136 -0
  321. package/src/brand-page/components/text-wrapper/TextWrapper.test.ts +358 -0
  322. package/src/brand-page/components/text-wrapper/TextWrapper.vue +82 -22
  323. package/src/brand-page/constants/font.constants.ts +37 -0
  324. package/src/brand-page/models/brand-page-context.model.ts +13 -0
  325. package/src/brand-page/models/footer-icon-color.test.ts +22 -0
  326. package/src/brand-page/models/index.ts +50 -28
  327. package/src/brand-page/models/navigation-styles.model.test.ts +77 -1
  328. package/src/brand-page/models/navigation-styles.model.ts +26 -9
  329. package/src/brand-page/models/payment.model.ts +15 -6
  330. package/src/brand-page/models/styles.model.test.ts +104 -0
  331. package/src/brand-page/models/styles.model.ts +181 -47
  332. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +619 -115
  333. package/src/brand-page/utils/__snapshots__/migration2.util.test.ts.snap +150 -9
  334. package/src/brand-page/utils/__snapshots__/model.util.test.ts.snap +300 -18
  335. package/src/brand-page/utils/color-palette.util.test.ts +126 -1
  336. package/src/brand-page/utils/color-palette.util.ts +92 -26
  337. package/src/brand-page/utils/color.util.test.ts +62 -29
  338. package/src/brand-page/utils/color.util.ts +77 -14
  339. package/src/brand-page/utils/container-text-color.util.test.ts +672 -26
  340. package/src/brand-page/utils/container-text-color.util.ts +472 -46
  341. package/src/brand-page/utils/index.ts +1 -0
  342. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/edited-brandsite-sections.v22.json +1191 -0
  343. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/new-brandcontact-every-section.v22.json +2026 -0
  344. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/new-brandpage-every-section.v22.json +2017 -0
  345. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/new-brandsite-every-section.v22.json +2031 -0
  346. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandcontact.v22.json +1145 -0
  347. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandpage-general-link.v22.json +645 -0
  348. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandpage-links.v22.json +1136 -0
  349. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandpage-social-links.v22.json +662 -0
  350. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandsite-about.v22.json +638 -0
  351. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandsite-footer.v22.json +610 -0
  352. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandsite-header-banner.v22.json +660 -0
  353. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandsite-sections.v22.json +1132 -0
  354. package/src/brand-page/utils/migration/migration-01.ts +293 -237
  355. package/src/brand-page/utils/migration/migration-02.ts +8 -1
  356. package/src/brand-page/utils/migration/migration-20.ts +6 -3
  357. package/src/brand-page/utils/migration/migration-20_5.test.ts +5 -2
  358. package/src/brand-page/utils/migration/migration-21.test.ts +34 -0
  359. package/src/brand-page/utils/migration/migration-21.ts +46 -10
  360. package/src/brand-page/utils/migration/migration-22.fixed-input.test.ts +43 -0
  361. package/src/brand-page/utils/migration/migration-22.test.ts +4103 -0
  362. package/src/brand-page/utils/migration/migration-22.text-font-family.test.ts +72 -0
  363. package/src/brand-page/utils/migration/migration-22.ts +1336 -0
  364. package/src/brand-page/utils/migration.util.test.ts +146 -32
  365. package/src/brand-page/utils/migration.util.ts +18 -0
  366. package/src/brand-page/utils/model.util.ts +4 -0
  367. package/src/brand-page/utils/personalisation.util.test.ts +148 -3
  368. package/src/brand-page/utils/personalisation.util.ts +9 -1
  369. package/src/brand-page/utils/rte-role-lift.util.ts +409 -0
  370. package/src/brand-page/utils/style.util.test.ts +35 -2
  371. package/src/brand-page/utils/style.util.ts +45 -11
  372. package/src/brand-page/utils/text-role-css.util.test.ts +525 -0
  373. package/src/brand-page/utils/text-role-css.util.ts +321 -0
  374. package/src/brand-page/utils/text-role-reconnect.util.test.ts +604 -0
  375. package/src/brand-page/utils/text-role-reconnect.util.ts +727 -0
  376. package/src/brand-page/utils/text-style.util.test.ts +305 -0
  377. package/src/brand-page/utils/text-style.util.ts +198 -24
  378. package/src/brand-page/utils/view-model.page-data.test.ts +232 -0
  379. package/src/brand-page/utils/view-model.util.test.ts +954 -133
  380. package/src/brand-page/utils/view-model.util.ts +187 -139
  381. package/src/brand-page/view-models.ts +4 -0
  382. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.blank-site.test.ts +57 -0
  383. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +9 -5
  384. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.ts +0 -10
  385. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/content-settings-items.fixture.ts +0 -1
  386. package/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.ts +0 -44
  387. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +253 -200
  388. package/src/brand-page-maker/composables/ai-chat/ai-builder-experiment.test.ts +82 -0
  389. package/src/brand-page-maker/composables/ai-chat/ai-builder-experiment.ts +19 -0
  390. package/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.ts +2 -0
  391. package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +19 -0
  392. package/src/brand-page-maker/composables/rich-text-editor/block-typography.test.ts +519 -0
  393. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +1 -1
  394. package/src/brand-page-maker/composables/rich-text-editor/extensions/MaxLength.test.ts +26 -1
  395. package/src/brand-page-maker/composables/rich-text-editor/extensions/RoleTypographyFill.test.ts +110 -0
  396. package/src/brand-page-maker/composables/rich-text-editor/extensions/RoleTypographyFill.ts +99 -0
  397. package/src/brand-page-maker/composables/rich-text-editor/extensions/RteClipboard.ts +397 -0
  398. package/src/brand-page-maker/composables/rich-text-editor/extensions/ViewDomParser.test.ts +139 -0
  399. package/src/brand-page-maker/composables/rich-text-editor/font.ts +36 -5
  400. package/src/brand-page-maker/composables/rich-text-editor/formatting.test.ts +375 -1
  401. package/src/brand-page-maker/composables/rich-text-editor/formatting.ts +301 -17
  402. package/src/brand-page-maker/composables/rich-text-editor/hyperlink.test.ts +82 -1
  403. package/src/brand-page-maker/composables/rich-text-editor/hyperlink.ts +28 -34
  404. package/src/brand-page-maker/composables/rich-text-editor/keepMarksOnEmpty.test.ts +80 -0
  405. package/src/brand-page-maker/composables/rich-text-editor/keepMarksOnEmpty.ts +75 -0
  406. package/src/brand-page-maker/composables/rich-text-editor/spacing.ts +31 -6
  407. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.test.ts +2485 -0
  408. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +435 -337
  409. package/src/brand-page-maker/constants/config-settings-constant.ts +0 -6
  410. package/src/brand-page-maker/convertors/text-css-style-options.convertor.test.ts +43 -0
  411. package/src/brand-page-maker/convertors/text-css-style-options.convertor.ts +6 -2
  412. package/src/brand-page-maker/convertors/text-size-value.convertor.test.ts +24 -0
  413. package/src/brand-page-maker/convertors/text-size-value.convertor.ts +17 -13
  414. package/src/brand-page-maker/default-getters/payment-text-and-border-color.default-getter.ts +7 -22
  415. package/src/brand-page-maker/default-getters/text-color.default-getter.ts +5 -21
  416. package/src/brand-page-maker/models/content-settings.test.ts +708 -0
  417. package/src/brand-page-maker/models/content-settings.ts +133 -82
  418. package/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store-utils.test.ts +63 -1
  419. package/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store-utils.ts +16 -3
  420. package/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store.test.ts +34 -0
  421. package/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store.ts +4 -2
  422. package/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.ts +30 -0
  423. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.test.ts +185 -1
  424. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +22 -34
  425. package/src/brand-page-maker/stores/history/history-store.ts +35 -2
  426. package/src/brand-page-maker/utils/__snapshots__/apply-color-strategy.golden.test.ts.snap +528 -0
  427. package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +12 -0
  428. package/src/brand-page-maker/utils/apply-color-strategy.golden.test.ts +2 -0
  429. package/src/brand-page-maker/utils/apply-color-strategy.test.ts +181 -3
  430. package/src/brand-page-maker/utils/apply-color-strategy.ts +36 -4
  431. package/src/brand-page-maker/utils/index.ts +2 -1
  432. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +447 -410
  433. package/src/brand-page-maker/utils/rich-text-editor.util.ts +610 -506
  434. package/src/brand-page-maker/utils/rte-authoring.util.test.ts +244 -0
  435. package/src/brand-page-maker/utils/rte-authoring.util.ts +175 -0
  436. package/src/brand-page-maker/utils/rte-clipboard.util.test.ts +558 -0
  437. package/src/brand-page-maker/utils/rte-clipboard.util.ts +473 -0
  438. package/src/brand-page-maker/utils/rte-features.test.ts +99 -3
  439. package/src/brand-page-maker/utils/rte-render-parity.test.ts +1 -1
  440. package/src/brand-page-maker/utils/rte-render.util.test.ts +230 -0
  441. package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +1 -1
  442. package/src/brand-page-maker/utils/rte-sanitize.util.ts +14 -3
  443. package/src/brand-page-maker/utils/rte-schema-version.test.ts +102 -0
  444. package/src/bundles/bundled-translations.de-DE.json +21 -2
  445. package/src/bundles/bundled-translations.es-ES.json +21 -2
  446. package/src/bundles/bundled-translations.fr-CA.json +21 -2
  447. package/src/bundles/bundled-translations.fr-FR.json +21 -2
  448. package/src/bundles/bundled-translations.json +21 -2
  449. package/src/bundles/bundled-translations.pt-BR.json +21 -2
  450. package/src/bundles/bundled-translations.pt-PT.json +21 -2
  451. package/src/clients/ai-assistant-client.ts +13 -18
  452. package/src/composables/useTranslate.ts +2 -1
  453. package/src/lib.ts +2 -2
  454. package/src/metadata-builder/builders/footer-builder.test.ts +70 -45
  455. package/src/metadata-builder/builders/footer-builder.ts +4 -8
  456. package/src/metadata-builder/builders/global-role-propagation.test.ts +907 -0
  457. package/src/metadata-builder/builders/global-role-propagation.ts +430 -0
  458. package/src/metadata-builder/builders/global-styles-builder.test.ts +1020 -0
  459. package/src/metadata-builder/builders/global-styles-builder.ts +194 -48
  460. package/src/metadata-builder/builders/header-builder.test.ts +112 -40
  461. package/src/metadata-builder/builders/header-builder.ts +18 -8
  462. package/src/metadata-builder/builders/section-builders/about-section-builder.ts +5 -3
  463. package/src/metadata-builder/builders/section-builders/banner-section-builder.ts +5 -3
  464. package/src/metadata-builder/builders/section-builders/booking-section-builder.ts +5 -3
  465. package/src/metadata-builder/builders/section-builders/contact-form-builder.ts +7 -4
  466. package/src/metadata-builder/builders/section-builders/images-section-builder.ts +5 -3
  467. package/src/metadata-builder/builders/section-builders/mailing-list-builder.ts +3 -2
  468. package/src/metadata-builder/builders/section-builders/mailing-list-section-builder.ts +3 -2
  469. package/src/metadata-builder/builders/section-builders/profile-section-builder.ts +7 -7
  470. package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +5 -3
  471. package/src/metadata-builder/builders/section-builders/social-links-builder.test.ts +2 -2
  472. package/src/metadata-builder/builders/section-builders/social-links-builder.ts +21 -10
  473. package/src/metadata-builder/builders/section-builders/support-me-builder.test.ts +10 -0
  474. package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +3 -2
  475. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +111 -61
  476. package/src/metadata-builder/builders/section-field-maps.ts +1 -0
  477. package/src/metadata-builder/builders/section-field-utils.test.ts +545 -2
  478. package/src/metadata-builder/builders/section-field-utils.ts +202 -69
  479. package/src/metadata-builder/builders/section-rules/global-styles.rules.ts +38 -13
  480. package/src/metadata-builder/builders/section-rules/social-links.rules.ts +5 -0
  481. package/src/metadata-builder/helpers/rte.helpers.test.ts +101 -0
  482. package/src/metadata-builder/helpers/rte.helpers.ts +129 -43
  483. package/src/metadata-builder/metadata-executor.test.ts +167 -0
  484. package/src/metadata-builder/metadata-executor.ts +34 -1
  485. package/src/metadata-builder/models.ts +2 -0
  486. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +23 -7
  487. package/src/metadata-builder/parsers/section-tool-arguments.ts +21 -1
  488. package/src/metadata-builder/sanitizers/__tests__/section-args-validator.test.ts +215 -0
  489. package/src/metadata-builder/sanitizers/rte-schema-check.ts +1 -1
  490. package/src/metadata-builder/sanitizers/sanitizer-rules.ts +4 -8
  491. package/src/metadata-builder/sanitizers/section-args-validator.ts +136 -53
  492. package/src/shared/color-palette/section-color-matches.test.ts +15 -11
  493. package/src/shared/color-palette/section-color-matches.ts +9 -28
  494. package/src/shared/color-strategies/color-strategies.ts +35 -0
  495. package/src/shared/color-strategies/rte-color.test.ts +1 -1
  496. package/src/shared/color-strategies/rte-color.ts +19 -0
  497. package/src/shared/color-strategies/rte-field-pairs.test.ts +26 -5
  498. package/src/shared/color-strategies/rte-field-pairs.ts +33 -0
  499. package/src/shared/content-settings/components/color-matches-panel/ColorMatchesPanel.test.ts +43 -66
  500. package/src/shared/content-settings/components/color-matches-panel/ColorMatchesPanel.vue +7 -1
  501. package/src/shared/content-settings/components/content-settings/ContentSettings.mixin.ts +1 -48
  502. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +33 -7
  503. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.test.ts +128 -1
  504. package/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.ts +0 -6
  505. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.test.ts +56 -3
  506. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +11 -4
  507. package/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.test.ts +335 -25
  508. package/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.ts +248 -70
  509. package/src/shared/content-settings/components/text-style-field/TextStyleField.vue +113 -25
  510. package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.de-DE.json +17 -10
  511. package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.es-ES.json +17 -10
  512. package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.fr-CA.json +17 -10
  513. package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.fr-FR.json +17 -10
  514. package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.json +8 -1
  515. package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.pt-BR.json +17 -10
  516. package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.pt-PT.json +17 -10
  517. package/src/shared/content-settings/components/text-style-role-dropdown/TextStyleRoleDropdown.test.ts +141 -0
  518. package/src/shared/content-settings/components/text-style-role-dropdown/TextStyleRoleDropdown.vue +136 -0
  519. package/src/shared/content-settings/store/content-settings-store.test.ts +491 -6
  520. package/src/shared/content-settings/store/content-settings-store.ts +159 -17
  521. package/src/shared/navigation-style-settings/store/index.test.ts +295 -0
  522. package/src/shared/navigation-style-settings/store/index.ts +65 -15
  523. package/src/shared/page-settings/components/page-settings-section-styles/HeaderButtonSettings.test.ts +44 -4
  524. package/src/shared/page-settings/components/page-settings-section-styles/HeaderButtonSettings.vue +13 -4
  525. package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.navigation.test.ts +89 -0
  526. package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.text-style.test.ts +91 -0
  527. package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.ts +14 -2
  528. package/src/shared/page-settings/store/page-settings-store.text-style.test.ts +84 -0
  529. package/src/shared/page-settings/store/page-settings-store.ts +30 -1
  530. package/src/shared/rte-extensions/AutoLink.test.ts +98 -0
  531. package/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/AutoLink.ts +87 -58
  532. package/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/MaxLength.ts +4 -1
  533. package/src/shared/rte-extensions/TextEffects.ts +151 -0
  534. package/src/shared/rte-extensions/ViewDomParser.ts +27 -0
  535. package/src/{brand-page-maker/composables/rich-text-editor/getRteExtensions.ts → shared/rte-extensions.util.ts} +66 -9
  536. package/src/shared/rte-link.util.ts +2 -0
  537. package/src/shared/rte-render.util.ts +385 -0
  538. package/src/shared/rte-shape.util.ts +29 -0
  539. package/src/shared/side-bar/store/side-bar-store.test.ts +27 -1
  540. package/src/shared/side-bar/store/side-bar-store.ts +11 -0
  541. package/src/shared/style-settings/components/StyleSettings.mixin.ts +7 -3
  542. package/src/shared/style-settings/components/StyleSettings.test.ts +116 -0
  543. package/src/shared/style-settings/components/StyleSettings.vue +74 -25
  544. package/src/shared/style-settings/components/i18n/style-settings.de-DE.json +32 -20
  545. package/src/shared/style-settings/components/i18n/style-settings.es-ES.json +32 -20
  546. package/src/shared/style-settings/components/i18n/style-settings.fr-CA.json +32 -20
  547. package/src/shared/style-settings/components/i18n/style-settings.fr-FR.json +32 -20
  548. package/src/shared/style-settings/components/i18n/style-settings.json +13 -3
  549. package/src/shared/style-settings/components/i18n/style-settings.pt-BR.json +32 -20
  550. package/src/shared/style-settings/components/i18n/style-settings.pt-PT.json +32 -20
  551. package/src/shared/style-settings/components/text-style-options-settings-with-state/TextStyleOptionsSettingsWithState.vue +14 -34
  552. package/src/shared/style-settings/store/style-settings-store.ts +3 -49
  553. package/src/shared/style-settings/text-style-size-order.test.ts +76 -0
  554. package/src/shared/style-settings/text-style-size-order.ts +28 -0
  555. package/src/shared/text-role-typography.util.ts +277 -0
  556. package/src/utils/minify-brand-page-metadata.test.ts +102 -7
  557. package/src/utils/minify-brand-page-metadata.ts +62 -8
  558. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/AutoLink.d.ts +0 -2
  559. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
  560. package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -63
  561. package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -106
  562. package/lib/src/brand-page-maker/utils/rte-shape.d.ts +0 -18
  563. package/lib/src/brand-page-maker/utils/rte-shape.js +0 -21
  564. package/lib/src/metadata-builder/builders/global-font-propagation.d.ts +0 -41
  565. package/lib/src/metadata-builder/builders/global-font-propagation.js +0 -146
  566. package/src/brand-page/models/brand-page-context.model.test.ts +0 -38
  567. package/src/brand-page-maker/utils/rte-render.util.ts +0 -179
  568. package/src/brand-page-maker/utils/rte-shape.ts +0 -29
  569. package/src/clients/ai-assistant-client.test.ts +0 -135
  570. package/src/metadata-builder/builders/global-font-propagation.test.ts +0 -401
  571. package/src/metadata-builder/builders/global-font-propagation.ts +0 -238
  572. package/src/shared/style-settings/components/StyleSettingsSection.vue +0 -8
  573. /package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/FontFamily.d.ts +0 -0
  574. /package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/FontFamily.js +0 -0
  575. /package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/LetterSpacing.d.ts +0 -0
  576. /package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/LetterSpacing.js +0 -0
  577. /package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/MaxLength.d.ts +0 -0
  578. /package/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/FontFamily.ts +0 -0
  579. /package/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/LetterSpacing.ts +0 -0
@@ -0,0 +1,1336 @@
1
+ import type { MigrationData, MigrationOptions } from '../migration.util';
2
+ import {
3
+ BRAND_PAGE_TYPES,
4
+ type BrandPageModel,
5
+ type HeaderButton,
6
+ type NavOrder,
7
+ type PageModel,
8
+ type TextStyleDeclaration,
9
+ } from '../../models';
10
+ import {
11
+ TEXT_STYLE_TYPES,
12
+ isCanonicalTextStyleType,
13
+ isLegacyTextStyleType,
14
+ type TextStyleOptions,
15
+ type TextStyleType,
16
+ type LegacyTextStyleType,
17
+ } from '../../models/styles.model';
18
+ import {
19
+ CONTENT_TEXT_STYLE_PROPERTY_KEYS,
20
+ type ContentTextStylePropertyKey,
21
+ FONT_FAMILY_CONFIGS,
22
+ DEFAULT_FONT_FAMILY_KEY,
23
+ FONT_SIZE_BODY,
24
+ LINE_HEIGHT_BODY,
25
+ ALIGN_LEFT_ALIGNED,
26
+ ALIGN_RIGHT_ALIGNED,
27
+ MAX_IMAGES_SECTION_ALIGNMENT_ITEMS_LENGTH,
28
+ type FontFamilyKey,
29
+ } from '../../constants';
30
+ import {
31
+ computeLegacyTargets,
32
+ reconnectTextStyleRoles,
33
+ contentFieldReconnectSpec,
34
+ drawnFontFamily,
35
+ siteTextFontName,
36
+ FAMILY_TIERS,
37
+ type LegacyTargetMap,
38
+ type TextRoleFamily,
39
+ type RenderedDefaultsForContent,
40
+ } from '../text-role-reconnect.util';
41
+ import {
42
+ pairedRteTextField,
43
+ rteStyleFieldsFor,
44
+ } from '@shared/color-strategies/rte-field-pairs';
45
+ import {
46
+ containerTextColorDefaults,
47
+ titleSurfaceContrastFill,
48
+ } from '../container-text-color.util';
49
+ import {
50
+ stripLiftContentRte,
51
+ textRunFontFamilies,
52
+ } from '../rte-role-lift.util';
53
+ import {
54
+ collectRteTextColors,
55
+ fillUncolouredRteText,
56
+ } from '@shared/color-strategies/rte-color';
57
+ import { isBlankColorValue } from '../color-palette.util';
58
+ import {
59
+ plainTextToRteJson,
60
+ resolveHtmlParser,
61
+ isTiptapDocument,
62
+ type HtmlParser,
63
+ } from '@shared/rte-render.util';
64
+ import type { JSONContent } from '@tiptap/vue-3';
65
+
66
+ // SNAPSHOT DEPENDENCY NOTICE: B/C/D import live shared helpers above. Their source modules carry
67
+ // `MIGRATION-22 SNAPSHOT DEPENDENCY` notices; once v22 ships, copy the v22 semantics here before
68
+ // changing any of them. B1 already owns an inline snapshot of its typography fill.
69
+ // `migration-22.fixed-input.test.ts` fails when a helper change moves this migration's output.
70
+
71
+ /**
72
+ * BP-5969 migration-22 — the single-source text-style migration. Version-gated: runs
73
+ * once when `v < 22`. The durable decisions are `0019-retire-rte-object-style-sync.md`,
74
+ * `0020-role-is-typography-only-colour-is-contrast-derived.md`, and
75
+ * `0021-migrated-content-reconnects-to-nearest-role-with-override.md`.
76
+ *
77
+ * A/B/C are appearance-preserving. D/E/G finish the ADR-0020 colour decouple in the same
78
+ * (still-unreleased) migration — they too preserve appearance, by freezing each element's
79
+ * currently-resolved colour before the role colour and base coat that fed it are removed.
80
+ * One decided exception (ADR-0020, Update 2026-09-29): a social-links section with no base
81
+ * coat to freeze bakes its icon fill to the title's contrast colour instead — a new colour,
82
+ * not a literal freeze of whatever `currentColor` happened to paint under `:root`.
83
+ * A. Seed each old role's colour onto its target tier (D3 base coat) — `seedRoleColours`.
84
+ * Roles keep their §4 size, weight and line height; the old size is preserved as a per-block
85
+ * override by B. The legacy → tier map is computed per-site by `computeLegacyTargets`.
86
+ * A2. (R16) Count the font the site's title/heading, description/paragraph and button text shows,
87
+ * and give each family the winner — each family's font comes from the site's text, not §4 —
88
+ * before B and C link equal text to it — `seedRoleFonts`.
89
+ * B. Seed the absent header-button / brand.contact Save Contact styles (they rendered as
90
+ * `buttons`), then reconnect every text field outside the ten — content, text items, footer
91
+ * copyright, nav, header button, Save Contact — to its nearest role, keeping its look as
92
+ * overrides — `reconnectContentRoles` (the shared `text-role-reconnect.util`). The three old
93
+ * no-role renderer defaults (mailing-list title, Support Me title, text block) are materialised
94
+ * here only, before reconnect persists their effective values; no runtime reader keeps them. A colourless nav
95
+ * link's colour is baked here, not in D: it comes from the legacy role, which B replaces. A
96
+ * payment or shop success message with no role of its own gets the look it drew
97
+ * (`seedSuccessMessageStyle`); D then bakes its colour, like any non-RTE style.
98
+ * C. Baked→override strip/lift on RTE fields (D4) — `stripLiftContentRte`; per field it also folds
99
+ * legacy bold/italic/underline/strike marks into the run's textStyle attrs (`foldSemanticFormattingMarks`)
100
+ * and pins a not-bold legacy block's weight so a bold heading tier can't bold it (`pinLegacyRunWeight`).
101
+ * B1. Convert plain-text RTE-eligible fields to Tiptap docs — `convertPlainRteFields`.
102
+ * D. Bake each element's resolved colour explicitly, social-links icon fills included —
103
+ * `bakeElementColours`.
104
+ * E. Clear the retired role colour — `clearRoleColours`.
105
+ * G. Colour-decouple RTE-paired style objects: remove `color` only — `stripRteStyleColours`.
106
+ * H. Delete the retired site text font and its footer copies — `deleteTextFontFamily`
107
+ * (`0036-retire-text-font-family.md`). A2 and B read it through `siteTextFontName`.
108
+ *
109
+ * Ordering is load-bearing: D reads the base coat off `paragraph1` and the role colours (which A
110
+ * seeds from the old roles), so it must run after A and before E; B must run before C and D, which
111
+ * read each field's reconnected role and pinned typography; G removes the RTE style object's
112
+ * colour, so it must run after D has baked that colour into the mark. B1 runs after C (so C's
113
+ * per-run typography bake sees real editor-baked docs, not a freshly-converted placeholder) and
114
+ * before D (so D colours the converted runs like any other doc). A2 runs before B, C, B1, which
115
+ * read the role's font: on the untouched model, so it counts what a legacy-role or no-role field
116
+ * showed before B relinks it, and links B and C to bake against the winner it seeds. H runs last,
117
+ * after every reader of the site text font.
118
+ *
119
+ * Version-gated (v only moves forward), so this runs exactly once. A hypothetical re-run is a
120
+ * no-op throughout: B (seed + reconnect), C (strip + weight pin) and B1 (convert) find every button
121
+ * style present and every field on one of the ten, no baked role-equal typography, every bare run
122
+ * already weight-pinned and no plain strings left; D/E/G find every element already coloured, role
123
+ * colours already cleared and RTE style objects already colourless; H finds nothing to delete; and
124
+ * A (`seedRoleColours`) finds nothing to copy — E already cleared the legacy roles' colour
125
+ * (persisted), so the tiers keep their §4 typography untouched. A2 re-counts what the migrated
126
+ * site shows, the same fonts, so it picks the same winners — also a no-op. Unreachable behind the
127
+ * gate; documented here per the migration rules.
128
+ */
129
+
130
+ export { computeLegacyTargets };
131
+ export {
132
+ stripLiftRteDoc,
133
+ foldSemanticFormattingMarks,
134
+ pinLegacyRunWeight,
135
+ stripLiftContentRte,
136
+ } from '../rte-role-lift.util';
137
+
138
+ /**
139
+ * Seed each old legacy role's colour onto its target tier — COLOUR ONLY; the tier keeps its
140
+ * §4 default typography. `bakeElementColours` (D) freezes each element's colour by reading its
141
+ * tier, and the base coat off `paragraph1`, so a legacy block's colour is preserved exactly as
142
+ * long as the tier it links to carries the old role's colour. `clearRoleColours` (E) then wipes
143
+ * the seed once D has read it — the tier ends typography-only, per ADR-0020.
144
+ */
145
+ export const seedRoleColours = (
146
+ options: TextStyleOptions,
147
+ targets: LegacyTargetMap,
148
+ ): void => {
149
+ for (const legacy of Object.keys(targets) as LegacyTextStyleType[]) {
150
+ const source = options[legacy]?.style as unknown as
151
+ | Record<string, unknown>
152
+ | undefined;
153
+ const colour = source?.color;
154
+
155
+ if (colour === undefined || colour === null || colour === '') continue;
156
+
157
+ const target = options[targets[legacy]]?.style as unknown as
158
+ | Record<string, unknown>
159
+ | undefined;
160
+
161
+ // The canonical target normally pre-exists (the model constructor seeds all 10); guard
162
+ // defensively for pre-10-role metadata rather than assume it.
163
+ if (target) target.color = colour;
164
+ }
165
+ };
166
+
167
+ // An absent header-button or brand.contact Save Contact style rendered as `buttons`. Seeding
168
+ // it lets the reconnect move it to `button` with that look kept as overrides.
169
+ const seedAbsentButtonStyles = (brandPageModel: BrandPageModel): void => {
170
+ for (const header of [
171
+ brandPageModel.pageHeaderDesktop,
172
+ brandPageModel.pageHeaderMobile,
173
+ ]) {
174
+ const headerButton = header?.headerButton;
175
+
176
+ if (headerButton) {
177
+ headerButton.buttonTextStyle ??= {
178
+ textStyleType: TEXT_STYLE_TYPES.buttons,
179
+ };
180
+ }
181
+ }
182
+
183
+ if (brandPageModel.brandPageType !== BRAND_PAGE_TYPES.brandContact) return;
184
+
185
+ // The same defaults the view model and the maker store apply when none are saved.
186
+ brandPageModel.saveContactSettings ??= {
187
+ isSavingEnabled: true,
188
+ hideBranding: false,
189
+ };
190
+ brandPageModel.saveContactSettings.buttonTextStyle ??= {
191
+ textStyleType: TEXT_STYLE_TYPES.buttons,
192
+ };
193
+ };
194
+
195
+ // ── R16 — job A2: role fonts on upgrade ───────────────────────────────────
196
+
197
+ const LATO = FONT_FAMILY_CONFIGS[DEFAULT_FONT_FAMILY_KEY].name;
198
+
199
+ // Every display NAME a stored font can resolve to — built once, not re-scanned per vote.
200
+ const FONT_FAMILY_NAMES = new Set(
201
+ Object.values(FONT_FAMILY_CONFIGS).map((config) => config.name),
202
+ );
203
+
204
+ // A stored font counts by its display NAME: a key spelling ('nunito') counts as its name
205
+ // ('Nunito'); a font outside the list casts no vote (Q11) and the stored override survives
206
+ // (B/C compare strictly, so a key-spelled override never drops).
207
+ const voteFontName = (fontFamily: string | undefined): string | undefined => {
208
+ if (!fontFamily) return undefined;
209
+
210
+ const byKey = FONT_FAMILY_CONFIGS[fontFamily as FontFamilyKey]?.name;
211
+
212
+ if (byKey) return byKey;
213
+
214
+ return FONT_FAMILY_NAMES.has(fontFamily) ? fontFamily : undefined;
215
+ };
216
+
217
+ // One tally per family. A `Map` keeps first-insertion order, and a later count never overtakes an
218
+ // EQUAL earlier one, so the winner scan gives the tie to whichever font was first voted for
219
+ // (migration-21's first-seen rule, D11).
220
+ class FontTally {
221
+ private readonly votes = new Map<string, number>();
222
+
223
+ add(fontFamily: string | undefined): void {
224
+ const name = voteFontName(fontFamily);
225
+
226
+ if (!name) return;
227
+
228
+ this.votes.set(name, (this.votes.get(name) ?? 0) + 1);
229
+ }
230
+
231
+ winner(defaultFont: string): string {
232
+ let winner = defaultFont;
233
+ let max = 0;
234
+
235
+ for (const [font, count] of this.votes) {
236
+ if (count > max) {
237
+ max = count;
238
+ winner = font;
239
+ }
240
+ }
241
+
242
+ return winner;
243
+ }
244
+ }
245
+
246
+ // Depth-first through the nav from its home page (`orders[0]`), then any page with no route, in
247
+ // its stored order — `page-settings-store.ts` `syncPagesWithOrders`'s algorithm, read-only here.
248
+ const sitePageOrder = (brandPageModel: BrandPageModel): PageModel[] => {
249
+ const routes = brandPageModel.nav?.routes ?? [];
250
+ const pageIds: string[] = [];
251
+
252
+ const collect = (orders: NavOrder[] | undefined): void => {
253
+ for (const order of orders ?? []) {
254
+ const route = routes.find((r) => r.id === order.routeId);
255
+
256
+ if (route?.pageId) pageIds.push(route.pageId);
257
+
258
+ collect(order.subRoutes);
259
+ }
260
+ };
261
+
262
+ collect(brandPageModel.nav?.orders);
263
+
264
+ const pages = brandPageModel.pages ?? [];
265
+ const byId = new Map(pages.map((page) => [page.id, page]));
266
+ const ordered: PageModel[] = [];
267
+
268
+ for (const id of pageIds) {
269
+ const page = byId.get(id);
270
+
271
+ if (page) {
272
+ ordered.push(page);
273
+ byId.delete(id);
274
+ }
275
+ }
276
+
277
+ ordered.push(...byId.values());
278
+
279
+ return ordered;
280
+ };
281
+
282
+ // Same predicate as `vcard.util`'s `hasProfileContent`, checked directly against the raw model:
283
+ // that one is typed for the rendered view model, which migration runs before building.
284
+ const hasProfileContent = (brandPageModel: BrandPageModel): boolean =>
285
+ (brandPageModel.pages ?? []).some((page) =>
286
+ page.contents?.some((content) => content.type === 'profile'),
287
+ );
288
+
289
+ // A Tiptap doc gives the distinct fonts its non-empty runs show; a non-blank plain string gives
290
+ // the block font; anything else (blank, `false`, absent) casts no vote — read the same way the
291
+ // reconnect will (`drawnFontFamily`), so the count can't disagree with what B and C keep.
292
+ const voteText = (
293
+ tally: FontTally,
294
+ style: TextStyleDeclaration | undefined,
295
+ text: unknown,
296
+ options: TextStyleOptions,
297
+ siteFont: string,
298
+ ): void => {
299
+ if (!style) return;
300
+
301
+ if (isTiptapDocument(text)) {
302
+ const blockFont = drawnFontFamily(style, options, siteFont);
303
+
304
+ for (const font of textRunFontFamilies(text, blockFont)) tally.add(font);
305
+
306
+ return;
307
+ }
308
+
309
+ if (typeof text !== 'string' || text.trim() === '') return;
310
+
311
+ tally.add(drawnFontFamily(style, options, siteFont));
312
+ };
313
+
314
+ // The content-level style key → its plain text field, for the sections whose title/description
315
+ // isn't RTE-paired (`pairedRteTextField` returns nothing for them) — a link button's `title`
316
+ // (`ButtonContent`) and a direct section button's `buttonText` (banner, booking, mailing-list…).
317
+ const CONTENT_LEVEL_TEXT_FIELD: Partial<Record<string, string>> = {
318
+ titleStyle: 'title',
319
+ descriptionStyle: 'description',
320
+ paragraphStyle: 'paragraph',
321
+ buttonTextStyle: 'buttonText',
322
+ };
323
+
324
+ // Payment (Continue/Done) and Support Me (the same widget, `Payment.vue`) always show their
325
+ // fixed-label button whenever the section renders — no persisted field holds the label, so
326
+ // there's nothing to check for blank. Shop's fixed-label "View cart" (`Shop.vue:59`) shows only
327
+ // once it has a product to sell; with none, its button (like an empty title) casts no vote.
328
+ const showsFixedLabelButton = (
329
+ contentType: string | undefined,
330
+ fields: Record<string, unknown>,
331
+ ): boolean =>
332
+ contentType === 'request-payment' ||
333
+ contentType === 'support-me' ||
334
+ (contentType === 'shop-section' &&
335
+ Array.isArray(fields.products) &&
336
+ fields.products.length > 0);
337
+
338
+ // One content: its own title/description/paragraph/button-text field, then (images/shapes only)
339
+ // each item's title/paragraph/buttonText against the section's single heading/paragraph/button
340
+ // style. `headingStyle` never has content-level text of its own, so it only votes per item.
341
+ const voteContentFields = (
342
+ tallies: Record<TextRoleFamily, FontTally>,
343
+ fields: Record<string, unknown>,
344
+ options: TextStyleOptions,
345
+ siteFont: string,
346
+ ): void => {
347
+ const contentType = fields.type as string | undefined;
348
+
349
+ for (const key of [
350
+ 'titleStyle',
351
+ 'descriptionStyle',
352
+ 'paragraphStyle',
353
+ 'buttonTextStyle',
354
+ ] as const) {
355
+ const style = fields[key];
356
+
357
+ if (!style || typeof style !== 'object') continue;
358
+
359
+ if (
360
+ key === 'buttonTextStyle' &&
361
+ showsFixedLabelButton(contentType, fields)
362
+ ) {
363
+ // A fixed-label button's text is an i18n string, never model data — there's nothing to
364
+ // check for blank, so vote its font directly rather than through `voteText`'s text gate.
365
+ tallies.button.add(
366
+ drawnFontFamily(style as TextStyleDeclaration, options, siteFont),
367
+ );
368
+
369
+ continue;
370
+ }
371
+
372
+ // `pairedRteTextField` returns nothing for `buttonTextStyle` (never RTE-paired) and for a
373
+ // button content's own `titleStyle` (a link's plain-text label) — `CONTENT_LEVEL_TEXT_FIELD`
374
+ // is exactly the fallback those two need.
375
+ const textField =
376
+ pairedRteTextField(contentType ?? '', key) ??
377
+ CONTENT_LEVEL_TEXT_FIELD[key];
378
+ // The reconnect's own family (`contentFieldReconnectSpec`), so the vote and the reconnect
379
+ // share one classification: Button text for a link button's `titleStyle` or any
380
+ // `buttonTextStyle`, otherwise fixed by the key.
381
+ const family = contentFieldReconnectSpec(contentType, key).family;
382
+
383
+ if (!textField || !family) continue;
384
+
385
+ voteText(
386
+ tallies[family],
387
+ style as TextStyleDeclaration,
388
+ fields[textField],
389
+ options,
390
+ siteFont,
391
+ );
392
+ }
393
+
394
+ const items = fields.items;
395
+
396
+ if (!Array.isArray(items)) return;
397
+
398
+ // Items belong only to `MediaSection`s (images/shapes) — the section's own `paragraphStyle`
399
+ // never also pairs an RTE field at content level there (only `about-section` does, and it has
400
+ // no items), so an item's paragraph vote never double-counts the section's own.
401
+ const perItemVotes: [
402
+ TextRoleFamily,
403
+ TextStyleDeclaration | undefined,
404
+ string,
405
+ ][] = [
406
+ [
407
+ 'headings',
408
+ fields.headingStyle as TextStyleDeclaration | undefined,
409
+ 'title',
410
+ ],
411
+ [
412
+ 'paragraphs',
413
+ fields.paragraphStyle as TextStyleDeclaration | undefined,
414
+ 'paragraph',
415
+ ],
416
+ [
417
+ 'button',
418
+ fields.buttonTextStyle as TextStyleDeclaration | undefined,
419
+ 'buttonText',
420
+ ],
421
+ ];
422
+
423
+ // A left/right-aligned images-section renders only its first few items
424
+ // (`view-model.util.ts` `mapToImagesSectionViewModel`) — an item past that cutoff never shows,
425
+ // so it casts no vote either.
426
+ const displayStyle = fields.displayStyle;
427
+ const renderedItems =
428
+ contentType === 'images-section' &&
429
+ (displayStyle === ALIGN_LEFT_ALIGNED ||
430
+ displayStyle === ALIGN_RIGHT_ALIGNED)
431
+ ? (items as Record<string, unknown>[]).slice(
432
+ 0,
433
+ MAX_IMAGES_SECTION_ALIGNMENT_ITEMS_LENGTH,
434
+ )
435
+ : (items as Record<string, unknown>[]);
436
+
437
+ for (const item of renderedItems) {
438
+ if (!item || typeof item !== 'object') continue;
439
+
440
+ for (const [family, itemStyle, textField] of perItemVotes) {
441
+ if (itemStyle) {
442
+ voteText(
443
+ tallies[family],
444
+ itemStyle,
445
+ item[textField],
446
+ options,
447
+ siteFont,
448
+ );
449
+ }
450
+ }
451
+ }
452
+ };
453
+
454
+ // The header button renders only with a non-blank label (`view-model.util.ts:1215`) — `isVisible`
455
+ // alone isn't enough, or a visible-but-blank desktop button would wrongly stop the mobile one
456
+ // (which might otherwise render) from getting a turn.
457
+ const headerButtonRenders = (
458
+ headerButton: HeaderButton | undefined,
459
+ ): headerButton is HeaderButton =>
460
+ !!headerButton?.isVisible && !!headerButton.text?.trim();
461
+
462
+ /**
463
+ * A2 — count the font the site's title/heading, description/paragraph and button text shows
464
+ * (`business-rules.md` *Text style roles*), and give each family the winner, before B and C link
465
+ * equal text to it. Runs on the untouched v21 model, after A and before B: a field's font is read
466
+ * the same way B will read its look (`drawnFontFamily`), so the count and the reconnect can't
467
+ * disagree about what a field showed. Only `fontFamily` changes on the ten canonical roles — size,
468
+ * weight and line height stay at their §4 defaults. A family with no vote keeps Lato.
469
+ */
470
+ export const seedRoleFonts = (
471
+ brandPageModel: BrandPageModel,
472
+ options: TextStyleOptions,
473
+ ): void => {
474
+ const siteFont = siteTextFontName(brandPageModel) ?? LATO;
475
+ const tallies: Record<TextRoleFamily, FontTally> = {
476
+ headings: new FontTally(),
477
+ paragraphs: new FontTally(),
478
+ button: new FontTally(),
479
+ };
480
+
481
+ // The header button votes once — the desktop header's, else the mobile one (Q12c).
482
+ for (const header of [
483
+ brandPageModel.pageHeaderDesktop,
484
+ brandPageModel.pageHeaderMobile,
485
+ ]) {
486
+ const headerButton = header?.headerButton;
487
+
488
+ if (!headerButtonRenders(headerButton)) continue;
489
+
490
+ voteText(
491
+ tallies.button,
492
+ headerButton.buttonTextStyle ?? {
493
+ textStyleType: TEXT_STYLE_TYPES.buttons,
494
+ },
495
+ headerButton.text,
496
+ options,
497
+ siteFont,
498
+ );
499
+ break;
500
+ }
501
+
502
+ for (const page of sitePageOrder(brandPageModel)) {
503
+ for (const content of page.contents ?? []) {
504
+ voteContentFields(
505
+ tallies,
506
+ content as unknown as Record<string, unknown>,
507
+ options,
508
+ siteFont,
509
+ );
510
+ }
511
+ }
512
+
513
+ // brand.contact only, and only once the widget is actually turned on (`Page.mixin.ts:535`) and the
514
+ // site has a profile section for it to attach to (`Page.mixin.ts` `shouldShowSaveContactWidget`,
515
+ // `checkHasProfileContent`) — otherwise it never renders, the same "nothing to show" rule as an
516
+ // empty title.
517
+ if (
518
+ brandPageModel.brandPageType === BRAND_PAGE_TYPES.brandContact &&
519
+ brandPageModel.saveContactSettings?.isSavingEnabled !== false &&
520
+ hasProfileContent(brandPageModel)
521
+ ) {
522
+ tallies.button.add(
523
+ drawnFontFamily(
524
+ brandPageModel.saveContactSettings?.buttonTextStyle ?? {
525
+ textStyleType: TEXT_STYLE_TYPES.buttons,
526
+ },
527
+ options,
528
+ siteFont,
529
+ ),
530
+ );
531
+ }
532
+
533
+ // The canonical roles normally pre-exist (the model constructor seeds all 10); guard
534
+ // defensively for pre-10-role metadata rather than assume it (as `seedRoleColours` does). A
535
+ // family with no vote keeps Lato — never the site font, which is only what an UNCOUNTED field
536
+ // inherits (`drawnFontFamily`), not a family-wide default.
537
+ for (const family of Object.keys(tallies) as TextRoleFamily[]) {
538
+ const winner = tallies[family].winner(LATO);
539
+
540
+ for (const role of FAMILY_TIERS[family]) {
541
+ if (options[role]) options[role].style.fontFamily = winner;
542
+ }
543
+ }
544
+ };
545
+
546
+ /**
547
+ * B — seed the absent button styles, then reconnect every text field outside the ten to its
548
+ * nearest role, keeping the look production drew: a field on a role drew only that role's
549
+ * typography (`drawnBeforeCascade`). After this every text field links one of the ten (R14).
550
+ */
551
+ const isUnset = (value: unknown): boolean =>
552
+ value === undefined || value === null || value === '';
553
+
554
+ const isUnlinkedLegacyStyle = (
555
+ style: unknown,
556
+ ): style is Record<string, unknown> => {
557
+ if (!style || typeof style !== 'object') return false;
558
+
559
+ const type = (style as Record<string, unknown>).textStyleType;
560
+
561
+ return (
562
+ !isCanonicalTextStyleType(type as string | undefined) &&
563
+ !isLegacyTextStyleType(type as string | undefined)
564
+ );
565
+ };
566
+
567
+ const setIfUnset = (
568
+ style: Record<string, unknown>,
569
+ key: string,
570
+ value: unknown,
571
+ ): void => {
572
+ if (isUnset(style[key]) && !isUnset(value)) style[key] = value;
573
+ };
574
+
575
+ const styleRecord = (value: unknown): Record<string, unknown> =>
576
+ value && typeof value === 'object' && !Array.isArray(value)
577
+ ? (value as Record<string, unknown>)
578
+ : {};
579
+
580
+ /**
581
+ * B0 — production composed three no-role text styles in its view-model layer. Recreate those
582
+ * values once on the persisted field before B picks the nearest canonical role. A valid legacy
583
+ * role was resolved instead of these component defaults, so it must remain untouched.
584
+ */
585
+ const materialiseNoRoleRendererDefaults = (
586
+ brandPageModel: BrandPageModel,
587
+ ): void => {
588
+ for (const page of brandPageModel.pages ?? []) {
589
+ for (const content of page.contents ?? []) {
590
+ const fields = content as unknown as Record<string, unknown>;
591
+
592
+ if (content.type === 'mailing-list') {
593
+ const style = fields.titleStyle;
594
+
595
+ if (isUnlinkedLegacyStyle(style)) {
596
+ setIfUnset(style, 'fontSize', FONT_SIZE_BODY);
597
+ setIfUnset(style, 'fontWeight', 'bold');
598
+ }
599
+ }
600
+
601
+ if (content.type === 'support-me') {
602
+ const style = fields.titleStyle;
603
+
604
+ if (isUnlinkedLegacyStyle(style)) {
605
+ setIfUnset(style, 'fontWeight', 'bold');
606
+ }
607
+ }
608
+
609
+ if (content.type === 'text') {
610
+ const original = styleRecord(fields.textStyle);
611
+ const isUnlinked = isUnlinkedLegacyStyle(original);
612
+
613
+ // Production's text mapper merged these deprecated sibling objects before the style
614
+ // object. Alignment and colour mattered even on an old role; textSize and the 1.5
615
+ // default only mattered without one, because a role replaced all typography.
616
+ fields.textStyle = isUnlinked
617
+ ? {
618
+ lineHeight: LINE_HEIGHT_BODY,
619
+ ...styleRecord(fields.textSize),
620
+ ...styleRecord(fields.textAlignment),
621
+ ...styleRecord(fields.textColor),
622
+ ...styleRecord(original),
623
+ }
624
+ : {
625
+ ...styleRecord(fields.textAlignment),
626
+ ...styleRecord(fields.textColor),
627
+ ...styleRecord(original),
628
+ };
629
+
630
+ delete fields.textSize;
631
+ delete fields.textAlignment;
632
+ delete fields.textColor;
633
+ }
634
+ }
635
+ }
636
+ };
637
+
638
+ const legacyRenderedDefaults: RenderedDefaultsForContent = (content, key) => {
639
+ const style = content[key];
640
+
641
+ if (!isUnlinkedLegacyStyle(style)) return undefined;
642
+
643
+ if (content.type === 'mailing-list' && key === 'titleStyle') {
644
+ return { fontSize: FONT_SIZE_BODY, fontWeight: 'bold' };
645
+ }
646
+
647
+ if (content.type === 'support-me' && key === 'titleStyle') {
648
+ return { fontWeight: 'bold' };
649
+ }
650
+
651
+ if (content.type === 'text' && key === 'textStyle') {
652
+ return { lineHeight: LINE_HEIGHT_BODY };
653
+ }
654
+
655
+ return undefined;
656
+ };
657
+
658
+ export const reconnectContentRoles = (
659
+ brandPageModel: BrandPageModel,
660
+ targets: LegacyTargetMap,
661
+ options: TextStyleOptions,
662
+ ): void => {
663
+ seedAbsentButtonStyles(brandPageModel);
664
+ materialiseNoRoleRendererDefaults(brandPageModel);
665
+ reconnectTextStyleRoles(brandPageModel, targets, options, {
666
+ drawnBeforeCascade: true,
667
+ renderedDefaultsForContent: legacyRenderedDefaults,
668
+ });
669
+ };
670
+
671
+ // ── BP-5969 ADR-0020: colour decouple (jobs D, E, G) ──────────────────────────
672
+ // Appended to the appearance-preserving A/B/C above. Colour is read via bracket access
673
+ // because `GlobalTextStyleDeclaration.color` is retired from the typed model.
674
+
675
+ type RawStyleObject = {
676
+ color?: string;
677
+ textStyleType?: string;
678
+ applyAutoScale?: boolean;
679
+ textAlign?: string;
680
+ fontWeight?: string;
681
+ };
682
+
683
+ type NoRoleTextStyles = WeakMap<object, Set<string>>;
684
+
685
+ const hasNoRole = (style: unknown): boolean => {
686
+ if (!style || typeof style !== 'object') return false;
687
+
688
+ const textStyleType = (style as RawStyleObject).textStyleType;
689
+
690
+ return (
691
+ !isCanonicalTextStyleType(textStyleType) &&
692
+ !isLegacyTextStyleType(textStyleType)
693
+ );
694
+ };
695
+
696
+ /**
697
+ * Job B assigns a canonical role to every unlinked field. Job D must still use the colour the
698
+ * field rendered with before that assignment: its own colour, container default, then the base coat
699
+ * (never the role B selected merely for typography). Keep the location rather than the style object,
700
+ * because reconnect replaces each style object.
701
+ */
702
+ const captureNoRoleTextStyles = (
703
+ brandPageModel: BrandPageModel,
704
+ ): NoRoleTextStyles => {
705
+ const locations: NoRoleTextStyles = new WeakMap();
706
+ const record = (holder: Record<string, unknown>, key: string): void => {
707
+ if (!hasNoRole(holder[key])) return;
708
+
709
+ const keys = locations.get(holder) ?? new Set<string>();
710
+
711
+ keys.add(key);
712
+ locations.set(holder, keys);
713
+ };
714
+
715
+ for (const page of brandPageModel.pages ?? []) {
716
+ for (const content of page.contents ?? []) {
717
+ const fields = content as unknown as Record<string, unknown>;
718
+
719
+ for (const key of Object.keys(CONTENT_TEXT_STYLE_PROPERTY_KEYS)) {
720
+ record(fields, key);
721
+ }
722
+
723
+ const textItems = fields.textItems;
724
+
725
+ if (Array.isArray(textItems)) {
726
+ for (const item of textItems) {
727
+ if (item && typeof item === 'object') {
728
+ record(item as Record<string, unknown>, 'textStyle');
729
+ }
730
+ }
731
+ }
732
+ }
733
+ }
734
+
735
+ for (const footer of [
736
+ brandPageModel.pageFooterDesktop,
737
+ brandPageModel.pageFooterMobile,
738
+ ]) {
739
+ if (footer && typeof footer === 'object') {
740
+ record(footer as unknown as Record<string, unknown>, 'copyrightStyle');
741
+ }
742
+ }
743
+
744
+ return locations;
745
+ };
746
+
747
+ /**
748
+ * An RTE-eligible field that is a plain string, not a Tiptap doc. On an old site nothing
749
+ * converts plain text → doc (migration-20 only converts HTML), so such a field renders via
750
+ * `TextWrapper`'s PLAIN path from `style.color` — it has no run mark to hold colour. So its
751
+ * colour must be baked onto (D) and never stripped from (G) the style object; only a genuine
752
+ * doc's colour moves into a mark. A non-string field is NOT plain text: a real doc holds its
753
+ * colour in the mark, and an absent (`undefined`/`null`) field renders nothing — either way G
754
+ * safely strips the (now inert) style-object colour. An empty string `''` DOES count as plain
755
+ * text and is kept: a field later filled with plain text must not have lost its colour.
756
+ * `convertPlainRteFields` completes that conversion in this migration; regular runtime paths do not rewrap.
757
+ */
758
+ const isPlainTextValue = (value: unknown): value is string =>
759
+ typeof value === 'string';
760
+
761
+ /** A linked role's colour, read defensively (the typed property is retired). */
762
+ const roleColour = (
763
+ options: TextStyleOptions,
764
+ textStyleType: string | undefined,
765
+ ): string | undefined =>
766
+ textStyleType
767
+ ? (((
768
+ options[textStyleType as TextStyleType]?.style as unknown as
769
+ | Record<string, unknown>
770
+ | undefined
771
+ )?.color as string | undefined) ?? undefined)
772
+ : undefined;
773
+
774
+ /**
775
+ * D — freeze each text element's currently-resolved colour so removing the base coat (F)
776
+ * and the role colour (E) changes no pixels. migration-21 already baked element→frame→role
777
+ * colour into RTE runs, so the only uncoloured runs reaching here inherit the base coat;
778
+ * non-RTE fields (21 left them alone) resolve element→container-default→role→base coat.
779
+ * `buttonTextStyle` is excluded — the button label takes its colour from `buttonStyles`,
780
+ * never the base coat. A social-links section's unset icon fill is frozen here too.
781
+ */
782
+ export const bakeElementColours = (
783
+ brandPageModel: BrandPageModel,
784
+ options: TextStyleOptions,
785
+ noRoleTextStyles: NoRoleTextStyles = new WeakMap(),
786
+ ): void => {
787
+ const baseCoat = roleColour(options, 'paragraph1');
788
+
789
+ for (const page of brandPageModel.pages ?? []) {
790
+ for (const content of page.contents ?? []) {
791
+ const fields = content as unknown as Record<string, unknown>;
792
+ const containerDefaults = containerTextColorDefaults(
793
+ content,
794
+ brandPageModel.styles,
795
+ brandPageModel.brandPageType,
796
+ );
797
+ const rteStyleFields = new Set(rteStyleFieldsFor(content.type));
798
+
799
+ const resolvedColour = (
800
+ style: RawStyleObject | undefined,
801
+ holder: object,
802
+ key: string,
803
+ ): string | undefined =>
804
+ style?.color ||
805
+ containerDefaults[key as ContentTextStylePropertyKey] ||
806
+ (noRoleTextStyles.get(holder)?.has(key)
807
+ ? undefined
808
+ : roleColour(options, style?.textStyleType)) ||
809
+ baseCoat;
810
+
811
+ // Non-RTE style objects → bake onto `style.color`.
812
+ for (const key of Object.keys(CONTENT_TEXT_STYLE_PROPERTY_KEYS)) {
813
+ if (key === 'buttonTextStyle' || rteStyleFields.has(key)) continue;
814
+
815
+ const style = fields[key] as RawStyleObject | undefined;
816
+
817
+ if (!style || !isBlankColorValue(style.color)) continue;
818
+
819
+ const resolved = resolvedColour(style, fields, key);
820
+
821
+ if (!isBlankColorValue(resolved)) style.color = resolved;
822
+ }
823
+
824
+ // RTE-eligible fields. A real doc holds colour in its run marks → fill the runs the mark
825
+ // left uncoloured (the base-coat stragglers). A plain string (an old site never converted
826
+ // to a doc) has NO mark, so its colour lives on the style object → bake it there, like a
827
+ // non-RTE field; G then leaves that colour alone (it only strips when a doc holds it).
828
+ for (const styleField of rteStyleFields) {
829
+ const textField = pairedRteTextField(content.type, styleField);
830
+
831
+ if (!textField) continue;
832
+
833
+ const doc = fields[textField];
834
+
835
+ if (isPlainTextValue(doc)) {
836
+ const style = fields[styleField] as RawStyleObject | undefined;
837
+
838
+ if (style && isBlankColorValue(style.color)) {
839
+ const resolved = resolvedColour(style, fields, styleField);
840
+
841
+ if (!isBlankColorValue(resolved)) style.color = resolved;
842
+ }
843
+
844
+ continue;
845
+ }
846
+
847
+ if (!collectRteTextColors(doc)?.hasUncolouredRuns) continue;
848
+
849
+ const resolved = resolvedColour(
850
+ fields[styleField] as RawStyleObject | undefined,
851
+ fields,
852
+ styleField,
853
+ );
854
+
855
+ if (!isBlankColorValue(resolved)) {
856
+ fields[textField] =
857
+ fillUncolouredRteText(doc, resolved as string) ?? doc;
858
+ }
859
+ }
860
+
861
+ // Social icons had no colour of their own: they painted the base coat (the paragraphs
862
+ // seed, or `currentColor` under `:root`). Freeze it; with no base coat, the title's
863
+ // contrast colour, so every icon ends with a stored fill.
864
+ if (content.type === 'social-links') {
865
+ const iconFillStyles = (fields.iconFillStyles ?? {}) as Record<
866
+ string,
867
+ unknown
868
+ >;
869
+
870
+ if (isBlankColorValue(iconFillStyles.fill as string | undefined)) {
871
+ const fill = !isBlankColorValue(baseCoat)
872
+ ? baseCoat
873
+ : titleSurfaceContrastFill(
874
+ content,
875
+ brandPageModel.styles,
876
+ brandPageModel.brandPageType,
877
+ );
878
+
879
+ if (!isBlankColorValue(fill)) {
880
+ fields.iconFillStyles = { ...iconFillStyles, fill };
881
+ }
882
+ }
883
+ }
884
+
885
+ // brandSite text `textItems` — same rule per item (RTE doc, else the style object).
886
+ const textItems = fields.textItems as
887
+ | { text?: unknown; textStyle?: RawStyleObject }[]
888
+ | undefined;
889
+
890
+ if (Array.isArray(textItems)) {
891
+ for (const item of textItems) {
892
+ if (!item || typeof item !== 'object') continue;
893
+
894
+ const resolved = resolvedColour(item.textStyle, item, 'textStyle');
895
+
896
+ if (isBlankColorValue(resolved)) continue;
897
+
898
+ if (collectRteTextColors(item.text)?.hasUncolouredRuns) {
899
+ item.text =
900
+ fillUncolouredRteText(item.text, resolved as string) ?? item.text;
901
+ } else if (
902
+ item.textStyle &&
903
+ isBlankColorValue(item.textStyle.color)
904
+ ) {
905
+ item.textStyle.color = resolved;
906
+ }
907
+ }
908
+ }
909
+ }
910
+ }
911
+
912
+ // Footer copyright is non-RTE text that resolved through role → base coat, but lives
913
+ // OUTSIDE `pages[].contents`, so freeze it here too — else it loses its colour when E
914
+ // clears the role and F removes the base coat (the copyright-on-dark-footer regression).
915
+ for (const footer of [
916
+ brandPageModel.pageFooterDesktop,
917
+ brandPageModel.pageFooterMobile,
918
+ ]) {
919
+ const style = footer?.copyrightStyle as RawStyleObject | undefined;
920
+
921
+ if (!style || !isBlankColorValue(style.color)) continue;
922
+
923
+ const resolved =
924
+ (noRoleTextStyles.get(footer)?.has('copyrightStyle')
925
+ ? undefined
926
+ : roleColour(options, style.textStyleType)) || baseCoat;
927
+
928
+ if (!isBlankColorValue(resolved)) style.color = resolved;
929
+ }
930
+ };
931
+
932
+ /** E — clear the retired role colour from every role definition (run after D reads it). */
933
+ export const clearRoleColours = (options: TextStyleOptions): void => {
934
+ for (const key of Object.keys(options)) {
935
+ const style = options[key as TextStyleType]?.style as unknown as
936
+ | Record<string, unknown>
937
+ | undefined;
938
+
939
+ if (style && 'color' in style) delete style.color;
940
+ }
941
+ };
942
+
943
+ /**
944
+ * G — colour-decouple the RTE-paired style objects: remove ONLY `color` (D baked it into the
945
+ * mark). Everything else stays — a block-level typography override (a title set to a non-role
946
+ * font/size, `textAlign`, `applyAutoScale`, the role link) is genuine state the renderer
947
+ * applies via `overrideTypographyStyle`; dropping it would lose the override through migration.
948
+ * So the RTE style object becomes `Omit<TextStyleDeclaration, 'color'>`, not a bare role link.
949
+ * Non-RTE per-item styles (`headingStyle`, shapes/images `paragraphStyle`, `testimonialStyle`,
950
+ * …) are not RTE and are untouched.
951
+ */
952
+ const stripRteStyleColour = (style: RawStyleObject | undefined): void => {
953
+ if (style && 'color' in style) delete style.color;
954
+ };
955
+
956
+ export const stripRteStyleColours = (brandPageModel: BrandPageModel): void => {
957
+ for (const page of brandPageModel.pages ?? []) {
958
+ for (const content of page.contents ?? []) {
959
+ const fields = content as unknown as Record<string, unknown>;
960
+
961
+ for (const styleField of rteStyleFieldsFor(content.type)) {
962
+ const textField = pairedRteTextField(content.type, styleField);
963
+
964
+ // A plain-string field keeps its style-object colour — that's its only home (D baked it
965
+ // there, there's no mark). A real doc holds colour in its mark, so strip the object.
966
+ if (textField && isPlainTextValue(fields[textField])) continue;
967
+
968
+ stripRteStyleColour(fields[styleField] as RawStyleObject | undefined);
969
+ }
970
+
971
+ const textItems = fields.textItems as
972
+ | { text?: unknown; textStyle?: RawStyleObject }[]
973
+ | undefined;
974
+
975
+ if (Array.isArray(textItems)) {
976
+ for (const item of textItems) {
977
+ if (!item || typeof item !== 'object') continue;
978
+
979
+ if (isPlainTextValue(item.text)) continue; // plain-text item keeps its baked colour
980
+
981
+ stripRteStyleColour(item.textStyle);
982
+ }
983
+ }
984
+ }
985
+ }
986
+ };
987
+
988
+ /** H — delete the retired site text font and the footers' stray copies. */
989
+ export const deleteTextFontFamily = (brandPageModel: BrandPageModel): void => {
990
+ const holders = [
991
+ brandPageModel.styles?.textStyles,
992
+ brandPageModel.pageFooterDesktop,
993
+ brandPageModel.pageFooterMobile,
994
+ ] as unknown as (Record<string, unknown> | undefined)[];
995
+
996
+ for (const holder of holders) {
997
+ if (holder) delete holder['textFontFamily'];
998
+ }
999
+ };
1000
+
1001
+ /**
1002
+ * B1 — convert plain-text RTE-eligible fields to Tiptap docs, so every such field is a doc at
1003
+ * load and the editor never runs its plain→RTE "rewrap" (the runtime anti-pattern this retires).
1004
+ *
1005
+ * Only PLAIN strings are converted: migration-20 already did HTML→doc, and the plain-by-design
1006
+ * sections (`shop-section`/`request-payment`/`support-me`) are excluded because `rteStyleFieldsFor`
1007
+ * returns `[]` for them — they render via `TextWrapper`'s plain path and keep their `style.color`.
1008
+ * A converted field's colour is filled by D (into the mark) and its style-object colour stripped
1009
+ * by G, exactly like any other doc.
1010
+ *
1011
+ * Runs AFTER C (see orchestrator) so C only ever sees editor-baked docs and its per-run typography
1012
+ * bake operates on the real runs, not a freshly-converted placeholder.
1013
+ *
1014
+ * Error-tolerant like migration-20: a parse failure (or no DOM parser in this runtime) leaves the
1015
+ * plain string in place, and Path A's D/G plain-text branches keep its colour resolvable — so the
1016
+ * two together are total. `textAlign` is stamped onto the block node from the style object, as the
1017
+ * runtime conversion (`getUpdatedHTMLFromEditor`) does via `setTextAlign`.
1018
+ *
1019
+ * The converted run is then baked with this migration's frozen typography resolver, so it carries all
1020
+ * 7 typography attrs as a role var (follows role edits) or a literal override, not colour alone. Job C
1021
+ * never saw this field (it was still a plain string), so without this bake the run would carry only
1022
+ * colour and the maker toolbar (which reads the run, not the style object) would fall back to the role,
1023
+ * ignoring a block-level override (e.g. a banner title set to a non-role font/size). It stays
1024
+ * appearance-preserving: the fill is idempotent over an already-baked run, so the persisted doc renders
1025
+ * identically — it is now just self-describing.
1026
+ */
1027
+ const setBlockTextAlign = (
1028
+ doc: JSONContent,
1029
+ textAlign: string | undefined,
1030
+ ): void => {
1031
+ if (!textAlign) return;
1032
+
1033
+ for (const block of doc.content ?? []) {
1034
+ block.attrs = { ...(block.attrs ?? {}), textAlign };
1035
+ }
1036
+ };
1037
+
1038
+ // Snapshot the B1 resolver here rather than calling the live render helper. A versioned migration
1039
+ // defines one permanent v21 -> v22 transformation; later renderer fixes must not change its output.
1040
+ const MIGRATION_RTE_TYPOGRAPHY_ATTRS = [
1041
+ ['fontFamily', '--role-font-family'],
1042
+ ['fontSize', '--role-font-size'],
1043
+ ['lineHeight', '--role-line-height'],
1044
+ ['letterSpacing', '--role-letter-spacing'],
1045
+ ['fontWeight', '--role-font-weight'],
1046
+ ['fontStyle', '--role-font-style'],
1047
+ ['textDecoration', '--role-text-decoration'],
1048
+ ] as const;
1049
+
1050
+ type MigrationRteTypographyAttr =
1051
+ (typeof MIGRATION_RTE_TYPOGRAPHY_ATTRS)[number][0];
1052
+
1053
+ type MigrationRteTypography = Partial<
1054
+ Record<MigrationRteTypographyAttr, string>
1055
+ >;
1056
+
1057
+ const MIGRATION_RTE_NEUTRAL_TYPOGRAPHY: Partial<
1058
+ Record<MigrationRteTypographyAttr, string>
1059
+ > = {
1060
+ fontWeight: 'normal',
1061
+ fontStyle: 'normal',
1062
+ textDecoration: 'none',
1063
+ letterSpacing: 'normal',
1064
+ };
1065
+
1066
+ const UNQUOTED_FONT_FAMILY_TOKEN = /^-?[A-Za-z_][A-Za-z0-9_-]*$/;
1067
+
1068
+ const migrationCssFontFamilyValue = (value: string): string =>
1069
+ value
1070
+ .split(',')
1071
+ .map((raw) => {
1072
+ const family = raw.trim();
1073
+
1074
+ if (
1075
+ !family ||
1076
+ family.startsWith('"') ||
1077
+ family.startsWith("'") ||
1078
+ family.startsWith('var(')
1079
+ ) {
1080
+ return family;
1081
+ }
1082
+
1083
+ return family
1084
+ .split(/\s+/)
1085
+ .every((token) => UNQUOTED_FONT_FAMILY_TOKEN.test(token))
1086
+ ? family
1087
+ : `"${family}"`;
1088
+ })
1089
+ .filter(Boolean)
1090
+ .join(', ');
1091
+
1092
+ type MigrationRteNode = {
1093
+ type?: string;
1094
+ marks?: { type: string; attrs?: Record<string, unknown> }[];
1095
+ content?: MigrationRteNode[];
1096
+ };
1097
+
1098
+ const migrationRoleVarRef = (attr: MigrationRteTypographyAttr): string => {
1099
+ const variable = MIGRATION_RTE_TYPOGRAPHY_ATTRS.find(
1100
+ ([name]) => name === attr,
1101
+ )?.[1];
1102
+
1103
+ return `var(${variable})`;
1104
+ };
1105
+
1106
+ const migrationRunTypographyFillValue = (
1107
+ attr: MigrationRteTypographyAttr,
1108
+ roleStyle: MigrationRteTypography | undefined,
1109
+ style: MigrationRteTypography | undefined,
1110
+ ): string | undefined => {
1111
+ const roleValue = roleStyle?.[attr];
1112
+ const blockValue = style?.[attr];
1113
+ const neutral = MIGRATION_RTE_NEUTRAL_TYPOGRAPHY[attr];
1114
+ const hasRole = roleValue != null && roleValue !== '';
1115
+ const hasBlock = blockValue != null && blockValue !== '';
1116
+
1117
+ if (roleStyle == null) return hasBlock ? blockValue : undefined;
1118
+
1119
+ const effectiveRole = hasRole ? roleValue : neutral;
1120
+
1121
+ if (hasBlock && blockValue !== effectiveRole) return blockValue;
1122
+
1123
+ return hasRole || neutral != null ? migrationRoleVarRef(attr) : undefined;
1124
+ };
1125
+
1126
+ const fillMigration22RunTypography = (
1127
+ doc: JSONContent,
1128
+ roleStyle: MigrationRteTypography | undefined,
1129
+ style: MigrationRteTypography | undefined,
1130
+ ): JSONContent => {
1131
+ const clone = JSON.parse(JSON.stringify(doc)) as MigrationRteNode;
1132
+ const runs: MigrationRteNode[] = [];
1133
+
1134
+ const collect = (node: MigrationRteNode | undefined): void => {
1135
+ if (!node) return;
1136
+
1137
+ if (node.type === 'text') runs.push(node);
1138
+
1139
+ node.content?.forEach(collect);
1140
+ };
1141
+
1142
+ collect(clone);
1143
+
1144
+ for (const run of runs) {
1145
+ let mark = run.marks?.find((candidate) => candidate.type === 'textStyle');
1146
+
1147
+ for (const [attr] of MIGRATION_RTE_TYPOGRAPHY_ATTRS) {
1148
+ const existing = mark?.attrs?.[attr];
1149
+
1150
+ if (existing !== undefined && existing !== null && existing !== '')
1151
+ continue;
1152
+
1153
+ let value = migrationRunTypographyFillValue(attr, roleStyle, style);
1154
+
1155
+ if (value == null) continue;
1156
+
1157
+ if (attr === 'fontFamily') value = migrationCssFontFamilyValue(value);
1158
+
1159
+ if (!mark) {
1160
+ mark = { type: 'textStyle', attrs: {} };
1161
+ (run.marks ??= []).push(mark);
1162
+ }
1163
+
1164
+ mark.attrs ??= {};
1165
+ mark.attrs[attr] = value;
1166
+ }
1167
+ }
1168
+
1169
+ return clone as JSONContent;
1170
+ };
1171
+
1172
+ // B1 converts a plain string after job C has run, so it needs C3's not-bold protection locally
1173
+ // too. Keep the whole B1 transform frozen: changing `rte-role-lift.util.ts` must not alter v22.
1174
+ const pinMigration22RunWeight = (
1175
+ doc: JSONContent,
1176
+ roleWeight: string | undefined,
1177
+ blockWeight: string | undefined,
1178
+ ): void => {
1179
+ if (
1180
+ blockWeight === undefined ||
1181
+ blockWeight === null ||
1182
+ blockWeight === '' ||
1183
+ blockWeight === roleWeight
1184
+ ) {
1185
+ return;
1186
+ }
1187
+
1188
+ const runs: MigrationRteNode[] = [];
1189
+ const collect = (node: MigrationRteNode | undefined): void => {
1190
+ if (!node) return;
1191
+
1192
+ if (node.type === 'text') runs.push(node);
1193
+
1194
+ node.content?.forEach(collect);
1195
+ };
1196
+
1197
+ collect(doc as MigrationRteNode);
1198
+
1199
+ for (const run of runs) {
1200
+ let mark = run.marks?.find((candidate) => candidate.type === 'textStyle');
1201
+
1202
+ if (mark?.attrs?.fontWeight != null && mark.attrs.fontWeight !== '')
1203
+ continue;
1204
+
1205
+ if (!mark) {
1206
+ mark = { type: 'textStyle', attrs: {} };
1207
+ (run.marks ??= []).push(mark);
1208
+ }
1209
+
1210
+ mark.attrs ??= {};
1211
+ mark.attrs.fontWeight = blockWeight;
1212
+ }
1213
+ };
1214
+
1215
+ export const convertPlainRteFields = async (
1216
+ brandPageModel: BrandPageModel,
1217
+ options: TextStyleOptions,
1218
+ ): Promise<void> => {
1219
+ let parser: HtmlParser;
1220
+
1221
+ try {
1222
+ parser = await resolveHtmlParser();
1223
+ } catch (error) {
1224
+ // No DOM parser here (e.g. jsdom failed to load on the server). Skip conversion — plain
1225
+ // strings survive to D/G, whose Path A branches keep their colour. Mirrors migration-20.
1226
+ console.error(
1227
+ 'migration-22: no HTML parser available, skipping plain→RTE conversion',
1228
+ error,
1229
+ );
1230
+
1231
+ return;
1232
+ }
1233
+
1234
+ const convert = (
1235
+ value: unknown,
1236
+ style: RawStyleObject | undefined,
1237
+ ): JSONContent | undefined => {
1238
+ if (!isPlainTextValue(value) || value.length === 0) return undefined;
1239
+
1240
+ try {
1241
+ // Pin the schema to v22 too: B1's structure and its attr-fill snapshot together define
1242
+ // the permanent v21 -> v22 result.
1243
+ const doc = plainTextToRteJson(value, parser, 22);
1244
+
1245
+ if (!doc) return undefined;
1246
+
1247
+ setBlockTextAlign(doc, style?.textAlign);
1248
+
1249
+ // Bake the block override + role typography onto the bare run (job C skipped this field
1250
+ // while it was a plain string) so the doc is self-describing for the maker toolbar.
1251
+ const roleStyle = style?.textStyleType
1252
+ ? (options[style.textStyleType as TextStyleType]?.style as
1253
+ | MigrationRteTypography
1254
+ | undefined)
1255
+ : undefined;
1256
+
1257
+ // C3 (before the fill): pin a not-bold legacy block's weight as a literal so the fill can't
1258
+ // treat its `normal` as inert and let the run inherit a bold heading tier. The plain string had
1259
+ // no bold marks, so every run is bare — the fill then keeps this literal and completes the rest.
1260
+ pinMigration22RunWeight(doc, roleStyle?.fontWeight, style?.fontWeight);
1261
+
1262
+ return fillMigration22RunTypography(
1263
+ doc,
1264
+ roleStyle,
1265
+ style as MigrationRteTypography,
1266
+ );
1267
+ } catch (error) {
1268
+ // Leave the plain string; Path A keeps its colour resolvable.
1269
+ console.error(
1270
+ 'migration-22: failed to convert plain RTE field, leaving as text',
1271
+ error,
1272
+ );
1273
+
1274
+ return undefined;
1275
+ }
1276
+ };
1277
+
1278
+ for (const page of brandPageModel.pages ?? []) {
1279
+ for (const content of page.contents ?? []) {
1280
+ const fields = content as unknown as Record<string, unknown>;
1281
+
1282
+ for (const styleField of rteStyleFieldsFor(content.type)) {
1283
+ const textField = pairedRteTextField(content.type, styleField);
1284
+
1285
+ if (!textField) continue;
1286
+
1287
+ const style = fields[styleField] as RawStyleObject | undefined;
1288
+ const doc = convert(fields[textField], style);
1289
+
1290
+ if (doc) fields[textField] = doc;
1291
+ }
1292
+
1293
+ const textItems = fields.textItems as
1294
+ | { text?: unknown; textStyle?: RawStyleObject }[]
1295
+ | undefined;
1296
+
1297
+ if (Array.isArray(textItems)) {
1298
+ for (const item of textItems) {
1299
+ if (!item || typeof item !== 'object') continue;
1300
+
1301
+ const doc = convert(item.text, item.textStyle);
1302
+
1303
+ if (doc) item.text = doc;
1304
+ }
1305
+ }
1306
+ }
1307
+ }
1308
+ };
1309
+
1310
+ export const migration22 = async (
1311
+ migrationResult: MigrationData,
1312
+ _options: MigrationOptions,
1313
+ ): Promise<MigrationData> => {
1314
+ const { brandPageModel } = migrationResult;
1315
+ const options = brandPageModel.styles?.textStyles?.textStyleOptions;
1316
+
1317
+ if (options) {
1318
+ const targets = computeLegacyTargets(options); // per-site legacy → nearest tier
1319
+
1320
+ seedRoleColours(options, targets); // A — seed each old role's colour onto its tier
1321
+ seedRoleFonts(brandPageModel, options); // A2 — count the site's fonts onto each role family
1322
+
1323
+ const noRoleTextStyles = captureNoRoleTextStyles(brandPageModel);
1324
+
1325
+ reconnectContentRoles(brandPageModel, targets, options); // B — seed button styles, reconnect every text field
1326
+ stripLiftContentRte(brandPageModel, options); // C — RTE baked → override
1327
+ await convertPlainRteFields(brandPageModel, options); // B1 — plain text → Tiptap doc (after C)
1328
+ bakeElementColours(brandPageModel, options, noRoleTextStyles); // D — freeze resolved colour
1329
+ clearRoleColours(options); // E — retire role colour
1330
+ stripRteStyleColours(brandPageModel); // G — colour-decouple RTE style objects
1331
+ }
1332
+
1333
+ deleteTextFontFamily(brandPageModel); // H — after every reader of the site text font
1334
+
1335
+ return migrationResult;
1336
+ };