@jobber/components-native 0.12.0 → 0.14.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 (35) hide show
  1. package/dist/src/Chip/Chip.js +43 -0
  2. package/dist/src/Chip/Chip.style.js +32 -0
  3. package/dist/src/Chip/index.js +1 -0
  4. package/dist/src/ProgressBar/ProgressBar.js +21 -0
  5. package/dist/src/ProgressBar/ProgressBar.style.js +24 -0
  6. package/dist/src/ProgressBar/ProgressBarInner.js +29 -0
  7. package/dist/src/ProgressBar/index.js +1 -0
  8. package/dist/src/ProgressBar/messages.js +13 -0
  9. package/dist/src/ProgressBar/types.js +1 -0
  10. package/dist/src/index.js +2 -0
  11. package/dist/tsconfig.tsbuildinfo +1 -1
  12. package/dist/types/src/Chip/Chip.d.ts +45 -0
  13. package/dist/types/src/Chip/Chip.style.d.ts +29 -0
  14. package/dist/types/src/Chip/index.d.ts +2 -0
  15. package/dist/types/src/ProgressBar/ProgressBar.d.ts +3 -0
  16. package/dist/types/src/ProgressBar/ProgressBar.style.d.ts +22 -0
  17. package/dist/types/src/ProgressBar/ProgressBarInner.d.ts +9 -0
  18. package/dist/types/src/ProgressBar/index.d.ts +2 -0
  19. package/dist/types/src/ProgressBar/messages.d.ts +12 -0
  20. package/dist/types/src/ProgressBar/types.d.ts +28 -0
  21. package/dist/types/src/index.d.ts +2 -0
  22. package/package.json +2 -2
  23. package/src/Chip/Chip.style.ts +34 -0
  24. package/src/Chip/Chip.test.tsx +133 -0
  25. package/src/Chip/Chip.tsx +142 -0
  26. package/src/Chip/index.ts +2 -0
  27. package/src/ProgressBar/ProgressBar.style.ts +25 -0
  28. package/src/ProgressBar/ProgressBar.test.tsx +41 -0
  29. package/src/ProgressBar/ProgressBar.tsx +58 -0
  30. package/src/ProgressBar/ProgressBarInner.tsx +47 -0
  31. package/src/ProgressBar/__snapshots__/ProgressBar.test.tsx.snap +138 -0
  32. package/src/ProgressBar/index.tsx +2 -0
  33. package/src/ProgressBar/messages.ts +14 -0
  34. package/src/ProgressBar/types.ts +34 -0
  35. package/src/index.ts +2 -0
