@butternutbox/pawprint-native 0.9.0 → 0.10.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/.turbo/turbo-build.log +9 -9
- package/CHANGELOG.md +18 -0
- package/dist/index.cjs +1141 -961
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -7
- package/dist/index.d.ts +22 -7
- package/dist/index.js +640 -461
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__mocks__/react-native.tsx +7 -0
- package/src/__mocks__/rn-primitives/select.tsx +58 -21
- package/src/components/atoms/Input/InputField.tsx +37 -6
- package/src/components/atoms/Input/index.ts +1 -1
- package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.stories.tsx +75 -0
- package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.tsx +49 -32
- package/src/components/molecules/ProductListingCard/ProductListingCard.stories.tsx +65 -1
- package/src/components/molecules/ProductListingCard/ProductListingCard.tsx +16 -6
- package/src/components/molecules/SelectField/SelectField.stories.tsx +110 -5
- package/src/components/molecules/SelectField/SelectField.test.tsx +174 -2
- package/src/components/molecules/SelectField/SelectField.tsx +186 -30
- package/src/components/molecules/SelectField/SelectFieldContent.tsx +8 -12
- package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +43 -25
- package/src/components/molecules/SelectField/SelectFieldValue.tsx +17 -18
package/package.json
CHANGED
|
@@ -324,6 +324,13 @@ export const Platform = {
|
|
|
324
324
|
select: (obj: any) => obj.ios ?? obj.default
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
export const Keyboard = {
|
|
328
|
+
addListener: (_event: string, _handler: () => void) => ({
|
|
329
|
+
remove: () => {}
|
|
330
|
+
}),
|
|
331
|
+
dismiss: () => {}
|
|
332
|
+
}
|
|
333
|
+
|
|
327
334
|
// ─── Type exports (re-exported as empty for TS compat) ───────────────────
|
|
328
335
|
export type ViewProps = any
|
|
329
336
|
export type TextProps = any
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import React from "react"
|
|
2
2
|
|
|
3
|
+
const SelectCtx = React.createContext<{
|
|
4
|
+
open: boolean
|
|
5
|
+
setOpen: (open: boolean) => void
|
|
6
|
+
}>({ open: false, setOpen: () => {} })
|
|
7
|
+
|
|
3
8
|
export const Root = React.forwardRef<any, any>(
|
|
4
9
|
(
|
|
5
|
-
{
|
|
10
|
+
{
|
|
11
|
+
value,
|
|
12
|
+
defaultValue,
|
|
13
|
+
onValueChange,
|
|
14
|
+
disabled,
|
|
15
|
+
children,
|
|
16
|
+
onOpenChange,
|
|
17
|
+
...rest
|
|
18
|
+
},
|
|
6
19
|
ref
|
|
7
20
|
) => {
|
|
21
|
+
const [open, setOpenState] = React.useState(false)
|
|
8
22
|
const [internalValue, setInternalValue] = React.useState(
|
|
9
23
|
value ?? defaultValue ?? null
|
|
10
24
|
)
|
|
@@ -17,12 +31,19 @@ export const Root = React.forwardRef<any, any>(
|
|
|
17
31
|
}
|
|
18
32
|
}
|
|
19
33
|
|
|
34
|
+
const setOpen = (nextOpen: boolean) => {
|
|
35
|
+
setOpenState(nextOpen)
|
|
36
|
+
onOpenChange?.(nextOpen)
|
|
37
|
+
}
|
|
38
|
+
|
|
20
39
|
return (
|
|
21
|
-
<
|
|
22
|
-
{
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
<SelectCtx.Provider value={{ open, setOpen }}>
|
|
41
|
+
<div ref={ref} data-disabled={disabled} {...rest}>
|
|
42
|
+
{typeof children === "function"
|
|
43
|
+
? children({ value: currentValue, onChange: handleChange })
|
|
44
|
+
: children}
|
|
45
|
+
</div>
|
|
46
|
+
</SelectCtx.Provider>
|
|
26
47
|
)
|
|
27
48
|
}
|
|
28
49
|
)
|
|
@@ -30,11 +51,18 @@ Root.displayName = "SelectPrimitive.Root"
|
|
|
30
51
|
|
|
31
52
|
export const Trigger = React.forwardRef<any, any>(
|
|
32
53
|
({ asChild, children, ...rest }, ref) => {
|
|
54
|
+
const { open, setOpen } = React.useContext(SelectCtx)
|
|
55
|
+
const handlePress = () => setOpen(!open)
|
|
56
|
+
|
|
33
57
|
if (asChild && React.isValidElement(children)) {
|
|
34
|
-
return React.cloneElement(children, {
|
|
58
|
+
return React.cloneElement(children, {
|
|
59
|
+
ref,
|
|
60
|
+
onPress: handlePress,
|
|
61
|
+
...rest
|
|
62
|
+
} as any)
|
|
35
63
|
}
|
|
36
64
|
return (
|
|
37
|
-
<button ref={ref} role="button" {...rest}>
|
|
65
|
+
<button ref={ref} role="button" onClick={handlePress} {...rest}>
|
|
38
66
|
{children}
|
|
39
67
|
</button>
|
|
40
68
|
)
|
|
@@ -59,22 +87,20 @@ Value.displayName = "SelectPrimitive.Value"
|
|
|
59
87
|
export const Portal = ({ children }: any) => <>{children}</>
|
|
60
88
|
Portal.displayName = "SelectPrimitive.Portal"
|
|
61
89
|
|
|
62
|
-
export const Viewport = React.forwardRef<any, any>(
|
|
63
|
-
({ children, ...rest }, ref) => (
|
|
64
|
-
<div ref={ref} {...rest}>
|
|
65
|
-
{children}
|
|
66
|
-
</div>
|
|
67
|
-
)
|
|
68
|
-
)
|
|
69
|
-
Viewport.displayName = "SelectPrimitive.Viewport"
|
|
70
|
-
|
|
71
90
|
export const Overlay = React.forwardRef<any, any>(
|
|
72
|
-
({
|
|
73
|
-
|
|
74
|
-
|
|
91
|
+
({ onPress, closeOnPress = true, children, ...rest }, ref) => {
|
|
92
|
+
const { setOpen } = React.useContext(SelectCtx)
|
|
93
|
+
const handlePress = () => {
|
|
94
|
+
if (closeOnPress) setOpen(false)
|
|
95
|
+
onPress?.()
|
|
75
96
|
}
|
|
76
97
|
return (
|
|
77
|
-
<div
|
|
98
|
+
<div
|
|
99
|
+
ref={ref}
|
|
100
|
+
data-testid="select-overlay"
|
|
101
|
+
onClick={handlePress}
|
|
102
|
+
{...rest}
|
|
103
|
+
>
|
|
78
104
|
{children}
|
|
79
105
|
</div>
|
|
80
106
|
)
|
|
@@ -82,8 +108,19 @@ export const Overlay = React.forwardRef<any, any>(
|
|
|
82
108
|
)
|
|
83
109
|
Overlay.displayName = "SelectPrimitive.Overlay"
|
|
84
110
|
|
|
111
|
+
export const Viewport = React.forwardRef<any, any>(
|
|
112
|
+
({ children, ...rest }, ref) => (
|
|
113
|
+
<div ref={ref} {...rest}>
|
|
114
|
+
{children}
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
Viewport.displayName = "SelectPrimitive.Viewport"
|
|
119
|
+
|
|
85
120
|
export const Content = React.forwardRef<any, any>(
|
|
86
121
|
({ asChild, children, ...rest }, ref) => {
|
|
122
|
+
const { open } = React.useContext(SelectCtx)
|
|
123
|
+
if (!open) return null
|
|
87
124
|
if (asChild && React.isValidElement(children)) {
|
|
88
125
|
return React.cloneElement(children, { ref, ...rest } as any)
|
|
89
126
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
Error as ErrorIcon,
|
|
14
14
|
CheckCircle as SuccessIcon
|
|
15
15
|
} from "@butternutbox/pawprint-icons/core"
|
|
16
|
+
import { resolveFont } from "../../../fonts"
|
|
16
17
|
import { Icon } from "../Icon"
|
|
17
18
|
|
|
18
19
|
export type InputState = "default" | "error" | "success"
|
|
@@ -61,18 +62,24 @@ const StyledInputWrapper = styled(Animated.View)<{
|
|
|
61
62
|
|
|
62
63
|
const StyledInput = styled(TextInput)(({ theme }) => {
|
|
63
64
|
const { colour, field } = theme.tokens.components.inputs
|
|
64
|
-
const
|
|
65
|
+
const typography = field.placeholder.default
|
|
66
|
+
const { fontFamily } = resolveFont(
|
|
67
|
+
typography.fontFamily,
|
|
68
|
+
typography.fontWeight
|
|
69
|
+
)
|
|
70
|
+
const fontSize = parseTokenValue(typography.fontSize)
|
|
71
|
+
const letterSpacing = typography.letterSpacing?.endsWith("%")
|
|
72
|
+
? (parseFloat(typography.letterSpacing) / 100) * fontSize
|
|
73
|
+
: parseFloat(typography.letterSpacing ?? "0")
|
|
65
74
|
return {
|
|
66
75
|
flex: 1,
|
|
67
76
|
minWidth: 0,
|
|
68
77
|
minHeight: 0,
|
|
69
78
|
padding: 0,
|
|
70
79
|
color: colour.field.text.default,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
lineHeight: parseTokenValue(placeholderTypography.lineHeight),
|
|
75
|
-
letterSpacing: parseTokenValue(placeholderTypography.letterSpacing),
|
|
80
|
+
fontFamily,
|
|
81
|
+
fontSize,
|
|
82
|
+
letterSpacing,
|
|
76
83
|
// Suppress the browser default focus outline on React Native Web —
|
|
77
84
|
// the focus indicator is rendered on the wrapper instead.
|
|
78
85
|
...({
|
|
@@ -275,3 +282,27 @@ export const InputField = React.forwardRef<TextInput, InputFieldProps>(
|
|
|
275
282
|
)
|
|
276
283
|
|
|
277
284
|
InputField.displayName = "Input.Field"
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Bare text input with design-system typography and colors applied.
|
|
288
|
+
* No container, border, or padding — use inside components that provide
|
|
289
|
+
* their own visual wrapper (e.g. SelectField trigger).
|
|
290
|
+
*/
|
|
291
|
+
export const InputText = React.forwardRef<TextInput, TextInputProps>(
|
|
292
|
+
({ placeholderTextColor, ...rest }, ref) => {
|
|
293
|
+
const theme = useTheme()
|
|
294
|
+
const { colour } = theme.tokens.components.inputs
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<StyledInput
|
|
298
|
+
ref={ref}
|
|
299
|
+
placeholderTextColor={
|
|
300
|
+
placeholderTextColor ?? colour.field.text.placeholder
|
|
301
|
+
}
|
|
302
|
+
{...rest}
|
|
303
|
+
/>
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
InputText.displayName = "Input.Text"
|
|
@@ -2,7 +2,7 @@ export { Input } from "./Input"
|
|
|
2
2
|
export type { InputProps } from "./Input"
|
|
3
3
|
export { InputLabel } from "./InputLabel"
|
|
4
4
|
export type { InputLabelProps } from "./InputLabel"
|
|
5
|
-
export { InputField } from "./InputField"
|
|
5
|
+
export { InputField, InputText } from "./InputField"
|
|
6
6
|
export type { InputFieldProps } from "./InputField"
|
|
7
7
|
export { InputDescription } from "./InputDescription"
|
|
8
8
|
export type { InputDescriptionProps } from "./InputDescription"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from "react"
|
|
2
2
|
import { View, StyleSheet } from "react-native"
|
|
3
3
|
import { ProductDisplayCard } from "./ProductDisplayCard"
|
|
4
|
+
import { Typography } from "../../atoms/Typography"
|
|
4
5
|
import type { ProductDisplayCardProps } from "./ProductDisplayCard"
|
|
5
6
|
|
|
6
7
|
const placeholderImageUrl =
|
|
@@ -205,3 +206,77 @@ export const AllVariants = () => (
|
|
|
205
206
|
/>
|
|
206
207
|
</View>
|
|
207
208
|
)
|
|
209
|
+
|
|
210
|
+
export const WithMinQuantity = () => (
|
|
211
|
+
<View style={styles.column}>
|
|
212
|
+
<ProductDisplayCard
|
|
213
|
+
title="Bulk Recipe Pack"
|
|
214
|
+
subtext="+£2.50/pouch"
|
|
215
|
+
showSubtext
|
|
216
|
+
quantity={5}
|
|
217
|
+
minQuantity={5}
|
|
218
|
+
showQuantityPicker
|
|
219
|
+
showBanner
|
|
220
|
+
bannerType="info"
|
|
221
|
+
showBannerIcon={false}
|
|
222
|
+
banner="Minimum quantity: 5 pouches required"
|
|
223
|
+
image={placeholderImageUrl}
|
|
224
|
+
/>
|
|
225
|
+
</View>
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
export const WithImagePress = () => {
|
|
229
|
+
const [pressed, setPressed] = React.useState(false)
|
|
230
|
+
return (
|
|
231
|
+
<View style={styles.column}>
|
|
232
|
+
<ProductDisplayCard
|
|
233
|
+
title="Recipe Name"
|
|
234
|
+
subtext="+£0.00/pouch"
|
|
235
|
+
showSubtext
|
|
236
|
+
quantity={1}
|
|
237
|
+
showQuantityPicker
|
|
238
|
+
onImagePress={() => setPressed(true)}
|
|
239
|
+
showBanner={pressed}
|
|
240
|
+
bannerType="success"
|
|
241
|
+
banner="Image pressed!"
|
|
242
|
+
image={placeholderImageUrl}
|
|
243
|
+
/>
|
|
244
|
+
</View>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export const WithCustomBanner = () => (
|
|
249
|
+
<View style={styles.column}>
|
|
250
|
+
<ProductDisplayCard
|
|
251
|
+
title="Recipe Name"
|
|
252
|
+
subtext="+£0.00/pouch"
|
|
253
|
+
showSubtext
|
|
254
|
+
quantity={1}
|
|
255
|
+
showQuantityPicker
|
|
256
|
+
showBanner
|
|
257
|
+
bannerType="info"
|
|
258
|
+
showBannerIcon={false}
|
|
259
|
+
banner={
|
|
260
|
+
<View style={{ gap: 8 }}>
|
|
261
|
+
<View style={{ gap: 4 }}>
|
|
262
|
+
<Typography variant="body" size="sm" weight="semiBold">
|
|
263
|
+
Nutritional Benefits
|
|
264
|
+
</Typography>
|
|
265
|
+
</View>
|
|
266
|
+
<View style={{ gap: 4 }}>
|
|
267
|
+
<Typography variant="body" size="xs" color="secondary">
|
|
268
|
+
• High in protein
|
|
269
|
+
</Typography>
|
|
270
|
+
<Typography variant="body" size="xs" color="secondary">
|
|
271
|
+
• Natural ingredients
|
|
272
|
+
</Typography>
|
|
273
|
+
<Typography variant="body" size="xs" color="secondary">
|
|
274
|
+
• Vet approved
|
|
275
|
+
</Typography>
|
|
276
|
+
</View>
|
|
277
|
+
</View>
|
|
278
|
+
}
|
|
279
|
+
image={placeholderImageUrl}
|
|
280
|
+
/>
|
|
281
|
+
</View>
|
|
282
|
+
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react"
|
|
2
|
-
import { View, ViewProps, Image } from "react-native"
|
|
2
|
+
import { View, ViewProps, Image, Pressable } from "react-native"
|
|
3
3
|
import styled from "@emotion/native"
|
|
4
4
|
import { Typography } from "../../atoms/Typography"
|
|
5
5
|
import { NumberField } from "../../molecules/NumberField"
|
|
@@ -17,25 +17,28 @@ export type ProductDisplayCardProps = ViewProps & {
|
|
|
17
17
|
hideQuantityButtons?: boolean
|
|
18
18
|
incrementDisabled?: boolean
|
|
19
19
|
decrementDisabled?: boolean
|
|
20
|
+
minQuantity?: number
|
|
20
21
|
image?: string | React.ReactNode
|
|
21
22
|
imageBackgroundColor?: string
|
|
22
23
|
thumbnailWidth?: number
|
|
23
|
-
|
|
24
|
+
onImagePress?: () => void
|
|
25
|
+
banner?: string | React.ReactNode
|
|
24
26
|
showBanner?: boolean
|
|
25
27
|
bannerType?: "error" | "success" | "warning" | "info"
|
|
26
28
|
showBannerIcon?: boolean
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
const StyledCardContainer = styled(View
|
|
31
|
+
const StyledCardContainer = styled(View, {
|
|
32
|
+
shouldForwardProp: (prop) => prop !== "showBanner"
|
|
33
|
+
})<{ showBanner?: boolean }>(({ theme }) => {
|
|
30
34
|
const { spacing, borderRadius } = theme.tokens.semantics.dimensions
|
|
31
35
|
const { colour } = theme.tokens.semantics
|
|
32
36
|
const spacingMd = parseTokenValue(spacing.md)
|
|
33
37
|
const spacingXl = parseTokenValue(spacing.xl)
|
|
38
|
+
const radiusMd = parseTokenValue(borderRadius.md)
|
|
34
39
|
return {
|
|
35
40
|
flexDirection: "row",
|
|
36
41
|
width: "100%",
|
|
37
|
-
// Cap at the Figma mobile card width (can be narrower — responsive).
|
|
38
|
-
maxWidth: 358,
|
|
39
42
|
// The 148px image sets the baseline height; the card grows if the content
|
|
40
43
|
// is taller (e.g. a long or translated title wrapping to multiple lines)
|
|
41
44
|
// so nothing is clipped.
|
|
@@ -48,9 +51,12 @@ const StyledCardContainer = styled(View)(({ theme }) => {
|
|
|
48
51
|
shadowOpacity: 0.05,
|
|
49
52
|
shadowRadius: spacingXl - 4,
|
|
50
53
|
elevation: 5,
|
|
51
|
-
borderRadius:
|
|
54
|
+
borderRadius: radiusMd,
|
|
52
55
|
overflow: "hidden",
|
|
53
|
-
zIndex: 1
|
|
56
|
+
zIndex: 1,
|
|
57
|
+
// Don't apply negative margin when banner is shown; banner wrapper handles positioning
|
|
58
|
+
marginBottom: 0,
|
|
59
|
+
flex: 1
|
|
54
60
|
}
|
|
55
61
|
})
|
|
56
62
|
|
|
@@ -88,20 +94,23 @@ const StyledTextContainer = styled(View)(() => ({
|
|
|
88
94
|
|
|
89
95
|
const StyledBannerWrapper = styled(View)(({ theme }) => {
|
|
90
96
|
const { spacing, borderRadius } = theme.tokens.semantics.dimensions
|
|
97
|
+
const radiusMd = parseTokenValue(borderRadius.md)
|
|
98
|
+
const spacingMd = parseTokenValue(spacing.md)
|
|
91
99
|
return {
|
|
92
100
|
width: "100%",
|
|
93
|
-
// Tuck the banner up
|
|
94
|
-
//
|
|
95
|
-
marginTop: -
|
|
96
|
-
|
|
101
|
+
// Tuck the banner up to overlap with card bottom; the 27px notification padding
|
|
102
|
+
// shows the cream reveal under the card's rounded corners
|
|
103
|
+
marginTop: -(radiusMd + spacingMd),
|
|
104
|
+
marginBottom: 0,
|
|
105
|
+
paddingTop: spacingMd,
|
|
106
|
+
paddingBottom: spacingMd
|
|
97
107
|
}
|
|
98
108
|
})
|
|
99
109
|
|
|
100
110
|
const StyledRootWrapper = styled(View)(() => {
|
|
101
111
|
return {
|
|
102
112
|
flexDirection: "column",
|
|
103
|
-
width: "100%"
|
|
104
|
-
maxWidth: 358
|
|
113
|
+
width: "100%"
|
|
105
114
|
}
|
|
106
115
|
})
|
|
107
116
|
|
|
@@ -144,8 +153,8 @@ const StyledNotification = styled(Notification as any)(({ theme }) => {
|
|
|
144
153
|
* @param quantity - *(optional)* Current quantity value
|
|
145
154
|
* @param onQuantityChange - *(optional)* Callback when quantity changes
|
|
146
155
|
* @param showQuantityPicker - *(optional)* Show/hide quantity picker
|
|
147
|
-
* @param image - *(optional)* Image
|
|
148
|
-
* @param banner - *(optional)* Banner content to show below card
|
|
156
|
+
* @param image - *(optional)* Image URL string or React element
|
|
157
|
+
* @param banner - *(optional)* Banner content string or React element to show below card
|
|
149
158
|
* @param showBanner - *(optional)* Show/hide banner
|
|
150
159
|
* @param bannerType - *(optional)* Banner notification type: "error", "success", "warning", or "info" (default)
|
|
151
160
|
* @param showBannerIcon - *(optional)* Show/hide banner notification icon
|
|
@@ -166,9 +175,11 @@ export const ProductDisplayCard = React.forwardRef<
|
|
|
166
175
|
hideQuantityButtons = false,
|
|
167
176
|
incrementDisabled = false,
|
|
168
177
|
decrementDisabled = false,
|
|
178
|
+
minQuantity = 1,
|
|
169
179
|
image,
|
|
170
180
|
imageBackgroundColor,
|
|
171
181
|
thumbnailWidth,
|
|
182
|
+
onImagePress,
|
|
172
183
|
banner,
|
|
173
184
|
showBanner = false,
|
|
174
185
|
bannerType = "info",
|
|
@@ -178,28 +189,34 @@ export const ProductDisplayCard = React.forwardRef<
|
|
|
178
189
|
ref
|
|
179
190
|
) => {
|
|
180
191
|
const handleQuantityChange = (newQuantity: number) => {
|
|
181
|
-
if (newQuantity >=
|
|
192
|
+
if (newQuantity >= minQuantity) {
|
|
182
193
|
onQuantityChange?.(newQuantity)
|
|
183
194
|
}
|
|
184
195
|
}
|
|
185
196
|
|
|
186
197
|
const cardContent = (
|
|
187
|
-
<StyledCardContainer
|
|
198
|
+
<StyledCardContainer
|
|
199
|
+
ref={ref}
|
|
200
|
+
showBanner={showBanner && !!banner}
|
|
201
|
+
{...rest}
|
|
202
|
+
>
|
|
188
203
|
{image && (
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
204
|
+
<Pressable disabled={!onImagePress} onPress={onImagePress}>
|
|
205
|
+
<StyledImage
|
|
206
|
+
imageBackgroundColor={imageBackgroundColor}
|
|
207
|
+
thumbnailWidth={thumbnailWidth}
|
|
208
|
+
>
|
|
209
|
+
{typeof image === "string" ? (
|
|
210
|
+
<Image
|
|
211
|
+
source={{ uri: image }}
|
|
212
|
+
resizeMode="cover"
|
|
213
|
+
style={{ flex: 1, width: "100%" }}
|
|
214
|
+
/>
|
|
215
|
+
) : (
|
|
216
|
+
image
|
|
217
|
+
)}
|
|
218
|
+
</StyledImage>
|
|
219
|
+
</Pressable>
|
|
203
220
|
)}
|
|
204
221
|
|
|
205
222
|
<StyledContentArea>
|
|
@@ -228,7 +245,7 @@ export const ProductDisplayCard = React.forwardRef<
|
|
|
228
245
|
}}
|
|
229
246
|
onIncrement={() => handleQuantityChange(quantity + 1)}
|
|
230
247
|
onDecrement={() => handleQuantityChange(quantity - 1)}
|
|
231
|
-
min={
|
|
248
|
+
min={minQuantity}
|
|
232
249
|
accessible
|
|
233
250
|
accessibilityLabel={`Quantity: ${quantity}`}
|
|
234
251
|
showIncrementButton={!hideQuantityButtons}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react"
|
|
2
|
-
import { View } from "react-native"
|
|
2
|
+
import { View, Text } from "react-native"
|
|
3
3
|
import { ProductListingCard } from "./ProductListingCard"
|
|
4
4
|
import { Badge } from "./Badge"
|
|
5
5
|
import { ThumbsUpFilledPrimary } from "@butternutbox/pawprint-icons/marketing"
|
|
@@ -207,3 +207,67 @@ export const WithWasPrice = {
|
|
|
207
207
|
</View>
|
|
208
208
|
)
|
|
209
209
|
}
|
|
210
|
+
|
|
211
|
+
export const WithStringImage = {
|
|
212
|
+
render: () => (
|
|
213
|
+
<View style={{ width: 232, alignSelf: "flex-start" }}>
|
|
214
|
+
<ProductListingCard
|
|
215
|
+
image="https://placehold.co/232x232/E8E8E8/666?text=Product"
|
|
216
|
+
imageAlt="Product Image"
|
|
217
|
+
badge={defaultBadge}
|
|
218
|
+
category="Daily supplements"
|
|
219
|
+
title="Fish Oil"
|
|
220
|
+
size="200ml"
|
|
221
|
+
price="From £7.49"
|
|
222
|
+
showBadge={true}
|
|
223
|
+
showCategory={true}
|
|
224
|
+
showTitle={true}
|
|
225
|
+
showSize={true}
|
|
226
|
+
showPrice={true}
|
|
227
|
+
/>
|
|
228
|
+
</View>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export const WithCustomComponent = {
|
|
233
|
+
render: () => (
|
|
234
|
+
<View style={{ width: 232, alignSelf: "flex-start" }}>
|
|
235
|
+
<ProductListingCard
|
|
236
|
+
image={
|
|
237
|
+
<View
|
|
238
|
+
style={{
|
|
239
|
+
flex: 1,
|
|
240
|
+
backgroundColor: "#F5E6D3",
|
|
241
|
+
justifyContent: "center",
|
|
242
|
+
alignItems: "center"
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
<View
|
|
246
|
+
style={{
|
|
247
|
+
width: 80,
|
|
248
|
+
height: 80,
|
|
249
|
+
backgroundColor: "#D4A574",
|
|
250
|
+
borderRadius: 40,
|
|
251
|
+
justifyContent: "center",
|
|
252
|
+
alignItems: "center"
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
<Text style={{ fontSize: 40 }}>🐟</Text>
|
|
256
|
+
</View>
|
|
257
|
+
</View>
|
|
258
|
+
}
|
|
259
|
+
imageAlt="Custom component"
|
|
260
|
+
badge={defaultBadge}
|
|
261
|
+
category="Daily supplements"
|
|
262
|
+
title="Fish Oil"
|
|
263
|
+
size="200ml"
|
|
264
|
+
price="From £7.49"
|
|
265
|
+
showBadge={true}
|
|
266
|
+
showCategory={true}
|
|
267
|
+
showTitle={true}
|
|
268
|
+
showSize={true}
|
|
269
|
+
showPrice={true}
|
|
270
|
+
/>
|
|
271
|
+
</View>
|
|
272
|
+
)
|
|
273
|
+
}
|
|
@@ -12,7 +12,7 @@ import { Badge } from "./Badge"
|
|
|
12
12
|
import { Grid } from "./Grid"
|
|
13
13
|
|
|
14
14
|
type ProductListingCardOwnProps = {
|
|
15
|
-
image?: ImageSourcePropType
|
|
15
|
+
image?: string | React.ReactNode | ImageSourcePropType
|
|
16
16
|
imageAlt?: string
|
|
17
17
|
badge?: React.ReactNode
|
|
18
18
|
category?: string
|
|
@@ -145,11 +145,21 @@ const _ProductListingCard = React.forwardRef<View, ProductListingCardProps>(
|
|
|
145
145
|
<ImageContainer>
|
|
146
146
|
{image && (
|
|
147
147
|
<View style={{ flex: 1, width: "100%", height: "100%" }}>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
148
|
+
{typeof image === "string" ? (
|
|
149
|
+
<StyledImage
|
|
150
|
+
source={{ uri: image }}
|
|
151
|
+
accessibilityLabel={imageAlt}
|
|
152
|
+
accessible
|
|
153
|
+
/>
|
|
154
|
+
) : React.isValidElement(image) ? (
|
|
155
|
+
image
|
|
156
|
+
) : (
|
|
157
|
+
<StyledImage
|
|
158
|
+
source={image as ImageSourcePropType}
|
|
159
|
+
accessibilityLabel={imageAlt}
|
|
160
|
+
accessible
|
|
161
|
+
/>
|
|
162
|
+
)}
|
|
153
163
|
</View>
|
|
154
164
|
)}
|
|
155
165
|
{showBadge && badge && <BadgeContainer>{badge}</BadgeContainer>}
|