Skip to content

Commit

Permalink
Update badge component (#1019)
Browse files Browse the repository at this point in the history
* Add interactive/non-interactive badge variants and sizes

* Use the new badge on the site
  • Loading branch information
almsh authored Jan 24, 2025
1 parent a9fe18c commit 79b50af
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 73 deletions.
42 changes: 31 additions & 11 deletions ui/src/components/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,49 @@ import * as React from 'react';
import { cn } from '../lib/utils';

const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
'inline-flex items-center rounded-full border transition-colors focus:outline-none font-semibold',
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary-muted',
secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary-muted',
destructive: 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive-muted',
muted: 'border-muted-foreground text-muted-foreground hover:bg-muted-foreground hover:text-muted',
accent: 'border-transparent bg-accent text-accent-foreground hover:bg-muted-foreground hover:text-muted',
outline:
'text-primary border-primary hover:bg-primary-muted hover:text-primary-foreground hover:border-primary-muted',
default: 'border-transparent bg-primary text-primary-foreground',
faded: 'border-transparent bg-primary bg-opacity-10 text-primary',
secondary: 'border-transparent bg-secondary text-secondary-foreground',
destructive: 'border-transparent bg-destructive text-destructive-foreground',
muted: 'border-muted-foreground text-muted-foreground',
accent: 'border-transparent bg-accent text-accent-foreground',
outline: 'text-primary border-primary',
interactive:
'border-transparent bg-primary bg-opacity-10 text-primary hover:bg-opacity-100 hover:text-white focus:ring-2 focus:ring-ring focus:ring-offset-2',
'interactive-accent':
'border-transparent bg-accent bg-opacity-50 text-primary hover:bg-opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2',
'interactive-secondary':
'border-transparent bg-secondary bg-opacity-10 text-secondary hover:bg-opacity-100 hover:text-white focus:ring-2 focus:ring-ring focus:ring-offset-2',
'interactive-destructive':
'border-transparent bg-destructive bg-opacity-10 text-destructive hover:bg-opacity-100 hover:text-white focus:ring-2 focus:ring-ring focus:ring-offset-2',
'interactive-muted':
'border-muted-foreground text-muted-foreground hover:bg-muted-foreground hover:text-muted focus:ring-2 focus:ring-ring focus:ring-offset-2',
'interactive-outline':
'border-primary bg-transparent text-primary hover:bg-primary hover:text-white focus:ring-2 focus:ring-ring focus:ring-offset-2',
},
size: {
sm: 'px-2 py-0.5 text-xs',
md: 'px-2.5 py-1 text-sm',
lg: 'px-3 py-1.5 text-base',
},
},
defaultVariants: {
variant: 'default',
size: 'sm',
},
},
);

export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {
size?: 'sm' | 'md' | 'lg';
}

function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
function Badge({ className, variant, size, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant, size }), className)} {...props} />;
}

export { Badge, badgeVariants };
245 changes: 202 additions & 43 deletions ui/src/stories/badge.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,117 @@
import { ArrowRightIcon, CheckCircleIcon, ExclamationTriangleIcon } from '@heroicons/react/24/solid';
import type { Meta, StoryObj } from '@storybook/react';
import { Badge } from '../components/badge';
import { Badge, BadgeProps } from '../components/badge';

const meta = {
title: 'Components/Badge',
component: Badge,
tags: ['autodocs'],
argTypes: {
variant: {
description: 'Style variant of the badge',
options: [
'default',
'secondary',
'destructive',
'muted',
'accent',
'outline',
'interactive',
'interactive-accent',
'interactive-secondary',
'interactive-destructive',
'interactive-muted',
'interactive-outline',
],
control: { type: 'select' },
},
size: {
description: 'Size of the badge',
options: ['sm', 'md', 'lg'],
control: { type: 'select' },
},
},
} satisfies Meta<typeof Badge>;

export default meta;
type Story = StoryObj<typeof Badge>;

