@butternutbox/pawprint-native 1.6.3 → 2.0.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.
Files changed (44) hide show
  1. package/.turbo/turbo-build.log +9 -9
  2. package/CHANGELOG.md +52 -0
  3. package/COMPONENT_GUIDELINES.md +57 -1
  4. package/dist/index.cjs +1459 -1380
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +81 -117
  7. package/dist/index.d.ts +81 -117
  8. package/dist/index.js +1206 -1125
  9. package/dist/index.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/__mocks__/react-native.tsx +33 -5
  12. package/src/__mocks__/rn-primitives/portal.tsx +71 -0
  13. package/src/components/atoms/Input/InputField.tsx +12 -2
  14. package/src/components/molecules/Drawer/Drawer.test.tsx +54 -2
  15. package/src/components/molecules/Drawer/DrawerBody.tsx +2 -17
  16. package/src/components/molecules/Drawer/DrawerContent.tsx +49 -2
  17. package/src/components/molecules/Drawer/DrawerOverlay.tsx +28 -16
  18. package/src/components/molecules/SelectField/AsyncSearch.test.tsx +373 -0
  19. package/src/components/molecules/SelectField/SelectDrawerField.stories.tsx +192 -0
  20. package/src/components/molecules/SelectField/SelectDrawerField.test.tsx +210 -0
  21. package/src/components/molecules/SelectField/SelectDrawerField.tsx +245 -0
  22. package/src/components/molecules/SelectField/SelectField.stories.tsx +221 -421
  23. package/src/components/molecules/SelectField/SelectField.test.tsx +214 -662
  24. package/src/components/molecules/SelectField/SelectField.tsx +269 -539
  25. package/src/components/molecules/SelectField/SelectFieldDropdown.tsx +90 -0
  26. package/src/components/molecules/SelectField/SelectFieldList.tsx +88 -0
  27. package/src/components/molecules/SelectField/SelectFieldOption.tsx +90 -0
  28. package/src/components/molecules/SelectField/core/index.ts +8 -0
  29. package/src/components/molecules/SelectField/core/listHeight.test.ts +22 -0
  30. package/src/components/molecules/SelectField/core/listHeight.ts +8 -0
  31. package/src/components/molecules/SelectField/core/options.ts +35 -0
  32. package/src/components/molecules/SelectField/core/useSelectOptions.ts +116 -0
  33. package/src/components/molecules/SelectField/index.ts +12 -9
  34. package/src/index.ts +0 -1
  35. package/src/test-utils.tsx +5 -1
  36. package/vitest.config.ts +7 -0
  37. package/src/components/molecules/SelectField/SelectFieldContent.tsx +0 -81
  38. package/src/components/molecules/SelectField/SelectFieldItem.tsx +0 -133
  39. package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +0 -188
  40. package/src/components/molecules/SelectField/SelectFieldValue.tsx +0 -33
  41. package/src/components/molecules/SelectField/hooks/index.ts +0 -2
  42. package/src/components/molecules/SelectField/hooks/useSelectField.ts +0 -84
  43. package/src/hooks/index.ts +0 -1
  44. package/src/hooks/useDrawerKeyboardAvoidance.ts +0 -29
