@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.
Files changed (112) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-Cn0M-eZ1.js → CreatedOnBrandCrowd-de-de-ystrO2LW.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-DRLMSDZr.js → CreatedOnBrandCrowd-en-us-BU7hNzTZ.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-BglTu73U.js → CreatedOnBrandCrowd-es-es-CbuxneQ3.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-Bj6YZ_el.js → CreatedOnBrandCrowd-fr-ca-BuNUGOFr.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-Bj6YZ_el.js → CreatedOnBrandCrowd-fr-fr-BuNUGOFr.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-DEoOmQ1F.js → CreatedOnBrandCrowd-pt-br-BehL88mc.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-DEoOmQ1F.js → CreatedOnBrandCrowd-pt-pt-BehL88mc.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-BZ7gTt4Z.js → CreatedOnDcom-de-de-CMxjFZt9.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-CD6Km5sH.js → CreatedOnDcom-en-us-2Cf6WBvx.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-CNpZHvEl.js → CreatedOnDcom-es-es-CATNnbTq.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-URS-ydJ1.js → CreatedOnDcom-fr-ca-DQtMfB-a.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-D2AVeA3H.js → CreatedOnDcom-fr-fr-C1_FWOQf.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-C32frnFs.js → CreatedOnDcom-pt-br-C0QOCW0L.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-TDYGjRXS.js → CreatedOnDcom-pt-pt-lHD-p6o4.js} +1 -1
  15. package/lib/src/brand-page/models/brand-page-context.model.d.ts +5 -0
  16. package/lib/src/brand-page/models/brand-page-context.model.js +8 -1
  17. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +21 -0
  18. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +21 -0
  19. package/lib/src/brand-page-maker/components/preview/Preview.mixin.d.ts +7 -0
  20. package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.d.ts +21 -0
  21. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +22 -1
  22. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +21 -0
  23. package/lib/src/brand-page-maker/components/seo-settings/UseExistingDomainModal.mixin.d.ts +21 -0
  24. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +21 -0
  25. package/lib/src/brand-page-maker/components/upgrade-to-premium-button/UpgradeToPremiumButton.mixin.d.ts +21 -0
  26. package/lib/src/brand-page-maker/composables/ai-chat/ai-builder-experiment.d.ts +10 -0
  27. package/lib/src/brand-page-maker/composables/ai-chat/ai-builder-experiment.js +11 -0
  28. package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +14 -4
  29. package/lib/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.d.ts +9 -2
  30. package/lib/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.js +20 -0
  31. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.d.ts +21 -0
  32. package/lib/src/clients/ai-assistant-client.d.ts +1 -0
  33. package/lib/src/clients/ai-assistant-client.js +1 -0
  34. package/lib/src/index.mjs +224 -223
  35. package/lib/src/{lib-Dm9AuOJL.js → lib-CqHUUPUW.js} +21920 -21691
  36. package/lib/src/metadata-builder/builders/global-font-propagation.d.ts +41 -0
  37. package/lib/src/metadata-builder/builders/global-font-propagation.js +146 -0
  38. package/lib/src/metadata-builder/builders/global-styles-builder.js +29 -1
  39. package/lib/src/metadata-builder/builders/header-builder.js +8 -0
  40. package/lib/src/metadata-builder/builders/reorder-pages-builder.d.ts +13 -0
  41. package/lib/src/metadata-builder/builders/reorder-pages-builder.js +49 -0
  42. package/lib/src/metadata-builder/builders/section-builders/index.d.ts +1 -0
  43. package/lib/src/metadata-builder/builders/section-builders/index.js +1 -0
  44. package/lib/src/metadata-builder/builders/section-builders/support-me-builder.d.ts +8 -0
  45. package/lib/src/metadata-builder/builders/section-builders/support-me-builder.js +25 -0
  46. package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +5 -2
  47. package/lib/src/metadata-builder/builders/section-field-maps.js +15 -0
  48. package/lib/src/metadata-builder/builders/section-field-utils.d.ts +6 -1
  49. package/lib/src/metadata-builder/builders/section-field-utils.js +48 -12
  50. package/lib/src/metadata-builder/builders/section-rules/support-me.rules.d.ts +2 -0
  51. package/lib/src/metadata-builder/builders/section-rules/support-me.rules.js +17 -0
  52. package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +9 -0
  53. package/lib/src/metadata-builder/helpers/rte.helpers.js +60 -1
  54. package/lib/src/metadata-builder/metadata-executor.js +3 -1
  55. package/lib/src/metadata-builder/models.d.ts +2 -0
  56. package/lib/src/metadata-builder/models.js +2 -0
  57. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +1 -0
  58. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +4 -0
  59. package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +14 -2
  60. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +1 -1
  61. package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +1 -1
  62. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +21 -0
  63. package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +1 -1
  64. package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +1 -1
  65. package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +1 -1
  66. package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +1 -1
  67. package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +1 -1
  68. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +21 -0
  69. package/lib/src/shared/page-settings/components/delete-page-modal/DeletePageModal.mixin.d.ts +1 -1
  70. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +21 -0
  71. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +21 -0
  72. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +28 -0
  73. package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +21 -0
  74. package/lib/src/shared/style-settings/button-style-preset.d.ts +12 -0
  75. package/lib/src/shared/style-settings/button-style-preset.js +39 -0
  76. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +21 -0
  77. package/lib/src/shared/style-settings/store/style-settings-store.js +6 -32
  78. package/lib/src/utils/minify-brand-page-metadata.d.ts +9 -0
  79. package/lib/src/utils/minify-brand-page-metadata.js +23 -1
  80. package/package.json +1 -12
  81. package/src/brand-page/models/brand-page-context.model.ts +14 -1
  82. package/src/brand-page-maker/components/ai-website-builder/AiWebsiteBuilder.vue +1 -1
  83. package/src/brand-page-maker/composables/ai-chat/ai-builder-experiment.test.ts +82 -0
  84. package/src/brand-page-maker/composables/ai-chat/ai-builder-experiment.ts +19 -0
  85. package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +18 -4
  86. package/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.ts +30 -0
  87. package/src/clients/ai-assistant-client.ts +1 -0
  88. package/src/metadata-builder/builders/global-font-propagation.test.ts +401 -0
  89. package/src/metadata-builder/builders/global-font-propagation.ts +238 -0
  90. package/src/metadata-builder/builders/global-styles-builder.test.ts +107 -0
  91. package/src/metadata-builder/builders/global-styles-builder.ts +38 -1
  92. package/src/metadata-builder/builders/header-builder.test.ts +45 -0
  93. package/src/metadata-builder/builders/header-builder.ts +20 -0
  94. package/src/metadata-builder/builders/reorder-pages-builder.ts +80 -0
  95. package/src/metadata-builder/builders/section-builders/index.ts +1 -0
  96. package/src/metadata-builder/builders/section-builders/support-me-builder.test.ts +177 -0
  97. package/src/metadata-builder/builders/section-builders/support-me-builder.ts +43 -0
  98. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +5 -2
  99. package/src/metadata-builder/builders/section-field-maps.ts +18 -0
  100. package/src/metadata-builder/builders/section-field-utils.test.ts +83 -0
  101. package/src/metadata-builder/builders/section-field-utils.ts +69 -18
  102. package/src/metadata-builder/builders/section-rules/support-me.rules.ts +39 -0
  103. package/src/metadata-builder/helpers/rte.helpers.ts +82 -1
  104. package/src/metadata-builder/metadata-executor.ts +3 -0
  105. package/src/metadata-builder/models.ts +2 -0
  106. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +4 -0
  107. package/src/metadata-builder/parsers/section-tool-arguments.ts +14 -1
  108. package/src/shared/style-settings/button-style-preset.test.ts +67 -0
  109. package/src/shared/style-settings/button-style-preset.ts +56 -0
  110. package/src/shared/style-settings/store/style-settings-store.ts +9 -38
  111. package/src/utils/minify-brand-page-metadata.test.ts +166 -1
  112. 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
