@4alldigital/foundation-ui--core 3.2.1 → 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.
Files changed (30) hide show
  1. package/package.json +2 -2
  2. package/src/components/Button/Button.tsx +1 -1
  3. package/src/components/Form/Form.tsx +1 -1
  4. package/src/components/ProductCard/ProductCard.stories.tsx +90 -0
  5. package/src/components/ProductCard/ProductCard.tsx +110 -0
  6. package/src/components/ProductCard/ProductCard.types.ts +21 -0
  7. package/src/components/ProductCard/index.ts +3 -0
  8. package/src/components/ProductDetail/ProductDetail.stories.tsx +97 -0
  9. package/src/components/ProductDetail/ProductDetail.tsx +251 -0
  10. package/src/components/ProductDetail/ProductDetail.types.ts +16 -0
  11. package/src/components/ProductDetail/index.ts +2 -0
  12. package/src/components/ShadcnButton/ShadcnButton.stories.tsx +152 -0
  13. package/src/components/ShadcnCarousel/ShadcnCarousel.stories.tsx +179 -0
  14. package/src/components/VariantSelector/VariantSelector.stories.tsx +107 -0
  15. package/src/components/VariantSelector/VariantSelector.tsx +118 -0
  16. package/src/components/VariantSelector/VariantSelector.types.ts +15 -0
  17. package/src/components/VariantSelector/index.ts +2 -0
  18. package/src/components/index.ts +17 -0
  19. package/src/context/Amplify/index.tsx +1 -0
  20. package/src/forms/LoginForm/LoginForm.tsx +1 -3
  21. package/src/styles/tokens.css +117 -0
  22. package/src/templates/ProductDetailScreen/ProductDetailScreen.stories.tsx +108 -0
  23. package/src/templates/ProductDetailScreen/ProductDetailScreen.tsx +35 -0
  24. package/src/templates/ProductDetailScreen/ProductDetailScreen.types.ts +11 -0
  25. package/src/templates/ProductDetailScreen/index.ts +1 -0
  26. package/src/templates/ProductListScreen/ProductListScreen.stories.tsx +121 -0
  27. package/src/templates/ProductListScreen/ProductListScreen.tsx +59 -0
  28. package/src/templates/ProductListScreen/ProductListScreen.types.ts +10 -0
  29. package/src/templates/ProductListScreen/index.ts +1 -0
  30. package/src/templates/index.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4alldigital/foundation-ui--core",
3
- "version": "3.2.1",
3
+ "version": "3.4.0",
4
4
  "description": "Foundation UI Core Component Library (source distribution)",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -32,5 +32,5 @@
32
32
  },
33
33
  "author": "Joe Mewes",
34
34
  "license": "MIT",
35
- "gitHead": "0c4e012771a223879958443f3977c2cdf02a5b1a"
35
+ "gitHead": "588323f234033e2394e15a7ff030cf57888f6708"
36
36
  }
