@designcrowd/library.brand-page 5.19.1-error-html2canvas-2 → 5.19.1-id-based-items-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-DmKe7W8L.js → CreatedOnBrandCrowd-de-de-0PcGqy0N.js} +3 -3
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-D2EeiNep.js → CreatedOnBrandCrowd-en-us-DB8wcFMI.js} +3 -3
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-BqVwNY1D.js → CreatedOnBrandCrowd-es-es-DVWQ243C.js} +3 -3
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-C756a27-.js → CreatedOnBrandCrowd-fr-ca-D-M_L0Du.js} +3 -3
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-C756a27-.js → CreatedOnBrandCrowd-fr-fr-D-M_L0Du.js} +3 -3
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-FNHAnZUw.js → CreatedOnBrandCrowd-pt-br-CNpzknxU.js} +3 -3
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-FNHAnZUw.js → CreatedOnBrandCrowd-pt-pt-CNpzknxU.js} +3 -3
  8. package/lib/src/{CreatedOnDcom-de-de-8AZZFmDt.js → CreatedOnDcom-de-de-DMJoD5wE.js} +3 -3
  9. package/lib/src/{CreatedOnDcom-en-us-DizTP32i.js → CreatedOnDcom-en-us-B-F3Pmjv.js} +3 -3
  10. package/lib/src/{CreatedOnDcom-es-es-qnp9_n_A.js → CreatedOnDcom-es-es-DmLk-cA2.js} +3 -3
  11. package/lib/src/{CreatedOnDcom-fr-ca-jUs352uK.js → CreatedOnDcom-fr-ca-ntnsCzvA.js} +3 -3
  12. package/lib/src/{CreatedOnDcom-fr-fr-CTxXtHyk.js → CreatedOnDcom-fr-fr-DFcO0gHL.js} +3 -3
  13. package/lib/src/{CreatedOnDcom-pt-br-nQHj8P7d.js → CreatedOnDcom-pt-br-CtuJoVXW.js} +3 -3
  14. package/lib/src/{CreatedOnDcom-pt-pt-DJmVYQuL.js → CreatedOnDcom-pt-pt-B7shZVVn.js} +3 -3
  15. package/lib/src/brand-page/clients/brand-page.client.d.ts +9 -9
  16. package/lib/src/brand-page/components/image-gallery/masonry/MasonryGallery.mixin.d.ts +2 -1
  17. package/lib/src/brand-page/components/navigation/hamburger/HamburgerNavigation.mixin.d.ts +2 -4
  18. package/lib/src/brand-page/components/navigation/hamburger/HamburgerNavigation.mixin.js +0 -1
  19. package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.d.ts +2 -4
  20. package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.js +4 -5
  21. package/lib/src/brand-page/components/page/Page.mixin.d.ts +1 -1
  22. package/lib/src/brand-page/components/page/PageComponent.mixin.d.ts +1 -1
  23. package/lib/src/brand-page/components/payment/Payment.mixin.d.ts +3 -3
  24. package/lib/src/brand-page/components/payment/__fixtures__/request-payment.d.ts +13 -13
  25. package/lib/src/brand-page/components/payment/__fixtures__/support-me.d.ts +13 -13
  26. package/lib/src/brand-page/constants/sections/footer-display.constants.js +1 -1
  27. package/lib/src/brand-page/models/index.d.ts +13 -2
  28. package/lib/src/brand-page/models/index.js +0 -3
  29. package/lib/src/brand-page/models/payment.model.d.ts +1 -1
  30. package/lib/src/brand-page/utils/personalisation.util.js +14 -72
  31. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +19 -21
  32. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.js +0 -1
  33. package/lib/src/brand-page-maker/components/admin-background-settings/AdminBackgroundSettings.mixin.d.ts +10 -16
  34. package/lib/src/brand-page-maker/components/admin-logo-settings/AdminLogoSettings.mixin.d.ts +3 -3
  35. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +170 -181
  36. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.js +0 -3
  37. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.stories.js +0 -1
  38. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMakerTemplateSettings.mixin.d.ts +12 -12
  39. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMakerTemplateSettings.stories.js +2 -2
  40. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-payment.js +7 -0
  41. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +149 -148
  42. package/lib/src/brand-page-maker/components/brandpage-settings/ReplaceImageSettingsForBrandPageSettings.mixin.d.ts +21 -21
  43. package/lib/src/brand-page-maker/components/brandpage-settings/ReplaceImageSettingsForBrandPageSettings.mixin.js +1 -1
  44. package/lib/src/brand-page-maker/components/color-picker/components/ColorModeSelect.mixin.d.ts +2 -4
  45. package/lib/src/brand-page-maker/components/color-picker/components/ColorModeSelect.mixin.js +0 -1
  46. package/lib/src/brand-page-maker/components/color-picker/components/GradientColorSlider.mixin.d.ts +2 -4
  47. package/lib/src/brand-page-maker/components/color-picker/components/GradientColorSlider.mixin.js +0 -1
  48. package/lib/src/brand-page-maker/components/color-picker/components/GradientColorSliderHandler.mixin.d.ts +2 -5
  49. package/lib/src/brand-page-maker/components/color-picker/components/GradientColorSliderHandler.mixin.js +0 -1
  50. package/lib/src/brand-page-maker/components/color-picker/components/GradientTypeSelector.mixin.d.ts +2 -4
  51. package/lib/src/brand-page-maker/components/color-picker/components/GradientTypeSelector.mixin.js +0 -1
  52. package/lib/src/brand-page-maker/components/device/Device.mixin.d.ts +2 -4
  53. package/lib/src/brand-page-maker/components/device/Device.mixin.js +0 -1
  54. package/lib/src/brand-page-maker/components/navigation/Navigation.mixin.d.ts +2 -4
  55. package/lib/src/brand-page-maker/components/navigation/Navigation.mixin.js +0 -1
  56. package/lib/src/brand-page-maker/components/preview/Preview.mixin.d.ts +3 -3
  57. package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.d.ts +11 -11
  58. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +25 -27
  59. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.js +0 -1
  60. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +27 -27
  61. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.js +1 -1
  62. package/lib/src/brand-page-maker/components/seo-settings/UseExistingDomainModal.mixin.d.ts +12 -17
  63. package/lib/src/brand-page-maker/components/social-share/SocialShare.mixin.d.ts +12 -12
  64. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +32 -30
  65. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.js +5 -16
  66. package/lib/src/brand-page-maker/components/upgrade-to-premium-button/UpgradeToPremiumButton.mixin.d.ts +11 -11
  67. package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +1 -1
  68. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +1 -1
  69. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +6 -6
  70. package/lib/src/brand-page-maker/composables/useEventTracker.d.ts +2 -2
  71. package/lib/src/brand-page-maker/constants/config-settings-constant.d.ts +6 -6
  72. package/lib/src/brand-page-maker/constants/config-settings-constant.js +1 -1
  73. package/lib/src/brand-page-maker/convertors/text-image-display-style.convertor.d.ts +1 -1
  74. package/lib/src/brand-page-maker/convertors/text-image-display-style.convertor.js +1 -1
  75. package/lib/src/brand-page-maker/models/content-settings.d.ts +1 -1
  76. package/lib/src/brand-page-maker/models/navigation-style-configs.js +1 -1
  77. package/lib/src/brand-page-maker/routing/brand-page-maker-router.d.ts +2 -1
  78. package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +378 -366
  79. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.d.ts +4 -5
  80. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.js +5 -8
  81. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.d.ts +1 -3
  82. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +3 -7
  83. package/lib/src/brand-page-maker/stores/history/history-store.d.ts +15 -15
  84. package/lib/src/brand-page-maker/stores/ui/ui-store.d.ts +0 -6
  85. package/lib/src/brand-page-maker/stores/ui/ui-store.js +0 -8
  86. package/lib/src/brand-page-maker/utils/content-settings.util.js +1 -1
  87. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +1 -1
  88. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +4 -4
  89. package/lib/src/clients/brand-page-client.d.ts +15 -16
  90. package/lib/src/clients/design-template-client.d.ts +0 -3
  91. package/lib/src/clients/media-client.d.ts +2 -2
  92. package/lib/src/clients/models.d.ts +0 -2
  93. package/lib/src/composables/useSnapshot.js +18 -18
  94. package/lib/src/css/library.brand-page-brandCrowd.min.css +324 -342
  95. package/lib/src/css/library.brand-page-brandPage.min.css +324 -620
  96. package/lib/src/css/library.brand-page-designCom.min.css +324 -342
  97. package/lib/src/css/library.brand-page-preview.min.css +324 -598
  98. package/lib/src/index.d.ts +1 -1
  99. package/lib/src/index.mjs +15 -16
  100. package/lib/src/{lib-DityivZa.js → lib-BQmJBVbS.js} +33238 -36458
  101. package/lib/src/lib.js +1 -1
  102. package/lib/src/metadata-builder/builders/footer-builder.js +56 -91
  103. package/lib/src/metadata-builder/builders/item-id-utils.d.ts +41 -0
  104. package/lib/src/metadata-builder/builders/item-id-utils.js +68 -0
  105. package/lib/src/metadata-builder/builders/section-builders/image-gallery-builder.js +57 -68
  106. package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +58 -93
  107. package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +66 -99
  108. package/lib/src/metadata-builder/builders/section-builders/social-links-builder.js +57 -17
  109. package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +51 -85
  110. package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +49 -74
  111. package/lib/src/metadata-builder/builders/shape-color-utils.js +1 -1
  112. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +13 -4
  113. package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +66 -48
  114. package/lib/src/plugins/google-maps-plugin/google-maps-plugin.d.ts +1 -1
  115. package/lib/src/plugins/google-maps-plugin/google-maps-plugin.js +8 -8
  116. package/lib/src/shared/background-settings/components/background-shapes/BackgroundShapeSettings.mixin.d.ts +13 -13
  117. package/lib/src/shared/background-settings/store/background-settings-store.d.ts +3 -3
  118. package/lib/src/shared/color-picker/components/ColorPicker.mixin.d.ts +2 -4
  119. package/lib/src/shared/color-picker/components/ColorPicker.mixin.js +2 -2
  120. package/lib/src/shared/color-picker/components/ColorPickerSwatch.mixin.d.ts +2 -4
  121. package/lib/src/shared/color-picker/components/ColorPickerSwatch.mixin.js +0 -1
  122. package/lib/src/shared/confirm-modal/ConfirmModal.mixin.d.ts +2 -5
  123. package/lib/src/shared/confirm-modal/ConfirmModal.mixin.js +0 -1
  124. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +1 -0
  125. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +129 -131
  126. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.js +1 -2
  127. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +110 -111
  128. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +81 -84
  129. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.js +0 -5
  130. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +109 -117
  131. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +14 -1
  132. package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +1 -0
  133. package/lib/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.d.ts +70 -72
  134. package/lib/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.js +3 -4
  135. package/lib/src/shared/content-settings/components/form-fields-field/FormFieldsField.mixin.d.ts +4 -4
  136. package/lib/src/shared/content-settings/components/icon-library/IconLibrary.mixin.d.ts +1 -3
  137. package/lib/src/shared/content-settings/components/icon-library/IconLibrary.mixin.js +0 -1
  138. package/lib/src/shared/content-settings/components/images-field/ImagesField.mixin.d.ts +2 -2
  139. package/lib/src/shared/content-settings/components/items-field/ItemsField.mixin.d.ts +3 -4
  140. package/lib/src/shared/content-settings/components/items-field/ItemsField.mixin.js +1 -1
  141. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +40 -40
  142. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigOptionField.mixin.d.ts +6 -5
  143. package/lib/src/shared/content-settings/components/shape-field/ShapeField.mixin.d.ts +1 -0
  144. package/lib/src/shared/content-settings/components/slider-field/SliderField.mixin.d.ts +2 -2
  145. package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +1 -0
  146. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.d.ts +2 -1
  147. package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +1 -0
  148. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +2 -1
  149. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +1 -1
  150. package/lib/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.d.ts +28 -28
  151. package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +1 -0
  152. package/lib/src/shared/content-settings/components/toggle-field/ToggleField.mixin.d.ts +2 -2
  153. package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +1 -0
  154. package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +1 -0
  155. package/lib/src/shared/content-settings/store/content-settings-store.d.ts +39 -39
  156. package/lib/src/shared/content-settings/utils.js +1 -0
  157. package/lib/src/shared/font-family-selector/FontFamilySelector.mixin.d.ts +4 -6
  158. package/lib/src/shared/font-family-selector/FontFamilySelector.mixin.js +0 -1
  159. package/lib/src/shared/image-gallery-style-settings/ImageGalleryStyleSettings.mixin.d.ts +2 -4
  160. package/lib/src/shared/image-gallery-style-settings/ImageGalleryStyleSettings.mixin.js +0 -1
  161. package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.d.ts +21 -29
  162. package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.js +1 -1
  163. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +51 -50
  164. package/lib/src/shared/logo-settings/components/LogoSettingsHeader.mixin.d.ts +4 -6
  165. package/lib/src/shared/logo-settings/components/LogoSettingsHeader.mixin.js +0 -1
  166. package/lib/src/shared/logo-settings/store/editable-logo-store.js +1 -1
  167. package/lib/src/shared/logo-settings/store/logo-settings-store.d.ts +1 -1
  168. package/lib/src/shared/logo-versions/LogoVersions.mixin.d.ts +5 -13
  169. package/lib/src/shared/logo-versions/LogoVersions.mixin.js +0 -1
  170. package/lib/src/shared/models.d.ts +0 -2
  171. package/lib/src/shared/my-designs/MyDesigns.mixin.d.ts +6 -14
  172. package/lib/src/shared/my-designs/MyDesigns.mixin.js +0 -1
  173. package/lib/src/shared/my-uploads/MyMedia.mixin.d.ts +5 -14
  174. package/lib/src/shared/my-uploads/MyMedia.mixin.js +0 -1
  175. package/lib/src/shared/my-uploads/MyUploads.mixin.d.ts +5 -14
  176. package/lib/src/shared/my-uploads/MyUploads.mixin.js +0 -1
  177. package/lib/src/shared/navigation-style-settings/components/navigation-style-settings/NavigationStyleSettings.mixin.d.ts +3 -5
  178. package/lib/src/shared/navigation-style-settings/components/navigation-style-settings/NavigationStyleSettings.mixin.js +0 -1
  179. package/lib/src/shared/navigation-style-settings/store/index.d.ts +2 -2
  180. package/lib/src/shared/page-navigation/store/page-navigation-store.d.ts +2 -2
  181. package/lib/src/shared/page-settings/components/new-page-modal/NewPageModal.mixin.d.ts +28 -27
  182. package/lib/src/shared/page-settings/components/new-page-modal/NewPageModal.mixin.js +2 -0
  183. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +179 -188
  184. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +177 -181
  185. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.js +1 -1
  186. package/lib/src/shared/page-settings/store/page-settings-store.d.ts +9 -9
  187. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +39 -39
  188. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.js +1 -0
  189. package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +18 -18
  190. package/lib/src/shared/side-bar/components/SideBarTabButton.mixin.d.ts +6 -6
  191. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +60 -66
  192. package/lib/src/shared/style-settings/store/style-settings-store.d.ts +1 -1
  193. package/lib/src/shared/style-settings/store/style-settings-store.js +1 -1
  194. package/lib/src/shared/template-settings/components/TemplatesSettings.mixin.d.ts +450 -20
  195. package/lib/src/shared/template-settings/components/TemplatesSettings.mixin.js +2 -2
  196. package/lib/src/shared/template-settings/models.d.ts +0 -1
  197. package/lib/src/shared/template-settings/store/template-settings-store.d.ts +0 -4
  198. package/lib/src/shared/template-settings/store/template-settings-store.js +11 -79
  199. package/lib/src/shared/unsplash-library/UnsplashLibrary.mixin.d.ts +4 -9
  200. package/package.json +57 -61
  201. package/src/brand-page/components/about-section/AboutSection.vue.test.ts +5 -7
  202. package/src/brand-page/components/brand-page-footer/BrandPageFooter.vue +1 -1
  203. package/src/brand-page/components/image/Image.mixin.ts +1 -1
  204. package/src/brand-page/components/image/Image.test.ts +1 -1
  205. package/src/brand-page/components/image-gallery/masonry/MasonryGallery.mixin.ts +2 -1
  206. package/src/brand-page/components/images-section/ImagesSection.vue +1 -0
  207. package/src/brand-page/components/logo/LogoRender.vue +188 -188
  208. package/src/brand-page/components/navigation/hamburger/HamburgerNavigation.mixin.ts +0 -1
  209. package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.ts +6 -15
  210. package/src/brand-page/components/page-footer/PageFooter.vue +0 -1
  211. package/src/brand-page/components/page-header/HeaderLayout.vue +6 -12
  212. package/src/brand-page/components/page-header/PageHeader.vue +0 -1
  213. package/src/brand-page/components/shapes-section/ShapesSection.vue +1 -1
  214. package/src/brand-page/components/shop/CartModal.vue +3 -1
  215. package/src/brand-page/components/shop/FeaturedDisplay.vue +2 -2
  216. package/src/brand-page/components/shop/PaymentModal.vue +5 -1
  217. package/src/brand-page/components/shop/ProductDetailModal.vue +1 -1
  218. package/src/brand-page/components/shop/Shop.vue +1 -1
  219. package/src/brand-page/components/shop/TextDisplay.vue +1 -1
  220. package/src/brand-page/components/shop/ThumbnailDisplay.vue +2 -2
  221. package/src/brand-page/components/testimonial-section/TestimonialSection.vue +3 -3
  222. package/src/brand-page/components/text-wrapper/TextWrapper.vue +1 -3
  223. package/src/brand-page/constants/sections/footer-display.constants.ts +1 -1
  224. package/src/brand-page/models/index.ts +12 -4
  225. package/src/brand-page/models/payment.model.ts +1 -2
  226. package/src/brand-page/models/shop.model.ts +2 -0
  227. package/src/brand-page/models/user-personalisation.model.ts +2 -1
  228. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +0 -3
  229. package/src/brand-page/utils/__snapshots__/migration2.util.test.ts.snap +0 -1
  230. package/src/brand-page/utils/__snapshots__/model.util.test.ts.snap +0 -2
  231. package/src/brand-page/utils/migration/migration-01_1.ts +1 -1
  232. package/src/brand-page/utils/migration/migration-02.ts +1 -0
  233. package/src/brand-page/utils/migration/migration-03.ts +1 -0
  234. package/src/brand-page/utils/migration/migration-04.ts +1 -0
  235. package/src/brand-page/utils/migration/migration-05.ts +5 -1
  236. package/src/brand-page/utils/migration/migration-05_2.ts +1 -1
  237. package/src/brand-page/utils/migration/migration-08.test.ts +1 -1
  238. package/src/brand-page/utils/personalisation.util.test.ts +0 -35
  239. package/src/brand-page/utils/personalisation.util.ts +23 -119
  240. package/src/brand-page/view-models.ts +56 -38
  241. package/src/brand-page-maker/assessors/banner-section-description-visibility.assessor.ts +6 -6
  242. package/src/brand-page-maker/assessors/color-replacement-icon-visibility.assessor.ts +2 -2
  243. package/src/brand-page-maker/assessors/color-replacement-profile-visibility.assessor.ts +2 -2
  244. package/src/brand-page-maker/assessors/color-replacement-visibility.assessor.ts +2 -2
  245. package/src/brand-page-maker/assessors/contact-form-visibility.assessor.ts +14 -14
  246. package/src/brand-page-maker/components/action-bar/ActionBar.mixin.ts +0 -1
  247. package/src/brand-page-maker/components/ai-text-edit/TextBubble.vue +0 -1
  248. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +1 -5
  249. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.stories.ts +0 -1
  250. package/src/brand-page-maker/components/brand-page-maker/BrandPageMakerTemplateSettings.stories.ts +5 -2
  251. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.ts +5 -0
  252. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-payment.ts +8 -0
  253. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-site-template.examples.ts +2 -0
  254. package/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.vue +0 -1
  255. package/src/brand-page-maker/components/brandpage-settings/ReplaceImageSettingsForBrandPageSettings.mixin.ts +1 -1
  256. package/src/brand-page-maker/components/color-palette/BpColorPalette.vue +2 -1
  257. package/src/brand-page-maker/components/color-picker/components/ColorModeSelect.mixin.ts +0 -1
  258. package/src/brand-page-maker/components/color-picker/components/GradientColorSlider.mixin.ts +0 -1
  259. package/src/brand-page-maker/components/color-picker/components/GradientColorSliderHandler.mixin.ts +0 -1
  260. package/src/brand-page-maker/components/color-picker/components/GradientTypeSelector.mixin.ts +0 -1
  261. package/src/brand-page-maker/components/device/Device.mixin.ts +0 -1
  262. package/src/brand-page-maker/components/navigation/Navigation.mixin.ts +0 -1
  263. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +1 -3
  264. package/src/brand-page-maker/components/sensitive-content/SensitiveContent.vue +4 -1
  265. package/src/brand-page-maker/components/seo-settings/DeleteDomainModal.vue +1 -1
  266. package/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.ts +0 -1
  267. package/src/brand-page-maker/components/seo-settings/SeoPageSettings.vue +1 -1
  268. package/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.ts +1 -0
  269. package/src/brand-page-maker/components/seo-settings/UrlSettings.vue +1 -0
  270. package/src/brand-page-maker/components/seo-settings/UseExistingDomainModal.vue +5 -1
  271. package/src/brand-page-maker/components/shop/AddOrEditProductContainer.vue +1 -1
  272. package/src/brand-page-maker/components/shop/ReplaceImageSettingsForShop.vue +1 -1
  273. package/src/brand-page-maker/components/social-share/SocialShare.vue +1 -1
  274. package/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.ts +8 -22
  275. package/src/brand-page-maker/components/template-preview/TemplatePreview.vue +10 -5
  276. package/src/brand-page-maker/components/video-play/VideoPlayButton.vue +1 -1
  277. package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +1 -1
  278. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +6 -6
  279. package/src/brand-page-maker/constants/config-settings-constant.ts +3 -3
  280. package/src/brand-page-maker/convertors/text-image-display-style.convertor.ts +1 -1
  281. package/src/brand-page-maker/models/button-styles-config.ts +2 -4
  282. package/src/brand-page-maker/models/content-settings-configs.ts +2 -4
  283. package/src/brand-page-maker/models/navigation-style-configs.ts +6 -5
  284. package/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store.ts +1 -0
  285. package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.ts +5 -8
  286. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.test.ts +3 -1
  287. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +4 -12
  288. package/src/brand-page-maker/stores/ui/ui-store.ts +0 -16
  289. package/src/brand-page-maker/utils/content-settings.util.ts +1 -1
  290. package/src/brand-page-maker/utils/rich-text-editor.util.ts +4 -4
  291. package/src/clients/design-template-client.ts +0 -3
  292. package/src/clients/models.ts +0 -2
  293. package/src/composables/useSnapshot.ts +22 -19
  294. package/src/lib.ts +0 -1
  295. package/src/metadata-builder/builders/footer-builder.test.ts +245 -84
  296. package/src/metadata-builder/builders/footer-builder.ts +81 -130
  297. package/src/metadata-builder/builders/item-id-utils.test.ts +118 -0
  298. package/src/metadata-builder/builders/item-id-utils.ts +85 -0
  299. package/src/metadata-builder/builders/section-builders/image-gallery-builder.ts +62 -88
  300. package/src/metadata-builder/builders/section-builders/images-section-builder.ts +66 -129
  301. package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +105 -160
  302. package/src/metadata-builder/builders/section-builders/social-links-builder.ts +65 -20
  303. package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +83 -146
  304. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +70 -122
  305. package/src/metadata-builder/builders/shape-color-utils.ts +2 -2
  306. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +13 -4
  307. package/src/metadata-builder/parsers/section-tool-arguments.ts +66 -48
  308. package/src/plugins/google-maps-plugin/google-maps-plugin.ts +8 -8
  309. package/src/shared/background-settings/components/BackgroundSettings.vue +1 -1
  310. package/src/shared/background-settings/components/background-color/BackgroundColorSettings.vue +1 -1
  311. package/src/shared/color-picker/components/ColorPicker.mixin.ts +7 -2
  312. package/src/shared/color-picker/components/ColorPickerSwatch.mixin.ts +0 -1
  313. package/src/shared/common/NumberInput.vue +0 -1
  314. package/src/shared/common/SideBarSettingsHeader.vue +2 -2
  315. package/src/shared/confirm-modal/ConfirmModal.mixin.ts +0 -1
  316. package/src/shared/content-settings/components/booking-url-field/BookingUrlField.vue +0 -1
  317. package/src/shared/content-settings/components/button-configuration-field/ButtonConfigurationField.vue +0 -1
  318. package/src/shared/content-settings/components/color-field/ColorField.vue +10 -3
  319. package/src/shared/content-settings/components/content-settings/ContentSettings.mixin.ts +1 -1
  320. package/src/shared/content-settings/components/content-settings/ContentSettingsLockedButton.vue +0 -1
  321. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.ts +0 -5
  322. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +14 -1
  323. package/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.ts +6 -4
  324. package/src/shared/content-settings/components/icon-library/IconLibrary.mixin.ts +0 -1
  325. package/src/shared/content-settings/components/items-field/ItemsField.mixin.ts +1 -1
  326. package/src/shared/content-settings/components/logo-selection-field/LogoSelectionField.vue +1 -1
  327. package/src/shared/content-settings/components/logo-size-field/LogoSizeSliderField.vue +1 -1
  328. package/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.vue +1 -1
  329. package/src/shared/content-settings/components/personalisation-role-field/PersonalisationRoleField.vue +4 -4
  330. package/src/shared/content-settings/components/text-image-field/TextImageField.vue +8 -2
  331. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +1 -1
  332. package/src/shared/content-settings/models.ts +2 -3
  333. package/src/shared/content-settings/utils.ts +1 -0
  334. package/src/shared/dropzone-image-uploader/components/DropzoneImageUploader.vue +0 -1
  335. package/src/shared/ecommerce-settings/store/cart-store.test.ts +1 -1
  336. package/src/shared/font-family-selector/FontFamilySelector.mixin.ts +0 -1
  337. package/src/shared/image-gallery-style-settings/ImageGalleryStyleSettings.mixin.ts +0 -1
  338. package/src/shared/logo-settings/components/EditLogoSettings.mixin.ts +1 -1
  339. package/src/shared/logo-settings/components/LogoSettings.mixin.ts +1 -0
  340. package/src/shared/logo-settings/components/LogoSettingsHeader.mixin.ts +0 -1
  341. package/src/shared/logo-settings/components/LogoStyleSettingsWithState.vue +2 -2
  342. package/src/shared/logo-settings/components/LogoTemplateSearch.vue +1 -1
  343. package/src/shared/logo-settings/store/editable-logo-store.ts +1 -1
  344. package/src/shared/logo-settings/store/logo-settings-store.test.ts +4 -4
  345. package/src/shared/logo-versions/LogoVersions.mixin.ts +0 -1
  346. package/src/shared/models.ts +0 -6
  347. package/src/shared/my-designs/MyDesigns.mixin.ts +0 -1
  348. package/src/shared/my-uploads/MyMedia.mixin.ts +0 -1
  349. package/src/shared/my-uploads/MyMediaSelectionItem.vue +1 -2
  350. package/src/shared/my-uploads/MyUploads.mixin.ts +0 -1
  351. package/src/shared/my-uploads/models.ts +2 -1
  352. package/src/shared/navigation-style-settings/components/navigation-style-settings/NavigationStyleSettings.mixin.ts +0 -1
  353. package/src/shared/navigation-style-settings/components/navigation-style-settings/NavigationStyleSettings.vue +1 -1
  354. package/src/shared/page-settings/components/new-page-modal/NewPageModal.mixin.ts +3 -0
  355. package/src/shared/page-settings/components/page-settings/NestedDraggable.vue +1 -1
  356. package/src/shared/page-settings/components/page-settings/PageSettings.vue +2 -1
  357. package/src/shared/page-settings/components/page-settings-section-styles/HeaderDisplayStyle.vue +1 -1
  358. package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.ts +1 -1
  359. package/src/shared/page-settings/store/page-settings-store.test.ts +8 -4
  360. package/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.ts +1 -0
  361. package/src/shared/shape-library/ShapeLibrary.vue +2 -2
  362. package/src/shared/style-settings/store/style-settings-store.ts +1 -1
  363. package/src/shared/template-settings/components/TemplatesSettings.mixin.ts +2 -2
  364. package/src/shared/template-settings/models.ts +0 -1
  365. package/src/shared/template-settings/store/template-settings-store.ts +18 -105
  366. package/src/shared/unsplash-library/UnsplashLibrary.vue +6 -2
  367. package/src/shared/video-library/VideoLibrary.vue +1 -1
  368. package/lib/src/brand-page/components/banner-section/BannerSection.mixin.d.ts +0 -103
  369. package/lib/src/brand-page/components/banner-section/BannerSection.mixin.js +0 -178
  370. package/src/brand-page/components/banner-section/BannerSection.mixin.ts +0 -210
  371. package/src/brand-page-maker/components/template-preview/TemplatePreviewAiBadge.vue +0 -12
