@butternutbox/pawprint-native 0.0.1
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 +30 -0
- package/COMPONENT_GUIDELINES.md +610 -0
- package/README.md +72 -0
- package/dist/ibm-plex-sans-condensed-400-normal-I2XLJNNB.woff2 +0 -0
- package/dist/ibm-plex-sans-condensed-500-normal-IEQBNVGX.woff2 +0 -0
- package/dist/ibm-plex-sans-condensed-600-normal-UX5ZU5T6.woff2 +0 -0
- package/dist/ibm-plex-sans-condensed-700-normal-4PFYFTSO.woff2 +0 -0
- package/dist/ida-narrow-500-normal-C6I2PK4T.woff2 +0 -0
- package/dist/ida-narrow-700-normal-UPHPRIN6.woff2 +0 -0
- package/dist/index.cjs +2686 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +780 -0
- package/dist/index.d.ts +780 -0
- package/dist/index.js +2617 -0
- package/dist/index.js.map +1 -0
- package/eslint.config.js +3 -0
- package/llms.txt +458 -0
- package/package.json +57 -0
- package/src/components/atoms/Avatar/Avatar.stories.tsx +125 -0
- package/src/components/atoms/Avatar/Avatar.tsx +159 -0
- package/src/components/atoms/Avatar/index.ts +7 -0
- package/src/components/atoms/Badge/Badge.stories.tsx +231 -0
- package/src/components/atoms/Badge/Badge.tsx +184 -0
- package/src/components/atoms/Badge/index.ts +2 -0
- package/src/components/atoms/Button/Button.stories.tsx +145 -0
- package/src/components/atoms/Button/Button.tsx +261 -0
- package/src/components/atoms/Button/index.ts +7 -0
- package/src/components/atoms/Hint/Hint.stories.tsx +84 -0
- package/src/components/atoms/Hint/Hint.tsx +59 -0
- package/src/components/atoms/Hint/index.ts +2 -0
- package/src/components/atoms/Icon/Icon.stories.tsx +200 -0
- package/src/components/atoms/Icon/Icon.tsx +112 -0
- package/src/components/atoms/Icon/index.ts +8 -0
- package/src/components/atoms/IconButton/IconButton.stories.tsx +162 -0
- package/src/components/atoms/IconButton/IconButton.tsx +227 -0
- package/src/components/atoms/IconButton/index.ts +7 -0
- package/src/components/atoms/Illustration/Illustration.stories.tsx +167 -0
- package/src/components/atoms/Illustration/Illustration.tsx +81 -0
- package/src/components/atoms/Illustration/index.ts +6 -0
- package/src/components/atoms/Input/Input.stories.tsx +142 -0
- package/src/components/atoms/Input/Input.tsx +110 -0
- package/src/components/atoms/Input/InputDescription.tsx +49 -0
- package/src/components/atoms/Input/InputError.tsx +39 -0
- package/src/components/atoms/Input/InputField.tsx +119 -0
- package/src/components/atoms/Input/InputLabel.tsx +61 -0
- package/src/components/atoms/Input/index.ts +10 -0
- package/src/components/atoms/Link/Link.stories.tsx +119 -0
- package/src/components/atoms/Link/Link.tsx +118 -0
- package/src/components/atoms/Link/index.ts +2 -0
- package/src/components/atoms/Logo/Logo.registry.ts +39 -0
- package/src/components/atoms/Logo/Logo.tsx +68 -0
- package/src/components/atoms/Logo/index.ts +4 -0
- package/src/components/atoms/Spinner/Spinner.stories.tsx +98 -0
- package/src/components/atoms/Spinner/Spinner.tsx +91 -0
- package/src/components/atoms/Spinner/index.ts +2 -0
- package/src/components/atoms/Switch/Switch.stories.tsx +120 -0
- package/src/components/atoms/Switch/Switch.tsx +196 -0
- package/src/components/atoms/Switch/index.ts +2 -0
- package/src/components/atoms/Tag/Tag.stories.tsx +89 -0
- package/src/components/atoms/Tag/Tag.tsx +122 -0
- package/src/components/atoms/Tag/index.ts +2 -0
- package/src/components/atoms/Typography/Typography.stories.tsx +315 -0
- package/src/components/atoms/Typography/Typography.tsx +284 -0
- package/src/components/atoms/Typography/index.ts +2 -0
- package/src/components/atoms/index.ts +14 -0
- package/src/components/index.ts +2 -0
- package/src/components/molecules/ButtonDock/ButtonDock.stories.tsx +95 -0
- package/src/components/molecules/ButtonDock/ButtonDock.tsx +148 -0
- package/src/components/molecules/ButtonDock/index.ts +2 -0
- package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +82 -0
- package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +94 -0
- package/src/components/molecules/ButtonGroup/index.ts +2 -0
- package/src/components/molecules/Checkbox/Checkbox.stories.tsx +148 -0
- package/src/components/molecules/Checkbox/Checkbox.tsx +279 -0
- package/src/components/molecules/Checkbox/CheckboxGroup.tsx +53 -0
- package/src/components/molecules/Checkbox/index.ts +4 -0
- package/src/components/molecules/Radio/Radio.stories.tsx +182 -0
- package/src/components/molecules/Radio/Radio.tsx +249 -0
- package/src/components/molecules/Radio/RadioGroup.tsx +142 -0
- package/src/components/molecules/Radio/index.ts +4 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +151 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +323 -0
- package/src/components/molecules/SegmentedControl/index.ts +5 -0
- package/src/components/molecules/Slider/Slider.stories.tsx +144 -0
- package/src/components/molecules/Slider/Slider.tsx +303 -0
- package/src/components/molecules/Slider/index.ts +2 -0
- package/src/components/molecules/index.ts +6 -0
- package/src/fonts/ibm-plex-sans-condensed-400-normal.woff2 +0 -0
- package/src/fonts/ibm-plex-sans-condensed-500-normal.woff2 +0 -0
- package/src/fonts/ibm-plex-sans-condensed-600-normal.woff2 +0 -0
- package/src/fonts/ibm-plex-sans-condensed-700-normal.woff2 +0 -0
- package/src/fonts/ida-narrow-500-normal.woff2 +0 -0
- package/src/fonts/ida-narrow-700-normal.woff2 +0 -0
- package/src/fonts/index.ts +49 -0
- package/src/index.ts +9 -0
- package/src/theme/PawprintProvider.tsx +26 -0
- package/src/theme/ThemeProvider.tsx +63 -0
- package/src/theme/index.ts +5 -0
- package/src/theme/theme.ts +3 -0
- package/src/theme/utils.ts +31 -0
- package/src/types/fonts.d.ts +4 -0
- package/src/types/index.ts +1 -0
- package/src/types/theme.ts +24 -0
- package/tsconfig.json +5 -0
- package/tsup.config.ts +11 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, StyleSheet, Text } from "react-native"
|
|
3
|
+
import { Illustration } from "./Illustration"
|
|
4
|
+
import type {
|
|
5
|
+
IllustrationProps,
|
|
6
|
+
PawprintIllustration,
|
|
7
|
+
IllustrationSize
|
|
8
|
+
} from "./Illustration"
|
|
9
|
+
import * as poses from "@butternutbox/pawprint-illustrations/poses"
|
|
10
|
+
import * as usps from "@butternutbox/pawprint-illustrations/usps"
|
|
11
|
+
import * as breeds from "@butternutbox/pawprint-illustrations/breeds"
|
|
12
|
+
import * as breedSizes from "@butternutbox/pawprint-illustrations/breed-sizes"
|
|
13
|
+
|
|
14
|
+
function isComponent(value: unknown): value is PawprintIllustration {
|
|
15
|
+
if (typeof value === "function") return true
|
|
16
|
+
if (value && typeof value === "object" && "$$typeof" in value) return true
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const categorised: Record<string, Record<string, unknown>> = {
|
|
21
|
+
poses,
|
|
22
|
+
usps,
|
|
23
|
+
breeds,
|
|
24
|
+
"breed-sizes": breedSizes
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const illustrationMap: Record<string, PawprintIllustration> = {}
|
|
28
|
+
for (const [category, mod] of Object.entries(categorised)) {
|
|
29
|
+
for (const [name, value] of Object.entries(mod)) {
|
|
30
|
+
if (isComponent(value)) {
|
|
31
|
+
illustrationMap[`${category}/${name}`] = value
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const illustrationNames = Object.keys(illustrationMap)
|
|
37
|
+
const defaultIllustration = illustrationNames[0]!
|
|
38
|
+
|
|
39
|
+
export default {
|
|
40
|
+
title: "Atoms/Illustration",
|
|
41
|
+
component: Illustration,
|
|
42
|
+
argTypes: {
|
|
43
|
+
size: {
|
|
44
|
+
control: { type: "select" },
|
|
45
|
+
options: ["sm", "lg"] as IllustrationSize[],
|
|
46
|
+
description: "Size variant (controls height)"
|
|
47
|
+
},
|
|
48
|
+
illustration: {
|
|
49
|
+
control: { type: "select" },
|
|
50
|
+
options: illustrationNames,
|
|
51
|
+
mapping: illustrationMap,
|
|
52
|
+
description: "Illustration component from pawprint-illustrations"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const Default = (
|
|
58
|
+
args: Omit<IllustrationProps, "illustration"> & { illustration?: string }
|
|
59
|
+
) => (
|
|
60
|
+
<Illustration
|
|
61
|
+
illustration={
|
|
62
|
+
(illustrationMap[args.illustration ?? defaultIllustration] ??
|
|
63
|
+
illustrationMap[defaultIllustration]) as PawprintIllustration
|
|
64
|
+
}
|
|
65
|
+
size={args.size}
|
|
66
|
+
aria-label="Illustration"
|
|
67
|
+
/>
|
|
68
|
+
)
|
|
69
|
+
Default.args = {
|
|
70
|
+
illustration: defaultIllustration,
|
|
71
|
+
size: "sm"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const allSizes: IllustrationSize[] = ["sm", "lg"]
|
|
75
|
+
|
|
76
|
+
export const AllSizes = () => {
|
|
77
|
+
const sample = illustrationMap[defaultIllustration] as PawprintIllustration
|
|
78
|
+
return (
|
|
79
|
+
<View style={styles.row}>
|
|
80
|
+
{allSizes.map((size) => (
|
|
81
|
+
<View key={size} style={styles.cell}>
|
|
82
|
+
<Illustration illustration={sample} size={size} />
|
|
83
|
+
<Text style={styles.sizeLabel}>{size}</Text>
|
|
84
|
+
</View>
|
|
85
|
+
))}
|
|
86
|
+
</View>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const AllIllustrations = () => (
|
|
91
|
+
<View style={styles.column}>
|
|
92
|
+
{Object.entries(categorised).map(([category, mod]) => (
|
|
93
|
+
<View key={category} style={styles.section}>
|
|
94
|
+
<Text style={styles.label}>{category}</Text>
|
|
95
|
+
<View style={styles.grid}>
|
|
96
|
+
{Object.entries(mod)
|
|
97
|
+
.filter(([, value]) => isComponent(value))
|
|
98
|
+
.map(([name, Component]) => (
|
|
99
|
+
<View key={name} style={styles.cell}>
|
|
100
|
+
<Illustration
|
|
101
|
+
illustration={Component as PawprintIllustration}
|
|
102
|
+
size="sm"
|
|
103
|
+
aria-label={name}
|
|
104
|
+
/>
|
|
105
|
+
<Text style={styles.sizeLabel} numberOfLines={1}>
|
|
106
|
+
{name}
|
|
107
|
+
</Text>
|
|
108
|
+
</View>
|
|
109
|
+
))}
|
|
110
|
+
</View>
|
|
111
|
+
</View>
|
|
112
|
+
))}
|
|
113
|
+
</View>
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
export const Playground = (
|
|
117
|
+
args: Omit<IllustrationProps, "illustration"> & { illustration?: string }
|
|
118
|
+
) => (
|
|
119
|
+
<Illustration
|
|
120
|
+
illustration={
|
|
121
|
+
(illustrationMap[args.illustration ?? defaultIllustration] ??
|
|
122
|
+
illustrationMap[defaultIllustration]) as PawprintIllustration
|
|
123
|
+
}
|
|
124
|
+
size={args.size}
|
|
125
|
+
aria-label="Playground illustration"
|
|
126
|
+
/>
|
|
127
|
+
)
|
|
128
|
+
Playground.args = {
|
|
129
|
+
illustration: defaultIllustration,
|
|
130
|
+
size: "sm"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const styles = StyleSheet.create({
|
|
134
|
+
column: {
|
|
135
|
+
flexDirection: "column",
|
|
136
|
+
gap: 24
|
|
137
|
+
},
|
|
138
|
+
section: {
|
|
139
|
+
flexDirection: "column",
|
|
140
|
+
gap: 8
|
|
141
|
+
},
|
|
142
|
+
row: {
|
|
143
|
+
flexDirection: "row",
|
|
144
|
+
gap: 16,
|
|
145
|
+
alignItems: "center"
|
|
146
|
+
},
|
|
147
|
+
grid: {
|
|
148
|
+
flexDirection: "row",
|
|
149
|
+
gap: 12,
|
|
150
|
+
flexWrap: "wrap"
|
|
151
|
+
},
|
|
152
|
+
cell: {
|
|
153
|
+
alignItems: "center",
|
|
154
|
+
gap: 4,
|
|
155
|
+
minWidth: 64
|
|
156
|
+
},
|
|
157
|
+
label: {
|
|
158
|
+
fontSize: 13,
|
|
159
|
+
fontWeight: "600",
|
|
160
|
+
color: "#666"
|
|
161
|
+
},
|
|
162
|
+
sizeLabel: {
|
|
163
|
+
fontSize: 10,
|
|
164
|
+
color: "#999",
|
|
165
|
+
textAlign: "center"
|
|
166
|
+
}
|
|
167
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
|
|
6
|
+
type IllustrationSize = "sm" | "lg"
|
|
7
|
+
|
|
8
|
+
type PawprintIllustration = React.ComponentType<{
|
|
9
|
+
width?: number
|
|
10
|
+
height?: number
|
|
11
|
+
}>
|
|
12
|
+
|
|
13
|
+
type IllustrationOwnProps = {
|
|
14
|
+
illustration: PawprintIllustration
|
|
15
|
+
size?: IllustrationSize
|
|
16
|
+
"aria-label"?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type IllustrationProps = IllustrationOwnProps &
|
|
20
|
+
Omit<ViewProps, keyof IllustrationOwnProps>
|
|
21
|
+
|
|
22
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
23
|
+
|
|
24
|
+
const StyledRoot = styled(View)<{
|
|
25
|
+
illustrationHeight: number
|
|
26
|
+
}>(({ illustrationHeight }) => ({
|
|
27
|
+
alignItems: "center",
|
|
28
|
+
justifyContent: "center",
|
|
29
|
+
flexShrink: 0,
|
|
30
|
+
height: illustrationHeight
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Renders a multi-colour SVG illustration with token-based sizing.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* import { Illustration } from "@butternutbox/pawprint-native"
|
|
39
|
+
* import { DogSittingFilled } from "@butternutbox/pawprint-illustrations/poses"
|
|
40
|
+
*
|
|
41
|
+
* <Illustration illustration={DogSittingFilled} size="sm" aria-label="Dog sitting" />
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @param illustration - **(required)** Illustration component
|
|
45
|
+
* @param size - *(optional)* Size variant: sm (default), lg
|
|
46
|
+
* @param aria-label - *(optional)* Accessible label
|
|
47
|
+
*/
|
|
48
|
+
const Illustration = React.forwardRef<View, IllustrationProps>(
|
|
49
|
+
(
|
|
50
|
+
{
|
|
51
|
+
illustration: IllustrationComponent,
|
|
52
|
+
size = "sm",
|
|
53
|
+
"aria-label": ariaLabel,
|
|
54
|
+
...rest
|
|
55
|
+
},
|
|
56
|
+
ref
|
|
57
|
+
) => {
|
|
58
|
+
const theme = useTheme()
|
|
59
|
+
const dimension = parseTokenValue(
|
|
60
|
+
theme.tokens.components.illustrations.sizing.illustrations[size]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<StyledRoot
|
|
65
|
+
ref={ref}
|
|
66
|
+
illustrationHeight={dimension}
|
|
67
|
+
accessibilityRole={ariaLabel ? "image" : undefined}
|
|
68
|
+
accessibilityLabel={ariaLabel}
|
|
69
|
+
accessible={!!ariaLabel}
|
|
70
|
+
{...rest}
|
|
71
|
+
>
|
|
72
|
+
<IllustrationComponent height={dimension} />
|
|
73
|
+
</StyledRoot>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
Illustration.displayName = "Illustration"
|
|
79
|
+
|
|
80
|
+
export { Illustration }
|
|
81
|
+
export type { IllustrationProps, PawprintIllustration, IllustrationSize }
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React, { useState } from "react"
|
|
2
|
+
import { View, StyleSheet } from "react-native"
|
|
3
|
+
import { Input } from "./Input"
|
|
4
|
+
import type { InputProps } from "./Input"
|
|
5
|
+
import { Typography } from "../Typography"
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: "Atoms/Input",
|
|
9
|
+
component: Input,
|
|
10
|
+
argTypes: {
|
|
11
|
+
label: {
|
|
12
|
+
control: { type: "text" },
|
|
13
|
+
description: "Label text"
|
|
14
|
+
},
|
|
15
|
+
placeholder: {
|
|
16
|
+
control: { type: "text" },
|
|
17
|
+
description: "Placeholder text"
|
|
18
|
+
},
|
|
19
|
+
description: {
|
|
20
|
+
control: { type: "text" },
|
|
21
|
+
description: "Help text below input"
|
|
22
|
+
},
|
|
23
|
+
error: {
|
|
24
|
+
control: { type: "text" },
|
|
25
|
+
description: "Error message"
|
|
26
|
+
},
|
|
27
|
+
state: {
|
|
28
|
+
control: { type: "select" },
|
|
29
|
+
options: ["default", "error", "success"],
|
|
30
|
+
description: "Visual state of the input"
|
|
31
|
+
},
|
|
32
|
+
optionalText: {
|
|
33
|
+
control: { type: "text" },
|
|
34
|
+
description: "Optional indicator next to label"
|
|
35
|
+
},
|
|
36
|
+
disabled: {
|
|
37
|
+
control: { type: "boolean" },
|
|
38
|
+
description: "Prevents interaction"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const Default = (args: InputProps) => <Input {...args} />
|
|
44
|
+
Default.args = {
|
|
45
|
+
label: "Email",
|
|
46
|
+
placeholder: "you@butternutbox.com",
|
|
47
|
+
description: "We'll never share your email",
|
|
48
|
+
state: "default"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const States = () => (
|
|
52
|
+
<View style={styles.column}>
|
|
53
|
+
<View style={styles.section}>
|
|
54
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
55
|
+
Default
|
|
56
|
+
</Typography>
|
|
57
|
+
<Input
|
|
58
|
+
label="Email"
|
|
59
|
+
placeholder="you@butternutbox.com"
|
|
60
|
+
description="We'll never share your email"
|
|
61
|
+
state="default"
|
|
62
|
+
/>
|
|
63
|
+
</View>
|
|
64
|
+
<View style={styles.section}>
|
|
65
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
66
|
+
Error
|
|
67
|
+
</Typography>
|
|
68
|
+
<Input
|
|
69
|
+
label="Email"
|
|
70
|
+
placeholder="you@butternutbox.com"
|
|
71
|
+
error="Please enter a valid email address"
|
|
72
|
+
state="error"
|
|
73
|
+
/>
|
|
74
|
+
</View>
|
|
75
|
+
<View style={styles.section}>
|
|
76
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
77
|
+
Success
|
|
78
|
+
</Typography>
|
|
79
|
+
<Input
|
|
80
|
+
label="Email"
|
|
81
|
+
placeholder="you@butternutbox.com"
|
|
82
|
+
description="Email is valid"
|
|
83
|
+
state="success"
|
|
84
|
+
/>
|
|
85
|
+
</View>
|
|
86
|
+
<View style={styles.section}>
|
|
87
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
88
|
+
Disabled
|
|
89
|
+
</Typography>
|
|
90
|
+
<Input
|
|
91
|
+
label="Email"
|
|
92
|
+
placeholder="you@butternutbox.com"
|
|
93
|
+
editable={false}
|
|
94
|
+
state="default"
|
|
95
|
+
/>
|
|
96
|
+
</View>
|
|
97
|
+
<View style={styles.section}>
|
|
98
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
99
|
+
With optional text
|
|
100
|
+
</Typography>
|
|
101
|
+
<Input
|
|
102
|
+
label="Phone"
|
|
103
|
+
placeholder="07123 456789"
|
|
104
|
+
optionalText="(optional)"
|
|
105
|
+
state="default"
|
|
106
|
+
/>
|
|
107
|
+
</View>
|
|
108
|
+
</View>
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
export const Controlled = () => {
|
|
112
|
+
const [value, setValue] = useState("")
|
|
113
|
+
const hasError = value.length > 0 && !value.includes("@")
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<View style={styles.column}>
|
|
117
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
118
|
+
Controlled: {value || "(empty)"}
|
|
119
|
+
</Typography>
|
|
120
|
+
<Input
|
|
121
|
+
label="Email"
|
|
122
|
+
placeholder="you@butternutbox.com"
|
|
123
|
+
value={value}
|
|
124
|
+
onChangeText={setValue}
|
|
125
|
+
state={hasError ? "error" : "default"}
|
|
126
|
+
error={hasError ? "Must contain @" : undefined}
|
|
127
|
+
description="Type to see validation"
|
|
128
|
+
/>
|
|
129
|
+
</View>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const styles = StyleSheet.create({
|
|
134
|
+
column: {
|
|
135
|
+
flexDirection: "column",
|
|
136
|
+
gap: 24
|
|
137
|
+
},
|
|
138
|
+
section: {
|
|
139
|
+
flexDirection: "column",
|
|
140
|
+
gap: 8
|
|
141
|
+
}
|
|
142
|
+
})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, ViewProps } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import { InputLabel } from "./InputLabel"
|
|
5
|
+
import { InputField, type InputFieldProps, type InputState } from "./InputField"
|
|
6
|
+
import { InputDescription } from "./InputDescription"
|
|
7
|
+
import { InputError } from "./InputError"
|
|
8
|
+
|
|
9
|
+
type InputOwnProps = {
|
|
10
|
+
label?: string
|
|
11
|
+
description?: string
|
|
12
|
+
error?: string
|
|
13
|
+
state?: InputState
|
|
14
|
+
optionalText?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type InputProps = InputOwnProps &
|
|
18
|
+
Omit<InputFieldProps, keyof InputOwnProps> &
|
|
19
|
+
Omit<ViewProps, keyof InputOwnProps>
|
|
20
|
+
|
|
21
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
22
|
+
|
|
23
|
+
const StyledRoot = styled(View)(({ theme }) => {
|
|
24
|
+
const { spacing } = theme.tokens.components.inputs
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
gap: parseTokenValue(spacing.gap)
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Text input component for collecting user data.
|
|
33
|
+
* Supports both a simple props API and a flexible compound component API.
|
|
34
|
+
*
|
|
35
|
+
* **Simple Props API:**
|
|
36
|
+
* @example
|
|
37
|
+
* <Input
|
|
38
|
+
* label="Email"
|
|
39
|
+
* placeholder="you@butternutbox.com"
|
|
40
|
+
* description="We'll never share your email"
|
|
41
|
+
* error="Invalid email address"
|
|
42
|
+
* state="error"
|
|
43
|
+
* optionalText="(optional)"
|
|
44
|
+
* />
|
|
45
|
+
*
|
|
46
|
+
* **Compound Component API:**
|
|
47
|
+
* @example
|
|
48
|
+
* <Input.Root>
|
|
49
|
+
* <Input.Label optionalText="(optional)">Email</Input.Label>
|
|
50
|
+
* <Input.Field placeholder="you@butternutbox.com" />
|
|
51
|
+
* <Input.Description>We'll never share your email</Input.Description>
|
|
52
|
+
* <Input.Error>Invalid email</Input.Error>
|
|
53
|
+
* </Input.Root>
|
|
54
|
+
*/
|
|
55
|
+
const InputRoot = React.forwardRef<View, InputProps>(
|
|
56
|
+
(
|
|
57
|
+
{
|
|
58
|
+
label,
|
|
59
|
+
description,
|
|
60
|
+
error,
|
|
61
|
+
state = "default",
|
|
62
|
+
optionalText,
|
|
63
|
+
children,
|
|
64
|
+
// InputField props
|
|
65
|
+
...inputFieldProps
|
|
66
|
+
},
|
|
67
|
+
ref
|
|
68
|
+
) => {
|
|
69
|
+
// If children are provided, use compound component API
|
|
70
|
+
if (children) {
|
|
71
|
+
return <StyledRoot ref={ref}>{children}</StyledRoot>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Otherwise use Props API
|
|
75
|
+
return (
|
|
76
|
+
<StyledRoot ref={ref}>
|
|
77
|
+
{label && (
|
|
78
|
+
<InputLabel optionalText={optionalText} state={state}>
|
|
79
|
+
{label}
|
|
80
|
+
</InputLabel>
|
|
81
|
+
)}
|
|
82
|
+
<InputField state={state} {...inputFieldProps} />
|
|
83
|
+
{description && (
|
|
84
|
+
<InputDescription state={state}>{description}</InputDescription>
|
|
85
|
+
)}
|
|
86
|
+
{error && state === "error" && <InputError>{error}</InputError>}
|
|
87
|
+
</StyledRoot>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
InputRoot.displayName = "Input"
|
|
93
|
+
|
|
94
|
+
type InputComponent = React.ForwardRefExoticComponent<
|
|
95
|
+
InputProps & React.RefAttributes<View>
|
|
96
|
+
> & {
|
|
97
|
+
Root: typeof StyledRoot
|
|
98
|
+
Label: typeof InputLabel
|
|
99
|
+
Field: typeof InputField
|
|
100
|
+
Description: typeof InputDescription
|
|
101
|
+
Error: typeof InputError
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const Input = Object.assign(InputRoot, {
|
|
105
|
+
Root: StyledRoot,
|
|
106
|
+
Label: InputLabel,
|
|
107
|
+
Field: InputField,
|
|
108
|
+
Description: InputDescription,
|
|
109
|
+
Error: InputError
|
|
110
|
+
}) as InputComponent
|
|
@@ -0,0 +1,49 @@
|
|
|
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 "../Typography"
|
|
6
|
+
import type { InputState } from "./InputField"
|
|
7
|
+
|
|
8
|
+
type InputDescriptionOwnProps = {
|
|
9
|
+
state?: InputState
|
|
10
|
+
children: React.ReactNode
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type InputDescriptionProps = InputDescriptionOwnProps &
|
|
14
|
+
Omit<ViewProps, keyof InputDescriptionOwnProps>
|
|
15
|
+
|
|
16
|
+
const StyledDescriptionRow = styled(View)({
|
|
17
|
+
flexDirection: "row",
|
|
18
|
+
alignItems: "center"
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Description/help text component for Input fields.
|
|
23
|
+
*
|
|
24
|
+
* @param state - Visual state of the input (affects description color for error state)
|
|
25
|
+
* @param children - Description text content
|
|
26
|
+
*/
|
|
27
|
+
export const InputDescription = React.forwardRef<View, InputDescriptionProps>(
|
|
28
|
+
({ state = "default", children, ...rest }, ref) => {
|
|
29
|
+
const theme = useTheme()
|
|
30
|
+
const { colour, description } = theme.tokens.components.inputs
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<StyledDescriptionRow ref={ref} {...rest}>
|
|
34
|
+
<Typography
|
|
35
|
+
token={description.text.default}
|
|
36
|
+
color={
|
|
37
|
+
state === "error"
|
|
38
|
+
? colour.description.error
|
|
39
|
+
: colour.description.default
|
|
40
|
+
}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</Typography>
|
|
44
|
+
</StyledDescriptionRow>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
InputDescription.displayName = "Input.Description"
|
|
@@ -0,0 +1,39 @@
|
|
|
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 "../Typography"
|
|
6
|
+
|
|
7
|
+
export type InputErrorProps = {
|
|
8
|
+
children?: React.ReactNode
|
|
9
|
+
} & ViewProps
|
|
10
|
+
|
|
11
|
+
const StyledErrorRow = styled(View)({
|
|
12
|
+
flexDirection: "row",
|
|
13
|
+
alignItems: "center"
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Error message component for Input fields.
|
|
18
|
+
*
|
|
19
|
+
* @param children - Error message text
|
|
20
|
+
*/
|
|
21
|
+
export const InputError = React.forwardRef<View, InputErrorProps>(
|
|
22
|
+
({ children, ...rest }, ref) => {
|
|
23
|
+
const theme = useTheme()
|
|
24
|
+
const { colour, description } = theme.tokens.components.inputs
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<StyledErrorRow ref={ref} {...rest}>
|
|
28
|
+
<Typography
|
|
29
|
+
token={description.text.default}
|
|
30
|
+
color={colour.description.error}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
</Typography>
|
|
34
|
+
</StyledErrorRow>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
InputError.displayName = "Input.Error"
|