@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
package/lib/src/lib.js CHANGED
@@ -30,8 +30,6 @@ export { minifyBrandPageMetadata, } from './utils/minify-brand-page-metadata';
30
30
  export * as brandPageUtils from './brand-page/utils';
31
31
  export { getOptimalLogoSize, calculateOptimizedLogoDimensions, applyLogoImageBase64UrlToBrandPageTemplateModel, recalculateLogoContentDimensions, recalculateHeaderLogoSizes, } from './brand-page/utils/logo.util';
32
32
  export { sanitizeRTEHtml } from './brand-page-maker/utils';
33
- // BP-5787: RTE JSON storage boundary helpers (render/normalise/sanitise/extract).
34
- export { renderRteToHtml, toRteJson, isTiptapDocument, sanitizeRteJson, getRteJsonPlainText, } from './brand-page-maker/utils';
35
33
  export { getPageSlugFromPageTitle } from './brand-page-maker/utils/slug.util';
36
34
  // AI-pipeline preview (longclaw per-turn snapshots): web-renderer's
37
35
  // /preview/ai-pipeline render page + manifest server route apply the agent's
@@ -6,6 +6,7 @@ import { type MetadataExecutorResult } from './models';
6
6
  import { type ParsedToolCall } from './parsers/ai-toolcall-parser';
