@designcrowd/library.brand-page 6.1.1-json-prereq-3 → 6.1.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 (216) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-hj_ievMc.js → CreatedOnBrandCrowd-de-de-B5B5ThJ6.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-jAOSpfCw.js → CreatedOnBrandCrowd-en-us-a48CaORy.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-DZMVc-y3.js → CreatedOnBrandCrowd-es-es-xpuZ317-.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-DvB54H38.js → CreatedOnBrandCrowd-fr-ca-V-iUMArW.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-DvB54H38.js → CreatedOnBrandCrowd-fr-fr-V-iUMArW.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-bf-x1xhi.js → CreatedOnBrandCrowd-pt-br-DZ0NKSAy.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-bf-x1xhi.js → CreatedOnBrandCrowd-pt-pt-DZ0NKSAy.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-B-NY1B44.js → CreatedOnDcom-de-de-_950cgGp.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-Cd4C6KQf.js → CreatedOnDcom-en-us-DogKzH-g.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-DONlsQsm.js → CreatedOnDcom-es-es-CmGM3IPX.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-Dsv3FRpZ.js → CreatedOnDcom-fr-ca-BonEDkWZ.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-DOgyvY5C.js → CreatedOnDcom-fr-fr-DQHtKrd_.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-DX66MhJS.js → CreatedOnDcom-pt-br-CtYJSBjs.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-COe2G0bU.js → CreatedOnDcom-pt-pt-DoO9r4lA.js} +1 -1
  15. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +7 -8
  16. package/lib/src/brand-page/components/mailing-list/MailingList.mixin.js +6 -4
  17. package/lib/src/brand-page/components/page-footer/PageFooter.mixin.d.ts +1 -0
  18. package/lib/src/brand-page/components/page-footer/PageFooter.mixin.js +4 -1
  19. package/lib/src/brand-page/components/payment/Payment.mixin.js +4 -3
  20. package/lib/src/brand-page/components/payment/__fixtures__/request-payment.d.ts +1 -0
  21. package/lib/src/brand-page/components/payment/__fixtures__/support-me.d.ts +1 -0
  22. package/lib/src/brand-page/components/payment/request-payment/RequestPayment.mixin.d.ts +1 -0
  23. package/lib/src/brand-page/components/payment/request-payment/RequestPayment.mixin.js +4 -1
  24. package/lib/src/brand-page/composables/consent.composable.d.ts +2 -0
  25. package/lib/src/brand-page/composables/consent.composable.js +9 -3
  26. package/lib/src/brand-page/models/brand-page-context.model.d.ts +7 -0
  27. package/lib/src/brand-page/models/index.d.ts +26 -34
  28. package/lib/src/brand-page/utils/context.util.d.ts +8 -0
  29. package/lib/src/brand-page/utils/context.util.js +8 -0
  30. package/lib/src/brand-page/utils/migration.util.js +0 -11
  31. package/lib/src/brand-page/utils/text.util.d.ts +1 -13
  32. package/lib/src/brand-page/utils/text.util.js +0 -64
  33. package/lib/src/brand-page/utils/vcard.util.js +4 -4
  34. package/lib/src/brand-page/utils/view-model.util.d.ts +0 -1
  35. package/lib/src/brand-page/utils/view-model.util.js +20 -68
  36. package/lib/src/brand-page/view-models.d.ts +3 -3
  37. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +1 -1
  38. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.js +51 -18
  39. package/lib/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.d.ts +1 -0
  40. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.d.ts +9 -2
  41. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.js +10 -6
  42. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.d.ts +20 -0
  43. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.js +44 -0
  44. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.d.ts +27 -0
  45. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.js +44 -0
  46. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +1 -1
  47. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +103 -24
  48. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.js +8 -0
  49. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +9 -3
  50. package/lib/src/brand-page-maker/stores/ui/ui-store.d.ts +1 -1
  51. package/lib/src/brand-page-maker/utils/index.d.ts +0 -2
  52. package/lib/src/brand-page-maker/utils/index.js +0 -2
  53. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +5 -28
  54. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +5 -48
  55. package/lib/src/brand-page-maker/validators/field.validator.js +2 -10
  56. package/lib/src/clients/models.d.ts +2 -0
  57. package/lib/src/css/library.brand-page-brandCrowd.min.css +99 -102
  58. package/lib/src/css/library.brand-page-brandPage.min.css +99 -102
  59. package/lib/src/css/library.brand-page-designCom.min.css +99 -102
  60. package/lib/src/css/library.brand-page-preview.min.css +99 -102
  61. package/lib/src/index.d.ts +1 -1
  62. package/lib/src/index.mjs +219 -221
  63. package/lib/src/{lib-scDtk-Pi.js → lib-DtalJKI3.js} +72445 -75920
  64. package/lib/src/lib.js +1 -2
  65. package/lib/src/metadata-builder/ai-metadata-executor.d.ts +1 -0
  66. package/lib/src/metadata-builder/ai-metadata-executor.js +19 -0
  67. package/lib/src/metadata-builder/builders/section-builders/about-section-builder.js +3 -2
  68. package/lib/src/metadata-builder/builders/section-builders/banner-section-builder.js +3 -2
  69. package/lib/src/metadata-builder/builders/section-builders/booking-section-builder.js +3 -2
  70. package/lib/src/metadata-builder/builders/section-builders/contact-form-builder.js +4 -3
  71. package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +3 -2
  72. package/lib/src/metadata-builder/builders/section-builders/mailing-list-builder.js +2 -1
  73. package/lib/src/metadata-builder/builders/section-builders/mailing-list-section-builder.js +2 -1
  74. package/lib/src/metadata-builder/builders/section-builders/profile-section-builder.js +4 -3
  75. package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +3 -2
  76. package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +2 -1
  77. package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +11 -15
  78. package/lib/src/metadata-builder/builders/update-content-text-builder.d.ts +17 -0
  79. package/lib/src/metadata-builder/builders/update-content-text-builder.js +103 -0
  80. package/lib/src/metadata-builder/builders/update-content-text-style-builder.d.ts +16 -0
  81. package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +216 -0
  82. package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +13 -11
  83. package/lib/src/metadata-builder/helpers/rte.helpers.js +73 -14
  84. package/lib/src/metadata-builder/metadata-executor.js +4 -0
  85. package/lib/src/metadata-builder/models.d.ts +2 -0
  86. package/lib/src/metadata-builder/models.js +2 -0
  87. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +19 -0
  88. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +4 -0
  89. package/lib/src/metadata-builder/sanitizers/rte-schema-check.d.ts +8 -7
  90. package/lib/src/metadata-builder/sanitizers/rte-schema-check.js +52 -21
  91. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +1 -1
  92. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +10 -35
  93. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +1 -1
  94. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +3 -12
  95. package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +1 -1
  96. package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +1 -1
  97. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.d.ts +2 -3
  98. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.js +3 -10
  99. package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +1 -1
  100. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +1 -2
  101. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +1 -4
  102. package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +1 -1
  103. package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +1 -1
  104. package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +1 -1
  105. package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.d.ts +1 -1
  106. package/lib/src/shared/logo-versions/LogoVersions.mixin.d.ts +1 -1
  107. package/lib/src/shared/my-designs/MyDesigns.mixin.d.ts +1 -1
  108. package/lib/src/shared/my-uploads/MyMedia.mixin.d.ts +1 -1
  109. package/lib/src/shared/my-uploads/MyUploads.mixin.d.ts +1 -1
  110. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +1 -1
  111. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +1 -1
  112. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +1 -1
  113. package/lib/src/shared/template-settings/components/TemplatesSettings.mixin.d.ts +1 -1
  114. package/lib/src/shared/template-settings/store/template-settings-store.d.ts +2 -0
  115. package/lib/src/shared/template-settings/store/template-settings-store.js +72 -24
  116. package/lib/src/shared/unsplash-library/UnsplashLibrary.mixin.d.ts +1 -1
  117. package/lib/src/utils/minify-brand-page-metadata.js +1 -4
  118. package/package.json +15 -17
  119. package/src/brand-page/components/contact-form/ContactForm.mixin.ts +9 -7
  120. package/src/brand-page/components/cookie-consent/CookieConsent.vue +1 -0
  121. package/src/brand-page/components/mailing-list/MailingList.mixin.ts +8 -3
  122. package/src/brand-page/components/navigation/navigation-carousel/NavigationCarousel.vue +5 -1
  123. package/src/brand-page/components/page-footer/PageFooter.mixin.ts +4 -1
  124. package/src/brand-page/components/page-footer/PageFooter.vue +1 -1
  125. package/src/brand-page/components/payment/Payment.mixin.ts +4 -3
  126. package/src/brand-page/components/payment/request-payment/RequestPayment.mixin.ts +8 -1
  127. package/src/brand-page/components/payment/request-payment/RequestPayment.vue +1 -1
  128. package/src/brand-page/components/save-contact/SaveContactWidget.vue +2 -1
  129. package/src/brand-page/components/shop/Shop.vue +6 -3
  130. package/src/brand-page/components/social-links/SocialLinks.vue +5 -2
  131. package/src/brand-page/components/text-wrapper/TextWrapper.vue +6 -17
  132. package/src/brand-page/composables/consent.composable.ts +18 -4
  133. package/src/brand-page/models/brand-page-context.model.ts +7 -0
  134. package/src/brand-page/models/index.ts +26 -35
  135. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
  136. package/src/brand-page/utils/context.util.test.ts +43 -0
  137. package/src/brand-page/utils/context.util.ts +10 -0
  138. package/src/brand-page/utils/migration.util.ts +0 -12
  139. package/src/brand-page/utils/style.util.test.ts +0 -51
  140. package/src/brand-page/utils/text.util.ts +4 -83
  141. package/src/brand-page/utils/vcard.util.ts +4 -6
  142. package/src/brand-page/utils/view-model.util.ts +26 -94
  143. package/src/brand-page/view-models.ts +2 -3
  144. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +4 -5
  145. package/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.test.ts +135 -0
  146. package/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.ts +62 -21
  147. package/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.ts +1 -0
  148. package/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.ts +10 -6
  149. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.ts +73 -0
  150. package/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.ts +85 -0
  151. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +111 -27
  152. package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.ts +9 -0
  153. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +11 -3
  154. package/src/brand-page-maker/stores/ui/ui-store.ts +1 -1
  155. package/src/brand-page-maker/utils/index.ts +0 -2
  156. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +3 -37
  157. package/src/brand-page-maker/utils/rich-text-editor.util.ts +11 -66
  158. package/src/brand-page-maker/validators/field.validator.ts +2 -10
  159. package/src/clients/models.ts +3 -0
  160. package/src/lib.ts +1 -9
  161. package/src/metadata-builder/ai-metadata-executor.ts +26 -0
  162. package/src/metadata-builder/builders/section-builders/about-section-builder.ts +3 -2
  163. package/src/metadata-builder/builders/section-builders/banner-section-builder.ts +3 -2
  164. package/src/metadata-builder/builders/section-builders/booking-section-builder.ts +5 -2
  165. package/src/metadata-builder/builders/section-builders/contact-form-builder.ts +4 -3
  166. package/src/metadata-builder/builders/section-builders/images-section-builder.ts +3 -2
  167. package/src/metadata-builder/builders/section-builders/mailing-list-builder.ts +2 -1
  168. package/src/metadata-builder/builders/section-builders/mailing-list-section-builder.ts +2 -1
  169. package/src/metadata-builder/builders/section-builders/profile-section-builder.ts +6 -3
  170. package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +3 -2
  171. package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +2 -1
  172. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +13 -19
  173. package/src/metadata-builder/builders/update-content-text-builder.ts +148 -0
  174. package/src/metadata-builder/builders/update-content-text-style-builder.ts +352 -0
  175. package/src/metadata-builder/helpers/rte.helpers.ts +101 -15
  176. package/src/metadata-builder/metadata-executor.ts +5 -0
  177. package/src/metadata-builder/models.ts +3 -0
  178. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +27 -0
  179. package/src/metadata-builder/sanitizers/rte-schema-check.ts +53 -24
  180. package/src/shared/content-settings/components/GenericTextInput.mixin.ts +11 -42
  181. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +3 -16
  182. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.ts +4 -13
  183. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.vue +4 -6
  184. package/src/shared/content-settings/components/text-image-field/TextImageField.vue +3 -6
  185. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +1 -6
  186. package/src/shared/content-settings/components/text-items-field/TextItemsField.vue +1 -1
  187. package/src/shared/template-settings/store/template-settings-store.ts +93 -27
  188. package/src/utils/minify-brand-page-metadata.ts +1 -4
  189. package/lib/src/brand-page/utils/migration/migration-20.d.ts +0 -2
  190. package/lib/src/brand-page/utils/migration/migration-20.js +0 -66
  191. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +0 -22
  192. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +0 -22
  193. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +0 -14
  194. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +0 -39
  195. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
  196. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +0 -117
  197. package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -63
  198. package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -95
  199. package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +0 -6
  200. package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +0 -156
  201. package/src/brand-page/utils/migration/migration-20.server.test.ts +0 -125
  202. package/src/brand-page/utils/migration/migration-20.test.ts +0 -72
  203. package/src/brand-page/utils/migration/migration-20.ts +0 -87
  204. package/src/brand-page/utils/text.util.test.ts +0 -106
  205. package/src/brand-page/utils/view-model.pageid-rte.test.ts +0 -104
  206. package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +0 -23
  207. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +0 -52
  208. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +0 -42
  209. package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +0 -162
  210. package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +0 -202
  211. package/src/brand-page-maker/utils/rte-features.test.ts +0 -378
  212. package/src/brand-page-maker/utils/rte-render-parity.test.ts +0 -164
  213. package/src/brand-page-maker/utils/rte-render.util.ts +0 -165
  214. package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +0 -172
  215. package/src/brand-page-maker/utils/rte-sanitize.util.ts +0 -192
  216. package/src/brand-page-maker/validators/field.validator.test.ts +0 -55
