From a1941101434d24b57aec5f3feab39cdf13926bcb Mon Sep 17 00:00:00 2001 From: Robert Niznik Date: Mon, 4 Nov 2024 09:24:42 -0500 Subject: [PATCH] feat(components): add `Meter` (#1462) * feat(components): add `Meter` * feat: update styles * fix: styles * fix: remove tabular --- .changeset/small-beds-jam.md | 5 ++ packages/components/__tests__/Meter.spec.tsx | 11 ++++ packages/components/src/Meter.tsx | 64 +++++++++++++++++++ packages/components/src/index.ts | 2 + .../components/src/styles/Meter.module.css | 33 ++++++++++ packages/components/stories/Meter.stories.tsx | 50 +++++++++++++++ 6 files changed, 165 insertions(+) create mode 100644 .changeset/small-beds-jam.md create mode 100644 packages/components/__tests__/Meter.spec.tsx create mode 100644 packages/components/src/Meter.tsx create mode 100644 packages/components/src/styles/Meter.module.css create mode 100644 packages/components/stories/Meter.stories.tsx diff --git a/.changeset/small-beds-jam.md b/.changeset/small-beds-jam.md new file mode 100644 index 000000000..b0a3ec1a9 --- /dev/null +++ b/.changeset/small-beds-jam.md @@ -0,0 +1,5 @@ +--- +"@launchpad-ui/components": patch +--- + +Add `Meter` diff --git a/packages/components/__tests__/Meter.spec.tsx b/packages/components/__tests__/Meter.spec.tsx new file mode 100644 index 000000000..9c9119f47 --- /dev/null +++ b/packages/components/__tests__/Meter.spec.tsx @@ -0,0 +1,11 @@ +import { describe, expect, it } from 'vitest'; + +import { render, screen } from '../../../test/utils'; +import { Meter } from '../src'; + +describe('Meter', () => { + it('renders', () => { + render(); + expect(screen.getByRole('meter')).toBeVisible(); + }); +}); diff --git a/packages/components/src/Meter.tsx b/packages/components/src/Meter.tsx new file mode 100644 index 000000000..9f16e3a76 --- /dev/null +++ b/packages/components/src/Meter.tsx @@ -0,0 +1,64 @@ +import type { ForwardedRef } from 'react'; +import type { MeterProps } from 'react-aria-components'; + +import { cva } from 'class-variance-authority'; +import { forwardRef } from 'react'; +import { Meter as AriaMeter, composeRenderProps } from 'react-aria-components'; +import styles from './styles/Meter.module.css'; + +const meter = cva(styles.meter); + +const icon = cva(styles.base); + +const _Meter = (props: MeterProps, ref: ForwardedRef) => { + const center = 64; + const strokeWidth = 8; + const r = 64 - strokeWidth; + const c = 2 * r * Math.PI; + + return ( + + meter({ ...renderProps, className }), + )} + > + {({ percentage, valueText }) => ( + // biome-ignore lint/a11y/noSvgWithoutTitle: + + + + + {valueText?.match(/\d+/)?.[0]} + {valueText?.replace(/\d+/, '')} + + + )} + + ); +}; + +/** + * A meter represents a quantity within a known range, or a fractional value. + * + * https://react-spectrum.adobe.com/react-aria/Meter.html + */ +const Meter = forwardRef(_Meter); + +export { Meter }; +export type { MeterProps }; diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index ff7137d4b..165234d97 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -38,6 +38,7 @@ export type { LinkButtonProps } from './LinkButton'; export type { LinkIconButtonProps } from './LinkIconButton'; export type { ListBoxProps, ListBoxItemProps } from './ListBox'; export type { MenuProps, MenuItemProps, MenuTriggerProps, SubmenuTriggerProps } from './Menu'; +export type { MeterProps } from './Meter'; export type { ModalProps, ModalOverlayProps } from './Modal'; export type { NumberFieldProps } from './NumberField'; export type { OverlayArrowProps, PopoverProps } from './Popover'; @@ -113,6 +114,7 @@ export { LinkButton } from './LinkButton'; export { LinkIconButton } from './LinkIconButton'; export { ListBox, ListBoxItem } from './ListBox'; export { Menu, MenuItem, MenuTrigger, SubmenuTrigger } from './Menu'; +export { Meter } from './Meter'; export { Modal, ModalOverlay } from './Modal'; export { NumberField } from './NumberField'; export { OverlayArrow, Popover } from './Popover'; diff --git a/packages/components/src/styles/Meter.module.css b/packages/components/src/styles/Meter.module.css new file mode 100644 index 000000000..6d5a33c8f --- /dev/null +++ b/packages/components/src/styles/Meter.module.css @@ -0,0 +1,33 @@ +.meter { + --fill-color: var(--lp-color-brand-cyan-base); + + display: inline-flex; +} + +.outerCircle { + stroke: var(--lp-color-bg-ui-tertiary); +} + +.innerCircle { + stroke: var(--fill-color); + stroke-linecap: round; +} + +.value { + fill: var(--lp-color-text-ui-primary-base); + font: var(--lp-text-body-1-semibold); + font-size: var(--lp-font-size-600); + text-anchor: middle; + dominant-baseline: middle; +} + +.unit { + font-size: var(--lp-font-size-200); + text-anchor: middle; + dominant-baseline: middle; +} + +.base { + height: var(--lp-size-128); + width: var(--lp-size-128); +} diff --git a/packages/components/stories/Meter.stories.tsx b/packages/components/stories/Meter.stories.tsx new file mode 100644 index 000000000..08e3a91bb --- /dev/null +++ b/packages/components/stories/Meter.stories.tsx @@ -0,0 +1,50 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { vars } from '@launchpad-ui/vars'; + +import { Meter } from '../src'; + +const meta: Meta = { + component: Meter, + title: 'Components/Status/Meter', + parameters: { + status: { + type: import.meta.env.STORYBOOK_PACKAGE_STATUS__COMPONENTS, + }, + a11y: { + options: { + rules: { + // https://github.com/adobe/react-spectrum/issues/6627#issuecomment-2192595638 + 'aria-allowed-attr': { enabled: false }, + 'aria-prohibited-attr': { enabled: false }, + }, + }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Example: Story = { + args: { value: 14, 'aria-label': 'percent diff' }, +}; + +export const Values: Story = { + render: () => { + return ( +
+ + + +
+ ); + }, +};