2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import React from "react";
import classNames from "@calcom/lib/classNames";
import { Icon, type IconName } from "../..";
export const badgeStyles = cva("font-medium inline-flex items-center justify-center rounded gap-x-1", {
variants: {
variant: {
default: "bg-attention text-attention",
warning: "bg-attention text-attention",
orange: "bg-attention text-attention",
success: "bg-success text-success",
green: "bg-success text-success",
gray: "bg-subtle text-emphasis",
blue: "bg-info text-info",
red: "bg-error text-error",
error: "bg-error text-error",
grayWithoutHover: "bg-gray-100 text-gray-800 dark:bg-darkgray-200 dark:text-darkgray-800",
},
size: {
sm: "px-1 py-0.5 text-xs leading-3",
md: "py-1 px-1.5 text-xs leading-3",
lg: "py-1 px-2 text-sm leading-4",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
});
type InferredBadgeStyles = VariantProps<typeof badgeStyles>;
type IconOrDot =
| {
startIcon?: IconName;
withDot?: never;
}
| { startIcon?: never; withDot?: true };
export type BadgeBaseProps = InferredBadgeStyles & {
children: React.ReactNode;
rounded?: boolean;
customStartIcon?: React.ReactNode;
} & IconOrDot;
export type BadgeProps =
/**
* This union type helps TypeScript understand that there's two options for this component:
* Either it's a div element on which the onClick prop is not allowed, or it's a button element
* on which the onClick prop is required. This is because the onClick prop is used to determine
* whether the component should be a button or a div.
*/
| (BadgeBaseProps & Omit<React.HTMLAttributes<HTMLDivElement>, "onClick"> & { onClick?: never })
| (BadgeBaseProps & Omit<React.HTMLAttributes<HTMLButtonElement>, "onClick"> & { onClick: () => void });
export const Badge = function Badge(props: BadgeProps) {
const {
customStartIcon,
variant,
className,
size,
startIcon,
withDot,
children,
rounded,
...passThroughProps
} = props;
const isButton = "onClick" in passThroughProps && passThroughProps.onClick !== undefined;
const StartIcon = startIcon;
const classes = classNames(
badgeStyles({ variant, size }),
rounded && "h-5 w-5 rounded-full p-0",
className
);
const Children = () => (
<>
{withDot ? <Icon name="dot" data-testid="go-primitive-dot" className="h-3 w-3 stroke-[3px]" /> : null}
{customStartIcon ||
(StartIcon ? (
<Icon name={StartIcon} data-testid="start-icon" className="h-3 w-3 stroke-[3px]" />
) : null)}
{children}
</>
);
const Wrapper = isButton ? "button" : "div";
return React.createElement(Wrapper, { ...passThroughProps, className: classes }, <Children />);
};

View File

@@ -0,0 +1,14 @@
import { Icon } from "../..";
import { Tooltip } from "../tooltip/Tooltip";
export function InfoBadge({ content }: { content: string }) {
return (
<>
<Tooltip side="top" content={content}>
<span title={content}>
<Icon name="info" className="text-subtle relative left-1 right-1 top-px mt-px h-4 w-4" />
</span>
</Tooltip>
</>
);
}

View File

@@ -0,0 +1,16 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Tooltip } from "../tooltip";
import { Badge } from "./Badge";
export const UpgradeOrgsBadge = function UpgradeOrgsBadge() {
const { t } = useLocale();
return (
<Tooltip content={t("orgs_upgrade_to_enable_feature")}>
<a href="https://cal.com/enterprise" target="_blank">
<Badge variant="gray">{t("upgrade")}</Badge>
</a>
</Tooltip>
);
};

View File

@@ -0,0 +1,22 @@
import Link from "next/link";
import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Tooltip } from "../tooltip";
import { Badge } from "./Badge";
export const UpgradeTeamsBadge = function UpgradeTeamsBadge() {
const { t } = useLocale();
const { hasPaidPlan } = useHasPaidPlan();
if (hasPaidPlan) return null;
return (
<Tooltip content={t("upgrade_to_enable_feature")}>
<Link href="/teams">
<Badge variant="gray">{t("upgrade")}</Badge>
</Link>
</Tooltip>
);
};

View File