@@ -0,0 +1,45 @@
1
+ /// <reference types="react" />
2
+ import { AccessibilityRole } from "react-native";
3
+ import { IconNames } from "@jobber/design";
4
+ export type AccentType = "client" | "invoice" | "job" | "request" | "quote";
5
+ export interface ChipProps {
6
+ /**
7
+ * label of the chip.
8
+ */
9
+ readonly label?: string;
10
+ /**
11
+ * chip's active status
12
+ */
13
+ readonly isActive: boolean;
14
+ /**
15
+ * Boolean for chip's ability to be dismissed
16
+ */
17
+ readonly isDismissible?: boolean;
18
+ /**
19
+ * Background color to be used for inactive chips
20
+ *
21
+ * @default "background"
22
+ */
23
+ readonly inactiveBackgroundColor?: "surface" | "background";
24
+ /**
25
+ * Accessibility label for the component. It's also used for testing
26
+ */
27
+ readonly accessibilityLabel?: string;
28
+ /**
29
+ * Accessibility role for the component
30
+ */
31
+ readonly accessibilityRole?: AccessibilityRole;
32
+ /**
33
+ * Press handler
34
+ */
35
+ onPress?(): void;
36
+ /**
37
+ * Optional Icon
38
+ */
39
+ readonly icon?: IconNames;
40
+ /**
41
+ * Background color to be used for Active chips
42
+ */
43
+ readonly accent?: AccentType;
44
+ }
45
+ export declare function Chip({ icon, label, onPress, isDismissible, isActive, inactiveBackgroundColor, accessibilityLabel, accessibilityRole, accent, }: ChipProps): JSX.Element;
@@ -0,0 +1,29 @@
1
+ export declare const styles: {
2
+ container: {
3
+ alignItems: "center";
4
+ borderRadius: number;
5
+ flexDirection: "row";
6
+ height: number;
7
+ justifyContent: "center";
8
+ marginHorizontal: number;
9
+ marginTop: number;
10
+ paddingHorizontal: number;
11
+ };
12
+ iconLeft: {
13
+ marginHorizontal: number;
14
+ };
15
+ chipText: {
16
+ flexGrow: number;
17
+ flexShrink: number;
18
+ marginHorizontal: number;
19
+ };
20
+ dismissIcon: {
21
+ backgroundColor: string;
22
+ borderRadius: number;
23
+ marginLeft: number;
24
+ padding: number;
25
+ };
26
+ activeDismissIcon: {
27
+ backgroundColor: string;
28
+ };
29
+ };
@@ -0,0 +1,2 @@
1
+ export { Chip } from "./Chip";
2
+ export type { AccentType } from "./Chip";
@@ -0,0 +1,3 @@
1
+ /// <reference types="react" />
2
+ import { ProgressBarProps } from "./types";
3
+ export declare function ProgressBar({ loading, total, current, inProgress, reverseTheme, header, }: ProgressBarProps): JSX.Element;
@@ -0,0 +1,22 @@
1
+ export declare const styles: {
2
+ progressBarContainer: {
3
+ marginTop: number;
4
+ marginBottom: number;
5
+ height: number;
6
+ position: "relative";
7
+ flexDirection: "row";
8
+ };
9
+ progressBarInner: {
10
+ backgroundColor: string;
11
+ width: string;
12
+ height: string;
13
+ position: "absolute";
14
+ left: number;
15
+ top: number;
16
+ borderRadius: number;
17
+ };
18
+ progressInner: {
19
+ height: string;
20
+ borderRadius: number;
21
+ };
22
+ };
@@ -0,0 +1,9 @@
1
+ /// <reference types="react" />
2
+ interface ProgressBarInnerProps {
3
+ readonly width: number;
4
+ readonly animationDuration?: number;
5
+ readonly color?: string;
6
+ }
7
+ export declare function ProgressBarInner({ width, color, }: ProgressBarInnerProps): JSX.Element;
8
+ export declare function calculateWidth(total: number, current: number): number;
9
+ export {};
@@ -0,0 +1,2 @@
1
+ export { ProgressBar } from "./ProgressBar";
2
+ export type { ProgressBarProps } from "./types";
@@ -0,0 +1,12 @@
1
+ export declare const messages: {
2
+ complete: {
3
+ id: string;
4
+ defaultMessage: string;
5
+ description: string;
6
+ };
7
+ inProgress: {
8
+ id: string;
9
+ defaultMessage: string;
10
+ description: string;
11
+ };
12
+ };
@@ -0,0 +1,28 @@
1
+ import { ReactNode } from "react";
2
+ export interface ProgressBarProps {
3
+ /**
4
+ * The total number of items to be completed
5
+ */
6
+ readonly total: number;
7
+ /**
8
+ * The number of items that are currently completed
9
+ */
10
+ readonly current: number;
11
+ /**
12
+ * The number of items in progress (not completed, but to be less than the total)
13
+ */
14
+ readonly inProgress?: number;
15
+ /**
16
+ * If the progress bar is loading, the progress indicators aren't rendered on the screen
17
+ */
18
+ readonly loading?: boolean;
19
+ /**
20
+ * If the amountFormatted and totalAmountFormatted text needs to appear more visibile because of the
21
+ * background, for example
22
+ */
23
+ readonly reverseTheme?: boolean;
24
+ /**
25
+ * Component to render above the progress bar.
26
+ */
27
+ readonly header?: ReactNode;
28
+ }
@@ -11,4 +11,6 @@ export * from "./StatusLabel";
11
11
  export * from "./AtlantisContext";
12
12
  export * from "./Button";
13
13
  export * from "./InputFieldWrapper";
14
+ export * from "./ProgressBar";
14
15
  export * from "./Heading";
16
+ export * from "./Chip";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "license": "MIT",
5
5
  "main": "dist/src/index.js",
6
6
  "module": "dist/src/index.js",
@@ -47,5 +47,5 @@
47
47
  "react": "^18",
48
48
  "react-native": ">=0.69.2"
49
49
  },
