@designcrowd/library.brand-page 6.0.21 → 6.0.22

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 (82) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-C52IyXZu.js → CreatedOnBrandCrowd-de-de-BS8dQhaZ.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-DhTy_dEV.js → CreatedOnBrandCrowd-en-us-45ZyXSxn.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-BJelIAQ4.js → CreatedOnBrandCrowd-es-es-B1K3Ojw-.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-DwJohy0T.js → CreatedOnBrandCrowd-fr-ca-4v9c20Q9.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-DwJohy0T.js → CreatedOnBrandCrowd-fr-fr-4v9c20Q9.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-CMzKbxdG.js → CreatedOnBrandCrowd-pt-br-BdDmm1GE.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-CMzKbxdG.js → CreatedOnBrandCrowd-pt-pt-BdDmm1GE.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-DAy1GHrw.js → CreatedOnDcom-de-de-Dc8rOxAE.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-CgBDAkJN.js → CreatedOnDcom-en-us-Bq6rusW6.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-Ds9d5rem.js → CreatedOnDcom-es-es-C5gNEuoi.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-CZL0BQ-u.js → CreatedOnDcom-fr-ca-NL2KoADg.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-BVYh5Wvo.js → CreatedOnDcom-fr-fr-DZaLN0j-.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-1hsWNXfq.js → CreatedOnDcom-pt-br-DXZvo45o.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-DvM0N-3M.js → CreatedOnDcom-pt-pt-GUJ4aTM2.js} +1 -1
  15. package/lib/src/brand-page/components/collapsible/Collapsible.mixin.d.ts +11 -0
  16. package/lib/src/brand-page/components/collapsible/Collapsible.mixin.js +13 -1
  17. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.d.ts +1 -0
  18. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +12 -3
  19. package/lib/src/brand-page/components/mailing-list/MailingList.mixin.js +5 -0
  20. package/lib/src/brand-page/components/payment/Payment.mixin.js +5 -0
  21. package/lib/src/brand-page/models/index.d.ts +1 -0
  22. package/lib/src/brand-page/models/payment.model.d.ts +2 -0
  23. package/lib/src/brand-page/utils/button.util.d.ts +5 -0
  24. package/lib/src/brand-page/utils/button.util.js +13 -0
  25. package/lib/src/brand-page/utils/migration/migration-19.d.ts +2 -0
  26. package/lib/src/brand-page/utils/migration/migration-19.js +41 -0
  27. package/lib/src/brand-page/utils/migration.util.js +11 -0
  28. package/lib/src/brand-page/utils/view-model.util.js +1 -0
  29. package/lib/src/brand-page-maker/assessors/contact-form-visibility.assessor.d.ts +1 -0
  30. package/lib/src/brand-page-maker/assessors/contact-form-visibility.assessor.js +16 -7
  31. package/lib/src/brand-page-maker/constants/config-settings-constant.js +40 -3
  32. package/lib/src/brand-page-maker/default-getters/contact-form-card-color.default-getter.d.ts +8 -0
  33. package/lib/src/brand-page-maker/default-getters/contact-form-card-color.default-getter.js +16 -0
  34. package/lib/src/brand-page-maker/default-getters/index.d.ts +1 -0
  35. package/lib/src/brand-page-maker/default-getters/index.js +1 -0
  36. package/lib/src/brand-page-maker/models/button-styles-config.js +15 -3
  37. package/lib/src/brand-page-maker/models/content-settings-configs.d.ts +2 -1
  38. package/lib/src/brand-page-maker/models/content-settings.js +15 -3
  39. package/lib/src/css/library.brand-page-brandCrowd.min.css +96 -68
  40. package/lib/src/css/library.brand-page-brandPage.min.css +96 -68
  41. package/lib/src/css/library.brand-page-designCom.min.css +96 -68
  42. package/lib/src/css/library.brand-page-preview.min.css +96 -68
  43. package/lib/src/index.mjs +1 -1
  44. package/lib/src/{lib-CK2FPBv5.js → lib-Dh4vrmod.js} +18439 -18090
  45. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +5 -0
  46. package/lib/src/shared/content-settings/models.d.ts +1 -0
  47. package/lib/src/shared/content-settings/store/content-settings-store.js +3 -0
  48. package/package.json +2 -2
  49. package/src/brand-page/components/button/ButtonStyleWrapper.vue +112 -44
  50. package/src/brand-page/components/button/ButtonStyleWrapper.vue.test.ts +167 -0
  51. package/src/brand-page/components/collapsible/Collapsible.mixin.ts +14 -1
  52. package/src/brand-page/components/contact-form/ContactForm.mixin.test.ts +18 -0
  53. package/src/brand-page/components/contact-form/ContactForm.mixin.ts +15 -3
  54. package/src/brand-page/components/contact-form/ContactForm.vue +97 -36
  55. package/src/brand-page/components/mailing-list/MailingList.mixin.ts +6 -0
  56. package/src/brand-page/components/mailing-list/MailingList.vue +18 -10
  57. package/src/brand-page/components/payment/Payment.mixin.ts +6 -0
  58. package/src/brand-page/components/payment/Payment.vue +43 -24
  59. package/src/brand-page/components/payment/RequestPaymentStripe.vue +26 -22
  60. package/src/brand-page/components/shop/Shop.vue +15 -6
  61. package/src/brand-page/models/index.ts +1 -0
  62. package/src/brand-page/models/payment.model.ts +2 -0
  63. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
  64. package/src/brand-page/utils/button.util.ts +17 -0
  65. package/src/brand-page/utils/migration/migration-19.test.ts +172 -0
  66. package/src/brand-page/utils/migration/migration-19.ts +65 -0
  67. package/src/brand-page/utils/migration.util.ts +12 -0
  68. package/src/brand-page/utils/view-model.util.ts +1 -0
  69. package/src/brand-page-maker/assessors/contact-form-visibility.assessor.ts +34 -26
  70. package/src/brand-page-maker/constants/config-settings-constant.ts +43 -2
  71. package/src/brand-page-maker/default-getters/contact-form-card-color.default-getter.ts +23 -0
  72. package/src/brand-page-maker/default-getters/index.ts +1 -0
  73. package/src/brand-page-maker/models/button-styles-config.ts +15 -3
  74. package/src/brand-page-maker/models/content-settings-configs.ts +4 -1
  75. package/src/brand-page-maker/models/content-settings.ts +16 -3
  76. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +6 -0
  77. package/src/shared/content-settings/models.ts +3 -0
  78. package/src/shared/content-settings/store/content-settings-store.ts +3 -0
  79. package/src/shared/style-settings/components/StyleSettings.vue +1 -0
  80. package/lib/src/metadata-builder/builders/reorder-pages-builder.d.ts +0 -13
  81. package/lib/src/metadata-builder/builders/reorder-pages-builder.js +0 -49
  82. package/src/metadata-builder/builders/reorder-pages-builder.ts +0 -80
