@designcrowd/library.brand-page 6.0.26 → 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 (60) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-DU6YEVcW.js → CreatedOnBrandCrowd-de-de-BpFKOKIo.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-DNWWK6Tj.js → CreatedOnBrandCrowd-en-us-USGAnC_n.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-DVFvV9Qp.js → CreatedOnBrandCrowd-es-es-CvVx3h4H.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-B4ZkjNdw.js → CreatedOnBrandCrowd-fr-ca-BCwamW5a.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-B4ZkjNdw.js → CreatedOnBrandCrowd-fr-fr-BCwamW5a.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-DP7AQGgW.js → CreatedOnBrandCrowd-pt-br-Bt72hMrQ.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-DP7AQGgW.js → CreatedOnBrandCrowd-pt-pt-Bt72hMrQ.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-CaC3GtG9.js → CreatedOnDcom-de-de-BxUDrgG9.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-D16yT0-P.js → CreatedOnDcom-en-us-wjVp5eOs.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-DA5sHgD_.js → CreatedOnDcom-es-es-CwFL2q5h.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-CDv2SG1-.js → CreatedOnDcom-fr-ca-BPZ2_PNq.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-Cygc7c5d.js → CreatedOnDcom-fr-fr-D0vLOikZ.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-B_3RIzgF.js → CreatedOnDcom-pt-br-CTwbxm9H.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-Bz5w-rrD.js → CreatedOnDcom-pt-pt-Cfqf8LaM.js} +1 -1
  15. package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +7 -3
  16. package/lib/src/clients/ai-assistant-client.d.ts +1 -0
  17. package/lib/src/clients/ai-assistant-client.js +1 -0
  18. package/lib/src/index.mjs +1 -1
  19. package/lib/src/{lib-6_P6kXPu.js → lib-Cg4BExY4.js} +15537 -15375
  20. package/lib/src/metadata-builder/builders/global-font-propagation.d.ts +28 -0
  21. package/lib/src/metadata-builder/builders/global-font-propagation.js +125 -0
  22. package/lib/src/metadata-builder/builders/global-styles-builder.js +16 -0
  23. package/lib/src/metadata-builder/builders/section-builders/index.d.ts +1 -0
  24. package/lib/src/metadata-builder/builders/section-builders/index.js +1 -0
  25. package/lib/src/metadata-builder/builders/section-builders/support-me-builder.d.ts +8 -0
  26. package/lib/src/metadata-builder/builders/section-builders/support-me-builder.js +25 -0
  27. package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +5 -2
  28. package/lib/src/metadata-builder/builders/section-field-maps.js +15 -0
  29. package/lib/src/metadata-builder/builders/section-field-utils.d.ts +6 -1
  30. package/lib/src/metadata-builder/builders/section-field-utils.js +48 -12
  31. package/lib/src/metadata-builder/builders/section-rules/support-me.rules.d.ts +2 -0
  32. package/lib/src/metadata-builder/builders/section-rules/support-me.rules.js +17 -0
  33. package/lib/src/metadata-builder/metadata-executor.js +3 -1
  34. package/lib/src/metadata-builder/models.d.ts +2 -0
  35. package/lib/src/metadata-builder/models.js +2 -0
  36. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +1 -0
  37. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +4 -0
  38. package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +14 -2
  39. package/lib/src/utils/minify-brand-page-metadata.js +7 -0
  40. package/package.json +1 -1
  41. package/src/brand-page-maker/components/ai-website-builder/AiWebsiteBuilder.vue +1 -1
  42. package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +7 -3
  43. package/src/clients/ai-assistant-client.ts +1 -0
  44. package/src/metadata-builder/builders/global-font-propagation.test.ts +269 -0
  45. package/src/metadata-builder/builders/global-font-propagation.ts +205 -0
  46. package/src/metadata-builder/builders/global-styles-builder.ts +21 -0
  47. package/src/metadata-builder/builders/section-builders/index.ts +1 -0
  48. package/src/metadata-builder/builders/section-builders/support-me-builder.test.ts +177 -0
  49. package/src/metadata-builder/builders/section-builders/support-me-builder.ts +43 -0
  50. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +5 -2
  51. package/src/metadata-builder/builders/section-field-maps.ts +18 -0
  52. package/src/metadata-builder/builders/section-field-utils.test.ts +83 -0
  53. package/src/metadata-builder/builders/section-field-utils.ts +69 -18
  54. package/src/metadata-builder/builders/section-rules/support-me.rules.ts +39 -0
  55. package/src/metadata-builder/metadata-executor.ts +3 -0
  56. package/src/metadata-builder/models.ts +2 -0
  57. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +4 -0
  58. package/src/metadata-builder/parsers/section-tool-arguments.ts +14 -1
  59. package/src/utils/minify-brand-page-metadata.test.ts +61 -0
  60. package/src/utils/minify-brand-page-metadata.ts +8 -0