// Default Badge
/**
* The Badge component is used to highlight and display short pieces of information.
* It supports various variants and sizes to accommodate different use cases.
*
* Variants:
* - Basic variants:
* - default: Primary colored badge for general use
* - secondary: Secondary colored badge for less emphasis
* - destructive: For error or warning states
* - muted: For less prominent information
* - accent: For highlighted information
* - outline: Border-only style for subtle emphasis
*
* - Interactive variants (with hover effects):
* - interactive: Primary colored with opacity hover effect
* - interactive-accent: Accent colored for important interactive elements
* - interactive-secondary: Secondary colored for less prominent actions
* - interactive-destructive: For removable or dangerous actions
* - interactive-muted: For subtle interactive elements
* - interactive-outline: Outlined style that fills on hover
*
* Features:
* - Three size options (sm, md, lg)
* - Customizable through className prop
* - Support for icons and custom content
* - Accessible by default
*
* @see {@link https://www.figma.com/file/...} Figma Design
*/
export const Default: Story = {
render: () => <Badge>Default Badge</Badge>,
args: {
children: 'Badge',
},
};

// All Variants
export const Variants: Story = {
/**
* Interactive badges feature opacity and color effects on hover.
* This is the primary interactive variant, using the primary color scheme.
* For other interactive styles, see interactive-accent, interactive-secondary,
* interactive-destructive, interactive-muted, and interactive-outline variants.
*/
export const Interactive: Story = {
args: {
variant: 'interactive',
children: 'Interactive Badge',
},
};

/**
* Interactive accent badge with opacity effects.
* Useful for highlighting important interactive elements.
*/
export const InteractiveAccent: Story = {
args: {
variant: 'interactive-accent',
children: 'Interactive Accent',
},
};

/**
* Badges come in three sizes: small (default), medium, and large.
*/
export const Sizes: Story = {
render: () => (
<div className="flex flex-wrap items-center gap-4">
<Badge size="sm">Small Badge</Badge>
<Badge size="md">Medium Badge</Badge>
<Badge size="lg">Large Badge</Badge>
</div>
),
};

/**
* All available badge variants.
*/
export const AllVariants: Story = {
render: () => (
<div className="flex flex-wrap gap-4">
<Badge variant="default">Default</Badge>
Expand All @@ -25,62 +120,126 @@ export const Variants: Story = {
<Badge variant="muted">Muted</Badge>
<Badge variant="accent">Accent</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="interactive">Interactive</Badge>
<Badge variant="interactive-accent">Interactive Accent</Badge>
<Badge variant="interactive-secondary">Interactive Secondary</Badge>
<Badge variant="interactive-destructive">Interactive Destructive</Badge>
<Badge variant="interactive-muted">Interactive Muted</Badge>
<Badge variant="interactive-outline">Interactive Outline</Badge>
</div>
),
};

// Interactive States
export const Interactive: Story = {
/**
* Examples of common use cases for badges in the application:
* - Status indicators
* - Category labels
* - Counters
* - Interactive filters
*/
export const CommonUseCases: Story = {
render: () => (
<div className="flex flex-wrap gap-4">
<Badge className="cursor-pointer">Clickable Badge</Badge>
<Badge className="cursor-pointer" variant="secondary">
Clickable Secondary
</Badge>
<Badge className="cursor-pointer" variant="destructive">
Clickable Destructive
</Badge>
<div className="flex flex-col gap-4">
{/* Status indicators */}
<div className="flex gap-2">
<Badge variant="interactive" size="md">
Active
</Badge>
<Badge variant="interactive-muted" size="md">
Pending
</Badge>
<Badge variant="interactive-destructive" size="md">
Blocked
</Badge>
<Badge variant="muted" size="md">
Archived
</Badge>
</div>
{/* Content labels */}
<div className="flex gap-2">
<Badge variant="interactive-accent">Featured</Badge>
<Badge variant="interactive-secondary">Trending</Badge>
<Badge variant="interactive">New</Badge>
<Badge variant="interactive-outline">Premium</Badge>
</div>
{/* Categories and counts */}
<div className="flex gap-2">
<Badge variant="outline">Documentation</Badge>
<Badge variant="outline">Tutorial</Badge>
<Badge variant="accent">5 new</Badge>
</div>
</div>
),
};

// With Icons
export const WithIcons: Story = {
render: () => (
<div className="flex flex-wrap gap-4">
<Badge>
<span className="mr-1"></span> Online
interface WithIconsProps {
size: BadgeProps['size'];
onlineVariant: BadgeProps['variant'];
newVariant: BadgeProps['variant'];
warningVariant: BadgeProps['variant'];
onlineText: string;
newText: string;
warningText: string;
}

/**
* Example of badges with icons.
* Icons can be added as children of the Badge component.
*/
export const WithIcons: StoryObj<WithIconsProps> = {
args: {
size: 'md',
onlineVariant: 'interactive',
newVariant: 'interactive',
warningVariant: 'destructive',
onlineText: 'Online',
newText: 'New',
warningText: 'Warning',
},
argTypes: {
onlineVariant: {
control: 'select',
options: ['default', 'secondary', 'destructive', 'outline', 'interactive', 'interactive-accent'],
},
newVariant: {
control: 'select',
options: ['default', 'secondary', 'destructive', 'outline', 'interactive', 'interactive-accent'],
},
warningVariant: {
control: 'select',
options: ['default', 'secondary', 'destructive', 'outline', 'interactive', 'interactive-accent'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
},
render: (args) => (
<div className="flex gap-4">
<Badge variant={args.onlineVariant} size={args.size}>
<CheckCircleIcon className="mr-1 h-4 w-4" /> {args.onlineText}
</Badge>
<Badge variant="secondary">
New <span className="ml-1">+</span>
<Badge variant={args.newVariant} size={args.size}>
{args.newText} <ArrowRightIcon className="ml-1 h-4 w-4" />
</Badge>
<Badge variant="destructive">
<span className="mr-1"></span> Critical
<Badge variant={args.warningVariant} size={args.size}>
<ExclamationTriangleIcon className="mr-1 h-4 w-4" /> {args.warningText}
</Badge>
</div>
),
};

// Custom Styled
export const CustomStyled: Story = {
/**
* Example of customizing badges using className prop.
* While we recommend using the built-in variants and sizes,
* you can still customize badges using Tailwind classes when needed.
*/
export const CustomStyling: Story = {
render: () => (
<div className="flex flex-wrap gap-4">
<div className="flex gap-4">
<Badge className="bg-blue-500 hover:bg-blue-600">Custom Blue</Badge>
<Badge className="bg-green-500 hover:bg-green-600">Custom Green</Badge>
<Badge className="border-2">Custom Border</Badge>
<Badge className="rounded-lg">Custom Rounded</Badge>
</div>
),
};

// Different Sizes
export const Sizes: Story = {
render: () => (
<div className="flex flex-wrap items-center gap-4">
<Badge className="px-2 py-0 text-xs">Small</Badge>
<Badge>Default</Badge>
<Badge className="px-3 py-1 text-sm">Large</Badge>
<Badge className="px-4 py-1.5 text-base">Extra Large</Badge>
<Badge className="border-2 border-green-500 text-green-500">Custom Border</Badge>
<Badge className="bg-gradient-to-r from-purple-500 to-pink-500 text-white">Gradient</Badge>
</div>
),
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default async function LandingPage({ lang }: { lang: WebsiteLanguage }) {
<div>
<Popover>
<PopoverTrigger>
<Badge variant="muted">
<Badge variant="interactive-muted">
<Typography size="md" weight="normal" className="p-1">
{translator.t('landing-page.contact')}
</Typography>
Expand All @@ -52,7 +52,7 @@ export default async function LandingPage({ lang }: { lang: WebsiteLanguage }) {
<div>
<Popover>
<PopoverTrigger>
<Badge variant="muted">
<Badge variant="interactive-muted">
<Typography size="md" weight="normal" className="p-1">
{translator.t('landing-page.registration')}
</Typography>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function OurWork({ params }: DefaultPageProps) {
<div className="pt-5">
<Dialog>
<DialogTrigger className="flex cursor-pointer flex-col items-center">
<Badge variant="outline">
<Badge variant="interactive-outline">
<Typography size="md" weight="normal" className="flex items-center p-1">
<PlayIcon className="group-hover:text-secondary-foreground mr-2 h-5 w-5" />
{translator.t('our-work.watch')}
Expand Down
Loading

0 comments on commit 79b50af

Please sign in to comment.