@butternutbox/pawprint-native 0.10.6 → 0.10.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@butternutbox/pawprint-native",
3
- "version": "0.10.6",
3
+ "version": "0.10.7",
4
4
  "type": "module",
5
5
  "description": "ButternutBox Pawprint Design System - React Native Components",
6
6
  "main": "./dist/index.cjs",
@@ -10,6 +10,7 @@ import * as poses from "@butternutbox/pawprint-illustrations/poses"
10
10
  import * as usps from "@butternutbox/pawprint-illustrations/usps"
11
11
  import * as breeds from "@butternutbox/pawprint-illustrations/breeds"
12
12
  import * as breedSizes from "@butternutbox/pawprint-illustrations/breed-sizes"
13
+ import * as traits from "@butternutbox/pawprint-illustrations/traits"
13
14
 
14
15
  function isComponent(value: unknown): value is PawprintIllustration {
15
16
  if (typeof value === "function") return true
@@ -21,7 +22,8 @@ const categorised: Record<string, Record<string, unknown>> = {
21
22
  poses,
22
23
  usps,
23
24
  breeds,
24
- "breed-sizes": breedSizes
25
+ "breed-sizes": breedSizes,
26
+ traits
25
27
  }
26
28
 
27
29
  const illustrationMap: Record<string, PawprintIllustration> = {}
@@ -26,6 +26,7 @@ type InputFieldOwnProps = {
26
26
  hideStateIcons?: boolean
27
27
  containerWidth?: number
28
28
  containerHeight?: number
29
+ hasInlineHelp?: boolean
29
30
  }
30
31
 
31
32
  export type InputFieldProps = InputFieldOwnProps &
@@ -60,7 +61,10 @@ const StyledInputWrapper = styled(Animated.View)<{
60
61
  }
61
62
  })
62
63
 