- for (const [aiKey, modelKey] of Object.entries(styleMap)) {
202
- if (!(aiKey in args) || !args[aiKey] || typeof args[aiKey] !== 'object')
203
- continue;
204
-
205
- const aiStyle = args[aiKey] as Record<string, unknown>;
206
- const filtered: Record<string, unknown> = {};
207
-
208
- for (const [k, v] of Object.entries(aiStyle)) {
209
- const normalized = normalizeStyleValue(k, v);
210
-
211
- if (normalized !== undefined) filtered[k] = normalized;
224
+ try {
225
+ for (const [aiKey, modelKey] of Object.entries(styleMap)) {
226
+ if (!(aiKey in args) || !args[aiKey] || typeof args[aiKey] !== 'object')
227
+ continue;
228
+
229
+ const aiStyle = args[aiKey] as Record<string, unknown>;
230
+ const filtered: Record<string, unknown> = {};
231
+
232
+ for (const [k, v] of Object.entries(aiStyle)) {
233
+ const normalized = normalizeStyleValue(k, v);
234
+
235
+ if (normalized !== undefined) filtered[k] = normalized;
236
+ }
237
+
238
+ const resolved = resolveGlobalPresetIntoStyle(
239
+ content[modelKey] as object,
240
+ textStyleOptions,
241
+ );
242
+
243
+ record[modelKey] = { ...resolved, ...filtered, textStyleType: undefined };
244
+
245
+ // A colour patch without new text leaves the field's EXISTING inline
246
+ // textStyle marks in place — and inline marks override every style layer
247
+ // at render, so the old colour survives on a recoloured band (invisible
248
+ // text, AI QA 2026-07-05 rerun). Propagate the new colour into the
249
+ // stored HTML, mirroring global-font-propagation for font families.
250
+ const textKey = modelKey.endsWith('Style')
251
+ ? modelKey.slice(0, -'Style'.length)
252
+ : undefined;
253
+ const textAiKey = aiKey.endsWith('_style')
254
+ ? aiKey.slice(0, -'_style'.length)
255
+ : undefined;
256
+ const newColor = filtered.color;
257
+
258
+ if (
259
+ textKey &&
260
+ typeof newColor === 'string' &&
261
+ newColor &&
262
+ !(textAiKey && textAiKey in args) &&
263
+ typeof record[textKey] === 'string' &&
264
+ hasRTEContent(record[textKey] as string)
265
+ ) {
266
+ record[textKey] = applyColorToHtml(record[textKey] as string, newColor);
267
+ }
212
268
  }
213
-
214
- const resolved = resolveGlobalPresetIntoStyle(
215
- content[modelKey] as object,
216
- textStyleOptions,
217
- );
218
-
219
- record[modelKey] = { ...resolved, ...filtered, textStyleType: undefined };
269
+ } finally {
270
+ editor?.destroy();
220
271
  }
221
272
  };
@@ -0,0 +1,39 @@
1
+ import {
2
+ MAX_TITLE_LENGTH,
3
+ MAX_DESCRIPTION_LENGTH,
4
+ } from '../../../brand-page/constants/font.constants';
5
+ import {
6
+ DISPLAY_STYLE_PREVIEW,
7
+ DISPLAY_STYLE_COLLAPSED,
8
+ } from '../../../brand-page/constants';
9
+ import {
10
+ enumCheck,
11
+ runChecks,
12
+ styleCheck,
13
+ textCheck,
14
+ type SectionCheck,
15
+ type ValidationContext,
16
+ } from '../../sanitizers/section-args-validator';
17
+
18
+ const CHECKS: SectionCheck[] = [
19
+ textCheck('title', MAX_TITLE_LENGTH),
20
+ textCheck('description', MAX_DESCRIPTION_LENGTH),
21
+ textCheck('success_message', MAX_DESCRIPTION_LENGTH),
22
+ styleCheck('title_style'),
23
+ styleCheck('heading_style'),
24
+ styleCheck('description_style'),
25
+ styleCheck('item_text_style'),
26
+ styleCheck('button_text_style'),
27
+ enumCheck(
28
+ 'display_style',
29
+ [DISPLAY_STYLE_PREVIEW, DISPLAY_STYLE_COLLAPSED],
30
+ 'support-me',
31
+ ),
32
+ ];
33
+
34
+ export function validateSupportMe(
35
+ args: Record<string, unknown>,
36
+ ctx: ValidationContext,
37
+ ): string[] {
38
+ return runChecks(CHECKS, args, ctx);
39
+ }
@@ -1,7 +1,88 @@
1
1
  import type { RichTextValue } from '../../brand-page';
2
- import { renderRteToHtml } from '../../brand-page-maker/utils';
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
- const buttonStyleConfig = this.availableButtonStyleConfigs.find(
347
- (c) => c.id === payload.id,
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 (!buttonStyleConfig) {
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();