@designcrowd/library.brand-page 6.0.26-json-prereq-2 → 6.0.27-websitev2-dev-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 (186) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-CKBqjDH3.js → CreatedOnBrandCrowd-de-de-BpFKOKIo.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-CRXiimvT.js → CreatedOnBrandCrowd-en-us-USGAnC_n.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-DCYdY6sj.js → CreatedOnBrandCrowd-es-es-CvVx3h4H.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-Dk7LOhxE.js → CreatedOnBrandCrowd-fr-ca-BCwamW5a.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-Dk7LOhxE.js → CreatedOnBrandCrowd-fr-fr-BCwamW5a.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-Cp4ufHJP.js → CreatedOnBrandCrowd-pt-br-Bt72hMrQ.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-Cp4ufHJP.js → CreatedOnBrandCrowd-pt-pt-Bt72hMrQ.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-ZSLkWMwM.js → CreatedOnDcom-de-de-BxUDrgG9.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-BgdIGfT3.js → CreatedOnDcom-en-us-wjVp5eOs.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-D0KemkCZ.js → CreatedOnDcom-es-es-CwFL2q5h.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-BuwNjNbC.js → CreatedOnDcom-fr-ca-BPZ2_PNq.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-DCQcsFPf.js → CreatedOnDcom-fr-fr-D0vLOikZ.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-DD-oLAS1.js → CreatedOnDcom-pt-br-CTwbxm9H.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-D8qLq2wW.js → CreatedOnDcom-pt-pt-Cfqf8LaM.js} +1 -1
  15. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +2 -5
  16. package/lib/src/brand-page/models/index.d.ts +26 -34
  17. package/lib/src/brand-page/utils/migration.util.js +0 -11
  18. package/lib/src/brand-page/utils/text.util.d.ts +1 -1
  19. package/lib/src/brand-page/utils/text.util.js +0 -32
  20. package/lib/src/brand-page/utils/vcard.util.js +4 -4
  21. package/lib/src/brand-page/utils/view-model.util.d.ts +0 -1
  22. package/lib/src/brand-page/utils/view-model.util.js +20 -68
  23. package/lib/src/brand-page/view-models.d.ts +3 -3
  24. package/lib/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.d.ts +1 -0
  25. package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +7 -3
  26. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.d.ts +9 -2
  27. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.js +10 -6
  28. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.d.ts +20 -0
  29. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.js +44 -0
  30. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.d.ts +27 -0
  31. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.js +44 -0
  32. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +1 -1
  33. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +103 -24
  34. package/lib/src/brand-page-maker/utils/index.d.ts +0 -2
  35. package/lib/src/brand-page-maker/utils/index.js +0 -2
  36. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +5 -28
  37. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +5 -48
  38. package/lib/src/brand-page-maker/validators/field.validator.js +2 -10
  39. package/lib/src/clients/ai-assistant-client.d.ts +1 -0
  40. package/lib/src/clients/ai-assistant-client.js +1 -0
  41. package/lib/src/css/library.brand-page-brandCrowd.min.css +39 -39
  42. package/lib/src/css/library.brand-page-brandPage.min.css +39 -39
  43. package/lib/src/css/library.brand-page-designCom.min.css +39 -39
  44. package/lib/src/css/library.brand-page-preview.min.css +39 -39
  45. package/lib/src/index.d.ts +0 -1
  46. package/lib/src/index.mjs +138 -143
  47. package/lib/src/{lib-Db6j9fQZ.js → lib-Cg4BExY4.js} +72135 -75458
  48. package/lib/src/lib.js +0 -2
  49. package/lib/src/metadata-builder/ai-metadata-executor.d.ts +1 -0
  50. package/lib/src/metadata-builder/ai-metadata-executor.js +19 -0
  51. package/lib/src/metadata-builder/builders/global-font-propagation.d.ts +28 -0
  52. package/lib/src/metadata-builder/builders/global-font-propagation.js +125 -0
  53. package/lib/src/metadata-builder/builders/global-styles-builder.js +16 -0
  54. package/lib/src/metadata-builder/builders/section-builders/about-section-builder.js +3 -2
  55. package/lib/src/metadata-builder/builders/section-builders/banner-section-builder.js +3 -2
  56. package/lib/src/metadata-builder/builders/section-builders/booking-section-builder.js +3 -2
  57. package/lib/src/metadata-builder/builders/section-builders/contact-form-builder.js +4 -3
  58. package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +3 -2
  59. package/lib/src/metadata-builder/builders/section-builders/index.d.ts +1 -0
  60. package/lib/src/metadata-builder/builders/section-builders/index.js +1 -0
  61. package/lib/src/metadata-builder/builders/section-builders/mailing-list-builder.js +2 -1
  62. package/lib/src/metadata-builder/builders/section-builders/mailing-list-section-builder.js +2 -1
  63. package/lib/src/metadata-builder/builders/section-builders/profile-section-builder.js +4 -3
  64. package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +3 -2
  65. package/lib/src/metadata-builder/builders/section-builders/support-me-builder.d.ts +8 -0
  66. package/lib/src/metadata-builder/builders/section-builders/support-me-builder.js +25 -0
  67. package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +2 -1
  68. package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +16 -17
  69. package/lib/src/metadata-builder/builders/section-field-maps.js +15 -0
  70. package/lib/src/metadata-builder/builders/section-field-utils.d.ts +6 -1
  71. package/lib/src/metadata-builder/builders/section-field-utils.js +48 -12
  72. package/lib/src/metadata-builder/builders/section-rules/support-me.rules.d.ts +2 -0
  73. package/lib/src/metadata-builder/builders/section-rules/support-me.rules.js +17 -0
  74. package/lib/src/metadata-builder/builders/update-content-text-builder.d.ts +17 -0
  75. package/lib/src/metadata-builder/builders/update-content-text-builder.js +103 -0
  76. package/lib/src/metadata-builder/builders/update-content-text-style-builder.d.ts +16 -0
  77. package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +216 -0
  78. package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +13 -11
  79. package/lib/src/metadata-builder/helpers/rte.helpers.js +73 -14
  80. package/lib/src/metadata-builder/metadata-executor.js +7 -1
  81. package/lib/src/metadata-builder/models.d.ts +4 -0
  82. package/lib/src/metadata-builder/models.js +4 -0
  83. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +20 -0
  84. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +8 -0
  85. package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +14 -2
  86. package/lib/src/metadata-builder/sanitizers/rte-schema-check.d.ts +8 -7
  87. package/lib/src/metadata-builder/sanitizers/rte-schema-check.js +52 -21
  88. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +1 -14
  89. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +3 -12
  90. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.d.ts +2 -3
  91. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.js +3 -10
  92. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +1 -2
  93. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +1 -4
  94. package/lib/src/utils/minify-brand-page-metadata.js +8 -4
  95. package/package.json +15 -17
  96. package/src/brand-page/components/contact-form/ContactForm.mixin.ts +2 -5
  97. package/src/brand-page/components/text-wrapper/TextWrapper.vue +6 -17
  98. package/src/brand-page/models/index.ts +26 -35
  99. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
  100. package/src/brand-page/utils/migration.util.ts +0 -12
  101. package/src/brand-page/utils/style.util.test.ts +0 -51
  102. package/src/brand-page/utils/text.util.ts +4 -43
  103. package/src/brand-page/utils/vcard.util.ts +4 -6
  104. package/src/brand-page/utils/view-model.util.ts +26 -94
  105. package/src/brand-page/view-models.ts +2 -3
  106. package/src/brand-page-maker/components/ai-website-builder/AiWebsiteBuilder.vue +1 -1
  107. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +3 -4
  108. package/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.ts +1 -0
  109. package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +7 -3
  110. package/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.ts +10 -6
  111. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.ts +73 -0
  112. package/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.ts +85 -0
  113. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +111 -27
  114. package/src/brand-page-maker/utils/index.ts +0 -2
  115. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +3 -37
  116. package/src/brand-page-maker/utils/rich-text-editor.util.ts +11 -66
  117. package/src/brand-page-maker/validators/field.validator.ts +2 -10
  118. package/src/clients/ai-assistant-client.ts +1 -0
  119. package/src/lib.ts +0 -9
  120. package/src/metadata-builder/ai-metadata-executor.ts +26 -0
  121. package/src/metadata-builder/builders/global-font-propagation.test.ts +269 -0
  122. package/src/metadata-builder/builders/global-font-propagation.ts +205 -0
  123. package/src/metadata-builder/builders/global-styles-builder.ts +21 -0
  124. package/src/metadata-builder/builders/section-builders/about-section-builder.ts +3 -2
  125. package/src/metadata-builder/builders/section-builders/banner-section-builder.ts +3 -2
  126. package/src/metadata-builder/builders/section-builders/booking-section-builder.ts +5 -2
  127. package/src/metadata-builder/builders/section-builders/contact-form-builder.ts +4 -3
  128. package/src/metadata-builder/builders/section-builders/images-section-builder.ts +3 -2
  129. package/src/metadata-builder/builders/section-builders/index.ts +1 -0
  130. package/src/metadata-builder/builders/section-builders/mailing-list-builder.ts +2 -1
  131. package/src/metadata-builder/builders/section-builders/mailing-list-section-builder.ts +2 -1
  132. package/src/metadata-builder/builders/section-builders/profile-section-builder.ts +6 -3
  133. package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +3 -2
  134. package/src/metadata-builder/builders/section-builders/support-me-builder.test.ts +177 -0
  135. package/src/metadata-builder/builders/section-builders/support-me-builder.ts +43 -0
  136. package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +2 -1
  137. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +18 -21
  138. package/src/metadata-builder/builders/section-field-maps.ts +18 -0
  139. package/src/metadata-builder/builders/section-field-utils.test.ts +83 -0
  140. package/src/metadata-builder/builders/section-field-utils.ts +69 -18
  141. package/src/metadata-builder/builders/section-rules/support-me.rules.ts +39 -0
  142. package/src/metadata-builder/builders/update-content-text-builder.ts +148 -0
  143. package/src/metadata-builder/builders/update-content-text-style-builder.ts +352 -0
  144. package/src/metadata-builder/helpers/rte.helpers.ts +101 -15
  145. package/src/metadata-builder/metadata-executor.ts +8 -0
  146. package/src/metadata-builder/models.ts +5 -0
  147. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +31 -0
  148. package/src/metadata-builder/parsers/section-tool-arguments.ts +14 -1
  149. package/src/metadata-builder/sanitizers/rte-schema-check.ts +53 -24
  150. package/src/shared/content-settings/components/GenericTextInput.mixin.ts +1 -17
  151. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +3 -16
  152. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.ts +4 -13
  153. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.vue +4 -6
  154. package/src/shared/content-settings/components/text-image-field/TextImageField.vue +3 -6
  155. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +1 -6
  156. package/src/shared/content-settings/components/text-items-field/TextItemsField.vue +1 -1
  157. package/src/utils/minify-brand-page-metadata.test.ts +61 -0
  158. package/src/utils/minify-brand-page-metadata.ts +9 -4
  159. package/lib/src/brand-page/utils/migration/migration-20.d.ts +0 -2
  160. package/lib/src/brand-page/utils/migration/migration-20.js +0 -66
  161. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +0 -22
  162. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +0 -22
  163. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +0 -14
  164. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +0 -39
  165. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
  166. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +0 -90
  167. package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -63
  168. package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -95
  169. package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +0 -6
  170. package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +0 -158
  171. package/src/brand-page/utils/migration/migration-20.server.test.ts +0 -125
  172. package/src/brand-page/utils/migration/migration-20.test.ts +0 -72
  173. package/src/brand-page/utils/migration/migration-20.ts +0 -87
  174. package/src/brand-page/utils/text.util.test.ts +0 -47
  175. package/src/brand-page/utils/view-model.pageid-rte.test.ts +0 -104
  176. package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +0 -23
  177. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +0 -52
  178. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +0 -42
  179. package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +0 -137
  180. package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +0 -202
  181. package/src/brand-page-maker/utils/rte-features.test.ts +0 -351
  182. package/src/brand-page-maker/utils/rte-render-parity.test.ts +0 -157
  183. package/src/brand-page-maker/utils/rte-render.util.ts +0 -165
  184. package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +0 -158
  185. package/src/brand-page-maker/utils/rte-sanitize.util.ts +0 -195
  186. package/src/brand-page-maker/validators/field.validator.test.ts +0 -55
