diff --git a/ui/src/components/badge.tsx b/ui/src/components/badge.tsx index 6e00e44b1..e7f738cd0 100644 --- a/ui/src/components/badge.tsx +++ b/ui/src/components/badge.tsx @@ -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, VariantProps {} +export interface BadgeProps extends React.HTMLAttributes, VariantProps { + size?: 'sm' | 'md' | 'lg'; +} -function Badge({ className, variant, ...props }: BadgeProps) { - return
; +function Badge({ className, variant, size, ...props }: BadgeProps) { + return
; } export { Badge, badgeVariants }; diff --git a/ui/src/stories/badge.stories.tsx b/ui/src/stories/badge.stories.tsx index 55059394d..e05d4b439 100644 --- a/ui/src/stories/badge.stories.tsx +++ b/ui/src/stories/badge.stories.tsx @@ -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; export default meta; type Story = StoryObj; -// 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: () => Default 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: () => ( +
+ Small Badge + Medium Badge + Large Badge +
+ ), +}; + +/** + * All available badge variants. + */ +export const AllVariants: Story = { render: () => (
Default @@ -25,62 +120,126 @@ export const Variants: Story = { Muted Accent Outline + Interactive + Interactive Accent + Interactive Secondary + Interactive Destructive + Interactive Muted + Interactive Outline
), }; -// 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: () => ( -
- Clickable Badge - - Clickable Secondary - - - Clickable Destructive - +
+ {/* Status indicators */} +
+ + Active + + + Pending + + + Blocked + + + Archived + +
+ {/* Content labels */} +
+ Featured + Trending + New + Premium +
+ {/* Categories and counts */} +
+ Documentation + Tutorial + 5 new +
), }; -// With Icons -export const WithIcons: Story = { - render: () => ( -
- - 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 = { + 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) => ( +
+ + {args.onlineText} - - New + + + {args.newText} - - Critical + + {args.warningText}
), }; -// 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: () => ( -
+
Custom Blue - Custom Green - Custom Border - Custom Rounded -
- ), -}; - -// Different Sizes -export const Sizes: Story = { - render: () => ( -
- Small - Default - Large - Extra Large + Custom Border + Gradient
), }; diff --git a/website/src/app/[lang]/[region]/(website)/about-us/(sections)/landing-page.tsx b/website/src/app/[lang]/[region]/(website)/about-us/(sections)/landing-page.tsx index d049cf8e8..aba31f916 100644 --- a/website/src/app/[lang]/[region]/(website)/about-us/(sections)/landing-page.tsx +++ b/website/src/app/[lang]/[region]/(website)/about-us/(sections)/landing-page.tsx @@ -27,7 +27,7 @@ export default async function LandingPage({ lang }: { lang: WebsiteLanguage }) {
- + {translator.t('landing-page.contact')} @@ -52,7 +52,7 @@ export default async function LandingPage({ lang }: { lang: WebsiteLanguage }) {
- + {translator.t('landing-page.registration')} diff --git a/website/src/app/[lang]/[region]/(website)/our-work/(sections)/our-work.tsx b/website/src/app/[lang]/[region]/(website)/our-work/(sections)/our-work.tsx index 832148102..851c979c1 100644 --- a/website/src/app/[lang]/[region]/(website)/our-work/(sections)/our-work.tsx +++ b/website/src/app/[lang]/[region]/(website)/our-work/(sections)/our-work.tsx @@ -27,7 +27,7 @@ export async function OurWork({ params }: DefaultPageProps) {
- + {translator.t('our-work.watch')} diff --git a/website/src/app/[lang]/[region]/(website)/partners/(components)/PartnerBadges.tsx b/website/src/app/[lang]/[region]/(website)/partners/(components)/PartnerBadges.tsx index 8d6c78446..86df6e12b 100644 --- a/website/src/app/[lang]/[region]/(website)/partners/(components)/PartnerBadges.tsx +++ b/website/src/app/[lang]/[region]/(website)/partners/(components)/PartnerBadges.tsx @@ -6,7 +6,6 @@ import { } from '@/app/[lang]/[region]/(website)/partners/(types)/PartnerBadges'; import { UsersIcon } from '@heroicons/react/24/solid'; import { Badge, HoverCard, HoverCardContent, HoverCardTrigger, Separator, Typography } from '@socialincome/ui'; -import { cn } from '@socialincome/ui/src/lib/utils'; import Image from 'next/image'; function RecipientsBadge({ @@ -22,18 +21,15 @@ function RecipientsBadge({ translatorBadgeFormer, translatorBadgeSuspended, }: RecipientsBadgeType) { - let badgeClassName = 'bg-primary hover:bg-primary text-primary bg-opacity-10 hover:bg-opacity-100 hover:text-white'; - if (isInsideHoverCard) { - badgeClassName = cn(badgeClassName, ' py-2'); - } + const size = isInsideHoverCard ? 'md' : 'sm'; const userIconClassName = isInsideHoverCard ? 'mr-2 h-5 w-5 rounded-full' : 'mr-1 h-4 w-4 rounded-full'; return ( - + - + {hoverCardTotalRecipients || 0} {isInsideHoverCard ? translatorBadgeRecipients : ''} @@ -46,17 +42,17 @@ function RecipientsBadge({
- + {hoverCardTotalActiveRecipients || 0} {translatorBadgeActive} - + {hoverCardTotalFormerRecipients || 0} {translatorBadgeFormer} - + {hoverCardTotalSuspendedRecipients || 0} {translatorBadgeSuspended} @@ -78,7 +74,7 @@ function SDGBadge({ return ( - + {translatorSdgTitle} @@ -106,7 +102,7 @@ function FundraiserBadge({ fundRaiserTranslation }: FundRaiserBadgeType) { return ( - + {fundRaiserTranslation} diff --git a/website/src/app/[lang]/[region]/(website)/partners/(components)/PartnerHome.tsx b/website/src/app/[lang]/[region]/(website)/partners/(components)/PartnerHome.tsx index 60eace2f8..60a975ee6 100644 --- a/website/src/app/[lang]/[region]/(website)/partners/(components)/PartnerHome.tsx +++ b/website/src/app/[lang]/[region]/(website)/partners/(components)/PartnerHome.tsx @@ -101,7 +101,7 @@ export function PartnerHome({ currentNgo, currentNgoCountry, translations, lang, translatorBadgeFormer={translations.badgeFormer} translatorBadgeSuspended={translations.badgeSuspended} /> - + {countryBadge?.countryFlagComponent || } {currentNgoCountry} @@ -129,7 +129,7 @@ export function PartnerHome({ currentNgo, currentNgoCountry, translations, lang,
)} - {ngoHoverCard.orgDescriptionParagraphs.map((paragraph, index) => { + {ngoHoverCard.orgDescriptionParagraphs?.map((paragraph, index) => { return (
{paragraph.map((fragment, index2) => { diff --git a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/selection-process.tsx b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/selection-process.tsx index a870a7ef6..c7f344cac 100644 --- a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/selection-process.tsx +++ b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/selection-process.tsx @@ -108,7 +108,7 @@ export function SelectionProcess({ lang }: DefaultParams) { {translator?.t('section-3.preselection-desc')}
- + {translator?.t('section-3.goal')} {translator?.t('section-3.preselection-goal')} @@ -142,7 +142,7 @@ export function SelectionProcess({ lang }: DefaultParams) { {translator?.t('section-3.selection-desc')}
- + {translator?.t('section-3.goal')} {translator?.t('section-3.selection-goal')}