@utilitywarehouse/hearth-react-native 0.9.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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +16 -0
- package/build/components/Avatar/Avatar.d.ts +6 -0
- package/build/components/Avatar/Avatar.js +80 -0
- package/build/components/Avatar/Avatar.props.d.ts +28 -0
- package/build/components/Avatar/Avatar.props.js +1 -0
- package/build/components/Avatar/index.d.ts +2 -0
- package/build/components/Avatar/index.js +1 -0
- package/build/components/DateInput/DateInput.d.ts +6 -0
- package/build/components/DateInput/DateInput.js +19 -0
- package/build/components/DateInput/DateInput.props.d.ts +79 -0
- package/build/components/DateInput/DateInput.props.js +1 -0
- package/build/components/DateInput/DateInputSegment.d.ts +20 -0
- package/build/components/DateInput/DateInputSegment.js +31 -0
- package/build/components/DateInput/index.d.ts +2 -0
- package/build/components/DateInput/index.js +1 -0
- package/build/components/PillGroup/Pill.d.ts +16 -0
- package/build/components/PillGroup/Pill.js +94 -0
- package/build/components/PillGroup/Pill.props.d.ts +10 -0
- package/build/components/PillGroup/Pill.props.js +1 -0
- package/build/components/PillGroup/PillGroup.context.d.ts +6 -0
- package/build/components/PillGroup/PillGroup.context.js +5 -0
- package/build/components/PillGroup/PillGroup.d.ts +5 -0
- package/build/components/PillGroup/PillGroup.js +34 -0
- package/build/components/PillGroup/PillGroup.props.d.ts +15 -0
- package/build/components/PillGroup/PillGroup.props.js +1 -0
- package/build/components/PillGroup/index.d.ts +4 -0
- package/build/components/PillGroup/index.js +2 -0
- package/build/components/Select/Select.js +2 -1
- package/build/components/Toast/Toast.context.d.ts +9 -0
- package/build/components/Toast/Toast.context.js +90 -0
- package/build/components/Toast/Toast.props.d.ts +29 -0
- package/build/components/Toast/Toast.props.js +1 -0
- package/build/components/Toast/ToastItem.d.ts +10 -0
- package/build/components/Toast/ToastItem.js +129 -0
- package/build/components/Toast/index.d.ts +3 -0
- package/build/components/Toast/index.js +2 -0
- package/build/components/index.d.ts +4 -0
- package/build/components/index.js +4 -0
- package/build/tokens/components/dark/checkbox.d.ts +3 -0
- package/build/tokens/components/dark/checkbox.js +3 -0
- package/build/tokens/components/dark/input.d.ts +6 -0
- package/build/tokens/components/dark/input.js +6 -0
- package/build/tokens/components/dark/radio.d.ts +3 -0
- package/build/tokens/components/dark/radio.js +3 -0
- package/build/tokens/components/dark/table.d.ts +2 -0
- package/build/tokens/components/dark/table.js +2 -0
- package/build/tokens/components/dark/toast.d.ts +6 -2
- package/build/tokens/components/dark/toast.js +6 -2
- package/build/tokens/components/light/checkbox.d.ts +3 -0
- package/build/tokens/components/light/checkbox.js +3 -0
- package/build/tokens/components/light/input.d.ts +6 -0
- package/build/tokens/components/light/input.js +6 -0
- package/build/tokens/components/light/radio.d.ts +3 -0
- package/build/tokens/components/light/radio.js +3 -0
- package/build/tokens/components/light/table.d.ts +2 -0
- package/build/tokens/components/light/table.js +2 -0
- package/build/tokens/components/light/toast.d.ts +6 -2
- package/build/tokens/components/light/toast.js +6 -2
- package/build/utils/getInitials.d.ts +1 -0
- package/build/utils/getInitials.js +8 -0
- package/build/utils/index.d.ts +1 -0
- package/build/utils/index.js +1 -0
- package/docs/assets/toast-ios.MP4 +0 -0
- package/docs/components/AllComponents.web.tsx +43 -0
- package/package.json +3 -3
- package/src/components/Avatar/Avatar.docs.mdx +105 -0
- package/src/components/Avatar/Avatar.props.ts +31 -0
- package/src/components/Avatar/Avatar.stories.tsx +77 -0
- package/src/components/Avatar/Avatar.tsx +136 -0
- package/src/components/Avatar/index.ts +2 -0
- package/src/components/DateInput/DateInput.docs.mdx +163 -0
- package/src/components/DateInput/DateInput.props.ts +80 -0
- package/src/components/DateInput/DateInput.stories.tsx +269 -0
- package/src/components/DateInput/DateInput.tsx +117 -0
- package/src/components/DateInput/DateInputSegment.tsx +83 -0
- package/src/components/DateInput/index.ts +2 -0
- package/src/components/PillGroup/Pill.props.ts +13 -0
- package/src/components/PillGroup/Pill.tsx +120 -0
- package/src/components/PillGroup/PillGroup.context.tsx +12 -0
- package/src/components/PillGroup/PillGroup.docs.mdx +96 -0
- package/src/components/PillGroup/PillGroup.props.ts +22 -0
- package/src/components/PillGroup/PillGroup.stories.tsx +159 -0
- package/src/components/PillGroup/PillGroup.tsx +66 -0
- package/src/components/PillGroup/index.ts +4 -0
- package/src/components/Select/Select.tsx +2 -0
- package/src/components/Toast/Toast.context.tsx +118 -0
- package/src/components/Toast/Toast.docs.mdx +164 -0
- package/src/components/Toast/Toast.props.ts +33 -0
- package/src/components/Toast/Toast.stories.tsx +356 -0
- package/src/components/Toast/ToastItem.tsx +200 -0
- package/src/components/Toast/index.ts +3 -0
- package/src/components/index.ts +4 -0
- package/src/tokens/components/dark/checkbox.ts +3 -0
- package/src/tokens/components/dark/input.ts +6 -0
- package/src/tokens/components/dark/radio.ts +3 -0
- package/src/tokens/components/dark/table.ts +2 -0
- package/src/tokens/components/dark/toast.ts +6 -2
- package/src/tokens/components/light/checkbox.ts +3 -0
- package/src/tokens/components/light/input.ts +6 -0
- package/src/tokens/components/light/radio.ts +3 -0
- package/src/tokens/components/light/table.ts +2 -0
- package/src/tokens/components/light/toast.ts +6 -2
- package/src/utils/getInitials.ts +7 -0
- package/src/utils/index.ts +1 -0
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
Accordion,
|
|
24
24
|
AccordionItem,
|
|
25
25
|
Alert,
|
|
26
|
+
Avatar,
|
|
26
27
|
Badge,
|
|
27
28
|
Banner,
|
|
28
29
|
BodyText,
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
Checkbox,
|
|
40
41
|
Container,
|
|
41
42
|
CurrencyInput,
|
|
43
|
+
DateInput,
|
|
42
44
|
DatePicker,
|
|
43
45
|
DatePickerInput,
|
|
44
46
|
DateType,
|
|
@@ -68,6 +70,8 @@ import {
|
|
|
68
70
|
MenuTrigger,
|
|
69
71
|
Modal,
|
|
70
72
|
OL,
|
|
73
|
+
Pill,
|
|
74
|
+
PillGroup,
|
|
71
75
|
ProgressStep,
|
|
72
76
|
ProgressStepper,
|
|
73
77
|
Radio,
|
|
@@ -85,6 +89,7 @@ import {
|
|
|
85
89
|
TabsList,
|
|
86
90
|
Textarea,
|
|
87
91
|
ThemedImage,
|
|
92
|
+
ToastItem,
|
|
88
93
|
ToggleButtonCard,
|
|
89
94
|
ToggleButtonCardGroup,
|
|
90
95
|
UL,
|
|
@@ -193,6 +198,11 @@ const AllComponents: React.FC = () => {
|
|
|
193
198
|
<Alert text="This is an alert" />
|
|
194
199
|
</Center>
|
|
195
200
|
</ComponentWrapper>
|
|
201
|
+
<ComponentWrapper name="Avatar" link="/?path=/docs/components-avatar--docs">
|
|
202
|
+
<Center flex={1} gap="200">
|
|
203
|
+
<Avatar name="John Doe" />
|
|
204
|
+
</Center>
|
|
205
|
+
</ComponentWrapper>
|
|
196
206
|
<ComponentWrapper name="Badge" link="/?path=/docs/components-badge--docs">
|
|
197
207
|
<Center gap="200" flex={1}>
|
|
198
208
|
<View>
|
|
@@ -337,6 +347,11 @@ const AllComponents: React.FC = () => {
|
|
|
337
347
|
<CurrencyInput />
|
|
338
348
|
</Center>
|
|
339
349
|
</ComponentWrapper>
|
|
350
|
+
<ComponentWrapper name="Date Input" link="/?path=/docs/forms-date-input--docs">
|
|
351
|
+
<Center flex={1} padding="200">
|
|
352
|
+
<DateInput />
|
|
353
|
+
</Center>
|
|
354
|
+
</ComponentWrapper>
|
|
340
355
|
<ComponentWrapper name="Date Picker" link="/?path=/docs/components-date-picker--docs">
|
|
341
356
|
<Center flex={1}>
|
|
342
357
|
<Button onPress={handleDatePickerOpenPress}>Open Date Picker</Button>
|
|
@@ -627,6 +642,26 @@ const AllComponents: React.FC = () => {
|
|
|
627
642
|
</OL>
|
|
628
643
|
</Center>
|
|
629
644
|
</ComponentWrapper>
|
|
645
|
+
<ComponentWrapper name="Pill Group" link="/?path=/docs/components-pill-group--docs">
|
|
646
|
+
<Center flex={1} p="200">
|
|
647
|
+
{(() => {
|
|
648
|
+
const [pillValue, setPillValue] = React.useState<string[]>(['energy', 'mobile']);
|
|
649
|
+
return (
|
|
650
|
+
<PillGroup
|
|
651
|
+
value={pillValue}
|
|
652
|
+
onChange={v => setPillValue(v as string[])}
|
|
653
|
+
wrap={false}
|
|
654
|
+
multiple
|
|
655
|
+
>
|
|
656
|
+
<Pill value="all" label="All" />
|
|
657
|
+
<Pill value="energy" label="Energy" icon={ElectricityMediumIcon} />
|
|
658
|
+
<Pill value="broadband" label="Broadband" icon={BroadbandMediumIcon} />
|
|
659
|
+
<Pill value="mobile" label="Mobile" icon={MobileMediumIcon} />
|
|
660
|
+
</PillGroup>
|
|
661
|
+
);
|
|
662
|
+
})()}
|
|
663
|
+
</Center>
|
|
664
|
+
</ComponentWrapper>
|
|
630
665
|
<ComponentWrapper
|
|
631
666
|
name="Progress Stepper"
|
|
632
667
|
link="/?path=/docs/components-progress-stepper--docs"
|
|
@@ -745,6 +780,14 @@ const AllComponents: React.FC = () => {
|
|
|
745
780
|
/>
|
|
746
781
|
</Center>
|
|
747
782
|
</ComponentWrapper>
|
|
783
|
+
<ComponentWrapper name="Toast" link="/?path=/docs/components-toast--docs">
|
|
784
|
+
<Center flex={1} p="200">
|
|
785
|
+
<ToastItem
|
|
786
|
+
onClose={() => {}}
|
|
787
|
+
toast={{ id: 'tst', text: "I'm a toast", duration: 0 }}
|
|
788
|
+
/>
|
|
789
|
+
</Center>
|
|
790
|
+
</ComponentWrapper>
|
|
748
791
|
<ComponentWrapper
|
|
749
792
|
name="Toggle Button Card"
|
|
750
793
|
link="/?path=/docs/components-toggle-button-card--docs"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilitywarehouse/hearth-react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Utility Warehouse React Native UI library",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"vite-plugin-svgr": "^4.5.0",
|
|
58
58
|
"vitest": "^3.2.4",
|
|
59
59
|
"@utilitywarehouse/hearth-fonts": "^0.0.4",
|
|
60
|
-
"@utilitywarehouse/hearth-react-icons": "^0.7.
|
|
61
|
-
"@utilitywarehouse/hearth-react-native-icons": "^0.7.
|
|
60
|
+
"@utilitywarehouse/hearth-react-icons": "^0.7.4",
|
|
61
|
+
"@utilitywarehouse/hearth-react-native-icons": "^0.7.4",
|
|
62
62
|
"@utilitywarehouse/hearth-svg-assets": "^0.2.0",
|
|
63
63
|
"@utilitywarehouse/hearth-tokens": "^0.2.0"
|
|
64
64
|
},
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
import { Avatar, Box, Center } from '../..';
|
|
3
|
+
import { BackToTopButton, UsageWrap, ViewFigmaButton } from '../../../docs/components';
|
|
4
|
+
import * as Stories from './Avatar.stories';
|
|
5
|
+
|
|
6
|
+
<Meta title="Components / Avatar" />
|
|
7
|
+
|
|
8
|
+
<BackToTopButton />
|
|
9
|
+
|
|
10
|
+
<ViewFigmaButton url="https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components-%26-Tokens?node-id=1%3A148" />
|
|
11
|
+
|
|
12
|
+
# Avatar
|
|
13
|
+
|
|
14
|
+
Avatars help humanise the product experience by connecting users with the product and to each other.
|
|
15
|
+
|
|
16
|
+
- [Playground](#playground)
|
|
17
|
+
- [Usage](#usage)
|
|
18
|
+
- [Props](#props)
|
|
19
|
+
- [Accessibility](#accessibility)
|
|
20
|
+
|
|
21
|
+
## Playground
|
|
22
|
+
|
|
23
|
+
<Canvas of={Stories.Playground} />
|
|
24
|
+
|
|
25
|
+
<Controls of={Stories.Playground} />
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Icon
|
|
30
|
+
|
|
31
|
+
The icon variant is the default `Avatar` type. It ensures UI remains balanced and visually complete, even with limited data.
|
|
32
|
+
|
|
33
|
+
<UsageWrap>
|
|
34
|
+
<Center>
|
|
35
|
+
<Box>
|
|
36
|
+
<Avatar />
|
|
37
|
+
</Box>
|
|
38
|
+
</Center>
|
|
39
|
+
</UsageWrap>
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { Avatar } from '@utilitywarehouse/hearth-react-native';
|
|
43
|
+
|
|
44
|
+
const MyComponent = () => <Avatar />;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Initials
|
|
48
|
+
|
|
49
|
+
The initials variant can be used when a profile image is not available, but you have the user's name.
|
|
50
|
+
|
|
51
|
+
<UsageWrap>
|
|
52
|
+
<Center>
|
|
53
|
+
<Box>
|
|
54
|
+
<Avatar name="Jane Doe" />
|
|
55
|
+
</Box>
|
|
56
|
+
</Center>
|
|
57
|
+
</UsageWrap>
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { Avatar } from '@utilitywarehouse/hearth-react-native';
|
|
61
|
+
|
|
62
|
+
const MyComponent = () => <Avatar name="Jane Doe" />;
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Image
|
|
66
|
+
|
|
67
|
+
The image variant is available to use in journeys where a user is able to upload their own profile picture.
|
|
68
|
+
|
|
69
|
+
<UsageWrap>
|
|
70
|
+
<Center>
|
|
71
|
+
<Box>
|
|
72
|
+
<Avatar
|
|
73
|
+
name="Jane Doe"
|
|
74
|
+
src={{ uri: 'https://ca.slack-edge.com/T0HR00WDA-U05SHRATW7Q-3ad4ae7c75b8-512' }}
|
|
75
|
+
/>
|
|
76
|
+
</Box>
|
|
77
|
+
</Center>
|
|
78
|
+
</UsageWrap>
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { Avatar } from '@utilitywarehouse/hearth-react-native';
|
|
82
|
+
|
|
83
|
+
const MyComponent = () => (
|
|
84
|
+
<Avatar
|
|
85
|
+
name="Jane Doe"
|
|
86
|
+
src={{ uri: 'https://ca.slack-edge.com/T0HR00WDA-U05SHRATW7Q-3ad4ae7c75b8-512' }}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Props
|
|
92
|
+
|
|
93
|
+
| Property | Type | Description | Default |
|
|
94
|
+
| ----------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------- |
|
|
95
|
+
| `src` | `ImageSourcePropType` | The image source to display. | |
|
|
96
|
+
| `name` | `string` | The name associated with the avatar, used for creating initials and accessibility. | |
|
|
97
|
+
| `size` | `'sm' \| 'md'` | Sets the avatar size. | `md` |
|
|
98
|
+
| `delayMs` | `number` | Delay in milliseconds before the image is rendered to avoid flickering. | `0` |
|
|
99
|
+
| `onLoadingStatusChange` | `(status: 'idle' \| 'loading' \| 'loaded' \| 'error') => void` | Callback fired when the loading status of the image changes. | |
|
|
100
|
+
|
|
101
|
+
## Accessibility
|
|
102
|
+
|
|
103
|
+
When passing a user image to the `src` prop, you should also pass their name to the `name` prop so it can be used as an accessible description for the image.
|
|
104
|
+
|
|
105
|
+
If the image fails to load, the component will automatically fall back to displaying the initials (if `name` is provided) or the default user icon.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ImageSourcePropType, ViewProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export type AvatarLoadingStatus = 'idle' | 'loading' | 'loaded' | 'error';
|
|
4
|
+
|
|
5
|
+
interface AvatarProps extends ViewProps {
|
|
6
|
+
/**
|
|
7
|
+
* The image source to display.
|
|
8
|
+
*/
|
|
9
|
+
src?: ImageSourcePropType;
|
|
10
|
+
/**
|
|
11
|
+
* The name associated with the avatar, used for creating initials and accessibility label.
|
|
12
|
+
*/
|
|
13
|
+
name?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Sets the avatar size.
|
|
16
|
+
* @default md
|
|
17
|
+
*/
|
|
18
|
+
size?: 'sm' | 'md';
|
|
19
|
+
/**
|
|
20
|
+
* Delay in milliseconds before the image is rendered.
|
|
21
|
+
* Useful to prevent flickering when the image loads very quickly.
|
|
22
|
+
* @default 0
|
|
23
|
+
*/
|
|
24
|
+
delayMs?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Callback fired when the loading status of the image changes.
|
|
27
|
+
*/
|
|
28
|
+
onLoadingStatusChange?: (status: AvatarLoadingStatus) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default AvatarProps;
|
|
@@ -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,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
|