@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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +6 -0
- package/dist/index.cjs +148 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +148 -44
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/atoms/Illustration/Illustration.stories.tsx +3 -1
- package/src/components/atoms/Input/InputField.tsx +9 -1
- package/src/components/atoms/NumberInput/NumberInput.stories.tsx +22 -0
- package/src/components/atoms/NumberInput/NumberInput.tsx +5 -1
- package/src/components/atoms/NumberInput/NumberInputField.tsx +81 -18
- package/src/components/atoms/Typography/Typography.test.tsx +2 -2
- package/src/components/molecules/NumberField/NumberField.stories.tsx +23 -0
- package/src/components/molecules/NumberField/NumberFieldInput.tsx +49 -13
package/package.json
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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=
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|