@@ -336,7 +334,6 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
type="button"
size="icon"
color="secondary"
- combined
StartIcon={Icon.FiMoreHorizontal}
/>
diff --git a/apps/web/pages/getting-started/[[...step]].tsx b/apps/web/pages/getting-started/[[...step]].tsx
index e088354175..624a50917e 100644
--- a/apps/web/pages/getting-started/[[...step]].tsx
+++ b/apps/web/pages/getting-started/[[...step]].tsx
@@ -123,7 +123,7 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
{headers[currentStepIndex]?.skipText && (
-
onCancel()}>
+ onCancel()}>
{t("cancel")}
onClick(selected)}>
diff --git a/packages/lib/cva/cva.test.ts b/packages/lib/cva/cva.test.ts
new file mode 100644
index 0000000000..d73a113b4e
--- /dev/null
+++ b/packages/lib/cva/cva.test.ts
@@ -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" },
+ ]);
+ });
+});
diff --git a/packages/lib/cva/cva.ts b/packages/lib/cva/cva.ts
new file mode 100644
index 0000000000..14799bb7a1
--- /dev/null
+++ b/packages/lib/cva/cva.ts
@@ -0,0 +1,61 @@
+type ValidVariantTypes = string | number | null | boolean | undefined;
+type Variants = Record & { 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 = (sets: T[][]) =>
+ sets.reduce(
+ (accSets, set) => accSets.flatMap((accSet) => set.map((value) => [...accSet, value])),
+ [[]]
+ );
diff --git a/packages/lib/cva/index.ts b/packages/lib/cva/index.ts
new file mode 100644
index 0000000000..d9b53fd53c
--- /dev/null
+++ b/packages/lib/cva/index.ts
@@ -0,0 +1 @@
+export * from "./cva";
diff --git a/packages/ui/components/button/Button.tsx b/packages/ui/components/button/Button.tsx
index 8ca9edcb9c..6901a4454c 100644
--- a/packages/ui/components/button/Button.tsx
+++ b/packages/ui/components/button/Button.tsx
@@ -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) => 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;
+
export type ButtonProps = ButtonBaseProps &
(
| (Omit & LinkProps)
| (Omit & { 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(function Button(
props: ButtonProps,
@@ -63,13 +138,12 @@ export const Button = forwardRef