@@ -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,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';
@@ -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
+ }
@@ -131,14 +131,17 @@ export class TextSectionBuilder extends BaseSectionBuilder {
131
131
  applyTextItems(content, args.items, textStyleOptions);
132
132
  }
133
133
  else if (this.mode === 'create') {
134
- 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) {
135
138
  applyTextItems(content, args.items, textStyleOptions);
136
139
  }
137
140
  else {
138
141
  applySingleTextField(content, args, textStyleOptions);
139
142
  }
140
143
  }
141
- else if ((args.items && Array.isArray(args.items)) ||
144
+ else if ((args.items && Array.isArray(args.items) && args.items.length > 0) ||
142
145
  args._remove_item_ids?.length) {
143
146
  // Update: id-based add / edit / move / remove in a single pass.
144
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,
@@ -53,5 +53,10 @@ export declare const resolveGlobalPresetIntoStyle: (style: object | undefined, t
53
53
  * updates without re-resolving a preset.
54
54
  */
55
55
  export declare const applyDetachedStyleOverride: <T extends object>(current: T | undefined, updates: Partial<T>, textStyleOptions: TextStyleOptions | undefined) => T;
56
- export declare const applyStyleFields: (content: Content, args: SectionToolArguments, sectionType: string, textStyleOptions?: TextStyleOptions) => void;
56
+ /**
57
+ * Rewrites the stored RTE HTML's inline colour marks. Injectable for tests;
58
+ * the default uses the external Tiptap editor (font-propagation precedent).
59
+ */
60
+ export type ApplyColorToRTE = (html: string, color: string) => string;
61
+ export declare const applyStyleFields: (content: Content, args: SectionToolArguments, sectionType: string, textStyleOptions?: TextStyleOptions, applyColor?: ApplyColorToRTE) => void;
57
62
  export {};
@@ -1,5 +1,8 @@
1
1
  import { BUTTON_LINK_TYPE } from '../../brand-page/models';
2
2
  import { isTiptapDocument, } from '../parsers/section-tool-arguments';
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';
3
6
  import { getSectionFieldMap, getStyleFieldMap } from './section-field-maps';
4
7
  const isUrlField = (aiKey) => aiKey.endsWith('_url');
5
8
  export const isValidUrl = (value) => {
@@ -117,22 +120,55 @@ export const applyDetachedStyleOverride = (current, updates, textStyleOptions) =
117
120
  ...resolveGlobalPresetIntoStyle(current, textStyleOptions),
118
121
  ...updates,
119
122
  });
120
- export const applyStyleFields = (content, args, sectionType, textStyleOptions) => {
123
+ export const applyStyleFields = (content, args, sectionType, textStyleOptions, applyColor) => {
121
124
  const styleMap = getStyleFieldMap(sectionType);
122
125
  if (!styleMap)
123
126
  return;
124
127
  const record = content;
125
- for (const [aiKey, modelKey] of Object.entries(styleMap)) {
126
- if (!(aiKey in args) || !args[aiKey] || typeof args[aiKey] !== 'object')
127
- continue;
128
- const aiStyle = args[aiKey];
129
- const filtered = {};
130
- for (const [k, v] of Object.entries(aiStyle)) {
131
- const normalized = normalizeStyleValue(k, v);
132
- if (normalized !== undefined)
133
- filtered[k] = normalized;
128
+ let editor;
129
+ const applyColorToHtml = applyColor ??
130
+ ((html, color) => {
131
+ editor = editor ?? useExternalRichTextEditor().externalEditor;
132
+ return applyStyleToRTEText(editor, html, {
133
+ color,
134
+ });
135
+ });
136
+ try {
137
+ for (const [aiKey, modelKey] of Object.entries(styleMap)) {
138
+ if (!(aiKey in args) || !args[aiKey] || typeof args[aiKey] !== 'object')
139
+ continue;
140
+ const aiStyle = args[aiKey];
141
+ const filtered = {};
142
+ for (const [k, v] of Object.entries(aiStyle)) {
143
+ const normalized = normalizeStyleValue(k, v);
144
+ if (normalized !== undefined)
145
+ filtered[k] = normalized;
146
+ }
147
+ const resolved = resolveGlobalPresetIntoStyle(content[modelKey], textStyleOptions);
148
+ record[modelKey] = { ...resolved, ...filtered, textStyleType: undefined };
149
+ // A colour patch without new text leaves the field's EXISTING inline
150
+ // textStyle marks in place — and inline marks override every style layer
151
+ // at render, so the old colour survives on a recoloured band (invisible
152
+ // text, AI QA 2026-07-05 rerun). Propagate the new colour into the
153
+ // stored HTML, mirroring global-font-propagation for font families.
154
+ const textKey = modelKey.endsWith('Style')
155
+ ? modelKey.slice(0, -'Style'.length)
156
+ : undefined;
157
+ const textAiKey = aiKey.endsWith('_style')
158
+ ? aiKey.slice(0, -'_style'.length)
159
+ : undefined;
160
+ const newColor = filtered.color;
161
+ if (textKey &&
162
+ typeof newColor === 'string' &&
163
+ newColor &&
164
+ !(textAiKey && textAiKey in args) &&
165
+ typeof record[textKey] === 'string' &&
166
+ hasRTEContent(record[textKey])) {
167
+ record[textKey] = applyColorToHtml(record[textKey], newColor);
168
+ }
134
169
  }
135
- const resolved = resolveGlobalPresetIntoStyle(content[modelKey], textStyleOptions);
136
- record[modelKey] = { ...resolved, ...filtered, textStyleType: undefined };
170
+ }
171
+ finally {
172
+ editor?.destroy();
137
173
  }
138
174
  };
@@ -0,0 +1,2 @@
1
+ import { type ValidationContext } from '../../sanitizers/section-args-validator';
2
+ export declare function validateSupportMe(args: Record<string, unknown>, ctx: ValidationContext): string[];
@@ -0,0 +1,17 @@
1
+ import { MAX_TITLE_LENGTH, MAX_DESCRIPTION_LENGTH, } from '../../../brand-page/constants/font.constants';
2
+ import { DISPLAY_STYLE_PREVIEW, DISPLAY_STYLE_COLLAPSED, } from '../../../brand-page/constants';
3
+ import { enumCheck, runChecks, styleCheck, textCheck, } from '../../sanitizers/section-args-validator';
4
+ const CHECKS = [
5
+ textCheck('title', MAX_TITLE_LENGTH),
6
+ textCheck('description', MAX_DESCRIPTION_LENGTH),
7
+ textCheck('success_message', MAX_DESCRIPTION_LENGTH),
8
+ styleCheck('title_style'),
9
+ styleCheck('heading_style'),
10
+ styleCheck('description_style'),
11
+ styleCheck('item_text_style'),
12
+ styleCheck('button_text_style'),
13
+ enumCheck('display_style', [DISPLAY_STYLE_PREVIEW, DISPLAY_STYLE_COLLAPSED], 'support-me'),
14
+ ];
15
+ export function validateSupportMe(args, ctx) {
16
+ return runChecks(CHECKS, args, ctx);
17
+ }
@@ -3,7 +3,7 @@ import {} from '../brand-page/models';
3
3
  import { BUILDER_TYPES, } from './models';
4
4
  import { UpdateContentTextBuilder } from './builders/update-content-text-builder';
5
5
  import { UpdateContentTextStyleBuilder } from './builders/update-content-text-style-builder';
6
- import { BannerSectionBuilder, BookingSectionBuilder, AboutSectionBuilder, ContactFormBuilder, DividerSectionBuilder, TextSectionBuilder, TestimonialSectionBuilder, ImagesSectionBuilder, ShapesSectionBuilder, SocialLinksBuilder, ImageGalleryBuilder, ProfileSectionBuilder, MailingListSectionBuilder, ButtonSectionBuilder, DeleteSectionBuilder, MoveSectionBuilder, } from './builders/section-builders';
6
+ import { BannerSectionBuilder, BookingSectionBuilder, SupportMeBuilder, AboutSectionBuilder, ContactFormBuilder, DividerSectionBuilder, TextSectionBuilder, TestimonialSectionBuilder, ImagesSectionBuilder, ShapesSectionBuilder, SocialLinksBuilder, ImageGalleryBuilder, ProfileSectionBuilder, MailingListSectionBuilder, ButtonSectionBuilder, DeleteSectionBuilder, MoveSectionBuilder, } from './builders/section-builders';
7
7
  import { AddPageBuilder } from './builders/add-page-builder';
8
8
  import { RemovePageBuilder } from './builders/remove-page-builder';
9
9
  import { RenamePageBuilder } from './builders/rename-page-builder';
@@ -20,6 +20,7 @@ const getBuilderRegistry = () => ({
20
20
  // Section builders (create)
21
21
  [BUILDER_TYPES.CREATE_BANNER_SECTION]: BannerSectionBuilder,
22
22
  [BUILDER_TYPES.CREATE_BOOKING_SECTION]: BookingSectionBuilder,
23
+ [BUILDER_TYPES.CREATE_SUPPORT_ME]: SupportMeBuilder,
23
24
  [BUILDER_TYPES.CREATE_ABOUT_SECTION]: AboutSectionBuilder,
24
25
  [BUILDER_TYPES.CREATE_CONTACT_FORM]: ContactFormBuilder,
25
26
  [BUILDER_TYPES.CREATE_DIVIDER_SECTION]: DividerSectionBuilder,
@@ -46,6 +47,7 @@ const getBuilderRegistry = () => ({
46
47
  // Section builders (update)
47
48
  [BUILDER_TYPES.UPDATE_BANNER_SECTION]: BannerSectionBuilder,
48
49
  [BUILDER_TYPES.UPDATE_BOOKING_SECTION]: BookingSectionBuilder,
50
+ [BUILDER_TYPES.UPDATE_SUPPORT_ME]: SupportMeBuilder,
49
51
  [BUILDER_TYPES.UPDATE_ABOUT_SECTION]: AboutSectionBuilder,
50
52
  [BUILDER_TYPES.UPDATE_CONTACT_FORM]: ContactFormBuilder,
51
53
  [BUILDER_TYPES.UPDATE_DIVIDER_SECTION]: DividerSectionBuilder,
@@ -29,6 +29,7 @@ export declare const BUILDER_TYPES: {
29
29
  readonly CREATE_TWITCH: "create-twitch";
30
30
  readonly CREATE_TIKTOK: "create-tiktok";
31
31
  readonly CREATE_BOOKING_SECTION: "create-booking-section";
32
+ readonly CREATE_SUPPORT_ME: "create-support-me";
32
33
  readonly UPDATE_BANNER_SECTION: "update-banner-section";
33
34
  readonly UPDATE_ABOUT_SECTION: "update-about-section";
34
35
  readonly UPDATE_CONTACT_FORM: "update-contact-form";
@@ -54,6 +55,7 @@ export declare const BUILDER_TYPES: {
54
55
  readonly UPDATE_TWITCH: "update-twitch";
55
56
  readonly UPDATE_TIKTOK: "update-tiktok";
56
57
  readonly UPDATE_BOOKING_SECTION: "update-booking-section";
58
+ readonly UPDATE_SUPPORT_ME: "update-support-me";
57
59
  readonly DELETE_SECTION: "delete-section";
58
60
  readonly MOVE_SECTION: "move-section";
59
61
  readonly ADD_PAGE: "add-page";
@@ -28,6 +28,7 @@ export const BUILDER_TYPES = {
28
28
  CREATE_TWITCH: 'create-twitch',
29
29
  CREATE_TIKTOK: 'create-tiktok',
30
30
  CREATE_BOOKING_SECTION: 'create-booking-section',
31
+ CREATE_SUPPORT_ME: 'create-support-me',
31
32
  // Per-section builder types (update)
32
33
  UPDATE_BANNER_SECTION: 'update-banner-section',
33
34
  UPDATE_ABOUT_SECTION: 'update-about-section',
@@ -54,6 +55,7 @@ export const BUILDER_TYPES = {
54
55
  UPDATE_TWITCH: 'update-twitch',
55
56
  UPDATE_TIKTOK: 'update-tiktok',
56
57
  UPDATE_BOOKING_SECTION: 'update-booking-section',
58
+ UPDATE_SUPPORT_ME: 'update-support-me',
57
59
  // Section-level operations
58
60
  DELETE_SECTION: 'delete-section',
59
61
  MOVE_SECTION: 'move-section',
@@ -30,6 +30,7 @@ export declare const AI_ASSISTANT_TOOL_NAMES: {
30
30
  readonly BUILD_TESTIMONIAL_SECTION: "build_testimonial-section_section";
31
31
  readonly BUILD_DIVIDER_SECTION: "build_divider-section_section";
32
32
  readonly BUILD_BOOKING_SECTION: "build_booking-section_section";
33
+ readonly BUILD_SUPPORT_ME_SECTION: "build_support-me_section";
33
34
  readonly DELETE_SECTION: "delete_section";
34
35
  readonly MOVE_SECTION: "move_section";
35
36
  readonly ADD_PAGE: "add_page";
@@ -34,6 +34,7 @@ export const AI_ASSISTANT_TOOL_NAMES = {
34
34
  BUILD_TESTIMONIAL_SECTION: 'build_testimonial-section_section',
35
35
  BUILD_DIVIDER_SECTION: 'build_divider-section_section',
36
36
  BUILD_BOOKING_SECTION: 'build_booking-section_section',
37
+ BUILD_SUPPORT_ME_SECTION: 'build_support-me_section',
37
38
  // Section-level operation tools
38
39
  DELETE_SECTION: 'delete_section',
39
40
  MOVE_SECTION: 'move_section',
@@ -113,6 +114,9 @@ export const SECTION_TOOL_MAP = {
113
114
  [AI_ASSISTANT_TOOL_NAMES.BUILD_BOOKING_SECTION]: {
114
115
  sectionType: 'booking-section',
115
116
  },
117
+ [AI_ASSISTANT_TOOL_NAMES.BUILD_SUPPORT_ME_SECTION]: {
118
+ sectionType: 'support-me',
119
+ },
116
120
  };
117
121
  export const parseToolCalls = (toolCalls) => {
118
122
  if (!toolCalls || toolCalls.length === 0) {
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import type { ContentType } from '../../brand-page/models';
8
8
  /** Content types not supported by the AI builder. */
9
- type UnsupportedContentType = 'logo' | 'video' | 'map' | 'request-payment' | 'support-me' | 'shop-section';
9
+ type UnsupportedContentType = 'logo' | 'video' | 'map' | 'request-payment' | 'shop-section';
10
10
  export type SectionType = Exclude<ContentType, UnsupportedContentType>;
11
11
  export interface TiptapMark {
12
12
  type: 'bold' | 'italic' | 'underline' | 'strike' | 'link' | 'textStyle' | 'highlight';
@@ -407,13 +407,25 @@ export interface BookingSectionArguments {
407
407
  padding_top?: number;
408
408
  padding_bottom?: number;
409
409
  }
410
+ export interface SupportMeArguments {
411
+ title?: string;
412
+ description?: string;
413
+ success_message?: string;
414
+ /** Matches DISPLAY_STYLE_PREVIEW / DISPLAY_STYLE_COLLAPSED. */
415
+ display_style?: 'preview' | 'collapsed';
416
+ title_style?: TextStyleArguments;
417
+ heading_style?: TextStyleArguments;
418
+ description_style?: TextStyleArguments;
419
+ item_text_style?: TextStyleArguments;
420
+ button_text_style?: ButtonTextStyleArguments;
421
+ }
410
422
  export interface DividerSectionArguments {
411
423
  divider_style?: 'none' | 'thin' | 'thick';
412
424
  divider_color?: string | null;
413
425
  background_color?: string | null;
414
426
  padding?: number;
415
427
  }
416
- export type SectionToolArguments = ProfileSectionArguments | TextSectionArguments | ImageSectionArguments | ImageGallerySectionArguments | GeneralLinkSectionArguments | WebsiteSectionArguments | EmailSectionArguments | PhoneSectionArguments | FacebookSectionArguments | XSectionArguments | InstagramSectionArguments | YoutubeSectionArguments | LinkedinSectionArguments | PinterestSectionArguments | TwitchSectionArguments | TiktokSectionArguments | SocialLinksSectionArguments | MailingListSectionArguments | ContactFormSectionArguments | BannerSectionArguments | AboutSectionArguments | ImagesSectionArguments | TestimonialSectionArguments | ShapesSectionArguments | BookingSectionArguments | DividerSectionArguments;
428
+ export type SectionToolArguments = ProfileSectionArguments | TextSectionArguments | ImageSectionArguments | ImageGallerySectionArguments | GeneralLinkSectionArguments | WebsiteSectionArguments | EmailSectionArguments | PhoneSectionArguments | FacebookSectionArguments | XSectionArguments | InstagramSectionArguments | YoutubeSectionArguments | LinkedinSectionArguments | PinterestSectionArguments | TwitchSectionArguments | TiktokSectionArguments | SocialLinksSectionArguments | MailingListSectionArguments | ContactFormSectionArguments | BannerSectionArguments | AboutSectionArguments | ImagesSectionArguments | TestimonialSectionArguments | ShapesSectionArguments | BookingSectionArguments | SupportMeArguments | DividerSectionArguments;
417
429
  export interface SectionToolCallShape {
418
430
  mode: 'create' | 'update';
419
431
  type: string;
@@ -355,11 +355,18 @@ const minifyContent = (content, editor, assets) => {
355
355
  case 'logo':
356
356
  return base;
357
357
  case 'request-payment':
358
+ return {
359
+ ...base,
360
+ ...st('title', c.title, editor),
361
+ ...st('description', c.description, editor),
362
+ };
358
363
  case 'support-me':
359
364
  return {
360
365
  ...base,
361
366
  ...st('title', c.title, editor),
362
367
  ...st('description', c.description, editor),
368
+ ...st('successMessage', c.successMessage, editor),
369
+ ...(includeString(c.displayStyle) && { displayStyle: c.displayStyle }),
363
370
  };
364
371
  default:
365
372
  return base;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@designcrowd/library.brand-page",
3
3
  "description": "A collection of Maker/Designer applications",
4
- "version": "6.0.26",
4
+ "version": "6.0.27-websitev2-dev-1",
5
5
  "author": "Design.com Eng",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="ai-container">
2
+ <div data-component="ai-website-builder" class="ai-container">
3
3
  <div class="tw-px-5">
4
4
  <SideBarSettingsHeader
5
5
  title="AI Website Builder"
@@ -513,7 +513,7 @@ export function useAiWebsiteBuilderChat(): UseAiChatResult {
513
513
  message,
514
514
  websiteContext,
515
515
  websiteSnapshot: snapshotStore.snapshotBase64 || undefined,
516
- agentType: AI_ASSISTANT_AGENT_TYPES.WEBSITE_BUILDER,
516
+ agentType: AI_ASSISTANT_AGENT_TYPES.WEBSITE_BUILDER_V2,
517
517
  userPlan,
518
518
  connectionId,
519
519
  });
@@ -524,7 +524,7 @@ export function useAiWebsiteBuilderChat(): UseAiChatResult {
524
524
  message,
525
525
  websiteContext,
526
526
  websiteSnapshot: snapshotStore.snapshotBase64 || undefined,
527
- agentType: AI_ASSISTANT_AGENT_TYPES.WEBSITE_BUILDER,
527
+ agentType: AI_ASSISTANT_AGENT_TYPES.WEBSITE_BUILDER_V2,
528
528
  userPlan,
529
529
  connectionId,
530
530
  },
@@ -727,11 +727,15 @@ export function useAiWebsiteBuilderChat(): UseAiChatResult {
727
727
  retryCount,
728
728
  });
729
729
 
730
+ // Tool submissions must go to the SAME agent that ran the thread
731
+ // (AI QA 2026-07-05 T19: a mismatched agentType routes tool results
732
+ // to a service with no state for the thread and fails the build).
733
+ // brandSite AI is V2-only; every call site sends WEBSITE_BUILDER_V2.
730
734
  const submitResult = await client.submitToolResultsBuildAsync(
731
735
  aiThreadStore.threadId!,
732
736
  {
733
737
  toolCalls: toolResults,
734
- agentType: AI_ASSISTANT_AGENT_TYPES.WEBSITE_BUILDER,
738
+ agentType: AI_ASSISTANT_AGENT_TYPES.WEBSITE_BUILDER_V2,
735
739
  connectionId,
736
740
  retryCount,
737
741
  userPlan,
@@ -30,6 +30,7 @@ export interface WebsiteContext {
30
30
  // Agent types
31
31
  export const AI_ASSISTANT_AGENT_TYPES = {
32
32
  WEBSITE_BUILDER: 'website_builder',
33
+ WEBSITE_BUILDER_V2: 'website_builder_v2',
33
34
  } as const;
34
35
 
35
36
  export type AiAssistantAgentType =