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

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 (155) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-CKBqjDH3.js → CreatedOnBrandCrowd-de-de-DU6YEVcW.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-CRXiimvT.js → CreatedOnBrandCrowd-en-us-DNWWK6Tj.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-DCYdY6sj.js → CreatedOnBrandCrowd-es-es-DVFvV9Qp.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-Dk7LOhxE.js → CreatedOnBrandCrowd-fr-ca-B4ZkjNdw.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-Dk7LOhxE.js → CreatedOnBrandCrowd-fr-fr-B4ZkjNdw.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-Cp4ufHJP.js → CreatedOnBrandCrowd-pt-br-DP7AQGgW.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-Cp4ufHJP.js → CreatedOnBrandCrowd-pt-pt-DP7AQGgW.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-ZSLkWMwM.js → CreatedOnDcom-de-de-CaC3GtG9.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-BgdIGfT3.js → CreatedOnDcom-en-us-D16yT0-P.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-D0KemkCZ.js → CreatedOnDcom-es-es-DA5sHgD_.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-BuwNjNbC.js → CreatedOnDcom-fr-ca-CDv2SG1-.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-DCQcsFPf.js → CreatedOnDcom-fr-fr-Cygc7c5d.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-DD-oLAS1.js → CreatedOnDcom-pt-br-B_3RIzgF.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-D8qLq2wW.js → CreatedOnDcom-pt-pt-Bz5w-rrD.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/rich-text-editor/bubbleMenu.d.ts +9 -2
  26. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.js +10 -6
  27. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.d.ts +20 -0
  28. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.js +44 -0
  29. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.d.ts +27 -0
  30. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.js +44 -0
  31. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +1 -1
  32. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +103 -24
  33. package/lib/src/brand-page-maker/utils/index.d.ts +0 -2
  34. package/lib/src/brand-page-maker/utils/index.js +0 -2
  35. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +5 -28
  36. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +5 -48
  37. package/lib/src/brand-page-maker/validators/field.validator.js +2 -10
  38. package/lib/src/css/library.brand-page-brandCrowd.min.css +39 -39
  39. package/lib/src/css/library.brand-page-brandPage.min.css +39 -39
  40. package/lib/src/css/library.brand-page-designCom.min.css +39 -39
  41. package/lib/src/css/library.brand-page-preview.min.css +39 -39
  42. package/lib/src/index.d.ts +0 -1
  43. package/lib/src/index.mjs +138 -143
  44. package/lib/src/{lib-Db6j9fQZ.js → lib-6_P6kXPu.js} +51709 -55194
  45. package/lib/src/lib.js +0 -2
  46. package/lib/src/metadata-builder/ai-metadata-executor.d.ts +1 -0
  47. package/lib/src/metadata-builder/ai-metadata-executor.js +19 -0
  48. package/lib/src/metadata-builder/builders/section-builders/about-section-builder.js +3 -2
  49. package/lib/src/metadata-builder/builders/section-builders/banner-section-builder.js +3 -2
  50. package/lib/src/metadata-builder/builders/section-builders/booking-section-builder.js +3 -2
  51. package/lib/src/metadata-builder/builders/section-builders/contact-form-builder.js +4 -3
  52. package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +3 -2
  53. package/lib/src/metadata-builder/builders/section-builders/mailing-list-builder.js +2 -1
  54. package/lib/src/metadata-builder/builders/section-builders/mailing-list-section-builder.js +2 -1
  55. package/lib/src/metadata-builder/builders/section-builders/profile-section-builder.js +4 -3
  56. package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +3 -2
  57. package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +2 -1
  58. package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +11 -15
  59. package/lib/src/metadata-builder/builders/update-content-text-builder.d.ts +17 -0
  60. package/lib/src/metadata-builder/builders/update-content-text-builder.js +103 -0
  61. package/lib/src/metadata-builder/builders/update-content-text-style-builder.d.ts +16 -0
  62. package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +216 -0
  63. package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +13 -11
  64. package/lib/src/metadata-builder/helpers/rte.helpers.js +73 -14
  65. package/lib/src/metadata-builder/metadata-executor.js +4 -0
  66. package/lib/src/metadata-builder/models.d.ts +2 -0
  67. package/lib/src/metadata-builder/models.js +2 -0
  68. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +19 -0
  69. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +4 -0
  70. package/lib/src/metadata-builder/sanitizers/rte-schema-check.d.ts +8 -7
  71. package/lib/src/metadata-builder/sanitizers/rte-schema-check.js +52 -21
  72. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +1 -14
  73. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +3 -12
  74. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.d.ts +2 -3
  75. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.js +3 -10
  76. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +1 -2
  77. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +1 -4
  78. package/lib/src/utils/minify-brand-page-metadata.js +1 -4
  79. package/package.json +15 -17
  80. package/src/brand-page/components/contact-form/ContactForm.mixin.ts +2 -5
  81. package/src/brand-page/components/text-wrapper/TextWrapper.vue +6 -17
  82. package/src/brand-page/models/index.ts +26 -35
  83. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
  84. package/src/brand-page/utils/migration.util.ts +0 -12
  85. package/src/brand-page/utils/style.util.test.ts +0 -51
  86. package/src/brand-page/utils/text.util.ts +4 -43
  87. package/src/brand-page/utils/vcard.util.ts +4 -6
  88. package/src/brand-page/utils/view-model.util.ts +26 -94
  89. package/src/brand-page/view-models.ts +2 -3
  90. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +3 -4
  91. package/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.ts +1 -0
  92. package/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.ts +10 -6
  93. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.ts +73 -0
  94. package/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.ts +85 -0
  95. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +111 -27
  96. package/src/brand-page-maker/utils/index.ts +0 -2
  97. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +3 -37
  98. package/src/brand-page-maker/utils/rich-text-editor.util.ts +11 -66
  99. package/src/brand-page-maker/validators/field.validator.ts +2 -10
  100. package/src/lib.ts +0 -9
  101. package/src/metadata-builder/ai-metadata-executor.ts +26 -0
  102. package/src/metadata-builder/builders/section-builders/about-section-builder.ts +3 -2
  103. package/src/metadata-builder/builders/section-builders/banner-section-builder.ts +3 -2
  104. package/src/metadata-builder/builders/section-builders/booking-section-builder.ts +5 -2
  105. package/src/metadata-builder/builders/section-builders/contact-form-builder.ts +4 -3
  106. package/src/metadata-builder/builders/section-builders/images-section-builder.ts +3 -2
  107. package/src/metadata-builder/builders/section-builders/mailing-list-builder.ts +2 -1
  108. package/src/metadata-builder/builders/section-builders/mailing-list-section-builder.ts +2 -1
  109. package/src/metadata-builder/builders/section-builders/profile-section-builder.ts +6 -3
  110. package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +3 -2
  111. package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +2 -1
  112. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +13 -19
  113. package/src/metadata-builder/builders/update-content-text-builder.ts +148 -0
  114. package/src/metadata-builder/builders/update-content-text-style-builder.ts +352 -0
  115. package/src/metadata-builder/helpers/rte.helpers.ts +101 -15
  116. package/src/metadata-builder/metadata-executor.ts +5 -0
  117. package/src/metadata-builder/models.ts +3 -0
  118. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +27 -0
  119. package/src/metadata-builder/sanitizers/rte-schema-check.ts +53 -24
  120. package/src/shared/content-settings/components/GenericTextInput.mixin.ts +1 -17
  121. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +3 -16
  122. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.ts +4 -13
  123. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.vue +4 -6
  124. package/src/shared/content-settings/components/text-image-field/TextImageField.vue +3 -6
  125. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +1 -6
  126. package/src/shared/content-settings/components/text-items-field/TextItemsField.vue +1 -1
  127. package/src/utils/minify-brand-page-metadata.ts +1 -4
  128. package/lib/src/brand-page/utils/migration/migration-20.d.ts +0 -2
  129. package/lib/src/brand-page/utils/migration/migration-20.js +0 -66
  130. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +0 -22
  131. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +0 -22
  132. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +0 -14
  133. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +0 -39
  134. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
  135. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +0 -90
  136. package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -63
  137. package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -95
  138. package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +0 -6
  139. package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +0 -158
  140. package/src/brand-page/utils/migration/migration-20.server.test.ts +0 -125
  141. package/src/brand-page/utils/migration/migration-20.test.ts +0 -72
  142. package/src/brand-page/utils/migration/migration-20.ts +0 -87
  143. package/src/brand-page/utils/text.util.test.ts +0 -47
  144. package/src/brand-page/utils/view-model.pageid-rte.test.ts +0 -104
  145. package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +0 -23
  146. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +0 -52
  147. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +0 -42
  148. package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +0 -137
  149. package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +0 -202
  150. package/src/brand-page-maker/utils/rte-features.test.ts +0 -351
  151. package/src/brand-page-maker/utils/rte-render-parity.test.ts +0 -157
  152. package/src/brand-page-maker/utils/rte-render.util.ts +0 -165
  153. package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +0 -158
  154. package/src/brand-page-maker/utils/rte-sanitize.util.ts +0 -195
  155. package/src/brand-page-maker/validators/field.validator.test.ts +0 -55
