@utilitywarehouse/hearth-react-native 0.23.0 → 0.24.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 (31) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +13 -13
  3. package/CHANGELOG.md +36 -0
  4. package/build/components/Modal/Modal.js +5 -4
  5. package/build/components/Modal/Modal.props.d.ts +10 -4
  6. package/build/components/ProgressBar/ProgressBar.d.ts +6 -0
  7. package/build/components/ProgressBar/ProgressBar.js +35 -0
  8. package/build/components/ProgressBar/ProgressBar.props.d.ts +60 -0
  9. package/build/components/ProgressBar/ProgressBar.props.js +1 -0
  10. package/build/components/ProgressBar/ProgressBarCircular.d.ts +6 -0
  11. package/build/components/ProgressBar/ProgressBarCircular.js +115 -0
  12. package/build/components/ProgressBar/ProgressBarLinear.d.ts +6 -0
  13. package/build/components/ProgressBar/ProgressBarLinear.js +79 -0
  14. package/build/components/ProgressBar/index.d.ts +2 -0
  15. package/build/components/ProgressBar/index.js +1 -0
  16. package/build/components/index.d.ts +1 -0
  17. package/build/components/index.js +1 -0
  18. package/docs/components/AllComponents.web.tsx +6 -0
  19. package/package.json +1 -1
  20. package/src/components/Modal/Modal.props.ts +13 -4
  21. package/src/components/Modal/Modal.stories.tsx +1 -1
  22. package/src/components/Modal/Modal.tsx +28 -11
  23. package/src/components/ProgressBar/ProgressBar.docs.mdx +90 -0
  24. package/src/components/ProgressBar/ProgressBar.figma.tsx +79 -0
  25. package/src/components/ProgressBar/ProgressBar.props.ts +60 -0
  26. package/src/components/ProgressBar/ProgressBar.stories.tsx +117 -0
  27. package/src/components/ProgressBar/ProgressBar.tsx +74 -0
  28. package/src/components/ProgressBar/ProgressBarCircular.tsx +181 -0
  29. package/src/components/ProgressBar/ProgressBarLinear.tsx +127 -0
  30. package/src/components/ProgressBar/index.ts +7 -0
  31. package/src/components/index.ts +1 -0
@@ -1,4 +1,4 @@
1
1
 
2
- > @utilitywarehouse/hearth-react-native@0.23.0 build /home/runner/work/hearth/hearth/packages/react-native
2
+ > @utilitywarehouse/hearth-react-native@0.24.0 build /home/runner/work/hearth/hearth/packages/react-native
3
3
  > tsc
4
4
 
@@ -1,5 +1,5 @@
1
1
 
2
- > @utilitywarehouse/hearth-react-native@0.23.0 lint /home/runner/work/hearth/hearth/packages/react-native
2
+ > @utilitywarehouse/hearth-react-native@0.24.0 lint /home/runner/work/hearth/hearth/packages/react-native
3
3
  > TIMING=1 eslint .
4
4
 
5
5
 
@@ -31,8 +31,8 @@
31
31
  78:8 warning React Hook useEffect has a missing dependency: 'formFieldContext'. Either include it or remove the dependency array react-hooks/exhaustive-deps
32
32
 
33
33
  /home/runner/work/hearth/hearth/packages/react-native/src/components/Modal/Modal.tsx
34
- 74:6 warning React Hook useCallback has an unnecessary dependency: 'Platform.OS'. Either exclude it or remove the dependency array. Outer scope values like 'Platform.OS' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
35
- 274:5 warning React Hook useCallback has a missing dependency: 'footer'. Either include it or remove the dependency array react-hooks/exhaustive-deps
34
+ 75:6 warning React Hook useCallback has an unnecessary dependency: 'Platform.OS'. Either exclude it or remove the dependency array. Outer scope values like 'Platform.OS' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
35
+ 291:5 warning React Hook useCallback has a missing dependency: 'footer'. Either include it or remove the dependency array react-hooks/exhaustive-deps
36
36
 
37
37
  /home/runner/work/hearth/hearth/packages/react-native/src/components/Modal/Modal.web.tsx
38
38
  66:6 warning React Hook useCallback has an unnecessary dependency: 'Platform.OS'. Either exclude it or remove the dependency array. Outer scope values like 'Platform.OS' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
@@ -60,13 +60,13 @@
60
60
 
