@designcrowd/library.brand-page 6.0.18 → 6.0.19-ast-btns-2

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 (76) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-CIrk6Brm.js → CreatedOnBrandCrowd-de-de-BI4rJV_V.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-CvXk0moH.js → CreatedOnBrandCrowd-en-us-Crf-sBeO.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-BhtZbjjU.js → CreatedOnBrandCrowd-es-es-DE_kK1yb.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-B6TD57O-.js → CreatedOnBrandCrowd-fr-ca-BXhEu1vY.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-B6TD57O-.js → CreatedOnBrandCrowd-fr-fr-BXhEu1vY.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-Bto0xOtS.js → CreatedOnBrandCrowd-pt-br-DG1azcKI.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-Bto0xOtS.js → CreatedOnBrandCrowd-pt-pt-DG1azcKI.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-2h2W3y5z.js → CreatedOnDcom-de-de-BFL99-ya.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-D6Jgk6yv.js → CreatedOnDcom-en-us-DqWMpFgs.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-sBCqYQIm.js → CreatedOnDcom-es-es-Ds0zIzXx.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-C8hg5hTU.js → CreatedOnDcom-fr-ca-X-GSmpwv.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-_DP1Umyh.js → CreatedOnDcom-fr-fr-Cm1eyNiD.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-BdwYAy5p.js → CreatedOnDcom-pt-br-BUOePaaJ.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-Bm1NHVaH.js → CreatedOnDcom-pt-pt-CJSZsybQ.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 +10 -0
  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 +7 -0
  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 +48 -0
  27. package/lib/src/brand-page/utils/migration.util.js +9 -0
  28. package/lib/src/brand-page/utils/view-model.util.js +1 -0
  29. package/lib/src/brand-page-maker/constants/config-settings-constant.js +41 -1
  30. package/lib/src/brand-page-maker/default-getters/contact-form-background-color.default-getter.d.ts +8 -0
  31. package/lib/src/brand-page-maker/default-getters/contact-form-background-color.default-getter.js +16 -0
  32. package/lib/src/brand-page-maker/default-getters/index.d.ts +1 -0
  33. package/lib/src/brand-page-maker/default-getters/index.js +1 -0
  34. package/lib/src/brand-page-maker/models/button-styles-config.js +15 -3
  35. package/lib/src/brand-page-maker/models/content-settings-configs.d.ts +1 -1
  36. package/lib/src/brand-page-maker/models/content-settings.js +3 -0
  37. package/lib/src/css/library.brand-page-brandCrowd.min.css +96 -68
  38. package/lib/src/css/library.brand-page-brandPage.min.css +96 -68
  39. package/lib/src/css/library.brand-page-designCom.min.css +96 -68
  40. package/lib/src/css/library.brand-page-preview.min.css +96 -68
  41. package/lib/src/index.mjs +1 -1
  42. package/lib/src/{lib-CPTMajMv.js → lib-CfENvLvt.js} +18121 -17791
  43. package/lib/src/metadata-builder/builders/section-rules/banner-section.rules.js +6 -0
  44. package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +1 -1
  45. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +5 -0
  46. package/package.json +2 -2
  47. package/src/brand-page/components/button/ButtonStyleWrapper.vue +112 -44
  48. package/src/brand-page/components/button/ButtonStyleWrapper.vue.test.ts +167 -0
  49. package/src/brand-page/components/collapsible/Collapsible.mixin.ts +11 -0
  50. package/src/brand-page/components/contact-form/ContactForm.mixin.ts +10 -0
  51. package/src/brand-page/components/contact-form/ContactForm.vue +94 -39
  52. package/src/brand-page/components/mailing-list/MailingList.mixin.ts +6 -0
  53. package/src/brand-page/components/mailing-list/MailingList.vue +18 -10
  54. package/src/brand-page/components/payment/Payment.mixin.ts +6 -0
  55. package/src/brand-page/components/payment/Payment.vue +43 -24
  56. package/src/brand-page/components/payment/RequestPaymentStripe.vue +26 -22
  57. package/src/brand-page/components/shop/Shop.vue +15 -6
  58. package/src/brand-page/models/index.ts +1 -0
  59. package/src/brand-page/models/payment.model.ts +2 -0
  60. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
  61. package/src/brand-page/utils/button.util.ts +17 -0
  62. package/src/brand-page/utils/migration/migration-19.test.ts +120 -0
  63. package/src/brand-page/utils/migration/migration-19.ts +67 -0
  64. package/src/brand-page/utils/migration.util.ts +10 -0
  65. package/src/brand-page/utils/view-model.util.ts +1 -0
  66. package/src/brand-page-maker/constants/config-settings-constant.ts +44 -0
  67. package/src/brand-page-maker/default-getters/contact-form-background-color.default-getter.ts +23 -0
  68. package/src/brand-page-maker/default-getters/index.ts +1 -0
  69. package/src/brand-page-maker/models/button-styles-config.ts +15 -3
  70. package/src/brand-page-maker/models/content-settings-configs.ts +1 -1
  71. package/src/brand-page-maker/models/content-settings.ts +3 -0
  72. package/src/metadata-builder/builders/section-rules/banner-section.rules.ts +6 -0
  73. package/src/metadata-builder/parsers/section-tool-arguments.ts +7 -1
  74. package/src/metadata-builder/sanitizers/__tests__/section-args-validator.test.ts +13 -0
  75. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +6 -0
  76. package/src/shared/style-settings/components/StyleSettings.vue +1 -0
