@purpurds/promotion-card 3.4.0

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/readme.mdx ADDED
@@ -0,0 +1,60 @@
1
+ import { Meta, Stories, ArgTypes, Primary, Subtitle } from "@storybook/blocks";
2
+
3
+ import * as PromotionCardStories from "./src/promotion-card.stories";
4
+ import packageInfo from "./package.json";
5
+
6
+ <Meta name="Docs" title="Components/PromotionCard" of={PromotionCardStories} />
7
+
8
+ # PromotionCard
9
+
10
+ <Subtitle>Version {packageInfo.version}</Subtitle>
11
+
12
+ ### Showcase
13
+
14
+ <Primary />
15
+
16
+ ### Properties
17
+
18
+ <ArgTypes />
19
+
20
+ ### Installation
21
+
22
+ #### Via NPM
23
+
24
+ Add the dependency to your consumer app like `"@purpurds/purpur": "^x.y.z"`
25
+
26
+ In MyApp.tsx
27
+
28
+ ```tsx
29
+ import "@purpurds/purpur/styles";
30
+ ```
31
+
32
+ In MyComponent.tsx
33
+
34
+ ```tsx
35
+ import { PromotionCard } from "@purpurds/purpur";
36
+
37
+ export const MyComponent = () => {
38
+ return (
39
+ <PromotionCard
40
+ variant="primary"
41
+ badgeText="Buy now"
42
+ badgeIcon={<IconOffering size="sm" />}
43
+ title="Promotion title"
44
+ hasGradient
45
+ decorativeImage={
46
+ <img
47
+ src="https://www.telia.se/images/i15skfqwpurk/5YYelnwdIJGush05RYsE6A/04d4eeb571bca6d5c72b557f6da92c92/Telia_Company_Reinvention_69.jpg?fit=scale&w=536&fm=jpg&q=70"
48
+ alt="Lorem ipsum dolor sit amet consectetur adipisicing elit"
49
+ />
50
+ }
51
+ >
52
+ <Paragraph>
53
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Excepturi corporis amet
54
+ exercitationem atque consequuntur.
55
+ </Paragraph>
56
+ <Button variant="primary">Ask us</Button>
57
+ </PromotionCard>
58
+ );
59
+ };
60
+ ```
@@ -0,0 +1,4 @@
1
+ declare module "*.scss" {
2
+ const styles: { [className: string]: string };
3
+ export default styles;
4
+ }
@@ -0,0 +1,186 @@
1
+ @import "@purpurds/tokens/breakpoint/variables";
2
+
3
+ .purpur-promotion-card {
4
+ $root: &;
5
+
6
+ position: relative;
7
+ width: 100%;
8
+ height: 100%;
9
+ color: var(--promotion-card-color);
10
+ box-sizing: border-box;
11
+
12
+ &--without-image::before {
13
+ content: "";
14
+ position: absolute;
15
+ z-index: -1;
16
+ width: 100%;
17
+ height: 100%;
18
+ background: var(--promotion-card-background-image) right 50% center;
19
+ background-size: cover;
20
+ transform: rotateY(180deg);
21
+
22
+ @container promotion-card (min-width: #{$purpur-breakpoint-md}) {
23
+ background-position: center center;
24
+ }
25
+
26
+ @container promotion-card (min-width: #{$purpur-breakpoint-lg}) {
27
+ transform: rotateY(0);
28
+ }
29
+ }
30
+
31
+ &--with-image {
32
+ display: grid;
33
+ grid-template-areas: "image" "body";
34
+ grid-template-rows: auto 1fr;
35
+ background: var(--promotion-card-background);
36
+
37
+ @container promotion-card (min-width: #{$purpur-breakpoint-md}) {
38
+ grid-template-areas: "body image";
39
+ grid-template-columns: repeat(2, 1fr);
40
+ }
41
+ }
42
+
43
+ &__card-container,
44
+ &__card {
45
+ height: 100%;
46
+ }
47
+
48
+ &__card {
49
+ container: promotion-card / inline-size;
50
+ }
51
+
52
+ &__image-gradient {
53
+ position: absolute;
54
+ bottom: 0;
55
+ width: 100%;
56
+ height: 100%;
57
+ background: linear-gradient(0, var(--promotion-card-background), transparent 75%);
58
+
59
+ @container promotion-card (min-width: #{$purpur-breakpoint-md}) {
60
+ left: 0;
61
+ top: 0;
62
+ width: 100%;
63
+ height: 100%;
64
+ background: linear-gradient(90deg, var(--promotion-card-background), transparent 75%);
65
+ }
66
+ }
67
+
68
+ &__content {
69
+ grid-area: body;
70
+ display: flex;
71
+ flex-direction: column;
72
+ justify-content: flex-end;
73
+ height: 100%;
74
+ padding: var(--purpur-spacing-400);
75
+ box-sizing: border-box;
76
+
77
+ &--without-image {
78
+ @container promotion-card (min-width: #{$purpur-breakpoint-lg}) {
79
+ padding: var(--purpur-spacing-600);
80
+
81
+ #{$root}__main-content {
82
+ display: flex;
83
+ gap: var(--purpur-spacing-300);
84
+ }
85
+
86
+ #{$root}__heading {
87
+ width: 100%;
88
+ }
89
+ }
90
+ }
91
+
92
+ &--with-image {
93
+ @container promotion-card (min-width: #{$purpur-breakpoint-md}) {
94
+ flex-direction: column;
95
+ justify-content: center;
96
+ padding: var(--purpur-spacing-600) var(--purpur-spacing-400);
97
+ }
98
+
99
+ @container promotion-card (min-width: #{$purpur-breakpoint-lg}) {
100
+ padding: var(--purpur-spacing-1200) var(--purpur-spacing-600);
101
+ }
102
+ }
103
+ }
104
+
105
+ &__badge-wrapper {
106
+ width: 100%;
107
+ padding-bottom: var(--purpur-spacing-200);
108
+ }
109
+
110
+ &__heading,
111
+ &__body {
112
+ width: 100%;
113
+ }
114
+
115
+ &__heading {
116
+ margin-bottom: var(--purpur-spacing-150);
117
+ color: var(--promotion-card-color);
118
+ }
119
+
120
+ &__body {
121
+ display: flex;
122
+ flex-direction: column;
123
+
124
+ &,
125
+ p,
126
+ h1,
127
+ h2,
128
+ h3,
129
+ h4,
130
+ h5,
131
+ h6,
132
+ ul,
133
+ ol {
134
+ color: var(--promotion-card-color);
135
+ }
136
+ }
137
+
138
+ &__image-wrapper {
139
+ grid-area: image;
140
+
141
+ &--decorative,
142
+ &--product {
143
+
144
+ picture,
145
+ img {
146
+ display: block;
147
+ width: 100%;
148
+ height: auto;
149
+ }
150
+
151
+ img {
152
+ object-fit: cover;
153
+ }
154
+ }
155
+
156
+ &--decorative {
157
+ position: relative;
158
+ width: 100%;
159
+ aspect-ratio: 3/2;
160
+
161
+ @container promotion-card (min-width: #{$purpur-breakpoint-md}) {
162
+ aspect-ratio: unset;
163
+
164
+ img {
165
+ height: 100%;
166
+ }
167
+ }
168
+ }
169
+
170
+ &--product {
171
+ display: flex;
172
+ justify-content: center;
173
+ align-items: center;
174
+ padding: var(--purpur-spacing-400);
175
+ }
176
+ }
177
+
178
+ [class*="_purpur-cta-link_"],
179
+ [class*="_purpur-button_"] {
180
+ width: 100%;
181
+
182
+ @container promotion-card (min-width: #{$purpur-breakpoint-md}) {
183
+ width: auto;
184
+ }
185
+ }
186
+ }
@@ -0,0 +1,188 @@
1
+ /* eslint-disable react/jsx-wrap-multilines */
2
+ import React from "react";
3
+ import { CtaLink } from "@purpurds/cta-link";
4
+ import { Grid } from "@purpurds/grid";
5
+ import { IconCheckmark, IconOffering } from "@purpurds/icon";
6
+ import { Paragraph } from "@purpurds/paragraph";
7
+ import type { Meta, StoryObj } from "@storybook/react";
8
+
9
+ import "@purpurds/badge/styles";
10
+ import "@purpurds/card/styles";
11
+ import "@purpurds/cta-link/styles";
12
+ import "@purpurds/grid/styles";
13
+ import "@purpurds/heading/styles";
14
+ import "@purpurds/icon/styles";
15
+ import "@purpurds/paragraph/styles";
16
+ import { PromotionCard, PromotionCardVariant, promotionCardVariants } from "./promotion-card";
17
+
18
+ const meta: Meta<typeof PromotionCard> = {
19
+ title: "Components/PromotionCard",
20
+ component: PromotionCard,
21
+ parameters: {
22
+ design: [
23
+ {
24
+ name: "PromotionCard",
25
+ type: "figma",
26
+ url: "https://www.figma.com/file/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?type=design&node-id=2942-430&mode=design&t=zoKkl2HAtk3UuKoP-0",
27
+ },
28
+ ],
29
+ },
30
+ argTypes: {
31
+ variant: { control: "select", options: promotionCardVariants },
32
+ },
33
+ };
34
+
35
+ export default meta;
36
+ type Story = StoryObj<typeof PromotionCard>;
37
+
38
+ const Children = ({ variant = "primary" }: { variant?: PromotionCardVariant }) => (
39
+ <>
40
+ <Paragraph style={{ marginBottom: "var(--purpur-spacing-150)" }}>
41
+ Lorem ipsum dolor sit amet consectetur. At velit mi ultricies in orci est condimentum. Et
42
+ venenatis fringilla vel dictum.
43
+ </Paragraph>
44
+
45
+ <ul
46
+ style={{
47
+ margin: "0 0 var(--purpur-spacing-400)",
48
+ padding: 0,
49
+ listStyle: "none",
50
+ }}
51
+ >
52
+ {[1, 2].map((number) => (
53
+ <li key={number}>
54
+ <Paragraph
55
+ style={{ display: "flex", gap: "var(--purpur-spacing-100)", alignItems: "center" }}
56
+ >
57
+ <IconCheckmark size="xs" /> List item {number}
58
+ </Paragraph>
59
+ </li>
60
+ ))}
61
+ </ul>
62
+
63
+ <div style={{ display: "flex", gap: "var(--purpur-spacing-200)", flexWrap: "wrap" }}>
64
+ <CtaLink variant={variant !== "primary" ? "primary" : "primary-negative"} href="#">
65
+ Primary CTA link
66
+ </CtaLink>
67
+ <CtaLink variant={variant !== "primary" ? "secondary" : "secondary-negative"} href="#">
68
+ Secondary CTA link
69
+ </CtaLink>
70
+ </div>
71
+ </>
72
+ );
73
+
74
+ const props = {
75
+ variant: "primary" as const,
76
+ badgeText: "Badge title",
77
+ badgeIcon: <IconOffering size="xs" />,
78
+ title: "Title goes here lorem ipsum",
79
+ titleTag: "h2" as const,
80
+ titleVariant: "title-300" as const,
81
+ };
82
+
83
+ export const WithDecorativeImage: Story = {
84
+ args: {
85
+ ...props,
86
+ hasGradient: true,
87
+ decorativeImage: (
88
+ <img
89
+ src="https://www.telia.se/images/i15skfqwpurk/5YYelnwdIJGush05RYsE6A/04d4eeb571bca6d5c72b557f6da92c92/Telia_Company_Reinvention_69.jpg?fit=scale&w=536&fm=jpg&q=70"
90
+ alt="Familjens unga ser på tv tillsammans mysig stämning men spännande"
91
+ />
92
+ ),
93
+ },
94
+ render: (args) => {
95
+ return (
96
+ <PromotionCard {...args}>
97
+ <Children variant={args.variant} />
98
+ </PromotionCard>
99
+ );
100
+ },
101
+ };
102
+
103
+ export const WithProductImage: Story = {
104
+ args: {
105
+ ...props,
106
+ productImage: (
107
+ <picture
108
+ style={{
109
+ maxWidth: 300,
110
+ }}
111
+ >
112
+ <source
113
+ type="image/webp"
114
+ srcSet="https://www.telia.se/images/j6b4qnxw7ufu/5E89mxmBBAYshpycYSoPFD/1912f041265530726efb07097f035b91/101023__160_b2ddd755-1bb7-415c-83f8-cbcd4bf7eb0c?fit=scale&w=500&fm=webp&q=99"
115
+ />
116
+ <img
117
+ src="https://www.telia.se/images/j6b4qnxw7ufu/5E89mxmBBAYshpycYSoPFD/1912f041265530726efb07097f035b91/101023__160_b2ddd755-1bb7-415c-83f8-cbcd4bf7eb0c?fit=scale&w=500&fm=webp&q=99"
118
+ alt="Familjens unga ser på tv tillsammans"
119
+ />
120
+ </picture>
121
+ ),
122
+ },
123
+ render: (args) => {
124
+ return (
125
+ <PromotionCard {...args}>
126
+ <Children variant={args.variant} />
127
+ </PromotionCard>
128
+ );
129
+ },
130
+ };
131
+
132
+ export const CardGroup: Story = {
133
+ args: {
134
+ ...props,
135
+ hasGradient: true,
136
+ decorativeImage: (
137
+ <img
138
+ src="https://www.telia.se/images/i15skfqwpurk/5YYelnwdIJGush05RYsE6A/04d4eeb571bca6d5c72b557f6da92c92/Telia_Company_Reinvention_69.jpg?fit=scale&w=536&fm=jpg&q=70"
139
+ alt="Familjens unga ser på tv tillsammans mysig stämning men spännande"
140
+ />
141
+ ),
142
+ },
143
+
144
+ render: (args) => {
145
+ return (
146
+ <Grid>
147
+ <Grid.Row colGap>
148
+ <Grid.Col width={12} widthMd={4}>
149
+ <PromotionCard {...args}>
150
+ <Paragraph>
151
+ Buy mobile telephony, broadband or IT support from Telia receive half the price of
152
+ Microsoft 365 for six months.
153
+ </Paragraph>
154
+ </PromotionCard>
155
+ </Grid.Col>
156
+ <Grid.Col width={12} widthMd={4}>
157
+ <PromotionCard {...args}>
158
+ <Paragraph>
159
+ Get our popular broadband and we&apos;ll offer you two months and the initial fee.
160
+ </Paragraph>
161
+ </PromotionCard>
162
+ </Grid.Col>
163
+ <Grid.Col width={12} widthMd={4}>
164
+ <PromotionCard {...args}>
165
+ <Paragraph>
166
+ Check out all our offers to explore the best we have to offer right now.
167
+ </Paragraph>
168
+ </PromotionCard>
169
+ </Grid.Col>
170
+ </Grid.Row>
171
+ </Grid>
172
+ );
173
+ },
174
+ };
175
+
176
+ export const Showcase: Story = {
177
+ args: {
178
+ ...props,
179
+ },
180
+
181
+ render: (args) => {
182
+ return (
183
+ <PromotionCard {...args}>
184
+ <Children variant={args.variant} />
185
+ </PromotionCard>
186
+ );
187
+ },
188
+ };
@@ -0,0 +1,166 @@
1
+ import React from "react";
2
+ import * as matchers from "@testing-library/jest-dom/matchers";
3
+ import { cleanup, render, screen } from "@testing-library/react";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+
6
+ import { PromotionCard } from "./promotion-card";
7
+
8
+ expect.extend(matchers);
9
+
10
+ const props = {
11
+ title: "Promotion card title",
12
+ "data-testid": "promotion-id",
13
+ } as const;
14
+
15
+ describe("PromotionCard", () => {
16
+ afterEach(() => {
17
+ cleanup();
18
+ });
19
+
20
+ it("should render a PromotionCard", () => {
21
+ render(
22
+ <PromotionCard {...props}>
23
+ <div />
24
+ </PromotionCard>
25
+ );
26
+
27
+ expect(screen.getByTestId("promotion-id")).toBeInTheDocument();
28
+ });
29
+
30
+ it("should set default custom properties", () => {
31
+ render(
32
+ <PromotionCard {...props}>
33
+ <div />
34
+ </PromotionCard>
35
+ );
36
+
37
+ expect(screen.getByTestId("promotion-id")).toHaveStyle({
38
+ "--promotion-card-background": "var(--purpur-color-background-tone-on-tone-primary)",
39
+ "--promotion-card-color": "var(--purpur-color-text-tone-on-tone-primary)",
40
+ });
41
+ });
42
+
43
+ it("should set custom properties depending on variant", () => {
44
+ render(
45
+ <PromotionCard {...props} variant="secondary">
46
+ <div />
47
+ </PromotionCard>
48
+ );
49
+
50
+ expect(screen.getByTestId("promotion-id")).toHaveStyle({
51
+ "--promotion-card-background": "var(--purpur-color-background-tone-on-tone-secondary)",
52
+ "--promotion-card-color": "var(--purpur-color-text-tone-on-tone-secondary)",
53
+ });
54
+ });
55
+
56
+ it("should render a title", () => {
57
+ render(
58
+ <PromotionCard {...props}>
59
+ <div />
60
+ </PromotionCard>
61
+ );
62
+
63
+ expect(screen.getByText(props.title)).toBeInTheDocument();
64
+ });
65
+
66
+ it("should set 'h2' as default tag", () => {
67
+ render(
68
+ <PromotionCard {...props}>
69
+ <div />
70
+ </PromotionCard>
71
+ );
72
+
73
+ expect(screen.getByText(props.title).tagName).toEqual("H2");
74
+ });
75
+
76
+ it("should change heading tag depending on 'titleTag' prop", () => {
77
+ render(
78
+ <PromotionCard {...props} titleTag="h3">
79
+ <div />
80
+ </PromotionCard>
81
+ );
82
+
83
+ expect(screen.getByText(props.title).tagName).toEqual("H3");
84
+ });
85
+
86
+ it("should render a badge", () => {
87
+ render(
88
+ <PromotionCard {...props} badgeText="Campaign">
89
+ <div />
90
+ </PromotionCard>
91
+ );
92
+
93
+ expect(screen.getByTestId("badgeWrapper")).toBeInTheDocument();
94
+ });
95
+
96
+ it("should render without badge", () => {
97
+ render(
98
+ <PromotionCard {...props}>
99
+ <div />
100
+ </PromotionCard>
101
+ );
102
+
103
+ expect(screen.queryByTestId("badgeWrapper")).not.toBeInTheDocument();
104
+ });
105
+
106
+ it("should render with an image and an alt text", () => {
107
+ render(
108
+ <PromotionCard
109
+ {...props}
110
+ productImage={<img alt="nice placeholder" src="https://via.placeholder.com/150" />}
111
+ >
112
+ <div />
113
+ </PromotionCard>
114
+ );
115
+
116
+ expect(screen.getByAltText("nice placeholder")).toBeInTheDocument();
117
+ expect(screen.getByRole("img")).toBeInTheDocument();
118
+ });
119
+
120
+ it("should render without image", () => {
121
+ render(
122
+ <PromotionCard {...props}>
123
+ <div />
124
+ </PromotionCard>
125
+ );
126
+
127
+ expect(screen.queryByRole("img")).not.toBeInTheDocument();
128
+ });
129
+
130
+ it("should render with image gradient", () => {
131
+ render(
132
+ <PromotionCard
133
+ {...props}
134
+ decorativeImage={<img alt="fin placeholder bild" src="https://via.placeholder.com/150" />}
135
+ hasGradient
136
+ >
137
+ <div />
138
+ </PromotionCard>
139
+ );
140
+
141
+ expect(screen.getByTestId("promotion-card-gradient")).toBeInTheDocument();
142
+ });
143
+
144
+ it("should render without image gradient if it doesn't have an image", () => {
145
+ render(
146
+ <PromotionCard {...props} hasGradient>
147
+ <div />
148
+ </PromotionCard>
149
+ );
150
+
151
+ expect(screen.queryByTestId("promotion-card-gradient")).not.toBeInTheDocument();
152
+ });
153
+
154
+ it("should render without image gradient if 'hasGradient' isn't set", () => {
155
+ render(
156
+ <PromotionCard
157
+ {...props}
158
+ decorativeImage={<img alt="fin placeholder bild" src="https://via.placeholder.com/150" />}
159
+ >
160
+ <div />
161
+ </PromotionCard>
162
+ );
163
+
164
+ expect(screen.queryByTestId("promotion-card-gradient")).not.toBeInTheDocument();
165
+ });
166
+ });