@@ -0,0 +1,216 @@
1
+ import {} from '../../brand-page/models';
2
+ import { BaseMetadataBuilder, BUILDER_TYPES, } from '../models';
3
+ import { hasRTEContent } from '../../brand-page-maker/utils';
4
+ import { useExternalRichTextEditor } from '../../brand-page-maker/composables/rich-text-editor/useRichTextEditor';
5
+ import { applyStyleToRTEText } from '../helpers/rte.helpers';
6
+ import { letterSpacingValueConvertor } from '../../brand-page-maker/convertors/letter-spacing-value.convertor';
7
+ export const buildUpdateTextStyleRequest = (id, payload) => {
8
+ return {
9
+ id,
10
+ type: BUILDER_TYPES.UPDATE_CONTENT_TEXT_STYLE,
11
+ payload,
12
+ };
13
+ };
14
+ const mergeStyles = (currentStyle, stylesToUpdate, textStyleOptions) => {
15
+ let merged = { ...currentStyle };
16
+ // If content uses a textStyleType preset (e.g. "titles"), resolve the preset
17
+ // styles into the style object before clearing the preset reference.
18
+ // This ensures properties like fontSize, fontFamily etc. are preserved
19
+ // when the AI only changes a single property like color.
20
+ if (merged.textStyleType && textStyleOptions?.[merged.textStyleType]) {
21
+ const presetStyle = textStyleOptions[merged.textStyleType].style;
22
+ merged = { ...merged, ...presetStyle };
23
+ merged.textStyleType = undefined;
24
+ }
25
+ Object.entries(stylesToUpdate).forEach(([key, value]) => {
26
+ if (value === null) {
27
+ // backgroundColor: null means "remove background color"
28
+ if (key === 'backgroundColor') {
29
+ merged.backgroundColor = '';
30
+ }
31
+ // All other null values mean "not changed" — skip to preserve existing values
32
+ return;
33
+ }
34
+ merged[key] = value;
35
+ });
36
+ return merged;
37
+ };
38
+ const updateRTEText = (text, style) => {
39
+ if (!text || typeof text !== 'string' || !hasRTEContent(text)) {
40
+ return text;
41
+ }
42
+ const { externalEditor } = useExternalRichTextEditor();
43
+ const updatedText = applyStyleToRTEText(externalEditor, text, style);
44
+ externalEditor.destroy();
45
+ return updatedText;
46
+ };
47
+ const findStyleKey = (target, property) => {
48
+ // Try exact match first (e.g., 'testimonialsStyle')
49
+ const exactKey = `${property}Style`;
50
+ if (exactKey in target)
51
+ return exactKey;
52
+ // Try singular form (e.g., 'testimonialStyle' for 'testimonials')
53
+ if (property.endsWith('s')) {
54
+ const singularKey = `${property.slice(0, -1)}Style`;
55
+ if (singularKey in target)
56
+ return singularKey;
57
+ }
58
+ // Try plural form (edge case)
59
+ const pluralKey = `${property}sStyle`;
60
+ if (pluralKey in target)
61
+ return pluralKey;
62
+ return exactKey; // fallback to exact match
63
+ };
64
+ const updateSharedArrayStyle = (content, property, stylesToUpdate, textStyleOptions) => {
65
+ const styleKey = findStyleKey(content, property);
66
+ if (!styleKey)
67
+ return;
68
+ const currentStyle = content[styleKey] || {};
69
+ const mergedStyle = mergeStyles(currentStyle, stylesToUpdate, textStyleOptions);
70
+ content[styleKey] = mergedStyle;
71
+ };
72
+ const updateArrayItemStyle = (content, fieldName, index, property, stylesToUpdate, textStyleOptions) => {
73
+ const currentContentItems = content[fieldName];
74
+ if (!Array.isArray(currentContentItems) || !currentContentItems[+index])
75
+ return;
76
+ const contentItem = currentContentItems[+index];
77
+ const styleKey = `${property}Style`;
78
+ const currentStyle = contentItem[styleKey] || {};
79
+ const mergedStyle = mergeStyles(currentStyle, stylesToUpdate, textStyleOptions);
80
+ const newContentItems = [...currentContentItems];
81
+ newContentItems[+index] = {
82
+ ...newContentItems[+index],
83
+ [styleKey]: mergedStyle,
84
+ [property]: updateRTEText(contentItem[property], mergedStyle),
85
+ };
86
+ content[fieldName] = newContentItems;
87
+ };
88
+ const updateTextStyle = (content, fieldName, stylesToUpdate, textStyleOptions) => {
89
+ // Check if fieldName is in the format of "fieldName.index.fieldItem"
90
+ const matchFieldItems = fieldName.match(/^(\w+)\.(\d+)\.(.+)$/);
91
+ if (matchFieldItems) {
92
+ const [, fieldName, index, fieldItem] = matchFieldItems;
93
+ const array = content[fieldName];
94
+ if (!Array.isArray(array) || !array[+index])
95
+ return;
96
+ const item = array[+index];
97
+ const itemStyleKey = `${fieldItem}Style`;
98
+ const hasItemStyle = itemStyleKey in item;
99
+ if (hasItemStyle) {
100
+ updateArrayItemStyle(content, fieldName, +index, fieldItem, stylesToUpdate, textStyleOptions);
101
+ }
102
+ else {
103
+ updateSharedArrayStyle(content, fieldItem, stylesToUpdate, textStyleOptions);
104
+ }
105
+ }
106
+ else {
107
+ const styleKey = `${fieldName}Style`;
108
+ const currentStyle = content[styleKey] || {};
109
+ const mergedStyle = mergeStyles(currentStyle, stylesToUpdate, textStyleOptions);
110
+ content[styleKey] = mergedStyle;
111
+ content[fieldName] = updateRTEText(content[fieldName], mergedStyle);
112
+ }
113
+ };
114
+ export class UpdateContentTextStyleBuilder extends BaseMetadataBuilder {
115
+ constructor(request, logger) {
116
+ super(request, logger);
117
+ }
118
+ validateAsync() {
119
+ const { newStyles } = this.request.payload;
120
+ const errors = [];
121
+ // Validate lineHeight: must be between 0 and 2.5 inclusive, snap to 0.1 step
122
+ if (newStyles.lineHeight !== undefined && newStyles.lineHeight !== null) {
123
+ const lineHeightValue = parseFloat(String(newStyles.lineHeight));
124
+ if (isNaN(lineHeightValue) ||
125
+ lineHeightValue < 0 ||
126
+ lineHeightValue > 2.5) {
127
+ errors.push('Line height must be a value between 0 and 2.5 inclusive.');
128
+ }
129
+ else {
130
+ // Snap to 0.1 increments to match the UI slider step
131
+ newStyles.lineHeight = String(Math.floor(lineHeightValue * 10) / 10);
132
+ }
133
+ }
134
+ // Validate and normalize letterSpacing: AI sends unitless 0-100, convert to em
135
+ if (newStyles.letterSpacing !== undefined &&
136
+ newStyles.letterSpacing !== null) {
137
+ const letterSpacingStr = String(newStyles.letterSpacing);
138
+ if (/[a-zA-Z]/.test(letterSpacingStr)) {
139
+ errors.push('Letter spacing must be a unitless value between 0 and 100.');
140
+ }
141
+ else {
142
+ const letterSpacingValue = parseFloat(letterSpacingStr);
143
+ if (isNaN(letterSpacingValue) ||
144
+ letterSpacingValue < 0 ||
145
+ letterSpacingValue > 100) {
146
+ errors.push('Letter spacing must be between 0 and 100 inclusive.');
147
+ }
148
+ else {
149
+ newStyles.letterSpacing = letterSpacingValueConvertor.convertBack(String(letterSpacingValue)).letterSpacing;
150
+ }
151
+ }
152
+ }
153
+ // Validate fontSize: must be in 'em' unit and between 0.75em and 8em inclusive
154
+ if (newStyles?.fontSize != null) {
155
+ const fontSizeStr = String(newStyles.fontSize);
156
+ const fontSizeMatch = fontSizeStr.match(/^(-?\d*\.?\d+)em$/);
157
+ if (!fontSizeMatch) {
158
+ errors.push('Font size must be in "em" unit (e.g., "1.5em").');
159
+ }
160
+ else {
161
+ const fontSizeValue = parseFloat(fontSizeMatch[1]);
162
+ if (isNaN(fontSizeValue) || fontSizeValue < 0.75 || fontSizeValue > 8) {
163
+ errors.push('Font size must be between 0.75em and 8em inclusive. If unitless from user always treat as px and convert to em using 16px = 1em ratio.');
164
+ }
165
+ }
166
+ }
167
+ return Promise.resolve({ errors });
168
+ }
169
+ async buildAsync(model) {
170
+ this.log(`Build called`);
171
+ const validation = await this.validateAsync();
172
+ if (validation.errors.length) {
173
+ return {
174
+ request: this.request,
175
+ model,
176
+ errors: validation.errors,
177
+ };
178
+ }
179
+ const { contentId, fieldName, newStyles } = this.request.payload;
180
+ // Filter out undefined values
181
+ const stylesToUpdate = Object.entries(newStyles).reduce((acc, [key, value]) => {
182
+ if (value !== undefined) {
183
+ acc[key] = value;
184
+ }
185
+ return acc;
186
+ }, {});
187
+ let updated = false;
188
+ for (const page of model.pages) {
189
+ for (const content of page.contents) {
190
+ // Use includes to handle potential ID format differences
191
+ if (content.id.includes(contentId) || contentId.includes(content.id)) {
192
+ this.log({
193
+ message: `Content found`,
194
+ model,
195
+ });
196
+ updateTextStyle(content, fieldName, stylesToUpdate, model.styles?.textStyles?.textStyleOptions);
197
+ this.log({
198
+ message: `Successfully updated text style content.`,
199
+ model,
200
+ });
201
+ updated = true;
202
+ break;
203
+ }
204
+ }
205
+ }
206
+ return {
207
+ request: this.request,
208
+ model,
209
+ errors: updated
210
+ ? undefined
211
+ : [
212
+ `Could not update the text style because content with ID "${contentId}" not found.`,
213
+ ],
214
+ };
215
+ }
216
+ }
@@ -1,18 +1,20 @@
1
+ import type { TextStyleDeclaration } from '../../brand-page';
2
+ import type { Editor } from '@tiptap/vue-3';
3
+ export declare const applyStyleToRTEText: (externalEditor: Editor, contentText: string, newContentStyle: TextStyleDeclaration) => string;
1
4
  /**
2
5
  * Converts a Tiptap JSON document (ProseMirror document model) to an HTML
3
- * string via Tiptap's schema-driven static renderer (renderRteToHtml →
4
- * renderToHTMLString). DOM-less and editor-free, so it runs on the server
5
- * (Nitro/Node), not just the browser — unlike the previous headless-Editor
6
- * implementation (BP-5787: the AI section builders no longer need a `document`).
6
+ * string using a headless Tiptap Editor instance.
7
7
  *
8
8
  * Returns the value unchanged if it is already a string.
9
- * Returns an empty string for null/undefined or any non-Tiptap-document value.
9
+ * Returns an empty string for null/undefined.
10
10
  *
11
- * Note: to prevent saved copy being wiped (BP-5669), structural validity of any
12
- * AI-emitted Tiptap doc is checked upstream by `rteSchemaCheck` in
13
- * `section-args-validator.ts` against the editor's real ProseMirror schema. The
14
- * validator surfaces a clear error into `BuilderResult.errors`, which the
15
- * AI-chat retry loop reads via `tool_result.errorMessage` and uses to re-emit a
16
- * corrected payload.
11
+ * Note: Tiptap's `setContent` does NOT throw on malformed input it
12
+ * silently drops content and returns `<p></p>`. To prevent saved copy
13
+ * being wiped (BP-5669), structural validity of any AI-emitted Tiptap
14
+ * doc is checked upstream by `rteSchemaCheck` in
15
+ * `section-args-validator.ts` using the editor's real ProseMirror
16
+ * schema. The validator surfaces a clear error into
17
+ * `BuilderResult.errors`, which the AI-chat retry loop reads via
18
+ * `tool_result.errorMessage` and uses to re-emit a corrected payload.
17
19
  */
