@utilitywarehouse/hearth-react-native 0.32.2 → 0.32.4
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/.storybook/main.ts +17 -7
- package/.storybook/manager.ts +4 -2
- package/.storybook/preview.tsx +4 -1
- package/.turbo/turbo-build.log +1 -4
- package/.turbo/{turbo-lint.log → turbo-lint$colon$fix.log} +11 -14
- package/CHANGELOG.md +36 -0
- package/build/components/Accordion/Accordion.d.ts +6 -6
- package/build/components/Banner/Banner.props.d.ts +2 -0
- package/build/components/Card/Card.d.ts +1 -1
- package/build/components/Card/CardAction/CardAction.d.ts +1 -1
- package/build/components/Checkbox/Checkbox.d.ts +1 -1
- package/build/components/ExpandableCard/ExpandableCardTrigger.d.ts +1 -1
- package/build/components/Icons/CircleIcon.d.ts +2 -2
- package/build/components/Input/InputField.js +1 -3
- package/build/components/Link/Link.d.ts +1 -1
- package/build/components/List/ListItem/ListItem.d.ts +1 -1
- package/build/components/Radio/Radio.d.ts +1 -1
- package/build/components/Rating/Rating.d.ts +1 -1
- package/build/components/Rating/Rating.js +40 -8
- package/build/components/Rating/Rating.props.d.ts +10 -2
- package/build/components/Rating/Rating.utils.d.ts +6 -0
- package/build/components/Rating/Rating.utils.js +7 -0
- package/build/components/Rating/Rating.utils.test.d.ts +1 -0
- package/build/components/Rating/Rating.utils.test.js +13 -0
- package/build/components/Rating/RatingEmoji.d.ts +3 -0
- package/build/components/Rating/RatingEmoji.js +21 -0
- package/build/components/Rating/index.d.ts +1 -1
- package/build/components/StepperInput/StepperButton.d.ts +1 -1
- package/build/components/Tabs/Tab.js +2 -2
- package/build/components/ToggleButton/ToggleButton.d.ts +2 -2
- package/docs/changelog.mdx +36 -0
- package/package.json +15 -11
- package/src/components/Banner/Banner.props.ts +2 -0
- package/src/components/ExpandableCard/ExpandableCardTrigger.tsx +0 -1
- package/src/components/Input/InputField.tsx +0 -1
- package/src/components/Rating/Rating.docs.mdx +44 -5
- package/src/components/Rating/Rating.figma.tsx +19 -0
- package/src/components/Rating/Rating.props.ts +8 -2
- package/src/components/Rating/Rating.stories.tsx +17 -0
- package/src/components/Rating/Rating.tsx +86 -28
- package/src/components/Rating/Rating.utils.test.ts +15 -0
- package/src/components/Rating/Rating.utils.ts +14 -0
- package/src/components/Rating/RatingEmoji.tsx +28 -0
- package/src/components/Rating/index.ts +1 -1
- package/src/components/Tabs/Tab.tsx +2 -3
- package/src/vite-env.d.ts +7 -0
- package/tsconfig.json +4 -0
- package/vitest.config.js +0 -2
package/.storybook/main.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { fileURLToPath } from
|
|
2
|
-
import { dirname } from
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
3
|
import remarkGfm from 'remark-gfm';
|
|
4
4
|
import svgr from 'vite-plugin-svgr';
|
|
5
5
|
|
|
@@ -14,9 +14,9 @@ const unistylesPluginOptions = {
|
|
|
14
14
|
const config = {
|
|
15
15
|
stories: ['../**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
|
16
16
|
addons: [
|
|
17
|
-
getAbsolutePath(
|
|
17
|
+
getAbsolutePath('@chromatic-com/storybook'),
|
|
18
18
|
{
|
|
19
|
-
name: getAbsolutePath(
|
|
19
|
+
name: getAbsolutePath('@storybook/addon-docs'),
|
|
20
20
|
options: {
|
|
21
21
|
mdxPluginOptions: {
|
|
22
22
|
mdxCompileOptions: {
|
|
@@ -25,11 +25,11 @@ const config = {
|
|
|
25
25
|
},
|
|
26
26
|
},
|
|
27
27
|
},
|
|
28
|
-
getAbsolutePath(
|
|
29
|
-
getAbsolutePath(
|
|
28
|
+
getAbsolutePath('@storybook/addon-a11y'),
|
|
29
|
+
getAbsolutePath('@storybook/addon-vitest'),
|
|
30
30
|
],
|
|
31
31
|
framework: {
|
|
32
|
-
name: getAbsolutePath(
|
|
32
|
+
name: getAbsolutePath('@storybook/react-native-web-vite'),
|
|
33
33
|
options: {
|
|
34
34
|
pluginReactOptions: {
|
|
35
35
|
babel: {
|
|
@@ -71,6 +71,16 @@ const config = {
|
|
|
71
71
|
...config.optimizeDeps,
|
|
72
72
|
exclude: [...(config.optimizeDeps?.exclude || []), '@utilitywarehouse/hearth-svg-assets'],
|
|
73
73
|
},
|
|
74
|
+
build: {
|
|
75
|
+
...config.build,
|
|
76
|
+
rolldownOptions: {
|
|
77
|
+
...config.build?.rolldownOptions,
|
|
78
|
+
external: [
|
|
79
|
+
...(config.build?.rolldownOptions?.external || []),
|
|
80
|
+
'@react-stately/utils',
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
74
84
|
};
|
|
75
85
|
},
|
|
76
86
|
};
|
package/.storybook/manager.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import '@utilitywarehouse/hearth-fonts';
|
|
2
2
|
import { addons } from 'storybook/manager-api';
|
|
3
|
-
import '@utilitywarehouse/hearth-tokens/index.css';
|
|
4
3
|
import '../../../shared/storybook/styles/manager.css';
|
|
5
|
-
import
|
|
4
|
+
import { config } from '../../../shared/storybook/theme';
|
|
5
|
+
import { create } from 'storybook/theming';
|
|
6
|
+
|
|
7
|
+
const theme = create(config);
|
|
6
8
|
|
|
7
9
|
addons.setConfig({
|
|
8
10
|
theme,
|
package/.storybook/preview.tsx
CHANGED
|
@@ -6,9 +6,12 @@ import { useEffect } from 'react';
|
|
|
6
6
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
|
7
7
|
import '../../../shared/storybook/styles/diff-highlighting.css';
|
|
8
8
|
import '../../../shared/storybook/styles/preview.css';
|
|
9
|
-
import theme from '../../../shared/storybook/theme';
|
|
10
9
|
import { breakpoints, StyleSheet, themes, UnistylesRuntime } from '../src/core';
|
|
11
10
|
import { initializePrism } from './prism-setup';
|
|
11
|
+
import { config } from '../../../shared/storybook/theme';
|
|
12
|
+
import { create } from 'storybook/theming';
|
|
13
|
+
|
|
14
|
+
const theme = create(config);
|
|
12
15
|
|
|
13
16
|
// Initialize Prism.js for syntax highlighting
|
|
14
17
|
initializePrism();
|
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
> @utilitywarehouse/hearth-react-native@0.32.2 lint /home/runner/work/hearth/hearth/packages/react-native
|
|
3
|
-
> TIMING=1 eslint .
|
|
4
|
-
|
|
1
|
+
$ TIMING=1 eslint --fix .
|
|
5
2
|
|
|
6
3
|
/home/runner/work/hearth/hearth/packages/react-native/src/components/Carousel/Carousel.context.tsx
|
|
7
4
|
6:14 warning Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
|
|
@@ -47,13 +44,13 @@
|
|
|
47
44
|
|
|
48
45
|
Rule | Time (ms) | Relative
|
|
49
46
|
:----------------------------------------|----------:|--------:
|
|
50
|
-
@typescript-eslint/no-unused-vars |
|
|
51
|
-
react-hooks/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
no-misleading-character-class |
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
no-
|
|
58
|
-
no-
|
|
59
|
-
@typescript-eslint/no-unused-expressions |
|
|
47
|
+
@typescript-eslint/no-unused-vars | 2115.502 | 60.0%
|
|
48
|
+
react-hooks/rules-of-hooks | 141.299 | 4.0%
|
|
49
|
+
no-global-assign | 128.266 | 3.6%
|
|
50
|
+
react-hooks/exhaustive-deps | 107.897 | 3.1%
|
|
51
|
+
no-misleading-character-class | 72.815 | 2.1%
|
|
52
|
+
@typescript-eslint/ban-ts-comment | 70.843 | 2.0%
|
|
53
|
+
no-loss-of-precision | 55.192 | 1.6%
|
|
54
|
+
no-unexpected-multiline | 52.206 | 1.5%
|
|
55
|
+
no-regex-spaces | 50.033 | 1.4%
|
|
56
|
+
@typescript-eslint/no-unused-expressions | 49.226 | 1.4%
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# @utilitywarehouse/hearth-react-native
|
|
2
2
|
|
|
3
|
+
## 0.32.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#1222](https://github.com/utilitywarehouse/hearth/pull/1222) [`3f35a43`](https://github.com/utilitywarehouse/hearth/commit/3f35a431ba4a0fae45cc640a62ea6cb53f85c384) Thanks [@Utakato](https://github.com/Utakato)! - 🌟 [FEATURE]: `Rating` component `emojis` variant
|
|
8
|
+
|
|
9
|
+
The `Rating` component now supports an `emojis` variant that renders emoji faces
|
|
10
|
+
instead of stars. When selected, the chosen emoji appears larger whilst
|
|
11
|
+
unselected emojis become grayscale. Two static labels ("Very dissatisfied" /
|
|
12
|
+
"Very satisfied") are displayed at the extremes.
|
|
13
|
+
|
|
14
|
+
**Components affected**:
|
|
15
|
+
- `Rating`
|
|
16
|
+
|
|
17
|
+
**Developer changes**:
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { Rating } from '@utilitywarehouse/hearth-react-native';
|
|
21
|
+
|
|
22
|
+
<Rating value={rating} onChange={setRating} variant="emojis" />;
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
All existing props (`value`, `defaultValue`, `onChange`, `disabled`, `hideLabel`)
|
|
26
|
+
work with the emoji variant. The new `rangeLabels` prop allows overriding the
|
|
27
|
+
endpoint labels (defaulting to "Very dissatisfied" / "Very satisfied"):
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
<Rating variant="emojis" rangeLabels={{ low: 'Not at all', high: 'Absolutely' }} />
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 0.32.3
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- [#1192](https://github.com/utilitywarehouse/hearth/pull/1192) [`a74bf02`](https://github.com/utilitywarehouse/hearth/commit/a74bf02c58c12e1b42351e0d7f8e3e79ea0acbd6) Thanks [@robphoenix](https://github.com/robphoenix)! - 🧹 [HOUSEKEEPING]: Fix dependencies and types
|
|
38
|
+
|
|
3
39
|
## 0.32.2
|
|
4
40
|
|
|
5
41
|
### Patch Changes
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { AccordionProps } from './Accordion.props';
|
|
2
2
|
import { AccordionItemProps } from './AccordionItem.props';
|
|
3
|
-
export declare const AccordionHeader: import("react").ForwardRefExoticComponent<import("react-native").ViewProps & import("react").RefAttributes<import("react-native").ViewProps>>;
|
|
4
|
-
export declare const AccordionTrigger: import("react").ForwardRefExoticComponent<Omit<import("react-native").PressableProps & {
|
|
3
|
+
export declare const AccordionHeader: import("react").ForwardRefExoticComponent<import("react-native/types").ViewProps & import("react").RefAttributes<import("react-native/types").ViewProps>>;
|
|
4
|
+
export declare const AccordionTrigger: import("react").ForwardRefExoticComponent<Omit<import("react-native/types").PressableProps & {
|
|
5
5
|
states?: {
|
|
6
6
|
active?: boolean;
|
|
7
7
|
};
|
|
8
|
-
}, "children"> & import("@gluestack-ui/accordion/lib/typescript/types").IAccordionTriggerProps & import("react").RefAttributes<import("react-native").PressableProps & {
|
|
8
|
+
}, "children"> & import("@gluestack-ui/accordion/lib/typescript/types").IAccordionTriggerProps & import("react").RefAttributes<import("react-native/types").PressableProps & {
|
|
9
9
|
states?: {
|
|
10
10
|
active?: boolean;
|
|
11
11
|
};
|
|
12
12
|
}>>;
|
|
13
|
-
export declare const AccordionContent: import("react").ForwardRefExoticComponent<import("react-native").ViewProps & import("react").RefAttributes<import("react-native").ViewProps>>;
|
|
14
|
-
export declare const AccordionContentText: import("react").ForwardRefExoticComponent<import("react-native").TextProps & import("react").RefAttributes<import("react-native").TextProps>>;
|
|
13
|
+
export declare const AccordionContent: import("react").ForwardRefExoticComponent<import("react-native/types").ViewProps & import("react").RefAttributes<import("react-native/types").ViewProps>>;
|
|
14
|
+
export declare const AccordionContentText: import("react").ForwardRefExoticComponent<import("react-native/types").TextProps & import("react").RefAttributes<import("react-native/types").TextProps>>;
|
|
15
15
|
export declare const AccordionIcon: import("react").ForwardRefExoticComponent<import("..").IconProps & import("react").RefAttributes<import("..").IconProps>>;
|
|
16
|
-
export declare const AccordionTitleText: import("react").ForwardRefExoticComponent<import("react-native").TextProps & import("react").RefAttributes<import("react-native").TextProps>>;
|
|
16
|
+
export declare const AccordionTitleText: import("react").ForwardRefExoticComponent<import("react-native/types").TextProps & import("react").RefAttributes<import("react-native/types").TextProps>>;
|
|
17
17
|
declare const Accordion: {
|
|
18
18
|
({ children, collapsible, type, heading, helperText, ...props }: AccordionProps): import("react/jsx-runtime").JSX.Element;
|
|
19
19
|
displayName: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComponentType, ReactElement, ReactNode } from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
3
|
import type CardProps from '../Card/Card.props';
|
|
3
4
|
export type BannerDirection = 'horizontal' | 'vertical';
|
|
4
5
|
export interface BannerProps extends Omit<CardProps, 'noPadding' | 'variant' | 'space' | 'gap' | 'rowGap' | 'columnGap' | 'flexDirection' | 'flexWrap' | 'alignItems' | 'justifyContent'> {
|
|
@@ -71,5 +72,6 @@ export interface BannerProps extends Omit<CardProps, 'noPadding' | 'variant' | '
|
|
|
71
72
|
* @default 'center'
|
|
72
73
|
*/
|
|
73
74
|
alignChevron?: 'center' | 'start' | 'end';
|
|
75
|
+
style?: StyleProp<ViewStyle>;
|
|
74
76
|
}
|
|
75
77
|
export default BannerProps;
|
|
@@ -3,7 +3,7 @@ declare const Card: import("react").ForwardRefExoticComponent<import("./Card.pro
|
|
|
3
3
|
active?: boolean;
|
|
4
4
|
disabled?: boolean;
|
|
5
5
|
};
|
|
6
|
-
} & Omit<import("react-native").PressableProps, "children"> & {
|
|
6
|
+
} & Omit<import("react-native/types").PressableProps, "children"> & {
|
|
7
7
|
tabIndex?: 0 | -1 | undefined;
|
|
8
8
|
} & {
|
|
9
9
|
children?: import("react").ReactNode | (({ hovered, pressed, focused, focusVisible, disabled, }: {
|
|
@@ -4,7 +4,7 @@ declare const CardAction: import("react").ForwardRefExoticComponent<(((import(".
|
|
|
4
4
|
disabled?: boolean;
|
|
5
5
|
};
|
|
6
6
|
isFirst?: boolean;
|
|
7
|
-
}) & Omit<import("react-native").PressableProps, "children">) & {
|
|
7
|
+
}) & Omit<import("react-native/types").PressableProps, "children">) & {
|
|
8
8
|
tabIndex?: 0 | -1 | undefined;
|
|
9
9
|
} & {
|
|
10
10
|
children?: import("react").ReactNode | (({ hovered, pressed, focused, focusVisible, disabled, }: {
|
|
@@ -4,7 +4,7 @@ declare const CheckboxGroup: import("react").ForwardRefExoticComponent<import("r
|
|
|
4
4
|
}> & import("./CheckboxGroup.props").default & {
|
|
5
5
|
isCard?: boolean;
|
|
6
6
|
} & import("@gluestack-ui/checkbox/lib/typescript/types").ICheckboxGroup>;
|
|
7
|
-
declare const CheckboxIndicator: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("react-native").ViewProps> & import("react-native").ViewProps>;
|
|
7
|
+
declare const CheckboxIndicator: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("react-native/types").ViewProps> & import("react-native/types").ViewProps>;
|
|
8
8
|
declare const CheckboxIcon: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("..").IconProps> & import("..").IconProps & {
|
|
9
9
|
forceMount?: boolean;
|
|
10
10
|
}>;
|
|
@@ -3,7 +3,7 @@ declare const ExpandableCardTrigger: import("react").ForwardRefExoticComponent<(
|
|
|
3
3
|
active?: boolean;
|
|
4
4
|
disabled?: boolean;
|
|
5
5
|
};
|
|
6
|
-
}) & Omit<import("react-native").PressableProps, "children">) & {
|
|
6
|
+
}) & Omit<import("react-native/types").PressableProps, "children">) & {
|
|
7
7
|
tabIndex?: 0 | -1 | undefined;
|
|
8
8
|
} & {
|
|
9
9
|
children?: import("react").ReactNode | (({ hovered, pressed, focused, focusVisible, disabled, }: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
declare const CircleIcon: import("@gluestack-ui/icon/lib/typescript/createIcon").IIconComponentType<import("react-native-svg").SvgProps | {
|
|
2
|
-
fill?: import("react-native").ColorValue | undefined;
|
|
3
|
-
stroke?: import("react-native").ColorValue | undefined;
|
|
2
|
+
fill?: import("react-native/types").ColorValue | undefined;
|
|
3
|
+
stroke?: import("react-native/types").ColorValue | undefined;
|
|
4
4
|
}>;
|
|
5
5
|
export default CircleIcon;
|
|
@@ -10,9 +10,7 @@ const InputField = forwardRef(({ style, inBottomSheet = false, ...props }, ref)
|
|
|
10
10
|
styles.useVariants({ focused, type });
|
|
11
11
|
const { color } = useTheme();
|
|
12
12
|
if (inBottomSheet) {
|
|
13
|
-
return (
|
|
14
|
-
// @ts-expect-error - BottomSheetTextInput has incompatible event types with TextInput
|
|
15
|
-
_jsx(BottomSheetTextInput, { ref: ref, placeholderTextColor: color.text.secondary, selectionColor: color.surface.brand.default, cursorColor: color.surface.brand.default, verticalAlign: "middle", "aria-disabled": disabled, ...props, style: [styles.input, style] }));
|
|
13
|
+
return (_jsx(BottomSheetTextInput, { ref: ref, placeholderTextColor: color.text.secondary, selectionColor: color.surface.brand.default, cursorColor: color.surface.brand.default, verticalAlign: "middle", "aria-disabled": disabled, ...props, style: [styles.input, style] }));
|
|
16
14
|
}
|
|
17
15
|
return (_jsx(RNTextInput, { ref: ref, placeholderTextColor: color.text.secondary, selectionColor: color.surface.brand.default, cursorColor: color.surface.brand.default, verticalAlign: "middle", "aria-disabled": disabled, ...props, style: [styles.input, style] }));
|
|
18
16
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LinkProps } from './Link.props';
|
|
2
|
-
export declare const LinkText: import("react").ForwardRefExoticComponent<import("react-native").TextProps & import("react").RefAttributes<import("react-native").TextProps>>;
|
|
2
|
+
export declare const LinkText: import("react").ForwardRefExoticComponent<import("react-native/types").TextProps & import("react").RefAttributes<import("react-native/types").TextProps>>;
|
|
3
3
|
declare const Link: {
|
|
4
4
|
({ children, icon, disabled, target, iconPosition, showIcon, textStyle, iconStyle, ...props }: LinkProps): import("react/jsx-runtime").JSX.Element;
|
|
5
5
|
displayName: string;
|
|
@@ -3,7 +3,7 @@ declare const ListItem: import("react").ForwardRefExoticComponent<(((import("./L
|
|
|
3
3
|
active?: boolean;
|
|
4
4
|
disabled?: boolean;
|
|
5
5
|
};
|
|
6
|
-
}) & Omit<import("react-native").PressableProps, "children">) & {
|
|
6
|
+
}) & Omit<import("react-native/types").PressableProps, "children">) & {
|
|
7
7
|
tabIndex?: 0 | -1 | undefined;
|
|
8
8
|
} & {
|
|
9
9
|
children?: import("react").ReactNode | (({ hovered, pressed, focused, focusVisible, disabled, }: {
|
|
@@ -4,7 +4,7 @@ declare const RadioGroup: import("react").ForwardRefExoticComponent<import("reac
|
|
|
4
4
|
}> & import("./RadioGroup.props").default & {
|
|
5
5
|
isCard?: boolean;
|
|
6
6
|
} & import("@gluestack-ui/radio/lib/typescript/types").IRadioGroupProps>;
|
|
7
|
-
declare const RadioIndicator: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("react-native").ViewProps> & import("react-native").ViewProps>;
|
|
7
|
+
declare const RadioIndicator: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("react-native/types").ViewProps> & import("react-native/types").ViewProps>;
|
|
8
8
|
declare const RadioIcon: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("..").IconProps> & import("..").IconProps & {
|
|
9
9
|
forceMount?: boolean;
|
|
10
10
|
}>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type RatingProps from './Rating.props';
|
|
2
2
|
declare const Rating: {
|
|
3
|
-
({ value, defaultValue, onChange, disabled, labels, hideLabel, style, accessibilityLabel, ...props }: RatingProps): import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
({ variant, value, defaultValue, onChange, disabled, labels, rangeLabels, hideLabel, style, accessibilityLabel, ...props }: RatingProps): import("react/jsx-runtime").JSX.Element;
|
|
4
4
|
displayName: string;
|
|
5
5
|
};
|
|
6
6
|
export default Rating;
|
|
@@ -3,12 +3,21 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
3
3
|
import { Pressable, View } from 'react-native';
|
|
4
4
|
import { StyleSheet } from 'react-native-unistyles';
|
|
5
5
|
import { BodyText } from '../BodyText';
|
|
6
|
+
import { getEmojiSvg } from './RatingEmoji';
|
|
6
7
|
import RatingStarEmpty from './RatingStarEmpty';
|
|
7
8
|
import RatingStarFilled from './RatingStarFilled';
|
|
9
|
+
import { EMOJI_LIST } from './Rating.utils';
|
|
8
10
|
const MAX_RATING = 5;
|
|
9
11
|
const STAR_WIDTH = 32;
|
|
10
12
|
const STAR_HEIGHT = 30;
|
|
11
13
|
const STAR_CONTAINER_SIZE = 40;
|
|
14
|
+
const EMOJI_SIZE_DEFAULT = 32;
|
|
15
|
+
const EMOJI_SIZE_SELECTED = 40;
|
|
16
|
+
const EMOJI_CONTAINER_SIZE = 44;
|
|
17
|
+
const DEFAULT_RANGE_LABELS = {
|
|
18
|
+
low: EMOJI_LIST[0].accessibilityLabel,
|
|
19
|
+
high: EMOJI_LIST[EMOJI_LIST.length - 1].accessibilityLabel,
|
|
20
|
+
};
|
|
12
21
|
const DEFAULT_LABELS = {
|
|
13
22
|
0: 'Select a rating',
|
|
14
23
|
1: 'Awful',
|
|
@@ -18,7 +27,7 @@ const DEFAULT_LABELS = {
|
|
|
18
27
|
5: 'Great!',
|
|
19
28
|
};
|
|
20
29
|
const clampRating = (value) => Math.min(MAX_RATING, Math.max(0, Math.round(value)));
|
|
21
|
-
const Rating = ({ value, defaultValue = 0, onChange, disabled = false, labels, hideLabel = false, style, accessibilityLabel, ...props }) => {
|
|
30
|
+
const Rating = ({ variant = 'stars', value, defaultValue = 0, onChange, disabled = false, labels, rangeLabels = DEFAULT_RANGE_LABELS, hideLabel = false, style, accessibilityLabel, ...props }) => {
|
|
22
31
|
const isControlled = value !== undefined;
|
|
23
32
|
const [internalValue, setInternalValue] = useState(clampRating(defaultValue));
|
|
24
33
|
useEffect(() => {
|
|
@@ -39,11 +48,22 @@ const Rating = ({ value, defaultValue = 0, onChange, disabled = false, labels, h
|
|
|
39
48
|
onChange?.(nextValue);
|
|
40
49
|
}, [disabled, isControlled, onChange]);
|
|
41
50
|
styles.useVariants({ disabled });
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
const isEmojis = variant === 'emojis';
|
|
52
|
+
const hasSelection = resolvedValue > 0;
|
|
53
|
+
const startLabel = rangeLabels.low;
|
|
54
|
+
const endLabel = rangeLabels.high;
|
|
55
|
+
return (_jsxs(View, { ...props, accessibilityRole: "radiogroup", accessibilityState: { disabled }, accessibilityLabel: accessibilityLabel ?? (isEmojis ? 'Rate your experience' : currentLabel), style: [styles.container, style], children: [_jsx(View, { style: styles.items, children: isEmojis
|
|
56
|
+
? EMOJI_LIST.map(entry => {
|
|
57
|
+
const isSelected = resolvedValue === entry.value;
|
|
58
|
+
const size = isSelected ? EMOJI_SIZE_SELECTED : EMOJI_SIZE_DEFAULT;
|
|
59
|
+
const EmojiSvg = getEmojiSvg(entry.value, hasSelection && !isSelected);
|
|
60
|
+
return (_jsx(Pressable, { accessibilityRole: "radio", accessibilityState: { selected: isSelected, disabled }, accessibilityLabel: `Rate ${entry.accessibilityLabel}`, disabled: disabled, hitSlop: 8, onPress: () => handlePress(entry.value), style: styles.emojiItem, children: _jsx(EmojiSvg, { width: size, height: size }) }, entry.value));
|
|
61
|
+
})
|
|
62
|
+
: [1, 2, 3, 4, 5].map(starValue => {
|
|
63
|
+
const isFilled = starValue <= resolvedValue;
|
|
64
|
+
const starLabel = resolvedLabels[starValue] ?? DEFAULT_LABELS[starValue];
|
|
65
|
+
return (_jsx(Pressable, { accessibilityRole: "radio", accessibilityState: { selected: resolvedValue === starValue, disabled }, accessibilityLabel: `Rate ${starLabel}`, disabled: disabled, hitSlop: 8, onPress: () => handlePress(starValue), style: styles.starItem, children: isFilled ? (_jsx(RatingStarFilled, { width: STAR_WIDTH, height: STAR_HEIGHT })) : (_jsx(RatingStarEmpty, { width: STAR_WIDTH, height: STAR_HEIGHT })) }, starValue));
|
|
66
|
+
}) }), isEmojis ? (!hideLabel ? (_jsxs(View, { style: styles.emojiLabels, children: [_jsx(BodyText, { size: "md", color: "secondary", children: startLabel }), _jsx(BodyText, { size: "md", color: "secondary", children: endLabel })] })) : null) : !hideLabel ? (_jsx(BodyText, { size: "md", color: labelColor, style: styles.label, children: currentLabel })) : null] }));
|
|
47
67
|
};
|
|
48
68
|
Rating.displayName = 'Rating';
|
|
49
69
|
const styles = StyleSheet.create(theme => ({
|
|
@@ -58,19 +78,31 @@ const styles = StyleSheet.create(theme => ({
|
|
|
58
78
|
},
|
|
59
79
|
},
|
|
60
80
|
},
|
|
61
|
-
|
|
81
|
+
items: {
|
|
62
82
|
flexDirection: 'row',
|
|
63
83
|
gap: theme.components.rating.gap,
|
|
64
84
|
},
|
|
65
|
-
|
|
85
|
+
starItem: {
|
|
66
86
|
width: STAR_CONTAINER_SIZE,
|
|
67
87
|
height: STAR_CONTAINER_SIZE,
|
|
68
88
|
alignItems: 'center',
|
|
69
89
|
justifyContent: 'center',
|
|
70
90
|
padding: theme.components.rating.borderWidth,
|
|
71
91
|
},
|
|
92
|
+
emojiItem: {
|
|
93
|
+
width: EMOJI_CONTAINER_SIZE,
|
|
94
|
+
height: EMOJI_CONTAINER_SIZE,
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
justifyContent: 'center',
|
|
97
|
+
padding: theme.components.rating.borderWidth,
|
|
98
|
+
},
|
|
72
99
|
label: {
|
|
73
100
|
textAlign: 'center',
|
|
74
101
|
},
|
|
102
|
+
emojiLabels: {
|
|
103
|
+
flexDirection: 'row',
|
|
104
|
+
justifyContent: 'space-between',
|
|
105
|
+
width: '100%',
|
|
106
|
+
},
|
|
75
107
|
}));
|
|
76
108
|
export default Rating;
|
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import type { ViewProps } from 'react-native';
|
|
2
2
|
export type RatingValue = 0 | 1 | 2 | 3 | 4 | 5;
|
|
3
3
|
export type RatingLabels = Partial<Record<RatingValue, string>>;
|
|
4
|
+
export type RatingVariant = 'stars' | 'emojis';
|
|
4
5
|
export interface RatingProps extends Omit<ViewProps, 'children'> {
|
|
6
|
+
/** Visual variant for the rating indicators. */
|
|
7
|
+
variant?: RatingVariant;
|
|
5
8
|
/** Current rating value. */
|
|
6
9
|
value?: RatingValue;
|
|
7
10
|
/** Initial rating value when uncontrolled. */
|
|
8
11
|
defaultValue?: RatingValue;
|
|
9
|
-
/** Called when a
|
|
12
|
+
/** Called when a rating is selected. */
|
|
10
13
|
onChange?: (value: RatingValue) => void;
|
|
11
14
|
/** Disables the rating input. */
|
|
12
15
|
disabled?: boolean;
|
|
13
16
|
/** Override labels for specific rating values. */
|
|
14
17
|
labels?: RatingLabels;
|
|
15
|
-
/**
|
|
18
|
+
/** Override the low and high end labels shown below the emoji variant. */
|
|
19
|
+
rangeLabels?: {
|
|
20
|
+
low: string;
|
|
21
|
+
high: string;
|
|
22
|
+
};
|
|
23
|
+
/** Hide the label text below the rating. */
|
|
16
24
|
hideLabel?: boolean;
|
|
17
25
|
}
|
|
18
26
|
export default RatingProps;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const EMOJI_LIST = [
|
|
2
|
+
{ value: 1, accessibilityLabel: 'Very dissatisfied' },
|
|
3
|
+
{ value: 2, accessibilityLabel: 'Dissatisfied' },
|
|
4
|
+
{ value: 3, accessibilityLabel: 'Neutral' },
|
|
5
|
+
{ value: 4, accessibilityLabel: 'Satisfied' },
|
|
6
|
+
{ value: 5, accessibilityLabel: 'Very satisfied' },
|
|
7
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { EMOJI_LIST } from './Rating.utils';
|
|
3
|
+
describe('EMOJI_LIST', () => {
|
|
4
|
+
it('has exactly 5 emojis mapped to values 1-5', () => {
|
|
5
|
+
expect(EMOJI_LIST).toHaveLength(5);
|
|
6
|
+
expect(EMOJI_LIST.map(e => e.value)).toEqual([1, 2, 3, 4, 5]);
|
|
7
|
+
});
|
|
8
|
+
it('each entry has an accessibility label', () => {
|
|
9
|
+
for (const entry of EMOJI_LIST) {
|
|
10
|
+
expect(entry.accessibilityLabel).toBeTruthy();
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import BeamingFace from '@utilitywarehouse/hearth-svg-assets/lib/beaming-face.svg';
|
|
2
|
+
import BeamingFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/beaming-face-grey.svg';
|
|
3
|
+
import DissapointedFace from '@utilitywarehouse/hearth-svg-assets/lib/dissapointed-face.svg';
|
|
4
|
+
import DissapointedFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/dissapointed-face-grey.svg';
|
|
5
|
+
import FrowningFace from '@utilitywarehouse/hearth-svg-assets/lib/frowning-face.svg';
|
|
6
|
+
import FrowningFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/frowning-face-grey.svg';
|
|
7
|
+
import NeutralFace from '@utilitywarehouse/hearth-svg-assets/lib/neutral-face.svg';
|
|
8
|
+
import NeutralFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/neutral-face-grey.svg';
|
|
9
|
+
import SlightlySmilingFace from '@utilitywarehouse/hearth-svg-assets/lib/slightly-smiling-face.svg';
|
|
10
|
+
import SlightlySmilingFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/slightly-smiling-face-grey.svg';
|
|
11
|
+
const EMOJI_ASSETS = {
|
|
12
|
+
1: { color: DissapointedFace, grey: DissapointedFaceGrey },
|
|
13
|
+
2: { color: FrowningFace, grey: FrowningFaceGrey },
|
|
14
|
+
3: { color: NeutralFace, grey: NeutralFaceGrey },
|
|
15
|
+
4: { color: SlightlySmilingFace, grey: SlightlySmilingFaceGrey },
|
|
16
|
+
5: { color: BeamingFace, grey: BeamingFaceGrey },
|
|
17
|
+
};
|
|
18
|
+
export const getEmojiSvg = (value, grayscale) => {
|
|
19
|
+
const assets = EMOJI_ASSETS[value];
|
|
20
|
+
return grayscale ? assets.grey : assets.color;
|
|
21
|
+
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as Rating } from './Rating';
|
|
2
|
-
export type { RatingLabels, RatingProps, RatingValue } from './Rating.props';
|
|
2
|
+
export type { RatingLabels, RatingProps, RatingValue, RatingVariant } from './Rating.props';
|
|
@@ -8,7 +8,7 @@ declare const StepperButton: import("react").ForwardRefExoticComponent<{
|
|
|
8
8
|
active?: boolean;
|
|
9
9
|
disabled?: boolean;
|
|
10
10
|
};
|
|
11
|
-
} &
|
|
11
|
+
} & {
|
|
12
12
|
tabIndex?: 0 | -1 | undefined;
|
|
13
13
|
} & {
|
|
14
14
|
children?: import("react").ReactNode | (({ hovered, pressed, focused, focusVisible, disabled, }: {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createPressable } from '@gluestack-ui/pressable';
|
|
2
3
|
import { useCallback, useRef } from 'react';
|
|
3
4
|
import { Platform, Pressable, View } from 'react-native';
|
|
4
5
|
import { StyleSheet } from 'react-native-unistyles';
|
|
6
|
+
import { BodyText } from '../BodyText';
|
|
5
7
|
import { Icon } from '../Icon';
|
|
6
8
|
import { useTabsContext } from './Tabs.context';
|
|
7
|
-
import { createPressable } from '@gluestack-ui/pressable';
|
|
8
|
-
import { BodyText } from '../BodyText';
|
|
9
9
|
const Tab = ({ value, children, icon, disabled, style, states, ...props }) => {
|
|
10
10
|
const { value: active, select, size, disabled: allDisabled, registerTabLayout, } = useTabsContext();
|
|
11
11
|
const { active: pressed } = states || { active: false };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ToggleButtonProps } from './ToggleButton.props';
|
|
2
|
-
export declare const ToggleButtonText: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("react-native").TextProps & {
|
|
2
|
+
export declare const ToggleButtonText: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("react-native/types").TextProps & {
|
|
3
3
|
toggled: boolean;
|
|
4
|
-
}> & import("react-native").TextProps & {
|
|
4
|
+
}> & import("react-native/types").TextProps & {
|
|
5
5
|
toggled: boolean;
|
|
6
6
|
}>;
|
|
7
7
|
export declare const ToggleButtonIcon: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("..").IconProps & {
|
package/docs/changelog.mdx
CHANGED
|
@@ -9,6 +9,42 @@ import { BackToTopButton, NextPrevPage } from './components';
|
|
|
9
9
|
The changelog for the Hearth React Native library. Here you can find all the changes, improvements, and bug fixes for each version.
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
## 0.32.4
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- [#1222](https://github.com/utilitywarehouse/hearth/pull/1222) [`3f35a43`](https://github.com/utilitywarehouse/hearth/commit/3f35a431ba4a0fae45cc640a62ea6cb53f85c384) Thanks [@Utakato](https://github.com/Utakato)! - 🌟 [FEATURE]: `Rating` component `emojis` variant
|
|
17
|
+
|
|
18
|
+
The `Rating` component now supports an `emojis` variant that renders emoji faces
|
|
19
|
+
instead of stars. When selected, the chosen emoji appears larger whilst
|
|
20
|
+
unselected emojis become grayscale. Two static labels ("Very dissatisfied" /
|
|
21
|
+
"Very satisfied") are displayed at the extremes.
|
|
22
|
+
|
|
23
|
+
**Components affected**:
|
|
24
|
+
- `Rating`
|
|
25
|
+
|
|
26
|
+
**Developer changes**:
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { Rating } from '@utilitywarehouse/hearth-react-native';
|
|
30
|
+
|
|
31
|
+
<Rating value={rating} onChange={setRating} variant="emojis" />;
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
All existing props (`value`, `defaultValue`, `onChange`, `disabled`, `hideLabel`)
|
|
35
|
+
work with the emoji variant. The new `rangeLabels` prop allows overriding the
|
|
36
|
+
endpoint labels (defaulting to "Very dissatisfied" / "Very satisfied"):
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
<Rating variant="emojis" rangeLabels={{ low: 'Not at all', high: 'Absolutely' }} />
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 0.32.3
|
|
43
|
+
|
|
44
|
+
### Patch Changes
|
|
45
|
+
|
|
46
|
+
- [#1192](https://github.com/utilitywarehouse/hearth/pull/1192) [`a74bf02`](https://github.com/utilitywarehouse/hearth/commit/a74bf02c58c12e1b42351e0d7f8e3e79ea0acbd6) Thanks [@robphoenix](https://github.com/robphoenix)! - 🧹 [HOUSEKEEPING]: Fix dependencies and types
|
|
47
|
+
|
|
12
48
|
## 0.32.2
|
|
13
49
|
|
|
14
50
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilitywarehouse/hearth-react-native",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.4",
|
|
4
4
|
"description": "Utility Warehouse React Native UI library",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"@gluestack-ui/alert": "0.1.15",
|
|
13
13
|
"@gluestack-ui/button": "1.0.7",
|
|
14
14
|
"@gluestack-ui/checkbox": "0.1.32",
|
|
15
|
+
"@gluestack-ui/form-control": "0.1.19",
|
|
15
16
|
"@gluestack-ui/icon": "0.1.22",
|
|
16
17
|
"@gluestack-ui/input": "0.1.31",
|
|
17
18
|
"@gluestack-ui/link": "0.1.22",
|
|
@@ -21,25 +22,28 @@
|
|
|
21
22
|
"@gluestack-ui/switch": "0.1.22",
|
|
22
23
|
"@gluestack-ui/textarea": "0.1.23",
|
|
23
24
|
"@quidone/react-native-wheel-picker": "^1.6.1",
|
|
24
|
-
"dayjs": "^1.11.13"
|
|
25
|
+
"dayjs": "^1.11.13",
|
|
26
|
+
"nanoid": "3.3.11"
|
|
25
27
|
},
|
|
26
28
|
"devDependencies": {
|
|
27
29
|
"@babel/plugin-proposal-export-namespace-from": "^7.18.9",
|
|
28
30
|
"@chromatic-com/storybook": "^4.1.3",
|
|
29
31
|
"@figma/code-connect": "^1.3.12",
|
|
30
32
|
"@gorhom/bottom-sheet": "5.2.6",
|
|
33
|
+
"@react-stately/utils": "^3.12.0",
|
|
31
34
|
"@storybook/addon-a11y": "^10.2.1",
|
|
32
35
|
"@storybook/addon-docs": "^10.2.1",
|
|
33
36
|
"@storybook/addon-vitest": "^10.2.1",
|
|
34
37
|
"@storybook/react-native-web-vite": "^10.2.1",
|
|
35
38
|
"@types/prismjs": "^1.26.5",
|
|
36
|
-
"@types/react-dom": "^19.1.6",
|
|
37
39
|
"@types/react": "^19.1.10",
|
|
40
|
+
"@types/react-dom": "^19.1.6",
|
|
41
|
+
"@types/react-native": "^0.72.8",
|
|
38
42
|
"@vitest/browser": "^3.2.4",
|
|
39
43
|
"@vitest/coverage-v8": "^3.2.4",
|
|
40
44
|
"chromatic": "^13.3.0",
|
|
41
|
-
"
|
|
42
|
-
"playwright": "^1.
|
|
45
|
+
"globals": "^15.15.0",
|
|
46
|
+
"playwright": "^1.59.1",
|
|
43
47
|
"prismjs": "^1.30.0",
|
|
44
48
|
"react": "^19.1.0",
|
|
45
49
|
"react-dom": "^19.1.0",
|
|
@@ -60,9 +64,9 @@
|
|
|
60
64
|
"vite-plugin-svgr": "^4.5.0",
|
|
61
65
|
"vitest": "^3.2.4",
|
|
62
66
|
"@utilitywarehouse/hearth-fonts": "^0.0.4",
|
|
63
|
-
"@utilitywarehouse/hearth-react-icons": "^0.8.
|
|
67
|
+
"@utilitywarehouse/hearth-react-icons": "^0.8.2",
|
|
64
68
|
"@utilitywarehouse/hearth-react-native-icons": "^0.8.1",
|
|
65
|
-
"@utilitywarehouse/hearth-svg-assets": "^0.6.
|
|
69
|
+
"@utilitywarehouse/hearth-svg-assets": "^0.6.2",
|
|
66
70
|
"@utilitywarehouse/hearth-tokens": "^0.2.4"
|
|
67
71
|
},
|
|
68
72
|
"peerDependencies": {
|
|
@@ -90,10 +94,10 @@
|
|
|
90
94
|
"figma:publish": "figma connect publish",
|
|
91
95
|
"test": "vitest run --config vitest.unit.config.ts",
|
|
92
96
|
"test:storybook": "vitest run --project storybook",
|
|
93
|
-
"dev": "
|
|
94
|
-
"dev:docs": "storybook dev -p 6002 --no-open
|
|
95
|
-
"build:storybook": "
|
|
96
|
-
"build:storybook:docs": "
|
|
97
|
+
"dev": "storybook dev -p 6006",
|
|
98
|
+
"dev:docs": "storybook dev -p 6002 --no-open",
|
|
99
|
+
"build:storybook": "storybook build",
|
|
100
|
+
"build:storybook:docs": "storybook build --docs",
|
|
97
101
|
"chromatic": "npx chromatic --project-token=chpt_cce0fb1ebd95d2a --build-script-name build:storybook"
|
|
98
102
|
}
|
|
99
103
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComponentType, ReactElement, ReactNode } from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
3
|
import type CardProps from '../Card/Card.props';
|
|
3
4
|
|
|
4
5
|
export type BannerDirection = 'horizontal' | 'vertical';
|
|
@@ -92,6 +93,7 @@ export interface BannerProps extends Omit<
|
|
|
92
93
|
* @default 'center'
|
|
93
94
|
*/
|
|
94
95
|
alignChevron?: 'center' | 'start' | 'end';
|
|
96
|
+
style?: StyleProp<ViewStyle>;
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
export default BannerProps;
|
|
@@ -13,7 +13,6 @@ const InputField = forwardRef<RNTextInput, TextInputProps & { inBottomSheet?: bo
|
|
|
13
13
|
|
|
14
14
|
if (inBottomSheet) {
|
|
15
15
|
return (
|
|
16
|
-
// @ts-expect-error - BottomSheetTextInput has incompatible event types with TextInput
|
|
17
16
|
<BottomSheetTextInput
|
|
18
17
|
ref={ref as any}
|
|
19
18
|
placeholderTextColor={color.text.secondary}
|
|
@@ -11,12 +11,13 @@ import * as Stories from './Rating.stories';
|
|
|
11
11
|
|
|
12
12
|
# Rating
|
|
13
13
|
|
|
14
|
-
Use Rating to collect a
|
|
14
|
+
Use Rating to collect a score with an optional descriptive label. Supports star and emoji variants.
|
|
15
15
|
|
|
16
16
|
- [Playground](#playground)
|
|
17
17
|
- [Usage](#usage)
|
|
18
18
|
- [Props](#props)
|
|
19
19
|
- [Examples](#examples)
|
|
20
|
+
- [Emoji variant](#emoji-variant)
|
|
20
21
|
- [Accessibility](#accessibility)
|
|
21
22
|
|
|
22
23
|
## Playground
|
|
@@ -52,10 +53,12 @@ const MyComponent = () => {
|
|
|
52
53
|
| -------------- | -------------------------------------- | ------------------------------------------- | ----------- |
|
|
53
54
|
| `value` | `0 \| 1 \| 2 \| 3 \| 4 \| 5` | Current rating value. | `0` |
|
|
54
55
|
| `defaultValue` | `0 \| 1 \| 2 \| 3 \| 4 \| 5` | Initial rating value when uncontrolled. | `0` |
|
|
55
|
-
| `onChange` | `(value: RatingValue) => void` | Called when a
|
|
56
|
+
| `onChange` | `(value: RatingValue) => void` | Called when a rating is selected. | `undefined` |
|
|
56
57
|
| `disabled` | `boolean` | Disables the rating input. | `false` |
|
|
57
58
|
| `labels` | `Partial<Record<RatingValue, string>>` | Override labels for specific rating values. | `undefined` |
|
|
58
|
-
| `
|
|
59
|
+
| `rangeLabels` | `{ low: string; high: string }` | Override the low and high end labels shown below the emoji variant. | `{ low: 'Very dissatisfied', high: 'Very satisfied' }` |
|
|
60
|
+
| `hideLabel` | `boolean` | Hide the label text below the rating. | `false` |
|
|
61
|
+
| `variant` | `'stars' \| 'emojis'` | Visual variant for the rating indicators. | `'stars'` |
|
|
59
62
|
|
|
60
63
|
## Examples
|
|
61
64
|
|
|
@@ -171,8 +174,44 @@ import { Rating } from '@utilitywarehouse/hearth-react-native';
|
|
|
171
174
|
const MyComponent = () => <Rating value={4} disabled />;
|
|
172
175
|
```
|
|
173
176
|
|
|
177
|
+
### Emoji variant
|
|
178
|
+
|
|
179
|
+
Use `variant="emojis"` to show emoji faces instead of stars. Labels default to "Very dissatisfied" and "Very satisfied" at the extremes. Override them with `rangeLabels={{ low: 'Custom low', high: 'Custom high' }}`.
|
|
180
|
+
|
|
181
|
+
<UsageWrap>
|
|
182
|
+
<Center>
|
|
183
|
+
<Box>
|
|
184
|
+
<Rating value={4} variant="emojis" />
|
|
185
|
+
</Box>
|
|
186
|
+
</Center>
|
|
187
|
+
</UsageWrap>
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
import { Rating } from '@utilitywarehouse/hearth-react-native';
|
|
191
|
+
|
|
192
|
+
const MyComponent = () => <Rating value={4} variant="emojis" />;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Emoji variant (no selection)
|
|
196
|
+
|
|
197
|
+
When no emoji is selected, all emojis display at normal size with full colour.
|
|
198
|
+
|
|
199
|
+
<UsageWrap>
|
|
200
|
+
<Center>
|
|
201
|
+
<Box>
|
|
202
|
+
<Rating value={0} variant="emojis" />
|
|
203
|
+
</Box>
|
|
204
|
+
</Center>
|
|
205
|
+
</UsageWrap>
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import { Rating } from '@utilitywarehouse/hearth-react-native';
|
|
209
|
+
|
|
210
|
+
const MyComponent = () => <Rating value={0} variant="emojis" />;
|
|
211
|
+
```
|
|
212
|
+
|
|
174
213
|
## Accessibility
|
|
175
214
|
|
|
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.
|
|
215
|
+
- Rating uses a `radiogroup` container with `radio` items for each star or emoji.
|
|
216
|
+
- Each star announces a descriptive label (e.g., "Rate Okay"). Each emoji announces its sentiment (e.g., "Rate Neutral"). Override labels with the `labels` prop to match your content.
|
|
178
217
|
- Provide `accessibilityLabel` when the default label text is not sufficient for your screen reader context.
|
|
@@ -18,3 +18,22 @@ figma.connect(
|
|
|
18
18
|
example: props => <Rating value={props.value} />,
|
|
19
19
|
}
|
|
20
20
|
);
|
|
21
|
+
|
|
22
|
+
figma.connect(
|
|
23
|
+
Rating,
|
|
24
|
+
'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=10620-4185',
|
|
25
|
+
{
|
|
26
|
+
variant: { Variant: 'Emojis' },
|
|
27
|
+
props: {
|
|
28
|
+
value: figma.enum('Rating', {
|
|
29
|
+
'0 Star': 0,
|
|
30
|
+
'1 Star': 1,
|
|
31
|
+
'2 Star': 2,
|
|
32
|
+
'3 Star': 3,
|
|
33
|
+
'4 Star': 4,
|
|
34
|
+
'5 Star': 5,
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
example: props => <Rating value={props.value} variant="emojis" />,
|
|
38
|
+
}
|
|
39
|
+
);
|
|
@@ -4,18 +4,24 @@ export type RatingValue = 0 | 1 | 2 | 3 | 4 | 5;
|
|
|
4
4
|
|
|
5
5
|
export type RatingLabels = Partial<Record<RatingValue, string>>;
|
|
6
6
|
|
|
7
|
+
export type RatingVariant = 'stars' | 'emojis';
|
|
8
|
+
|
|
7
9
|
export interface RatingProps extends Omit<ViewProps, 'children'> {
|
|
10
|
+
/** Visual variant for the rating indicators. */
|
|
11
|
+
variant?: RatingVariant;
|
|
8
12
|
/** Current rating value. */
|
|
9
13
|
value?: RatingValue;
|
|
10
14
|
/** Initial rating value when uncontrolled. */
|
|
11
15
|
defaultValue?: RatingValue;
|
|
12
|
-
/** Called when a
|
|
16
|
+
/** Called when a rating is selected. */
|
|
13
17
|
onChange?: (value: RatingValue) => void;
|
|
14
18
|
/** Disables the rating input. */
|
|
15
19
|
disabled?: boolean;
|
|
16
20
|
/** Override labels for specific rating values. */
|
|
17
21
|
labels?: RatingLabels;
|
|
18
|
-
/**
|
|
22
|
+
/** Override the low and high end labels shown below the emoji variant. */
|
|
23
|
+
rangeLabels?: { low: string; high: string };
|
|
24
|
+
/** Hide the label text below the rating. */
|
|
19
25
|
hideLabel?: boolean;
|
|
20
26
|
}
|
|
21
27
|
|
|
@@ -23,17 +23,25 @@ const meta = {
|
|
|
23
23
|
labels: {
|
|
24
24
|
control: 'object',
|
|
25
25
|
},
|
|
26
|
+
rangeLabels: {
|
|
27
|
+
control: 'object',
|
|
28
|
+
},
|
|
26
29
|
hideLabel: {
|
|
27
30
|
control: 'boolean',
|
|
28
31
|
},
|
|
29
32
|
disabled: {
|
|
30
33
|
control: 'boolean',
|
|
31
34
|
},
|
|
35
|
+
variant: {
|
|
36
|
+
options: ['stars', 'emojis'],
|
|
37
|
+
control: 'radio',
|
|
38
|
+
},
|
|
32
39
|
},
|
|
33
40
|
args: {
|
|
34
41
|
value: 3,
|
|
35
42
|
hideLabel: false,
|
|
36
43
|
disabled: false,
|
|
44
|
+
variant: 'stars',
|
|
37
45
|
},
|
|
38
46
|
} satisfies Meta<typeof Rating>;
|
|
39
47
|
|
|
@@ -90,6 +98,15 @@ export const Variants: Story = {
|
|
|
90
98
|
<VariantTitle title="Disabled">
|
|
91
99
|
<Rating value={5} disabled />
|
|
92
100
|
</VariantTitle>
|
|
101
|
+
<VariantTitle title="Emojis">
|
|
102
|
+
<Rating value={4} variant="emojis" />
|
|
103
|
+
</VariantTitle>
|
|
104
|
+
<VariantTitle title="Emojis (No Selection)">
|
|
105
|
+
<Rating value={0} variant="emojis" />
|
|
106
|
+
</VariantTitle>
|
|
107
|
+
<VariantTitle title="Emojis (Disabled)">
|
|
108
|
+
<Rating value={3} variant="emojis" disabled />
|
|
109
|
+
</VariantTitle>
|
|
93
110
|
</Box>
|
|
94
111
|
),
|
|
95
112
|
};
|
|
@@ -4,13 +4,23 @@ import { StyleSheet } from 'react-native-unistyles';
|
|
|
4
4
|
import { BodyText } from '../BodyText';
|
|
5
5
|
import type RatingProps from './Rating.props';
|
|
6
6
|
import type { RatingLabels, RatingValue } from './Rating.props';
|
|
7
|
+
import { getEmojiSvg } from './RatingEmoji';
|
|
7
8
|
import RatingStarEmpty from './RatingStarEmpty';
|
|
8
9
|
import RatingStarFilled from './RatingStarFilled';
|
|
10
|
+
import { EMOJI_LIST } from './Rating.utils';
|
|
9
11
|
|
|
10
12
|
const MAX_RATING: RatingValue = 5;
|
|
11
13
|
const STAR_WIDTH = 32;
|
|
12
14
|
const STAR_HEIGHT = 30;
|
|
13
15
|
const STAR_CONTAINER_SIZE = 40;
|
|
16
|
+
const EMOJI_SIZE_DEFAULT = 32;
|
|
17
|
+
const EMOJI_SIZE_SELECTED = 40;
|
|
18
|
+
const EMOJI_CONTAINER_SIZE = 44;
|
|
19
|
+
|
|
20
|
+
const DEFAULT_RANGE_LABELS = {
|
|
21
|
+
low: EMOJI_LIST[0].accessibilityLabel,
|
|
22
|
+
high: EMOJI_LIST[EMOJI_LIST.length - 1].accessibilityLabel,
|
|
23
|
+
};
|
|
14
24
|
|
|
15
25
|
const DEFAULT_LABELS: Record<RatingValue, string> = {
|
|
16
26
|
0: 'Select a rating',
|
|
@@ -25,11 +35,13 @@ const clampRating = (value: number) =>
|
|
|
25
35
|
Math.min(MAX_RATING, Math.max(0, Math.round(value))) as RatingValue;
|
|
26
36
|
|
|
27
37
|
const Rating = ({
|
|
38
|
+
variant = 'stars',
|
|
28
39
|
value,
|
|
29
40
|
defaultValue = 0,
|
|
30
41
|
onChange,
|
|
31
42
|
disabled = false,
|
|
32
43
|
labels,
|
|
44
|
+
rangeLabels = DEFAULT_RANGE_LABELS,
|
|
33
45
|
hideLabel = false,
|
|
34
46
|
style,
|
|
35
47
|
accessibilityLabel,
|
|
@@ -65,40 +77,74 @@ const Rating = ({
|
|
|
65
77
|
|
|
66
78
|
styles.useVariants({ disabled });
|
|
67
79
|
|
|
80
|
+
const isEmojis = variant === 'emojis';
|
|
81
|
+
const hasSelection = resolvedValue > 0;
|
|
82
|
+
|
|
83
|
+
const startLabel = rangeLabels.low;
|
|
84
|
+
const endLabel = rangeLabels.high;
|
|
85
|
+
|
|
68
86
|
return (
|
|
69
87
|
<View
|
|
70
88
|
{...props}
|
|
71
89
|
accessibilityRole="radiogroup"
|
|
72
90
|
accessibilityState={{ disabled }}
|
|
73
|
-
accessibilityLabel={accessibilityLabel ?? currentLabel}
|
|
91
|
+
accessibilityLabel={accessibilityLabel ?? (isEmojis ? 'Rate your experience' : currentLabel)}
|
|
74
92
|
style={[styles.container, style]}
|
|
75
93
|
>
|
|
76
|
-
<View style={styles.
|
|
77
|
-
{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
)
|
|
99
|
-
|
|
94
|
+
<View style={styles.items}>
|
|
95
|
+
{isEmojis
|
|
96
|
+
? EMOJI_LIST.map(entry => {
|
|
97
|
+
const isSelected = resolvedValue === entry.value;
|
|
98
|
+
const size = isSelected ? EMOJI_SIZE_SELECTED : EMOJI_SIZE_DEFAULT;
|
|
99
|
+
const EmojiSvg = getEmojiSvg(entry.value, hasSelection && !isSelected);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Pressable
|
|
103
|
+
key={entry.value}
|
|
104
|
+
accessibilityRole="radio"
|
|
105
|
+
accessibilityState={{ selected: isSelected, disabled }}
|
|
106
|
+
accessibilityLabel={`Rate ${entry.accessibilityLabel}`}
|
|
107
|
+
disabled={disabled}
|
|
108
|
+
hitSlop={8}
|
|
109
|
+
onPress={() => handlePress(entry.value)}
|
|
110
|
+
style={styles.emojiItem}
|
|
111
|
+
>
|
|
112
|
+
<EmojiSvg width={size} height={size} />
|
|
113
|
+
</Pressable>
|
|
114
|
+
);
|
|
115
|
+
})
|
|
116
|
+
: ([1, 2, 3, 4, 5] as RatingValue[]).map(starValue => {
|
|
117
|
+
const isFilled = starValue <= resolvedValue;
|
|
118
|
+
const starLabel = resolvedLabels[starValue] ?? DEFAULT_LABELS[starValue];
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<Pressable
|
|
122
|
+
key={starValue}
|
|
123
|
+
accessibilityRole="radio"
|
|
124
|
+
accessibilityState={{ selected: resolvedValue === starValue, disabled }}
|
|
125
|
+
accessibilityLabel={`Rate ${starLabel}`}
|
|
126
|
+
disabled={disabled}
|
|
127
|
+
hitSlop={8}
|
|
128
|
+
onPress={() => handlePress(starValue)}
|
|
129
|
+
style={styles.starItem}
|
|
130
|
+
>
|
|
131
|
+
{isFilled ? (
|
|
132
|
+
<RatingStarFilled width={STAR_WIDTH} height={STAR_HEIGHT} />
|
|
133
|
+
) : (
|
|
134
|
+
<RatingStarEmpty width={STAR_WIDTH} height={STAR_HEIGHT} />
|
|
135
|
+
)}
|
|
136
|
+
</Pressable>
|
|
137
|
+
);
|
|
138
|
+
})}
|
|
100
139
|
</View>
|
|
101
|
-
{
|
|
140
|
+
{isEmojis ? (
|
|
141
|
+
!hideLabel ? (
|
|
142
|
+
<View style={styles.emojiLabels}>
|
|
143
|
+
<BodyText size="md" color="secondary">{startLabel}</BodyText>
|
|
144
|
+
<BodyText size="md" color="secondary">{endLabel}</BodyText>
|
|
145
|
+
</View>
|
|
146
|
+
) : null
|
|
147
|
+
) : !hideLabel ? (
|
|
102
148
|
<BodyText size="md" color={labelColor} style={styles.label}>
|
|
103
149
|
{currentLabel}
|
|
104
150
|
</BodyText>
|
|
@@ -121,20 +167,32 @@ const styles = StyleSheet.create(theme => ({
|
|
|
121
167
|
},
|
|
122
168
|
},
|
|
123
169
|
},
|
|
124
|
-
|
|
170
|
+
items: {
|
|
125
171
|
flexDirection: 'row',
|
|
126
172
|
gap: theme.components.rating.gap,
|
|
127
173
|
},
|
|
128
|
-
|
|
174
|
+
starItem: {
|
|
129
175
|
width: STAR_CONTAINER_SIZE,
|
|
130
176
|
height: STAR_CONTAINER_SIZE,
|
|
131
177
|
alignItems: 'center',
|
|
132
178
|
justifyContent: 'center',
|
|
133
179
|
padding: theme.components.rating.borderWidth,
|
|
134
180
|
},
|
|
181
|
+
emojiItem: {
|
|
182
|
+
width: EMOJI_CONTAINER_SIZE,
|
|
183
|
+
height: EMOJI_CONTAINER_SIZE,
|
|
184
|
+
alignItems: 'center',
|
|
185
|
+
justifyContent: 'center',
|
|
186
|
+
padding: theme.components.rating.borderWidth,
|
|
187
|
+
},
|
|
135
188
|
label: {
|
|
136
189
|
textAlign: 'center',
|
|
137
190
|
},
|
|
191
|
+
emojiLabels: {
|
|
192
|
+
flexDirection: 'row',
|
|
193
|
+
justifyContent: 'space-between',
|
|
194
|
+
width: '100%',
|
|
195
|
+
},
|
|
138
196
|
}));
|
|
139
197
|
|
|
140
198
|
export default Rating;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { EMOJI_LIST } from './Rating.utils';
|
|
3
|
+
|
|
4
|
+
describe('EMOJI_LIST', () => {
|
|
5
|
+
it('has exactly 5 emojis mapped to values 1-5', () => {
|
|
6
|
+
expect(EMOJI_LIST).toHaveLength(5);
|
|
7
|
+
expect(EMOJI_LIST.map(e => e.value)).toEqual([1, 2, 3, 4, 5]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('each entry has an accessibility label', () => {
|
|
11
|
+
for (const entry of EMOJI_LIST) {
|
|
12
|
+
expect(entry.accessibilityLabel).toBeTruthy();
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RatingValue } from './Rating.props';
|
|
2
|
+
|
|
3
|
+
export interface EmojiEntry {
|
|
4
|
+
value: Exclude<RatingValue, 0>;
|
|
5
|
+
accessibilityLabel: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const EMOJI_LIST: EmojiEntry[] = [
|
|
9
|
+
{ value: 1, accessibilityLabel: 'Very dissatisfied' },
|
|
10
|
+
{ value: 2, accessibilityLabel: 'Dissatisfied' },
|
|
11
|
+
{ value: 3, accessibilityLabel: 'Neutral' },
|
|
12
|
+
{ value: 4, accessibilityLabel: 'Satisfied' },
|
|
13
|
+
{ value: 5, accessibilityLabel: 'Very satisfied' },
|
|
14
|
+
];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import BeamingFace from '@utilitywarehouse/hearth-svg-assets/lib/beaming-face.svg';
|
|
2
|
+
import BeamingFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/beaming-face-grey.svg';
|
|
3
|
+
import DissapointedFace from '@utilitywarehouse/hearth-svg-assets/lib/dissapointed-face.svg';
|
|
4
|
+
import DissapointedFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/dissapointed-face-grey.svg';
|
|
5
|
+
import FrowningFace from '@utilitywarehouse/hearth-svg-assets/lib/frowning-face.svg';
|
|
6
|
+
import FrowningFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/frowning-face-grey.svg';
|
|
7
|
+
import NeutralFace from '@utilitywarehouse/hearth-svg-assets/lib/neutral-face.svg';
|
|
8
|
+
import NeutralFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/neutral-face-grey.svg';
|
|
9
|
+
import SlightlySmilingFace from '@utilitywarehouse/hearth-svg-assets/lib/slightly-smiling-face.svg';
|
|
10
|
+
import SlightlySmilingFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/slightly-smiling-face-grey.svg';
|
|
11
|
+
import type { FC } from 'react';
|
|
12
|
+
import type { SvgProps } from 'react-native-svg';
|
|
13
|
+
|
|
14
|
+
const EMOJI_ASSETS: Record<1 | 2 | 3 | 4 | 5, { color: FC<SvgProps>; grey: FC<SvgProps> }> = {
|
|
15
|
+
1: { color: DissapointedFace, grey: DissapointedFaceGrey },
|
|
16
|
+
2: { color: FrowningFace, grey: FrowningFaceGrey },
|
|
17
|
+
3: { color: NeutralFace, grey: NeutralFaceGrey },
|
|
18
|
+
4: { color: SlightlySmilingFace, grey: SlightlySmilingFaceGrey },
|
|
19
|
+
5: { color: BeamingFace, grey: BeamingFaceGrey },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const getEmojiSvg = (
|
|
23
|
+
value: 1 | 2 | 3 | 4 | 5,
|
|
24
|
+
grayscale: boolean
|
|
25
|
+
): FC<SvgProps> => {
|
|
26
|
+
const assets = EMOJI_ASSETS[value];
|
|
27
|
+
return grayscale ? assets.grey : assets.color;
|
|
28
|
+
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as Rating } from './Rating';
|
|
2
|
-
export type { RatingLabels, RatingProps, RatingValue } from './Rating.props';
|
|
2
|
+
export type { RatingLabels, RatingProps, RatingValue, RatingVariant } from './Rating.props';
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
+
import { createPressable } from '@gluestack-ui/pressable';
|
|
1
2
|
import { useCallback, useRef } from 'react';
|
|
2
3
|
import { Platform, Pressable, View } from 'react-native';
|
|
3
4
|
import { StyleSheet } from 'react-native-unistyles';
|
|
5
|
+
import { BodyText } from '../BodyText';
|
|
4
6
|
import { Icon } from '../Icon';
|
|
5
7
|
import type TabProps from './Tab.props';
|
|
6
8
|
import { useTabsContext } from './Tabs.context';
|
|
7
9
|
|
|
8
|
-
import { createPressable } from '@gluestack-ui/pressable';
|
|
9
|
-
import { BodyText } from '../BodyText';
|
|
10
|
-
|
|
11
10
|
const Tab = ({
|
|
12
11
|
value,
|
|
13
12
|
children,
|
package/src/vite-env.d.ts
CHANGED
|
@@ -10,3 +10,10 @@ declare module '*.svg' {
|
|
|
10
10
|
const content: React.FC<SvgProps>;
|
|
11
11
|
export default content;
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
declare module '@utilitywarehouse/hearth-svg-assets/lib/*.svg' {
|
|
15
|
+
import React from 'react';
|
|
16
|
+
import { SvgProps } from 'react-native-svg';
|
|
17
|
+
const content: React.FC<SvgProps>;
|
|
18
|
+
export default content;
|
|
19
|
+
}
|
package/tsconfig.json
CHANGED