@@ -171,6 +171,11 @@ export default defineComponent({
171
171
  return this.field.showToggle;
172
172
  },
173
173
  disableTransparent() {
174
+ // Supports a function of brandPageType (like variant/min/maxLength) so a
175
+ // field can disable transparent only on some website types.
176
+ if (typeof this.field.disableTransparent === 'function') {
177
+ return this.field.disableTransparent(this.brandPageType);
178
+ }
174
179
  return this.field.disableTransparent || false;
175
180
  },
176
181
  plainTextValue() {
@@ -71,6 +71,7 @@ export type FieldGroup = ContentSettingsFieldGroup & {
71
71
  export interface CreateInitialContentOptions {
72
72
  userState: UserState;
73
73
  brandPageType: BrandPageType;
74
+ buttonFillColor?: string;
74
75
  }
75
76
  export interface AddContentOption<TContent> {
76
77
  type: ContentType;
@@ -5,6 +5,7 @@ import { UI_EVENT_SOURCES } from '../../models';
5
5
  import { CONTENT_SETTINGS_DISPLAY_MODES, ITEM_CATEGORIES, ITEM_CATEGORY_NAMES, } from '@brand-page-maker/models';
6
6
  import { useUiStore, useBrandPageMakerUserStore, useBrandPageMakerStore, useVideoStore, } from '@brand-page-maker/stores';
7
7
  import { useHistoryStore } from '@brand-page-maker/stores/history/history-store';
8
+ import { useUiStyleSettingsStore } from '@shared/style-settings/store';
8
9
  import { useUiSideBarStore } from '../../side-bar/store';
9
10
  import { createDefaultContentItemGroupState, createDefaultContentItemState, } from '../../navigation-style-settings/store';
10
11
  import { adminValidatorsDict, dummyValidator, validatorDict, } from '@brand-page-maker/validators';
@@ -358,6 +359,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
358
359
  addItemByType({ type, index, }) {
359
360
  const userStore = useBrandPageMakerUserStore();
360
361
  const brandPageMakerStore = useBrandPageMakerStore();
362
+ const styleSettingsStore = useUiStyleSettingsStore();
361
363
  const option = this.addContentOptions.find((o) => o.type === type);
362
364
  if (!option) {
363
365
  console.error(`Unable to locate add content option type for '${type}'`);
@@ -366,6 +368,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
366
368
  const content = option.createInitialContent({
367
369
  userState: userStore.$state,
368
370
  brandPageType: brandPageMakerStore.brandPageType,
371
+ buttonFillColor: styleSettingsStore.buttonFillColor,
369
372
  });
370
373
  const settingsConfig = CONTENT_SETTINGS_CONFIGS()[content.type];
371
374
  const fields = Object.keys(content)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@designcrowd/library.brand-page",
3
3
  "description": "A collection of Maker/Designer applications",
4
- "version": "6.0.21",
4
+ "version": "6.0.22",
5
5
  "author": "Design.com Eng",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -71,7 +71,7 @@
71
71
  },
72
72
  "dependencies": {
73
73
  "@ckpack/vue-color": "1.6.0",
74
- "@designcrowd/fe-shared-lib": "2.0.0",
74
+ "@designcrowd/fe-shared-lib": "2.0.7",
75
75
  "@pinia/nuxt": "0.11.3",
76
76
  "@storybook/react": "9.0.4",
77
77
  "@tiptap/extension-bubble-menu": "^2.14.0",
@@ -3,8 +3,14 @@
3
3
  class="bp-style-wrapper"
4
4
  :class="{
5
5
  'bp-button-arrow': isArrowIcon,
6
- 'tw-flex tw-justify-center tw-w-full': isBrushStrokeIcon,
7
- 'tw-relative tw-mb-6': isPaperIcon,
6
+ 'tw-flex tw-justify-center': isBrushStrokeIcon,
7
+ 'bp-button-shape': isPaperIcon,
8
+ 'tw-mb-6': isPaperIcon && !noMargin,
9
+ 'bp-style-wrapper--no-margin': isDecorative && noMargin,
10
+ 'tw-mt-4': isDecorative && noMargin,
11
+ 'tw-opacity-50': disabled,
12
+ 'tw-w-full': isDecorative && fullWidth,
13
+ 'tw-w-fit': isDecorative && !fullWidth,
8
14
  }"
9
15
  >
10
16
  <template v-if="isBrushStrokeIcon">
@@ -25,21 +31,20 @@
25
31
  />
26
32
  </template>
27
33
  <template v-else-if="isPaperIcon">
34
+ <!-- Torn-paper: the fe-shared button-paper icon, stretched to the button box
35
+ (preserveAspectRatio none), sits behind the transparent button as its
36
+ shape. Inert (pointer-events:none) inset layer, so it fills exactly,
37
+ doesn't inflate the button's size, and doesn't capture clicks
38
+ (BP-5882/BP-5883). -->
28
39
  <IconV2
29
- name="button-paper-top"
30
- view-box="41 0 120 16"
31
- size="auto"
32
- class="tw-absolute tw-w-full bp-button-paper-top"
33
- :style="paperIconStyle"
40
+ name="button-paper"
41
+ view-box="0 0 94 28"
42
+ size="full"
43
+ preserve-aspect-ratio="none"
44
+ class="bp-icon-shape"
45
+ :style="{ color: iconColor }"
34
46
  />
35
47
  <slot :inner-class="'tw-px-5'" />
36
- <IconV2
37
- name="button-paper-bottom"
38
- view-box="41 0 120 16"
39
- size="auto"
40
- class="tw-absolute tw-w-full bp-button-paper-bottom"
41
- :style="paperIconStyle"
42
- />
43
48
  </template>
44
49
  <template v-else>
45
50
  <slot :inner-class="'tw-px-5'" />
@@ -50,25 +55,54 @@
50
55
  <script setup lang="ts">
51
56
  import { computed } from 'vue';
52
57
  import { Icon as IconV2 } from '@designcrowd/fe-shared-lib';
58
+ import {
59
+ BUTTON_STYLE_ARROW,
60
+ BUTTON_STYLE_BRUSHSTROKE,
61
+ BUTTON_STYLE_PAPER,
62
+ isDecorativeButtonStyle,
63
+ } from '../../utils/button.util';
53
64
 
54
- const props = defineProps<{
55
- selectedButtonStyleId?: string;
56
- iconColor?: string;
57
- }>();
65
+ const props = withDefaults(
66
+ defineProps<{
67
+ selectedButtonStyleId?: string;
68
+ iconColor?: string;
69
+ // Mirror the host button's width so the decoration matches its box.
70
+ // Defaults to true to preserve the original (full-width link) behaviour.
71
+ fullWidth?: boolean;
72
+ // Set by submit buttons (contact form, payment, mailing list, shop). Makes a
73
+ // decorative button sit flush at the container's bottom edge: drops the paper
74
+ // style's bottom margin, zeroes the host button's own margins (which would
75
+ // otherwise shift it out of sync with its wrapper-anchored decoration), and
76
+ // moves the top gap onto the wrapper. Standalone buttons keep it off (BP-5800).
77
+ noMargin?: boolean;
78
+ // Dims the whole wrapper to 50% when the host button is disabled — for every
79
+ // style, so disabled reads consistently. (Decorative styles were previously
80
+ // exempt to avoid a muddy tone with the old absolute-overlay rendering; the
81
+ // torn-paper mask removed that rendering, so decorative buttons now fade like
82
+ // the rest. BP-5882/BP-5883.)
83
+ disabled?: boolean;
84
+ }>(),
85
+ {
86
+ selectedButtonStyleId: undefined,
87
+ iconColor: undefined,
88
+ fullWidth: true,
89
+ noMargin: false,
90
+ disabled: false,
91
+ },
92
+ );
58
93
 
59
94
  const isPaperIcon = computed(
60
- () => props.selectedButtonStyleId === 'BUTTON_STYLE_21',
95
+ () => props.selectedButtonStyleId === BUTTON_STYLE_PAPER,
61
96
  );
62
97
  const isArrowIcon = computed(
63
- () => props.selectedButtonStyleId === 'BUTTON_STYLE_20',
98
+ () => props.selectedButtonStyleId === BUTTON_STYLE_ARROW,
64
99
  );
65
100
  const isBrushStrokeIcon = computed(
66
- () => props.selectedButtonStyleId === 'BUTTON_STYLE_19',
101
+ () => props.selectedButtonStyleId === BUTTON_STYLE_BRUSHSTROKE,
102
+ );
103
+ const isDecorative = computed(() =>
104
+ isDecorativeButtonStyle(props.selectedButtonStyleId),
67
105
  );
68
-
69
- const paperIconStyle = computed(() => ({
70
- '--icon-color': props.iconColor,
71
- }));
72
106
 
73
107
  const brushLeftStyle = computed(() => ({
74
108
  '--icon-color': props.iconColor,
@@ -86,37 +120,71 @@ const brushRightStyle = computed(() => ({
86
120
  </script>
87
121
 
88
122
  <style scoped>
89
- .bp-button-paper-top {
90
- @apply tw-translate-y-[-10%] tw-text-[var(--icon-color)];
123
+ /* Decorative styles on submit buttons render flush at the bottom. Zero the host
124
+ button's own margins: its decoration is positioned relative to the WRAPPER, so a
125
+ margin on the button itself shifts it out of alignment with the decoration. The
126
+ top gap is instead applied to the wrapper (tw-mt-4 above) so the whole decorated
127
+ unit moves together and stays flush at the container's bottom edge (BP-5800). */
128
+ .bp-style-wrapper--no-margin :deep(button) {
129
+ margin-top: 0;
130
+ margin-bottom: 0;
91
131
  }
92
132
 
93
- .bp-button-paper-bottom {
94
- @apply tw-translate-y-[-4%] tw-text-[var(--icon-color)];
95
- }
96
-
97
- .bp-button-paper-top :deep(svg),
98
- .bp-button-paper-bottom :deep(svg) {
99
- width: 100%;
100
- height: auto;
133
+ /* Brushstroke: flanking brush-end icons are transformed over the button's
134
+ left/right edges, so they must not capture clicks (BP-5882). */
135
+ .bp-icon-brush-left,
136
+ .bp-icon-brush-right {
137
+ pointer-events: none;
101
138
  }
102
-
103
139
  .bp-icon-brush-left :deep(svg) {
104
140
  transform: translateX(-99%);
105
141
  width: auto;
106
142
  }
107
-
108
143
  .bp-icon-brush-right :deep(svg) {
109
144
  width: auto;
110
145
  }
111
146
 
112
- .bp-button-arrow,
113
- .device-wrapper--mobile .bp-button-arrow {
114
- clip-path: polygon(5% 100%, 0% 50%, 5% 0%, 95% 0%, 100% 50%, 95% 100%);
147
+ /* Torn-paper: the button-paper icon sits BEHIND the button as its shape. The
148
+ wrapper is the positioning context; the icon is an inert inset layer that fills
149
+ the button box exactly (inset:0 + preserveAspectRatio none) without inflating
150
+ its size or capturing clicks (BP-5882/BP-5883). The button's own fill is made
151
+ transparent so the icon's colour shows through. */
152
+ .bp-button-shape {
153
+ position: relative;
154
+ }
155
+ .bp-icon-shape {
156
+ position: absolute;
157
+ inset: 0;
158
+ width: 100%;
159
+ height: 100%;
160
+ pointer-events: none;
161
+ }
162
+ .bp-button-shape :deep(button),
163
+ .bp-button-shape :deep(a) {
164
+ position: relative; /* sit above the inert background */
165
+ /* !important to override the host button's inline background-color. */
166
+ background-color: transparent !important;
167
+ }
168
+ /* button-paper ships hardcoded fill="black"; recolour to the button's fill. CSS
169
+ fill overrides the presentation attribute; clip-path rects are geometric so
170
+ recolouring them is harmless. */
171
+ .bp-icon-shape :deep(path),
172
+ .bp-icon-shape :deep(rect) {
173
+ fill: currentColor;
115
174
  }
116
175
 
117
- @media (min-width: 767px) {
118
- .bp-button-arrow {
119
- clip-path: polygon(3% 100%, 0% 50%, 3% 0%, 97% 0%, 100% 50%, 97% 100%);
120
- }
176
+ .bp-button-arrow,
177
+ .device-wrapper--mobile .bp-button-arrow {
178
+ /* Fixed-px arrowhead (not %) so the chevron is equally sharp at any button
179
+ width — a %-based point collapses on narrow buttons (e.g. the 120px contact
180
+ form submit) and only reads as an arrow on wide ones. */
181
+ clip-path: polygon(
182
+ 12px 100%,
183
+ 0 50%,
184
+ 12px 0,
185
+ calc(100% - 12px) 0,
186
+ 100% 50%,
187
+ calc(100% - 12px) 100%
188
+ );
121
189
  }
122
190
  </style>
@@ -0,0 +1,167 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import ButtonStyleWrapper from './ButtonStyleWrapper.vue';
3
+
4
+ // The three decorative styles that ONLY render via this wrapper (BP-5546).
5
+ const BRUSHSTROKE = 'BUTTON_STYLE_19';
6
+ const ARROW = 'BUTTON_STYLE_20';
7
+ const PAPER = 'BUTTON_STYLE_21';
8
+
9
+ const HOST_SLOT = '<button class="host-button">Send</button>';
10
+
11
+ const mountWrapper = (props: Record<string, unknown> = {}) =>
12
+ shallowMount(ButtonStyleWrapper, {
13
+ props,
14
+ slots: { default: HOST_SLOT },
15
+ });
16
+
17
+ describe('ButtonStyleWrapper', () => {
18
+ describe('renders the host button (slot) for every style', () => {
19
+ test.each([
20
+ ['no style', undefined],
21
+ ['plain css style', 'BUTTON_STYLE_1'],
22
+ ['brushstroke', BRUSHSTROKE],
23
+ ['arrow', ARROW],
24
+ ['paper', PAPER],
25
+ ])('%s', (_label, selectedButtonStyleId) => {
26
+ const wrapper = mountWrapper({ selectedButtonStyleId });
27
+
28
+ expect(wrapper.find('button.host-button').exists()).toBe(true);
29
+ });
30
+ });
31
+
32
+ describe('decorative styles draw their SVG decoration', () => {
33
+ test('brushstroke (19) renders flanking brush ends', () => {
34
+ const wrapper = mountWrapper({ selectedButtonStyleId: BRUSHSTROKE });
35
+
36
+ expect(wrapper.classes()).toEqual(
37
+ expect.arrayContaining(['tw-flex', 'tw-justify-center']),
38
+ );
39
+ expect(wrapper.find('.bp-icon-brush-left').exists()).toBe(true);
40
+ expect(wrapper.find('.bp-icon-brush-right').exists()).toBe(true);
41
+ });
42
+
43
+ test('arrow (20) clips the button into a chevron', () => {
44
+ const wrapper = mountWrapper({ selectedButtonStyleId: ARROW });
45
+
46
+ expect(wrapper.classes()).toContain('bp-button-arrow');
47
+ });
48
+
49
+ test('paper (21) renders the torn button-paper icon behind the button', () => {
50
+ const wrapper = mountWrapper({ selectedButtonStyleId: PAPER });
51
+
52
+ // Icon shape behind the button — not absolute overlay elements (BP-5882/5883).
53
+ expect(wrapper.classes()).toEqual(
54
+ expect.arrayContaining(['bp-button-shape', 'tw-mb-6']),
55
+ );
56
+ expect(wrapper.find('.bp-button-paper-top').exists()).toBe(false);
57
+ expect(wrapper.find('.bp-button-paper-bottom').exists()).toBe(false);
58
+ // The torn shape is the fe-shared button-paper icon, stretched to the button.
59
+ const shape = wrapper.find('.bp-icon-shape');
60
+
61
+ expect(shape.exists()).toBe(true);
62
+ expect(shape.attributes('name')).toBe('button-paper');
63
+ });
64
+ });
65
+
66
+ describe('submit buttons render decorative styles flush (BP-5800)', () => {
67
+ test.each([BRUSHSTROKE, ARROW, PAPER])(
68
+ '%s gets the no-margin class + wrapper top margin when noMargin is set',
69
+ (selectedButtonStyleId) => {
70
+ const wrapper = mountWrapper({ selectedButtonStyleId, noMargin: true });
71
+
72
+ // The top gap moves onto the wrapper so the decoration stays synced;
73
+ // the CSS zeroes the host button's own margins.
74
+ expect(wrapper.classes()).toContain('bp-style-wrapper--no-margin');
75
+ expect(wrapper.classes()).toContain('tw-mt-4');
76
+ },
77
+ );
78
+
79
+ test('paper drops its bottom margin when noMargin is set', () => {
80
+ const wrapper = mountWrapper({
81
+ selectedButtonStyleId: PAPER,
82
+ noMargin: true,
83
+ });
84
+
85
+ expect(wrapper.classes()).not.toContain('tw-mb-6');
86
+ });
87
+
88
+ test('non-decorative styles never get the no-margin class', () => {
89
+ const wrapper = mountWrapper({
90
+ selectedButtonStyleId: 'BUTTON_STYLE_1',
91
+ noMargin: true,
92
+ });
93
+
94
+ expect(wrapper.classes()).not.toContain('bp-style-wrapper--no-margin');
95
+ });
96
+ });
97
+
98
+ describe('disabled dimming (BP-5800)', () => {
99
+ test('dims non-decorative buttons when disabled', () => {
100
+ const wrapper = mountWrapper({
101
+ selectedButtonStyleId: 'BUTTON_STYLE_1',
102
+ disabled: true,
103
+ });
104
+
105
+ expect(wrapper.classes()).toContain('tw-opacity-50');
106
+ });
107
+
108
+ test.each([BRUSHSTROKE, ARROW, PAPER])(
109
+ 'dims decorative style %s when disabled (consistent with other styles)',
110
+ (selectedButtonStyleId) => {
111
+ const wrapper = mountWrapper({ selectedButtonStyleId, disabled: true });
112
+
113
+ expect(wrapper.classes()).toContain('tw-opacity-50');
114
+ },
115
+ );
116
+
117
+ test('is not dimmed by default', () => {
118
+ const wrapper = mountWrapper({ selectedButtonStyleId: 'BUTTON_STYLE_1' });
119
+
120
+ expect(wrapper.classes()).not.toContain('tw-opacity-50');
121
+ });
122
+ });
123
+
124
+ describe('non-decorative styles are a layout no-op', () => {
125
+ test.each([
126
+ ['no style', undefined],
127
+ ['css style', 'BUTTON_STYLE_1'],
128
+ ])(
129
+ '%s adds no decoration and no width class',
130
+ (_label, selectedButtonStyleId) => {
131
+ const wrapper = mountWrapper({ selectedButtonStyleId });
132
+
133
+ expect(wrapper.classes()).not.toContain('bp-button-arrow');
134
+ expect(wrapper.find('.bp-icon-brush-left').exists()).toBe(false);
135
+ expect(wrapper.find('.bp-button-paper-top').exists()).toBe(false);
136
+ // Width-neutral: must not force the host's width either way.
137
+ expect(wrapper.classes()).not.toContain('tw-w-full');
138
+ expect(wrapper.classes()).not.toContain('tw-w-fit');
139
+ },
140
+ );
141
+ });
142
+
143
+ describe('width is preserved via the fullWidth prop', () => {
144
+ test.each([BRUSHSTROKE, ARROW, PAPER])(
145
+ '%s stretches full-width when fullWidth (default)',
146
+ (selectedButtonStyleId) => {
147
+ const wrapper = mountWrapper({ selectedButtonStyleId });
148
+
149
+ expect(wrapper.classes()).toContain('tw-w-full');
150
+ expect(wrapper.classes()).not.toContain('tw-w-fit');
151
+ },
152
+ );
153
+
154
+ test.each([BRUSHSTROKE, ARROW, PAPER])(
155
+ '%s shrinks to the host box when fullWidth is false',
156
+ (selectedButtonStyleId) => {
157
+ const wrapper = mountWrapper({
158
+ selectedButtonStyleId,
159
+ fullWidth: false,
160
+ });
161
+
162
+ expect(wrapper.classes()).toContain('tw-w-fit');
163
+ expect(wrapper.classes()).not.toContain('tw-w-full');
164
+ },
165
+ );
166
+ });
167
+ });
@@ -28,6 +28,11 @@ export default defineComponent({
28
28
  required: false,
29
29
  default: undefined,
30
30
  },
31
+ backgroundStyle: {
32
+ type: Object as PropType<BrandPageCssStyle>,
33
+ required: false,
34
+ default: undefined,
35
+ },
31
36
  context: {
32
37
  type: Object as PropType<BrandPageContext>,
33
38
  required: true,
@@ -45,10 +50,18 @@ export default defineComponent({
45
50
  containerStyle.borderRadius = '26px';
46
51
  }
47
52
 
48
- if (this.cardStyle) {
53
+ // Guarded on backgroundColor so an empty {} never blanks the container
54
+ // (BP/BC contact-form card colour defaults to the button fill).
55
+ if (this.cardStyle?.backgroundColor) {
49
56
  containerStyle.backgroundColor = this.cardStyle.backgroundColor;
50
57
  }
51
58
 
59
+ // Own background colour decouples the container from the button fill (BP-5800).
60
+ // Guarded on backgroundColor so an empty {} never blanks the container.
61
+ if (this.backgroundStyle?.backgroundColor) {
62
+ containerStyle.backgroundColor = this.backgroundStyle.backgroundColor;
63
+ }
64
+
52
65
  return containerStyle;
53
66
  },
54
67
  arrowStyle(): BrandPageCssStyle {
@@ -25,6 +25,24 @@ describe('ContactForm disclosureColor', () => {
25
25
  expect(color).not.toBe('#ff0000');
26
26
  });
27
27
 
28
+ test('brand.page / brand.contact anchor to the card colour when set', () => {
29
+ const color = disclosureColor({
30
+ context: { isBrandSite: false },
31
+ hasCard: false,
32
+ isDividedContent: false,
33
+ hasBgColor: false,
34
+ viewModel: {
35
+ cardStyle: { backgroundColor: '#101010' }, // dark card colour
36
+ buttonStyles: { style: { backgroundColor: '#f5f0e6' } }, // light fill
37
+ titleStyle: { color: '#ff0000' },
38
+ },
39
+ });
40
+
41
+ // Anchored to the dark card colour → a lighter colour, not the title red.
42
+ expect(color).toBeDefined();
43
+ expect(color).not.toBe('#ff0000');
44
+ });
45
+
28
46
  test('brandSite card layout anchors to the card background', () => {
29
47
  const color = disclosureColor({
30
48
  context: { isBrandSite: true },
@@ -13,6 +13,7 @@ import {
13
13
  getNameValidationError,
14
14
  getPhoneValidationError,
15
15
  } from '@brand-page/utils';
16
+ import { isDecorativeButtonStyle } from '../../utils/button.util';
16
17
  import brandPageClient from '../../clients/brand-page.client';
17
18
  import { isEmail } from '../../../utils/validator.util';
18
19
  import {
@@ -78,6 +79,15 @@ export default defineComponent({
78
79
  submitError: undefined as string | undefined,
79
80
  }),
80
81
  computed: {
82
+ // Decorative styles (brushstroke/arrow/paper) render a shape that needs width
83
+ // to read — at pure w-fit the arrow chevron collapses. Give the submit button a
84
+ // ~200px min-width so the decoration looks intentional without going full-width.
85
+ isDecorativeSubmitStyle(): boolean {
86
+ return isDecorativeButtonStyle(
87
+ this.viewModel.buttonStyles?.selectedButtonStyleId,
88
+ );
89
+ },
90
+
81
91
  submitButtonStyle(): BrandPageCssStyle {
82
92
  const buttonStyle = { ...this.viewModel.buttonStyles?.style };
83
93
  const buttonTextStyle = {
@@ -214,8 +224,8 @@ export default defineComponent({
214
224
  // differs by website type:
215
225
  // - brandSite: the card style (card + divided layouts) or the section
216
226
  // background (other layouts).
217
- // - brand.page / brand.contact: there's no section background, so the
218
- // coloured surface is the global button fill.
227
+ // - brand.page / brand.contact: the container is the collapsible, whose
228
+ // colour is the card colour (defaulting to the global button fill).
219
229
  // Falls back to the title colour when there's no solid colour to anchor to.
220
230
  const style = this.context.isBrandSite
221
231
  ? this.hasCard || this.isDividedContent
@@ -223,7 +233,9 @@ export default defineComponent({
223
233
  : this.hasBgColor
224
234
  ? this.viewModel.backgroundStyle
225
235
  : undefined
226
- : this.viewModel.buttonStyles?.style;
236
+ : this.viewModel.cardStyle?.backgroundColor
237
+ ? this.viewModel.cardStyle
238
+ : this.viewModel.buttonStyles?.style;
227
239
 
228
240
  return (
229
241
  mutedTextForBackground(backgroundColorFromStyle(style)) ??