@@ -3,6 +3,7 @@ import type {
3
3
  BrandPageModel,
4
4
  TextStyleDeclaration,
5
5
  } from '../../../brand-page/models';
6
+ import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
6
7
  import type { BuilderResult } from '../../models';
7
8
  import {
8
9
  isTiptapDocument,
@@ -47,7 +48,7 @@ export class MailingListSectionBuilder extends BaseSectionBuilder<MailingListCon
47
48
 
48
49
  if (mailingListArgs.title !== undefined) {
49
50
  if (isTiptapDocument(mailingListArgs.title)) {
50
- resolved.content.title = mailingListArgs.title;
51
+ resolved.content.title = tiptapJsonToHtml(mailingListArgs.title);
51
52
  resolved.content.titleStyle = resolveGlobalPresetIntoStyle(
52
53
  resolved.content.titleStyle,
53
54
  textStyleOptions,
@@ -3,6 +3,7 @@ import type {
3
3
  BrandPageModel,
4
4
  TextStyleDeclaration,
5
5
  } from '../../../brand-page/models';
6
+ import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
6
7
  import type { BuilderResult } from '../../models';
7
8
  import {
8
9
  isTiptapDocument,
@@ -40,7 +41,7 @@ export class ProfileSectionBuilder extends BaseSectionBuilder<ProfileContent> {
40
41
 
41
42
  if (profileArgs.name !== undefined) {
42
43
  if (isTiptapDocument(profileArgs.name)) {
43
- resolved.content.name = profileArgs.name;
44
+ resolved.content.name = tiptapJsonToHtml(profileArgs.name);
44
45
  resolved.content.nameStyle = resolveGlobalPresetIntoStyle(
45
46
  resolved.content.nameStyle,
46
47
  textStyleOptions,
@@ -52,7 +53,7 @@ export class ProfileSectionBuilder extends BaseSectionBuilder<ProfileContent> {
52
53
 
53
54
  if (profileArgs.title !== undefined) {
54
55
  if (isTiptapDocument(profileArgs.title)) {
55
- resolved.content.title = profileArgs.title;
56
+ resolved.content.title = tiptapJsonToHtml(profileArgs.title);
56
57
  resolved.content.titleStyle = resolveGlobalPresetIntoStyle(
57
58
  resolved.content.titleStyle,
58
59
  textStyleOptions,
@@ -64,7 +65,9 @@ export class ProfileSectionBuilder extends BaseSectionBuilder<ProfileContent> {
64
65
 
65
66
  if (profileArgs.description !== undefined) {
66
67
  if (isTiptapDocument(profileArgs.description)) {
67
- resolved.content.description = profileArgs.description;
68
+ resolved.content.description = tiptapJsonToHtml(
69
+ profileArgs.description,
70
+ );
68
71
  resolved.content.descriptionStyle = resolveGlobalPresetIntoStyle(
69
72
  resolved.content.descriptionStyle,
70
73
  textStyleOptions,
@@ -5,6 +5,7 @@ import type {
5
5
  TextShapeItem,
6
6
  TextStyleDeclaration,
7
7
  } from '../../../brand-page/models';
8
+ import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
8
9
  import type { BuilderResult } from '../../models';
9
10
  import {
10
11
  isTiptapDocument,
@@ -194,7 +195,7 @@ export class ShapesSectionBuilder extends BaseSectionBuilder<ShapesSectionConten
194
195
 
195
196
  if (args.title !== undefined) {
196
197
  if (isTiptapDocument(args.title)) {
197
- content.title = args.title;
198
+ content.title = tiptapJsonToHtml(args.title);
198
199
  content.titleStyle = resolveGlobalPresetIntoStyle(
199
200
  content.titleStyle,
200
201
  textStyleOptions,
@@ -206,7 +207,7 @@ export class ShapesSectionBuilder extends BaseSectionBuilder<ShapesSectionConten
206
207
 
207
208
  if (args.description !== undefined) {
208
209
  if (isTiptapDocument(args.description)) {
209
- content.description = args.description;
210
+ content.description = tiptapJsonToHtml(args.description);
210
211
  content.descriptionStyle = resolveGlobalPresetIntoStyle(
211
212
  content.descriptionStyle,
212
213
  textStyleOptions,
@@ -0,0 +1,177 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type {
3
+ BrandPageModel,
4
+ SupportMeContent,
5
+ } from '../../../brand-page/models';
6
+ import { BUILDER_TYPES } from '../../models';
7
+ import type { SupportMeArguments } from '../../parsers/section-tool-arguments';
8
+ import { SupportMeBuilder } from './support-me-builder';
9
+
10
+ const makeModel = (contents: SupportMeContent[] = []): BrandPageModel =>
11
+ ({
12
+ brandPageType: 'brandSite',
13
+ styles: { textStyles: { textStyleOptions: {} } },
14
+ pages: [{ id: 'page-1', contents }],
15
+ }) as unknown as BrandPageModel;
16
+
17
+ const seedContent = (): SupportMeContent =>
18
+ ({
19
+ id: 'sm-1',
20
+ type: 'support-me',
21
+ title: 'Existing title',
22
+ description: 'Existing description',
23
+ successMessage: 'Existing success',
24
+ displayStyle: 'preview',
25
+ supportMeConfig: {
26
+ paymentOptions: [{ amount: '0', id: 'po-1', name: 'Custom' }],
27
+ isCustomerMessageEnabled: true,
28
+ },
29
+ titleStyle: {},
30
+ headingStyle: {},
31
+ descriptionStyle: {},
32
+ buttonTextStyle: {},
33
+ itemTextStyle: {},
34
+ }) as unknown as SupportMeContent;
35
+
36
+ const create = (args: SupportMeArguments, model: BrandPageModel) =>
37
+ new SupportMeBuilder({
38
+ id: 't-create',
39
+ type: BUILDER_TYPES.CREATE_SUPPORT_ME,
40
+ payload: { sectionType: 'support-me', arguments: args, pageId: 'page-1' },
41
+ }).buildAsync(model);
42
+
43
+ const update = (
44
+ args: SupportMeArguments,
45
+ model: BrandPageModel,
46
+ contentId = 'sm-1',
47
+ ) =>
48
+ new SupportMeBuilder({
49
+ id: 't-update',
50
+ type: BUILDER_TYPES.UPDATE_SUPPORT_ME,
51
+ payload: { sectionType: 'support-me', arguments: args, contentId },
52
+ }).buildAsync(model);
53
+
54
+ describe('SupportMeBuilder — create', () => {
55
+ it('creates a support-me section with factory defaults', async () => {
56
+ const model = makeModel();
57
+
58
+ const { model: out, errors } = await create({}, model);
59
+
60
+ expect(errors ?? []).toEqual([]);
61
+
62
+ const created = out.pages[0].contents[0] as SupportMeContent;
63
+
64
+ expect(created.type).toBe('support-me');
65
+ // display style default comes from the factory
66
+ expect(created.displayStyle).toBe('preview');
67
+ // supportMeConfig (payment options) comes free from the factory, never the AI
68
+ expect(created.supportMeConfig.paymentOptions.length).toBeGreaterThan(0);
69
+ });
70
+
71
+ it('applies AI-provided scalar fields over the defaults', async () => {
72
+ const model = makeModel();
73
+
74
+ const { model: out, errors } = await create(
75
+ {
76
+ title: 'Buy me a coffee',
77
+ description: 'Support my work',
78
+ success_message: 'Thanks!',
79
+ display_style: 'collapsed',
80
+ },
81
+ model,
82
+ );
83
+
84
+ expect(errors ?? []).toEqual([]);
85
+
86
+ const created = out.pages[0].contents[0] as SupportMeContent;
87
+
88
+ expect(created.title).toBe('Buy me a coffee');
89
+ expect(created.description).toBe('Support my work');
90
+ expect(created.successMessage).toBe('Thanks!');
91
+ expect(created.displayStyle).toBe('collapsed');
92
+ });
93
+ });
94
+
95
+ describe('SupportMeBuilder — update', () => {
96
+ it('updates scalar fields on an existing section', async () => {
97
+ const model = makeModel([seedContent()]);
98
+
99
+ const { model: out, errors } = await update(
100
+ {
101
+ title: 'New title',
102
+ success_message: 'New success',
103
+ display_style: 'collapsed',
104
+ },
105
+ model,
106
+ );
107
+
108
+ expect(errors ?? []).toEqual([]);
109
+
110
+ const updated = out.pages[0].contents[0] as SupportMeContent;
111
+
112
+ expect(updated.title).toBe('New title');
113
+ expect(updated.successMessage).toBe('New success');
114
+ expect(updated.displayStyle).toBe('collapsed');
115
+ // untouched fields survive
116
+ expect(updated.description).toBe('Existing description');
117
+ // supportMeConfig is never AI-editable
118
+ expect(updated.supportMeConfig.paymentOptions.length).toBe(1);
119
+ });
120
+
121
+ it('applies the five style fields to their model keys', async () => {
122
+ const model = makeModel([seedContent()]);
123
+
124
+ const { model: out, errors } = await update(
125
+ {
126
+ title_style: { textAlign: 'left' },
127
+ heading_style: { textAlign: 'right' },
128
+ description_style: { textAlign: 'center' },
129
+ item_text_style: { textAlign: 'justify' },
130
+ button_text_style: { color: '#123456' },
131
+ },
132
+ model,
133
+ );
134
+
135
+ expect(errors ?? []).toEqual([]);
136
+
137
+ const updated = out.pages[0].contents[0] as SupportMeContent;
138
+
139
+ expect(updated.titleStyle).toMatchObject({ textAlign: 'left' });
140
+ expect(updated.headingStyle).toMatchObject({ textAlign: 'right' });
141
+ expect(updated.descriptionStyle).toMatchObject({ textAlign: 'center' });
142
+ expect(updated.itemTextStyle).toMatchObject({ textAlign: 'justify' });
143
+ expect(updated.buttonTextStyle).toMatchObject({ color: '#123456' });
144
+ });
145
+ });
146
+
147
+ describe('SupportMeBuilder — validation', () => {
148
+ it('rejects an invalid display_style enum value', async () => {
149
+ const model = makeModel([seedContent()]);
150
+
151
+ const { errors } = await update({ display_style: 'fancy' as never }, model);
152
+
153
+ expect(errors?.length).toBeGreaterThan(0);
154
+ expect(errors!.join(' ')).toContain('display_style');
155
+ });
156
+
157
+ it('rejects an over-length title', async () => {
158
+ const model = makeModel([seedContent()]);
159
+
160
+ const { errors } = await update({ title: 'x'.repeat(201) }, model);
161
+
162
+ expect(errors?.length).toBeGreaterThan(0);
163
+ expect(errors!.join(' ')).toContain('title');
164
+ });
165
+
166
+ it('rejects an over-length success_message', async () => {
167
+ const model = makeModel([seedContent()]);
168
+
169
+ const { errors } = await update(
170
+ { success_message: 'x'.repeat(1001) },
171
+ model,
172
+ );
173
+
174
+ expect(errors?.length).toBeGreaterThan(0);
175
+ expect(errors!.join(' ')).toContain('success_message');
176
+ });
177
+ });
@@ -0,0 +1,43 @@
1
+ import type {
2
+ BrandPageModel,
3
+ SupportMeContent,
4
+ } from '../../../brand-page/models';
5
+ import type { BuilderResult } from '../../models';
6
+ import { applyScalarFields, applyStyleFields } from '../section-field-utils';
7
+ import { BaseSectionBuilder } from './base-section-builder';
8
+ import { validateSupportMe } from '../section-rules/support-me.rules';
9
+
10
+ export class SupportMeBuilder extends BaseSectionBuilder<SupportMeContent> {
11
+ protected get sectionValidate() {
12
+ return validateSupportMe;
13
+ }
14
+
15
+ async buildAsync(model: BrandPageModel): Promise<BuilderResult> {
16
+ this.log('Build called');
17
+
18
+ const validation = await this.validateAsync();
19
+
20
+ if (validation.errors.length) {
21
+ return { request: this.request, model, errors: validation.errors };
22
+ }
23
+
24
+ const resolved = this.resolveAndRepositionContent(model);
25
+
26
+ if ('error' in resolved) return resolved.error;
27
+
28
+ const content = resolved.content;
29
+
30
+ // title/description/success_message are plain-text fields (no RTE support),
31
+ // so scalar application covers all content fields. supportMeConfig (payment
32
+ // options) is intentionally not AI-editable — it comes from the factory.
33
+ applyScalarFields(content, this.args, this.sectionType);
34
+ applyStyleFields(
35
+ content,
36
+ this.args,
37
+ this.sectionType,
38
+ this.getGlobalTextStyles(model),
39
+ );
40
+
41
+ return { request: this.request, model };
42
+ }
43
+ }
@@ -4,6 +4,7 @@ import type {
4
4
  BrandPageModel,
5
5
  TextStyleDeclaration,
6
6
  } from '../../../brand-page/models';
7
+ import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
7
8
  import {
8
9
  isTiptapDocument,
9
10
  type TestimonialItem,
@@ -172,7 +173,7 @@ export class TestimonialSectionBuilder extends BaseSectionBuilder<TestimonialSec
172
173
 
173
174
  if (args.title !== undefined) {
174
175
  if (isTiptapDocument(args.title)) {
175
- content.title = args.title;
176
+ content.title = tiptapJsonToHtml(args.title);
176
177
  content.titleStyle = resolveGlobalPresetIntoStyle(
177
178
  content.titleStyle,
178
179
  textStyleOptions,
@@ -4,9 +4,9 @@ import type {
4
4
  TextItem,
5
5
  TextStyleDeclaration,
6
6
  BrandPageModel,
7
- RichTextValue,
8
7
  } from '../../../brand-page/models';
9
8
  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,18 +26,12 @@ import {
26
26
  repositionItemByAnchor,
27
27
  } from '../item-id-utils';
28
28
 
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 };
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 };
39
33
 
40
- return { value: typeof value === 'string' ? value : '', wasTiptap: false };
34
+ return { html: typeof value === 'string' ? value : '', wasTiptap: false };
41
35
  };
42
36
 
43
37
  const clearTextStyleTypeIfTiptap = (
@@ -61,11 +55,11 @@ const createTextItem = (
61
55
  item.text_style ? { ...baseStyle, ...item.text_style } : { ...baseStyle }
62
56
  ) as TextStyleDeclaration;
63
57
 
64
- const { value, wasTiptap } = resolveText(item.text);
58
+ const { html, wasTiptap } = resolveText(item.text);
65
59
 
66
60
  return {
67
61
  id: item.item_id || generateItemId(),
68
- text: value,
62
+ text: html,
69
63
  textStyle: clearTextStyleTypeIfTiptap(
70
64
  textStyle,
71
65
  wasTiptap,
@@ -117,9 +111,9 @@ const mergeTextItems = (
117
111
  if (!existing) continue;
118
112
 
119
113
  if (item.text !== undefined) {
120
- const { value, wasTiptap } = resolveText(item.text);
114
+ const { html, wasTiptap } = resolveText(item.text);
121
115
 
122
- existing.text = value;
116
+ existing.text = html;
123
117
  existing.textStyle = clearTextStyleTypeIfTiptap(
124
118
  existing.textStyle || {},
125
119
  wasTiptap,
@@ -166,12 +160,12 @@ const applySingleTextField = (
166
160
  textStyleOptions: TextStyleOptions | undefined,
167
161
  ): void => {
168
162
  if (args.text !== undefined) {
169
- const { value, wasTiptap } = resolveText(args.text);
163
+ const { html, wasTiptap } = resolveText(args.text);
170
164
 
171
165
  if (Array.isArray(content.textItems) && content.textItems.length > 0) {
172
166
  content.textItems[0] = {
173
167
  ...content.textItems[0],
174
- text: value,
168
+ text: html,
175
169
  textStyle: clearTextStyleTypeIfTiptap(
176
170
  content.textItems[0].textStyle || {},
177
171
  wasTiptap,
@@ -182,7 +176,7 @@ const applySingleTextField = (
182
176
  content.textItems = [
183
177
  {
184
178
  id: generateItemId(),
185
- text: value,
179
+ text: html,
186
180
  textStyle: clearTextStyleTypeIfTiptap(
187
181
  { ...(content.textStyle || {}) },
188
182
  wasTiptap,
@@ -246,13 +240,16 @@ export class TextSectionBuilder extends BaseSectionBuilder<TextContent> {
246
240
  // Full replacement: discard existing items and rebuild from the payload.
247
241
  applyTextItems(content, args.items, textStyleOptions);
248
242
  } else if (this.mode === 'create') {
249
- if (args.items && Array.isArray(args.items)) {
243
+ // An EMPTY items array must not shadow the single text field — the AI emits
244
+ // `items: []` alongside `text` and the section rendered blank (QA 2026-07-05,
245
+ // T06: items=[] set textItems=[] and `text` was silently discarded).
246
+ if (args.items && Array.isArray(args.items) && args.items.length > 0) {
250
247
  applyTextItems(content, args.items, textStyleOptions);
251
248
  } else {
252
249
  applySingleTextField(content, args, textStyleOptions);
253
250
  }
254
251
  } else if (
255
- (args.items && Array.isArray(args.items)) ||
252
+ (args.items && Array.isArray(args.items) && args.items.length > 0) ||
256
253
  args._remove_item_ids?.length
257
254
  ) {
258
255
  // Update: id-based add / edit / move / remove in a single pass.
@@ -23,6 +23,7 @@ import type {
23
23
  ImageContent,
24
24
  ButtonContent,
25
25
  ImageGalleryContent,
26
+ SupportMeContent,
26
27
  } from '../../brand-page/models';
27
28
 
28
29
  export type SectionFieldMap = Record<string, string>;
@@ -175,9 +176,17 @@ const socialButtonFields = defineFieldMap<ButtonContent>({
175
176
  url: 'url',
176
177
  });
177
178
 
179
+ const supportMeFields = defineFieldMap<SupportMeContent>({
180
+ title: 'title',
181
+ description: 'description',
182
+ success_message: 'successMessage',
183
+ display_style: 'displayStyle',
184
+ });
185
+
178
186
  export const SECTION_FIELD_MAPS: Record<string, SectionFieldMap> = {
179
187
  'banner-section': bannerFields,
180
188
  'booking-section': bookingFields,
189
+ 'support-me': supportMeFields,
181
190
  'about-section': aboutFields,
182
191
  'testimonial-section': testimonialFields,
183
192
  'images-section': imagesSectionFields,
@@ -265,6 +274,14 @@ const mailingListStyleFields = defineFieldMap<MailingListContent>({
265
274
  button_text_style: 'buttonTextStyle',
266
275
  });
267
276
 
277
+ const supportMeStyleFields = defineFieldMap<SupportMeContent>({
278
+ title_style: 'titleStyle',
279
+ heading_style: 'headingStyle',
280
+ description_style: 'descriptionStyle',
281
+ item_text_style: 'itemTextStyle',
282
+ button_text_style: 'buttonTextStyle',
283
+ });
284
+
268
285
  const socialLinksStyleFields = defineFieldMap<SocialLinksContent>({
269
286
  icon_fill_style: 'iconFillStyles',
270
287
  icon_background_style: 'iconBackgroundStyles',
@@ -273,6 +290,7 @@ const socialLinksStyleFields = defineFieldMap<SocialLinksContent>({
273
290
  export const STYLE_FIELD_MAPS: Record<string, SectionFieldMap> = {
274
291
  'banner-section': bannerStyleFields,
275
292
  'booking-section': bookingStyleFields,
293
+ 'support-me': supportMeStyleFields,
276
294
  'about-section': aboutStyleFields,
277
295
  text: textStyleFields,
278
296
  'testimonial-section': testimonialStyleFields,
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { Content } from '../../brand-page/models';
3
+ import { applyStyleFields } from './section-field-utils';
4
+
5
+ /**
6
+ * Regression tests for the AI QA 2026-07-05 rerun P0: a colour-only *_style
7
+ * patch left the field's existing inline textStyle marks (old colour) in
8
+ * place — inline marks override the style layer at render, so recoloured
9
+ * bands showed invisible old-colour text. applyStyleFields now propagates a
10
+ * new style colour into the stored RTE HTML when the patch carries no new
11
+ * text for that field.
12
+ */
13
+ describe('applyStyleFields inline-colour propagation', () => {
14
+ const makeContent = (): Content =>
15
+ ({
16
+ type: 'about-section',
17
+ title: '<h3><span style="color: #111111">Fine Grooming</span></h3>',
18
+ titleStyle: { color: '#111111', fontSize: '2.188em' },
19
+ paragraph: '<p><span style="color: #111111">Body copy</span></p>',
20
+ paragraphStyle: { color: '#111111' },
21
+ }) as unknown as Content;
22
+
23
+ const recolour = (html: string, color: string): string =>
24
+ html.replace(/color: #[0-9a-fA-F]{6}/g, `color: ${color}`);
25
+
26
+ it('colour-only patch recolours the existing inline marks', () => {
27
+ const content = makeContent();
28
+
29
+ applyStyleFields(
30
+ content,
31
+ { title_style: { color: '#f8f2e9' } },
32
+ 'about-section',
33
+ undefined,
34
+ recolour,
35
+ );
36
+
37
+ const record = content as unknown as Record<string, unknown>;
38
+
39
+ expect(record.title).toContain('color: #f8f2e9');
40
+ expect((record.titleStyle as Record<string, unknown>).color).toBe(
41
+ '#f8f2e9',
42
+ );
43
+ // Untouched fields keep their marks.
44
+ expect(record.paragraph).toContain('color: #111111');
45
+ });
46
+
47
+ it('does NOT rewrite marks when the patch also carries new text for the field', () => {
48
+ const content = makeContent();
49
+
50
+ applyStyleFields(
51
+ content,
52
+ {
53
+ title: { type: 'doc', content: [] },
54
+ title_style: { color: '#f8f2e9' },
55
+ },
56
+ 'about-section',
57
+ undefined,
58
+ recolour,
59
+ );
60
+
61
+ // The builder's own text path owns the rewrite — HTML left alone here.
62
+ expect((content as unknown as Record<string, unknown>).title).toContain(
63
+ 'color: #111111',
64
+ );
65
+ });
66
+
67
+ it('merged style keeps non-colour properties from the current style', () => {
68
+ const content = makeContent();
69
+
70
+ applyStyleFields(
71
+ content,
72
+ { title_style: { color: '#f8f2e9' } },
73
+ 'about-section',
74
+ undefined,
75
+ recolour,
76
+ );
77
+
78
+ const style = (content as unknown as Record<string, unknown>)
79
+ .titleStyle as Record<string, unknown>;
80
+
81
+ expect(style.fontSize).toBe('2.188em');
82
+ });
83
+ });
@@ -9,6 +9,10 @@ import {
9
9
  isTiptapDocument,
10
10
  type SectionToolArguments,
11
11
  } from '../parsers/section-tool-arguments';
12
+ import { hasRTEContent } from '../../brand-page-maker/utils';
13
+ import { useExternalRichTextEditor } from '../../brand-page-maker/composables/rich-text-editor/useRichTextEditor';
14
+ import { applyStyleToRTEText } from '../helpers/rte.helpers';
15
+ import type { TextStyleDeclaration } from '../../brand-page/models';
12
16
  import { getSectionFieldMap, getStyleFieldMap } from './section-field-maps';
13
17
 
14
18
  /**
@@ -186,36 +190,83 @@ export const applyDetachedStyleOverride = <T extends object>(
186
190
  ...updates,
187
191
  }) as T;
188
192
 
193
+ /**
194
+ * Rewrites the stored RTE HTML's inline colour marks. Injectable for tests;
195
+ * the default uses the external Tiptap editor (font-propagation precedent).
196
+ */
197
+ export type ApplyColorToRTE = (html: string, color: string) => string;
198
+
189
199
  export const applyStyleFields = (
190
200
  content: Content,
191
201
  args: SectionToolArguments,
192
202
  sectionType: string,
193
203
  textStyleOptions?: TextStyleOptions,
204
+ applyColor?: ApplyColorToRTE,
194
205
  ): void => {
195
206
  const styleMap = getStyleFieldMap(sectionType);
196
207
 
197
208
  if (!styleMap) return;
198
209
 
199
210
  const record = content as IndexableContent;
211
+ let editor:
212
+ | ReturnType<typeof useExternalRichTextEditor>['externalEditor']
213
+ | undefined;
214
+ const applyColorToHtml: ApplyColorToRTE =
215
+ applyColor ??
216
+ ((html, color) => {
217
+ editor = editor ?? useExternalRichTextEditor().externalEditor;
218
+
219
+ return applyStyleToRTEText(editor, html, {
220
+ color,
221
+ } as TextStyleDeclaration);
222
+ });
200
223
 
201
- for (const [aiKey, modelKey] of Object.entries(styleMap)) {
202
- if (!(aiKey in args) || !args[aiKey] || typeof args[aiKey] !== 'object')
203
- continue;
204
-
205
- const aiStyle = args[aiKey] as Record<string, unknown>;
206
- const filtered: Record<string, unknown> = {};
207
-
208
- for (const [k, v] of Object.entries(aiStyle)) {
209
- const normalized = normalizeStyleValue(k, v);
210
-
211
- if (normalized !== undefined) filtered[k] = normalized;
224
+ try {
225
+ for (const [aiKey, modelKey] of Object.entries(styleMap)) {
226
+ if (!(aiKey in args) || !args[aiKey] || typeof args[aiKey] !== 'object')
227
+ continue;
228
+
229
+ const aiStyle = args[aiKey] as Record<string, unknown>;
230
+ const filtered: Record<string, unknown> = {};
231
+
232
+ for (const [k, v] of Object.entries(aiStyle)) {
233
+ const normalized = normalizeStyleValue(k, v);
234
+
235
+ if (normalized !== undefined) filtered[k] = normalized;
236
+ }
237
+
238
+ const resolved = resolveGlobalPresetIntoStyle(
239
+ content[modelKey] as object,
240
+ textStyleOptions,
241
+ );
242
+
243
+ record[modelKey] = { ...resolved, ...filtered, textStyleType: undefined };
244
+
245
+ // A colour patch without new text leaves the field's EXISTING inline
246
+ // textStyle marks in place — and inline marks override every style layer
247
+ // at render, so the old colour survives on a recoloured band (invisible
248
+ // text, AI QA 2026-07-05 rerun). Propagate the new colour into the
249
+ // stored HTML, mirroring global-font-propagation for font families.
250
+ const textKey = modelKey.endsWith('Style')
251
+ ? modelKey.slice(0, -'Style'.length)
252
+ : undefined;
253
+ const textAiKey = aiKey.endsWith('_style')
254
+ ? aiKey.slice(0, -'_style'.length)
255
+ : undefined;
256
+ const newColor = filtered.color;
257
+
258
+ if (
259
+ textKey &&
260
+ typeof newColor === 'string' &&
261
+ newColor &&
262
+ !(textAiKey && textAiKey in args) &&
263
+ typeof record[textKey] === 'string' &&
264
+ hasRTEContent(record[textKey] as string)
265
+ ) {
266
+ record[textKey] = applyColorToHtml(record[textKey] as string, newColor);
267
+ }
212
268
  }
213
-
214
- const resolved = resolveGlobalPresetIntoStyle(
215
- content[modelKey] as object,
216
- textStyleOptions,
217
- );
218
-
219
- record[modelKey] = { ...resolved, ...filtered, textStyleType: undefined };
269
+ } finally {
270
+ editor?.destroy();
220
271
  }
221
272
  };