@butternutbox/pawprint-native 0.0.1 → 0.1.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 +16 -0
- package/COMPONENT_GUIDELINES.md +111 -4
- package/dist/index.cjs +12370 -1455
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1110 -11
- package/dist/index.d.ts +1110 -11
- package/dist/index.js +12324 -1455
- package/dist/index.js.map +1 -1
- package/package.json +28 -9
- package/src/__mocks__/asset-stub.ts +1 -0
- package/src/__mocks__/emotion-native.tsx +18 -0
- package/src/__mocks__/react-native-gesture-handler.tsx +41 -0
- package/src/__mocks__/react-native-reanimated.tsx +79 -0
- package/src/__mocks__/react-native-safe-area-context.tsx +6 -0
- package/src/__mocks__/react-native-svg.tsx +27 -0
- package/src/__mocks__/react-native-worklets.tsx +11 -0
- package/src/__mocks__/react-native.tsx +338 -0
- package/src/__mocks__/rn-primitives/avatar.tsx +24 -0
- package/src/__mocks__/rn-primitives/checkbox.tsx +19 -0
- package/src/__mocks__/rn-primitives/select.tsx +116 -0
- package/src/__mocks__/rn-primitives/slider.tsx +40 -0
- package/src/__mocks__/rn-primitives/slot.tsx +30 -0
- package/src/__mocks__/rn-primitives/switch.tsx +24 -0
- package/src/__mocks__/rn-primitives/toggle.tsx +16 -0
- package/src/components/atoms/Avatar/Avatar.stories.tsx +57 -49
- package/src/components/atoms/Avatar/Avatar.test.tsx +269 -0
- package/src/components/atoms/Avatar/Avatar.tsx +68 -22
- package/src/components/atoms/Avatar/index.ts +1 -6
- package/src/components/atoms/Badge/Badge.stories.tsx +5 -29
- package/src/components/atoms/Badge/Badge.test.tsx +90 -0
- package/src/components/atoms/Button/Button.test.tsx +123 -0
- package/src/components/atoms/Button/Button.tsx +1 -1
- package/src/components/atoms/CarouselControls/CarouselControls.stories.tsx +217 -0
- package/src/components/atoms/CarouselControls/CarouselControls.tsx +127 -0
- package/src/components/atoms/CarouselControls/index.ts +2 -0
- package/src/components/atoms/Hint/Hint.test.tsx +36 -0
- package/src/components/atoms/Icon/Icon.test.tsx +98 -0
- package/src/components/atoms/Icon/Icon.tsx +5 -1
- package/src/components/atoms/IconButton/IconButton.test.tsx +101 -0
- package/src/components/atoms/Illustration/Illustration.test.tsx +55 -0
- package/src/components/atoms/Input/Input.stories.tsx +129 -86
- package/src/components/atoms/Input/Input.test.tsx +306 -0
- package/src/components/atoms/Input/Input.tsx +9 -1
- package/src/components/atoms/Input/InputField.tsx +226 -74
- package/src/components/atoms/Link/Link.test.tsx +89 -0
- package/src/components/atoms/Logo/Logo.registry.ts +30 -5
- package/src/components/atoms/Logo/Logo.stories.tsx +108 -0
- package/src/components/atoms/Logo/Logo.test.tsx +56 -0
- package/src/components/atoms/Logo/assets/BCorp.tsx +113 -0
- package/src/components/atoms/Logo/assets/ButternutFavicon.tsx +33 -0
- package/src/components/atoms/Logo/assets/ButternutPrimary.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutTabbedBottom.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutTabbedTop.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutWordmark.tsx +274 -0
- package/src/components/atoms/Logo/assets/PsiBufetFavicon.tsx +45 -0
- package/src/components/atoms/Logo/assets/PsiBufetPrimary.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetTabbedBottom.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetTabbedTop.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetWordmark.tsx +195 -0
- package/src/components/atoms/Logo/assets/index.ts +11 -0
- package/src/components/atoms/NumberInput/NumberInput.stories.tsx +183 -0
- package/src/components/atoms/NumberInput/NumberInput.test.tsx +261 -0
- package/src/components/atoms/NumberInput/NumberInput.tsx +129 -0
- package/src/components/atoms/NumberInput/NumberInputField.tsx +77 -0
- package/src/components/atoms/NumberInput/index.ts +4 -0
- package/src/components/atoms/Spinner/Spinner.test.tsx +46 -0
- package/src/components/atoms/Spinner/Spinner.tsx +14 -5
- package/src/components/atoms/Switch/Switch.test.tsx +92 -0
- package/src/components/atoms/Switch/Switch.tsx +16 -13
- package/src/components/atoms/Tag/Tag.test.tsx +70 -0
- package/src/components/atoms/TextArea/TextArea.stories.tsx +303 -0
- package/src/components/atoms/TextArea/TextArea.test.tsx +416 -0
- package/src/components/atoms/TextArea/TextArea.tsx +171 -0
- package/src/components/atoms/TextArea/TextAreaField.tsx +304 -0
- package/src/components/atoms/TextArea/TextAreaLabel.tsx +103 -0
- package/src/components/atoms/TextArea/index.ts +6 -0
- package/src/components/atoms/Typography/Typography.test.tsx +94 -0
- package/src/components/atoms/index.ts +3 -0
- package/src/components/molecules/Accordion/Accordion.stories.tsx +177 -0
- package/src/components/molecules/Accordion/Accordion.test.tsx +185 -0
- package/src/components/molecules/Accordion/Accordion.tsx +284 -0
- package/src/components/molecules/Accordion/index.ts +6 -0
- package/src/components/molecules/Animated/Animated.stories.tsx +254 -0
- package/src/components/molecules/Animated/Animated.tsx +283 -0
- package/src/components/molecules/Animated/index.ts +10 -0
- package/src/components/molecules/ButtonDock/ButtonDock.test.tsx +83 -0
- package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +8 -14
- package/src/components/molecules/ButtonGroup/ButtonGroup.test.tsx +73 -0
- package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +25 -3
- package/src/components/molecules/Checkbox/Checkbox.stories.tsx +72 -0
- package/src/components/molecules/Checkbox/Checkbox.test.tsx +117 -0
- package/src/components/molecules/Checkbox/Checkbox.tsx +101 -95
- package/src/components/molecules/CopyField/CopyField.stories.tsx +313 -0
- package/src/components/molecules/CopyField/CopyField.test.tsx +431 -0
- package/src/components/molecules/CopyField/CopyField.tsx +156 -0
- package/src/components/molecules/CopyField/CopyFieldInput.tsx +127 -0
- package/src/components/molecules/CopyField/hooks/index.ts +1 -0
- package/src/components/molecules/CopyField/hooks/useCopyField.ts +25 -0
- package/src/components/molecules/CopyField/index.ts +4 -0
- package/src/components/molecules/DatePicker/DatePicker.stories.tsx +298 -0
- package/src/components/molecules/DatePicker/DatePicker.test.tsx +201 -0
- package/src/components/molecules/DatePicker/DatePicker.tsx +590 -0
- package/src/components/molecules/DatePicker/index.ts +2 -0
- package/src/components/molecules/Drawer/Drawer.stories.tsx +285 -0
- package/src/components/molecules/Drawer/Drawer.test.tsx +180 -0
- package/src/components/molecules/Drawer/Drawer.tsx +187 -0
- package/src/components/molecules/Drawer/DrawerBody.tsx +80 -0
- package/src/components/molecules/Drawer/DrawerClose.tsx +76 -0
- package/src/components/molecules/Drawer/DrawerContent.tsx +339 -0
- package/src/components/molecules/Drawer/DrawerContext.ts +19 -0
- package/src/components/molecules/Drawer/DrawerDescription.tsx +31 -0
- package/src/components/molecules/Drawer/DrawerDragContext.ts +11 -0
- package/src/components/molecules/Drawer/DrawerFooter.tsx +49 -0
- package/src/components/molecules/Drawer/DrawerFooterContext.ts +6 -0
- package/src/components/molecules/Drawer/DrawerGrabber.tsx +62 -0
- package/src/components/molecules/Drawer/DrawerHeader.tsx +244 -0
- package/src/components/molecules/Drawer/DrawerHeaderContext.ts +13 -0
- package/src/components/molecules/Drawer/DrawerOverlay.tsx +53 -0
- package/src/components/molecules/Drawer/DrawerTitle.tsx +32 -0
- package/src/components/molecules/Drawer/index.ts +12 -0
- package/src/components/molecules/FilterTab/FilterTab.stories.tsx +210 -0
- package/src/components/molecules/FilterTab/FilterTab.tsx +310 -0
- package/src/components/molecules/FilterTab/index.ts +2 -0
- package/src/components/molecules/MessageCard/MessageCard.stories.tsx +169 -0
- package/src/components/molecules/MessageCard/MessageCard.tsx +362 -0
- package/src/components/molecules/MessageCard/index.ts +10 -0
- package/src/components/molecules/Notification/Notification.stories.tsx +219 -0
- package/src/components/molecules/Notification/Notification.tsx +426 -0
- package/src/components/molecules/Notification/index.ts +2 -0
- package/src/components/molecules/NumberField/NumberField.stories.tsx +231 -0
- package/src/components/molecules/NumberField/NumberField.tsx +186 -0
- package/src/components/molecules/NumberField/NumberFieldInput.tsx +287 -0
- package/src/components/molecules/NumberField/index.ts +2 -0
- package/src/components/molecules/PasswordField/PasswordField.stories.tsx +362 -0
- package/src/components/molecules/PasswordField/PasswordField.test.tsx +369 -0
- package/src/components/molecules/PasswordField/PasswordField.tsx +194 -0
- package/src/components/molecules/PasswordField/PasswordFieldError.tsx +52 -0
- package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
- package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +92 -0
- package/src/components/molecules/PasswordField/hooks/index.ts +2 -0
- package/src/components/molecules/PasswordField/hooks/usePasswordField.ts +113 -0
- package/src/components/molecules/PasswordField/index.ts +10 -0
- package/src/components/molecules/PictureSelector/PictureSelector.stories.tsx +243 -0
- package/src/components/molecules/PictureSelector/PictureSelector.tsx +313 -0
- package/src/components/molecules/PictureSelector/index.ts +5 -0
- package/src/components/molecules/Progress/Progress.stories.tsx +145 -0
- package/src/components/molecules/Progress/Progress.tsx +184 -0
- package/src/components/molecules/Progress/index.ts +2 -0
- package/src/components/molecules/Radio/Radio.test.tsx +104 -0
- package/src/components/molecules/Radio/Radio.tsx +1 -2
- package/src/components/molecules/SearchField/SearchField.stories.tsx +242 -0
- package/src/components/molecules/SearchField/SearchField.test.tsx +318 -0
- package/src/components/molecules/SearchField/SearchField.tsx +143 -0
- package/src/components/molecules/SearchField/SearchFieldInput.tsx +63 -0
- package/src/components/molecules/SearchField/hooks/index.ts +1 -0
- package/src/components/molecules/SearchField/hooks/useSearchField.ts +56 -0
- package/src/components/molecules/SearchField/index.ts +4 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +31 -8
- package/src/components/molecules/SegmentedControl/SegmentedControl.test.tsx +141 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +237 -23
- package/src/components/molecules/SelectField/SelectField.stories.tsx +320 -0
- package/src/components/molecules/SelectField/SelectField.test.tsx +254 -0
- package/src/components/molecules/SelectField/SelectField.tsx +236 -0
- package/src/components/molecules/SelectField/SelectFieldContent.tsx +85 -0
- package/src/components/molecules/SelectField/SelectFieldItem.tsx +133 -0
- package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +170 -0
- package/src/components/molecules/SelectField/SelectFieldValue.tsx +31 -0
- package/src/components/molecules/SelectField/hooks/index.ts +2 -0
- package/src/components/molecules/SelectField/hooks/useSelectField.ts +84 -0
- package/src/components/molecules/SelectField/index.ts +10 -0
- package/src/components/molecules/Slider/Slider.test.tsx +102 -0
- package/src/components/molecules/Slider/Slider.tsx +293 -180
- package/src/components/molecules/Tooltip/Tooltip.stories.tsx +168 -0
- package/src/components/molecules/Tooltip/Tooltip.tsx +326 -0
- package/src/components/molecules/Tooltip/index.ts +2 -0
- package/src/components/molecules/index.ts +15 -0
- package/src/test-utils.tsx +20 -0
- package/tsconfig.json +1 -1
- package/tsup.config.ts +16 -2
- package/vitest.config.ts +114 -0
- package/vitest.setup.ts +16 -0
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
import React from "react"
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Pressable,
|
|
5
|
+
ScrollView,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
ViewProps,
|
|
8
|
+
NativeScrollEvent,
|
|
9
|
+
NativeSyntheticEvent
|
|
10
|
+
} from "react-native"
|
|
11
|
+
import Svg, {
|
|
12
|
+
Defs,
|
|
13
|
+
LinearGradient as SvgLinearGradient,
|
|
14
|
+
Rect,
|
|
15
|
+
Stop
|
|
16
|
+
} from "react-native-svg"
|
|
3
17
|
import styled from "@emotion/native"
|
|
4
18
|
import { useTheme } from "@emotion/react"
|
|
19
|
+
import {
|
|
20
|
+
KeyboardArrowLeft,
|
|
21
|
+
KeyboardArrowRight
|
|
22
|
+
} from "@butternutbox/pawprint-icons/core"
|
|
23
|
+
import { Icon } from "../../atoms/Icon"
|
|
5
24
|
import { Typography } from "../../atoms/Typography"
|
|
6
25
|
|
|
7
26
|
type SegmentedControlLayout = "fixed" | "intrinsic"
|
|
@@ -38,7 +57,6 @@ const parseTokenValue = (value: string): number => parseFloat(value)
|
|
|
38
57
|
const StyledSegmentItem = styled(Pressable)<{
|
|
39
58
|
itemHeight: number
|
|
40
59
|
itemMinWidth: number
|
|
41
|
-
itemPaddingVertical: number
|
|
42
60
|
itemPaddingHorizontal: number
|
|
43
61
|
itemBorderRadius: number
|
|
44
62
|
itemBgColor: string
|
|
@@ -48,7 +66,6 @@ const StyledSegmentItem = styled(Pressable)<{
|
|
|
48
66
|
({
|
|
49
67
|
itemHeight,
|
|
50
68
|
itemMinWidth,
|
|
51
|
-
itemPaddingVertical,
|
|
52
69
|
itemPaddingHorizontal,
|
|
53
70
|
itemBorderRadius,
|
|
54
71
|
itemBgColor,
|
|
@@ -57,7 +74,6 @@ const StyledSegmentItem = styled(Pressable)<{
|
|
|
57
74
|
}) => ({
|
|
58
75
|
height: itemHeight,
|
|
59
76
|
minWidth: itemMinWidth,
|
|
60
|
-
paddingVertical: itemPaddingVertical,
|
|
61
77
|
paddingHorizontal: itemPaddingHorizontal,
|
|
62
78
|
borderRadius: itemBorderRadius,
|
|
63
79
|
alignItems: "center",
|
|
@@ -68,6 +84,91 @@ const StyledSegmentItem = styled(Pressable)<{
|
|
|
68
84
|
})
|
|
69
85
|
)
|
|
70
86
|
|
|
87
|
+
const StyledIntrinsicClip = styled(View)<{
|
|
88
|
+
clipBorderRadius: number
|
|
89
|
+
clipBgColor: string
|
|
90
|
+
clipWidth?: number
|
|
91
|
+
}>(({ clipBorderRadius, clipBgColor, clipWidth }) => ({
|
|
92
|
+
overflow: "hidden",
|
|
93
|
+
borderRadius: clipBorderRadius,
|
|
94
|
+
backgroundColor: clipBgColor,
|
|
95
|
+
alignSelf: "flex-start",
|
|
96
|
+
maxWidth: "100%",
|
|
97
|
+
...(clipWidth !== undefined ? { width: clipWidth } : {})
|
|
98
|
+
}))
|
|
99
|
+
|
|
100
|
+
const StyledScrollIndicator = styled(Pressable)<{
|
|
101
|
+
indicatorSide: "leading" | "trailing"
|
|
102
|
+
indicatorBorderRadius: number
|
|
103
|
+
indicatorPaddingOuter: number
|
|
104
|
+
indicatorPaddingInner: number
|
|
105
|
+
}>(
|
|
106
|
+
({
|
|
107
|
+
indicatorSide,
|
|
108
|
+
indicatorBorderRadius,
|
|
109
|
+
indicatorPaddingOuter,
|
|
110
|
+
indicatorPaddingInner
|
|
111
|
+
}) => ({
|
|
112
|
+
position: "absolute",
|
|
113
|
+
top: 0,
|
|
114
|
+
bottom: 0,
|
|
115
|
+
zIndex: 2,
|
|
116
|
+
alignItems: "center",
|
|
117
|
+
justifyContent: "center",
|
|
118
|
+
overflow: "hidden",
|
|
119
|
+
...(indicatorSide === "trailing"
|
|
120
|
+
? {
|
|
121
|
+
right: 0,
|
|
122
|
+
paddingLeft: indicatorPaddingOuter,
|
|
123
|
+
paddingRight: indicatorPaddingInner,
|
|
124
|
+
borderTopRightRadius: indicatorBorderRadius,
|
|
125
|
+
borderBottomRightRadius: indicatorBorderRadius
|
|
126
|
+
}
|
|
127
|
+
: {
|
|
128
|
+
left: 0,
|
|
129
|
+
paddingRight: indicatorPaddingOuter,
|
|
130
|
+
paddingLeft: indicatorPaddingInner,
|
|
131
|
+
borderTopLeftRadius: indicatorBorderRadius,
|
|
132
|
+
borderBottomLeftRadius: indicatorBorderRadius
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const ScrollIndicatorGradient = ({
|
|
138
|
+
side,
|
|
139
|
+
color
|
|
140
|
+
}: {
|
|
141
|
+
side: "leading" | "trailing"
|
|
142
|
+
color: string
|
|
143
|
+
}) => {
|
|
144
|
+
const uid = React.useId()
|
|
145
|
+
const gradientId = `sc-scroll-indicator-${side}-${uid}`
|
|
146
|
+
const isTrailing = side === "trailing"
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<View style={StyleSheet.absoluteFill} pointerEvents="none">
|
|
150
|
+
<Svg width="100%" height="100%" preserveAspectRatio="none">
|
|
151
|
+
<Defs>
|
|
152
|
+
<SvgLinearGradient
|
|
153
|
+
id={gradientId}
|
|
154
|
+
x1={isTrailing ? "0" : "1"}
|
|
155
|
+
y1="0"
|
|
156
|
+
x2={isTrailing ? "1" : "0"}
|
|
157
|
+
y2="0"
|
|
158
|
+
>
|
|
159
|
+
<Stop offset="0" stopColor={color} stopOpacity="0" />
|
|
160
|
+
<Stop offset="0.25" stopColor={color} stopOpacity="0.5" />
|
|
161
|
+
<Stop offset="0.5" stopColor={color} stopOpacity="0.85" />
|
|
162
|
+
<Stop offset="0.75" stopColor={color} stopOpacity="1" />
|
|
163
|
+
<Stop offset="1" stopColor={color} stopOpacity="1" />
|
|
164
|
+
</SvgLinearGradient>
|
|
165
|
+
</Defs>
|
|
166
|
+
<Rect width="100%" height="100%" fill={`url(#${gradientId})`} />
|
|
167
|
+
</Svg>
|
|
168
|
+
</View>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
71
172
|
const StyledContainer = styled(View)<{
|
|
72
173
|
containerGap: number
|
|
73
174
|
containerPaddingVertical: number
|
|
@@ -118,6 +219,10 @@ const SegmentedControlItemBase = ({
|
|
|
118
219
|
const { controlItem } = theme.tokens.components.segmentedControl
|
|
119
220
|
const { size, spacing, borderRadius, colour, opacity } = controlItem
|
|
120
221
|
const { typography } = controlItem
|
|
222
|
+
// Note: verticalPadding is intentionally skipped on native — the item has a
|
|
223
|
+
// fixed `height` and `alignItems: "center"`, which already handles vertical
|
|
224
|
+
// centring. Applying paddingVertical on top would clip text on RN, where
|
|
225
|
+
// height is strictly bounded (unlike CSS flex on web).
|
|
121
226
|
|
|
122
227
|
const handlePress = () => {
|
|
123
228
|
if (disabled) return
|
|
@@ -142,7 +247,6 @@ const SegmentedControlItemBase = ({
|
|
|
142
247
|
accessibilityState={{ selected, disabled }}
|
|
143
248
|
itemHeight={parseTokenValue(size.small.height)}
|
|
144
249
|
itemMinWidth={parseTokenValue(size.small.minWidth)}
|
|
145
|
-
itemPaddingVertical={parseTokenValue(spacing.verticalPadding)}
|
|
146
250
|
itemPaddingHorizontal={parseTokenValue(spacing.horizontalPadding)}
|
|
147
251
|
itemBorderRadius={parseTokenValue(borderRadius.default)}
|
|
148
252
|
itemBgColor={selected ? colour.background.selected : "transparent"}
|
|
@@ -167,6 +271,124 @@ const SegmentedControlItem = React.forwardRef<View, SegmentedControlItemProps>(
|
|
|
167
271
|
|
|
168
272
|
SegmentedControlItem.displayName = "SegmentedControl.Item"
|
|
169
273
|
|
|
274
|
+
type IntrinsicTrackProps = ViewProps & {
|
|
275
|
+
containerProps: {
|
|
276
|
+
containerGap: number
|
|
277
|
+
containerPaddingVertical: number
|
|
278
|
+
containerPaddingHorizontal: number
|
|
279
|
+
containerBorderRadius: number
|
|
280
|
+
containerBgColor: string
|
|
281
|
+
containerFullWidth: boolean
|
|
282
|
+
scrollIndicatorPaddingOuter: number
|
|
283
|
+
scrollIndicatorPaddingInner: number
|
|
284
|
+
}
|
|
285
|
+
renderItems: () => React.ReactNode
|
|
286
|
+
forwardedRef: React.Ref<View>
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const IntrinsicTrack = ({
|
|
290
|
+
containerProps,
|
|
291
|
+
renderItems,
|
|
292
|
+
forwardedRef,
|
|
293
|
+
...rest
|
|
294
|
+
}: IntrinsicTrackProps) => {
|
|
295
|
+
const scrollRef = React.useRef<ScrollView>(null)
|
|
296
|
+
const [containerWidth, setContainerWidth] = React.useState(0)
|
|
297
|
+
const [contentWidth, setContentWidth] = React.useState(0)
|
|
298
|
+
const [scrollX, setScrollX] = React.useState(0)
|
|
299
|
+
|
|
300
|
+
const epsilon = 2
|
|
301
|
+
const maxScroll = Math.max(0, contentWidth - containerWidth)
|
|
302
|
+
const canScrollLeft = scrollX > epsilon
|
|
303
|
+
const canScrollRight = scrollX < maxScroll - epsilon
|
|
304
|
+
|
|
305
|
+
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
306
|
+
setScrollX(event.nativeEvent.contentOffset.x)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const scrollBy = (direction: "leading" | "trailing") => {
|
|
310
|
+
if (!scrollRef.current || containerWidth === 0) return
|
|
311
|
+
const step = containerWidth * 0.6
|
|
312
|
+
const target =
|
|
313
|
+
direction === "trailing"
|
|
314
|
+
? Math.min(maxScroll, scrollX + step)
|
|
315
|
+
: Math.max(0, scrollX - step)
|
|
316
|
+
scrollRef.current.scrollTo({ x: target, animated: true })
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<View
|
|
321
|
+
ref={forwardedRef}
|
|
322
|
+
onLayout={(event) => setContainerWidth(event.nativeEvent.layout.width)}
|
|
323
|
+
{...rest}
|
|
324
|
+
>
|
|
325
|
+
<StyledIntrinsicClip
|
|
326
|
+
clipBorderRadius={containerProps.containerBorderRadius}
|
|
327
|
+
clipBgColor={containerProps.containerBgColor}
|
|
328
|
+
clipWidth={
|
|
329
|
+
containerWidth > 0 && contentWidth > 0
|
|
330
|
+
? Math.min(contentWidth, containerWidth)
|
|
331
|
+
: undefined
|
|
332
|
+
}
|
|
333
|
+
>
|
|
334
|
+
<ScrollView
|
|
335
|
+
ref={scrollRef}
|
|
336
|
+
horizontal
|
|
337
|
+
showsHorizontalScrollIndicator={false}
|
|
338
|
+
onScroll={handleScroll}
|
|
339
|
+
scrollEventThrottle={16}
|
|
340
|
+
onContentSizeChange={(width) => setContentWidth(width)}
|
|
341
|
+
contentContainerStyle={{
|
|
342
|
+
flexDirection: "row",
|
|
343
|
+
alignItems: "stretch",
|
|
344
|
+
gap: containerProps.containerGap,
|
|
345
|
+
paddingVertical: containerProps.containerPaddingVertical,
|
|
346
|
+
paddingHorizontal: containerProps.containerPaddingHorizontal
|
|
347
|
+
}}
|
|
348
|
+
>
|
|
349
|
+
{renderItems()}
|
|
350
|
+
</ScrollView>
|
|
351
|
+
</StyledIntrinsicClip>
|
|
352
|
+
|
|
353
|
+
{canScrollLeft && (
|
|
354
|
+
<StyledScrollIndicator
|
|
355
|
+
indicatorSide="leading"
|
|
356
|
+
indicatorBorderRadius={containerProps.containerBorderRadius}
|
|
357
|
+
indicatorPaddingOuter={containerProps.scrollIndicatorPaddingOuter}
|
|
358
|
+
indicatorPaddingInner={containerProps.scrollIndicatorPaddingInner}
|
|
359
|
+
onPress={() => scrollBy("leading")}
|
|
360
|
+
accessibilityRole="button"
|
|
361
|
+
accessibilityLabel="Scroll left"
|
|
362
|
+
>
|
|
363
|
+
<ScrollIndicatorGradient
|
|
364
|
+
side="leading"
|
|
365
|
+
color={containerProps.containerBgColor}
|
|
366
|
+
/>
|
|
367
|
+
<Icon icon={KeyboardArrowLeft} size="lg" colour="action-inverse" />
|
|
368
|
+
</StyledScrollIndicator>
|
|
369
|
+
)}
|
|
370
|
+
|
|
371
|
+
{canScrollRight && (
|
|
372
|
+
<StyledScrollIndicator
|
|
373
|
+
indicatorSide="trailing"
|
|
374
|
+
indicatorBorderRadius={containerProps.containerBorderRadius}
|
|
375
|
+
indicatorPaddingOuter={containerProps.scrollIndicatorPaddingOuter}
|
|
376
|
+
indicatorPaddingInner={containerProps.scrollIndicatorPaddingInner}
|
|
377
|
+
onPress={() => scrollBy("trailing")}
|
|
378
|
+
accessibilityRole="button"
|
|
379
|
+
accessibilityLabel="Scroll right"
|
|
380
|
+
>
|
|
381
|
+
<ScrollIndicatorGradient
|
|
382
|
+
side="trailing"
|
|
383
|
+
color={containerProps.containerBgColor}
|
|
384
|
+
/>
|
|
385
|
+
<Icon icon={KeyboardArrowRight} size="lg" colour="action-inverse" />
|
|
386
|
+
</StyledScrollIndicator>
|
|
387
|
+
)}
|
|
388
|
+
</View>
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
|
|
170
392
|
/**
|
|
171
393
|
* Segmented control component for selecting exactly one option from a set.
|
|
172
394
|
*
|
|
@@ -206,6 +428,7 @@ const SegmentedControlRoot = React.forwardRef<View, SegmentedControlProps>(
|
|
|
206
428
|
const theme = useTheme()
|
|
207
429
|
const { spacing, borderRadius, colour } =
|
|
208
430
|
theme.tokens.components.segmentedControl
|
|
431
|
+
const { dimensions } = theme.tokens.semantics
|
|
209
432
|
|
|
210
433
|
const optionsFirstValue = options?.[0]?.value
|
|
211
434
|
const childrenArray = React.Children.toArray(children)
|
|
@@ -277,28 +500,19 @@ const SegmentedControlRoot = React.forwardRef<View, SegmentedControlProps>(
|
|
|
277
500
|
containerPaddingHorizontal: parseTokenValue(spacing.horizontalPadding),
|
|
278
501
|
containerBorderRadius: parseTokenValue(borderRadius.default),
|
|
279
502
|
containerBgColor: colour.background.default,
|
|
280
|
-
containerFullWidth: layout === "fixed"
|
|
503
|
+
containerFullWidth: layout === "fixed",
|
|
504
|
+
scrollIndicatorPaddingOuter: parseTokenValue(dimensions.spacing["2xl"]),
|
|
505
|
+
scrollIndicatorPaddingInner: parseTokenValue(dimensions.spacing.sm)
|
|
281
506
|
}
|
|
282
507
|
|
|
283
508
|
if (layout === "intrinsic") {
|
|
284
509
|
return (
|
|
285
|
-
<
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
alignItems: "stretch",
|
|
292
|
-
gap: containerProps.containerGap,
|
|
293
|
-
paddingVertical: containerProps.containerPaddingVertical,
|
|
294
|
-
paddingHorizontal: containerProps.containerPaddingHorizontal,
|
|
295
|
-
borderRadius: containerProps.containerBorderRadius,
|
|
296
|
-
backgroundColor: containerProps.containerBgColor
|
|
297
|
-
}}
|
|
298
|
-
>
|
|
299
|
-
{renderItems()}
|
|
300
|
-
</ScrollView>
|
|
301
|
-
</View>
|
|
510
|
+
<IntrinsicTrack
|
|
511
|
+
containerProps={containerProps}
|
|
512
|
+
renderItems={renderItems}
|
|
513
|
+
forwardedRef={ref}
|
|
514
|
+
{...rest}
|
|
515
|
+
/>
|
|
302
516
|
)
|
|
303
517
|
}
|
|
304
518
|
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import React, { useState } from "react"
|
|
2
|
+
import { View, StyleSheet } from "react-native"
|
|
3
|
+
import { PortalHost } from "@rn-primitives/portal"
|
|
4
|
+
import { SelectField, useSelectField } from "."
|
|
5
|
+
import { Typography } from "../../atoms/Typography"
|
|
6
|
+
import { Icon } from "../../atoms/Icon"
|
|
7
|
+
import { Search, Settings, Lock, Info } from "@butternutbox/pawprint-icons/core"
|
|
8
|
+
import type { Option } from "@rn-primitives/select"
|
|
9
|
+
|
|
10
|
+
const StoryWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
11
|
+
<>
|
|
12
|
+
{children}
|
|
13
|
+
<PortalHost />
|
|
14
|
+
</>
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
title: "Molecules/SelectField",
|
|
19
|
+
component: SelectField,
|
|
20
|
+
parameters: {
|
|
21
|
+
docs: {
|
|
22
|
+
description: {
|
|
23
|
+
component:
|
|
24
|
+
"Select field component for choosing a value from a dropdown list. Built on @rn-primitives/select with design system styling. Supports both simple props API and compound component API."
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
argTypes: {
|
|
29
|
+
label: {
|
|
30
|
+
control: { type: "text" },
|
|
31
|
+
description: "Label text"
|
|
32
|
+
},
|
|
33
|
+
placeholder: {
|
|
34
|
+
control: { type: "text" },
|
|
35
|
+
description: "Placeholder text"
|
|
36
|
+
},
|
|
37
|
+
description: {
|
|
38
|
+
control: { type: "text" },
|
|
39
|
+
description: "Help text below select"
|
|
40
|
+
},
|
|
41
|
+
error: {
|
|
42
|
+
control: { type: "text" },
|
|
43
|
+
description: "Error message"
|
|
44
|
+
},
|
|
45
|
+
state: {
|
|
46
|
+
control: { type: "select" },
|
|
47
|
+
options: ["default", "error", "success"],
|
|
48
|
+
description: "Visual state of the select"
|
|
49
|
+
},
|
|
50
|
+
optionalText: {
|
|
51
|
+
control: { type: "text" },
|
|
52
|
+
description: "Optional text to display next to label"
|
|
53
|
+
},
|
|
54
|
+
disabled: {
|
|
55
|
+
control: { type: "boolean" },
|
|
56
|
+
description: "Disables the select"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const Default = () => (
|
|
62
|
+
<StoryWrapper>
|
|
63
|
+
<View style={styles.container}>
|
|
64
|
+
<SelectField
|
|
65
|
+
label="Search Type"
|
|
66
|
+
placeholder="Select search type"
|
|
67
|
+
description="Choose how you want to search"
|
|
68
|
+
leadingIcon={<Icon icon={Search} size="md" />}
|
|
69
|
+
>
|
|
70
|
+
<SelectField.Item
|
|
71
|
+
value="products"
|
|
72
|
+
leadingIcon={<Icon icon={Search} size="md" />}
|
|
73
|
+
hintText="Search all products"
|
|
74
|
+
>
|
|
75
|
+
Products
|
|
76
|
+
</SelectField.Item>
|
|
77
|
+
<SelectField.Item
|
|
78
|
+
value="orders"
|
|
79
|
+
leadingIcon={<Icon icon={Settings} size="md" />}
|
|
80
|
+
hintText="Search order history"
|
|
81
|
+
>
|
|
82
|
+
Orders
|
|
83
|
+
</SelectField.Item>
|
|
84
|
+
<SelectField.Item
|
|
85
|
+
value="admin"
|
|
86
|
+
leadingIcon={<Icon icon={Lock} size="md" />}
|
|
87
|
+
hintText="Admin access required"
|
|
88
|
+
disabled
|
|
89
|
+
>
|
|
90
|
+
Admin Panel
|
|
91
|
+
</SelectField.Item>
|
|
92
|
+
<SelectField.Item
|
|
93
|
+
value="help"
|
|
94
|
+
leadingIcon={<Icon icon={Info} size="md" />}
|
|
95
|
+
>
|
|
96
|
+
Help & Support
|
|
97
|
+
</SelectField.Item>
|
|
98
|
+
</SelectField>
|
|
99
|
+
</View>
|
|
100
|
+
</StoryWrapper>
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
export const States = () => (
|
|
104
|
+
<StoryWrapper>
|
|
105
|
+
<View style={styles.column}>
|
|
106
|
+
<View style={styles.section}>
|
|
107
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
108
|
+
Default State
|
|
109
|
+
</Typography>
|
|
110
|
+
<SelectField
|
|
111
|
+
label="Default State"
|
|
112
|
+
placeholder="Select an option"
|
|
113
|
+
description="Normal select state"
|
|
114
|
+
>
|
|
115
|
+
<SelectField.Item value="option1">Option 1</SelectField.Item>
|
|
116
|
+
<SelectField.Item value="option2">Option 2</SelectField.Item>
|
|
117
|
+
<SelectField.Item value="option3">Option 3</SelectField.Item>
|
|
118
|
+
</SelectField>
|
|
119
|
+
</View>
|
|
120
|
+
|
|
121
|
+
<View style={styles.section}>
|
|
122
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
123
|
+
Error State
|
|
124
|
+
</Typography>
|
|
125
|
+
<SelectField
|
|
126
|
+
label="Error State"
|
|
127
|
+
placeholder="Select an option"
|
|
128
|
+
state="error"
|
|
129
|
+
error="This field is required"
|
|
130
|
+
description="Error state"
|
|
131
|
+
>
|
|
132
|
+
<SelectField.Item value="option1">Option 1</SelectField.Item>
|
|
133
|
+
<SelectField.Item value="option2">Option 2</SelectField.Item>
|
|
134
|
+
</SelectField>
|
|
135
|
+
</View>
|
|
136
|
+
|
|
137
|
+
<View style={styles.section}>
|
|
138
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
139
|
+
Success State
|
|
140
|
+
</Typography>
|
|
141
|
+
<SelectField
|
|
142
|
+
label="Success State"
|
|
143
|
+
placeholder="Select an option"
|
|
144
|
+
state="success"
|
|
145
|
+
description="Success state"
|
|
146
|
+
defaultValue="option1"
|
|
147
|
+
>
|
|
148
|
+
<SelectField.Item value="option1">Option 1</SelectField.Item>
|
|
149
|
+
<SelectField.Item value="option2">Option 2</SelectField.Item>
|
|
150
|
+
</SelectField>
|
|
151
|
+
</View>
|
|
152
|
+
|
|
153
|
+
<View style={styles.section}>
|
|
154
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
155
|
+
Disabled
|
|
156
|
+
</Typography>
|
|
157
|
+
<SelectField
|
|
158
|
+
label="Disabled"
|
|
159
|
+
placeholder="Select an option"
|
|
160
|
+
description="Disabled select field"
|
|
161
|
+
disabled
|
|
162
|
+
>
|
|
163
|
+
<SelectField.Item value="option1">Option 1</SelectField.Item>
|
|
164
|
+
<SelectField.Item value="option2">Option 2</SelectField.Item>
|
|
165
|
+
</SelectField>
|
|
166
|
+
</View>
|
|
167
|
+
</View>
|
|
168
|
+
</StoryWrapper>
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
export const WithOptionalText = () => (
|
|
172
|
+
<StoryWrapper>
|
|
173
|
+
<View style={styles.column}>
|
|
174
|
+
<View style={styles.section}>
|
|
175
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
176
|
+
With Optional Text
|
|
177
|
+
</Typography>
|
|
178
|
+
<SelectField
|
|
179
|
+
label="Country"
|
|
180
|
+
optionalText="(optional)"
|
|
181
|
+
placeholder="Select a country"
|
|
182
|
+
description="Optional select field"
|
|
183
|
+
>
|
|
184
|
+
<SelectField.Item value="uk">United Kingdom</SelectField.Item>
|
|
185
|
+
<SelectField.Item value="us">United States</SelectField.Item>
|
|
186
|
+
<SelectField.Item value="ca">Canada</SelectField.Item>
|
|
187
|
+
</SelectField>
|
|
188
|
+
</View>
|
|
189
|
+
</View>
|
|
190
|
+
</StoryWrapper>
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
export const CustomValidation = () => {
|
|
194
|
+
const [value, setValue] = useState<Option | string | null>(null)
|
|
195
|
+
|
|
196
|
+
// Extract actual value for validation
|
|
197
|
+
const actualValue =
|
|
198
|
+
value && typeof value === "object" && "value" in value ? value.value : value
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<StoryWrapper>
|
|
202
|
+
<View style={styles.column}>
|
|
203
|
+
<View style={styles.section}>
|
|
204
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
205
|
+
Custom Validation
|
|
206
|
+
</Typography>
|
|
207
|
+
<SelectField
|
|
208
|
+
label="Product Category"
|
|
209
|
+
placeholder="Select category"
|
|
210
|
+
value={value}
|
|
211
|
+
onValueChange={(newValue) => setValue(newValue)}
|
|
212
|
+
description="Select a valid category"
|
|
213
|
+
error={
|
|
214
|
+
actualValue === "invalid"
|
|
215
|
+
? "This option is not allowed"
|
|
216
|
+
: undefined
|
|
217
|
+
}
|
|
218
|
+
state={actualValue === "invalid" ? "error" : "default"}
|
|
219
|
+
>
|
|
220
|
+
<SelectField.Item value="food">Food</SelectField.Item>
|
|
221
|
+
<SelectField.Item value="toys">Toys</SelectField.Item>
|
|
222
|
+
<SelectField.Item value="invalid">Invalid Option</SelectField.Item>
|
|
223
|
+
</SelectField>
|
|
224
|
+
</View>
|
|
225
|
+
</View>
|
|
226
|
+
</StoryWrapper>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const CustomStateValidationWithSuccess = () => {
|
|
231
|
+
const selectProps = useSelectField({
|
|
232
|
+
validationRule: {
|
|
233
|
+
test: (value) => {
|
|
234
|
+
if (!value) return false
|
|
235
|
+
// Handle object with value property
|
|
236
|
+
const actualValue =
|
|
237
|
+
typeof value === "object" && value !== null && "value" in value
|
|
238
|
+
? (value as { value: string }).value
|
|
239
|
+
: value
|
|
240
|
+
return actualValue !== "invalid"
|
|
241
|
+
},
|
|
242
|
+
message: "Please select a valid option"
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<StoryWrapper>
|
|
248
|
+
<View style={styles.column}>
|
|
249
|
+
<View style={styles.section}>
|
|
250
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
251
|
+
Real-time Validation with Success State
|
|
252
|
+
</Typography>
|
|
253
|
+
<SelectField
|
|
254
|
+
{...selectProps}
|
|
255
|
+
label="Product Category"
|
|
256
|
+
placeholder="Select category"
|
|
257
|
+
description="Select a valid category (not 'Invalid Option')"
|
|
258
|
+
>
|
|
259
|
+
<SelectField.Item value="food">Food</SelectField.Item>
|
|
260
|
+
<SelectField.Item value="toys">Toys</SelectField.Item>
|
|
261
|
+
<SelectField.Item value="accessories">Accessories</SelectField.Item>
|
|
262
|
+
<SelectField.Item value="invalid">Invalid Option</SelectField.Item>
|
|
263
|
+
</SelectField>
|
|
264
|
+
</View>
|
|
265
|
+
</View>
|
|
266
|
+
</StoryWrapper>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export const Controlled = () => {
|
|
271
|
+
const selectProps = useSelectField()
|
|
272
|
+
|
|
273
|
+
// Extract the actual value for display (handle object with value property)
|
|
274
|
+
const displayValue = (() => {
|
|
275
|
+
const val = selectProps.value
|
|
276
|
+
if (!val) return "(none)"
|
|
277
|
+
if (typeof val === "object" && val !== null && "value" in val) {
|
|
278
|
+
return (val as { value: string }).value
|
|
279
|
+
}
|
|
280
|
+
return String(val)
|
|
281
|
+
})()
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<StoryWrapper>
|
|
285
|
+
<View style={styles.column}>
|
|
286
|
+
<View style={styles.section}>
|
|
287
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
288
|
+
Controlled: {displayValue}
|
|
289
|
+
</Typography>
|
|
290
|
+
<SelectField
|
|
291
|
+
{...selectProps}
|
|
292
|
+
label="Country"
|
|
293
|
+
placeholder="Select a country"
|
|
294
|
+
description={`Current value: "${displayValue}"`}
|
|
295
|
+
>
|
|
296
|
+
<SelectField.Item value="uk">United Kingdom</SelectField.Item>
|
|
297
|
+
<SelectField.Item value="us">United States</SelectField.Item>
|
|
298
|
+
<SelectField.Item value="ca">Canada</SelectField.Item>
|
|
299
|
+
<SelectField.Item value="au">Australia</SelectField.Item>
|
|
300
|
+
</SelectField>
|
|
301
|
+
</View>
|
|
302
|
+
</View>
|
|
303
|
+
</StoryWrapper>
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const styles = StyleSheet.create({
|
|
308
|
+
container: {
|
|
309
|
+
width: 320
|
|
310
|
+
},
|
|
311
|
+
column: {
|
|
312
|
+
flexDirection: "column",
|
|
313
|
+
gap: 24,
|
|
314
|
+
width: 320
|
|
315
|
+
},
|
|
316
|
+
section: {
|
|
317
|
+
flexDirection: "column",
|
|
318
|
+
gap: 8
|
|
319
|
+
}
|
|
320
|
+
})
|