@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
@@ -8,18 +8,7 @@ import {
8
8
  watch,
9
9
  } from 'vue';
10
10
  import { Editor } from '@tiptap/vue-3';
11
- import StarterKit from '@tiptap/starter-kit';
12
- import Underline from '@tiptap/extension-underline';
13
- import TextStyle from '@tiptap/extension-text-style';
14
- import TextAlign from '@tiptap/extension-text-align';
15
- import FontFamily from '@tiptap/extension-font-family';
16
- import ColorExtension from '@tiptap/extension-color';
17
- import { FontSize } from './extensions/FontSize';
18
- import { LetterSpacing } from './extensions/LetterSpacing';
19
- import { LineHeight } from './extensions/LineHeight';
20
- import Highlight from '@tiptap/extension-highlight';
21
- import Placeholder from '@tiptap/extension-placeholder';
22
- import Link from '@tiptap/extension-link';
11
+ import { getRteExtensions } from './getRteExtensions';
23
12
  import type { StyleTextFontFamilyOption } from '@shared/style-settings/models';
24
13
  import { createFormattingActions } from './formatting';
25
14
  import { useTextAlign } from './textAlign';
@@ -31,7 +20,7 @@ import {
31
20
  emToPx,
32
21
  isStyleChanged,
33
22
  pxToEm,
34
- sanitiseClientSideRTE,
23
+ sanitizeRteJson,
35
24
  skipStyleDetection,
36
25
  } from '@brand-page-maker/utils';
37
26
  import type { HyperlinkType, TextStyles } from '@brand-page';