18
20
  export declare const tiptapJsonToHtml: (value: unknown) => string;
@@ -1,26 +1,85 @@
1
- import { renderRteToHtml } from '../../brand-page-maker/utils';
2
- import { isTiptapDocument } from '../parsers/section-tool-arguments';
1
+ import { getRTEColor } from '../../brand-page-maker/utils';
2
+ import { useExternalRichTextEditor } from '../../brand-page-maker/composables/rich-text-editor/useRichTextEditor';
3
+ import { getRenderText } from '../../brand-page/utils';
4
+ import { ESCAPED_NEWLINE_CHARACTER } from '../../constants/ui';
5
+ import { isTiptapDocument, } from '../parsers/section-tool-arguments';
6
+ export const applyStyleToRTEText = (externalEditor, contentText, newContentStyle) => {
7
+ if (!contentText) {
8
+ return contentText;
9
+ }
10
+ const content = contentText.includes(ESCAPED_NEWLINE_CHARACTER)
11
+ ? getRenderText(contentText)
12
+ : contentText;
13
+ externalEditor.commands.setContent(content);
14
+ let chain = externalEditor.chain().selectAll();
15
+ if (newContentStyle?.fontFamily) {
16
+ chain = chain.setFontFamily(newContentStyle.fontFamily);
17
+ }
18
+ if (newContentStyle?.fontSize) {
19
+ chain = chain.setFontSize(newContentStyle.fontSize);
20
+ }
21
+ if (newContentStyle?.letterSpacing) {
22
+ chain = chain.setLetterSpacing(newContentStyle.letterSpacing);
23
+ }
24
+ if (newContentStyle?.lineHeight) {
25
+ chain = chain.setLineHeight(newContentStyle.lineHeight);
26
+ }
27
+ if (newContentStyle?.color) {
28
+ chain = chain.setMark('textStyle', {
29
+ color: getRTEColor(newContentStyle.color, 'text'),
30
+ });
31
+ }
32
+ // Handle backgroundColor
33
+ if ('backgroundColor' in (newContentStyle || {})) {
34
+ chain = newContentStyle.backgroundColor
35
+ ? chain.setMark('highlight', {
36
+ color: getRTEColor(newContentStyle.backgroundColor, 'background'),
37
+ })
38
+ : chain.unsetHighlight();
39
+ }
40
+ if (newContentStyle?.textAlign) {
41
+ chain = chain.setTextAlign(newContentStyle.textAlign);
42
+ }
43
+ const hasStrike = newContentStyle?.textDecoration?.includes('line-through');
44
+ const hasUnderline = newContentStyle?.textDecoration?.includes('underline');
45
+ chain = hasStrike ? chain.setStrike() : chain.unsetStrike();
46
+ chain = hasUnderline ? chain.setUnderline() : chain.unsetUnderline();
47
+ chain =
48
+ newContentStyle?.fontWeight === 'bold'
49
+ ? chain.setBold()
50
+ : chain.unsetBold();
51
+ chain =
52
+ newContentStyle?.fontStyle === 'italic'
53
+ ? chain.setItalic()
54
+ : chain.unsetItalic();
55
+ chain.run();
56
+ const html = externalEditor.getHTML();
57
+ return html;
58
+ };
3
59
  /**
4
60
  * Converts a Tiptap JSON document (ProseMirror document model) to an HTML
5
- * string via Tiptap's schema-driven static renderer (renderRteToHtml →
6
- * renderToHTMLString). DOM-less and editor-free, so it runs on the server
7
- * (Nitro/Node), not just the browser — unlike the previous headless-Editor
8
- * implementation (BP-5787: the AI section builders no longer need a `document`).
61
+ * string using a headless Tiptap Editor instance.
9
62
  *
10
63
  * Returns the value unchanged if it is already a string.
11
- * Returns an empty string for null/undefined or any non-Tiptap-document value.
64
+ * Returns an empty string for null/undefined.
12
65
  *
13
- * Note: to prevent saved copy being wiped (BP-5669), structural validity of any
14
- * AI-emitted Tiptap doc is checked upstream by `rteSchemaCheck` in
15
- * `section-args-validator.ts` against the editor's real ProseMirror schema. The
16
- * validator surfaces a clear error into `BuilderResult.errors`, which the
17
- * AI-chat retry loop reads via `tool_result.errorMessage` and uses to re-emit a
18
- * corrected payload.
66
+ * Note: Tiptap's `setContent` does NOT throw on malformed input it
67
+ * silently drops content and returns `<p></p>`. To prevent saved copy
68
+ * being wiped (BP-5669), structural validity of any AI-emitted Tiptap
69
+ * doc is checked upstream by `rteSchemaCheck` in
70
+ * `section-args-validator.ts` using the editor's real ProseMirror
71
+ * schema. The validator surfaces a clear error into
72
+ * `BuilderResult.errors`, which the AI-chat retry loop reads via
73
+ * `tool_result.errorMessage` and uses to re-emit a corrected payload.
19
74
  */
