diff --git a/.changeset/mean-monkeys-look.md b/.changeset/mean-monkeys-look.md new file mode 100644 index 00000000000..97cf63b747d --- /dev/null +++ b/.changeset/mean-monkeys-look.md @@ -0,0 +1,6 @@ +--- +"@talend/design-system": minor +"@talend/react-components": minor +--- + +feat(DGT-342): Moved QualityBar and RatioBar components to the Design System and use those components on @talend/react-components diff --git a/packages/components/src/QualityBar/QualityBar.component.tsx b/packages/components/src/QualityBar/QualityBar.component.tsx index f165136252d..28839a44db8 100644 --- a/packages/components/src/QualityBar/QualityBar.component.tsx +++ b/packages/components/src/QualityBar/QualityBar.component.tsx @@ -1,100 +1,72 @@ -import type { MouseEvent } from 'react'; +import { useTranslation } from 'react-i18next'; -import { EnrichedQualityType, QualityBarPercentages, QualityCommonProps } from './QualityBar.types'; -import { QualityBarRatioBars } from './QualityBarRatioBars.component'; -import { SplitQualityBar } from './SplitQualityBar.component'; +import { QualityBar as QualityBarDS, type QualityCommonProps } from '@talend/design-system'; + +import I18N_DOMAIN_COMPONENTS from '../constants'; export type QualityBarProps = QualityCommonProps & { - disabled?: boolean; - placeholder?: number; digits?: number; split?: boolean; - onClick?: (e: MouseEvent, data: { type: EnrichedQualityType }) => void; - getDataFeature?: (type: string) => string; }; -/** - * This function round up the percentages to make it to 100% - * based on https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100#answer-13483486 - * @param {number} invalidRaw number of invalid raw - * @param {number} emptyRaw number of empty raw - * @param {number} validRaw number of valid raw - * @param {number} naRaw number of not applicable raw - * @param {number} digits number of digits we want to keep - */ -function getQualityPercentagesRounded( - digits: number, - invalid = 0, - empty = 0, - valid = 0, - na = 0, - placeholder = 0, -): Required { - const output: Required = { - empty: 0, - invalid: 0, - na: 0, - placeholder: 0, - valid: 0, - }; - - let sumValues = 0; - let sumRounded = 0; - const digitMultiplier = Math.pow(10, digits); - const multiplier = 100 * digitMultiplier; - - const total = invalid + empty + valid + na + placeholder; - - sumValues = (invalid * multiplier) / total; - output.invalid = Math.round(sumValues - sumRounded) / digitMultiplier; - sumRounded = Math.round(sumValues); - - sumValues += (empty * multiplier) / total; - output.empty = Math.round(sumValues - sumRounded) / digitMultiplier; - sumRounded = Math.round(sumValues); - - sumValues += (valid * multiplier) / total; - output.valid = Math.round(sumValues - sumRounded) / digitMultiplier; - sumRounded = Math.round(sumValues); - - sumValues += (na * multiplier) / total; - output.na = Math.round(sumValues - sumRounded) / digitMultiplier; - - sumValues += (placeholder * multiplier) / total; - output.placeholder = Math.round(sumValues - sumRounded) / digitMultiplier; - - return output; -} - -export function QualityBar({ +export const QualityBar = ({ valid, invalid, empty, na, placeholder, digits = 1, - split = false, ...rest -}: QualityBarProps) { - const percentages = getQualityPercentagesRounded(digits, invalid, empty, valid, na, placeholder); - return split ? ( - - ) : ( - { + const { t } = useTranslation(I18N_DOMAIN_COMPONENTS); + + const percentages = QualityBarDS.getQualityPercentagesRounded( + digits, + invalid, + empty, + valid, + na, + placeholder, + ); + + return ( + ); -} +}; diff --git a/packages/components/src/QualityBar/QualityBar.stories.tsx b/packages/components/src/QualityBar/QualityBar.stories.tsx index 10b48c5dde4..cbbd8240282 100644 --- a/packages/components/src/QualityBar/QualityBar.stories.tsx +++ b/packages/components/src/QualityBar/QualityBar.stories.tsx @@ -1,4 +1,5 @@ import { action } from '@storybook/addon-actions'; + import { QualityBar } from './QualityBar.component'; export default { diff --git a/packages/components/src/QualityBar/QualityBar.types.ts b/packages/components/src/QualityBar/QualityBar.types.ts deleted file mode 100644 index 4136204b7c2..00000000000 --- a/packages/components/src/QualityBar/QualityBar.types.ts +++ /dev/null @@ -1,19 +0,0 @@ -export enum QualityType { - VALID = 'valid', - INVALID = 'invalid', - EMPTY = 'empty', - NA = 'na', -} - -export type EnrichedQualityType = QualityType | 'placeholder'; - -export type QualityCommonProps = { - valid?: number; - invalid?: number; - empty?: number; - na?: number; -}; - -export type QualityBarPercentages = Required & { - placeholder: number; -}; diff --git a/packages/components/src/QualityBar/QualityRatioBar.component.tsx b/packages/components/src/QualityBar/QualityRatioBar.component.tsx deleted file mode 100644 index fe3b5aa5b74..00000000000 --- a/packages/components/src/QualityBar/QualityRatioBar.component.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import type { MouseEvent } from 'react'; -import { useTranslation } from 'react-i18next'; - -import I18N_DOMAIN_COMPONENTS from '../constants'; -import { RatioBarLine } from '../RatioBar'; -import { getTheme } from '../theme'; -import { EnrichedQualityType, QualityType } from './QualityBar.types'; - -import qualityBarTheme from './QualityRatioBar.module.scss'; - -const theme = getTheme(qualityBarTheme); - -/** - * formatNumber - format a number with a space for the thousand separator - * - * @param {number} value number to format - * @return {string} formated number - * @example - * formatNumber(1200); // return 1 200 - */ -function formatNumber(value?: number) { - if (!value) { - return ''; - } - const parts = value.toString().split('.'); - parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ' '); - return parts.join('.'); -} - -type SpecificQualityBarProps = { - percentage: number; - value: number; - getDataFeature?: (type: EnrichedQualityType) => string; - onClick?: (e: MouseEvent, data: { type: EnrichedQualityType }) => void; -}; - -type QualityRatioBarProps = SpecificQualityBarProps & { - type: EnrichedQualityType; - tooltipLabel?: string; -}; - -function QualityRatioBar({ onClick, type, getDataFeature, ...props }: QualityRatioBarProps) { - const specificProps = { - className: theme('quality-ratio-bar', `quality-ratio-bar--${type}`), - onClick: onClick ? (e: MouseEvent) => onClick(e, { type }) : null, - dataFeature: getDataFeature ? getDataFeature(type) : null, - dataTestId: `quality-bar-${type}`, - }; - - return ; -} - -export function QualityInvalidLine({ percentage, value, ...rest }: SpecificQualityBarProps) { - const { t } = useTranslation(I18N_DOMAIN_COMPONENTS); - - return ( - - ); -} - -export function QualityValidLine({ percentage, value, ...rest }: SpecificQualityBarProps) { - const { t } = useTranslation(I18N_DOMAIN_COMPONENTS); - - return ( - - ); -} - -export function QualityEmptyLine({ percentage, value, ...rest }: SpecificQualityBarProps) { - const { t } = useTranslation(I18N_DOMAIN_COMPONENTS); - - return ( - - ); -} - -export function QualityNotApplicableLine({ percentage, value, ...rest }: SpecificQualityBarProps) { - const { t } = useTranslation(I18N_DOMAIN_COMPONENTS); - - return ( - - ); -} - -export function QualityPlaceholderLine(props: Omit) { - return ; -} diff --git a/packages/components/src/QualityBar/index.ts b/packages/components/src/QualityBar/index.ts index 115aa8fe5d8..8d67359151d 100644 --- a/packages/components/src/QualityBar/index.ts +++ b/packages/components/src/QualityBar/index.ts @@ -1,7 +1,5 @@ -import { QualityBar } from './QualityBar.component'; -import { QualityType } from './QualityBar.types'; +import { QualityType } from '@talend/design-system'; -// @ts-ignore -QualityBar.QualityType = QualityType; +import { QualityBar } from './QualityBar.component'; export { QualityBar, QualityType }; diff --git a/packages/components/src/RatioBar/RatioBar.component.js b/packages/components/src/RatioBar/RatioBar.component.js deleted file mode 100644 index 064cdd0a082..00000000000 --- a/packages/components/src/RatioBar/RatioBar.component.js +++ /dev/null @@ -1,70 +0,0 @@ -import PropTypes from 'prop-types'; -import { Trans } from 'react-i18next'; -import { RatioBarComposition } from './RatioBarComposition.component'; -import { EmptyLine, FilledLine, ErrorLine } from './RatioBarLines.component'; -import { getTheme } from '../theme'; -import ratioBarTheme from './RatioBar.module.scss'; - -const theme = getTheme(ratioBarTheme); - -function getFilledValues(amount, total) { - if (!amount || amount < 0) { - return { percentage: 0, amount: 0 }; - } - - if (amount > total) { - return { percentage: 100, amount }; - } - - return { percentage: (amount / total) * 100, amount }; -} - -function getEmptyValues(amount, total) { - if (!amount || amount < 0) { - return { percentage: 100, amount: total }; - } - if (amount > total) { - return { percentage: 0, amount: 0 }; - } - - return { percentage: (1 - amount / total) * 100, amount: total - amount }; -} - -function getLabel(amount, errors, total) { - if (!amount && amount !== 0) { - return ( -
- - N/A - -
- ); - } - return ( -
- {amount + errors}/{total} -
- ); -} - -export function RatioBar({ amount, total, errors = 0, hideLabel = false }) { - const filled = getFilledValues(amount, total); - const error = getFilledValues(errors, total); - const empty = getEmptyValues(amount + errors, total); - - return ( - - - - - {!hideLabel && getLabel(amount, errors, total)} - - ); -} - -RatioBar.propTypes = { - amount: PropTypes.number, - errors: PropTypes.number, - total: PropTypes.number.isRequired, - hideLabel: PropTypes.bool, -}; diff --git a/packages/components/src/RatioBar/RatioBar.component.test.js b/packages/components/src/RatioBar/RatioBar.component.test.js deleted file mode 100644 index fbf3a42b20e..00000000000 --- a/packages/components/src/RatioBar/RatioBar.component.test.js +++ /dev/null @@ -1,112 +0,0 @@ -import { render } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -import { RatioBar } from './RatioBar.component'; - -// as this is SVG we need to rely on custom selector -function getCounter() { - return document.querySelector('.tc-ratio-bar-counter'); -} -function getRatioBar() { - return document.querySelector('.tc-ratio-bar'); -} -function getRatioBarLine() { - return document.querySelector('.tc-ratio-bar-line'); -} -function getEmptyLine() { - return document.querySelector('.tc-ratio-bar-line-empty'); -} -function getErrorLine() { - return document.querySelector('.tc-ratio-bar-line-error'); -} -describe('RatioBar', () => { - describe('RatioBar component', () => { - it('should render an empty bar', () => { - // given - const props = { - amount: 0, - total: 12, - }; - // when - const { container } = render(); - // then - expect(container.firstChild).toMatchSnapshot(); - expect(getCounter()).toHaveTextContent('0/12'); - expect(getRatioBar()).toBeVisible(); - expect(getEmptyLine()).toBeVisible(); - expect(getEmptyLine()).toHaveStyle('flex-basis: 100%'); - }); - - it('should render a not applicable chart', () => { - // given - const props = { - amount: null, - total: 12, - }; - // when - render(); - // then - expect(getCounter()).toHaveTextContent('N/A'); - }); - - it('should render an full sized chart', async () => { - const user = userEvent.setup(); - - // given - const props = { - amount: 12, - total: 12, - }; - // when - render(); - // then - await user.tab(); - expect(getCounter()).toHaveTextContent('12/12'); - expect(getRatioBarLine()).toHaveStyle('flex-basis: 100%'); - }); - - it('should render a classic ratio bar', () => { - // given - const props = { - amount: 5, - total: 12, - }; - // when - render(); - // then - expect(getRatioBarLine()).toHaveStyle('flex-basis: 41.66666666666667%'); - expect(getEmptyLine()).toHaveStyle('flex-basis: 58.33333333333333%'); - expect(getCounter()).toHaveTextContent('5/12'); - }); - }); - - it('should render a classic ratio bar with errors', () => { - // given - const props = { - amount: 5, - total: 12, - errors: 2, - }; - // when - render(); - // then - expect(getRatioBarLine()).toHaveStyle('flex-basis: 41.66666666666667%'); - expect(getEmptyLine()).toHaveStyle('flex-basis: 41.666666666666664%'); - expect(getErrorLine()).toHaveStyle('flex-basis: 16.666666666666664%'); - expect(getCounter()).toHaveTextContent('7/12'); - }); - - it('should render a classic ratio bar without label', () => { - // given - const props = { - amount: 5, - total: 12, - }; - // when - render(); - // then - expect(getRatioBarLine()).toHaveStyle('flex-basis: 41.66666666666667%'); - expect(getEmptyLine()).toHaveStyle('flex-basis: 58.33333333333333%'); - expect(getCounter()).not.toBeInTheDocument(); - }); -}); diff --git a/packages/components/src/RatioBar/RatioBar.component.tsx b/packages/components/src/RatioBar/RatioBar.component.tsx new file mode 100644 index 00000000000..edf21018c89 --- /dev/null +++ b/packages/components/src/RatioBar/RatioBar.component.tsx @@ -0,0 +1,34 @@ +import { ReactNode } from 'react'; +import { Trans } from 'react-i18next'; + +import { RatioBar as RatioBarDS } from '@talend/design-system'; + +type RatioBarProps = { + amount?: number; + errors?: number; + hideLabel?: boolean; + notApplicableLabel?: ReactNode; + total: number; +}; + +export const RatioBar = ({ + amount, + total, + errors = 0, + hideLabel = false, + notApplicableLabel, +}: RatioBarProps) => ( + + N/A + + ) + } + total={total} + /> +); diff --git a/packages/components/src/RatioBar/RatioBar.stories.js b/packages/components/src/RatioBar/RatioBar.stories.tsx similarity index 93% rename from packages/components/src/RatioBar/RatioBar.stories.js rename to packages/components/src/RatioBar/RatioBar.stories.tsx index 03d82f60828..7c9465cf22e 100644 --- a/packages/components/src/RatioBar/RatioBar.stories.js +++ b/packages/components/src/RatioBar/RatioBar.stories.tsx @@ -5,7 +5,7 @@ export default { }; export const _RatioBar = () => ( -
+
Ratio Bar
Not applicable amount
diff --git a/packages/components/src/RatioBar/RatioBarComposition.component.js b/packages/components/src/RatioBar/RatioBarComposition.component.js deleted file mode 100644 index 9c4168f086a..00000000000 --- a/packages/components/src/RatioBar/RatioBarComposition.component.js +++ /dev/null @@ -1,92 +0,0 @@ -import PropTypes from 'prop-types'; - -import { Tooltip } from '@talend/design-system'; - -import { getTheme } from '../theme'; - -import ratioBarTheme from './RatioBar.module.scss'; - -const theme = getTheme(ratioBarTheme); -const minPercentage = 5; - -export function RatioBarLine({ - percentage, - tooltipLabel, - className, - value, - dataFeature, - onClick, - dataTestId, -}) { - const canGrow = percentage >= minPercentage; - - if (!value || value < 0) return null; - - function onKeyDown(event) { - switch (event.key) { - case 'Enter': - onClick(event); - break; - case ' ': - case 'Spacebar': - event.preventDefault(); // prevent scroll with space - event.stopPropagation(); - onClick(event); - break; - default: - break; - } - } - - const content = ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions -
- {tooltipLabel && {tooltipLabel}} -
- ); - - if (!tooltipLabel) { - return content; - } - - return ( - - {content} - - ); -} -RatioBarLine.propTypes = { - percentage: PropTypes.number.isRequired, - value: PropTypes.number.isRequired, - tooltipLabel: PropTypes.string, - className: PropTypes.string.isRequired, - dataTestId: PropTypes.string, - dataFeature: PropTypes.string, - onClick: PropTypes.func, -}; - -export function RatioBarComposition({ children }) { - return
{children}
; -} -RatioBarComposition.propTypes = { - children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]), -}; diff --git a/packages/components/src/RatioBar/RatioBarLines.component.js b/packages/components/src/RatioBar/RatioBarLines.component.js deleted file mode 100644 index 98a10b00625..00000000000 --- a/packages/components/src/RatioBar/RatioBarLines.component.js +++ /dev/null @@ -1,44 +0,0 @@ -import PropTypes from 'prop-types'; -import { getTheme } from '../theme'; -import { RatioBarLine } from './RatioBarComposition.component'; -import ratioBarTheme from './RatioBar.module.scss'; - -const theme = getTheme(ratioBarTheme); - -const ratioBarLinePropTypes = { - value: PropTypes.number.isRequired, - percentage: PropTypes.number.isRequired, -}; - -export function FilledLine({ value, percentage }) { - return ( - - ); -} -FilledLine.propTypes = ratioBarLinePropTypes; - -export function EmptyLine({ value, percentage }) { - return ( - - ); -} -EmptyLine.propTypes = ratioBarLinePropTypes; - -export function ErrorLine({ value, percentage }) { - return ( - - ); -} -ErrorLine.propTypes = ratioBarLinePropTypes; diff --git a/packages/components/src/RatioBar/__snapshots__/RatioBar.component.test.js.snap b/packages/components/src/RatioBar/__snapshots__/RatioBar.component.test.js.snap deleted file mode 100644 index e241b4afe24..00000000000 --- a/packages/components/src/RatioBar/__snapshots__/RatioBar.component.test.js.snap +++ /dev/null @@ -1,22 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`RatioBar RatioBar component should render an empty bar 1`] = ` -
-
-
- - 0 - - / - 12 -
-
-`; diff --git a/packages/components/src/RatioBar/index.js b/packages/components/src/RatioBar/index.js deleted file mode 100644 index d74d271bb1d..00000000000 --- a/packages/components/src/RatioBar/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import { RatioBar } from './RatioBar.component'; -import { RatioBarComposition, RatioBarLine } from './RatioBarComposition.component'; - -RatioBar.Composition = RatioBarComposition; -RatioBar.Line = RatioBarLine; - -export { RatioBarComposition, RatioBarLine }; - -export default RatioBar; diff --git a/packages/components/src/RatioBar/index.ts b/packages/components/src/RatioBar/index.ts new file mode 100644 index 00000000000..cb22bebb3c2 --- /dev/null +++ b/packages/components/src/RatioBar/index.ts @@ -0,0 +1,7 @@ +import { RatioBarComposition, RatioBarLine } from '@talend/design-system'; + +import { RatioBar } from './RatioBar.component'; + +export { RatioBarComposition, RatioBarLine }; + +export default RatioBar; diff --git a/packages/components/src/VirtualizedList/CellQualityBar/CellQualityBar.test.js b/packages/components/src/VirtualizedList/CellQualityBar/CellQualityBar.test.js index 71c4610d6ea..7683b4d6461 100644 --- a/packages/components/src/VirtualizedList/CellQualityBar/CellQualityBar.test.js +++ b/packages/components/src/VirtualizedList/CellQualityBar/CellQualityBar.test.js @@ -3,6 +3,8 @@ import noop from 'lodash/noop'; import { CellQualityBar } from './CellQualityBar.component'; +jest.unmock('@talend/design-system'); + const props = { invalid: 1, empty: 2, diff --git a/packages/components/src/VirtualizedList/CellQualityBar/__snapshots__/CellQualityBar.test.js.snap b/packages/components/src/VirtualizedList/CellQualityBar/__snapshots__/CellQualityBar.test.js.snap index 8d6ccd8392a..893f6f6e947 100644 --- a/packages/components/src/VirtualizedList/CellQualityBar/__snapshots__/CellQualityBar.test.js.snap +++ b/packages/components/src/VirtualizedList/CellQualityBar/__snapshots__/CellQualityBar.test.js.snap @@ -4,88 +4,213 @@ exports[`CellQualityBar should render a valid quality bar 1`] = `
- -
- - 1 invalid value (10%) - -
-
- +
+
-
- - 2 empty value (20%) - -
- - +
+
-
- - 4 not applicable value (40%) - -
- - +
+
-
- - 3 valid value (30%) - -
- + 3 valid value (30%) + +
+
+
+
+ +
+
+ +
+
+ +
+
+
diff --git a/packages/design-system/src/components/Accordion/Primitive/CollapsiblePanelHeader.tsx b/packages/design-system/src/components/Accordion/Primitive/CollapsiblePanelHeader.tsx index d02a5359b3c..b94c8a4bfb7 100644 --- a/packages/design-system/src/components/Accordion/Primitive/CollapsiblePanelHeader.tsx +++ b/packages/design-system/src/components/Accordion/Primitive/CollapsiblePanelHeader.tsx @@ -119,11 +119,14 @@ const CollapsiblePanelHeader = forwardRef( { + actionItem.onClick?.(e); + actionItem.callback(); + }} > {actionItem.tooltip} diff --git a/packages/design-system/src/components/Accordion/Primitive/types.ts b/packages/design-system/src/components/Accordion/Primitive/types.ts index 62929e3b488..f5699d81db5 100644 --- a/packages/design-system/src/components/Accordion/Primitive/types.ts +++ b/packages/design-system/src/components/Accordion/Primitive/types.ts @@ -3,5 +3,6 @@ import { ButtonIconType } from 'src/components/ButtonIcon/variations/ButtonIcon' export type PanelHeaderAction = ButtonIconType & { icon: string; tooltip: string; + dataFeature?: string; callback: () => unknown; }; diff --git a/packages/components/src/QualityBar/QualityBar.component.test.tsx b/packages/design-system/src/components/QualityBar/QualityBar.component.test.tsx similarity index 98% rename from packages/components/src/QualityBar/QualityBar.component.test.tsx rename to packages/design-system/src/components/QualityBar/QualityBar.component.test.tsx index 3083bd304fc..9310c6bdf94 100644 --- a/packages/components/src/QualityBar/QualityBar.component.test.tsx +++ b/packages/design-system/src/components/QualityBar/QualityBar.component.test.tsx @@ -1,4 +1,6 @@ +import { describe, expect, it } from '@jest/globals'; import { render, screen } from '@testing-library/react'; + import { QualityBar, QualityBarProps } from './QualityBar.component'; describe('QualityBar', () => { diff --git a/packages/design-system/src/components/QualityBar/QualityBar.component.tsx b/packages/design-system/src/components/QualityBar/QualityBar.component.tsx new file mode 100644 index 00000000000..dba4d77b593 --- /dev/null +++ b/packages/design-system/src/components/QualityBar/QualityBar.component.tsx @@ -0,0 +1,43 @@ +import { QualityCommonProps } from './QualityBar.types'; +import { QualityBarRatioBars } from './QualityBarRatioBars.component'; +import { getQualityPercentagesRounded } from './QualityRatioBar.utils'; +import { SplitQualityBar } from './SplitQualityBar.component'; + +export type QualityBarProps = QualityCommonProps & { + digits?: number; + split?: boolean; +}; + +export const QualityBar = ({ + valid, + invalid, + empty, + na, + placeholder, + digits = 1, + split = false, + ...rest +}: QualityBarProps) => { + const percentages = getQualityPercentagesRounded(digits, invalid, empty, valid, na, placeholder); + + return split ? ( + + ) : ( + + ); +}; diff --git a/packages/design-system/src/components/QualityBar/QualityBar.stories.tsx b/packages/design-system/src/components/QualityBar/QualityBar.stories.tsx new file mode 100644 index 00000000000..6e03e8e4bcf --- /dev/null +++ b/packages/design-system/src/components/QualityBar/QualityBar.stories.tsx @@ -0,0 +1,145 @@ +import { action } from '@storybook/addon-actions'; + +import { QualityBar } from './QualityBar.component'; + +export default { + title: 'Dataviz/QualityBar', +}; + +export const _QualityBar = () => ( +
+
Quality Bar
+ +
+
Homogeneous Quality
+ + +
Very invalid
+ + +
Not applicable
+ + +
Best quality ever
+ + +
Nothing to see here
+ + +
Invalid and Empty
+ + +
Classic look
+ + +
Classic look (again yep)
+ + +
I really like the digits !
+ + +
With a placeholder
+ + +
Disabled
+ + +
Classic look with action button
+ `data-feature.${qualityType}`} + tooltipLabels={{ + empty: '3 empty values', + invalid: '2 invalid values', + valid: '88 valid values', + }} + /> +
+
+); diff --git a/packages/design-system/src/components/QualityBar/QualityBar.types.ts b/packages/design-system/src/components/QualityBar/QualityBar.types.ts new file mode 100644 index 00000000000..fccfcd62fac --- /dev/null +++ b/packages/design-system/src/components/QualityBar/QualityBar.types.ts @@ -0,0 +1,36 @@ +import type { MouseEvent } from 'react'; + +export enum QualityType { + VALID = 'valid', + INVALID = 'invalid', + EMPTY = 'empty', + NA = 'na', +} + +export type EnrichedQualityType = QualityType | 'placeholder'; + +export type QualityTypeValues = { + valid?: number; + invalid?: number; + empty?: number; + na?: number; +}; + +export type QualityBarPercentages = Required & { + placeholder: number; +}; + +export type QualityCommonProps = QualityTypeValues & { + disabled?: boolean; + getDataFeature?: (type: string) => string; + onClick?: (e: MouseEvent, data: { type: EnrichedQualityType }) => void; + placeholder?: number; + tooltipLabels?: QualityBarTooltips; +}; + +export type QualityBarTooltips = { + empty?: string; + invalid?: string; + na?: string; + valid?: string; +}; diff --git a/packages/components/src/QualityBar/QualityBarRatioBars.component.tsx b/packages/design-system/src/components/QualityBar/QualityBarRatioBars.component.tsx similarity index 77% rename from packages/components/src/QualityBar/QualityBarRatioBars.component.tsx rename to packages/design-system/src/components/QualityBar/QualityBarRatioBars.component.tsx index 20e98234bb3..097cba45456 100644 --- a/packages/components/src/QualityBar/QualityBarRatioBars.component.tsx +++ b/packages/design-system/src/components/QualityBar/QualityBarRatioBars.component.tsx @@ -1,5 +1,5 @@ -import type { MouseEvent } from 'react'; - +import { RatioBarComposition } from '../RatioBar'; +import { QualityBarPercentages, QualityCommonProps } from './QualityBar.types'; import { QualityEmptyLine, QualityInvalidLine, @@ -7,18 +7,12 @@ import { QualityPlaceholderLine, QualityValidLine, } from './QualityRatioBar.component'; -import { RatioBarComposition } from '../RatioBar'; -import { EnrichedQualityType, QualityBarPercentages, QualityCommonProps } from './QualityBar.types'; type QualityBarRatioBarsProps = QualityCommonProps & { - placeholder?: number; - disabled?: boolean; percentages: QualityBarPercentages; - onClick?: (e: MouseEvent, data: { type: EnrichedQualityType }) => void; - getDataFeature?: (type: string) => string; }; -export function QualityBarRatioBars({ +export const QualityBarRatioBars = ({ valid = 0, invalid = 0, empty = 0, @@ -28,7 +22,8 @@ export function QualityBarRatioBars({ getDataFeature, onClick, disabled, -}: QualityBarRatioBarsProps) { + tooltipLabels, +}: QualityBarRatioBarsProps) => { if (disabled) { return ( @@ -44,24 +39,28 @@ export function QualityBarRatioBars({ getDataFeature={getDataFeature} value={invalid} percentage={percentages.invalid} + tooltipLabel={tooltipLabels?.invalid} /> ); -} +}; diff --git a/packages/design-system/src/components/QualityBar/QualityRatioBar.component.tsx b/packages/design-system/src/components/QualityBar/QualityRatioBar.component.tsx new file mode 100644 index 00000000000..06a9ff931c7 --- /dev/null +++ b/packages/design-system/src/components/QualityBar/QualityRatioBar.component.tsx @@ -0,0 +1,55 @@ +import type { MouseEvent } from 'react'; + +import classNames from 'classnames'; + +import { RatioBarLine } from '../RatioBar'; +import { EnrichedQualityType, QualityType } from './QualityBar.types'; + +import styles from './QualityRatioBar.module.scss'; + +type SpecificQualityBarProps = { + percentage: number; + value: number; + getDataFeature?: (type: EnrichedQualityType) => string; + onClick?: (e: MouseEvent, data: { type: EnrichedQualityType }) => void; + tooltipLabel?: string; +}; + +type QualityRatioBarProps = SpecificQualityBarProps & { + type: EnrichedQualityType; +}; + +const QualityRatioBar = ({ onClick, type, getDataFeature, ...props }: QualityRatioBarProps) => { + const specificProps = { + className: classNames(styles['quality-ratio-bar'], styles[`quality-ratio-bar--${type}`]), + onClick: onClick ? (e: MouseEvent) => onClick(e, { type }) : null, + dataFeature: getDataFeature ? getDataFeature(type) : null, + dataTestId: `quality-bar-${type}`, + }; + + return ; +}; + +export const QualityInvalidLine = ({ percentage, value, ...rest }: SpecificQualityBarProps) => ( + +); + +export const QualityValidLine = ({ percentage, value, ...rest }: SpecificQualityBarProps) => ( + +); + +export const QualityEmptyLine = ({ percentage, value, ...rest }: SpecificQualityBarProps) => ( + +); + +export const QualityNotApplicableLine = ({ + percentage, + value, + ...rest +}: SpecificQualityBarProps) => ( + +); + +export const QualityPlaceholderLine = (props: Omit) => ( + +); diff --git a/packages/components/src/QualityBar/QualityRatioBar.module.scss b/packages/design-system/src/components/QualityBar/QualityRatioBar.module.scss similarity index 100% rename from packages/components/src/QualityBar/QualityRatioBar.module.scss rename to packages/design-system/src/components/QualityBar/QualityRatioBar.module.scss index 627c1fdc503..b9f5fa53533 100644 --- a/packages/components/src/QualityBar/QualityRatioBar.module.scss +++ b/packages/design-system/src/components/QualityBar/QualityRatioBar.module.scss @@ -4,9 +4,9 @@ $custom-quality-bar-placeholder-line-hover-height: 0.25rem; .quality-ratio-bar { - width: 100%; - height: 100%; border-radius: 1px; + height: 100%; + width: 100%; &--empty { background-color: tokens.$coral-color-charts-neutral; diff --git a/packages/design-system/src/components/QualityBar/QualityRatioBar.utils.ts b/packages/design-system/src/components/QualityBar/QualityRatioBar.utils.ts new file mode 100644 index 00000000000..ad141045f73 --- /dev/null +++ b/packages/design-system/src/components/QualityBar/QualityRatioBar.utils.ts @@ -0,0 +1,74 @@ +import { QualityBarPercentages } from './QualityBar.types'; + +/** + * formatNumber - format a number with a space for the thousand separator + * + * @param {number} value number to format + * @return {string} formated number + * @example + * formatNumber(1200); // return 1 200 + */ +export const formatNumber = (value?: number): string => { + if (!value) { + return ''; + } + + const parts = value.toString().split('.'); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ' '); + + return parts.join('.'); +}; + +/** + * This function round up the percentages to make it to 100% + * + * @param {number} digits number of digits we want to keep + * @param {number} invalid number of invalid + * @param {number} empty number of empty + * @param {number} valid number of valid + * @param {number} na number of not applicable + * @param {number} placeholder number for the placeholder + */ +export const getQualityPercentagesRounded = ( + digits: number, + invalid: number = 0, + empty: number = 0, + valid: number = 0, + na: number = 0, + placeholder = 0, +): Required => { + const output: Required = { + empty: 0, + invalid: 0, + na: 0, + placeholder: 0, + valid: 0, + }; + + let sumValues = 0; + let sumRounded = 0; + const digitMultiplier = Math.pow(10, digits); + const multiplier = 100 * digitMultiplier; + + const total = invalid + empty + valid + na + placeholder; + + sumValues = (invalid * multiplier) / total; + output.invalid = Math.round(sumValues - sumRounded) / digitMultiplier; + sumRounded = Math.round(sumValues); + + sumValues += (empty * multiplier) / total; + output.empty = Math.round(sumValues - sumRounded) / digitMultiplier; + sumRounded = Math.round(sumValues); + + sumValues += (valid * multiplier) / total; + output.valid = Math.round(sumValues - sumRounded) / digitMultiplier; + sumRounded = Math.round(sumValues); + + sumValues += (na * multiplier) / total; + output.na = Math.round(sumValues - sumRounded) / digitMultiplier; + + sumValues += (placeholder * multiplier) / total; + output.placeholder = Math.round(sumValues - sumRounded) / digitMultiplier; + + return output; +}; diff --git a/packages/components/src/QualityBar/SplitQualityBar.component.tsx b/packages/design-system/src/components/QualityBar/SplitQualityBar.component.tsx similarity index 92% rename from packages/components/src/QualityBar/SplitQualityBar.component.tsx rename to packages/design-system/src/components/QualityBar/SplitQualityBar.component.tsx index 7f4c5a9311e..b9fcdd862a7 100644 --- a/packages/components/src/QualityBar/SplitQualityBar.component.tsx +++ b/packages/design-system/src/components/QualityBar/SplitQualityBar.component.tsx @@ -1,7 +1,6 @@ import type { MouseEvent } from 'react'; -import { StackHorizontal } from '@talend/design-system'; - +import { StackHorizontal } from '../Stack'; import { EnrichedQualityType, QualityBarPercentages, @@ -19,16 +18,16 @@ type SplitQualityBarProps = QualityCommonProps & { getDataFeature?: (type: string) => string; }; -export function SplitQualityBar({ +export const SplitQualityBar = ({ + disabled, empty, getDataFeature, invalid, na, onClick, - valid, percentages, - disabled, -}: SplitQualityBarProps) { + valid, +}: SplitQualityBarProps) => { const totalValues = (empty || 0) + (invalid || 0) + (na || 0) + (valid || 0); const usedValues = { empty, invalid, na, valid }; const fwd = { getDataFeature, onClick }; @@ -56,4 +55,4 @@ export function SplitQualityBar({ })} ); -} +}; diff --git a/packages/design-system/src/components/QualityBar/SplitQualityBar.stories.tsx b/packages/design-system/src/components/QualityBar/SplitQualityBar.stories.tsx new file mode 100644 index 00000000000..1ffa6f8ee15 --- /dev/null +++ b/packages/design-system/src/components/QualityBar/SplitQualityBar.stories.tsx @@ -0,0 +1,62 @@ +import { action } from '@storybook/addon-actions'; + +import { QualityBar } from './QualityBar.component'; + +export default { + title: 'Dataviz/SplitQualityBar', +}; + +export const SplitQualityBar = () => ( +
+
Quality Bar
+ +
+
Split quality bar
+ `data-feature.${qualityType}`} + split + /> + `data-feature.${qualityType}`} + split + /> + `data-feature.${qualityType}`} + split + /> + `data-feature.${qualityType}`} + split + /> + +
Disabled
+ `data-feature.${qualityType}`} + split + /> +
+
+); diff --git a/packages/design-system/src/components/QualityBar/index.ts b/packages/design-system/src/components/QualityBar/index.ts new file mode 100644 index 00000000000..8e45c9a3c1d --- /dev/null +++ b/packages/design-system/src/components/QualityBar/index.ts @@ -0,0 +1,17 @@ +import { QualityBar as QualityBarComponent } from './QualityBar.component'; +import { QualityCommonProps, QualityType } from './QualityBar.types'; +import { formatNumber, getQualityPercentagesRounded } from './QualityRatioBar.utils'; + +export type QualityBarType = typeof QualityBarComponent & { + QualityType: typeof QualityType; + formatNumber: typeof formatNumber; + getQualityPercentagesRounded: typeof getQualityPercentagesRounded; +}; + +const QualityBar = QualityBarComponent as QualityBarType; +QualityBar.QualityType = QualityType; +QualityBar.formatNumber = formatNumber; +QualityBar.getQualityPercentagesRounded = getQualityPercentagesRounded; + +export { QualityBar, QualityType }; +export type { QualityCommonProps }; diff --git a/packages/design-system/src/components/RatioBar/RatioBar.component.test.js b/packages/design-system/src/components/RatioBar/RatioBar.component.test.js new file mode 100644 index 00000000000..3edb0e70e87 --- /dev/null +++ b/packages/design-system/src/components/RatioBar/RatioBar.component.test.js @@ -0,0 +1,98 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { RatioBar } from './RatioBar.component'; + +describe('RatioBar', () => { + describe('RatioBar component', () => { + it('should render an empty bar', () => { + // given + const props = { + amount: 0, + total: 12, + }; + // when + const { container } = render(); + // then + expect(container.firstChild).toMatchSnapshot(); + expect(screen.getByTestId('ratio-bar-counter')).toHaveTextContent('0/12'); + expect(screen.getByTestId('ratio-bar')).toBeVisible(); + expect(screen.getByTestId('ratio-bar-empty')).toBeVisible(); + expect(screen.getByTestId('ratio-bar-empty')).toHaveStyle('flex-basis: 100%'); + }); + + it('should render a not applicable chart', () => { + // given + const props = { + amount: null, + total: 12, + }; + // when + render(); + // then + expect(screen.getByTestId('ratio-bar-counter')).toHaveTextContent('N/A'); + }); + + it('should render an full sized chart', async () => { + const user = userEvent.setup(); + + // given + const props = { + amount: 12, + total: 12, + }; + // when + render(); + // then + await user.tab(); + expect(screen.getByTestId('ratio-bar-counter')).toHaveTextContent('12/12'); + expect(screen.getByTestId('ratio-bar-filled')).toHaveStyle('flex-basis: 100%'); + }); + + it('should render a classic ratio bar', () => { + // given + const props = { + amount: 5, + total: 12, + }; + // when + render(); + // then + expect(screen.getByTestId('ratio-bar-filled')).toHaveStyle('flex-basis: 41.66666666666667%'); + expect(screen.getByTestId('ratio-bar-empty')).toHaveStyle('flex-basis: 58.33333333333333%'); + expect(screen.getByTestId('ratio-bar-counter')).toHaveTextContent('5/12'); + }); + }); + + it('should render a classic ratio bar with errors', () => { + // given + const props = { + amount: 5, + total: 12, + errors: 2, + }; + // when + render(); + // then + expect(screen.getByTestId('ratio-bar-filled')).toHaveStyle('flex-basis: 41.66666666666667%'); + expect(screen.getByTestId('ratio-bar-empty')).toHaveStyle('flex-basis: 41.666666666666664%'); + expect(screen.getByTestId('ratio-bar-error')).toHaveStyle('flex-basis: 16.666666666666664%'); + expect(screen.getByTestId('ratio-bar-counter')).toHaveTextContent('7/12'); + }); + + it('should render a classic ratio bar without label', () => { + // given + const props = { + amount: 5, + total: 12, + }; + // when + render(); + screen; + // then + expect(screen.getByTestId('ratio-bar-filled')).toHaveStyle('flex-basis: 41.66666666666667%'); + expect(screen.getByTestId('ratio-bar-empty')).toHaveStyle('flex-basis: 58.33333333333333%'); + expect(screen.queryByTestId('ratio-bar-counter')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/design-system/src/components/RatioBar/RatioBar.component.tsx b/packages/design-system/src/components/RatioBar/RatioBar.component.tsx new file mode 100644 index 00000000000..c0ebea0b67d --- /dev/null +++ b/packages/design-system/src/components/RatioBar/RatioBar.component.tsx @@ -0,0 +1,84 @@ +import { ReactNode } from 'react'; + +import { RatioBarComposition } from './RatioBarComposition.component'; +import { EmptyLine, ErrorLine, FilledLine } from './RatioBarLines.component'; + +import styles from './RatioBar.module.scss'; + +const getFilledValues = (amount: number, total: number) => { + if (!amount || amount < 0) { + return { percentage: 0, amount: 0 }; + } + + if (amount > total) { + return { percentage: 100, amount }; + } + + return { percentage: (amount / total) * 100, amount }; +}; + +const getEmptyValues = (amount: number, total: number) => { + if (!amount || amount < 0) { + return { percentage: 100, amount: total }; + } + + if (amount > total) { + return { percentage: 0, amount: 0 }; + } + + return { percentage: (1 - amount / total) * 100, amount: total - amount }; +}; + +const getLabel = ( + errors: number, + total: number, + amount?: number, + notApplicableLabel?: ReactNode, +) => { + if (!amount && amount !== 0) { + return ( +
+ {notApplicableLabel || ( + <> + N/A + + )} +
+ ); + } + + return ( +
+ {amount + errors}/{total} +
+ ); +}; + +type RatioBarProps = { + amount?: number; + errors?: number; + hideLabel?: boolean; + notApplicableLabel?: ReactNode; + total: number; +}; + +export const RatioBar = ({ + amount, + errors = 0, + hideLabel = false, + notApplicableLabel, + total, +}: RatioBarProps) => { + const filled = getFilledValues(amount || 0, total); + const error = getFilledValues(errors, total); + const empty = getEmptyValues((amount || 0) + errors, total); + + return ( + + + + + {!hideLabel && getLabel(errors, total, amount, notApplicableLabel)} + + ); +}; diff --git a/packages/components/src/RatioBar/RatioBar.module.scss b/packages/design-system/src/components/RatioBar/RatioBar.module.scss similarity index 59% rename from packages/components/src/RatioBar/RatioBar.module.scss rename to packages/design-system/src/components/RatioBar/RatioBar.module.scss index 0d541494c75..351dc2dbaf4 100644 --- a/packages/components/src/RatioBar/RatioBar.module.scss +++ b/packages/design-system/src/components/RatioBar/RatioBar.module.scss @@ -1,59 +1,62 @@ @use '~@talend/bootstrap-theme/src/theme/guidelines' as *; - @use '~@talend/design-tokens/lib/tokens'; $custom-ratio-bar-height: 0.5rem; -$custom-ratio-bar-line-height: 0.25rem; +$custom-ratio-bar-line-height: 0.4rem; $custom-ratio-bar-line-hover-height: 0.5rem; -.tc-ratio-bar-lines { - width: 100%; - height: 100%; - border-radius: 1px; -} - .tc-ratio-bar { - padding: $padding-small 0; + align-items: center; display: flex; height: $custom-ratio-bar-height; + padding: $padding-small 0; width: 100%; - align-items: center; &-counter { padding-left: $padding-smaller; } - .tc-ratio-bar-line { + &-lines { + border-radius: 1px; + height: 100%; + width: 100%; + } + + & &-line { height: $custom-ratio-bar-line-height; margin: 0 2px 0 0; - transition: tokens.$coral-transition-instant; min-width: $custom-ratio-bar-line-height; + transition: tokens.$coral-transition-instant; + &:hover { height: $custom-ratio-bar-line-hover-height; } - &-grow { - flex-grow: 1; - } + &:first-child { margin-left: 0; } + &:last-child { margin-right: 0; } - } - .tc-ratio-bar-line-empty { - @extend .tc-ratio-bar-lines; - background-color: tokens.$coral-color-charts-neutral-weak; - } + &-grow { + flex-grow: 1; + } - .tc-ratio-bar-line-filled { - @extend .tc-ratio-bar-lines; - background-color: tokens.$coral-color-charts-default; - } + &-empty { + @extend .tc-ratio-bar-lines; + background-color: tokens.$coral-color-charts-neutral-weak; + } - .tc-ratio-bar-line-error { - @extend .tc-ratio-bar-lines; - background-color: tokens.$coral-color-charts-danger; + &-filled { + @extend .tc-ratio-bar-lines; + background-color: tokens.$coral-color-charts-default; + } + + &-error { + @extend .tc-ratio-bar-lines; + background-color: tokens.$coral-color-charts-danger; + } } } diff --git a/packages/design-system/src/components/RatioBar/RatioBar.stories.tsx b/packages/design-system/src/components/RatioBar/RatioBar.stories.tsx new file mode 100644 index 00000000000..f3cf550b034 --- /dev/null +++ b/packages/design-system/src/components/RatioBar/RatioBar.stories.tsx @@ -0,0 +1,27 @@ +import { RatioBar } from './RatioBar.component'; + +export default { + title: 'Dataviz/RatioBar', +}; + +export const _RatioBar = () => ( +
+
Ratio Bar
+
+
Not applicable amount
+ +
With an amount of 0
+ +
With an amount of 10/12
+ +
With an amount of 12/12
+ +
With an amount of 532/1000
+ +
With an amount of 10/20 with 1 error
+ +
With an amount of 532/1000 and no label
+ +
+
+); diff --git a/packages/design-system/src/components/RatioBar/RatioBarComposition.component.tsx b/packages/design-system/src/components/RatioBar/RatioBarComposition.component.tsx new file mode 100644 index 00000000000..2656982bb01 --- /dev/null +++ b/packages/design-system/src/components/RatioBar/RatioBarComposition.component.tsx @@ -0,0 +1,89 @@ +import { KeyboardEvent, MouseEvent, ReactNode } from 'react'; + +import classNames from 'classnames'; + +import { Tooltip } from '../Tooltip'; + +import styles from './RatioBar.module.scss'; + +const minPercentage = 5; + +type RadioBarLineProps = { + percentage: number; + value: number; + tooltipLabel?: string; + className: string; + dataTestId?: string; + dataFeature?: string | null; + onClick?: (e: MouseEvent) => void; +}; + +export function RatioBarLine({ + percentage, + tooltipLabel, + className, + value, + dataFeature, + onClick, + dataTestId, +}: RadioBarLineProps) { + const canGrow = percentage >= minPercentage; + + if (!value || value < 0) return null; + + function onKeyDown(event: any) { + switch (event.key) { + case 'Enter': + onClick?.(event); + break; + case ' ': + case 'Spacebar': + event.preventDefault(); // prevent scroll with space + event.stopPropagation(); + onClick?.(event); + break; + default: + break; + } + } + + const content = ( +
+ {tooltipLabel && {tooltipLabel}} +
+ ); + + if (!tooltipLabel) { + return content; + } + + return ( + + {content} + + ); +} + +export const RatioBarComposition = ({ children }: { children: ReactNode }) => ( +
+ {children} +
+); diff --git a/packages/design-system/src/components/RatioBar/RatioBarLines.component.tsx b/packages/design-system/src/components/RatioBar/RatioBarLines.component.tsx new file mode 100644 index 00000000000..fe91503857d --- /dev/null +++ b/packages/design-system/src/components/RatioBar/RatioBarLines.component.tsx @@ -0,0 +1,35 @@ +import { RatioBarLine } from './RatioBarComposition.component'; + +import styles from './RatioBar.module.scss'; + +type RatioBarLineProps = { + value: number; + percentage: number; +}; + +export const FilledLine = ({ value, percentage }: RatioBarLineProps) => ( + +); + +export const EmptyLine = ({ value, percentage }: RatioBarLineProps) => ( + +); + +export const ErrorLine = ({ value, percentage }: RatioBarLineProps) => ( + +); diff --git a/packages/design-system/src/components/RatioBar/__snapshots__/RatioBar.component.test.js.snap b/packages/design-system/src/components/RatioBar/__snapshots__/RatioBar.component.test.js.snap new file mode 100644 index 00000000000..a00a0a6066d --- /dev/null +++ b/packages/design-system/src/components/RatioBar/__snapshots__/RatioBar.component.test.js.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RatioBar RatioBar component should render an empty bar 1`] = ` +
+
+
+ + 0 + + / + 12 +
+
+`; diff --git a/packages/design-system/src/components/RatioBar/index.ts b/packages/design-system/src/components/RatioBar/index.ts new file mode 100644 index 00000000000..3a98b227f13 --- /dev/null +++ b/packages/design-system/src/components/RatioBar/index.ts @@ -0,0 +1,13 @@ +import { RatioBar as RatioBarComponent } from './RatioBar.component'; +import { RatioBarComposition, RatioBarLine } from './RatioBarComposition.component'; + +export type RatioBarType = typeof RatioBarComponent & { + Composition: typeof RatioBarComposition; + Line: typeof RatioBarLine; +}; + +const RatioBar = RatioBarComponent as RatioBarType; +RatioBar.Composition = RatioBarComposition; +RatioBar.Line = RatioBarLine; + +export { RatioBar, RatioBarComposition, RatioBarLine }; diff --git a/packages/design-system/src/index.ts b/packages/design-system/src/index.ts index 2410b07df7b..e83ad4aa4f1 100644 --- a/packages/design-system/src/index.ts +++ b/packages/design-system/src/index.ts @@ -22,6 +22,8 @@ export * from './components/Link'; export * from './components/LinkAsButton'; export * from './components/Loading'; export * from './components/Message'; +export * from './components/QualityBar'; +export * from './components/RatioBar'; export * from './components/RichRadioButton'; export * from './components/Skeleton'; export * from './components/Status';