@@ -49,9 +38,7 @@ import {
49
38
  import { useUiContentSettingsStore } from '@shared/content-settings/store';
50
39
  import { useHistoryStore } from '@brand-page-maker/stores/history/history-store';
51
40
  import { AllSelection } from 'prosemirror-state';
52
- import { MaxLength } from './extensions/MaxLength';
53
- import { AutoLink } from './extensions/AutoLink';
54
- import { isHrefPageId, useHyperlink } from './hyperlink';
41
+ import { useHyperlink } from './hyperlink';
55
42
  import { useBubbleMenu } from './bubbleMenu';
56
43
  import {
57
44
  hasLogoColorVar,
@@ -92,7 +79,7 @@ interface UseRichTextEditorReturn {
92
79
  hyperlinkValue: ComputedRef<{ id: string; name: string; value: string }>;
93
80
  setHyperlinkUrlValue: (url: string) => void;
94
81
  shouldShow: ({ editor, state }: { editor: any; state: any }) => boolean;
95
- tippyOptions: Record<string, any>;
82
+ bubbleMenuOptions: Record<string, any>;
96
83
  handleAdminColorChange: (
97
84
  color: {
98
85
  id: string;
@@ -129,44 +116,7 @@ export const useExternalRichTextEditor = (): {
129
116
  } => {
130
117
  const externalEditor = new Editor({
131
118
  editable: false,
132
- extensions: [
133
- StarterKit,
134
- Underline,
135
- TextStyle.configure({ mergeNestedSpanStyles: true }),
136
- TextAlign.configure({
137
- types: ['heading', 'paragraph'],
138
- }),
139
- FontFamily.configure({
140
- types: ['textStyle'],
141
- }),
142
- FontSize,
143
- LetterSpacing,
144
- LineHeight,
145
- ColorExtension.configure({
146
- types: ['textStyle'],
147
- }),
148
- Highlight.configure({
149
- multicolor: true,
150
- }),
151
- Link.configure({
152
- openOnClick: false,
153
- defaultProtocol: 'https',
154
- autolink: true,
155
- HTMLAttributes: (attrs) => {
156
- const href = attrs.href;
157
- const isExternal =
158
- href && (href.startsWith('http://') || href.startsWith('https://'));
159
-
160
- return {
161
- rel: 'noopener noreferrer',
162
- target: isExternal ? '_blank' : null,
163
- };
164
- },
165
- isAllowedUri: (url, ctx) =>
166
- ctx.defaultValidate(url) || isHrefPageId(url),
167
- }),
168
- AutoLink,
169
- ],
119
+ extensions: getRteExtensions({ includeLinkRel: true }),
170
120
  });
171
121
 
172
122
  return {
@@ -214,54 +164,13 @@ export const useRichTextEditor = (
214
164
  const editor = new Editor({
215
165
  content: model.value ?? '',
216
166
  editable: !props.readOnly,
217
- extensions: [
218
- StarterKit.configure({
219
- bulletList: false,
220
- orderedList: false,
221
- listItem: false,
222
- horizontalRule: false,
223
- codeBlock: false,
224
- code: false,
225
- }),
226
- Underline,
227
- TextStyle.configure({ mergeNestedSpanStyles: true }),
228
- TextAlign.configure({
229
- types: ['heading', 'paragraph'],
230
- }),
231
- FontFamily.configure({
232
- types: ['textStyle'],
233
- }),
234
- FontSize,
235
- LetterSpacing,
236
- LineHeight,
237
- ColorExtension.configure({
238
- types: ['textStyle'],
239
- }),
240
- Highlight.configure({
241
- multicolor: true,
242
- }),
243
- Placeholder.configure({
167
+ extensions: getRteExtensions({
168
+ restrictStarterKit: true,
169
+ editing: {
244
170
  placeholder: props.placeholder,
245
- }),
246
- MaxLength.configure({ maxLength: props.maxLength }),
247
- Link.configure({
248
- openOnClick: false,
249
- defaultProtocol: 'https',
250
- autolink: true,
251
- HTMLAttributes: (attrs) => {
252
- const href = attrs.href;
253
- const isExternal =
254
- href && (href.startsWith('http://') || href.startsWith('https://'));
255
-
256
- return {
257
- target: isExternal ? '_blank' : null,
258
- };
259
- },
260
- isAllowedUri: (url, ctx) =>
261
- ctx.defaultValidate(url) || isHrefPageId(url),
262
- }),
263
- AutoLink,
264
- ],
171
+ maxLength: props.maxLength,
172
+ },
173
+ }),
265
174
  editorProps: {
266
175
  handleDOMEvents: {
267
176
  drop(view, event: DragEvent) {
@@ -440,7 +349,7 @@ export const useRichTextEditor = (
440
349
  applyHyperlinkToEditor,
441
350
  } = useHyperlink(editor);
442
351
 
443
- const { shouldShow, tippyOptions } = useBubbleMenu(
352
+ const { shouldShow, bubbleMenuOptions } = useBubbleMenu(
444
353
  editor,
445
354
  isHyperlinkPopupOpen,
446
355
  );
@@ -472,8 +381,14 @@ export const useRichTextEditor = (
472
381
 
473
382
  const isExternalMutation = updateRichTextEditor.value || appliedSinceLast;
474
383
 
475
- if (isExternalMutation && editor.getHTML() !== newValue) {
476
- editor.commands.setContent(newValue, false);
384
+ // Compare in the stored (JSON) format so a JSON model value isn't always
385
+ // seen as "changed" against the editor's HTML (which would re-run
386
+ // setContent and reset the user's just-applied styles).
387
+ const hasChanged =
388
+ JSON.stringify(editor.getJSON()) !== JSON.stringify(newValue);
389
+
390
+ if (isExternalMutation && hasChanged) {
391
+ editor.commands.setContent(newValue, { emitUpdate: false });
477
392
  updateRichTextEditor.value = false;
478
393
  //text style type is not switching when reordering
479
394
  textStyleTypeOptions.forEach((item) => {
@@ -503,12 +418,13 @@ export const useRichTextEditor = (
503
418
  }
504
419
  });
505
420
 
506
- // Update model when editor content changes
421
+ // Update model when editor content changes. RTE content is stored as Tiptap
422
+ // JSON (BP-5787). getJSON() is already in hand (the editor holds the doc) and
423
+ // is sanitised natively — no getHTML(), no DOMPurify, no extra editor — so
424
+ // the per-keystroke path allocates nothing heavy.
507
425
  editor.on('update', () => {
508
- const sanitizedData = sanitiseClientSideRTE(editor.getHTML());
509
-
510
426
  // An update fired while the editor is unfocused is TipTap rewriting the
511
- // model back into its canonical HTML during mount rehydration or a
427
+ // model back into its canonical form during mount rehydration or a
512
428
  // programmatic setContent — not a user edit. Tag the emit so the write
513
429
  // folds into the current undo head instead of pushing a phantom entry
514
430
  // (BP-5810). The flag travels WITH the value through the debounced
@@ -522,7 +438,7 @@ export const useRichTextEditor = (
522
438
  // the RTE only mounts inside an already-open settings drawer and the user
523
439
  // focuses it before formatting, so this doesn't occur — revisit if a
524
440
  // consumer ever drives toolbar actions on an unfocused editor.
525
- emit('update:modelValue', sanitizedData.sanitizedHtml, {
441
+ emit('update:modelValue', sanitizeRteJson(editor.getJSON()), {
526
442
  isHydrationWrite: !editor.isFocused,
527
443
  });
528
444
  });
@@ -1049,7 +965,7 @@ export const useRichTextEditor = (
1049
965
  hyperlinkValue,
1050
966
  setHyperlinkUrlValue,
1051
967
  shouldShow,
1052
- tippyOptions,
968
+ bubbleMenuOptions,
1053
969
  handleAdminColorChange,
1054
970
  };
1055
971
  };
@@ -14,6 +14,27 @@ export const useAiWebsiteBuilderThreadStore = defineStore(
14
14
  const promptText = ref<string>('');
15
15
  const stopGeneration = ref<(() => void) | null>(null);
16
16
 
17
+ // Bumped on every reset so consumers (useAiWebsiteBuilderChat) can react
18
+ // without watching `messages`, which would loop with its own deep watch.
19
+ const resetToken = ref<number>(0);
20
+
21
+ const reset = () => {
22
+ if (isProcessing.value || isBuildInProgress.value) {
23
+ stopGeneration.value?.();
24
+ }
25
+
26
+ threadId.value = null;
27
+ runId.value = null;
28
+ messages.value = [];
29
+ isProcessing.value = false;
30
+ buildId.value = null;
31
+ isBuildInProgress.value = false;
32
+ buildSectionsCompleted.value = 0;
33
+ promptText.value = '';
34
+ stopGeneration.value = null;
35
+ resetToken.value += 1;
36
+ };
37
+
17
38
  return {
18
39
  threadId,
19
40
  runId,
@@ -24,6 +45,8 @@ export const useAiWebsiteBuilderThreadStore = defineStore(
24
45
  buildSectionsCompleted,
25
46
  promptText,
26
47
  stopGeneration,
48
+ resetToken,
49
+ reset,
27
50
  };
28
51
  },
29
52
  );
@@ -30,6 +30,8 @@ import type {
30
30
  TemplateCategory,
31
31
  } from '@shared/template-settings/models';
32
32
  import { useBrandPageMakerUserStore } from '../user';
33
+ import { useAiWebsiteBuilderThreadStore } from '../ai-thread/ai-website-builder-thread-store';
34
+ import { useAiCheckpointStore } from '../ai-checkpoint/ai-checkpoint-store';
33
35
  import { defineStore } from 'pinia';
34
36
  import { toRaw } from 'vue';
35
37
  import { useUiStore } from '../ui';
@@ -620,6 +622,12 @@ export const useBrandPageMakerStore = defineStore('brandPageMaker', {
620
622
 
621
623
  await this.recalculateLogoSizes(true);
622
624
 
625
+ // A template switch invalidates the AI conversation — the server-side
626
+ // thread's history still describes the previous template (BP-5896).
627
+ // reset() also stops any in-flight generation before clearing.
628
+ useAiWebsiteBuilderThreadStore().reset();
629
+ useAiCheckpointStore().clearAll();
630
+
623
631
  this.setIsModifiedAfterTemplateApplied({
624
632
  isModified: false,
625
633
  });
@@ -0,0 +1,202 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`BP-5787 RTE render parity (golden master) > T1 — editor renders the complex fixture to the frozen HTML baseline > editor-normalized-html 1`] = `"<h2 style="text-align: center;"><span style="color: var(--logo-text-color);">Themed heading</span></h2><p><strong>bold</strong> <em>italic</em> <u>underline</u> <s>strike</s> <span style="font-family: Montserrat; font-size: 1.5em; letter-spacing: 0.05em; line-height: 1.4;">styled span</span> <mark data-color="#ffe08a" style="background-color: rgb(255, 224, 138); color: inherit;">highlight</mark> <span style="color: rgb(255, 0, 0);">literal hex</span> <a href="/about" target="_self">internal link</a> <a href="https://example.com" target="_blank" rel="noopener noreferrer nofollow">external link</a></p><p style="text-align: right;">second paragraph, right-aligned</p>"`;
4
+
5
+ exports[`BP-5787 RTE render parity (golden master) > T1 — render-from-JSON (DOM-less renderer) produces equivalent HTML > serializer-html 1`] = `"<h2 style="text-align: center"><span style="color: var(--logo-text-color)">Themed heading</span></h2><p><strong>bold</strong> <em>italic</em> <u>underline</u> <s>strike</s> <span style="font-family: Montserrat; font-size: 1.5em; letter-spacing: 0.05em; line-height: 1.4">styled span</span> <mark data-color="#ffe08a" style="background-color: #ffe08a; color: inherit">highlight</mark> <span style="color: #FF0000">literal hex</span> <a href="/about" target="_self">internal link</a> <a href="https://example.com" target="_blank" rel="noopener noreferrer nofollow">external link</a></p><p style="text-align: right">second paragraph, right-aligned</p>"`;
6
+
7
+ exports[`BP-5787 RTE render parity (golden master) > T6 — freezes the JSON shape that becomes the stored form > editor-json 1`] = `
8
+ {
9
+ "content": [
10
+ {
11
+ "attrs": {
12
+ "level": 2,
13
+ "textAlign": "center",
14
+ },
15
+ "content": [
16
+ {
17
+ "marks": [
18
+ {
19
+ "attrs": {
20
+ "color": "var(--logo-text-color)",
21
+ "fontFamily": "",
22
+ "fontSize": "",
23
+ "letterSpacing": "",
24
+ "lineHeight": "",
25
+ },
26
+ "type": "textStyle",
27
+ },
28
+ ],
29
+ "text": "Themed heading",
30
+ "type": "text",
31
+ },
32
+ ],
33
+ "type": "heading",
34
+ },
35
+ {
36
+ "attrs": {
37
+ "textAlign": null,
38
+ },
39
+ "content": [
40
+ {
41
+ "marks": [
42
+ {
43
+ "type": "bold",
44
+ },
45
+ ],
46
+ "text": "bold",
47
+ "type": "text",
48
+ },
49
+ {
50
+ "text": " ",
51
+ "type": "text",
52
+ },
53
+ {
54
+ "marks": [
55
+ {
56
+ "type": "italic",
57
+ },
58
+ ],
59
+ "text": "italic",
60
+ "type": "text",
61
+ },
62
+ {
63
+ "text": " ",
64
+ "type": "text",
65
+ },
66
+ {
67
+ "marks": [
68
+ {
69
+ "type": "underline",
70
+ },
71
+ ],
72
+ "text": "underline",
73
+ "type": "text",
74
+ },
75
+ {
76
+ "text": " ",
77
+ "type": "text",
78
+ },
79
+ {
80
+ "marks": [
81
+ {
82
+ "type": "strike",
83
+ },
84
+ ],
85
+ "text": "strike",
86
+ "type": "text",
87
+ },
88
+ {
89
+ "text": " ",
90
+ "type": "text",
91
+ },
92
+ {
93
+ "marks": [
94
+ {
95
+ "attrs": {
96
+ "color": "",
97
+ "fontFamily": "Montserrat",
98
+ "fontSize": "1.5em",
99
+ "letterSpacing": "0.05em",
100
+ "lineHeight": "1.4",
101
+ },
102
+ "type": "textStyle",
103
+ },
104
+ ],
105
+ "text": "styled span",
106
+ "type": "text",
107
+ },
108
+ {
109
+ "text": " ",
110
+ "type": "text",
111
+ },
112
+ {
113
+ "marks": [
114
+ {
115
+ "attrs": {
116
+ "color": "#ffe08a",
117
+ },
118
+ "type": "highlight",
119
+ },
120
+ ],
121
+ "text": "highlight",
122
+ "type": "text",
123
+ },
124
+ {
125
+ "text": " ",
126
+ "type": "text",
127
+ },
128
+ {
129
+ "marks": [
130
+ {
131
+ "attrs": {
132
+ "color": "#FF0000",
133
+ "fontFamily": "",
134
+ "fontSize": "",
135
+ "letterSpacing": "",
136
+ "lineHeight": "",
137
+ },
138
+ "type": "textStyle",
139
+ },
140
+ ],
141
+ "text": "literal hex",
142
+ "type": "text",
143
+ },
144
+ {
145
+ "text": " ",
146
+ "type": "text",
147
+ },
148
+ {
149
+ "marks": [
150
+ {
151
+ "attrs": {
152
+ "class": null,
153
+ "href": "/about",
154
+ "rel": null,
155
+ "target": "_self",
156
+ "title": null,
157
+ },
158
+ "type": "link",
159
+ },
160
+ ],
161
+ "text": "internal link",
162
+ "type": "text",
163
+ },
164
+ {
165
+ "text": " ",
166
+ "type": "text",
167
+ },
168
+ {
169
+ "marks": [
170
+ {
171
+ "attrs": {
172
+ "class": null,
173
+ "href": "https://example.com",
174
+ "rel": "noopener noreferrer nofollow",
175
+ "target": "_blank",
176
+ "title": null,
177
+ },
178
+ "type": "link",
179
+ },
180
+ ],
181
+ "text": "external link",
182
+ "type": "text",
183
+ },
184
+ ],
185
+ "type": "paragraph",
186
+ },
187
+ {
188
+ "attrs": {
189
+ "textAlign": "right",
190
+ },
191
+ "content": [
192
+ {
193
+ "text": "second paragraph, right-aligned",
194
+ "type": "text",
195
+ },
196
+ ],
197
+ "type": "paragraph",
198
+ },
199
+ ],
200
+ "type": "doc",
201
+ }
202
+ `;
@@ -7,6 +7,8 @@ export * from './url.util';
7
7
  export * from './slug.util';
8
8
  export * from './text.util';
9
9
  export * from './rich-text-editor.util';
10
+ export * from './rte-render.util';
11
+ export * from './rte-sanitize.util';
10
12
 
11
13
  export const isColorMatched = (color: string | null | undefined): boolean => {
12
14
  if (!color) return false;
@@ -8,6 +8,7 @@ import {
8
8
  getUpdatedHTMLFromEditor,
9
9
  sanitiseClientSideRTE,
10
10
  getRTEColor,
11
+ getRteTextLength,
11
12
  } from './rich-text-editor.util';
12
13
  import type { TextStyleDeclaration } from '@brand-page';
13
14
 
@@ -75,8 +76,40 @@ describe('hasHTML', () => {
75
76
  });
76
77
  });
77
78
 
79
+ describe('getRteTextLength', () => {
80
+ it('returns the raw length for a plain string (unchanged legacy behaviour)', () => {
81
+ expect(getRteTextLength('hello')).toBe(5);
82
+ expect(getRteTextLength('<p>hello</p>')).toBe('<p>hello</p>'.length);
83
+ expect(getRteTextLength('')).toBe(0);
84
+ });
85
+
86
+ it('returns visible-text length for a Tiptap JSON doc (not undefined/throw)', () => {
87
+ const doc = {
88
+ type: 'doc',
89
+ content: [
90
+ {
91
+ type: 'paragraph',
92
+ content: [
93
+ { type: 'text', text: 'hello ' },
94
+ { type: 'text', marks: [{ type: 'bold' }], text: 'world' },
95
+ ],
96
+ },
97
+ ],
98
+ };
99
+
100
+ // "hello world" — measured from the text nodes, not the JSON wrapper.
101
+ expect(getRteTextLength(doc)).toBe(11);
102
+ });
103
+
104
+ it('returns 0 for null/undefined and an empty doc', () => {
105
+ expect(getRteTextLength(null)).toBe(0);
106
+ expect(getRteTextLength(undefined)).toBe(0);
107
+ expect(getRteTextLength({ type: 'doc', content: [] })).toBe(0);
108
+ });
109
+ });
110
+
78
111
  describe('getUpdatedHTMLFromEditor', () => {
79
- it('applies style commands and returns updated HTML', () => {
112
+ it('applies style commands and returns updated JSON', () => {
80
113
  // Mock editor
81
114
  const runMock = vi.fn();
82
115
  const chain = {
@@ -119,7 +152,7 @@ describe('getUpdatedHTMLFromEditor', () => {
119
152
  },
120
153
  },
121
154
  chain: () => chain as any,
122
- getHTML: () => '<div>styled</div>',
155
+ getJSON: () => ({ type: 'doc', content: [{ type: 'paragraph' }] }),
123
156
  } as unknown as Editor;
124
157
 
125
158
  const newStyle = {
@@ -137,7 +170,8 @@ describe('getUpdatedHTMLFromEditor', () => {
137
170
 
138
171
  expect(externalEditor.commands.setContent).toHaveBeenCalledWith('foo');
139
172
  expect(runMock).toHaveBeenCalled();
140
- expect(result).toBe('<div>styled</div>');
173
+ // BP-5787: returns the JSON form (consistent with JSON storage).
174
+ expect(result).toEqual({ type: 'doc', content: [{ type: 'paragraph' }] });
141
175
  });
142
176
  });
143
177
 
@@ -1,4 +1,4 @@
1
- import type { ChainedCommands, Editor } from '@tiptap/vue-3';
1
+ import type { ChainedCommands, Editor, JSONContent } from '@tiptap/vue-3';
2
2
  import type { EditorState, Transaction, Selection } from 'prosemirror-state';
3
3
  import {
4
4
  STYLE_THEME_KEYS,
@@ -19,6 +19,7 @@ import {
19
19
  type RequestPaymentContent,
20
20
  type SupportMeContent,
21
21
  type ShopContent,
22
+ type RichTextValue,
22
23
  } from '@brand-page';
23
24
  import type { PageSettingsState } from '@shared/page-settings/store';
24
25
  import type { StyleSettingsState } from '@shared/style-settings/store';
@@ -243,8 +244,38 @@ export const skipStyleDetection = (chain: ChainedCommands, marks?: Mark[]) =>
243
244
  return true;
244
245
  });
245
246
 
246
- export const hasRTEContent = (content: string): boolean => {
247
- if (!content || content.length === 0) return false;
247
+ /**
248
+ * BP-5787 — canonical discriminator for the JSON RTE model: a ProseMirror
249
+ * document is an object with `type: 'doc'`.
250
+ */
251
+ export const isTiptapDocument = (value: unknown): value is JSONContent =>
252
+ typeof value === 'object' &&
253
+ value !== null &&
254
+ (value as JSONContent).type === 'doc';
255
+
256
+ /**
257
+ * Extract visible plain text from a Tiptap JSON document (concatenated text
258
+ * nodes). Dependency-free — no editor instantiation.
259
+ */
260
+ export const getRteJsonPlainText = (doc: JSONContent): string => {
261
+ const parts: string[] = [];
262
+
263
+ const walk = (node: JSONContent): void => {
264
+ if (typeof node.text === 'string') parts.push(node.text);
265
+
266
+ node.content?.forEach(walk);
267
+ };
268
+
269
+ walk(doc);
270
+
271
+ return parts.join(' ').replace(/\s+/g, ' ').trim();
272
+ };
273
+
274
+ // Accepts a legacy inline-HTML string or a Tiptap JSON document (BP-5787).
275
+ export const hasRTEContent = (content: unknown): boolean => {
276
+ if (isTiptapDocument(content)) return true;
277
+
278
+ if (typeof content !== 'string' || content.length === 0) return false;
248
279
 
249
280
  const trimmed = content.trim();
250
281
  const tagWrapRegex = /^<([A-Za-z][A-Za-z0-9-]*)\b[^>]*>[\s\S]*<\/\1>$/;
@@ -252,7 +283,11 @@ export const hasRTEContent = (content: string): boolean => {
252
283
  return tagWrapRegex.test(trimmed);
253
284
  };
254
285
 
255
- export const getPlainTextFromRTE = (content: string): string => {
286
+ export const getPlainTextFromRTE = (content: unknown): string => {
287
+ if (isTiptapDocument(content)) {
288
+ return getRteJsonPlainText(content);
289
+ }
290
+
256
291
  if (typeof content !== 'string') {
257
292
  return '';
258
293
  }
@@ -274,6 +309,22 @@ export const getPlainTextFromRTE = (content: string): string => {
274
309
  return plainText.trim();
275
310
  };
276
311
 
312
+ /**
313
+ * BP-5787 — visible-text length of an RTE value, for the maker item-field
314
+ * max-length / emptiness checks. A plain string keeps its raw `.length`, so the
315
+ * pre-JSON behaviour is byte-for-byte identical; a Tiptap JSON doc measures its
316
+ * concatenated visible text. Without this, `doc.length` is `undefined` (silently
317
+ * disabling the check) and `doc.trim()` throws — the class of bug that survived
318
+ * the `RichTextValue` widening because Vue Options-API `this`/item access is `any`.
319
+ */
320
+ export const getRteTextLength = (
321
+ value: RichTextValue | null | undefined,
322
+ ): number => {
323
+ if (typeof value === 'string') return value.length;
324
+
325
+ return value ? getRteJsonPlainText(value).length : 0;
326
+ };
327
+
277
328
  export const replaceTextInRTE = (
278
329
  existingHtml: string,
279
330
  newText: string,
@@ -721,16 +772,20 @@ export const switchAllContentToRTE = (
721
772
 
722
773
  export const getUpdatedHTMLFromEditor = (
723
774
  externalEditor: Editor,
724
- contentText: string,
775
+ contentText: RichTextValue,
725
776
  newContentStyle: TextStyleDeclaration,
726
777
  ) => {
727
778
  if (!contentText) {
728
779
  return contentText;
729
780
  }
730
781
 
731
- const content = contentText.includes(ESCAPED_NEWLINE_CHARACTER)
732
- ? getRenderText(contentText)
733
- : contentText;
782
+ // BP-5787: accept a string (legacy HTML) or a Tiptap JSON doc. setContent
783
+ // handles both; only the newline-escaping shim applies to strings.
784
+ const content =
785
+ typeof contentText === 'string' &&
786
+ contentText.includes(ESCAPED_NEWLINE_CHARACTER)
787
+ ? getRenderText(contentText)
788
+ : contentText;
734
789
 
735
790
  externalEditor.commands.setContent(content);
736
791
 
@@ -788,9 +843,9 @@ export const getUpdatedHTMLFromEditor = (
788
843
  chain = newContentStyle?.fontStyle ? chain.setItalic() : chain.unsetItalic();
789
844
 
790
845
  chain.run();
791
- const html = externalEditor.getHTML();
792
846
 
793
- return html;
847
+ // BP-5787: return the JSON form, consistent with how RTE is now stored.
848
+ return externalEditor.getJSON();
794
849
  };
795
850
 
796
851
  // Helper to build stored marks from a style declaration
@@ -914,7 +969,7 @@ export const sanitiseClientSideRTE = (html: string): SanitizationResult => {
914
969
  /**
915
970
  * Validates if an href value is safe and returns a sanitized version
916
971
  */
917
- const validateAndSanitizeHref = (href: string): string | null => {
972
+ export const validateAndSanitizeHref = (href: string): string | null => {
918
973
  if (!href || typeof href !== 'string') return null;
919
974
 
920
975
  // Trim whitespace and decode entities