50
- "gitHead": "b059ff05b89ebdd41e79b4b2cf761e91706bc9c9"
50
+ "gitHead": "7237667e48edeb962f88a75bacaaca79e510f415"
51
51
  }
@@ -0,0 +1,34 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { tokens } from "../utils/design";
3
+
4
+ const chipHeight = tokens["space-larger"] + tokens["space-small"];
5
+
6
+ export const styles = StyleSheet.create({
7
+ container: {
8
+ alignItems: "center",
9
+ borderRadius: tokens["radius-circle"],
10
+ flexDirection: "row",
11
+ height: chipHeight,
12
+ justifyContent: "center",
13
+ marginHorizontal: tokens["space-smaller"],
14
+ marginTop: tokens["space-small"],
15
+ paddingHorizontal: tokens["space-small"],
16
+ },
17
+ iconLeft: {
18
+ marginHorizontal: tokens["space-smallest"],
19
+ },
20
+ chipText: {
21
+ flexGrow: 1,
22
+ flexShrink: 1,
23
+ marginHorizontal: tokens["space-smallest"],
24
+ },
25
+ dismissIcon: {
26
+ backgroundColor: tokens["color-surface"],
27
+ borderRadius: tokens["radius-circle"],
28
+ marginLeft: tokens["space-smaller"],
29
+ padding: tokens["space-smaller"],
30
+ },
31
+ activeDismissIcon: {
32
+ backgroundColor: tokens["color-surface--background"],
33
+ },
34
+ });
@@ -0,0 +1,133 @@
1
+ import React from "react";
2
+ import { fireEvent, render } from "@testing-library/react-native";
3
+ import { Chip } from "./Chip";
4
+ import { tokens } from "../utils/design";
5
+
6
+ it("renders an active Chip", () => {
7
+ const { getByText, getByLabelText } = render(
8
+ <Chip
9
+ label="Foo"
10
+ onPress={jest.fn()}
11
+ accessibilityLabel={"Foo chip"}
12
+ isActive
13
+ />,
14
+ );
15
+ expect(getByText("Foo")).toBeDefined();
16
+ expect(getByLabelText("Foo chip").props.style).toContainEqual({
17
+ backgroundColor: tokens["color-surface--reverse"],
18
+ });
19
+ });
20
+
21
+ it("renders an active Chip without onPress as disabled", () => {
22
+ const { getByTestId } = render(
23
+ <Chip label="Foo" accessibilityLabel={"Foo chip"} isActive />,
24
+ );
25
+ expect(
26
+ getByTestId("chipTest").props.accessibilityState.disabled,
27
+ ).toBeTruthy();
28
+ });
29
+
30
+ it("renders an inactive Chip with a default backgroundColor", () => {
31
+ const { getByTestId } = render(
32
+ <Chip label="Foo" onPress={jest.fn()} isActive={false} />,
33
+ );
34
+ expect(getByTestId("chipTest").props.style).not.toContainEqual({
35
+ backgroundColor: tokens["color-surface--reverse"],
36
+ });
37
+ expect(getByTestId("chipTest").props.style).toContainEqual({
38
+ backgroundColor: tokens["color-surface--background"],
39
+ });
40
+ });
41
+
42
+ it("renders an inactive Chip with a surface backgroundColor", () => {
43
+ const { getByTestId } = render(
44
+ <Chip
45
+ label="Foo"
46
+ onPress={jest.fn()}
47
+ isActive={false}
48
+ inactiveBackgroundColor={"surface"}
49
+ />,
50
+ );
51
+ expect(getByTestId("chipTest").props.style).toContainEqual({
52
+ backgroundColor: tokens["color-surface"],
53
+ });
54
+ });
55
+
56
+ it("renders an active Chip with icon", () => {
57
+ const { getByTestId } = render(
58
+ <Chip onPress={jest.fn()} icon="invoice" isActive />,
59
+ );
60
+ expect(getByTestId("invoice")).toBeDefined();
61
+ });
62
+
63
+ it("renders an inactive Chip with icon", () => {
64
+ const { getByTestId } = render(
65
+ <Chip onPress={jest.fn()} icon="invoice" isActive={false} />,
66
+ );
67
+ expect(getByTestId("invoice")).toBeDefined();
68
+ });
69
+
70
+ it("renders a Chip with the dismiss icon", () => {
71
+ const { getByTestId } = render(
72
+ <Chip label="Foo" onPress={jest.fn()} isActive isDismissible={true} />,
73
+ );
74
+ expect(getByTestId("remove")).toBeDefined();
75
+ });
76
+
77
+ it("should call the handler with the new value", () => {
78
+ const pressHandler = jest.fn();
79
+ const accessibilityLabel = "test chip";
80
+ const { getByLabelText } = render(
81
+ <Chip
82
+ onPress={pressHandler}
83
+ label={"foo"}
84
+ accessibilityLabel={accessibilityLabel}
85
+ isActive
86
+ />,
87
+ );
88
+
89
+ fireEvent.press(getByLabelText(accessibilityLabel));
90
+ expect(pressHandler).toHaveBeenCalled();
91
+ });
92
+
93
+ describe("accessibilityLabel", () => {
94
+ it("uses accessibilityLabel if specified", () => {
95
+ const pressHandler = jest.fn();
96
+ const { getByLabelText } = render(
97
+ <Chip
98
+ onPress={pressHandler}
99
+ label="label"
100
+ accessibilityLabel="accessibilityLabel"
101
+ isActive
102
+ />,
103
+ );
104
+
105
+ expect(getByLabelText("accessibilityLabel")).toBeTruthy();
106
+ });
107
+
108
+ it("uses label if unspecified", () => {
109
+ const pressHandler = jest.fn();
110
+ const { getByLabelText } = render(
111
+ <Chip onPress={pressHandler} label="label" isActive />,
112
+ );
113
+
114
+ expect(getByLabelText("label")).toBeTruthy();
115
+ });
116
+ });
117
+
118
+ describe("accent", () => {
119
+ it("uses accent color when present and chip is Active", () => {
120
+ const { getByTestId } = render(
121
+ <Chip
122
+ label="Foo"
123
+ onPress={jest.fn()}
124
+ accessibilityLabel={"Foo chip"}
125
+ isActive={true}
126
+ accent={"client"}
127
+ />,
128
+ );
129
+ expect(getByTestId("chipTest").props.style).toContainEqual({
130
+ backgroundColor: tokens["color-client"],
131
+ });
132
+ });
133
+ });
@@ -0,0 +1,142 @@
1
+ import React, { useMemo } from "react";
2
+ import { AccessibilityRole, Pressable, View } from "react-native";
3
+ import { IconNames } from "@jobber/design";
4
+ import { styles } from "./Chip.style";
5
+ import { Icon } from "../Icon";
6
+ import { Typography } from "../Typography";
7
+ import { tokens } from "../utils/design";
8
+
9
+ export type AccentType = "client" | "invoice" | "job" | "request" | "quote";
10
+
11
+ export interface ChipProps {
12
+ /**
13
+ * label of the chip.
14
+ */
15
+ readonly label?: string;
16
+
17
+ /**
18
+ * chip's active status
19
+ */
20
+ readonly isActive: boolean;
21
+
22
+ /**
23
+ * Boolean for chip's ability to be dismissed
24
+ */
25
+ readonly isDismissible?: boolean;
26
+
27
+ /**
28
+ * Background color to be used for inactive chips
29
+ *
30
+ * @default "background"
31
+ */
32
+ readonly inactiveBackgroundColor?: "surface" | "background";
33
+
34
+ /**
35
+ * Accessibility label for the component. It's also used for testing
36
+ */
37
+ readonly accessibilityLabel?: string;
38
+
39
+ /**
40
+ * Accessibility role for the component
41
+ */
42
+ readonly accessibilityRole?: AccessibilityRole;
43
+
44
+ /**
45
+ * Press handler
46
+ */
47
+ onPress?(): void;
48
+
49
+ /**
50
+ * Optional Icon
51
+ */
52
+ readonly icon?: IconNames;
53
+
54
+ /**
55
+ * Background color to be used for Active chips
56
+ */
57
+ readonly accent?: AccentType;
58
+ }
59
+
60
+ const defaultAccentColor = tokens["color-surface--reverse"];
61
+
62
+ export function Chip({
63
+ icon,
64
+ label,
65
+ onPress,
66
+ isDismissible,
67
+ isActive,
68
+ inactiveBackgroundColor = "background",
69
+ accessibilityLabel,
70
+ accessibilityRole = "radio",
71
+ accent,
72
+ }: ChipProps): JSX.Element {
73
+ const { chipStyle, iconCustomColor, dismissColor } = useMemo(() => {
74
+ const accentColor = accent ? tokens[`color-${accent}`] : defaultAccentColor;
75
+
76
+ const iconColor = isActive ? tokens["color-surface"] : accentColor;
77
+ const chip = [
78
+ styles.container,
79
+ {
80
+ backgroundColor:
81
+ inactiveBackgroundColor === "surface"
82
+ ? tokens["color-surface"]
83
+ : tokens["color-surface--background"],
84
+ },
85
+ isActive && { backgroundColor: accentColor },
86
+ ];
87
+ const dismiss =
88
+ (isActive || inactiveBackgroundColor === "surface") &&
89
+ styles.activeDismissIcon;
90
+
91
+ return {
92
+ chipStyle: chip,
93
+ iconCustomColor: iconColor,
94
+ dismissColor: dismiss,
95
+ };
96
+ }, [accent, isActive, inactiveBackgroundColor]);
97
+
98
+ const accessibilityState = useMemo(() => {
99
+ const checkableRoles = ["radio", "switch", "togglebutton", "checkbox"];
100
+ if (checkableRoles.includes(accessibilityRole)) {
101
+ return { checked: isActive };
102
+ }
103
+ return {};
104
+ }, [accessibilityRole, isActive]);
105
+
106
+ return (
107
+ <Pressable
108
+ testID="chipTest"
109
+ hitSlop={{ top: 8, bottom: 8, left: 4, right: 4 }}
110
+ onPress={onPress}
111
+ disabled={typeof onPress !== "function"}
112
+ style={chipStyle}
113
+ accessibilityLabel={accessibilityLabel || label}
114
+ accessibilityRole={accessibilityRole}
115
+ accessibilityState={accessibilityState}
116
+ >
117
+ {icon && (
118
+ <View style={styles.iconLeft}>
119
+ <Icon name={icon} size={"base"} customColor={iconCustomColor} />
120
+ </View>
121
+ )}
122
+ {label && (
123
+ <View style={styles.chipText}>
124
+ <Typography
125
+ color="base"
126
+ fontWeight="medium"
127
+ maxFontScaleSize={tokens["typography--fontSize-large"]}
128
+ maxLines="single"
129
+ reverseTheme={isActive}
130
+ >
131
+ {label}
132
+ </Typography>
133
+ </View>
134
+ )}
135
+ {isDismissible && (
136
+ <View style={[styles.dismissIcon, dismissColor]}>
137
+ <Icon name={"remove"} size={"small"} />
138
+ </View>
139
+ )}
140
+ </Pressable>
141
+ );
142
+ }
@@ -0,0 +1,2 @@
1
+ export { Chip } from "./Chip";
2
+ export type { AccentType } from "./Chip";
@@ -0,0 +1,25 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { tokens } from "../utils/design";
3
+
4
+ export const styles = StyleSheet.create({
5
+ progressBarContainer: {
6
+ marginTop: tokens["space-small"],
7
+ marginBottom: tokens["space-small"],
8
+ height: 20,
9
+ position: "relative",
10
+ flexDirection: "row",
11
+ },
12
+ progressBarInner: {
13
+ backgroundColor: "rgba(255,255,255,0.3)",
14
+ width: "100%",
15
+ height: "100%",
16
+ position: "absolute",
17
+ left: 0,
18
+ top: 0,
19
+ borderRadius: 100,
20
+ },
21
+ progressInner: {
22
+ height: "100%",
23
+ borderRadius: 100,
24
+ },
25
+ });
@@ -0,0 +1,41 @@
1
+ import React from "react";
2
+ import { render } from "@testing-library/react-native";
3
+ import { ProgressBar, ProgressBarProps } from ".";
4
+
5
+ const defaultSetupProps = {
6
+ total: 0,
7
+ current: 0,
8
+ inProgress: 0,
9
+ loading: false,
10
+ };
11
+
12
+ type ProgressBarCombinedProps = ProgressBarProps;
13
+
14
+ function renderProgressBarComponent({
15
+ total,
16
+ current,
17
+ inProgress,
18
+ }: ProgressBarCombinedProps) {
19
+ return render(
20
+ <ProgressBar total={total} current={current} inProgress={inProgress} />,
21
+ );
22
+ }
23
+
24
+ it("renders blue inProgress bar when 1 or more jobs is in progress", () => {
25
+ const bar = renderProgressBarComponent({
26
+ ...defaultSetupProps,
27
+ inProgress: 1,
28
+ }).toJSON();
29
+ expect(bar).toMatchSnapshot();
30
+ });
31
+
32
+ it("renders green CompletedProgress bar when 1 or more jobs is completed", () => {
33
+ const current = 1;
34
+ const bar = renderProgressBarComponent({
35
+ ...defaultSetupProps,
36
+ current: current,
37
+ total: 2,
38
+ });
39
+ expect(bar).toMatchSnapshot();
40
+ expect(bar.getByLabelText("1 of 2 complete")).toBeDefined();
41
+ });
@@ -0,0 +1,58 @@
1
+ import React from "react";
2
+ import { View } from "react-native";
3
+ import { useIntl } from "react-intl";
4
+ import { ProgressBarProps } from "./types";
5
+ import { styles } from "./ProgressBar.style";
6
+ import { ProgressBarInner, calculateWidth } from "./ProgressBarInner";
7
+ import { messages } from "./messages";
8
+ import { tokens } from "../utils/design";
9
+
10
+ export function ProgressBar({
11
+ loading,
12
+ total,
13
+ current,
14
+ inProgress = 0,
15
+ reverseTheme = false,
16
+ header,
17
+ }: ProgressBarProps): JSX.Element {
18
+ const { formatMessage } = useIntl();
19
+ const accessibilityLabel = [];
20
+ accessibilityLabel.push(formatMessage(messages.complete, { current, total }));
21
+ inProgress &&
22
+ accessibilityLabel.push(formatMessage(messages.inProgress, { inProgress }));
23
+
24
+ return (
25
+ <View
26
+ accessible
27
+ accessibilityRole="progressbar"
28
+ accessibilityLabel={accessibilityLabel.join(", ")}
29
+ >
30
+ {header}
31
+ <View style={styles.progressBarContainer}>
32
+ <ProgressBarInner
33
+ width={100}
34
+ animationDuration={0}
35
+ color={reverseTheme ? undefined : tokens["color-surface--background"]}
36
+ />
37
+ {!loading && (
38
+ <>
39
+ {inProgress && inProgress > 0 ? (
40
+ <ProgressBarInner
41
+ width={calculateWidth(total, current + inProgress)}
42
+ color={tokens["color-informative"]}
43
+ animationDuration={800}
44
+ />
45
+ ) : (
46
+ <></>
47
+ )}
48
+ <ProgressBarInner
49
+ width={calculateWidth(total, current)}
50
+ color={tokens["color-interactive"]}
51
+ animationDuration={600}
52
+ />
53
+ </>
54
+ )}
55
+ </View>
56
+ </View>
57
+ );
58
+ }
@@ -0,0 +1,47 @@
1
+ import React from "react";
2
+ import { View } from "react-native";
3
+ // import Reanimated from "react-native-reanimated";
4
+ // import { useTiming } from "utils/reanimated";
5
+ import { styles } from "./ProgressBar.style";
6
+
7
+ interface ProgressBarInnerProps {
8
+ readonly width: number;
9
+ readonly animationDuration?: number;
10
+ readonly color?: string;
11
+ }
12
+
13
+ export function ProgressBarInner({
14
+ width,
15
+ // animationDuration = 0,
16
+ color,
17
+ }: ProgressBarInnerProps): JSX.Element {
18
+ // Animation breaking on Android
19
+ // const [animatedOpacity] = useTiming({
20
+ // duration: animationDuration,
21
+ // fromValue: 0,
22
+ // toValue: 1,
23
+ // });
24
+
25
+ return (
26
+ <View
27
+ style={[
28
+ styles.progressBarInner,
29
+ {
30
+ width: `${width}%`,
31
+ // opacity: animatedOpacity,
32
+ ...(color && { backgroundColor: color }),
33
+ },
34
+ ]}
35
+ />
36
+ );
37
+ }
38
+
39
+ export function calculateWidth(total: number, current: number): number {
40
+ if (total <= 0) return 0;
41
+ if (current >= total) return 100;
42
+
43
+ const curr = Math.max(0, current);
44
+
45
+ if (curr >= total) return 100;
46
+ return (curr / total) * 100;
47
+ }