@@ -14,6 +14,10 @@ import { FooterBuilder } from './footer-builder';
14
14
  * The fix routes copyright style writes through `applyDetachedStyleOverride`,
15
15
  * which resolves the preset in and clears `textStyleType` so the per-element
16
16
  * override sticks.
17
+ *
18
+ * BP-5603: social link mutations converted from index-based to id-based
19
+ * (item_id / move_before_item_id / _remove_social_link_ids), mirroring the
20
+ * section-level move_before_content_id contract one level deeper.
17
21
  */
18
22
 
19
23
  const textStyleOptions = {
@@ -42,6 +46,37 @@ const makeModel = (copyrightStyle: Record<string, unknown>): BrandPageModel =>
42
46
  pageFooterMobile: { copyrightStyle: { ...copyrightStyle } },
43
47
  }) as unknown as BrandPageModel;
44
48
 
49
+ const makeModelWithLinks = (
50
+ links: { id?: string; imageUrl: string; url: string }[],
51
+ ): BrandPageModel =>
52
+ ({
53
+ styles: { textStyles: { textStyleOptions } },
54
+ pageFooterDesktop: {
55
+ copyrightStyle: {},
56
+ socialLinks: links.map((l) => ({
57
+ id: l.id,
58
+ imageUrl: l.imageUrl,
59
+ url: l.url,
60
+ imageTitle: '',
61
+ width: 0,
62
+ height: 0,
63
+ imageLink: '',
64
+ })),
65
+ },
66
+ pageFooterMobile: {
67
+ copyrightStyle: {},
68
+ socialLinks: links.map((l) => ({
69
+ id: l.id,
70
+ imageUrl: l.imageUrl,
71
+ url: l.url,
72
+ imageTitle: '',
73
+ width: 0,
74
+ height: 0,
75
+ imageLink: '',
76
+ })),
77
+ },
78
+ }) as unknown as BrandPageModel;
79
+
45
80
  const run = (args: UpdateFooterArguments, model: BrandPageModel) =>
