@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,77 @@
1
+ import { Meta, StoryObj } from '@storybook/react-vite';
2
+ import React from 'react';
3
+ import { Avatar } from '.';
4
+ import { Flex } from '../Flex';
5
+ import { VariantTitle } from '../../../docs/components';
6
+
7
+ const meta = {
8
+ title: 'Stories / Avatar',
9
+ component: Avatar,
10
+ parameters: {
11
+ layout: 'centered',
12
+ },
13
+ argTypes: {
14
+ size: {
15
+ options: ['sm', 'md'],
16
+ control: 'radio',
17
+ description: 'Size of the avatar.',
18
+ },
19
+ name: {
20
+ control: 'text',
21
+ description: 'Name used for initials and accessibility.',
22
+ },
23
+ src: {
24
+ control: 'object',
25
+ description: 'Image source object (e.g. { uri: "..." }).',
26
+ },
27
+ delayMs: {
28
+ control: 'number',
29
+ description: 'Delay in ms before showing the fallback.',
30
+ },
31
+ },
32
+ args: {
33
+ size: 'md',
34
+ },
35
+ } satisfies Meta<typeof Avatar>;
36
+
37
+ export default meta;
38
+ type Story = StoryObj<typeof meta>;
39
+
40
+ export const Playground: Story = {
41
+ args: {
42
+ name: 'Jane Doe',
43
+ src: { uri: 'https://ca.slack-edge.com/T0HR00WDA-U05SHRATW7Q-3ad4ae7c75b8-512' },
44
+ },
45
+ };
46
+
47
+ export const Icon: Story = {};
48
+
49
+ export const Initials: Story = {
50
+ args: {
51
+ name: 'Jane Doe',
52
+ },
53
+ };
54
+
55
+ export const Image: Story = {
56
+ args: {
57
+ name: 'Jane Doe',
58
+ src: { uri: 'https://ca.slack-edge.com/T0HR00WDA-U05SHRATW7Q-3ad4ae7c75b8-512' },
59
+ },
60
+ };
61
+
62
+ export const Sizes: Story = {
63
+ render: () => (
64
+ <Flex direction="row" space="xl" align="center">
65
+ <Flex direction="column" space="md" align="center">
66
+ <VariantTitle title="SM">
67
+ <Avatar size="sm" name="Jane Doe" />
68
+ </VariantTitle>
69
+ </Flex>
70
+ <Flex direction="column" space="md" align="center">
71
+ <VariantTitle title="MD">
72
+ <Avatar size="md" name="Jane Doe" />
73
+ </VariantTitle>
74
+ </Flex>
75
+ </Flex>
76
+ ),
77
+ };
@@ -0,0 +1,136 @@
1
+ import { UserMediumIcon, UserSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
2
+ import { useEffect, useState } from 'react';
3
+ import { Image, View } from 'react-native';
4
+ import { StyleSheet } from 'react-native-unistyles';
5
+ import { useTheme } from '../../hooks';
6
+ import { getInitials } from '../../utils';
7
+ import BodyText from '../BodyText/BodyText';
8
+ import AvatarProps, { AvatarLoadingStatus } from './Avatar.props';
9
+
10
+ const Avatar = ({
11
+ src,
12
+ name,
13
+ size = 'md',
14
+ delayMs = 0,
15
+ onLoadingStatusChange,
16
+ style,
17
+ ...props
18
+ }: AvatarProps) => {
19
+ const [status, setStatus] = useState<AvatarLoadingStatus>('idle');
20
+ const [isDelayed, setIsDelayed] = useState(delayMs > 0);
21
+ const { components } = useTheme();
22
+
23
+ useEffect(() => {
24
+ if (!src) {
25
+ setStatus('idle');
26
+ return;
27
+ }
28
+
29
+ setStatus('loading');
30
+ }, [src]);
31
+
32
+ useEffect(() => {
33
+ onLoadingStatusChange?.(status);
34
+ }, [status, onLoadingStatusChange]);
35
+
36
+ useEffect(() => {
37
+ if (delayMs <= 0) {
38
+ setIsDelayed(false);
39
+ return;
40
+ }
41
+
42
+ const timerId = setTimeout(() => {
43
+ setIsDelayed(false);
44
+ }, delayMs);
45
+ return () => clearTimeout(timerId);
46
+ }, [delayMs]);
47
+
48
+ styles.useVariants({ size });
49
+
50
+ const initials = getInitials(name);
51
+
52
+ const handleLoad = () => setStatus('loaded');
53
+ const handleError = () => setStatus('error');
54
+
55
+ const showImage = src && status === 'loaded';
56
+ const showFallback = !showImage && !isDelayed;
57
+
58
+ const textSize = size === 'sm' ? 'md' : 'lg';
59
+ const FallbackIcon = size === 'sm' ? UserSmallIcon : UserMediumIcon;
60
+
61
+ return (
62
+ <View
63
+ {...props}
64
+ style={[styles.container, style]}
65
+ accessibilityRole={showImage && name ? 'image' : undefined}
66
+ accessibilityLabel={showImage ? name : undefined}
67
+ >
68
+ {src && (
69
+ <Image
70
+ source={src}
71
+ style={styles.image}
72
+ onLoad={handleLoad}
73
+ onError={handleError}
74
+ accessibilityElementsHidden
75
+ />
76
+ )}
77
+ {showFallback && (
78
+ <View style={styles.fallback}>
79
+ {name ? (
80
+ <BodyText
81
+ size={textSize}
82
+ weight="semibold"
83
+ textTransform="uppercase"
84
+ style={styles.text}
85
+ >
86
+ {initials}
87
+ </BodyText>
88
+ ) : (
89
+ <FallbackIcon />
90
+ )}
91
+ </View>
92
+ )}
93
+ </View>
94
+ );
95
+ };
96
+
97
+ Avatar.displayName = 'Avatar';
98
+
99
+ const styles = StyleSheet.create(theme => ({
100
+ container: {
101
+ justifyContent: 'center',
102
+ alignItems: 'center',
103
+ overflow: 'hidden',
104
+ backgroundColor: theme.color.surface.pig.subtle,
105
+ variants: {
106
+ size: {
107
+ sm: {
108
+ width: theme.components.avatar.sm.width,
109
+ height: theme.components.avatar.sm.height,
110
+ borderRadius: theme.components.avatar.sm.borderRadius,
111
+ },
112
+ md: {
113
+ width: theme.components.avatar.md.width,
114
+ height: theme.components.avatar.md.height,
115
+ borderRadius: theme.components.avatar.md.borderRadius,
116
+ },
117
+ },
118
+ },
119
+ },
120
+ image: {
121
+ width: '100%',
122
+ height: '100%',
123
+ position: 'absolute',
124
+ },
125
+ fallback: {
126
+ width: '100%',
127
+ height: '100%',
128
+ justifyContent: 'center',
129
+ alignItems: 'center',
130
+ },
131
+ text: {
132
+ color: theme.color.text.primary,
133
+ },
134
+ }));
135
+
136
+ export default Avatar;
@@ -0,0 +1,2 @@
1
+ export { default as Avatar } from './Avatar';
2
+ export type { default as AvatarProps, AvatarLoadingStatus } from './Avatar.props';
@@ -0,0 +1,163 @@
1
+ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks';
2
+ import { BackToTopButton, ViewFigmaButton } from '../../../docs/components';
3
+ import * as Stories from './DateInput.stories';
4
+
5
+ <Meta title="Forms / Date Input" />
6
+
7
+ <ViewFigmaButton url="https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=2277-14708&t=dbFGPEbIX93BQlur-4" />
8
+
9
+ <BackToTopButton />
10
+
11
+ # Date Input
12
+
13
+ The `DateInput` component allows users to enter dates manually using separate input fields for day, month, and year. It provides flexibility to show only the date segments you need, supports validation states, and integrates seamlessly with the React Native form system.
14
+
15
+ - [Playground](#playground)
16
+ - [Usage](#usage)
17
+ - [Props](#props)
18
+ - [Examples](#examples)
19
+ - [Date of Birth](#date-of-birth)
20
+ - [Card Expiry](#card-expiry)
21
+ - [Year Only](#year-only)
22
+ - [Validation](#validation)
23
+ - [Disabled](#disabled)
24
+ - [Default Value](#default-value)
25
+ - [Custom Validation](#custom-validation)
26
+ - [Flexible Segments](#flexible-segments)
27
+ - [Grouping Inputs](#grouping-inputs)
28
+ - [With State](#with-state)
29
+ - [Accessibility](#accessibility)
30
+
31
+ ## Playground
32
+
33
+ <Canvas of={Stories.Playground} />
34
+
35
+ <Controls of={Stories.Playground} />
36
+
37
+ ## Usage
38
+
39
+ Use state to control the date values:
40
+
41
+ ```tsx
42
+ import { useState } from 'react';
43
+ import { DateInput } from '@utilitywarehouse/hearth-react-native';
44
+
45
+ const MyComponent = () => {
46
+ const [day, setDay] = useState('');
47
+ const [month, setMonth] = useState('');
48
+ const [year, setYear] = useState('');
49
+
50
+ return (
51
+ <DateInput
52
+ label="Date of birth"
53
+ dayValue={day}
54
+ monthValue={month}
55
+ yearValue={year}
56
+ onDayChange={setDay}
57
+ onMonthChange={setMonth}
58
+ onYearChange={setYear}
59
+ />
60
+ );
61
+ };
62
+ ```
63
+
64
+ ## Props
65
+
66
+ ### DateInputProps
67
+
68
+ | Prop | Type | Default | Description |
69
+ | ------------------ | ----------------------------------- | ----------- | ----------------------------------------------------- |
70
+ | `label` | `string` | - | Label text displayed above the inputs |
71
+ | `helperText` | `string` | - | Helper text displayed below the inputs |
72
+ | `helperIcon` | `ComponentType` | - | Icon component to display with helper/validation text |
73
+ | `validationStatus` | `'initial' \| 'valid' \| 'invalid'` | `'initial'` | Validation status of the input |
74
+ | `validText` | `string` | - | Text to display when validation status is valid |
75
+ | `invalidText` | `string` | - | Text to display when validation status is invalid |
76
+ | `required` | `boolean` | `false` | Whether the input is required |
77
+ | `disabled` | `boolean` | `false` | Whether the input is disabled |
78
+ | `readonly` | `boolean` | `false` | Whether the input is read-only |
79
+ | `hideDay` | `boolean` | `false` | Whether to hide the day segment |
80
+ | `hideMonth` | `boolean` | `false` | Whether to hide the month segment |
81
+ | `hideYear` | `boolean` | `false` | Whether to hide the year segment |
82
+ | `dayPlaceholder` | `string` | `'DD'` | Placeholder text for the day segment |
83
+ | `monthPlaceholder` | `string` | `'MM'` | Placeholder text for the month segment |
84
+ | `yearPlaceholder` | `string` | `'YYYY'` | Placeholder text for the year segment |
85
+ | `dayValue` | `string` | - | Controlled value for the day segment |
86
+ | `monthValue` | `string` | - | Controlled value for the month segment |
87
+ | `yearValue` | `string` | - | Controlled value for the year segment |
88
+ | `onDayChange` | `(text: string) => void` | - | Callback fired when the day value changes |
89
+ | `onMonthChange` | `(text: string) => void` | - | Callback fired when the month value changes |
90
+ | `onYearChange` | `(text: string) => void` | - | Callback fired when the year value changes |
91
+ | `onDayFocus` | `(e: NativeSyntheticEvent) => void` | - | Callback fired when the day segment receives focus |
92
+ | `onMonthFocus` | `(e: NativeSyntheticEvent) => void` | - | Callback fired when the month segment receives focus |
93
+ | `onYearFocus` | `(e: NativeSyntheticEvent) => void` | - | Callback fired when the year segment receives focus |
94
+ | `onDayBlur` | `(e: NativeSyntheticEvent) => void` | - | Callback fired when the day segment loses focus |
95
+ | `onMonthBlur` | `(e: NativeSyntheticEvent) => void` | - | Callback fired when the month segment loses focus |
96
+ | `onYearBlur` | `(e: NativeSyntheticEvent) => void` | - | Callback fired when the year segment loses focus |
97
+
98
+ ## Examples
99
+
100
+ ### Date of Birth
101
+
102
+ <Canvas of={Stories.DateOfBirth} />
103
+
104
+ ### Card Expiry
105
+
106
+ Hide the day segment for card expiry dates:
107
+
108
+ <Canvas of={Stories.CardExpiry} />
109
+
110
+ ### Year Only
111
+
112
+ Hide day and month segments for year-only inputs:
113
+
114
+ <Canvas of={Stories.YearOnly} />
115
+
116
+ ### Validation
117
+
118
+ Show validation states with helper text and icons:
119
+
120
+ <Canvas of={Stories.Validation} />
121
+
122
+ ### Disabled
123
+
124
+ Disabled date inputs:
125
+
126
+ <Canvas of={Stories.Disabled} />
127
+
128
+ ### Default Value
129
+
130
+ Set initial values with `value` props:
131
+
132
+ <Canvas of={Stories.DefaultValue} />
133
+
134
+ ### Custom Validation
135
+
136
+ Implement custom validation logic:
137
+
138
+ <Canvas of={Stories.WithCustomValidation} />
139
+
140
+ ### Flexible Segments
141
+
142
+ Control which segments are visible:
143
+
144
+ <Canvas of={Stories.FlexibleSegments} />
145
+
146
+ ### Grouping Inputs
147
+
148
+ Group date inputs with other form fields in a card:
149
+
150
+ <Canvas of={Stories.GroupingInputs} />
151
+
152
+ ### With State
153
+
154
+ Programmatically control date values:
155
+
156
+ <Canvas of={Stories.WithState} />
157
+
158
+ ## Accessibility
159
+
160
+ - Each segment has a descriptive label ("Day", "Month", "Year") for screen readers
161
+ - Keyboard type is set to `number-pad` for easier numeric input
162
+ - Validation states are communicated through helper text and icons
163
+ - Disabled state properly communicated to assistive technologies
@@ -0,0 +1,80 @@
1
+ import type { TextInputProps } from 'react-native';
2
+ import type { FormFieldBaseProps } from '../FormField/FormField.props';
3
+
4
+ export interface DateInputProps extends FormFieldBaseProps {
5
+ /**
6
+ * Whether the day segment is hidden.
7
+ * @default false
8
+ */
9
+ hideDay?: boolean;
10
+ /**
11
+ * Whether the month segment is hidden.
12
+ * @default false
13
+ */
14
+ hideMonth?: boolean;
15
+ /**
16
+ * Whether the year segment is hidden.
17
+ * @default false
18
+ */
19
+ hideYear?: boolean;
20
+ /**
21
+ * Placeholder text for the day segment.
22
+ */
23
+ dayPlaceholder?: string;
24
+ /**
25
+ * Placeholder text for the month segment.
26
+ */
27
+ monthPlaceholder?: string;
28
+ /**
29
+ * Placeholder text for the year segment.
30
+ */
31
+ yearPlaceholder?: string;
32
+ /**
33
+ * The controlled value for the day segment. Must be used with an `onDayChange` handler.
34
+ */
35
+ dayValue?: string;
36
+ /**
37
+ * The controlled value for the month segment. Must be used with an `onMonthChange` handler.
38
+ */
39
+ monthValue?: string;
40
+ /**
41
+ * The controlled value for the year segment. Must be used with an `onYearChange` handler.
42
+ */
43
+ yearValue?: string;
44
+ /**
45
+ * Callback fired when the day value changes.
46
+ */
47
+ onDayChange?: (text: string) => void;
48
+ /**
49
+ * Callback fired when the month value changes.
50
+ */
51
+ onMonthChange?: (text: string) => void;
52
+ /**
53
+ * Callback fired when the year value changes.
54
+ */
55
+ onYearChange?: (text: string) => void;
56
+ /**
57
+ * Callback fired when the day segment receives focus.
58
+ */
59
+ onDayFocus?: TextInputProps['onFocus'];
60
+ /**
61
+ * Callback fired when the month segment receives focus.
62
+ */
63
+ onMonthFocus?: TextInputProps['onFocus'];
64
+ /**
65
+ * Callback fired when the year segment receives focus.
66
+ */
67
+ onYearFocus?: TextInputProps['onFocus'];
68
+ /**
69
+ * Callback fired when the day segment loses focus.
70
+ */
71
+ onDayBlur?: TextInputProps['onBlur'];
72
+ /**
73
+ * Callback fired when the month segment loses focus.
74
+ */
75
+ onMonthBlur?: TextInputProps['onBlur'];
76
+ /**
77
+ * Callback fired when the year segment loses focus.
78
+ */
79
+ onYearBlur?: TextInputProps['onBlur'];
80
+ }