From 39acfd3ecac939ae6f2f9fa9502b5607582072e2 Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Thu, 1 Feb 2024 06:04:48 -0800 Subject: [PATCH] NV-3417: Pagination component + vitest setup (#5107) --- libs/design-system/.storybook/preview.jsx | 31 +- libs/design-system/package.json | 7 +- libs/design-system/src/button/Button.tsx | 82 +-- libs/design-system/src/index.ts | 1 + libs/design-system/src/input/Input.tsx | 6 +- .../src/pagination/ControlBar.tsx | 99 +++ .../src/pagination/ControlButton.tsx | 84 +++ .../src/pagination/GoToPageInput.tsx | 123 ++++ .../src/pagination/IconControlButton.tsx | 37 ++ .../src/pagination/PageSizeSelect.tsx | 81 +++ .../src/pagination/Pagination.const.ts | 19 + .../src/pagination/Pagination.stories.tsx | 44 ++ .../src/pagination/Pagination.tsx | 99 +++ .../src/pagination/PaginationContext.tsx | 19 + libs/design-system/src/pagination/index.ts | 1 + .../pagination/util/clampPageNumber.spec.ts | 36 ++ .../src/pagination/util/clampPageNumber.ts | 20 + .../util/getPaginationSymbols.spec.ts | 411 +++++++++++++ .../pagination/util/getPaginationSymbols.ts | 156 +++++ .../src/pagination/util/index.ts | 2 + libs/design-system/src/select/Select.tsx | 6 +- libs/design-system/src/utils/clamp.spec.ts | 52 ++ libs/design-system/src/utils/clamp.ts | 7 + libs/design-system/src/utils/index.ts | 2 + libs/design-system/vite.config.ts | 6 + pnpm-lock.yaml | 579 +++++++++++++++++- 26 files changed, 1944 insertions(+), 66 deletions(-) create mode 100644 libs/design-system/src/pagination/ControlBar.tsx create mode 100644 libs/design-system/src/pagination/ControlButton.tsx create mode 100644 libs/design-system/src/pagination/GoToPageInput.tsx create mode 100644 libs/design-system/src/pagination/IconControlButton.tsx create mode 100644 libs/design-system/src/pagination/PageSizeSelect.tsx create mode 100644 libs/design-system/src/pagination/Pagination.const.ts create mode 100644 libs/design-system/src/pagination/Pagination.stories.tsx create mode 100644 libs/design-system/src/pagination/Pagination.tsx create mode 100644 libs/design-system/src/pagination/PaginationContext.tsx create mode 100644 libs/design-system/src/pagination/index.ts create mode 100644 libs/design-system/src/pagination/util/clampPageNumber.spec.ts create mode 100644 libs/design-system/src/pagination/util/clampPageNumber.ts create mode 100644 libs/design-system/src/pagination/util/getPaginationSymbols.spec.ts create mode 100644 libs/design-system/src/pagination/util/getPaginationSymbols.ts create mode 100644 libs/design-system/src/pagination/util/index.ts create mode 100644 libs/design-system/src/utils/clamp.spec.ts create mode 100644 libs/design-system/src/utils/clamp.ts create mode 100644 libs/design-system/src/utils/index.ts diff --git a/libs/design-system/.storybook/preview.jsx b/libs/design-system/.storybook/preview.jsx index 7208da7afc6..d6f5ee37763 100644 --- a/libs/design-system/.storybook/preview.jsx +++ b/libs/design-system/.storybook/preview.jsx @@ -1,7 +1,9 @@ -import React from 'react'; -import { useDarkMode } from 'storybook-dark-mode'; +import React, { useEffect } from 'react'; +import { addons } from '@storybook/preview-api'; +import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode'; import { ThemeProvider } from '../src/ThemeProvider'; import { DocsContainer } from './Doc.container'; +import { useLocalThemePreference } from '@novu/shared-web'; export const parameters = { layout: 'fullscreen', @@ -9,9 +11,6 @@ export const parameters = { docs: { container: DocsContainer, }, - darkMode: { - current: 'dark', - }, actions: { argTypesRegex: '^on[A-Z].*' }, controls: { matchers: { @@ -19,14 +18,30 @@ export const parameters = { date: /Date$/, }, }, + darkMode: { + current: 'dark', + classTarget: 'html', + }, }; -function ThemeWrapper(props) { +const channel = addons.getChannel(); +function ColorSchemeThemeWrapper({ children }) { + const { setThemeStatus } = useLocalThemePreference(); + + const handleColorScheme = (value) => { + setThemeStatus(value ? 'dark' : 'light'); + }; + + useEffect(() => { + channel.on(DARK_MODE_EVENT_NAME, handleColorScheme); + return () => channel.off(DARK_MODE_EVENT_NAME, handleColorScheme); + }, [channel]); + return (
- {props.children} + {children}
); } -export const decorators = [(renderStory) => {renderStory()}]; +export const decorators = [(renderStory) => {renderStory()}]; diff --git a/libs/design-system/package.json b/libs/design-system/package.json index f29de195a0c..75d4d898885 100644 --- a/libs/design-system/package.json +++ b/libs/design-system/package.json @@ -30,7 +30,8 @@ "build-storybook": "storybook build", "cypress:install": "cypress install", "cypress:open": "cross-env NODE_ENV=test cypress open", - "cypress:run": "cross-env NODE_OPTIONS=--max_old_space_size=4096 NODE_ENV=test cypress run --component" + "cypress:run": "cross-env NODE_OPTIONS=--max_old_space_size=4096 NODE_ENV=test cypress run --component", + "test": "vitest" }, "dependencies": { "@cypress/react": "^7.0.3", @@ -57,6 +58,7 @@ "devDependencies": { "@storybook/addon-actions": "^7.5.0", "@storybook/addon-docs": "^7.4.2", + "@storybook/client-api": "^7.6.10", "@storybook/react": "^7.4.2", "@storybook/react-webpack5": "^7.4.2", "@storybook/theming": "^7.4.2", @@ -78,7 +80,8 @@ "typescript": "4.9.5", "url-loader": "^4.1.1", "vite": "^4.4.5", - "vite-plugin-dts": "^3.6.0" + "vite-plugin-dts": "^3.6.0", + "vitest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0", diff --git a/libs/design-system/src/button/Button.tsx b/libs/design-system/src/button/Button.tsx index 28fd19e8575..a251f00ceda 100644 --- a/libs/design-system/src/button/Button.tsx +++ b/libs/design-system/src/button/Button.tsx @@ -1,4 +1,4 @@ -import React, { MouseEventHandler } from 'react'; +import React, { forwardRef, MouseEventHandler } from 'react'; import { Button as MantineButton, ButtonProps, Sx } from '@mantine/core'; import useStyles from './Button.styles'; @@ -27,41 +27,47 @@ export interface IButtonProps extends ButtonProps { * Button component * */ -export function Button({ - id, - loading, - children, - submit = false, - icon, - size = 'md', - fullWidth, - disabled = false, - inherit = false, - onClick, - variant = 'gradient', - pulse, - iconPosition = 'left', - ...props -}: IButtonProps) { - const { classes } = useStyles({ disabled, inherit, variant, pulse }); - const withIconProps = icon ? (iconPosition === 'left' ? { leftIcon: icon } : { rightIcon: icon }) : {}; +export const Button = forwardRef( + ( + { + id, + loading, + children, + submit = false, + icon, + size = 'md', + fullWidth, + disabled = false, + inherit = false, + onClick, + variant = 'gradient', + pulse, + iconPosition = 'left', + ...props + }, + buttonRef + ) => { + const { classes } = useStyles({ disabled, inherit, variant, pulse }); + const withIconProps = icon ? (iconPosition === 'left' ? { leftIcon: icon } : { rightIcon: icon }) : {}; - return ( - - {children} - - ); -} + return ( + + {children} + + ); + } +); diff --git a/libs/design-system/src/index.ts b/libs/design-system/src/index.ts index 2921630df2d..4b9bc33f0f6 100644 --- a/libs/design-system/src/index.ts +++ b/libs/design-system/src/index.ts @@ -29,6 +29,7 @@ export { NotificationBadge } from './notification-badge/NotificationBadge'; export { Modal } from './modal/Modal'; export { LoadingOverlay } from './loading-overlay/LoadingOverlay'; export { NameInput } from './name-input/NameInput'; +export * from './pagination'; export * from './cards'; export * from './arrow-button'; export * from './popover'; diff --git a/libs/design-system/src/input/Input.tsx b/libs/design-system/src/input/Input.tsx index aa32e5574bb..13e7fa7b4c7 100644 --- a/libs/design-system/src/input/Input.tsx +++ b/libs/design-system/src/input/Input.tsx @@ -1,9 +1,9 @@ import React, { ChangeEvent, FocusEvent } from 'react'; -import { TextInputProps, TextInput as MantineTextInput, Styles } from '@mantine/core'; +import { TextInputProps, TextInput as MantineTextInput, Styles, InputProps } from '@mantine/core'; import { inputStyles } from '../config/inputs.styles'; import { SpacingProps } from '../shared/spacing.props'; -interface IInputProps extends SpacingProps { +export interface IInputProps extends SpacingProps, Pick { label?: React.ReactNode; error?: React.ReactNode; placeholder?: string; @@ -20,6 +20,8 @@ interface IInputProps extends SpacingProps { max?: string | number; onBlur?: (event: FocusEvent) => void; styles?: Styles>; + className?: string; + id?: string; } /** diff --git a/libs/design-system/src/pagination/ControlBar.tsx b/libs/design-system/src/pagination/ControlBar.tsx new file mode 100644 index 00000000000..67af4b548d2 --- /dev/null +++ b/libs/design-system/src/pagination/ControlBar.tsx @@ -0,0 +1,99 @@ +import styled from '@emotion/styled'; +import { Box, BoxProps } from '@mantine/core'; +import { forwardRef, PropsWithChildren, useContext, useEffect, useState } from 'react'; +import { ChevronLeft, ChevronRight } from '../icons'; +import { ControlButton } from './ControlButton'; +import { DEFAULT_ELLIPSIS_NODE, DEFAULT_SIBLING_COUNT, MAX_SIBLING_COUNT, MIN_SIBLING_COUNT } from './Pagination.const'; +import { PaginationContext } from './PaginationContext'; +import { getPaginationSymbols, PaginationSymbol } from './util'; +import { clamp } from '../utils'; +import { IconControlButton } from './IconControlButton'; + +const Group = styled(Box)>( + ({ theme }) => ` + display: flex; + flex-direction: row; + align-items: center; + /* TODO: use theme value */ + gap: 0.25rem; +` +); + +export interface IControlBarProps { + /** the quantity of items to show on each side of the "current page" */ + siblingCount?: number; + /** the node to render when showing a gap between two disparate page numbers. Defaults to "..." */ + ellipsisNode?: JSX.Element; + className?: string; +} + +/** + * Primary pagination navigation component. + * + * `children` is optional, and if included, will override the default behavior. + * If using your own children, use `Pagination.ControlButton` to hook into the PaginationContext. + * @requires this component to be a child of a Pagination component + */ +export const ControlBar = forwardRef>( + ({ className, siblingCount = DEFAULT_SIBLING_COUNT, ellipsisNode = DEFAULT_ELLIPSIS_NODE, children }, ref) => { + const { currentPageNumber, totalPageCount } = useContext(PaginationContext); + const [clampedSiblingCount, setClampedSiblingCount] = useState(siblingCount); + + useEffect(() => { + // ensure the sibling count is within the allowed range + if (siblingCount < MIN_SIBLING_COUNT || siblingCount > MAX_SIBLING_COUNT) { + setClampedSiblingCount(clamp(siblingCount, MIN_SIBLING_COUNT, MAX_SIBLING_COUNT)); + } + }, [siblingCount, setClampedSiblingCount]); + + const renderCentralButton = (curPageSymbol: PaginationSymbol, index: number) => { + if (curPageSymbol === 'ELLIPSIS') { + return ( + + {ellipsisNode} + + ); + } + + return ( + { + onPageChange(curPageSymbol); + }} + isCurrentPage={curPageSymbol === currentPageNumber} + > + {curPageSymbol} + + ); + }; + + return ( + + {children || ( + <> + { + onPageChange(curPageNum - 1); + }} + disabled={currentPageNumber === 1} + > + + + {getPaginationSymbols({ totalPageCount, currentPageNumber, siblingCount: clampedSiblingCount }).map( + renderCentralButton + )} + { + onPageChange(curPageNum + 1); + }} + disabled={currentPageNumber === totalPageCount} + > + {} + + + )} + + ); + } +); diff --git a/libs/design-system/src/pagination/ControlButton.tsx b/libs/design-system/src/pagination/ControlButton.tsx new file mode 100644 index 00000000000..fc86f688c1b --- /dev/null +++ b/libs/design-system/src/pagination/ControlButton.tsx @@ -0,0 +1,84 @@ +import styled from '@emotion/styled'; +import { CSSProperties, forwardRef, useContext } from 'react'; +import { colors } from '../config'; +import { Button, IButtonProps } from '../button/Button'; +import { IPaginationContext, PaginationContext } from './PaginationContext'; + +export type TPageButtonClickHandler = (ctx: IPaginationContext) => void; + +type StylingProps = Pick; + +// TODO: Fix `theme` type once design system is ready and then use theme values +const getFontColor = ({ theme, isCurrentPage }: { theme: any } & StylingProps): string => { + return theme.colorScheme === 'dark' + ? isCurrentPage + ? colors.white + : colors.B60 + : isCurrentPage + ? colors.BGDark // TODO: speak with Design -- this is bad, we should not be using a "BG" color for font + : colors.B60; +}; + +// TODO: Fix `theme` type once design system is ready and then use theme values +const getFontWeight = ({ theme, isCurrentPage }: { theme: any } & StylingProps): CSSProperties['fontWeight'] => { + return isCurrentPage ? 700 : 600; +}; + +// TODO: Fix `theme` type once design system is ready and then use theme values +const getBackgroundColor = ({ theme, isCurrentPage }: { theme: any } & StylingProps): CSSProperties['fontWeight'] => { + return isCurrentPage ? (theme.colorScheme === 'dark' ? colors.B30 : colors.BGLight) : 'none'; +}; + +const StyledButton = styled(Button)( + ({ theme, isCurrentPage }) => ` + font-weight: ${getFontWeight({ theme, isCurrentPage })}; + background: ${getBackgroundColor({ theme, isCurrentPage })}; + color: ${getFontColor({ theme, isCurrentPage })}; + + &:disabled { + background: ${getBackgroundColor({ theme, isCurrentPage })}; + color: ${getFontColor({ theme, isCurrentPage })}; + } + + /* override mantine */ + height: inherit; + + /* TODO: theme values for next few lines */ + border-radius: 4px; + line-height: 20px; + padding: 2px 3.5px; + min-width: 24px; +` +); + +export interface IControlButtonProps extends Omit { + onClick?: TPageButtonClickHandler; + /** Does the button represent the currently-selected page */ + isCurrentPage?: boolean; +} + +/** + * Button for navigating to a specific page. + * @requires this component to be a child of a Pagination component + */ +export const ControlButton: React.FC = forwardRef( + ({ onClick, className, id, disabled, isCurrentPage, ...buttonProps }, buttonRef) => { + const paginationCtx = useContext(PaginationContext); + + // hydrate the click handler with the context + const handleClick = () => onClick?.(paginationCtx); + + return ( + + {buttonProps.children} + + ); + } +); diff --git a/libs/design-system/src/pagination/GoToPageInput.tsx b/libs/design-system/src/pagination/GoToPageInput.tsx new file mode 100644 index 00000000000..65f21a32595 --- /dev/null +++ b/libs/design-system/src/pagination/GoToPageInput.tsx @@ -0,0 +1,123 @@ +import { + FocusEventHandler, + forwardRef, + KeyboardEventHandler, + useContext, + useImperativeHandle, + useRef, + useState, +} from 'react'; +import { PaginationContext } from './PaginationContext'; +import styled from '@emotion/styled'; +import { Input, NumberInput, NumberInputProps } from '@mantine/core'; +import { Tooltip } from '../tooltip/Tooltip'; +import { FIRST_PAGE_NUMBER } from './Pagination.const'; + +export interface IGoToPageInputProps extends NumberInputProps { + firstPageNumber?: number; +} + +const InputWrapper = styled(Input.Wrapper)(({ theme }) => { + return ` + display: flex; + flex-direction: row; + align-items: center; + + input { + margin: 0; + min-height: inherit; + height: 32px; + text-align: center; + min-width: 56px; + max-width: 60px; + } + + label { + color: inherit; + font-size: inherit; + line-height: inherit; + text-wrap: nowrap; + text-overflow: ellipsis; + overflow: hidden; + /* TODO: use theme values */ + margin-right: ${'0.5rem'}; + } +`; +}); + +/** + * Input for navigating to the specified page size for Pagination. + * @requires this component to be a child of a Pagination component + */ +export const GoToPageInput: React.FC = forwardRef( + ({ label, firstPageNumber = FIRST_PAGE_NUMBER, ...inputProps }, forwardedRef) => { + const { onPageChange, totalPageCount } = useContext(PaginationContext); + const [hasError, setHasError] = useState(false); + + const validateValue = (val: number | string) => { + const numVal = +val; + + return !!numVal && numVal >= 1 && numVal <= totalPageCount; + }; + + /* + * since we are forwarding the ref, we must use useImperativeHandle to be able to + * invoke behavior on the input element + */ + const internalRef = useRef(null); + useImperativeHandle(forwardedRef, () => internalRef.current, []); + + const handleBlurEvent: FocusEventHandler = (event) => { + const val = event.currentTarget.value; + + // check for empty value and ensure user can clear error + if (!val) { + setHasError(false); + + return; + } + + if (!validateValue(val)) { + setHasError(val !== undefined); + + return; + } + onPageChange(+val); + internalRef.current.value = undefined; + setHasError(false); + }; + + const handleKeyPress: KeyboardEventHandler = (event) => { + if (event.key !== 'Enter') { + return; + } + internalRef.current.blur(); + }; + + return ( + + + + + + ); + } +); diff --git a/libs/design-system/src/pagination/IconControlButton.tsx b/libs/design-system/src/pagination/IconControlButton.tsx new file mode 100644 index 00000000000..8d8cc9178d4 --- /dev/null +++ b/libs/design-system/src/pagination/IconControlButton.tsx @@ -0,0 +1,37 @@ +import styled from '@emotion/styled'; +import { ControlButton, IControlButtonProps } from './ControlButton'; +import { colors } from '../config'; + +// TODO: Fix `theme` type once design system is ready and then use theme values +const getFontColor = ({ theme }: { theme: any }): string => { + // TODO: speak with Design -- this is bad, we should not be using a "BG" color for font + return theme.colorScheme === 'dark' ? colors.white : colors.BGDark; +}; + +// TODO: Fix `theme` type once design system is ready and then use theme values +const getDisabledFontColor = ({ theme }: { theme: any }): string => { + return theme.colorScheme === 'dark' ? colors.B40 : colors.B80; +}; + +export type IconControlButtonProps = Omit; + +/** + * Specialized ControlButton for "rich" nodes such as Icons or custom JSX elements. + */ +export const IconControlButton = styled(ControlButton)( + ({ theme }) => ` + color: ${getFontColor({ theme })}; + /* SVG / icon overrides */ + path { + fill: ${getFontColor({ theme })}; + } + + &:disabled { + color: ${getDisabledFontColor({ theme })}; + /* SVG / icon overrides */ + path { + fill: ${getDisabledFontColor({ theme })}; + } + } +` +); diff --git a/libs/design-system/src/pagination/PageSizeSelect.tsx b/libs/design-system/src/pagination/PageSizeSelect.tsx new file mode 100644 index 00000000000..253df4fe277 --- /dev/null +++ b/libs/design-system/src/pagination/PageSizeSelect.tsx @@ -0,0 +1,81 @@ +import styled from '@emotion/styled'; +import { Input } from '@mantine/core'; +import { forwardRef, useContext, useMemo } from 'react'; +import { ISelectProps, Select } from '../select/Select'; +import { DEFAULT_PAGINATION_PAGE_SIZES } from './Pagination.const'; +import { PaginationContext } from './PaginationContext'; + +const InputWrapper = styled(Input.Wrapper)(({ theme }) => { + return ` + display: flex; + flex-direction: row; + align-items: center; + + label { + color: inherit; + font-size: inherit; + line-height: inherit; + text-wrap: nowrap; + text-overflow: ellipsis; + overflow: hidden; + + /* TODO: use theme values */ + margin-left: ${'0.5rem'}; + } +`; +}); + +const StyledSelect = styled(Select)( + ({ theme }) => ` + max-width: 71px; + input { + padding-left: 12px; + padding-right: 8px; + margin: 0; + min-height: inherit; + /** Explicitly set by designs */ + height: 32px; + min-width: 44px; + } + + input:not([type=hidden]) + div { + /* TODO: use theme values */ + width: 20px; + } +` +); + +export type TPageSizeSelectOption = string; +export interface IPageSizeSelectProps extends Omit { + onPageSizeChange: (pageSize: number) => void; + pageSizes?: (number | TPageSizeSelectOption)[]; + className?: string; +} + +/** + * Component for selecting the desired page size for Pagination. + * @requires this component to be a child of a Pagination component + */ +export const PageSizeSelect: React.FC = forwardRef( + ({ onPageSizeChange, pageSizes = DEFAULT_PAGINATION_PAGE_SIZES, ...selectProps }, selectRef) => { + const { pageSize } = useContext(PaginationContext); + + const handlePageSizeChange = (val: string | string[]) => { + onPageSizeChange(+val); + }; + + const options = useMemo(() => pageSizes.map((val) => `${val}`), [pageSizes]); + + return ( + + + + ); + } +); diff --git a/libs/design-system/src/pagination/Pagination.const.ts b/libs/design-system/src/pagination/Pagination.const.ts new file mode 100644 index 00000000000..77c39e0c2b3 --- /dev/null +++ b/libs/design-system/src/pagination/Pagination.const.ts @@ -0,0 +1,19 @@ +/** Default page size options for pagination */ +export const DEFAULT_PAGINATION_PAGE_SIZES = [10, 25, 50, 100]; +export const DEFAULT_PAGE_SIZE = 10; + +export const FIRST_PAGE_NUMBER = 1; +/** based on designs -- the maximum value for which all page numbers should be shown simultaneously */ +export const MAX_PAGE_COUNT_WITHOUT_ELLIPSIS = 10; + +/** + * default number of "siblings" on pagination. A "sibling" is a page number on each side of the current page number + * E.g. pagination with 2 siblings for a current page of 14 would look like the below -- note there are 2 values to each side: + * + * 12 13 [14] 15 16 + */ +export const DEFAULT_SIBLING_COUNT = 2; +export const MIN_SIBLING_COUNT = 1; +export const MAX_SIBLING_COUNT = 5; + +export const DEFAULT_ELLIPSIS_NODE = '...'; diff --git a/libs/design-system/src/pagination/Pagination.stories.tsx b/libs/design-system/src/pagination/Pagination.stories.tsx new file mode 100644 index 00000000000..21be056043a --- /dev/null +++ b/libs/design-system/src/pagination/Pagination.stories.tsx @@ -0,0 +1,44 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import React from 'react'; +import { StoryFn, Meta } from '@storybook/react'; +import Pagination, { IPaginationProps } from './Pagination'; +import { useArgs } from '@storybook/client-api'; + +export default { + title: 'Components/Pagination', + component: Pagination, + args: { + currentPageNumber: 6, + totalPageCount: 11, + totalItemCount: 100, + onPageChange: (pageNum) => alert(pageNum), + pageSize: 10, + siblingCount: 2, + ellipsisNode: '...', + }, +} as Meta; + +const TEST_PAGE_SIZES = [10, 25, 50, 100]; + +// @ts-ignore-next-line +const Template: StoryFn = ({ siblingCount, ellipsisNode, ...args }) => { + const [{ currentPageNumber }, updateArgs] = useArgs(); + + const handlePageSizeChange = (size: number) => { + updateArgs({ pageSize: size }); + }; + + const handlePageChange = (pageNum: number) => { + updateArgs({ currentPageNumber: pageNum }); + }; + + return ( + + + + + + ); +}; + +export const PaginationStory = Template.bind({}); diff --git a/libs/design-system/src/pagination/Pagination.tsx b/libs/design-system/src/pagination/Pagination.tsx new file mode 100644 index 00000000000..7ae7398cbb9 --- /dev/null +++ b/libs/design-system/src/pagination/Pagination.tsx @@ -0,0 +1,99 @@ +import styled from '@emotion/styled'; +import { PropsWithChildren } from 'react'; +import { clamp } from '../utils'; +import { colors } from '../config'; +import { mantineConfig } from '../config/theme.config'; +import { ControlBar } from './ControlBar'; +import { ControlButton } from './ControlButton'; +import { GoToPageInput } from './GoToPageInput'; +import { PageSizeSelect } from './PageSizeSelect'; +import { FIRST_PAGE_NUMBER } from './Pagination.const'; +import { IPaginationContext, PaginationContext } from './PaginationContext'; +import { clampPageNumber } from './util'; +import { IconControlButton } from './IconControlButton'; + +const PaginationWrapper = styled.div( + ({ theme }) => ` + display: flex; + flex-direction: row; + justify-content: space-between; + + /** + * TODO: use theme values for all styles below. + * Should be enforced for all child elements as these are in the design system. + */ + color: ${theme.colorScheme === 'dark' ? colors.B60 : colors.B40}; + font-size: ${mantineConfig.fontSizes.md}px; + line-height: 20px; + + & input { + background-color: ${theme.colorScheme === 'dark' ? colors.B15 : colors.white}; + border-radius: 7px; + font-size: 14px; + } +` +); +export interface IPaginationProps extends IPaginationContext { + /** Ignore validations which clamp the current page number between 1 and totalPageCount */ + shouldSkipValidation?: boolean; + className?: string; +} + +/** + * Primary Pagination component. + * + * Use the namespaced components (like Pagination.ControlBar) as children to compose your controls. + * + * + * + * + */ +export const Pagination = ({ + currentPageNumber, + totalPageCount, + totalItemCount, + onPageChange, + pageSize, + className, + shouldSkipValidation, + children, +}: PropsWithChildren) => { + const handlePageChangeWithValidation = (pageNum: number) => { + const val = shouldSkipValidation ? pageNum : clampPageNumber(pageNum, { totalPageCount, currentPageNumber }); + onPageChange(val); + }; + + const totalPageCountToUse = shouldSkipValidation + ? totalPageCount + : clamp(totalPageCount, FIRST_PAGE_NUMBER, Number.MAX_SAFE_INTEGER); + + const curPageNumToUse = shouldSkipValidation + ? currentPageNumber + : clampPageNumber(currentPageNumber, { + totalPageCount: totalPageCountToUse, + currentPageNumber, + }); + + return ( + + {children} + + ); +}; + +Pagination.Context = PaginationContext; +Pagination.ControlButton = ControlButton; +Pagination.IconControlButton = IconControlButton; +Pagination.ControlBar = ControlBar; +Pagination.GoToPageInput = GoToPageInput; +Pagination.PageSizeSelect = PageSizeSelect; + +export default Pagination; diff --git a/libs/design-system/src/pagination/PaginationContext.tsx b/libs/design-system/src/pagination/PaginationContext.tsx new file mode 100644 index 00000000000..4c0692057a8 --- /dev/null +++ b/libs/design-system/src/pagination/PaginationContext.tsx @@ -0,0 +1,19 @@ +import { createContext } from 'react'; + +export interface IPaginationContext { + totalItemCount: number; + totalPageCount: number; + /** Note: this is a page *number*, not index */ + currentPageNumber: number; + pageSize: number; + onPageChange: (pageNumber: number) => void; +} + +const DEFAULT_PAGINATION_CONTEXT: IPaginationContext = { + totalItemCount: 0, + totalPageCount: 1, + currentPageNumber: 1, + pageSize: 10, + onPageChange: () => {}, +}; +export const PaginationContext = createContext(DEFAULT_PAGINATION_CONTEXT); diff --git a/libs/design-system/src/pagination/index.ts b/libs/design-system/src/pagination/index.ts new file mode 100644 index 00000000000..e016c96b72e --- /dev/null +++ b/libs/design-system/src/pagination/index.ts @@ -0,0 +1 @@ +export * from './Pagination'; diff --git a/libs/design-system/src/pagination/util/clampPageNumber.spec.ts b/libs/design-system/src/pagination/util/clampPageNumber.spec.ts new file mode 100644 index 00000000000..02c536679ca --- /dev/null +++ b/libs/design-system/src/pagination/util/clampPageNumber.spec.ts @@ -0,0 +1,36 @@ +import { expect, describe, it } from 'vitest'; +import { clampPageNumber } from './clampPageNumber'; + +const FIRST_PAGE_NUMBER = 1; + +describe('clampPageNumber', () => { + it('should clamp page number to the first page if it is less than the first page number', () => { + const result = clampPageNumber(0, { totalPageCount: 10, currentPageNumber: 5 }); + expect(result).to.equal(FIRST_PAGE_NUMBER); + }); + + it('should clamp page number to the total page count if it is greater than the total page count', () => { + const result = clampPageNumber(15, { totalPageCount: 10, currentPageNumber: 5 }); + expect(result).to.equal(10); + }); + + it('should return the new page number if it is within the safe bounds', () => { + const result = clampPageNumber(3, { totalPageCount: 10, currentPageNumber: 5 }); + expect(result).to.equal(3); + }); + + it('should return the current page number if the new page number is not an integer', () => { + const result = clampPageNumber(3.5, { totalPageCount: 10, currentPageNumber: 5 }); + expect(result).to.equal(5); + }); + + it('should return the first page number if total page count is zero', () => { + const result = clampPageNumber(3, { totalPageCount: 0, currentPageNumber: 5 }); + expect(result).to.equal(FIRST_PAGE_NUMBER); + }); + + it('should return the first page number if total page count is negative', () => { + const result = clampPageNumber(3, { totalPageCount: -5, currentPageNumber: 5 }); + expect(result).to.equal(FIRST_PAGE_NUMBER); + }); +}); diff --git a/libs/design-system/src/pagination/util/clampPageNumber.ts b/libs/design-system/src/pagination/util/clampPageNumber.ts new file mode 100644 index 00000000000..4de84097a68 --- /dev/null +++ b/libs/design-system/src/pagination/util/clampPageNumber.ts @@ -0,0 +1,20 @@ +import { clamp } from '../../utils'; +import { FIRST_PAGE_NUMBER } from '../Pagination.const'; +import { IPaginationContext } from '../PaginationContext'; + +/** + * Ensure that the page number is within the safe bounds of the total page count. + */ +export const clampPageNumber = ( + newPageNum: number, + { totalPageCount, currentPageNumber }: Pick, + firstPageNumber: number = FIRST_PAGE_NUMBER +): number => { + if (!Number.isInteger(newPageNum)) { + return !Number.isInteger(currentPageNumber) + ? firstPageNumber + : clamp(currentPageNumber, firstPageNumber, totalPageCount); + } + + return clamp(newPageNum, firstPageNumber, totalPageCount); +}; diff --git a/libs/design-system/src/pagination/util/getPaginationSymbols.spec.ts b/libs/design-system/src/pagination/util/getPaginationSymbols.spec.ts new file mode 100644 index 00000000000..f9b3fbae746 --- /dev/null +++ b/libs/design-system/src/pagination/util/getPaginationSymbols.spec.ts @@ -0,0 +1,411 @@ +import { expect, it, describe } from 'vitest'; +import { MAX_PAGE_COUNT_WITHOUT_ELLIPSIS } from '../Pagination.const'; +import { getPaginationSymbols, PaginationSymbol } from './getPaginationSymbols'; + +describe(getPaginationSymbols.name, () => { + it('should have a single page number for 1 page', () => { + expect(getPaginationSymbols({ totalPageCount: 1, currentPageNumber: 1, siblingCount: 2 })).toEqual([1]); + }); + + it('should have 3 page numbers for 3 pages', () => { + expect(getPaginationSymbols({ totalPageCount: 3, currentPageNumber: 1, siblingCount: 2 })).toEqual([1, 2, 3]); + }); + + it('should show all page numbers for "max pages without ellipsis" regardless of sibling count', () => { + expect( + getPaginationSymbols({ totalPageCount: MAX_PAGE_COUNT_WITHOUT_ELLIPSIS, currentPageNumber: 3, siblingCount: 0 }) + ).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + expect( + getPaginationSymbols({ totalPageCount: MAX_PAGE_COUNT_WITHOUT_ELLIPSIS, currentPageNumber: 3, siblingCount: 2 }) + ).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + expect( + getPaginationSymbols({ totalPageCount: MAX_PAGE_COUNT_WITHOUT_ELLIPSIS, currentPageNumber: 3, siblingCount: 6 }) + ).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + }); + + it('should show omit ellipsis and show page numbers when within the "window size" - 1 of page 1', () => { + expect(getPaginationSymbols({ totalPageCount: 11, currentPageNumber: 5, siblingCount: 2 })).toEqual([ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 'ELLIPSIS', + 11, + ] as PaginationSymbol[]); + }); + + it('should show show ellipsis a when at "window size" or above of page 1', () => { + expect(getPaginationSymbols({ totalPageCount: 11, currentPageNumber: 6, siblingCount: 2 })).toEqual([ + 1, + 'ELLIPSIS', + 4, + 5, + 6, + 7, + 8, + 'ELLIPSIS', + 11, + ] as PaginationSymbol[]); + }); + + it('should show assymetric siblings for a >10 pages', () => { + expect(getPaginationSymbols({ totalPageCount: 48, currentPageNumber: 24, siblingCount: 2 })).toEqual([ + 1, + 'ELLIPSIS', + 22, + 23, + 24, + 25, + 26, + 'ELLIPSIS', + 48, + ] as PaginationSymbol[]); + }); + + it('should show symmetric siblings for 100 <= pages < 10000', () => { + expect(getPaginationSymbols({ totalPageCount: 500, currentPageNumber: 101, siblingCount: 2 })).toEqual([ + 1, + 'ELLIPSIS', + 99, + 100, + 101, + 102, + 103, + 'ELLIPSIS', + 500, + ] as PaginationSymbol[]); + }); + + describe('1 sibling', () => { + const siblingCount = 1; + const totalPageCount = 100; + it('should work within the window of the first page', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 4, siblingCount })).toEqual([ + 1, + 2, + 3, + 4, + 5, + 'ELLIPSIS', + 100, + ] as PaginationSymbol[]); + }); + it('should work near, but not in window of the first page', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 5, siblingCount })).toEqual([ + 1, + 'ELLIPSIS', + 4, + 5, + 6, + 'ELLIPSIS', + 100, + ] as PaginationSymbol[]); + }); + it('should work in the middle-ish', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 20, siblingCount })).toEqual([ + 1, + 'ELLIPSIS', + 19, + 20, + 21, + 'ELLIPSIS', + 100, + ] as PaginationSymbol[]); + }); + it('should work near, but not in window of the last page', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 96, siblingCount })).toEqual([ + 1, + 'ELLIPSIS', + 95, + 96, + 97, + 'ELLIPSIS', + 100, + ] as PaginationSymbol[]); + }); + it('should work within the window of the last page', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 97, siblingCount })).toEqual([ + 1, + 'ELLIPSIS', + 96, + 97, + 98, + 99, + 100, + ] as PaginationSymbol[]); + }); + }); + + describe('3 siblings', () => { + const siblingCount = 3; + const totalPageCount = 100; + it('should work within the window of the first page', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 6, siblingCount })).toEqual([ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 'ELLIPSIS', + 100, + ] as PaginationSymbol[]); + }); + it('should work near, but not in window of the first page', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 7, siblingCount })).toEqual([ + 1, + 'ELLIPSIS', + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 'ELLIPSIS', + 100, + ] as PaginationSymbol[]); + }); + it('should work in the middle-ish', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 20, siblingCount })).toEqual([ + 1, + 'ELLIPSIS', + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 'ELLIPSIS', + 100, + ] as PaginationSymbol[]); + }); + it('should work near, but not in window of the last page', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 94, siblingCount })).toEqual([ + 1, + 'ELLIPSIS', + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 'ELLIPSIS', + 100, + ] as PaginationSymbol[]); + }); + it('should work within the window of the last page', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 95, siblingCount })).toEqual([ + 1, + 'ELLIPSIS', + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + ] as PaginationSymbol[]); + }); + }); + + describe('4 siblings', () => { + const siblingCount = 4; + const totalPageCount = 100; + it('should work within the window of the first page', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 7, siblingCount })).toEqual([ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 'ELLIPSIS', + 100, + ] as PaginationSymbol[]); + }); + it('should work near, but not in window of the first page', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 8, siblingCount })).toEqual([ + 1, + 'ELLIPSIS', + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 'ELLIPSIS', + 100, + ] as PaginationSymbol[]); + }); + it('should work in the middle-ish', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 20, siblingCount })).toEqual([ + 1, + 'ELLIPSIS', + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 'ELLIPSIS', + 100, + ] as PaginationSymbol[]); + }); + it('should work near, but not in window of the last page', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 93, siblingCount })).toEqual([ + 1, + 'ELLIPSIS', + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 'ELLIPSIS', + 100, + ] as PaginationSymbol[]); + }); + it('should work within the window of the last page', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 94, siblingCount })).toEqual([ + 1, + 'ELLIPSIS', + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + ] as PaginationSymbol[]); + }); + }); + + describe('5 siblings', () => { + const siblingCount = 5; + const totalPageCount = 100; + it('should work within the window of the first page', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 8, siblingCount })).toEqual([ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 'ELLIPSIS', + 100, + ] as PaginationSymbol[]); + }); + it('should work near, but not in window of the first page', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 9, siblingCount })).toEqual([ + 1, + 'ELLIPSIS', + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 'ELLIPSIS', + 100, + ] as PaginationSymbol[]); + }); + it('should work in the middle-ish', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 20, siblingCount })).toEqual([ + 1, + 'ELLIPSIS', + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 'ELLIPSIS', + 100, + ] as PaginationSymbol[]); + }); + it('should work near, but not in window of the last page', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 92, siblingCount })).toEqual([ + 1, + 'ELLIPSIS', + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 'ELLIPSIS', + 100, + ] as PaginationSymbol[]); + }); + it('should work within the window of the last page', () => { + expect(getPaginationSymbols({ totalPageCount, currentPageNumber: 93, siblingCount })).toEqual([ + 1, + 'ELLIPSIS', + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + ] as PaginationSymbol[]); + }); + }); +}); diff --git a/libs/design-system/src/pagination/util/getPaginationSymbols.ts b/libs/design-system/src/pagination/util/getPaginationSymbols.ts new file mode 100644 index 00000000000..b1e066c10bd --- /dev/null +++ b/libs/design-system/src/pagination/util/getPaginationSymbols.ts @@ -0,0 +1,156 @@ +import { FIRST_PAGE_NUMBER, MAX_PAGE_COUNT_WITHOUT_ELLIPSIS } from '../Pagination.const'; +import { IPaginationContext } from '../PaginationContext'; + +/** 'ELLIPSIS' is used to indicate that there are page numbers between the adjacent values that are not being shown. */ +export type PaginationSymbol = number | 'ELLIPSIS'; + +type TGetPaginationSymbolsParams = Pick & { + siblingCount: number; +}; + +/** + * Top-level function to determine what values should be used for a pagination ControlBar. + * 'ELLIPSIS' is used to indicate that there are page numbers between the adjacent values that are not being shown. + * + * For example, if the totalPageCount is 11, the currentPageNumber is 6, the siblingCount is 2, this function will return: + * [1, 'ELLIPSIS', 4, 5, 6, 7, 8, 'ELLIPSIS', 11] + */ +export const getPaginationSymbols = ({ + totalPageCount, + siblingCount, + currentPageNumber, +}: TGetPaginationSymbolsParams): PaginationSymbol[] => { + // show every page if we have {MAX_PAGE_COUNT_WITHOUT_ELLIPSIS} or fewer + if (totalPageCount <= MAX_PAGE_COUNT_WITHOUT_ELLIPSIS) { + return createArrayForRange(totalPageCount); + } + + // determine if the current page is close enough to either end to omit the ellipsis + const windowRange = getWindowRange(siblingCount); + const isInRangeOfFirstPage = checkIfCurrentPageIsNearStart({ currentPageNumber, windowRange }); + const isInRangeOfLastPage = checkIfCurrentPageIsNearEnd({ + currentPageNumber, + windowRange, + totalPageCount, + }); + + // determine how many values to show (not inluding first and last) + const windowSize = getWindowSize({ + totalPageCount, + siblingCount, + isInRangeOfFirstPage, + isInRangeOfLastPage, + }); + + /* + * determine where the window should start -- this behave differently depending on if + * the current page is in the middle or within range of either end + */ + const windowStart = getWindowStart({ + windowSize, + currentPageNumber, + totalPageCount, + isInRangeOfFirstPage, + isInRangeOfLastPage, + }); + const pageSymbols: PaginationSymbol[] = createArrayForRange(windowSize, windowStart); + + // add first page and ellipsis at the beginning + if (!isInRangeOfFirstPage) { + pageSymbols.unshift('ELLIPSIS'); + } + + // add ellipsis and last page at the end + if (!isInRangeOfLastPage) { + pageSymbols.push('ELLIPSIS'); + } + + // add first and last page numbers + pageSymbols.unshift(FIRST_PAGE_NUMBER); + pageSymbols.push(totalPageCount); + + return pageSymbols; +}; + +/** + * Note: the `function` keyword is used below to leverage function hoisting for helper functions. + */ + +/** Creates an array of specified length where each value is baseValue + its index */ +function createArrayForRange(length: number, baseValue = 1) { + return Array.from({ length }, (_, i) => i + baseValue); +} + +/** Determines if the current page is within a range of the totalPageCount */ +function checkIfCurrentPageIsNearEnd({ + currentPageNumber, + windowRange, + totalPageCount, +}: Omit & { windowRange: number }) { + return totalPageCount - currentPageNumber < windowRange; +} + +/** Determines if the current page is within a range of the first page */ +function checkIfCurrentPageIsNearStart({ + currentPageNumber, + windowRange, +}: Omit & { windowRange: number }) { + return currentPageNumber - FIRST_PAGE_NUMBER < windowRange; +} + +interface TInPaginationRangeParams { + isInRangeOfFirstPage: boolean; + isInRangeOfLastPage: boolean; +} + +/** Determines the quantity of page numbers to show, ignoring the first and last pages (which are always shown) */ +function getWindowSize({ + siblingCount, + totalPageCount, + isInRangeOfFirstPage, + isInRangeOfLastPage, +}: Omit & TInPaginationRangeParams) { + if (totalPageCount <= MAX_PAGE_COUNT_WITHOUT_ELLIPSIS) { + return totalPageCount; + } + + // window size in the middle of the range + const windowSize = siblingCount * 2 + 1; + + // determine if there's it's necessary to subsititute the ellipsis with an additional page number + const numReplacementSlots = isInRangeOfFirstPage || isInRangeOfLastPage ? 1 : 0; + + return windowSize + numReplacementSlots; +} + +/** + * Gets the "distance" within which the current page number is considered to be in range of the first or last page. + * This stems from replicating MaterialUI's implementation which has multiple layers of nuance. + */ +function getWindowRange(siblingCount: number) { + return siblingCount * 2 + (3 - siblingCount); +} + +/** + * Determines the first page number that will be included in the viewing window, ignoring the first and last page. + * + * For example, if the goal is to display 3 4 5 6 7, this function will return 3. + */ +function getWindowStart({ + windowSize, + currentPageNumber, + totalPageCount, + isInRangeOfFirstPage, + isInRangeOfLastPage, +}: Omit & { + windowSize: number; +} & TInPaginationRangeParams) { + if (isInRangeOfLastPage) { + return totalPageCount - windowSize; + } else if (isInRangeOfFirstPage) { + return FIRST_PAGE_NUMBER + 1; + } + const windowOffset = Math.ceil(windowSize / 2) - 1; + + return Math.max(currentPageNumber - windowOffset, 1); +} diff --git a/libs/design-system/src/pagination/util/index.ts b/libs/design-system/src/pagination/util/index.ts new file mode 100644 index 00000000000..03a77455241 --- /dev/null +++ b/libs/design-system/src/pagination/util/index.ts @@ -0,0 +1,2 @@ +export * from './getPaginationSymbols'; +export * from './clampPageNumber'; diff --git a/libs/design-system/src/select/Select.tsx b/libs/design-system/src/select/Select.tsx index de14ed4d0af..f2cbc6bc4b6 100644 --- a/libs/design-system/src/select/Select.tsx +++ b/libs/design-system/src/select/Select.tsx @@ -19,7 +19,7 @@ import { colors } from '../config'; import { Text } from '../index'; import { SpacingProps } from '../shared/spacing.props'; -interface ISelectProps extends SpacingProps { +export interface ISelectProps extends SpacingProps { data: (string | { value: string; label?: string } | SelectItem)[]; value?: string[] | string | null; onChange?: (value: string[] | string | null) => void; @@ -47,6 +47,7 @@ interface ISelectProps extends SpacingProps { withinPortal?: boolean; limit?: SelectProps['limit']; icon?: React.ReactNode; + className?: string; } /** @@ -69,6 +70,7 @@ export const Select = React.forwardRef( inputProps = {}, dataTestId, withinPortal = false, + className, ...props }: ISelectProps, ref @@ -121,7 +123,7 @@ export const Select = React.forwardRef( }, [loading, theme]); return ( - + {multiselect ? ( { + it('should return the value itself if it is within the inclusive bounds', () => { + const result = clamp(5, 0, 10); + expect(result).to.equal(5); + }); + + it('should clamp the value to the minimum bound if it is less than the minimum bound', () => { + const result = clamp(-5, 0, 10); + expect(result).to.equal(0); + }); + + it('should clamp the value to the maximum bound if it is greater than the maximum bound', () => { + const result = clamp(15, 0, 10); + expect(result).to.equal(10); + }); + + it('should work correctly with negative values', () => { + const result = clamp(-7, -10, 5); + expect(result).to.equal(-7); + }); + + it('should work correctly with decimal values', () => { + const result = clamp(3.5, 1, 5); + expect(result).to.equal(3.5); + }); + + it('should handle min and max being the same value', () => { + const result = clamp(5, 5, 5); + expect(result).to.equal(5); + }); + + it('should handle min being greater than max by swapping them', () => { + const result = clamp(8, 10, 5); + expect(result).to.equal(8); + }); + + it('should handle NaN values by returning NaN', () => { + const result = clamp(NaN, 0, 10); + expect(result).to.be.NaN; + }); + + it('should handle Infinity and -Infinity values correctly', () => { + const result1 = clamp(Infinity, 0, 10); + expect(result1).to.equal(10); + + const result2 = clamp(-Infinity, 0, 10); + expect(result2).to.equal(0); + }); +}); diff --git a/libs/design-system/src/utils/clamp.ts b/libs/design-system/src/utils/clamp.ts new file mode 100644 index 00000000000..6722b95cfe5 --- /dev/null +++ b/libs/design-system/src/utils/clamp.ts @@ -0,0 +1,7 @@ +/** Return value within inclusive bounds of min and max */ +export const clamp = (value: number, min: number, max: number): number => { + const verifiedMin = Math.min(min, max); + const verifiedMax = Math.max(min, max); + + return Math.max(Math.min(value, verifiedMax), verifiedMin); +}; diff --git a/libs/design-system/src/utils/index.ts b/libs/design-system/src/utils/index.ts new file mode 100644 index 00000000000..982d5fd5215 --- /dev/null +++ b/libs/design-system/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './clamp'; +export * from './notifications'; diff --git a/libs/design-system/vite.config.ts b/libs/design-system/vite.config.ts index 9d789d15ab4..1f89a27dbb8 100644 --- a/libs/design-system/vite.config.ts +++ b/libs/design-system/vite.config.ts @@ -1,3 +1,5 @@ +/// + import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { resolve } from 'path'; @@ -15,4 +17,8 @@ export default defineConfig({ external: ['react', 'react/jsx-runtime'], }, }, + /* + * // Configure vitest here -- for now the defaults work well :) + * test: {}, + */ }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46b8d111997..b6f3ffa3e5d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2107,6 +2107,9 @@ importers: '@storybook/addon-docs': specifier: ^7.4.2 version: 7.4.2(@types/react-dom@17.0.20)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2) + '@storybook/client-api': + specifier: ^7.6.10 + version: 7.6.10 '@storybook/react': specifier: ^7.4.2 version: 7.4.2(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) @@ -2170,6 +2173,9 @@ importers: vite-plugin-dts: specifier: ^3.6.0 version: 3.6.2(@types/node@18.18.5)(typescript@4.9.5)(vite@4.4.9) + vitest: + specifier: ^1.2.1 + version: 1.2.1(@types/node@18.18.5) libs/embed: dependencies: @@ -15506,6 +15512,13 @@ packages: dependencies: '@sinclair/typebox': 0.25.24 + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + /@jest/source-map@27.5.1: resolution: {integrity: sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -16609,7 +16622,7 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: - '@mantine/core': 5.10.5(@emotion/react@11.10.6)(@mantine/hooks@5.10.5)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) + '@mantine/core': 5.10.5(@emotion/react@11.10.6)(@mantine/hooks@5.10.5)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2) '@mantine/hooks': 5.10.5(react@17.0.2) '@mantine/utils': 5.10.5(react@17.0.2) react: 17.0.2 @@ -16674,7 +16687,7 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: - '@emotion/react': 11.10.6(@types/react@17.0.53)(react@17.0.2) + '@emotion/react': 11.10.6(@types/react@17.0.62)(react@17.0.2) clsx: 1.1.1 csstype: 3.0.9 react: 17.0.2 @@ -21602,6 +21615,110 @@ packages: picomatch: 2.3.1 dev: true + /@rollup/rollup-android-arm-eabi@4.9.6: + resolution: {integrity: sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm64@4.9.6: + resolution: {integrity: sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-arm64@4.9.6: + resolution: {integrity: sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-x64@4.9.6: + resolution: {integrity: sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.9.6: + resolution: {integrity: sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.9.6: + resolution: {integrity: sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.9.6: + resolution: {integrity: sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.9.6: + resolution: {integrity: sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.9.6: + resolution: {integrity: sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.9.6: + resolution: {integrity: sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.9.6: + resolution: {integrity: sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.9.6: + resolution: {integrity: sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.9.6: + resolution: {integrity: sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@rushstack/eslint-patch@1.2.0: resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==} dev: true @@ -22131,6 +22248,10 @@ packages: /@sinclair/typebox@0.25.24: resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==} + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + /@sinonjs/commons@1.8.6: resolution: {integrity: sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==} dependencies: @@ -24975,6 +25096,17 @@ packages: tiny-invariant: 1.3.1 dev: true + /@storybook/channels@7.6.10: + resolution: {integrity: sha512-ITCLhFuDBKgxetuKnWwYqMUWlU7zsfH3gEKZltTb+9/2OAWR7ez0iqU7H6bXP1ridm0DCKkt2UMWj2mmr9iQqg==} + dependencies: + '@storybook/client-logger': 7.6.10 + '@storybook/core-events': 7.6.10 + '@storybook/global': 5.0.0 + qs: 6.11.2 + telejson: 7.2.0 + tiny-invariant: 1.3.1 + dev: true + /@storybook/cli@7.4.2: resolution: {integrity: sha512-WleObtC7OU2lT+pI2vTdXZPFMKDGbg3bkUJ+PG8+yqGg53ea5ZkwKWg9qHpXuiMkYDztqhbA8kYrny1GqFuVdg==} hasBin: true @@ -25034,6 +25166,13 @@ packages: '@storybook/preview-api': 7.4.2 dev: true + /@storybook/client-api@7.6.10: + resolution: {integrity: sha512-Y9z6Uy4h3/hDAUVBEEGLLbbvnSKQJhr4Sn1wJ328PhMppcZ1+GW1iGphFBmthm+O0cun1Zevl18Y081kqiGzSQ==} + dependencies: + '@storybook/client-logger': 7.6.10 + '@storybook/preview-api': 7.6.10 + dev: true + /@storybook/client-logger@7.4.2: resolution: {integrity: sha512-LC8tYrYSJwF4DHRdNYh6y8hSvccwUIv5/WOZKJDmKx7mcEm6HsVuUu16C9jsl7iy6IqJYxgVz1va3WS6852E+A==} dependencies: @@ -25045,6 +25184,12 @@ packages: '@storybook/global': 5.0.0 dev: true + /@storybook/client-logger@7.6.10: + resolution: {integrity: sha512-U7bbpu21ntgePMz/mKM18qvCSWCUGCUlYru8mgVlXLCKqFqfTeP887+CsPEQf29aoE3cLgDrxqbRJ1wxX9kL9A==} + dependencies: + '@storybook/global': 5.0.0 + dev: true + /@storybook/codemod@7.4.2: resolution: {integrity: sha512-wU+SLHG/PpLptI0aWEhPxwFPcX7uYe+Id21DKNPg/HvYaLG3N+/DPDef+lm3Vaov9w4OD74iuQ3knT67SSkvmw==} dependencies: @@ -25182,6 +25327,12 @@ packages: ts-dedent: 2.2.0 dev: true + /@storybook/core-events@7.6.10: + resolution: {integrity: sha512-yccDH67KoROrdZbRKwxgTswFMAco5nlCyxszCDASCLygGSV2Q2e+YuywrhchQl3U6joiWi3Ps1qWu56NeNafag==} + dependencies: + ts-dedent: 2.2.0 + dev: true + /@storybook/core-server@7.4.2: resolution: {integrity: sha512-4aaFQTjb8jsbzJeCs+VTo3gdyK3r3VhQN2sxn6k/lcKjQFeO84+iqGgGmb+oWUVz2TJL+JrNh7SUXkVsMZBXVQ==} dependencies: @@ -25281,6 +25432,12 @@ packages: dependencies: type-fest: 2.19.0 + /@storybook/csf@0.1.2: + resolution: {integrity: sha512-ePrvE/pS1vsKR9Xr+o+YwdqNgHUyXvg+1Xjx0h9LrVx7Zq4zNe06pd63F5EvzTbCbJsHj7GHr9tkiaqm7U8WRA==} + dependencies: + type-fest: 2.19.0 + dev: true + /@storybook/docs-mdx@0.1.0: resolution: {integrity: sha512-JDaBR9lwVY4eSH5W8EGHrhODjygPd6QImRbwjAuJNEnY0Vw4ie3bPkeGfnacB3OBW6u/agqPv2aRlR46JcAQLg==} dev: true @@ -25536,6 +25693,25 @@ packages: util-deprecate: 1.0.2 dev: true + /@storybook/preview-api@7.6.10: + resolution: {integrity: sha512-5A3etoIwZCx05yuv3KSTv1wynN4SR4rrzaIs/CTBp3BC4q1RBL+Or/tClk0IJPXQMlx/4Y134GtNIBbkiDofpw==} + dependencies: + '@storybook/channels': 7.6.10 + '@storybook/client-logger': 7.6.10 + '@storybook/core-events': 7.6.10 + '@storybook/csf': 0.1.2 + '@storybook/global': 5.0.0 + '@storybook/types': 7.6.10 + '@types/qs': 6.9.7 + dequal: 2.0.3 + lodash: 4.17.21 + memoizerific: 1.11.3 + qs: 6.11.2 + synchronous-promise: 2.0.17 + ts-dedent: 2.2.0 + util-deprecate: 1.0.2 + dev: true + /@storybook/preview@7.4.2: resolution: {integrity: sha512-T7rAV6qQ7tBeSvgi0RuA5EU8tm1OMhU8PcCqosWGaLhMCR0VMzw1/WGKuR11g1XmzvaAm2xGgMp82nqi4G0i7A==} dev: true @@ -25820,6 +25996,15 @@ packages: file-system-cache: 2.3.0 dev: true + /@storybook/types@7.6.10: + resolution: {integrity: sha512-hcS2HloJblaMpCAj2axgGV+53kgSRYPT0a1PG1IHsZaYQILfHSMmBqM8XzXXYTsgf9250kz3dqFX1l0n3EqMlQ==} + dependencies: + '@storybook/channels': 7.6.10 + '@types/babel__core': 7.20.3 + '@types/express': 4.17.17 + file-system-cache: 2.3.0 + dev: true + /@surma/rollup-plugin-off-main-thread@2.2.3: resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} dependencies: @@ -26663,6 +26848,10 @@ packages: /@types/estree@1.0.1: resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + dev: true + /@types/express-serve-static-core@4.17.33: resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} dependencies: @@ -27987,6 +28176,45 @@ packages: vue: 3.2.47 dev: true + /@vitest/expect@1.2.1: + resolution: {integrity: sha512-/bqGXcHfyKgFWYwIgFr1QYDaR9e64pRKxgBNWNXPefPFRhgm+K3+a/dS0cUGEreWngets3dlr8w8SBRw2fCfFQ==} + dependencies: + '@vitest/spy': 1.2.1 + '@vitest/utils': 1.2.1 + chai: 4.4.1 + dev: true + + /@vitest/runner@1.2.1: + resolution: {integrity: sha512-zc2dP5LQpzNzbpaBt7OeYAvmIsRS1KpZQw4G3WM/yqSV1cQKNKwLGmnm79GyZZjMhQGlRcSFMImLjZaUQvNVZQ==} + dependencies: + '@vitest/utils': 1.2.1 + p-limit: 5.0.0 + pathe: 1.1.1 + dev: true + + /@vitest/snapshot@1.2.1: + resolution: {integrity: sha512-Tmp/IcYEemKaqAYCS08sh0vORLJkMr0NRV76Gl8sHGxXT5151cITJCET20063wk0Yr/1koQ6dnmP6eEqezmd/Q==} + dependencies: + magic-string: 0.30.5 + pathe: 1.1.1 + pretty-format: 29.7.0 + dev: true + + /@vitest/spy@1.2.1: + resolution: {integrity: sha512-vG3a/b7INKH7L49Lbp0IWrG6sw9j4waWAucwnksPB1r1FTJgV7nkBByd9ufzu6VWya/QTvQW4V9FShZbZIB2UQ==} + dependencies: + tinyspy: 2.2.0 + dev: true + + /@vitest/utils@1.2.1: + resolution: {integrity: sha512-bsH6WVZYe/J2v3+81M5LDU8kW76xWObKIURpPrOXm2pjBniBu2MERI/XP60GpS4PHU3jyK50LUutOwrx4CyHUg==} + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + dev: true + /@volar/language-core@1.10.7: resolution: {integrity: sha512-6+WI7HGqWCsKJ/bms4V45WP7eDeoGxDtLjYPrHB7QkIWVkRLIeGPzzBoonZz9kERM+Kld3W89Y+IlICejVAKhA==} dependencies: @@ -28835,6 +29063,11 @@ packages: resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} engines: {node: '>=0.4.0'} + /acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + dev: true + /acorn@7.4.1: resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} engines: {node: '>=0.4.0'} @@ -28846,6 +29079,12 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + /acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + /acorn@8.8.2: resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} engines: {node: '>=0.4.0'} @@ -30834,6 +31073,11 @@ packages: yargs-parser: 20.2.9 dev: true + /cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + /cacache@16.1.3: resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -31081,6 +31325,19 @@ packages: type-detect: 4.0.8 dev: true + /chai@4.4.1: + resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.3 + get-func-name: 2.0.2 + loupe: 2.3.6 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + /chalk-animation@1.6.0: resolution: {integrity: sha512-Q8vVq6eD5IOhWI0s9WdUawDzMRjNrR4rOCiu409eZRTIHID5OjoTTEkpGZngL/BPQnL7yYmBhlXXpPJ9SYuARw==} engines: {node: '>=4'} @@ -31199,6 +31456,12 @@ packages: resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} dev: true + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 + dev: true + /check-more-types@2.24.0: resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} engines: {node: '>= 0.8.0'} @@ -33243,7 +33506,6 @@ packages: dependencies: ms: 2.1.3 supports-color: 5.5.0 - dev: true /debug@3.2.7(supports-color@8.1.1): resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} @@ -33605,6 +33867,11 @@ packages: resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -34337,7 +34604,6 @@ packages: '@esbuild/win32-ia32': 0.19.5 '@esbuild/win32-x64': 0.19.5 dev: true - optional: true /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} @@ -34466,7 +34732,7 @@ packages: /eslint-import-resolver-node@0.3.7: resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) is-core-module: 2.13.0 resolve: 1.22.2 transitivePeerDependencies: @@ -34481,7 +34747,7 @@ packages: webpack: '>=1.11.0' dependencies: array.prototype.find: 2.2.2 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) enhanced-resolve: 0.9.1 eslint-plugin-import: 2.28.1(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-webpack@0.13.7)(eslint@8.38.0) find-root: 1.1.0 @@ -34519,7 +34785,7 @@ packages: optional: true dependencies: '@typescript-eslint/parser': 5.58.0(eslint@8.38.0)(typescript@4.9.5) - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) eslint: 8.38.0 eslint-import-resolver-node: 0.3.7 eslint-import-resolver-webpack: 0.13.7(eslint-plugin-import@2.28.1)(webpack@5.78.0) @@ -34549,7 +34815,7 @@ packages: optional: true dependencies: '@typescript-eslint/parser': 5.58.0(eslint@8.51.0)(typescript@4.9.5) - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) eslint: 8.51.0 eslint-import-resolver-node: 0.3.7 eslint-import-resolver-webpack: 0.13.7(eslint-plugin-import@2.28.1)(webpack@5.78.0) @@ -34630,7 +34896,7 @@ packages: array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) doctrine: 2.1.0 eslint: 8.38.0 eslint-import-resolver-node: 0.3.7 @@ -34665,7 +34931,7 @@ packages: array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) doctrine: 2.1.0 eslint: 8.51.0 eslint-import-resolver-node: 0.3.7 @@ -35333,6 +35599,21 @@ packages: strip-final-newline: 3.0.0 dev: true + /execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.1.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + dev: true + /executable@4.1.1: resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} engines: {node: '>=4'} @@ -36580,6 +36861,11 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + /get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + dev: true + /get-symbol-description@1.0.0: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} engines: {node: '>= 0.4'} @@ -37800,6 +38086,11 @@ packages: engines: {node: '>=12.20.0'} dev: true + /human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + dev: true + /humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} dependencies: @@ -39190,7 +39481,7 @@ packages: pretty-format: 27.5.1 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) + ts-node: 10.9.1(@types/node@16.11.7)(typescript@4.9.5) transitivePeerDependencies: - bufferutil - canvas @@ -41176,6 +41467,14 @@ packages: engines: {node: '>=14'} dev: false + /local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + engines: {node: '>=14'} + dependencies: + mlly: 1.5.0 + pkg-types: 1.0.3 + dev: true + /localforage@1.10.0: resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} dependencies: @@ -41476,6 +41775,12 @@ packages: get-func-name: 2.0.2 dev: true + /loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + dependencies: + get-func-name: 2.0.2 + dev: true + /lower-case-first@1.0.2: resolution: {integrity: sha512-UuxaYakO7XeONbKrZf5FEgkantPf5DUqDayzP5VXZrtRPdH86s4kN47I8B3TW10S4QKiE3ziHNf3kRN//okHjA==} dependencies: @@ -41591,6 +41896,13 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /magic-string@0.30.5: + resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /mailersend@1.4.6: resolution: {integrity: sha512-H01TEAQIS2xciqow401yNjvFeooCH0BHF6DuSl7H9wsiP2GEvjeuiMaeSdUOGNd2N45t+qyKCuBeyxu7J0xWEg==} dependencies: @@ -42739,6 +43051,15 @@ packages: engines: {node: '>=10'} hasBin: true + /mlly@1.5.0: + resolution: {integrity: sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==} + dependencies: + acorn: 8.11.3 + pathe: 1.1.2 + pkg-types: 1.0.3 + ufo: 1.3.2 + dev: true + /mocha@10.2.0: resolution: {integrity: sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==} engines: {node: '>= 14.0.0'} @@ -43023,6 +43344,12 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + /nanomatch@1.2.13: resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} engines: {node: '>=0.10.0'} @@ -43080,7 +43407,7 @@ packages: hasBin: true requiresBuild: true dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) iconv-lite: 0.6.3 sax: 1.2.4 transitivePeerDependencies: @@ -44523,6 +44850,13 @@ packages: dependencies: yocto-queue: 1.0.0 + /p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + dependencies: + yocto-queue: 1.0.0 + dev: true + /p-locate@2.0.0: resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} engines: {node: '>=4'} @@ -45091,6 +45425,10 @@ packages: resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} dev: true + /pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + dev: true + /pathval@1.1.1: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} dev: true @@ -45321,6 +45659,14 @@ packages: find-up: 6.3.0 dev: true + /pkg-types@1.0.3: + resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + dependencies: + jsonc-parser: 3.2.0 + mlly: 1.5.0 + pathe: 1.1.1 + dev: true + /pkg-up@3.1.0: resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} engines: {node: '>=8'} @@ -45397,7 +45743,7 @@ packages: engines: {node: '>= 0.12.0'} dependencies: async: 2.6.4 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) mkdirp: 0.5.6 transitivePeerDependencies: - supports-color @@ -46223,6 +46569,15 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 + /postcss@8.4.33: + resolution: {integrity: sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + /postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -46343,6 +46698,15 @@ packages: ansi-styles: 5.2.0 react-is: 18.2.0 + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + /pretty-hrtime@1.0.3: resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} engines: {node: '>= 0.8'} @@ -49058,6 +49422,29 @@ packages: fsevents: 2.3.3 dev: true + /rollup@4.9.6: + resolution: {integrity: sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.9.6 + '@rollup/rollup-android-arm64': 4.9.6 + '@rollup/rollup-darwin-arm64': 4.9.6 + '@rollup/rollup-darwin-x64': 4.9.6 + '@rollup/rollup-linux-arm-gnueabihf': 4.9.6 + '@rollup/rollup-linux-arm64-gnu': 4.9.6 + '@rollup/rollup-linux-arm64-musl': 4.9.6 + '@rollup/rollup-linux-riscv64-gnu': 4.9.6 + '@rollup/rollup-linux-x64-gnu': 4.9.6 + '@rollup/rollup-linux-x64-musl': 4.9.6 + '@rollup/rollup-win32-arm64-msvc': 4.9.6 + '@rollup/rollup-win32-ia32-msvc': 4.9.6 + '@rollup/rollup-win32-x64-msvc': 4.9.6 + fsevents: 2.3.3 + dev: true + /rrule@2.7.2: resolution: {integrity: sha512-NkBsEEB6FIZOZ3T8frvEBOB243dm46SPufpDckY/Ap/YH24V1zLeMmDY8OA10lk452NdrF621+ynDThE7FQU2A==} dependencies: @@ -49634,6 +50021,10 @@ packages: resolution: {integrity: sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==} dev: false + /siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + /sigmund@1.0.1: resolution: {integrity: sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==} dev: false @@ -49645,6 +50036,11 @@ packages: resolution: {integrity: sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==} engines: {node: '>=14'} + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + /signale@1.4.0: resolution: {integrity: sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==} engines: {node: '>=6'} @@ -50210,6 +50606,10 @@ packages: dependencies: escape-string-regexp: 2.0.0 + /stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + /stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} dev: true @@ -50244,6 +50644,10 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + /std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + dev: true + /stop-iteration-iterator@1.0.0: resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} engines: {node: '>= 0.4'} @@ -50582,6 +50986,12 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + /strip-literal@1.3.0: + resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} + dependencies: + acorn: 8.10.0 + dev: true + /strip-outer@1.0.1: resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} engines: {node: '>=0.10.0'} @@ -51455,6 +51865,10 @@ packages: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} dev: false + /tinybench@2.6.0: + resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} + dev: true + /tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} @@ -51471,6 +51885,16 @@ packages: '@types/tinycolor2': 1.4.3 tinycolor2: 1.6.0 + /tinypool@0.8.2: + resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy@2.2.0: + resolution: {integrity: sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==} + engines: {node: '>=14.0.0'} + dev: true + /title-case@2.1.1: resolution: {integrity: sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==} dependencies: @@ -51854,7 +52278,7 @@ packages: micromatch: 4.0.5 semver: 7.5.2 typescript: 4.9.5 - webpack: 5.78.0 + webpack: 5.78.0(@swc/core@1.3.49)(esbuild@0.18.20)(webpack-cli@5.1.4) dev: true /ts-loader@9.4.2(typescript@4.9.5)(webpack@5.82.1): @@ -52332,6 +52756,10 @@ packages: /uc.micro@1.0.6: resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + /ufo@1.3.2: + resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==} + dev: true + /uglify-js@3.17.4: resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} engines: {node: '>=0.8.0'} @@ -53037,6 +53465,27 @@ packages: vfile-message: 3.1.4 dev: true + /vite-node@1.2.1(@types/node@18.18.5): + resolution: {integrity: sha512-fNzHmQUSOY+y30naohBvSW7pPn/xn3Ib/uqm+5wAJQJiqQsU0NBR78XdRJb04l4bOFKjpTWld0XAfkKlrDbySg==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4(supports-color@8.1.1) + pathe: 1.1.1 + picocolors: 1.0.0 + vite: 5.0.12(@types/node@18.18.5) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vite-plugin-dts@3.6.2(@types/node@18.18.5)(typescript@4.9.5)(vite@4.4.9): resolution: {integrity: sha512-P2o3IJtQLJZP1aSbiAzQM35QWOucrEH7ZQlfE9cWZYetTgP0apEgd/ZxaNQGPBCWEuTYNjWWHUW9MyIlweRiRQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -53172,6 +53621,99 @@ packages: fsevents: 2.3.3 dev: true + /vite@5.0.12(@types/node@18.18.5): + resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 18.18.5 + esbuild: 0.19.5 + postcss: 8.4.33 + rollup: 4.9.6 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /vitest@1.2.1(@types/node@18.18.5): + resolution: {integrity: sha512-TRph8N8rnSDa5M2wKWJCMnztCZS9cDcgVTQ6tsTFTG/odHJ4l5yNVqvbeDJYJRZ6is3uxaEpFs8LL6QM+YFSdA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': ^1.0.0 + '@vitest/ui': ^1.0.0 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/node': 18.18.5 + '@vitest/expect': 1.2.1 + '@vitest/runner': 1.2.1 + '@vitest/snapshot': 1.2.1 + '@vitest/spy': 1.2.1 + '@vitest/utils': 1.2.1 + acorn-walk: 8.3.2 + cac: 6.7.14 + chai: 4.4.1 + debug: 4.3.4(supports-color@8.1.1) + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.5 + pathe: 1.1.1 + picocolors: 1.0.0 + std-env: 3.7.0 + strip-literal: 1.3.0 + tinybench: 2.6.0 + tinypool: 0.8.2 + vite: 5.0.12(@types/node@18.18.5) + vite-node: 1.2.1(@types/node@18.18.5) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /void-elements@2.0.1: resolution: {integrity: sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==} engines: {node: '>=0.10.0'} @@ -53935,6 +54477,15 @@ packages: isexe: 2.0.0 dev: true + /why-is-node-running@2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + /wide-align@1.1.3: resolution: {integrity: sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==} dependencies: