@designcrowd/library.brand-page 6.2.3 → 6.2.4
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-Cn0M-eZ1.js → CreatedOnBrandCrowd-de-de-ystrO2LW.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-DRLMSDZr.js → CreatedOnBrandCrowd-en-us-BU7hNzTZ.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-BglTu73U.js → CreatedOnBrandCrowd-es-es-CbuxneQ3.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-Bj6YZ_el.js → CreatedOnBrandCrowd-fr-ca-BuNUGOFr.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-Bj6YZ_el.js → CreatedOnBrandCrowd-fr-fr-BuNUGOFr.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-DEoOmQ1F.js → CreatedOnBrandCrowd-pt-br-BehL88mc.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-DEoOmQ1F.js → CreatedOnBrandCrowd-pt-pt-BehL88mc.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-BZ7gTt4Z.js → CreatedOnDcom-de-de-CMxjFZt9.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-CD6Km5sH.js → CreatedOnDcom-en-us-2Cf6WBvx.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-CNpZHvEl.js → CreatedOnDcom-es-es-CATNnbTq.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-URS-ydJ1.js → CreatedOnDcom-fr-ca-DQtMfB-a.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-D2AVeA3H.js → CreatedOnDcom-fr-fr-C1_FWOQf.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-C32frnFs.js → CreatedOnDcom-pt-br-C0QOCW0L.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-TDYGjRXS.js → CreatedOnDcom-pt-pt-lHD-p6o4.js} +1 -1
- package/lib/src/brand-page/models/brand-page-context.model.d.ts +5 -0
- package/lib/src/brand-page/models/brand-page-context.model.js +8 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +21 -0
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +21 -0
- package/lib/src/brand-page-maker/components/preview/Preview.mixin.d.ts +7 -0
- package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.d.ts +21 -0
- package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +22 -1
- package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +21 -0
- package/lib/src/brand-page-maker/components/seo-settings/UseExistingDomainModal.mixin.d.ts +21 -0
- package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +21 -0
- package/lib/src/brand-page-maker/components/upgrade-to-premium-button/UpgradeToPremiumButton.mixin.d.ts +21 -0
- package/lib/src/brand-page-maker/composables/ai-chat/ai-builder-experiment.d.ts +10 -0
- package/lib/src/brand-page-maker/composables/ai-chat/ai-builder-experiment.js +11 -0
- package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +14 -4
- package/lib/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.d.ts +9 -2
- package/lib/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.js +20 -0
- package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.d.ts +21 -0
- 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 +224 -223
- package/lib/src/{lib-Dm9AuOJL.js → lib-CqHUUPUW.js} +21920 -21691
- package/lib/src/metadata-builder/builders/global-font-propagation.d.ts +41 -0
- package/lib/src/metadata-builder/builders/global-font-propagation.js +146 -0
- package/lib/src/metadata-builder/builders/global-styles-builder.js +29 -1
- package/lib/src/metadata-builder/builders/header-builder.js +8 -0
- package/lib/src/metadata-builder/builders/reorder-pages-builder.d.ts +13 -0
- package/lib/src/metadata-builder/builders/reorder-pages-builder.js +49 -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/helpers/rte.helpers.d.ts +9 -0
- package/lib/src/metadata-builder/helpers/rte.helpers.js +60 -1
- 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/shared/content-settings/components/GenericTextInput.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +21 -0
- package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +1 -1
- package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +21 -0
- package/lib/src/shared/page-settings/components/delete-page-modal/DeletePageModal.mixin.d.ts +1 -1
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +21 -0
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +21 -0
- package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +28 -0
- package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +21 -0
- package/lib/src/shared/style-settings/button-style-preset.d.ts +12 -0
- package/lib/src/shared/style-settings/button-style-preset.js +39 -0
- package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +21 -0
- package/lib/src/shared/style-settings/store/style-settings-store.js +6 -32
- package/lib/src/utils/minify-brand-page-metadata.d.ts +9 -0
- package/lib/src/utils/minify-brand-page-metadata.js +23 -1
- package/package.json +1 -12
- package/src/brand-page/models/brand-page-context.model.ts +14 -1
- package/src/brand-page-maker/components/ai-website-builder/AiWebsiteBuilder.vue +1 -1
- package/src/brand-page-maker/composables/ai-chat/ai-builder-experiment.test.ts +82 -0
- package/src/brand-page-maker/composables/ai-chat/ai-builder-experiment.ts +19 -0
- package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +18 -4
- package/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.ts +30 -0
- package/src/clients/ai-assistant-client.ts +1 -0
- package/src/metadata-builder/builders/global-font-propagation.test.ts +401 -0
- package/src/metadata-builder/builders/global-font-propagation.ts +238 -0
- package/src/metadata-builder/builders/global-styles-builder.test.ts +107 -0
- package/src/metadata-builder/builders/global-styles-builder.ts +38 -1
- package/src/metadata-builder/builders/header-builder.test.ts +45 -0
- package/src/metadata-builder/builders/header-builder.ts +20 -0
- package/src/metadata-builder/builders/reorder-pages-builder.ts +80 -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/helpers/rte.helpers.ts +82 -1
- 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/shared/style-settings/button-style-preset.test.ts +67 -0
- package/src/shared/style-settings/button-style-preset.ts +56 -0
- package/src/shared/style-settings/store/style-settings-store.ts +9 -38
- package/src/utils/minify-brand-page-metadata.test.ts +166 -1
- package/src/utils/minify-brand-page-metadata.ts +31 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { BrandPageModel } from '../../brand-page/models';
|
|
3
|
+
import type { UpdateGlobalStylesArguments } from '../parsers/ai-toolcall-parser';
|
|
4
|
+
import { BUILDER_TYPES } from '../models';
|
|
5
|
+
import { GlobalStylesBuilder } from './global-styles-builder';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* BP-6082: for a button shape change the AI's update_global_styles carries only
|
|
9
|
+
* `selectedButtonStyleId` (the tool schema hands shape to the preset). The builder
|
|
10
|
+
* used to store the id and leave `buttonStyles.style` untouched, so the site — which
|
|
11
|
+
* renders buttons from `style` — showed no change while the assistant reported
|
|
12
|
+
* rounded corners.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const makeModel = () =>
|
|
16
|
+
({
|
|
17
|
+
styles: {
|
|
18
|
+
buttonStyles: {
|
|
19
|
+
style: {
|
|
20
|
+
color: 'var(--palette-1)',
|
|
21
|
+
boxShadow: '',
|
|
22
|
+
borderColor: 'var(--palette-0)',
|
|
23
|
+
borderStyle: 'solid',
|
|
24
|
+
borderWidth: '2px',
|
|
25
|
+
borderRadius: '0',
|
|
26
|
+
backgroundColor: 'var(--palette-1)',
|
|
27
|
+
},
|
|
28
|
+
iconStyle: { fill: 'var(--palette-custom-pfeg22)' },
|
|
29
|
+
selectedButtonStyleId: 'BUTTON_STYLE_4',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
}) as unknown as BrandPageModel;
|
|
33
|
+
|
|
34
|
+
const run = (args: UpdateGlobalStylesArguments, model: BrandPageModel) =>
|
|
35
|
+
new GlobalStylesBuilder({
|
|
36
|
+
id: 'g1',
|
|
37
|
+
type: BUILDER_TYPES.UPDATE_GLOBAL_STYLES,
|
|
38
|
+
payload: args,
|
|
39
|
+
}).buildAsync(model);
|
|
40
|
+
|
|
41
|
+
describe('GlobalStylesBuilder — button preset shape (BP-6082)', () => {
|
|
42
|
+
it('applies the chosen preset shape to the style the site renders from', async () => {
|
|
43
|
+
const { model } = await run(
|
|
44
|
+
{
|
|
45
|
+
buttonStyles: {
|
|
46
|
+
style: {},
|
|
47
|
+
iconStyle: {},
|
|
48
|
+
selectedButtonStyleId: 'BUTTON_STYLE_5',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
makeModel(),
|
|
52
|
+
);
|
|
53
|
+
const bs = model.styles.buttonStyles;
|
|
54
|
+
|
|
55
|
+
expect(bs.selectedButtonStyleId).toBe('BUTTON_STYLE_5');
|
|
56
|
+
expect(bs.style.borderRadius).toBe('8px');
|
|
57
|
+
expect(bs.style.borderStyle).toBe('solid');
|
|
58
|
+
expect(bs.style.borderWidth).toBe('2px');
|
|
59
|
+
expect(bs.style.boxShadow).toBe('');
|
|
60
|
+
// colours are not the preset's to change
|
|
61
|
+
expect(bs.style.color).toBe('var(--palette-1)');
|
|
62
|
+
expect(bs.style.backgroundColor).toBe('var(--palette-1)');
|
|
63
|
+
expect(bs.iconStyle.fill).toBe('var(--palette-custom-pfeg22)');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('keeps explicit colours from the same call while the preset owns the shape', async () => {
|
|
67
|
+
const { model } = await run(
|
|
68
|
+
{
|
|
69
|
+
buttonStyles: {
|
|
70
|
+
style: { backgroundColor: '#ff0000' },
|
|
71
|
+
selectedButtonStyleId: 'BUTTON_STYLE_5',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
makeModel(),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(model.styles.buttonStyles.style.backgroundColor).toBe('#ff0000');
|
|
78
|
+
expect(model.styles.buttonStyles.style.borderRadius).toBe('8px');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('stores an unknown id and leaves the shape alone', async () => {
|
|
82
|
+
const { model } = await run(
|
|
83
|
+
{ buttonStyles: { selectedButtonStyleId: 'BUTTON_STYLE_999' } },
|
|
84
|
+
makeModel(),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(model.styles.buttonStyles.selectedButtonStyleId).toBe(
|
|
88
|
+
'BUTTON_STYLE_999',
|
|
89
|
+
);
|
|
90
|
+
expect(model.styles.buttonStyles.style).toEqual(
|
|
91
|
+
makeModel().styles.buttonStyles.style,
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('a style-only call still merges without touching the shape', async () => {
|
|
96
|
+
const { model } = await run(
|
|
97
|
+
{ buttonStyles: { style: { color: '#000000' } } },
|
|
98
|
+
makeModel(),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(model.styles.buttonStyles.style.color).toBe('#000000');
|
|
102
|
+
expect(model.styles.buttonStyles.style.borderRadius).toBe('0');
|
|
103
|
+
expect(model.styles.buttonStyles.selectedButtonStyleId).toBe(
|
|
104
|
+
'BUTTON_STYLE_4',
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -14,6 +14,11 @@ import {
|
|
|
14
14
|
GLOBAL_STYLE_CHECKS,
|
|
15
15
|
TEXT_STYLE_OPTION_KEYS,
|
|
16
16
|
} from './section-rules/global-styles.rules';
|
|
17
|
+
import {
|
|
18
|
+
collectChangedFontFamilies,
|
|
19
|
+
propagateFontFamiliesToContents,
|
|
20
|
+
} from './global-font-propagation';
|
|
21
|
+
import { buttonStylePresetCss } from '../../shared/style-settings/button-style-preset';
|
|
17
22
|
|
|
18
23
|
export class GlobalStylesBuilder extends BaseMetadataBuilder<UpdateGlobalStylesArguments> {
|
|
19
24
|
constructor(
|
|
@@ -54,6 +59,23 @@ export class GlobalStylesBuilder extends BaseMetadataBuilder<UpdateGlobalStylesA
|
|
|
54
59
|
|
|
55
60
|
if (args.textStyles) {
|
|
56
61
|
this.applyTextStyles(model, args.textStyles);
|
|
62
|
+
|
|
63
|
+
// A category font change only reaches preset-bound content by itself;
|
|
64
|
+
// detached section styles and inline RTE font marks would still render
|
|
65
|
+
// the old family. Propagate so the change is actually visible (BP-5780).
|
|
66
|
+
const changedFamilies = collectChangedFontFamilies(args.textStyles);
|
|
67
|
+
|
|
68
|
+
if (Object.keys(changedFamilies).length) {
|
|
69
|
+
try {
|
|
70
|
+
propagateFontFamiliesToContents(model, changedFamilies);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
this.log({
|
|
73
|
+
message:
|
|
74
|
+
'Global font propagation to existing contents failed — presets still updated',
|
|
75
|
+
error,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
57
79
|
}
|
|
58
80
|
|
|
59
81
|
if (args.buttonStyles) {
|
|
@@ -117,9 +139,24 @@ export class GlobalStylesBuilder extends BaseMetadataBuilder<UpdateGlobalStylesA
|
|
|
117
139
|
bs.iconStyle = { ...bs.iconStyle, ...buttonStyles.iconStyle };
|
|
118
140
|
}
|
|
119
141
|
|
|
120
|
-
// Merge selectedButtonStyleId
|
|
142
|
+
// Merge selectedButtonStyleId and apply that preset's shape and shadow the way the
|
|
143
|
+
// editor's picker does: the site renders buttons from `style`, so the id alone is
|
|
144
|
+
// invisible (BP-6082). The preset owns the shape keys; colours from `style` stay.
|
|
121
145
|
if (buttonStyles.selectedButtonStyleId !== undefined) {
|
|
122
146
|
bs.selectedButtonStyleId = buttonStyles.selectedButtonStyleId;
|
|
147
|
+
|
|
148
|
+
const presetCss = buttonStylePresetCss(
|
|
149
|
+
buttonStyles.selectedButtonStyleId,
|
|
150
|
+
bs.style,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (presetCss) {
|
|
154
|
+
bs.style = { ...bs.style, ...presetCss };
|
|
155
|
+
} else {
|
|
156
|
+
this.log({
|
|
157
|
+
message: `Unknown selectedButtonStyleId ${buttonStyles.selectedButtonStyleId} — id stored, button shape unchanged`,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
123
160
|
}
|
|
124
161
|
}
|
|
125
162
|
|
|
@@ -104,3 +104,48 @@ describe('HeaderBuilder — nav menu font vs global style', () => {
|
|
|
104
104
|
).toBe('buttons');
|
|
105
105
|
});
|
|
106
106
|
});
|
|
107
|
+
|
|
108
|
+
describe('HeaderBuilder — nav menu font reaches the hamburger overlay (BP-6079)', () => {
|
|
109
|
+
it('applies link_text_font to the hamburger default and selected button styles', async () => {
|
|
110
|
+
const model = makeModel({ textStyleType: 'buttons' });
|
|
111
|
+
const nav = model.pageHeaderMobile!.navigation as unknown as Record<
|
|
112
|
+
string,
|
|
113
|
+
unknown
|
|
114
|
+
>;
|
|
115
|
+
|
|
116
|
+
nav.hamburgerMenuStyle = {
|
|
117
|
+
defaultButtonStyle: { fontFamily: 'Lato', color: '#111111' },
|
|
118
|
+
selectedButtonStyle: { textStyleType: 'buttons' },
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const { model: out, errors } = await run(
|
|
122
|
+
{ mobile: { link_text_font: 'Oswald' } },
|
|
123
|
+
model,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const menu = (
|
|
127
|
+
out.pageHeaderMobile!.navigation as unknown as Record<string, unknown>
|
|
128
|
+
).hamburgerMenuStyle as Record<string, Record<string, unknown>>;
|
|
129
|
+
|
|
130
|
+
expect(errors ?? []).toEqual([]);
|
|
131
|
+
expect(menu.defaultButtonStyle.fontFamily).toBe('Oswald');
|
|
132
|
+
expect(menu.defaultButtonStyle.color).toBe('#111111');
|
|
133
|
+
expect(menu.selectedButtonStyle.fontFamily).toBe('Oswald');
|
|
134
|
+
expect(menu.selectedButtonStyle.textStyleType).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('a header without a hamburger menu style is left alone', async () => {
|
|
138
|
+
const model = makeModel({ textStyleType: 'buttons' });
|
|
139
|
+
|
|
140
|
+
const { model: out, errors } = await run(
|
|
141
|
+
{ desktop: { link_text_font: 'Oswald' } },
|
|
142
|
+
model,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(errors ?? []).toEqual([]);
|
|
146
|
+
expect(
|
|
147
|
+
(out.pageHeaderDesktop!.navigation as unknown as Record<string, unknown>)
|
|
148
|
+
.hamburgerMenuStyle,
|
|
149
|
+
).toBeUndefined();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -65,6 +65,26 @@ const applyViewportHeaderChanges = (
|
|
|
65
65
|
);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// The hamburger overlay renders the same nav links from its own styles, so the
|
|
69
|
+
// menu font has to reach them too or the overlay keeps the old family (BP-6079).
|
|
70
|
+
if (
|
|
71
|
+
overrides.link_text_font !== undefined &&
|
|
72
|
+
header.navigation?.hamburgerMenuStyle
|
|
73
|
+
) {
|
|
74
|
+
const menu = header.navigation.hamburgerMenuStyle;
|
|
75
|
+
|
|
76
|
+
menu.defaultButtonStyle = applyDetachedStyleOverride(
|
|
77
|
+
menu.defaultButtonStyle,
|
|
78
|
+
{ fontFamily: overrides.link_text_font },
|
|
79
|
+
textStyleOptions,
|
|
80
|
+
);
|
|
81
|
+
menu.selectedButtonStyle = applyDetachedStyleOverride(
|
|
82
|
+
menu.selectedButtonStyle,
|
|
83
|
+
{ fontFamily: overrides.link_text_font },
|
|
84
|
+
textStyleOptions,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
68
88
|
if (overrides.link_bg_color !== undefined && header.navigation) {
|
|
69
89
|
header.navigation.defaultButtonStyle = {
|
|
70
90
|
...header.navigation.defaultButtonStyle,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { BrandPageModel, NavOrder } from '../../brand-page/models';
|
|
2
|
+
import {
|
|
3
|
+
BaseMetadataBuilder,
|
|
4
|
+
type BuilderResult,
|
|
5
|
+
type MetadataBuilderRequest,
|
|
6
|
+
} from '../models';
|
|
7
|
+
import type { Logger } from '../logger';
|
|
8
|
+
|
|
9
|
+
export interface ReorderPagesPayload {
|
|
10
|
+
pageOrder: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class ReorderPagesBuilder extends BaseMetadataBuilder<ReorderPagesPayload> {
|
|
14
|
+
constructor(
|
|
15
|
+
request: MetadataBuilderRequest<ReorderPagesPayload>,
|
|
16
|
+
logger?: Logger,
|
|
17
|
+
) {
|
|
18
|
+
super(request, logger);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async validateAsync(): Promise<{ errors: string[] }> {
|
|
22
|
+
const errors: string[] = [];
|
|
23
|
+
const payload = this.request.payload;
|
|
24
|
+
|
|
25
|
+
if (!payload?.pageOrder?.length) {
|
|
26
|
+
errors.push('page_order is required for reorder_pages');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { errors };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async buildAsync(model: BrandPageModel): Promise<BuilderResult> {
|
|
33
|
+
this.log('Build called');
|
|
34
|
+
|
|
35
|
+
const validation = await this.validateAsync();
|
|
36
|
+
|
|
37
|
+
if (validation.errors.length) {
|
|
38
|
+
return { request: this.request, model, errors: validation.errors };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { pageOrder } = this.request.payload!;
|
|
42
|
+
|
|
43
|
+
// Build pageId → routeId lookup from nav.routes
|
|
44
|
+
const pageIdToRouteId = new Map<string, string>();
|
|
45
|
+
|
|
46
|
+
for (const route of model.nav.routes) {
|
|
47
|
+
pageIdToRouteId.set(route.pageId, route.id);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Build new nav.orders from the flat page_order (array of pageIds)
|
|
51
|
+
const newOrders: NavOrder[] = [];
|
|
52
|
+
const seenRouteIds = new Set<string>();
|
|
53
|
+
|
|
54
|
+
for (const pageId of pageOrder) {
|
|
55
|
+
const routeId = pageIdToRouteId.get(pageId);
|
|
56
|
+
|
|
57
|
+
if (!routeId) continue;
|
|
58
|
+
|
|
59
|
+
newOrders.push({ routeId, subRoutes: [] });
|
|
60
|
+
seenRouteIds.add(routeId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Safety: append any routes not mentioned (don't lose pages)
|
|
64
|
+
for (const route of model.nav.routes) {
|
|
65
|
+
const routeId = pageIdToRouteId.get(route.pageId);
|
|
66
|
+
|
|
67
|
+
if (routeId && !seenRouteIds.has(routeId)) {
|
|
68
|
+
newOrders.push({ routeId, subRoutes: [] });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
model.nav.orders = newOrders;
|
|
73
|
+
|
|
74
|
+
this.log({
|
|
75
|
+
message: `Reordered pages: ${pageOrder.join(', ')}`,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return { request: this.request, model };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -5,6 +5,7 @@ export {
|
|
|
5
5
|
|
|
6
6
|
export { BannerSectionBuilder } from './banner-section-builder';
|
|
7
7
|
export { BookingSectionBuilder } from './booking-section-builder';
|
|
8
|
+
export { SupportMeBuilder } from './support-me-builder';
|
|
8
9
|
export { AboutSectionBuilder } from './about-section-builder';
|
|
9
10
|
export { ContactFormBuilder } from './contact-form-builder';
|
|
10
11
|
export { DividerSectionBuilder } from './divider-section-builder';
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type {
|
|
3
|
+
BrandPageModel,
|
|
4
|
+
SupportMeContent,
|
|
5
|
+
} from '../../../brand-page/models';
|
|
6
|
+
import { BUILDER_TYPES } from '../../models';
|
|
7
|
+
import type { SupportMeArguments } from '../../parsers/section-tool-arguments';
|
|
8
|
+
import { SupportMeBuilder } from './support-me-builder';
|
|
9
|
+
|
|
10
|
+
const makeModel = (contents: SupportMeContent[] = []): BrandPageModel =>
|
|
11
|
+
({
|
|
12
|
+
brandPageType: 'brandSite',
|
|
13
|
+
styles: { textStyles: { textStyleOptions: {} } },
|
|
14
|
+
pages: [{ id: 'page-1', contents }],
|
|
15
|
+
}) as unknown as BrandPageModel;
|
|
16
|
+
|
|
17
|
+
const seedContent = (): SupportMeContent =>
|
|
18
|
+
({
|
|
19
|
+
id: 'sm-1',
|
|
20
|
+
type: 'support-me',
|
|
21
|
+
title: 'Existing title',
|
|
22
|
+
description: 'Existing description',
|
|
23
|
+
successMessage: 'Existing success',
|
|
24
|
+
displayStyle: 'preview',
|
|
25
|
+
supportMeConfig: {
|
|
26
|
+
paymentOptions: [{ amount: '0', id: 'po-1', name: 'Custom' }],
|
|
27
|
+
isCustomerMessageEnabled: true,
|
|
28
|
+
},
|
|
29
|
+
titleStyle: {},
|
|
30
|
+
headingStyle: {},
|
|
31
|
+
descriptionStyle: {},
|
|
32
|
+
buttonTextStyle: {},
|
|
33
|
+
itemTextStyle: {},
|
|
34
|
+
}) as unknown as SupportMeContent;
|
|
35
|
+
|
|
36
|
+
const create = (args: SupportMeArguments, model: BrandPageModel) =>
|
|
37
|
+
new SupportMeBuilder({
|
|
38
|
+
id: 't-create',
|
|
39
|
+
type: BUILDER_TYPES.CREATE_SUPPORT_ME,
|
|
40
|
+
payload: { sectionType: 'support-me', arguments: args, pageId: 'page-1' },
|
|
41
|
+
}).buildAsync(model);
|
|
42
|
+
|
|
43
|
+
const update = (
|
|
44
|
+
args: SupportMeArguments,
|
|
45
|
+
model: BrandPageModel,
|
|
46
|
+
contentId = 'sm-1',
|
|
47
|
+
) =>
|
|
48
|
+
new SupportMeBuilder({
|
|
49
|
+
id: 't-update',
|
|
50
|
+
type: BUILDER_TYPES.UPDATE_SUPPORT_ME,
|
|
51
|
+
payload: { sectionType: 'support-me', arguments: args, contentId },
|
|
52
|
+
}).buildAsync(model);
|
|
53
|
+
|
|
54
|
+
describe('SupportMeBuilder — create', () => {
|
|
55
|
+
it('creates a support-me section with factory defaults', async () => {
|
|
56
|
+
const model = makeModel();
|
|
57
|
+
|
|
58
|
+
const { model: out, errors } = await create({}, model);
|
|
59
|
+
|
|
60
|
+
expect(errors ?? []).toEqual([]);
|
|
61
|
+
|
|
62
|
+
const created = out.pages[0].contents[0] as SupportMeContent;
|
|
63
|
+
|
|
64
|
+
expect(created.type).toBe('support-me');
|
|
65
|
+
// display style default comes from the factory
|
|
66
|
+
expect(created.displayStyle).toBe('preview');
|
|
67
|
+
// supportMeConfig (payment options) comes free from the factory, never the AI
|
|
68
|
+
expect(created.supportMeConfig.paymentOptions.length).toBeGreaterThan(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('applies AI-provided scalar fields over the defaults', async () => {
|
|
72
|
+
const model = makeModel();
|
|
73
|
+
|
|
74
|
+
const { model: out, errors } = await create(
|
|
75
|
+
{
|
|
76
|
+
title: 'Buy me a coffee',
|
|
77
|
+
description: 'Support my work',
|
|
78
|
+
success_message: 'Thanks!',
|
|
79
|
+
display_style: 'collapsed',
|
|
80
|
+
},
|
|
81
|
+
model,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(errors ?? []).toEqual([]);
|
|
85
|
+
|
|
86
|
+
const created = out.pages[0].contents[0] as SupportMeContent;
|
|
87
|
+
|
|
88
|
+
expect(created.title).toBe('Buy me a coffee');
|
|
89
|
+
expect(created.description).toBe('Support my work');
|
|
90
|
+
expect(created.successMessage).toBe('Thanks!');
|
|
91
|
+
expect(created.displayStyle).toBe('collapsed');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('SupportMeBuilder — update', () => {
|
|
96
|
+
it('updates scalar fields on an existing section', async () => {
|
|
97
|
+
const model = makeModel([seedContent()]);
|
|
98
|
+
|
|
99
|
+
const { model: out, errors } = await update(
|
|
100
|
+
{
|
|
101
|
+
title: 'New title',
|
|
102
|
+
success_message: 'New success',
|
|
103
|
+
display_style: 'collapsed',
|
|
104
|
+
},
|
|
105
|
+
model,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(errors ?? []).toEqual([]);
|
|
109
|
+
|
|
110
|
+
const updated = out.pages[0].contents[0] as SupportMeContent;
|
|
111
|
+
|
|
112
|
+
expect(updated.title).toBe('New title');
|
|
113
|
+
expect(updated.successMessage).toBe('New success');
|
|
114
|
+
expect(updated.displayStyle).toBe('collapsed');
|
|
115
|
+
// untouched fields survive
|
|
116
|
+
expect(updated.description).toBe('Existing description');
|
|
117
|
+
// supportMeConfig is never AI-editable
|
|
118
|
+
expect(updated.supportMeConfig.paymentOptions.length).toBe(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('applies the five style fields to their model keys', async () => {
|
|
122
|
+
const model = makeModel([seedContent()]);
|
|
123
|
+
|
|
124
|
+
const { model: out, errors } = await update(
|
|
125
|
+
{
|
|
126
|
+
title_style: { textAlign: 'left' },
|
|
127
|
+
heading_style: { textAlign: 'right' },
|
|
128
|
+
description_style: { textAlign: 'center' },
|
|
129
|
+
item_text_style: { textAlign: 'justify' },
|
|
130
|
+
button_text_style: { color: '#123456' },
|
|
131
|
+
},
|
|
132
|
+
model,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(errors ?? []).toEqual([]);
|
|
136
|
+
|
|
137
|
+
const updated = out.pages[0].contents[0] as SupportMeContent;
|
|
138
|
+
|
|
139
|
+
expect(updated.titleStyle).toMatchObject({ textAlign: 'left' });
|
|
140
|
+
expect(updated.headingStyle).toMatchObject({ textAlign: 'right' });
|
|
141
|
+
expect(updated.descriptionStyle).toMatchObject({ textAlign: 'center' });
|
|
142
|
+
expect(updated.itemTextStyle).toMatchObject({ textAlign: 'justify' });
|
|
143
|
+
expect(updated.buttonTextStyle).toMatchObject({ color: '#123456' });
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('SupportMeBuilder — validation', () => {
|
|
148
|
+
it('rejects an invalid display_style enum value', async () => {
|
|
149
|
+
const model = makeModel([seedContent()]);
|
|
150
|
+
|
|
151
|
+
const { errors } = await update({ display_style: 'fancy' as never }, model);
|
|
152
|
+
|
|
153
|
+
expect(errors?.length).toBeGreaterThan(0);
|
|
154
|
+
expect(errors!.join(' ')).toContain('display_style');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('rejects an over-length title', async () => {
|
|
158
|
+
const model = makeModel([seedContent()]);
|
|
159
|
+
|
|
160
|
+
const { errors } = await update({ title: 'x'.repeat(201) }, model);
|
|
161
|
+
|
|
162
|
+
expect(errors?.length).toBeGreaterThan(0);
|
|
163
|
+
expect(errors!.join(' ')).toContain('title');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('rejects an over-length success_message', async () => {
|
|
167
|
+
const model = makeModel([seedContent()]);
|
|
168
|
+
|
|
169
|
+
const { errors } = await update(
|
|
170
|
+
{ success_message: 'x'.repeat(1001) },
|
|
171
|
+
model,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(errors?.length).toBeGreaterThan(0);
|
|
175
|
+
expect(errors!.join(' ')).toContain('success_message');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -246,13 +246,16 @@ export class TextSectionBuilder extends BaseSectionBuilder<TextContent> {
|
|
|
246
246
|
// Full replacement: discard existing items and rebuild from the payload.
|
|
247
247
|
applyTextItems(content, args.items, textStyleOptions);
|
|
248
248
|
} else if (this.mode === 'create') {
|
|
249
|
-
|
|
249
|
+
// An EMPTY items array must not shadow the single text field — the AI emits
|
|
250
|
+
// `items: []` alongside `text` and the section rendered blank (QA 2026-07-05,
|
|
251
|
+
// T06: items=[] set textItems=[] and `text` was silently discarded).
|
|
252
|
+
if (args.items && Array.isArray(args.items) && args.items.length > 0) {
|
|
250
253
|
applyTextItems(content, args.items, textStyleOptions);
|
|
251
254
|
} else {
|
|
252
255
|
applySingleTextField(content, args, textStyleOptions);
|
|
253
256
|
}
|
|
254
257
|
} else if (
|
|
255
|
-
(args.items && Array.isArray(args.items)) ||
|
|
258
|
+
(args.items && Array.isArray(args.items) && args.items.length > 0) ||
|
|
256
259
|
args._remove_item_ids?.length
|
|
257
260
|
) {
|
|
258
261
|
// 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,
|