From 2e393f1ed52d6fcc55b91be92c07f6a96564bb35 Mon Sep 17 00:00:00 2001 From: Arno V Date: Tue, 27 Feb 2024 10:08:39 -0500 Subject: [PATCH] feat(Pill): introducing Pill component --- lib/tokens.ts | 14 ++++ packages/documentation/.ladle/components.tsx | 10 ++- .../src/Components/Pill.stories.tsx | 38 +++++++++ .../ui-components/src/common/constants.ts | 2 + .../src/components/Pill/Pill.tsx | 23 ++++++ .../src/components/Pill/PillTypes.d.ts | 23 ++++++ .../components/Pill/__tests__/Pill.test.tsx | 82 +++++++++++++++++++ .../src/components/Pill/utilities.ts | 46 +++++++++++ .../ui-components/src/components/index.ts | 4 + 9 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 packages/documentation/src/Components/Pill.stories.tsx create mode 100644 packages/ui-components/src/components/Pill/Pill.tsx create mode 100644 packages/ui-components/src/components/Pill/PillTypes.d.ts create mode 100644 packages/ui-components/src/components/Pill/__tests__/Pill.test.tsx create mode 100644 packages/ui-components/src/components/Pill/utilities.ts diff --git a/lib/tokens.ts b/lib/tokens.ts index 122cecc5..68414e15 100644 --- a/lib/tokens.ts +++ b/lib/tokens.ts @@ -24,6 +24,10 @@ export const tokens = { "surface-light": colors.slate[300], "surface-lighter": colors.slate[200], "surface-accent": "#0B93F6", + "surface-information": colors.violet[200], + "surface-success": colors.green[200], + "surface-warning": colors.orange[200], + "surface-error": colors.red[200], /** * Typography tokens. @@ -47,6 +51,11 @@ export const tokens = { "copy-error-dark": errorColorDark, "copy-error-light": errorColorLight, + "copy-information": colors.violet[800], + "copy-success": colors.green[800], + "copy-warning": colors.orange[800], + "copy-error": colors.red[800], + /** * Border tokens. */ @@ -58,6 +67,11 @@ export const tokens = { "border-error-dark": errorColorDark, "border-error-light": errorColorLight, + "border-information": colors.violet[400], + "border-success": colors.green[400], + "border-warning": colors.orange[400], + "border-error": colors.red[400], + /** * Focus tokens. */ diff --git a/packages/documentation/.ladle/components.tsx b/packages/documentation/.ladle/components.tsx index 21570666..db7f734c 100644 --- a/packages/documentation/.ladle/components.tsx +++ b/packages/documentation/.ladle/components.tsx @@ -1,6 +1,6 @@ import "./styles.css"; -import { ButtonIcon, Footer } from "@versini/ui-components"; +import { ButtonIcon, Footer, Pill } from "@versini/ui-components"; import { Flexgrid, FlexgridItem } from "@versini/ui-system"; import type { GlobalProvider } from "@ladle/react"; @@ -11,12 +11,16 @@ const renderImportLine = (importName: string, stage?: string) => { const releaseTag = stage ? stage : "alpha"; return (
- +

{importName}

-

stage: {releaseTag}

+
diff --git a/packages/documentation/src/Components/Pill.stories.tsx b/packages/documentation/src/Components/Pill.stories.tsx new file mode 100644 index 00000000..e8a1de78 --- /dev/null +++ b/packages/documentation/src/Components/Pill.stories.tsx @@ -0,0 +1,38 @@ +import type { Story } from "@ladle/react"; +import { Pill } from "@versini/ui-components"; + +export default { + title: "Components/Pill", + meta: { + importName: "Pill", + }, +}; + +export const Basic: Story = (args) => { + return ( +
+ +
+ ); +}; +Basic.args = { + description: "", +}; +Basic.argTypes = { + variant: { + options: ["error", "information", "success", "warning"], + control: { type: "radio" }, + defaultValue: "information", + }, +}; + +export const HardCoded: Story = () => { + return ( +
+ + + + +
+ ); +}; diff --git a/packages/ui-components/src/common/constants.ts b/packages/ui-components/src/common/constants.ts index ea553fe7..7774b1af 100644 --- a/packages/ui-components/src/common/constants.ts +++ b/packages/ui-components/src/common/constants.ts @@ -31,3 +31,5 @@ export const BUBBLE_CLASSNAME = "av-bubble"; export const FLEXGRID_MAX_COLUMNS = 12; export const FLEXGRID_GAP_RATIO = 0.25; + +export const PILL_CLASSNAME = "av-pill"; diff --git a/packages/ui-components/src/components/Pill/Pill.tsx b/packages/ui-components/src/components/Pill/Pill.tsx new file mode 100644 index 00000000..c59dace8 --- /dev/null +++ b/packages/ui-components/src/components/Pill/Pill.tsx @@ -0,0 +1,23 @@ +import type { PillProps } from "./PillTypes"; +import { getPillClasses } from "./utilities"; + +export const Pill = ({ + label, + className, + variant = "information", + description, + spacing, +}: PillProps) => { + const pillClassName = getPillClasses({ + label, + className, + variant, + spacing, + }); + return ( +
+ {description && {description}} + {label} +
+ ); +}; diff --git a/packages/ui-components/src/components/Pill/PillTypes.d.ts b/packages/ui-components/src/components/Pill/PillTypes.d.ts new file mode 100644 index 00000000..5ec6cbb1 --- /dev/null +++ b/packages/ui-components/src/components/Pill/PillTypes.d.ts @@ -0,0 +1,23 @@ +import type { SpacingProps } from "@versini/ui-private/dist/utilities"; + +export type PillProps = { + /** + * Content of the Pill. + */ + label: string; + /** + * CSS class(es) to add to the main component wrapper. + */ + className?: string; + /** + * Hidden label adjacent to the pill text to provide added + * context for screen reader users, ideally no more + * than 2-3 words. + */ + description?: string; + /** + * Theme of the Pill. + * @default "information" + */ + variant?: "information" | "warning" | "error" | "success"; +} & SpacingProps; diff --git a/packages/ui-components/src/components/Pill/__tests__/Pill.test.tsx b/packages/ui-components/src/components/Pill/__tests__/Pill.test.tsx new file mode 100644 index 00000000..ccdc1708 --- /dev/null +++ b/packages/ui-components/src/components/Pill/__tests__/Pill.test.tsx @@ -0,0 +1,82 @@ +import { render, screen } from "@testing-library/react"; + +import { expectToHaveClasses } from "../../../common/__tests__/helpers"; +import { PILL_CLASSNAME } from "../../../common/constants"; +import { Pill } from "../.."; + +describe("Pill (exceptions)", () => { + it("should be able to require/import from root", () => { + expect(Pill).toBeDefined(); + }); +}); + +describe("Pill modifiers", () => { + it("should render a default pill", async () => { + render(); + const pill = await screen.findByRole("text"); + expectToHaveClasses(pill, [ + PILL_CLASSNAME, + "px-2", + "py-0.5", + "text-xs", + "bg-surface-information", + "text-copy-information", + "border-border-information", + ]); + }); + + it("should render a pill with variant warning", async () => { + render(); + const pill = await screen.findByRole("text"); + expectToHaveClasses(pill, [ + PILL_CLASSNAME, + "px-2", + "py-0.5", + "text-xs", + "bg-surface-warning", + "text-copy-warning", + "border-border-warning", + ]); + }); + + it("should render a pill with variant success", async () => { + render(); + const pill = await screen.findByRole("text"); + expectToHaveClasses(pill, [ + PILL_CLASSNAME, + "px-2", + "py-0.5", + "text-xs", + "bg-surface-success", + "text-copy-success", + "border-border-success", + ]); + }); + + it("should render a pill with variant error", async () => { + render(); + const pill = await screen.findByRole("text"); + expectToHaveClasses(pill, [ + PILL_CLASSNAME, + "px-2", + "py-0.5", + "text-xs", + "bg-surface-error", + "text-copy-error", + "border-border-error", + ]); + }); + + it("should render a pill with a custom class", async () => { + render(); + const pill = await screen.findByRole("text"); + expectToHaveClasses(pill, [PILL_CLASSNAME, "custom-class"]); + }); + + it("should render a pill with a description", async () => { + render(); + await screen.findByRole("text"); + const description = await screen.findByText("this is a description"); + expect(description.className).toContain("sr-only"); + }); +}); diff --git a/packages/ui-components/src/components/Pill/utilities.ts b/packages/ui-components/src/components/Pill/utilities.ts new file mode 100644 index 00000000..7221319a --- /dev/null +++ b/packages/ui-components/src/components/Pill/utilities.ts @@ -0,0 +1,46 @@ +import { getSpacing } from "@versini/ui-private/dist/utilities"; +import clsx from "clsx"; + +import { PILL_CLASSNAME } from "../../common/constants"; +import type { PillProps } from "./PillTypes"; + +const getPillBorderClasses = ({ variant }: { variant?: string }) => { + return clsx("rounded-sm border", { + "border-border-information": variant === "information", + "border-border-warning": variant === "warning", + "border-border-success": variant === "success", + "border-border-error": variant === "error", + }); +}; + +const getPillCopyClasses = ({ variant }: { variant?: string }) => { + return clsx("not-prose", { + "text-copy-information": variant === "information", + "text-copy-warning": variant === "warning", + "text-copy-success": variant === "success", + "text-copy-error": variant === "error", + }); +}; + +const getPillBackgroundClasses = ({ variant }: { variant?: string }) => { + return clsx({ + "bg-surface-information": variant === "information", + "bg-surface-warning": variant === "warning", + "bg-surface-success": variant === "success", + "bg-surface-error": variant === "error", + }); +}; + +export const getPillClasses = (props: PillProps) => { + const { className, spacing, variant } = props; + + return clsx( + PILL_CLASSNAME, + "px-2 py-0.5 text-xs", + getSpacing(spacing), + getPillBorderClasses({ variant }), + getPillCopyClasses({ variant }), + getPillBackgroundClasses(props), + className, + ); +}; diff --git a/packages/ui-components/src/components/index.ts b/packages/ui-components/src/components/index.ts index 2b20268a..267eed35 100644 --- a/packages/ui-components/src/components/index.ts +++ b/packages/ui-components/src/components/index.ts @@ -18,6 +18,7 @@ import { Menu } from "./Menu/Menu"; import { MenuItem } from "./Menu/MenuItem"; import { MenuSeparator } from "./Menu/MenuItem"; import { Panel } from "./Panel/Panel"; +import { Pill } from "./Pill/Pill"; import { Spinner } from "./Spinner/Spinner"; import { Table, @@ -31,6 +32,8 @@ import { TextInput } from "./TextInput/TextInput"; import { TextInputMask } from "./TextInput/TextInputMask"; import { Toggle } from "./Toggle/Toggle"; +Pill; + export { Anchor, Bubble, @@ -45,6 +48,7 @@ export { MenuItem, MenuSeparator, Panel, + Pill, Spinner, Table, TableBody,