@utilitywarehouse/hearth-react-native 0.10.0 → 0.11.0

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 (39) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/CHANGELOG.md +8 -0
  4. package/build/components/Avatar/Avatar.d.ts +6 -0
  5. package/build/components/Avatar/Avatar.js +80 -0
  6. package/build/components/Avatar/Avatar.props.d.ts +28 -0
  7. package/build/components/Avatar/Avatar.props.js +1 -0
  8. package/build/components/Avatar/index.d.ts +2 -0
  9. package/build/components/Avatar/index.js +1 -0
  10. package/build/components/DateInput/DateInput.d.ts +6 -0
  11. package/build/components/DateInput/DateInput.js +19 -0
  12. package/build/components/DateInput/DateInput.props.d.ts +79 -0
  13. package/build/components/DateInput/DateInput.props.js +1 -0
  14. package/build/components/DateInput/DateInputSegment.d.ts +20 -0
  15. package/build/components/DateInput/DateInputSegment.js +31 -0
  16. package/build/components/DateInput/index.d.ts +2 -0
  17. package/build/components/DateInput/index.js +1 -0
  18. package/build/components/index.d.ts +2 -0
  19. package/build/components/index.js +2 -0
  20. package/build/utils/getInitials.d.ts +1 -0
  21. package/build/utils/getInitials.js +8 -0
  22. package/build/utils/index.d.ts +1 -0
  23. package/build/utils/index.js +1 -0
  24. package/docs/components/AllComponents.web.tsx +18 -1
  25. package/package.json +1 -1
  26. package/src/components/Avatar/Avatar.docs.mdx +105 -0
  27. package/src/components/Avatar/Avatar.props.ts +31 -0
  28. package/src/components/Avatar/Avatar.stories.tsx +77 -0
  29. package/src/components/Avatar/Avatar.tsx +136 -0
  30. package/src/components/Avatar/index.ts +2 -0
  31. package/src/components/DateInput/DateInput.docs.mdx +163 -0
  32. package/src/components/DateInput/DateInput.props.ts +80 -0
  33. package/src/components/DateInput/DateInput.stories.tsx +269 -0
  34. package/src/components/DateInput/DateInput.tsx +117 -0
  35. package/src/components/DateInput/DateInputSegment.tsx +83 -0
  36. package/src/components/DateInput/index.ts +2 -0
  37. package/src/components/index.ts +2 -0
  38. package/src/utils/getInitials.ts +7 -0
  39. package/src/utils/index.ts +1 -0