61
61
  Rule | Time (ms) | Relative
62
62
  :-----------------------------------------|----------:|--------:
63
- @typescript-eslint/no-unused-vars | 2022.725 | 59.1%
64
- react-hooks/exhaustive-deps | 192.308 | 5.6%
65
- no-global-assign | 140.412 | 4.1%
66
- react-hooks/rules-of-hooks | 101.894 | 3.0%
67
- @typescript-eslint/ban-ts-comment | 78.579 | 2.3%
68
- no-misleading-character-class | 64.143 | 1.9%
69
- no-unexpected-multiline | 47.419 | 1.4%
70
- @typescript-eslint/triple-slash-reference | 40.424 | 1.2%
71
- no-loss-of-precision | 39.686 | 1.2%
72
- no-regex-spaces | 38.266 | 1.1%
63
+ @typescript-eslint/no-unused-vars | 1400.036 | 60.2%
64
+ react-hooks/exhaustive-deps | 100.065 | 4.3%
65
+ react-hooks/rules-of-hooks | 83.204 | 3.6%
66
+ no-global-assign | 71.807 | 3.1%
67
+ @typescript-eslint/ban-ts-comment | 53.514 | 2.3%
68
+ no-loss-of-precision | 35.722 | 1.5%
69
+ @typescript-eslint/triple-slash-reference | 35.355 | 1.5%
70
+ no-unexpected-multiline | 32.651 | 1.4%
71
+ no-useless-escape | 30.776 | 1.3%
72
+ no-misleading-character-class | 29.528 | 1.3%
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # @utilitywarehouse/hearth-react-native
2
2
 
