@designcrowd/library.brand-page 6.1.1-json-prereq-3 → 6.1.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 (216) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-hj_ievMc.js → CreatedOnBrandCrowd-de-de-B5B5ThJ6.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-jAOSpfCw.js → CreatedOnBrandCrowd-en-us-a48CaORy.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-DZMVc-y3.js → CreatedOnBrandCrowd-es-es-xpuZ317-.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-DvB54H38.js → CreatedOnBrandCrowd-fr-ca-V-iUMArW.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-DvB54H38.js → CreatedOnBrandCrowd-fr-fr-V-iUMArW.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-bf-x1xhi.js → CreatedOnBrandCrowd-pt-br-DZ0NKSAy.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-bf-x1xhi.js → CreatedOnBrandCrowd-pt-pt-DZ0NKSAy.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-B-NY1B44.js → CreatedOnDcom-de-de-_950cgGp.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-Cd4C6KQf.js → CreatedOnDcom-en-us-DogKzH-g.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-DONlsQsm.js → CreatedOnDcom-es-es-CmGM3IPX.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-Dsv3FRpZ.js → CreatedOnDcom-fr-ca-BonEDkWZ.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-DOgyvY5C.js → CreatedOnDcom-fr-fr-DQHtKrd_.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-DX66MhJS.js → CreatedOnDcom-pt-br-CtYJSBjs.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-COe2G0bU.js → CreatedOnDcom-pt-pt-DoO9r4lA.js} +1 -1
  15. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +7 -8
  16. package/lib/src/brand-page/components/mailing-list/MailingList.mixin.js +6 -4
  17. package/lib/src/brand-page/components/page-footer/PageFooter.mixin.d.ts +1 -0
  18. package/lib/src/brand-page/components/page-footer/PageFooter.mixin.js +4 -1
  19. package/lib/src/brand-page/components/payment/Payment.mixin.js +4 -3
  20. package/lib/src/brand-page/components/payment/__fixtures__/request-payment.d.ts +1 -0
  21. package/lib/src/brand-page/components/payment/__fixtures__/support-me.d.ts +1 -0
  22. package/lib/src/brand-page/components/payment/request-payment/RequestPayment.mixin.d.ts +1 -0
  23. package/lib/src/brand-page/components/payment/request-payment/RequestPayment.mixin.js +4 -1
  24. package/lib/src/brand-page/composables/consent.composable.d.ts +2 -0
  25. package/lib/src/brand-page/composables/consent.composable.js +9 -3
  26. package/lib/src/brand-page/models/brand-page-context.model.d.ts +7 -0
  27. package/lib/src/brand-page/models/index.d.ts +26 -34
  28. package/lib/src/brand-page/utils/context.util.d.ts +8 -0
  29. package/lib/src/brand-page/utils/context.util.js +8 -0
  30. package/lib/src/brand-page/utils/migration.util.js +0 -11
  31. package/lib/src/brand-page/utils/text.util.d.ts +1 -13
  32. package/lib/src/brand-page/utils/text.util.js +0 -64
  33. package/lib/src/brand-page/utils/vcard.util.js +4 -4
  34. package/lib/src/brand-page/utils/view-model.util.d.ts +0 -1
  35. package/lib/src/brand-page/utils/view-model.util.js +20 -68
  36. package/lib/src/brand-page/view-models.d.ts +3 -3
  37. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +1 -1
  38. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.js +51 -18
  39. package/lib/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.d.ts +1 -0
  40. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.d.ts +9 -2
  41. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.js +10 -6
  42. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.d.ts +20 -0
  43. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.js +44 -0
  44. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.d.ts +27 -0
  45. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.js +44 -0
  46. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +1 -1
  47. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +103 -24
  48. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.js +8 -0
  49. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +9 -3
  50. package/lib/src/brand-page-maker/stores/ui/ui-store.d.ts +1 -1
  51. package/lib/src/brand-page-maker/utils/index.d.ts +0 -2
  52. package/lib/src/brand-page-maker/utils/index.js +0 -2
  53. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +5 -28
  54. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +5 -48
  55. package/lib/src/brand-page-maker/validators/field.validator.js +2 -10
  56. package/lib/src/clients/models.d.ts +2 -0
  57. package/lib/src/css/library.brand-page-brandCrowd.min.css +99 -102
  58. package/lib/src/css/library.brand-page-brandPage.min.css +99 -102
  59. package/lib/src/css/library.brand-page-designCom.min.css +99 -102
  60. package/lib/src/css/library.brand-page-preview.min.css +99 -102
  61. package/lib/src/index.d.ts +1 -1
  62. package/lib/src/index.mjs +219 -221
  63. package/lib/src/{lib-scDtk-Pi.js → lib-DtalJKI3.js} +72445 -75920
  64. package/lib/src/lib.js +1 -2
  65. package/lib/src/metadata-builder/ai-metadata-executor.d.ts +1 -0
  66. package/lib/src/metadata-builder/ai-metadata-executor.js +19 -0
  67. package/lib/src/metadata-builder/builders/section-builders/about-section-builder.js +3 -2
  68. package/lib/src/metadata-builder/builders/section-builders/banner-section-builder.js +3 -2
  69. package/lib/src/metadata-builder/builders/section-builders/booking-section-builder.js +3 -2
  70. package/lib/src/metadata-builder/builders/section-builders/contact-form-builder.js +4 -3
  71. package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +3 -2
  72. package/lib/src/metadata-builder/builders/section-builders/mailing-list-builder.js +2 -1
  73. package/lib/src/metadata-builder/builders/section-builders/mailing-list-section-builder.js +2 -1
  74. package/lib/src/metadata-builder/builders/section-builders/profile-section-builder.js +4 -3
  75. package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +3 -2
  76. package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +2 -1
  77. package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +11 -15
  78. package/lib/src/metadata-builder/builders/update-content-text-builder.d.ts +17 -0
  79. package/lib/src/metadata-builder/builders/update-content-text-builder.js +103 -0
  80. package/lib/src/metadata-builder/builders/update-content-text-style-builder.d.ts +16 -0
  81. package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +216 -0
  82. package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +13 -11
  83. package/lib/src/metadata-builder/helpers/rte.helpers.js +73 -14
  84. package/lib/src/metadata-builder/metadata-executor.js +4 -0
  85. package/lib/src/metadata-builder/models.d.ts +2 -0
  86. package/lib/src/metadata-builder/models.js +2 -0
  87. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +19 -0
  88. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +4 -0
  89. package/lib/src/metadata-builder/sanitizers/rte-schema-check.d.ts +8 -7
  90. package/lib/src/metadata-builder/sanitizers/rte-schema-check.js +52 -21
  91. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +1 -1
  92. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +10 -35
  93. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +1 -1
  94. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +3 -12
  95. package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +1 -1
  96. package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +1 -1
  97. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.d.ts +2 -3
  98. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.js +3 -10
  99. package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +1 -1
  100. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +1 -2
  101. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +1 -4
  102. package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +1 -1
  103. package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +1 -1
  104. package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +1 -1
  105. package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.d.ts +1 -1
  106. package/lib/src/shared/logo-versions/LogoVersions.mixin.d.ts +1 -1
  107. package/lib/src/shared/my-designs/MyDesigns.mixin.d.ts +1 -1
  108. package/lib/src/shared/my-uploads/MyMedia.mixin.d.ts +1 -1
  109. package/lib/src/shared/my-uploads/MyUploads.mixin.d.ts +1 -1
  110. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +1 -1
  111. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +1 -1
  112. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +1 -1
  113. package/lib/src/shared/template-settings/components/TemplatesSettings.mixin.d.ts +1 -1
  114. package/lib/src/shared/template-settings/store/template-settings-store.d.ts +2 -0
  115. package/lib/src/shared/template-settings/store/template-settings-store.js +72 -24
  116. package/lib/src/shared/unsplash-library/UnsplashLibrary.mixin.d.ts +1 -1
  117. package/lib/src/utils/minify-brand-page-metadata.js +1 -4
  118. package/package.json +15 -17
  119. package/src/brand-page/components/contact-form/ContactForm.mixin.ts +9 -7
  120. package/src/brand-page/components/cookie-consent/CookieConsent.vue +1 -0
  121. package/src/brand-page/components/mailing-list/MailingList.mixin.ts +8 -3
  122. package/src/brand-page/components/navigation/navigation-carousel/NavigationCarousel.vue +5 -1
  123. package/src/brand-page/components/page-footer/PageFooter.mixin.ts +4 -1
  124. package/src/brand-page/components/page-footer/PageFooter.vue +1 -1
  125. package/src/brand-page/components/payment/Payment.mixin.ts +4 -3
  126. package/src/brand-page/components/payment/request-payment/RequestPayment.mixin.ts +8 -1
  127. package/src/brand-page/components/payment/request-payment/RequestPayment.vue +1 -1
  128. package/src/brand-page/components/save-contact/SaveContactWidget.vue +2 -1
  129. package/src/brand-page/components/shop/Shop.vue +6 -3
  130. package/src/brand-page/components/social-links/SocialLinks.vue +5 -2
  131. package/src/brand-page/components/text-wrapper/TextWrapper.vue +6 -17
  132. package/src/brand-page/composables/consent.composable.ts +18 -4
  133. package/src/brand-page/models/brand-page-context.model.ts +7 -0
  134. package/src/brand-page/models/index.ts +26 -35
  135. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
  136. package/src/brand-page/utils/context.util.test.ts +43 -0
  137. package/src/brand-page/utils/context.util.ts +10 -0
  138. package/src/brand-page/utils/migration.util.ts +0 -12
  139. package/src/brand-page/utils/style.util.test.ts +0 -51
  140. package/src/brand-page/utils/text.util.ts +4 -83
  141. package/src/brand-page/utils/vcard.util.ts +4 -6
  142. package/src/brand-page/utils/view-model.util.ts +26 -94
  143. package/src/brand-page/view-models.ts +2 -3
  144. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +4 -5
  145. package/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.test.ts +135 -0
  146. package/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.ts +62 -21
  147. package/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.ts +1 -0
  148. package/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.ts +10 -6
  149. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.ts +73 -0
  150. package/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.ts +85 -0
  151. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +111 -27
  152. package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.ts +9 -0
  153. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +11 -3
  154. package/src/brand-page-maker/stores/ui/ui-store.ts +1 -1
  155. package/src/brand-page-maker/utils/index.ts +0 -2
  156. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +3 -37
  157. package/src/brand-page-maker/utils/rich-text-editor.util.ts +11 -66
  158. package/src/brand-page-maker/validators/field.validator.ts +2 -10
  159. package/src/clients/models.ts +3 -0
  160. package/src/lib.ts +1 -9
  161. package/src/metadata-builder/ai-metadata-executor.ts +26 -0
  162. package/src/metadata-builder/builders/section-builders/about-section-builder.ts +3 -2
  163. package/src/metadata-builder/builders/section-builders/banner-section-builder.ts +3 -2
  164. package/src/metadata-builder/builders/section-builders/booking-section-builder.ts +5 -2
  165. package/src/metadata-builder/builders/section-builders/contact-form-builder.ts +4 -3
  166. package/src/metadata-builder/builders/section-builders/images-section-builder.ts +3 -2
  167. package/src/metadata-builder/builders/section-builders/mailing-list-builder.ts +2 -1
  168. package/src/metadata-builder/builders/section-builders/mailing-list-section-builder.ts +2 -1
  169. package/src/metadata-builder/builders/section-builders/profile-section-builder.ts +6 -3
  170. package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +3 -2
  171. package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +2 -1
  172. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +13 -19
  173. package/src/metadata-builder/builders/update-content-text-builder.ts +148 -0
  174. package/src/metadata-builder/builders/update-content-text-style-builder.ts +352 -0
  175. package/src/metadata-builder/helpers/rte.helpers.ts +101 -15
  176. package/src/metadata-builder/metadata-executor.ts +5 -0
  177. package/src/metadata-builder/models.ts +3 -0
  178. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +27 -0
  179. package/src/metadata-builder/sanitizers/rte-schema-check.ts +53 -24
  180. package/src/shared/content-settings/components/GenericTextInput.mixin.ts +11 -42
  181. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +3 -16
  182. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.ts +4 -13
  183. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.vue +4 -6
  184. package/src/shared/content-settings/components/text-image-field/TextImageField.vue +3 -6
  185. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +1 -6
  186. package/src/shared/content-settings/components/text-items-field/TextItemsField.vue +1 -1
  187. package/src/shared/template-settings/store/template-settings-store.ts +93 -27
  188. package/src/utils/minify-brand-page-metadata.ts +1 -4
  189. package/lib/src/brand-page/utils/migration/migration-20.d.ts +0 -2
  190. package/lib/src/brand-page/utils/migration/migration-20.js +0 -66
  191. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +0 -22
  192. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +0 -22
  193. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +0 -14
  194. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +0 -39
  195. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
  196. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +0 -117
  197. package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -63
  198. package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -95
  199. package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +0 -6
  200. package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +0 -156
  201. package/src/brand-page/utils/migration/migration-20.server.test.ts +0 -125
  202. package/src/brand-page/utils/migration/migration-20.test.ts +0 -72
  203. package/src/brand-page/utils/migration/migration-20.ts +0 -87
  204. package/src/brand-page/utils/text.util.test.ts +0 -106
  205. package/src/brand-page/utils/view-model.pageid-rte.test.ts +0 -104
  206. package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +0 -23
  207. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +0 -52
  208. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +0 -42
  209. package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +0 -162
  210. package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +0 -202
  211. package/src/brand-page-maker/utils/rte-features.test.ts +0 -378
  212. package/src/brand-page-maker/utils/rte-render-parity.test.ts +0 -164
  213. package/src/brand-page-maker/utils/rte-render.util.ts +0 -165
  214. package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +0 -172
  215. package/src/brand-page-maker/utils/rte-sanitize.util.ts +0 -192
  216. package/src/brand-page-maker/validators/field.validator.test.ts +0 -55
