diff --git a/packages/ui/src/v2/Badge/Badge.stories.tsx b/packages/ui/src/v2/Badge/Badge.stories.tsx new file mode 100644 index 000000000..423d13f67 --- /dev/null +++ b/packages/ui/src/v2/Badge/Badge.stories.tsx @@ -0,0 +1,100 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import { CheckCircleIcon } from "@phosphor-icons/react"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { Badge } from "./Badge"; +import { BadgeSkeleton } from "./BadgeSkeleton"; + +const sizes = [1, 2, 3] as const; +const variants = ["solid", "soft", "surface", "outline"] as const; +const colors = ["neutral", "gold", "red", "green", "amber", "sky"] as const; + +export default { + title: "v2/Badge", + component: Badge, + args: { + children: "Badge", + size: 1, + variant: "soft", + color: "neutral", + highContrast: false, + }, +} satisfies Meta; + +type Story = StoryObj; + +export const Playground: Story = {}; + +export const Sizes: Story = { + render: () => ( +
+ {sizes.map(size => ( + Badge + ))} +
+ ), +}; + +export const Variants: Story = { + render: () => ( +
+ {variants.map(variant => ( + {variant} + ))} +
+ ), +}; + +export const Colors: Story = { + render: () => ( +
+ {variants.map(variant => ( +
+ {colors.map(color => ( + {color} + ))} +
+ ))} +
+ ), +}; + +export const HighContrast: Story = { + render: () => ( +
+ {colors.map(color => ( + {color} + ))} +
+ ), +}; + +export const WithIcon: Story = { + render: () => ( +
+ }>Approved +
+ ), +}; + +export const Skeleton: Story = { + render: () => ( +
+ + +
+ ), +}; diff --git a/packages/ui/src/v2/Badge/Badge.tsx b/packages/ui/src/v2/Badge/Badge.tsx new file mode 100644 index 000000000..f5131468e --- /dev/null +++ b/packages/ui/src/v2/Badge/Badge.tsx @@ -0,0 +1,38 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { ComponentProps, ReactNode } from "react"; +import type { VariantProps } from "tailwind-variants/lite"; + +import { badge } from "./variants"; + +export type BadgeProps + = Omit, "color"> + & VariantProps + & { + iconStart?: ReactNode; + }; + +// Small status label (Radix "Badge"). Renders a non-interactive . See +// contrib/claude/ui.md. +export function Badge(props: BadgeProps) { + const { size, variant, color, highContrast, className, iconStart, children, ...rest } = props; + + return ( + + {iconStart} + {children} + + ); +} diff --git a/packages/ui/src/v2/Badge/BadgeSkeleton.tsx b/packages/ui/src/v2/Badge/BadgeSkeleton.tsx new file mode 100644 index 000000000..9eabaf544 --- /dev/null +++ b/packages/ui/src/v2/Badge/BadgeSkeleton.tsx @@ -0,0 +1,28 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { ComponentProps } from "react"; +import type { VariantProps } from "tailwind-variants/lite"; + +import { badgeSkeleton } from "./variants"; + +export type BadgeSkeletonProps = Omit, "children"> & VariantProps; + +// Loading placeholder paired with Badge: a pulse block matching its size and +// radius. +export function BadgeSkeleton(props: BadgeSkeletonProps) { + const { size, className, ...rest } = props; + + return ; +} diff --git a/packages/ui/src/v2/Badge/variants.ts b/packages/ui/src/v2/Badge/variants.ts new file mode 100644 index 000000000..391419349 --- /dev/null +++ b/packages/ui/src/v2/Badge/variants.ts @@ -0,0 +1,115 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import { tv } from "tailwind-variants/lite"; + +// Small status label (Radix "Badge"). A non-interactive . The +// variant × color surface treatment resolves in the compound variants below. +export const badge = tv({ + base: "inline-flex w-fit items-center justify-center border border-transparent font-medium whitespace-nowrap align-middle", + variants: { + // Corner radius is bound to size (per the Figma, radius is global/theme, + // not a per-instance prop). + size: { + 1: "gap-1 rounded-1 px-1.5 py-0.5 text-1 [&_svg]:size-3", + 2: "gap-1 rounded-2 px-2 py-0.5 text-2 [&_svg]:size-4", + 3: "gap-1.5 rounded-2 px-2.5 py-1 text-3 [&_svg]:size-4", + }, + variant: { + solid: "", + soft: "", + surface: "", + outline: "", + }, + color: { + neutral: "", + gold: "", + red: "", + green: "", + amber: "", + sky: "", + }, + highContrast: { + true: "", + false: "", + }, + }, + compoundVariants: [ + // solid: filled step-9 background, white text (dark for light hues) + { variant: "solid", color: "neutral", class: "bg-sand-9 text-white" }, + { variant: "solid", color: "gold", class: "bg-gold-9 text-white" }, + { variant: "solid", color: "red", class: "bg-red-9 text-white" }, + { variant: "solid", color: "green", class: "bg-green-9 text-white" }, + { variant: "solid", color: "amber", class: "bg-amber-9 text-amber-12" }, + { variant: "solid", color: "sky", class: "bg-sky-9 text-sky-12" }, + { variant: "solid", color: "neutral", highContrast: true, class: "bg-sand-12 text-sand-1" }, + { variant: "solid", color: "gold", highContrast: true, class: "bg-gold-12 text-gold-1" }, + { variant: "solid", color: "red", highContrast: true, class: "bg-red-12 text-red-1" }, + { variant: "solid", color: "green", highContrast: true, class: "bg-green-12 text-green-1" }, + { variant: "solid", color: "amber", highContrast: true, class: "bg-amber-12 text-amber-1" }, + { variant: "solid", color: "sky", highContrast: true, class: "bg-sky-12 text-sky-1" }, + + // soft: tinted step-3 background + { variant: "soft", color: "neutral", class: "bg-sand-3 text-sand-11" }, + { variant: "soft", color: "gold", class: "bg-gold-3 text-gold-11" }, + { variant: "soft", color: "red", class: "bg-red-3 text-red-11" }, + { variant: "soft", color: "green", class: "bg-green-3 text-green-11" }, + { variant: "soft", color: "amber", class: "bg-amber-3 text-amber-11" }, + { variant: "soft", color: "sky", class: "bg-sky-3 text-sky-11" }, + + // surface: subtle background + border + { variant: "surface", color: "neutral", class: "bg-sand-2 border-sand-6 text-sand-11" }, + { variant: "surface", color: "gold", class: "bg-gold-2 border-gold-6 text-gold-11" }, + { variant: "surface", color: "red", class: "bg-red-2 border-red-6 text-red-11" }, + { variant: "surface", color: "green", class: "bg-green-2 border-green-6 text-green-11" }, + { variant: "surface", color: "amber", class: "bg-amber-2 border-amber-6 text-amber-11" }, + { variant: "surface", color: "sky", class: "bg-sky-2 border-sky-6 text-sky-11" }, + + // outline: border only + { variant: "outline", color: "neutral", class: "border-sand-6 text-sand-11" }, + { variant: "outline", color: "gold", class: "border-gold-6 text-gold-11" }, + { variant: "outline", color: "red", class: "border-red-6 text-red-11" }, + { variant: "outline", color: "green", class: "border-green-6 text-green-11" }, + { variant: "outline", color: "amber", class: "border-amber-6 text-amber-11" }, + { variant: "outline", color: "sky", class: "border-sky-6 text-sky-11" }, + + // high-contrast text for the tinted variants + { variant: ["soft", "surface", "outline"], color: "neutral", highContrast: true, class: "text-sand-12" }, + { variant: ["soft", "surface", "outline"], color: "gold", highContrast: true, class: "text-gold-12" }, + { variant: ["soft", "surface", "outline"], color: "red", highContrast: true, class: "text-red-12" }, + { variant: ["soft", "surface", "outline"], color: "green", highContrast: true, class: "text-green-12" }, + { variant: ["soft", "surface", "outline"], color: "amber", highContrast: true, class: "text-amber-12" }, + { variant: ["soft", "surface", "outline"], color: "sky", highContrast: true, class: "text-sky-12" }, + ], + defaultVariants: { + size: 1, + variant: "soft", + color: "neutral", + highContrast: false, + }, +}); + +export const badgeSkeleton = tv({ + base: "inline-block shrink-0 animate-pulse bg-sand-3 align-middle", + variants: { + size: { + 1: "h-5 w-12 rounded-1", + 2: "h-6 w-14 rounded-2", + 3: "h-7 w-16 rounded-2", + }, + }, + defaultVariants: { + size: 1, + }, +});