@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.
- package/lib/src/{CreatedOnBrandCrowd-de-de-DU6YEVcW.js → CreatedOnBrandCrowd-de-de-BpFKOKIo.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-DNWWK6Tj.js → CreatedOnBrandCrowd-en-us-USGAnC_n.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-DVFvV9Qp.js → CreatedOnBrandCrowd-es-es-CvVx3h4H.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-B4ZkjNdw.js → CreatedOnBrandCrowd-fr-ca-BCwamW5a.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-B4ZkjNdw.js → CreatedOnBrandCrowd-fr-fr-BCwamW5a.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-DP7AQGgW.js → CreatedOnBrandCrowd-pt-br-Bt72hMrQ.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-DP7AQGgW.js → CreatedOnBrandCrowd-pt-pt-Bt72hMrQ.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-CaC3GtG9.js → CreatedOnDcom-de-de-BxUDrgG9.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-D16yT0-P.js → CreatedOnDcom-en-us-wjVp5eOs.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-DA5sHgD_.js → CreatedOnDcom-es-es-CwFL2q5h.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-CDv2SG1-.js → CreatedOnDcom-fr-ca-BPZ2_PNq.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-Cygc7c5d.js → CreatedOnDcom-fr-fr-D0vLOikZ.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-B_3RIzgF.js → CreatedOnDcom-pt-br-CTwbxm9H.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-Bz5w-rrD.js → CreatedOnDcom-pt-pt-Cfqf8LaM.js} +1 -1
- package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +7 -3
- package/lib/src/clients/ai-assistant-client.d.ts +1 -0
- package/lib/src/clients/ai-assistant-client.js +1 -0
- package/lib/src/index.mjs +1 -1
- package/lib/src/{lib-6_P6kXPu.js → lib-Cg4BExY4.js} +15537 -15375
- package/lib/src/metadata-builder/builders/global-font-propagation.d.ts +28 -0
- package/lib/src/metadata-builder/builders/global-font-propagation.js +125 -0
- package/lib/src/metadata-builder/builders/global-styles-builder.js +16 -0
- package/lib/src/metadata-builder/builders/section-builders/index.d.ts +1 -0
- package/lib/src/metadata-builder/builders/section-builders/index.js +1 -0
- package/lib/src/metadata-builder/builders/section-builders/support-me-builder.d.ts +8 -0
- package/lib/src/metadata-builder/builders/section-builders/support-me-builder.js +25 -0
- package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +5 -2
- package/lib/src/metadata-builder/builders/section-field-maps.js +15 -0
- package/lib/src/metadata-builder/builders/section-field-utils.d.ts +6 -1
- package/lib/src/metadata-builder/builders/section-field-utils.js +48 -12
- package/lib/src/metadata-builder/builders/section-rules/support-me.rules.d.ts +2 -0
- package/lib/src/metadata-builder/builders/section-rules/support-me.rules.js +17 -0
- package/lib/src/metadata-builder/metadata-executor.js +3 -1
- package/lib/src/metadata-builder/models.d.ts +2 -0
- package/lib/src/metadata-builder/models.js +2 -0
- package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +1 -0
- package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +4 -0
- package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +14 -2
- package/lib/src/utils/minify-brand-page-metadata.js +7 -0
- package/package.json +1 -1
- package/src/brand-page-maker/components/ai-website-builder/AiWebsiteBuilder.vue +1 -1
- package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +7 -3
- package/src/clients/ai-assistant-client.ts +1 -0
- package/src/metadata-builder/builders/global-font-propagation.test.ts +269 -0
- package/src/metadata-builder/builders/global-font-propagation.ts +205 -0
- package/src/metadata-builder/builders/global-styles-builder.ts +21 -0
- package/src/metadata-builder/builders/section-builders/index.ts +1 -0
- package/src/metadata-builder/builders/section-builders/support-me-builder.test.ts +177 -0
- package/src/metadata-builder/builders/section-builders/support-me-builder.ts +43 -0
- package/src/metadata-builder/builders/section-builders/text-section-builder.ts +5 -2
- package/src/metadata-builder/builders/section-field-maps.ts +18 -0
- package/src/metadata-builder/builders/section-field-utils.test.ts +83 -0
- package/src/metadata-builder/builders/section-field-utils.ts +69 -18
- package/src/metadata-builder/builders/section-rules/support-me.rules.ts +39 -0
- package/src/metadata-builder/metadata-executor.ts +3 -0
- package/src/metadata-builder/models.ts +2 -0
- package/src/metadata-builder/parsers/ai-toolcall-parser.ts +4 -0
- package/src/metadata-builder/parsers/section-tool-arguments.ts +14 -1
- package/src/utils/minify-brand-page-metadata.test.ts +61 -0
- 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
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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:
|