@@ -6,7 +6,7 @@ import type React from 'react';
66import { cn , type SharedProps } from '../lib/utils' ;
77
88const badgeVariants = cva (
9- 'group/badge inline-flex max-w-full shrink-0 items-center justify-center overflow-hidden truncate text-ellipsis whitespace-nowrap rounded-md border font-medium transition-[color,box-shadow] selection:bg-selected selection:text-selected-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none' ,
9+ 'group/badge inline-flex max-w-full shrink-0 items-center justify-center overflow-hidden truncate text-ellipsis whitespace-nowrap rounded-full border font-medium transition-[color,box-shadow] selection:bg-selected selection:text-selected-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none' ,
1010 {
1111 variants : {
1212 variant : {
@@ -21,9 +21,9 @@ const badgeVariants = cva(
2121
2222 info : 'border-transparent bg-surface-informative text-inverse [a&]:hover:bg-surface-informative-hover' ,
2323 'info-inverted' :
24- 'border-transparent bg-background-informative-subtle text-info [a&]:hover:bg-background-informative-subtle-hover' ,
24+ 'border-transparent bg-background-informative-subtle text-informative [a&]:hover:bg-background-informative-subtle-hover' ,
2525 'info-outline' :
26- 'border-outline-informative bg-transparent text-info [a&]:hover:bg-background-informative-subtle' ,
26+ 'border-outline-informative bg-transparent text-informative [a&]:hover:bg-background-informative-subtle' ,
2727
2828 accent : 'border-transparent bg-brand text-inverse [a&]:hover:bg-surface-brand-hover' ,
2929 'accent-inverted' : 'border-transparent bg-background-brand-subtle text-brand [a&]:hover:bg-brand-alpha-default' ,
@@ -81,17 +81,93 @@ const badgeVariants = cva(
8181 }
8282) ;
8383
84+ /**
85+ * Recommended semantic color axis. Pair with {@link BadgeEmphasis} via the
86+ * `tone` and `variant` props: `<Badge tone="success" variant="subtle" />`.
87+ */
88+ export type BadgeTone = 'neutral' | 'primary' | 'accent' | 'info' | 'success' | 'warning' | 'destructive' ;
89+
90+ /** Recommended emphasis axis. `subtle` is the soft-fill style (formerly `*-inverted`). */
91+ export type BadgeEmphasis = 'solid' | 'subtle' | 'outline' ;
92+
93+ /**
94+ * @deprecated Use the two-axis `tone` + `variant` (solid|subtle|outline) API instead.
95+ * The flat semantic strings (e.g. `success-inverted`, `primary-outline`) are retained for
96+ * back-compat and render identically, but will be removed in a future major version.
97+ * Migration: `variant="success-inverted"` → `tone="success" variant="subtle"`.
98+ */
8499export type BadgeVariant = VariantProps < typeof badgeVariants > [ 'variant' ] ;
85100export type BadgeSize = VariantProps < typeof badgeVariants > [ 'size' ] ;
86101
102+ const EMPHASIS_VALUES = new Set < BadgeEmphasis > ( [ 'solid' , 'subtle' , 'outline' ] ) ;
103+
104+ const isEmphasis = ( value : unknown ) : value is BadgeEmphasis => EMPHASIS_VALUES . has ( value as BadgeEmphasis ) ;
105+
106+ /** Map a (tone, emphasis) pair to the underlying flat `badgeVariants` key. */
107+ function toneToVariant ( tone : BadgeTone , emphasis : BadgeEmphasis ) : BadgeVariant {
108+ if ( emphasis === 'solid' ) {
109+ return tone ;
110+ }
111+ return `${ tone } -${ emphasis === 'subtle' ? 'inverted' : 'outline' } ` as BadgeVariant ;
112+ }
113+
114+ /**
115+ * Resolve the two-axis API (and disabled state) down to a single flat
116+ * `badgeVariants` key, preserving back-compat for deprecated flat strings.
117+ */
118+ function resolveBadgeVariant ( tone : BadgeTone | undefined , variant : BadgeEmphasis | BadgeVariant , disabled : boolean ) {
119+ if ( disabled ) {
120+ if ( variant === 'subtle' ) {
121+ return 'disabled-inverted' ;
122+ }
123+ if ( variant === 'outline' ) {
124+ return 'disabled-outline' ;
125+ }
126+ return 'disabled' ;
127+ }
128+ // Two-axis path: an explicit tone means `variant` is read as an emphasis (default solid).
129+ if ( tone ) {
130+ return toneToVariant ( tone , isEmphasis ( variant ) ? variant : 'solid' ) ;
131+ }
132+ // Emphasis shorthand without a tone falls back to the neutral tone.
133+ if ( variant === 'solid' ) {
134+ return 'neutral' ;
135+ }
136+ if ( variant === 'subtle' ) {
137+ return 'neutral-inverted' ;
138+ }
139+ // Anything else is a (deprecated) flat variant string — including the generic `outline`.
140+ return variant as BadgeVariant ;
141+ }
142+
87143export type BadgeProps = useRender . ComponentProps < 'span' > &
88144 SharedProps & {
89145 icon ?: React . ReactNode ;
90- variant ?: BadgeVariant ;
146+ /** Semantic color. Recommended; pair with `variant` for emphasis. */
147+ tone ?: BadgeTone ;
148+ /**
149+ * Emphasis (`solid` | `subtle` | `outline`) when `tone` is set. Deprecated flat
150+ * strings (e.g. `success-inverted`) are still accepted — see {@link BadgeVariant}.
151+ */
152+ variant ?: BadgeEmphasis | BadgeVariant ;
91153 size ?: BadgeSize ;
154+ /** Renders the disabled appearance regardless of `tone`. */
155+ disabled ?: boolean ;
92156 } ;
93157
94- function Badge ( { className, variant = 'neutral' , size, testId, icon, children, render, ...props } : BadgeProps ) {
158+ function Badge ( {
159+ className,
160+ tone,
161+ variant = 'solid' ,
162+ size,
163+ testId,
164+ icon,
165+ children,
166+ render,
167+ disabled = false ,
168+ ...props
169+ } : BadgeProps ) {
170+ const resolvedVariant = resolveBadgeVariant ( tone , variant , disabled ) ;
95171 // A custom `render` element owns its children (no `icon` composition); the default span composes `icon` + children.
96172 let content : React . ReactNode = children ;
97173 if ( ! render ) {
@@ -117,12 +193,13 @@ function Badge({ className, variant = 'neutral', size, testId, icon, children, r
117193 render,
118194 state : {
119195 slot : 'badge' ,
120- variant,
196+ variant : resolvedVariant ,
121197 } ,
122198 props : mergeProps < 'span' > (
123199 {
124- className : cn ( badgeVariants ( { variant, size } ) , className ) ,
200+ className : cn ( badgeVariants ( { variant : resolvedVariant , size } ) , className ) ,
125201 'data-testid' : testId ,
202+ 'aria-disabled' : disabled || undefined ,
126203 children : content ,
127204 } as React . ComponentPropsWithRef < 'span' > ,
128205 props
0 commit comments