@utilitywarehouse/hearth-react-native 0.23.0 → 0.25.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 +13 -13
- package/CHANGELOG.md +77 -0
- package/build/components/DatePicker/DatePickerCalendar.js +4 -9
- package/build/components/Modal/Modal.js +5 -4
- package/build/components/Modal/Modal.props.d.ts +10 -4
- package/build/components/ProgressBar/ProgressBar.d.ts +6 -0
- package/build/components/ProgressBar/ProgressBar.js +35 -0
- package/build/components/ProgressBar/ProgressBar.props.d.ts +60 -0
- package/build/components/ProgressBar/ProgressBar.props.js +1 -0
- package/build/components/ProgressBar/ProgressBarCircular.d.ts +6 -0
- package/build/components/ProgressBar/ProgressBarCircular.js +115 -0
- package/build/components/ProgressBar/ProgressBarLinear.d.ts +6 -0
- package/build/components/ProgressBar/ProgressBarLinear.js +79 -0
- package/build/components/ProgressBar/index.d.ts +2 -0
- package/build/components/ProgressBar/index.js +1 -0
- package/build/components/TimePicker/TimePicker.d.ts +6 -0
- package/build/components/TimePicker/TimePicker.js +78 -0
- package/build/components/TimePicker/TimePicker.props.d.ts +45 -0
- package/build/components/TimePicker/TimePicker.props.js +1 -0
- package/build/components/TimePicker/TimePickerView.d.ts +12 -0
- package/build/components/TimePicker/TimePickerView.js +130 -0
- package/build/components/TimePicker/TimePickerWheel.d.ts +8 -0
- package/build/components/TimePicker/TimePickerWheel.js +78 -0
- package/build/components/{DatePicker/time-picker/wheel-web.d.ts → TimePicker/TimePickerWheel.web.d.ts} +4 -4
- package/build/components/TimePicker/TimePickerWheel.web.js +122 -0
- package/build/components/TimePicker/index.d.ts +6 -0
- package/build/components/TimePicker/index.js +3 -0
- package/build/components/TimePickerInput/TimePickerInput.d.ts +6 -0
- package/build/components/TimePickerInput/TimePickerInput.js +127 -0
- package/build/components/TimePickerInput/TimePickerInput.props.d.ts +52 -0
- package/build/components/TimePickerInput/TimePickerInput.props.js +1 -0
- package/build/components/TimePickerInput/TimePickerInputDoneButton.d.ts +8 -0
- package/build/components/TimePickerInput/TimePickerInputDoneButton.js +19 -0
- package/build/components/TimePickerInput/TimePickerInputDoneButton.web.d.ts +5 -0
- package/build/components/TimePickerInput/TimePickerInputDoneButton.web.js +5 -0
- package/build/components/TimePickerInput/index.d.ts +2 -0
- package/build/components/TimePickerInput/index.js +1 -0
- package/build/components/index.d.ts +3 -0
- package/build/components/index.js +3 -0
- package/docs/components/AllComponents.web.tsx +36 -0
- package/package.json +2 -1
- package/src/components/DatePicker/DatePickerCalendar.tsx +30 -13
- package/src/components/Modal/Modal.props.ts +13 -4
- package/src/components/Modal/Modal.stories.tsx +1 -1
- package/src/components/Modal/Modal.tsx +28 -11
- package/src/components/ProgressBar/ProgressBar.docs.mdx +90 -0
- package/src/components/ProgressBar/ProgressBar.figma.tsx +79 -0
- package/src/components/ProgressBar/ProgressBar.props.ts +60 -0
- package/src/components/ProgressBar/ProgressBar.stories.tsx +117 -0
- package/src/components/ProgressBar/ProgressBar.tsx +74 -0
- package/src/components/ProgressBar/ProgressBarCircular.tsx +181 -0
- package/src/components/ProgressBar/ProgressBarLinear.tsx +127 -0
- package/src/components/ProgressBar/index.ts +7 -0
- package/src/components/TimePicker/TimePicker.docs.mdx +84 -0
- package/src/components/TimePicker/TimePicker.figma.tsx +29 -0
- package/src/components/TimePicker/TimePicker.props.ts +45 -0
- package/src/components/TimePicker/TimePicker.stories.tsx +85 -0
- package/src/components/TimePicker/TimePicker.tsx +150 -0
- package/src/components/TimePicker/TimePickerView.tsx +216 -0
- package/src/components/TimePicker/TimePickerWheel.tsx +154 -0
- package/src/components/TimePicker/TimePickerWheel.web.tsx +217 -0
- package/src/components/TimePicker/index.ts +8 -0
- package/src/components/TimePickerInput/TimePickerInput.docs.mdx +135 -0
- package/src/components/TimePickerInput/TimePickerInput.figma.tsx +34 -0
- package/src/components/TimePickerInput/TimePickerInput.props.ts +55 -0
- package/src/components/TimePickerInput/TimePickerInput.stories.tsx +175 -0
- package/src/components/TimePickerInput/TimePickerInput.tsx +283 -0
- package/src/components/TimePickerInput/TimePickerInputDoneButton.tsx +42 -0
- package/src/components/TimePickerInput/TimePickerInputDoneButton.web.tsx +7 -0
- package/src/components/TimePickerInput/index.ts +2 -0
- package/src/components/index.ts +3 -0
- package/build/components/DatePicker/TimePicker.d.ts +0 -3
- package/build/components/DatePicker/TimePicker.js +0 -84
- package/build/components/DatePicker/time-picker/animated-math.d.ts +0 -4
- package/build/components/DatePicker/time-picker/animated-math.js +0 -19
- package/build/components/DatePicker/time-picker/period-native.d.ts +0 -6
- package/build/components/DatePicker/time-picker/period-native.js +0 -17
- package/build/components/DatePicker/time-picker/period-picker.d.ts +0 -6
- package/build/components/DatePicker/time-picker/period-picker.js +0 -10
- package/build/components/DatePicker/time-picker/period-web.d.ts +0 -6
- package/build/components/DatePicker/time-picker/period-web.js +0 -21
- package/build/components/DatePicker/time-picker/wheel-native.d.ts +0 -8
- package/build/components/DatePicker/time-picker/wheel-native.js +0 -19
- package/build/components/DatePicker/time-picker/wheel-picker/index.d.ts +0 -2
- package/build/components/DatePicker/time-picker/wheel-picker/index.js +0 -2
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.d.ts +0 -16
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.js +0 -97
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.d.ts +0 -21
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.js +0 -88
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.d.ts +0 -23
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.js +0 -21
- package/build/components/DatePicker/time-picker/wheel-web.js +0 -146
- package/build/components/DatePicker/time-picker/wheel.d.ts +0 -8
- package/build/components/DatePicker/time-picker/wheel.js +0 -10
- package/src/components/DatePicker/TimePicker.tsx +0 -141
- package/src/components/DatePicker/time-picker/animated-math.ts +0 -33
- package/src/components/DatePicker/time-picker/period-native.tsx +0 -34
- package/src/components/DatePicker/time-picker/period-picker.tsx +0 -16
- package/src/components/DatePicker/time-picker/period-web.tsx +0 -36
- package/src/components/DatePicker/time-picker/wheel-native.tsx +0 -37
- package/src/components/DatePicker/time-picker/wheel-picker/index.ts +0 -3
- package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.tsx +0 -132
- package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.ts +0 -22
- package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker.tsx +0 -200
- package/src/components/DatePicker/time-picker/wheel-web.tsx +0 -180
- package/src/components/DatePicker/time-picker/wheel.tsx +0 -18
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-native';
|
|
2
|
+
import type { ComponentProps } from 'react';
|
|
3
|
+
import { ProgressBar } from '.';
|
|
4
|
+
import { VariantTitle } from '../../../docs/components';
|
|
5
|
+
import { Box } from '../Box';
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: 'Stories / ProgressBar',
|
|
9
|
+
component: ProgressBar,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'centered',
|
|
12
|
+
},
|
|
13
|
+
argTypes: {
|
|
14
|
+
variant: {
|
|
15
|
+
options: ['linear', 'circular'],
|
|
16
|
+
control: 'select',
|
|
17
|
+
description: 'The progress bar variant.',
|
|
18
|
+
},
|
|
19
|
+
colorScheme: {
|
|
20
|
+
options: ['default', 'success', 'danger'],
|
|
21
|
+
control: 'select',
|
|
22
|
+
description: 'The color scheme for the progress indicator.',
|
|
23
|
+
},
|
|
24
|
+
size: {
|
|
25
|
+
options: ['sm', 'md'],
|
|
26
|
+
control: 'select',
|
|
27
|
+
description: 'The circular size. Only applies to the circular variant.',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
args: {
|
|
31
|
+
variant: 'linear',
|
|
32
|
+
colorScheme: 'default',
|
|
33
|
+
size: 'md',
|
|
34
|
+
value: 35,
|
|
35
|
+
min: 0,
|
|
36
|
+
max: 100,
|
|
37
|
+
label: 'Progress',
|
|
38
|
+
},
|
|
39
|
+
} satisfies Meta<typeof ProgressBar>;
|
|
40
|
+
|
|
41
|
+
export default meta;
|
|
42
|
+
|
|
43
|
+
type Story = StoryObj<typeof meta>;
|
|
44
|
+
type ProgressBarStoryArgs = ComponentProps<typeof ProgressBar>;
|
|
45
|
+
|
|
46
|
+
export const Playground: Story = {};
|
|
47
|
+
|
|
48
|
+
export const Variants: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
value: 55,
|
|
51
|
+
label: 'Downloads',
|
|
52
|
+
},
|
|
53
|
+
render: (args: ProgressBarStoryArgs) => (
|
|
54
|
+
<Box gap="300" style={{ width: 260 }}>
|
|
55
|
+
<VariantTitle title="Linear">
|
|
56
|
+
<ProgressBar {...args} variant="linear" />
|
|
57
|
+
</VariantTitle>
|
|
58
|
+
<VariantTitle title="Circular">
|
|
59
|
+
<ProgressBar {...args} variant="circular" />
|
|
60
|
+
</VariantTitle>
|
|
61
|
+
</Box>
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const ColorSchemes: Story = {
|
|
66
|
+
args: {
|
|
67
|
+
value: 72,
|
|
68
|
+
label: 'Storage',
|
|
69
|
+
variant: 'linear',
|
|
70
|
+
},
|
|
71
|
+
render: (args: ProgressBarStoryArgs) => (
|
|
72
|
+
<Box gap="300" style={{ width: 260 }}>
|
|
73
|
+
<VariantTitle title="Default">
|
|
74
|
+
<ProgressBar {...args} colorScheme="default" />
|
|
75
|
+
</VariantTitle>
|
|
76
|
+
<VariantTitle title="Success">
|
|
77
|
+
<ProgressBar {...args} colorScheme="success" />
|
|
78
|
+
</VariantTitle>
|
|
79
|
+
<VariantTitle title="Danger">
|
|
80
|
+
<ProgressBar {...args} colorScheme="danger" />
|
|
81
|
+
</VariantTitle>
|
|
82
|
+
</Box>
|
|
83
|
+
),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const CircularSizes: Story = {
|
|
87
|
+
args: {
|
|
88
|
+
value: 65,
|
|
89
|
+
label: 'Usage',
|
|
90
|
+
variant: 'circular',
|
|
91
|
+
},
|
|
92
|
+
render: (args: ProgressBarStoryArgs) => (
|
|
93
|
+
<Box gap="300">
|
|
94
|
+
<VariantTitle title="Small">
|
|
95
|
+
<ProgressBar {...args} size="sm" />
|
|
96
|
+
</VariantTitle>
|
|
97
|
+
<VariantTitle title="Medium">
|
|
98
|
+
<ProgressBar {...args} size="md" />
|
|
99
|
+
</VariantTitle>
|
|
100
|
+
</Box>
|
|
101
|
+
),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const CustomValueLabels: Story = {
|
|
105
|
+
args: {
|
|
106
|
+
value: 68,
|
|
107
|
+
max: 100,
|
|
108
|
+
label: 'Data allowance',
|
|
109
|
+
variant: 'linear',
|
|
110
|
+
formatValueText: (value: number, { max }: { max: number }) => `${max - value}GB remaining`,
|
|
111
|
+
},
|
|
112
|
+
render: (args: ProgressBarStoryArgs) => (
|
|
113
|
+
<Box style={{ width: 260 }}>
|
|
114
|
+
<ProgressBar {...args} />
|
|
115
|
+
</Box>
|
|
116
|
+
),
|
|
117
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { View } from 'react-native';
|
|
2
|
+
import type ProgressBarProps from './ProgressBar.props';
|
|
3
|
+
import ProgressBarCircular from './ProgressBarCircular';
|
|
4
|
+
import ProgressBarLinear from './ProgressBarLinear';
|
|
5
|
+
|
|
6
|
+
const clampValue = (value: number, min: number, max: number) => {
|
|
7
|
+
if (max <= min) return min;
|
|
8
|
+
return Math.min(Math.max(value, min), max);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const valueToPercent = (value: number, min: number, max: number) => {
|
|
12
|
+
const range = max - min;
|
|
13
|
+
if (range <= 0) return 0;
|
|
14
|
+
return ((value - min) / range) * 100;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const ProgressBar = ({
|
|
18
|
+
variant = 'linear',
|
|
19
|
+
colorScheme = 'default',
|
|
20
|
+
size = 'md',
|
|
21
|
+
value,
|
|
22
|
+
min = 0,
|
|
23
|
+
max = 100,
|
|
24
|
+
label,
|
|
25
|
+
hideLabel,
|
|
26
|
+
formatValueText,
|
|
27
|
+
'aria-valuetext': ariaValueText,
|
|
28
|
+
accessibilityLabel,
|
|
29
|
+
...rest
|
|
30
|
+
}: ProgressBarProps) => {
|
|
31
|
+
const effectiveValue =
|
|
32
|
+
colorScheme === 'success' && !formatValueText ? max : clampValue(value, min, max);
|
|
33
|
+
const percentValue = valueToPercent(effectiveValue, min, max);
|
|
34
|
+
const clampedPercent = Math.max(0, Math.min(100, percentValue));
|
|
35
|
+
const valueText = formatValueText
|
|
36
|
+
? formatValueText(effectiveValue, { min, max, percent: clampedPercent })
|
|
37
|
+
: `${Math.round(clampedPercent)}%`;
|
|
38
|
+
const valueTextForAria = ariaValueText ?? valueText;
|
|
39
|
+
|
|
40
|
+
const internalProps = {
|
|
41
|
+
percent: clampedPercent,
|
|
42
|
+
label,
|
|
43
|
+
valueText,
|
|
44
|
+
hideLabel,
|
|
45
|
+
colorScheme,
|
|
46
|
+
size,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<View
|
|
51
|
+
{...rest}
|
|
52
|
+
accessible
|
|
53
|
+
role="progressbar"
|
|
54
|
+
accessibilityRole="progressbar"
|
|
55
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
56
|
+
accessibilityValue={{ min, max, now: effectiveValue, text: valueTextForAria }}
|
|
57
|
+
aria-valuenow={effectiveValue}
|
|
58
|
+
aria-valuemin={min}
|
|
59
|
+
aria-valuemax={max}
|
|
60
|
+
aria-valuetext={valueTextForAria}
|
|
61
|
+
data-colorscheme={colorScheme}
|
|
62
|
+
>
|
|
63
|
+
{variant === 'circular' ? (
|
|
64
|
+
<ProgressBarCircular {...internalProps} />
|
|
65
|
+
) : (
|
|
66
|
+
<ProgressBarLinear {...internalProps} />
|
|
67
|
+
)}
|
|
68
|
+
</View>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
ProgressBar.displayName = 'ProgressBar';
|
|
73
|
+
|
|
74
|
+
export default ProgressBar;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { Text, View } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
Easing,
|
|
5
|
+
useAnimatedProps,
|
|
6
|
+
useReducedMotion,
|
|
7
|
+
useSharedValue,
|
|
8
|
+
withTiming,
|
|
9
|
+
} from 'react-native-reanimated';
|
|
10
|
+
import { Circle, G, Svg } from 'react-native-svg';
|
|
11
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
12
|
+
import useTheme from '../../hooks/useTheme';
|
|
13
|
+
import { BodyText } from '../BodyText';
|
|
14
|
+
import type { ProgressBarInternalProps } from './ProgressBar.props';
|
|
15
|
+
|
|
16
|
+
const AnimatedCircle = Animated.createAnimatedComponent(Circle as React.ComponentType<any>);
|
|
17
|
+
|
|
18
|
+
const ProgressBarCircular = ({
|
|
19
|
+
percent,
|
|
20
|
+
label,
|
|
21
|
+
valueText,
|
|
22
|
+
hideLabel,
|
|
23
|
+
colorScheme,
|
|
24
|
+
size,
|
|
25
|
+
}: ProgressBarInternalProps) => {
|
|
26
|
+
const { components } = useTheme();
|
|
27
|
+
const isReducedMotion = useReducedMotion();
|
|
28
|
+
const progress = useSharedValue(0);
|
|
29
|
+
const hasMountedRef = useRef(false);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const target = Math.max(0, Math.min(100, percent)) / 100;
|
|
33
|
+
if (isReducedMotion) {
|
|
34
|
+
progress.value = target;
|
|
35
|
+
hasMountedRef.current = true;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!hasMountedRef.current) {
|
|
40
|
+
progress.value = target;
|
|
41
|
+
hasMountedRef.current = true;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
progress.value = withTiming(target, { duration: 300, easing: Easing.out(Easing.ease) });
|
|
46
|
+
}, [percent, isReducedMotion, progress]);
|
|
47
|
+
|
|
48
|
+
const circularTokens = components.progressBar.circular[size];
|
|
49
|
+
const barWidth = 'bar' in circularTokens ? circularTokens.bar.width : circularTokens.barWidth;
|
|
50
|
+
const diameter = circularTokens.height;
|
|
51
|
+
const radius = (diameter - barWidth) / 2;
|
|
52
|
+
const circumference = 2 * Math.PI * radius;
|
|
53
|
+
|
|
54
|
+
const animatedCircleProps = useAnimatedProps(() => ({
|
|
55
|
+
strokeDashoffset: circumference * (1 - progress.value),
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
const indicatorColor =
|
|
59
|
+
colorScheme === 'success'
|
|
60
|
+
? components.progressBar.progress.successColor
|
|
61
|
+
: colorScheme === 'danger'
|
|
62
|
+
? components.progressBar.progress.dangerColor
|
|
63
|
+
: components.progressBar.progress.defaultColor;
|
|
64
|
+
|
|
65
|
+
styles.useVariants({ size });
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<View style={styles.container}>
|
|
69
|
+
<Svg
|
|
70
|
+
width={diameter}
|
|
71
|
+
height={diameter}
|
|
72
|
+
viewBox={`0 0 ${diameter} ${diameter}`}
|
|
73
|
+
style={styles.svg}
|
|
74
|
+
>
|
|
75
|
+
<G origin={`${diameter / 2}, ${diameter / 2}`} rotation={-90}>
|
|
76
|
+
<Circle
|
|
77
|
+
cx="50%"
|
|
78
|
+
cy="50%"
|
|
79
|
+
r={radius}
|
|
80
|
+
stroke={components.progressBar.barColor}
|
|
81
|
+
strokeWidth={barWidth}
|
|
82
|
+
fill="transparent"
|
|
83
|
+
/>
|
|
84
|
+
<AnimatedCircle
|
|
85
|
+
cx="50%"
|
|
86
|
+
cy="50%"
|
|
87
|
+
r={radius}
|
|
88
|
+
stroke={indicatorColor}
|
|
89
|
+
strokeWidth={barWidth}
|
|
90
|
+
fill="transparent"
|
|
91
|
+
strokeLinecap="round"
|
|
92
|
+
strokeDasharray={circumference}
|
|
93
|
+
animatedProps={animatedCircleProps}
|
|
94
|
+
/>
|
|
95
|
+
</G>
|
|
96
|
+
</Svg>
|
|
97
|
+
<View style={styles.content}>
|
|
98
|
+
<Text style={styles.valueText}>{valueText}</Text>
|
|
99
|
+
{!hideLabel && size === 'md' ? (
|
|
100
|
+
<BodyText style={styles.label} size="md" weight="semibold">
|
|
101
|
+
{label}
|
|
102
|
+
</BodyText>
|
|
103
|
+
) : null}
|
|
104
|
+
</View>
|
|
105
|
+
</View>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
ProgressBarCircular.displayName = 'ProgressBarCircular';
|
|
110
|
+
|
|
111
|
+
const styles = StyleSheet.create(theme => ({
|
|
112
|
+
container: {
|
|
113
|
+
alignItems: 'center',
|
|
114
|
+
justifyContent: 'center',
|
|
115
|
+
position: 'relative',
|
|
116
|
+
variants: {
|
|
117
|
+
size: {
|
|
118
|
+
md: {
|
|
119
|
+
width: theme.components.progressBar.circular.md.height,
|
|
120
|
+
height: theme.components.progressBar.circular.md.height,
|
|
121
|
+
},
|
|
122
|
+
sm: {
|
|
123
|
+
width: theme.components.progressBar.circular.sm.height,
|
|
124
|
+
height: theme.components.progressBar.circular.sm.height,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
svg: {
|
|
130
|
+
position: 'absolute',
|
|
131
|
+
top: 0,
|
|
132
|
+
left: 0,
|
|
133
|
+
},
|
|
134
|
+
content: {
|
|
135
|
+
alignItems: 'center',
|
|
136
|
+
justifyContent: 'center',
|
|
137
|
+
_web: {
|
|
138
|
+
position: 'absolute',
|
|
139
|
+
top: 0,
|
|
140
|
+
left: 0,
|
|
141
|
+
width: '100%',
|
|
142
|
+
height: '100%',
|
|
143
|
+
},
|
|
144
|
+
variants: {
|
|
145
|
+
size: {
|
|
146
|
+
md: {
|
|
147
|
+
gap: theme.components.progressBar.circular.md.gap,
|
|
148
|
+
},
|
|
149
|
+
sm: {
|
|
150
|
+
gap: 0,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
valueText: {
|
|
156
|
+
color: theme.color.text.primary,
|
|
157
|
+
textAlign: 'center',
|
|
158
|
+
variants: {
|
|
159
|
+
size: {
|
|
160
|
+
md: {
|
|
161
|
+
fontFamily: theme.components.progressBar.circular.md.label.fontFamily,
|
|
162
|
+
fontSize: theme.components.progressBar.circular.md.label.fontSize,
|
|
163
|
+
lineHeight: theme.components.progressBar.circular.md.label.lineHeight,
|
|
164
|
+
fontWeight: theme.components.progressBar.circular.md.label.fontWeight,
|
|
165
|
+
},
|
|
166
|
+
sm: {
|
|
167
|
+
fontFamily: theme.typography.mobile.bodyText.fontFamily,
|
|
168
|
+
fontSize: theme.typography.mobile.bodyText.md.fontSize,
|
|
169
|
+
lineHeight: theme.typography.mobile.bodyText.md.lineHeight,
|
|
170
|
+
fontWeight: theme.fontWeight.semibold,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
label: {
|
|
176
|
+
textAlign: 'center',
|
|
177
|
+
maxWidth: 90,
|
|
178
|
+
},
|
|
179
|
+
}));
|
|
180
|
+
|
|
181
|
+
export default ProgressBarCircular;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { LayoutChangeEvent, Platform, View } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
Easing,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
useReducedMotion,
|
|
7
|
+
useSharedValue,
|
|
8
|
+
withTiming,
|
|
9
|
+
} from 'react-native-reanimated';
|
|
10
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
11
|
+
import useTheme from '../../hooks/useTheme';
|
|
12
|
+
import { BodyText } from '../BodyText';
|
|
13
|
+
import type { ProgressBarInternalProps } from './ProgressBar.props';
|
|
14
|
+
|
|
15
|
+
const ProgressBarLinear = ({
|
|
16
|
+
percent,
|
|
17
|
+
label,
|
|
18
|
+
valueText,
|
|
19
|
+
hideLabel,
|
|
20
|
+
colorScheme,
|
|
21
|
+
}: ProgressBarInternalProps) => {
|
|
22
|
+
const { components } = useTheme();
|
|
23
|
+
const isReducedMotion = useReducedMotion();
|
|
24
|
+
const progress = useSharedValue(0);
|
|
25
|
+
const hasMountedRef = useRef(false);
|
|
26
|
+
const [trackWidth, setTrackWidth] = useState(0);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const target = Math.max(0, Math.min(100, percent)) / 100;
|
|
30
|
+
if (isReducedMotion) {
|
|
31
|
+
progress.value = target;
|
|
32
|
+
hasMountedRef.current = true;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!hasMountedRef.current) {
|
|
37
|
+
progress.value = target;
|
|
38
|
+
hasMountedRef.current = true;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
progress.value = withTiming(target, { duration: 300, easing: Easing.out(Easing.ease) });
|
|
43
|
+
}, [percent, isReducedMotion, progress]);
|
|
44
|
+
|
|
45
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
46
|
+
width: trackWidth * progress.value,
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
const indicatorColor =
|
|
50
|
+
colorScheme === 'success'
|
|
51
|
+
? components.progressBar.progress.successColor
|
|
52
|
+
: colorScheme === 'danger'
|
|
53
|
+
? components.progressBar.progress.dangerColor
|
|
54
|
+
: components.progressBar.progress.defaultColor;
|
|
55
|
+
|
|
56
|
+
const handleTrackLayout = (event: LayoutChangeEvent) => {
|
|
57
|
+
setTrackWidth(event.nativeEvent.layout.width);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<View style={styles.container}>
|
|
62
|
+
{!hideLabel && (
|
|
63
|
+
<View style={styles.header}>
|
|
64
|
+
<BodyText size="md" weight="semibold" style={styles.label}>
|
|
65
|
+
{label}
|
|
66
|
+
</BodyText>
|
|
67
|
+
<BodyText size="md" style={styles.value}>
|
|
68
|
+
{valueText}
|
|
69
|
+
</BodyText>
|
|
70
|
+
</View>
|
|
71
|
+
)}
|
|
72
|
+
<View style={styles.track} onLayout={handleTrackLayout}>
|
|
73
|
+
{Platform.OS === 'web' ? (
|
|
74
|
+
<View
|
|
75
|
+
style={[
|
|
76
|
+
styles.indicator,
|
|
77
|
+
{ width: `${Math.max(0, Math.min(100, percent))}%` },
|
|
78
|
+
{ backgroundColor: indicatorColor },
|
|
79
|
+
]}
|
|
80
|
+
/>
|
|
81
|
+
) : (
|
|
82
|
+
<Animated.View
|
|
83
|
+
style={[styles.indicator, animatedStyle, { backgroundColor: indicatorColor }]}
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
86
|
+
</View>
|
|
87
|
+
</View>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
ProgressBarLinear.displayName = 'ProgressBarLinear';
|
|
92
|
+
|
|
93
|
+
const styles = StyleSheet.create(theme => ({
|
|
94
|
+
container: {
|
|
95
|
+
width: '100%',
|
|
96
|
+
gap: theme.components.progressBar.linear.gap,
|
|
97
|
+
},
|
|
98
|
+
header: {
|
|
99
|
+
flexDirection: 'row',
|
|
100
|
+
alignItems: 'center',
|
|
101
|
+
justifyContent: 'space-between',
|
|
102
|
+
gap: theme.components.progressBar.linear.label.gap,
|
|
103
|
+
},
|
|
104
|
+
label: {
|
|
105
|
+
flex: 1,
|
|
106
|
+
},
|
|
107
|
+
value: {
|
|
108
|
+
flexShrink: 0,
|
|
109
|
+
textAlign: 'right',
|
|
110
|
+
},
|
|
111
|
+
track: {
|
|
112
|
+
width: '100%',
|
|
113
|
+
height: theme.components.progressBar.linear.bar.height,
|
|
114
|
+
backgroundColor: theme.components.progressBar.barColor,
|
|
115
|
+
borderRadius: theme.components.progressBar.linear.bar.borderRadius,
|
|
116
|
+
overflow: 'hidden',
|
|
117
|
+
},
|
|
118
|
+
indicator: {
|
|
119
|
+
height: '100%',
|
|
120
|
+
borderRadius: theme.components.progressBar.linear.bar.borderRadius,
|
|
121
|
+
_web: {
|
|
122
|
+
height: '100%',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
export default ProgressBarLinear;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
import { Button, Center } from '../../';
|
|
3
|
+
import { BackToTopButton, UsageWrap, ViewFigmaButton } from '../../../docs/components';
|
|
4
|
+
import * as Stories from './TimePicker.stories';
|
|
5
|
+
|
|
6
|
+
<ViewFigmaButton url="https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=10334-16770&t=Jg2fPJPQNzOyspmQ-4" />
|
|
7
|
+
|
|
8
|
+
<Meta title="Components / Time Picker" />
|
|
9
|
+
|
|
10
|
+
<BackToTopButton />
|
|
11
|
+
|
|
12
|
+
# Time Picker
|
|
13
|
+
|
|
14
|
+
`TimePicker` presents a wheel-based time selector inside a bottom sheet, letting people pick hours and minutes without leaving the current context. It supports 12-hour and 24-hour clocks and returns a JavaScript `Date` whenever the selection changes.
|
|
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
|
+
Use the `TimePicker` with a ref to present the bottom sheet when people tap a trigger button.
|
|
30
|
+
|
|
31
|
+
<UsageWrap>
|
|
32
|
+
<Center>
|
|
33
|
+
<Button onPress={() => {}}>Pick a time</Button>
|
|
34
|
+
</Center>
|
|
35
|
+
</UsageWrap>
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { useRef, useState } from 'react';
|
|
39
|
+
import {
|
|
40
|
+
BottomSheetModalProvider,
|
|
41
|
+
Button,
|
|
42
|
+
TimePicker,
|
|
43
|
+
type DateType,
|
|
44
|
+
} from '@utilitywarehouse/hearth-react-native';
|
|
45
|
+
|
|
46
|
+
const BookingTime = () => {
|
|
47
|
+
const pickerRef = useRef(null);
|
|
48
|
+
const [time, setTime] = useState<DateType>();
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<BottomSheetModalProvider>
|
|
52
|
+
<Button onPress={() => pickerRef.current?.present()}>Choose time</Button>
|
|
53
|
+
|
|
54
|
+
<TimePicker
|
|
55
|
+
ref={pickerRef}
|
|
56
|
+
date={time}
|
|
57
|
+
onChange={({ date }) => setTime(date)}
|
|
58
|
+
onCancel={() => setTime(undefined)}
|
|
59
|
+
use12Hours={false}
|
|
60
|
+
/>
|
|
61
|
+
</BottomSheetModalProvider>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Props
|
|
67
|
+
|
|
68
|
+
`TimePicker` extends the `BottomSheetModal` component. The table below highlights the main props.
|
|
69
|
+
|
|
70
|
+
| Prop | Type | Default | Description |
|
|
71
|
+
| ---------------- | --------------------------------------- | ---------- | --------------------------------------------------------------- |
|
|
72
|
+
| `date` | `DateType` | `-` | Selected time value. |
|
|
73
|
+
| `timeZone` | `string` | `-` | IANA time zone identifier applied to the selected time. |
|
|
74
|
+
| `use12Hours` | `boolean` | `false` | Displays an AM/PM selector and formats hours from 1 to 12. |
|
|
75
|
+
| `minuteInterval` | `number` | `1` | Step interval for minutes shown in the picker. |
|
|
76
|
+
| `hideFooter` | `boolean` | `false` | Hides the Cancel/Ok actions. |
|
|
77
|
+
| `onChange` | `(payload: { date: DateType }) => void` | `-` | Fired whenever the selected time changes. |
|
|
78
|
+
| `onCancel` | `() => void` | `() => {}` | Fired when the cancel action is triggered. |
|
|
79
|
+
| `ref` | `Ref<BottomSheetModalMethods>` | `-` | Gives imperative access to present or dismiss the bottom sheet. |
|
|
80
|
+
|
|
81
|
+
## Accessibility
|
|
82
|
+
|
|
83
|
+
- Screen readers announce the picker when the sheet opens and focus the wheel area on Android.
|
|
84
|
+
- Action buttons are exposed as standard buttons with clear labels.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import figma from '@figma/code-connect';
|
|
2
|
+
import { useRef, useState } from 'react';
|
|
3
|
+
import { BottomSheetModalProvider, Button, DateType, TimePicker } from '../';
|
|
4
|
+
|
|
5
|
+
figma.connect(
|
|
6
|
+
TimePicker,
|
|
7
|
+
'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=10334-16770&t=Jg2fPJPQNzOyspmQ-4',
|
|
8
|
+
{
|
|
9
|
+
props: {},
|
|
10
|
+
example: props => {
|
|
11
|
+
const pickerRef = useRef(null);
|
|
12
|
+
const [time, setTime] = useState<DateType>();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<BottomSheetModalProvider>
|
|
16
|
+
<Button onPress={() => pickerRef.current?.present()}>Choose time</Button>
|
|
17
|
+
|
|
18
|
+
<TimePicker
|
|
19
|
+
ref={pickerRef}
|
|
20
|
+
date={time}
|
|
21
|
+
onChange={({ date }) => setTime(date)}
|
|
22
|
+
onCancel={() => setTime(undefined)}
|
|
23
|
+
use12Hours={false}
|
|
24
|
+
/>
|
|
25
|
+
</BottomSheetModalProvider>
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
|
|
2
|
+
import type { Ref } from 'react';
|
|
3
|
+
import type { ViewStyle } from 'react-native';
|
|
4
|
+
import type { DateType, PickerOption } from '../DatePicker/DatePicker.props';
|
|
5
|
+
|
|
6
|
+
export interface TimePickerProps {
|
|
7
|
+
/**
|
|
8
|
+
* IANA time zone identifier applied when normalising and comparing times.
|
|
9
|
+
*/
|
|
10
|
+
timeZone?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Controlled time value.
|
|
13
|
+
*/
|
|
14
|
+
date?: DateType;
|
|
15
|
+
/**
|
|
16
|
+
* Fired whenever a time is picked.
|
|
17
|
+
*/
|
|
18
|
+
onChange?: (params: { date: DateType }) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Display a 12-hour clock with AM/PM selector.
|
|
21
|
+
*/
|
|
22
|
+
use12Hours?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Step interval for minutes shown in the picker.
|
|
25
|
+
*/
|
|
26
|
+
minuteInterval?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Hide the footer actions.
|
|
29
|
+
*/
|
|
30
|
+
hideFooter?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Custom container styling for the time picker surface.
|
|
33
|
+
*/
|
|
34
|
+
style?: ViewStyle;
|
|
35
|
+
/**
|
|
36
|
+
* Gives imperative access to the bottom sheet instance.
|
|
37
|
+
*/
|
|
38
|
+
ref?: Ref<BottomSheetModalMethods<any>>;
|
|
39
|
+
/**
|
|
40
|
+
* Fired when the cancel action is triggered.
|
|
41
|
+
*/
|
|
42
|
+
onCancel?: () => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type { DateType, PickerOption };
|