@butternutbox/pawprint-native 0.9.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 +9 -9
- package/CHANGELOG.md +11 -0
- package/dist/index.cjs +1140 -959
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -3
- package/dist/index.d.ts +18 -3
- package/dist/index.js +639 -459
- 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 +38 -0
- package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.tsx +45 -27
- 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"
|
|
@@ -205,3 +205,41 @@ export const AllVariants = () => (
|
|
|
205
205
|
/>
|
|
206
206
|
</View>
|
|
207
207
|
)
|
|
208
|
+
|
|
209
|
+
export const WithMinQuantity = () => (
|
|
210
|
+
<View style={styles.column}>
|
|
211
|
+
<ProductDisplayCard
|
|
212
|
+
title="Bulk Recipe Pack"
|
|
213
|
+
subtext="+£2.50/pouch"
|
|
214
|
+
showSubtext
|
|
215
|
+
quantity={5}
|
|
216
|
+
minQuantity={5}
|
|
217
|
+
showQuantityPicker
|
|
218
|
+
showBanner
|
|
219
|
+
bannerType="info"
|
|
220
|
+
showBannerIcon={false}
|
|
221
|
+
banner="Minimum quantity: 5 pouches required"
|
|
222
|
+
image={placeholderImageUrl}
|
|
223
|
+
/>
|
|
224
|
+
</View>
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
export const WithImagePress = () => {
|
|
228
|
+
const [pressed, setPressed] = React.useState(false)
|
|
229
|
+
return (
|
|
230
|
+
<View style={styles.column}>
|
|
231
|
+
<ProductDisplayCard
|
|
232
|
+
title="Recipe Name"
|
|
233
|
+
subtext="+£0.00/pouch"
|
|
234
|
+
showSubtext
|
|
235
|
+
quantity={1}
|
|
236
|
+
showQuantityPicker
|
|
237
|
+
onImagePress={() => setPressed(true)}
|
|
238
|
+
showBanner={pressed}
|
|
239
|
+
bannerType="success"
|
|
240
|
+
banner="Image pressed!"
|
|
241
|
+
image={placeholderImageUrl}
|
|
242
|
+
/>
|
|
243
|
+
</View>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
@@ -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
|
|
24
|
+
onImagePress?: () => void
|
|
23
25
|
banner?: 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,12 +94,16 @@ 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
|
|
|
@@ -166,9 +176,11 @@ export const ProductDisplayCard = React.forwardRef<
|
|
|
166
176
|
hideQuantityButtons = false,
|
|
167
177
|
incrementDisabled = false,
|
|
168
178
|
decrementDisabled = false,
|
|
179
|
+
minQuantity = 1,
|
|
169
180
|
image,
|
|
170
181
|
imageBackgroundColor,
|
|
171
182
|
thumbnailWidth,
|
|
183
|
+
onImagePress,
|
|
172
184
|
banner,
|
|
173
185
|
showBanner = false,
|
|
174
186
|
bannerType = "info",
|
|
@@ -178,28 +190,34 @@ export const ProductDisplayCard = React.forwardRef<
|
|
|
178
190
|
ref
|
|
179
191
|
) => {
|
|
180
192
|
const handleQuantityChange = (newQuantity: number) => {
|
|
181
|
-
if (newQuantity >=
|
|
193
|
+
if (newQuantity >= minQuantity) {
|
|
182
194
|
onQuantityChange?.(newQuantity)
|
|
183
195
|
}
|
|
184
196
|
}
|
|
185
197
|
|
|
186
198
|
const cardContent = (
|
|
187
|
-
<StyledCardContainer
|
|
199
|
+
<StyledCardContainer
|
|
200
|
+
ref={ref}
|
|
201
|
+
showBanner={showBanner && !!banner}
|
|
202
|
+
{...rest}
|
|
203
|
+
>
|
|
188
204
|
{image && (
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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>
|
|
203
221
|
)}
|
|
204
222
|
|
|
205
223
|
<StyledContentArea>
|
|
@@ -228,7 +246,7 @@ export const ProductDisplayCard = React.forwardRef<
|
|
|
228
246
|
}}
|
|
229
247
|
onIncrement={() => handleQuantityChange(quantity + 1)}
|
|
230
248
|
onDecrement={() => handleQuantityChange(quantity - 1)}
|
|
231
|
-
min={
|
|
249
|
+
min={minQuantity}
|
|
232
250
|
accessible
|
|
233
251
|
accessibilityLabel={`Quantity: ${quantity}`}
|
|
234
252
|
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>}
|
|
@@ -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
|