@purpurds/radio-card-group 3.0.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,188 @@
1
+ import { Meta, ArgTypes, Primary, Subtitle } from "@storybook/blocks";
2
+
3
+ import * as RadioCardGroupStories from "./src/radio-card-group.stories";
4
+ import packageInfo from "./package.json";
5
+
6
+ <Meta name="Docs" title="Components/RadioCardGroup" of={RadioCardGroupStories} />
7
+
8
+ # RadioCardGroup
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/radio-card-group": "x.y.z"`
25
+
26
+ #### From outside the monorepo (build-time)
27
+
28
+ To install this package, you need to setup access to the artifactory. [Click here to go to the guide on how to do that](https://github.com/telia-company/jfrog-documentation/blob/main/doc/JFrog/JFrog_Onboarding.md#getting-access-to-artifactory-and-other-jfrog-applications).
29
+
30
+ ---
31
+
32
+ In MyApp.tsx
33
+
34
+ ```tsx
35
+ import "@purpurds/tokens/index.css";
36
+ ```
37
+
38
+ or
39
+
40
+ ```tsx
41
+ import "@purpurds/radio-card-group/styles";
42
+ ```
43
+
44
+ ### Examples
45
+
46
+ In MyComponent.tsx
47
+
48
+ #### Controlled
49
+
50
+ ```tsx
51
+ import { RadioCardGroup } from "@purpurds/radio-card-group";
52
+
53
+ export const MyComponent = ({ dataSet }: Props) => {
54
+ const [selectedItem, setSelectedItem] = useState();
55
+ const items = dataSet.map((itemData) => ({
56
+ id: itemData.id,
57
+ image: { src: itemData.imageSrc, altText: itemData.imageAltText },
58
+ title: itemData.name,
59
+ body: itemData.description,
60
+ value: itemData.id,
61
+ }));
62
+
63
+ const onValueChange = (value?: string) => {
64
+ const itemToSelect = items.find((item) => item.value === value);
65
+ setSelectedItem(itemToSelect.value);
66
+ };
67
+
68
+ return (
69
+ <div>
70
+ <RadioCardGroup
71
+ {...someProps}
72
+ items={items}
73
+ value={selectedItem}
74
+ onValueChange={onValueChange}
75
+ />
76
+ </div>
77
+ );
78
+ };
79
+ ```
80
+
81
+ With items as children
82
+
83
+ ```tsx
84
+ import { RadioCardGroup } from "@purpurds/radio-card-group";
85
+
86
+ export const MyComponent = ({ dataSet }: Props) => {
87
+ const [selectedItem, setSelectedItem] = useState();
88
+
89
+ const onValueChange = (value?: string) => {
90
+ const itemToSelect = dataSet.find((itemData) => itemData.id === value);
91
+ setSelectedItem(itemToSelect.id);
92
+ };
93
+
94
+ return (
95
+ <RadioCardGroup
96
+ id="example"
97
+ lable="Items as children"
98
+ value={selectedItem}
99
+ onValueChange={onValueChange}
100
+ >
101
+ {dataSet.map((itemData) => (
102
+ <RadioCardItem
103
+ key={itemData.id}
104
+ id={itemData.id}
105
+ image={{ src: itemData.imageSrc, altText: itemData.imageAltText }}
106
+ title={itemData.name}
107
+ body={itemData.description}
108
+ value={itemData.id}
109
+ />
110
+ ))}
111
+ <div>This div will no be rendered since its no a RadioCardItem</div>
112
+ </RadioCardGroup>
113
+ );
114
+ };
115
+ ```
116
+
117
+ #### Uncontrolled
118
+
119
+ ```tsx
120
+ import { RadioCardGroup } from "@purpurds/radio-card-group";
121
+
122
+ export const MyComponent = ({ dataSet }: Props) => {
123
+ const items = dataSet.map((itemData) => ({
124
+ id: itemData.id,
125
+ image: { src: itemData.imageSrc, altText: itemData.imageAltText },
126
+ title: itemData.name,
127
+ body: itemData.description,
128
+ value: itemData.id,
129
+ }));
130
+
131
+ return (
132
+ <div>
133
+ <RadioCardGroup {...someProps} items={items} />
134
+ </div>
135
+ );
136
+ };
137
+ ```
138
+
139
+ #### With custom title, body and image
140
+
141
+ ```tsx
142
+ import { RadioCardGroup } from "@purpurds/radio-card-group";
143
+ import { Badge } from "@purpurds/badge";
144
+ import { IconPebble } from "@purpurds/icon";
145
+ import { Paragraph } from "@purpurds/paragraph";
146
+
147
+ export const MyComponent = ({ dataSet }: Props) => {
148
+ const items = dataSet.map((itemData) => ({
149
+ id: itemData.id,
150
+ image: (
151
+ <div styles={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
152
+ <IconPebble />
153
+ </div>
154
+ ),
155
+ title: ({ disabled }) => (
156
+ <div style={{ display: "flex", gap: "var(--purpur-spacing-150)" }}>
157
+ <Paragraph disabled={disabled} variant="paragraph-100">
158
+ <b>Custom title</b>
159
+ </Paragraph>
160
+ <Badge variant="attention">Nice price!</Badge>
161
+ </div>
162
+ ),
163
+ body: ({ disabled }) => (
164
+ <Paragraph disabled={disabled} variant="paragraph-100">
165
+ <span
166
+ style={{
167
+ textDecoration: "line-through",
168
+ color: "var(--purpur-color-gray-400)",
169
+ marginRight: "var(--purpur-spacing-50)",
170
+ }}
171
+ >
172
+ {itemdata.ordinaryPrice} kr/mån
173
+ </span>
174
+ <span style={{ fontWeight: "var(--purpur-typography-weight-medium)" }}>
175
+ {itemdata.price} kr/mån
176
+ </span>
177
+ </Paragraph>
178
+ ),,
179
+ value: itemData.id,
180
+ }));
181
+
182
+ return (
183
+ <div>
184
+ <RadioCardGroup {...someProps} items={items} />
185
+ </div>
186
+ );
187
+ };
188
+ ```
@@ -0,0 +1,4 @@
1
+ import c from "classnames/bind";
2
+
3
+ import styles from "./radio-card-group.module.scss";
4
+ export const cx = c.bind(styles);
@@ -0,0 +1,6 @@
1
+ declare module "*.scss" {
2
+ const styles: { [className: string]: string };
3
+ export default styles;
4
+ }
5
+
6
+ declare module "*.png";
@@ -0,0 +1,257 @@
1
+ @import "@purpurds/tokens/breakpoint/variables";
2
+
3
+ $target-area-size: calc(var(--purpur-spacing-400) + var(--purpur-spacing-150));
4
+ $radio-size: var(--purpur-spacing-300);
5
+ $radio-target-area-padding: calc(calc($target-area-size - $radio-size) / 2);
6
+
7
+ @mixin border($color, $width) {
8
+ box-shadow: $color 0px 0px 0px $width inset;
9
+ }
10
+
11
+ .purpur-radio-card-group {
12
+ $root: &;
13
+ width: 100%;
14
+ gap: var(--purpur-spacing-200);
15
+
16
+ &--vertical {
17
+ display: flex;
18
+ flex-direction: column;
19
+ }
20
+
21
+ &--horizontal {
22
+ display: grid;
23
+ grid-template-columns: 1fr;
24
+
25
+ @container (min-width: #{$purpur-breakpoint-md}) {
26
+ grid-template-columns: repeat(auto-fill, minmax(calc(var(--purpur-breakpoint-lg) / 4), 1fr));
27
+ }
28
+ }
29
+
30
+ &--radio-left {
31
+ & #{$root}__item {
32
+ flex-direction: row-reverse;
33
+ }
34
+
35
+ & #{$root}__item-bottom-container {
36
+ padding-left: calc($radio-size + var(--purpur-spacing-150));
37
+ }
38
+
39
+ & #{$root}__item-top-container {
40
+ flex-direction: row-reverse;
41
+ justify-content: flex-end;
42
+ }
43
+ }
44
+
45
+ &[data-orientation="horizontal"] {
46
+ flex-direction: row;
47
+ flex-wrap: wrap;
48
+
49
+ &:not([dir="rtl"]) {
50
+ gap: var(--purpur-spacing-150);
51
+ }
52
+ }
53
+
54
+ &__container {
55
+ display: flex;
56
+ flex-direction: column;
57
+ gap: var(--purpur-spacing-150);
58
+ align-items: flex-start;
59
+ container: purpur-radio-card-group / inline-size;
60
+ }
61
+
62
+ &__item-container {
63
+ display: flex;
64
+ flex-direction: column;
65
+ position: relative;
66
+ background-color: var(--purpur-color-background-primary);
67
+ border-radius: var(--purpur-border-radius-md);
68
+ }
69
+
70
+ &__item {
71
+ all: unset;
72
+ height: 100%;
73
+ width: 100%;
74
+ display: flex;
75
+ box-sizing: border-box;
76
+ border-radius: var(--purpur-border-radius-md);
77
+ cursor: pointer;
78
+ overflow: hidden;
79
+
80
+ &::after {
81
+ content: "";
82
+ pointer-events: none;
83
+ position: absolute;
84
+ inset: 0;
85
+ border-radius: var(--purpur-border-radius-md);
86
+ transition: box-shadow var(--purpur-motion-duration-150) ease;
87
+ @include border(var(--purpur-color-border-interactive-subtle), var(--purpur-border-width-xs));
88
+ }
89
+
90
+ &:disabled {
91
+ background-color: var(--purpur-color-background-interactive-disabled);
92
+
93
+ &::after {
94
+ @include border(var(--purpur-color-border-medium), var(--purpur-border-width-xs));
95
+ }
96
+
97
+ &[data-state="checked"] {
98
+ &::after {
99
+ @include border(var(--purpur-color-border-medium), var(--purpur-border-width-sm));
100
+ }
101
+ }
102
+
103
+ & #{$root}__item-radio {
104
+ background-color: var(--purpur-color-background-interactive-disabled);
105
+
106
+ cursor: default;
107
+ @include border(
108
+ var(--purpur-color-background-interactive-disabled),
109
+ var(--purpur-border-width-xs)
110
+ );
111
+ }
112
+ }
113
+
114
+ &:active:not(:disabled) {
115
+ &::after {
116
+ @include border(
117
+ var(--purpur-color-border-interactive-subtle-hover),
118
+ var(--purpur-border-width-xs)
119
+ );
120
+ }
121
+
122
+ & #{$root}__item-radio {
123
+ @include border(
124
+ var(--purpur-color-border-interactive-subtle-hover),
125
+ var(--purpur-border-width-xs)
126
+ );
127
+ }
128
+ }
129
+
130
+ &:not([data-state="checked"]):not(:disabled):not(:active):hover {
131
+ &::after {
132
+ @include border(
133
+ var(--purpur-color-border-interactive-subtle-hover),
134
+ var(--purpur-border-width-sm)
135
+ );
136
+ }
137
+
138
+ & #{$root}__item-radio {
139
+ @include border(
140
+ var(--purpur-color-border-interactive-subtle-hover),
141
+ var(--purpur-border-width-sm)
142
+ );
143
+ }
144
+ }
145
+
146
+ &[data-state="checked"]:not(:disabled) {
147
+ &,
148
+ &:hover,
149
+ &:active {
150
+ cursor: default;
151
+
152
+ &::after {
153
+ @include border(
154
+ var(--purpur-color-border-interactive-primary),
155
+ var(--purpur-border-width-sm)
156
+ );
157
+ }
158
+
159
+ & #{$root}__item-radio {
160
+ cursor: default;
161
+ @include border(
162
+ var(--purpur-color-border-interactive-primary),
163
+ var(--purpur-border-width-xs)
164
+ );
165
+ }
166
+ }
167
+ }
168
+
169
+ &:focus-visible::after {
170
+ outline: var(--purpur-border-width-sm) solid var(--purpur-color-border-interactive-focus);
171
+ outline-offset: var(--purpur-spacing-25);
172
+ }
173
+ }
174
+
175
+ &__item-content {
176
+ display: flex;
177
+ flex-direction: column;
178
+ gap: var(--purpur-spacing-50);
179
+ width: 100%;
180
+ padding: var(--purpur-spacing-200);
181
+ }
182
+
183
+ &__item-top-container {
184
+ display: flex;
185
+ justify-content: space-between;
186
+ gap: var(--purpur-spacing-150);
187
+ width: 100%;
188
+ }
189
+
190
+ &__item-title {
191
+ font-weight: var(--purpur-typography-weight-bold);
192
+ }
193
+
194
+ &__item-bottom-container {
195
+ display: flex;
196
+ flex-direction: column;
197
+ }
198
+
199
+ &__item-radio {
200
+ width: $radio-size;
201
+ height: $radio-size;
202
+ display: flex;
203
+ flex-shrink: 0;
204
+ align-items: center;
205
+ justify-content: center;
206
+ cursor: pointer;
207
+ box-sizing: border-box;
208
+ border-radius: var(--purpur-border-radius-full);
209
+ transition: box-shadow var(--purpur-motion-duration-150) ease;
210
+ @include border(var(--purpur-color-border-interactive-subtle), var(--purpur-border-width-xs));
211
+ }
212
+
213
+ &__item-indicator {
214
+ background-color: var(--purpur-color-background-primary);
215
+ width: var(--purpur-spacing-200);
216
+ height: var(--purpur-spacing-200);
217
+ border-radius: var(--purpur-border-radius-full);
218
+ background-color: var(--purpur-color-background-interactive-primary);
219
+
220
+ &[data-disabled] {
221
+ background-color: var(--purpur-color-text-weak);
222
+ }
223
+ }
224
+
225
+ &__item-image-container {
226
+ width: var(--purpur-spacing-1000);
227
+ overflow: hidden;
228
+ flex-shrink: 0;
229
+ position: relative;
230
+ }
231
+
232
+ &__item-image {
233
+ width: 100%;
234
+ height: 100%;
235
+ object-fit: cover;
236
+ opacity: 0;
237
+ transition: opacity var(--purpur-motion-duration-300);
238
+
239
+ &--loaded {
240
+ opacity: 1;
241
+ }
242
+ }
243
+
244
+ &__item-image-placeholder {
245
+ position: absolute !important;
246
+ inset: 0;
247
+ transition: opacity var(--purpur-motion-duration-300);
248
+
249
+ &--loaded {
250
+ opacity: 0 !important;
251
+ }
252
+ }
253
+
254
+ &__item-children {
255
+ padding: 0 var(--purpur-spacing-200) var(--purpur-spacing-200);
256
+ }
257
+ }
@@ -0,0 +1,172 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import imageFile from "../static/story-image.png";
3
+ import { useArgs } from "@storybook/client-api";
4
+
5
+ import { RadioCardGroup, RadioCardGroupItem, RadioCardItem } from "./radio-card-group";
6
+ import React from "react";
7
+ import { Badge } from "@purpurds/badge";
8
+ import { Paragraph } from "@purpurds/paragraph";
9
+ import { Grid } from "@purpurds/grid";
10
+
11
+ import "@purpurds/badge/styles";
12
+ import "@purpurds/grid/styles";
13
+ import "@purpurds/heading/styles";
14
+ import "@purpurds/paragraph/styles";
15
+ import "@purpurds/skeleton/styles";
16
+
17
+ const meta: Meta<typeof RadioCardGroup> = {
18
+ title: "Inputs/RadioCardGroup",
19
+ component: RadioCardGroup,
20
+ };
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof RadioCardGroup>;
24
+
25
+ const items: RadioCardGroupItem[] = Array.from(Array(4).keys()).map((key) => ({
26
+ id: `${key + 1}`,
27
+ image: { src: imageFile, altText: "Example image" },
28
+ title: `Card title ${key + 1}`,
29
+ body: `Body ${key + 1}`,
30
+ value: `item-${key + 1}`,
31
+ }));
32
+
33
+ const decorators: Story["decorators"] = [
34
+ (Story, { args }) => (
35
+ <Grid>
36
+ <Grid.Row>
37
+ <Grid.Col width={12} widthMd={12} widthLg={args.orientation === "horizontal" ? 12 : 6}>
38
+ <Story />
39
+ </Grid.Col>
40
+ </Grid.Row>
41
+ </Grid>
42
+ ),
43
+ ];
44
+
45
+ export const Controlled: Story = {
46
+ args: {
47
+ items,
48
+ label: "Controlled Radio card group",
49
+ id: "radio-card-story",
50
+ value: items[0].value,
51
+ },
52
+ parameters: {
53
+ design: [
54
+ {
55
+ name: "RadioCardGroup",
56
+ type: "figma",
57
+ url: "https://www.figma.com/file/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?type=design&node-id=36624-5815",
58
+ },
59
+ ],
60
+ },
61
+ decorators,
62
+ render: ({ ...args }) => {
63
+ const [{ value }, updateArgs] = useArgs(); // eslint-disable-line react-hooks/rules-of-hooks
64
+ const onValueChange = (value: string) => {
65
+ args.onValueChange?.(value);
66
+ updateArgs({ value });
67
+ };
68
+
69
+ return <RadioCardGroup {...args} value={value} onValueChange={onValueChange} />;
70
+ },
71
+ };
72
+
73
+ export const Uncontrolled: Story = {
74
+ args: {
75
+ items,
76
+ label: "Uncontrolled radio card group label",
77
+ id: "radio-card-story",
78
+ defaultValue: items[0].value,
79
+ },
80
+ parameters: {
81
+ design: [
82
+ {
83
+ name: "RadioCardGroup",
84
+ type: "figma",
85
+ url: "https://www.figma.com/file/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?type=design&node-id=36624-5815",
86
+ },
87
+ ],
88
+ },
89
+ decorators,
90
+ };
91
+
92
+ const itemsAsChildren: RadioCardGroupItem[] = [
93
+ {
94
+ id: "1",
95
+ image: { src: imageFile, altText: "Example image" },
96
+ title: "With image",
97
+ body: "A medium long body",
98
+ value: "item-1",
99
+ },
100
+ {
101
+ id: "2",
102
+ title: ({ disabled }) => (
103
+ <div style={{ display: "flex", gap: "var(--purpur-spacing-150)" }}>
104
+ <Paragraph disabled={disabled} variant="paragraph-100">
105
+ <b>Custom title</b>
106
+ </Paragraph>
107
+ <Badge variant="attention">Nice price!</Badge>
108
+ </div>
109
+ ),
110
+ body: "14 599 kr",
111
+ value: "item-2",
112
+ },
113
+ {
114
+ id: "3",
115
+ title: "Item with custom body",
116
+ body: ({ disabled }) => (
117
+ <Paragraph disabled={disabled} variant="paragraph-100">
118
+ <span
119
+ style={{
120
+ textDecoration: "line-through",
121
+ color: "var(--purpur-color-gray-400)",
122
+ marginRight: "var(--purpur-spacing-50)",
123
+ }}
124
+ >
125
+ 499 kr/mån
126
+ </span>
127
+ <span style={{ fontWeight: "var(--purpur-typography-weight-medium)" }}>199 kr/mån</span>
128
+ </Paragraph>
129
+ ),
130
+ value: "item-3",
131
+ },
132
+ {
133
+ id: "4",
134
+ title: "Item with more text content overall, too see how it looks.",
135
+ body: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Saepe rem nostrum, perferendis cupiditate officia dignissimos dicta beatae nesciunt.",
136
+ value: "item-4",
137
+ },
138
+ ];
139
+
140
+ export const ItemsAsChildren: Story = {
141
+ args: {
142
+ label: "Cards passed as children",
143
+ id: "radio-card-story",
144
+ defaultValue: items[0].value,
145
+ },
146
+ parameters: {
147
+ design: [
148
+ {
149
+ name: "RadioCardGroup",
150
+ type: "figma",
151
+ url: "https://www.figma.com/file/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?type=design&node-id=36624-5815",
152
+ },
153
+ ],
154
+ },
155
+ decorators,
156
+ render: ({ ...args }) => {
157
+ const [{ value }, updateArgs] = useArgs(); // eslint-disable-line react-hooks/rules-of-hooks
158
+ const onValueChange = (value: string) => {
159
+ args.onValueChange?.(value);
160
+ updateArgs({ value });
161
+ };
162
+
163
+ return (
164
+ <RadioCardGroup {...args} value={value} onValueChange={onValueChange}>
165
+ {itemsAsChildren.map((item) => (
166
+ <RadioCardItem key={item.id} {...item} />
167
+ ))}
168
+ <div>This div will no be rendered since its no a RadioCardItem</div>
169
+ </RadioCardGroup>
170
+ );
171
+ },
172
+ };