@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,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
|
+
}
|
|
@@ -1,7 +1,88 @@
|
|
|
1
1
|
import type { RichTextValue } from '../../brand-page';
|
|
2
|
-
import {
|
|
2
|
+
import type { TextStyleDeclaration } from '../../brand-page/models/styles.model';
|
|
3
|
+
import { renderRteToHtml, getRTEColor } from '../../brand-page-maker/utils';
|
|
4
|
+
import { getRenderText } from '../../brand-page/utils';
|
|
5
|
+
import { ESCAPED_NEWLINE_CHARACTER } from '../../constants/ui';
|
|
6
|
+
import type { Editor } from '@tiptap/vue-3';
|
|
3
7
|
import { isTiptapDocument } from '../parsers/section-tool-arguments';
|
|
4
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Applies a TextStyleDeclaration to RTE HTML by loading it into the given
|
|
11
|
+
* (external/headless) Tiptap editor and rewriting inline marks via command
|
|
12
|
+
* chains. DOM-bound — client-only callers (font propagation, section style
|
|
13
|
+
* fields).
|
|
14
|
+
*/
|
|
15
|
+
export const applyStyleToRTEText = (
|
|
16
|
+
externalEditor: Editor,
|
|
17
|
+
contentText: string,
|
|
18
|
+
newContentStyle: TextStyleDeclaration,
|
|
19
|
+
) => {
|
|
20
|
+
if (!contentText) {
|
|
21
|
+
return contentText;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const content = contentText.includes(ESCAPED_NEWLINE_CHARACTER)
|
|
25
|
+
? getRenderText(contentText)
|
|
26
|
+
: contentText;
|
|
27
|
+
|
|
28
|
+
externalEditor.commands.setContent(content);
|
|
29
|
+
|
|
30
|
+
let chain = externalEditor.chain().selectAll();
|
|
31
|
+
|
|
32
|
+
if (newContentStyle?.fontFamily) {
|
|
33
|
+
chain = chain.setFontFamily(newContentStyle.fontFamily);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (newContentStyle?.fontSize) {
|
|
37
|
+
chain = chain.setFontSize(newContentStyle.fontSize);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (newContentStyle?.letterSpacing) {
|
|
41
|
+
chain = chain.setLetterSpacing(newContentStyle.letterSpacing);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (newContentStyle?.lineHeight) {
|
|
45
|
+
chain = chain.setLineHeight(newContentStyle.lineHeight);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (newContentStyle?.color) {
|
|
49
|
+
chain = chain.setMark('textStyle', {
|
|
50
|
+
color: getRTEColor(newContentStyle.color, 'text'),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if ('backgroundColor' in (newContentStyle || {})) {
|
|
55
|
+
chain = newContentStyle.backgroundColor
|
|
56
|
+
? chain.setMark('highlight', {
|
|
57
|
+
color: getRTEColor(newContentStyle.backgroundColor, 'background'),
|
|
58
|
+
})
|
|
59
|
+
: chain.unsetHighlight();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (newContentStyle?.textAlign) {
|
|
63
|
+
chain = chain.setTextAlign(newContentStyle.textAlign);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const hasStrike = newContentStyle?.textDecoration?.includes('line-through');
|
|
67
|
+
const hasUnderline = newContentStyle?.textDecoration?.includes('underline');
|
|
68
|
+
|
|
69
|
+
chain = hasStrike ? chain.setStrike() : chain.unsetStrike();
|
|
70
|
+
chain = hasUnderline ? chain.setUnderline() : chain.unsetUnderline();
|
|
71
|
+
|
|
72
|
+
chain =
|
|
73
|
+
newContentStyle?.fontWeight === 'bold'
|
|
74
|
+
? chain.setBold()
|
|
75
|
+
: chain.unsetBold();
|
|
76
|
+
chain =
|
|
77
|
+
newContentStyle?.fontStyle === 'italic'
|
|
78
|
+
? chain.setItalic()
|
|
79
|
+
: chain.unsetItalic();
|
|
80
|
+
|
|
81
|
+
chain.run();
|
|
82
|
+
|
|
83
|
+
return externalEditor.getHTML();
|
|
84
|
+
};
|
|
85
|
+
|
|
5
86
|
/**
|
|
6
87
|
* Converts a Tiptap JSON document (ProseMirror document model) to an HTML
|
|
7
88
|
* string via Tiptap's schema-driven static renderer (renderRteToHtml →
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
BannerSectionBuilder,
|
|
14
14
|
BookingSectionBuilder,
|
|
15
|
+
SupportMeBuilder,
|
|
15
16
|
AboutSectionBuilder,
|
|
16
17
|
ContactFormBuilder,
|
|
17
18
|
DividerSectionBuilder,
|
|
@@ -50,6 +51,7 @@ const getBuilderRegistry = (): Record<BuilderType, BuilderConstructor> => ({
|
|
|
50
51
|
// Section builders (create)
|
|
51
52
|
[BUILDER_TYPES.CREATE_BANNER_SECTION]: BannerSectionBuilder,
|
|
52
53
|
[BUILDER_TYPES.CREATE_BOOKING_SECTION]: BookingSectionBuilder,
|
|
54
|
+
[BUILDER_TYPES.CREATE_SUPPORT_ME]: SupportMeBuilder,
|
|
53
55
|
[BUILDER_TYPES.CREATE_ABOUT_SECTION]: AboutSectionBuilder,
|
|
54
56
|
[BUILDER_TYPES.CREATE_CONTACT_FORM]: ContactFormBuilder,
|
|
55
57
|
[BUILDER_TYPES.CREATE_DIVIDER_SECTION]: DividerSectionBuilder,
|
|
@@ -77,6 +79,7 @@ const getBuilderRegistry = (): Record<BuilderType, BuilderConstructor> => ({
|
|
|
77
79
|
// Section builders (update)
|
|
78
80
|
[BUILDER_TYPES.UPDATE_BANNER_SECTION]: BannerSectionBuilder,
|
|
79
81
|
[BUILDER_TYPES.UPDATE_BOOKING_SECTION]: BookingSectionBuilder,
|
|
82
|
+
[BUILDER_TYPES.UPDATE_SUPPORT_ME]: SupportMeBuilder,
|
|
80
83
|
[BUILDER_TYPES.UPDATE_ABOUT_SECTION]: AboutSectionBuilder,
|
|
81
84
|
[BUILDER_TYPES.UPDATE_CONTACT_FORM]: ContactFormBuilder,
|
|
82
85
|
[BUILDER_TYPES.UPDATE_DIVIDER_SECTION]: DividerSectionBuilder,
|
|
@@ -29,6 +29,7 @@ export const BUILDER_TYPES = {
|
|
|
29
29
|
CREATE_TWITCH: 'create-twitch',
|
|
30
30
|
CREATE_TIKTOK: 'create-tiktok',
|
|
31
31
|
CREATE_BOOKING_SECTION: 'create-booking-section',
|
|
32
|
+
CREATE_SUPPORT_ME: 'create-support-me',
|
|
32
33
|
|
|
33
34
|
// Per-section builder types (update)
|
|
34
35
|
UPDATE_BANNER_SECTION: 'update-banner-section',
|
|
@@ -56,6 +57,7 @@ export const BUILDER_TYPES = {
|
|
|
56
57
|
UPDATE_TWITCH: 'update-twitch',
|
|
57
58
|
UPDATE_TIKTOK: 'update-tiktok',
|
|
58
59
|
UPDATE_BOOKING_SECTION: 'update-booking-section',
|
|
60
|
+
UPDATE_SUPPORT_ME: 'update-support-me',
|
|
59
61
|
|
|
60
62
|
// Section-level operations
|
|
61
63
|
DELETE_SECTION: 'delete-section',
|
|
@@ -38,6 +38,7 @@ export const AI_ASSISTANT_TOOL_NAMES = {
|
|
|
38
38
|
BUILD_TESTIMONIAL_SECTION: 'build_testimonial-section_section',
|
|
39
39
|
BUILD_DIVIDER_SECTION: 'build_divider-section_section',
|
|
40
40
|
BUILD_BOOKING_SECTION: 'build_booking-section_section',
|
|
41
|
+
BUILD_SUPPORT_ME_SECTION: 'build_support-me_section',
|
|
41
42
|
|
|
42
43
|
// Section-level operation tools
|
|
43
44
|
DELETE_SECTION: 'delete_section',
|
|
@@ -136,6 +137,9 @@ export const SECTION_TOOL_MAP: Record<string, { sectionType: SectionType }> = {
|
|
|
136
137
|
[AI_ASSISTANT_TOOL_NAMES.BUILD_BOOKING_SECTION]: {
|
|
137
138
|
sectionType: 'booking-section',
|
|
138
139
|
},
|
|
140
|
+
[AI_ASSISTANT_TOOL_NAMES.BUILD_SUPPORT_ME_SECTION]: {
|
|
141
|
+
sectionType: 'support-me',
|
|
142
|
+
},
|
|
139
143
|
};
|
|
140
144
|
|
|
141
145
|
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,67 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { availableButtonStyleConfigs } from '../../brand-page-maker/models/button-styles-config';
|
|
3
|
+
import {
|
|
4
|
+
ButtonBoxShadowSize,
|
|
5
|
+
ButtonSoftBoxShadowSize,
|
|
6
|
+
} from '../../brand-page/constants/misc.constants';
|
|
7
|
+
import { DEFAULT_BUTTON_SHADOW_COLOR } from '../../constants/ui';
|
|
8
|
+
import { buttonStylePresetCss } from './button-style-preset';
|
|
9
|
+
|
|
10
|
+
const flatPreset = availableButtonStyleConfigs.find(
|
|
11
|
+
(c) => c.id === 'BUTTON_STYLE_5',
|
|
12
|
+
)!;
|
|
13
|
+
const shadowPreset = availableButtonStyleConfigs.find((c) => c.hasShadow)!;
|
|
14
|
+
const softShadowPreset = availableButtonStyleConfigs.find(
|
|
15
|
+
(c) => c.hasSoftShadow,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
describe('buttonStylePresetCss', () => {
|
|
19
|
+
it('copies the preset shape and clears the shadow for a preset without one', () => {
|
|
20
|
+
expect(flatPreset.style.borderRadius).toBe('8px');
|
|
21
|
+
|
|
22
|
+
expect(
|
|
23
|
+
buttonStylePresetCss('BUTTON_STYLE_5', {
|
|
24
|
+
boxShadow: '3px 3px 0px 0px #123456',
|
|
25
|
+
}),
|
|
26
|
+
).toEqual({ ...flatPreset.style, boxShadow: '' });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('a shadow preset keeps the current shadow colour', () => {
|
|
30
|
+
const css = buttonStylePresetCss(shadowPreset.id, {
|
|
31
|
+
boxShadow: '3px 3px 0px 0px #123456',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(css?.boxShadow).toBe(`${ButtonBoxShadowSize} #123456`);
|
|
35
|
+
expect(css?.borderRadius).toBe(shadowPreset.style.borderRadius);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('a shadow preset uses the default colour when there is no shadow yet', () => {
|
|
39
|
+
expect(buttonStylePresetCss(shadowPreset.id, {})?.boxShadow).toBe(
|
|
40
|
+
`${ButtonBoxShadowSize} ${DEFAULT_BUTTON_SHADOW_COLOR}`,
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('keeps a current shadow it cannot parse', () => {
|
|
45
|
+
expect(
|
|
46
|
+
buttonStylePresetCss(shadowPreset.id, { boxShadow: 'not-a-shadow' })
|
|
47
|
+
?.boxShadow,
|
|
48
|
+
).toBe('not-a-shadow');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it.skipIf(!softShadowPreset)(
|
|
52
|
+
'a soft-shadow preset uses the soft size',
|
|
53
|
+
() => {
|
|
54
|
+
expect(buttonStylePresetCss(softShadowPreset!.id, {})?.boxShadow).toBe(
|
|
55
|
+
`${ButtonSoftBoxShadowSize} ${DEFAULT_BUTTON_SHADOW_COLOR}`,
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
it('returns undefined for an unknown preset', () => {
|
|
61
|
+
expect(buttonStylePresetCss('BUTTON_STYLE_999', {})).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('honours a restricted config list', () => {
|
|
65
|
+
expect(buttonStylePresetCss('BUTTON_STYLE_5', {}, [])).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
availableButtonStyleConfigs,
|
|
3
|
+
type ButtonStyleConfig,
|
|
4
|
+
} from '../../brand-page-maker/models/button-styles-config';
|
|
5
|
+
import {
|
|
6
|
+
ButtonBoxShadowSize,
|
|
7
|
+
ButtonSoftBoxShadowSize,
|
|
8
|
+
} from '../../brand-page/constants/misc.constants';
|
|
9
|
+
import { DEFAULT_BUTTON_SHADOW_COLOR } from '../../constants/ui';
|
|
10
|
+
import { CssShadowRE } from '../../utils/helpers';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The button CSS a preset contributes when it is selected: its shape (border radius,
|
|
14
|
+
* style, width) and its shadow rule. One rule shared by the editor's picker and the AI
|
|
15
|
+
* `update_global_styles` path — the site renders buttons from `buttonStyles.style`, so
|
|
16
|
+
* storing the preset id alone changes nothing on the page (BP-6082).
|
|
17
|
+
*
|
|
18
|
+
* Shadow: a shadow preset keeps the current shadow colour when one is set (default
|
|
19
|
+
* colour otherwise); a preset without a shadow clears it. `undefined` for an id that is
|
|
20
|
+
* not in `configs`.
|
|
21
|
+
*/
|
|
22
|
+
export const buttonStylePresetCss = (
|
|
23
|
+
presetId: string,
|
|
24
|
+
currentStyle: BrandPageCssStyle,
|
|
25
|
+
configs: readonly ButtonStyleConfig[] = availableButtonStyleConfigs,
|
|
26
|
+
): BrandPageCssStyle | undefined => {
|
|
27
|
+
const config = configs.find((c) => c.id === presetId);
|
|
28
|
+
|
|
29
|
+
if (!config) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const shadowFor = (size: string): string => {
|
|
34
|
+
const current = currentStyle.boxShadow;
|
|
35
|
+
|
|
36
|
+
if (!current) {
|
|
37
|
+
return `${size} ${DEFAULT_BUTTON_SHADOW_COLOR}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const match = CssShadowRE.exec(current);
|
|
41
|
+
|
|
42
|
+
return match ? `${size} ${match[1]}` : current;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const style: BrandPageCssStyle = { ...config.style };
|
|
46
|
+
|
|
47
|
+
if (config.hasShadow) {
|
|
48
|
+
style.boxShadow = shadowFor(ButtonBoxShadowSize);
|
|
49
|
+
} else if (config.hasSoftShadow) {
|
|
50
|
+
style.boxShadow = shadowFor(ButtonSoftBoxShadowSize);
|
|
51
|
+
} else {
|
|
52
|
+
style.boxShadow = '';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return style;
|
|
56
|
+
};
|
|
@@ -3,12 +3,7 @@ import type {
|
|
|
3
3
|
ButtonStyleConfig,
|
|
4
4
|
ButtonStyleConfigOption,
|
|
5
5
|
} from '@brand-page-maker/models';
|
|
6
|
-
import {
|
|
7
|
-
getColorFromCSSShadow,
|
|
8
|
-
hexToRGBA,
|
|
9
|
-
isHexColor,
|
|
10
|
-
CssShadowRE,
|
|
11
|
-
} from '@utils/helpers';
|
|
6
|
+
import { getColorFromCSSShadow, hexToRGBA, isHexColor } from '@utils/helpers';
|
|
12
7
|
import type {
|
|
13
8
|
BrandPageType,
|
|
14
9
|
TextStyleDeclaration,
|
|
@@ -35,7 +30,6 @@ import {
|
|
|
35
30
|
import type { OtherStyleConfigOption } from '@brand-page-maker/models';
|
|
36
31
|
import { assertUnreachable } from '../../../helpers/ts.helper';
|
|
37
32
|
import { isColorVarReference } from '@brand-page/utils';
|
|
38
|
-
import { DEFAULT_BUTTON_SHADOW_COLOR } from '../../../constants/ui';
|
|
39
33
|
import type {
|
|
40
34
|
SetGalleryTypePayload,
|
|
41
35
|
StyleTextFontFamilyOption,
|
|
@@ -50,6 +44,7 @@ import type { ContentItem } from '../../content-settings/models';
|
|
|
50
44
|
import { useUiPageSettingsStore } from '../../page-settings/store';
|
|
51
45
|
import { PAGE_SECTIONS } from '../../page-settings/models';
|
|
52
46
|
import { availableButtonStyleConfigs } from '@brand-page-maker/models';
|
|
47
|
+
import { buttonStylePresetCss } from '../button-style-preset';
|
|
53
48
|
import {
|
|
54
49
|
TRIGGER_TYPE,
|
|
55
50
|
UPGRADE_CTA_LOCATIONS,
|
|
@@ -343,42 +338,18 @@ export const useUiStyleSettingsStore = defineStore('uiStyleSettings', {
|
|
|
343
338
|
setSelectedButtonStyle(payload: { id: string }) {
|
|
344
339
|
this._setSelectedButtonStyle(payload);
|
|
345
340
|
|
|
346
|
-
|
|
347
|
-
|
|
341
|
+
// Shape + shadow derivation lives in buttonStylePresetCss so the AI metadata
|
|
342
|
+
// builder applies a preset exactly as this picker does (BP-6082).
|
|
343
|
+
const buttonStyle = buttonStylePresetCss(
|
|
344
|
+
payload.id,
|
|
345
|
+
this.buttonStyles.style,
|
|
346
|
+
this.availableButtonStyleConfigs,
|
|
348
347
|
);
|
|
349
348
|
|
|
350
|
-
if (!
|
|
349
|
+
if (!buttonStyle) {
|
|
351
350
|
return;
|
|
352
351
|
}
|
|
353
352
|
|
|
354
|
-
let buttonStyle: BrandPageCssStyle = {
|
|
355
|
-
...buttonStyleConfig.style,
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
if (buttonStyleConfig.hasShadow) {
|
|
359
|
-
if (!this.buttonStyles.style.boxShadow) {
|
|
360
|
-
buttonStyle.boxShadow = `${ButtonBoxShadowSize} ${DEFAULT_BUTTON_SHADOW_COLOR}`;
|
|
361
|
-
} else {
|
|
362
|
-
const match = CssShadowRE.exec(this.buttonStyles.style.boxShadow);
|
|
363
|
-
|
|
364
|
-
buttonStyle.boxShadow = match
|
|
365
|
-
? `${ButtonBoxShadowSize} ${match[1]}`
|
|
366
|
-
: this.buttonStyles.style.boxShadow;
|
|
367
|
-
}
|
|
368
|
-
} else if (buttonStyleConfig.hasSoftShadow) {
|
|
369
|
-
if (!this.buttonStyles.style.boxShadow) {
|
|
370
|
-
buttonStyle.boxShadow = `${ButtonSoftBoxShadowSize} ${DEFAULT_BUTTON_SHADOW_COLOR}`;
|
|
371
|
-
} else {
|
|
372
|
-
const match = CssShadowRE.exec(this.buttonStyles.style.boxShadow);
|
|
373
|
-
|
|
374
|
-
buttonStyle.boxShadow = match
|
|
375
|
-
? `${ButtonSoftBoxShadowSize} ${match[1]}`
|
|
376
|
-
: this.buttonStyles.style.boxShadow;
|
|
377
|
-
}
|
|
378
|
-
} else {
|
|
379
|
-
buttonStyle = { ...buttonStyle, boxShadow: '' };
|
|
380
|
-
}
|
|
381
|
-
|
|
382
353
|
this._setButtonStyle(buttonStyle);
|
|
383
354
|
|
|
384
355
|
onStyleChanged();
|