@@ -19,6 +19,12 @@ const CHECKS = [
19
19
  'left-aligned-full-image',
20
20
  'center-card-aligned-image',
21
21
  'left-card-aligned-image',
22
+ 'top-card-aligned-image',
23
+ 'bottom-card-aligned-image',
24
+ 'middle-card-aligned-image',
25
+ 'top-aligned-full-image',
26
+ 'bottom-aligned-full-image',
27
+ 'middle-aligned-full-image',
22
28
  ], 'banner-section'),
23
29
  enumCheck('button_link_type', Object.keys(BUTTON_LINK_TYPE), 'banner-section'),
24
30
  ];
@@ -268,7 +268,7 @@ export interface BannerSectionArguments {
268
268
  background_image_url?: string;
269
269
  background_shape_url?: string;
270
270
  color_theme?: string;
271
- display_style?: 'left-aligned' | 'right-aligned' | 'centered' | 'big-image' | 'left-aligned-big-image' | 'right-aligned-full-image' | 'left-aligned-full-image' | 'center-card-aligned-image' | 'left-card-aligned-image';
271
+ display_style?: 'left-aligned' | 'right-aligned' | 'centered' | 'big-image' | 'left-aligned-big-image' | 'right-aligned-full-image' | 'left-aligned-full-image' | 'center-card-aligned-image' | 'left-card-aligned-image' | 'top-card-aligned-image' | 'bottom-card-aligned-image' | 'middle-card-aligned-image' | 'top-aligned-full-image' | 'bottom-aligned-full-image' | 'middle-aligned-full-image';
272
272
  title_style?: TextStyleArguments;
273
273
  description_style?: TextStyleArguments;
274
274
  button_text_style?: ButtonTextStyleArguments;
@@ -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() {
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.18",
4
+ "version": "6.0.19-ast-btns-2",
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.3-ast-btn",
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,
@@ -49,6 +54,12 @@ export default defineComponent({
49
54
  containerStyle.backgroundColor = this.cardStyle.backgroundColor;
50
55
  }
51
56
 
57
+ // Own background colour decouples the container from the button fill (BP-5800).
58
+ // Guarded on backgroundColor so an empty {} never blanks the container.
59
+ if (this.backgroundStyle?.backgroundColor) {
60
+ containerStyle.backgroundColor = this.backgroundStyle.backgroundColor;
61
+ }
62
+
52
63
  return containerStyle;
53
64
  },
54
65
  arrowStyle(): BrandPageCssStyle {
@@ -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 = {