@4alldigital/foundation-ui--core 3.3.0 → 3.4.1
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/package.json +2 -2
- package/src/components/Button/Button.tsx +1 -1
- package/src/components/Form/Form.tsx +1 -1
- package/src/components/ProductCard/ProductCard.stories.tsx +90 -0
- package/src/components/ProductCard/ProductCard.tsx +110 -0
- package/src/components/ProductCard/ProductCard.types.ts +21 -0
- package/src/components/ProductCard/index.ts +3 -0
- package/src/components/ProductDetail/ProductDetail.stories.tsx +97 -0
- package/src/components/ProductDetail/ProductDetail.tsx +251 -0
- package/src/components/ProductDetail/ProductDetail.types.ts +16 -0
- package/src/components/ProductDetail/index.ts +2 -0
- package/src/components/ShadcnButton/ShadcnButton.stories.tsx +152 -0
- package/src/components/ShadcnCarousel/ShadcnCarousel.stories.tsx +179 -0
- package/src/components/VariantSelector/VariantSelector.stories.tsx +107 -0
- package/src/components/VariantSelector/VariantSelector.tsx +118 -0
- package/src/components/VariantSelector/VariantSelector.types.ts +15 -0
- package/src/components/VariantSelector/index.ts +2 -0
- package/src/components/index.ts +17 -0
- package/src/context/Amplify/index.tsx +1 -0
- package/src/context/Theme/index.tsx +0 -2
- package/src/forms/LoginForm/LoginForm.tsx +1 -3
- package/src/templates/ProductDetailScreen/ProductDetailScreen.stories.tsx +108 -0
- package/src/templates/ProductDetailScreen/ProductDetailScreen.tsx +35 -0
- package/src/templates/ProductDetailScreen/ProductDetailScreen.types.ts +11 -0
- package/src/templates/ProductDetailScreen/index.ts +1 -0
- package/src/templates/ProductListScreen/ProductListScreen.stories.tsx +121 -0
- package/src/templates/ProductListScreen/ProductListScreen.tsx +59 -0
- package/src/templates/ProductListScreen/ProductListScreen.types.ts +10 -0
- package/src/templates/ProductListScreen/index.ts +1 -0
- package/src/templates/index.ts +4 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { ShadcnButton } from './ShadcnButton';
|
|
3
|
+
import { ChevronRight, Mail, Loader2 } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof ShadcnButton> = {
|
|
6
|
+
title: 'Components/ShadcnButton',
|
|
7
|
+
component: ShadcnButton,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
variant: {
|
|
11
|
+
control: 'select',
|
|
12
|
+
options: ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'],
|
|
13
|
+
description: 'Button visual variant',
|
|
14
|
+
},
|
|
15
|
+
size: {
|
|
16
|
+
control: 'select',
|
|
17
|
+
options: ['default', 'sm', 'lg', 'icon'],
|
|
18
|
+
description: 'Button size',
|
|
19
|
+
},
|
|
20
|
+
disabled: {
|
|
21
|
+
control: 'boolean',
|
|
22
|
+
description: 'Disabled state',
|
|
23
|
+
},
|
|
24
|
+
asChild: {
|
|
25
|
+
control: 'boolean',
|
|
26
|
+
description: 'Merge props with child element',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default meta;
|
|
32
|
+
type Story = StoryObj<typeof ShadcnButton>;
|
|
33
|
+
|
|
34
|
+
export const Default: Story = {
|
|
35
|
+
args: {
|
|
36
|
+
children: 'Button',
|
|
37
|
+
variant: 'default',
|
|
38
|
+
size: 'default',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const Destructive: Story = {
|
|
43
|
+
args: {
|
|
44
|
+
children: 'Delete',
|
|
45
|
+
variant: 'destructive',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const Outline: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
children: 'Outline',
|
|
52
|
+
variant: 'outline',
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const Secondary: Story = {
|
|
57
|
+
args: {
|
|
58
|
+
children: 'Secondary',
|
|
59
|
+
variant: 'secondary',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const Ghost: Story = {
|
|
64
|
+
args: {
|
|
65
|
+
children: 'Ghost',
|
|
66
|
+
variant: 'ghost',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const Link: Story = {
|
|
71
|
+
args: {
|
|
72
|
+
children: 'Link',
|
|
73
|
+
variant: 'link',
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const Small: Story = {
|
|
78
|
+
args: {
|
|
79
|
+
children: 'Small Button',
|
|
80
|
+
size: 'sm',
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const Large: Story = {
|
|
85
|
+
args: {
|
|
86
|
+
children: 'Large Button',
|
|
87
|
+
size: 'lg',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const Icon: Story = {
|
|
92
|
+
args: {
|
|
93
|
+
size: 'icon',
|
|
94
|
+
children: <ChevronRight className="h-4 w-4" />,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const WithIcon: Story = {
|
|
99
|
+
args: {
|
|
100
|
+
children: (
|
|
101
|
+
<>
|
|
102
|
+
<Mail className="mr-2 h-4 w-4" />
|
|
103
|
+
Login with Email
|
|
104
|
+
</>
|
|
105
|
+
),
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const Loading: Story = {
|
|
110
|
+
args: {
|
|
111
|
+
disabled: true,
|
|
112
|
+
children: (
|
|
113
|
+
<>
|
|
114
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
115
|
+
Please wait
|
|
116
|
+
</>
|
|
117
|
+
),
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const Disabled: Story = {
|
|
122
|
+
args: {
|
|
123
|
+
children: 'Disabled',
|
|
124
|
+
disabled: true,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const AllVariants: Story = {
|
|
129
|
+
render: () => (
|
|
130
|
+
<div className="flex flex-wrap gap-4">
|
|
131
|
+
<ShadcnButton variant="default">Default</ShadcnButton>
|
|
132
|
+
<ShadcnButton variant="destructive">Destructive</ShadcnButton>
|
|
133
|
+
<ShadcnButton variant="outline">Outline</ShadcnButton>
|
|
134
|
+
<ShadcnButton variant="secondary">Secondary</ShadcnButton>
|
|
135
|
+
<ShadcnButton variant="ghost">Ghost</ShadcnButton>
|
|
136
|
+
<ShadcnButton variant="link">Link</ShadcnButton>
|
|
137
|
+
</div>
|
|
138
|
+
),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const AllSizes: Story = {
|
|
142
|
+
render: () => (
|
|
143
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
144
|
+
<ShadcnButton size="sm">Small</ShadcnButton>
|
|
145
|
+
<ShadcnButton size="default">Default</ShadcnButton>
|
|
146
|
+
<ShadcnButton size="lg">Large</ShadcnButton>
|
|
147
|
+
<ShadcnButton size="icon">
|
|
148
|
+
<ChevronRight className="h-4 w-4" />
|
|
149
|
+
</ShadcnButton>
|
|
150
|
+
</div>
|
|
151
|
+
),
|
|
152
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import {
|
|
3
|
+
ShadcnCarousel,
|
|
4
|
+
ShadcnCarouselContent,
|
|
5
|
+
ShadcnCarouselItem,
|
|
6
|
+
ShadcnCarouselPrevious,
|
|
7
|
+
ShadcnCarouselNext,
|
|
8
|
+
} from './ShadcnCarousel';
|
|
9
|
+
import Image from '../Image';
|
|
10
|
+
|
|
11
|
+
const meta: Meta<typeof ShadcnCarousel> = {
|
|
12
|
+
title: 'Components/ShadcnCarousel',
|
|
13
|
+
component: ShadcnCarousel,
|
|
14
|
+
tags: ['autodocs'],
|
|
15
|
+
argTypes: {
|
|
16
|
+
orientation: {
|
|
17
|
+
control: 'select',
|
|
18
|
+
options: ['horizontal', 'vertical'],
|
|
19
|
+
description: 'Carousel orientation',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default meta;
|
|
25
|
+
type Story = StoryObj<typeof ShadcnCarousel>;
|
|
26
|
+
|
|
27
|
+
const sampleImages = [
|
|
28
|
+
'https://picsum.photos/800/600?random=1',
|
|
29
|
+
'https://picsum.photos/800/600?random=2',
|
|
30
|
+
'https://picsum.photos/800/600?random=3',
|
|
31
|
+
'https://picsum.photos/800/600?random=4',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export const Default: Story = {
|
|
35
|
+
render: () => (
|
|
36
|
+
<div className="mx-auto w-full max-w-2xl">
|
|
37
|
+
<ShadcnCarousel className="w-full">
|
|
38
|
+
<ShadcnCarouselContent>
|
|
39
|
+
{sampleImages.map((src, index) => (
|
|
40
|
+
<ShadcnCarouselItem key={index} className="basis-full">
|
|
41
|
+
<div className="aspect-video overflow-hidden rounded-lg bg-muted">
|
|
42
|
+
<Image
|
|
43
|
+
src={src}
|
|
44
|
+
alt={`Slide ${index + 1}`}
|
|
45
|
+
className="h-full w-full object-cover"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
</ShadcnCarouselItem>
|
|
49
|
+
))}
|
|
50
|
+
</ShadcnCarouselContent>
|
|
51
|
+
<ShadcnCarouselPrevious />
|
|
52
|
+
<ShadcnCarouselNext />
|
|
53
|
+
</ShadcnCarousel>
|
|
54
|
+
</div>
|
|
55
|
+
),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const MultipleSlides: Story = {
|
|
59
|
+
render: () => (
|
|
60
|
+
<div className="mx-auto w-full max-w-4xl">
|
|
61
|
+
<ShadcnCarousel className="w-full">
|
|
62
|
+
<ShadcnCarouselContent>
|
|
63
|
+
{sampleImages.map((src, index) => (
|
|
64
|
+
<ShadcnCarouselItem key={index} className="basis-1/2 lg:basis-1/3">
|
|
65
|
+
<div className="aspect-square overflow-hidden rounded-lg bg-muted">
|
|
66
|
+
<Image
|
|
67
|
+
src={src}
|
|
68
|
+
alt={`Slide ${index + 1}`}
|
|
69
|
+
className="h-full w-full object-cover"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
</ShadcnCarouselItem>
|
|
73
|
+
))}
|
|
74
|
+
</ShadcnCarouselContent>
|
|
75
|
+
<ShadcnCarouselPrevious />
|
|
76
|
+
<ShadcnCarouselNext />
|
|
77
|
+
</ShadcnCarousel>
|
|
78
|
+
</div>
|
|
79
|
+
),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const WithContent: Story = {
|
|
83
|
+
render: () => (
|
|
84
|
+
<div className="mx-auto w-full max-w-2xl">
|
|
85
|
+
<ShadcnCarousel className="w-full">
|
|
86
|
+
<ShadcnCarouselContent>
|
|
87
|
+
{[1, 2, 3, 4, 5].map((num) => (
|
|
88
|
+
<ShadcnCarouselItem key={num} className="basis-full">
|
|
89
|
+
<div className="rounded-lg border border-border bg-card p-8">
|
|
90
|
+
<h3 className="mb-4 text-2xl font-bold">Slide {num}</h3>
|
|
91
|
+
<p className="text-muted-foreground">
|
|
92
|
+
This is content for slide {num}. You can put any content here including
|
|
93
|
+
images, text, cards, or other components.
|
|
94
|
+
</p>
|
|
95
|
+
</div>
|
|
96
|
+
</ShadcnCarouselItem>
|
|
97
|
+
))}
|
|
98
|
+
</ShadcnCarouselContent>
|
|
99
|
+
<ShadcnCarouselPrevious />
|
|
100
|
+
<ShadcnCarouselNext />
|
|
101
|
+
</ShadcnCarousel>
|
|
102
|
+
</div>
|
|
103
|
+
),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const ProductImages: Story = {
|
|
107
|
+
render: () => (
|
|
108
|
+
<div className="mx-auto w-full max-w-2xl">
|
|
109
|
+
<div className="rounded-lg bg-muted p-4">
|
|
110
|
+
<ShadcnCarousel className="w-full">
|
|
111
|
+
<ShadcnCarouselContent>
|
|
112
|
+
{sampleImages.map((src, index) => (
|
|
113
|
+
<ShadcnCarouselItem key={index} className="basis-full">
|
|
114
|
+
<div className="aspect-square overflow-hidden rounded-lg">
|
|
115
|
+
<Image
|
|
116
|
+
src={src}
|
|
117
|
+
alt={`Product image ${index + 1}`}
|
|
118
|
+
className="h-full w-full object-cover"
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
</ShadcnCarouselItem>
|
|
122
|
+
))}
|
|
123
|
+
</ShadcnCarouselContent>
|
|
124
|
+
<ShadcnCarouselPrevious />
|
|
125
|
+
<ShadcnCarouselNext />
|
|
126
|
+
</ShadcnCarousel>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const WithoutControls: Story = {
|
|
133
|
+
render: () => (
|
|
134
|
+
<div className="mx-auto w-full max-w-2xl">
|
|
135
|
+
<ShadcnCarousel className="w-full">
|
|
136
|
+
<ShadcnCarouselContent>
|
|
137
|
+
{sampleImages.map((src, index) => (
|
|
138
|
+
<ShadcnCarouselItem key={index} className="basis-full">
|
|
139
|
+
<div className="aspect-video overflow-hidden rounded-lg bg-muted">
|
|
140
|
+
<Image
|
|
141
|
+
src={src}
|
|
142
|
+
alt={`Slide ${index + 1}`}
|
|
143
|
+
className="h-full w-full object-cover"
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
</ShadcnCarouselItem>
|
|
147
|
+
))}
|
|
148
|
+
</ShadcnCarouselContent>
|
|
149
|
+
</ShadcnCarousel>
|
|
150
|
+
<p className="mt-4 text-center text-sm text-muted-foreground">
|
|
151
|
+
Swipe or use keyboard arrows to navigate
|
|
152
|
+
</p>
|
|
153
|
+
</div>
|
|
154
|
+
),
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const ThumbnailGallery: Story = {
|
|
158
|
+
render: () => (
|
|
159
|
+
<div className="mx-auto w-full max-w-4xl">
|
|
160
|
+
<ShadcnCarousel className="w-full">
|
|
161
|
+
<ShadcnCarouselContent>
|
|
162
|
+
{sampleImages.map((src, index) => (
|
|
163
|
+
<ShadcnCarouselItem key={index} className="basis-1/4">
|
|
164
|
+
<div className="aspect-square overflow-hidden rounded-lg border-2 border-border transition-colors hover:border-primary">
|
|
165
|
+
<Image
|
|
166
|
+
src={src}
|
|
167
|
+
alt={`Thumbnail ${index + 1}`}
|
|
168
|
+
className="h-full w-full object-cover"
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
</ShadcnCarouselItem>
|
|
172
|
+
))}
|
|
173
|
+
</ShadcnCarouselContent>
|
|
174
|
+
<ShadcnCarouselPrevious />
|
|
175
|
+
<ShadcnCarouselNext />
|
|
176
|
+
</ShadcnCarousel>
|
|
177
|
+
</div>
|
|
178
|
+
),
|
|
179
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { VariantSelector } from './VariantSelector';
|
|
4
|
+
import type { VariantOption } from './VariantSelector.types';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof VariantSelector> = {
|
|
7
|
+
title: 'Components/VariantSelector',
|
|
8
|
+
component: VariantSelector,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
type Story = StoryObj<typeof VariantSelector>;
|
|
14
|
+
|
|
15
|
+
const sizeOptions: VariantOption[] = [
|
|
16
|
+
{ value: 'xs', label: 'XS' },
|
|
17
|
+
{ value: 's', label: 'S' },
|
|
18
|
+
{ value: 'm', label: 'M' },
|
|
19
|
+
{ value: 'l', label: 'L' },
|
|
20
|
+
{ value: 'xl', label: 'XL' },
|
|
21
|
+
{ value: 'xxl', label: 'XXL', disabled: true },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const colorOptions: VariantOption[] = [
|
|
25
|
+
{ value: '#000000', label: 'Black' },
|
|
26
|
+
{ value: '#1e3a8a', label: 'Navy' },
|
|
27
|
+
{ value: '#7f1d1d', label: 'Burgundy' },
|
|
28
|
+
{ value: '#064e3b', label: 'Forest Green' },
|
|
29
|
+
{ value: '#78716c', label: 'Stone', disabled: true },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export const SizeButtons: Story = {
|
|
33
|
+
render: () => {
|
|
34
|
+
const [selected, setSelected] = useState('m');
|
|
35
|
+
return (
|
|
36
|
+
<VariantSelector
|
|
37
|
+
label="Size"
|
|
38
|
+
options={sizeOptions}
|
|
39
|
+
value={selected}
|
|
40
|
+
onChange={setSelected}
|
|
41
|
+
type="buttons"
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const ColorSwatches: Story = {
|
|
48
|
+
render: () => {
|
|
49
|
+
const [selected, setSelected] = useState('#000000');
|
|
50
|
+
return (
|
|
51
|
+
<VariantSelector
|
|
52
|
+
label="Color"
|
|
53
|
+
options={colorOptions}
|
|
54
|
+
value={selected}
|
|
55
|
+
onChange={setSelected}
|
|
56
|
+
type="swatches"
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const SizeDropdown: Story = {
|
|
63
|
+
render: () => {
|
|
64
|
+
const [selected, setSelected] = useState('');
|
|
65
|
+
return (
|
|
66
|
+
<VariantSelector
|
|
67
|
+
label="Size"
|
|
68
|
+
options={sizeOptions}
|
|
69
|
+
value={selected}
|
|
70
|
+
onChange={setSelected}
|
|
71
|
+
type="dropdown"
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const Combined: Story = {
|
|
78
|
+
render: () => {
|
|
79
|
+
const [size, setSize] = useState('m');
|
|
80
|
+
const [color, setColor] = useState('#000000');
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="space-y-6">
|
|
84
|
+
<VariantSelector
|
|
85
|
+
label="Size"
|
|
86
|
+
options={sizeOptions}
|
|
87
|
+
value={size}
|
|
88
|
+
onChange={setSize}
|
|
89
|
+
type="buttons"
|
|
90
|
+
/>
|
|
91
|
+
<VariantSelector
|
|
92
|
+
label="Color"
|
|
93
|
+
options={colorOptions}
|
|
94
|
+
value={color}
|
|
95
|
+
onChange={setColor}
|
|
96
|
+
type="swatches"
|
|
97
|
+
/>
|
|
98
|
+
<div className="rounded-md bg-muted p-4">
|
|
99
|
+
<p className="text-sm">
|
|
100
|
+
Selected: <strong>Size {size.toUpperCase()}</strong> in{' '}
|
|
101
|
+
<strong>{colorOptions.find(c => c.value === color)?.label}</strong>
|
|
102
|
+
</p>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { cn } from '../../utils';
|
|
4
|
+
import { VariantSelectorProps } from './VariantSelector.types';
|
|
5
|
+
import { Check } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
const variantButtonVariants = cva(
|
|
8
|
+
'inline-flex items-center justify-center rounded-md border text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
selected: {
|
|
12
|
+
true: 'border-primary bg-primary text-primary-foreground',
|
|
13
|
+
false: 'border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
selected: false,
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const swatchVariants = cva(
|
|
23
|
+
'relative h-10 w-10 rounded-full border-2 transition-all cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
|
24
|
+
{
|
|
25
|
+
variants: {
|
|
26
|
+
selected: {
|
|
27
|
+
true: 'border-primary ring-2 ring-primary ring-offset-2',
|
|
28
|
+
false: 'border-border hover:border-primary/50',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: {
|
|
32
|
+
selected: false,
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export const VariantSelector = React.forwardRef<HTMLDivElement, VariantSelectorProps>(
|
|
38
|
+
({ label, options, value, onChange, type = 'buttons', className, testID }, ref) => {
|
|
39
|
+
const handleOptionClick = (optionValue: string, disabled?: boolean) => {
|
|
40
|
+
if (!disabled) {
|
|
41
|
+
onChange(optionValue);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const renderButtons = () => (
|
|
46
|
+
<div className="flex flex-wrap gap-2">
|
|
47
|
+
{options.map((option) => (
|
|
48
|
+
<button
|
|
49
|
+
key={option.value}
|
|
50
|
+
type="button"
|
|
51
|
+
onClick={() => handleOptionClick(option.value, option.disabled)}
|
|
52
|
+
disabled={option.disabled}
|
|
53
|
+
className={cn(
|
|
54
|
+
variantButtonVariants({ selected: value === option.value }),
|
|
55
|
+
'min-w-16 px-4 py-2'
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
{option.label}
|
|
59
|
+
</button>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const renderSwatches = () => (
|
|
65
|
+
<div className="flex flex-wrap gap-3">
|
|
66
|
+
{options.map((option) => (
|
|
67
|
+
<button
|
|
68
|
+
key={option.value}
|
|
69
|
+
type="button"
|
|
70
|
+
onClick={() => handleOptionClick(option.value, option.disabled)}
|
|
71
|
+
disabled={option.disabled}
|
|
72
|
+
className={cn(swatchVariants({ selected: value === option.value }))}
|
|
73
|
+
style={{ backgroundColor: option.value }}
|
|
74
|
+
title={option.label}
|
|
75
|
+
aria-label={option.label}
|
|
76
|
+
>
|
|
77
|
+
{value === option.value && (
|
|
78
|
+
<Check className="absolute inset-0 m-auto h-5 w-5 text-white drop-shadow-lg" />
|
|
79
|
+
)}
|
|
80
|
+
</button>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const renderDropdown = () => (
|
|
86
|
+
<select
|
|
87
|
+
value={value || ''}
|
|
88
|
+
onChange={(e) => onChange(e.target.value)}
|
|
89
|
+
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
|
90
|
+
>
|
|
91
|
+
<option value="" disabled>
|
|
92
|
+
Select {label}
|
|
93
|
+
</option>
|
|
94
|
+
{options.map((option) => (
|
|
95
|
+
<option key={option.value} value={option.value} disabled={option.disabled}>
|
|
96
|
+
{option.label}
|
|
97
|
+
</option>
|
|
98
|
+
))}
|
|
99
|
+
</select>
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div ref={ref} data-testid={testID || 'VariantSelector'} className={cn('space-y-2', className)}>
|
|
104
|
+
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
|
105
|
+
{label}
|
|
106
|
+
</label>
|
|
107
|
+
{type === 'dropdown' && renderDropdown()}
|
|
108
|
+
{type === 'buttons' && renderButtons()}
|
|
109
|
+
{type === 'swatches' && renderSwatches()}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
VariantSelector.displayName = 'VariantSelector';
|
|
116
|
+
|
|
117
|
+
export { variantButtonVariants, swatchVariants };
|
|
118
|
+
export type { VariantSelectorProps };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface VariantOption {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface VariantSelectorProps {
|
|
8
|
+
label: string;
|
|
9
|
+
options: VariantOption[];
|
|
10
|
+
value?: string;
|
|
11
|
+
onChange: (value: string) => void;
|
|
12
|
+
type?: 'dropdown' | 'buttons' | 'swatches';
|
|
13
|
+
className?: string;
|
|
14
|
+
testID?: string;
|
|
15
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -70,6 +70,15 @@ export { default as FullScreenVideoModal } from './FullScreenVideoModal';
|
|
|
70
70
|
export { default as Cart } from './Cart';
|
|
71
71
|
export { default as FullContentBackgroundImage } from './FullContentBackgroundImage';
|
|
72
72
|
|
|
73
|
+
// SHADCN UI COMPONENTS
|
|
74
|
+
export { ShadcnButton, buttonVariants } from './ShadcnButton';
|
|
75
|
+
export { ShadcnCarousel } from './ShadcnCarousel';
|
|
76
|
+
|
|
77
|
+
// PRODUCT COMPONENTS
|
|
78
|
+
export { ProductCard, productCardVariants } from './ProductCard';
|
|
79
|
+
export { VariantSelector, variantButtonVariants, swatchVariants } from './VariantSelector';
|
|
80
|
+
export { ProductDetail } from './ProductDetail';
|
|
81
|
+
|
|
73
82
|
// TYPES
|
|
74
83
|
export type { Props as BlockquoteProps } from './Blockquote/Blockquote.types';
|
|
75
84
|
export type { Props as ButtonProps } from './Button/Button.types';
|
|
@@ -141,3 +150,11 @@ export type { Props as FullScreenVideoModalProps } from './FullScreenVideoModal/
|
|
|
141
150
|
export type { Props as CartProps } from './Cart/Cart.types';
|
|
142
151
|
export type { Props as FullContentBackgroundImageProps } from './FullContentBackgroundImage/FullContentBackgroundImage.types';
|
|
143
152
|
export type { Props as HeaderProps } from './Header/Header.types';
|
|
153
|
+
|
|
154
|
+
// SHADCN UI TYPES
|
|
155
|
+
export type { ShadcnButtonProps } from './ShadcnButton';
|
|
156
|
+
|
|
157
|
+
// PRODUCT COMPONENT TYPES
|
|
158
|
+
export type { ProductCardProps, ProductCardVariantProps, Product } from './ProductCard';
|
|
159
|
+
export type { VariantSelectorProps, VariantOption } from './VariantSelector';
|
|
160
|
+
export type { ProductDetailProps, ProductDetailState } from './ProductDetail';
|
|
@@ -5,13 +5,11 @@ import React, { useContext } from 'react';
|
|
|
5
5
|
interface ThemeContextProps {
|
|
6
6
|
isDarkTheme: boolean;
|
|
7
7
|
toggleTheme: () => void;
|
|
8
|
-
theme?: any;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
10
|
export const defaultTheme: ThemeContextProps = {
|
|
12
11
|
isDarkTheme: false,
|
|
13
12
|
toggleTheme: () => {},
|
|
14
|
-
theme: undefined,
|
|
15
13
|
};
|
|
16
14
|
|
|
17
15
|
export const ThemeContext = React.createContext<ThemeContextProps>(defaultTheme);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
2
|
import cx from 'classnames';
|
|
3
3
|
import { Props } from './LoginForm.types';
|
|
4
4
|
import { socialLoginUrlApple, socialLoginUrlGoogle } from '../constants';
|
|
@@ -80,7 +80,6 @@ const LoginForm = ({
|
|
|
80
80
|
<div className="flex flex-col gap-4 max-w-[250px]">
|
|
81
81
|
{socialLoginUrlGoogle && (
|
|
82
82
|
<Button
|
|
83
|
-
// onClick={() => window.location.assign(socialLoginUrlGoogle)}
|
|
84
83
|
onClick={() => authWithSocial(CognitoHostedUIIdentityProvider.Google)}
|
|
85
84
|
variant={BTN_VARIANTS.PRIMARY}
|
|
86
85
|
iconFirst
|
|
@@ -90,7 +89,6 @@ const LoginForm = ({
|
|
|
90
89
|
)}
|
|
91
90
|
{socialLoginUrlApple && (
|
|
92
91
|
<Button
|
|
93
|
-
// onClick={() => window.location.assign(socialLoginUrlApple)}
|
|
94
92
|
onClick={() => authWithSocial(CognitoHostedUIIdentityProvider.Apple)}
|
|
95
93
|
variant={BTN_VARIANTS.PRIMARY}
|
|
96
94
|
iconFirst
|