@butternutbox/pawprint-native 0.8.0 → 0.10.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 +7 -7
- package/CHANGELOG.md +32 -0
- package/dist/index.cjs +1194 -1061
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -13
- package/dist/index.d.ts +18 -13
- package/dist/index.js +693 -561
- 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 +31 -75
- package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.test.tsx +1 -7
- package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.tsx +93 -121
- package/src/components/molecules/ProductListingCard/Badge.tsx +2 -2
- package/src/components/molecules/ProductListingCard/ProductListingCard.stories.tsx +65 -1
- package/src/components/molecules/ProductListingCard/ProductListingCard.tsx +38 -52
- 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
|
@@ -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"
|
|
@@ -15,107 +15,74 @@ export type ProductDisplayCardProps = ViewProps & {
|
|
|
15
15
|
showQuantityPicker?: boolean
|
|
16
16
|
disableQuantityButtons?: boolean
|
|
17
17
|
hideQuantityButtons?: boolean
|
|
18
|
-
showIncrementButton?: boolean
|
|
19
|
-
showDecrementButton?: boolean
|
|
20
18
|
incrementDisabled?: boolean
|
|
21
19
|
decrementDisabled?: boolean
|
|
20
|
+
minQuantity?: number
|
|
22
21
|
image?: string | React.ReactNode
|
|
23
22
|
imageBackgroundColor?: string
|
|
24
23
|
thumbnailWidth?: number
|
|
25
|
-
|
|
26
|
-
showImageQuantityBadge?: boolean
|
|
27
|
-
borderless?: boolean
|
|
24
|
+
onImagePress?: () => void
|
|
28
25
|
banner?: React.ReactNode
|
|
29
26
|
showBanner?: boolean
|
|
30
27
|
bannerType?: "error" | "success" | "warning" | "info"
|
|
31
28
|
showBannerIcon?: boolean
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
const StyledCardContainer = styled(View
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}) => {
|
|
31
|
+
const StyledCardContainer = styled(View, {
|
|
32
|
+
shouldForwardProp: (prop) => prop !== "showBanner"
|
|
33
|
+
})<{ showBanner?: boolean }>(({ theme }) => {
|
|
38
34
|
const { spacing, borderRadius } = theme.tokens.semantics.dimensions
|
|
39
35
|
const { colour } = theme.tokens.semantics
|
|
40
36
|
const spacingMd = parseTokenValue(spacing.md)
|
|
41
37
|
const spacingXl = parseTokenValue(spacing.xl)
|
|
38
|
+
const radiusMd = parseTokenValue(borderRadius.md)
|
|
42
39
|
return {
|
|
43
40
|
flexDirection: "row",
|
|
44
41
|
width: "100%",
|
|
45
|
-
height
|
|
46
|
-
|
|
42
|
+
// The 148px image sets the baseline height; the card grows if the content
|
|
43
|
+
// is taller (e.g. a long or translated title wrapping to multiple lines)
|
|
44
|
+
// so nothing is clipped.
|
|
45
|
+
minHeight: 148,
|
|
47
46
|
backgroundColor: colour.background.surface.default,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
shadowRadius: spacingXl - 4,
|
|
57
|
-
elevation: 5
|
|
58
|
-
}),
|
|
59
|
-
borderRadius: parseTokenValue(borderRadius.md),
|
|
47
|
+
borderWidth: 1,
|
|
48
|
+
borderColor: colour.border.default,
|
|
49
|
+
shadowColor: "#522a10",
|
|
50
|
+
shadowOffset: { width: 0, height: spacingMd / 4 },
|
|
51
|
+
shadowOpacity: 0.05,
|
|
52
|
+
shadowRadius: spacingXl - 4,
|
|
53
|
+
elevation: 5,
|
|
54
|
+
borderRadius: radiusMd,
|
|
60
55
|
overflow: "hidden",
|
|
61
|
-
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
|
|
62
60
|
}
|
|
63
61
|
})
|
|
64
62
|
|
|
65
63
|
const StyledImage = styled(View)<{
|
|
66
64
|
imageBackgroundColor?: string
|
|
67
65
|
thumbnailWidth?: number
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
theme.tokens.semantics.colour.background.container.secondary,
|
|
81
|
-
overflow: "hidden",
|
|
82
|
-
aspectRatio: "1 / 1",
|
|
83
|
-
width: thumbnailWidth,
|
|
84
|
-
height: thumbnailWidth,
|
|
85
|
-
position: "relative"
|
|
86
|
-
})
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
const StyledQuantityBadge = styled(View)(({ theme }) => {
|
|
90
|
-
const spacing = theme.tokens.semantics.dimensions.spacing
|
|
91
|
-
const borderRadius = theme.tokens.semantics.dimensions.borderRadius
|
|
92
|
-
const { colour } = theme.tokens.semantics
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
position: "absolute",
|
|
96
|
-
top: parseTokenValue(spacing["2xs"]),
|
|
97
|
-
right: parseTokenValue(spacing["2xs"]),
|
|
98
|
-
backgroundColor: colour.background.container.brand,
|
|
99
|
-
borderRadius: parseTokenValue(borderRadius.xs),
|
|
100
|
-
paddingHorizontal: parseTokenValue(spacing.sm),
|
|
101
|
-
height: parseTokenValue(spacing["2xl"]),
|
|
102
|
-
minWidth: parseTokenValue(spacing["2xl"]),
|
|
103
|
-
alignItems: "center",
|
|
104
|
-
justifyContent: "center",
|
|
105
|
-
zIndex: 2
|
|
106
|
-
}
|
|
107
|
-
})
|
|
66
|
+
}>(({ theme, imageBackgroundColor, thumbnailWidth = 148 }) => ({
|
|
67
|
+
flexShrink: 0,
|
|
68
|
+
backgroundColor:
|
|
69
|
+
imageBackgroundColor ||
|
|
70
|
+
theme.tokens.semantics.colour.background.container.secondary,
|
|
71
|
+
overflow: "hidden",
|
|
72
|
+
width: thumbnailWidth,
|
|
73
|
+
// Square (148) by default; stretches taller to fill the card when the
|
|
74
|
+
// content makes it taller than the image.
|
|
75
|
+
minHeight: thumbnailWidth,
|
|
76
|
+
position: "relative"
|
|
77
|
+
}))
|
|
108
78
|
|
|
109
|
-
const StyledContentArea = styled(View)
|
|
110
|
-
theme,
|
|
111
|
-
borderless
|
|
112
|
-
}) => {
|
|
79
|
+
const StyledContentArea = styled(View)(({ theme }) => {
|
|
113
80
|
const { spacing } = theme.tokens.semantics.dimensions
|
|
114
81
|
const paddingValue = parseTokenValue(spacing.md)
|
|
115
82
|
return {
|
|
116
83
|
flex: 1,
|
|
117
84
|
paddingHorizontal: paddingValue,
|
|
118
|
-
paddingVertical:
|
|
85
|
+
paddingVertical: paddingValue,
|
|
119
86
|
gap: parseTokenValue(spacing.xs),
|
|
120
87
|
justifyContent: "space-between"
|
|
121
88
|
}
|
|
@@ -125,27 +92,44 @@ const StyledTextContainer = styled(View)(() => ({
|
|
|
125
92
|
gap: 2
|
|
126
93
|
}))
|
|
127
94
|
|
|
128
|
-
const StyledBannerWrapper = styled(View)(() =>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
95
|
+
const StyledBannerWrapper = styled(View)(({ theme }) => {
|
|
96
|
+
const { spacing, borderRadius } = theme.tokens.semantics.dimensions
|
|
97
|
+
const radiusMd = parseTokenValue(borderRadius.md)
|
|
98
|
+
const spacingMd = parseTokenValue(spacing.md)
|
|
99
|
+
return {
|
|
100
|
+
width: "100%",
|
|
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
|
|
107
|
+
}
|
|
108
|
+
})
|
|
134
109
|
|
|
135
110
|
const StyledRootWrapper = styled(View)(() => {
|
|
136
111
|
return {
|
|
137
112
|
flexDirection: "column",
|
|
138
113
|
width: "100%",
|
|
139
|
-
|
|
114
|
+
maxWidth: 358
|
|
140
115
|
}
|
|
141
116
|
})
|
|
142
117
|
|
|
143
118
|
const StyledNotification = styled(Notification as any)(({ theme }) => {
|
|
144
|
-
const { spacing } = theme.tokens.semantics.dimensions
|
|
119
|
+
const { spacing, borderRadius } = theme.tokens.semantics.dimensions
|
|
120
|
+
const spacingMd = parseTokenValue(spacing.md)
|
|
121
|
+
const radiusMd = parseTokenValue(borderRadius.md)
|
|
145
122
|
return {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
123
|
+
paddingHorizontal: spacingMd,
|
|
124
|
+
// 27px top padding (Figma) leaves a small cream reveal above the text once
|
|
125
|
+
// the banner tucks behind the card.
|
|
126
|
+
paddingTop: 27,
|
|
127
|
+
paddingBottom: spacingMd,
|
|
128
|
+
// Square top corners, rounded bottom corners (banner tucks under the card).
|
|
129
|
+
borderTopLeftRadius: 0,
|
|
130
|
+
borderTopRightRadius: 0,
|
|
131
|
+
borderBottomLeftRadius: radiusMd,
|
|
132
|
+
borderBottomRightRadius: radiusMd
|
|
149
133
|
}
|
|
150
134
|
})
|
|
151
135
|
|
|
@@ -190,16 +174,13 @@ export const ProductDisplayCard = React.forwardRef<
|
|
|
190
174
|
showQuantityPicker = false,
|
|
191
175
|
disableQuantityButtons = false,
|
|
192
176
|
hideQuantityButtons = false,
|
|
193
|
-
showIncrementButton,
|
|
194
|
-
showDecrementButton,
|
|
195
177
|
incrementDisabled = false,
|
|
196
178
|
decrementDisabled = false,
|
|
179
|
+
minQuantity = 1,
|
|
197
180
|
image,
|
|
198
181
|
imageBackgroundColor,
|
|
199
182
|
thumbnailWidth,
|
|
200
|
-
|
|
201
|
-
showImageQuantityBadge = false,
|
|
202
|
-
borderless = false,
|
|
183
|
+
onImagePress,
|
|
203
184
|
banner,
|
|
204
185
|
showBanner = false,
|
|
205
186
|
bannerType = "info",
|
|
@@ -209,38 +190,37 @@ export const ProductDisplayCard = React.forwardRef<
|
|
|
209
190
|
ref
|
|
210
191
|
) => {
|
|
211
192
|
const handleQuantityChange = (newQuantity: number) => {
|
|
212
|
-
if (newQuantity >=
|
|
193
|
+
if (newQuantity >= minQuantity) {
|
|
213
194
|
onQuantityChange?.(newQuantity)
|
|
214
195
|
}
|
|
215
196
|
}
|
|
216
197
|
|
|
217
198
|
const cardContent = (
|
|
218
|
-
<StyledCardContainer
|
|
199
|
+
<StyledCardContainer
|
|
200
|
+
ref={ref}
|
|
201
|
+
showBanner={showBanner && !!banner}
|
|
202
|
+
{...rest}
|
|
203
|
+
>
|
|
219
204
|
{image && (
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
{quantity}
|
|
237
|
-
</Typography>
|
|
238
|
-
</StyledQuantityBadge>
|
|
239
|
-
)}
|
|
240
|
-
</StyledImage>
|
|
205
|
+
<Pressable disabled={!onImagePress} onPress={onImagePress}>
|
|
206
|
+
<StyledImage
|
|
207
|
+
imageBackgroundColor={imageBackgroundColor}
|
|
208
|
+
thumbnailWidth={thumbnailWidth}
|
|
209
|
+
>
|
|
210
|
+
{typeof image === "string" ? (
|
|
211
|
+
<Image
|
|
212
|
+
source={{ uri: image }}
|
|
213
|
+
resizeMode="cover"
|
|
214
|
+
style={{ flex: 1, width: "100%" }}
|
|
215
|
+
/>
|
|
216
|
+
) : (
|
|
217
|
+
image
|
|
218
|
+
)}
|
|
219
|
+
</StyledImage>
|
|
220
|
+
</Pressable>
|
|
241
221
|
)}
|
|
242
222
|
|
|
243
|
-
<StyledContentArea
|
|
223
|
+
<StyledContentArea>
|
|
244
224
|
<StyledTextContainer>
|
|
245
225
|
<Typography variant="heading" size="2xs" color="primary">
|
|
246
226
|
{title}
|
|
@@ -266,19 +246,11 @@ export const ProductDisplayCard = React.forwardRef<
|
|
|
266
246
|
}}
|
|
267
247
|
onIncrement={() => handleQuantityChange(quantity + 1)}
|
|
268
248
|
onDecrement={() => handleQuantityChange(quantity - 1)}
|
|
269
|
-
min={
|
|
249
|
+
min={minQuantity}
|
|
270
250
|
accessible
|
|
271
251
|
accessibilityLabel={`Quantity: ${quantity}`}
|
|
272
|
-
showIncrementButton={
|
|
273
|
-
|
|
274
|
-
? showIncrementButton
|
|
275
|
-
: !hideQuantityButtons
|
|
276
|
-
}
|
|
277
|
-
showDecrementButton={
|
|
278
|
-
showDecrementButton !== undefined
|
|
279
|
-
? showDecrementButton
|
|
280
|
-
: !hideQuantityButtons
|
|
281
|
-
}
|
|
252
|
+
showIncrementButton={!hideQuantityButtons}
|
|
253
|
+
showDecrementButton={!hideQuantityButtons}
|
|
282
254
|
incrementDisabled={incrementDisabled || disableQuantityButtons}
|
|
283
255
|
decrementDisabled={decrementDisabled || disableQuantityButtons}
|
|
284
256
|
/>
|
|
@@ -27,8 +27,8 @@ const BadgeContainer = styled(View)(({ theme }) => {
|
|
|
27
27
|
alignItems: "center",
|
|
28
28
|
justifyContent: "center",
|
|
29
29
|
height: parseTokenValue(sizing["2xl"]),
|
|
30
|
-
paddingRight: parseTokenValue(spacing.
|
|
31
|
-
backgroundColor: colour.background.container.
|
|
30
|
+
paddingRight: parseTokenValue(spacing.xs),
|
|
31
|
+
backgroundColor: colour.background.container.brand,
|
|
32
32
|
borderTopRightRadius: parseTokenValue(borderRadius.xs),
|
|
33
33
|
borderBottomRightRadius: parseTokenValue(borderRadius.xs)
|
|
34
34
|
}
|
|
@@ -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
|
|
@@ -48,12 +48,15 @@ const StyledRoot = styled(Pressable)(({ theme }) => {
|
|
|
48
48
|
}
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
const ImageContainer = styled(View)({
|
|
51
|
+
const ImageContainer = styled(View)(({ theme }) => ({
|
|
52
52
|
position: "relative",
|
|
53
53
|
width: "100%",
|
|
54
54
|
aspectRatio: 1,
|
|
55
|
-
overflow: "hidden"
|
|
56
|
-
|
|
55
|
+
overflow: "hidden",
|
|
56
|
+
borderRadius: parseTokenValue(
|
|
57
|
+
theme.tokens.semantics.dimensions.borderRadius.md
|
|
58
|
+
)
|
|
59
|
+
}))
|
|
57
60
|
|
|
58
61
|
const StyledImage = styled(Image)({
|
|
59
62
|
width: "100%",
|
|
@@ -61,27 +64,13 @@ const StyledImage = styled(Image)({
|
|
|
61
64
|
resizeMode: "cover"
|
|
62
65
|
})
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
top: parseTokenValue(spacing.md),
|
|
72
|
-
left: 0,
|
|
73
|
-
display: "flex",
|
|
74
|
-
flexDirection: "row",
|
|
75
|
-
gap: parseTokenValue(spacing["2xs"]),
|
|
76
|
-
alignItems: "center",
|
|
77
|
-
justifyContent: "center",
|
|
78
|
-
height: parseTokenValue(sizing["2xl"]),
|
|
79
|
-
paddingRight: parseTokenValue(spacing.sm),
|
|
80
|
-
backgroundColor: colour.background.container.alt,
|
|
81
|
-
borderTopRightRadius: parseTokenValue(borderRadius.sm),
|
|
82
|
-
borderBottomRightRadius: parseTokenValue(borderRadius.sm)
|
|
83
|
-
}
|
|
84
|
-
})
|
|
67
|
+
// Positioning only — the visual badge (background, padding, radius) is the
|
|
68
|
+
// `Badge` component rendered inside, so it isn't double-wrapped.
|
|
69
|
+
const BadgeContainer = styled(View)(({ theme }) => ({
|
|
70
|
+
position: "absolute",
|
|
71
|
+
top: parseTokenValue(theme.tokens.semantics.dimensions.spacing.md),
|
|
72
|
+
left: 0
|
|
73
|
+
}))
|
|
85
74
|
|
|
86
75
|
const ContentContainer = styled(View)(({ theme }) => {
|
|
87
76
|
const spacing = theme.tokens.semantics.dimensions.spacing
|
|
@@ -110,23 +99,12 @@ const CategoryWrapper = styled(View)(({ theme }) => {
|
|
|
110
99
|
}
|
|
111
100
|
})
|
|
112
101
|
|
|
113
|
-
const TitleContainer = styled(View)(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
width: "100%",
|
|
120
|
-
marginBottom: parseTokenValue(spacing.xs)
|
|
121
|
-
}
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
const SizeWrapper = styled(View)(({ theme }) => {
|
|
125
|
-
const spacing = theme.tokens.semantics.dimensions.spacing
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
marginBottom: parseTokenValue(spacing.md)
|
|
129
|
-
}
|
|
102
|
+
const TitleContainer = styled(View)({
|
|
103
|
+
display: "flex",
|
|
104
|
+
flexDirection: "row",
|
|
105
|
+
width: "100%",
|
|
106
|
+
// 6px below the title to match Figma (4px title bottom padding + 2px gap).
|
|
107
|
+
marginBottom: 6
|
|
130
108
|
})
|
|
131
109
|
|
|
132
110
|
const PricingContainer = styled(View)(({ theme }) => {
|
|
@@ -167,11 +145,21 @@ const _ProductListingCard = React.forwardRef<View, ProductListingCardProps>(
|
|
|
167
145
|
<ImageContainer>
|
|
168
146
|
{image && (
|
|
169
147
|
<View style={{ flex: 1, width: "100%", height: "100%" }}>
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
)}
|
|
175
163
|
</View>
|
|
176
164
|
)}
|
|
177
165
|
{showBadge && badge && <BadgeContainer>{badge}</BadgeContainer>}
|
|
@@ -194,11 +182,9 @@ const _ProductListingCard = React.forwardRef<View, ProductListingCardProps>(
|
|
|
194
182
|
</TitleContainer>
|
|
195
183
|
)}
|
|
196
184
|
{showSize && size && (
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
</Typography>
|
|
201
|
-
</SizeWrapper>
|
|
185
|
+
<Typography variant="body" size="md" color="secondary">
|
|
186
|
+
{size}
|
|
187
|
+
</Typography>
|
|
202
188
|
)}
|
|
203
189
|
</DetailsContainer>
|
|
204
190
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React, { useState } from "react"
|
|
2
2
|
import { View, StyleSheet } from "react-native"
|
|
3
|
-
import { PortalHost } from "@rn-primitives/portal"
|
|
4
3
|
import { SelectField, useSelectField } from "."
|
|
5
4
|
import { Typography } from "../../atoms/Typography"
|
|
6
5
|
import { Icon } from "../../atoms/Icon"
|
|
@@ -8,10 +7,7 @@ import { Search, Settings, Lock, Info } from "@butternutbox/pawprint-icons/core"
|
|
|
8
7
|
import type { Option } from "@rn-primitives/select"
|
|
9
8
|
|
|
10
9
|
const StoryWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
11
|
-
<>
|
|
12
|
-
{children}
|
|
13
|
-
<PortalHost />
|
|
14
|
-
</>
|
|
10
|
+
<>{children}</>
|
|
15
11
|
)
|
|
16
12
|
|
|
17
13
|
export default {
|
|
@@ -304,6 +300,115 @@ export const Controlled = () => {
|
|
|
304
300
|
)
|
|
305
301
|
}
|
|
306
302
|
|
|
303
|
+
export const Searchable = () => (
|
|
304
|
+
<StoryWrapper>
|
|
305
|
+
<View style={styles.column}>
|
|
306
|
+
<View style={styles.section}>
|
|
307
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
308
|
+
Searchable Select
|
|
309
|
+
</Typography>
|
|
310
|
+
<SelectField
|
|
311
|
+
label="Country"
|
|
312
|
+
placeholder="Select a country"
|
|
313
|
+
description="Type to filter countries"
|
|
314
|
+
searchable
|
|
315
|
+
searchPlaceholder="Search countries..."
|
|
316
|
+
>
|
|
317
|
+
<SelectField.Item value="au">Australia</SelectField.Item>
|
|
318
|
+
<SelectField.Item value="br">Brazil</SelectField.Item>
|
|
319
|
+
<SelectField.Item value="ca">Canada</SelectField.Item>
|
|
320
|
+
<SelectField.Item value="dk">Denmark</SelectField.Item>
|
|
321
|
+
<SelectField.Item value="eg">Egypt</SelectField.Item>
|
|
322
|
+
<SelectField.Item value="fr">France</SelectField.Item>
|
|
323
|
+
<SelectField.Item value="de">Germany</SelectField.Item>
|
|
324
|
+
<SelectField.Item value="in">India</SelectField.Item>
|
|
325
|
+
<SelectField.Item value="jp">Japan</SelectField.Item>
|
|
326
|
+
<SelectField.Item value="mx">Mexico</SelectField.Item>
|
|
327
|
+
<SelectField.Item value="nl">Netherlands</SelectField.Item>
|
|
328
|
+
<SelectField.Item value="nz">New Zealand</SelectField.Item>
|
|
329
|
+
<SelectField.Item value="no">Norway</SelectField.Item>
|
|
330
|
+
<SelectField.Item value="pl">Poland</SelectField.Item>
|
|
331
|
+
<SelectField.Item value="pt">Portugal</SelectField.Item>
|
|
332
|
+
<SelectField.Item value="es">Spain</SelectField.Item>
|
|
333
|
+
<SelectField.Item value="se">Sweden</SelectField.Item>
|
|
334
|
+
<SelectField.Item value="ch">Switzerland</SelectField.Item>
|
|
335
|
+
<SelectField.Item value="uk">United Kingdom</SelectField.Item>
|
|
336
|
+
<SelectField.Item value="us">United States</SelectField.Item>
|
|
337
|
+
</SelectField>
|
|
338
|
+
</View>
|
|
339
|
+
</View>
|
|
340
|
+
</StoryWrapper>
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
export const SearchableControlled = () => {
|
|
344
|
+
const [value, setValue] = useState<Option | string | null>(null)
|
|
345
|
+
|
|
346
|
+
const displayValue = (() => {
|
|
347
|
+
if (!value) return "(none)"
|
|
348
|
+
if (typeof value === "object" && "value" in value) return value.value
|
|
349
|
+
return String(value)
|
|
350
|
+
})()
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<StoryWrapper>
|
|
354
|
+
<View style={styles.column}>
|
|
355
|
+
<View style={styles.section}>
|
|
356
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
357
|
+
Controlled value: {displayValue}
|
|
358
|
+
</Typography>
|
|
359
|
+
<SelectField
|
|
360
|
+
label="Country"
|
|
361
|
+
placeholder="Select a country"
|
|
362
|
+
description="Select a country — the clear button resets the controlled value"
|
|
363
|
+
searchable
|
|
364
|
+
searchPlaceholder="Search countries..."
|
|
365
|
+
value={value}
|
|
366
|
+
onValueChange={(newValue) => setValue(newValue)}
|
|
367
|
+
>
|
|
368
|
+
<SelectField.Item value="au">Australia</SelectField.Item>
|
|
369
|
+
<SelectField.Item value="ca">Canada</SelectField.Item>
|
|
370
|
+
<SelectField.Item value="fr">France</SelectField.Item>
|
|
371
|
+
<SelectField.Item value="de">Germany</SelectField.Item>
|
|
372
|
+
<SelectField.Item value="jp">Japan</SelectField.Item>
|
|
373
|
+
<SelectField.Item value="uk">United Kingdom</SelectField.Item>
|
|
374
|
+
<SelectField.Item value="us">United States</SelectField.Item>
|
|
375
|
+
</SelectField>
|
|
376
|
+
</View>
|
|
377
|
+
</View>
|
|
378
|
+
</StoryWrapper>
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export const SearchableDefaultPlaceholder = () => (
|
|
383
|
+
<StoryWrapper>
|
|
384
|
+
<View style={styles.column}>
|
|
385
|
+
<View style={styles.section}>
|
|
386
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
387
|
+
Default Search Placeholder
|
|
388
|
+
</Typography>
|
|
389
|
+
<SelectField label="Pet breed" placeholder="Select a breed" searchable>
|
|
390
|
+
<SelectField.Item value="labrador">
|
|
391
|
+
Labrador Retriever
|
|
392
|
+
</SelectField.Item>
|
|
393
|
+
<SelectField.Item value="german-shepherd">
|
|
394
|
+
German Shepherd
|
|
395
|
+
</SelectField.Item>
|
|
396
|
+
<SelectField.Item value="golden-retriever">
|
|
397
|
+
Golden Retriever
|
|
398
|
+
</SelectField.Item>
|
|
399
|
+
<SelectField.Item value="bulldog">Bulldog</SelectField.Item>
|
|
400
|
+
<SelectField.Item value="poodle">Poodle</SelectField.Item>
|
|
401
|
+
<SelectField.Item value="beagle">Beagle</SelectField.Item>
|
|
402
|
+
<SelectField.Item value="rottweiler">Rottweiler</SelectField.Item>
|
|
403
|
+
<SelectField.Item value="yorkshire-terrier">
|
|
404
|
+
Yorkshire Terrier
|
|
405
|
+
</SelectField.Item>
|
|
406
|
+
</SelectField>
|
|
407
|
+
</View>
|
|
408
|
+
</View>
|
|
409
|
+
</StoryWrapper>
|
|
410
|
+
)
|
|
411
|
+
|
|
307
412
|
const styles = StyleSheet.create({
|
|
308
413
|
container: {
|
|
309
414
|
width: 320
|