@@ -0,0 +1,269 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-native';
2
+ import { TickSmallIcon, WarningSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
3
+ import { useState } from 'react';
4
+ import { View } from 'react-native';
5
+ import { Button, Card, Flex, Heading } from '../../components';
6
+ import { DateInput } from './';
7
+
8
+ const DateInputMeta: Meta<typeof DateInput> = {
9
+ title: 'Stories / DateInput',
10
+ component: DateInput,
11
+ parameters: {
12
+ // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
13
+ layout: 'centered',
14
+ },
15
+ argTypes: {
16
+ label: {
17
+ control: 'text',
18
+ },
19
+ helperText: {
20
+ control: 'text',
21
+ },
22
+ validationStatus: {
23
+ control: 'select',
24
+ options: ['initial', 'valid', 'invalid'],
25
+ },
26
+ disabled: {
27
+ control: 'boolean',
28
+ },
29
+ required: {
30
+ control: 'boolean',
31
+ },
32
+ hideDay: {
33
+ control: 'boolean',
34
+ },
35
+ hideMonth: {
36
+ control: 'boolean',
37
+ },
38
+ hideYear: {
39
+ control: 'boolean',
40
+ },
41
+ },
42
+ };
43
+
44
+ export default DateInputMeta;
45
+
46
+ type Story = StoryObj<typeof DateInput>;
47
+
48
+ export const Playground: Story = {
49
+ args: {
50
+ label: 'Date',
51
+ helperText: 'Helper text',
52
+ },
53
+ };
54
+
55
+ export const DateOfBirth: Story = {
56
+ render: () => {
57
+ return <DateInput label="Date of birth" helperText="Enter your date of birth" required />;
58
+ },
59
+ };
60
+
61
+ export const CardExpiry: Story = {
62
+ render: () => {
63
+ const [month, setMonth] = useState('');
64
+ const [year, setYear] = useState('');
65
+ return (
66
+ <DateInput
67
+ label="Card expiry"
68
+ helperText="Enter the expiry month and year"
69
+ monthValue={month}
70
+ yearValue={year}
71
+ onMonthChange={setMonth}
72
+ onYearChange={setYear}
73
+ hideDay
74
+ required
75
+ />
76
+ );
77
+ },
78
+ };
79
+
80
+ export const YearOnly: Story = {
81
+ render: () => {
82
+ return <DateInput label="Year" helperText="Enter the year" hideDay hideMonth />;
83
+ },
84
+ };
85
+
86
+ export const Validation: Story = {
87
+ render: () => {
88
+ const [day, setDay] = useState('01');
89
+ const [month, setMonth] = useState('02');
90
+ const [year, setYear] = useState('2025');
91
+ return (
92
+ <View style={{ gap: 16 }}>
93
+ <DateInput
94
+ label="Valid date"
95
+ dayValue={day}
96
+ monthValue={month}
97
+ yearValue={year}
98
+ onDayChange={setDay}
99
+ onMonthChange={setMonth}
100
+ onYearChange={setYear}
101
+ validationStatus="valid"
102
+ validText="Date is valid"
103
+ helperIcon={TickSmallIcon}
104
+ required
105
+ />
106
+ <DateInput
107
+ label="Invalid date"
108
+ dayValue="32"
109
+ monthValue="13"
110
+ yearValue="2025"
111
+ validationStatus="invalid"
112
+ invalidText="Please enter a valid date"
113
+ helperIcon={WarningSmallIcon}
114
+ required
115
+ />
116
+ </View>
117
+ );
118
+ },
119
+ };
120
+
121
+ export const Disabled: Story = {
122
+ render: () => {
123
+ return (
124
+ <DateInput
125
+ label="Date of birth"
126
+ helperText="This field is disabled"
127
+ dayValue="15"
128
+ monthValue="06"
129
+ yearValue="1990"
130
+ disabled
131
+ />
132
+ );
133
+ },
134
+ };
135
+
136
+ export const DefaultValue: Story = {
137
+ render: () => {
138
+ return <DateInput label="Date of birth" dayValue="01" monthValue="01" yearValue="2000" />;
139
+ },
140
+ };
141
+
142
+ export const WithCustomValidation: Story = {
143
+ render: () => {
144
+ const [day, setDay] = useState('');
145
+ const [month, setMonth] = useState('');
146
+ const [year, setYear] = useState('');
147
+
148
+ const validateDate = () => {
149
+ if (!day || !month || !year) return { status: 'initial' as const, message: '' };
150
+
151
+ const dayNum = parseInt(day, 10);
152
+ const monthNum = parseInt(month, 10);
153
+ const yearNum = parseInt(year, 10);
154
+
155
+ // Basic validation
156
+ if (dayNum < 1 || dayNum > 31) {
157
+ return { status: 'invalid' as const, message: 'Day must be between 1 and 31' };
158
+ }
159
+ if (monthNum < 1 || monthNum > 12) {
160
+ return { status: 'invalid' as const, message: 'Month must be between 1 and 12' };
161
+ }
162
+ if (yearNum < 1900 || yearNum > new Date().getFullYear()) {
163
+ return {
164
+ status: 'invalid' as const,
165
+ message: `Year must be between 1900 and ${new Date().getFullYear()}`,
166
+ };
167
+ }
168
+
169
+ // Check valid date
170
+ const date = new Date(yearNum, monthNum - 1, dayNum);
171
+ if (
172
+ date.getDate() !== dayNum ||
173
+ date.getMonth() !== monthNum - 1 ||
174
+ date.getFullYear() !== yearNum
175
+ ) {
176
+ return { status: 'invalid' as const, message: 'Please enter a valid date' };
177
+ }
178
+
179
+ return { status: 'valid' as const, message: 'Valid date' };
180
+ };
181
+
182
+ const validation = validateDate();
183
+
184
+ return (
185
+ <DateInput
186
+ label="Date of birth"
187
+ helperText="Enter a valid date between 1900 and today"
188
+ dayValue={day}
189
+ monthValue={month}
190
+ yearValue={year}
191
+ onDayChange={setDay}
192
+ onMonthChange={setMonth}
193
+ onYearChange={setYear}
194
+ validationStatus={validation.status}
195
+ validText={validation.status === 'valid' ? validation.message : undefined}
196
+ invalidText={validation.status === 'invalid' ? validation.message : undefined}
197
+ required
198
+ />
199
+ );
200
+ },
201
+ };
202
+
203
+ export const FlexibleSegments: Story = {
204
+ render: () => (
205
+ <View style={{ gap: 16 }}>
206
+ <DateInput label="Full date" helperText="DD/MM/YYYY" />
207
+ <DateInput label="Month and year" helperText="MM/YYYY" hideDay required />
208
+ <DateInput label="Year only" helperText="YYYY" hideDay hideMonth required />
209
+ </View>
210
+ ),
211
+ };
212
+
213
+ export const GroupingInputs: Story = {
214
+ render: () => (
215
+ <Flex space="sm">
216
+ <Heading size="lg">Event Registration</Heading>
217
+ <Card variant="subtle" gap="250">
218
+ <DateInput label="Date of birth" helperText="Enter your date of birth" required />
219
+ <DateInput
220
+ label="Event date preference"
221
+ helperText="Select your preferred date"
222
+ required={false}
223
+ />
224
+ </Card>
225
+ </Flex>
226
+ ),
227
+ };
228
+
229
+ export const WithState: Story = {
230
+ render: () => {
231
+ const [day, setDay] = useState('');
232
+ const [month, setMonth] = useState('');
233
+ const [year, setYear] = useState('');
234
+
235
+ const handleReset = () => {
236
+ setDay('');
237
+ setMonth('');
238
+ setYear('');
239
+ };
240
+
241
+ const handleSetToday = () => {
242
+ const today = new Date();
243
+ setDay(String(today.getDate()).padStart(2, '0'));
244
+ setMonth(String(today.getMonth() + 1).padStart(2, '0'));
245
+ setYear(String(today.getFullYear()));
246
+ };
247
+
248
+ return (
249
+ <Flex space="md">
250
+ <DateInput
251
+ label="Date"
252
+ helperText="Select or enter a date"
253
+ dayValue={day}
254
+ monthValue={month}
255
+ yearValue={year}
256
+ onDayChange={setDay}
257
+ onMonthChange={setMonth}
258
+ onYearChange={setYear}
259
+ />
260
+ <Flex space="xs">
261
+ <Button onPress={handleSetToday}>Set to Today</Button>
262
+ <Button onPress={handleReset} variant="solid">
263
+ Reset
264
+ </Button>
265
+ </Flex>
266
+ </Flex>
267
+ );
268
+ },
269
+ };
@@ -0,0 +1,117 @@
1
+ import { View } from 'react-native';
2
+ import { StyleSheet } from 'react-native-unistyles';
3
+ import { FormField } from '../FormField';
4
+ import type { DateInputProps } from './DateInput.props';
5
+ import DateInputSegment from './DateInputSegment';
6
+
7
+ const DateInput = ({
8
+ label,
9
+ helperText,
10
+ helperIcon,
11
+ validationStatus = 'initial',
12
+ validText,
13
+ invalidText,
14
+ disabled,
15
+ readonly,
16
+ required,
17
+ hideDay = false,
18
+ hideMonth = false,
19
+ hideYear = false,
20
+ dayPlaceholder = 'DD',
21
+ monthPlaceholder = 'MM',
22
+ yearPlaceholder = 'YYYY',
23
+ dayValue,
24
+ monthValue,
25
+ yearValue,
26
+ onDayChange,
27
+ onMonthChange,
28
+ onYearChange,
29
+ onDayFocus,
30
+ onMonthFocus,
31
+ onYearFocus,
32
+ onDayBlur,
33
+ onMonthBlur,
34
+ onYearBlur,
35
+ ...props
36
+ }: DateInputProps) => {
37
+ return (
38
+ <FormField
39
+ label={label}
40
+ helperText={helperText}
41
+ helperIcon={helperIcon}
42
+ validationStatus={validationStatus}
43
+ validText={validText}
44
+ invalidText={invalidText}
45
+ disabled={disabled}
46
+ readonly={readonly}
47
+ required={required}
48
+ style={styles.wrap}
49
+ {...props}
50
+ >
51
+ <View style={styles.container}>
52
+ {!hideDay && (
53
+ <DateInputSegment
54
+ label="Day"
55
+ placeholder={dayPlaceholder}
56
+ value={dayValue}
57
+ onChange={onDayChange}
58
+ onFocus={onDayFocus}
59
+ onBlur={onDayBlur}
60
+ disabled={disabled}
61
+ required={required}
62
+ readonly={readonly}
63
+ validationStatus={validationStatus}
64
+ maxLength={2}
65
+ testID="date-input-day"
66
+ />
67
+ )}
68
+ {!hideMonth && (
69
+ <DateInputSegment
70
+ label="Month"
71
+ placeholder={monthPlaceholder}
72
+ value={monthValue}
73
+ onChange={onMonthChange}
74
+ onFocus={onMonthFocus}
75
+ onBlur={onMonthBlur}
76
+ disabled={disabled}
77
+ required={required}
78
+ readonly={readonly}
79
+ validationStatus={validationStatus}
80
+ maxLength={2}
81
+ testID="date-input-month"
82
+ />
83
+ )}
84
+ {!hideYear && (
85
+ <DateInputSegment
86
+ label="Year"
87
+ placeholder={yearPlaceholder}
88
+ value={yearValue}
89
+ onChange={onYearChange}
90
+ onFocus={onYearFocus}
91
+ onBlur={onYearBlur}
92
+ disabled={disabled}
93
+ required={required}
94
+ readonly={readonly}
95
+ validationStatus={validationStatus}
96
+ maxLength={4}
97
+ testID="date-input-year"
98
+ />
99
+ )}
100
+ </View>
101
+ </FormField>
102
+ );
103
+ };
104
+
105
+ DateInput.displayName = 'DateInput';
106
+
107
+ const styles = StyleSheet.create(theme => ({
108
+ wrap: {
109
+ gap: theme.components.input.gap,
110
+ },
111
+ container: {
112
+ flexDirection: 'row',
113
+ gap: theme.components.input.date.gap,
114
+ },
115
+ }));
116
+
117
+ export default DateInput;
@@ -0,0 +1,83 @@
1
+ import { View } from 'react-native';
2
+ import { StyleSheet } from 'react-native-unistyles';
3
+ import { BodyText } from '../BodyText';
4
+ import { Input } from '../Input';
5
+ import type { DateInputProps } from './DateInput.props';
6
+
7
+ interface DateInputSegmentProps {
8
+ label: string;
9
+ placeholder?: string;
10
+ value?: string;
11
+ onChange?: (text: string) => void;
12
+ onFocus?: DateInputProps['onDayFocus'];
13
+ onBlur?: DateInputProps['onDayBlur'];
14
+ disabled?: boolean;
15
+ required?: boolean;
16
+ validationStatus?: DateInputProps['validationStatus'];
17
+ maxLength?: number;
18
+ readonly?: boolean;
19
+ testID?: string;
20
+ }
21
+
22
+ const DateInputSegment = ({
23
+ label,
24
+ placeholder,
25
+ value,
26
+ onChange,
27
+ onFocus,
28
+ onBlur,
29
+ disabled,
30
+ validationStatus,
31
+ maxLength,
32
+ readonly,
33
+ testID,
34
+ }: DateInputSegmentProps) => {
35
+ styles.useVariants({ disabled });
36
+ return (
37
+ <View style={styles.container}>
38
+ <BodyText size="md" style={styles.label}>
39
+ {label}
40
+ </BodyText>
41
+ <Input
42
+ value={value}
43
+ onChangeText={onChange}
44
+ onFocus={onFocus}
45
+ onBlur={onBlur}
46
+ placeholder={disabled ? undefined : placeholder}
47
+ keyboardType="number-pad"
48
+ maxLength={maxLength}
49
+ testID={testID}
50
+ accessibilityLabel={label}
51
+ disabled={disabled}
52
+ validationStatus={validationStatus}
53
+ readonly={readonly}
54
+ style={styles.input}
55
+ />
56
+ </View>
57
+ );
58
+ };
59
+
60
+ DateInputSegment.displayName = 'DateInputSegment';
61
+
62
+ const styles = StyleSheet.create(theme => ({
63
+ container: {
64
+ flex: 1,
65
+ gap: theme.components.input.gap,
66
+ maxWidth: 96,
67
+ },
68
+ label: {
69
+ variants: {
70
+ disabled: {
71
+ true: {
72
+ opacity: theme.opacity.disabled,
73
+ },
74
+ },
75
+ },
76
+ },
77
+ input: {
78
+ flex: 1,
79
+ maxWidth: 96,
80
+ },
81
+ }));
82
+
83
+ export default DateInputSegment;
@@ -0,0 +1,2 @@
1
+ export { default as DateInput } from './DateInput';
2
+ export type { DateInputProps } from './DateInput.props';
@@ -1,6 +1,7 @@
1
1
  // Custom
2
2
  export * from './Accordion';
3
3
  export * from './Alert';
4
+ export * from './Avatar';
4
5
  export * from './Badge';
5
6
  export * from './Banner';
6
7
  export * from './BodyText';
@@ -13,6 +14,7 @@ export * from './Center';
13
14
  export * from './Checkbox';
14
15
  export * from './Container';
15
16
  export * from './CurrencyInput';
17
+ export * from './DateInput';
16
18
  export * from './DatePicker';
17
19
  export * from './DatePickerInput';
18
20
  export * from './DescriptionList';
@@ -0,0 +1,7 @@
1
+ export function getInitials(name?: string) {
2
+ if (!name) return undefined;
3
+ const regex = new RegExp(/(\p{L}{1})\p{L}+/gu);
4
+ const names = [...name.matchAll(regex)];
5
+ const initials = (names.shift()?.[1] || '') + (names.pop()?.[1] || '');
6
+ return initials.toUpperCase();
7
+ }
@@ -3,6 +3,7 @@ export { default as formatThousands } from './formatThousands';
3
3
  export { default as getFlattenedColorValue } from './getFlattenedColorValue';
4
4
  export { default as getStyleValue } from './getStyleValue';
5
5
  export { default as hexWithOpacity } from './hexWithOpacity';
6
+ export { getInitials } from './getInitials';
6
7
  export { default as isEqual } from './isEqual';
7
8
  export * from './styleUtils';
8
9
  export * from './themeValueHelpers';