@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.
Files changed (105) hide show
  1. package/.turbo/turbo-build.log +30 -0
  2. package/COMPONENT_GUIDELINES.md +610 -0
  3. package/README.md +72 -0
  4. package/dist/ibm-plex-sans-condensed-400-normal-I2XLJNNB.woff2 +0 -0
  5. package/dist/ibm-plex-sans-condensed-500-normal-IEQBNVGX.woff2 +0 -0
  6. package/dist/ibm-plex-sans-condensed-600-normal-UX5ZU5T6.woff2 +0 -0
  7. package/dist/ibm-plex-sans-condensed-700-normal-4PFYFTSO.woff2 +0 -0
  8. package/dist/ida-narrow-500-normal-C6I2PK4T.woff2 +0 -0
  9. package/dist/ida-narrow-700-normal-UPHPRIN6.woff2 +0 -0
  10. package/dist/index.cjs +2686 -0
  11. package/dist/index.cjs.map +1 -0
  12. package/dist/index.d.cts +780 -0
  13. package/dist/index.d.ts +780 -0
  14. package/dist/index.js +2617 -0
  15. package/dist/index.js.map +1 -0
  16. package/eslint.config.js +3 -0
  17. package/llms.txt +458 -0
  18. package/package.json +57 -0
  19. package/src/components/atoms/Avatar/Avatar.stories.tsx +125 -0
  20. package/src/components/atoms/Avatar/Avatar.tsx +159 -0
  21. package/src/components/atoms/Avatar/index.ts +7 -0
  22. package/src/components/atoms/Badge/Badge.stories.tsx +231 -0
  23. package/src/components/atoms/Badge/Badge.tsx +184 -0
  24. package/src/components/atoms/Badge/index.ts +2 -0
  25. package/src/components/atoms/Button/Button.stories.tsx +145 -0
  26. package/src/components/atoms/Button/Button.tsx +261 -0
  27. package/src/components/atoms/Button/index.ts +7 -0
  28. package/src/components/atoms/Hint/Hint.stories.tsx +84 -0
  29. package/src/components/atoms/Hint/Hint.tsx +59 -0
  30. package/src/components/atoms/Hint/index.ts +2 -0
  31. package/src/components/atoms/Icon/Icon.stories.tsx +200 -0
  32. package/src/components/atoms/Icon/Icon.tsx +112 -0
  33. package/src/components/atoms/Icon/index.ts +8 -0
  34. package/src/components/atoms/IconButton/IconButton.stories.tsx +162 -0
  35. package/src/components/atoms/IconButton/IconButton.tsx +227 -0
  36. package/src/components/atoms/IconButton/index.ts +7 -0
  37. package/src/components/atoms/Illustration/Illustration.stories.tsx +167 -0
  38. package/src/components/atoms/Illustration/Illustration.tsx +81 -0
  39. package/src/components/atoms/Illustration/index.ts +6 -0
  40. package/src/components/atoms/Input/Input.stories.tsx +142 -0
  41. package/src/components/atoms/Input/Input.tsx +110 -0
  42. package/src/components/atoms/Input/InputDescription.tsx +49 -0
  43. package/src/components/atoms/Input/InputError.tsx +39 -0
  44. package/src/components/atoms/Input/InputField.tsx +119 -0
  45. package/src/components/atoms/Input/InputLabel.tsx +61 -0
  46. package/src/components/atoms/Input/index.ts +10 -0
  47. package/src/components/atoms/Link/Link.stories.tsx +119 -0
  48. package/src/components/atoms/Link/Link.tsx +118 -0
  49. package/src/components/atoms/Link/index.ts +2 -0
  50. package/src/components/atoms/Logo/Logo.registry.ts +39 -0
  51. package/src/components/atoms/Logo/Logo.tsx +68 -0
  52. package/src/components/atoms/Logo/index.ts +4 -0
  53. package/src/components/atoms/Spinner/Spinner.stories.tsx +98 -0
  54. package/src/components/atoms/Spinner/Spinner.tsx +91 -0
  55. package/src/components/atoms/Spinner/index.ts +2 -0
  56. package/src/components/atoms/Switch/Switch.stories.tsx +120 -0
  57. package/src/components/atoms/Switch/Switch.tsx +196 -0
  58. package/src/components/atoms/Switch/index.ts +2 -0
  59. package/src/components/atoms/Tag/Tag.stories.tsx +89 -0
  60. package/src/components/atoms/Tag/Tag.tsx +122 -0
  61. package/src/components/atoms/Tag/index.ts +2 -0
  62. package/src/components/atoms/Typography/Typography.stories.tsx +315 -0
  63. package/src/components/atoms/Typography/Typography.tsx +284 -0
  64. package/src/components/atoms/Typography/index.ts +2 -0
  65. package/src/components/atoms/index.ts +14 -0
  66. package/src/components/index.ts +2 -0
  67. package/src/components/molecules/ButtonDock/ButtonDock.stories.tsx +95 -0
  68. package/src/components/molecules/ButtonDock/ButtonDock.tsx +148 -0
  69. package/src/components/molecules/ButtonDock/index.ts +2 -0
  70. package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +82 -0
  71. package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +94 -0
  72. package/src/components/molecules/ButtonGroup/index.ts +2 -0
  73. package/src/components/molecules/Checkbox/Checkbox.stories.tsx +148 -0
  74. package/src/components/molecules/Checkbox/Checkbox.tsx +279 -0
  75. package/src/components/molecules/Checkbox/CheckboxGroup.tsx +53 -0
  76. package/src/components/molecules/Checkbox/index.ts +4 -0
  77. package/src/components/molecules/Radio/Radio.stories.tsx +182 -0
  78. package/src/components/molecules/Radio/Radio.tsx +249 -0
  79. package/src/components/molecules/Radio/RadioGroup.tsx +142 -0
  80. package/src/components/molecules/Radio/index.ts +4 -0
  81. package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +151 -0
  82. package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +323 -0
  83. package/src/components/molecules/SegmentedControl/index.ts +5 -0
  84. package/src/components/molecules/Slider/Slider.stories.tsx +144 -0
  85. package/src/components/molecules/Slider/Slider.tsx +303 -0
  86. package/src/components/molecules/Slider/index.ts +2 -0
  87. package/src/components/molecules/index.ts +6 -0
  88. package/src/fonts/ibm-plex-sans-condensed-400-normal.woff2 +0 -0
  89. package/src/fonts/ibm-plex-sans-condensed-500-normal.woff2 +0 -0
  90. package/src/fonts/ibm-plex-sans-condensed-600-normal.woff2 +0 -0
  91. package/src/fonts/ibm-plex-sans-condensed-700-normal.woff2 +0 -0
  92. package/src/fonts/ida-narrow-500-normal.woff2 +0 -0
  93. package/src/fonts/ida-narrow-700-normal.woff2 +0 -0
  94. package/src/fonts/index.ts +49 -0
  95. package/src/index.ts +9 -0
  96. package/src/theme/PawprintProvider.tsx +26 -0
  97. package/src/theme/ThemeProvider.tsx +63 -0
  98. package/src/theme/index.ts +5 -0
  99. package/src/theme/theme.ts +3 -0
  100. package/src/theme/utils.ts +31 -0
  101. package/src/types/fonts.d.ts +4 -0
  102. package/src/types/index.ts +1 -0
  103. package/src/types/theme.ts +24 -0
  104. package/tsconfig.json +5 -0
  105. package/tsup.config.ts +11 -0
@@ -0,0 +1,7 @@
1
+ export { IconButton } from "./IconButton"
2
+ export type {
3
+ IconButtonProps,
4
+ IconButtonVariant,
5
+ IconButtonSize,
6
+ IconButtonColour
7
+ } from "./IconButton"
@@ -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,6 @@
1
+ export { Illustration } from "./Illustration"
2
+ export type {
3
+ IllustrationProps,
4
+ PawprintIllustration,
5
+ IllustrationSize
6
+ } from "./Illustration"
@@ -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"