@@ -22,7 +22,6 @@ import { migration16 } from './migration/migration-16';
22
22
  import { migration17 } from './migration/migration-17';
23
23
  import { migration18 } from './migration/migration-18';
24
24
  import { migration19 } from './migration/migration-19';
25
- import { migration20 } from './migration/migration-20';
26
25
 
27
26
  export interface MigrationData {
28
27
  brandPageModel: BrandPageModel;
@@ -180,17 +179,6 @@ const MIGRATION_TABLE = [
180
179
  description:
181
180
  'BP-5800: seed the button fill colour as each block’s own container colour so it is independent of the site button (existing sites render unchanged) — backgroundStyle for mailing-list, request-payment and support-me (all types); cardStyle for the BP/BC contact form (skipped on brand-site)',
182
181
  },
183
- {
184
- version: 20,
185
- migrateFn: migration20,
186
- // One-time versioned migration (NOT runAlways): legacy sites (v < 20)
187
- // convert on their first load in any environment — the maker (browser DOM)
188
- // and web-renderer SSR (migration-20 installs a jsdom window). It no longer
189
- // needs to re-run every load because the AI section builders now emit Tiptap
190
- // JSON directly, so there is no recurring post-load source of HTML to catch.
191
- description:
192
- 'BP-5787: convert inline-HTML RTE content to Tiptap JSON on first load (client or server); AI builders emit JSON directly, so this runs once per site',
193
- },
194
182
  ] as MigrationTableItem[];
