From 554c2300695839c29fd17717ecddb3732e32e111 Mon Sep 17 00:00:00 2001 From: Nora Krantz <75342690+nkrantz@users.noreply.github.com> Date: Thu, 7 Sep 2023 17:28:59 -0400 Subject: [PATCH] chore(meter): add docs, update designs (#3463) --- .changeset/blue-phones-battle.md | 6 + .changeset/olive-lizards-enjoy.md | 5 + cypress/integration/sitemap-vrt/constants.ts | 1 + .../components/meter/__tests__/index.spec.tsx | 32 ++- .../paste-core/components/meter/package.json | 2 +- .../paste-core/components/meter/src/Meter.tsx | 126 ++++++---- .../components/meter/src/MeterLabel.tsx | 33 ++- .../meter/stories/index.stories.tsx | 186 +++++++++++--- packages/paste-website/package.json | 1 + .../src/component-examples/MeterExamples.ts | 35 +++ .../src/pages/components/meter/index.mdx | 233 ++++++++++++++++++ .../src/pages/components/slider/index.mdx | 53 ++-- yarn.lock | 1 + 13 files changed, 571 insertions(+), 143 deletions(-) create mode 100644 .changeset/blue-phones-battle.md create mode 100644 .changeset/olive-lizards-enjoy.md create mode 100644 packages/paste-website/src/component-examples/MeterExamples.ts create mode 100644 packages/paste-website/src/pages/components/meter/index.mdx diff --git a/.changeset/blue-phones-battle.md b/.changeset/blue-phones-battle.md new file mode 100644 index 0000000000..416433d473 --- /dev/null +++ b/.changeset/blue-phones-battle.md @@ -0,0 +1,6 @@ +--- +'@twilio-paste/meter': patch +'@twilio-paste/core': patch +--- + +[Meter] Finalize designs of Meter component, bump to Production status, and add documentation. diff --git a/.changeset/olive-lizards-enjoy.md b/.changeset/olive-lizards-enjoy.md new file mode 100644 index 0000000000..2c2322a319 --- /dev/null +++ b/.changeset/olive-lizards-enjoy.md @@ -0,0 +1,5 @@ +--- +'@twilio-paste/codemods': patch +--- + +[Codemods] New export from Meter package: MeterLabel diff --git a/cypress/integration/sitemap-vrt/constants.ts b/cypress/integration/sitemap-vrt/constants.ts index 24bbe3ed67..f4ffb42c6f 100644 --- a/cypress/integration/sitemap-vrt/constants.ts +++ b/cypress/integration/sitemap-vrt/constants.ts @@ -60,6 +60,7 @@ export const SITEMAP = [ '/components/list/', '/components/minimizable-dialog/', '/components/media-object/', + '/components/meter', '/components/pagination/', '/components/modal/', '/components/menu/', diff --git a/packages/paste-core/components/meter/__tests__/index.spec.tsx b/packages/paste-core/components/meter/__tests__/index.spec.tsx index b6f1d08d51..7a91e382c5 100644 --- a/packages/paste-core/components/meter/__tests__/index.spec.tsx +++ b/packages/paste-core/components/meter/__tests__/index.spec.tsx @@ -3,7 +3,7 @@ import {render, screen} from '@testing-library/react'; import {Theme} from '@twilio-paste/theme'; import type {RenderOptions} from '@testing-library/react'; -import {Default, HiddenValueLabelAriaLabel, Customized} from '../stories/index.stories'; +import {Default, AriaLabel, Customized} from '../stories/index.stories'; const ThemeWrapper: RenderOptions['wrapper'] = ({children}) => ( {children} @@ -21,14 +21,14 @@ describe('Meter', () => { expect(meter).toHaveAttribute('aria-valuemax', '100'); expect(meter).toHaveAttribute('aria-valuenow', '75'); expect(meter).toHaveAttribute('aria-valuetext', '75%'); - expect(meter).toHaveAttribute('id', 'meter'); - expect(meter).toHaveAttribute('aria-labelledby', 'meterMETER_LABEL'); + expect(meter).toHaveAttribute('id'); + expect(meter).toHaveAttribute('aria-labelledby'); }); it('should apply aria-label correctly', () => { - render(, {wrapper: ThemeWrapper}); + render(, {wrapper: ThemeWrapper}); const meter = screen.getByRole('meter'); - expect(meter).toHaveAttribute('aria-label', 'Fuel level'); + expect(meter).toHaveAttribute('aria-label', 'Storage space'); expect(meter).not.toHaveAttribute('aria-labelledby'); }); }); @@ -36,18 +36,32 @@ describe('Meter', () => { describe('Customization', () => { it('should set default data-paste-element attribute on meter', () => { render(); - const meterOne = screen.getByTestId('meter_one'); - expect(meterOne).toHaveAttribute(elementAttr, 'METER'); const meterLabelOne = screen.getByTestId('meter_label_one'); expect(meterLabelOne).toHaveAttribute(elementAttr, 'METER_LABEL'); + expect(meterLabelOne.parentElement).toHaveAttribute(elementAttr, 'METER_LABEL_WRAPPER'); + expect(meterLabelOne.nextElementSibling).toHaveAttribute(elementAttr, 'METER_LABEL_VALUE_LABEL'); + const meterOne = screen.getByTestId('meter_one'); + expect(meterOne).toHaveAttribute(elementAttr, 'METER'); + expect(meterOne.firstElementChild).toHaveAttribute(elementAttr, 'METER_BAR'); + expect(meterOne.firstElementChild?.firstElementChild).toHaveAttribute(elementAttr, 'METER_FILL'); + expect(meterOne.lastElementChild).toHaveAttribute(elementAttr, 'METER_MIN_MAX_WRAPPER'); + expect(meterOne.lastElementChild?.firstElementChild).toHaveAttribute(elementAttr, 'METER_MIN'); + expect(meterOne.lastElementChild?.lastElementChild).toHaveAttribute(elementAttr, 'METER_MAX'); }); it('should set custom data-paste-element attribute on meter', () => { render(); - const meterTwo = screen.getByTestId('meter_two'); - expect(meterTwo).toHaveAttribute(elementAttr, 'FOO'); const meterLabelTwo = screen.getByTestId('meter_label_two'); expect(meterLabelTwo).toHaveAttribute(elementAttr, 'FOO_LABEL'); + expect(meterLabelTwo.parentElement).toHaveAttribute(elementAttr, 'FOO_LABEL_WRAPPER'); + expect(meterLabelTwo.nextElementSibling).toHaveAttribute(elementAttr, 'FOO_LABEL_VALUE_LABEL'); + const meterTwo = screen.getByTestId('meter_two'); + expect(meterTwo).toHaveAttribute(elementAttr, 'FOO'); + expect(meterTwo.firstElementChild).toHaveAttribute(elementAttr, 'FOO_BAR'); + expect(meterTwo.firstElementChild?.firstElementChild).toHaveAttribute(elementAttr, 'FOO_FILL'); + expect(meterTwo.lastElementChild).toHaveAttribute(elementAttr, 'FOO_MIN_MAX_WRAPPER'); + expect(meterTwo.lastElementChild?.firstElementChild).toHaveAttribute(elementAttr, 'FOO_MIN'); + expect(meterTwo.lastElementChild?.lastElementChild).toHaveAttribute(elementAttr, 'FOO_MAX'); }); }); }); diff --git a/packages/paste-core/components/meter/package.json b/packages/paste-core/components/meter/package.json index 00bc19dd31..578536ea37 100644 --- a/packages/paste-core/components/meter/package.json +++ b/packages/paste-core/components/meter/package.json @@ -2,7 +2,7 @@ "name": "@twilio-paste/meter", "version": "1.0.0", "category": "data display", - "status": "alpha", + "status": "production", "description": "Meter is a visual representation of a numerical value within a known range.", "author": "Twilio Inc.", "license": "MIT", diff --git a/packages/paste-core/components/meter/src/Meter.tsx b/packages/paste-core/components/meter/src/Meter.tsx index 8439ba73d7..348f6b3a36 100644 --- a/packages/paste-core/components/meter/src/Meter.tsx +++ b/packages/paste-core/components/meter/src/Meter.tsx @@ -9,73 +9,93 @@ import {LABEL_SUFFIX} from './constants'; export interface MeterProps extends HTMLPasteProps<'meter'>, Pick { minValue?: number; maxValue?: number; + minLabel?: string; + maxLabel?: string; value?: number; id: string; - showValueLabel?: boolean; - formatOptions?: Intl.NumberFormatOptions; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#options - valueLabel?: string; 'aria-label'?: string; 'aria-describedby'?: string; 'aria-labelledby'?: string; - /* - * The following props don't exist on the react-aria useMeter hook but do exist on the HTML meter element. - * They can be added back into the Paste Meter API depending on the finalized spec & designs. - * - * low?: number; - * high?: number; - * optimum?: number; - */ } -const Meter = React.forwardRef(({element = 'METER', id, ...props}, ref) => { - const {value = 0, minValue = 0, maxValue = 100, showValueLabel = true} = props; - const {meterProps} = useMeter(props); +const Meter = React.forwardRef( + ({element = 'METER', id, minLabel, maxLabel, ...props}, ref) => { + const {value = 0, minValue = 0, maxValue = 100} = props; + const {meterProps} = useMeter(props); - // Calculate the width of the bar as a percentage - const percentage = (value - minValue) / (maxValue - minValue); - const fillWidth = `${Math.round(percentage * 100)}%`; + // Calculate the width of the bar as a percentage + const percentage = (value - minValue) / (maxValue - minValue); + const fillWidth = `${Math.round(percentage * 100)}%`; - /* - * Since ProgressBar isn't a form element, we cannot use htmlFor from the regular label - * so we create a ProgressBarLabel component that behaves like a regular form Label - * but leverages aria-labelledby instead of htmlFor transparently. - */ - let labelledBy = props['aria-labelledby']; - if (labelledBy == null && props['aria-label'] == null && id != null) { - labelledBy = `${id}${LABEL_SUFFIX}`; - } + /* + * Since Meter isn't a form element, we cannot use htmlFor from the regular Label + * so we created a MeterLabel component that behaves like a regular form Label + * but leverages aria-labelledby instead of htmlFor under the hood. + * `aria-labelledby` and `aria-label` can still be passed for custom labelling options. + */ + let labelledBy = props['aria-labelledby']; + if (labelledBy == null && props['aria-label'] == null && id != null) { + labelledBy = `${id}${LABEL_SUFFIX}`; + } - return ( - + return ( - {showValueLabel && ( - - {meterProps['aria-valuetext']} - + + + + {(minLabel || maxLabel) && ( + )} - - - - - ); -}); + ); + } +); Meter.displayName = 'Meter'; diff --git a/packages/paste-core/components/meter/src/MeterLabel.tsx b/packages/paste-core/components/meter/src/MeterLabel.tsx index 1f3a5100f4..7f8a3cb57b 100644 --- a/packages/paste-core/components/meter/src/MeterLabel.tsx +++ b/packages/paste-core/components/meter/src/MeterLabel.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; -import {type BoxProps} from '@twilio-paste/box'; +import {type BoxProps, Box} from '@twilio-paste/box'; import {Label} from '@twilio-paste/label'; +import {Text} from '@twilio-paste/text'; import type {HTMLPasteProps} from '@twilio-paste/types'; import {LABEL_SUFFIX} from './constants'; @@ -8,14 +9,36 @@ import {LABEL_SUFFIX} from './constants'; export interface MeterLabelProps extends HTMLPasteProps<'div'>, Pick { children: string; htmlFor: string; + valueLabel?: string; } const MeterLabel = React.forwardRef( - ({element = 'METER_LABEL', children, htmlFor, ...labelProps}, ref) => { + ({element = 'METER_LABEL', children, htmlFor, valueLabel, ...labelProps}, ref) => { return ( - + + + {valueLabel && ( + + )} + ); } ); diff --git a/packages/paste-core/components/meter/stories/index.stories.tsx b/packages/paste-core/components/meter/stories/index.stories.tsx index 6f6368352c..ca1d60e9db 100644 --- a/packages/paste-core/components/meter/stories/index.stories.tsx +++ b/packages/paste-core/components/meter/stories/index.stories.tsx @@ -2,6 +2,7 @@ import {CustomizationProvider} from '@twilio-paste/customization'; import {useTheme} from '@twilio-paste/theme'; import {useUID} from '@twilio-paste/uid-library'; import {HelpText} from '@twilio-paste/help-text'; +import {Box} from '@twilio-paste/box'; import * as React from 'react'; import {Meter, MeterLabel} from '../src'; @@ -13,38 +14,94 @@ export default { }; export const Default = (): React.ReactElement => { - const meterId = 'meter'; + const meterId = useUID(); return ( - <> - Storage space + + + Storage used + - + + ); +}; + +export const Full = (): React.ReactElement => { + const meterId = useUID(); + return ( + + + Storage used + + + + ); +}; + +export const Empty = (): React.ReactElement => { + const meterId = useUID(); + return ( + + + Storage used + + + ); }; -export const HiddenValueLabelAriaLabel = (): React.ReactElement => { +export const MinMax = (): React.ReactElement => { const meterId = useUID(); - return ; + return ( + + + Storage space used + + + + ); }; -export const FormattedValueLabel = (): React.ReactElement => { +export const MinOnly = (): React.ReactElement => { const meterId = useUID(); return ( - <> - Account funds - - + + + Storage space + + + ); }; -export const CustomValueLabelCustomLabel = (): React.ReactElement => { +export const MaxOnly = (): React.ReactElement => { + const meterId = useUID(); + return ( + + + Storage space + + + + ); +}; + +export const AriaLabel = (): React.ReactElement => { + const meterId = useUID(); + return ( + + + + ); +}; + +export const CustomLabel = (): React.ReactElement => { const labelId = useUID(); const meterId = useUID(); return ( - <> + Storage space used - - + + ); }; @@ -52,11 +109,35 @@ export const WithHelpText = (): React.ReactElement => { const meterId = useUID(); const helpTextId = useUID(); return ( - <> - Storage space used - + + + Storage space used + + + Additional storage may be purchased on your account page. + + ); +}; + +export const Wrapped = (): React.ReactElement => { + const meterId = useUID(); + const helpTextId = useUID(); + return ( + + + Storage space used on this account that belongs to you + + Helpful text - + ); }; @@ -68,39 +149,70 @@ export const Customized = (): React.ReactElement => { - - Storage space - - - - Storage space - - + + + + Storage space + + + + + + Storage space + + + + ); }; diff --git a/packages/paste-website/package.json b/packages/paste-website/package.json index 47cbf867e8..03540aa86e 100644 --- a/packages/paste-website/package.json +++ b/packages/paste-website/package.json @@ -83,6 +83,7 @@ "@twilio-paste/media-object": "^10.0.0", "@twilio-paste/menu": "^14.0.1", "@twilio-paste/menu-primitive": "^2.0.0", + "@twilio-paste/meter": "^1.0.0", "@twilio-paste/minimizable-dialog": "^4.0.0", "@twilio-paste/modal": "^16.0.0", "@twilio-paste/modal-dialog-primitive": "^2.0.0", diff --git a/packages/paste-website/src/component-examples/MeterExamples.ts b/packages/paste-website/src/component-examples/MeterExamples.ts new file mode 100644 index 0000000000..1792566ac5 --- /dev/null +++ b/packages/paste-website/src/component-examples/MeterExamples.ts @@ -0,0 +1,35 @@ +export const defaultMeter = ` +const DefaultMeterExample = () => { + const meterId = useUID() + const helpTextId = useUID() + return ( + + Emails delivered + + Showing successful deliveries of June email campaign. + + ); +}; + +render( + +) +`.trim(); + +export const minMaxMeter = ` +const DefaultMeterExample = () => { + const meterId = useUID() + const helpTextId = useUID() + return ( + + Account balance paid + + Remaining balance must be paid by the end of the billing period. + + ); +}; + +render( + +) +`.trim(); diff --git a/packages/paste-website/src/pages/components/meter/index.mdx b/packages/paste-website/src/pages/components/meter/index.mdx new file mode 100644 index 0000000000..e20f08fd61 --- /dev/null +++ b/packages/paste-website/src/pages/components/meter/index.mdx @@ -0,0 +1,233 @@ +export const meta = { + title: 'Meter', + package: '@twilio-paste/meter', + description: 'Meter is a visual representation of a numerical value within a known range.', + slug: '/components/meter/', +}; + +import {Box} from '@twilio-paste/box'; +import {Meter, MeterLabel} from '@twilio-paste/meter'; +import {HelpText} from '@twilio-paste/help-text'; +import {useUID} from '@twilio-paste/uid-library'; +import Changelog from '@twilio-paste/meter/CHANGELOG.md'; +import packageJson from '@twilio-paste/meter/package.json'; + +import DefaultLayout from '../../../layouts/DefaultLayout'; +import {SidebarCategoryRoutes} from '../../../constants'; +import {getFeature, getNavigationData} from '../../../utils/api'; +import {DoDont, Do, Dont} from '../../../components/DoDont'; +import {defaultMeter, minMaxMeter, hiddenValueLabel} from '../../../component-examples/MeterExamples'; + +export default DefaultLayout; + +export const getStaticProps = async () => { + const navigationData = await getNavigationData(); + const feature = await getFeature('Meter'); + return { + props: { + data: { + ...packageJson, + ...feature, + }, + navigationData, + }, + }; +}; + + + +--- + + + + + + + + + {defaultMeter} + + +## Guidelines + +### About Meter + +A Meter is a visual representation to indicate how full something is. + +### Meter vs. Progress Bar + +A Meter represents a bucket that can be empty, full, or somewhere in between. Use a Meter when you need to show capacity. For example, use a Meter to show how much data is being used or how many emails were sent successfully. + +A [Progress Bar](/components/progress-bar) represents **only** task completion, like a file upload or filling out a form. If you’re not displaying progress on a particular task, use a Meter. + +### Accessibility + +A label is required when using Meter. Use one of these options: + +- Visible label using `MeterLabel`, with `htmlFor` set equal to the `id` of the Meter (preferred) +- Visible label that's associated to the Meter with `aria-labelledby` +- Label directly using `aria-label` + +## Examples + +### Default + +Use a Meter to communicate an amount of something within a range, like number of emails delivered. Use the `valueLabel` prop on the `MeterLabel` component to display the current value being represented by the Meter. + +Consider what type of value would be most useful for a user to see (for example, “50%” vs. “5,000 of 10,000”). Avoid using multiple formats to represent the same value (for example, "5,000 of 10,000 (50%)"). + + + {defaultMeter} + + +### Min and max values + +Meter has a default value of 0, a default minimum value of 0, and a default maximum value of 100. + +Passing `minValue` and `maxValue` to Meter allow you to set a non 0-100 scale. Use `minLabel` and `maxLabel` to display minimum and maximum values below the Meter. If using a non 0-100 scale, displaying min and max labels is required. + + + {minMaxMeter} + + +## Composition notes + +The Meter label should communicate what the Meter is measuring. Where possible, avoid a label that wraps onto two lines. + +A Meter almost always will include a numerical value, the value label. When using the `valueLabel` prop, consider what type of value would be most useful for a user to see. For example, choose either “50%” or “5,000 of 10,000”, not both. + +Use Help Text to offer additional information to contextualize or help the user understand the Meter. + +## When to use a Meter + + + + + Data usage + + + + + + File upload status + + + + + + + + + + Balance + + + Complete balance due at the end of the billing cycle. + + + + + + Balance + + + Complete balance of $1,500 due at the end of the billing cycle. + + + + +## Usage Guide + +### API + +#### Installation + +```bash +yarn add @twilio-paste/core - or - yarn add @twilio-paste/meter +``` + +#### Usage + +```jsx +import {Meter, MeterLabel} from '@twilio-paste/core/meter'; +import {HelpText} from '@twilio-paste/core/help-text' +import {useUID} from '@twilio-paste/core/uid-library' + +const Component = () => { + const meterId = useUID(); + const helpTextId = useUID(); // Help text is optional + + return ( + <> + Label + + Help text + + ); +}; +``` + +#### Meter props + +| Prop | Type | Description | Default | +| ----------------- | -------- | -------------------------------------------------------------------------------------------------- | ------- | +| id | `string` | Must provide an ID to match with a label | | +| aria-describedby? | `string` | Optional ID to pair the Meter to its help text | | +| aria-labelledby? | `string` | Optional ID to pair the Meter to its label text (if not using a regular MeterLabel with `htmlFor`) | | +| aria-label? | `string` | Label text of the Meter (if not using a regular MeterLabel with `htmlFor` or `aria-labelledby`) | | +| minValue? | `number` | Minimum value of the Meter | 0 | +| maxValue? | `number` | Maximum valuae of the Meter | 100 | +| value? | `number` | The current value | 0 | +| minLabel? | `string` | Label displayed for min value. Only shown when this prop is passed. | | +| maxLabel? | `string` | Label displayed for max value. Only shown when this prop is passed. | | +| element? | `string` | Overrides the default element name to apply unique styles with the Customization Provider | 'METER' | + +#### MeterLabel props + +| Prop | Type | Description | Default | +| ----------- | -------- | ----------------------------------------------------------------------------------------- | ------------- | +| valueLabel? | `string` | Custom value label of the Meter | | +| children | `string` | Label text | | +| htmlFor | `string` | Pass the id of the associated Meter | | +| element? | `string` | Overrides the default element name to apply unique styles with the Customization Provider | 'METER_LABEL' | + + + + + + + + diff --git a/packages/paste-website/src/pages/components/slider/index.mdx b/packages/paste-website/src/pages/components/slider/index.mdx index 3aa4f604f9..0ddd81b7dc 100644 --- a/packages/paste-website/src/pages/components/slider/index.mdx +++ b/packages/paste-website/src/pages/components/slider/index.mdx @@ -18,6 +18,7 @@ import {Label} from '@twilio-paste/label'; import {HelpText} from '@twilio-paste/help-text'; import {Form, FormControl} from '@twilio-paste/form'; import {useUID} from '@twilio-paste/uid-library'; +import {Meter, MeterLabel} from '@twilio-paste/meter'; import DefaultLayout from '../../../layouts/DefaultLayout'; import {SidebarCategoryRoutes} from '../../../constants'; @@ -80,7 +81,7 @@ A Slider allows a user to select a numerical value when: Slider uses [Adobe's Spectrum React-Aria useSlider](https://react-spectrum.adobe.com/react-aria/useSlider.html) under the hood. -### Slider vs. number Input vs. Meter +### Slider vs. number Input Both Sliders and [number Inputs](/components/input#input-with-number-functionality) are form fields that take numerical values. Because the mouse and touch interaction on a Slider is less @@ -91,10 +92,6 @@ If the user needs to select an exact value, use a [number Input](/components/inp instead. If you want to let users select between consecutive values, you can also use a [Radio Button Group](/components/radio-button-group). -In cases where visually showing the size of a numerical value is important and -it’s not enough to show it as text, use a [number Input](/components/input#input-with-number-functionality) -paired with a Meter (component to come!) instead. - ### Accessibility Slider is a form element and follows the same accessibility guidelines as other form fields: @@ -138,9 +135,6 @@ For additional guidance on how to compose error messages, refer to the Use a disabled Slider to show users that they can't interact with the Slider. -If you want to show a value that can't be edited, use a Meter (to come) or -consider another way of showing static information. - {disabledSlider} @@ -214,10 +208,19 @@ Use a Slider with hidden range labels when the range is obvious or the labels ar - Coming soon! + + Current balance + + - - - Coming soon! - - - -
- - -
-
- - -
-
-
-
- { | id | `string` | Must provide an id to match with a label | undefined | | numberFormatter | [`Intl.NumberFormatter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) | Used to format the value into i18n formats. Can return localized currencies and percentages | | | aria-describedby? | `string` | Optional id to pair the input to its help text | undefined | -| aria-labeledby? | `string` | Optional id to pair the input to its label text (if not using a regular label with `htmlFor`) | undefined | +| aria-labelledby? | `string` | Optional id to pair the input to its label text (if not using a regular label with `htmlFor`) | undefined | | disabled? | `boolean` | Disables the slider | false | | hasError? | `boolean` | Shows error styling on the Slider | false | | hideRangeLabels? | `boolean` | Hides the min and max values that appear over the slider | false | diff --git a/yarn.lock b/yarn.lock index 14b44b0fac..91478d3aa6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17010,6 +17010,7 @@ __metadata: "@twilio-paste/media-object": ^10.0.0 "@twilio-paste/menu": ^14.0.1 "@twilio-paste/menu-primitive": ^2.0.0 + "@twilio-paste/meter": ^1.0.0 "@twilio-paste/minimizable-dialog": ^4.0.0 "@twilio-paste/modal": ^16.0.0 "@twilio-paste/modal-dialog-primitive": ^2.0.0