46
81
  new FooterBuilder({
47
82
  id: 'f1',
@@ -49,6 +84,10 @@ const run = (args: UpdateFooterArguments, model: BrandPageModel) =>
49
84
  payload: args,
50
85
  }).buildAsync(model);
51
86
 
87
+ // ---------------------------------------------------------------------------
88
+ // Copyright font (BP-5420)
89
+ // ---------------------------------------------------------------------------
90
+
52
91
  describe('FooterBuilder — copyright font vs global style', () => {
53
92
  it('detaches from the global preset: applies the font, clears textStyleType, retains preset values', async () => {
54
93
  const model = makeModel({
@@ -75,139 +114,261 @@ describe('FooterBuilder — copyright font vs global style', () => {
75
114
  });
76
115
  });
77
116
 
78
- type Link = { imageUrl: string; url: string };
117
+ // ---------------------------------------------------------------------------
118
+ // Social links — id-based mutations (BP-5603)
119
+ // ---------------------------------------------------------------------------
79
120
 
80
- const link = (imageUrl: string): Link => ({
81
- imageUrl,
82
- url: `https://${imageUrl}.com`,
83
- });
121
+ describe('FooterBuilder — social links id-based mutations', () => {
122
+ it('backfills ids on legacy links that have none', async () => {
123
+ // Links without ids (legacy persisted data)
124
+ const model = makeModelWithLinks([
125
+ { imageUrl: 'facebook', url: 'https://fb.com' },
126
+ { imageUrl: 'instagram', url: 'https://ig.com' },
127
+ ]);
84
128
 
85
- /**
86
- * Footers migrated from the legacy single `footerStyles` model
87
- * (migration-01) split into desktop + mobile via a shallow spread, leaving
88
- * both viewports' `socialLinks` pointing at ONE shared array. `cloneDeep`
89
- * preserves that shared reference. This factory reproduces that shape.
90
- */
91
- const makeSharedSocialModel = (links: Link[]): BrandPageModel => {
92
- const shared = links.map((l) => ({ ...l }));
129
+ const { model: out } = await run({}, model);
93
130
 
94
- return {
95
- styles: { textStyles: { textStyleOptions } },
96
- pageFooterDesktop: { socialLinks: shared },
97
- pageFooterMobile: { socialLinks: shared },
98
- } as unknown as BrandPageModel;
99
- };
131
+ const links = out.pageFooterDesktop!.socialLinks;
100
132
 
101
- const makeIndependentSocialModel = (links: Link[]): BrandPageModel =>
102
- ({
103
- styles: { textStyles: { textStyleOptions } },
104
- pageFooterDesktop: { socialLinks: links.map((l) => ({ ...l })) },
105
- pageFooterMobile: { socialLinks: links.map((l) => ({ ...l })) },
106
- }) as unknown as BrandPageModel;
133
+ expect(links[0].id).toBeTruthy();
134
+ expect(links[1].id).toBeTruthy();
135
+ expect(links[0].id).not.toBe(links[1].id);
136
+ });
107
137
 
108
- const socialIcons = (footer: { socialLinks?: Link[] } | undefined) =>
109
- (footer?.socialLinks ?? []).map((l) => l.imageUrl);
110
-
111
- describe('FooterBuilder — social link removal (shared-array regression)', () => {
112
- // Reproduces the bug: a single requested removal index dropped TWO links
113
- // because both viewports shared one array and the builder spliced it twice.
114
- it('removes exactly one link per requested index even when viewports share the array', async () => {
115
- const model = makeSharedSocialModel([
116
- link('facebook'),
117
- link('instagram'),
118
- link('x'),
119
- link('youtube'),
138
+ it('updates an existing link in place by item_id', async () => {
139
+ const model = makeModelWithLinks([
140
+ { id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
141
+ { id: 'link-2', imageUrl: 'instagram', url: 'https://ig.com' },
120
142
  ]);
121
143
 
122
144
  const { model: out } = await run(
123
- { _remove_social_link_indices: [2] },
145
+ {
146
+ social_links: [
147
+ { item_id: 'link-1', url: 'https://facebook.com/mybrand' },
148
+ ],
149
+ },
124
150
  model,
125
151
  );
126
152
 
127
- expect(socialIcons(out.pageFooterDesktop)).toEqual([
128
- 'facebook',
129
- 'instagram',
130
- 'youtube',
153
+ const links = out.pageFooterDesktop!.socialLinks;
154
+
155
+ expect(links).toHaveLength(2);
156
+ expect(links[0].url).toBe('https://facebook.com/mybrand');
157
+ expect(links[0].id).toBe('link-1');
158
+ // untouched link unchanged
159
+ expect(links[1].id).toBe('link-2');
160
+ expect(links[1].url).toBe('https://ig.com');
161
+ });
162
+
163
+ it('updates social_link_icon on an existing link', async () => {
164
+ const model = makeModelWithLinks([
165
+ { id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
131
166
  ]);
132
- expect(socialIcons(out.pageFooterMobile)).toEqual([
133
- 'facebook',
134
- 'instagram',
135
- 'youtube',
167
+
168
+ const { model: out } = await run(
169
+ {
170
+ social_links: [{ item_id: 'link-1', social_link_icon: 'facebook-v2' }],
171
+ },
172
+ model,
173
+ );
174
+
175
+ expect(out.pageFooterDesktop!.socialLinks[0].imageUrl).toBe('facebook-v2');
176
+ });
177
+
178
+ it('skips unknown item_id silently', async () => {
179
+ const model = makeModelWithLinks([
180
+ { id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
136
181
  ]);
182
+
183
+ const { model: out } = await run(
184
+ {
185
+ social_links: [{ item_id: 'no-such-id', url: 'https://nowhere.com' }],
186
+ },
187
+ model,
188
+ );
189
+
190
+ const links = out.pageFooterDesktop!.socialLinks;
191
+
192
+ expect(links).toHaveLength(1);
193
+ expect(links[0].url).toBe('https://fb.com');
137
194
  });
138
195
 
139
- it('un-shares the viewport arrays so later in-place edits cannot cross-corrupt', async () => {
140
- const model = makeSharedSocialModel([
141
- link('facebook'),
142
- link('instagram'),
143
- link('x'),
196
+ it('inserts a new link (no item_id) appended by default', async () => {
197
+ const model = makeModelWithLinks([
198
+ { id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
144
199
  ]);
145
200
 
146
201
  const { model: out } = await run(
147
- { _remove_social_link_indices: [0] },
202
+ {
203
+ social_links: [
204
+ { social_link_icon: 'instagram', url: 'https://ig.com' },
205
+ ],
206
+ },
148
207
  model,
149
208
  );
150
209
 
151
- expect(out.pageFooterDesktop!.socialLinks).not.toBe(
152
- out.pageFooterMobile!.socialLinks,
210
+ const links = out.pageFooterDesktop!.socialLinks;
211
+
212
+ expect(links).toHaveLength(2);
213
+ expect(links[1].imageUrl).toBe('instagram');
214
+ expect(links[1].id).toBeTruthy();
215
+ });
216
+
217
+ it('inserts a new link before an anchor via move_before_item_id', async () => {
218
+ const model = makeModelWithLinks([
219
+ { id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
220
+ { id: 'link-2', imageUrl: 'instagram', url: 'https://ig.com' },
221
+ ]);
222
+
223
+ const { model: out } = await run(
224
+ {
225
+ social_links: [
226
+ {
227
+ social_link_icon: 'x',
228
+ url: 'https://x.com',
229
+ move_before_item_id: 'link-2',
230
+ },
231
+ ],
232
+ },
233
+ model,
153
234
  );
235
+
236
+ const links = out.pageFooterDesktop!.socialLinks;
237
+
238
+ expect(links).toHaveLength(3);
239
+ expect(links[0].id).toBe('link-1');
240
+ expect(links[1].imageUrl).toBe('x');
241
+ expect(links[2].id).toBe('link-2');
154
242
  });
155
243
 
156
- it('handles multiple removal indices on a shared array (descending splice)', async () => {
157
- const model = makeSharedSocialModel([
158
- link('facebook'),
159
- link('instagram'),
160
- link('x'),
161
- link('youtube'),
244
+ it('inserts a new link at end via move_before_item_id "__end__"', async () => {
245
+ const model = makeModelWithLinks([
246
+ { id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
247
+ { id: 'link-2', imageUrl: 'instagram', url: 'https://ig.com' },
162
248
  ]);
163
249
 
164
250
  const { model: out } = await run(
165
- { _remove_social_link_indices: [0, 2] },
251
+ {
252
+ social_links: [
253
+ {
254
+ social_link_icon: 'x',
255
+ url: 'https://x.com',
256
+ move_before_item_id: '__end__',
257
+ },
258
+ ],
259
+ },
166
260
  model,
167
261
  );
168
262
 
169
- expect(socialIcons(out.pageFooterDesktop)).toEqual([
170
- 'instagram',
171
- 'youtube',
263
+ const links = out.pageFooterDesktop!.socialLinks;
264
+
265
+ expect(links).toHaveLength(3);
266
+ expect(links[2].imageUrl).toBe('x');
267
+ });
268
+
269
+ it('moves an existing link before an anchor via move_before_item_id', async () => {
270
+ const model = makeModelWithLinks([
271
+ { id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
272
+ { id: 'link-2', imageUrl: 'instagram', url: 'https://ig.com' },
273
+ { id: 'link-3', imageUrl: 'x', url: 'https://x.com' },
172
274
  ]);
173
- expect(socialIcons(out.pageFooterMobile)).toEqual(['instagram', 'youtube']);
275
+
276
+ const { model: out } = await run(
277
+ {
278
+ social_links: [{ item_id: 'link-3', move_before_item_id: 'link-1' }],
279
+ },
280
+ model,
281
+ );
282
+
283
+ const links = out.pageFooterDesktop!.socialLinks;
284
+
285
+ expect(links.map((l) => l.id)).toEqual(['link-3', 'link-1', 'link-2']);
174
286
  });
175
287
 
176
- it('removes one link per index for independent viewport arrays (no regression)', async () => {
177
- const model = makeIndependentSocialModel([
178
- link('facebook'),
179
- link('instagram'),
180
- link('x'),
288
+ it('removes links by id', async () => {
289
+ const model = makeModelWithLinks([
290
+ { id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
291
+ { id: 'link-2', imageUrl: 'instagram', url: 'https://ig.com' },
292
+ { id: 'link-3', imageUrl: 'x', url: 'https://x.com' },
181
293
  ]);
182
294
 
183
295
  const { model: out } = await run(
184
- { _remove_social_link_indices: [1] },
296
+ { _remove_social_link_ids: ['link-1', 'link-3'] },
185
297
  model,
186
298
  );
187
299
 
188
- expect(socialIcons(out.pageFooterDesktop)).toEqual(['facebook', 'x']);
189
- expect(socialIcons(out.pageFooterMobile)).toEqual(['facebook', 'x']);
300
+ const links = out.pageFooterDesktop!.socialLinks;
301
+
302
+ expect(links).toHaveLength(1);
303
+ expect(links[0].id).toBe('link-2');
190
304
  });
191
305
 
192
- it('inserts exactly one link on a shared array (symmetric add guard)', async () => {
193
- const model = makeSharedSocialModel([link('facebook'), link('instagram')]);
306
+ it('_replace_social_links rebuilds the entire list and assigns fresh ids', async () => {
307
+ const model = makeModelWithLinks([
308
+ { id: 'old-1', imageUrl: 'facebook', url: 'https://fb.com' },
309
+ ]);
194
310
 
195
311
  const { model: out } = await run(
196
312
  {
197
- social_links: [{ insert_at: 1, social_link_icon: 'x', url: 'x.com' }],
313
+ _replace_social_links: true,
314
+ social_links: [
315
+ { social_link_icon: 'instagram', url: 'https://ig.com' },
316
+ { social_link_icon: 'x', url: 'https://x.com' },
317
+ ],
198
318
  },
199
319
  model,
200
320
  );
201
321
 
202
- expect(socialIcons(out.pageFooterDesktop)).toEqual([
203
- 'facebook',
204
- 'x',
205
- 'instagram',
322
+ const links = out.pageFooterDesktop!.socialLinks;
323
+
324
+ expect(links).toHaveLength(2);
325
+ expect(links[0].imageUrl).toBe('instagram');
326
+ expect(links[1].imageUrl).toBe('x');
327
+ // fresh ids assigned, not the old one
328
+ expect(links[0].id).toBeTruthy();
329
+ expect(links[0].id).not.toBe('old-1');
330
+ });
331
+
332
+ it('_replace_social_links with empty social_links clears all', async () => {
333
+ const model = makeModelWithLinks([
334
+ { id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
206
335
  ]);
207
- expect(socialIcons(out.pageFooterMobile)).toEqual([
208
- 'facebook',
209
- 'x',
210
- 'instagram',
336
+
337
+ const { model: out } = await run({ _replace_social_links: true }, model);
338
+
339
+ expect(out.pageFooterDesktop!.socialLinks).toHaveLength(0);
340
+ });
341
+
342
+ it('normalises mailto: and tel: prefixes on new links', async () => {
343
+ const model = makeModelWithLinks([]);
344
+
345
+ const { model: out } = await run(
346
+ {
347
+ social_links: [
348
+ { social_link_icon: 'email', url: 'mailto:me@example.com' },
349
+ { social_link_icon: 'phone', url: 'tel:+1234567890' },
350
+ ],
351
+ },
352
+ model,
353
+ );
354
+
355
+ const links = out.pageFooterDesktop!.socialLinks;
356
+
357
+ expect(links[0].url).toBe('me@example.com');
358
+ expect(links[1].url).toBe('+1234567890');
359
+ });
360
+
361
+ it('applies mutations to both desktop and mobile footers', async () => {
362
+ const model = makeModelWithLinks([
363
+ { id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
211
364
  ]);
365
+
366
+ const { model: out } = await run(
367
+ { _remove_social_link_ids: ['link-1'] },
368
+ model,
369
+ );
370
+
371
+ expect(out.pageFooterDesktop!.socialLinks).toHaveLength(0);
372
+ expect(out.pageFooterMobile!.socialLinks).toHaveLength(0);
212
373
  });
213
374
  });
@@ -10,16 +10,17 @@ import {
10
10
  import type {
11
11
  UpdateFooterArguments,
12
12
  ViewportFooterOverrides,
13
- FooterSocialLinkItem,
14
13
  } from '../parsers/ai-toolcall-parser';
15
14
  import type { Logger } from '../logger';
16
15
  import type { TextStyleOptions } from '../../brand-page/models/styles.model';
17
16
  import { FOOTER_CHECKS } from './section-rules/footer.rules';
18
17
  import { applyDetachedStyleOverride } from './section-field-utils';
19
-
20
- type FooterSocialLink = NonNullable<
21
- AbstractPageFooterStyles['socialLinks']
22
- >[number];
18
+ import {
19
+ ensureItemIds,
20
+ generateItemId,
21
+ removeItemsByIds,
22
+ repositionItemByAnchor,
23
+ } from './item-id-utils';
23
24
 
24
25
  const normaliseSocialUrl = (url: string): string => {
25
26
  for (const prefix of ['mailto:', 'tel:']) {
@@ -29,113 +30,6 @@ const normaliseSocialUrl = (url: string): string => {
29
30
  return url;
30
31
  };
31
32
 
32
- const makeSocialLink = (
33
- icon: string | undefined,
34
- url: string | undefined,
35
- ): FooterSocialLink =>
36
- ({
37
- imageUrl: icon || '',
38
- url: normaliseSocialUrl(url || ''),
39
- imageTitle: '',
40
- width: 0,
41
- height: 0,
42
- imageLink: '',
43
- }) as FooterSocialLink;
44
-
45
- const hasSocialLinkMutation = (args: UpdateFooterArguments): boolean =>
46
- args._replace_social_links === true ||
47
- (args._remove_social_link_indices?.length ?? 0) > 0 ||
48
- (args._reorder_social_links?.length ?? 0) > 0 ||
49
- (args.social_links?.length ?? 0) > 0;
50
-
51
- /**
52
- * Social links are a GLOBAL footer concern — one logical list that both
53
- * viewports render identically. Derive the new list ONCE from the current one
54
- * WITHOUT mutating the input, so the caller can assign an independent copy to
55
- * each viewport.
56
- *
57
- * Why this can't mutate per-viewport in place: footers migrated from the legacy
58
- * single `footerStyles` model (migration-01) share ONE `socialLinks` array
59
- * reference across pageFooterDesktop and pageFooterMobile, and `cloneDeep`
60
- * preserves that shared reference. Running an in-place `splice` once per
61
- * viewport therefore applies it TWICE to the same array — e.g. one requested
62
- * removal index drops two links. Compute-once + assign-fresh is correct for
63
- * both shared (legacy) and independent (new-model) footers.
64
- */
65
- const computeSocialLinks = (
66
- current: FooterSocialLink[] | undefined,
67
- args: UpdateFooterArguments,
68
- ): FooterSocialLink[] => {
69
- // _replace_social_links: replace entire list (empty/absent social_links = clear all).
70
- // A replacement supersedes every other social link mutation.
71
- if (args._replace_social_links) {
72
- return (args.social_links ?? []).map((l) =>
73
- makeSocialLink(l.social_link_icon, l.url),
74
- );
75
- }
76
-
77
- const links: FooterSocialLink[] = [...(current ?? [])];
78
-
79
- // item_index-based updates for existing links — replace the item reference
80
- // (never mutate the original object, which may be shared across viewports).
81
- for (const link of args.social_links ?? []) {
82
- if (link.item_index === undefined) continue;
83
-
84
- const existing = links[link.item_index];
85
-
86
- if (!existing) continue;
87
-
88
- links[link.item_index] = {
89
- ...existing,
90
- ...(link.social_link_icon !== undefined
91
- ? { imageUrl: link.social_link_icon }
92
- : {}),
93
- ...(link.url !== undefined ? { url: normaliseSocialUrl(link.url) } : {}),
94
- };
95
- }
96
-
97
- // Remove by index (descending order to preserve indices during splice).
98
- if (args._remove_social_link_indices?.length) {
99
- const sorted = [...args._remove_social_link_indices].sort((a, b) => b - a);
100
-
101
- for (const idx of sorted) {
102
- if (idx >= 0 && idx < links.length) links.splice(idx, 1);
103
- }
104
- }
105
-
106
- // Insert new links (descending insert_at order to preserve positions).
107
- const insertLinks = (args.social_links ?? []).filter(
108
- (l): l is FooterSocialLinkItem & { insert_at: number } =>
109
- l.insert_at !== undefined && l.item_index === undefined,
110
- );
111
- const sortedInserts = [...insertLinks].sort(
112
- (a, b) => b.insert_at - a.insert_at,
113
- );
114
-
115
- for (const link of sortedInserts) {
116
- const pos = Math.max(0, Math.min(link.insert_at, links.length));
117
-
118
- links.splice(pos, 0, makeSocialLink(link.social_link_icon, link.url));
119
- }
120
-
121
- // Reorder (requires ALL indices — partial reorder is not supported).
122
- if (args._reorder_social_links?.length) {
123
- const newOrder = args._reorder_social_links;
124
- const unique = new Set(newOrder);
125
-
126
- if (
127
- unique.size === newOrder.length &&
128
- newOrder.every((idx) => idx >= 0 && idx < links.length)
129
- ) {
130
- const reordered = newOrder.map((idx) => links[idx]);
131
-
132
- if (reordered.length === links.length) return reordered;
133
- }
134
- }
135
-
136
- return links;
137
- };
138
-
139
33
  const applyGlobalFooterChanges = (
140
34
  footer: AbstractPageFooterStyles,
141
35
  args: UpdateFooterArguments,
@@ -150,6 +44,80 @@ const applyGlobalFooterChanges = (
150
44
  { fontFamily: args.copyright_text_font_family },
151
45
  textStyleOptions,
152
46
  );
47
+
48
+ const links = args.social_links;
49
+
50
+ // _replace_social_links: replace entire list (empty/absent social_links = clear all).
51
+ // When replacing, all other social link mutations (remove, insert, reorder) are skipped —
52
+ // they are superseded by the replacement and running them would cause duplicates.
53
+ if (args._replace_social_links) {
54
+ footer.socialLinks = (links ?? []).map((l) => ({
55
+ id: generateItemId(),
56
+ imageUrl: l.social_link_icon || '',
57
+ url: normaliseSocialUrl(l.url || ''),
58
+ imageTitle: '',
59
+ width: 0,
60
+ height: 0,
61
+ imageLink: '',
62
+ }));
63
+ } else {
64
+ // Targeted mutations — only run when NOT replacing the entire list.
65
+ // BP-5603: id-based contract (item_id / move_before_item_id / _remove_social_link_ids).
66
+ // Backfill ids on legacy links so they become referenceable after this call.
67
+ ensureItemIds(footer.socialLinks);
68
+
69
+ // Remove by id (order-independent — no index shifting needed).
70
+ footer.socialLinks = removeItemsByIds(
71
+ footer.socialLinks,
72
+ args._remove_social_link_ids,
73
+ );
74
+
75
+ // Update existing links (item_id present) or insert new links (no item_id).
76
+ if (links?.length) {
77
+ footer.socialLinks = footer.socialLinks ?? [];
78
+
79
+ for (const link of links) {
80
+ if (link.item_id) {
81
+ // Update existing link in place; reposition if move_before_item_id set.
82
+ const existing = footer.socialLinks.find(
83
+ (l) => l.id === link.item_id,
84
+ );
85
+
86
+ // Unknown id — skip rather than guess at a position.
87
+ if (!existing) continue;
88
+
89
+ if (link.social_link_icon !== undefined)
90
+ existing.imageUrl = link.social_link_icon;
91
+
92
+ if (link.url !== undefined)
93
+ existing.url = normaliseSocialUrl(link.url);
94
+
95
+ repositionItemByAnchor(
96
+ footer.socialLinks,
97
+ existing,
98
+ link.move_before_item_id,
99
+ );
100
+ } else {
101
+ // New link — assign a stable id and position via move_before_item_id.
102
+ const newLink = {
103
+ id: generateItemId(),
104
+ imageUrl: link.social_link_icon || '',
105
+ url: normaliseSocialUrl(link.url || ''),
106
+ imageTitle: '',
107
+ width: 0,
108
+ height: 0,
109
+ imageLink: '' as string | false,
110
+ };
111
+
112
+ repositionItemByAnchor(
113
+ footer.socialLinks,
114
+ newLink,
115
+ link.move_before_item_id,
116
+ );
117
+ }
118
+ }
119
+ }
120
+ }
153
121
  };
154
122
 
155
123
  const applyViewportFooterChanges = (
@@ -230,30 +198,13 @@ export class FooterBuilder extends BaseMetadataBuilder<UpdateFooterArguments> {
230
198
  const args = this.request.payload ?? {};
231
199
  const textStyleOptions = model.styles?.textStyles?.textStyleOptions;
232
200
 
233
- // Global scalar/style fields (copyright text + font) apply to both
234
- // viewports. These are idempotent assignments, safe to run per-viewport.
201
+ // Global fields (copyright_text, social links) always apply to both viewports.
235
202
  if (model.pageFooterDesktop)
236
203
  applyGlobalFooterChanges(model.pageFooterDesktop, args, textStyleOptions);
237
204
 
238
205
  if (model.pageFooterMobile)
239
206
  applyGlobalFooterChanges(model.pageFooterMobile, args, textStyleOptions);
240
207
 
241
- // Social links are a single global list. Derive the new list ONCE, then
242
- // assign an INDEPENDENT copy to each viewport — never mutate a (possibly
243
- // shared) viewport array in place. See computeSocialLinks.
244
- if (hasSocialLinkMutation(args)) {
245
- const current =
246
- model.pageFooterDesktop?.socialLinks ??
247
- model.pageFooterMobile?.socialLinks;
248
- const updated = computeSocialLinks(current, args);
249
-
250
- if (model.pageFooterDesktop)
251
- model.pageFooterDesktop.socialLinks = updated.map((l) => ({ ...l }));
252
-
253
- if (model.pageFooterMobile)
254
- model.pageFooterMobile.socialLinks = updated.map((l) => ({ ...l }));
255
- }
256
-
257
208
  // Viewport-specific styling via explicit sub-objects.
258
209
  if (args.desktop && model.pageFooterDesktop)
259
210
  applyViewportFooterChanges(model.pageFooterDesktop, args.desktop);