From 7e7d10f9660aca55bc09a7340e6c295f35dfc24d Mon Sep 17 00:00:00 2001 From: MyPrototypeWhat Date: Wed, 24 Dec 2025 16:58:56 +0800 Subject: [PATCH] feat(badge): add Badge component and its variants with Storybook examples - Introduced a new Badge component with multiple visual style variants: default, secondary, destructive, and outline. - Added comprehensive Storybook stories to demonstrate the Badge component's usage, including examples with icons and as links. - Updated the component index to export the new Badge component. --- packages/ui/src/components/index.ts | 1 + .../ui/src/components/primitives/badge.tsx | 35 +++ .../components/primitives/Badge.stories.tsx | 207 ++++++++++++++++++ 3 files changed, 243 insertions(+) create mode 100644 packages/ui/src/components/primitives/badge.tsx create mode 100644 packages/ui/stories/components/primitives/Badge.stories.tsx diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index c8ed30ea0a..0c00ca2786 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -59,6 +59,7 @@ export { export { Sortable } from './composites/Sortable' /* Shadcn Primitive Components */ +export * from './primitives/badge' export * from './primitives/breadcrumb' export * from './primitives/button' export * from './primitives/checkbox' diff --git a/packages/ui/src/components/primitives/badge.tsx b/packages/ui/src/components/primitives/badge.tsx new file mode 100644 index 0000000000..e63b6dde4c --- /dev/null +++ b/packages/ui/src/components/primitives/badge.tsx @@ -0,0 +1,35 @@ +import { cn } from '@cherrystudio/ui/utils/index' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' +import * as React from 'react' + +const badgeVariants = cva( + 'inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', + { + variants: { + variant: { + default: 'border-transparent bg-background-subtle text-secondary-foreground [a&]:hover:bg-primary/90', + secondary: 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', + destructive: + 'border-transparent text-destructive bg-[red]/10 [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground' + } + }, + defaultVariants: { + variant: 'default' + } + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<'span'> & VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'span' + + return +} + +export { Badge, badgeVariants } diff --git a/packages/ui/stories/components/primitives/Badge.stories.tsx b/packages/ui/stories/components/primitives/Badge.stories.tsx new file mode 100644 index 0000000000..cf02c202bb --- /dev/null +++ b/packages/ui/stories/components/primitives/Badge.stories.tsx @@ -0,0 +1,207 @@ +import { Badge } from '@cherrystudio/ui' +import type { Meta, StoryObj } from '@storybook/react' +import { Check, X } from 'lucide-react' + +const meta: Meta = { + title: 'Components/Primitives/Badge', + component: Badge, + parameters: { + layout: 'centered', + docs: { + description: { + component: 'Displays a badge or a component that looks like a badge. Based on shadcn/ui.' + } + } + }, + tags: ['autodocs'], + argTypes: { + variant: { + control: { type: 'select' }, + options: ['default', 'secondary', 'destructive', 'outline'], + description: 'The visual style variant of the badge' + }, + asChild: { + control: { type: 'boolean' }, + description: 'Render as a child element' + }, + className: { + control: { type: 'text' }, + description: 'Additional CSS classes' + } + } +} + +export default meta +type Story = StoryObj + +// Default +export const Default: Story = { + args: { + children: 'Badge' + } +} + +// Variants +export const Secondary: Story = { + args: { + variant: 'secondary', + children: 'Secondary' + } +} + +export const Destructive: Story = { + args: { + variant: 'destructive', + children: 'Destructive' + } +} + +export const Outline: Story = { + args: { + variant: 'outline', + children: 'Outline' + } +} + +// All Variants +export const AllVariants: Story = { + render: () => ( +
+ Default + Secondary + Destructive + Outline +
+ ) +} + +// With Icons +export const WithIcon: Story = { + render: () => ( +
+ + + Success + + + + Error + + + + Completed + + + + Verified + +
+ ) +} + +// As Link +export const AsLink: Story = { + render: () => ( +
+
+

Using asChild to render as an anchor tag:

+ + + GitHub + + +
+
+

All variants as links (hover to see effect):

+ +
+
+ ) +} + +// Status Badges +export const StatusBadges: Story = { + render: () => ( +
+ Active + Pending + Failed + Draft +
+ ) +} + +// Real World Examples +export const RealWorldExamples: Story = { + render: () => ( +
+ {/* Status Indicators */} +
+

Status Indicators

+
+ Online + Away + Offline + Unknown +
+
+ + {/* Labels */} +
+

Labels

+
+ New + Featured + Hot + Beta +
+
+ + {/* Tags */} +
+

Tags

+
+ React + TypeScript + Tailwind + Shadcn +
+
+ + {/* Notification Counts */} +
+

Notification Counts

+
+ 3 + 99+ + 12 +
+
+ + {/* With Icons */} +
+

With Icons

+
+ + + Verified + + + + Rejected + +
+
+
+ ) +}