@praxiis/ui 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/dist/index.d.mts +52556 -0
- package/dist/index.d.ts +52556 -0
- package/dist/index.js +8753 -0
- package/dist/index.mjs +8777 -0
- package/package.json +70 -0
- package/src/__test-utils__/index.tsx +39 -0
- package/src/components/CalendarStrip/CalendarStrip.helpers.ts +106 -0
- package/src/components/CalendarStrip/CalendarStrip.tsx +83 -0
- package/src/components/CalendarStrip/CalendarStrip.types.ts +133 -0
- package/src/components/CalendarStrip/DayCard/DayCard.helpers.ts +44 -0
- package/src/components/CalendarStrip/DayCard/DayCard.tsx +71 -0
- package/src/components/CalendarStrip/DayCard/DayCard.types.ts +134 -0
- package/src/components/CalendarStrip/DayCard/index.ts +2 -0
- package/src/components/CalendarStrip/DayCard/useDayCardLogic.ts +45 -0
- package/src/components/CalendarStrip/index.ts +9 -0
- package/src/components/CalendarStrip/useCalendarStripLogic.ts +53 -0
- package/src/components/EmptyState/EmptyState.helpers.ts +104 -0
- package/src/components/EmptyState/EmptyState.tsx +205 -0
- package/src/components/EmptyState/EmptyState.types.ts +213 -0
- package/src/components/EmptyState/index.ts +44 -0
- package/src/components/EmptyState/useEmptyStateLogic.ts +131 -0
- package/src/components/Header/Header.helpers.ts +93 -0
- package/src/components/Header/Header.tsx +185 -0
- package/src/components/Header/Header.types.ts +153 -0
- package/src/components/Header/index.ts +44 -0
- package/src/components/Header/useHeaderLogic.ts +146 -0
- package/src/components/ScheduleItem/ScheduleItem/ScheduleItem.helpers.ts +50 -0
- package/src/components/ScheduleItem/ScheduleItem/ScheduleItem.tsx +78 -0
- package/src/components/ScheduleItem/ScheduleItem/ScheduleItem.types.ts +99 -0
- package/src/components/ScheduleItem/ScheduleItem/index.ts +16 -0
- package/src/components/ScheduleItem/ScheduleItem/useScheduleItemLogic.ts +31 -0
- package/src/components/ScheduleItem/index.ts +15 -0
- package/src/components/index.ts +40 -0
- package/src/core/index.ts +34 -0
- package/src/core/restyle/RestyleThemeProviderWrapper.tsx +31 -0
- package/src/core/restyle/index.ts +38 -0
- package/src/core/restyle/restylePresetRegistry.ts +195 -0
- package/src/core/restyle/restyleTheme.ts +1352 -0
- package/src/core/restyle/restyleTypes.ts +8 -0
- package/src/core/restyle/useRestyleTheme.ts +10 -0
- package/src/hooks/animations/index.ts +3 -0
- package/src/hooks/animations/useAnimatedValue.ts +10 -0
- package/src/hooks/animations/useEntranceAnimation.ts +106 -0
- package/src/hooks/animations/usePulseAnimation.ts +63 -0
- package/src/hooks/index.ts +30 -0
- package/src/hooks/useReducedMotion.ts +60 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/labels/en.ts +120 -0
- package/src/i18n/labels/es.ts +120 -0
- package/src/i18n/labels/index.ts +6 -0
- package/src/i18n/labels/types.ts +165 -0
- package/src/index.tsx +215 -0
- package/src/primitives/actions/Button/Button.helpers.ts +243 -0
- package/src/primitives/actions/Button/Button.tsx +198 -0
- package/src/primitives/actions/Button/Button.types.ts +207 -0
- package/src/primitives/actions/Button/index.ts +41 -0
- package/src/primitives/actions/Button/useButtonLogic.ts +160 -0
- package/src/primitives/actions/IconButton/IconButton.helpers.ts +235 -0
- package/src/primitives/actions/IconButton/IconButton.tsx +177 -0
- package/src/primitives/actions/IconButton/IconButton.types.ts +273 -0
- package/src/primitives/actions/IconButton/index.ts +30 -0
- package/src/primitives/actions/IconButton/useIconButtonLogic.ts +172 -0
- package/src/primitives/actions/index.ts +20 -0
- package/src/primitives/content/Avatar/Avatar.helpers.ts +177 -0
- package/src/primitives/content/Avatar/Avatar.tsx +199 -0
- package/src/primitives/content/Avatar/Avatar.types.ts +222 -0
- package/src/primitives/content/Avatar/index.ts +46 -0
- package/src/primitives/content/Avatar/useAvatarLogic.ts +149 -0
- package/src/primitives/content/Badge/Badge.helpers.ts +175 -0
- package/src/primitives/content/Badge/Badge.tsx +174 -0
- package/src/primitives/content/Badge/Badge.types.ts +223 -0
- package/src/primitives/content/Badge/index.ts +40 -0
- package/src/primitives/content/Badge/useBadgeLogic.ts +128 -0
- package/src/primitives/content/Card/Card.helpers.ts +27 -0
- package/src/primitives/content/Card/Card.tsx +123 -0
- package/src/primitives/content/Card/Card.types.ts +95 -0
- package/src/primitives/content/Card/index.ts +20 -0
- package/src/primitives/content/Card/useCardLogic.ts +48 -0
- package/src/primitives/content/Chip/Chip.helpers.ts +304 -0
- package/src/primitives/content/Chip/Chip.tsx +205 -0
- package/src/primitives/content/Chip/Chip.types.ts +234 -0
- package/src/primitives/content/Chip/index.ts +47 -0
- package/src/primitives/content/Chip/useChipLogic.ts +167 -0
- package/src/primitives/content/Icon/Icon.helpers.ts +54 -0
- package/src/primitives/content/Icon/Icon.tsx +110 -0
- package/src/primitives/content/Icon/Icon.types.ts +95 -0
- package/src/primitives/content/Icon/index.ts +20 -0
- package/src/primitives/content/Icon/useIconLogic.ts +73 -0
- package/src/primitives/content/index.ts +45 -0
- package/src/primitives/feedback/ProgressBar/ProgressBar.helpers.ts +122 -0
- package/src/primitives/feedback/ProgressBar/ProgressBar.tsx +154 -0
- package/src/primitives/feedback/ProgressBar/ProgressBar.types.ts +178 -0
- package/src/primitives/feedback/ProgressBar/index.ts +17 -0
- package/src/primitives/feedback/ProgressBar/useProgressBarLogic.ts +120 -0
- package/src/primitives/feedback/Skeleton/Skeleton.helpers.ts +145 -0
- package/src/primitives/feedback/Skeleton/Skeleton.tsx +155 -0
- package/src/primitives/feedback/Skeleton/Skeleton.types.ts +223 -0
- package/src/primitives/feedback/Skeleton/index.ts +44 -0
- package/src/primitives/feedback/Skeleton/useSkeletonLogic.ts +125 -0
- package/src/primitives/feedback/Spinner/Spinner.helpers.ts +40 -0
- package/src/primitives/feedback/Spinner/Spinner.tsx +105 -0
- package/src/primitives/feedback/Spinner/Spinner.types.ts +114 -0
- package/src/primitives/feedback/Spinner/index.ts +18 -0
- package/src/primitives/feedback/Spinner/useSpinnerLogic.ts +84 -0
- package/src/primitives/feedback/Toast/Toast.helpers.ts +163 -0
- package/src/primitives/feedback/Toast/Toast.tsx +190 -0
- package/src/primitives/feedback/Toast/Toast.types.ts +270 -0
- package/src/primitives/feedback/Toast/ToastContext.tsx +96 -0
- package/src/primitives/feedback/Toast/ToastProvider.tsx +241 -0
- package/src/primitives/feedback/Toast/index.ts +59 -0
- package/src/primitives/feedback/Toast/useToastLogic.ts +112 -0
- package/src/primitives/feedback/index.ts +45 -0
- package/src/primitives/index.ts +158 -0
- package/src/primitives/inputs/Checkbox/Checkbox.helpers.ts +132 -0
- package/src/primitives/inputs/Checkbox/Checkbox.tsx +150 -0
- package/src/primitives/inputs/Checkbox/Checkbox.types.ts +106 -0
- package/src/primitives/inputs/Checkbox/index.ts +30 -0
- package/src/primitives/inputs/Checkbox/useCheckboxLogic.ts +121 -0
- package/src/primitives/inputs/RadioButton/RadioButton.helpers.ts +123 -0
- package/src/primitives/inputs/RadioButton/RadioButton.tsx +159 -0
- package/src/primitives/inputs/RadioButton/RadioButton.types.ts +106 -0
- package/src/primitives/inputs/RadioButton/index.ts +25 -0
- package/src/primitives/inputs/RadioButton/useRadioButtonLogic.ts +117 -0
- package/src/primitives/inputs/SegmentedControl/SegmentedControl.helpers.ts +174 -0
- package/src/primitives/inputs/SegmentedControl/SegmentedControl.tsx +224 -0
- package/src/primitives/inputs/SegmentedControl/SegmentedControl.types.ts +187 -0
- package/src/primitives/inputs/SegmentedControl/index.ts +39 -0
- package/src/primitives/inputs/SegmentedControl/useSegmentedControlLogic.ts +151 -0
- package/src/primitives/inputs/SelectSheet/SelectSheet.helpers.ts +147 -0
- package/src/primitives/inputs/SelectSheet/SelectSheet.tsx +247 -0
- package/src/primitives/inputs/SelectSheet/SelectSheet.types.ts +196 -0
- package/src/primitives/inputs/SelectSheet/SelectSheetOption.tsx +177 -0
- package/src/primitives/inputs/SelectSheet/index.ts +48 -0
- package/src/primitives/inputs/SelectSheet/useSelectSheetLogic.ts +309 -0
- package/src/primitives/inputs/Switch/Switch.helpers.ts +109 -0
- package/src/primitives/inputs/Switch/Switch.tsx +191 -0
- package/src/primitives/inputs/Switch/Switch.types.ts +154 -0
- package/src/primitives/inputs/Switch/index.ts +40 -0
- package/src/primitives/inputs/Switch/useSwitchLogic.ts +192 -0
- package/src/primitives/inputs/TextInput/TextInput.helpers.ts +206 -0
- package/src/primitives/inputs/TextInput/TextInput.tsx +392 -0
- package/src/primitives/inputs/TextInput/TextInput.types.ts +216 -0
- package/src/primitives/inputs/TextInput/index.ts +37 -0
- package/src/primitives/inputs/TextInput/useTextInputLogic.ts +195 -0
- package/src/primitives/inputs/index.ts +52 -0
- package/src/primitives/layout/AnimatedBox.tsx +44 -0
- package/src/primitives/layout/Box.tsx +71 -0
- package/src/primitives/layout/Divider/Divider.helpers.ts +115 -0
- package/src/primitives/layout/Divider/Divider.tsx +139 -0
- package/src/primitives/layout/Divider/Divider.types.ts +178 -0
- package/src/primitives/layout/Divider/index.ts +24 -0
- package/src/primitives/layout/Divider/useDividerLogic.ts +109 -0
- package/src/primitives/layout/FlatList.tsx +66 -0
- package/src/primitives/layout/Pressable.tsx +74 -0
- package/src/primitives/layout/ScrollView.tsx +63 -0
- package/src/primitives/layout/Stack.tsx +69 -0
- package/src/primitives/layout/index.ts +40 -0
- package/src/primitives/navigation/index.ts +6 -0
- package/src/primitives/overlays/Modal/Modal.helpers.ts +31 -0
- package/src/primitives/overlays/Modal/Modal.tsx +264 -0
- package/src/primitives/overlays/Modal/Modal.types.ts +193 -0
- package/src/primitives/overlays/Modal/index.ts +43 -0
- package/src/primitives/overlays/Modal/useModalLogic.ts +103 -0
- package/src/primitives/overlays/index.ts +12 -0
- package/src/primitives/typography/Text.tsx +51 -0
- package/src/primitives/typography/index.ts +1 -0
- package/src/provider/DesignSystemContext.ts +22 -0
- package/src/provider/DesignSystemProvider.tsx +121 -0
- package/src/provider/index.ts +7 -0
- package/src/providers/ThemeProvider/createTheme.ts +304 -0
- package/src/providers/ThemeProvider/defaultTheme.ts +70 -0
- package/src/providers/ThemeProvider/index.ts +34 -0
- package/src/providers/ThemeProvider/types.ts +249 -0
- package/src/providers/index.ts +29 -0
- package/src/tokens/colors.ts +371 -0
- package/src/tokens/index.ts +145 -0
- package/src/tokens/motion.ts +176 -0
- package/src/tokens/radii.ts +82 -0
- package/src/tokens/scales.ts +588 -0
- package/src/tokens/shadows.ts +190 -0
- package/src/tokens/spacing.ts +140 -0
- package/src/tokens/tokens.json +207 -0
- package/src/tokens/typography.ts +251 -0
- package/src/types.ts +50 -0
- package/src/utils/accessibility.ts +169 -0
- package/src/utils/index.ts +25 -0
- package/src/utils/platform.ts +72 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TextInput Component Helpers
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for computing TextInput sizes, styles, and mappings.
|
|
5
|
+
*
|
|
6
|
+
* ## Size Configuration
|
|
7
|
+
* | Size | Height | Font | Icon | Padding |
|
|
8
|
+
* |------|--------|------|------|---------|
|
|
9
|
+
* | sm | 40px | 14px | 18px | 12px |
|
|
10
|
+
* | md | 48px | 16px | 20px | 16px |
|
|
11
|
+
* | lg | 56px | 17px | 24px | 16px |
|
|
12
|
+
*
|
|
13
|
+
* ## Text Variant Mapping
|
|
14
|
+
* | Input Size | Text Variant | Label Variant |
|
|
15
|
+
* |------------|--------------|---------------|
|
|
16
|
+
* | sm | bodySmall | labelSmall |
|
|
17
|
+
* | md | bodyMedium | labelSmall |
|
|
18
|
+
* | lg | bodyLarge | labelMedium |
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { IconSize } from "../../content/Icon";
|
|
22
|
+
import { BaseThemeColor, RestyleColor } from "../../../types";
|
|
23
|
+
import {
|
|
24
|
+
ResolveTextInputStyleParams,
|
|
25
|
+
TextInputSize,
|
|
26
|
+
TextInputSizeConfig,
|
|
27
|
+
TextInputStyleResult,
|
|
28
|
+
TextInputTextVariant,
|
|
29
|
+
} from "./TextInput.types";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* TextInput size configuration mapping.
|
|
33
|
+
*
|
|
34
|
+
* Height follows a consistent scale:
|
|
35
|
+
* - sm: 40px - Compact forms
|
|
36
|
+
* - md: 48px - Default mobile size (meets 44px touch target)
|
|
37
|
+
* - lg: 56px - Tablets, emphasized inputs
|
|
38
|
+
*/
|
|
39
|
+
const TEXT_INPUT_SIZE_CONFIG: Record<TextInputSize, TextInputSizeConfig> = {
|
|
40
|
+
sm: {
|
|
41
|
+
height: 40,
|
|
42
|
+
paddingHorizontal: 12,
|
|
43
|
+
fontSize: 14,
|
|
44
|
+
iconSize: 18,
|
|
45
|
+
},
|
|
46
|
+
md: {
|
|
47
|
+
height: 48,
|
|
48
|
+
paddingHorizontal: 16,
|
|
49
|
+
fontSize: 16,
|
|
50
|
+
iconSize: 20,
|
|
51
|
+
},
|
|
52
|
+
lg: {
|
|
53
|
+
height: 56,
|
|
54
|
+
paddingHorizontal: 16,
|
|
55
|
+
fontSize: 17,
|
|
56
|
+
iconSize: 24,
|
|
57
|
+
},
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Text variant mapping for input text.
|
|
62
|
+
*
|
|
63
|
+
* - sm input → bodySmall text
|
|
64
|
+
* - md input → bodyMedium text
|
|
65
|
+
* - lg input → bodyLarge text
|
|
66
|
+
*/
|
|
67
|
+
const TEXT_INPUT_TEXT_VARIANTS: Record<TextInputSize, TextInputTextVariant> = {
|
|
68
|
+
sm: "bodySmall",
|
|
69
|
+
md: "bodyMedium",
|
|
70
|
+
lg: "bodyLarge",
|
|
71
|
+
} as const;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Text variant mapping for labels.
|
|
75
|
+
*
|
|
76
|
+
* - sm input → labelSmall label
|
|
77
|
+
* - md input → labelSmall label
|
|
78
|
+
* - lg input → labelMedium label
|
|
79
|
+
*/
|
|
80
|
+
const TEXT_INPUT_LABEL_VARIANTS: Record<TextInputSize, TextInputTextVariant> = {
|
|
81
|
+
sm: "labelSmall",
|
|
82
|
+
md: "labelSmall",
|
|
83
|
+
lg: "labelMedium",
|
|
84
|
+
} as const;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Icon size mapping - derives icon size from input size.
|
|
88
|
+
* Icons should match text size for visual balance.
|
|
89
|
+
*
|
|
90
|
+
* - sm input → sm icon (16px)
|
|
91
|
+
* - md input → md icon (20px)
|
|
92
|
+
* - lg input → lg icon (24px)
|
|
93
|
+
*/
|
|
94
|
+
const SIZE_TO_ICON_SIZE: Record<TextInputSize, IconSize> = {
|
|
95
|
+
sm: "sm",
|
|
96
|
+
md: "md",
|
|
97
|
+
lg: "lg",
|
|
98
|
+
} as const;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get size configuration based on input size.
|
|
102
|
+
*
|
|
103
|
+
* @param size - The input size variant
|
|
104
|
+
* @returns Size configuration object
|
|
105
|
+
*/
|
|
106
|
+
export function getTextInputSizeConfig(size: TextInputSize): TextInputSizeConfig {
|
|
107
|
+
return TEXT_INPUT_SIZE_CONFIG[size] || TEXT_INPUT_SIZE_CONFIG.md;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get text variant for input text based on size.
|
|
112
|
+
*
|
|
113
|
+
* @param size - The input size variant
|
|
114
|
+
* @returns Text variant key
|
|
115
|
+
*/
|
|
116
|
+
export function getTextInputTextVariant(size: TextInputSize): TextInputTextVariant {
|
|
117
|
+
return TEXT_INPUT_TEXT_VARIANTS[size] || TEXT_INPUT_TEXT_VARIANTS.md;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get text variant for label based on size.
|
|
122
|
+
*
|
|
123
|
+
* @param size - The input size variant
|
|
124
|
+
* @returns Text variant key for label
|
|
125
|
+
*/
|
|
126
|
+
export function getTextInputLabelVariant(size: TextInputSize): TextInputTextVariant {
|
|
127
|
+
return TEXT_INPUT_LABEL_VARIANTS[size] || TEXT_INPUT_LABEL_VARIANTS.md;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get icon size based on input size.
|
|
132
|
+
*
|
|
133
|
+
* @param size - The input size variant
|
|
134
|
+
* @returns Icon size key
|
|
135
|
+
*/
|
|
136
|
+
export function getTextInputIconSize(size: TextInputSize): IconSize {
|
|
137
|
+
return SIZE_TO_ICON_SIZE[size];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get icon color based on error and focus states.
|
|
142
|
+
*
|
|
143
|
+
* @param hasError - Whether the input has an error
|
|
144
|
+
* @param isFocused - Whether the input is focused
|
|
145
|
+
* @param color - The primary color
|
|
146
|
+
* @returns Icon color key
|
|
147
|
+
*/
|
|
148
|
+
export function getTextInputIconColor(
|
|
149
|
+
hasError: boolean,
|
|
150
|
+
isFocused: boolean,
|
|
151
|
+
color: RestyleColor
|
|
152
|
+
): RestyleColor {
|
|
153
|
+
if (hasError) {
|
|
154
|
+
return "feedbackError";
|
|
155
|
+
}
|
|
156
|
+
if (isFocused) {
|
|
157
|
+
return color;
|
|
158
|
+
}
|
|
159
|
+
return "textTertiary";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Resolve dynamic style properties for the input container.
|
|
164
|
+
*
|
|
165
|
+
* @param params - Style resolution parameters
|
|
166
|
+
* @returns Computed styles for the input container
|
|
167
|
+
*/
|
|
168
|
+
export function resolveTextInputStyle(
|
|
169
|
+
params: ResolveTextInputStyleParams
|
|
170
|
+
): TextInputStyleResult {
|
|
171
|
+
const { variant, disabled, error, isFocused, color, theme } = params;
|
|
172
|
+
|
|
173
|
+
const hasError = !!error;
|
|
174
|
+
|
|
175
|
+
// Background color
|
|
176
|
+
let backgroundColor: string;
|
|
177
|
+
if (disabled) {
|
|
178
|
+
backgroundColor = theme.colors.interactiveDisabled;
|
|
179
|
+
} else if (variant === "filled") {
|
|
180
|
+
backgroundColor = theme.colors.backgroundSecondary;
|
|
181
|
+
} else {
|
|
182
|
+
backgroundColor = theme.colors.surfacePrimary;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Border color
|
|
186
|
+
let borderColor: string;
|
|
187
|
+
if (disabled) {
|
|
188
|
+
borderColor = theme.colors.borderDefault;
|
|
189
|
+
} else if (hasError) {
|
|
190
|
+
borderColor = theme.colors.feedbackError;
|
|
191
|
+
} else if (isFocused) {
|
|
192
|
+
borderColor = theme.colors[color as BaseThemeColor];
|
|
193
|
+
} else {
|
|
194
|
+
borderColor = theme.colors.borderDefault;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Border width
|
|
198
|
+
const borderWidth = isFocused ? 2 : 1;
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
backgroundColor,
|
|
202
|
+
borderColor,
|
|
203
|
+
borderWidth,
|
|
204
|
+
opacity: 1,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TextInput Component
|
|
3
|
+
*
|
|
4
|
+
* @description Text entry field with labels and validation - Molecule
|
|
5
|
+
*
|
|
6
|
+
* TextInput combines multiple atoms (Icon, Text, Box) into a
|
|
7
|
+
* complete form field with label, input, helper text, and error states.
|
|
8
|
+
*
|
|
9
|
+
* ## Size Scale
|
|
10
|
+
* | Size | Height | Font | Icon | Padding |
|
|
11
|
+
* |------|--------|------|------|---------|
|
|
12
|
+
* | sm | 40px | 14px | 18px | 12px |
|
|
13
|
+
* | md | 48px | 16px | 20px | 16px |
|
|
14
|
+
* | lg | 56px | 17px | 24px | 16px |
|
|
15
|
+
*
|
|
16
|
+
* ## Variants
|
|
17
|
+
* - `outlined`: Border with transparent/white background (default)
|
|
18
|
+
* - `filled`: Filled background with subtle border
|
|
19
|
+
* - `textarea`: Multi-line with configurable height
|
|
20
|
+
*
|
|
21
|
+
* ## Visual States
|
|
22
|
+
* - Default: Gray border
|
|
23
|
+
* - Focused: Colored border (2px), colored label
|
|
24
|
+
* - Error: Red border, red label, error message
|
|
25
|
+
* - Disabled: Reduced opacity (0.5)
|
|
26
|
+
*
|
|
27
|
+
* ## Features
|
|
28
|
+
* - Responsive size prop (phone/tablet breakpoints)
|
|
29
|
+
* - Optional left and right icons
|
|
30
|
+
* - Label with required indicator
|
|
31
|
+
* - Helper text and error states
|
|
32
|
+
* - Character count display
|
|
33
|
+
* - Full accessibility support
|
|
34
|
+
* - Textarea with cursor at top
|
|
35
|
+
*
|
|
36
|
+
* @performance
|
|
37
|
+
* - Wrapped with React.memo() to prevent unnecessary re-renders
|
|
38
|
+
* - Uses useMemo() for expensive calculations (style resolution, state-based colors)
|
|
39
|
+
* - Border width and color calculations cached per state change
|
|
40
|
+
* - Icon sizing optimized with lookup tables for instant resolution
|
|
41
|
+
*
|
|
42
|
+
* @see TextInput.types.ts - Type definitions
|
|
43
|
+
* @see TextInput.helpers.ts - Size and style functions
|
|
44
|
+
* @see TextInput.a11y.ts - Accessibility prop generation
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Basic usage
|
|
48
|
+
* <TextInput label="Email" value={email} onChangeText={setEmail} />
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* // With icons and validation
|
|
52
|
+
* <TextInput
|
|
53
|
+
* label="Email"
|
|
54
|
+
* value={email}
|
|
55
|
+
* onChangeText={setEmail}
|
|
56
|
+
* leftIconName="email"
|
|
57
|
+
* rightIconName={isValid ? "check-circle" : undefined}
|
|
58
|
+
* error={emailError}
|
|
59
|
+
* helperText="Enter your email address"
|
|
60
|
+
* required
|
|
61
|
+
* />
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* // Textarea variant
|
|
65
|
+
* <TextInput
|
|
66
|
+
* label="Description"
|
|
67
|
+
* variant="textarea"
|
|
68
|
+
* textareaHeight={120}
|
|
69
|
+
* value={description}
|
|
70
|
+
* onChangeText={setDescription}
|
|
71
|
+
* />
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
import {
|
|
75
|
+
backgroundColor,
|
|
76
|
+
border,
|
|
77
|
+
createRestyleComponent,
|
|
78
|
+
layout,
|
|
79
|
+
spacing,
|
|
80
|
+
spacingShorthand,
|
|
81
|
+
useResponsiveProp,
|
|
82
|
+
} from "@shopify/restyle";
|
|
83
|
+
import React, { forwardRef, memo, useCallback, useMemo, useState } from "react";
|
|
84
|
+
import {
|
|
85
|
+
NativeSyntheticEvent,
|
|
86
|
+
TextInput as RNTextInput,
|
|
87
|
+
TargetedEvent,
|
|
88
|
+
StyleSheet,
|
|
89
|
+
} from "react-native";
|
|
90
|
+
import { RestyleTheme, useRestyleTheme } from "../../../core/restyle";
|
|
91
|
+
import { BaseThemeColor } from "../../../types";
|
|
92
|
+
import { Icon } from "../../content/Icon";
|
|
93
|
+
import { Box } from "../../layout";
|
|
94
|
+
import { Text } from "../../typography";
|
|
95
|
+
import {
|
|
96
|
+
getTextInputLabelVariant,
|
|
97
|
+
getTextInputSizeConfig,
|
|
98
|
+
} from "./TextInput.helpers";
|
|
99
|
+
import { BaseTextInputContainerProps, TextInputProps, TextInputSize } from "./TextInput.types";
|
|
100
|
+
import { useTextInputLogic } from "./useTextInputLogic";
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Base container for the input field with Restyle support.
|
|
104
|
+
* @internal
|
|
105
|
+
*/
|
|
106
|
+
const BaseTextInputContainer = createRestyleComponent<
|
|
107
|
+
BaseTextInputContainerProps,
|
|
108
|
+
RestyleTheme
|
|
109
|
+
>([backgroundColor, border, layout, spacing, spacingShorthand], Box);
|
|
110
|
+
|
|
111
|
+
const TextInputComponent = forwardRef<RNTextInput, TextInputProps>(
|
|
112
|
+
function TextInputComponent({
|
|
113
|
+
label,
|
|
114
|
+
helperText,
|
|
115
|
+
error,
|
|
116
|
+
size = "md",
|
|
117
|
+
variant = "outlined",
|
|
118
|
+
leftIconName,
|
|
119
|
+
rightIconName,
|
|
120
|
+
disabled = false,
|
|
121
|
+
required = false,
|
|
122
|
+
showCharCount = false,
|
|
123
|
+
maxLength,
|
|
124
|
+
value = "",
|
|
125
|
+
color = "accentPrimary",
|
|
126
|
+
onFocus,
|
|
127
|
+
onBlur,
|
|
128
|
+
containerStyle,
|
|
129
|
+
inputContainerStyle,
|
|
130
|
+
testID,
|
|
131
|
+
accessibilityLabel,
|
|
132
|
+
accessibilityHint,
|
|
133
|
+
textareaHeight = 120,
|
|
134
|
+
// Extract BoxProps for outer container
|
|
135
|
+
margin,
|
|
136
|
+
marginTop,
|
|
137
|
+
marginBottom,
|
|
138
|
+
marginLeft,
|
|
139
|
+
marginRight,
|
|
140
|
+
marginHorizontal,
|
|
141
|
+
marginVertical,
|
|
142
|
+
padding,
|
|
143
|
+
paddingTop,
|
|
144
|
+
paddingBottom,
|
|
145
|
+
paddingLeft,
|
|
146
|
+
paddingRight,
|
|
147
|
+
paddingHorizontal,
|
|
148
|
+
paddingVertical,
|
|
149
|
+
flex,
|
|
150
|
+
flexGrow,
|
|
151
|
+
flexShrink,
|
|
152
|
+
width,
|
|
153
|
+
minWidth,
|
|
154
|
+
maxWidth,
|
|
155
|
+
...rest
|
|
156
|
+
}: TextInputProps, ref) {
|
|
157
|
+
const theme = useRestyleTheme();
|
|
158
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
159
|
+
|
|
160
|
+
const {
|
|
161
|
+
isDisabled,
|
|
162
|
+
hasError,
|
|
163
|
+
opacity,
|
|
164
|
+
finalBackgroundColor,
|
|
165
|
+
finalBorderColor,
|
|
166
|
+
finalTextColor,
|
|
167
|
+
finalPlaceholderColor,
|
|
168
|
+
finalLabelColor,
|
|
169
|
+
borderWidth,
|
|
170
|
+
leftIconSize,
|
|
171
|
+
rightIconSize,
|
|
172
|
+
iconColor,
|
|
173
|
+
a11yProps,
|
|
174
|
+
} = useTextInputLogic({
|
|
175
|
+
variant,
|
|
176
|
+
size,
|
|
177
|
+
color,
|
|
178
|
+
disabled,
|
|
179
|
+
error,
|
|
180
|
+
isFocused,
|
|
181
|
+
leftIconName,
|
|
182
|
+
rightIconName,
|
|
183
|
+
label,
|
|
184
|
+
accessibilityLabel,
|
|
185
|
+
accessibilityHint,
|
|
186
|
+
required,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Resolve responsive size value
|
|
190
|
+
const resolvedSize = (useResponsiveProp(size) ?? "md") as TextInputSize;
|
|
191
|
+
|
|
192
|
+
// Get size configuration for dimensions
|
|
193
|
+
const sizeConfig = getTextInputSizeConfig(resolvedSize);
|
|
194
|
+
const labelVariant = getTextInputLabelVariant(resolvedSize);
|
|
195
|
+
|
|
196
|
+
const handleFocus = useCallback(
|
|
197
|
+
(e: NativeSyntheticEvent<TargetedEvent>) => {
|
|
198
|
+
setIsFocused(true);
|
|
199
|
+
onFocus?.(e);
|
|
200
|
+
},
|
|
201
|
+
[onFocus],
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const handleBlur = useCallback(
|
|
205
|
+
(e: NativeSyntheticEvent<TargetedEvent>) => {
|
|
206
|
+
setIsFocused(false);
|
|
207
|
+
onBlur?.(e);
|
|
208
|
+
},
|
|
209
|
+
[onBlur],
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const charCount = typeof value === "string" ? value.length : 0;
|
|
213
|
+
const isTextarea = variant === "textarea";
|
|
214
|
+
|
|
215
|
+
const backgroundColorStyle = finalBackgroundColor === "transparent"
|
|
216
|
+
? "transparent"
|
|
217
|
+
: theme.colors[finalBackgroundColor as BaseThemeColor];
|
|
218
|
+
|
|
219
|
+
const borderColorStyle = theme.colors[finalBorderColor as BaseThemeColor];
|
|
220
|
+
|
|
221
|
+
const textColorStyle = theme.colors[finalTextColor as BaseThemeColor];
|
|
222
|
+
|
|
223
|
+
const placeholderColorStyle = theme.colors[finalPlaceholderColor as BaseThemeColor];
|
|
224
|
+
|
|
225
|
+
// Calculate container height for textarea
|
|
226
|
+
const containerHeight = isTextarea ? textareaHeight : sizeConfig.height;
|
|
227
|
+
|
|
228
|
+
// Calculate padding for textarea (needs vertical padding)
|
|
229
|
+
const containerPadding = useMemo(() => {
|
|
230
|
+
if (isTextarea) {
|
|
231
|
+
return {
|
|
232
|
+
paddingHorizontal: sizeConfig.paddingHorizontal,
|
|
233
|
+
paddingVertical: theme.spacing.sm,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
paddingHorizontal: sizeConfig.paddingHorizontal,
|
|
238
|
+
paddingVertical: 0,
|
|
239
|
+
};
|
|
240
|
+
}, [isTextarea, sizeConfig.paddingHorizontal, theme.spacing.sm]);
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<Box
|
|
244
|
+
style={containerStyle}
|
|
245
|
+
testID={testID}
|
|
246
|
+
opacity={opacity}
|
|
247
|
+
margin={margin}
|
|
248
|
+
marginTop={marginTop}
|
|
249
|
+
marginBottom={marginBottom}
|
|
250
|
+
marginLeft={marginLeft}
|
|
251
|
+
marginRight={marginRight}
|
|
252
|
+
marginHorizontal={marginHorizontal}
|
|
253
|
+
marginVertical={marginVertical}
|
|
254
|
+
padding={padding}
|
|
255
|
+
paddingTop={paddingTop}
|
|
256
|
+
paddingBottom={paddingBottom}
|
|
257
|
+
paddingLeft={paddingLeft}
|
|
258
|
+
paddingRight={paddingRight}
|
|
259
|
+
paddingHorizontal={paddingHorizontal}
|
|
260
|
+
paddingVertical={paddingVertical}
|
|
261
|
+
flex={flex}
|
|
262
|
+
flexGrow={flexGrow}
|
|
263
|
+
flexShrink={flexShrink}
|
|
264
|
+
width={width}
|
|
265
|
+
minWidth={minWidth}
|
|
266
|
+
maxWidth={maxWidth}
|
|
267
|
+
>
|
|
268
|
+
{label && (
|
|
269
|
+
<Box marginBottom="xs" flexDirection="row" alignItems="center">
|
|
270
|
+
<Text variant={labelVariant} color={finalLabelColor as BaseThemeColor}>
|
|
271
|
+
{label}
|
|
272
|
+
{required && (
|
|
273
|
+
<Text variant={labelVariant} color="feedbackError">
|
|
274
|
+
{" *"}
|
|
275
|
+
</Text>
|
|
276
|
+
)}
|
|
277
|
+
</Text>
|
|
278
|
+
</Box>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
<BaseTextInputContainer
|
|
282
|
+
flexDirection={isTextarea ? "column" : "row"}
|
|
283
|
+
alignItems={isTextarea ? "flex-start" : "center"}
|
|
284
|
+
borderRadius="md"
|
|
285
|
+
style={[
|
|
286
|
+
{
|
|
287
|
+
height: containerHeight,
|
|
288
|
+
paddingHorizontal: containerPadding.paddingHorizontal,
|
|
289
|
+
paddingTop: containerPadding.paddingVertical,
|
|
290
|
+
paddingBottom: containerPadding.paddingVertical,
|
|
291
|
+
backgroundColor: backgroundColorStyle,
|
|
292
|
+
borderColor: borderColorStyle,
|
|
293
|
+
borderWidth: borderWidth,
|
|
294
|
+
},
|
|
295
|
+
inputContainerStyle,
|
|
296
|
+
]}
|
|
297
|
+
>
|
|
298
|
+
{!isTextarea && leftIconName && leftIconSize && (
|
|
299
|
+
<Box marginRight="sm" alignItems="center" justifyContent="center">
|
|
300
|
+
<Icon
|
|
301
|
+
name={leftIconName}
|
|
302
|
+
size={leftIconSize}
|
|
303
|
+
color={iconColor}
|
|
304
|
+
accessibilityLabel={leftIconName}
|
|
305
|
+
/>
|
|
306
|
+
</Box>
|
|
307
|
+
)}
|
|
308
|
+
|
|
309
|
+
<RNTextInput
|
|
310
|
+
ref={ref}
|
|
311
|
+
style={[
|
|
312
|
+
styles.input,
|
|
313
|
+
{
|
|
314
|
+
fontSize: sizeConfig.fontSize,
|
|
315
|
+
color: textColorStyle,
|
|
316
|
+
...(isTextarea && {
|
|
317
|
+
textAlignVertical: "top", // Android: cursor starts at top
|
|
318
|
+
paddingTop: 0,
|
|
319
|
+
paddingBottom: 0,
|
|
320
|
+
flex: 1,
|
|
321
|
+
width: "100%",
|
|
322
|
+
minHeight:
|
|
323
|
+
containerHeight -
|
|
324
|
+
containerPadding.paddingVertical * 2 -
|
|
325
|
+
borderWidth * 2,
|
|
326
|
+
}),
|
|
327
|
+
},
|
|
328
|
+
]}
|
|
329
|
+
placeholderTextColor={placeholderColorStyle}
|
|
330
|
+
editable={!isDisabled}
|
|
331
|
+
value={value}
|
|
332
|
+
maxLength={maxLength}
|
|
333
|
+
multiline={isTextarea}
|
|
334
|
+
onFocus={handleFocus}
|
|
335
|
+
onBlur={handleBlur}
|
|
336
|
+
accessibilityLabel={a11yProps.accessibilityLabel}
|
|
337
|
+
accessibilityState={a11yProps.accessibilityState}
|
|
338
|
+
accessibilityHint={a11yProps.accessibilityHint}
|
|
339
|
+
{...rest}
|
|
340
|
+
/>
|
|
341
|
+
|
|
342
|
+
{!isTextarea && rightIconName && rightIconSize && (
|
|
343
|
+
<Box marginLeft="sm" alignItems="center" justifyContent="center">
|
|
344
|
+
<Icon
|
|
345
|
+
name={rightIconName}
|
|
346
|
+
size={rightIconSize}
|
|
347
|
+
color={iconColor}
|
|
348
|
+
accessibilityLabel={rightIconName}
|
|
349
|
+
/>
|
|
350
|
+
</Box>
|
|
351
|
+
)}
|
|
352
|
+
</BaseTextInputContainer>
|
|
353
|
+
|
|
354
|
+
{(helperText || error || showCharCount) && (
|
|
355
|
+
<Box
|
|
356
|
+
marginTop="xs"
|
|
357
|
+
flexDirection="row"
|
|
358
|
+
alignItems="center"
|
|
359
|
+
justifyContent="space-between"
|
|
360
|
+
>
|
|
361
|
+
<Box flex={1}>
|
|
362
|
+
{(error || helperText) && (
|
|
363
|
+
<Text
|
|
364
|
+
variant="caption"
|
|
365
|
+
color={hasError ? "feedbackError" : "textSecondary"}
|
|
366
|
+
>
|
|
367
|
+
{error || helperText}
|
|
368
|
+
</Text>
|
|
369
|
+
)}
|
|
370
|
+
</Box>
|
|
371
|
+
|
|
372
|
+
{showCharCount && maxLength && (
|
|
373
|
+
<Text variant="caption" color="textTertiary">
|
|
374
|
+
{charCount}/{maxLength}
|
|
375
|
+
</Text>
|
|
376
|
+
)}
|
|
377
|
+
</Box>
|
|
378
|
+
)}
|
|
379
|
+
</Box>
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const styles = StyleSheet.create({
|
|
384
|
+
input: {
|
|
385
|
+
flex: 1,
|
|
386
|
+
padding: 0,
|
|
387
|
+
margin: 0,
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
export const TextInput = memo(TextInputComponent);
|
|
392
|
+
TextInput.displayName = "TextInput";
|