Skip to content

Commit c6976ec

Browse files
committed
feat(ui): re-sync alert/badge/badge-group/sonner/status-badge + theme to ui-registry v2.2.0
v2.2.0 adopts the Badge semantic-colour palette across Alert/Badge/StatusBadge and deepens the dark-mode warning/error surfaces (theme.css token additions). Re-synced the 5 vendored components Console carries (json-viewer not vendored) to the v2.2.0 published bytes + theme. Backward-compatible (variants added, none removed; type-check clean). Clears the UI audit's outdated-component finding so the cleanup PR's audit goes fully green.
1 parent 467a0e8 commit c6976ec

6 files changed

Lines changed: 140 additions & 46 deletions

File tree

frontend/src/components/redpanda-ui/components/alert.tsx

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,17 @@ import React from 'react';
55
import { cn, type SharedProps } from '../lib/utils';
66

77
const alertVariants = cva(
8-
'relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
8+
// Body text is neutral high-contrast; the tone lives in the surface, border, and icon.
9+
// NOTE: the dark-mode palette is provisional and not yet contrast-tested.
10+
'relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-grey-900 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 *:data-[slot=alert-description]:text-grey-900 dark:text-grey-50 dark:*:data-[slot=alert-description]:text-grey-50 [&>svg]:size-4 [&>svg]:translate-y-0.5',
911
{
12+
// `!border-*` overrides the global `*` border-color set in the base layer.
1013
variants: {
1114
variant: {
12-
info: 'bg-card text-card-foreground',
13-
destructive:
14-
'!border-destructive/20 bg-destructive/10 text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current',
15-
// `warning` is a neutral informational alert built from shadcn base
16-
// tokens only (bg-card + the shared border), matching shadcn's `default`
17-
// variant. It carries no custom color token, so it is inherently
18-
// dark-safe. The old value was a light-only raw-blue palette that glared
19-
// as a near-white blob in dark mode. Meaning is conveyed by the caller's
20-
// icon/content; for a colored status use `destructive` or the Badge
21-
// variants rather than tinting the Alert surface.
22-
warning: 'bg-card text-card-foreground',
23-
success:
24-
'!border-green-200 dark:!border-green-800/40 bg-green-50 text-green-800 *:data-[slot=alert-description]:text-green-800 dark:bg-green-950/30 dark:text-green-300 dark:*:data-[slot=alert-description]:text-green-300 [&>svg]:text-current',
15+
info: '!border-outline-informative bg-background-informative-subtle [&>svg]:text-informative',
16+
success: '!border-outline-success bg-background-success-subtle [&>svg]:text-success',
17+
warning: '!border-outline-warning bg-background-warning-subtle [&>svg]:text-warning',
18+
destructive: '!border-outline-error bg-background-error-subtle [&>svg]:text-destructive',
2519
},
2620
},
2721
defaultVariants: {

frontend/src/components/redpanda-ui/components/badge-group.tsx

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { cva, type VariantProps } from 'class-variance-authority';
22
import React from 'react';
33

4-
import type { BadgeSize, BadgeVariant } from './badge';
4+
// biome-ignore lint/nursery/noDeprecatedImports: BadgeVariant is intentionally re-exposed so the overflow badge keeps accepting deprecated flat strings for back-compat.
5+
import type { BadgeEmphasis, BadgeSize, BadgeTone, BadgeVariant } from './badge';
56
import { Badge } from './badge';
67
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip';
78
import { cn, type SharedProps } from '../lib/utils';
@@ -34,8 +35,10 @@ export interface BadgeGroupProps
3435
maxVisible?: number;
3536
/** Size of the overflow badge */
3637
size?: BadgeSize;
37-
/** Variant for the overflow badge */
38-
variant?: BadgeVariant;
38+
/** Semantic color (tone) for the overflow badge */
39+
tone?: BadgeTone;
40+
/** Emphasis for the overflow badge. Deprecated flat variant strings are still accepted. */
41+
variant?: BadgeEmphasis | BadgeVariant;
3942
/** Custom render function for overflow tooltip content. Receives the overflow children as an array. If omitted, no tooltip is rendered. */
4043
renderOverflowContent?: (overflowChildren: React.ReactNode[]) => React.ReactNode;
4144
}
@@ -50,7 +53,8 @@ const BadgeGroup = React.forwardRef<HTMLDivElement, BadgeGroupProps>(
5053
children,
5154
maxVisible,
5255
size = 'sm',
53-
variant = 'neutral-inverted',
56+
tone,
57+
variant = 'subtle',
5458
renderOverflowContent,
5559
...props
5660
},
@@ -62,7 +66,7 @@ const BadgeGroup = React.forwardRef<HTMLDivElement, BadgeGroupProps>(
6266
const hasOverflow = overflowChildren.length > 0;
6367

6468
const overflowBadge = (
65-
<Badge size={size} variant={variant}>
69+
<Badge size={size} tone={tone} variant={variant}>
6670
+{overflowChildren.length}
6771
</Badge>
6872
);
@@ -81,7 +85,22 @@ const BadgeGroup = React.forwardRef<HTMLDivElement, BadgeGroupProps>(
8185
(renderOverflowContent ? (
8286
<TooltipProvider>
8387
<Tooltip>
84-
<TooltipTrigger render={<span className="inline-flex cursor-pointer">{overflowBadge}</span>} />
88+
{/* Render a real <button> as the trigger: Base UI's Trigger defaults to
89+
nativeButton=true, so a non-button child (a <span>) makes it warn and
90+
drop native button semantics. A <button> keeps semantics and makes the
91+
overflow badge keyboard-focusable, opening the tooltip on focus and not
92+
just hover. */}
93+
<TooltipTrigger
94+
render={
95+
<button
96+
aria-label={`Show ${overflowChildren.length} more`}
97+
className="inline-flex cursor-pointer appearance-none rounded-full border-0 bg-transparent p-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
98+
type="button"
99+
>
100+
{overflowBadge}
101+
</button>
102+
}
103+
/>
85104
<TooltipContent>{renderOverflowContent(overflowChildren)}</TooltipContent>
86105
</Tooltip>
87106
</TooltipProvider>

frontend/src/components/redpanda-ui/components/badge.tsx

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type React from 'react';
66
import { cn, type SharedProps } from '../lib/utils';
77

88
const 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+
*/
8499
export type BadgeVariant = VariantProps<typeof badgeVariants>['variant'];
85100
export 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+
87143
export 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

frontend/src/components/redpanda-ui/components/sonner.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const Toaster = ({ testId, ...props }: ToasterProps & SharedProps) => {
2525
data-testid={testId}
2626
icons={{
2727
success: <CheckCircle className="h-4 w-4 text-success" />,
28-
info: <Info className="h-4 w-4 text-info" />,
28+
info: <Info className="h-4 w-4 text-informative" />,
2929
warning: <AlertTriangle className="h-4 w-4 text-warning" />,
3030
error: <XCircle className="h-4 w-4 text-destructive" />,
3131
loading: <Loader className="h-4 w-4 animate-spin text-muted-foreground" />,

frontend/src/components/redpanda-ui/components/status-badge.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ const DEFAULT_LABEL: Record<StatusBadgeVariant, string> = {
3636
stopping: 'Stopping',
3737
};
3838

39+
// StatusBadge keeps its historical faint secondary-tinted chrome. The `secondary`
40+
// color family is no longer a recommended Badge tone, so these styles are applied
41+
// directly here rather than via the deprecated `variant="secondary-inverted"`.
42+
const STATUS_BADGE_CHROME = 'border-transparent bg-secondary/10 text-secondary [a&]:hover:bg-secondary/20';
43+
3944
const badgeSizeStyles = cva('rounded-full', {
4045
variants: {
4146
size: {
@@ -77,12 +82,13 @@ function StatusBadge({
7782

7883
return (
7984
<Badge
80-
className={cn(badgeSizeStyles({ size }), className)}
85+
className={cn(badgeSizeStyles({ size }), STATUS_BADGE_CHROME, className)}
8186
data-slot="status-badge"
8287
data-testid={testId}
8388
icon={icon}
8489
size={size}
85-
variant="secondary-inverted"
90+
tone="neutral"
91+
variant="subtle"
8692
{...props}
8793
>
8894
{label}

frontend/src/components/redpanda-ui/style/theme.css

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
--color-red-700: #be1b0e;
106106
--color-red-800: #a21107;
107107
--color-red-900: #901704;
108+
--color-red-950: #4a1106;
108109

109110
/* Orange Scale */
110111
--color-orange-50: #fef0e7;
@@ -117,6 +118,7 @@
117118
--color-orange-700: #f77923;
118119
--color-orange-800: #da5d08;
119120
--color-orange-900: #be5107;
121+
--color-orange-950: #52260a;
120122

121123
/* Yellow Scale */
122124
--color-yellow-50: #fefbe8;
@@ -379,15 +381,15 @@
379381
--color-warning: var(--color-orange-800);
380382
--color-warning-foreground: var(--color-grey-white);
381383
--color-warning-subtle: var(--color-orange-alpha-100);
382-
--color-info: var(--color-blue-800);
383-
--color-info-foreground: var(--color-grey-white);
384-
--color-info-subtle: var(--color-blue-alpha-100);
385-
--color-neutral: var(--color-grey-600);
386-
--color-neutral-foreground: var(--color-grey-50);
387-
388-
/* Informative (alias for info) */
389384
--color-informative: var(--color-blue-800);
390385
--color-informative-foreground: var(--color-grey-white);
386+
--color-informative-subtle: var(--color-blue-alpha-100);
387+
/* `info` duplicates `informative`, kept so existing `*-info` utilities (text-info, …) don't regress. Aliased via var() so dark-mode overrides of `informative` carry through automatically. */
388+
--color-info: var(--color-informative);
389+
--color-info-foreground: var(--color-informative-foreground);
390+
--color-info-subtle: var(--color-informative-subtle);
391+
--color-neutral: var(--color-grey-600);
392+
--color-neutral-foreground: var(--color-grey-50);
391393

392394
/* Border, Input, Ring */
393395
--color-border: var(--color-grey-200);
@@ -743,9 +745,9 @@
743745
/* Background - Semantic (Dark Mode) */
744746
--color-background-success-subtle: var(--color-green-900);
745747
--color-background-success-strong: var(--color-green-500);
746-
--color-background-warning-subtle: var(--color-orange-900);
748+
--color-background-warning-subtle: var(--color-orange-950);
747749
--color-background-warning-strong: var(--color-orange-500);
748-
--color-background-error-subtle: var(--color-red-900);
750+
--color-background-error-subtle: var(--color-red-950);
749751
--color-background-error-strong: var(--color-red-500);
750752
--color-background-informative-subtle: var(--color-blue-900);
751753
--color-background-informative-strong: var(--color-blue-500);
@@ -810,16 +812,12 @@
810812
--color-warning: var(--color-orange-200);
811813
--color-warning-foreground: var(--color-grey-900);
812814
--color-warning-subtle: var(--color-orange-50);
813-
--color-info: var(--color-blue-500);
814-
--color-info-foreground: var(--color-blue-900);
815-
--color-info-subtle: var(--color-blue-alpha-200);
815+
--color-informative: var(--color-blue-500);
816+
--color-informative-foreground: var(--color-blue-900);
817+
--color-informative-subtle: var(--color-blue-alpha-200);
816818
--color-neutral: var(--color-grey-500);
817819
--color-neutral-foreground: var(--color-grey-50);
818820

819-
/* Informative */
820-
--color-informative: var(--color-blue-400);
821-
--color-informative-foreground: var(--color-blue-900);
822-
823821
/* Border, Input, Ring */
824822
--color-border-subtle: var(--color-grey-800);
825823
--color-border: var(--color-grey-700);

0 commit comments

Comments
 (0)