63
- const StyledInput = styled(TextInput)(({ theme }) => {
64
+ const StyledInput = styled(TextInput)<{ hasInlineHelp?: boolean }>(({
65
+ theme,
66
+ hasInlineHelp
67
+ }) => {
64
68
  const { colour, field } = theme.tokens.components.inputs
65
69
  const typography = field.placeholder.default
66
70
  const { fontFamily } = resolveFont(
@@ -71,6 +75,7 @@ const StyledInput = styled(TextInput)(({ theme }) => {
71
75
  const letterSpacing = typography.letterSpacing?.endsWith("%")
72
76
  ? (parseFloat(typography.letterSpacing) / 100) * fontSize
73
77
  : parseFloat(typography.letterSpacing ?? "0")
78
+
74
79
  return {
75
80
  flex: 1,
76
81
  minWidth: 0,
@@ -80,6 +85,7 @@ const StyledInput = styled(TextInput)(({ theme }) => {
80
85
  fontFamily,
81
86
  fontSize,
82
87
  letterSpacing,
88
+ ...(hasInlineHelp ? { position: "relative", top: -8 } : {}),
83
89
  // Suppress the browser default focus outline on React Native Web —
84
90
  // the focus indicator is rendered on the wrapper instead.
85
91
  ...({
@@ -120,6 +126,7 @@ export const InputField = React.forwardRef<TextInput, InputFieldProps>(
120
126
  hideStateIcons,
121
127
  containerWidth,
122
128
  containerHeight,
129
+ hasInlineHelp,
123
130
  style,
124
131
  editable,
125
132
  onFocus,
@@ -270,6 +277,7 @@ export const InputField = React.forwardRef<TextInput, InputFieldProps>(
270
277
  editable={editable}
271
278
  onFocus={handleFocus}
272
279
  onBlur={handleBlur}
280
+ hasInlineHelp={hasInlineHelp}
273
281
  {...rest}
274
282
  />
275
283
  {trailingIcon && <StyledIconSlot>{trailingIcon}</StyledIconSlot>}
@@ -31,6 +31,10 @@ export default {
31
31
  fieldText: {
32
32
  control: { type: "text" },
33
33
  description: "Unit label shown to the right of the input (e.g. kg, lbs)"
34
+ },
35
+ inlineHelpText: {
36
+ control: { type: "text" },
37
+ description: "Help text displayed below the input field"
34
38
  }
35
39
  }
36
40
  }
@@ -155,6 +159,24 @@ export const CustomStateValidationWithSuccess = () => {
155
159
  )
156
160
  }
157
161
 
162
+ export const WithInlineHelpText = () => (
163
+ <View style={styles.column}>
164
+ <NumberInput
165
+ label="Daily amount"
166
+ placeholder="0"
167
+ inlineHelpText="~ 840 kcals"
168
+ description="Help text"
169
+ />
170
+ <NumberInput
171
+ label="Quantity"
172
+ placeholder="0"
173
+ inlineHelpText="per serving"
174
+ fieldText="portions"
175
+ description="Additional context"
176
+ />
177
+ </View>
178
+ )
179
+
158
180
  export const Controlled = () => {
159
181
  const [value, setValue] = React.useState("")
160
182
 
@@ -13,6 +13,7 @@ type NumberInputOwnProps = Pick<
13
13
  "label" | "description" | "error" | "state" | "optionalText" | "onValueChange"
14
14
  > & {
15
15
  fieldText?: string
16
+ inlineHelpText?: string
16
17
  }
17
18
 
18
19
  export type NumberInputProps = NumberInputOwnProps &
@@ -60,7 +61,8 @@ const StyledRoot = styled(View)(({ theme }) => {
60
61
  * </NumberInput.Root>
61
62
  *
62
63
  * @param {string} [label] - Label text (props API)
63
- * @param {string} [description] - Description/help text (props API)
64
+ * @param {string} [description] - Description/help text below the field (props API)
65
+ * @param {string} [inlineHelpText] - Help text displayed below the input field (props API)
64
66
  * @param {string} [error] - Error message to display (props API, does not affect visual state - use state prop for that)
65
67
  * @param {InputState} [state] - Visual state of the input: 'default', 'error', or 'success' (props API)
66
68
  * @param {string} [optionalText] - Optional text to display next to label (props API)
@@ -77,6 +79,7 @@ const NumberInputRoot = React.forwardRef<View, NumberInputProps>(
77
79
  optionalText,
78
80
  onValueChange,
79
81
  fieldText,
82
+ inlineHelpText,
80
83
  children,
81
84
  ...inputProps
82
85
  },
@@ -97,6 +100,7 @@ const NumberInputRoot = React.forwardRef<View, NumberInputProps>(
97
100
  state={state}
98
101
  onChangeText={onValueChange}
99
102
  fieldText={fieldText}
103
+ inlineHelpText={inlineHelpText}
100
104
  {...inputProps}
101
105
  />
102
106
  {description && (
@@ -21,8 +21,16 @@ const StyledFieldTextWrapper = styled(View)({
21
21
  justifyContent: "center"
22
22
  })
23
23
 
24
+ const StyledInputContainer = styled(View)<{
25
+ hasInlineHelp?: boolean
26
+ }>(({ hasInlineHelp }) => ({
27
+ flexDirection: "column",
28
+ gap: hasInlineHelp ? 2 : 0
29
+ }))
30
+
24
31
  type NumberInputFieldOwnProps = {
25
32
  fieldText?: string
33
+ inlineHelpText?: string
26
34
  }
27
35
 
28
36
  export type NumberInputFieldProps = NumberInputFieldOwnProps &
@@ -36,7 +44,7 @@ export type NumberInputFieldProps = NumberInputFieldOwnProps &
36
44
  export const NumberInputField = React.forwardRef<
37
45
  TextInput,
38
46
  NumberInputFieldProps
39
- >(({ fieldText, ...props }, ref) => {
47
+ >(({ fieldText, inlineHelpText, ...props }, ref) => {
40
48
  const theme = useTheme()
41
49
  const { colour, description } = theme.tokens.components.inputs
42
50
  const fieldSize = parseTokenValue(
@@ -48,30 +56,85 @@ export const NumberInputField = React.forwardRef<
48
56
  keyboardType="numeric"
49
57
  hideStateIcons
50
58
  containerWidth={fieldSize}
51
- containerHeight={fieldSize}
59
+ containerHeight={inlineHelpText ? undefined : fieldSize}
60
+ hasInlineHelp={!!inlineHelpText}
52
61
  ref={ref}
53
62
  style={{ textAlign: "center" }}
54
63
  {...props}
55
64
  />
56
65
  )
57
66
 
58
- if (!fieldText) {
59
- return inputElement
60
- }
61
-
62
- return (
63
- <StyledFieldGroup>
64
- {inputElement}
65
- <StyledFieldTextWrapper>
66
- <Typography
67
- token={description.text.default}
68
- color={colour.description.default}
67
+ const fieldContent =
68
+ inlineHelpText && fieldText ? (
69
+ <StyledFieldGroup>
70
+ <StyledInputContainer hasInlineHelp>
71
+ {inputElement}
72
+ <View
73
+ style={{
74
+ position: "absolute",
75
+ bottom: 4,
76
+ left: 0,
77
+ width: fieldSize,
78
+ display: "flex",
79
+ justifyContent: "center",
80
+ alignItems: "center"
81
+ }}
82
+ >
83
+ <Typography
84
+ token={description.text.default}
85
+ color={colour.description.default}
86
+ >
87
+ {inlineHelpText}
88
+ </Typography>
89
+ </View>
90
+ </StyledInputContainer>
91
+ <StyledFieldTextWrapper>
92
+ <Typography
93
+ token={description.text.default}
94
+ color={colour.description.default}
95
+ >
96
+ {fieldText}
97
+ </Typography>
98
+ </StyledFieldTextWrapper>
99
+ </StyledFieldGroup>
100
+ ) : fieldText ? (
101
+ <StyledFieldGroup>
102
+ {inputElement}
103
+ <StyledFieldTextWrapper>
104
+ <Typography
105
+ token={description.text.default}
106
+ color={colour.description.default}
107
+ >
108
+ {fieldText}
109
+ </Typography>
110
+ </StyledFieldTextWrapper>
111
+ </StyledFieldGroup>
112
+ ) : inlineHelpText ? (
113
+ <StyledInputContainer hasInlineHelp>
114
+ {inputElement}
115
+ <View
116
+ style={{
117
+ position: "absolute",
118
+ bottom: 4,
119
+ width: fieldSize,
120
+ display: "flex",
121
+ justifyContent: "center",
122
+ alignItems: "center"
123
+ }}
69
124
  >
70
- {fieldText}
71
- </Typography>
72
- </StyledFieldTextWrapper>
73
- </StyledFieldGroup>
74
- )
125
+ <Typography
126
+ token={description.text.default}
127
+ color={colour.description.default}
128
+ >
129
+ {inlineHelpText}
130
+ </Typography>
131
+ </View>
132
+ </StyledInputContainer>
133
+ ) : (
134
+ inputElement
135
+ )
136
+
137
+ return fieldContent
75
138
  })
76
139
 
77
140
  NumberInputField.displayName = "NumberInput.Field"
@@ -1,7 +1,7 @@
1
1
  import React from "react"
2
2
  import { screen } from "@testing-library/react"
3
3
  import { describe, it, expect } from "vitest"
4
- import { renderWithTheme } from "../../../test-utils"
4
+ import { renderWithTheme, theme } from "../../../test-utils"
5
5
  import { Typography } from "./Typography"
6
6
 
7
7
  describe("Typography", () => {
@@ -64,7 +64,7 @@ describe("Typography", () => {
64
64
  fontSize: "16",
65
65
  lineHeight: "24"
66
66
  }}
67
- color="#000000"
67
+ color={theme.tokens.semantics.colour.text.primary}
68
68
  >
69
69
  Token text
70
70
  </Typography>
@@ -25,6 +25,10 @@ export default {
25
25
  control: { type: "text" },
26
26
  description: "Help text displayed below the field"
27
27
  },
28
+ inlineHelpText: {
29
+ control: { type: "text" },
30
+ description: "Help text displayed inside the input field"
31
+ },
28
32
  error: {
29
33
  control: { type: "text" },
30
34
  description: "Error message when state is error"
@@ -221,6 +225,25 @@ export const FullWidth = () => {
221
225
  )
222
226
  }
223
227
 
228
+ export const WithInlineHelpText = () => {
229
+ const [value, setValue] = useState(12)
230
+
231
+ return (
232
+ <View style={styles.section}>
233
+ <NumberField
234
+ label="Daily amount"
235
+ size="lg"
236
+ inlineHelpText="~ 840 kcals"
237
+ description="Help text"
238
+ value={String(value)}
239
+ onChangeText={(t) => setValue(Number(t) || 0)}
240
+ onIncrement={() => setValue((v) => v + 1)}
241
+ onDecrement={() => setValue((v) => Math.max(0, v - 1))}
242
+ />
243
+ </View>
244
+ )
245
+ }
246
+
224
247
  export const Playground = {
225
248
  args: {
226
249
  size: "lg",
@@ -5,6 +5,7 @@ import { useTheme } from "@emotion/react"
5
5
  import { IconButton } from "../../atoms/IconButton"
6
6
  import { Add, Remove } from "@butternutbox/pawprint-icons/core"
7
7
  import { resolveFont } from "../../../fonts"
8
+ import { Typography } from "../../atoms/Typography"
8
9
 
9
10
  type NumberFieldSize = "sm" | "lg"
10
11
 
@@ -21,6 +22,7 @@ type NumberFieldInputOwnProps = {
21
22
  decrementDisabled?: boolean
22
23
  min?: number
23
24
  max?: number
25
+ inlineHelpText?: string
24
26
  }
25
27
 
26
28
  export type NumberFieldInputProps = NumberFieldInputOwnProps &
@@ -48,6 +50,7 @@ const StyledFieldWrapper = styled(View)<{
48
50
  }) => ({
49
51
  alignItems: "center",
50
52
  justifyContent: "center",
53
+ position: "relative",
51
54
  width: fullWidth ? "100%" : fieldMinWidth,
52
55
  minWidth: fieldMinWidth,
53
56
  height: fieldHeight,
@@ -79,19 +82,29 @@ const StyledTextInput = styled(TextInput)<{
79
82
  inputFontFamily: string
80
83
  inputFontWeight?: FontWeight
81
84
  inputFontSize: number
82
- }>(({ inputColor, inputFontFamily, inputFontWeight, inputFontSize }) => ({
83
- textAlign: "center" as const,
84
- outlineStyle: "none" as unknown as undefined,
85
- padding: 0,
86
- minWidth: 0,
87
- minHeight: 0,
88
- width: "100%",
89
- height: "100%",
90
- color: inputColor,
91
- fontFamily: inputFontFamily,
92
- ...(inputFontWeight ? { fontWeight: inputFontWeight } : {}),
93
- fontSize: inputFontSize
94
- }))
85
+ hasInlineHelp?: boolean
86
+ }>(
87
+ ({
88
+ inputColor,
89
+ inputFontFamily,
90
+ inputFontWeight,
91
+ inputFontSize,
92
+ hasInlineHelp
93
+ }) => ({
94
+ textAlign: "center" as const,
95
+ outlineStyle: "none" as unknown as undefined,
96
+ padding: 0,
97
+ minWidth: 0,
98
+ minHeight: 0,
99
+ width: "100%",
100
+ height: "100%",
101
+ color: inputColor,
102
+ fontFamily: inputFontFamily,
103
+ ...(inputFontWeight ? { fontWeight: inputFontWeight } : {}),
104
+ fontSize: inputFontSize,
105
+ ...(hasInlineHelp ? { position: "relative", top: -8 } : {})
106
+ })
107
+ )
95
108
 
96
109
  const StyledRow = styled(View)<{
97
110
  rowGap: number
@@ -106,6 +119,8 @@ const StyledRow = styled(View)<{
106
119
  * Renders decrement button, centered numeric input, increment button,
107
120
  * and optional side text (e.g. "kg").
108
121
  *
122
+ * @param {string} [inlineHelpText] - Help text displayed inside the input field at the bottom
123
+ *
109
124
  * Border states:
110
125
  * - default: `colour.field.border.default`
111
126
  * - focused: `colour.field.border.selected`
@@ -130,6 +145,7 @@ export const NumberFieldInput = React.forwardRef<
130
145
  decrementDisabled = false,
131
146
  min,
132
147
  max,
148
+ inlineHelpText,
133
149
  style,
134
150
  onFocus,
135
151
  onBlur,
@@ -252,6 +268,7 @@ export const NumberFieldInput = React.forwardRef<
252
268
  : undefined
253
269
  }
254
270
  inputFontSize={parseTokenValue(typographyToken.fontSize)}
271
+ hasInlineHelp={!!inlineHelpText}
255
272
  keyboardType="numeric"
256
273
  editable={!disabled}
257
274
  onFocus={handleFocus}
@@ -262,6 +279,25 @@ export const NumberFieldInput = React.forwardRef<
262
279
  style={style}
263
280
  {...rest}
264
281
  />
282
+ {inlineHelpText && (
283
+ <View
284
+ style={{
285
+ position: "absolute",
286
+ bottom: 4,
287
+ width: fieldMinWidth,
288
+ display: "flex",
289
+ justifyContent: "center",
290
+ alignItems: "center"
291
+ }}
292
+ >
293
+ <Typography
294
+ token={inputTokens.description.text.default}
295
+ color={inputTokens.colour.description.default}
296
+ >
297
+ {inlineHelpText}
298
+ </Typography>
299
+ </View>
300
+ )}
265
301
  </StyledFieldWrapper>
266
302
  {showIncrementButton && (
267
303
  <IconButton