7
7
  export declare const executeToolCallsAsync: (metadata: BrandPageModel, toolcalls: ParsedToolCall[], options: {
8
8
  logger?: Logger;
9
+ maxCharacters?: number;
9
10
  /**
10
11
  * Callback invoked by the UndoBuilder at execution time to resolve the
11
12
  * pre-prompt snapshot to restore to. The library is agnostic about where
@@ -1,4 +1,6 @@
1
1
  import {} from '../brand-page/models';
2
+ import { buildUpdateTextRequest } from './builders/update-content-text-builder';
3
+ import { buildUpdateTextStyleRequest } from './builders/update-content-text-style-builder';
2
4
  import { MetadataExecutor } from './metadata-executor';
3
5
  import { BUILDER_TYPES, } from './models';
4
6
  import { AI_ASSISTANT_TOOL_NAMES, SECTION_TOOL_MAP,
@@ -9,6 +11,23 @@ export const executeToolCallsAsync = async (metadata, toolcalls, options) => {
9
11
  const requests = toolcalls
10
12
  .map((toolcall) => {
11
13
  switch (toolcall.function) {
14
+ case AI_ASSISTANT_TOOL_NAMES.UPDATE_TEXT: {
15
+ const arg = toolcall.arguments;
16
+ return buildUpdateTextRequest(toolcall.toolId, {
17
+ contentId: arg.contentId,
18
+ fieldName: arg.fieldName,
19
+ newText: arg.metadata.text,
20
+ maxCharacters: options.maxCharacters,
21
+ });
22
+ }
23
+ case AI_ASSISTANT_TOOL_NAMES.UPDATE_STYLE: {
24
+ const arg = toolcall.arguments;
25
+ return buildUpdateTextStyleRequest(toolcall.toolId, {
26
+ contentId: arg.contentId,
27
+ fieldName: arg.fieldName,
28
+ newStyles: arg.metadata.style,
29
+ });
30
+ }
12
31
  case AI_ASSISTANT_TOOL_NAMES.DELETE_SECTION: {
13
32
  const arg = toolcall.arguments;
14
33
  return {
@@ -0,0 +1,28 @@
1
+ import type { BrandPageModel } from '../../brand-page/models';
2
+ import type { UpdateGlobalStylesArguments } from '../parsers/ai-toolcall-parser';
3
+ /**
4
+ * Propagation of AI global font-family changes into existing section content.
5
+ *
6
+ * A `tool_update_global_styles` call merges new per-category font families into
7
+ * `styles.textStyles.textStyleOptions` — but only content whose style is still
8
+ * bound to a preset (`textStyleType`) follows that change. Template-authored
9
+ * content carries detached `*Style` objects AND inline `font-family` marks in
10
+ * its RTE HTML, both of which override the preset at render time, so a global
11
+ * font change was invisible on most templates (BP-5780 ISSUE-01).
12
+ *
13
+ * This module walks every page's contents and, for each text field whose
14
+ * resolved category received a new family, aligns the detached style object and
15
+ * rewrites the RTE HTML's inline font-family marks. Preset-bound styles are NOT
16
+ * detached — they already follow the preset; only their RTE inline marks are
17
+ * rewritten (the marks would otherwise still win over the preset).
18
+ */
19
+ export type GlobalFontCategory = 'titles' | 'headings' | 'paragraphs' | 'buttons';
20
+ /** Extract the per-category families this AI call actually changes. */
21
+ export declare const collectChangedFontFamilies: (textStyles: UpdateGlobalStylesArguments["textStyles"]) => Partial<Record<GlobalFontCategory, string>>;
22
+ export type ApplyFontToRTE = (text: string, fontFamily: string) => string;
23
+ /**
24
+ * Walk every page's contents and propagate the changed families.
25
+ * `applyFont` is injectable for tests; the default rewrites inline marks via
26
+ * the external Tiptap editor.
27
+ */
28
+ export declare const propagateFontFamiliesToContents: (model: BrandPageModel, changed: Partial<Record<GlobalFontCategory, string>>, applyFont?: ApplyFontToRTE) => void;
@@ -0,0 +1,125 @@
1
+ import { hasRTEContent } from '../../brand-page-maker/utils';
2
+ import { useExternalRichTextEditor } from '../../brand-page-maker/composables/rich-text-editor/useRichTextEditor';
3
+ import { applyStyleToRTEText } from '../helpers/rte.helpers';
4
+ /**
5
+ * Default category per text field when its style is detached (no `textStyleType`).
6
+ * Mirrors the AI service's slot→category stamping (titles←h1, headings←h2-h4,
7
+ * paragraphs←body, buttons←button). `title` upgrades to `titles` on banner
8
+ * sections (the hero headline) via `resolveFieldCategory`.
9
+ */
10
+ const FIELD_DEFAULT_CATEGORY = {
11
+ title: 'headings',
12
+ heading: 'headings',
13
+ name: 'headings',
14
+ description: 'paragraphs',
15
+ paragraph: 'paragraphs',
16
+ text: 'paragraphs',
17
+ testimonial: 'paragraphs',
18
+ position: 'paragraphs',
19
+ question: 'headings',
20
+ answer: 'paragraphs',
21
+ buttonText: 'buttons',
22
+ successMessage: 'paragraphs',
23
+ };
24
+ const CATEGORIES = [
25
+ 'titles',
26
+ 'headings',
27
+ 'paragraphs',
28
+ 'buttons',
29
+ ];
30
+ const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
31
+ /** Extract the per-category families this AI call actually changes. */
32
+ export const collectChangedFontFamilies = (textStyles) => {
33
+ const changed = {};
34
+ if (!textStyles)
35
+ return changed;
36
+ for (const category of CATEGORIES) {
37
+ const family = textStyles.textStyleOptions?.[category]?.style?.fontFamily;
38
+ if (typeof family === 'string' && family.trim()) {
39
+ changed[category] = family;
40
+ }
41
+ }
42
+ // Base text family covers paragraphs when no explicit paragraphs entry came in.
43
+ if (!changed.paragraphs &&
44
+ typeof textStyles.textFontFamily === 'string' &&
45
+ textStyles.textFontFamily.trim()) {
46
+ changed.paragraphs = textStyles.textFontFamily;
47
+ }
48
+ return changed;
49
+ };
50
+ const resolveFieldCategory = (fieldName, style, content) => {
51
+ const preset = isPlainObject(style)
52
+ ? style.textStyleType
53
+ : undefined;
54
+ if (preset && CATEGORIES.includes(preset))
55
+ return preset;
56
+ if (fieldName === 'title' && content.type === 'banner-section') {
57
+ return 'titles';
58
+ }
59
+ return FIELD_DEFAULT_CATEGORY[fieldName];
60
+ };
61
+ /** `testimonialsStyle` → `testimonial`, `titleStyle` → `title`. */
62
+ const styleKeyToFieldName = (styleKey) => {
63
+ const base = styleKey.slice(0, -'Style'.length);
64
+ return base.endsWith('s') && !(base in FIELD_DEFAULT_CATEGORY)
65
+ ? base.slice(0, -1)
66
+ : base;
67
+ };
68
+ const walkRecord = (record, content, changed, applyFont) => {
69
+ for (const [key, value] of Object.entries(record)) {
70
+ if (key.endsWith('Style') && isPlainObject(value)) {
71
+ // Detached style objects carry their own fontFamily override — align it.
72
+ // Preset-bound styles keep following the preset (never detach here).
73
+ const category = resolveFieldCategory(styleKeyToFieldName(key), value, content);
74
+ const family = category ? changed[category] : undefined;
75
+ if (family && !value.textStyleType) {
76
+ value.fontFamily =
77
+ family;
78
+ }
79
+ }
80
+ else if (key in FIELD_DEFAULT_CATEGORY &&
81
+ typeof value === 'string' &&
82
+ hasRTEContent(value)) {
83
+ // Inline RTE marks override every style layer at render — rewrite them.
84
+ const category = resolveFieldCategory(key, record[`${key}Style`], content);
85
+ const family = category ? changed[category] : undefined;
86
+ if (family) {
87
+ record[key] = applyFont(value, family);
88
+ }
89
+ }
90
+ else if (Array.isArray(value)) {
91
+ for (const item of value) {
92
+ if (isPlainObject(item)) {
93
+ walkRecord(item, content, changed, applyFont);
94
+ }
95
+ }
96
+ }
97
+ }
98
+ };
99
+ /**
100
+ * Walk every page's contents and propagate the changed families.
101
+ * `applyFont` is injectable for tests; the default rewrites inline marks via
102
+ * the external Tiptap editor.
103
+ */
104
+ export const propagateFontFamiliesToContents = (model, changed, applyFont) => {
105
+ if (!Object.keys(changed).length)
106
+ return;
107
+ let editor;
108
+ const apply = applyFont ??
109
+ ((text, fontFamily) => {
110
+ editor = editor ?? useExternalRichTextEditor().externalEditor;
111
+ return applyStyleToRTEText(editor, text, {
112
+ fontFamily,
113
+ });
114
+ });
115
+ try {
116
+ for (const page of model.pages) {
117
+ for (const content of page.contents) {
118
+ walkRecord(content, content, changed, apply);
119
+ }
120
+ }
121
+ }
122
+ finally {
123
+ editor?.destroy();
124
+ }
125
+ };
@@ -1,6 +1,7 @@
1
1
  import {} from '../../brand-page/models';
2
2
  import { BaseMetadataBuilder, } from '../models';
3
3
  import { GLOBAL_STYLE_CHECKS, TEXT_STYLE_OPTION_KEYS, } from './section-rules/global-styles.rules';
4
+ import { collectChangedFontFamilies, propagateFontFamiliesToContents, } from './global-font-propagation';
4
5
  export class GlobalStylesBuilder extends BaseMetadataBuilder {
5
6
  constructor(request, logger) {
6
7
  super(request, logger);
@@ -30,6 +31,21 @@ export class GlobalStylesBuilder extends BaseMetadataBuilder {
30
31
  }
31
32
  if (args.textStyles) {
32
33
  this.applyTextStyles(model, args.textStyles);
34
+ // A category font change only reaches preset-bound content by itself;
35
+ // detached section styles and inline RTE font marks would still render
36
+ // the old family. Propagate so the change is actually visible (BP-5780).
37
+ const changedFamilies = collectChangedFontFamilies(args.textStyles);
38
+ if (Object.keys(changedFamilies).length) {
39
+ try {
40
+ propagateFontFamiliesToContents(model, changedFamilies);
41
+ }
42
+ catch (error) {
43
+ this.log({
44
+ message: 'Global font propagation to existing contents failed — presets still updated',
45
+ error,
46
+ });
47
+ }
48
+ }
33
49
  }
34
50
  if (args.buttonStyles) {
35
51
  this.applyButtonStyles(model, args.buttonStyles);
@@ -1,3 +1,4 @@
1
+ import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
1
2
  import { isTiptapDocument, } from '../../parsers/section-tool-arguments';
2
3
  import { applyScalarFields, applyStyleFields, resolveGlobalPresetIntoStyle, } from '../section-field-utils';
3
4
  import { applyColorThemeToReplacements, resolveShapeColorReplacementsAsync, } from '../shape-color-utils';
@@ -23,7 +24,7 @@ export class AboutSectionBuilder extends BaseSectionBuilder {
23
24
  const textStyleOptions = model.styles?.textStyles?.textStyleOptions;
24
25
  if (aboutArgs.title !== undefined) {
25
26
  if (isTiptapDocument(aboutArgs.title)) {
26
- resolved.content.title = aboutArgs.title;
27
+ resolved.content.title = tiptapJsonToHtml(aboutArgs.title);
27
28
  resolved.content.titleStyle = resolveGlobalPresetIntoStyle(resolved.content.titleStyle, textStyleOptions);
28
29
  }
29
30
  else {
@@ -32,7 +33,7 @@ export class AboutSectionBuilder extends BaseSectionBuilder {
32
33
  }
33
34
  if (aboutArgs.paragraph !== undefined) {
34
35
  if (isTiptapDocument(aboutArgs.paragraph)) {
35
- resolved.content.paragraph = aboutArgs.paragraph;
36
+ resolved.content.paragraph = tiptapJsonToHtml(aboutArgs.paragraph);
36
37
  resolved.content.paragraphStyle = resolveGlobalPresetIntoStyle(resolved.content.paragraphStyle, textStyleOptions);
37
38
  }
38
39
  else {
@@ -1,3 +1,4 @@
1
+ import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
1
2
  import { isTiptapDocument, } from '../../parsers/section-tool-arguments';
2
3
  import { applyButtonLink, applyScalarFields, applyStyleFields, resolveGlobalPresetIntoStyle, } from '../section-field-utils';
3
4
  import { applyColorThemeToReplacements, resolveShapeColorReplacementsAsync, } from '../shape-color-utils';
@@ -43,7 +44,7 @@ export class BannerSectionBuilder extends BaseSectionBuilder {
43
44
  const textStyleOptions = model.styles?.textStyles?.textStyleOptions;
44
45
  if (bannerArgs.title !== undefined) {
45
46
  if (isTiptapDocument(bannerArgs.title)) {
46
- resolved.content.title = bannerArgs.title;
47
+ resolved.content.title = tiptapJsonToHtml(bannerArgs.title);
47
48
  resolved.content.titleStyle = resolveGlobalPresetIntoStyle(resolved.content.titleStyle, textStyleOptions);
48
49
  }
49
50
  else {
@@ -52,7 +53,7 @@ export class BannerSectionBuilder extends BaseSectionBuilder {
52
53
  }
53
54
  if (bannerArgs.description !== undefined) {
54
55
  if (isTiptapDocument(bannerArgs.description)) {
55
- resolved.content.description = bannerArgs.description;
56
+ resolved.content.description = tiptapJsonToHtml(bannerArgs.description);
56
57
  resolved.content.descriptionStyle = resolveGlobalPresetIntoStyle(resolved.content.descriptionStyle, textStyleOptions);
57
58
  }
58
59
  else {
@@ -1,3 +1,4 @@
1
+ import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
1
2
  import { isTiptapDocument, } from '../../parsers/section-tool-arguments';
2
3
  import { applyButtonLink, applyScalarFields, applyStyleFields, resolveGlobalPresetIntoStyle, } from '../section-field-utils';
3
4
  import { BaseSectionBuilder } from './base-section-builder';
@@ -22,7 +23,7 @@ export class BookingSectionBuilder extends BaseSectionBuilder {
22
23
  const textStyleOptions = model.styles?.textStyles?.textStyleOptions;
23
24
  if (bookingArgs.title !== undefined) {
24
25
  if (isTiptapDocument(bookingArgs.title)) {
25
- resolved.content.title = bookingArgs.title;
26
+ resolved.content.title = tiptapJsonToHtml(bookingArgs.title);
26
27
  resolved.content.titleStyle = resolveGlobalPresetIntoStyle(resolved.content.titleStyle, textStyleOptions);
27
28
  }
28
29
  else {
@@ -31,7 +32,7 @@ export class BookingSectionBuilder extends BaseSectionBuilder {
31
32
  }
32
33
  if (bookingArgs.description !== undefined) {
33
34
  if (isTiptapDocument(bookingArgs.description)) {
34
- resolved.content.description = bookingArgs.description;
35
+ resolved.content.description = tiptapJsonToHtml(bookingArgs.description);
35
36
  resolved.content.descriptionStyle = resolveGlobalPresetIntoStyle(resolved.content.descriptionStyle, textStyleOptions);
36
37
  }
37
38
  else {
@@ -1,3 +1,4 @@
1
+ import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
1
2
  import { isTiptapDocument, } from '../../parsers/section-tool-arguments';
2
3
  import { applyScalarFields, applyStyleFields, resolveGlobalPresetIntoStyle, } from '../section-field-utils';
3
4
  import { BaseSectionBuilder } from './base-section-builder';
@@ -22,7 +23,7 @@ export class ContactFormBuilder extends BaseSectionBuilder {
22
23
  const textStyleOptions = model.styles?.textStyles?.textStyleOptions;
23
24
  if (args.title !== undefined) {
24
25
  if (isTiptapDocument(args.title)) {
25
- content.title = args.title;
26
+ content.title = tiptapJsonToHtml(args.title);
26
27
  content.titleStyle = resolveGlobalPresetIntoStyle(content.titleStyle, textStyleOptions);
27
28
  }
28
29
  else {
@@ -31,7 +32,7 @@ export class ContactFormBuilder extends BaseSectionBuilder {
31
32
  }
32
33
  if (args.description !== undefined) {
33
34
  if (isTiptapDocument(args.description)) {
34
- content.description = args.description;
35
+ content.description = tiptapJsonToHtml(args.description);
35
36
  content.descriptionStyle = resolveGlobalPresetIntoStyle(content.descriptionStyle, textStyleOptions);
36
37
  }
37
38
  else {
@@ -40,7 +41,7 @@ export class ContactFormBuilder extends BaseSectionBuilder {
40
41
  }
41
42
  if (args.success_message !== undefined) {
42
43
  if (isTiptapDocument(args.success_message)) {
43
- content.successMessage = args.success_message;
44
+ content.successMessage = tiptapJsonToHtml(args.success_message);
44
45
  content.successMessageStyle = resolveGlobalPresetIntoStyle(content.successMessageStyle, textStyleOptions);
45
46
  }
46
47
  else {
@@ -1,3 +1,4 @@
1
+ import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
1
2
  import { isTiptapDocument, } from '../../parsers/section-tool-arguments';
2
3
  import { applyScalarFields, applyStyleFields, isValidUrl, resolveGlobalPresetIntoStyle, } from '../section-field-utils';
3
4
  import { BaseSectionBuilder } from './base-section-builder';
@@ -95,7 +96,7 @@ export class ImagesSectionBuilder extends BaseSectionBuilder {
95
96
  const textStyleOptions = model.styles?.textStyles?.textStyleOptions;
96
97
  if (args.title !== undefined) {
97
98
  if (isTiptapDocument(args.title)) {
98
- content.title = args.title;
99
+ content.title = tiptapJsonToHtml(args.title);
99
100
  content.titleStyle = resolveGlobalPresetIntoStyle(content.titleStyle, textStyleOptions);
100
101
  }
101
102
  else {
@@ -104,7 +105,7 @@ export class ImagesSectionBuilder extends BaseSectionBuilder {
104
105
  }
105
106
  if (args.description !== undefined) {
106
107
  if (isTiptapDocument(args.description)) {
107
- content.description = args.description;
108
+ content.description = tiptapJsonToHtml(args.description);
108
109
  content.descriptionStyle = resolveGlobalPresetIntoStyle(content.descriptionStyle, textStyleOptions);
109
110
  }
110
111
  else {
@@ -1,6 +1,7 @@
1
1
  export { BaseSectionBuilder, type SectionBuilderPayload, } from './base-section-builder';
2
2
  export { BannerSectionBuilder } from './banner-section-builder';
3
3
  export { BookingSectionBuilder } from './booking-section-builder';
4
+ export { SupportMeBuilder } from './support-me-builder';
4
5
  export { AboutSectionBuilder } from './about-section-builder';
5
6
  export { ContactFormBuilder } from './contact-form-builder';
6
7
  export { DividerSectionBuilder } from './divider-section-builder';
@@ -1,6 +1,7 @@
1
1
  export { BaseSectionBuilder, } from './base-section-builder';
2
2
  export { BannerSectionBuilder } from './banner-section-builder';
3
3
  export { BookingSectionBuilder } from './booking-section-builder';
4
+ export { SupportMeBuilder } from './support-me-builder';
4
5
  export { AboutSectionBuilder } from './about-section-builder';
5
6
  export { ContactFormBuilder } from './contact-form-builder';
6
7
  export { DividerSectionBuilder } from './divider-section-builder';
@@ -1,3 +1,4 @@
1
+ import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
1
2
  import { isTiptapDocument, } from '../../parsers/section-tool-arguments';
2
3
  import { applyScalarFields, applyStyleFields, resolveGlobalPresetIntoStyle, } from '../section-field-utils';
3
4
  import { BaseSectionBuilder } from './base-section-builder';
@@ -22,7 +23,7 @@ export class MailingListBuilder extends BaseSectionBuilder {
22
23
  const textStyleOptions = model.styles?.textStyles?.textStyleOptions;
23
24
  if (mailingListArgs.title !== undefined) {
24
25
  if (isTiptapDocument(mailingListArgs.title)) {
25
- content.title = mailingListArgs.title;
26
+ content.title = tiptapJsonToHtml(mailingListArgs.title);
26
27
  content.titleStyle = resolveGlobalPresetIntoStyle(content.titleStyle, textStyleOptions);
27
28
  }
28
29
  else {
@@ -1,3 +1,4 @@
1
+ import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
1
2
  import { isTiptapDocument, } from '../../parsers/section-tool-arguments';
2
3
  import { applyScalarFields, applyStyleFields, resolveGlobalPresetIntoStyle, } from '../section-field-utils';
3
4
  import { BaseSectionBuilder } from './base-section-builder';
@@ -21,7 +22,7 @@ export class MailingListSectionBuilder extends BaseSectionBuilder {
21
22
  const textStyleOptions = model.styles?.textStyles?.textStyleOptions;
22
23
  if (mailingListArgs.title !== undefined) {
23
24
  if (isTiptapDocument(mailingListArgs.title)) {
24
- resolved.content.title = mailingListArgs.title;
25
+ resolved.content.title = tiptapJsonToHtml(mailingListArgs.title);
25
26
  resolved.content.titleStyle = resolveGlobalPresetIntoStyle(resolved.content.titleStyle, textStyleOptions);
26
27
  }
27
28
  else {
@@ -1,3 +1,4 @@
1
+ import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
1
2
  import { isTiptapDocument, } from '../../parsers/section-tool-arguments';
2
3
  import { applyScalarFields, resolveGlobalPresetIntoStyle, } from '../section-field-utils';
3
4
  import { BaseSectionBuilder } from './base-section-builder';
@@ -20,7 +21,7 @@ export class ProfileSectionBuilder extends BaseSectionBuilder {
20
21
  const textStyleOptions = model.styles?.textStyles?.textStyleOptions;
21
22
  if (profileArgs.name !== undefined) {
22
23
  if (isTiptapDocument(profileArgs.name)) {
23
- resolved.content.name = profileArgs.name;
24
+ resolved.content.name = tiptapJsonToHtml(profileArgs.name);
24
25
  resolved.content.nameStyle = resolveGlobalPresetIntoStyle(resolved.content.nameStyle, textStyleOptions);
25
26
  }
26
27
  else {
@@ -29,7 +30,7 @@ export class ProfileSectionBuilder extends BaseSectionBuilder {
29
30
  }
30
31
  if (profileArgs.title !== undefined) {
31
32
  if (isTiptapDocument(profileArgs.title)) {
32
- resolved.content.title = profileArgs.title;
33
+ resolved.content.title = tiptapJsonToHtml(profileArgs.title);
33
34
  resolved.content.titleStyle = resolveGlobalPresetIntoStyle(resolved.content.titleStyle, textStyleOptions);
34
35
  }
35
36
  else {
@@ -38,7 +39,7 @@ export class ProfileSectionBuilder extends BaseSectionBuilder {
38
39
  }
39
40
  if (profileArgs.description !== undefined) {
40
41
  if (isTiptapDocument(profileArgs.description)) {
41
- resolved.content.description = profileArgs.description;
42
+ resolved.content.description = tiptapJsonToHtml(profileArgs.description);
42
43
  resolved.content.descriptionStyle = resolveGlobalPresetIntoStyle(resolved.content.descriptionStyle, textStyleOptions);
43
44
  }
44
45
  else {
@@ -1,3 +1,4 @@
1
+ import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
1
2
  import { isTiptapDocument, } from '../../parsers/section-tool-arguments';
2
3
  import { applyScalarFields, applyStyleFields, resolveGlobalPresetIntoStyle, } from '../section-field-utils';
3
4
  import { applyColorThemeToReplacements, resolveShapeColorReplacementsAsync, } from '../shape-color-utils';
@@ -116,7 +117,7 @@ export class ShapesSectionBuilder extends BaseSectionBuilder {
116
117
  const textStyleOptions = model.styles?.textStyles?.textStyleOptions;
117
118
  if (args.title !== undefined) {
118
119
  if (isTiptapDocument(args.title)) {
119
- content.title = args.title;
120
+ content.title = tiptapJsonToHtml(args.title);
120
121
  content.titleStyle = resolveGlobalPresetIntoStyle(content.titleStyle, textStyleOptions);
121
122
  }
122
123
  else {
@@ -125,7 +126,7 @@ export class ShapesSectionBuilder extends BaseSectionBuilder {
125
126
  }
126
127
  if (args.description !== undefined) {
127
128
  if (isTiptapDocument(args.description)) {
128
- content.description = args.description;
129
+ content.description = tiptapJsonToHtml(args.description);
129
130
  content.descriptionStyle = resolveGlobalPresetIntoStyle(content.descriptionStyle, textStyleOptions);
130
131
  }
131
132
  else {
@@ -0,0 +1,8 @@
1
+ import type { BrandPageModel, SupportMeContent } from '../../../brand-page/models';
2
+ import type { BuilderResult } from '../../models';
3
+ import { BaseSectionBuilder } from './base-section-builder';
4
+ import { validateSupportMe } from '../section-rules/support-me.rules';
5
+ export declare class SupportMeBuilder extends BaseSectionBuilder<SupportMeContent> {
6
+ protected get sectionValidate(): typeof validateSupportMe;
7
+ buildAsync(model: BrandPageModel): Promise<BuilderResult>;
8
+ }
@@ -0,0 +1,25 @@
1
+ import { applyScalarFields, applyStyleFields } from '../section-field-utils';
2
+ import { BaseSectionBuilder } from './base-section-builder';
3
+ import { validateSupportMe } from '../section-rules/support-me.rules';
4
+ export class SupportMeBuilder extends BaseSectionBuilder {
5
+ get sectionValidate() {
6
+ return validateSupportMe;
7
+ }
8
+ async buildAsync(model) {
9
+ this.log('Build called');
10
+ const validation = await this.validateAsync();
11
+ if (validation.errors.length) {
12
+ return { request: this.request, model, errors: validation.errors };
13
+ }
14
+ const resolved = this.resolveAndRepositionContent(model);
15
+ if ('error' in resolved)
16
+ return resolved.error;
17
+ const content = resolved.content;
18
+ // title/description/success_message are plain-text fields (no RTE support),
19
+ // so scalar application covers all content fields. supportMeConfig (payment
20
+ // options) is intentionally not AI-editable — it comes from the factory.
21
+ applyScalarFields(content, this.args, this.sectionType);
22
+ applyStyleFields(content, this.args, this.sectionType, this.getGlobalTextStyles(model));
23
+ return { request: this.request, model };
24
+ }
25
+ }
@@ -1,3 +1,4 @@
1
+ import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
1
2
  import { isTiptapDocument, } from '../../parsers/section-tool-arguments';
2
3
  import { applyScalarFields, applyStyleFields, resolveGlobalPresetIntoStyle, } from '../section-field-utils';
3
4
  import { applyColorThemeToReplacements, resolveShapeColorReplacementsAsync, } from '../shape-color-utils';
@@ -102,7 +103,7 @@ export class TestimonialSectionBuilder extends BaseSectionBuilder {
102
103
  const textStyleOptions = model.styles?.textStyles?.textStyleOptions;
103
104
  if (args.title !== undefined) {
104
105
  if (isTiptapDocument(args.title)) {
105
- content.title = args.title;
106
+ content.title = tiptapJsonToHtml(args.title);
106
107
  content.titleStyle = resolveGlobalPresetIntoStyle(content.titleStyle, textStyleOptions);
107
108
  }
108
109
  else {
@@ -1,28 +1,24 @@
1
+ import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
1
2
  import { isTiptapDocument, } from '../../parsers/section-tool-arguments';
2
3
  import { applyScalarFields, applyStyleFields, resolveGlobalPresetIntoStyle, } from '../section-field-utils';
3
4
  import { BaseSectionBuilder } from './base-section-builder';
4
5
  import { validateTextSection } from '../section-rules/text-section.rules';
5
6
  import { ensureItemIds, generateItemId, removeItemsByIds, repositionItemByAnchor, } from '../item-id-utils';
6
- /**
7
- * Resolve a RichTextField to its stored RTE value. BP-5787: Tiptap JSON is
8
- * stored as-is (no JSON→HTML round-trip — migration-20 is now a one-time
9
- * legacy conversion), plain strings pass through. `wasTiptap` drives the
10
- * text-style preset resolution for rich content.
11
- */
7
+ /** Resolve a RichTextField (string or TiptapDocument) to a plain HTML string. */
12
8
  const resolveText = (value) => {
13
9
  if (isTiptapDocument(value))
14
- return { value, wasTiptap: true };
15
- return { value: typeof value === 'string' ? value : '', wasTiptap: false };
10
+ return { html: tiptapJsonToHtml(value), wasTiptap: true };
11
+ return { html: typeof value === 'string' ? value : '', wasTiptap: false };
16
12
  };
17
13
  const clearTextStyleTypeIfTiptap = (style, wasTiptap, textStyleOptions) => wasTiptap
18
14
  ? resolveGlobalPresetIntoStyle(style, textStyleOptions)
19
15
  : style;
20
16
  const createTextItem = (item, baseStyle, textStyleOptions) => {
21
17
  const textStyle = (item.text_style ? { ...baseStyle, ...item.text_style } : { ...baseStyle });
22
- const { value, wasTiptap } = resolveText(item.text);
18
+ const { html, wasTiptap } = resolveText(item.text);
23
19
  return {
24
20
  id: item.item_id || generateItemId(),
25
- text: value,
21
+ text: html,
26
22
  textStyle: clearTextStyleTypeIfTiptap(textStyle, wasTiptap, textStyleOptions),
27
23
  };
28
24
  };
@@ -55,8 +51,8 @@ const mergeTextItems = (content, items, removeIds, textStyleOptions) => {
55
51
  if (!existing)
56
52
  continue;
57
53
  if (item.text !== undefined) {
58
- const { value, wasTiptap } = resolveText(item.text);
59
- existing.text = value;
54
+ const { html, wasTiptap } = resolveText(item.text);
55
+ existing.text = html;
60
56
  existing.textStyle = clearTextStyleTypeIfTiptap(existing.textStyle || {}, wasTiptap, textStyleOptions);
61
57
  }
62
58
  if (item.text_style && typeof item.text_style === 'object') {
@@ -80,11 +76,11 @@ const mergeTextItems = (content, items, removeIds, textStyleOptions) => {
80
76
  */
81
77
  const applySingleTextField = (content, args, textStyleOptions) => {
82
78
  if (args.text !== undefined) {
83
- const { value, wasTiptap } = resolveText(args.text);
79
+ const { html, wasTiptap } = resolveText(args.text);
84
80
  if (Array.isArray(content.textItems) && content.textItems.length > 0) {
85
81
  content.textItems[0] = {
86
82
  ...content.textItems[0],
87
- text: value,
83
+ text: html,
88
84
  textStyle: clearTextStyleTypeIfTiptap(content.textItems[0].textStyle || {}, wasTiptap, textStyleOptions),
89
85
  };
90
86
  }
@@ -92,7 +88,7 @@ const applySingleTextField = (content, args, textStyleOptions) => {
92
88
  content.textItems = [
93
89
  {
94
90
  id: generateItemId(),
95
- text: value,
91
+ text: html,
96
92
  textStyle: clearTextStyleTypeIfTiptap({ ...(content.textStyle || {}) }, wasTiptap, textStyleOptions),
97
93
  },
98
94
  ];
@@ -135,14 +131,17 @@ export class TextSectionBuilder extends BaseSectionBuilder {
135
131
  applyTextItems(content, args.items, textStyleOptions);
136
132
  }
137
133
  else if (this.mode === 'create') {
138
- if (args.items && Array.isArray(args.items)) {
134
+ // An EMPTY items array must not shadow the single text field — the AI emits
135
+ // `items: []` alongside `text` and the section rendered blank (QA 2026-07-05,
136
+ // T06: items=[] set textItems=[] and `text` was silently discarded).
137
+ if (args.items && Array.isArray(args.items) && args.items.length > 0) {
139
138
  applyTextItems(content, args.items, textStyleOptions);
140
139
  }
141
140
  else {
142
141
  applySingleTextField(content, args, textStyleOptions);
143
142
  }
144
143
  }
145
- else if ((args.items && Array.isArray(args.items)) ||
144
+ else if ((args.items && Array.isArray(args.items) && args.items.length > 0) ||
146
145
  args._remove_item_ids?.length) {
147
146
  // Update: id-based add / edit / move / remove in a single pass.
148
147
  mergeTextItems(content, args.items ?? [], args._remove_item_ids, textStyleOptions);
@@ -131,9 +131,16 @@ const socialButtonFields = defineFieldMap({
131
131
  title: 'title',
132
132
  url: 'url',
133
133
  });
134
+ const supportMeFields = defineFieldMap({
135
+ title: 'title',
136
+ description: 'description',
137
+ success_message: 'successMessage',
138
+ display_style: 'displayStyle',
139
+ });
134
140
  export const SECTION_FIELD_MAPS = {
135
141
  'banner-section': bannerFields,
136
142
  'booking-section': bookingFields,
143
+ 'support-me': supportMeFields,
137
144
  'about-section': aboutFields,
138
145
  'testimonial-section': testimonialFields,
139
146
  'images-section': imagesSectionFields,
@@ -208,6 +215,13 @@ const mailingListStyleFields = defineFieldMap({
208
215
  title_style: 'titleStyle',
209
216
  button_text_style: 'buttonTextStyle',
210
217
  });
218
+ const supportMeStyleFields = defineFieldMap({
219
+ title_style: 'titleStyle',
220
+ heading_style: 'headingStyle',
221
+ description_style: 'descriptionStyle',
222
+ item_text_style: 'itemTextStyle',
223
+ button_text_style: 'buttonTextStyle',
224
+ });
211
225
  const socialLinksStyleFields = defineFieldMap({
212
226
  icon_fill_style: 'iconFillStyles',
213
227
  icon_background_style: 'iconBackgroundStyles',
@@ -215,6 +229,7 @@ const socialLinksStyleFields = defineFieldMap({
215
229
  export const STYLE_FIELD_MAPS = {
216
230
  'banner-section': bannerStyleFields,
217
231
  'booking-section': bookingStyleFields,
232
+ 'support-me': supportMeStyleFields,
218
233
  'about-section': aboutStyleFields,
219
234
  text: textStyleFields,
220
235
  'testimonial-section': testimonialStyleFields,