@butternutbox/pawprint-native 0.3.2 → 0.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/.turbo/turbo-build.log +9 -9
- package/CHANGELOG.md +8 -0
- package/dist/index.cjs +965 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +246 -5
- package/dist/index.d.ts +246 -5
- package/dist/index.js +960 -151
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/components/atoms/Hint/Hint.tsx +1 -2
- package/src/components/atoms/Input/InputField.tsx +7 -1
- package/src/components/molecules/Animated/Animated.tsx +12 -3
- package/src/components/molecules/Countdown/Countdown.stories.tsx +218 -0
- package/src/components/molecules/Countdown/Countdown.tsx +315 -0
- package/src/components/molecules/Countdown/index.ts +2 -0
- package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.stories.tsx +248 -0
- package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.test.tsx +198 -0
- package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.tsx +243 -0
- package/src/components/molecules/ProductDisplayCard/index.ts +5 -0
- package/src/components/molecules/ProductListingCard/Badge.tsx +65 -0
- package/src/components/molecules/ProductListingCard/Grid.tsx +59 -0
- package/src/components/molecules/ProductListingCard/ProductListingCard.stories.tsx +209 -0
- package/src/components/molecules/ProductListingCard/ProductListingCard.tsx +235 -0
- package/src/components/molecules/ProductListingCard/index.ts +2 -0
- package/src/components/molecules/TabNavigation/TabNavigation.stories.tsx +183 -0
- package/src/components/molecules/TabNavigation/TabNavigation.tsx +354 -0
- package/src/components/molecules/TabNavigation/index.ts +7 -0
- package/src/components/molecules/index.ts +4 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/token.ts +1 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, StyleSheet, Image } from "react-native"
|
|
3
|
+
import { ProductDisplayCard } from "./ProductDisplayCard"
|
|
4
|
+
import type { ProductDisplayCardProps } from "./ProductDisplayCard"
|
|
5
|
+
|
|
6
|
+
const PlaceholderImage = () => (
|
|
7
|
+
<Image
|
|
8
|
+
source={{ uri: "https://placeholder.co/148x148?text=Recipe" }}
|
|
9
|
+
style={{ width: "100%", height: "100%" }}
|
|
10
|
+
/>
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
const styles = StyleSheet.create({
|
|
14
|
+
column: { flexDirection: "column", gap: 24, padding: 16 }
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
title: "Molecules/ProductDisplayCard",
|
|
19
|
+
component: ProductDisplayCard,
|
|
20
|
+
argTypes: {
|
|
21
|
+
device: {
|
|
22
|
+
control: { type: "select" },
|
|
23
|
+
options: ["desktop", "mobile"],
|
|
24
|
+
description: "Device variant affecting sizing and typography"
|
|
25
|
+
},
|
|
26
|
+
title: {
|
|
27
|
+
control: { type: "text" },
|
|
28
|
+
description: "Product title"
|
|
29
|
+
},
|
|
30
|
+
subtext: {
|
|
31
|
+
control: { type: "text" },
|
|
32
|
+
description: "Secondary text (e.g. price)"
|
|
33
|
+
},
|
|
34
|
+
showSubtext: {
|
|
35
|
+
control: { type: "boolean" },
|
|
36
|
+
description: "Show/hide subtext"
|
|
37
|
+
},
|
|
38
|
+
quantity: {
|
|
39
|
+
control: { type: "number", min: 1 },
|
|
40
|
+
description: "Current quantity"
|
|
41
|
+
},
|
|
42
|
+
showQuantityPicker: {
|
|
43
|
+
control: { type: "boolean" },
|
|
44
|
+
description: "Show/hide quantity picker"
|
|
45
|
+
},
|
|
46
|
+
disableQuantityButtons: {
|
|
47
|
+
control: { type: "boolean" },
|
|
48
|
+
description: "Disable quantity buttons"
|
|
49
|
+
},
|
|
50
|
+
hideQuantityButtons: {
|
|
51
|
+
control: { type: "boolean" },
|
|
52
|
+
description: "Hide quantity buttons"
|
|
53
|
+
},
|
|
54
|
+
showIncrementButton: {
|
|
55
|
+
control: { type: "boolean" },
|
|
56
|
+
description: "Show/hide increment button"
|
|
57
|
+
},
|
|
58
|
+
showDecrementButton: {
|
|
59
|
+
control: { type: "boolean" },
|
|
60
|
+
description: "Show/hide decrement button"
|
|
61
|
+
},
|
|
62
|
+
incrementDisabled: {
|
|
63
|
+
control: { type: "boolean" },
|
|
64
|
+
description: "Disable increment button"
|
|
65
|
+
},
|
|
66
|
+
decrementDisabled: {
|
|
67
|
+
control: { type: "boolean" },
|
|
68
|
+
description: "Disable decrement button"
|
|
69
|
+
},
|
|
70
|
+
showBanner: {
|
|
71
|
+
control: { type: "boolean" },
|
|
72
|
+
description: "Show/hide banner below card"
|
|
73
|
+
},
|
|
74
|
+
onQuantityChange: {
|
|
75
|
+
control: false,
|
|
76
|
+
description: "Callback when quantity changes"
|
|
77
|
+
},
|
|
78
|
+
image: {
|
|
79
|
+
control: false,
|
|
80
|
+
description: "Image element or content"
|
|
81
|
+
},
|
|
82
|
+
banner: {
|
|
83
|
+
control: { type: "text" },
|
|
84
|
+
description: "Banner content to show below card"
|
|
85
|
+
},
|
|
86
|
+
bannerType: {
|
|
87
|
+
control: { type: "select" },
|
|
88
|
+
options: ["error", "success", "warning", "info"],
|
|
89
|
+
description: "Banner notification type"
|
|
90
|
+
},
|
|
91
|
+
showBannerIcon: {
|
|
92
|
+
control: { type: "boolean" },
|
|
93
|
+
description: "Show/hide banner notification icon"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const Playground = (args: ProductDisplayCardProps) => (
|
|
99
|
+
<View style={styles.column}>
|
|
100
|
+
<ProductDisplayCard {...args} />
|
|
101
|
+
</View>
|
|
102
|
+
)
|
|
103
|
+
Playground.args = {
|
|
104
|
+
device: "mobile",
|
|
105
|
+
title: "Recipe Name",
|
|
106
|
+
subtext: "+£0.00/pouch",
|
|
107
|
+
showSubtext: true,
|
|
108
|
+
quantity: 1,
|
|
109
|
+
showQuantityPicker: true,
|
|
110
|
+
showBanner: true,
|
|
111
|
+
bannerType: "info",
|
|
112
|
+
showBannerIcon: false,
|
|
113
|
+
banner: " High in protein • Natural ingredients • Vet approved",
|
|
114
|
+
image: <PlaceholderImage />
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const Default = () => (
|
|
118
|
+
<View style={styles.column}>
|
|
119
|
+
<ProductDisplayCard
|
|
120
|
+
device="mobile"
|
|
121
|
+
title="Recipe Name"
|
|
122
|
+
subtext="+£0.00/pouch"
|
|
123
|
+
showSubtext
|
|
124
|
+
quantity={1}
|
|
125
|
+
showQuantityPicker={false}
|
|
126
|
+
showBanner={false}
|
|
127
|
+
image={<PlaceholderImage />}
|
|
128
|
+
/>
|
|
129
|
+
</View>
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
export const Desktop = () => (
|
|
133
|
+
<View style={styles.column}>
|
|
134
|
+
<ProductDisplayCard
|
|
135
|
+
device="desktop"
|
|
136
|
+
title="Recipe Name"
|
|
137
|
+
subtext="+£0.00/pouch"
|
|
138
|
+
showSubtext
|
|
139
|
+
quantity={1}
|
|
140
|
+
showQuantityPicker
|
|
141
|
+
showBanner={false}
|
|
142
|
+
image={<PlaceholderImage />}
|
|
143
|
+
/>
|
|
144
|
+
</View>
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
export const Mobile = () => (
|
|
148
|
+
<View style={styles.column}>
|
|
149
|
+
<ProductDisplayCard
|
|
150
|
+
device="mobile"
|
|
151
|
+
title="Recipe Name"
|
|
152
|
+
subtext="+£0.50/pouch"
|
|
153
|
+
showSubtext
|
|
154
|
+
quantity={1}
|
|
155
|
+
showQuantityPicker
|
|
156
|
+
showBanner={false}
|
|
157
|
+
image={<PlaceholderImage />}
|
|
158
|
+
/>
|
|
159
|
+
</View>
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
export const WithBanner = () => (
|
|
163
|
+
<View style={styles.column}>
|
|
164
|
+
<ProductDisplayCard
|
|
165
|
+
device="mobile"
|
|
166
|
+
title="Recipe Name"
|
|
167
|
+
subtext="+£0.00/pouch"
|
|
168
|
+
showSubtext
|
|
169
|
+
quantity={1}
|
|
170
|
+
showQuantityPicker
|
|
171
|
+
showBanner
|
|
172
|
+
bannerType="success"
|
|
173
|
+
showBannerIcon={false}
|
|
174
|
+
banner="High in protein • Natural ingredients • Vet approved"
|
|
175
|
+
image={<PlaceholderImage />}
|
|
176
|
+
/>
|
|
177
|
+
</View>
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
export const WithBannerIcon = () => (
|
|
181
|
+
<View style={styles.column}>
|
|
182
|
+
<ProductDisplayCard
|
|
183
|
+
device="mobile"
|
|
184
|
+
title="Recipe Name"
|
|
185
|
+
subtext="+£0.00/pouch"
|
|
186
|
+
showSubtext
|
|
187
|
+
quantity={1}
|
|
188
|
+
showQuantityPicker
|
|
189
|
+
showBanner
|
|
190
|
+
bannerType="success"
|
|
191
|
+
showBannerIcon
|
|
192
|
+
banner="High in protein • Natural ingredients • Vet approved"
|
|
193
|
+
image={<PlaceholderImage />}
|
|
194
|
+
/>
|
|
195
|
+
</View>
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
export const NoSubtext = () => (
|
|
199
|
+
<View style={styles.column}>
|
|
200
|
+
<ProductDisplayCard
|
|
201
|
+
device="mobile"
|
|
202
|
+
title="Recipe Name"
|
|
203
|
+
showSubtext={false}
|
|
204
|
+
quantity={1}
|
|
205
|
+
showQuantityPicker
|
|
206
|
+
showBanner={false}
|
|
207
|
+
image={<PlaceholderImage />}
|
|
208
|
+
/>
|
|
209
|
+
</View>
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
export const AllVariants = () => (
|
|
213
|
+
<View style={styles.column}>
|
|
214
|
+
<ProductDisplayCard
|
|
215
|
+
device="desktop"
|
|
216
|
+
title="Recipe Name"
|
|
217
|
+
subtext="+£0.00/pouch"
|
|
218
|
+
showSubtext
|
|
219
|
+
quantity={1}
|
|
220
|
+
showQuantityPicker
|
|
221
|
+
showBanner={false}
|
|
222
|
+
image={<PlaceholderImage />}
|
|
223
|
+
/>
|
|
224
|
+
|
|
225
|
+
<ProductDisplayCard
|
|
226
|
+
device="mobile"
|
|
227
|
+
title="Recipe Name"
|
|
228
|
+
subtext="+£0.50/pouch"
|
|
229
|
+
showSubtext
|
|
230
|
+
quantity={2}
|
|
231
|
+
showQuantityPicker
|
|
232
|
+
showBanner={false}
|
|
233
|
+
image={<PlaceholderImage />}
|
|
234
|
+
/>
|
|
235
|
+
|
|
236
|
+
<ProductDisplayCard
|
|
237
|
+
device="mobile"
|
|
238
|
+
title="Recipe Name"
|
|
239
|
+
subtext="+£0.00/pouch"
|
|
240
|
+
showSubtext
|
|
241
|
+
quantity={1}
|
|
242
|
+
showQuantityPicker
|
|
243
|
+
showBanner
|
|
244
|
+
banner=" High in protein • Natural ingredients"
|
|
245
|
+
image={<PlaceholderImage />}
|
|
246
|
+
/>
|
|
247
|
+
</View>
|
|
248
|
+
)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { screen } from "@testing-library/react"
|
|
3
|
+
import { View, Text } from "react-native"
|
|
4
|
+
import { ProductDisplayCard } from "./ProductDisplayCard"
|
|
5
|
+
import { renderWithTheme } from "../../../test-utils"
|
|
6
|
+
|
|
7
|
+
describe("ProductDisplayCard (Native)", () => {
|
|
8
|
+
it("renders with required title prop", () => {
|
|
9
|
+
renderWithTheme(<ProductDisplayCard title="Test Product" />)
|
|
10
|
+
|
|
11
|
+
expect(screen.getByText("Test Product")).toBeTruthy()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it("renders subtext when provided and showSubtext is true", () => {
|
|
15
|
+
renderWithTheme(
|
|
16
|
+
<ProductDisplayCard
|
|
17
|
+
title="Test Product"
|
|
18
|
+
subtext="£9.99"
|
|
19
|
+
showSubtext={true}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
expect(screen.getByText("£9.99")).toBeTruthy()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("hides subtext when showSubtext is false", () => {
|
|
27
|
+
renderWithTheme(
|
|
28
|
+
<ProductDisplayCard
|
|
29
|
+
title="Test Product"
|
|
30
|
+
subtext="£9.99"
|
|
31
|
+
showSubtext={false}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
expect(screen.queryByText("£9.99")).toBeFalsy()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("renders image when provided", () => {
|
|
39
|
+
renderWithTheme(
|
|
40
|
+
<ProductDisplayCard
|
|
41
|
+
title="Test Product"
|
|
42
|
+
image={
|
|
43
|
+
<View>
|
|
44
|
+
<Text>Image</Text>
|
|
45
|
+
</View>
|
|
46
|
+
}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
expect(screen.getByText("Image")).toBeTruthy()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("shows quantity picker when showQuantityPicker is true", () => {
|
|
54
|
+
renderWithTheme(
|
|
55
|
+
<ProductDisplayCard
|
|
56
|
+
title="Test Product"
|
|
57
|
+
showQuantityPicker={true}
|
|
58
|
+
quantity={1}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const numberInput = screen.getByDisplayValue("1")
|
|
63
|
+
expect(numberInput).toBeTruthy()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it("does not show quantity picker when showQuantityPicker is false", () => {
|
|
67
|
+
renderWithTheme(
|
|
68
|
+
<ProductDisplayCard title="Test Product" showQuantityPicker={false} />
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
expect(screen.queryByDisplayValue("1")).toBeFalsy()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("renders banner when showBanner is true", () => {
|
|
75
|
+
renderWithTheme(
|
|
76
|
+
<ProductDisplayCard
|
|
77
|
+
title="Test Product"
|
|
78
|
+
showBanner={true}
|
|
79
|
+
banner={<Text>Test Banner</Text>}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
expect(screen.getByText("Test Banner")).toBeTruthy()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("does not render banner when showBanner is false", () => {
|
|
87
|
+
renderWithTheme(
|
|
88
|
+
<ProductDisplayCard
|
|
89
|
+
title="Test Product"
|
|
90
|
+
showBanner={false}
|
|
91
|
+
banner={<Text>Test Banner</Text>}
|
|
92
|
+
/>
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
expect(screen.queryByText("Test Banner")).toBeFalsy()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it("renders with desktop device variant by default", () => {
|
|
99
|
+
renderWithTheme(<ProductDisplayCard title="Test Product" />)
|
|
100
|
+
|
|
101
|
+
expect(screen.getByText("Test Product")).toBeTruthy()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("renders with mobile device variant when specified", () => {
|
|
105
|
+
renderWithTheme(<ProductDisplayCard title="Test Product" device="mobile" />)
|
|
106
|
+
|
|
107
|
+
expect(screen.getByText("Test Product")).toBeTruthy()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it("forwards ref correctly", () => {
|
|
111
|
+
const ref = { current: null as any }
|
|
112
|
+
|
|
113
|
+
renderWithTheme(<ProductDisplayCard ref={ref} title="Test Product" />)
|
|
114
|
+
|
|
115
|
+
expect(ref.current).toBeTruthy()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("renders title and subtext together", () => {
|
|
119
|
+
renderWithTheme(
|
|
120
|
+
<ProductDisplayCard
|
|
121
|
+
title="Premium Recipe"
|
|
122
|
+
subtext="+£0.00/pouch"
|
|
123
|
+
showSubtext={true}
|
|
124
|
+
/>
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
expect(screen.getByText("Premium Recipe")).toBeTruthy()
|
|
128
|
+
expect(screen.getByText("+£0.00/pouch")).toBeTruthy()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it("accepts React element as image", () => {
|
|
132
|
+
renderWithTheme(
|
|
133
|
+
<ProductDisplayCard
|
|
134
|
+
title="Test Product"
|
|
135
|
+
image={
|
|
136
|
+
<View>
|
|
137
|
+
<Text>Custom Image</Text>
|
|
138
|
+
</View>
|
|
139
|
+
}
|
|
140
|
+
/>
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
expect(screen.getByText("Custom Image")).toBeTruthy()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it("accepts React element as banner", () => {
|
|
147
|
+
renderWithTheme(
|
|
148
|
+
<ProductDisplayCard
|
|
149
|
+
title="Test Product"
|
|
150
|
+
showBanner={true}
|
|
151
|
+
banner={
|
|
152
|
+
<View>
|
|
153
|
+
<Text>Custom Banner</Text>
|
|
154
|
+
</View>
|
|
155
|
+
}
|
|
156
|
+
/>
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
expect(screen.getByText("Custom Banner")).toBeTruthy()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it("renders banner with different types", () => {
|
|
163
|
+
const { rerender } = renderWithTheme(
|
|
164
|
+
<ProductDisplayCard
|
|
165
|
+
title="Test Product"
|
|
166
|
+
showBanner={true}
|
|
167
|
+
banner={<Text>Test Banner</Text>}
|
|
168
|
+
bannerType="success"
|
|
169
|
+
/>
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
expect(screen.getByText("Test Banner")).toBeTruthy()
|
|
173
|
+
|
|
174
|
+
rerender(
|
|
175
|
+
<ProductDisplayCard
|
|
176
|
+
title="Test Product"
|
|
177
|
+
showBanner={true}
|
|
178
|
+
banner={<Text>Error Banner</Text>}
|
|
179
|
+
bannerType="error"
|
|
180
|
+
/>
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
expect(screen.getByText("Error Banner")).toBeTruthy()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it("displays banner with icon when showBannerIcon is true", () => {
|
|
187
|
+
renderWithTheme(
|
|
188
|
+
<ProductDisplayCard
|
|
189
|
+
title="Test Product"
|
|
190
|
+
showBanner={true}
|
|
191
|
+
banner={<Text>Test Banner</Text>}
|
|
192
|
+
showBannerIcon={true}
|
|
193
|
+
/>
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
expect(screen.getByText("Test Banner")).toBeTruthy()
|
|
197
|
+
})
|
|
198
|
+
})
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, ViewProps } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import { Typography } from "../../atoms/Typography"
|
|
5
|
+
import { NumberField } from "../../molecules/NumberField"
|
|
6
|
+
import { Notification } from "../Notification"
|
|
7
|
+
import { parseTokenValue } from "../../../utils"
|
|
8
|
+
|
|
9
|
+
export type ProductDisplayCardDevice = "desktop" | "mobile"
|
|
10
|
+
|
|
11
|
+
export type ProductDisplayCardProps = ViewProps & {
|
|
12
|
+
device?: ProductDisplayCardDevice
|
|
13
|
+
title: string
|
|
14
|
+
subtext?: string
|
|
15
|
+
showSubtext?: boolean
|
|
16
|
+
quantity?: number
|
|
17
|
+
onQuantityChange?: (quantity: number) => void
|
|
18
|
+
showQuantityPicker?: boolean
|
|
19
|
+
disableQuantityButtons?: boolean
|
|
20
|
+
hideQuantityButtons?: boolean
|
|
21
|
+
showIncrementButton?: boolean
|
|
22
|
+
showDecrementButton?: boolean
|
|
23
|
+
incrementDisabled?: boolean
|
|
24
|
+
decrementDisabled?: boolean
|
|
25
|
+
image?: React.ReactNode
|
|
26
|
+
banner?: React.ReactNode
|
|
27
|
+
showBanner?: boolean
|
|
28
|
+
bannerType?: "error" | "success" | "warning" | "info"
|
|
29
|
+
showBannerIcon?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const StyledCardContainer = styled(View)<{
|
|
33
|
+
cardDevice: ProductDisplayCardDevice
|
|
34
|
+
}>(({ theme }) => {
|
|
35
|
+
const { spacing, borderRadius } = theme.tokens.semantics.dimensions
|
|
36
|
+
const { colour } = theme.tokens.semantics
|
|
37
|
+
const spacingMd = parseTokenValue(spacing.md)
|
|
38
|
+
const spacingXl = parseTokenValue(spacing.xl)
|
|
39
|
+
return {
|
|
40
|
+
flexDirection: "row",
|
|
41
|
+
width: "100%",
|
|
42
|
+
height: "100%",
|
|
43
|
+
maxHeight: spacingXl * 6 + spacingMd,
|
|
44
|
+
backgroundColor: colour.background.surface.default,
|
|
45
|
+
borderWidth: 1,
|
|
46
|
+
borderColor: colour.border.default,
|
|
47
|
+
borderRadius: parseTokenValue(borderRadius.md),
|
|
48
|
+
overflow: "hidden",
|
|
49
|
+
shadowColor: "#522a10",
|
|
50
|
+
shadowOffset: { width: 0, height: spacingMd / 4 },
|
|
51
|
+
shadowOpacity: 0.05,
|
|
52
|
+
shadowRadius: spacingXl - 4,
|
|
53
|
+
elevation: 5,
|
|
54
|
+
zIndex: 1
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const StyledImage = styled(View)(() => ({
|
|
59
|
+
flexShrink: 0,
|
|
60
|
+
backgroundColor: "#f5f5f5",
|
|
61
|
+
overflow: "hidden",
|
|
62
|
+
aspectRatio: "1 / 1",
|
|
63
|
+
height: "100%"
|
|
64
|
+
}))
|
|
65
|
+
|
|
66
|
+
const StyledContentArea = styled(View)<{
|
|
67
|
+
contentDevice: ProductDisplayCardDevice
|
|
68
|
+
}>(({ theme }) => {
|
|
69
|
+
const { spacing } = theme.tokens.semantics.dimensions
|
|
70
|
+
return {
|
|
71
|
+
flex: 1,
|
|
72
|
+
paddingHorizontal: parseTokenValue(spacing.md),
|
|
73
|
+
paddingVertical: parseTokenValue(spacing.md),
|
|
74
|
+
gap: parseTokenValue(spacing.xs),
|
|
75
|
+
justifyContent: "space-between"
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const StyledTextContainer = styled(View)(() => ({
|
|
80
|
+
gap: 2
|
|
81
|
+
}))
|
|
82
|
+
|
|
83
|
+
const StyledBannerWrapper = styled(View)(() => ({
|
|
84
|
+
width: "100%",
|
|
85
|
+
marginTop: -48,
|
|
86
|
+
paddingTop: 24,
|
|
87
|
+
paddingBottom: 16
|
|
88
|
+
}))
|
|
89
|
+
|
|
90
|
+
const StyledRootWrapper = styled(View)(() => {
|
|
91
|
+
return {
|
|
92
|
+
flexDirection: "column",
|
|
93
|
+
width: "100%",
|
|
94
|
+
height: "100%"
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const StyledNotification = styled(Notification as any)(({ theme }) => {
|
|
99
|
+
const { spacing } = theme.tokens.semantics.dimensions
|
|
100
|
+
return {
|
|
101
|
+
top: parseTokenValue(spacing.md),
|
|
102
|
+
paddingHorizontal: parseTokenValue(spacing.xl),
|
|
103
|
+
paddingVertical: parseTokenValue(spacing.md)
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Product Display Card component for showing product information with optional quantity picker.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```tsx
|
|
112
|
+
* import { ProductDisplayCard } from "@butternutbox/pawprint-native"
|
|
113
|
+
*
|
|
114
|
+
* <ProductDisplayCard
|
|
115
|
+
* device="mobile"
|
|
116
|
+
* title="Premium Recipe"
|
|
117
|
+
* subtext="+£0.00/pouch"
|
|
118
|
+
* quantity={1}
|
|
119
|
+
* showQuantityPicker
|
|
120
|
+
* />
|
|
121
|
+
* ```
|
|
122
|
+
*
|
|
123
|
+
* @param device - *(optional)* Device variant: "desktop" or "mobile" (default)
|
|
124
|
+
* @param title - Product title
|
|
125
|
+
* @param subtext - *(optional)* Subtitle text (e.g. price)
|
|
126
|
+
* @param showSubtext - *(optional)* Show/hide subtext
|
|
127
|
+
* @param quantity - *(optional)* Current quantity value
|
|
128
|
+
* @param onQuantityChange - *(optional)* Callback when quantity changes
|
|
129
|
+
* @param showQuantityPicker - *(optional)* Show/hide quantity picker
|
|
130
|
+
* @param image - *(optional)* Image element or content
|
|
131
|
+
* @param banner - *(optional)* Banner content to show below card
|
|
132
|
+
* @param showBanner - *(optional)* Show/hide banner
|
|
133
|
+
* @param bannerType - *(optional)* Banner notification type: "error", "success", "warning", or "info" (default)
|
|
134
|
+
* @param showBannerIcon - *(optional)* Show/hide banner notification icon
|
|
135
|
+
*/
|
|
136
|
+
export const ProductDisplayCard = React.forwardRef<
|
|
137
|
+
View,
|
|
138
|
+
ProductDisplayCardProps
|
|
139
|
+
>(
|
|
140
|
+
(
|
|
141
|
+
{
|
|
142
|
+
device = "mobile",
|
|
143
|
+
title,
|
|
144
|
+
subtext,
|
|
145
|
+
showSubtext = true,
|
|
146
|
+
quantity = 1,
|
|
147
|
+
onQuantityChange,
|
|
148
|
+
showQuantityPicker = false,
|
|
149
|
+
disableQuantityButtons = false,
|
|
150
|
+
hideQuantityButtons = false,
|
|
151
|
+
showIncrementButton,
|
|
152
|
+
showDecrementButton,
|
|
153
|
+
incrementDisabled = false,
|
|
154
|
+
decrementDisabled = false,
|
|
155
|
+
image,
|
|
156
|
+
banner,
|
|
157
|
+
showBanner = false,
|
|
158
|
+
bannerType = "info",
|
|
159
|
+
showBannerIcon = false,
|
|
160
|
+
...rest
|
|
161
|
+
},
|
|
162
|
+
ref
|
|
163
|
+
) => {
|
|
164
|
+
const handleQuantityChange = (newQuantity: number) => {
|
|
165
|
+
if (newQuantity >= 1) {
|
|
166
|
+
onQuantityChange?.(newQuantity)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const cardContent = (
|
|
171
|
+
<StyledCardContainer ref={ref} cardDevice={device} {...rest}>
|
|
172
|
+
{image && <StyledImage>{image}</StyledImage>}
|
|
173
|
+
|
|
174
|
+
<StyledContentArea contentDevice={device}>
|
|
175
|
+
<StyledTextContainer>
|
|
176
|
+
<Typography variant="heading" size="xs" color="primary">
|
|
177
|
+
{title}
|
|
178
|
+
</Typography>
|
|
179
|
+
|
|
180
|
+
{showSubtext && subtext && (
|
|
181
|
+
<Typography variant="body" size="sm" color="secondary">
|
|
182
|
+
{subtext}
|
|
183
|
+
</Typography>
|
|
184
|
+
)}
|
|
185
|
+
</StyledTextContainer>
|
|
186
|
+
|
|
187
|
+
{showQuantityPicker && (
|
|
188
|
+
<View style={{ marginLeft: "auto" }}>
|
|
189
|
+
<NumberField
|
|
190
|
+
size="sm"
|
|
191
|
+
value={String(quantity)}
|
|
192
|
+
onChangeText={(text) => {
|
|
193
|
+
const value = parseInt(text, 10)
|
|
194
|
+
if (!isNaN(value)) {
|
|
195
|
+
handleQuantityChange(value)
|
|
196
|
+
}
|
|
197
|
+
}}
|
|
198
|
+
onIncrement={() => handleQuantityChange(quantity + 1)}
|
|
199
|
+
onDecrement={() => handleQuantityChange(quantity - 1)}
|
|
200
|
+
min={1}
|
|
201
|
+
accessible
|
|
202
|
+
accessibilityLabel={`Quantity: ${quantity}`}
|
|
203
|
+
showIncrementButton={
|
|
204
|
+
showIncrementButton !== undefined
|
|
205
|
+
? showIncrementButton
|
|
206
|
+
: !hideQuantityButtons
|
|
207
|
+
}
|
|
208
|
+
showDecrementButton={
|
|
209
|
+
showDecrementButton !== undefined
|
|
210
|
+
? showDecrementButton
|
|
211
|
+
: !hideQuantityButtons
|
|
212
|
+
}
|
|
213
|
+
incrementDisabled={incrementDisabled || disableQuantityButtons}
|
|
214
|
+
decrementDisabled={decrementDisabled || disableQuantityButtons}
|
|
215
|
+
/>
|
|
216
|
+
</View>
|
|
217
|
+
)}
|
|
218
|
+
</StyledContentArea>
|
|
219
|
+
</StyledCardContainer>
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if (showBanner && banner) {
|
|
223
|
+
return (
|
|
224
|
+
<StyledRootWrapper>
|
|
225
|
+
{cardContent}
|
|
226
|
+
<StyledBannerWrapper>
|
|
227
|
+
<StyledNotification
|
|
228
|
+
variant="inline"
|
|
229
|
+
type={bannerType}
|
|
230
|
+
showIcon={showBannerIcon}
|
|
231
|
+
>
|
|
232
|
+
{banner}
|
|
233
|
+
</StyledNotification>
|
|
234
|
+
</StyledBannerWrapper>
|
|
235
|
+
</StyledRootWrapper>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return cardContent
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
ProductDisplayCard.displayName = "ProductDisplayCard"
|