@@ -0,0 +1,352 @@
1
+ import {
2
+ type BrandPageModel,
3
+ type Content,
4
+ type TextStyleDeclaration,
5
+ type TextStyleOptions,
6
+ } from '../../brand-page/models';
7
+ import {
8
+ BaseMetadataBuilder,
9
+ BUILDER_TYPES,
10
+ type BuilderResult,
11
+ type MetadataBuilderRequest,
12
+ } from '../models';
13
+ import { hasRTEContent } from '../../brand-page-maker/utils';
14
+ import { useExternalRichTextEditor } from '../../brand-page-maker/composables/rich-text-editor/useRichTextEditor';
15
+ import type { Logger } from '../logger';
16
+ import { applyStyleToRTEText } from '../helpers/rte.helpers';
17
+ import { letterSpacingValueConvertor } from '../../brand-page-maker/convertors/letter-spacing-value.convertor';
18
+
19
+ export interface UpdateTextStylePayload {
20
+ contentId: string;
21
+ fieldName: string;
22
+ newStyles: TextStyleDeclaration;
23
+ }
24
+
25
+ export const buildUpdateTextStyleRequest = (
26
+ id: string,
27
+ payload: UpdateTextStylePayload,
28
+ ): MetadataBuilderRequest<UpdateTextStylePayload> => {
29
+ return {
30
+ id,
31
+ type: BUILDER_TYPES.UPDATE_CONTENT_TEXT_STYLE,
32
+ payload,
33
+ };
34
+ };
35
+
36
+ const mergeStyles = (
37
+ currentStyle: Partial<TextStyleDeclaration>,
38
+ stylesToUpdate: TextStyleDeclaration,
39
+ textStyleOptions?: TextStyleOptions,
40
+ ): Partial<TextStyleDeclaration> => {
41
+ let merged = { ...currentStyle };
42
+
43
+ // If content uses a textStyleType preset (e.g. "titles"), resolve the preset
44
+ // styles into the style object before clearing the preset reference.
45
+ // This ensures properties like fontSize, fontFamily etc. are preserved
46
+ // when the AI only changes a single property like color.
47
+ if (merged.textStyleType && textStyleOptions?.[merged.textStyleType]) {
48
+ const presetStyle = textStyleOptions[merged.textStyleType].style;
49
+
50
+ merged = { ...merged, ...presetStyle };
51
+ merged.textStyleType = undefined;
52
+ }
53
+
54
+ Object.entries(stylesToUpdate).forEach(([key, value]) => {
55
+ if (value === null) {
56
+ // backgroundColor: null means "remove background color"
57
+ if (key === 'backgroundColor') {
58
+ merged.backgroundColor = '';
59
+ }
60
+
61
+ // All other null values mean "not changed" — skip to preserve existing values
62
+ return;
63
+ }
64
+
65
+ (merged as Record<string, unknown>)[key] = value;
66
+ });
67
+
68
+ return merged;
69
+ };
70
+
71
+ const updateRTEText = (text: string, style: TextStyleDeclaration): string => {
72
+ if (!text || typeof text !== 'string' || !hasRTEContent(text)) {
73
+ return text;
74
+ }
75
+
76
+ const { externalEditor } = useExternalRichTextEditor();
77
+ const updatedText = applyStyleToRTEText(externalEditor, text, style);
78
+
79
+ externalEditor.destroy();
80
+
81
+ return updatedText;
82
+ };
83
+
84
+ const findStyleKey = (target: Content, property: string): string | null => {
85
+ // Try exact match first (e.g., 'testimonialsStyle')
86
+ const exactKey = `${property}Style`;
87
+
88
+ if (exactKey in target) return exactKey;
89
+
90
+ // Try singular form (e.g., 'testimonialStyle' for 'testimonials')
91
+ if (property.endsWith('s')) {
92
+ const singularKey = `${property.slice(0, -1)}Style`;
93
+
94
+ if (singularKey in target) return singularKey;
95
+ }
96
+
97
+ // Try plural form (edge case)
98
+ const pluralKey = `${property}sStyle`;
99
+
100
+ if (pluralKey in target) return pluralKey;
101
+
102
+ return exactKey; // fallback to exact match
103
+ };
104
+
105
+ const updateSharedArrayStyle = (
106
+ content: Content,
107
+ property: string,
108
+ stylesToUpdate: TextStyleDeclaration,
109
+ textStyleOptions?: TextStyleOptions,
110
+ ) => {
111
+ const styleKey = findStyleKey(content, property);
112
+
113
+ if (!styleKey) return;
114
+
115
+ const currentStyle = content[styleKey] || {};
116
+ const mergedStyle = mergeStyles(
117
+ currentStyle,
118
+ stylesToUpdate,
119
+ textStyleOptions,
120
+ );
121
+
122
+ content[styleKey] = mergedStyle;
123
+ };
124
+
125
+ const updateArrayItemStyle = (
126
+ content: Content,
127
+ fieldName: string,
128
+ index: number,
129
+ property: string,
130
+ stylesToUpdate: TextStyleDeclaration,
131
+ textStyleOptions?: TextStyleOptions,
132
+ ) => {
133
+ const currentContentItems = content[fieldName];
134
+
135
+ if (!Array.isArray(currentContentItems) || !currentContentItems[+index])
136
+ return;
137
+
138
+ const contentItem = currentContentItems[+index];
139
+ const styleKey = `${property}Style`;
140
+ const currentStyle =
141
+ (contentItem[styleKey] as Partial<TextStyleDeclaration>) || {};
142
+ const mergedStyle = mergeStyles(
143
+ currentStyle,
144
+ stylesToUpdate,
145
+ textStyleOptions,
146
+ );
147
+
148
+ const newContentItems = [...currentContentItems];
149
+
150
+ newContentItems[+index] = {
151
+ ...newContentItems[+index],
152
+ [styleKey]: mergedStyle,
153
+ [property]: updateRTEText(contentItem[property], mergedStyle),
154
+ };
155
+ content[fieldName] = newContentItems;
156
+ };
157
+
158
+ const updateTextStyle = (
159
+ content: Content,
160
+ fieldName: string,
161
+ stylesToUpdate: TextStyleDeclaration,
162
+ textStyleOptions?: TextStyleOptions,
163
+ ) => {
164
+ // Check if fieldName is in the format of "fieldName.index.fieldItem"
165
+ const matchFieldItems = fieldName.match(/^(\w+)\.(\d+)\.(.+)$/);
166
+
167
+ if (matchFieldItems) {
168
+ const [, fieldName, index, fieldItem] = matchFieldItems;
169
+ const array = content[fieldName];
170
+
171
+ if (!Array.isArray(array) || !array[+index]) return;
172
+
173
+ const item = array[+index];
174
+ const itemStyleKey = `${fieldItem}Style`;
175
+ const hasItemStyle = itemStyleKey in item;
176
+
177
+ if (hasItemStyle) {
178
+ updateArrayItemStyle(
179
+ content,
180
+ fieldName,
181
+ +index,
182
+ fieldItem,
183
+ stylesToUpdate,
184
+ textStyleOptions,
185
+ );
186
+ } else {
187
+ updateSharedArrayStyle(
188
+ content,
189
+ fieldItem,
190
+ stylesToUpdate,
191
+ textStyleOptions,
192
+ );
193
+ }
194
+ } else {
195
+ const styleKey = `${fieldName}Style`;
196
+ const currentStyle =
197
+ (content[styleKey] as Partial<TextStyleDeclaration>) || {};
198
+ const mergedStyle = mergeStyles(
199
+ currentStyle,
200
+ stylesToUpdate,
201
+ textStyleOptions,
202
+ );
203
+
204
+ content[styleKey] = mergedStyle;
205
+ content[fieldName] = updateRTEText(content[fieldName], mergedStyle);
206
+ }
207
+ };
208
+
209
+ export class UpdateContentTextStyleBuilder extends BaseMetadataBuilder<UpdateTextStylePayload> {
210
+ constructor(
211
+ request: MetadataBuilderRequest<UpdateTextStylePayload>,
212
+ logger?: Logger,
213
+ ) {
214
+ super(request, logger);
215
+ }
216
+
217
+ validateAsync(): Promise<{ errors: string[] }> {
218
+ const { newStyles } = this.request.payload!;
219
+ const errors: string[] = [];
220
+
221
+ // Validate lineHeight: must be between 0 and 2.5 inclusive, snap to 0.1 step
222
+ if (newStyles.lineHeight !== undefined && newStyles.lineHeight !== null) {
223
+ const lineHeightValue = parseFloat(String(newStyles.lineHeight));
224
+
225
+ if (
226
+ isNaN(lineHeightValue) ||
227
+ lineHeightValue < 0 ||
228
+ lineHeightValue > 2.5
229
+ ) {
230
+ errors.push('Line height must be a value between 0 and 2.5 inclusive.');
231
+ } else {
232
+ // Snap to 0.1 increments to match the UI slider step
233
+ newStyles.lineHeight = String(Math.floor(lineHeightValue * 10) / 10);
234
+ }
235
+ }
236
+
237
+ // Validate and normalize letterSpacing: AI sends unitless 0-100, convert to em
238
+ if (
239
+ newStyles.letterSpacing !== undefined &&
240
+ newStyles.letterSpacing !== null
241
+ ) {
242
+ const letterSpacingStr = String(newStyles.letterSpacing);
243
+
244
+ if (/[a-zA-Z]/.test(letterSpacingStr)) {
245
+ errors.push(
246
+ 'Letter spacing must be a unitless value between 0 and 100.',
247
+ );
248
+ } else {
249
+ const letterSpacingValue = parseFloat(letterSpacingStr);
250
+
251
+ if (
252
+ isNaN(letterSpacingValue) ||
253
+ letterSpacingValue < 0 ||
254
+ letterSpacingValue > 100
255
+ ) {
256
+ errors.push('Letter spacing must be between 0 and 100 inclusive.');
257
+ } else {
258
+ newStyles.letterSpacing = letterSpacingValueConvertor.convertBack(
259
+ String(letterSpacingValue),
260
+ ).letterSpacing;
261
+ }
262
+ }
263
+ }
264
+
265
+ // Validate fontSize: must be in 'em' unit and between 0.75em and 8em inclusive
266
+ if (newStyles?.fontSize != null) {
267
+ const fontSizeStr = String(newStyles.fontSize);
268
+ const fontSizeMatch = fontSizeStr.match(/^(-?\d*\.?\d+)em$/);
269
+
270
+ if (!fontSizeMatch) {
271
+ errors.push('Font size must be in "em" unit (e.g., "1.5em").');
272
+ } else {
273
+ const fontSizeValue = parseFloat(fontSizeMatch[1]);
274
+
275
+ if (isNaN(fontSizeValue) || fontSizeValue < 0.75 || fontSizeValue > 8) {
276
+ errors.push(
277
+ '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.',
278
+ );
279
+ }
280
+ }
281
+ }
282
+
283
+ return Promise.resolve({ errors });
284
+ }
285
+
286
+ async buildAsync(model: BrandPageModel): Promise<BuilderResult> {
287
+ this.log(`Build called`);
288
+
289
+ const validation = await this.validateAsync();
290
+
291
+ if (validation.errors.length) {
292
+ return {
293
+ request: this.request,
294
+ model,
295
+ errors: validation.errors,
296
+ };
297
+ }
298
+
299
+ const { contentId, fieldName, newStyles } = this.request.payload!;
300
+
301
+ // Filter out undefined values
302
+ const stylesToUpdate: TextStyleDeclaration = Object.entries(
303
+ newStyles,
304
+ ).reduce((acc, [key, value]) => {
305
+ if (value !== undefined) {
306
+ acc[key as keyof TextStyleDeclaration] = value;
307
+ }
308
+
309
+ return acc;
310
+ }, {} as TextStyleDeclaration);
311
+
312
+ let updated = false;
313
+
314
+ for (const page of model.pages) {
315
+ for (const content of page.contents) {
316
+ // Use includes to handle potential ID format differences
317
+ if (content.id.includes(contentId) || contentId.includes(content.id)) {
318
+ this.log({
319
+ message: `Content found`,
320
+ model,
321
+ });
322
+
323
+ updateTextStyle(
324
+ content,
325
+ fieldName,
326
+ stylesToUpdate,
327
+ model.styles?.textStyles?.textStyleOptions,
328
+ );
329
+
330
+ this.log({
331
+ message: `Successfully updated text style content.`,
332
+ model,
333
+ });
334
+
335
+ updated = true;
336
+
337
+ break;
338
+ }
339
+ }
340
+ }
341
+
342
+ return {
343
+ request: this.request,
344
+ model,
345
+ errors: updated
346
+ ? undefined
347
+ : [
348
+ `Could not update the text style because content with ID "${contentId}" not found.`,
349
+ ],
350
+ };
351
+ }
352
+ }
@@ -1,28 +1,114 @@
1
- import type { RichTextValue } from '../../brand-page';
2
- import { renderRteToHtml } from '../../brand-page-maker/utils';
3
- import { isTiptapDocument } from '../parsers/section-tool-arguments';
1
+ import type { TextStyleDeclaration } from '../../brand-page';
2
+ import { getRTEColor } from '../../brand-page-maker/utils';
3
+ import { useExternalRichTextEditor } from '../../brand-page-maker/composables/rich-text-editor/useRichTextEditor';
4
+ import { getRenderText } from '../../brand-page/utils';
5
+ import { ESCAPED_NEWLINE_CHARACTER } from '../../constants/ui';
6
+ import type { Editor } from '@tiptap/vue-3';
7
+ import {
8
+ isTiptapDocument,
9
+ type TiptapDocument,
10
+ } from '../parsers/section-tool-arguments';
11
+
12
+ export const applyStyleToRTEText = (
13
+ externalEditor: Editor,
14
+ contentText: string,
15
+ newContentStyle: TextStyleDeclaration,
16
+ ) => {
17
+ if (!contentText) {
18
+ return contentText;
19
+ }
20
+
21
+ const content = contentText.includes(ESCAPED_NEWLINE_CHARACTER)
22
+ ? getRenderText(contentText)
23
+ : contentText;
24
+
25
+ externalEditor.commands.setContent(content);
26
+
27
+ let chain = externalEditor.chain().selectAll();
28
+
29
+ if (newContentStyle?.fontFamily) {
30
+ chain = chain.setFontFamily(newContentStyle.fontFamily);
31
+ }
32
+
33
+ if (newContentStyle?.fontSize) {
34
+ chain = chain.setFontSize(newContentStyle.fontSize);
35
+ }
36
+
37
+ if (newContentStyle?.letterSpacing) {
38
+ chain = chain.setLetterSpacing(newContentStyle.letterSpacing);
39
+ }
40
+
41
+ if (newContentStyle?.lineHeight) {
42
+ chain = chain.setLineHeight(newContentStyle.lineHeight);
43
+ }
44
+
45
+ if (newContentStyle?.color) {
46
+ chain = chain.setMark('textStyle', {
47
+ color: getRTEColor(newContentStyle.color, 'text'),
48
+ });
49
+ }
50
+
51
+ // Handle backgroundColor
52
+ if ('backgroundColor' in (newContentStyle || {})) {
53
+ chain = newContentStyle.backgroundColor
54
+ ? chain.setMark('highlight', {
55
+ color: getRTEColor(newContentStyle.backgroundColor, 'background'),
56
+ })
57
+ : chain.unsetHighlight();
58
+ }
59
+
60
+ if (newContentStyle?.textAlign) {
61
+ chain = chain.setTextAlign(newContentStyle.textAlign);
62
+ }
63
+
64
+ const hasStrike = newContentStyle?.textDecoration?.includes('line-through');
65
+ const hasUnderline = newContentStyle?.textDecoration?.includes('underline');
66
+
67
+ chain = hasStrike ? chain.setStrike() : chain.unsetStrike();
68
+ chain = hasUnderline ? chain.setUnderline() : chain.unsetUnderline();
69
+
70
+ chain =
71
+ newContentStyle?.fontWeight === 'bold'
72
+ ? chain.setBold()
73
+ : chain.unsetBold();
74
+ chain =
75
+ newContentStyle?.fontStyle === 'italic'
76
+ ? chain.setItalic()
77
+ : chain.unsetItalic();
78
+
79
+ chain.run();
80
+ const html = externalEditor.getHTML();
81
+
82
+ return html;
83
+ };
4
84
 
