@designcrowd/library.brand-page 6.0.25 → 6.0.26-json-prereq-2

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 (176) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-CS3ct0wa.js → CreatedOnBrandCrowd-de-de-CKBqjDH3.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-CmBcnDu6.js → CreatedOnBrandCrowd-en-us-CRXiimvT.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-BdTuJtqV.js → CreatedOnBrandCrowd-es-es-DCYdY6sj.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-Cx_H9X6l.js → CreatedOnBrandCrowd-fr-ca-Dk7LOhxE.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-Cx_H9X6l.js → CreatedOnBrandCrowd-fr-fr-Dk7LOhxE.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-BZZ9hjUi.js → CreatedOnBrandCrowd-pt-br-Cp4ufHJP.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-BZZ9hjUi.js → CreatedOnBrandCrowd-pt-pt-Cp4ufHJP.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-SbONDQN1.js → CreatedOnDcom-de-de-ZSLkWMwM.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-Beno1_gw.js → CreatedOnDcom-en-us-BgdIGfT3.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-BBE_zdRu.js → CreatedOnDcom-es-es-D0KemkCZ.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-C6pgDB5x.js → CreatedOnDcom-fr-ca-BuwNjNbC.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-FD1eqtg3.js → CreatedOnDcom-fr-fr-DCQcsFPf.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-BiSx3OwM.js → CreatedOnDcom-pt-br-DD-oLAS1.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-Dn4DmKGA.js → CreatedOnDcom-pt-pt-D8qLq2wW.js} +1 -1
  15. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +5 -2
  16. package/lib/src/brand-page/models/index.d.ts +34 -26
  17. package/lib/src/brand-page/utils/migration/migration-20.d.ts +2 -0
  18. package/lib/src/brand-page/utils/migration/migration-20.js +66 -0
  19. package/lib/src/brand-page/utils/migration.util.js +11 -0
  20. package/lib/src/brand-page/utils/text.util.d.ts +1 -1
  21. package/lib/src/brand-page/utils/text.util.js +32 -0
  22. package/lib/src/brand-page/utils/vcard.util.js +4 -4
  23. package/lib/src/brand-page/utils/view-model.util.d.ts +1 -0
  24. package/lib/src/brand-page/utils/view-model.util.js +68 -20
  25. package/lib/src/brand-page/view-models.d.ts +3 -3
  26. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +22 -0
  27. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +22 -0
  28. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +1 -1
  29. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +1 -1
  30. package/lib/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.d.ts +0 -1
  31. package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +21 -9
  32. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.d.ts +2 -9
  33. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.js +6 -10
  34. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +14 -0
  35. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +39 -0
  36. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +28 -0
  37. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +90 -0
  38. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +1 -1
  39. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +24 -103
  40. package/lib/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.d.ts +8 -2
  41. package/lib/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.js +20 -0
  42. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +7 -0
  43. package/lib/src/brand-page-maker/utils/index.d.ts +2 -0
  44. package/lib/src/brand-page-maker/utils/index.js +2 -0
  45. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +28 -5
  46. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +48 -5
  47. package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +63 -0
  48. package/lib/src/brand-page-maker/utils/rte-render.util.js +95 -0
  49. package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +6 -0
  50. package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +158 -0
  51. package/lib/src/brand-page-maker/validators/field.validator.js +10 -2
  52. package/lib/src/css/library.brand-page-brandCrowd.min.css +39 -39
  53. package/lib/src/css/library.brand-page-brandPage.min.css +39 -39
  54. package/lib/src/css/library.brand-page-designCom.min.css +39 -39
  55. package/lib/src/css/library.brand-page-preview.min.css +39 -39
  56. package/lib/src/index.d.ts +1 -0
  57. package/lib/src/index.mjs +143 -138
  58. package/lib/src/{lib-BVHFYSLL.js → lib-Db6j9fQZ.js} +56262 -52768
  59. package/lib/src/lib.js +2 -0
  60. package/lib/src/metadata-builder/ai-metadata-executor.d.ts +0 -1
  61. package/lib/src/metadata-builder/ai-metadata-executor.js +0 -19
  62. package/lib/src/metadata-builder/builders/section-builders/about-section-builder.js +2 -3
  63. package/lib/src/metadata-builder/builders/section-builders/banner-section-builder.js +2 -3
  64. package/lib/src/metadata-builder/builders/section-builders/booking-section-builder.js +2 -3
  65. package/lib/src/metadata-builder/builders/section-builders/contact-form-builder.js +3 -4
  66. package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +2 -3
  67. package/lib/src/metadata-builder/builders/section-builders/mailing-list-builder.js +1 -2
  68. package/lib/src/metadata-builder/builders/section-builders/mailing-list-section-builder.js +1 -2
  69. package/lib/src/metadata-builder/builders/section-builders/profile-section-builder.js +3 -4
  70. package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +2 -3
  71. package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +1 -2
  72. package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +15 -11
  73. package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +11 -13
  74. package/lib/src/metadata-builder/helpers/rte.helpers.js +14 -73
  75. package/lib/src/metadata-builder/metadata-executor.js +0 -4
  76. package/lib/src/metadata-builder/models.d.ts +0 -2
  77. package/lib/src/metadata-builder/models.js +0 -2
  78. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +0 -19
  79. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +0 -4
  80. package/lib/src/metadata-builder/sanitizers/rte-schema-check.d.ts +7 -8
  81. package/lib/src/metadata-builder/sanitizers/rte-schema-check.js +21 -52
  82. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +2 -2
  83. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +14 -1
  84. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +1 -1
  85. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +1 -1
  86. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +12 -3
  87. package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +2 -2
  88. package/lib/src/shared/content-settings/components/radio-field/RadioField.mixin.d.ts +1 -1
  89. package/lib/src/shared/content-settings/components/slider-field/SliderField.mixin.d.ts +1 -1
  90. package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +2 -2
  91. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.d.ts +3 -2
  92. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.js +10 -3
  93. package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +2 -2
  94. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +2 -1
  95. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +4 -1
  96. package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +2 -2
  97. package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +2 -2
  98. package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +2 -2
  99. package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +1 -1
  100. package/lib/src/utils/minify-brand-page-metadata.js +4 -1
  101. package/package.json +17 -15
  102. package/src/brand-page/components/contact-form/ContactForm.mixin.ts +5 -2
  103. package/src/brand-page/components/text-wrapper/TextWrapper.vue +17 -6
  104. package/src/brand-page/models/index.ts +35 -26
  105. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
  106. package/src/brand-page/utils/migration/migration-20.server.test.ts +125 -0
  107. package/src/brand-page/utils/migration/migration-20.test.ts +72 -0
  108. package/src/brand-page/utils/migration/migration-20.ts +87 -0
  109. package/src/brand-page/utils/migration.util.ts +12 -0
  110. package/src/brand-page/utils/style.util.test.ts +51 -0
  111. package/src/brand-page/utils/text.util.test.ts +47 -0
  112. package/src/brand-page/utils/text.util.ts +43 -4
  113. package/src/brand-page/utils/vcard.util.ts +6 -4
  114. package/src/brand-page/utils/view-model.pageid-rte.test.ts +104 -0
  115. package/src/brand-page/utils/view-model.util.ts +94 -26
  116. package/src/brand-page/view-models.ts +3 -2
  117. package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +23 -0
  118. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +4 -3
  119. package/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.ts +0 -1
  120. package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +28 -9
  121. package/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.ts +6 -10
  122. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +52 -0
  123. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +42 -0
  124. package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +137 -0
  125. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +27 -111
  126. package/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.ts +23 -0
  127. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +8 -0
  128. package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +202 -0
  129. package/src/brand-page-maker/utils/index.ts +2 -0
  130. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +37 -3
  131. package/src/brand-page-maker/utils/rich-text-editor.util.ts +66 -11
  132. package/src/brand-page-maker/utils/rte-features.test.ts +351 -0
  133. package/src/brand-page-maker/utils/rte-render-parity.test.ts +157 -0
  134. package/src/brand-page-maker/utils/rte-render.util.ts +165 -0
  135. package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +158 -0
  136. package/src/brand-page-maker/utils/rte-sanitize.util.ts +195 -0
  137. package/src/brand-page-maker/validators/field.validator.test.ts +55 -0
  138. package/src/brand-page-maker/validators/field.validator.ts +10 -2
  139. package/src/lib.ts +9 -0
  140. package/src/metadata-builder/ai-metadata-executor.ts +0 -26
  141. package/src/metadata-builder/builders/section-builders/about-section-builder.ts +2 -3
  142. package/src/metadata-builder/builders/section-builders/banner-section-builder.ts +2 -3
  143. package/src/metadata-builder/builders/section-builders/booking-section-builder.ts +2 -5
  144. package/src/metadata-builder/builders/section-builders/contact-form-builder.ts +3 -4
  145. package/src/metadata-builder/builders/section-builders/images-section-builder.ts +2 -3
  146. package/src/metadata-builder/builders/section-builders/mailing-list-builder.ts +1 -2
  147. package/src/metadata-builder/builders/section-builders/mailing-list-section-builder.ts +1 -2
  148. package/src/metadata-builder/builders/section-builders/profile-section-builder.ts +3 -6
  149. package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +2 -3
  150. package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +1 -2
  151. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +19 -13
  152. package/src/metadata-builder/helpers/rte.helpers.ts +15 -101
  153. package/src/metadata-builder/metadata-executor.ts +0 -5
  154. package/src/metadata-builder/models.ts +0 -3
  155. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +0 -27
  156. package/src/metadata-builder/sanitizers/rte-schema-check.ts +24 -53
  157. package/src/shared/content-settings/components/GenericTextInput.mixin.ts +17 -1
  158. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +16 -3
  159. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.ts +13 -4
  160. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.vue +6 -4
  161. package/src/shared/content-settings/components/text-image-field/TextImageField.vue +6 -3
  162. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +6 -1
  163. package/src/shared/content-settings/components/text-items-field/TextItemsField.vue +1 -1
  164. package/src/utils/minify-brand-page-metadata.ts +4 -1
  165. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.d.ts +0 -20
  166. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.js +0 -44
  167. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.d.ts +0 -27
  168. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.js +0 -44
  169. package/lib/src/metadata-builder/builders/update-content-text-builder.d.ts +0 -17
  170. package/lib/src/metadata-builder/builders/update-content-text-builder.js +0 -103
  171. package/lib/src/metadata-builder/builders/update-content-text-style-builder.d.ts +0 -16
  172. package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +0 -216
  173. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.ts +0 -73
  174. package/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.ts +0 -85
  175. package/src/metadata-builder/builders/update-content-text-builder.ts +0 -148
  176. package/src/metadata-builder/builders/update-content-text-style-builder.ts +0 -352
