@utilitywarehouse/hearth-react-native 0.31.1 → 0.32.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 (65) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +13 -13
  3. package/CHANGELOG.md +55 -0
  4. package/build/components/Rating/Rating.d.ts +6 -0
  5. package/build/components/Rating/Rating.js +76 -0
  6. package/build/components/Rating/Rating.props.d.ts +18 -0
  7. package/build/components/Rating/Rating.props.js +1 -0
  8. package/build/components/Rating/RatingStarEmpty.d.ts +6 -0
  9. package/build/components/Rating/RatingStarEmpty.js +9 -0
  10. package/build/components/Rating/RatingStarFilled.d.ts +6 -0
  11. package/build/components/Rating/RatingStarFilled.js +9 -0
  12. package/build/components/Rating/index.d.ts +2 -0
  13. package/build/components/Rating/index.js +1 -0
  14. package/build/components/Roundel/Roundel.d.ts +6 -0
  15. package/build/components/Roundel/Roundel.js +40 -0
  16. package/build/components/Roundel/Roundel.props.d.ts +6 -0
  17. package/build/components/Roundel/Roundel.props.js +1 -0
  18. package/build/components/Roundel/index.d.ts +2 -0
  19. package/build/components/Roundel/index.js +1 -0
  20. package/build/components/StepperInput/StepperButton.d.ts +22 -0
  21. package/build/components/StepperInput/StepperButton.js +55 -0
  22. package/build/components/StepperInput/StepperInput.d.ts +6 -0
  23. package/build/components/StepperInput/StepperInput.js +196 -0
  24. package/build/components/StepperInput/StepperInput.props.d.ts +31 -0
  25. package/build/components/StepperInput/StepperInput.props.js +1 -0
  26. package/build/components/StepperInput/index.d.ts +2 -0
  27. package/build/components/StepperInput/index.js +1 -0
  28. package/build/components/Textarea/Textarea.d.ts +1 -1
  29. package/build/components/Textarea/Textarea.js +10 -3
  30. package/build/components/Textarea/Textarea.props.d.ts +11 -0
  31. package/build/components/index.d.ts +3 -0
  32. package/build/components/index.js +3 -0
  33. package/docs/adding-shadows.mdx +2 -2
  34. package/docs/changelog.mdx +16 -0
  35. package/docs/components/AllComponents.web.tsx +30 -1
  36. package/docs/dark-mode-best-practice.mdx +328 -0
  37. package/package.json +3 -3
  38. package/src/components/Modal/Modal.docs.mdx +58 -4
  39. package/src/components/NavModal/NavModal.docs.mdx +2 -2
  40. package/src/components/Rating/Rating.docs.mdx +178 -0
  41. package/src/components/Rating/Rating.figma.tsx +20 -0
  42. package/src/components/Rating/Rating.props.ts +22 -0
  43. package/src/components/Rating/Rating.stories.tsx +95 -0
  44. package/src/components/Rating/Rating.tsx +140 -0
  45. package/src/components/Rating/RatingStarEmpty.tsx +22 -0
  46. package/src/components/Rating/RatingStarFilled.tsx +27 -0
  47. package/src/components/Rating/index.ts +2 -0
  48. package/src/components/Roundel/Roundel.docs.mdx +48 -0
  49. package/src/components/Roundel/Roundel.figma.tsx +17 -0
  50. package/src/components/Roundel/Roundel.props.ts +8 -0
  51. package/src/components/Roundel/Roundel.stories.tsx +49 -0
  52. package/src/components/Roundel/Roundel.tsx +51 -0
  53. package/src/components/Roundel/index.ts +2 -0
  54. package/src/components/StepperInput/StepperButton.tsx +83 -0
  55. package/src/components/StepperInput/StepperInput.docs.mdx +121 -0
  56. package/src/components/StepperInput/StepperInput.figma.tsx +45 -0
  57. package/src/components/StepperInput/StepperInput.props.ts +39 -0
  58. package/src/components/StepperInput/StepperInput.stories.tsx +270 -0
  59. package/src/components/StepperInput/StepperInput.tsx +349 -0
  60. package/src/components/StepperInput/index.ts +2 -0
  61. package/src/components/Textarea/Textarea.docs.mdx +2 -0
  62. package/src/components/Textarea/Textarea.props.ts +11 -0
  63. package/src/components/Textarea/Textarea.stories.tsx +14 -0
  64. package/src/components/Textarea/Textarea.tsx +11 -2
  65. package/src/components/index.ts +3 -0