20
75
  export const tiptapJsonToHtml = (value) => {
21
76
  if (typeof value === 'string')
22
77
  return value;
23
78
  if (!isTiptapDocument(value))
24
79
  return '';
25
- return renderRteToHtml(value);
80
+ const { externalEditor } = useExternalRichTextEditor();
81
+ externalEditor.commands.setContent(value);
82
+ const html = externalEditor.getHTML();
83
+ externalEditor.destroy();
84
+ return html;
26
85
  };
@@ -1,6 +1,8 @@
1
1
  import cloneDeep from 'lodash.clonedeep';
2
2
  import {} from '../brand-page/models';
3
3
  import { BUILDER_TYPES, } from './models';
4
+ import { UpdateContentTextBuilder } from './builders/update-content-text-builder';
5
+ import { UpdateContentTextStyleBuilder } from './builders/update-content-text-style-builder';
4
6
  import { BannerSectionBuilder, BookingSectionBuilder, AboutSectionBuilder, ContactFormBuilder, DividerSectionBuilder, TextSectionBuilder, TestimonialSectionBuilder, ImagesSectionBuilder, ShapesSectionBuilder, SocialLinksBuilder, ImageGalleryBuilder, ProfileSectionBuilder, MailingListSectionBuilder, ButtonSectionBuilder, DeleteSectionBuilder, MoveSectionBuilder, } from './builders/section-builders';