@@ -1,133 +0,0 @@
1
- import React from "react"
2
- import { Pressable, View, Text, PressableProps } from "react-native"
3
- import styled from "@emotion/native"
4
- import { useTheme } from "@emotion/react"
5
- import * as SelectPrimitive from "@rn-primitives/select"
6
- import { Slot } from "@rn-primitives/slot"
7
-
8
- type SelectFieldItemOwnProps = {
9
- value: string
10
- leadingIcon?: React.ReactNode
11
- hintText?: string
12
- disabled?: boolean
13
- isSelected?: boolean
14
- }
15
-
16
- export type SelectFieldItemProps = SelectFieldItemOwnProps &
17
- Omit<PressableProps, keyof SelectFieldItemOwnProps>
18
-
19
- const parseTokenValue = (value: string): number => parseFloat(value)
20
-
21
- const StyledItem = styled(Pressable)<{ disabled?: boolean }>(({ theme }) => {
22
- const { dropdown } = theme.tokens.components.dropdownList
23
-
24
- return {
25
- flexDirection: "row",
26
- alignItems: "center",
27
- gap: parseTokenValue(dropdown.listItem.content.gap),
28
- height: parseTokenValue(dropdown.listItem.size.height),
29
- paddingVertical: parseTokenValue(dropdown.listItem.spacing.verticalPadding),
30
- paddingHorizontal: parseTokenValue(
31
- dropdown.listItem.spacing.horiztonalPadding
32
- ),
33
- backgroundColor: dropdown.listItem.colour.background.default
34
- }
35
- })
36
-
37
- const StyledIconWrapper = styled(View)({
38
- flexShrink: 0,
39
- alignItems: "center",
40
- justifyContent: "center"
41
- })
42
-
43
- const StyledContentWrapper = styled(View)(({ theme }) => {
44
- const { dropdown } = theme.tokens.components.dropdownList
45
-
46
- return {
47
- flexDirection: "row",
48
- alignItems: "center",
49
- gap: parseTokenValue(dropdown.listItem.content.gap),
50
- flex: 1,
51
- minWidth: 0
52
- }
53
- })
54
-
55
- const StyledTextWrapper = styled(View)({
56
- flex: 1,
57
- minWidth: 0
58
- })
59
-
60
- const StyledItemText = styled(Text)(({ theme }) => {
61
- const { dropdown } = theme.tokens.components.dropdownList
62
-
63
- return {
64
- color: dropdown.listItem.colour.text.placeholder.default
65
- }
66
- })
67
-
68
- const StyledHintText = styled(Text)(({ theme }) => {
69
- const { dropdown } = theme.tokens.components.dropdownList
70
-
71
- return {
72
- color: dropdown.listItem.colour.text.hint.default,
73
- flexShrink: 0
74
- }
75
- })
76
-
77
- export const SelectFieldItem = React.forwardRef<View, SelectFieldItemProps>(
78
- (
79
- { value, leadingIcon, hintText, children, disabled, isSelected, ...rest },
80
- ref
81
- ) => {
82
- const theme = useTheme()
83
- const { dropdown } = theme.tokens.components.dropdownList
84
- const itemText = children as React.ReactNode
85
-
86
- const backgroundColor = disabled
87
- ? dropdown.listItem.colour.background.disabled
88
- : isSelected
89
- ? dropdown.listItem.colour.background.hover
90
- : dropdown.listItem.colour.background.default
91
-
92
- const textColor = disabled
93
- ? dropdown.listItem.colour.text.placeholder.disabled
94
- : dropdown.listItem.colour.text.placeholder.default
95
-
96
- return (
97
- <SelectPrimitive.Item
98
- label={String(children)}
99
- value={value}
100
- disabled={disabled}
101
- asChild
102
- >
103
- <Slot ref={ref}>
104
- <StyledItem disabled={disabled} style={{ backgroundColor }} {...rest}>
105
- {leadingIcon && (
106
- <StyledIconWrapper>{leadingIcon}</StyledIconWrapper>
107
- )}
108
- <StyledContentWrapper>
109
- <StyledTextWrapper>
110
- <StyledItemText style={{ color: textColor }}>
111
- {itemText}
112
- </StyledItemText>
113
- </StyledTextWrapper>
114
- {hintText && (
115
- <StyledHintText
116
- style={{
117
- color: disabled
118
- ? dropdown.listItem.colour.text.hint.disabled
119
- : dropdown.listItem.colour.text.hint.default
120
- }}
121
- >
122
- {hintText}
123
- </StyledHintText>
124
- )}
125
- </StyledContentWrapper>
126
- </StyledItem>
127
- </Slot>
128
- </SelectPrimitive.Item>
129
- )
130
- }
131
- )
132
-
133
- SelectFieldItem.displayName = "SelectField.Item"
@@ -1,188 +0,0 @@
1
- import React, { useEffect, useRef } from "react"
2
- import { Pressable, View, PressableProps, Animated, Easing } from "react-native"
3
- import styled from "@emotion/native"
4
- import { useTheme } from "@emotion/react"
5
- import * as SelectPrimitive from "@rn-primitives/select"
6
- import { Slot } from "@rn-primitives/slot"
7
- import { Icon } from "../../atoms/Icon"
8
- import {
9
- KeyboardArrowDown,
10
- Error as ErrorIcon,
11
- CheckCircle as SuccessIcon
12
- } from "@butternutbox/pawprint-icons/core"
13
- import type { InputState } from "../../atoms/Input/InputField"
14
-
15
- type SelectFieldTriggerOwnProps = {
16
- state?: InputState
17
- leadingIcon?: React.ReactNode
18
- children?: React.ReactNode
19
- isOpen?: boolean
20
- isSearching?: boolean
21
- actionIcon?: React.ReactNode
22
- }
23
-
24
- export type SelectFieldTriggerProps = SelectFieldTriggerOwnProps &
25
- Omit<PressableProps, keyof SelectFieldTriggerOwnProps>
26
-
27
- const parseTokenValue = (value: string): number => parseFloat(value)
28
-
29
- const StyledTriggerWrapper = styled(Animated.View)<{ state: InputState }>(({
30
- theme,
31
- state: _state
32
- }) => {
33
- const { spacing, colour, borderRadius, size } = theme.tokens.components.inputs
34
-
35
- return {
36
- flexDirection: "row",
37
- alignItems: "center",
38
- gap: parseTokenValue(spacing.field.gap),
39
- minWidth: parseTokenValue(size.field.minWidth),
40
- width: "100%",
41
- height: parseTokenValue(size.field.height),
42
- backgroundColor: colour.field.background.default,
43
- borderRadius: parseTokenValue(borderRadius.field.default)
44
- }
45
- })
46
-
47
- const StyledIconWrapper = styled(View)({
48
- flexShrink: 0,
49
- alignItems: "center",
50
- justifyContent: "center"
51
- })
52
-
53
- const StyledArrowIcon = styled(Animated.View)({
54
- flexShrink: 0,
55
- alignItems: "center",
56
- justifyContent: "center"
57
- })
58
-
59
- export const SelectFieldTrigger = React.forwardRef<
60
- View,
61
- SelectFieldTriggerProps
62
- >(
63
- (
64
- {
65
- state = "default",
66
- leadingIcon,
67
- children,
68
- isOpen = false,
69
- isSearching = false,
70
- actionIcon,
71
- ...rest
72
- },
73
- ref
74
- ) => {
75
- const theme = useTheme()
76
- const { colour, borderWidth, spacing } = theme.tokens.components.inputs
77
-
78
- const hasState = state === "error" || state === "success"
79
-
80
- const baseBorderWidth = parseTokenValue(borderWidth.field.default)
81
- const selectedBorderWidth = parseTokenValue(borderWidth.field.selected)
82
- const baseVerticalPadding = parseTokenValue(spacing.field.verticalPadding)
83
- const baseHorizontalPadding = parseTokenValue(
84
- spacing.field.horizontalPadding
85
- )
86
- const borderDelta = selectedBorderWidth - baseBorderWidth
87
-
88
- const borderColor =
89
- state === "error"
90
- ? colour.field.border.error
91
- : state === "success"
92
- ? colour.field.border.success
93
- : isOpen
94
- ? colour.field.border.selected
95
- : colour.field.border.default
96
-
97
- const isActiveBorder = isOpen || hasState
98
- const borderAnim = useRef(
99
- new Animated.Value(isActiveBorder ? 1 : 0)
100
- ).current
101
- const rotationAnim = useRef(new Animated.Value(isOpen ? 1 : 0)).current
102
-
103
- useEffect(() => {
104
- Animated.parallel([
105
- Animated.timing(borderAnim, {
106
- toValue: isActiveBorder ? 1 : 0,
107
- duration: 50,
108
- easing: Easing.ease,
109
- useNativeDriver: false
110
- }),
111
- Animated.timing(rotationAnim, {
112
- toValue: isOpen ? 1 : 0,
113
- duration: 250,
114
- easing: Easing.bezier(0.4, 0.0, 0.2, 1),
115
- useNativeDriver: true
116
- })
117
- ]).start()
118
- }, [isActiveBorder, isOpen, borderAnim, rotationAnim])
119
-
120
- const animatedBorderWidth = borderAnim.interpolate({
121
- inputRange: [0, 1],
122
- outputRange: [baseBorderWidth, selectedBorderWidth]
123
- })
124
-
125
- const animatedPaddingVertical = borderAnim.interpolate({
126
- inputRange: [0, 1],
127
- outputRange: [baseVerticalPadding, baseVerticalPadding - borderDelta]
128
- })
129
-
130
- const animatedPaddingHorizontal = borderAnim.interpolate({
131
- inputRange: [0, 1],
132
- outputRange: [baseHorizontalPadding, baseHorizontalPadding - borderDelta]
133
- })
134
-
135
- const animatedRotation = rotationAnim.interpolate({
136
- inputRange: [0, 1],
137
- outputRange: ["0deg", "180deg"]
138
- })
139
-
140
- const stateIcon =
141
- state === "error" ? (
142
- <Icon icon={ErrorIcon} size="md" colour="error" />
143
- ) : state === "success" ? (
144
- <Icon icon={SuccessIcon} size="md" colour="success" />
145
- ) : null
146
-
147
- const triggerContent = (
148
- <StyledTriggerWrapper
149
- state={state}
150
- style={[
151
- {
152
- borderColor,
153
- borderWidth: animatedBorderWidth,
154
- paddingVertical: animatedPaddingVertical,
155
- paddingHorizontal: animatedPaddingHorizontal
156
- }
157
- ]}
158
- >
159
- {leadingIcon && <StyledIconWrapper>{leadingIcon}</StyledIconWrapper>}
160
- {children}
161
- {stateIcon && <StyledIconWrapper>{stateIcon}</StyledIconWrapper>}
162
- {actionIcon ? (
163
- <StyledIconWrapper>{actionIcon}</StyledIconWrapper>
164
- ) : (
165
- <StyledArrowIcon
166
- style={{ transform: [{ rotate: animatedRotation }] }}
167
- >
168
- <Icon icon={KeyboardArrowDown} size="md" colour="primary" />
169
- </StyledArrowIcon>
170
- )}
171
- </StyledTriggerWrapper>
172
- )
173
-
174
- if (isSearching) {
175
- return <View ref={ref}>{triggerContent}</View>
176
- }
177
-
178
- return (
179
- <SelectPrimitive.Trigger asChild>
180
- <Slot ref={ref}>
181
- <Pressable {...rest}>{triggerContent}</Pressable>
182
- </Slot>
183
- </SelectPrimitive.Trigger>
184
- )
185
- }
186
- )
187
-
188
- SelectFieldTrigger.displayName = "SelectField.Trigger"
@@ -1,33 +0,0 @@
1
- import React from "react"
2
- import { View, ViewProps } from "react-native"
3
- import { useTheme } from "@emotion/react"
4
- import * as SelectPrimitive from "@rn-primitives/select"
5
- import { Typography } from "../../atoms/Typography"
6
-
7
- type SelectFieldValueProps = {
8
- placeholder?: string
9
- hasValue?: boolean
10
- } & Omit<ViewProps, "children">
11
-
12
- export const SelectFieldValue = React.forwardRef<View, SelectFieldValueProps>(
13
- ({ placeholder, hasValue = true, ...rest }, ref) => {
14
- const theme = useTheme()
15
- const { colour, field } = theme.tokens.components.inputs
16
-
17
- return (
18
- <View ref={ref} style={{ flex: 1, minWidth: 0 }} {...rest}>
19
- <Typography
20
- token={field.placeholder.default}
21
- color={
22
- hasValue ? colour.field.text.default : colour.field.text.placeholder
23
- }
24
- align="left"
25
- >
26
- <SelectPrimitive.Value placeholder={placeholder ?? ""} />
27
- </Typography>
28
- </View>
29
- )
30
- }
31
- )
32
-
33
- SelectFieldValue.displayName = "SelectField.Value"
@@ -1,2 +0,0 @@
1
- export { useSelectField } from "./useSelectField"
2
- export type { SelectValidationRule } from "./useSelectField"
@@ -1,84 +0,0 @@
1
- import { useState, useMemo } from "react"
2
- import type { SelectFieldProps } from "../SelectField"
3
- import type { InputState } from "../../../atoms/Input/InputField"
4
- import type { Option } from "@rn-primitives/select"
5
-
6
- export type SelectValidationRule<T = string | Option> = {
7
- test: (value: T | null) => boolean
8
- message: string
9
- }
10
-
11
- type SelectOptions<T = string | Option> = {
12
- initialValue?: T | null
13
- onValueChange?: (value: T | null) => void
14
- validationRule?: SelectValidationRule<T>
15
- }
16
-
17
- type SelectReturn<T = string | Option> = {
18
- value: T | null
19
- onValueChange: (value: T | null) => void
20
- state?: InputState
21
- error?: string
22
- }
23
-
24
- /**
25
- * Hook for managing SelectField state with built-in validation.
26
- * Handles value, validation state, and error message automatically.
27
- *
28
- * @example
29
- * ```tsx
30
- * // Simple usage without validation
31
- * const selectProps = useSelectField()
32
- * <SelectField {...selectProps} label="Country" />
33
- *
34
- * // With validation rule
35
- * const selectProps = useSelectField({
36
- * validationRule: {
37
- * test: (v) => v !== null,
38
- * message: "Please select an option"
39
- * }
40
- * })
41
- * <SelectField {...selectProps} label="Category" />
42
- * ```
43
- */
44
- export function useSelectField<T = string | Option>(
45
- options: SelectOptions<T> = {}
46
- ): SelectReturn<T> {
47
- const {
48
- initialValue,
49
- onValueChange: onValueChangeCallback,
50
- validationRule
51
- } = options
52
-
53
- const [value, setValue] = useState<T | null>(initialValue ?? null)
54
-
55
- const handleValueChange: SelectFieldProps<T>["onValueChange"] = (
56
- newValue
57
- ) => {
58
- const typedValue = Array.isArray(newValue) ? newValue[0] : newValue
59
- setValue(typedValue ?? null)
60
- onValueChangeCallback?.(typedValue as any)
61
- }
62
-
63
- const { state, error } = useMemo(() => {
64
- if (!validationRule) {
65
- return { state: undefined, error: undefined }
66
- }
67
-
68
- if (value === null || value === initialValue) {
69
- return { state: "default" as InputState, error: undefined }
70
- }
71
-
72
- const isValid = validationRule.test(value)
73
- return {
74
- state: (isValid ? "success" : "error") as InputState,
75
- error: isValid ? undefined : validationRule.message
76
- }
77
- }, [value, validationRule, initialValue])
78
-
79
- return {
80
- value,
81
- onValueChange: handleValueChange,
82
- ...(validationRule && { state, error })
83
- }
84
- }
@@ -1 +0,0 @@
1
- export { useDrawerKeyboardAvoidance } from "./useDrawerKeyboardAvoidance"
@@ -1,29 +0,0 @@
1
- import { useEffect, useState } from "react"
2
- import { Keyboard, StyleProp, ViewStyle } from "react-native"
3
-
4
- const useDrawerKeyboardAvoidance = (
5
- minPadding: number = 16
6
- ): { contentContainerStyle: StyleProp<ViewStyle> } => {
7
- const [keyboardHeight, setKeyboardHeight] = useState(0)
8
-
9
- useEffect(() => {
10
- const showListener = Keyboard.addListener("keyboardDidShow", (e) => {
11
- setKeyboardHeight(e.endCoordinates.height)
12
- })
13
- const hideListener = Keyboard.addListener("keyboardDidHide", () => {
14
- setKeyboardHeight(0)
15
- })
16
- return () => {
17
- showListener.remove()
18
- hideListener.remove()
19
- }
20
- }, [])
21
-
22
- return {
23
- contentContainerStyle: {
24
- paddingBottom: Math.max(minPadding, keyboardHeight)
25
- }
26
- }
27
- }
28
-
29
- export { useDrawerKeyboardAvoidance }