5
85
  /**
6
86
  * Converts a Tiptap JSON document (ProseMirror document model) to an HTML
7
- * string via Tiptap's schema-driven static renderer (renderRteToHtml →
8
- * renderToHTMLString). DOM-less and editor-free, so it runs on the server
9
- * (Nitro/Node), not just the browser — unlike the previous headless-Editor
10
- * implementation (BP-5787: the AI section builders no longer need a `document`).
87
+ * string using a headless Tiptap Editor instance.
11
88
  *
12
89
  * Returns the value unchanged if it is already a string.
13
- * Returns an empty string for null/undefined or any non-Tiptap-document value.
90
+ * Returns an empty string for null/undefined.
14
91
  *
15
- * Note: to prevent saved copy being wiped (BP-5669), structural validity of any
16
- * AI-emitted Tiptap doc is checked upstream by `rteSchemaCheck` in
17
- * `section-args-validator.ts` against the editor's real ProseMirror schema. The
18
- * validator surfaces a clear error into `BuilderResult.errors`, which the
19
- * AI-chat retry loop reads via `tool_result.errorMessage` and uses to re-emit a
20
- * corrected payload.
92
+ * Note: Tiptap's `setContent` does NOT throw on malformed input — it
93
+ * silently drops content and returns `<p></p>`. To prevent saved copy
94
+ * being wiped (BP-5669), structural validity of any AI-emitted Tiptap
95
+ * doc is checked upstream by `rteSchemaCheck` in
96
+ * `section-args-validator.ts` using the editor's real ProseMirror
97
+ * schema. The validator surfaces a clear error into
98
+ * `BuilderResult.errors`, which the AI-chat retry loop reads via
99
+ * `tool_result.errorMessage` and uses to re-emit a corrected payload.
21
100
  */
