@utilitywarehouse/hearth-react-native 0.31.0 → 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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +13 -13
- package/CHANGELOG.md +71 -0
- package/build/components/Rating/Rating.d.ts +6 -0
- package/build/components/Rating/Rating.js +76 -0
- package/build/components/Rating/Rating.props.d.ts +18 -0
- package/build/components/Rating/Rating.props.js +1 -0
- package/build/components/Rating/RatingStarEmpty.d.ts +6 -0
- package/build/components/Rating/RatingStarEmpty.js +9 -0
- package/build/components/Rating/RatingStarFilled.d.ts +6 -0
- package/build/components/Rating/RatingStarFilled.js +9 -0
- package/build/components/Rating/index.d.ts +2 -0
- package/build/components/Rating/index.js +1 -0
- package/build/components/Roundel/Roundel.d.ts +6 -0
- package/build/components/Roundel/Roundel.js +40 -0
- package/build/components/Roundel/Roundel.props.d.ts +6 -0
- package/build/components/Roundel/Roundel.props.js +1 -0
- package/build/components/Roundel/index.d.ts +2 -0
- package/build/components/Roundel/index.js +1 -0
- package/build/components/StepperInput/StepperButton.d.ts +22 -0
- package/build/components/StepperInput/StepperButton.js +55 -0
- package/build/components/StepperInput/StepperInput.d.ts +6 -0
- package/build/components/StepperInput/StepperInput.js +196 -0
- package/build/components/StepperInput/StepperInput.props.d.ts +31 -0
- package/build/components/StepperInput/StepperInput.props.js +1 -0
- package/build/components/StepperInput/index.d.ts +2 -0
- package/build/components/StepperInput/index.js +1 -0
- package/build/components/Table/TableHeaderCell.js +10 -1
- package/build/components/Textarea/Textarea.d.ts +1 -1
- package/build/components/Textarea/Textarea.js +10 -3
- package/build/components/Textarea/Textarea.props.d.ts +11 -0
- package/build/components/index.d.ts +3 -0
- package/build/components/index.js +3 -0
- package/build/core/themes.d.ts +92 -88
- package/build/tokens/color.d.ts +82 -80
- package/build/tokens/color.js +41 -40
- package/build/tokens/components/dark/alert.d.ts +6 -6
- package/build/tokens/components/dark/alert.js +6 -6
- package/build/tokens/components/dark/bottom-navigation.d.ts +2 -2
- package/build/tokens/components/dark/bottom-navigation.js +2 -2
- package/build/tokens/components/dark/checkbox.d.ts +1 -1
- package/build/tokens/components/dark/checkbox.js +1 -1
- package/build/tokens/components/dark/icon-button.d.ts +3 -3
- package/build/tokens/components/dark/icon-button.js +3 -3
- package/build/tokens/components/dark/inline-link.d.ts +1 -1
- package/build/tokens/components/dark/inline-link.js +1 -1
- package/build/tokens/components/dark/link.d.ts +3 -3
- package/build/tokens/components/dark/link.js +3 -3
- package/build/tokens/components/dark/navigation.d.ts +2 -2
- package/build/tokens/components/dark/navigation.js +2 -2
- package/build/tokens/components/dark/parts.d.ts +2 -2
- package/build/tokens/components/dark/parts.js +2 -2
- package/build/tokens/components/dark/progress-bar.d.ts +3 -3
- package/build/tokens/components/dark/progress-bar.js +3 -3
- package/build/tokens/components/dark/progress-stepper.d.ts +1 -1
- package/build/tokens/components/dark/progress-stepper.js +1 -1
- package/build/tokens/components/dark/spinner.d.ts +1 -1
- package/build/tokens/components/dark/spinner.js +1 -1
- package/build/tokens/components/dark/table.d.ts +2 -0
- package/build/tokens/components/dark/table.js +2 -0
- package/build/tokens/components/dark/time-picker.d.ts +1 -0
- package/build/tokens/components/dark/time-picker.js +1 -0
- package/build/tokens/components/light/parts.d.ts +3 -3
- package/build/tokens/components/light/parts.js +3 -3
- package/build/tokens/components/light/table.d.ts +2 -0
- package/build/tokens/components/light/table.js +2 -0
- package/build/tokens/components/light/time-picker.d.ts +1 -0
- package/build/tokens/components/light/time-picker.js +1 -0
- package/build/tokens/semantic-dark.d.ts +40 -40
- package/build/tokens/semantic-dark.js +40 -40
- package/docs/adding-shadows.mdx +2 -2
- package/docs/changelog.mdx +165 -0
- package/docs/components/AllComponents.web.tsx +30 -1
- package/docs/dark-mode-best-practice.mdx +328 -0
- package/package.json +1 -1
- package/src/components/Modal/Modal.docs.mdx +58 -4
- package/src/components/NavModal/NavModal.docs.mdx +2 -2
- package/src/components/Rating/Rating.docs.mdx +178 -0
- package/src/components/Rating/Rating.figma.tsx +20 -0
- package/src/components/Rating/Rating.props.ts +22 -0
- package/src/components/Rating/Rating.stories.tsx +95 -0
- package/src/components/Rating/Rating.tsx +140 -0
- package/src/components/Rating/RatingStarEmpty.tsx +22 -0
- package/src/components/Rating/RatingStarFilled.tsx +27 -0
- package/src/components/Rating/index.ts +2 -0
- package/src/components/Roundel/Roundel.docs.mdx +48 -0
- package/src/components/Roundel/Roundel.figma.tsx +17 -0
- package/src/components/Roundel/Roundel.props.ts +8 -0
- package/src/components/Roundel/Roundel.stories.tsx +49 -0
- package/src/components/Roundel/Roundel.tsx +51 -0
- package/src/components/Roundel/index.ts +2 -0
- package/src/components/StepperInput/StepperButton.tsx +83 -0
- package/src/components/StepperInput/StepperInput.docs.mdx +121 -0
- package/src/components/StepperInput/StepperInput.figma.tsx +45 -0
- package/src/components/StepperInput/StepperInput.props.ts +39 -0
- package/src/components/StepperInput/StepperInput.stories.tsx +270 -0
- package/src/components/StepperInput/StepperInput.tsx +349 -0
- package/src/components/StepperInput/index.ts +2 -0
- package/src/components/Table/TableHeaderCell.tsx +10 -1
- package/src/components/Textarea/Textarea.docs.mdx +2 -0
- package/src/components/Textarea/Textarea.props.ts +11 -0
- package/src/components/Textarea/Textarea.stories.tsx +14 -0
- package/src/components/Textarea/Textarea.tsx +11 -2
- package/src/components/index.ts +3 -0
- package/src/tokens/color.ts +41 -40
- package/src/tokens/components/dark/alert.ts +6 -6
- package/src/tokens/components/dark/bottom-navigation.ts +2 -2
- package/src/tokens/components/dark/checkbox.ts +1 -1
- package/src/tokens/components/dark/icon-button.ts +3 -3
- package/src/tokens/components/dark/inline-link.ts +1 -1
- package/src/tokens/components/dark/link.ts +3 -3
- package/src/tokens/components/dark/navigation.ts +2 -2
- package/src/tokens/components/dark/parts.ts +2 -2
- package/src/tokens/components/dark/progress-bar.ts +3 -3
- package/src/tokens/components/dark/progress-stepper.ts +1 -1
- package/src/tokens/components/dark/spinner.ts +1 -1
- package/src/tokens/components/dark/table.ts +2 -0
- package/src/tokens/components/dark/time-picker.ts +1 -0
- package/src/tokens/components/light/parts.ts +3 -3
- package/src/tokens/components/light/table.ts +2 -0
- package/src/tokens/components/light/time-picker.ts +1 -0
- package/src/tokens/semantic-dark.ts +40 -40
- package/vercel.json +0 -21
|
@@ -6,13 +6,13 @@ import modaliOSVideo from '../../../docs/assets/modal-ios.mp4';
|
|
|
6
6
|
import { BackToTopButton, ViewFigmaButton } from '../../../docs/components';
|
|
7
7
|
import * as Stories from './NavModal.stories';
|
|
8
8
|
|
|
9
|
-
<Meta title="Components /
|
|
9
|
+
<Meta title="Components / Nav Modal" />
|
|
10
10
|
|
|
11
11
|
<ViewFigmaButton url="https://www.figma.com/design/dLI9bmyMr42LV7dtFeW27J/Hearth-Patterns---Guides?node-id=6314-9103&t=oq3NaPLaAu3di6Db-4" />
|
|
12
12
|
|
|
13
13
|
<BackToTopButton />
|
|
14
14
|
|
|
15
|
-
#
|
|
15
|
+
# Nav Modal
|
|
16
16
|
|
|
17
17
|
The `NavModal` component is the screen-based modal layout for navigation flows. Use it when a screen is already being presented by React Navigation with `presentation: 'modal'` or `presentation: 'fullScreenModal'` and you want Hearth's modal structure, actions, and Android close animation support.
|
|
18
18
|
|
|
@@ -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,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,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
|
+
};
|