@@ -3,7 +3,6 @@ import type {
3
3
  BrandPageModel,
4
4
  TextStyleDeclaration,
5
5
  } from '../../../brand-page/models';
6
- import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
7
6
  import type { BuilderResult } from '../../models';
8
7
  import {
9
8
  isTiptapDocument,
@@ -41,7 +40,7 @@ export class ProfileSectionBuilder extends BaseSectionBuilder<ProfileContent> {
41
40
 
42
41
  if (profileArgs.name !== undefined) {
43
42
  if (isTiptapDocument(profileArgs.name)) {
44
- resolved.content.name = tiptapJsonToHtml(profileArgs.name);
43
+ resolved.content.name = profileArgs.name;
45
44
  resolved.content.nameStyle = resolveGlobalPresetIntoStyle(
46
45
  resolved.content.nameStyle,
47
46
  textStyleOptions,
@@ -53,7 +52,7 @@ export class ProfileSectionBuilder extends BaseSectionBuilder<ProfileContent> {
53
52
 
54
53
  if (profileArgs.title !== undefined) {
55
54
  if (isTiptapDocument(profileArgs.title)) {
56
- resolved.content.title = tiptapJsonToHtml(profileArgs.title);
55
+ resolved.content.title = profileArgs.title;
57
56
  resolved.content.titleStyle = resolveGlobalPresetIntoStyle(
58
57
  resolved.content.titleStyle,
59
58
  textStyleOptions,
@@ -65,9 +64,7 @@ export class ProfileSectionBuilder extends BaseSectionBuilder<ProfileContent> {
65
64
 
66
65
  if (profileArgs.description !== undefined) {
67
66
  if (isTiptapDocument(profileArgs.description)) {
68
- resolved.content.description = tiptapJsonToHtml(
69
- profileArgs.description,
70
- );
67
+ resolved.content.description = profileArgs.description;
71
68
  resolved.content.descriptionStyle = resolveGlobalPresetIntoStyle(
72
69
  resolved.content.descriptionStyle,
73
70
  textStyleOptions,
@@ -5,7 +5,6 @@ import type {
5
5
  TextShapeItem,
6
6
  TextStyleDeclaration,
7
7
  } from '../../../brand-page/models';
8
- import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
9
8
  import type { BuilderResult } from '../../models';
10
9
  import {
11
10
  isTiptapDocument,
@@ -195,7 +194,7 @@ export class ShapesSectionBuilder extends BaseSectionBuilder<ShapesSectionConten
195
194
 
196
195
  if (args.title !== undefined) {
197
196
  if (isTiptapDocument(args.title)) {
198
- content.title = tiptapJsonToHtml(args.title);
197
+ content.title = args.title;
199
198
  content.titleStyle = resolveGlobalPresetIntoStyle(
200
199
  content.titleStyle,
201
200
  textStyleOptions,
@@ -207,7 +206,7 @@ export class ShapesSectionBuilder extends BaseSectionBuilder<ShapesSectionConten
207
206
 
208
207
  if (args.description !== undefined) {
209
208
  if (isTiptapDocument(args.description)) {
210
- content.description = tiptapJsonToHtml(args.description);
209
+ content.description = args.description;
211
210
  content.descriptionStyle = resolveGlobalPresetIntoStyle(
212
211
  content.descriptionStyle,
213
212
  textStyleOptions,
@@ -4,7 +4,6 @@ import type {
4
4
  BrandPageModel,
5
5
  TextStyleDeclaration,
6
6
  } from '../../../brand-page/models';
7
- import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
8
7
  import {
9
8
  isTiptapDocument,
10
9
  type TestimonialItem,
@@ -173,7 +172,7 @@ export class TestimonialSectionBuilder extends BaseSectionBuilder<TestimonialSec
173
172
 
174
173
  if (args.title !== undefined) {
175
174
  if (isTiptapDocument(args.title)) {
176
- content.title = tiptapJsonToHtml(args.title);
175
+ content.title = args.title;
177
176
  content.titleStyle = resolveGlobalPresetIntoStyle(
178
177
  content.titleStyle,
179
178
  textStyleOptions,
@@ -4,9 +4,9 @@ import type {
4
4
  TextItem,
5
5
  TextStyleDeclaration,
6
6
  BrandPageModel,
7
+ RichTextValue,
7
8
  } from '../../../brand-page/models';
8
9
  import type { TextStyleOptions } from '../../../brand-page/models/styles.model';
9
- import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
10
10
  import {
11
11
  isTiptapDocument,
12
12
  type TextSectionArguments,
@@ -26,12 +26,18 @@ import {
26
26
  repositionItemByAnchor,
27
27
  } from '../item-id-utils';
28
28
 
29
- /** Resolve a RichTextField (string or TiptapDocument) to a plain HTML string. */
30
- const resolveText = (value: unknown): { html: string; wasTiptap: boolean } => {
31
- if (isTiptapDocument(value))
32
- return { html: tiptapJsonToHtml(value), wasTiptap: true };
29
+ /**
30
+ * Resolve a RichTextField to its stored RTE value. BP-5787: Tiptap JSON is
31
+ * stored as-is (no JSON→HTML round-trip — migration-20 is now a one-time
32
+ * legacy conversion), plain strings pass through. `wasTiptap` drives the
33
+ * text-style preset resolution for rich content.
34
+ */
35
+ const resolveText = (
36
+ value: unknown,
37
+ ): { value: RichTextValue; wasTiptap: boolean } => {
38
+ if (isTiptapDocument(value)) return { value, wasTiptap: true };
33
39
 
34
- return { html: typeof value === 'string' ? value : '', wasTiptap: false };
40
+ return { value: typeof value === 'string' ? value : '', wasTiptap: false };
35
41
  };
36
42
 
37
43
  const clearTextStyleTypeIfTiptap = (
@@ -55,11 +61,11 @@ const createTextItem = (
55
61
  item.text_style ? { ...baseStyle, ...item.text_style } : { ...baseStyle }
56
62
  ) as TextStyleDeclaration;
57
63
 
58
- const { html, wasTiptap } = resolveText(item.text);
64
+ const { value, wasTiptap } = resolveText(item.text);
59
65
 
60
66
  return {
61
67
  id: item.item_id || generateItemId(),
62
- text: html,
68
+ text: value,
63
69
  textStyle: clearTextStyleTypeIfTiptap(
64
70
  textStyle,
65
71
  wasTiptap,
@@ -111,9 +117,9 @@ const mergeTextItems = (
111
117
  if (!existing) continue;
112
118
 
113
119
  if (item.text !== undefined) {
114
- const { html, wasTiptap } = resolveText(item.text);
120
+ const { value, wasTiptap } = resolveText(item.text);
115
121
 
116
- existing.text = html;
122
+ existing.text = value;
117
123
  existing.textStyle = clearTextStyleTypeIfTiptap(
118
124
  existing.textStyle || {},
119
125
  wasTiptap,
@@ -160,12 +166,12 @@ const applySingleTextField = (
160
166
  textStyleOptions: TextStyleOptions | undefined,
161
167
  ): void => {
162
168
  if (args.text !== undefined) {
163
- const { html, wasTiptap } = resolveText(args.text);
169
+ const { value, wasTiptap } = resolveText(args.text);
164
170
 
165
171
  if (Array.isArray(content.textItems) && content.textItems.length > 0) {
166
172
  content.textItems[0] = {
167
173
  ...content.textItems[0],
168
- text: html,
174
+ text: value,
169
175
  textStyle: clearTextStyleTypeIfTiptap(
170
176
  content.textItems[0].textStyle || {},
171
177
  wasTiptap,
@@ -176,7 +182,7 @@ const applySingleTextField = (
176
182
  content.textItems = [
177
183
  {
178
184
  id: generateItemId(),
179
- text: html,
185
+ text: value,
180
186
  textStyle: clearTextStyleTypeIfTiptap(
181
187
  { ...(content.textStyle || {}) },
182
188
  wasTiptap,
@@ -1,114 +1,28 @@
1
- import type { TextStyleDeclaration } from '../../brand-page';
2
- import { getRTEColor } from '../../brand-page-maker/utils';
3
- import { useExternalRichTextEditor } from '../../brand-page-maker/composables/rich-text-editor/useRichTextEditor';
4
- import { getRenderText } from '../../brand-page/utils';
5
- import { ESCAPED_NEWLINE_CHARACTER } from '../../constants/ui';
6
- import type { Editor } from '@tiptap/vue-3';
7
- import {
8
- isTiptapDocument,
9
- type TiptapDocument,
10
- } from '../parsers/section-tool-arguments';
11
-
12
- export const applyStyleToRTEText = (
13
- externalEditor: Editor,
14
- contentText: string,
15
- newContentStyle: TextStyleDeclaration,
16
- ) => {
17
- if (!contentText) {
18
- return contentText;
19
- }
20
-
21
- const content = contentText.includes(ESCAPED_NEWLINE_CHARACTER)
22
- ? getRenderText(contentText)
23
- : contentText;
24
-
25
- externalEditor.commands.setContent(content);
26
-
27
- let chain = externalEditor.chain().selectAll();
28
-
29
- if (newContentStyle?.fontFamily) {
30
- chain = chain.setFontFamily(newContentStyle.fontFamily);
31
- }
32
-
33
- if (newContentStyle?.fontSize) {
34
- chain = chain.setFontSize(newContentStyle.fontSize);
35
- }
36
-
37
- if (newContentStyle?.letterSpacing) {
38
- chain = chain.setLetterSpacing(newContentStyle.letterSpacing);
39
- }
40
-
41
- if (newContentStyle?.lineHeight) {
42
- chain = chain.setLineHeight(newContentStyle.lineHeight);
43
- }
44
-
45
- if (newContentStyle?.color) {
46
- chain = chain.setMark('textStyle', {
47
- color: getRTEColor(newContentStyle.color, 'text'),
48
- });
49
- }
50
-
51
- // Handle backgroundColor
52
- if ('backgroundColor' in (newContentStyle || {})) {
53
- chain = newContentStyle.backgroundColor
54
- ? chain.setMark('highlight', {
55
- color: getRTEColor(newContentStyle.backgroundColor, 'background'),
56
- })
57
- : chain.unsetHighlight();
58
- }
59
-
60
- if (newContentStyle?.textAlign) {
61
- chain = chain.setTextAlign(newContentStyle.textAlign);
62
- }
63
-
64
- const hasStrike = newContentStyle?.textDecoration?.includes('line-through');
65
- const hasUnderline = newContentStyle?.textDecoration?.includes('underline');
66
-
67
- chain = hasStrike ? chain.setStrike() : chain.unsetStrike();
68
- chain = hasUnderline ? chain.setUnderline() : chain.unsetUnderline();
69
-
70
- chain =
71
- newContentStyle?.fontWeight === 'bold'
72
- ? chain.setBold()
73
- : chain.unsetBold();
74
- chain =
75
- newContentStyle?.fontStyle === 'italic'
76
- ? chain.setItalic()
77
- : chain.unsetItalic();
78
-
79
- chain.run();
80
- const html = externalEditor.getHTML();
81
-
82
- return html;
83
- };
1
+ import type { RichTextValue } from '../../brand-page';
2
+ import { renderRteToHtml } from '../../brand-page-maker/utils';
3
+ import { isTiptapDocument } from '../parsers/section-tool-arguments';
84
4
 
85
5
  /**
86
6
  * Converts a Tiptap JSON document (ProseMirror document model) to an HTML
87
- * string using a headless Tiptap Editor instance.
7
+ * string via Tiptap's schema-driven static renderer (renderRteToHtml →
8
+ * renderToHTMLString). DOM-less and editor-free, so it runs on the server
9
+ * (Nitro/Node), not just the browser — unlike the previous headless-Editor
10
+ * implementation (BP-5787: the AI section builders no longer need a `document`).
88
11
  *
89
12
  * Returns the value unchanged if it is already a string.
90
- * Returns an empty string for null/undefined.
13
+ * Returns an empty string for null/undefined or any non-Tiptap-document value.
91
14
  *
92
- * Note: Tiptap's `setContent` does NOT throw on malformed input — it
93
- * silently drops content and returns `<p></p>`. To prevent saved copy
94
- * being wiped (BP-5669), structural validity of any AI-emitted Tiptap
95
- * doc is checked upstream by `rteSchemaCheck` in
96
- * `section-args-validator.ts` using the editor's real ProseMirror
97
- * schema. The validator surfaces a clear error into
98
- * `BuilderResult.errors`, which the AI-chat retry loop reads via
99
- * `tool_result.errorMessage` and uses to re-emit a corrected payload.
15
+ * Note: to prevent saved copy being wiped (BP-5669), structural validity of any
16
+ * AI-emitted Tiptap doc is checked upstream by `rteSchemaCheck` in
17
+ * `section-args-validator.ts` against the editor's real ProseMirror schema. The
18
+ * validator surfaces a clear error into `BuilderResult.errors`, which the
19
+ * AI-chat retry loop reads via `tool_result.errorMessage` and uses to re-emit a
20
+ * corrected payload.
100
21
  */
101
22
  export const tiptapJsonToHtml = (value: unknown): string => {
102
23
  if (typeof value === 'string') return value;
103
24
 
104
25
  if (!isTiptapDocument(value)) return '';
105
26
 
106
- const { externalEditor } = useExternalRichTextEditor();
107
-
108
- externalEditor.commands.setContent(value as TiptapDocument);
109
- const html = externalEditor.getHTML();
110
-
111
- externalEditor.destroy();
112
-
113
- return html;
27
+ return renderRteToHtml(value as RichTextValue);
114
28
  };
@@ -8,8 +8,6 @@ import {
8
8
  BUILDER_TYPES,
9
9
  type BuilderResult,
10
10
  } from './models';
11
- import { UpdateContentTextBuilder } from './builders/update-content-text-builder';
12
- import { UpdateContentTextStyleBuilder } from './builders/update-content-text-style-builder';
13
11
  import {
14
12
  BannerSectionBuilder,
15
13
  BookingSectionBuilder,
@@ -48,9 +46,6 @@ type BuilderConstructor = new (
48
46
 
49
47
  // Lazy initialization to avoid circular dependency issues
50
48
  const getBuilderRegistry = (): Record<BuilderType, BuilderConstructor> => ({
51
- [BUILDER_TYPES.UPDATE_CONTENT_TEXT]: UpdateContentTextBuilder,
52
- [BUILDER_TYPES.UPDATE_CONTENT_TEXT_STYLE]: UpdateContentTextStyleBuilder,
53
-
54
49
  // Section builders (create)
55
50
  [BUILDER_TYPES.CREATE_BANNER_SECTION]: BannerSectionBuilder,
56
51
  [BUILDER_TYPES.CREATE_BOOKING_SECTION]: BookingSectionBuilder,
@@ -3,9 +3,6 @@ import type { EventTracker } from '../brand-page-maker/composables/useEventTrack
3
3
  import type { Logger } from './logger';
4
4
 
5
5
  export const BUILDER_TYPES = {
6
- UPDATE_CONTENT_TEXT: 'update-content-text',
7
- UPDATE_CONTENT_TEXT_STYLE: 'update-content-text-style',
8
-
9
6
  // Per-section builder types (create)
10
7
  CREATE_BANNER_SECTION: 'create-banner-section',
11
8
  CREATE_ABOUT_SECTION: 'create-about-section',
@@ -7,15 +7,10 @@ import type {
7
7
  import type { SectionType, TextStyleArguments } from './section-tool-arguments';
8
8
 
9
9
  // Tool names — must match the function names generated by the AI pipeline.
10
- // Text element tools use camelCase names.
11
10
  // Section tools follow the pattern: build_{sectionType}_section
12
11
  // where sectionType matches the ContentType value (e.g. 'banner-section', 'profile').
13
12
  // The mode ('create' | 'update') is a field in the tool arguments, not the tool name.
14
13
  export const AI_ASSISTANT_TOOL_NAMES = {
15
- // Text element agent tools
16
- UPDATE_TEXT: 'updateText',
17
- UPDATE_STYLE: 'updateStyle',
18
-
19
14
  // Website builder agent tools — unified build
20
15
  BUILD_PROFILE_SECTION: 'build_profile_section',
21
16
  BUILD_TEXT_SECTION: 'build_text_section',
@@ -84,28 +79,6 @@ export interface ParsedToolCall {
84
79
  usage_type?: 'free' | 'paid';
85
80
  }
86
81
 
87
- export interface BaseArguments {
88
- message?: string;
89
- }
90
-
91
- // updateText tool arguments
92
- export interface UpdateTextToolArguments extends BaseArguments {
93
- contentId: string;
94
- fieldName: string;
95
- metadata: {
96
- text: string;
97
- };
98
- }
99
-
100
- // updateStyle tool arguments
101
- export interface UpdateStyleToolArguments extends BaseArguments {
102
- contentId: string;
103
- fieldName: string;
104
- metadata: {
105
- style: BrandPageCssStyle;
106
- };
107
- }
108
-
109
82
  // Maps each section tool name to its SectionType.
110
83
  // Mode is now determined from the tool arguments, not the tool name.
111
84
  export const SECTION_TOOL_MAP: Record<string, { sectionType: SectionType }> = {
@@ -1,16 +1,6 @@
1
- import { Editor } from '@tiptap/vue-3';
2
- import StarterKit from '@tiptap/starter-kit';
3
- import Underline from '@tiptap/extension-underline';
4
- import TextStyle from '@tiptap/extension-text-style';
5
- import TextAlign from '@tiptap/extension-text-align';
6
- import FontFamily from '@tiptap/extension-font-family';
7
- import ColorExtension from '@tiptap/extension-color';
8
- import Highlight from '@tiptap/extension-highlight';
9
- import Link from '@tiptap/extension-link';
10
- import { FontSize } from '../../brand-page-maker/composables/rich-text-editor/extensions/FontSize';
11
- import { LetterSpacing } from '../../brand-page-maker/composables/rich-text-editor/extensions/LetterSpacing';
12
- import { LineHeight } from '../../brand-page-maker/composables/rich-text-editor/extensions/LineHeight';
13
- import { AutoLink } from '../../brand-page-maker/composables/rich-text-editor/extensions/AutoLink';
1
+ import { getSchema } from '@tiptap/core';
2
+ import type { Schema } from '@tiptap/pm/model';
3
+ import { getRteExtensions } from '../../brand-page-maker/composables/rich-text-editor/getRteExtensions';
14
4
  import { isTiptapDocument } from '../parsers/section-tool-arguments';
15
5
  import {
16
6
  rteStyleCheck,
@@ -19,45 +9,30 @@ import {
19
9
  } from './section-args-validator';
20
10
 
21
11
  /**
22
- * Constructs a headless Tiptap Editor with the same extension set as
23
- * `useExternalRichTextEditor` in the maker. We import the Tiptap extension
24
- * files directly (they are leaf modules) instead of going through the maker
25
- * composable, which would pull in the AI executor → section-builders →
26
- * section-rules graph and re-enter this file before its exports are bound.
12
+ * The ProseMirror schema built from the SAME extension set the editor and
13
+ * renderer use (`getRteExtensions`), so validation matches real editor output.
14
+ * `getSchema` is DOM-less (no Editor instance) and the schema is pure, so it is
15
+ * built once and cached.
16
+ *
17
+ * We import `getRteExtensions` (a leaf module — @tiptap + prosemirror only), not
18
+ * the maker composable (`useExternalRichTextEditor`), which would pull in the
19
+ * AI executor → section-builders → section-rules graph and re-enter this file
20
+ * before its exports are bound.
27
21
  */
28
- const buildHeadlessEditor = (): Editor =>
29
- new Editor({
30
- editable: false,
31
- extensions: [
32
- StarterKit,
33
- Underline,
34
- TextStyle.configure({ mergeNestedSpanStyles: true }),
35
- TextAlign.configure({ types: ['heading', 'paragraph'] }),
36
- FontFamily.configure({ types: ['textStyle'] }),
37
- FontSize,
38
- LetterSpacing,
39
- LineHeight,
40
- ColorExtension.configure({ types: ['textStyle'] }),
41
- Highlight.configure({ multicolor: true }),
42
- Link.configure({
43
- openOnClick: false,
44
- defaultProtocol: 'https',
45
- autolink: true,
46
- }),
47
- AutoLink,
48
- ],
49
- });
22
+ let cachedSchema: Schema | undefined;
23
+
24
+ const getRteSchema = (): Schema =>
25
+ (cachedSchema ??= getSchema(getRteExtensions({ includeLinkRel: true })));
50
26
 
51
27
  /**
52
- * Validates a Tiptap doc against the headless editor's real ProseMirror
53
- * schema (`schema.nodeFromJSON`). Catches malformed AI input — stray
54
- * non-node children in `content`, unknown node types, malformed attrs —
55
- * before the builder runs and the renderer's silent `setContent` drops
56
- * the doc to `<p></p>` (BP-5669).
28
+ * Validates a Tiptap doc against the real ProseMirror schema
29
+ * (`schema.nodeFromJSON`). Catches malformed AI input — stray non-node children
30
+ * in `content`, unknown node types, malformed attrs — before the builder runs
31
+ * and the renderer's silent `setContent` drops the doc to `<p></p>` (BP-5669).
57
32
  *
58
- * Returns the field-prefixed error string so the AI's tool-call retry
59
- * loop reads it via `BuilderResult.errors` → `tool_result.errorMessage`
60
- * and re-emits a corrected payload.
33
+ * Returns the field-prefixed error string so the AI's tool-call retry loop reads
34
+ * it via `BuilderResult.errors` → `tool_result.errorMessage` and re-emits a
35
+ * corrected payload.
61
36
  */
62
37
  export const rteSchemaCheck =
63
38
  (field: string): SectionCheck =>
@@ -66,18 +41,14 @@ export const rteSchemaCheck =
66
41
 
67
42
  if (!isTiptapDocument(value)) return null;
68
43
 
69
- const editor = buildHeadlessEditor();
70
-
71
44
  try {
72
- editor.schema.nodeFromJSON(value);
45
+ getRteSchema().nodeFromJSON(value);
73
46
 
74
47
  return null;
75
48
  } catch (err) {
76
49
  const detail = err instanceof Error ? err.message : String(err);
77
50
 
78
51
  return `"${field}" is not valid Tiptap JSON: ${detail}`;
79
- } finally {
80
- editor.destroy();
81
52
  }
82
53
  };
83
54
 
@@ -175,7 +175,23 @@ export default defineComponent({
175
175
  },
176
176
  },
177
177
  watch: {
178
- value(newVal: string | undefined) {
178
+ value(newVal: string | undefined, oldVal: string | undefined) {
179
+ // BP-5787: RTE values are Tiptap JSON objects. The parent (TextItemsField)
180
+ // re-clones its items array (cloneDeep) on ANY change — including a sibling
181
+ // field's style sync — yielding a NEW object reference with identical text
182
+ // content. For objects Vue fires this watcher on reference change, so that
183
+ // spurious re-clone would overwrite `_value` and clobber an in-flight edit
184
+ // (a mark applied but not yet debounced to the store), silently dropping
185
+ // the user's formatting. Strings never hit this (same content = same
186
+ // reference = no fire), so mirror that: ignore re-clones whose content is
187
+ // unchanged from the previous prop value.
188
+ if (
189
+ this.showRichTextEditor &&
190
+ JSON.stringify(newVal) === JSON.stringify(oldVal)
191
+ ) {
192
+ return;
193
+ }
194
+
179
195
  this._value = newVal;
180
196
 
181
197
  if (this.showRichTextEditor) {
@@ -21,6 +21,9 @@ import {
21
21
  replaceTextInRTE,
22
22
  RTE_ALLOWED_SECTION_TYPES,
23
23
  isRTEManagedStyleField,
24
+ isTiptapDocument,
25
+ renderRteToHtml,
26
+ toRteJson,
24
27
  } from '@brand-page-maker/utils';
25
28
  import { isDefaultContentText } from '@shared/content-settings/utils';
26
29
  import { useUiLogoSettingsStore } from '../../../logo-settings/store';
@@ -327,10 +330,20 @@ export default defineComponent({
327
330
  this.isToggled = isToggled;
328
331
  },
329
332
  onUseGeneratedText(generatedText: string) {
330
- let formattedText = generatedText;
333
+ let formattedText: unknown = generatedText;
331
334
 
332
- if (this.showRichTextEditor && typeof this.value === 'string') {
333
- formattedText = replaceTextInRTE(this.value, generatedText);
335
+ // BP-5787: preserve the field's formatting when dropping in AI text.
336
+ // replaceTextInRTE is HTML-based, so JSON is rendered/parsed around it
337
+ // (cold AI path, shared editor); string fields use it directly.
338
+ if (this.showRichTextEditor) {
339
+ if (isTiptapDocument(this.value)) {
340
+ formattedText =
341
+ toRteJson(
342
+ replaceTextInRTE(renderRteToHtml(this.value), generatedText),
343
+ ) ?? generatedText;
344
+ } else if (typeof this.value === 'string') {
345
+ formattedText = replaceTextInRTE(this.value, generatedText);
346
+ }
334
347
  }
335
348
 
336
349
  this.contentSettingsStore.updateItemValue({
@@ -4,6 +4,7 @@ import type {
4
4
  ColorReplacement,
5
5
  Content,
6
6
  LogoTheme,
7
+ RichTextValue,
7
8
  TestimonialItems,
8
9
  } from '@brand-page';
9
10
  import {
@@ -17,7 +18,7 @@ import debounce from 'lodash.debounce';
17
18
  import type { UpdateItemValuePayload } from '../../models';
18
19
  import cloneDeep from 'lodash.clonedeep';
19
20
  import { useSnapshotStore } from '@brand-page-maker/stores';
20
- import { getPlainTextFromRTE } from '@brand-page-maker/utils';
21
+ import { getPlainTextFromRTE, getRteTextLength } from '@brand-page-maker/utils';
21
22
  import { isDefaultContentText } from '../../utils';
22
23
  import { decodeHtmlEntities } from '@brand-page/utils';
23
24
  import { ESCAPED_NEWLINE_CHARACTER } from '../../../../constants/ui';
@@ -91,8 +92,10 @@ export default defineComponent({
91
92
  created() {
92
93
  this.options = cloneDeep(this.value);
93
94
 
95
+ // BP-5787: testimonials may be Tiptap JSON (migrated legacy content), so
96
+ // read visible text via getPlainTextFromRTE rather than `.trim()` on a doc.
94
97
  this.isEmpty = this.options.map(
95
- (option) => option.testimonials.trim().length === 0,
98
+ (option) => getPlainTextFromRTE(option.testimonials).length === 0,
96
99
  );
97
100
 
98
101
  if (!this.changeDebounce) {
@@ -206,11 +209,17 @@ export default defineComponent({
206
209
  this.options[index][payload.name] = payload.value;
207
210
  this.emitChange();
208
211
  },
209
- getItemTextPreview(text: string): string {
212
+ getItemTextPreview(text: RichTextValue): string {
213
+ // BP-5787: a JSON doc has no `.replace` — take its visible text first.
214
+ const value = typeof text === 'string' ? text : getPlainTextFromRTE(text);
215
+
210
216
  return decodeHtmlEntities(
211
- text.replace(new RegExp(ESCAPED_NEWLINE_CHARACTER, 'g'), ' '),
217
+ value.replace(new RegExp(ESCAPED_NEWLINE_CHARACTER, 'g'), ' '),
212
218
  );
213
219
  },
220
+ rteTextLength(value: RichTextValue): number {
221
+ return getRteTextLength(value);
222
+ },
214
223
  getPlainTextValue(index: number): string {
215
224
  const text = this.options[index]?.testimonials || '';
216
225
  const plainText = getPlainTextFromRTE(text);
@@ -26,7 +26,8 @@
26
26
  :error="
27
27
  isEmpty[option.index] ||
28
28
  (options[option.index].testimonials &&
29
- options[option.index].testimonials.length >= maxTestimonialLength)
29
+ rteTextLength(options[option.index].testimonials) >=
30
+ maxTestimonialLength)
30
31
  "
31
32
  :resize="false"
32
33
  class="tw-w-full"
@@ -42,7 +43,8 @@
42
43
  <p
43
44
  v-else-if="
44
45
  options[option.index].testimonials &&
45
- options[option.index].testimonials.length >= maxTestimonialLength
46
+ rteTextLength(options[option.index].testimonials) >=
47
+ maxTestimonialLength
46
48
  "
47
49
  class="tw-text-error-500 tw-text-sm"
48
50
  >
@@ -127,7 +129,7 @@
127
129
  v-if="
128
130
  options[option.index] &&
129
131
  options[option.index].name &&
130
- options[option.index].name.length >= maxTitleLength
132
+ rteTextLength(options[option.index].name) >= maxTitleLength
131
133
  "
132
134
  class="tw-text-error-500 tw-text-sm"
133
135
  >
@@ -158,7 +160,7 @@
158
160
  v-if="
159
161
  options[option.index] &&
160
162
  options[option.index].position &&
161
- options[option.index].position.length >= maxTitleLength
163
+ rteTextLength(options[option.index].position) >= maxTitleLength
162
164
  "
163
165
  class="tw-text-error-500 tw-text-sm"
164
166
  >
@@ -97,10 +97,10 @@
97
97
  :attrs="{
98
98
  'data-test-tag': 'image-title-text-input',
99
99
  }"
100
- :error="options[option.index].title.length >= titleLength"
100
+ :error="rteTextLength(options[option.index].title) >= titleLength"
101
101
  />
102
102
  <p
103
- v-if="options[option.index].title.length >= titleLength"
103
+ v-if="rteTextLength(options[option.index].title) >= titleLength"
104
104
  class="tw-text-error-500 tw-text-sm tw-mt-2"
105
105
  >
106
106
  {{ maxLengthError }}
@@ -134,7 +134,9 @@
134
134
  :attrs="{
135
135
  'data-test-tag': 'image-description-text-input',
136
136
  }"
137
- :error="options[option.index].paragraph.length >= descriptionLength"
137
+ :error="
138
+ rteTextLength(options[option.index].paragraph) >= descriptionLength
139
+ "
138
140
  @update:model-value="onTextAreaTextChange($event, option.index)"
139
141
  />
140
142
  <PersonalisationOverrideField
@@ -311,6 +313,7 @@ import debounce from 'lodash.debounce';
311
313
  import type { UpdateItemValuePayload } from '../../models';
312
314
  import cloneDeep from 'lodash.clonedeep';
313
315
  import { isURL } from '../../../../utils/validator.util';
316
+ import { getRteTextLength as rteTextLength } from '@brand-page-maker/utils';
314
317
 
315
318
  const DEBOUNCE_IN_MS = 250;
316
319