5
7
  import { AddPageBuilder } from './builders/add-page-builder';
6
8
  import { RemovePageBuilder } from './builders/remove-page-builder';
@@ -13,6 +15,8 @@ import { HeaderBuilder } from './builders/header-builder';
13
15
  import { UndoBuilder } from './builders/undo-builder';
14
16
  // Lazy initialization to avoid circular dependency issues
15
17
  const getBuilderRegistry = () => ({
18
+ [BUILDER_TYPES.UPDATE_CONTENT_TEXT]: UpdateContentTextBuilder,
19
+ [BUILDER_TYPES.UPDATE_CONTENT_TEXT_STYLE]: UpdateContentTextStyleBuilder,
16
20
  // Section builders (create)
17
21
  [BUILDER_TYPES.CREATE_BANNER_SECTION]: BannerSectionBuilder,
18
22
  [BUILDER_TYPES.CREATE_BOOKING_SECTION]: BookingSectionBuilder,
@@ -2,6 +2,8 @@ import { type BrandPageModel } from '../brand-page/models';
2
2
  import type { EventTracker } from '../brand-page-maker/composables/useEventTracker';
3
3
  import type { Logger } from './logger';
4
4
  export declare const BUILDER_TYPES: {
5
+ readonly UPDATE_CONTENT_TEXT: "update-content-text";
6
+ readonly UPDATE_CONTENT_TEXT_STYLE: "update-content-text-style";
5
7
  readonly CREATE_BANNER_SECTION: "create-banner-section";
6
8
  readonly CREATE_ABOUT_SECTION: "create-about-section";
7
9
  readonly CREATE_CONTACT_FORM: "create-contact-form";
@@ -1,5 +1,7 @@
1
1
  import {} from '../brand-page/models';
2
2
  export const BUILDER_TYPES = {
3
+ UPDATE_CONTENT_TEXT: 'update-content-text',
4
+ UPDATE_CONTENT_TEXT_STYLE: 'update-content-text-style',
3
5
  // Per-section builder types (create)
4
6
  CREATE_BANNER_SECTION: 'create-banner-section',
5
7
  CREATE_ABOUT_SECTION: 'create-about-section',
@@ -2,6 +2,8 @@ import type { AiAssistantToolCall } from '../../clients/ai-assistant-client';
2
2
  import type { FooterDisplayStyle, HeaderDisplayStyle, NavigationDisplayType } from '../../brand-page/constants';
3
3
  import type { SectionType, TextStyleArguments } from './section-tool-arguments';
4
4
  export declare const AI_ASSISTANT_TOOL_NAMES: {
5
+ readonly UPDATE_TEXT: "updateText";
6
+ readonly UPDATE_STYLE: "updateStyle";
5
7
  readonly BUILD_PROFILE_SECTION: "build_profile_section";
6
8
  readonly BUILD_TEXT_SECTION: "build_text_section";
7
9
  readonly BUILD_IMAGE_SECTION: "build_image_section";
@@ -47,6 +49,23 @@ export interface ParsedToolCall {
47
49
  arguments: Record<string, unknown>;
48
50
  usage_type?: 'free' | 'paid';
49
51
  }
52
+ export interface BaseArguments {
53
+ message?: string;
54
+ }
55
+ export interface UpdateTextToolArguments extends BaseArguments {
56
+ contentId: string;
57
+ fieldName: string;
58
+ metadata: {
59
+ text: string;
60
+ };
61
+ }
62
+ export interface UpdateStyleToolArguments extends BaseArguments {
63
+ contentId: string;
64
+ fieldName: string;
65
+ metadata: {
66
+ style: BrandPageCssStyle;
67
+ };
68
+ }
50
69
  export declare const SECTION_TOOL_MAP: Record<string, {
51
70
  sectionType: SectionType;
52
71
  }>;
@@ -1,8 +1,12 @@
1
1
  // Tool names — must match the function names generated by the AI pipeline.
2
+ // Text element tools use camelCase names.
2
3
  // Section tools follow the pattern: build_{sectionType}_section
3
4
  // where sectionType matches the ContentType value (e.g. 'banner-section', 'profile').
4
5
  // The mode ('create' | 'update') is a field in the tool arguments, not the tool name.
5
6
  export const AI_ASSISTANT_TOOL_NAMES = {
7
+ // Text element agent tools
8
+ UPDATE_TEXT: 'updateText',
9
+ UPDATE_STYLE: 'updateStyle',
6
10
  // Website builder agent tools — unified build
7
11
  BUILD_PROFILE_SECTION: 'build_profile_section',
8
12
  BUILD_TEXT_SECTION: 'build_text_section',
@@ -1,13 +1,14 @@
1
1
  import { type SectionCheck } from './section-args-validator';
2
2
  /**
3
- * Validates a Tiptap doc against the real ProseMirror schema
4
- * (`schema.nodeFromJSON`). Catches malformed AI input — stray non-node children
5
- * in `content`, unknown node types, malformed attrs — before the builder runs
6
- * and the renderer's silent `setContent` drops the doc to `<p></p>` (BP-5669).
3
+ * Validates a Tiptap doc against the headless editor's real ProseMirror
4
+ * schema (`schema.nodeFromJSON`). Catches malformed AI input — stray
5
+ * non-node children in `content`, unknown node types, malformed attrs —
6
+ * before the builder runs and the renderer's silent `setContent` drops
7
+ * the doc to `<p></p>` (BP-5669).
7
8
  *
8
- * Returns the field-prefixed error string so the AI's tool-call retry loop reads
9
- * it via `BuilderResult.errors` → `tool_result.errorMessage` and re-emits a
10
- * corrected payload.
9
+ * Returns the field-prefixed error string so the AI's tool-call retry
10
+ * loop reads it via `BuilderResult.errors` → `tool_result.errorMessage`
11
+ * and re-emits a corrected payload.
11
12
  */
12
13
  export declare const rteSchemaCheck: (field: string) => SectionCheck;
13
14
  /**
@@ -1,42 +1,73 @@
1
- import { getSchema } from '@tiptap/core';
2
- import { getRteExtensions } from '../../brand-page-maker/composables/rich-text-editor/getRteExtensions';
1
+ import { Editor } from '@tiptap/vue-3';
2
+ import StarterKit from '@tiptap/starter-kit';
3
+ import Underline from '@tiptap/extension-underline';
4
+ import TextStyle from '@tiptap/extension-text-style';
5
+ import TextAlign from '@tiptap/extension-text-align';
6
+ import FontFamily from '@tiptap/extension-font-family';
7
+ import ColorExtension from '@tiptap/extension-color';
8
+ import Highlight from '@tiptap/extension-highlight';
9
+ import Link from '@tiptap/extension-link';
10
+ import { FontSize } from '../../brand-page-maker/composables/rich-text-editor/extensions/FontSize';
11
+ import { LetterSpacing } from '../../brand-page-maker/composables/rich-text-editor/extensions/LetterSpacing';
12
+ import { LineHeight } from '../../brand-page-maker/composables/rich-text-editor/extensions/LineHeight';
13
+ import { AutoLink } from '../../brand-page-maker/composables/rich-text-editor/extensions/AutoLink';
3
14
  import { isTiptapDocument } from '../parsers/section-tool-arguments';
4
15
  import { rteStyleCheck, textCheck, } from './section-args-validator';
5
16
  /**
6
- * The ProseMirror schema built from the SAME extension set the editor and
7
- * renderer use (`getRteExtensions`), so validation matches real editor output.
8
- * `getSchema` is DOM-less (no Editor instance) and the schema is pure, so it is
9
- * built once and cached.
10
- *
11
- * We import `getRteExtensions` (a leaf module — @tiptap + prosemirror only), not
12
- * the maker composable (`useExternalRichTextEditor`), which would pull in the
13
- * AI executor → section-builders → section-rules graph and re-enter this file
14
- * before its exports are bound.
17
+ * Constructs a headless Tiptap Editor with the same extension set as
18
+ * `useExternalRichTextEditor` in the maker. We import the Tiptap extension
19
+ * files directly (they are leaf modules) instead of going through the maker
20
+ * composable, which would pull in the AI executor → section-builders →
21
+ * section-rules graph and re-enter this file before its exports are bound.
15
22
  */
16
- let cachedSchema;
17
- const getRteSchema = () => (cachedSchema ??= getSchema(getRteExtensions({ includeLinkRel: true })));
23
+ const buildHeadlessEditor = () => new Editor({
24
+ editable: false,
25
+ extensions: [
26
+ StarterKit,
27
+ Underline,
28
+ TextStyle.configure({ mergeNestedSpanStyles: true }),
29
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
30
+ FontFamily.configure({ types: ['textStyle'] }),
31
+ FontSize,
32
+ LetterSpacing,
33
+ LineHeight,
34
+ ColorExtension.configure({ types: ['textStyle'] }),
35
+ Highlight.configure({ multicolor: true }),
36
+ Link.configure({
37
+ openOnClick: false,
38
+ defaultProtocol: 'https',
39
+ autolink: true,
40
+ }),
41
+ AutoLink,
42
+ ],
43
+ });
18
44
  /**
19
- * Validates a Tiptap doc against the real ProseMirror schema
20
- * (`schema.nodeFromJSON`). Catches malformed AI input — stray non-node children
21
- * in `content`, unknown node types, malformed attrs — before the builder runs
22
- * and the renderer's silent `setContent` drops the doc to `<p></p>` (BP-5669).
45
+ * Validates a Tiptap doc against the headless editor's real ProseMirror
46
+ * schema (`schema.nodeFromJSON`). Catches malformed AI input — stray
47
+ * non-node children in `content`, unknown node types, malformed attrs —
48
+ * before the builder runs and the renderer's silent `setContent` drops
49
+ * the doc to `<p></p>` (BP-5669).
23
50
  *
24
- * Returns the field-prefixed error string so the AI's tool-call retry loop reads
25
- * it via `BuilderResult.errors` → `tool_result.errorMessage` and re-emits a
26
- * corrected payload.
51
+ * Returns the field-prefixed error string so the AI's tool-call retry
52
+ * loop reads it via `BuilderResult.errors` → `tool_result.errorMessage`
53
+ * and re-emits a corrected payload.
27
54
  */
28
55
  export const rteSchemaCheck = (field) => (record) => {
29
56
  const value = record[field];
30
57
  if (!isTiptapDocument(value))
31
58
  return null;
59
+ const editor = buildHeadlessEditor();
32
60
  try {
33
- getRteSchema().nodeFromJSON(value);
61
+ editor.schema.nodeFromJSON(value);
34
62
  return null;
35
63
  }
36
64
  catch (err) {
37
65
  const detail = err instanceof Error ? err.message : String(err);
38
66
  return `"${field}" is not valid Tiptap JSON: ${detail}`;
39
67
  }
68
+ finally {
69
+ editor.destroy();
70
+ }
40
71
  };
41
72
  /**
42
73
  * Returns [rteSchemaCheck(field), textCheck(field, max), rteStyleCheck(field)].
@@ -158,20 +158,7 @@ export default defineComponent({
158
158
  },
159
159
  },
160
160
  watch: {
161
- value(newVal, oldVal) {
162
- // BP-5787: RTE values are Tiptap JSON objects. The parent (TextItemsField)
163
- // re-clones its items array (cloneDeep) on ANY change — including a sibling
164
- // field's style sync — yielding a NEW object reference with identical text
165
- // content. For objects Vue fires this watcher on reference change, so that
166
- // spurious re-clone would overwrite `_value` and clobber an in-flight edit
167
- // (a mark applied but not yet debounced to the store), silently dropping
168
- // the user's formatting. Strings never hit this (same content = same
169
- // reference = no fire), so mirror that: ignore re-clones whose content is
170
- // unchanged from the previous prop value.
171
- if (this.showRichTextEditor &&
172
- JSON.stringify(newVal) === JSON.stringify(oldVal)) {
173
- return;
174
- }
161
+ value(newVal) {
175
162
  this._value = newVal;
176
163
  if (this.showRichTextEditor) {
177
164
  const textValue = escapeContentText(this._value);
@@ -8,7 +8,7 @@ import { SIDE_BAR_TAB_NAMES } from '@brand-page-maker/constants';
8
8
  import { ITEM_FIELD_TYPES } from '@brand-page-maker/models';
9
9
  import { useUiPageSettingsStore } from '../../../page-settings/store';
10
10
  import { PAGE_SECTIONS } from '../../../page-settings/models';
11
- import { getPlainTextFromRTE, replaceTextInRTE, RTE_ALLOWED_SECTION_TYPES, isRTEManagedStyleField, isTiptapDocument, renderRteToHtml, toRteJson, } from '@brand-page-maker/utils';
11
+ import { getPlainTextFromRTE, replaceTextInRTE, RTE_ALLOWED_SECTION_TYPES, isRTEManagedStyleField, } from '@brand-page-maker/utils';
12
12
  import { isDefaultContentText } from '@shared/content-settings/utils';
13
13
  import { useUiLogoSettingsStore } from '../../../logo-settings/store';
14
14
  const RTE_DISALLOWED_FIELD_NAMES = ['buttonText'];
@@ -267,17 +267,8 @@ export default defineComponent({
267
267
  },
268
268
  onUseGeneratedText(generatedText) {
269
269
  let formattedText = generatedText;
270
- // BP-5787: preserve the field's formatting when dropping in AI text.
271
- // replaceTextInRTE is HTML-based, so JSON is rendered/parsed around it
272
- // (cold AI path, shared editor); string fields use it directly.
273
- if (this.showRichTextEditor) {
274
- if (isTiptapDocument(this.value)) {
275
- formattedText =
276
- toRteJson(replaceTextInRTE(renderRteToHtml(this.value), generatedText)) ?? generatedText;
277
- }
278
- else if (typeof this.value === 'string') {
279
- formattedText = replaceTextInRTE(this.value, generatedText);
280
- }
270
+ if (this.showRichTextEditor && typeof this.value === 'string') {
271
+ formattedText = replaceTextInRTE(this.value, generatedText);
281
272
  }
282
273
  this.contentSettingsStore.updateItemValue({
283
274
  id: this.id,
@@ -1,5 +1,5 @@
1
1
  import type { PropType } from 'vue';
2
- import type { ColorReplacement, Content, LogoTheme, RichTextValue, TestimonialItems } from '@brand-page';
2
+ import type { ColorReplacement, Content, LogoTheme, TestimonialItems } from '@brand-page';
3
3
  import type { DebounceHandle } from 'lodash.debounce';
4
4
  import type { UpdateItemValuePayload } from '../../models';
5
5
  declare const _default: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
@@ -64,8 +64,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
64
64
  name: string;
65
65
  value: string;
66
66
  }, index: number): void;
67
- getItemTextPreview(text: RichTextValue): string;
68
- rteTextLength(value: RichTextValue): number;
67
+ getItemTextPreview(text: string): string;
69
68
  getPlainTextValue(index: number): string;
70
69
  onCaptureSnapshot(): void;
71
70
  }, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:modelValue" | "capture-preview-snapshot" | "on-intent-image-replace")[], "update:modelValue" | "capture-preview-snapshot" | "on-intent-image-replace", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{