3
+ ## 0.24.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#977](https://github.com/utilitywarehouse/hearth/pull/977) [`9d2b534`](https://github.com/utilitywarehouse/hearth/commit/9d2b5348a5748cb613f537808069de2e86bd21d7) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `ProgressBar` component with linear and circular variants.
8
+
9
+ **Developer changes**:
10
+
11
+ Use `ProgressBar` with a default percentage label, or override the label to show a custom value:
12
+
13
+ ```tsx
14
+ import { ProgressBar } from '@utilitywarehouse/hearth-react-native';
15
+
16
+ <ProgressBar value={42} label="Uploading documents" />
17
+
18
+ <ProgressBar
19
+ value={68}
20
+ max={100}
21
+ label="Data allowance"
22
+ variant="circular"
23
+ formatValueText={(value, { max }) => `${max - value}GB remaining`}
24
+ />
25
+ ```
26
+
27
+ ### Patch Changes
28
+
29
+ - [#978](https://github.com/utilitywarehouse/hearth/pull/978) [`26a1173`](https://github.com/utilitywarehouse/hearth/commit/26a11731a493a8b92ac2a3a183516376ab54663b) Thanks [@jordmccord](https://github.com/jordmccord)! - 💅 [ENHANCEMENT]: Tighten `Modal` prop types and fix brand background text styling
30
+
31
+ Improves TypeScript safety so `stickyFooter` is not allowed when `inNavModal` is true, and `background` can only be set when `inNavModal` is true. Also ensures headings, body text, and button content are correctly inverted when using the brand background.
32
+
33
+ **Components affected**:
34
+ - `Modal`
35
+
36
+ **Developer changes**:
37
+ No changes required unless you were relying on invalid prop combinations.
38
+
3
39
  ## 0.23.0
4
40
 
5
41
  ### Minor Changes
@@ -20,6 +20,7 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
20
20
  const theme = useTheme();
21
21
  const backgroundOpacity = useSharedValue(0);
22
22
  const pretendContentTranslateY = useSharedValue(20);
23
+ const isBrandBackground = background === 'brand';
23
24
  const triggerCloseAnimation = useCallback(() => {
24
25
  if (Platform.OS === 'android' && inNavModal) {
25
26
  pretendContentTranslateY.value = withTiming(20, {
@@ -105,10 +106,10 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
105
106
  noButtons,
106
107
  stickyFooter,
107
108
  showHandle: props.showHandle,
108
- background: background === 'brand' ? 'brand' : 'primary',
109
+ background: isBrandBackground ? 'brand' : 'primary',
109
110
  });
110
- const footer = (_jsxs(View, { style: styles.footer, children: [onPressPrimaryButton && primaryButtonText ? (_jsx(Button, { onPress: handlePrimaryButtonPress, text: primaryButtonText, inverted: background === 'brand' && inNavModal, ...primaryButtonProps, variant: primaryButtonProps?.variant ?? 'solid', colorScheme: primaryButtonProps?.colorScheme ?? 'highlight' })) : null, onPressSecondaryButton && secondaryButtonText ? (_jsx(Button, { onPress: handleSecondaryButtonPress, text: secondaryButtonText, inverted: background === 'brand' && inNavModal, ...secondaryButtonProps, variant: secondaryButtonProps?.variant ?? 'outline', colorScheme: secondaryButtonProps?.colorScheme ?? 'functional' })) : null] }));
111
- const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Loading' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsx(Spinner, { size: "lg" }), _jsx(Heading, { size: "lg", textAlign: "center", children: loadingHeading })] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, children: heading })) : null, description && !image ? _jsx(BodyText, { accessible: true, children: description }) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", inverted: background === 'brand' && inNavModal, ...closeButtonProps })) : null] }), image ? (_jsxs(View, { style: styles.imageContainer, children: [image, _jsxs(View, { style: styles.textContent, children: [heading ? (_jsx(Heading, { size: "lg", textAlign: "center", accessible: true, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, children: description })) : null] })] })) : null, inNavModal ? _jsx(ScrollView, { style: { flex: 1 }, children: children }) : children, (!stickyFooter || inNavModal) && !noButtons ? footer : null] })) }));
111
+ const footer = (_jsxs(View, { style: styles.footer, children: [onPressPrimaryButton && primaryButtonText ? (_jsx(Button, { onPress: handlePrimaryButtonPress, text: primaryButtonText, inverted: isBrandBackground && inNavModal, ...primaryButtonProps, variant: primaryButtonProps?.variant ?? 'solid', colorScheme: primaryButtonProps?.colorScheme ?? 'highlight' })) : null, onPressSecondaryButton && secondaryButtonText ? (_jsx(Button, { onPress: handleSecondaryButtonPress, text: secondaryButtonText, inverted: isBrandBackground && inNavModal, ...secondaryButtonProps, variant: secondaryButtonProps?.variant ?? 'outline', colorScheme: secondaryButtonProps?.colorScheme ?? 'functional' })) : null] }));
112
+ const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Loading' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsx(Spinner, { size: "lg", color: isBrandBackground && inNavModal ? theme.color.icon.inverted : undefined }), _jsx(Heading, { size: "lg", textAlign: "center", inverted: isBrandBackground && inNavModal, children: loadingHeading })] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, inverted: isBrandBackground && inNavModal, children: heading })) : null, description && !image ? (_jsx(BodyText, { accessible: true, inverted: isBrandBackground && inNavModal, children: description })) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", inverted: isBrandBackground && inNavModal, ...closeButtonProps })) : null] }), image ? (_jsxs(View, { style: styles.imageContainer, children: [image, _jsxs(View, { style: styles.textContent, children: [heading ? (_jsx(Heading, { size: "lg", textAlign: "center", accessible: true, inverted: isBrandBackground && inNavModal, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, inverted: isBrandBackground && inNavModal, children: description })) : null] })] })) : null, inNavModal ? _jsx(ScrollView, { style: { flex: 1 }, children: children }) : children, (!stickyFooter || inNavModal) && !noButtons ? footer : null] })) }));
112
113
  const renderFooter = useCallback((props) => (_jsx(BottomSheetFooter, { ...props, children: _jsx(View, { style: styles.footerWrap, children: footer }) })), [
113
114
  onPressPrimaryButton,
114
115
  primaryButtonText,
@@ -119,7 +120,7 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
119
120
  ]);
120
121
  return inNavModal ? (_jsxs(View, { style: {
121
122
  flex: 1,
122
- backgroundColor: theme.color.background[background === 'brand' ? 'brand' : 'primary'],
123
+ backgroundColor: theme.color.background[isBrandBackground ? 'brand' : 'primary'],
123
124
  }, children: [Platform.OS === 'android' ? (_jsx(Animated.View, { style: [styles.androidContainer, animatedBackgroundStyle], children: _jsx(Animated.View, { style: [styles.pretendContent, animatedPretendContentStyle] }) })) : null, _jsx(Animated.View, { style: [styles.inNavModalContainer, Platform.OS === 'android' && animatedInNavModalStyle], children: _jsx(View, { style: styles.inNavModalContent, children: content }) })] })) : (_jsxs(BottomSheetModal, { ref: bottomSheetModalRef, enableDynamicSizing: true, snapPoints: image || fullscreen ? ['90%'] : props.snapPoints, showHandle: typeof loading !== 'undefined' && loading ? false : props.showHandle, accessible: false, style: styles.modal, footerComponent: stickyFooter && !noButtons ? renderFooter : undefined, ...props, onChange: handleChange, children: [loading ? _jsx(View, { style: styles.loadingTop }) : null, _jsx(BottomSheetScrollView, { contentContainerStyle: styles.scrollView, ref: scrollViewRef, children: content })] }));
124
125
  };
125
126
  const styles = StyleSheet.create((theme, rt) => ({
@@ -3,16 +3,14 @@ import { ViewProps } from 'react-native';
3
3
  import { BottomSheetProps } from '../BottomSheet';
4
4
  import { ButtonWithoutChildrenProps } from '../Button/Button.props';
5
5
  import { UnstyledIconButtonProps } from '../UnstyledIconButton';
6
- interface ModalProps extends Omit<BottomSheetProps, 'children'> {
6
+ interface ModalPropsBase extends Omit<BottomSheetProps, 'children'> {
7
7
  loading?: boolean;
8
8
  image?: ReactNode;
9
9
  showCloseButton?: boolean;
10
10
  heading?: string;
11
11
  loadingHeading?: string;
12
12
  description?: string;
13
- inNavModal?: boolean;
14
13
  fullscreen?: boolean;
15
- stickyFooter?: boolean;
16
14
  children?: ViewProps['children'];
17
15
  onPressPrimaryButton?: () => void;
18
16
  primaryButtonText?: string;
@@ -24,6 +22,14 @@ interface ModalProps extends Omit<BottomSheetProps, 'children'> {
24
22
  primaryButtonProps?: Omit<ButtonWithoutChildrenProps, 'children'>;
25
23
  secondaryButtonProps?: Omit<ButtonWithoutChildrenProps, 'children'>;
26
24
  closeButtonProps?: Omit<UnstyledIconButtonProps, 'children'>;
27
- background?: 'default' | 'brand';
28
25
  }
26
+ type ModalProps = (ModalPropsBase & {
27
+ inNavModal?: false | undefined;
28
+ stickyFooter?: boolean;
29
+ background?: never;
30
+ }) | (ModalPropsBase & {
31
+ inNavModal: true;
32
+ stickyFooter?: never;
33
+ background?: 'default' | 'brand';
34
+ });
29
35
  export default ModalProps;
@@ -0,0 +1,6 @@
1
+ import type ProgressBarProps from './ProgressBar.props';
2
+ declare const ProgressBar: {
3
+ ({ variant, colorScheme, size, value, min, max, label, hideLabel, formatValueText, "aria-valuetext": ariaValueText, accessibilityLabel, ...rest }: ProgressBarProps): import("react/jsx-runtime").JSX.Element;
4
+ displayName: string;
5
+ };
6
+ export default ProgressBar;
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { View } from 'react-native';
3
+ import ProgressBarCircular from './ProgressBarCircular';
4
+ import ProgressBarLinear from './ProgressBarLinear';
5
+ const clampValue = (value, min, max) => {
6
+ if (max <= min)
7
+ return min;
8
+ return Math.min(Math.max(value, min), max);
9
+ };
10
+ const valueToPercent = (value, min, max) => {
11
+ const range = max - min;
12
+ if (range <= 0)
13
+ return 0;
14
+ return ((value - min) / range) * 100;
15
+ };
16
+ const ProgressBar = ({ variant = 'linear', colorScheme = 'default', size = 'md', value, min = 0, max = 100, label, hideLabel, formatValueText, 'aria-valuetext': ariaValueText, accessibilityLabel, ...rest }) => {
17
+ const effectiveValue = colorScheme === 'success' && !formatValueText ? max : clampValue(value, min, max);
18
+ const percentValue = valueToPercent(effectiveValue, min, max);
19
+ const clampedPercent = Math.max(0, Math.min(100, percentValue));
20
+ const valueText = formatValueText
21
+ ? formatValueText(effectiveValue, { min, max, percent: clampedPercent })
22
+ : `${Math.round(clampedPercent)}%`;
23
+ const valueTextForAria = ariaValueText ?? valueText;
24
+ const internalProps = {
25
+ percent: clampedPercent,
26
+ label,
27
+ valueText,
28
+ hideLabel,
29
+ colorScheme,
30
+ size,
31
+ };
32
+ return (_jsx(View, { ...rest, accessible: true, role: "progressbar", accessibilityRole: "progressbar", accessibilityLabel: accessibilityLabel ?? label, accessibilityValue: { min, max, now: effectiveValue, text: valueTextForAria }, "aria-valuenow": effectiveValue, "aria-valuemin": min, "aria-valuemax": max, "aria-valuetext": valueTextForAria, "data-colorscheme": colorScheme, children: variant === 'circular' ? (_jsx(ProgressBarCircular, { ...internalProps })) : (_jsx(ProgressBarLinear, { ...internalProps })) }));
33
+ };
34
+ ProgressBar.displayName = 'ProgressBar';
35
+ export default ProgressBar;
@@ -0,0 +1,60 @@
1
+ import { ViewProps } from 'react-native';
2
+ export type ProgressBarVariant = 'linear' | 'circular';
3
+ export type ProgressBarColorScheme = 'default' | 'success' | 'danger';
4
+ export type ProgressBarSize = 'sm' | 'md';
5
+ export interface ProgressBarProps extends ViewProps {
6
+ variant?: ProgressBarVariant;
7
+ /**
8
+ * Set the visual appearance.
9
+ * @default 'default'
10
+ */
11
+ colorScheme?: ProgressBarColorScheme;
12
+ /**
13
+ * Sets the circular variant size. Does not affect the appearance of the linear variant.
14
+ * @default 'md'
15
+ */
16
+ size?: ProgressBarSize;
17
+ /**
18
+ * The current progress value.
19
+ */
20
+ value: number;
21
+ /**
22
+ * The minimum value.
23
+ * @default 0
24
+ */
25
+ min?: number;
26
+ /**
27
+ * The maximum value.
28
+ * @default 100
29
+ */
30
+ max?: number;
31
+ /**
32
+ * Required text label for the progress bar.
33
+ */
34
+ label: string;
35
+ /**
36
+ * Visually hide the label and value text.
37
+ */
38
+ hideLabel?: boolean;
39
+ /**
40
+ * Override the default percentage value label formatting.
41
+ */
42
+ formatValueText?: (value: number, meta: {
43
+ min: number;
44
+ max: number;
45
+ percent: number;
46
+ }) => string;
47
+ /**
48
+ * A human-readable text alternative for the current value.
49
+ */
50
+ 'aria-valuetext'?: string;
51
+ }
52
+ export interface ProgressBarInternalProps {
53
+ percent: number;
54
+ label: string;
55
+ valueText: string;
56
+ hideLabel?: boolean;
57
+ colorScheme: ProgressBarColorScheme;
58
+ size: ProgressBarSize;
59
+ }
60
+ export default ProgressBarProps;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { ProgressBarInternalProps } from './ProgressBar.props';
2
+ declare const ProgressBarCircular: {
3
+ ({ percent, label, valueText, hideLabel, colorScheme, size, }: ProgressBarInternalProps): import("react/jsx-runtime").JSX.Element;
4
+ displayName: string;
5
+ };
6
+ export default ProgressBarCircular;
@@ -0,0 +1,115 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { Text, View } from 'react-native';
4
+ import Animated, { Easing, useAnimatedProps, useReducedMotion, useSharedValue, withTiming, } from 'react-native-reanimated';
5
+ import { Circle, G, Svg } from 'react-native-svg';
6
+ import { StyleSheet } from 'react-native-unistyles';
7
+ import useTheme from '../../hooks/useTheme';
8
+ import { BodyText } from '../BodyText';
9
+ const AnimatedCircle = Animated.createAnimatedComponent(Circle);
10
+ const ProgressBarCircular = ({ percent, label, valueText, hideLabel, colorScheme, size, }) => {
11
+ const { components } = useTheme();
12
+ const isReducedMotion = useReducedMotion();
13
+ const progress = useSharedValue(0);
14
+ const hasMountedRef = useRef(false);
15
+ useEffect(() => {
16
+ const target = Math.max(0, Math.min(100, percent)) / 100;
17
+ if (isReducedMotion) {
18
+ progress.value = target;
19
+ hasMountedRef.current = true;
20
+ return;
21
+ }
22
+ if (!hasMountedRef.current) {
23
+ progress.value = target;
24
+ hasMountedRef.current = true;
25
+ return;
26
+ }
27
+ progress.value = withTiming(target, { duration: 300, easing: Easing.out(Easing.ease) });
28
+ }, [percent, isReducedMotion, progress]);
29
+ const circularTokens = components.progressBar.circular[size];
30
+ const barWidth = 'bar' in circularTokens ? circularTokens.bar.width : circularTokens.barWidth;
31
+ const diameter = circularTokens.height;
32
+ const radius = (diameter - barWidth) / 2;
33
+ const circumference = 2 * Math.PI * radius;
34
+ const animatedCircleProps = useAnimatedProps(() => ({
35
+ strokeDashoffset: circumference * (1 - progress.value),
36
+ }));
37
+ const indicatorColor = colorScheme === 'success'
38
+ ? components.progressBar.progress.successColor
39
+ : colorScheme === 'danger'
40
+ ? components.progressBar.progress.dangerColor
41
+ : components.progressBar.progress.defaultColor;
42
+ styles.useVariants({ size });
43
+ return (_jsxs(View, { style: styles.container, children: [_jsx(Svg, { width: diameter, height: diameter, viewBox: `0 0 ${diameter} ${diameter}`, style: styles.svg, children: _jsxs(G, { origin: `${diameter / 2}, ${diameter / 2}`, rotation: -90, children: [_jsx(Circle, { cx: "50%", cy: "50%", r: radius, stroke: components.progressBar.barColor, strokeWidth: barWidth, fill: "transparent" }), _jsx(AnimatedCircle, { cx: "50%", cy: "50%", r: radius, stroke: indicatorColor, strokeWidth: barWidth, fill: "transparent", strokeLinecap: "round", strokeDasharray: circumference, animatedProps: animatedCircleProps })] }) }), _jsxs(View, { style: styles.content, children: [_jsx(Text, { style: styles.valueText, children: valueText }), !hideLabel && size === 'md' ? (_jsx(BodyText, { style: styles.label, size: "md", weight: "semibold", children: label })) : null] })] }));
44
+ };
45
+ ProgressBarCircular.displayName = 'ProgressBarCircular';
46
+ const styles = StyleSheet.create(theme => ({
47
+ container: {
48
+ alignItems: 'center',
49
+ justifyContent: 'center',
50
+ position: 'relative',
51
+ variants: {
52
+ size: {
53
+ md: {
54
+ width: theme.components.progressBar.circular.md.height,
55
+ height: theme.components.progressBar.circular.md.height,
56
+ },
57
+ sm: {
58
+ width: theme.components.progressBar.circular.sm.height,
59
+ height: theme.components.progressBar.circular.sm.height,
60
+ },
61
+ },
62
+ },
63
+ },
64
+ svg: {
65
+ position: 'absolute',
66
+ top: 0,
67
+ left: 0,
68
+ },
69
+ content: {
70
+ alignItems: 'center',
71
+ justifyContent: 'center',
72
+ _web: {
73
+ position: 'absolute',
74
+ top: 0,
75
+ left: 0,
76
+ width: '100%',
77
+ height: '100%',
78
+ },
79
+ variants: {
80
+ size: {
81
+ md: {
82
+ gap: theme.components.progressBar.circular.md.gap,
83
+ },
84
+ sm: {
85
+ gap: 0,
86
+ },
87
+ },
88
+ },
89
+ },
90
+ valueText: {
91
+ color: theme.color.text.primary,
92
+ textAlign: 'center',
93
+ variants: {
94
+ size: {
95
+ md: {
96
+ fontFamily: theme.components.progressBar.circular.md.label.fontFamily,
97
+ fontSize: theme.components.progressBar.circular.md.label.fontSize,
98
+ lineHeight: theme.components.progressBar.circular.md.label.lineHeight,
99
+ fontWeight: theme.components.progressBar.circular.md.label.fontWeight,
100
+ },
101
+ sm: {
102
+ fontFamily: theme.typography.mobile.bodyText.fontFamily,
103
+ fontSize: theme.typography.mobile.bodyText.md.fontSize,
104
+ lineHeight: theme.typography.mobile.bodyText.md.lineHeight,
105
+ fontWeight: theme.fontWeight.semibold,
106
+ },
107
+ },
108
+ },
109
+ },
110
+ label: {
111
+ textAlign: 'center',
112
+ maxWidth: 90,
113
+ },
114
+ }));
115
+ export default ProgressBarCircular;
@@ -0,0 +1,6 @@
1
+ import type { ProgressBarInternalProps } from './ProgressBar.props';
2
+ declare const ProgressBarLinear: {
3
+ ({ percent, label, valueText, hideLabel, colorScheme, }: ProgressBarInternalProps): import("react/jsx-runtime").JSX.Element;
4
+ displayName: string;
5
+ };
6
+ export default ProgressBarLinear;
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { Platform, View } from 'react-native';
4
+ import Animated, { Easing, useAnimatedStyle, useReducedMotion, useSharedValue, withTiming, } from 'react-native-reanimated';
5
+ import { StyleSheet } from 'react-native-unistyles';
6
+ import useTheme from '../../hooks/useTheme';
7
+ import { BodyText } from '../BodyText';
8
+ const ProgressBarLinear = ({ percent, label, valueText, hideLabel, colorScheme, }) => {
9
+ const { components } = useTheme();
10
+ const isReducedMotion = useReducedMotion();
11
+ const progress = useSharedValue(0);
12
+ const hasMountedRef = useRef(false);
13
+ const [trackWidth, setTrackWidth] = useState(0);
14
+ useEffect(() => {
15
+ const target = Math.max(0, Math.min(100, percent)) / 100;
16
+ if (isReducedMotion) {
17
+ progress.value = target;
18
+ hasMountedRef.current = true;
19
+ return;
20
+ }
21
+ if (!hasMountedRef.current) {
22
+ progress.value = target;
23
+ hasMountedRef.current = true;
24
+ return;
25
+ }
26
+ progress.value = withTiming(target, { duration: 300, easing: Easing.out(Easing.ease) });
27
+ }, [percent, isReducedMotion, progress]);
28
+ const animatedStyle = useAnimatedStyle(() => ({
29
+ width: trackWidth * progress.value,
30
+ }));
31
+ const indicatorColor = colorScheme === 'success'
32
+ ? components.progressBar.progress.successColor
33
+ : colorScheme === 'danger'
34
+ ? components.progressBar.progress.dangerColor
35
+ : components.progressBar.progress.defaultColor;
36
+ const handleTrackLayout = (event) => {
37
+ setTrackWidth(event.nativeEvent.layout.width);
38
+ };
39
+ return (_jsxs(View, { style: styles.container, children: [!hideLabel && (_jsxs(View, { style: styles.header, children: [_jsx(BodyText, { size: "md", weight: "semibold", style: styles.label, children: label }), _jsx(BodyText, { size: "md", style: styles.value, children: valueText })] })), _jsx(View, { style: styles.track, onLayout: handleTrackLayout, children: Platform.OS === 'web' ? (_jsx(View, { style: [
40
+ styles.indicator,
41
+ { width: `${Math.max(0, Math.min(100, percent))}%` },
42
+ { backgroundColor: indicatorColor },
43
+ ] })) : (_jsx(Animated.View, { style: [styles.indicator, animatedStyle, { backgroundColor: indicatorColor }] })) })] }));
44
+ };
45
+ ProgressBarLinear.displayName = 'ProgressBarLinear';
46
+ const styles = StyleSheet.create(theme => ({
47
+ container: {
48
+ width: '100%',
49
+ gap: theme.components.progressBar.linear.gap,
50
+ },
51
+ header: {
52
+ flexDirection: 'row',
53
+ alignItems: 'center',
54
+ justifyContent: 'space-between',
55
+ gap: theme.components.progressBar.linear.label.gap,
56
+ },
57
+ label: {
58
+ flex: 1,
59
+ },
60
+ value: {
61
+ flexShrink: 0,
62
+ textAlign: 'right',
63
+ },
64
+ track: {
65
+ width: '100%',
66
+ height: theme.components.progressBar.linear.bar.height,
67
+ backgroundColor: theme.components.progressBar.barColor,
68
+ borderRadius: theme.components.progressBar.linear.bar.borderRadius,
69
+ overflow: 'hidden',
70
+ },
71
+ indicator: {
72
+ height: '100%',
73
+ borderRadius: theme.components.progressBar.linear.bar.borderRadius,
74
+ _web: {
75
+ height: '100%',
76
+ },
77
+ },
78
+ }));
79
+ export default ProgressBarLinear;
@@ -0,0 +1,2 @@
1
+ export { default as ProgressBar } from './ProgressBar';
2
+ export type { ProgressBarColorScheme, ProgressBarProps, ProgressBarSize, ProgressBarVariant, } from './ProgressBar.props';
@@ -0,0 +1 @@
1
+ export { default as ProgressBar } from './ProgressBar';
@@ -40,6 +40,7 @@ export * from './List';
40
40
  export * from './Menu';
41
41
  export * from './Modal';
42
42
  export * from './PillGroup';
43
+ export * from './ProgressBar';
43
44
  export * from './ProgressStepper';
44
45
  export * from './Radio';
45
46
  export * from './RadioCard';
@@ -41,6 +41,7 @@ export * from './List';
41
41
  export * from './Menu';
42
42
  export * from './Modal';
43
43
  export * from './PillGroup';
44
+ export * from './ProgressBar';
44
45
  export * from './ProgressStepper';
45
46
  export * from './Radio';
46
47
  export * from './RadioCard';
@@ -74,6 +74,7 @@ import {
74
74
  OL,
75
75
  Pill,
76
76
  PillGroup,
77
+ ProgressBar,
77
78
  ProgressStep,
78
79
  ProgressStepper,
79
80
  Radio,
@@ -663,6 +664,11 @@ const AllComponents: React.FC = () => {
663
664
  </PillGroup>
664
665
  </Center>
665
666
  </ComponentWrapper>
667
+ <ComponentWrapper name="Progress Bar" link="/?path=/docs/components-progress-bar--docs">
668
+ <Center flex={1} px="300">
669
+ <ProgressBar value={58} label="Order progress" />
670
+ </Center>
671
+ </ComponentWrapper>
666
672
  <ComponentWrapper
667
673
  name="Progress Stepper"
668
674
  link="/?path=/docs/components-progress-stepper--docs"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilitywarehouse/hearth-react-native",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Utility Warehouse React Native UI library",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -4,16 +4,14 @@ import { BottomSheetProps } from '../BottomSheet';
4
4
  import { ButtonWithoutChildrenProps } from '../Button/Button.props';
5
5
  import { UnstyledIconButtonProps } from '../UnstyledIconButton';
6
6
 
7
- interface ModalProps extends Omit<BottomSheetProps, 'children'> {
7
+ interface ModalPropsBase extends Omit<BottomSheetProps, 'children'> {
8
8
  loading?: boolean;
9
9
  image?: ReactNode;
10
10
  showCloseButton?: boolean;
11
11
  heading?: string;
12
12
  loadingHeading?: string;
13
13
  description?: string;
14
- inNavModal?: boolean;
15
14
  fullscreen?: boolean;
16
- stickyFooter?: boolean;
17
15
  children?: ViewProps['children'];
18
16
  onPressPrimaryButton?: () => void;
19
17
  primaryButtonText?: string;
@@ -25,7 +23,18 @@ interface ModalProps extends Omit<BottomSheetProps, 'children'> {
25
23
  primaryButtonProps?: Omit<ButtonWithoutChildrenProps, 'children'>;
26
24
  secondaryButtonProps?: Omit<ButtonWithoutChildrenProps, 'children'>;
27
25
  closeButtonProps?: Omit<UnstyledIconButtonProps, 'children'>;
28
- background?: 'default' | 'brand';
29
26
  }
30
27
 
28
+ type ModalProps =
29
+ | (ModalPropsBase & {
30
+ inNavModal?: false | undefined;
31
+ stickyFooter?: boolean;
32
+ background?: never;
33
+ })
34
+ | (ModalPropsBase & {
35
+ inNavModal: true;
36
+ stickyFooter?: never;
37
+ background?: 'default' | 'brand';
38
+ });
39
+
31
40
  export default ModalProps;
@@ -1,4 +1,4 @@
1
- import { Meta, StoryObj } from '@storybook/react-vite';
1
+ import { Meta, StoryObj } from '@storybook/react-native';
2
2
  import { useRef } from 'react';
3
3
  import { ImageSourcePropType, Platform, View } from 'react-native';
4
4
  import { Modal, ModalImage } from '.';