@butternutbox/pawprint-native 0.0.1 → 0.2.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 (187) hide show
  1. package/.turbo/turbo-build.log +15 -15
  2. package/CHANGELOG.md +30 -0
  3. package/COMPONENT_GUIDELINES.md +111 -4
  4. package/dist/index.cjs +12413 -1459
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +1111 -13
  7. package/dist/index.d.ts +1111 -13
  8. package/dist/index.js +12365 -1457
  9. package/dist/index.js.map +1 -1
  10. package/package.json +29 -11
  11. package/src/__mocks__/asset-stub.ts +1 -0
  12. package/src/__mocks__/emotion-native.tsx +18 -0
  13. package/src/__mocks__/react-native-gesture-handler.tsx +41 -0
  14. package/src/__mocks__/react-native-reanimated.tsx +79 -0
  15. package/src/__mocks__/react-native-safe-area-context.tsx +6 -0
  16. package/src/__mocks__/react-native-svg.tsx +27 -0
  17. package/src/__mocks__/react-native-worklets.tsx +11 -0
  18. package/src/__mocks__/react-native.tsx +338 -0
  19. package/src/__mocks__/rn-primitives/avatar.tsx +24 -0
  20. package/src/__mocks__/rn-primitives/checkbox.tsx +19 -0
  21. package/src/__mocks__/rn-primitives/select.tsx +116 -0
  22. package/src/__mocks__/rn-primitives/slider.tsx +40 -0
  23. package/src/__mocks__/rn-primitives/slot.tsx +30 -0
  24. package/src/__mocks__/rn-primitives/switch.tsx +24 -0
  25. package/src/__mocks__/rn-primitives/toggle.tsx +16 -0
  26. package/src/components/atoms/Avatar/Avatar.stories.tsx +57 -49
  27. package/src/components/atoms/Avatar/Avatar.test.tsx +269 -0
  28. package/src/components/atoms/Avatar/Avatar.tsx +68 -22
  29. package/src/components/atoms/Avatar/index.ts +1 -6
  30. package/src/components/atoms/Badge/Badge.stories.tsx +5 -29
  31. package/src/components/atoms/Badge/Badge.test.tsx +90 -0
  32. package/src/components/atoms/Button/Button.test.tsx +123 -0
  33. package/src/components/atoms/Button/Button.tsx +1 -1
  34. package/src/components/atoms/CarouselControls/CarouselControls.stories.tsx +217 -0
  35. package/src/components/atoms/CarouselControls/CarouselControls.tsx +127 -0
  36. package/src/components/atoms/CarouselControls/index.ts +2 -0
  37. package/src/components/atoms/Hint/Hint.test.tsx +36 -0
  38. package/src/components/atoms/Icon/Icon.test.tsx +98 -0
  39. package/src/components/atoms/Icon/Icon.tsx +5 -1
  40. package/src/components/atoms/IconButton/IconButton.test.tsx +101 -0
  41. package/src/components/atoms/Illustration/Illustration.stories.tsx +2 -2
  42. package/src/components/atoms/Illustration/Illustration.test.tsx +55 -0
  43. package/src/components/atoms/Illustration/Illustration.tsx +3 -3
  44. package/src/components/atoms/Input/Input.stories.tsx +129 -86
  45. package/src/components/atoms/Input/Input.test.tsx +306 -0
  46. package/src/components/atoms/Input/Input.tsx +9 -1
  47. package/src/components/atoms/Input/InputField.tsx +226 -74
  48. package/src/components/atoms/Link/Link.test.tsx +89 -0
  49. package/src/components/atoms/Link/Link.tsx +7 -6
  50. package/src/components/atoms/Logo/Logo.registry.ts +30 -5
  51. package/src/components/atoms/Logo/Logo.stories.tsx +108 -0
  52. package/src/components/atoms/Logo/Logo.test.tsx +56 -0
  53. package/src/components/atoms/Logo/assets/BCorp.tsx +113 -0
  54. package/src/components/atoms/Logo/assets/ButternutFavicon.tsx +33 -0
  55. package/src/components/atoms/Logo/assets/ButternutPrimary.tsx +294 -0
  56. package/src/components/atoms/Logo/assets/ButternutTabbedBottom.tsx +294 -0
  57. package/src/components/atoms/Logo/assets/ButternutTabbedTop.tsx +294 -0
  58. package/src/components/atoms/Logo/assets/ButternutWordmark.tsx +274 -0
  59. package/src/components/atoms/Logo/assets/PsiBufetFavicon.tsx +45 -0
  60. package/src/components/atoms/Logo/assets/PsiBufetPrimary.tsx +218 -0
  61. package/src/components/atoms/Logo/assets/PsiBufetTabbedBottom.tsx +218 -0
  62. package/src/components/atoms/Logo/assets/PsiBufetTabbedTop.tsx +218 -0
  63. package/src/components/atoms/Logo/assets/PsiBufetWordmark.tsx +195 -0
  64. package/src/components/atoms/Logo/assets/index.ts +11 -0
  65. package/src/components/atoms/NumberInput/NumberInput.stories.tsx +183 -0
  66. package/src/components/atoms/NumberInput/NumberInput.test.tsx +261 -0
  67. package/src/components/atoms/NumberInput/NumberInput.tsx +129 -0
  68. package/src/components/atoms/NumberInput/NumberInputField.tsx +77 -0
  69. package/src/components/atoms/NumberInput/index.ts +4 -0
  70. package/src/components/atoms/Spinner/Spinner.test.tsx +46 -0
  71. package/src/components/atoms/Spinner/Spinner.tsx +14 -5
  72. package/src/components/atoms/Switch/Switch.test.tsx +92 -0
  73. package/src/components/atoms/Switch/Switch.tsx +28 -17
  74. package/src/components/atoms/Tag/Tag.test.tsx +70 -0
  75. package/src/components/atoms/TextArea/TextArea.stories.tsx +303 -0
  76. package/src/components/atoms/TextArea/TextArea.test.tsx +416 -0
  77. package/src/components/atoms/TextArea/TextArea.tsx +171 -0
  78. package/src/components/atoms/TextArea/TextAreaField.tsx +304 -0
  79. package/src/components/atoms/TextArea/TextAreaLabel.tsx +103 -0
  80. package/src/components/atoms/TextArea/index.ts +6 -0
  81. package/src/components/atoms/Typography/Typography.test.tsx +94 -0
  82. package/src/components/atoms/index.ts +3 -0
  83. package/src/components/molecules/Accordion/Accordion.stories.tsx +177 -0
  84. package/src/components/molecules/Accordion/Accordion.test.tsx +185 -0
  85. package/src/components/molecules/Accordion/Accordion.tsx +284 -0
  86. package/src/components/molecules/Accordion/index.ts +6 -0
  87. package/src/components/molecules/Animated/Animated.stories.tsx +254 -0
  88. package/src/components/molecules/Animated/Animated.tsx +283 -0
  89. package/src/components/molecules/Animated/index.ts +10 -0
  90. package/src/components/molecules/ButtonDock/ButtonDock.stories.tsx +44 -25
  91. package/src/components/molecules/ButtonDock/ButtonDock.test.tsx +83 -0
  92. package/src/components/molecules/ButtonDock/ButtonDock.tsx +16 -13
  93. package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +48 -29
  94. package/src/components/molecules/ButtonGroup/ButtonGroup.test.tsx +73 -0
  95. package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +25 -3
  96. package/src/components/molecules/Checkbox/Checkbox.stories.tsx +72 -0
  97. package/src/components/molecules/Checkbox/Checkbox.test.tsx +117 -0
  98. package/src/components/molecules/Checkbox/Checkbox.tsx +101 -95
  99. package/src/components/molecules/CopyField/CopyField.stories.tsx +313 -0
  100. package/src/components/molecules/CopyField/CopyField.test.tsx +431 -0
  101. package/src/components/molecules/CopyField/CopyField.tsx +156 -0
  102. package/src/components/molecules/CopyField/CopyFieldInput.tsx +127 -0
  103. package/src/components/molecules/CopyField/hooks/index.ts +1 -0
  104. package/src/components/molecules/CopyField/hooks/useCopyField.ts +25 -0
  105. package/src/components/molecules/CopyField/index.ts +4 -0
  106. package/src/components/molecules/DatePicker/DatePicker.stories.tsx +298 -0
  107. package/src/components/molecules/DatePicker/DatePicker.test.tsx +201 -0
  108. package/src/components/molecules/DatePicker/DatePicker.tsx +590 -0
  109. package/src/components/molecules/DatePicker/index.ts +2 -0
  110. package/src/components/molecules/Drawer/Drawer.stories.tsx +285 -0
  111. package/src/components/molecules/Drawer/Drawer.test.tsx +180 -0
  112. package/src/components/molecules/Drawer/Drawer.tsx +187 -0
  113. package/src/components/molecules/Drawer/DrawerBody.tsx +80 -0
  114. package/src/components/molecules/Drawer/DrawerClose.tsx +76 -0
  115. package/src/components/molecules/Drawer/DrawerContent.tsx +339 -0
  116. package/src/components/molecules/Drawer/DrawerContext.ts +19 -0
  117. package/src/components/molecules/Drawer/DrawerDescription.tsx +31 -0
  118. package/src/components/molecules/Drawer/DrawerDragContext.ts +11 -0
  119. package/src/components/molecules/Drawer/DrawerFooter.tsx +49 -0
  120. package/src/components/molecules/Drawer/DrawerFooterContext.ts +6 -0
  121. package/src/components/molecules/Drawer/DrawerGrabber.tsx +62 -0
  122. package/src/components/molecules/Drawer/DrawerHeader.tsx +244 -0
  123. package/src/components/molecules/Drawer/DrawerHeaderContext.ts +13 -0
  124. package/src/components/molecules/Drawer/DrawerOverlay.tsx +53 -0
  125. package/src/components/molecules/Drawer/DrawerTitle.tsx +32 -0
  126. package/src/components/molecules/Drawer/index.ts +12 -0
  127. package/src/components/molecules/FilterTab/FilterTab.stories.tsx +210 -0
  128. package/src/components/molecules/FilterTab/FilterTab.tsx +310 -0
  129. package/src/components/molecules/FilterTab/index.ts +2 -0
  130. package/src/components/molecules/MessageCard/MessageCard.stories.tsx +169 -0
  131. package/src/components/molecules/MessageCard/MessageCard.tsx +362 -0
  132. package/src/components/molecules/MessageCard/index.ts +10 -0
  133. package/src/components/molecules/Notification/Notification.stories.tsx +219 -0
  134. package/src/components/molecules/Notification/Notification.tsx +426 -0
  135. package/src/components/molecules/Notification/index.ts +2 -0
  136. package/src/components/molecules/NumberField/NumberField.stories.tsx +231 -0
  137. package/src/components/molecules/NumberField/NumberField.tsx +186 -0
  138. package/src/components/molecules/NumberField/NumberFieldInput.tsx +287 -0
  139. package/src/components/molecules/NumberField/index.ts +2 -0
  140. package/src/components/molecules/PasswordField/PasswordField.stories.tsx +362 -0
  141. package/src/components/molecules/PasswordField/PasswordField.test.tsx +369 -0
  142. package/src/components/molecules/PasswordField/PasswordField.tsx +194 -0
  143. package/src/components/molecules/PasswordField/PasswordFieldError.tsx +53 -0
  144. package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
  145. package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +95 -0
  146. package/src/components/molecules/PasswordField/hooks/index.ts +2 -0
  147. package/src/components/molecules/PasswordField/hooks/usePasswordField.ts +113 -0
  148. package/src/components/molecules/PasswordField/index.ts +10 -0
  149. package/src/components/molecules/PictureSelector/PictureSelector.stories.tsx +204 -0
  150. package/src/components/molecules/PictureSelector/PictureSelector.tsx +335 -0
  151. package/src/components/molecules/PictureSelector/index.ts +5 -0
  152. package/src/components/molecules/Progress/Progress.stories.tsx +145 -0
  153. package/src/components/molecules/Progress/Progress.tsx +184 -0
  154. package/src/components/molecules/Progress/index.ts +2 -0
  155. package/src/components/molecules/Radio/Radio.test.tsx +104 -0
  156. package/src/components/molecules/Radio/Radio.tsx +1 -2
  157. package/src/components/molecules/SearchField/SearchField.stories.tsx +242 -0
  158. package/src/components/molecules/SearchField/SearchField.test.tsx +318 -0
  159. package/src/components/molecules/SearchField/SearchField.tsx +143 -0
  160. package/src/components/molecules/SearchField/SearchFieldInput.tsx +63 -0
  161. package/src/components/molecules/SearchField/hooks/index.ts +1 -0
  162. package/src/components/molecules/SearchField/hooks/useSearchField.ts +56 -0
  163. package/src/components/molecules/SearchField/index.ts +4 -0
  164. package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +31 -8
  165. package/src/components/molecules/SegmentedControl/SegmentedControl.test.tsx +141 -0
  166. package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +237 -23
  167. package/src/components/molecules/SelectField/SelectField.stories.tsx +320 -0
  168. package/src/components/molecules/SelectField/SelectField.test.tsx +254 -0
  169. package/src/components/molecules/SelectField/SelectField.tsx +236 -0
  170. package/src/components/molecules/SelectField/SelectFieldContent.tsx +85 -0
  171. package/src/components/molecules/SelectField/SelectFieldItem.tsx +133 -0
  172. package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +170 -0
  173. package/src/components/molecules/SelectField/SelectFieldValue.tsx +31 -0
  174. package/src/components/molecules/SelectField/hooks/index.ts +2 -0
  175. package/src/components/molecules/SelectField/hooks/useSelectField.ts +84 -0
  176. package/src/components/molecules/SelectField/index.ts +10 -0
  177. package/src/components/molecules/Slider/Slider.test.tsx +102 -0
  178. package/src/components/molecules/Slider/Slider.tsx +293 -180
  179. package/src/components/molecules/Tooltip/Tooltip.stories.tsx +168 -0
  180. package/src/components/molecules/Tooltip/Tooltip.tsx +326 -0
  181. package/src/components/molecules/Tooltip/index.ts +2 -0
  182. package/src/components/molecules/index.ts +15 -0
  183. package/src/test-utils.tsx +20 -0
  184. package/tsconfig.json +1 -1
  185. package/tsup.config.ts +16 -2
  186. package/vitest.config.ts +114 -0
  187. package/vitest.setup.ts +16 -0