@@ -0,0 +1,178 @@
1
+ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks';
2
+ import { Box, Center, Rating } from '../..';
3
+ import { BackToTopButton, UsageWrap, ViewFigmaButton } from '../../../docs/components';
4
+ import * as Stories from './Rating.stories';
5
+
6
+ <Meta title="Components / Rating" />
7
+
8
+ <BackToTopButton />
9
+
10
+ <ViewFigmaButton url="https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=10620-4185" />
11
+
12
+ # Rating
13
+
14
+ Use Rating to collect a star-based score with an optional descriptive label.
15
+
16
+ - [Playground](#playground)
17
+ - [Usage](#usage)
18
+ - [Props](#props)
19
+ - [Examples](#examples)
20
+ - [Accessibility](#accessibility)
21
+
22
+ ## Playground
23
+
24
+ <Canvas of={Stories.Playground} />
25
+
26
+ <Controls of={Stories.Playground} />
27
+
28
+ ## Usage
29
+
30
+ <UsageWrap>
31
+ <Center>
32
+ <Box>
33
+ <Rating value={0} labels={{ 0: 'Not rated' }} />
34
+ </Box>
35
+ </Center>
36
+ </UsageWrap>
37
+
38
+ ```tsx
39
+ import { useState } from 'react';
40
+ import { Rating } from '@utilitywarehouse/hearth-react-native';
41
+
42
+ const MyComponent = () => {
43
+ const [rating, setRating] = useState<0 | 1 | 2 | 3 | 4 | 5>(0);
44
+
45
+ return <Rating value={rating} onChange={setRating} labels={{ 0: 'Not rated' }} />;
46
+ };
47
+ ```
48
+
49
+ ## Props
50
+
51
+ | Property | Type | Description | Default |
52
+ | -------------- | -------------------------------------- | ------------------------------------------- | ----------- |
53
+ | `value` | `0 \| 1 \| 2 \| 3 \| 4 \| 5` | Current rating value. | `0` |
54
+ | `defaultValue` | `0 \| 1 \| 2 \| 3 \| 4 \| 5` | Initial rating value when uncontrolled. | `0` |
55
+ | `onChange` | `(value: RatingValue) => void` | Called when a star is selected. | `undefined` |
56
+ | `disabled` | `boolean` | Disables the rating input. | `false` |
57
+ | `labels` | `Partial<Record<RatingValue, string>>` | Override labels for specific rating values. | `undefined` |
58
+ | `hideLabel` | `boolean` | Hide the label text below the stars. | `false` |
59
+
60
+ ## Examples
61
+
62
+ ### Default rating
63
+
64
+ Use the default labels for a quick feedback prompt.
65
+
66
+ <UsageWrap>
67
+ <Center>
68
+ <Box>
69
+ <Rating value={3} />
70
+ </Box>
71
+ </Center>
72
+ </UsageWrap>
73
+
74
+ ```tsx
75
+ import { Rating } from '@utilitywarehouse/hearth-react-native';
76
+
77
+ const MyComponent = () => <Rating value={3} />;
78
+ ```
79
+
80
+ ### Empty rating
81
+
82
+ Use `value={0}` to show a zero-star state.
83
+
84
+ <UsageWrap>
85
+ <Center>
86
+ <Box>
87
+ <Rating value={0} labels={{ 0: 'Not rated' }} />
88
+ </Box>
89
+ </Center>
90
+ </UsageWrap>
91
+
92
+ ```tsx
93
+ import { Rating } from '@utilitywarehouse/hearth-react-native';
94
+
95
+ const MyComponent = () => <Rating value={0} labels={{ 0: 'Not rated' }} />;
96
+ ```
97
+
98
+ ### Custom labels
99
+
100
+ Provide custom copy for each rating value.
101
+
102
+ <UsageWrap>
103
+ <Center>
104
+ <Box>
105
+ <Rating
106
+ value={5}
107
+ labels={{
108
+ 0: 'Not rated',
109
+ 1: 'Terrible',
110
+ 2: 'Poor',
111
+ 3: 'OK',
112
+ 4: 'Great',
113
+ 5: 'Outstanding',
114
+ }}
115
+ />
116
+ </Box>
117
+ </Center>
118
+ </UsageWrap>
119
+
120
+ ```tsx
121
+ import { Rating } from '@utilitywarehouse/hearth-react-native';
122
+
123
+ const MyComponent = () => (
124
+ <Rating
125
+ value={5}
126
+ labels={{
127
+ 0: 'Not rated',
128
+ 1: 'Terrible',
129
+ 2: 'Poor',
130
+ 3: 'OK',
131
+ 4: 'Great',
132
+ 5: 'Outstanding',
133
+ }}
134
+ />
135
+ );
136
+ ```
137
+
138
+ ### Hidden label
139
+
140
+ Use `hideLabel` when the context already explains the rating.
141
+
142
+ <UsageWrap>
143
+ <Center>
144
+ <Box>
145
+ <Rating value={2} hideLabel />
146
+ </Box>
147
+ </Center>
148
+ </UsageWrap>
149
+
150
+ ```tsx
151
+ import { Rating } from '@utilitywarehouse/hearth-react-native';
152
+
153
+ const MyComponent = () => <Rating value={2} hideLabel />;
154
+ ```
155
+
156
+ ### Disabled
157
+
158
+ Use `disabled` to show a read-only rating.
159
+
160
+ <UsageWrap>
161
+ <Center>
162
+ <Box>
163
+ <Rating value={4} disabled />
164
+ </Box>
165
+ </Center>
166
+ </UsageWrap>
167
+
168
+ ```tsx
169
+ import { Rating } from '@utilitywarehouse/hearth-react-native';
170
+
171
+ const MyComponent = () => <Rating value={4} disabled />;
172
+ ```
173
+
174
+ ## Accessibility
175
+
176
+ - Rating uses a `radiogroup` container with `radio` items for each star.
177
+ - Each star announces a descriptive label (e.g., "Rate Okay"). Override labels with the `labels` prop to match your content.
178
+ - Provide `accessibilityLabel` when the default label text is not sufficient for your screen reader context.
@@ -0,0 +1,20 @@
1
+ import figma from '@figma/code-connect';
2
+ import { Rating } from '../';
3
+
4
+ figma.connect(
5
+ Rating,
6
+ 'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=10620-4185',
7
+ {
8
+ props: {
9
+ value: figma.enum('Rating', {
10
+ '0 Star': 0,
11
+ '1 Star': 1,
12
+ '2 Star': 2,
13
+ '3 Star': 3,
14
+ '4 Star': 4,
15
+ '5 Star': 5,
16
+ }),
17
+ },
18
+ example: props => <Rating value={props.value} />,
19
+ }
20
+ );
@@ -0,0 +1,22 @@
1
+ import type { ViewProps } from 'react-native';
2
+
3
+ export type RatingValue = 0 | 1 | 2 | 3 | 4 | 5;
4
+
5
+ export type RatingLabels = Partial<Record<RatingValue, string>>;
6
+
7
+ export interface RatingProps extends Omit<ViewProps, 'children'> {
8
+ /** Current rating value. */
9
+ value?: RatingValue;
10
+ /** Initial rating value when uncontrolled. */
11
+ defaultValue?: RatingValue;
12
+ /** Called when a star is selected. */
13
+ onChange?: (value: RatingValue) => void;
14
+ /** Disables the rating input. */
15
+ disabled?: boolean;
16
+ /** Override labels for specific rating values. */
17
+ labels?: RatingLabels;
18
+ /** Hide the label text below the stars. */
19
+ hideLabel?: boolean;
20
+ }
21
+
22
+ export default RatingProps;
@@ -0,0 +1,95 @@
1
+ import { Meta, StoryObj } from '@storybook/react-native';
2
+ import type { ComponentProps } from 'react';
3
+ import { useEffect, useState } from 'react';
4
+ import { Rating } from '.';
5
+ import { VariantTitle } from '../../../docs/components';
6
+ import { Box } from '../Box';
7
+
8
+ type RatingStoryProps = ComponentProps<typeof Rating>;
9
+
10
+ const meta = {
11
+ title: 'Stories / Rating',
12
+ component: Rating,
13
+ parameters: {
14
+ layout: 'centered',
15
+ },
16
+ argTypes: {
17
+ value: {
18
+ control: { type: 'number', min: 0, max: 5, step: 1 },
19
+ },
20
+ defaultValue: {
21
+ control: { type: 'number', min: 0, max: 5, step: 1 },
22
+ },
23
+ labels: {
24
+ control: 'object',
25
+ },
26
+ hideLabel: {
27
+ control: 'boolean',
28
+ },
29
+ disabled: {
30
+ control: 'boolean',
31
+ },
32
+ },
33
+ args: {
34
+ value: 3,
35
+ hideLabel: false,
36
+ disabled: false,
37
+ },
38
+ } satisfies Meta<typeof Rating>;
39
+
40
+ export default meta;
41
+
42
+ type Story = StoryObj<typeof meta>;
43
+
44
+ export const Playground: Story = {
45
+ render: ({ value: initialValue, onChange, ...args }: RatingStoryProps) => {
46
+ const [value, setValue] = useState(initialValue ?? 0);
47
+
48
+ useEffect(() => {
49
+ setValue(initialValue ?? 0);
50
+ }, [initialValue]);
51
+
52
+ return (
53
+ <Rating
54
+ {...args}
55
+ value={value}
56
+ onChange={nextValue => {
57
+ setValue(nextValue);
58
+ onChange?.(nextValue);
59
+ }}
60
+ />
61
+ );
62
+ },
63
+ };
64
+
65
+ export const Variants: Story = {
66
+ parameters: {
67
+ controls: { include: [] },
68
+ },
69
+ render: () => (
70
+ <Box gap="300">
71
+ <VariantTitle title="Default">
72
+ <Rating value={3} />
73
+ </VariantTitle>
74
+ <VariantTitle title="Custom Labels">
75
+ <Rating
76
+ value={4}
77
+ labels={{
78
+ 0: 'Not rated',
79
+ 1: 'Terrible',
80
+ 2: 'Poor',
81
+ 3: 'OK',
82
+ 4: 'Great',
83
+ 5: 'Outstanding',
84
+ }}
85
+ />
86
+ </VariantTitle>
87
+ <VariantTitle title="Hidden Label">
88
+ <Rating value={2} hideLabel />
89
+ </VariantTitle>
90
+ <VariantTitle title="Disabled">
91
+ <Rating value={5} disabled />
92
+ </VariantTitle>
93
+ </Box>
94
+ ),
95
+ };
@@ -0,0 +1,140 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { Pressable, View } from 'react-native';
3
+ import { StyleSheet } from 'react-native-unistyles';
4
+ import { BodyText } from '../BodyText';
5
+ import type RatingProps from './Rating.props';
6
+ import type { RatingLabels, RatingValue } from './Rating.props';
7
+ import RatingStarEmpty from './RatingStarEmpty';
8
+ import RatingStarFilled from './RatingStarFilled';
9
+
10
+ const MAX_RATING: RatingValue = 5;
11
+ const STAR_WIDTH = 32;
12
+ const STAR_HEIGHT = 30;
13
+ const STAR_CONTAINER_SIZE = 40;
14
+
15
+ const DEFAULT_LABELS: Record<RatingValue, string> = {
16
+ 0: 'Select a rating',
17
+ 1: 'Awful',
18
+ 2: 'Bad',
19
+ 3: 'Okay',
20
+ 4: 'Good',
21
+ 5: 'Great!',
22
+ };
23
+
24
+ const clampRating = (value: number) =>
25
+ Math.min(MAX_RATING, Math.max(0, Math.round(value))) as RatingValue;
26
+
27
+ const Rating = ({
28
+ value,
29
+ defaultValue = 0,
30
+ onChange,
31
+ disabled = false,
32
+ labels,
33
+ hideLabel = false,
34
+ style,
35
+ accessibilityLabel,
36
+ ...props
37
+ }: RatingProps) => {
38
+ const isControlled = value !== undefined;
39
+ const [internalValue, setInternalValue] = useState<RatingValue>(clampRating(defaultValue));
40
+
41
+ useEffect(() => {
42
+ if (!isControlled) {
43
+ setInternalValue(clampRating(defaultValue));
44
+ }
45
+ }, [defaultValue, isControlled]);
46
+
47
+ const resolvedLabels = useMemo<RatingLabels>(() => ({ ...DEFAULT_LABELS, ...labels }), [labels]);
48
+
49
+ const resolvedValue = clampRating(isControlled ? value : internalValue);
50
+ const currentLabel = resolvedLabels[resolvedValue] ?? DEFAULT_LABELS[resolvedValue];
51
+ const labelColor = resolvedValue === 0 ? 'secondary' : 'primary';
52
+
53
+ const handlePress = useCallback(
54
+ (nextValue: RatingValue) => {
55
+ if (disabled) return;
56
+
57
+ if (!isControlled) {
58
+ setInternalValue(nextValue);
59
+ }
60
+
61
+ onChange?.(nextValue);
62
+ },
63
+ [disabled, isControlled, onChange]
64
+ );
65
+
66
+ styles.useVariants({ disabled });
67
+
68
+ return (
69
+ <View
70
+ {...props}
71
+ accessibilityRole="radiogroup"
72
+ accessibilityState={{ disabled }}
73
+ accessibilityLabel={accessibilityLabel ?? currentLabel}
74
+ style={[styles.container, style]}
75
+ >
76
+ <View style={styles.stars}>
77
+ {([1, 2, 3, 4, 5] as RatingValue[]).map(starValue => {
78
+ const isFilled = starValue <= resolvedValue;
79
+ const starLabel = resolvedLabels[starValue] ?? DEFAULT_LABELS[starValue];
80
+
81
+ return (
82
+ <Pressable
83
+ key={starValue}
84
+ accessibilityRole="radio"
85
+ accessibilityState={{ selected: resolvedValue === starValue, disabled }}
86
+ accessibilityLabel={`Rate ${starLabel}`}
87
+ disabled={disabled}
88
+ hitSlop={8}
89
+ onPress={() => handlePress(starValue)}
90
+ style={styles.star}
91
+ >
92
+ {isFilled ? (
93
+ <RatingStarFilled width={STAR_WIDTH} height={STAR_HEIGHT} />
94
+ ) : (
95
+ <RatingStarEmpty width={STAR_WIDTH} height={STAR_HEIGHT} />
96
+ )}
97
+ </Pressable>
98
+ );
99
+ })}
100
+ </View>
101
+ {!hideLabel ? (
102
+ <BodyText size="md" color={labelColor} style={styles.label}>
103
+ {currentLabel}
104
+ </BodyText>
105
+ ) : null}
106
+ </View>
107
+ );
108
+ };
109
+
110
+ Rating.displayName = 'Rating';
111
+
112
+ const styles = StyleSheet.create(theme => ({
113
+ container: {
114
+ alignItems: 'center',
115
+ gap: theme.components.rating.gap,
116
+ variants: {
117
+ disabled: {
118
+ true: {
119
+ opacity: theme.opacity.disabled,
120
+ },
121
+ },
122
+ },
123
+ },
124
+ stars: {
125
+ flexDirection: 'row',
126
+ gap: theme.components.rating.gap,
127
+ },
128
+ star: {
129
+ width: STAR_CONTAINER_SIZE,
130
+ height: STAR_CONTAINER_SIZE,
131
+ alignItems: 'center',
132
+ justifyContent: 'center',
133
+ padding: theme.components.rating.borderWidth,
134
+ },
135
+ label: {
136
+ textAlign: 'center',
137
+ },
138
+ }));
139
+
140
+ export default Rating;
@@ -0,0 +1,22 @@
1
+ import { Path, Svg } from 'react-native-svg';
2
+ import { useTheme } from '../../hooks';
3
+
4
+ type RatingStarProps = {
5
+ width?: number;
6
+ height?: number;
7
+ };
8
+
9
+ const STAR_PATH =
10
+ 'M16 1C16.0831 1 16.1883 1.02107 16.3262 1.09766C16.3896 1.13286 16.4424 1.183 16.4902 1.28711H16.4912L20.1777 9.8916L20.4141 10.4424L21.0117 10.4941L30.5107 11.3096C30.7062 11.3326 30.8 11.3864 30.8467 11.4238C30.915 11.4787 30.9492 11.5325 30.9707 11.6035C31.0029 11.71 31.0094 11.8192 30.9844 11.9463V11.9482C30.98 11.9702 30.9618 12.0389 30.832 12.1514L23.6338 18.2939L23.1689 18.6904L23.3096 19.2852L25.458 28.3926H25.459C25.4832 28.5045 25.4775 28.5714 25.4658 28.6133L25.4531 28.6475C25.4155 28.7291 25.3682 28.7951 25.3076 28.8516L25.2432 28.9043C25.172 28.9558 25.0868 28.9907 24.96 28.999C24.8763 29.0045 24.793 28.9887 24.6865 28.9229L24.6787 28.918L24.6699 28.9131L16.5088 24.0898L16 23.7891L15.4912 24.0898L7.33008 28.9131L7.32129 28.918L7.31348 28.9229C7.20704 28.9887 7.12371 29.0045 7.04004 28.999C6.94495 28.9928 6.87333 28.9713 6.81348 28.9395L6.75684 28.9043C6.68716 28.8537 6.63221 28.7954 6.58789 28.7236L6.54688 28.6475C6.53436 28.6202 6.51869 28.5743 6.52539 28.4902L6.54102 28.3926L8.69043 19.2852L8.83105 18.6904L8.36621 18.2939L1.16699 12.1514C1.03725 12.0389 1.01999 11.9702 1.01563 11.9482V11.9463L1.00195 11.8545C0.994777 11.765 1.00514 11.6834 1.0293 11.6035C1.05077 11.5325 1.08501 11.4787 1.15332 11.4238C1.19992 11.3864 1.2935 11.3327 1.48828 11.3096L10.9883 10.4941L11.5859 10.4424L11.8223 9.8916L15.5088 1.28711C15.5567 1.18274 15.6103 1.1329 15.6738 1.09766C15.8117 1.02107 15.9169 1 16 1Z';
11
+
12
+ const RatingStarEmpty = ({ width = 32, height = 30 }: RatingStarProps) => {
13
+ const theme = useTheme();
14
+
15
+ return (
16
+ <Svg width={width} height={height} viewBox="0 0 32 30" preserveAspectRatio="none">
17
+ <Path d={STAR_PATH} strokeWidth={2} fill="none" stroke={theme.color.border.subtle} />
18
+ </Svg>
19
+ );
20
+ };
21
+
22
+ export default RatingStarEmpty;
@@ -0,0 +1,27 @@
1
+ import { Path, Svg } from 'react-native-svg';
2
+ import { useTheme } from '../../hooks';
3
+
4
+ type RatingStarProps = {
5
+ width?: number;
6
+ height?: number;
7
+ };
8
+
9
+ const STAR_PATH =
10
+ 'M16 1C16.0831 1 16.1883 1.02107 16.3262 1.09766C16.3896 1.13286 16.4424 1.183 16.4902 1.28711H16.4912L20.1777 9.8916L20.4141 10.4424L21.0117 10.4941L30.5107 11.3096C30.7062 11.3326 30.8 11.3864 30.8467 11.4238C30.915 11.4787 30.9492 11.5325 30.9707 11.6035C31.0029 11.71 31.0094 11.8192 30.9844 11.9463V11.9482C30.98 11.9702 30.9618 12.0389 30.832 12.1514L23.6338 18.2939L23.1689 18.6904L23.3096 19.2852L25.458 28.3926H25.459C25.4832 28.5045 25.4775 28.5714 25.4658 28.6133L25.4531 28.6475C25.4155 28.7291 25.3682 28.7951 25.3076 28.8516L25.2432 28.9043C25.172 28.9558 25.0868 28.9907 24.96 28.999C24.8763 29.0045 24.793 28.9887 24.6865 28.9229L24.6787 28.918L24.6699 28.9131L16.5088 24.0898L16 23.7891L15.4912 24.0898L7.33008 28.9131L7.32129 28.918L7.31348 28.9229C7.20704 28.9887 7.12371 29.0045 7.04004 28.999C6.94495 28.9928 6.87333 28.9713 6.81348 28.9395L6.75684 28.9043C6.68716 28.8537 6.63221 28.7954 6.58789 28.7236L6.54688 28.6475C6.53436 28.6202 6.51869 28.5743 6.52539 28.4902L6.54102 28.3926L8.69043 19.2852L8.83105 18.6904L8.36621 18.2939L1.16699 12.1514C1.03725 12.0389 1.01999 11.9702 1.01563 11.9482V11.9463L1.00195 11.8545C0.994777 11.765 1.00514 11.6834 1.0293 11.6035C1.05077 11.5325 1.08501 11.4787 1.15332 11.4238C1.19992 11.3864 1.2935 11.3327 1.48828 11.3096L10.9883 10.4941L11.5859 10.4424L11.8223 9.8916L15.5088 1.28711C15.5567 1.18274 15.6103 1.1329 15.6738 1.09766C15.8117 1.02107 15.9169 1 16 1Z';
11
+
12
+ const RatingStarFilled = ({ width = 32, height = 30 }: RatingStarProps) => {
13
+ const theme = useTheme();
14
+
15
+ return (
16
+ <Svg width={width} height={height} viewBox="0 0 32 30" preserveAspectRatio="none">
17
+ <Path
18
+ d={STAR_PATH}
19
+ strokeWidth={2}
20
+ fill={theme.color.surface.highlight.default}
21
+ stroke={theme.color.border.strong}
22
+ />
23
+ </Svg>
24
+ );
25
+ };
26
+
27
+ export default RatingStarFilled;
@@ -0,0 +1,2 @@
1
+ export { default as Rating } from './Rating';
2
+ export type { RatingLabels, RatingProps, RatingValue } from './Rating.props';
@@ -0,0 +1,48 @@
1
+ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks';
2
+ import { Roundel } from '../../';
3
+ import { BackToTopButton, UsageWrap, ViewFigmaButton } from '../../../docs/components';
4
+ import * as Stories from './Roundel.stories';
5
+
6
+ <Meta title="Components / Roundel" />
7
+
8
+ <BackToTopButton />
9
+
10
+ <ViewFigmaButton url="https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=6414-8697&t=aFS22MoPV7Y08jdq-4" />
11
+
12
+ # Roundel
13
+
14
+ The `Roundel` component is a compact status indicator with success, pending, and error variants.
15
+
16
+ - [Playground](#playground)
17
+ - [Usage](#usage)
18
+ - [Props](#props)
19
+ - [Examples](#examples)
20
+
21
+ ## Playground
22
+
23
+ <Canvas of={Stories.Playground} />
24
+
25
+ <Controls of={Stories.Playground} />
26
+
27
+ ## Usage
28
+
29
+ <UsageWrap>
30
+ <Roundel variant="success" />
31
+ </UsageWrap>
32
+
33
+ ```tsx
34
+ import { Roundel } from '@utilitywarehouse/hearth-react-native';
35
+
36
+ const MyComponent = () => <Roundel variant="success" />;
37
+ ```
38
+
39
+ ## Props
40
+
41
+ | Prop | Type | Default | Description |
42
+ | --------- | ----------------------------------- | ----------- | ---------------------------------------- |
43
+ | `variant` | `'success' \| 'pending' \| 'error'` | `'success'` | Sets the status styling for the roundel. |
44
+ | `...View` | React Native `ViewProps` | - | Any additional View props. |
45
+
46
+ ## Examples
47
+
48
+ <Canvas of={Stories.Variants} />
@@ -0,0 +1,17 @@
1
+ import figma from '@figma/code-connect';
2
+ import { Roundel } from '..';
3
+
4
+ figma.connect(
5
+ Roundel,
6
+ 'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=6414%3A8697&m=dev',
7
+ {
8
+ props: {
9
+ variant: figma.enum('Variant', {
10
+ Success: 'success',
11
+ Pending: 'pending',
12
+ Error: 'error',
13
+ }),
14
+ },
15
+ example: props => <Roundel variant={props.variant} />,
16
+ }
17
+ );
@@ -0,0 +1,8 @@
1
+ import type { ViewProps } from 'react-native';
2
+
3
+ export interface RoundelProps extends Omit<ViewProps, 'children'> {
4
+ /** Visual variant for the roundel status. */
5
+ variant?: 'success' | 'pending' | 'error';
6
+ }
7
+
8
+ export default RoundelProps;
@@ -0,0 +1,49 @@
1
+ import { Meta, StoryObj } from '@storybook/react-native';
2
+ import { ComponentProps } from 'react';
3
+ import { VariantTitle } from '../../../docs/components';
4
+ import { Flex } from '../Flex';
5
+ import { Roundel } from './';
6
+
7
+ const meta: Meta<typeof Roundel> = {
8
+ title: 'Stories / Roundel',
9
+ component: Roundel,
10
+ parameters: {
11
+ layout: 'centered',
12
+ },
13
+ argTypes: {
14
+ variant: {
15
+ control: 'radio',
16
+ options: ['success', 'pending', 'error'],
17
+ },
18
+ },
19
+ args: {
20
+ variant: 'success',
21
+ },
22
+ };
23
+
24
+ export default meta;
25
+
26
+ type Story = StoryObj<typeof meta>;
27
+
28
+ export const Playground: Story = {
29
+ render: (args: ComponentProps<typeof Roundel>) => <Roundel {...args} />,
30
+ };
31
+
32
+ export const Variants: Story = {
33
+ parameters: {
34
+ controls: { exclude: ['variant'] },
35
+ },
36
+ render: () => (
37
+ <Flex direction="column" spacing="md" alignItems="center">
38
+ <VariantTitle title="Success">
39
+ <Roundel variant="success" />
40
+ </VariantTitle>
41
+ <VariantTitle title="Pending">
42
+ <Roundel variant="pending" />
43
+ </VariantTitle>
44
+ <VariantTitle title="Error">
45
+ <Roundel variant="error" />
46
+ </VariantTitle>
47
+ </Flex>
48
+ ),
49
+ };
@@ -0,0 +1,51 @@
1
+ import { CloseSmallIcon, TickSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
2
+ import { View } from 'react-native';
3
+ import { StyleSheet } from 'react-native-unistyles';
4
+ import { Icon } from '../Icon';
5
+ import type RoundelProps from './Roundel.props';
6
+
7
+ const Roundel = ({ variant = 'success', style, ...props }: RoundelProps) => {
8
+ styles.useVariants({ variant });
9
+
10
+ const icon =
11
+ variant === 'error' ? CloseSmallIcon : variant === 'success' ? TickSmallIcon : undefined;
12
+
13
+ return (
14
+ <View {...props} style={[styles.container, style]}>
15
+ {icon ? <Icon as={icon} size="sm" /> : null}
16
+ </View>
17
+ );
18
+ };
19
+
20
+ Roundel.displayName = 'Roundel';
21
+
22
+ const styles = StyleSheet.create(theme => ({
23
+ container: {
24
+ width: theme.space[300],
25
+ height: theme.space[300],
26
+ borderRadius: theme.components.parts.roundel.borderRadius,
27
+ alignItems: 'center',
28
+ justifyContent: 'center',
29
+ overflow: 'hidden',
30
+ backgroundColor: 'transparent',
31
+ borderWidth: 0,
32
+ borderStyle: 'solid',
33
+ variants: {
34
+ variant: {
35
+ success: {
36
+ backgroundColor: theme.color.feedback.positive.surface.default,
37
+ },
38
+ error: {
39
+ backgroundColor: theme.color.feedback.danger.surface.default,
40
+ },
41
+ pending: {
42
+ borderWidth: theme.components.parts.roundel.pending.borderWidth,
43
+ borderStyle: 'dashed',
44
+ borderColor: theme.color.border.strong,
45
+ },
46
+ },
47
+ },
48
+ },
49
+ }));
50
+
51
+ export default Roundel;