@@ -0,0 +1,99 @@
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
import {
Examples,
Example,
Note,
Title,
CustomArgsTable,
VariantsTable,
VariantRow,
} from "@calcom/storybook/components";
import { Plus } from "../icon";
import { Badge } from "./Badge";
<Meta title="UI/Badge" component={Badge} />
<Title title="Badge" suffix="Brief" subtitle="Version 2.0 — Last Update: 22 Aug 2022" />
## Definition
Badges are small status descriptors for UI elements. A badge consists of a small circle, typically containing a number or other short set of characters, that appears in proximity to another object. We provide three different types of badges such as status, alert, and brand badge.
Status badge communicate status information. It is generally used within a container such as accordion and tables to label status for easy scanning.
## Structure
<CustomArgsTable of={Badge} />
<Examples title="Badge style">
<Example title="Gray">
<Badge variant="gray">Badge text</Badge>
</Example>
<Example title="Green/Success">
<Badge variant="success">Badge text</Badge>
</Example>
<Example title="Orange/Default">
<Badge variant="default">Badge text</Badge>
</Example>
<Example title="Red/Error">
<Badge variant="red">Badge text</Badge>
</Example>
</Examples>
<Examples title="Variants">
<Example title="Default">
<Badge>Button text</Badge>
</Example>
<Example title="With Dot">
<Badge withDot>Button Text</Badge>
</Example>
<Example title="With Icon">
<Badge startIcon="plus">Button Text</Badge>
</Example>
</Examples>
## Alert Badges
## Usage
Alert badge is used in conjunction with an item, profile or label to indicate numeric value and messages associated with them.
<Title offset title="Badge" suffix="Variants" />
<Canvas>
<Story
name="All Variants"
args={{
severity: "default",
label: "Badge text",
}}
argTypes={{
severity: {
control: {
type: "inline-radio",
options: ["default", "success", "gray", "error"],
},
},
label: {
control: {
type: "text",
},
},
}}>
{({ severity, label }) => (
<VariantsTable titles={["Default", "With Dot", "With Icon"]} columnMinWidth={150}>
<VariantRow variant={severity}>
<Badge variant={severity}>{label}</Badge>
<Badge variant={severity} withDot>
{label}
</Badge>
<Badge variant={severity} startIcon="plus">
{label}
</Badge>
</VariantRow>
</VariantsTable>
)}
</Story>
</Canvas>

View File

@@ -0,0 +1,91 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable playwright/missing-playwright-await */
import { render, screen, fireEvent } from "@testing-library/react";
import { vi } from "vitest";
import { Badge, badgeStyles } from "./Badge";
describe("Tests for Badge component", () => {
const variants = [
"default",
"warning",
"orange",
"success",
"green",
"gray",
"blue",
"red",
"error",
"grayWithoutHover",
];
const sizes = ["sm", "md", "lg"];
const children = "Test Badge";
test.each(variants)("Should apply variant class", (variant) => {
render(<Badge variant={variant as any}>{children}</Badge>);
const badgeClass = screen.getByText(children).className;
const badgeComponentClass = badgeStyles({ variant: variant as any });
expect(badgeClass).toEqual(badgeComponentClass);
});
test.each(sizes)("Should apply size class", (size) => {
render(<Badge size={size as any}>{children}</Badge>);
const badgeClass = screen.getByText(children).className;
const badgeComponentClass = badgeStyles({ size: size as any });
expect(badgeClass).toEqual(badgeComponentClass);
});
test("Should render without errors", () => {
render(<Badge>{children}</Badge>);
expect(screen.getByText(children)).toBeInTheDocument();
});
test("Should render WithDot if the prop is true and shouldn't render if is false", async () => {
const { rerender } = render(<Badge withDot>{children}</Badge>);
expect(await screen.findByTestId("go-primitive-dot")).toBeInTheDocument();
rerender(<Badge>{children}</Badge>);
expect(screen.queryByTestId("go-primitive-dot")).not.toBeInTheDocument();
});
test("Should render with a startIcon when startIcon prop is provided shouldn't render if is false", () => {
const { rerender } = render(<Badge customStartIcon={<svg data-testid="start-icon" />}>{children}</Badge>);
expect(screen.getByTestId("start-icon")).toBeInTheDocument();
rerender(<Badge>{children}</Badge>);
expect(screen.queryByTestId("start-icon")).not.toBeInTheDocument();
});
test("Should render as a button when onClick prop is provided and shouldn't if is not", () => {
const handleClick = vi.fn();
const { rerender } = render(<Badge onClick={handleClick}>{children}</Badge>);
const badge = screen.getByText(children);
expect(badge.tagName).toBe("BUTTON");
fireEvent.click(badge);
expect(handleClick).toHaveBeenCalledTimes(1);
rerender(<Badge>{children}</Badge>);
const updateBadge = screen.getByText(children);
expect(updateBadge.tagName).not.toBe("BUTTON");
});
test("Should render as a div when onClick prop is not provided", () => {
render(<Badge>{children}</Badge>);
const badge = screen.getByText(children);
expect(badge.tagName).toBe("DIV");
});
test("Should render children when provided", () => {
const { getByText } = render(
<Badge>
<span>Child element 1</span>
<span>Child element 2</span>
</Badge>
);
expect(getByText("Child element 1")).toBeInTheDocument();
expect(getByText("Child element 2")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,5 @@
export { Badge } from "./Badge";
export { UpgradeTeamsBadge } from "./UpgradeTeamsBadge";
export { UpgradeOrgsBadge } from "./UpgradeOrgsBadge";
export { InfoBadge } from "./InfoBadge";
export type { BadgeProps } from "./Badge";