22
101
  export const tiptapJsonToHtml = (value: unknown): string => {
23
102
  if (typeof value === 'string') return value;
24
103
 
25
104
  if (!isTiptapDocument(value)) return '';
26
105
 
27
- return renderRteToHtml(value as RichTextValue);
106
+ const { externalEditor } = useExternalRichTextEditor();
107
+
108
+ externalEditor.commands.setContent(value as TiptapDocument);
109
+ const html = externalEditor.getHTML();
110
+
111
+ externalEditor.destroy();
112
+
113
+ return html;
28
114
  };
@@ -8,6 +8,8 @@ import {
8
8
  BUILDER_TYPES,
9
9
  type BuilderResult,
10
10
  } from './models';
11
+ import { UpdateContentTextBuilder } from './builders/update-content-text-builder';
12
+ import { UpdateContentTextStyleBuilder } from './builders/update-content-text-style-builder';
11
13
  import {
12
14
  BannerSectionBuilder,
13
15
  BookingSectionBuilder,
@@ -46,6 +48,9 @@ type BuilderConstructor = new (
46
48
 
47
49
  // Lazy initialization to avoid circular dependency issues
48
50
  const getBuilderRegistry = (): Record<BuilderType, BuilderConstructor> => ({
51
+ [BUILDER_TYPES.UPDATE_CONTENT_TEXT]: UpdateContentTextBuilder,
52
+ [BUILDER_TYPES.UPDATE_CONTENT_TEXT_STYLE]: UpdateContentTextStyleBuilder,
53
+
49
54
  // Section builders (create)
50
55
  [BUILDER_TYPES.CREATE_BANNER_SECTION]: BannerSectionBuilder,
51
56
  [BUILDER_TYPES.CREATE_BOOKING_SECTION]: BookingSectionBuilder,
@@ -3,6 +3,9 @@ import type { EventTracker } from '../brand-page-maker/composables/useEventTrack
3
3
  import type { Logger } from './logger';
4
4
 
5
5
  export const BUILDER_TYPES = {
6
+ UPDATE_CONTENT_TEXT: 'update-content-text',
7
+ UPDATE_CONTENT_TEXT_STYLE: 'update-content-text-style',
8
+
6
9
  // Per-section builder types (create)
7
10
  CREATE_BANNER_SECTION: 'create-banner-section',
8
11
  CREATE_ABOUT_SECTION: 'create-about-section',
@@ -7,10 +7,15 @@ import type {
7
7
  import type { SectionType, TextStyleArguments } from './section-tool-arguments';
8
8
 
9
9
  // Tool names — must match the function names generated by the AI pipeline.
10
+ // Text element tools use camelCase names.
10
11
  // Section tools follow the pattern: build_{sectionType}_section
11
12
  // where sectionType matches the ContentType value (e.g. 'banner-section', 'profile').
12
13
  // The mode ('create' | 'update') is a field in the tool arguments, not the tool name.
13
14
  export const AI_ASSISTANT_TOOL_NAMES = {
15
+ // Text element agent tools
16
+ UPDATE_TEXT: 'updateText',
17
+ UPDATE_STYLE: 'updateStyle',
18
+
14
19
  // Website builder agent tools — unified build
15
20
  BUILD_PROFILE_SECTION: 'build_profile_section',
16
21
  BUILD_TEXT_SECTION: 'build_text_section',
@@ -79,6 +84,28 @@ export interface ParsedToolCall {
79
84
  usage_type?: 'free' | 'paid';
80
85
  }
81
86
 
87
+ export interface BaseArguments {
88
+ message?: string;
89
+ }
90
+
91
+ // updateText tool arguments
92
+ export interface UpdateTextToolArguments extends BaseArguments {
93
+ contentId: string;
94
+ fieldName: string;
95
+ metadata: {
96
+ text: string;
97
+ };
98
+ }
99
+
100
+ // updateStyle tool arguments
101
+ export interface UpdateStyleToolArguments extends BaseArguments {
102
+ contentId: string;
103
+ fieldName: string;
104
+ metadata: {
105
+ style: BrandPageCssStyle;
106
+ };
107
+ }
108
+
82
109
  // Maps each section tool name to its SectionType.
83
110
  // Mode is now determined from the tool arguments, not the tool name.
84
111
  export const SECTION_TOOL_MAP: Record<string, { sectionType: SectionType }> = {
@@ -1,6 +1,16 @@
1
- import { getSchema } from '@tiptap/core';
2
- import type { Schema } from '@tiptap/pm/model';
3
- import { getRteExtensions } from '../../brand-page-maker/composables/rich-text-editor/getRteExtensions';
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';
4
14
  import { isTiptapDocument } from '../parsers/section-tool-arguments';
5
15
  import {
6
16
  rteStyleCheck,
@@ -9,30 +19,45 @@ import {
9
19
  } from './section-args-validator';
10
20
 
11
21
  /**
12
- * The ProseMirror schema built from the SAME extension set the editor and
13
- * renderer use (`getRteExtensions`), so validation matches real editor output.
14
- * `getSchema` is DOM-less (no Editor instance) and the schema is pure, so it is
15
- * built once and cached.
16
- *
17
- * We import `getRteExtensions` (a leaf module — @tiptap + prosemirror only), not
18
- * the maker composable (`useExternalRichTextEditor`), which would pull in the
19
- * AI executor → section-builders → section-rules graph and re-enter this file
20
- * before its exports are bound.
22
+ * Constructs a headless Tiptap Editor with the same extension set as
23
+ * `useExternalRichTextEditor` in the maker. We import the Tiptap extension
24
+ * files directly (they are leaf modules) instead of going through the maker
25
+ * composable, which would pull in the AI executor → section-builders →
26
+ * section-rules graph and re-enter this file before its exports are bound.
21
27
  */
22
- let cachedSchema: Schema | undefined;
23
-
24
- const getRteSchema = (): Schema =>
25
- (cachedSchema ??= getSchema(getRteExtensions()));
28
+ const buildHeadlessEditor = (): Editor =>
29
+ new Editor({
30
+ editable: false,
31
+ extensions: [
32
+ StarterKit,
33
+ Underline,
34
+ TextStyle.configure({ mergeNestedSpanStyles: true }),
35
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
36
+ FontFamily.configure({ types: ['textStyle'] }),
37
+ FontSize,
38
+ LetterSpacing,
39
+ LineHeight,
40
+ ColorExtension.configure({ types: ['textStyle'] }),
41
+ Highlight.configure({ multicolor: true }),
42
+ Link.configure({
43
+ openOnClick: false,
44
+ defaultProtocol: 'https',
45
+ autolink: true,
46
+ }),
47
+ AutoLink,
48
+ ],
49
+ });
26
50
 
27
51
  /**
28
- * Validates a Tiptap doc against the real ProseMirror schema
29
- * (`schema.nodeFromJSON`). Catches malformed AI input — stray non-node children
30
- * in `content`, unknown node types, malformed attrs — before the builder runs
31
- * and the renderer's silent `setContent` drops the doc to `<p></p>` (BP-5669).
52
+ * Validates a Tiptap doc against the headless editor's real ProseMirror
53
+ * schema (`schema.nodeFromJSON`). Catches malformed AI input — stray
54
+ * non-node children in `content`, unknown node types, malformed attrs —
55
+ * before the builder runs and the renderer's silent `setContent` drops
56
+ * the doc to `<p></p>` (BP-5669).
32
57
  *
33
- * Returns the field-prefixed error string so the AI's tool-call retry loop reads
34
- * it via `BuilderResult.errors` → `tool_result.errorMessage` and re-emits a
35
- * corrected payload.
58
+ * Returns the field-prefixed error string so the AI's tool-call retry
59
+ * loop reads it via `BuilderResult.errors` → `tool_result.errorMessage`
60
+ * and re-emits a corrected payload.
36
61
  */
37
62
  export const rteSchemaCheck =
38
63
  (field: string): SectionCheck =>
@@ -41,14 +66,18 @@ export const rteSchemaCheck =
41
66
 
42
67
  if (!isTiptapDocument(value)) return null;
43
68
 
69
+ const editor = buildHeadlessEditor();
70
+
44
71
  try {
45
- getRteSchema().nodeFromJSON(value);
72
+ editor.schema.nodeFromJSON(value);
46
73
 
47
74
  return null;
48
75
  } catch (err) {
49
76
  const detail = err instanceof Error ? err.message : String(err);
50
77
 
51
78
  return `"${field}" is not valid Tiptap JSON: ${detail}`;
79
+ } finally {
80
+ editor.destroy();
52
81
  }
53
82
  };
54
83