@butternutbox/pawprint-native 0.5.1 → 0.7.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 +15 -15
- package/CHANGELOG.md +17 -0
- package/dist/index.cjs +606 -299
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -5
- package/dist/index.d.ts +61 -5
- package/dist/index.js +564 -258
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/molecules/ButtonDock/ButtonDock.tsx +10 -4
- package/src/components/molecules/NativeSelectPicker/NativeSelectPicker.stories.tsx +98 -0
- package/src/components/molecules/NativeSelectPicker/NativeSelectPicker.styled.ts +127 -0
- package/src/components/molecules/NativeSelectPicker/NativeSelectPicker.tsx +250 -0
- package/src/components/molecules/NativeSelectPicker/index.ts +5 -0
- package/src/components/molecules/Notification/Notification.tsx +36 -10
- package/src/components/molecules/Radio/Radio.stories.tsx +46 -0
- package/src/components/molecules/Radio/Radio.test.tsx +18 -0
- package/src/components/molecules/Radio/Radio.tsx +19 -1
- package/src/components/molecules/index.ts +1 -0
package/package.json
CHANGED
|
@@ -22,12 +22,14 @@ const StyledDockRoot = styled(View)<{
|
|
|
22
22
|
dockBorderTopWidth: number
|
|
23
23
|
dockBorderTopColor: string
|
|
24
24
|
dockPaddingVertical: number
|
|
25
|
+
dockPaddingHorizontal: number
|
|
25
26
|
}>(
|
|
26
27
|
({
|
|
27
28
|
dockBgColor,
|
|
28
29
|
dockBorderTopWidth,
|
|
29
30
|
dockBorderTopColor,
|
|
30
|
-
dockPaddingVertical
|
|
31
|
+
dockPaddingVertical,
|
|
32
|
+
dockPaddingHorizontal
|
|
31
33
|
}) => ({
|
|
32
34
|
alignItems: "center",
|
|
33
35
|
justifyContent: "center",
|
|
@@ -35,7 +37,8 @@ const StyledDockRoot = styled(View)<{
|
|
|
35
37
|
backgroundColor: dockBgColor,
|
|
36
38
|
borderTopWidth: dockBorderTopWidth,
|
|
37
39
|
borderTopColor: dockBorderTopColor,
|
|
38
|
-
paddingVertical: dockPaddingVertical
|
|
40
|
+
paddingVertical: dockPaddingVertical,
|
|
41
|
+
paddingHorizontal: dockPaddingHorizontal
|
|
39
42
|
})
|
|
40
43
|
)
|
|
41
44
|
|
|
@@ -51,7 +54,7 @@ const StyledStackedInner = styled(View)<{ innerGap: number }>(
|
|
|
51
54
|
const StyledButtonGroup = styled(View)<{
|
|
52
55
|
groupDirection: "column" | "row"
|
|
53
56
|
groupAlign: "stretch" | "center"
|
|
54
|
-
groupJustify: "center" | "space-
|
|
57
|
+
groupJustify: "center" | "space-between"
|
|
55
58
|
groupGap: number
|
|
56
59
|
}>(({ groupDirection, groupAlign, groupJustify, groupGap }) => ({
|
|
57
60
|
flexDirection: groupDirection,
|
|
@@ -105,6 +108,9 @@ const ButtonDock = React.forwardRef<View, ButtonDockProps>(
|
|
|
105
108
|
dockPaddingVertical={parseTokenValue(
|
|
106
109
|
buttonDock.spacing[variant].mobile.topPadding
|
|
107
110
|
)}
|
|
111
|
+
dockPaddingHorizontal={parseTokenValue(
|
|
112
|
+
buttonDock.spacing[variant].mobile.horizontalPadding
|
|
113
|
+
)}
|
|
108
114
|
{...rest}
|
|
109
115
|
>
|
|
110
116
|
{isStacked ? (
|
|
@@ -134,7 +140,7 @@ const ButtonDock = React.forwardRef<View, ButtonDockProps>(
|
|
|
134
140
|
<StyledButtonGroup
|
|
135
141
|
groupDirection="row"
|
|
136
142
|
groupAlign="center"
|
|
137
|
-
groupJustify="space-
|
|
143
|
+
groupJustify="space-between"
|
|
138
144
|
groupGap={groupGap}
|
|
139
145
|
>
|
|
140
146
|
{children}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React, { useState } from "react"
|
|
2
|
+
import { View } from "react-native"
|
|
3
|
+
import { NativeSelectPicker } from "./NativeSelectPicker"
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: "Molecules/NativeSelectPicker",
|
|
7
|
+
component: NativeSelectPicker,
|
|
8
|
+
argTypes: {
|
|
9
|
+
disabled: {
|
|
10
|
+
control: "boolean"
|
|
11
|
+
},
|
|
12
|
+
hideIcon: {
|
|
13
|
+
control: "boolean"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
decorators: [
|
|
17
|
+
(Story: React.ComponentType) => (
|
|
18
|
+
<View style={{ padding: 20, flex: 1, justifyContent: "center" }}>
|
|
19
|
+
<Story />
|
|
20
|
+
</View>
|
|
21
|
+
)
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const BANK_OPTIONS = [
|
|
26
|
+
{ label: "ABN Amro", value: "abn_amro" },
|
|
27
|
+
{ label: "ASN Bank", value: "asn_bank" },
|
|
28
|
+
{ label: "bunq B.V.", value: "bunq" },
|
|
29
|
+
{ label: "ING Bank", value: "ing" },
|
|
30
|
+
{ label: "Knab", value: "knab" },
|
|
31
|
+
{ label: "Rabobank", value: "rabobank" }
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
export const Basic = () => {
|
|
35
|
+
const [value, setValue] = useState<string | number | null>(null)
|
|
36
|
+
return (
|
|
37
|
+
<NativeSelectPicker
|
|
38
|
+
items={BANK_OPTIONS}
|
|
39
|
+
placeholder={{ label: "Select a bank" }}
|
|
40
|
+
value={value}
|
|
41
|
+
onValueChange={setValue}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const WithSelectedValue = () => {
|
|
47
|
+
const [value, setValue] = useState<string | number | null>("ing")
|
|
48
|
+
return (
|
|
49
|
+
<NativeSelectPicker
|
|
50
|
+
items={BANK_OPTIONS}
|
|
51
|
+
placeholder={{ label: "Select a bank" }}
|
|
52
|
+
value={value}
|
|
53
|
+
onValueChange={setValue}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const Disabled = () => {
|
|
59
|
+
const [value, setValue] = useState<string | number | null>(null)
|
|
60
|
+
return (
|
|
61
|
+
<NativeSelectPicker
|
|
62
|
+
items={BANK_OPTIONS}
|
|
63
|
+
placeholder={{ label: "Select a bank" }}
|
|
64
|
+
value={value}
|
|
65
|
+
onValueChange={setValue}
|
|
66
|
+
disabled
|
|
67
|
+
/>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const HideIcon = () => {
|
|
72
|
+
const [value, setValue] = useState<string | number | null>(null)
|
|
73
|
+
return (
|
|
74
|
+
<NativeSelectPicker
|
|
75
|
+
items={BANK_OPTIONS}
|
|
76
|
+
placeholder={{ label: "Select a bank" }}
|
|
77
|
+
value={value}
|
|
78
|
+
onValueChange={setValue}
|
|
79
|
+
hideIcon
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const NumericValues = () => {
|
|
85
|
+
const [value, setValue] = useState<string | number | null>(null)
|
|
86
|
+
return (
|
|
87
|
+
<NativeSelectPicker
|
|
88
|
+
items={[
|
|
89
|
+
{ label: "Option 1", value: 1 },
|
|
90
|
+
{ label: "Option 2", value: 2 },
|
|
91
|
+
{ label: "Option 3", value: 3 }
|
|
92
|
+
]}
|
|
93
|
+
placeholder={{ label: "Select an option" }}
|
|
94
|
+
value={value}
|
|
95
|
+
onValueChange={setValue}
|
|
96
|
+
/>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import styled from "@emotion/native"
|
|
2
|
+
import { View, Text } from "react-native"
|
|
3
|
+
import Animated from "react-native-reanimated"
|
|
4
|
+
import { DEFAULT_THEME_OPTIONS } from "@butternutbox/pawprint-tokens"
|
|
5
|
+
|
|
6
|
+
type AndroidOptionTextProps = {
|
|
7
|
+
isSelected: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const themeTokens = DEFAULT_THEME_OPTIONS.tokens
|
|
11
|
+
const { components, semantics } = themeTokens
|
|
12
|
+
const inputTokens = components.inputs
|
|
13
|
+
const { dimensions } = semantics
|
|
14
|
+
|
|
15
|
+
const parseSize = (value: string): number => {
|
|
16
|
+
return parseInt(value.replace("px", ""), 10)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const Wrapper = styled(View)({
|
|
20
|
+
borderWidth: parseSize(inputTokens.borderWidth.field.selected),
|
|
21
|
+
borderColor: inputTokens.colour.field.border.selected,
|
|
22
|
+
borderRadius: parseSize(inputTokens.borderRadius.field.default),
|
|
23
|
+
backgroundColor: inputTokens.colour.field.background.default,
|
|
24
|
+
paddingHorizontal: parseSize(inputTokens.spacing.field.horizontalPadding),
|
|
25
|
+
paddingVertical: parseSize(inputTokens.spacing.field.verticalPadding),
|
|
26
|
+
height: parseSize(inputTokens.size.field.height),
|
|
27
|
+
flexDirection: "row",
|
|
28
|
+
alignItems: "center",
|
|
29
|
+
justifyContent: "space-between"
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const SelectIcon = styled(Animated.View)({
|
|
33
|
+
position: "absolute",
|
|
34
|
+
right: 0,
|
|
35
|
+
marginRight: 0
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const InputText = styled(Text)({
|
|
39
|
+
color: inputTokens.colour.field.text.default,
|
|
40
|
+
fontSize: parseSize(inputTokens.field.placeholder.default.fontSize),
|
|
41
|
+
fontWeight: inputTokens.field.placeholder.default.fontWeight,
|
|
42
|
+
flex: 1
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const PlaceholderText = styled(Text)({
|
|
46
|
+
color: inputTokens.colour.field.text.placeholder,
|
|
47
|
+
fontSize: parseSize(inputTokens.field.placeholder.default.fontSize),
|
|
48
|
+
fontWeight: inputTokens.field.placeholder.default.fontWeight,
|
|
49
|
+
flex: 1
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const ModalOverlay = styled(View)({
|
|
53
|
+
flex: 1,
|
|
54
|
+
backgroundColor: "rgba(0, 0, 0, 0.32)",
|
|
55
|
+
justifyContent: "flex-end"
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const AndroidDialog = styled(View)({
|
|
59
|
+
backgroundColor: inputTokens.colour.field.background.default,
|
|
60
|
+
borderTopLeftRadius: parseSize(dimensions.borderRadius.lg),
|
|
61
|
+
borderTopRightRadius: parseSize(dimensions.borderRadius.lg),
|
|
62
|
+
maxHeight: "80%",
|
|
63
|
+
minHeight: "40%",
|
|
64
|
+
paddingTop: parseSize(dimensions.spacing.md),
|
|
65
|
+
paddingBottom: parseSize(dimensions.spacing.xl),
|
|
66
|
+
paddingHorizontal: parseSize(dimensions.spacing.md)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const DialogTitle = styled(Text)({
|
|
70
|
+
fontSize: parseSize(inputTokens.field.placeholder.default.fontSize),
|
|
71
|
+
fontWeight: inputTokens.text.label.fontWeight,
|
|
72
|
+
color: inputTokens.colour.field.text.default,
|
|
73
|
+
marginBottom: parseSize(dimensions.spacing.md),
|
|
74
|
+
marginLeft: parseSize(dimensions.spacing.xs)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const AndroidOptionRow = styled(View)({
|
|
78
|
+
flexDirection: "row",
|
|
79
|
+
alignItems: "center",
|
|
80
|
+
paddingVertical: parseSize(dimensions.spacing.md),
|
|
81
|
+
paddingHorizontal: parseSize(dimensions.spacing.sm),
|
|
82
|
+
borderBottomWidth: parseSize(dimensions.borderWidth.sm),
|
|
83
|
+
borderBottomColor: themeTokens.primitives.colour.brand.brown[2]
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const RadioOuter = styled(View)({
|
|
87
|
+
width: parseSize(dimensions.spacing.lg),
|
|
88
|
+
height: parseSize(dimensions.spacing.lg),
|
|
89
|
+
borderRadius: parseSize(dimensions.spacing.lg) / 2,
|
|
90
|
+
borderWidth: parseSize(dimensions.borderWidth.md),
|
|
91
|
+
borderColor: inputTokens.colour.field.border.selected,
|
|
92
|
+
alignItems: "center",
|
|
93
|
+
justifyContent: "center",
|
|
94
|
+
marginRight: parseSize(dimensions.spacing.sm)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const RadioInner = styled(View)({
|
|
98
|
+
width: parseSize(dimensions.spacing.md) / 2,
|
|
99
|
+
height: parseSize(dimensions.spacing.md) / 2,
|
|
100
|
+
borderRadius: parseSize(dimensions.spacing.md) / 4,
|
|
101
|
+
backgroundColor: inputTokens.colour.field.border.selected
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const AndroidOptionText = styled(Text)<AndroidOptionTextProps>(
|
|
105
|
+
({ isSelected }) => ({
|
|
106
|
+
fontSize: parseSize(inputTokens.field.placeholder.default.fontSize),
|
|
107
|
+
fontWeight: isSelected
|
|
108
|
+
? inputTokens.text.label.fontWeight
|
|
109
|
+
: inputTokens.field.placeholder.default.fontWeight,
|
|
110
|
+
color: inputTokens.colour.field.text.default,
|
|
111
|
+
flex: 1
|
|
112
|
+
})
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
export {
|
|
116
|
+
Wrapper,
|
|
117
|
+
SelectIcon,
|
|
118
|
+
InputText,
|
|
119
|
+
PlaceholderText,
|
|
120
|
+
ModalOverlay,
|
|
121
|
+
AndroidDialog,
|
|
122
|
+
DialogTitle,
|
|
123
|
+
AndroidOptionRow,
|
|
124
|
+
RadioOuter,
|
|
125
|
+
RadioInner,
|
|
126
|
+
AndroidOptionText
|
|
127
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from "react"
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
ActionSheetIOS,
|
|
5
|
+
Modal,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
FlatList,
|
|
8
|
+
TouchableWithoutFeedback,
|
|
9
|
+
View,
|
|
10
|
+
ViewProps
|
|
11
|
+
} from "react-native"
|
|
12
|
+
import { useSharedValue, useAnimatedStyle } from "react-native-reanimated"
|
|
13
|
+
import { KeyboardArrowDown } from "@butternutbox/pawprint-icons/core"
|
|
14
|
+
import { DEFAULT_THEME_OPTIONS } from "@butternutbox/pawprint-tokens"
|
|
15
|
+
|
|
16
|
+
import * as S from "./NativeSelectPicker.styled"
|
|
17
|
+
|
|
18
|
+
const themeTokens = DEFAULT_THEME_OPTIONS.tokens
|
|
19
|
+
const { components, semantics } = themeTokens
|
|
20
|
+
const inputTokens = components.inputs
|
|
21
|
+
const { dimensions } = semantics
|
|
22
|
+
|
|
23
|
+
const parseSize = (value: string): number => {
|
|
24
|
+
return parseInt(value.replace("px", ""), 10)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type NativeSelectPickerItem = {
|
|
28
|
+
label: string
|
|
29
|
+
value: string | number
|
|
30
|
+
key?: string | number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type NativeSelectPickerProps = ViewProps & {
|
|
34
|
+
items: NativeSelectPickerItem[]
|
|
35
|
+
value?: string | number | null
|
|
36
|
+
placeholder?: {
|
|
37
|
+
label?: string
|
|
38
|
+
value?: string | number
|
|
39
|
+
}
|
|
40
|
+
onValueChange: (value: string | number) => void
|
|
41
|
+
disabled?: boolean
|
|
42
|
+
hideIcon?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type IconAnimation = {
|
|
46
|
+
iconAnimationStyles: {
|
|
47
|
+
transform: Array<{ rotate: string }>
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const useIconAnimation = (): IconAnimation => {
|
|
52
|
+
const iconRotation = useSharedValue(0)
|
|
53
|
+
|
|
54
|
+
const iconAnimationStyles = useAnimatedStyle(() => {
|
|
55
|
+
"worklet"
|
|
56
|
+
return {
|
|
57
|
+
transform: [{ rotate: `${iconRotation.value}deg` }]
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
iconAnimationStyles
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* NativeSelectPicker is a native UI dropdown component that uses:
|
|
68
|
+
* - ActionSheetIOS on iOS (native system picker)
|
|
69
|
+
* - Modal with FlatList on Android
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```tsx
|
|
73
|
+
* <NativeSelectPicker
|
|
74
|
+
* items={[
|
|
75
|
+
* { label: "Option 1", value: "opt1" },
|
|
76
|
+
* { label: "Option 2", value: "opt2" }
|
|
77
|
+
* ]}
|
|
78
|
+
* placeholder={{ label: "Select an option" }}
|
|
79
|
+
* value={selectedValue}
|
|
80
|
+
* onValueChange={setSelectedValue}
|
|
81
|
+
* />
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
const NativeSelectPicker = React.forwardRef<View, NativeSelectPickerProps>(
|
|
85
|
+
(
|
|
86
|
+
{
|
|
87
|
+
items,
|
|
88
|
+
value,
|
|
89
|
+
placeholder,
|
|
90
|
+
onValueChange,
|
|
91
|
+
disabled = false,
|
|
92
|
+
hideIcon = false,
|
|
93
|
+
...rest
|
|
94
|
+
},
|
|
95
|
+
ref
|
|
96
|
+
) => {
|
|
97
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
98
|
+
const { iconAnimationStyles } = useIconAnimation()
|
|
99
|
+
|
|
100
|
+
const hasPlaceholder =
|
|
101
|
+
placeholder &&
|
|
102
|
+
Object.keys(placeholder).length > 0 &&
|
|
103
|
+
placeholder.label !== undefined
|
|
104
|
+
|
|
105
|
+
const allItems = useMemo(() => {
|
|
106
|
+
if (hasPlaceholder) {
|
|
107
|
+
return [
|
|
108
|
+
{
|
|
109
|
+
label: placeholder.label!,
|
|
110
|
+
value: placeholder.value ?? "",
|
|
111
|
+
key: "__placeholder__"
|
|
112
|
+
},
|
|
113
|
+
...items
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
return items
|
|
117
|
+
}, [items, hasPlaceholder, placeholder])
|
|
118
|
+
|
|
119
|
+
const selectedItem = allItems.find((item) => item.value === value)
|
|
120
|
+
const displayLabel =
|
|
121
|
+
selectedItem?.label ?? (hasPlaceholder ? placeholder.label! : "")
|
|
122
|
+
|
|
123
|
+
const handleOpenIOS = useCallback(() => {
|
|
124
|
+
if (disabled) return
|
|
125
|
+
|
|
126
|
+
const options = [...allItems.map((item) => item.label), "Cancel"]
|
|
127
|
+
const cancelButtonIndex = options.length - 1
|
|
128
|
+
|
|
129
|
+
ActionSheetIOS.showActionSheetWithOptions(
|
|
130
|
+
{
|
|
131
|
+
options,
|
|
132
|
+
cancelButtonIndex
|
|
133
|
+
},
|
|
134
|
+
(buttonIndex) => {
|
|
135
|
+
if (buttonIndex !== cancelButtonIndex) {
|
|
136
|
+
onValueChange(allItems[buttonIndex].value)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
}, [disabled, allItems, onValueChange])
|
|
141
|
+
|
|
142
|
+
const handleOpenAndroid = useCallback(() => {
|
|
143
|
+
if (!disabled) {
|
|
144
|
+
setIsOpen(true)
|
|
145
|
+
}
|
|
146
|
+
}, [disabled])
|
|
147
|
+
|
|
148
|
+
const handleClose = useCallback(() => {
|
|
149
|
+
setIsOpen(false)
|
|
150
|
+
}, [])
|
|
151
|
+
|
|
152
|
+
const handleSelect = useCallback(
|
|
153
|
+
(itemValue: string | number) => {
|
|
154
|
+
onValueChange(itemValue)
|
|
155
|
+
setIsOpen(false)
|
|
156
|
+
},
|
|
157
|
+
[onValueChange]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<S.Wrapper ref={ref} {...rest}>
|
|
162
|
+
<TouchableOpacity
|
|
163
|
+
activeOpacity={0.8}
|
|
164
|
+
onPress={Platform.OS === "ios" ? handleOpenIOS : handleOpenAndroid}
|
|
165
|
+
disabled={disabled}
|
|
166
|
+
hitSlop={{
|
|
167
|
+
top: parseSize(dimensions.spacing.md),
|
|
168
|
+
right: parseSize(dimensions.spacing.md),
|
|
169
|
+
bottom: parseSize(dimensions.spacing.md),
|
|
170
|
+
left: parseSize(dimensions.spacing.md)
|
|
171
|
+
}}
|
|
172
|
+
accessibilityRole="button"
|
|
173
|
+
style={{ flexDirection: "row", alignItems: "center" }}
|
|
174
|
+
>
|
|
175
|
+
<S.InputText numberOfLines={1}>
|
|
176
|
+
{displayLabel || (
|
|
177
|
+
<S.PlaceholderText>
|
|
178
|
+
{placeholder?.label || "Select"}
|
|
179
|
+
</S.PlaceholderText>
|
|
180
|
+
)}
|
|
181
|
+
</S.InputText>
|
|
182
|
+
{!hideIcon && (
|
|
183
|
+
<S.SelectIcon style={[iconAnimationStyles]}>
|
|
184
|
+
<KeyboardArrowDown
|
|
185
|
+
width={parseSize(dimensions.spacing.xl)}
|
|
186
|
+
height={parseSize(dimensions.spacing.xl)}
|
|
187
|
+
color={inputTokens.colour.field.text.default}
|
|
188
|
+
/>
|
|
189
|
+
</S.SelectIcon>
|
|
190
|
+
)}
|
|
191
|
+
</TouchableOpacity>
|
|
192
|
+
|
|
193
|
+
{Platform.OS === "android" && (
|
|
194
|
+
<Modal
|
|
195
|
+
visible={isOpen}
|
|
196
|
+
transparent
|
|
197
|
+
animationType="fade"
|
|
198
|
+
onRequestClose={handleClose}
|
|
199
|
+
>
|
|
200
|
+
<TouchableWithoutFeedback
|
|
201
|
+
accessibilityRole="button"
|
|
202
|
+
onPress={handleClose}
|
|
203
|
+
>
|
|
204
|
+
<S.ModalOverlay>
|
|
205
|
+
<TouchableWithoutFeedback>
|
|
206
|
+
<S.AndroidDialog>
|
|
207
|
+
<S.DialogTitle>{"Select an option"}</S.DialogTitle>
|
|
208
|
+
<FlatList
|
|
209
|
+
data={allItems}
|
|
210
|
+
keyExtractor={(item, index) =>
|
|
211
|
+
item.key?.toString() ?? `${item.value}-${index}`
|
|
212
|
+
}
|
|
213
|
+
renderItem={({ item }) => {
|
|
214
|
+
const isSelected = item.value === value
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<TouchableOpacity
|
|
218
|
+
onPress={() => handleSelect(item.value)}
|
|
219
|
+
activeOpacity={0.7}
|
|
220
|
+
accessibilityRole="button"
|
|
221
|
+
>
|
|
222
|
+
<S.AndroidOptionRow>
|
|
223
|
+
<S.RadioOuter>
|
|
224
|
+
{isSelected && <S.RadioInner />}
|
|
225
|
+
</S.RadioOuter>
|
|
226
|
+
<S.AndroidOptionText isSelected={isSelected}>
|
|
227
|
+
{item.label}
|
|
228
|
+
</S.AndroidOptionText>
|
|
229
|
+
</S.AndroidOptionRow>
|
|
230
|
+
</TouchableOpacity>
|
|
231
|
+
)
|
|
232
|
+
}}
|
|
233
|
+
style={{
|
|
234
|
+
maxHeight: parseSize(dimensions.spacing["8xl"])
|
|
235
|
+
}}
|
|
236
|
+
/>
|
|
237
|
+
</S.AndroidDialog>
|
|
238
|
+
</TouchableWithoutFeedback>
|
|
239
|
+
</S.ModalOverlay>
|
|
240
|
+
</TouchableWithoutFeedback>
|
|
241
|
+
</Modal>
|
|
242
|
+
)}
|
|
243
|
+
</S.Wrapper>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
NativeSelectPicker.displayName = "NativeSelectPicker"
|
|
249
|
+
|
|
250
|
+
export { NativeSelectPicker }
|
|
@@ -29,7 +29,7 @@ type InlineVariantProps = BaseProps & {
|
|
|
29
29
|
title?: string
|
|
30
30
|
onClose?: () => void
|
|
31
31
|
size?: never
|
|
32
|
-
link?:
|
|
32
|
+
link?: { label: string; href?: string; onPress?: () => void }
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
type ToastVariantProps = BaseProps & {
|
|
@@ -45,7 +45,7 @@ type SystemVariantProps = BaseProps & {
|
|
|
45
45
|
size?: NotificationSize
|
|
46
46
|
title?: never
|
|
47
47
|
onClose?: never
|
|
48
|
-
link?:
|
|
48
|
+
link?: { label: string; href?: string; onPress?: () => void }
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
type NotificationOwnProps =
|
|
@@ -207,7 +207,7 @@ const StyledCloseButton = styled(Pressable)({
|
|
|
207
207
|
* variants — inline (within content flow), toast (floating overlay), and
|
|
208
208
|
* system (full-width banner) — each with four severity types.
|
|
209
209
|
*
|
|
210
|
-
* Note: Unlike the web version,
|
|
210
|
+
* Note: Unlike the web version, links use `onPress` callback instead
|
|
211
211
|
* of `href` for navigation. The `href` prop is also available and opens
|
|
212
212
|
* the URL via `Linking`.
|
|
213
213
|
*
|
|
@@ -216,7 +216,7 @@ const StyledCloseButton = styled(Pressable)({
|
|
|
216
216
|
* @param {boolean} [showIcon=true] - Whether to show the status icon.
|
|
217
217
|
* @param {string} [title] - Optional headline (inline variant only).
|
|
218
218
|
* @param {() => void} [onClose] - Close callback (inline and toast variants).
|
|
219
|
-
* @param {{ label: string; href?: string; onPress?: () => void }} [link] - Optional action link (
|
|
219
|
+
* @param {{ label: string; href?: string; onPress?: () => void }} [link] - Optional action link (all variants).
|
|
220
220
|
* @param {"sm" | "lg"} [size="sm"] - Size variant (system variant only).
|
|
221
221
|
* @param {React.ReactNode} children - The notification body text.
|
|
222
222
|
*
|
|
@@ -286,12 +286,24 @@ export const Notification = React.forwardRef<View, NotificationProps>(
|
|
|
286
286
|
aria-label={type}
|
|
287
287
|
/>
|
|
288
288
|
)}
|
|
289
|
-
<
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
289
|
+
<View style={{ flexDirection: "column", gap: 8, flex: 1 }}>
|
|
290
|
+
<Typography
|
|
291
|
+
token={systemNotifications.notifications.typography.default}
|
|
292
|
+
color={systemNotifications.notification.colour.text.default}
|
|
293
|
+
>
|
|
294
|
+
{children}
|
|
295
|
+
</Typography>
|
|
296
|
+
{link && (
|
|
297
|
+
<Link
|
|
298
|
+
href={link.href}
|
|
299
|
+
onPress={link.onPress}
|
|
300
|
+
weight="semiBold"
|
|
301
|
+
size="md"
|
|
302
|
+
>
|
|
303
|
+
{link.label}
|
|
304
|
+
</Link>
|
|
305
|
+
)}
|
|
306
|
+
</View>
|
|
295
307
|
</StyledSystemRoot>
|
|
296
308
|
)
|
|
297
309
|
}
|
|
@@ -408,6 +420,20 @@ export const Notification = React.forwardRef<View, NotificationProps>(
|
|
|
408
420
|
</Typography>
|
|
409
421
|
</StyledInlineCopy>
|
|
410
422
|
</StyledCopyRow>
|
|
423
|
+
{link && (
|
|
424
|
+
<StyledLinkWrapper
|
|
425
|
+
linkPaddingLeft={parseTokenValue(content.copy.spacing.gap)}
|
|
426
|
+
>
|
|
427
|
+
<Link
|
|
428
|
+
href={link.href}
|
|
429
|
+
onPress={link.onPress}
|
|
430
|
+
weight="semiBold"
|
|
431
|
+
size="md"
|
|
432
|
+
>
|
|
433
|
+
{link.label}
|
|
434
|
+
</Link>
|
|
435
|
+
</StyledLinkWrapper>
|
|
436
|
+
)}
|
|
411
437
|
</StyledContents>
|
|
412
438
|
{onClose && (
|
|
413
439
|
<StyledCloseButton
|
|
@@ -3,6 +3,7 @@ import { View, StyleSheet } from "react-native"
|
|
|
3
3
|
import { RadioGroup } from "./RadioGroup"
|
|
4
4
|
import type { RadioGroupProps } from "./RadioGroup"
|
|
5
5
|
import { Typography } from "../../atoms/Typography"
|
|
6
|
+
import { StarRate } from "@butternutbox/pawprint-icons/core"
|
|
6
7
|
|
|
7
8
|
export default {
|
|
8
9
|
title: "Molecules/Radio",
|
|
@@ -95,6 +96,51 @@ export const TileVariant = () => (
|
|
|
95
96
|
</View>
|
|
96
97
|
)
|
|
97
98
|
|
|
99
|
+
export const WithTag = () => (
|
|
100
|
+
<View style={styles.column}>
|
|
101
|
+
<View style={styles.section}>
|
|
102
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
103
|
+
Tile with tag
|
|
104
|
+
</Typography>
|
|
105
|
+
<RadioGroup name="tag-tile" defaultValue="1">
|
|
106
|
+
<RadioGroup.Radio
|
|
107
|
+
variant="tile"
|
|
108
|
+
value="1"
|
|
109
|
+
label="1 pouch over 2 days"
|
|
110
|
+
subText="400g pouches"
|
|
111
|
+
tag={{ children: "Save up to £12.20 per box" }}
|
|
112
|
+
/>
|
|
113
|
+
<RadioGroup.Radio
|
|
114
|
+
variant="tile"
|
|
115
|
+
value="2"
|
|
116
|
+
label="1 pouch per day"
|
|
117
|
+
subText="200g pouches"
|
|
118
|
+
/>
|
|
119
|
+
</RadioGroup>
|
|
120
|
+
</View>
|
|
121
|
+
|
|
122
|
+
<View style={styles.section}>
|
|
123
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
124
|
+
Tag variants & icon
|
|
125
|
+
</Typography>
|
|
126
|
+
<RadioGroup name="tag-variants" defaultValue="1">
|
|
127
|
+
<RadioGroup.Radio
|
|
128
|
+
value="1"
|
|
129
|
+
label="Most popular"
|
|
130
|
+
subText="Recommended for most dogs"
|
|
131
|
+
tag={{ children: "Best value", variant: "promo", icon: StarRate }}
|
|
132
|
+
/>
|
|
133
|
+
<RadioGroup.Radio
|
|
134
|
+
value="2"
|
|
135
|
+
label="Flexible plan"
|
|
136
|
+
subText="Change anytime"
|
|
137
|
+
tag={{ children: "New", variant: "success" }}
|
|
138
|
+
/>
|
|
139
|
+
</RadioGroup>
|
|
140
|
+
</View>
|
|
141
|
+
</View>
|
|
142
|
+
)
|
|
143
|
+
|
|
98
144
|
export const States = () => (
|
|
99
145
|
<View style={styles.column}>
|
|
100
146
|
<View style={styles.section}>
|