@@ -56,7 +56,7 @@ const Button = ({
56
56
 
57
57
  const classes = twMerge(
58
58
  cx(
59
- 'button rounded-btnBorderRadius',
59
+ 'button flex rounded-sm items-center',
60
60
  { shadow: raised },
61
61
  { uppercase: uppercase },
62
62
  { 'auto-cols-auto grid-cols-2 gap-4': isLoading || icon || external },
@@ -65,7 +65,7 @@ const Form = ({
65
65
  }
66
66
  }}
67
67
  className={cx(
68
- 'w-2xs relative',
68
+ 'relative',
69
69
  { 'form px-8 pt-6 pb-8 ': isBoxed },
70
70
  { 'opacity-50': isSubmitting },
71
71
  className,
@@ -0,0 +1,90 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { ProductCard } from './ProductCard';
3
+ import type { Product } from './ProductCard.types';
4
+
5
+ const meta: Meta<typeof ProductCard> = {
6
+ title: 'Components/ProductCard',
7
+ component: ProductCard,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ onClick: { action: 'clicked' },
11
+ },
12
+ };
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof ProductCard>;
16
+
17
+ const sampleProduct: Product = {
18
+ id: 'prod_123',
19
+ name: 'NRG Performance Leggings',
20
+ description:
21
+ 'High-performance leggings designed for intense workouts. Features moisture-wicking fabric and four-way stretch for maximum comfort and flexibility.',
22
+ images: [
23
+ 'https://picsum.photos/seed/product1/600/600',
24
+ 'https://picsum.photos/seed/product2/600/600',
25
+ ],
26
+ price: 4500, // £45.00 in pence
27
+ currency: 'gbp',
28
+ metadata: {
29
+ partnerId: 'lululemon',
30
+ sizes: ['XS', 'S', 'M', 'L', 'XL'],
31
+ colors: ['black', 'navy', 'burgundy'],
32
+ },
33
+ };
34
+
35
+ const shortDescriptionProduct: Product = {
36
+ id: 'prod_456',
37
+ name: 'Yoga Mat Pro',
38
+ description: 'Premium yoga mat with extra cushioning.',
39
+ images: ['https://picsum.photos/seed/product3/600/600'],
40
+ price: 2999,
41
+ currency: 'gbp',
42
+ metadata: {
43
+ partnerId: 'manduka',
44
+ colors: ['purple', 'blue', 'green'],
45
+ },
46
+ };
47
+
48
+ const noImageProduct: Product = {
49
+ id: 'prod_789',
50
+ name: 'Resistance Bands Set',
51
+ description: 'Set of 5 resistance bands for home workouts',
52
+ images: [],
53
+ price: 1999,
54
+ currency: 'gbp',
55
+ };
56
+
57
+ export const Default: Story = {
58
+ args: {
59
+ product: sampleProduct,
60
+ },
61
+ };
62
+
63
+ export const Compact: Story = {
64
+ args: {
65
+ product: sampleProduct,
66
+ variant: 'compact',
67
+ },
68
+ };
69
+
70
+ export const ShortDescription: Story = {
71
+ args: {
72
+ product: shortDescriptionProduct,
73
+ },
74
+ };
75
+
76
+ export const NoImage: Story = {
77
+ args: {
78
+ product: noImageProduct,
79
+ },
80
+ };
81
+
82
+ export const Grid: Story = {
83
+ render: () => (
84
+ <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
85
+ <ProductCard product={sampleProduct} />
86
+ <ProductCard product={shortDescriptionProduct} />
87
+ <ProductCard product={noImageProduct} />
88
+ </div>
89
+ ),
90
+ };
@@ -0,0 +1,110 @@
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { cn } from '../../utils';
4
+ import { ProductCardProps } from './ProductCard.types';
5
+ import Image from '../Image';
6
+ import { ShadcnButton } from '../ShadcnButton';
7
+
8
+ const productCardVariants = cva(
9
+ 'group relative flex flex-col overflow-hidden rounded-lg border border-border bg-card transition-all hover:shadow-lg',
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default: 'hover:border-primary/50',
14
+ compact: 'hover:border-primary/30',
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ variant: 'default',
19
+ },
20
+ }
21
+ );
22
+
23
+ export interface ProductCardVariantProps
24
+ extends ProductCardProps,
25
+ VariantProps<typeof productCardVariants> {}
26
+
27
+ const ProductCard = React.forwardRef<HTMLDivElement, ProductCardVariantProps>(
28
+ ({ product, onClick, className, testID, variant, ...props }, ref) => {
29
+ const formatPrice = (price: number, currency: string) => {
30
+ return new Intl.NumberFormat('en-GB', {
31
+ style: 'currency',
32
+ currency: currency.toUpperCase(),
33
+ }).format(price / 100);
34
+ };
35
+
36
+ const handleClick = () => {
37
+ if (onClick) {
38
+ onClick(product);
39
+ }
40
+ };
41
+
42
+ const primaryImage = product.images[0] || '';
43
+ const truncatedDescription = product.description
44
+ ? product.description.length > 100
45
+ ? `${product.description.substring(0, 100)}...`
46
+ : product.description
47
+ : '';
48
+
49
+ return (
50
+ <div
51
+ ref={ref}
52
+ data-testid={testID || 'ProductCard'}
53
+ className={cn(productCardVariants({ variant, className }))}
54
+ {...props}
55
+ >
56
+ {/* Image Section */}
57
+ <div className="relative aspect-square w-full overflow-hidden bg-muted">
58
+ {primaryImage ? (
59
+ <div className="relative h-full w-full transition-transform duration-300 group-hover:scale-105">
60
+ <Image
61
+ src={primaryImage}
62
+ alt={product.name}
63
+ className="h-full w-full object-cover"
64
+ />
65
+ </div>
66
+ ) : (
67
+ <div className="flex h-full w-full items-center justify-center bg-muted">
68
+ <span className="text-muted-foreground">No image</span>
69
+ </div>
70
+ )}
71
+ </div>
72
+
73
+ {/* Content Section */}
74
+ <div className="flex flex-1 flex-col p-4">
75
+ {/* Product Name */}
76
+ <h3 className="mb-2 line-clamp-2 text-lg font-semibold text-foreground">
77
+ {product.name}
78
+ </h3>
79
+
80
+ {/* Description */}
81
+ {truncatedDescription && (
82
+ <p className="mb-4 line-clamp-2 text-sm text-muted-foreground">
83
+ {truncatedDescription}
84
+ </p>
85
+ )}
86
+
87
+ {/* Price and Button */}
88
+ <div className="mt-auto flex items-center justify-between">
89
+ <span className="text-xl font-bold text-foreground">
90
+ {formatPrice(product.price, product.currency)}
91
+ </span>
92
+ <ShadcnButton
93
+ variant="outline"
94
+ size="sm"
95
+ onClick={handleClick}
96
+ className="transition-colors hover:bg-primary hover:text-primary-foreground"
97
+ >
98
+ View Details
99
+ </ShadcnButton>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ );
104
+ }
105
+ );
106
+
107
+ ProductCard.displayName = 'ProductCard';
108
+
109
+ export { ProductCard, productCardVariants };
110
+ export type { ProductCardProps, ProductCardVariantProps };
@@ -0,0 +1,21 @@
1
+ export interface Product {
2
+ id: string;
3
+ name: string;
4
+ description?: string;
5
+ images: string[];
6
+ price: number;
7
+ currency: string;
8
+ metadata?: {
9
+ partnerId?: string;
10
+ sizes?: string[];
11
+ colors?: string[];
12
+ [key: string]: any;
13
+ };
14
+ }
15
+
16
+ export interface ProductCardProps {
17
+ product: Product;
18
+ onClick?: (product: Product) => void;
19
+ className?: string;
20
+ testID?: string;
21
+ }
@@ -0,0 +1,3 @@
1
+ export { ProductCard, productCardVariants } from './ProductCard';
2
+ export type { ProductCardProps, ProductCardVariantProps } from './ProductCard';
3
+ export type { Product } from './ProductCard.types';
@@ -0,0 +1,97 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { ProductDetail } from './ProductDetail';
3
+ import type { Product } from '../ProductCard/ProductCard.types';
4
+ import type { ProductDetailState } from './ProductDetail.types';
5
+
6
+ const meta: Meta<typeof ProductDetail> = {
7
+ title: 'Components/ProductDetail',
8
+ component: ProductDetail,
9
+ tags: ['autodocs'],
10
+ parameters: {
11
+ layout: 'padded',
12
+ },
13
+ };
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof ProductDetail>;
17
+
18
+ const sampleProduct: Product = {
19
+ id: 'prod_123',
20
+ name: 'NRG Performance Leggings',
21
+ description:
22
+ 'High-performance leggings designed for intense workouts. Features moisture-wicking fabric, four-way stretch for maximum comfort and flexibility, and a high-waisted design for extra support. Perfect for yoga, running, or any high-intensity training.',
23
+ images: [
24
+ 'https://picsum.photos/seed/product1/800/800',
25
+ 'https://picsum.photos/seed/product2/800/800',
26
+ 'https://picsum.photos/seed/product3/800/800',
27
+ ],
28
+ price: 4500,
29
+ currency: 'gbp',
30
+ metadata: {
31
+ partnerId: 'lululemon',
32
+ sizes: ['XS', 'S', 'M', 'L', 'XL'],
33
+ colors: ['Black', 'Navy', 'Burgundy'],
34
+ },
35
+ };
36
+
37
+ const noVariantsProduct: Product = {
38
+ id: 'prod_456',
39
+ name: 'Yoga Mat Pro',
40
+ description:
41
+ 'Premium 6mm thick yoga mat with excellent grip and cushioning. Made from eco-friendly TPE material.',
42
+ images: ['https://picsum.photos/seed/product4/800/800'],
43
+ price: 2999,
44
+ currency: 'gbp',
45
+ metadata: {
46
+ partnerId: 'manduka',
47
+ },
48
+ };
49
+
50
+ const handlePurchase = (state: ProductDetailState) => {
51
+ console.log('Purchase clicked:', state);
52
+ alert(
53
+ `Purchase: ${state.quantity}x item(s)${state.selectedSize ? `, Size: ${state.selectedSize}` : ''}${state.selectedColor ? `, Color: ${state.selectedColor}` : ''}`
54
+ );
55
+ };
56
+
57
+ export const Default: Story = {
58
+ args: {
59
+ product: sampleProduct,
60
+ onPurchase: handlePurchase,
61
+ },
62
+ };
63
+
64
+ export const NoVariants: Story = {
65
+ args: {
66
+ product: noVariantsProduct,
67
+ onPurchase: handlePurchase,
68
+ },
69
+ };
70
+
71
+ export const Loading: Story = {
72
+ args: {
73
+ product: sampleProduct,
74
+ onPurchase: handlePurchase,
75
+ isLoading: true,
76
+ },
77
+ };
78
+
79
+ export const SingleImage: Story = {
80
+ args: {
81
+ product: {
82
+ ...sampleProduct,
83
+ images: ['https://picsum.photos/seed/product1/800/800'],
84
+ },
85
+ onPurchase: handlePurchase,
86
+ },
87
+ };
88
+
89
+ export const NoImages: Story = {
90
+ args: {
91
+ product: {
92
+ ...sampleProduct,
93
+ images: [],
94
+ },
95
+ onPurchase: handlePurchase,
96
+ },
97
+ };
@@ -0,0 +1,251 @@
1
+ import * as React from 'react';
2
+ import { useState, useMemo } from 'react';
3
+ import { cn } from '../../utils';
4
+ import { ProductDetailProps, ProductDetailState } from './ProductDetail.types';
5
+ import { VariantSelector } from '../VariantSelector';
6
+ import type { VariantOption } from '../VariantSelector';
7
+ import { ShadcnButton } from '../ShadcnButton';
8
+ import {
9
+ ShadcnCarousel,
10
+ ShadcnCarouselContent,
11
+ ShadcnCarouselItem,
12
+ ShadcnCarouselPrevious,
13
+ ShadcnCarouselNext,
14
+ } from '../ShadcnCarousel';
15
+ import Image from '../Image';
16
+ import Tabs from '../Tabs';
17
+ import { Minus, Plus, ShoppingCart } from 'lucide-react';
18
+
19
+ export const ProductDetail = React.forwardRef<HTMLDivElement, ProductDetailProps>(
20
+ ({ product, onPurchase, isLoading = false, isAuthenticated = true, className, testID }, ref) => {
21
+ const [selectedSize, setSelectedSize] = useState<string | undefined>();
22
+ const [selectedColor, setSelectedColor] = useState<string | undefined>();
23
+ const [quantity, setQuantity] = useState(1);
24
+
25
+ const formatPrice = (price: number, currency: string) => {
26
+ return new Intl.NumberFormat('en-GB', {
27
+ style: 'currency',
28
+ currency: currency.toUpperCase(),
29
+ }).format(price / 100);
30
+ };
31
+
32
+ // Parse size options from metadata
33
+ const sizeOptions: VariantOption[] = useMemo(() => {
34
+ if (!product.metadata?.sizes) return [];
35
+ try {
36
+ const sizes = Array.isArray(product.metadata.sizes)
37
+ ? product.metadata.sizes
38
+ : JSON.parse(product.metadata.sizes);
39
+ return sizes.map((size: string) => ({
40
+ value: size.toLowerCase(),
41
+ label: size,
42
+ }));
43
+ } catch {
44
+ return [];
45
+ }
46
+ }, [product.metadata?.sizes]);
47
+
48
+ // Parse color options from metadata
49
+ const colorOptions: VariantOption[] = useMemo(() => {
50
+ if (!product.metadata?.colors) return [];
51
+ try {
52
+ const colors = Array.isArray(product.metadata.colors)
53
+ ? product.metadata.colors
54
+ : JSON.parse(product.metadata.colors);
55
+ return colors.map((color: string) => ({
56
+ value: color.toLowerCase(),
57
+ label: color.charAt(0).toUpperCase() + color.slice(1),
58
+ }));
59
+ } catch {
60
+ return [];
61
+ }
62
+ }, [product.metadata?.colors]);
63
+
64
+ const handleQuantityChange = (delta: number) => {
65
+ setQuantity(prev => Math.max(1, prev + delta));
66
+ };
67
+
68
+ const handlePurchase = () => {
69
+ const state: ProductDetailState = {
70
+ selectedSize,
71
+ selectedColor,
72
+ quantity,
73
+ };
74
+ onPurchase(state);
75
+ };
76
+
77
+ const totalPrice = product.price * quantity;
78
+ const canPurchase =
79
+ !isLoading &&
80
+ (sizeOptions.length === 0 || selectedSize) &&
81
+ (colorOptions.length === 0 || selectedColor);
82
+
83
+ // Tab content
84
+ const tabItems = [
85
+ {
86
+ title: 'Details',
87
+ content: (
88
+ <div className="prose prose-sm max-w-none">
89
+ <p>{product.description || 'No description available.'}</p>
90
+ </div>
91
+ ),
92
+ },
93
+ {
94
+ title: 'Shipping',
95
+ content: (
96
+ <div className="prose prose-sm max-w-none">
97
+ <p>Free UK shipping on orders over £50.</p>
98
+ <p>Standard delivery: 3-5 business days</p>
99
+ <p>Express delivery: 1-2 business days</p>
100
+ </div>
101
+ ),
102
+ },
103
+ {
104
+ title: 'Returns',
105
+ content: (
106
+ <div className="prose prose-sm max-w-none">
107
+ <p>30-day return policy for unworn items with tags attached.</p>
108
+ <p>Free returns within the UK.</p>
109
+ </div>
110
+ ),
111
+ },
112
+ ];
113
+
114
+ return (
115
+ <div ref={ref} data-testid={testID || 'ProductDetail'} className={cn('grid gap-8 lg:grid-cols-2', className)}>
116
+ {/* Image Gallery */}
117
+ <div className="space-y-4">
118
+ {product.images.length > 0 ? (
119
+ <div className="relative overflow-hidden rounded-lg bg-muted">
120
+ <ShadcnCarousel className="w-full">
121
+ <ShadcnCarouselContent>
122
+ {product.images.map((image, index) => (
123
+ <ShadcnCarouselItem key={index} className="basis-full">
124
+ <div className="aspect-square overflow-hidden">
125
+ <Image
126
+ src={image}
127
+ alt={`${product.name} - Image ${index + 1}`}
128
+ className="h-full w-full object-cover"
129
+ />
130
+ </div>
131
+ </ShadcnCarouselItem>
132
+ ))}
133
+ </ShadcnCarouselContent>
134
+ {product.images.length > 1 && (
135
+ <>
136
+ <ShadcnCarouselPrevious />
137
+ <ShadcnCarouselNext />
138
+ </>
139
+ )}
140
+ </ShadcnCarousel>
141
+ </div>
142
+ ) : (
143
+ <div className="flex aspect-square items-center justify-center rounded-lg bg-muted">
144
+ <span className="text-muted-foreground">No images available</span>
145
+ </div>
146
+ )}
147
+ </div>
148
+
149
+ {/* Product Info */}
150
+ <div className="space-y-6">
151
+ {/* Header */}
152
+ <div>
153
+ <h1 className="mb-2 text-3xl font-bold text-foreground">{product.name}</h1>
154
+ <p className="text-2xl font-bold text-primary">{formatPrice(product.price, product.currency)}</p>
155
+ </div>
156
+
157
+ {/* Variants */}
158
+ <div className="space-y-4">
159
+ {sizeOptions.length > 0 && (
160
+ <VariantSelector
161
+ label="Size"
162
+ options={sizeOptions}
163
+ value={selectedSize}
164
+ onChange={setSelectedSize}
165
+ type="buttons"
166
+ />
167
+ )}
168
+
169
+ {colorOptions.length > 0 && (
170
+ <VariantSelector
171
+ label="Color"
172
+ options={colorOptions}
173
+ value={selectedColor}
174
+ onChange={setSelectedColor}
175
+ type="buttons"
176
+ />
177
+ )}
178
+ </div>
179
+
180
+ {/* Quantity Selector */}
181
+ <div className="space-y-2">
182
+ <label className="text-sm font-medium">Quantity</label>
183
+ <div className="flex items-center gap-3">
184
+ <ShadcnButton
185
+ variant="outline"
186
+ size="icon"
187
+ onClick={() => handleQuantityChange(-1)}
188
+ disabled={quantity <= 1 || isLoading}>
189
+ <Minus className="h-4 w-4" />
190
+ </ShadcnButton>
191
+ <span className="w-12 text-center text-lg font-medium">{quantity}</span>
192
+ <ShadcnButton variant="outline" size="icon" onClick={() => handleQuantityChange(1)} disabled={isLoading}>
193
+ <Plus className="h-4 w-4" />
194
+ </ShadcnButton>
195
+ </div>
196
+ </div>
197
+
198
+ {/* Total Price */}
199
+ {quantity > 1 && (
200
+ <div className="rounded-md bg-muted p-4">
201
+ <div className="flex items-center justify-between">
202
+ <span className="text-sm font-medium">Total:</span>
203
+ <span className="text-xl font-bold">{formatPrice(totalPrice, product.currency)}</span>
204
+ </div>
205
+ </div>
206
+ )}
207
+
208
+ {/* Purchase Button or Auth Message */}
209
+ {!isAuthenticated ? (
210
+ <div className="rounded-lg border border-primary bg-primary/10 p-6 text-center">
211
+ <p className="mb-4 text-lg font-medium">Sign in to purchase</p>
212
+ <p className="text-sm text-muted-foreground">
213
+ You must be logged in to make a purchase. Please sign in or create an account to continue.
214
+ </p>
215
+ </div>
216
+ ) : (
217
+ <>
218
+ <ShadcnButton size="lg" className="w-full" onClick={handlePurchase} disabled={!canPurchase}>
219
+ {isLoading ? (
220
+ 'Processing...'
221
+ ) : (
222
+ <>
223
+ <ShoppingCart className="mr-2 h-5 w-5" />
224
+ Buy Now
225
+ </>
226
+ )}
227
+ </ShadcnButton>
228
+
229
+ {/* Validation Messages */}
230
+ {!canPurchase && !isLoading && (
231
+ <p className="text-sm text-muted-foreground">
232
+ {!selectedSize && sizeOptions.length > 0 && 'Please select a size. '}
233
+ {!selectedColor && colorOptions.length > 0 && 'Please select a color.'}
234
+ </p>
235
+ )}
236
+ </>
237
+ )}
238
+
239
+ {/* Tabs */}
240
+ <div className="border-t pt-6">
241
+ <Tabs data={tabItems} />
242
+ </div>
243
+ </div>
244
+ </div>
245
+ );
246
+ },
247
+ );
248
+
249
+ ProductDetail.displayName = 'ProductDetail';
250
+
251
+ export type { ProductDetailProps, ProductDetailState };
@@ -0,0 +1,16 @@
1
+ import { Product } from '../ProductCard/ProductCard.types';
2
+
3
+ export interface ProductDetailState {
4
+ selectedSize?: string;
5
+ selectedColor?: string;
6
+ quantity: number;
7
+ }
8
+
9
+ export interface ProductDetailProps {
10
+ product: Product;
11
+ onPurchase: (state: ProductDetailState) => void;
12
+ isLoading?: boolean;
13
+ isAuthenticated?: boolean;
14
+ className?: string;
15
+ testID?: string;
16
+ }
@@ -0,0 +1,2 @@
1
+ export { ProductDetail } from './ProductDetail';
2
+ export type { ProductDetailProps, ProductDetailState } from './ProductDetail.types';