195
183
 
196
184
  export const GetLatestModelVersion = () =>
@@ -87,57 +87,6 @@ describe('style.util', () => {
87
87
  );
88
88
  });
89
89
 
90
- test('applyLogoThemeToBrandPageModel preserves var() token refs in JSON RTE content (BP-5787)', () => {
91
- // Arrange — title stored as Tiptap JSON with a theme-token colour mark.
92
- const logoTheme = logoWithFlatColorsTheme;
93
- const titleDoc = {
94
- type: 'doc',
95
- content: [
96
- {
97
- type: 'heading',
98
- attrs: { level: 2, textAlign: 'center' },
99
- content: [
100
- {
101
- type: 'text',
102
- marks: [
103
- {
104
- type: 'textStyle',
105
- attrs: { color: `var(--${STYLE_THEME_KEYS.LogoTextColor})` },
106
- },
107
- ],
108
- text: 'Themed',
109
- },
110
- ],
111
- },
112
- ],
113
- };
114
- const brandPageModel = new BrandPageModel({
115
- ...BRAND_SITE,
116
- pages: [
117
- {
118
- title: '1',
119
- slug: '1',
120
- contents: [
121
- {
122
- ...createInitialBannerSectionContent(),
123
- title: titleDoc,
124
- } as any,
125
- ],
126
- },
127
- ],
128
- });
129
-
130
- // Act
131
- const { model } = applyLogoThemeToBrandPageModel(logoTheme, brandPageModel);
132
-
133
- // Assert — JSON RTE content is an object, so the var() token is NOT
134
- // flattened to rgb(); it survives so a new theme can still propagate.
135
- const title = JSON.stringify((model.pages[0].contents[0] as any).title);
136
-
137
- expect(title).toContain(`var(--${STYLE_THEME_KEYS.LogoTextColor})`);
138
- expect(title).not.toContain(logoTheme.logoTextColor.rgb);
139
- });
140
-
141
90
  test('applyLogoThemeToBrandPageModel should apply logo theme color to content color when content color has a color matched theme key', () => {
142
91
  // Arrange
143
92
  const logoTheme = logoWithFlatColorsTheme;
@@ -1,56 +1,17 @@
1
1
  import { ESCAPED_NEWLINE_CHARACTER } from '../../constants/ui';
2
2
 
3
- /**
4
- * BP-5787 — a Tiptap JSON document is an object with `type: 'doc'`.
5
- * Inlined here (rather than importing from brand-page-maker) to avoid a
6
- * circular import: rich-text-editor.util imports getRenderText from this file.
7
- */
8
- const isTiptapDoc = (value: unknown): value is { type: 'doc' } =>
9
- typeof value === 'object' &&
10
- value !== null &&
11
- (value as { type?: string }).type === 'doc';
12
-
13
- const jsonDocPlainText = (doc: unknown): string => {
14
- const parts: string[] = [];
15
-
16
- const walk = (node: any): void => {
17
- if (typeof node?.text === 'string') parts.push(node.text);
18
-
19
- node?.content?.forEach(walk);
20
- };
21
-
22
- walk(doc);
23
-
24
- return parts.join(' ').replace(/\s+/g, ' ').trim();
25
- };
26
-
27
3
  export const getRenderText = (text: string): string => {
28
- // BP-5787: RTE fields may now be Tiptap JSON objects. Non-strings pass
29
- // through untouched so the renderer (TextWrapper) handles them via its
30
- // JSON-aware path, instead of throwing on `.replace`.
31
- if (typeof text !== 'string') {
32
- return text as unknown as string;
33
- }
34
-
35
4
  return text.replace(
36
5
  new RegExp(`(${ESCAPED_NEWLINE_CHARACTER}|\\n)`, 'g'),
37
6
  '<br/>',
38
7
  );
39
8
  };
40
9
 
41
- // Accepts a legacy inline-HTML string or a Tiptap JSON document (BP-5787):
42
- // for JSON it returns the concatenated visible text; for HTML it strips tags.
43
- export const escapeContentText = (text: unknown): string | undefined => {
10
+ export const escapeContentText = (
11
+ text: string | undefined,
12
+ ): string | undefined => {
44
13
  if (!text) {
45
- return text as undefined;
46
- }
47
-
48
- if (isTiptapDoc(text)) {
49
- return jsonDocPlainText(text);
50
- }
51
-
52
- if (typeof text !== 'string') {
53
- return undefined;
14
+ return text;
54
15
  }
55
16
 
56
17
  return text
@@ -58,46 +19,6 @@ export const escapeContentText = (text: unknown): string | undefined => {
58
19
  .replace(/(\r\n|\r|\n)/g, ESCAPED_NEWLINE_CHARACTER);
59
20
  };
60
21
 
61
- /**
62
- * Character length of RTE content as the *editor* counts it — ProseMirror
63
- * `doc.textContent`: text nodes concatenated with NO separator. This is the
64
- * metric the MaxLength extension gates on, so length indicators must use it too.
65
- *
66
- * Do NOT use escapeContentText/jsonDocPlainText for length limiting: those
67
- * join text nodes with a space for readable plain-text extraction, so they
68
- * over-count by one character per inline mark boundary. Converting a word to a
69
- * hyperlink (or bolding it, etc.) splits its text node, making the counter
70
- * report length+1 for content the editor considers exactly at the cap (BP-5787).
71
- */
72
- export const getContentTextLength = (value: unknown): number => {
73
- if (!value) {
74
- return 0;
75
- }
76
-
77
- if (isTiptapDoc(value)) {
78
- let length = 0;
79
-
80
- const walk = (node: any): void => {
81
- if (typeof node?.text === 'string') {
82
- length += node.text.length;
83
- }
84
-
85
- node?.content?.forEach(walk);
86
- };
87
-
88
- walk(value);
89
-
90
- return length;
91
- }
92
-
93
- if (typeof value !== 'string') {
94
- return 0;
95
- }
96
-
97
- // Legacy inline-HTML string: count visible text with tags stripped.
98
- return value.replace(/(<([^>]+)>)/gi, '').length;
99
- };
100
-
101
22
  export const decodeHtmlEntities = (text: string): string => {
102
23
  return text
103
24
  .replace(/&lt;/g, '<')
@@ -235,11 +235,9 @@ const addProfileInfo = (
235
235
  profile: ProfileViewModel,
236
236
  vcardLines: string[],
237
237
  ): void => {
238
- const name = sanitizeForVCard(getPlainTextFromRTE(profile.name) || '');
239
- const title = sanitizeForVCard(getPlainTextFromRTE(profile.title) || '');
240
- const description = sanitizeForVCard(
241
- getPlainTextFromRTE(profile.description) || '',
242
- );
238
+ const name = sanitizeForVCard(profile.name || '');
239
+ const title = sanitizeForVCard(profile.title || '');
240
+ const description = sanitizeForVCard(profile.description || '');
243
241
 
244
242
  if (name) {
245
243
  const nameParts = name.trim().split(/\s+/);
@@ -318,7 +316,7 @@ export const saveContact = (brandPageData: {
318
316
  }): void => {
319
317
  const vcard = generateVCard(brandPageData);
320
318
  const profile = findProfileContent(brandPageData);
321
- const filename = generateVCardFilename(getPlainTextFromRTE(profile?.name));
319
+ const filename = generateVCardFilename(profile?.name);
322
320
 
323
321
  downloadVCard(vcard, filename);
324
322
  };
@@ -119,7 +119,7 @@ import {
119
119
  getRouteNameByPageIndex,
120
120
  getRouteParamsByPage,
121
121
  } from './route.util';
122
- import { hasRTEContent, isTiptapDocument } from '../../brand-page-maker/utils';
122
+ import { hasRTEContent } from '../../brand-page-maker/utils';
123
123
  import {
124
124
  getPageIdFromHref,
125
125
  isHrefPageId,
@@ -1519,86 +1519,6 @@ const mapPageIdReferencesToRelativePaths = (
1519
1519
  });
1520
1520
  };
1521
1521
 
1522
- // Resolve a `pageid:<id>` internal-link href to the published relative path.
1523
- // Returns the resolved href, or null when `href` is not an internal pageid link
1524
- // (external/absolute links are left untouched). Falls back to the site root when
1525
- // the pageid can't be matched to a nav route.
1526
- const resolvePageIdHref = (
1527
- href: string,
1528
- nav: NavViewModel,
1529
- options: ViewModelMappingOptions,
1530
- ): string | null => {
1531
- // Only process internal links (no protocol, doesn't start with /)
1532
- if (!href || href.match(/^(https?:\/\/|\/)/) || !isHrefPageId(href)) {
1533
- return null;
1534
- }
1535
-
1536
- const pageId = getPageIdFromHref(href);
1537
- const route = findNavItemByPageId(nav.navItems, pageId);
1538
- const brandPageSlugRoute = options.brandPageSlug
1539
- ? `/${options.brandPageSlug}`
1540
- : '/';
1541
-
1542
- if (route) {
1543
- if (route.isRoot) {
1544
- return brandPageSlugRoute;
1545
- }
1546
-
1547
- return `${brandPageSlugRoute}${brandPageSlugRoute === '/' ? '' : '/'}${route.relativePath}`;
1548
- }
1549
-
1550
- // fallback to root if pageid cannot be resolved.
1551
- return brandPageSlugRoute;
1552
- };
1553
-
1554
- // BP-5847: walk a Tiptap JSON doc and rewrite every link mark whose href is a
1555
- // `pageid:` reference into its published relative path. Returns a NEW doc when
1556
- // something changed (so the shared model doc is never mutated), else the same
1557
- // reference.
1558
- export const resolvePageIdsInRteJson = (
1559
- doc: unknown,
1560
- nav: NavViewModel,
1561
- options: ViewModelMappingOptions,
1562
- ): unknown => {
1563
- let changed = false;
1564
-
1565
- const walk = (node: any): any => {
1566
- if (!node || typeof node !== 'object') {
1567
- return node;
1568
- }
1569
-
1570
- let next = node;
1571
-
1572
- if (Array.isArray(node.marks)) {
1573
- const marks = node.marks.map((mark: any) => {
1574
- if (mark?.type === 'link' && typeof mark.attrs?.href === 'string') {
1575
- const resolved = resolvePageIdHref(mark.attrs.href, nav, options);
1576
-
1577
- if (resolved !== null) {
1578
- changed = true;
1579
-
1580
- return { ...mark, attrs: { ...mark.attrs, href: resolved } };
1581
- }
1582
- }
1583
-
1584
- return mark;
1585
- });
1586
-
1587
- next = { ...next, marks };
1588
- }
1589
-
1590
- if (Array.isArray(node.content)) {
1591
- next = { ...next, content: node.content.map(walk) };
1592
- }
1593
-
1594
- return next;
1595
- };
1596
-
1597
- const result = walk(doc);
1598
-
1599
- return changed ? result : doc;
1600
- };
1601
-
1602
1522
  const dynamicallyMapPageIdInHyperlinksToRelativePaths = (
1603
1523
  content: any,
1604
1524
  nav: NavViewModel,
@@ -1611,15 +1531,7 @@ const dynamicallyMapPageIdInHyperlinksToRelativePaths = (
1611
1531
  return;
1612
1532
  }
1613
1533
 
1614
- if (isTiptapDocument(value)) {
1615
- // BP-5847: RTE fields may now be Tiptap JSON. Resolve pageid link marks
1616
- // here too, or internal links render as raw `pageid:` and don't navigate.
1617
- (content as unknown as Record<string, unknown>)[key] =
1618
- resolvePageIdsInRteJson(value, nav, options);
1619
- } else if (
1620
- typeof value === 'object' &&
1621
- (key === 'items' || key === 'textItems')
1622
- ) {
1534
+ if (typeof value === 'object' && (key === 'items' || key === 'textItems')) {
1623
1535
  if (Array.isArray(value)) {
1624
1536
  value.forEach((item) => {
1625
1537
  dynamicallyMapPageIdInHyperlinksToRelativePaths(item, nav, options);
@@ -1631,11 +1543,31 @@ const dynamicallyMapPageIdInHyperlinksToRelativePaths = (
1631
1543
  (content as unknown as Record<string, unknown>)[key] = value.replace(
1632
1544
  /<a\s+([^>]*?)href="([^"]*?)"([^>]*?)>/gi,
1633
1545
  (match, existingAttrsStart, href, existingAttrsEnd) => {
1634
- const resolved = resolvePageIdHref(href, nav, options);
1546
+ // Only process internal links (no protocol, doesn't start with /)
1547
+ if (href && !href.match(/^(https?:\/\/|\/)/) && isHrefPageId(href)) {
1548
+ const pageId = getPageIdFromHref(href);
1549
+
1550
+ const route = findNavItemByPageId(nav.navItems, pageId);
1551
+
1552
+ const brandPageSlugRoute = options.brandPageSlug
1553
+ ? `/${options.brandPageSlug}`
1554
+ : '/';
1555
+
1556
+ if (route) {
1557
+ let href = `${brandPageSlugRoute}`;
1558
+
1559
+ if (!route.isRoot) {
1560
+ href = `${brandPageSlugRoute}${brandPageSlugRoute === '/' ? '' : '/'}${route.relativePath}`;
1561
+ }
1562
+
1563
+ return `<a ${existingAttrsStart}href="${href}"${existingAttrsEnd}>`;
1564
+ }
1565
+
1566
+ // fallback to root if pageid cannot be resolved.
1567
+ return `<a ${existingAttrsStart}href="${brandPageSlugRoute}"${existingAttrsEnd}>`;
1568
+ }
1635
1569
 
1636
- return resolved === null
1637
- ? match
1638
- : `<a ${existingAttrsStart}href="${resolved}"${existingAttrsEnd}>`;
1570
+ return match;
1639
1571
  },
1640
1572
  );
1641
1573
  }
@@ -41,7 +41,6 @@ import type {
41
41
  BackgroundShape,
42
42
  PersonalisationRole,
43
43
  SaveContactOptions,
44
- RichTextValue,
45
44
  } from './models';
46
45
  import type { ImagesSectionContentDisplayStyle } from './constants';
47
46
 
@@ -197,8 +196,8 @@ export interface TextImageSectionViewModel {
197
196
  id: string;
198
197
  type: 'images-section';
199
198
  role: ContentType;
200
- title: RichTextValue;
201
- description: RichTextValue;
199
+ title: string;
200
+ description: string;
202
201
  displayStyle: ImagesSectionContentDisplayStyle;
203
202
  aspectRatio: AspectRatio;
204
203
  areImagesFullWidth: boolean;
@@ -386,10 +386,10 @@
386
386
  <div class="tw-relative" :class="{ 'editor-compact': isCompact }">
387
387
  <BubbleMenu
388
388
  :editor="editor"
389
- :options="bubbleMenuOptions"
389
+ :tippy-options="tippyOptions"
390
390
  :update-delay="0"
391
391
  :should-show="shouldShow"
392
- class="tw-w-fit tw-max-w-[350px]"
392
+ class="tw-w-fit"
393
393
  >
394
394
  <div class="bubble-menu">
395
395
  <div
@@ -455,8 +455,7 @@ import {
455
455
  ButtonGroup,
456
456
  TextInput,
457
457
  } from '@designcrowd/fe-shared-lib';
458
- import { EditorContent } from '@tiptap/vue-3';
459
- import { BubbleMenu } from '@tiptap/vue-3/menus';
458
+ import { EditorContent, BubbleMenu } from '@tiptap/vue-3';
460
459
  import { useRichTextEditor } from '../../composables/rich-text-editor/useRichTextEditor';
461
460
  import FontPicker from '@shared/font-family-selector/FontPicker.vue';
462
461
  import ColorField from '@shared/content-settings/components/color-field/ColorField.vue';
@@ -954,7 +953,7 @@ const {
954
953
  hyperlinkValue,
955
954
  setHyperlinkUrlValue,
956
955
  shouldShow,
957
- bubbleMenuOptions,
956
+ tippyOptions,
958
957
  handleAdminColorChange,
959
958
  } = useRichTextEditor(
960
959
  model,
@@ -0,0 +1,135 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import mixin from './TemplatePreview.mixin';
3
+
4
+ // Covers the `previewImageUrl` computed's content-key precedence. The getter is pure over
5
+ // `this` (template/isPersonalisable/userToken/logoImage), so it's invoked directly with a
6
+ // mocked `this` rather than mounting the component.
7
+ const previewImageUrl = (mixin as any).computed.previewImageUrl as (
8
+ this: unknown,
9
+ ) => string;
10
+
11
+ const BASE =
12
+ 'https://cdn/preview?templateToken=t&logoDraftToken=l&deviceType=Desktop';
13
+
14
+ function run(context: {
15
+ template: {
16
+ previewImageUrl?: string;
17
+ contentKey?: string;
18
+ usesUserPromptContentKey?: boolean;
19
+ };
20
+ isPersonalisable?: boolean;
21
+ userToken?: string | null;
22
+ logoImage?: { logoVariation?: number };
23
+ }): string {
24
+ return previewImageUrl.call({
25
+ isPersonalisable: true,
26
+ userToken: null,
27
+ logoImage: undefined,
28
+ ...context,
29
+ });
30
+ }
31
+
32
+ describe('TemplatePreview.mixin previewImageUrl', () => {
33
+ it('overrides a backend-baked logo-draft contentKey with the editor userPrompt key (bp-5941)', () => {
34
+ // Empty-search post-purchase: backend baked the per-template logo-draft contentKey.
35
+ const result = run({
36
+ template: {
37
+ previewImageUrl: `${BASE}&contentKey=5267ff6d&userToken=abc`,
38
+ contentKey: '2c077d2f',
39
+ usesUserPromptContentKey: true,
40
+ },
41
+ });
42
+
43
+ expect(result).toContain('userPromptContentKey=2c077d2f');
44
+ expect(result).not.toContain('contentKey=5267ff6d');
45
+ // Left in place — the renderer ignores it once userPromptContentKey is set.
46
+ expect(result).toContain('userToken=abc');
47
+ });
48
+
49
+ it('does not duplicate the userPrompt key when the backend already baked it', () => {
50
+ const result = run({
51
+ template: {
52
+ previewImageUrl: `${BASE}&userPromptContentKey=2c077d2f`,
53
+ contentKey: '2c077d2f',
54
+ usesUserPromptContentKey: true,
55
+ },
56
+ });
57
+
58
+ expect(result.match(/userPromptContentKey=2c077d2f/g)).toHaveLength(1);
59
+ });
60
+
61
+ it('uses the regular bucket (contentKey + userToken) when there is no userPrompt key', () => {
62
+ const result = run({
63
+ template: {
64
+ previewImageUrl: BASE,
65
+ contentKey: 'abc123',
66
+ usesUserPromptContentKey: false,
67
+ },
68
+ userToken: 'user-1',
69
+ });
70
+
71
+ expect(result).toContain('contentKey=abc123');
72
+ expect(result).toContain('userToken=user-1');
73
+ expect(result).not.toContain('userPromptContentKey=');
74
+ });
75
+
76
+ it('leaves an already-personalised regular-bucket url untouched', () => {
77
+ const result = run({
78
+ template: {
79
+ previewImageUrl: `${BASE}&contentKey=already&userToken=abc`,
80
+ contentKey: 'ignored',
81
+ usesUserPromptContentKey: false,
82
+ },
83
+ userToken: 'user-1',
84
+ });
85
+
86
+ expect(result).toContain('contentKey=already');
87
+ expect(result).not.toContain('contentKey=ignored');
88
+ });
89
+
90
+ it('appends the logo variation regardless of the content-key branch', () => {
91
+ const result = run({
92
+ template: {
93
+ previewImageUrl: `${BASE}&contentKey=5267ff6d`,
94
+ contentKey: '2c077d2f',
95
+ usesUserPromptContentKey: true,
96
+ },
97
+ logoImage: { logoVariation: 3 },
98
+ });
99
+
100
+ expect(result).toContain('userPromptContentKey=2c077d2f');
101
+ expect(result).toContain('logoVariation=3');
102
+ });
103
+
104
+ // The old regex only matched params preceded by `&`, so a leading `?contentKey=`
105
+ // survived and the URL carried BOTH keys — the exact divergence this override exists
106
+ // to prevent. Param order must not matter.
107
+ it('strips a baked contentKey even when it is the first query param', () => {
108
+ const result = run({
109
+ template: {
110
+ previewImageUrl:
111
+ 'https://cdn/preview?contentKey=5267ff6d&templateToken=t&userToken=abc',
112
+ contentKey: '2c077d2f',
113
+ usesUserPromptContentKey: true,
114
+ },
115
+ });
116
+
117
+ expect(result).not.toContain('contentKey=5267ff6d');
118
+ expect(result).toContain('userToken=abc');
119
+ expect(result).toContain('userPromptContentKey=2c077d2f');
120
+ expect(result).toContain('templateToken=t');
121
+ });
122
+
123
+ it('leaves a single userPromptContentKey when one was already baked', () => {
124
+ const result = run({
125
+ template: {
126
+ previewImageUrl: 'https://cdn/preview?userPromptContentKey=STALE&x=1',
127
+ contentKey: 'EDITOR',
128
+ usesUserPromptContentKey: true,
129
+ },
130
+ });
131
+
132
+ expect(result.match(/userPromptContentKey=/g)).toHaveLength(1);
133
+ expect(result).toContain('userPromptContentKey=EDITOR');
134
+ });
135
+ });
@@ -32,6 +32,21 @@ import {
32
32
  } from '@brand-page/utils/logo.util';
33
33
  import { useVideoStore } from '@src/lib';
34
34
 
35
+ // Content params the backend may bake into a preview URL. When the editor's key wins,
36
+ // none of these may survive — a stale one alongside the new key is the bp-5941 mismatch.
37
+ // contentKey MUST stay listed: preview-renderer prefers contentKey+userToken over
38
+ // userPromptContentKey, so a surviving contentKey would silently win. userToken alone
39
+ // can't trigger that branch, so it's left as the backend sent it.
40
+ const BAKED_CONTENT_PARAMS = ['contentKey', 'userPromptContentKey'];
41
+
42
+ const toUrl = (value: string): URL | null => {
43
+ try {
44
+ return new URL(value);
45
+ } catch {
46
+ return null;
47
+ }
48
+ };
49
+
35
50
  const DEBOUNCE_IN_MS = 250;
36
51
 
37
52
  interface TemplatePreviewData {
@@ -237,29 +252,55 @@ export default defineComponent({
237
252
  return '';
238
253
  }
239
254
 
240
- const isAlreadyPersonalised =
241
- previewImageUrl.includes('contentKey=') ||
242
- previewImageUrl.includes('userPromptContentKey=');
243
-
244
- if (
255
+ const hasAuthoritativeUserPromptKey =
245
256
  this.isPersonalisable &&
246
- this.template.contentKey &&
247
- !isAlreadyPersonalised
248
- ) {
249
- if (this.userToken && !this.template.usesUserPromptContentKey) {
250
- previewImageUrl += `&contentKey=${this.template.contentKey}`;
251
-
252
- // The backend already bakes userToken into brand-site preview URLs. Only add
253
- // it when missing - two userToken params parse as an array and the preview
254
- // renderer rejects it ("userToken must be a string type") (bp-5765).
255
- if (!previewImageUrl.includes('userToken=')) {
256
- previewImageUrl += `&userToken=${this.userToken}`;
257
+ this.template.usesUserPromptContentKey &&
258
+ !!this.template.contentKey;
259
+
260
+ if (hasAuthoritativeUserPromptKey) {
261
+ // The editor resolves post-purchase content from the current industry, so its
262
+ // userPrompt key is authoritative. On empty search the backend bakes a per-template
263
+ // logo-draft contentKey (DY-1305) that diverges from the editor (bp-5941); strip any
264
+ // baked content params and resolve from the userPrompt S3 bucket via the editor's key.
265
+ const url = toUrl(previewImageUrl);
266
+
267
+ // Not a parseable absolute URL — nothing to personalise, leave it alone.
268
+ if (!url) {
269
+ return previewImageUrl;
270
+ }
271
+
272
+ BAKED_CONTENT_PARAMS.forEach((param) => url.searchParams.delete(param));
273
+ url.searchParams.set(
274
+ 'userPromptContentKey',
275
+ this.template.contentKey as string,
276
+ );
277
+
278
+ previewImageUrl = url.toString();
279
+ } else {
280
+ const isAlreadyPersonalised =
281
+ previewImageUrl.includes('contentKey=') ||
282
+ previewImageUrl.includes('userPromptContentKey=');
283
+
284
+ if (
285
+ this.isPersonalisable &&
286
+ this.template.contentKey &&
287
+ !isAlreadyPersonalised
288
+ ) {
289
+ if (this.userToken) {
290
+ previewImageUrl += `&contentKey=${this.template.contentKey}`;
291
+
292
+ // The backend already bakes userToken into brand-site preview URLs. Only add
293
+ // it when missing - two userToken params parse as an array and the preview
294
+ // renderer rejects it ("userToken must be a string type") (bp-5765).
295
+ if (!previewImageUrl.includes('userToken=')) {
296
+ previewImageUrl += `&userToken=${this.userToken}`;
297
+ }
298
+ } else {
299
+ // userPromptContentKey resolves from the userPrompt S3 bucket (real AI images).
300
+ // Post-purchase designs with a userPrompt key route here too, NOT through the
301
+ // userToken/regular-bucket branch above which holds placeholders (bp-5454).
302
+ previewImageUrl += `&userPromptContentKey=${this.template.contentKey}`;
257
303
  }
258
- } else {
259
- // userPromptContentKey resolves from the userPrompt S3 bucket (real AI images).
260
- // Post-purchase designs with a userPrompt key route here too, NOT through the
261
- // userToken/regular-bucket branch above which holds placeholders (bp-5454).
262
- previewImageUrl += `&userPromptContentKey=${this.template.contentKey}`;
263
304
  }
264
305
  }
265
306
 
@@ -33,6 +33,7 @@ export function useAiMetadataBuilder(logger: Logger) {
33
33
  model: BrandPageModel,
34
34
  aiAssistantToolCall: AiAssistantToolCall[],
35
35
  options: {
36
+ maxCharacters?: number;
36
37
  /**
37
38
  * Invoked by the UndoBuilder to resolve the pre-prompt snapshot.
38
39
  * Required when the response includes an `undo` tool call.