@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,43 @@
1
+ import type {
2
+ BrandPageModel,
3
+ SupportMeContent,
4
+ } from '../../../brand-page/models';
5
+ import type { BuilderResult } from '../../models';
6
+ import { applyScalarFields, applyStyleFields } from '../section-field-utils';
7
+ import { BaseSectionBuilder } from './base-section-builder';
8
+ import { validateSupportMe } from '../section-rules/support-me.rules';
9
+
10
+ export class SupportMeBuilder extends BaseSectionBuilder<SupportMeContent> {
11
+ protected get sectionValidate() {
12
+ return validateSupportMe;
13
+ }
14
+
15
+ async buildAsync(model: BrandPageModel): Promise<BuilderResult> {
16
+ this.log('Build called');
17
+
18
+ const validation = await this.validateAsync();
19
+
20
+ if (validation.errors.length) {
21
+ return { request: this.request, model, errors: validation.errors };
22
+ }
23
+
24
+ const resolved = this.resolveAndRepositionContent(model);
25
+
26
+ if ('error' in resolved) return resolved.error;
27
+
28
+ const content = resolved.content;
29
+
30
+ // title/description/success_message are plain-text fields (no RTE support),
31
+ // so scalar application covers all content fields. supportMeConfig (payment
32
+ // options) is intentionally not AI-editable — it comes from the factory.
33
+ applyScalarFields(content, this.args, this.sectionType);
34
+ applyStyleFields(
35
+ content,
36
+ this.args,
37
+ this.sectionType,
38
+ this.getGlobalTextStyles(model),
39
+ );
40
+
41
+ return { request: this.request, model };
42
+ }
43
+ }
@@ -240,13 +240,16 @@ export class TextSectionBuilder extends BaseSectionBuilder<TextContent> {
240
240
  // Full replacement: discard existing items and rebuild from the payload.
241
241
  applyTextItems(content, args.items, textStyleOptions);
242
242
  } else if (this.mode === 'create') {
243
- if (args.items && Array.isArray(args.items)) {
243
+ // An EMPTY items array must not shadow the single text field — the AI emits
244
+ // `items: []` alongside `text` and the section rendered blank (QA 2026-07-05,
245
+ // T06: items=[] set textItems=[] and `text` was silently discarded).
246
+ if (args.items && Array.isArray(args.items) && args.items.length > 0) {
244
247
  applyTextItems(content, args.items, textStyleOptions);
245
248
  } else {
246
249
  applySingleTextField(content, args, textStyleOptions);
247
250
  }
248
251
  } else if (
249
- (args.items && Array.isArray(args.items)) ||
252
+ (args.items && Array.isArray(args.items) && args.items.length > 0) ||
250
253
  args._remove_item_ids?.length
251
254
  ) {
252
255
  // Update: id-based add / edit / move / remove in a single pass.
@@ -23,6 +23,7 @@ import type {
23
23
  ImageContent,
24
24
  ButtonContent,
25
25
  ImageGalleryContent,
26
+ SupportMeContent,
26
27
  } from '../../brand-page/models';
27
28
 
28
29
  export type SectionFieldMap = Record<string, string>;
@@ -175,9 +176,17 @@ const socialButtonFields = defineFieldMap<ButtonContent>({
175
176
  url: 'url',
176
177
  });
177
178
 
179
+ const supportMeFields = defineFieldMap<SupportMeContent>({
180
+ title: 'title',
181
+ description: 'description',
182
+ success_message: 'successMessage',
183
+ display_style: 'displayStyle',
184
+ });
185
+
178
186
  export const SECTION_FIELD_MAPS: Record<string, SectionFieldMap> = {
179
187
  'banner-section': bannerFields,
180
188
  'booking-section': bookingFields,
189
+ 'support-me': supportMeFields,
181
190
  'about-section': aboutFields,
182
191
  'testimonial-section': testimonialFields,
183
192
  'images-section': imagesSectionFields,
@@ -265,6 +274,14 @@ const mailingListStyleFields = defineFieldMap<MailingListContent>({
265
274
  button_text_style: 'buttonTextStyle',
266
275
  });
267
276
 
277
+ const supportMeStyleFields = defineFieldMap<SupportMeContent>({
278
+ title_style: 'titleStyle',
279
+ heading_style: 'headingStyle',
280
+ description_style: 'descriptionStyle',
281
+ item_text_style: 'itemTextStyle',
282
+ button_text_style: 'buttonTextStyle',
283
+ });
284
+
268
285
  const socialLinksStyleFields = defineFieldMap<SocialLinksContent>({
269
286
  icon_fill_style: 'iconFillStyles',
270
287
  icon_background_style: 'iconBackgroundStyles',
@@ -273,6 +290,7 @@ const socialLinksStyleFields = defineFieldMap<SocialLinksContent>({
273
290
  export const STYLE_FIELD_MAPS: Record<string, SectionFieldMap> = {
274
291
  'banner-section': bannerStyleFields,
275
292
  'booking-section': bookingStyleFields,
293
+ 'support-me': supportMeStyleFields,
276
294
  'about-section': aboutStyleFields,
277
295
  text: textStyleFields,
278
296
  'testimonial-section': testimonialStyleFields,
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { Content } from '../../brand-page/models';
3
+ import { applyStyleFields } from './section-field-utils';
4
+
5
+ /**
6
+ * Regression tests for the AI QA 2026-07-05 rerun P0: a colour-only *_style
7
+ * patch left the field's existing inline textStyle marks (old colour) in
8
+ * place — inline marks override the style layer at render, so recoloured
9
+ * bands showed invisible old-colour text. applyStyleFields now propagates a
10
+ * new style colour into the stored RTE HTML when the patch carries no new
11
+ * text for that field.
12
+ */
13
+ describe('applyStyleFields inline-colour propagation', () => {
14
+ const makeContent = (): Content =>
15
+ ({
16
+ type: 'about-section',
17
+ title: '<h3><span style="color: #111111">Fine Grooming</span></h3>',
18
+ titleStyle: { color: '#111111', fontSize: '2.188em' },
19
+ paragraph: '<p><span style="color: #111111">Body copy</span></p>',
20
+ paragraphStyle: { color: '#111111' },
21
+ }) as unknown as Content;
22
+
23
+ const recolour = (html: string, color: string): string =>
24
+ html.replace(/color: #[0-9a-fA-F]{6}/g, `color: ${color}`);
25
+
26
+ it('colour-only patch recolours the existing inline marks', () => {
27
+ const content = makeContent();
28
+
29
+ applyStyleFields(
30
+ content,
31
+ { title_style: { color: '#f8f2e9' } },
32
+ 'about-section',
33
+ undefined,
34
+ recolour,
35
+ );
36
+
37
+ const record = content as unknown as Record<string, unknown>;
38
+
39
+ expect(record.title).toContain('color: #f8f2e9');
40
+ expect((record.titleStyle as Record<string, unknown>).color).toBe(
41
+ '#f8f2e9',
42
+ );
43
+ // Untouched fields keep their marks.
44
+ expect(record.paragraph).toContain('color: #111111');
45
+ });
46
+
47
+ it('does NOT rewrite marks when the patch also carries new text for the field', () => {
48
+ const content = makeContent();
49
+
50
+ applyStyleFields(
51
+ content,
52
+ {
53
+ title: { type: 'doc', content: [] },
54
+ title_style: { color: '#f8f2e9' },
55
+ },
56
+ 'about-section',
57
+ undefined,
58
+ recolour,
59
+ );
60
+
61
+ // The builder's own text path owns the rewrite — HTML left alone here.
62
+ expect((content as unknown as Record<string, unknown>).title).toContain(
63
+ 'color: #111111',
64
+ );
65
+ });
66
+
67
+ it('merged style keeps non-colour properties from the current style', () => {
68
+ const content = makeContent();
69
+
70
+ applyStyleFields(
71
+ content,
72
+ { title_style: { color: '#f8f2e9' } },
73
+ 'about-section',
74
+ undefined,
75
+ recolour,
76
+ );
77
+
78
+ const style = (content as unknown as Record<string, unknown>)
79
+ .titleStyle as Record<string, unknown>;
80
+
81
+ expect(style.fontSize).toBe('2.188em');
82
+ });
83
+ });
@@ -9,6 +9,10 @@ import {
9
9
  isTiptapDocument,
10
10
  type SectionToolArguments,
11
11
  } from '../parsers/section-tool-arguments';
12
+ import { hasRTEContent } from '../../brand-page-maker/utils';
13
+ import { useExternalRichTextEditor } from '../../brand-page-maker/composables/rich-text-editor/useRichTextEditor';
14
+ import { applyStyleToRTEText } from '../helpers/rte.helpers';
15
+ import type { TextStyleDeclaration } from '../../brand-page/models';
12
16
  import { getSectionFieldMap, getStyleFieldMap } from './section-field-maps';
13
17
 
14
18
  /**
@@ -186,36 +190,83 @@ export const applyDetachedStyleOverride = <T extends object>(
186
190
  ...updates,
187
191
  }) as T;
188
192
 
193
+ /**
194
+ * Rewrites the stored RTE HTML's inline colour marks. Injectable for tests;
195
+ * the default uses the external Tiptap editor (font-propagation precedent).
196
+ */
197
+ export type ApplyColorToRTE = (html: string, color: string) => string;
198
+
189
199
  export const applyStyleFields = (
190
200
  content: Content,
191
201
  args: SectionToolArguments,
192
202
  sectionType: string,
193
203
  textStyleOptions?: TextStyleOptions,
204
+ applyColor?: ApplyColorToRTE,
194
205
  ): void => {
195
206
  const styleMap = getStyleFieldMap(sectionType);
196
207
 
197
208
  if (!styleMap) return;
198
209
 
199
210
  const record = content as IndexableContent;
211
+ let editor:
212
+ | ReturnType<typeof useExternalRichTextEditor>['externalEditor']
213
+ | undefined;
214
+ const applyColorToHtml: ApplyColorToRTE =
215
+ applyColor ??
216
+ ((html, color) => {
217
+ editor = editor ?? useExternalRichTextEditor().externalEditor;
218
+
219
+ return applyStyleToRTEText(editor, html, {
220
+ color,
221
+ } as TextStyleDeclaration);
222
+ });
200
223
 
201
- for (const [aiKey, modelKey] of Object.entries(styleMap)) {
202
- if (!(aiKey in args) || !args[aiKey] || typeof args[aiKey] !== 'object')
203
- continue;
204
-
205
- const aiStyle = args[aiKey] as Record<string, unknown>;
206
- const filtered: Record<string, unknown> = {};
207
-
208
- for (const [k, v] of Object.entries(aiStyle)) {
209
- const normalized = normalizeStyleValue(k, v);
210
-
211
- if (normalized !== undefined) filtered[k] = normalized;
224
+ try {
225
+ for (const [aiKey, modelKey] of Object.entries(styleMap)) {
226
+ if (!(aiKey in args) || !args[aiKey] || typeof args[aiKey] !== 'object')
227
+ continue;
228
+
229
+ const aiStyle = args[aiKey] as Record<string, unknown>;
230
+ const filtered: Record<string, unknown> = {};
231
+
232
+ for (const [k, v] of Object.entries(aiStyle)) {
233
+ const normalized = normalizeStyleValue(k, v);
234
+
235
+ if (normalized !== undefined) filtered[k] = normalized;
236
+ }
237
+
238
+ const resolved = resolveGlobalPresetIntoStyle(
239
+ content[modelKey] as object,
240
+ textStyleOptions,
241
+ );
242
+
243
+ record[modelKey] = { ...resolved, ...filtered, textStyleType: undefined };
244
+
245
+ // A colour patch without new text leaves the field's EXISTING inline
246
+ // textStyle marks in place — and inline marks override every style layer
247
+ // at render, so the old colour survives on a recoloured band (invisible
248
+ // text, AI QA 2026-07-05 rerun). Propagate the new colour into the
249
+ // stored HTML, mirroring global-font-propagation for font families.
250
+ const textKey = modelKey.endsWith('Style')
251
+ ? modelKey.slice(0, -'Style'.length)
252
+ : undefined;
253
+ const textAiKey = aiKey.endsWith('_style')
254
+ ? aiKey.slice(0, -'_style'.length)
255
+ : undefined;
256
+ const newColor = filtered.color;
257
+
258
+ if (
259
+ textKey &&
260
+ typeof newColor === 'string' &&
261
+ newColor &&
262
+ !(textAiKey && textAiKey in args) &&
263
+ typeof record[textKey] === 'string' &&
264
+ hasRTEContent(record[textKey] as string)
265
+ ) {
266
+ record[textKey] = applyColorToHtml(record[textKey] as string, newColor);
267
+ }
212
268
  }
213
-
214
- const resolved = resolveGlobalPresetIntoStyle(
215
- content[modelKey] as object,
216
- textStyleOptions,
217
- );
218
-
219
- record[modelKey] = { ...resolved, ...filtered, textStyleType: undefined };
269
+ } finally {
270
+ editor?.destroy();
220
271
  }
221
272
  };
@@ -0,0 +1,39 @@
1
+ import {
2
+ MAX_TITLE_LENGTH,
3
+ MAX_DESCRIPTION_LENGTH,
4
+ } from '../../../brand-page/constants/font.constants';
5
+ import {
6
+ DISPLAY_STYLE_PREVIEW,
7
+ DISPLAY_STYLE_COLLAPSED,
8
+ } from '../../../brand-page/constants';
9
+ import {
10
+ enumCheck,
11
+ runChecks,
12
+ styleCheck,
13
+ textCheck,
14
+ type SectionCheck,
15
+ type ValidationContext,
16
+ } from '../../sanitizers/section-args-validator';
17
+
18
+ const CHECKS: SectionCheck[] = [
19
+ textCheck('title', MAX_TITLE_LENGTH),
20
+ textCheck('description', MAX_DESCRIPTION_LENGTH),
21
+ textCheck('success_message', MAX_DESCRIPTION_LENGTH),
22
+ styleCheck('title_style'),
23
+ styleCheck('heading_style'),
24
+ styleCheck('description_style'),
25
+ styleCheck('item_text_style'),
26
+ styleCheck('button_text_style'),
27
+ enumCheck(
28
+ 'display_style',
29
+ [DISPLAY_STYLE_PREVIEW, DISPLAY_STYLE_COLLAPSED],
30
+ 'support-me',
31
+ ),
32
+ ];
33
+
34
+ export function validateSupportMe(
35
+ args: Record<string, unknown>,
36
+ ctx: ValidationContext,
37
+ ): string[] {
38
+ return runChecks(CHECKS, args, ctx);
39
+ }
@@ -13,6 +13,7 @@ import { UpdateContentTextStyleBuilder } from './builders/update-content-text-st
13
13
  import {
14
14
  BannerSectionBuilder,
15
15
  BookingSectionBuilder,
16
+ SupportMeBuilder,
16
17
  AboutSectionBuilder,
17
18
  ContactFormBuilder,
18
19
  DividerSectionBuilder,
@@ -54,6 +55,7 @@ const getBuilderRegistry = (): Record<BuilderType, BuilderConstructor> => ({
54
55
  // Section builders (create)
55
56
  [BUILDER_TYPES.CREATE_BANNER_SECTION]: BannerSectionBuilder,
56
57
  [BUILDER_TYPES.CREATE_BOOKING_SECTION]: BookingSectionBuilder,
58
+ [BUILDER_TYPES.CREATE_SUPPORT_ME]: SupportMeBuilder,
57
59
  [BUILDER_TYPES.CREATE_ABOUT_SECTION]: AboutSectionBuilder,
58
60
  [BUILDER_TYPES.CREATE_CONTACT_FORM]: ContactFormBuilder,
59
61
  [BUILDER_TYPES.CREATE_DIVIDER_SECTION]: DividerSectionBuilder,
@@ -81,6 +83,7 @@ const getBuilderRegistry = (): Record<BuilderType, BuilderConstructor> => ({
81
83
  // Section builders (update)
82
84
  [BUILDER_TYPES.UPDATE_BANNER_SECTION]: BannerSectionBuilder,
83
85
  [BUILDER_TYPES.UPDATE_BOOKING_SECTION]: BookingSectionBuilder,
86
+ [BUILDER_TYPES.UPDATE_SUPPORT_ME]: SupportMeBuilder,
84
87
  [BUILDER_TYPES.UPDATE_ABOUT_SECTION]: AboutSectionBuilder,
85
88
  [BUILDER_TYPES.UPDATE_CONTACT_FORM]: ContactFormBuilder,
86
89
  [BUILDER_TYPES.UPDATE_DIVIDER_SECTION]: DividerSectionBuilder,
@@ -32,6 +32,7 @@ export const BUILDER_TYPES = {
32
32
  CREATE_TWITCH: 'create-twitch',
33
33
  CREATE_TIKTOK: 'create-tiktok',
34
34
  CREATE_BOOKING_SECTION: 'create-booking-section',
35
+ CREATE_SUPPORT_ME: 'create-support-me',
35
36
 
36
37
  // Per-section builder types (update)
37
38
  UPDATE_BANNER_SECTION: 'update-banner-section',
@@ -59,6 +60,7 @@ export const BUILDER_TYPES = {
59
60
  UPDATE_TWITCH: 'update-twitch',
60
61
  UPDATE_TIKTOK: 'update-tiktok',
61
62
  UPDATE_BOOKING_SECTION: 'update-booking-section',
63
+ UPDATE_SUPPORT_ME: 'update-support-me',
62
64
 
63
65
  // Section-level operations
64
66
  DELETE_SECTION: 'delete-section',
@@ -43,6 +43,7 @@ export const AI_ASSISTANT_TOOL_NAMES = {
43
43
  BUILD_TESTIMONIAL_SECTION: 'build_testimonial-section_section',
44
44
  BUILD_DIVIDER_SECTION: 'build_divider-section_section',
45
45
  BUILD_BOOKING_SECTION: 'build_booking-section_section',
46
+ BUILD_SUPPORT_ME_SECTION: 'build_support-me_section',
46
47
 
47
48
  // Section-level operation tools
48
49
  DELETE_SECTION: 'delete_section',
@@ -163,6 +164,9 @@ export const SECTION_TOOL_MAP: Record<string, { sectionType: SectionType }> = {
163
164
  [AI_ASSISTANT_TOOL_NAMES.BUILD_BOOKING_SECTION]: {
164
165
  sectionType: 'booking-section',
165
166
  },
167
+ [AI_ASSISTANT_TOOL_NAMES.BUILD_SUPPORT_ME_SECTION]: {
168
+ sectionType: 'support-me',
169
+ },
166
170
  };
167
171
 
168
172
  export interface DeleteSectionArguments {
@@ -17,7 +17,6 @@ type UnsupportedContentType =
17
17
  | 'video'
18
18
  | 'map'
19
19
  | 'request-payment'
20
- | 'support-me'
21
20
  | 'shop-section';
22
21
 
23
22
  export type SectionType = Exclude<ContentType, UnsupportedContentType>;
@@ -537,6 +536,19 @@ export interface BookingSectionArguments {
537
536
  padding_bottom?: number;
538
537
  }
539
538
 
539
+ export interface SupportMeArguments {
540
+ title?: string;
541
+ description?: string;
542
+ success_message?: string;
543
+ /** Matches DISPLAY_STYLE_PREVIEW / DISPLAY_STYLE_COLLAPSED. */
544
+ display_style?: 'preview' | 'collapsed';
545
+ title_style?: TextStyleArguments;
546
+ heading_style?: TextStyleArguments;
547
+ description_style?: TextStyleArguments;
548
+ item_text_style?: TextStyleArguments;
549
+ button_text_style?: ButtonTextStyleArguments;
550
+ }
551
+
540
552
  export interface DividerSectionArguments {
541
553
  divider_style?: 'none' | 'thin' | 'thick';
542
554
  divider_color?: string | null;
@@ -574,6 +586,7 @@ export type SectionToolArguments =
574
586
  | TestimonialSectionArguments
575
587
  | ShapesSectionArguments
576
588
  | BookingSectionArguments
589
+ | SupportMeArguments
577
590
  | DividerSectionArguments;
578
591
 
579
592
  // ---------------------------------------------------------------------------
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { BrandPageModel } from '../brand-page/models';
3
+ import { minifyBrandPageMetadata } from './minify-brand-page-metadata';
4
+
5
+ /**
6
+ * The AI builder can now create/update `support-me` sections, so the minified
7
+ * model it reads on update must surface the fields it can edit — otherwise it
8
+ * can't see the current `displayStyle` / `successMessage` when deciding what to
9
+ * change. `request-payment` (not AI-editable) must keep its original shape.
10
+ */
11
+
12
+ const makeModel = (contents: unknown[]): BrandPageModel =>
13
+ ({
14
+ brandPageType: 'brandSite',
15
+ v: 1,
16
+ styles: {},
17
+ nav: { routes: [{ pageId: 'page-1', pageSlug: 'home' }] },
18
+ pages: [{ id: 'page-1', title: 'Home', contents }],
19
+ }) as unknown as BrandPageModel;
20
+
21
+ const supportMe = {
22
+ id: 'sm-1',
23
+ type: 'support-me',
24
+ title: 'Buy me a coffee',
25
+ description: 'Support my work',
26
+ successMessage: 'Thanks so much!',
27
+ displayStyle: 'collapsed',
28
+ };
29
+
30
+ const requestPayment = {
31
+ id: 'rp-1',
32
+ type: 'request-payment',
33
+ title: 'Pay now',
34
+ description: 'Invoice',
35
+ successMessage: 'Paid',
36
+ displayStyle: 'preview',
37
+ };
38
+
39
+ describe('minifyBrandPageMetadata — support-me', () => {
40
+ it('surfaces title, description, successMessage and displayStyle', () => {
41
+ const out = minifyBrandPageMetadata(makeModel([supportMe]));
42
+ const minified = out.pages[0].contents[0];
43
+
44
+ expect(minified.type).toBe('support-me');
45
+ expect(minified.title).toBe('Buy me a coffee');
46
+ expect(minified.description).toBe('Support my work');
47
+ expect(minified.successMessage).toBe('Thanks so much!');
48
+ expect(minified.displayStyle).toBe('collapsed');
49
+ });
50
+
51
+ it('leaves request-payment shape unchanged (no successMessage / displayStyle)', () => {
52
+ const out = minifyBrandPageMetadata(makeModel([requestPayment]));
53
+ const minified = out.pages[0].contents[0];
54
+
55
+ expect(minified.type).toBe('request-payment');
56
+ expect(minified.title).toBe('Pay now');
57
+ expect(minified.description).toBe('Invoice');
58
+ expect(minified.successMessage).toBeUndefined();
59
+ expect(minified.displayStyle).toBeUndefined();
60
+ });
61
+ });
@@ -507,11 +507,19 @@ const minifyContent = (
507
507
  return base;
508
508
 
509
509
  case 'request-payment':
510
+ return {
511
+ ...base,
512
+ ...st('title', c.title, editor),
513
+ ...st('description', c.description, editor),
514
+ };
515
+
510
516
  case 'support-me':
511
517
  return {
512
518
  ...base,
513
519
  ...st('title', c.title, editor),
514
520
  ...st('description', c.description, editor),
521
+ ...st('successMessage', c.successMessage, editor),
522
+ ...(includeString(c.displayStyle) && { displayStyle: c.displayStyle }),
515
523
  };
516
524
 
517
525
  default: