Added proper dark mode support for buttons (#5603)

* Added proper dark mode support for buttons, and converted buttons to use CVA for better maintainable variant styling.

* Added animations to buttons.

* Added cva types to buttonbase type since thats imported in different places

* Fixed issue with styled buttons when false was pas for disabled instead of undefined. Added a small util function that now accepts arrays of variants, and creates all the possible combinations. This way we have less duplicate compoundvariants defined. This fixes the styles in the eventsinglelayout component.

* Undo disabling of api jest tests.

* Fixed remaining buttons using combined prop, which is replace by button group.

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
This commit is contained in:
Jeroen Reumkens 2022-11-22 18:07:55 +01:00 committed by GitHub
parent 16eede9bb0
commit d64400d66b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 258 additions and 84 deletions

View File

@ -6,5 +6,8 @@
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"spellright.language": ["en"],
"spellright.documentTypes": ["markdown", "typescript"]
"spellright.documentTypes": ["markdown", "typescript"],
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}

@ -1 +1 @@
Subproject commit 2219900e06c3a683c85ce066e6ea3eb2d6ae14e9
Subproject commit 7d53a77c07ab2b2f0fd0567b4d8ee4905d448442

View File

@ -19,7 +19,7 @@ export function VariantsTable({
<div
className={classNames(
isDark &&
"relative py-8 before:absolute before:left-0 before:top-0 before:block before:h-full before:w-screen before:bg-[#22252A]"
"relative py-8 before:absolute before:left-0 before:top-0 before:block before:h-full before:w-screen before:bg-[#1C1C1C]"
)}>
<div className="z-1 relative mx-auto w-full max-w-[1200px] overflow-auto pr-8 pt-6">
<table>

View File

@ -860,7 +860,7 @@ const BookingPage = ({
{!eventType.disableGuests && !guestToggle && (
<Button
type="button"
color="minimalSecondary"
color="minimal"
size="icon"
tooltip={t("additional_guests")}
StartIcon={Icon.FiUserPlus}
@ -878,7 +878,6 @@ const BookingPage = ({
</Button>
<Button
type="submit"
className="dark:bg-darkmodebrand dark:text-darkmodebrandcontrast dark:hover:border-darkmodebrandcontrast mr-auto dark:border-transparent"
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
loading={mutation.isLoading || recurringMutation.isLoading}>
{rescheduleUid ? t("reschedule") : t("confirm")}

View File

@ -202,7 +202,6 @@ function EventTypeSingleLayout({
color="secondary"
size="icon"
StartIcon={Icon.FiLink}
combined
tooltip={t("copy_link")}
onClick={() => {
navigator.clipboard.writeText(permalink);

View File

@ -4,14 +4,14 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
import { Dialog } from "@calcom/ui/Dialog";
import { ButtonBaseProps } from "@calcom/ui/components/button";
import { ButtonProps } from "@calcom/ui/components/button";
import showToast from "@calcom/ui/v2/core/notifications";
export default function DisconnectIntegration(props: {
/** Integration credential id */
id: number;
externalId?: string;
render: (renderProps: ButtonBaseProps) => JSX.Element;
render: (renderProps: ButtonProps) => JSX.Element;
onOpenChange: (isOpen: boolean) => unknown | Promise<unknown>;
}) {
const { id, externalId = "" } = props;

View File

@ -311,7 +311,6 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
size="icon"
href={calLink}
StartIcon={Icon.FiExternalLink}
combined
/>
</Tooltip>
@ -324,7 +323,6 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
showToast(t("link_copied"), "success");
navigator.clipboard.writeText(calLink);
}}
combined
/>
</Tooltip>
<Dropdown modal={false}>
@ -336,7 +334,6 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
type="button"
size="icon"
color="secondary"
combined
StartIcon={Icon.FiMoreHorizontal}
/>
</DropdownMenuTrigger>

View File

@ -123,7 +123,7 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
{headers[currentStepIndex]?.skipText && (
<div className="flex w-full flex-row justify-center">
<Button
color="minimalSecondary"
color="minimal"
data-testid="skip-step"
onClick={(event) => {
event.preventDefault();

View File

@ -136,7 +136,6 @@ export const FormActionsDropdown = ({ form, children }: { form: RoutingForm; chi
<Button
type="button"
size="icon"
combined
color="secondary"
className={classNames("radix-state-open:rounded-r-md", disabled && "opacity-30")}
StartIcon={Icon.FiMoreHorizontal}

View File

@ -78,7 +78,6 @@ export default function RoutingForms({
target="_blank"
StartIcon={Icon.FiExternalLink}
color="secondary"
combined
size="icon"
disabled={disabled}
/>
@ -87,7 +86,6 @@ export default function RoutingForms({
routingForm={form}
action="copyLink"
color="secondary"
combined
size="icon"
StartIcon={Icon.FiLink}
disabled={disabled}

View File

@ -125,7 +125,6 @@ export default function MemberListItem(props: Props) {
color="secondary"
size="icon"
StartIcon={Icon.FiClock}
combined
/>
</Tooltip>
<Tooltip content={t("view_public_page")}>
@ -136,7 +135,6 @@ export default function MemberListItem(props: Props) {
className={classNames(!editMode ? "rounded-r-md" : "")}
size="icon"
StartIcon={Icon.FiExternalLink}
combined
/>
</Tooltip>
{editMode && (
@ -147,7 +145,6 @@ export default function MemberListItem(props: Props) {
size="icon"
className="rounded-r-md"
StartIcon={Icon.FiMoreHorizontal}
combined
/>
</DropdownMenuTrigger>
<DropdownMenuContent>

View File

@ -149,7 +149,6 @@ export default function TeamListItem(props: Props) {
}}
size="icon"
StartIcon={Icon.FiLink}
combined
/>
</Tooltip>
)}

View File

@ -144,7 +144,6 @@ export default function WorkflowListPage({ workflows }: Props) {
type="button"
color="secondary"
size="icon"
combined
StartIcon={Icon.FiEdit2}
onClick={async () => await router.replace("/workflows/" + workflow.id)}
/>
@ -156,7 +155,6 @@ export default function WorkflowListPage({ workflows }: Props) {
setwWorkflowToDeleteId(workflow.id);
}}
color="secondary"
combined
size="icon"
StartIcon={Icon.FiTrash2}
/>
@ -178,7 +176,6 @@ export default function WorkflowListPage({ workflows }: Props) {
<Button
type="button"
color="minimal"
combined
StartIcon={Icon.FiEdit2}
onClick={async () => await router.replace("/workflows/" + workflow.id)}>
{t("edit")}

View File

@ -384,7 +384,7 @@ const CopyTimes = ({
</div>
<hr />
<div className="space-x-2 px-2">
<Button color="minimalSecondary" onClick={() => onCancel()}>
<Button color="minimal" onClick={() => onCancel()}>
{t("cancel")}
</Button>
<Button color="primary" onClick={() => onClick(selected)}>

View File

@ -0,0 +1,52 @@
import { applyStyleToMultipleVariants } from "./cva";
describe("CVA Utils", () => {
it("Should return an array of all possible variants", () => {
const variants = {
color: ["blue", "red"],
size: ["small", "medium", "large"],
className: "text-blue w-10",
};
const result = applyStyleToMultipleVariants(variants);
expect(result).toEqual([
{ color: "blue", size: "small", className: "text-blue w-10" },
{ color: "blue", size: "medium", className: "text-blue w-10" },
{ color: "blue", size: "large", className: "text-blue w-10" },
{ color: "red", size: "small", className: "text-blue w-10" },
{ color: "red", size: "medium", className: "text-blue w-10" },
{ color: "red", size: "large", className: "text-blue w-10" },
]);
});
it("Should no erorr when no arrays are passed in", () => {
const variants = {
color: "blue",
size: "large",
className: "text-blue w-10",
};
const result = applyStyleToMultipleVariants(variants);
expect(result).toEqual([{ color: "blue", size: "large", className: "text-blue w-10" }]);
});
it("Should accept numbers, null values, booleans and undefined in arrays as well", () => {
const variants = {
color: ["blue", null],
size: ["small", 30, false, undefined],
className: "text-blue w-10",
};
const result = applyStyleToMultipleVariants(variants);
expect(result).toEqual([
{ color: "blue", size: "small", className: "text-blue w-10" },
{ color: "blue", size: 30, className: "text-blue w-10" },
{ color: "blue", size: false, className: "text-blue w-10" },
{ color: "blue", size: undefined, className: "text-blue w-10" },
{ color: null, size: "small", className: "text-blue w-10" },
{ color: null, size: 30, className: "text-blue w-10" },
{ color: null, size: false, className: "text-blue w-10" },
{ color: null, size: undefined, className: "text-blue w-10" },
]);
});
});

61
packages/lib/cva/cva.ts Normal file
View File

@ -0,0 +1,61 @@
type ValidVariantTypes = string | number | null | boolean | undefined;
type Variants = Record<string, ValidVariantTypes | ValidVariantTypes[]> & { className: string };
/**
* Lets you use arrays for variants as well. This util combines all possible
* variants and returns an array with all possible options. Simply
* spread this in the compoundVariants.
*/
export const applyStyleToMultipleVariants = (variants: Variants) => {
const allKeysThatAreArrays = Object.keys(variants).filter((key) => Array.isArray(variants[key]));
const allKeysThatAreNotArrays = Object.keys(variants).filter((key) => !Array.isArray(variants[key]));
// Creates an object of all static options, ready to be merged in later with the array values.
const nonArrayOptions = allKeysThatAreNotArrays.reduce((acc, key) => {
return { ...acc, [key]: variants[key] };
}, {});
// Creates an array of all possible combinations of the array values.
// Eg if the variants object is { color: ["blue", "red"], size: ["small", "medium"] }
// then the result will be:
// [
// { color: "blue", size: "small" },
// { color: "blue", size: "medium" },
// { color: "red", size: "small" },
// { color: "red", size: "medium" },
// ]
const cartesianProductOfAllArrays = cartesianProduct(
allKeysThatAreArrays.map((key) => variants[key]) as ValidVariantTypes[][]
);
return cartesianProductOfAllArrays.map((variant) => {
const variantObject = variant.reduce((acc, value, index) => {
return { ...acc, [allKeysThatAreArrays[index]]: value };
}, {});
return {
...nonArrayOptions,
...variantObject,
};
});
};
/**
* A cartesian product is a final array that combines multiple arrays in ALL
* variations possible. For example:
*
* You have 3 arrays: [a, b], [1, 2], [y, z]
* The final result will be an array with all the different combinations:
* ["a", 1, "y"], ["a", 1, "z"], ["a", 2, "y"], ["a", 2, "z"], ["b", 1, "y"], etc
*
* We use this to create a params object for the static pages that combine multiple
* dynamic properties like 'stage' and 'meansOfTransport'. Resulting in an array
* with all different path combinations possible.
*
* @source: https://stackoverflow.com/questions/12303989/cartesian-product-of-multiple-arrays-in-javascript
* TS Inspiration: https://gist.github.com/ssippe/1f92625532eef28be6974f898efb23ef
*/
export const cartesianProduct = <T extends ValidVariantTypes>(sets: T[][]) =>
sets.reduce<T[][]>(
(accSets, set) => accSets.flatMap((accSet) => set.map((value) => [...accSet, value])),
[[]]
);

View File

@ -0,0 +1 @@
export * from "./cva";

View File

@ -1,24 +1,14 @@
import { cva, VariantProps } from "class-variance-authority";
import Link, { LinkProps } from "next/link";
import React, { forwardRef } from "react";
import { Icon } from "react-feather";
import classNames from "@calcom/lib/classNames";
import { applyStyleToMultipleVariants } from "@calcom/lib/cva";
import Tooltip from "../../v2/core/Tooltip";
export type ButtonBaseProps = {
/* Primary: Signals most important actions at any given point in the application.
Secondary: Gives visual weight to actions that are important
Minimal: Used for actions that we want to give very little significane to */
color?: keyof typeof variantClassName;
/**Default: H = 36px (default)
Large: H = 38px (Onboarding, modals)
Icon: Makes the button be an icon button */
size?: "base" | "lg" | "icon";
/**Signals the button is loading */
loading?: boolean;
/** Disables the button from being clicked */
disabled?: boolean;
/** Action that happens when the button is clicked */
onClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
/**Left aligned icon*/
@ -28,34 +18,119 @@ export type ButtonBaseProps = {
shallow?: boolean;
/**Tool tip used when icon size is set to small */
tooltip?: string;
/** @deprecated This has now been replaced by button group. */
combined?: boolean;
flex?: boolean;
};
} & VariantProps<typeof buttonClasses>;
export type ButtonProps = ButtonBaseProps &
(
| (Omit<JSX.IntrinsicElements["a"], "href" | "onClick" | "ref"> & LinkProps)
| (Omit<JSX.IntrinsicElements["button"], "onClick" | "ref"> & { href?: never })
);
const variantClassName = {
primary:
"border border-transparent text-white bg-brand-500 hover:bg-brand-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-brand-500",
secondary: "border border-gray-200 text-brand-900 bg-white hover:bg-gray-100",
minimal:
"text-gray-700 bg-transparent hover:bg-gray-100 focus:outline-none focus:ring-offset-1 focus:bg-gray-100 focus:ring-brand-900 dark:text-darkgray-900 hover:dark:text-gray-50",
minimalSecondary:
"text-gray-700 bg-transparent hover:bg-gray-100 dark:hover:bg-darkgray-200 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:bg-gray-100 focus:ring-brand-900 dark:text-darkgray-900 hover:dark:text-gray-50 border border-transparent hover:border-gray-300 dark:hover:border-darkgray-300",
destructive:
"text-gray-900 focus:text-red-700 bg-transparent hover:bg-red-100 hover:text-red-700 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:bg-red-100 focus:ring-red-700",
};
const variantDisabledClassName = {
primary: "border border-transparent bg-brand-500 bg-opacity-20 text-white",
secondary: "border border-gray-200 text-brand-900 bg-white opacity-30",
minimal: "text-gray-400 bg-transparent",
minimalSecondary: "text-gray-400 bg-transparent",
destructive: "text-red-700 bg-transparent opacity-30",
};
const buttonClasses = cva(
"inline-flex items-center text-sm font-medium relative rounded-md transition-colors",
{
variants: {
color: {
primary: "text-white dark:text-black",
secondary: "text-gray-900 dark:text-darkgray-900",
minimal: "text-gray-900 dark:text-darkgray-900",
destructive: "",
},
size: {
base: "h-9 px-4 py-2.5 ",
lg: "h-[36px] px-4 py-2.5 ",
icon: "flex justify-center min-h-[36px] min-w-[36px] ",
},
loading: {
true: "cursor-wait",
},
disabled: {
true: "cursor-not-allowed",
},
},
compoundVariants: [
// Primary variants
{
disabled: true,
color: "primary",
className: "bg-gray-800 bg-opacity-30 dark:bg-opacity-30 dark:bg-darkgray-800",
},
{
loading: true,
color: "primary",
className: "bg-gray-800/30 text-white/30 dark:bg-opacity-30 dark:bg-darkgray-700 dark:text-black/30",
},
...applyStyleToMultipleVariants({
disabled: [undefined, false],
color: "primary",
className:
"bg-brand-500 hover:bg-brand-400 focus:border focus:border-white focus:outline-none focus:ring-2 focus:ring-offset focus:ring-brand-500 dark:hover:bg-darkgray-600 dark:bg-darkgray-900",
}),
// Secondary variants
{
disabled: true,
color: "secondary",
className:
"border border-gray-200 bg-opacity-30 text-gray-900/30 bg-white dark:bg-darkgray-100 dark:text-darkgray-900/30 dark:border-darkgray-200",
},
{
loading: true,
color: "secondary",
className:
"bg-gray-100 text-gray-900/30 dark:bg-darkgray-100 dark:text-darkgray-900/30 dark:border-darkgray-200",
},
...applyStyleToMultipleVariants({
disabled: [undefined, false],
color: "secondary",
className:
"border border-gray-300 dark:border-darkgray-300 hover:bg-gray-50 hover:border-gray-400 focus:bg-gray-100 dark:hover:bg-darkgray-200 dark:focus:bg-darkgray-200 focus:outline-none focus:ring-2 focus:ring-offset focus:ring-gray-900 dark:focus:ring-white",
}),
// Minimal variants
{
disabled: true,
color: "minimal",
className:
"border:gray-200 bg-opacity-30 text-gray-900/30 dark:bg-darkgray-100 dark:text-darkgray-900/30 dark:border-darkgray-200",
},
{
loading: true,
color: "minimal",
className:
"bg-gray-100 text-gray-900/30 dark:bg-darkgray-100 dark:text-darkgray-900/30 dark:border-darkgray-200",
},
applyStyleToMultipleVariants({
disabled: [undefined, false],
color: "minimal",
className:
"hover:bg-gray-100 focus:bg-gray-100 dark:hover:bg-darkgray-200 dark:focus:bg-darkgray-200 focus:outline-none focus:ring-2 focus:ring-offset focus:ring-gray-900 dark:focus:ring-white",
}),
// Destructive variants
{
disabled: true,
color: "destructive",
className:
"text-red-700/30 dark:text-red-700/30 bg-red-100/40 dark:bg-red-100/80 border border-red-200",
},
{
loading: true,
color: "destructive",
className:
"text-red-700/30 dark:text-red-700/30 hover:text-red-700/30 bg-red-100 border border-red-200",
},
...applyStyleToMultipleVariants({
disabled: [false, undefined],
color: "destructive",
className:
"border dark:text-white text-gray-900 hover:text-red-700 focus:text-red-700 dark:hover:text-red-700 dark:focus:text-red-700 hover:border-red-100 focus:border-red-100 hover:bg-red-100 focus:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset focus:ring-red-700",
}),
],
defaultVariants: {
color: "primary",
size: "base",
},
}
);
export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonProps>(function Button(
props: ButtonProps,
@ -63,13 +138,12 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonPr
) {
const {
loading = false,
color = "primary",
size = "base",
color,
size,
type = "button",
StartIcon,
EndIcon,
shallow,
combined = false,
// attributes propagated from `HTMLAnchorProps` or `HTMLButtonProps`
...passThroughProps
} = props;
@ -86,17 +160,7 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonPr
type: !isLink ? type : undefined,
ref: forwardedRef,
className: classNames(
// base styles independent what type of button it is
"inline-flex items-center text-sm font-medium relative",
// different styles depending on size
size === "base" && "h-9 px-4 py-2.5 ",
size === "lg" && "h-[36px] px-4 py-2.5 ",
size === "icon" && "flex justify-center min-h-[36px] min-w-[36px] ",
combined ? "" : "rounded-md",
// different styles depending on color
// set not-allowed cursor if disabled
disabled ? variantDisabledClassName[color] : variantClassName[color],
loading ? "cursor-wait" : disabled ? "cursor-not-allowed" : "",
buttonClasses({ color, size, loading, disabled: props.disabled }),
props.className
),
// if we click a disabled button, we prevent going through the click handler
@ -116,10 +180,7 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonPr
{loading && (
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform">
<svg
className={classNames(
"mx-4 h-5 w-5 animate-spin",
color === "primary" ? "text-white dark:text-black" : "text-black"
)}
className="mx-4 h-5 w-5 animate-spin text-black dark:text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24">

View File

@ -80,31 +80,36 @@ Button are clickable elements that initiates user actions. Labels in the button
<Canvas>
<Story name="All variants">
<VariantsTable titles={['Primary', 'Secondary', 'Minimal']} columnMinWidth={150}>
<VariantsTable titles={['Primary', 'Secondary', 'Minimal', 'Destructive']} columnMinWidth={150}>
<VariantRow variant="Default">
<Button>Button text</Button>
<Button color="secondary">Button text</Button>
<Button color="minimal">Button text</Button>
<Button color="destructive">Button text</Button>
</VariantRow>
<VariantRow variant="Hover">
<Button className="sb-pseudo--hover">Button text</Button>
<Button className="sb-pseudo--hover" color="secondary">Button text</Button>
<Button className="sb-pseudo--hover" color="minimal">Button text</Button>
<Button className="sb-pseudo--hover" color="destructive">Button text</Button>
</VariantRow>
<VariantRow variant="Focus">
<Button className="sb-pseudo--focus">Button text</Button>
<Button className="sb-pseudo--focus" color="secondary">Button text</Button>
<Button className="sb-pseudo--focus" color="minimal">Button text</Button>
<Button className="sb-pseudo--focus" color="destructive">Button text</Button>
</VariantRow>
<VariantRow variant="Loading">
<Button loading>Button text</Button>
<Button loading color="secondary">Button text</Button>
<Button loading color="minimal">Button text</Button>
<Button loading color="destructive">Button text</Button>
</VariantRow>
<VariantRow variant="Disabled">
<Button disabled>Button text</Button>
<Button disabled color="secondary">Button text</Button>
<Button disabled color="minimal">Button text</Button>
<Button disabled color="destructive">Button text</Button>
</VariantRow>
</VariantsTable>
</Story>

View File

@ -12,18 +12,19 @@
"lint:report": "eslint . --format json --output-file ../../lint-results/ui.json"
},
"dependencies": {
"@tanstack/react-query": "^4.3.9",
"@calcom/lib": "*",
"@calcom/trpc": "*",
"@formkit/auto-animate": "^1.0.0-beta.1",
"@radix-ui/react-dialog": "^1.0.0",
"@radix-ui/react-portal": "^1.0.0",
"@radix-ui/react-select": "^0.1.1",
"@tanstack/react-query": "^4.3.9",
"class-variance-authority": "^0.3.0",
"downshift": "^6.1.9",
"next": "^12.3.1",
"react": "^18.2.0",
"react-colorful": "^5.6.0",
"react-feather": "^2.0.10",
"@formkit/auto-animate": "^1.0.0-beta.1",
"react-hook-form": "^7.34.2",
"react-icons": "^4.4.0",
"react-select": "^5.4.0"

View File

@ -291,12 +291,7 @@ const MobileSettingsContainer = (props: { onSideContainerOpen?: () => void }) =>
<>
<nav className="fixed z-20 flex w-full items-center justify-between border-b border-gray-100 bg-gray-50 p-4 sm:relative lg:hidden">
<div className="flex items-center space-x-3 ">
<Button
StartIcon={Icon.FiMenu}
color="minimalSecondary"
size="icon"
onClick={props.onSideContainerOpen}
/>
<Button StartIcon={Icon.FiMenu} color="minimal" size="icon" onClick={props.onSideContainerOpen} />
<a href="/" className="flex items-center space-x-2 rounded-md px-3 py-1 hover:bg-gray-200">
<Icon.FiArrowLeft className="text-gray-700" />
<p className="font-semibold text-black">{t("settings")}</p>

View File

@ -7222,11 +7222,16 @@
dependencies:
"@types/node" "*"
"@types/node@*", "@types/node@16.9.1", "@types/node@>=8.1.0", "@types/node@^12.12.54", "@types/node@^12.12.6", "@types/node@^14.0.10 || ^16.0.0", "@types/node@^14.14.20 || ^16.0.0":
"@types/node@*", "@types/node@16.9.1", "@types/node@>=8.1.0", "@types/node@^14.0.10 || ^16.0.0", "@types/node@^14.14.20 || ^16.0.0":
version "16.9.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708"
integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==
"@types/node@^12.12.54", "@types/node@^12.12.6":
version "12.20.55"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240"
integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==
"@types/nodemailer@^6.4.5":
version "6.4.5"
resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.5.tgz#09011ac73259245475d1688e4ba101860567dc39"
@ -10272,6 +10277,11 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
class-variance-authority@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.3.0.tgz#40c824bfbe5c604ecb3e337a2353b4b86278888e"
integrity sha512-TFO+pzY9Gedqv8crPhprd647wxhvfpKevPPjiMcteEWsnkHX9yZrD1xMY3ZhRZnLwHUHCCP0LYO6KZIVag/5wQ==
classnames@^2.2.6, classnames@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"