@@ -0,0 +1,186 @@
1
+ import React from "react"
2
+ import { View, ViewProps } from "react-native"
3
+ import styled from "@emotion/native"
4
+ import { useTheme } from "@emotion/react"
5
+ import { Typography } from "../../atoms/Typography"
6
+ import {
7
+ NumberFieldInput,
8
+ type NumberFieldInputProps
9
+ } from "./NumberFieldInput"
10
+
11
+ type NumberFieldSize = "sm" | "lg"
12
+ type NumberFieldState = "default" | "error" | "success"
13
+
14
+ type NumberFieldOwnProps = {
15
+ label?: string
16
+ smallLabel?: string
17
+ description?: string
18
+ error?: string
19
+ state?: NumberFieldState
20
+ size?: NumberFieldSize
21
+ disabled?: boolean
22
+ onIncrement?: () => void
23
+ onDecrement?: () => void
24
+ showIncrementButton?: boolean
25
+ showDecrementButton?: boolean
26
+ incrementDisabled?: boolean
27
+ decrementDisabled?: boolean
28
+ min?: number
29
+ max?: number
30
+ }
31
+
32
+ export type NumberFieldProps = NumberFieldOwnProps &
33
+ Omit<NumberFieldInputProps, keyof NumberFieldOwnProps | "fieldSize"> &
34
+ Omit<ViewProps, keyof NumberFieldOwnProps>
35
+
36
+ const parseTokenValue = (value: string): number => parseFloat(value)
37
+
38
+ const StyledRoot = styled(View)<{
39
+ rootGap: number
40
+ }>(({ rootGap }) => ({
41
+ alignItems: "center",
42
+ gap: rootGap
43
+ }))
44
+
45
+ const StyledLabelGroup = styled(View)<{
46
+ labelGap: number
47
+ }>(({ labelGap }) => ({
48
+ gap: labelGap
49
+ }))
50
+
51
+ /**
52
+ * Number field component for collecting numeric data with increment/decrement
53
+ * buttons and an optional side text label.
54
+ * Built on React Native TextInput with design system styling.
55
+ *
56
+ * @param {"sm" | "lg"} [size="lg"] - Size variant controlling typography and spacing.
57
+ * @param {string} [label] - Primary label text above the field.
58
+ * @param {string} [smallLabel] - Secondary label text displayed below the primary label.
59
+ * @param {string} [description] - Help text displayed below the field.
60
+ * @param {string} [error] - Error message displayed when state is "error".
61
+ * @param {"default" | "error" | "success"} [state="default"] - Visual state of the field.
62
+ * @param {boolean} [disabled=false] - Whether the entire field (input + buttons) is disabled.
63
+ * @param {() => void} [onIncrement] - Called when the + button is pressed.
64
+ * @param {() => void} [onDecrement] - Called when the - button is pressed.
65
+ * @param {boolean} [showIncrementButton=true] - Whether to show the + button.
66
+ * @param {boolean} [showDecrementButton=true] - Whether to show the - button.
67
+ * @param {boolean} [incrementDisabled=false] - Whether the + button is independently disabled.
68
+ * @param {boolean} [decrementDisabled=false] - Whether the - button is independently disabled.
69
+ * @param {number} [min] - Minimum allowed value. Clamps on blur and disables decrement at min.
70
+ * @param {number} [max] - Maximum allowed value. Clamps on blur and disables increment at max.
71
+ *
72
+ * @example
73
+ * ```tsx
74
+ * import { NumberField } from "@butternutbox/pawprint-native"
75
+ *
76
+ * <NumberField
77
+ * label="Weight"
78
+ * size="lg"
79
+ * defaultValue="0"
80
+ * onIncrement={() => setValue(v => v + 1)}
81
+ * onDecrement={() => setValue(v => v - 1)}
82
+ * />
83
+ * ```
84
+ *
85
+ * Note: Unlike the web version, this component does not support a compound component API.
86
+ */
87
+ export const NumberField = React.forwardRef<View, NumberFieldProps>(
88
+ (
89
+ {
90
+ label,
91
+ smallLabel,
92
+ description,
93
+ error,
94
+ state = "default",
95
+ size = "lg",
96
+ disabled,
97
+ onIncrement,
98
+ onDecrement,
99
+ showIncrementButton,
100
+ showDecrementButton,
101
+ incrementDisabled,
102
+ decrementDisabled,
103
+ min,
104
+ max,
105
+ ...inputProps
106
+ },
107
+ ref
108
+ ) => {
109
+ const theme = useTheme()
110
+ const tokens = theme.tokens.components.numberField
111
+ const sizeTokens =
112
+ size === "lg" ? tokens.spacing.large : tokens.spacing.small
113
+ const typoTokens =
114
+ size === "lg" ? tokens.typography.large : tokens.typography.small
115
+
116
+ return (
117
+ <StyledRoot ref={ref} rootGap={parseTokenValue(sizeTokens.gap)}>
118
+ {(label || smallLabel) && (
119
+ <StyledLabelGroup
120
+ labelGap={parseTokenValue(
121
+ theme.tokens.components.inputs.spacing.label.gap
122
+ )}
123
+ >
124
+ {label && (
125
+ <Typography
126
+ token={typoTokens.label}
127
+ color={tokens.colour.text.label}
128
+ >
129
+ {label}
130
+ </Typography>
131
+ )}
132
+ {smallLabel && (
133
+ <Typography
134
+ token={typoTokens.smallLabel}
135
+ color={tokens.colour.text.description}
136
+ >
137
+ {smallLabel}
138
+ </Typography>
139
+ )}
140
+ </StyledLabelGroup>
141
+ )}
142
+ <NumberFieldInput
143
+ fieldSize={size}
144
+ state={state}
145
+ disabled={disabled}
146
+ onIncrement={onIncrement}
147
+ onDecrement={onDecrement}
148
+ showIncrementButton={showIncrementButton}
149
+ showDecrementButton={showDecrementButton}
150
+ incrementDisabled={incrementDisabled}
151
+ decrementDisabled={decrementDisabled}
152
+ min={min}
153
+ max={max}
154
+ accessibilityState={{ disabled }}
155
+ {...inputProps}
156
+ />
157
+ {description && state !== "error" && (
158
+ <Typography
159
+ token={
160
+ size === "lg"
161
+ ? tokens.typography.large.secondaryContent
162
+ : tokens.typography.small.smallLabel
163
+ }
164
+ color={tokens.colour.text.description}
165
+ >
166
+ {description}
167
+ </Typography>
168
+ )}
169
+ {error && state === "error" && (
170
+ <Typography
171
+ token={
172
+ size === "lg"
173
+ ? tokens.typography.large.secondaryContent
174
+ : tokens.typography.small.smallLabel
175
+ }
176
+ color={theme.tokens.components.inputs.colour.description.error}
177
+ >
178
+ {error}
179
+ </Typography>
180
+ )}
181
+ </StyledRoot>
182
+ )
183
+ }
184
+ )
185
+
186
+ NumberField.displayName = "NumberField"
@@ -0,0 +1,287 @@
1
+ import React from "react"
2
+ import { View, TextInput, TextInputProps } from "react-native"
3
+ import styled from "@emotion/native"
4
+ import { useTheme } from "@emotion/react"
5
+ import { IconButton } from "../../atoms/IconButton"
6
+ import { Add, Remove } from "@butternutbox/pawprint-icons/core"
7
+ import { resolveFont } from "../../../fonts"
8
+
9
+ type NumberFieldSize = "sm" | "lg"
10
+
11
+ type NumberFieldInputOwnProps = {
12
+ fieldSize?: NumberFieldSize
13
+ state?: "default" | "error" | "success"
14
+ onIncrement?: () => void
15
+ onDecrement?: () => void
16
+ showIncrementButton?: boolean
17
+ showDecrementButton?: boolean
18
+ disabled?: boolean
19
+ incrementDisabled?: boolean
20
+ decrementDisabled?: boolean
21
+ min?: number
22
+ max?: number
23
+ }
24
+
25
+ export type NumberFieldInputProps = NumberFieldInputOwnProps &
26
+ Omit<TextInputProps, keyof NumberFieldInputOwnProps>
27
+
28
+ const parseTokenValue = (value: string): number => parseFloat(value)
29
+
30
+ const StyledFieldWrapper = styled(View)<{
31
+ fieldBgColor: string
32
+ fieldBorderColor: string
33
+ fieldBorderRadius: number
34
+ fieldMinWidth: number
35
+ fieldHeight: number
36
+ fieldPaddingHorizontal: number
37
+ }>(
38
+ ({
39
+ fieldBgColor,
40
+ fieldBorderColor,
41
+ fieldBorderRadius,
42
+ fieldMinWidth,
43
+ fieldHeight,
44
+ fieldPaddingHorizontal
45
+ }) => ({
46
+ alignItems: "center",
47
+ justifyContent: "center",
48
+ width: fieldMinWidth,
49
+ height: fieldHeight,
50
+ paddingHorizontal: fieldPaddingHorizontal,
51
+ backgroundColor: fieldBgColor,
52
+ borderWidth: 1,
53
+ borderColor: fieldBorderColor,
54
+ borderRadius: fieldBorderRadius,
55
+ overflow: "hidden"
56
+ })
57
+ )
58
+
59
+ type FontWeight =
60
+ | "normal"
61
+ | "bold"
62
+ | "100"
63
+ | "200"
64
+ | "300"
65
+ | "400"
66
+ | "500"
67
+ | "600"
68
+ | "700"
69
+ | "800"
70
+ | "900"
71
+
72
+ const StyledTextInput = styled(TextInput)<{
73
+ inputColor: string
74
+ inputFontFamily: string
75
+ inputFontWeight?: FontWeight
76
+ inputFontSize: number
77
+ inputLineHeight: number
78
+ }>(
79
+ ({
80
+ inputColor,
81
+ inputFontFamily,
82
+ inputFontWeight,
83
+ inputFontSize,
84
+ inputLineHeight
85
+ }) => ({
86
+ textAlign: "center" as const,
87
+ outlineStyle: "none" as unknown as undefined,
88
+ padding: 0,
89
+ minWidth: 0,
90
+ minHeight: 0,
91
+ width: "100%",
92
+ height: "100%",
93
+ color: inputColor,
94
+ fontFamily: inputFontFamily,
95
+ ...(inputFontWeight ? { fontWeight: inputFontWeight } : {}),
96
+ fontSize: inputFontSize,
97
+ lineHeight: inputLineHeight
98
+ })
99
+ )
100
+
101
+ const StyledRow = styled(View)<{
102
+ rowGap: number
103
+ }>(({ rowGap }) => ({
104
+ flexDirection: "row",
105
+ alignItems: "center",
106
+ alignSelf: "center",
107
+ gap: rowGap
108
+ }))
109
+
110
+ /**
111
+ * Inner input field for NumberField.
112
+ * Renders decrement button, centered numeric input, increment button,
113
+ * and optional side text (e.g. "kg").
114
+ *
115
+ * Border states:
116
+ * - default: `colour.field.border.default`
117
+ * - focused: `colour.field.border.selected`
118
+ * - edited (blurred after change): `colour.field.border.edited`
119
+ * - error/success: from shared input tokens
120
+ */
121
+ export const NumberFieldInput = React.forwardRef<
122
+ TextInput,
123
+ NumberFieldInputProps
124
+ >(
125
+ (
126
+ {
127
+ fieldSize = "lg",
128
+ state = "default",
129
+ onIncrement,
130
+ onDecrement,
131
+ showIncrementButton = true,
132
+ showDecrementButton = true,
133
+ disabled = false,
134
+ incrementDisabled = false,
135
+ decrementDisabled = false,
136
+ min,
137
+ max,
138
+ style,
139
+ onFocus,
140
+ onBlur,
141
+ onChangeText,
142
+ defaultValue,
143
+ value,
144
+ ...rest
145
+ },
146
+ ref
147
+ ) => {
148
+ const theme = useTheme()
149
+ const tokens = theme.tokens.components.numberField
150
+ const inputTokens = theme.tokens.components.inputs
151
+
152
+ const [isFocused, setIsFocused] = React.useState(false)
153
+ const [isEdited, setIsEdited] = React.useState(false)
154
+ const initialValueRef = React.useRef(value ?? defaultValue ?? "")
155
+
156
+ const isLarge = fieldSize === "lg"
157
+ const sizeTokens = isLarge ? tokens.spacing.large : tokens.spacing.small
158
+ const buttonSize = isLarge ? "lg" : "sm"
159
+ const fieldSizeTokens = isLarge ? tokens.size.large : tokens.size.small
160
+ const fieldMinWidth = parseTokenValue(fieldSizeTokens.minWidth)
161
+ const fieldHeight = parseTokenValue(fieldSizeTokens.height)
162
+ const fieldPaddingHorizontal = parseTokenValue(
163
+ inputTokens.spacing.field.horizontalPadding
164
+ )
165
+
166
+ const getBorderColor = (): string => {
167
+ if (state === "error") return inputTokens.colour.field.border.error
168
+ if (state === "success") return inputTokens.colour.field.border.success
169
+ if (isFocused) return tokens.colour.field.border.selected
170
+ if (isEdited) return tokens.colour.field.border.edited
171
+ return tokens.colour.field.border.default
172
+ }
173
+
174
+ const handleFocus = React.useCallback(
175
+ (e: Parameters<NonNullable<TextInputProps["onFocus"]>>[0]) => {
176
+ setIsFocused(true)
177
+ onFocus?.(e)
178
+ },
179
+ [onFocus]
180
+ )
181
+
182
+ const handleBlur = React.useCallback(
183
+ (e: Parameters<NonNullable<TextInputProps["onBlur"]>>[0]) => {
184
+ setIsFocused(false)
185
+ if (value != null && onChangeText) {
186
+ const num = Number(value)
187
+ if (!Number.isNaN(num)) {
188
+ const clamped =
189
+ min != null && num < min
190
+ ? min
191
+ : max != null && num > max
192
+ ? max
193
+ : num
194
+ if (clamped !== num) {
195
+ onChangeText(String(clamped))
196
+ }
197
+ }
198
+ }
199
+ onBlur?.(e)
200
+ },
201
+ [onBlur, value, min, max, onChangeText]
202
+ )
203
+
204
+ const numericValue =
205
+ value != null ? Number(value) : Number(defaultValue ?? 0)
206
+ const isAtMin = min != null && numericValue <= min
207
+ const isAtMax = max != null && numericValue >= max
208
+
209
+ const handleChangeText = React.useCallback(
210
+ (text: string) => {
211
+ setIsEdited(text !== initialValueRef.current)
212
+ onChangeText?.(text)
213
+ },
214
+ [onChangeText]
215
+ )
216
+
217
+ const typographyToken =
218
+ fieldSize === "lg"
219
+ ? tokens.typography.field.large
220
+ : tokens.typography.field.small
221
+
222
+ const resolvedInputFontFamily =
223
+ typographyToken.fontFamily && typographyToken.fontWeight
224
+ ? resolveFont(typographyToken.fontFamily, typographyToken.fontWeight)
225
+ .fontFamily
226
+ : typographyToken.fontFamily
227
+
228
+ return (
229
+ <StyledRow rowGap={parseTokenValue(sizeTokens.group.gap)}>
230
+ {showDecrementButton && (
231
+ <IconButton
232
+ icon={Remove}
233
+ variant="filled"
234
+ size={buttonSize}
235
+ colour="secondary"
236
+ aria-label="Decrease value"
237
+ onPress={onDecrement}
238
+ disabled={disabled || decrementDisabled || isAtMin}
239
+ />
240
+ )}
241
+ <StyledFieldWrapper
242
+ fieldBgColor={tokens.colour.field.background.default}
243
+ fieldBorderColor={getBorderColor()}
244
+ fieldBorderRadius={parseTokenValue(tokens.borderRadius.field.default)}
245
+ fieldMinWidth={fieldMinWidth}
246
+ fieldHeight={fieldHeight}
247
+ fieldPaddingHorizontal={fieldPaddingHorizontal}
248
+ >
249
+ <StyledTextInput
250
+ ref={ref}
251
+ inputColor={tokens.colour.field.text.default}
252
+ inputFontFamily={resolvedInputFontFamily}
253
+ inputFontWeight={
254
+ resolvedInputFontFamily === typographyToken.fontFamily
255
+ ? (typographyToken.fontWeight as FontWeight)
256
+ : undefined
257
+ }
258
+ inputFontSize={parseTokenValue(typographyToken.fontSize)}
259
+ inputLineHeight={parseTokenValue(typographyToken.lineHeight)}
260
+ keyboardType="numeric"
261
+ editable={!disabled}
262
+ onFocus={handleFocus}
263
+ onBlur={handleBlur}
264
+ onChangeText={handleChangeText}
265
+ defaultValue={defaultValue}
266
+ value={value}
267
+ style={style}
268
+ {...rest}
269
+ />
270
+ </StyledFieldWrapper>
271
+ {showIncrementButton && (
272
+ <IconButton
273
+ icon={Add}
274
+ variant="filled"
275
+ size={buttonSize}
276
+ colour="secondary"
277
+ aria-label="Increase value"
278
+ onPress={onIncrement}
279
+ disabled={disabled || incrementDisabled || isAtMax}
280
+ />
281
+ )}
282
+ </StyledRow>
283
+ )
284
+ }
285
+ )
286
+
287
+ NumberFieldInput.displayName = "NumberField.Input"
@@ -0,0 +1,2 @@
1
+ export { NumberField } from "./NumberField"
2
+ export type { NumberFieldProps } from "./NumberField"