@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
@@ -59,6 +59,7 @@ const Modal = ({
59
59
  const theme = useTheme();
60
60
  const backgroundOpacity = useSharedValue(0);
61
61
  const pretendContentTranslateY = useSharedValue(20);
62
+ const isBrandBackground = background === 'brand';
62
63
 
63
64
  const triggerCloseAnimation = useCallback(() => {
64
65
  if (Platform.OS === 'android' && inNavModal) {
@@ -171,7 +172,7 @@ const Modal = ({
171
172
  noButtons,
172
173
  stickyFooter,
173
174
  showHandle: props.showHandle,
174
- background: background === 'brand' ? 'brand' : 'primary',
175
+ background: isBrandBackground ? 'brand' : 'primary',
175
176
  });
176
177
 
177
178
  const footer = (
@@ -180,7 +181,7 @@ const Modal = ({
180
181
  <Button
181
182
  onPress={handlePrimaryButtonPress}
182
183
  text={primaryButtonText}
183
- inverted={background === 'brand' && inNavModal}
184
+ inverted={isBrandBackground && inNavModal}
184
185
  {...primaryButtonProps}
185
186
  variant={(primaryButtonProps?.variant as 'solid') ?? 'solid'}
186
187
  colorScheme={(primaryButtonProps?.colorScheme as 'highlight') ?? 'highlight'}
@@ -190,7 +191,7 @@ const Modal = ({
190
191
  <Button
191
192
  onPress={handleSecondaryButtonPress}
192
193
  text={secondaryButtonText}
193
- inverted={background === 'brand' && inNavModal}
194
+ inverted={isBrandBackground && inNavModal}
194
195
  {...secondaryButtonProps}
195
196
  variant={(secondaryButtonProps?.variant as 'outline') ?? 'outline'}
196
197
  colorScheme={(secondaryButtonProps?.colorScheme as 'functional') ?? 'functional'}
@@ -209,8 +210,11 @@ const Modal = ({
209
210
  screenReaderFocusable
210
211
  ref={viewRef}
211
212
  >
212
- <Spinner size="lg" />
213
- <Heading size="lg" textAlign="center">
213
+ <Spinner
214
+ size="lg"
215
+ color={isBrandBackground && inNavModal ? theme.color.icon.inverted : undefined}
216
+ />
217
+ <Heading size="lg" textAlign="center" inverted={isBrandBackground && inNavModal}>
214
218
  {loadingHeading}
215
219
  </Heading>
216
220
  </View>
@@ -225,18 +229,22 @@ const Modal = ({
225
229
  <View style={styles.header}>
226
230
  <View style={styles.headerTextContent}>
227
231
  {heading && !image ? (
228
- <Heading size="lg" accessible>
232
+ <Heading size="lg" accessible inverted={isBrandBackground && inNavModal}>
229
233
  {heading}
230
234
  </Heading>
231
235
  ) : null}
232
- {description && !image ? <BodyText accessible>{description}</BodyText> : null}
236
+ {description && !image ? (
237
+ <BodyText accessible inverted={isBrandBackground && inNavModal}>
238
+ {description}
239
+ </BodyText>
240
+ ) : null}
233
241
  </View>
234
242
  {showCloseButton ? (
235
243
  <UnstyledIconButton
236
244
  icon={CloseMediumIcon}
237
245
  onPress={handleCloseButtonPress}
238
246
  accessibilityLabel="Close modal"
239
- inverted={background === 'brand' && inNavModal}
247
+ inverted={isBrandBackground && inNavModal}
240
248
  {...closeButtonProps}
241
249
  />
242
250
  ) : null}
@@ -246,12 +254,21 @@ const Modal = ({
246
254
  {image}
247
255
  <View style={styles.textContent}>
248
256
  {heading ? (
249
- <Heading size="lg" textAlign="center" accessible>
257
+ <Heading
258
+ size="lg"
259
+ textAlign="center"
260
+ accessible
261
+ inverted={isBrandBackground && inNavModal}
262
+ >
250
263
  {heading}
251
264
  </Heading>
252
265
  ) : null}
253
266
  {description ? (
254
- <BodyText textAlign="center" accessible>
267
+ <BodyText
268
+ textAlign="center"
269
+ accessible
270
+ inverted={isBrandBackground && inNavModal}
271
+ >
255
272
  {description}
256
273
  </BodyText>
257
274
  ) : null}
@@ -285,7 +302,7 @@ const Modal = ({
285
302
  <View
286
303
  style={{
287
304
  flex: 1,
288
- backgroundColor: theme.color.background[background === 'brand' ? 'brand' : 'primary'],
305
+ backgroundColor: theme.color.background[isBrandBackground ? 'brand' : 'primary'],
289
306
  }}
290
307
  >
291
308
  {Platform.OS === 'android' ? (
@@ -0,0 +1,90 @@
1
+ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks';
2
+ import { Box, Center, ProgressBar } from '../..';
3
+ import { BackToTopButton, UsageWrap, ViewFigmaButton } from '../../../docs/components';
4
+ import * as Stories from './ProgressBar.stories';
5
+
6
+ <Meta title="Components / Progress Bar" />
7
+
8
+ <BackToTopButton />
9
+
10
+ <ViewFigmaButton url="https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=7849-5704&t=Jg2fPJPQNzOyspmQ-4" />
11
+ <ViewFigmaButton url="https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=7863-3977&t=Jg2fPJPQNzOyspmQ-4" />
12
+
13
+ # Progress Bar
14
+
15
+ Progress bars communicate task completion for linear flows and bounded operations. Use the linear variant for
16
+ inline layouts and the circular variant when space is tighter or when progress needs more emphasis.
17
+
18
+ - [Playground](#playground)
19
+ - [Usage](#usage)
20
+ - [Props](#props)
21
+ - [Variants](#variants)
22
+ - [Circular Sizes](#circular-sizes)
23
+ - [Examples](#examples)
24
+ - [Custom Value Labels](#custom-value-labels)
25
+
26
+ ## Playground
27
+
28
+ <Canvas of={Stories.Playground} />
29
+
30
+ <Controls of={Stories.Playground} />
31
+
32
+ ## Usage
33
+
34
+ <UsageWrap>
35
+ <Center>
36
+ <Box style={{ width: 260 }}>
37
+ <ProgressBar value={42} label="Uploading documents" />
38
+ </Box>
39
+ </Center>
40
+ </UsageWrap>
41
+
42
+ ```tsx
43
+ import { ProgressBar } from '@utilitywarehouse/hearth-react-native';
44
+
45
+ const MyComponent = () => <ProgressBar value={42} label="Uploading documents" />;
46
+ ```
47
+
48
+ ## Props
49
+
50
+ | Property | Type | Description | Default |
51
+ | ----------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------- | --------- |
52
+ | `variant` | `'linear' \| 'circular'` | The progress bar variant. | `linear` |
53
+ | `colorScheme` | `'default' \| 'success' \| 'danger'` | The color scheme for the progress indicator. | `default` |
54
+ | `size` | `'sm' \| 'md'` | Circular size. Only applies to the circular variant. | `md` |
55
+ | `value` | `number` | Current progress value. | |
56
+ | `min` | `number` | Minimum value. | `0` |
57
+ | `max` | `number` | Maximum value. | `100` |
58
+ | `label` | `string` | Accessible label for the progress bar. | |
59
+ | `hideLabel` | `boolean` | Visually hide the label and value text. | `false` |
60
+ | `formatValueText` | `(value: number, meta: { min: number; `<br />` max: number; percent: number }) => string` | Override the default percentage label. | |
61
+ | `aria-valuetext` | `string` | A human-readable text alternative for the current value. | |
62
+
63
+ ## Variants
64
+
65
+ <Canvas of={Stories.Variants} />
66
+
67
+ ## Circular Sizes
68
+
69
+ <Canvas of={Stories.CircularSizes} />
70
+
71
+ ## Examples
72
+
73
+ ### Custom Value Labels
74
+
75
+ Use `formatValueText` to show values that are not percentages, such as remaining data allowance.
76
+
77
+ <Canvas of={Stories.CustomValueLabels} />
78
+
79
+ ```tsx
80
+ import { ProgressBar } from '@utilitywarehouse/hearth-react-native';
81
+
82
+ const MyComponent = () => (
83
+ <ProgressBar
84
+ value={68}
85
+ max={100}
86
+ label="Data allowance"
87
+ formatValueText={(value, { max }) => `${max - value}GB remaining`}
88
+ />
89
+ );
90
+ ```
@@ -0,0 +1,79 @@
1
+ import figma from '@figma/code-connect';
2
+ import { ProgressBar } from '../';
3
+
4
+ figma.connect(
5
+ ProgressBar,
6
+ 'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=7849-5704',
7
+ {
8
+ props: {
9
+ value: figma.enum('Progress', {
10
+ '0%': 0,
11
+ '10%': 10,
12
+ '20%': 20,
13
+ '30%': 30,
14
+ '40%': 40,
15
+ '50%': 50,
16
+ '60%': 60,
17
+ '70%': 70,
18
+ '80%': 80,
19
+ '90%': 90,
20
+ '100%': 100,
21
+ }),
22
+ colorScheme: figma.enum('Color Scheme', {
23
+ Default: 'default',
24
+ Success: 'success',
25
+ Danger: 'danger',
26
+ }),
27
+ label: figma.boolean('Label?', { true: figma.string('Label') }),
28
+ },
29
+ example: props => (
30
+ <ProgressBar
31
+ variant="linear"
32
+ value={props.value}
33
+ colorScheme={props.colorScheme}
34
+ label={props.label}
35
+ />
36
+ ),
37
+ }
38
+ );
39
+
40
+ figma.connect(
41
+ ProgressBar,
42
+ 'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=7863-3977',
43
+ {
44
+ props: {
45
+ value: figma.enum('Progress', {
46
+ '0%': 0,
47
+ '10%': 10,
48
+ '20%': 20,
49
+ '30%': 30,
50
+ '40%': 40,
51
+ '50%': 50,
52
+ '60%': 60,
53
+ '70%': 70,
54
+ '80%': 80,
55
+ '90%': 90,
56
+ '100%': 100,
57
+ }),
58
+ colorScheme: figma.enum('Color Scheme', {
59
+ Default: 'default',
60
+ Success: 'success',
61
+ Danger: 'danger',
62
+ }),
63
+ size: figma.enum('Size', {
64
+ 'SM-80': 'sm',
65
+ 'MD-140': 'md',
66
+ }),
67
+ label: figma.boolean('Label?', { true: figma.string('Label') }),
68
+ },
69
+ example: props => (
70
+ <ProgressBar
71
+ variant="circular"
72
+ value={props.value}
73
+ colorScheme={props.colorScheme}
74
+ size={props.size}
75
+ label={props.label}
76
+ />
77
+ ),
78
+ }
79
+ );
@@ -0,0 +1,60 @@
1
+ import { ViewProps } from 'react-native';
2
+
3
+ export type ProgressBarVariant = 'linear' | 'circular';
4
+ export type ProgressBarColorScheme = 'default' | 'success' | 'danger';
5
+ export type ProgressBarSize = 'sm' | 'md';
6
+
7
+ export interface ProgressBarProps extends ViewProps {
8
+ variant?: ProgressBarVariant;
9
+ /**
10
+ * Set the visual appearance.
11
+ * @default 'default'
12
+ */
13
+ colorScheme?: ProgressBarColorScheme;
14
+ /**
15
+ * Sets the circular variant size. Does not affect the appearance of the linear variant.
16
+ * @default 'md'
17
+ */
18
+ size?: ProgressBarSize;
19
+ /**
20
+ * The current progress value.
21
+ */
22
+ value: number;
23
+ /**
24
+ * The minimum value.
25
+ * @default 0
26
+ */
27
+ min?: number;
28
+ /**
29
+ * The maximum value.
30
+ * @default 100
31
+ */
32
+ max?: number;
33
+ /**
34
+ * Required text label for the progress bar.
35
+ */
36
+ label: string;
37
+ /**
38
+ * Visually hide the label and value text.
39
+ */
40
+ hideLabel?: boolean;
41
+ /**
42
+ * Override the default percentage value label formatting.
43
+ */
44
+ formatValueText?: (value: number, meta: { min: number; max: number; percent: number }) => string;
45
+ /**
46
+ * A human-readable text alternative for the current value.
47
+ */
48
+ 'aria-valuetext'?: string;
49
+ }
50
+
51
+ export interface ProgressBarInternalProps {
52
+ percent: number;
53
+ label: string;
54
+ valueText: string;
55
+ hideLabel?: boolean;
56
+ colorScheme: ProgressBarColorScheme;
57
+ size: ProgressBarSize;
58
+ }
59
+
60
+ export default ProgressBarProps;
@@ -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;