style(ui): update shared form and surface primitives

Align badge, checkbox, input, select, textarea, and tooltip with the new design system.

Made-with: Cursor
This commit is contained in:
Douglas 2026-04-24 00:17:27 +01:00
parent 7d7fccfc6f
commit b3c00caeb4
6 changed files with 50 additions and 15 deletions

View file

@ -26,7 +26,19 @@ import {
} from './semanticProps';
const badgeBase = cva(
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ds-ring-brand-default-focus focus:ring-offset-2 focus:ring-offset-ds-bg-neutral-subtle-default'
'inline-flex items-center rounded-md border font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ds-ring-brand-default-focus focus:ring-offset-2 focus:ring-offset-ds-bg-neutral-subtle-default',
{
variants: {
size: {
xs: 'gap-0.5 px-1 py-0 !text-label-xs',
default: 'px-2 py-1 !text-label-sm',
sm: 'gap-1 px-2 py-1 !text-label-sm',
},
},
defaultVariants: {
size: 'default',
},
}
);
type BadgeLegacyVariant = 'default' | 'secondary' | 'destructive' | 'outline';
@ -160,21 +172,32 @@ function badgeToneClasses(
return BADGE_GHOST[tone];
}
export type BadgeSize = 'xs' | 'default' | 'sm';
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: UiVariant | BadgeLegacyVariant;
emphasis?: UiEmphasis;
tone?: UiToneInput;
size?: BadgeSize;
}
function Badge({ className, variant, emphasis, tone, ...props }: BadgeProps) {
function Badge({
className,
variant,
emphasis,
tone,
size = 'default',
...props
}: BadgeProps) {
const resolved = resolveBadgeVisual(variant, tone, emphasis);
return (
<div
data-variant={resolved.publicVariant}
data-tone={resolved.tone}
data-emphasis={resolved.emphasis}
data-size={size === 'default' ? undefined : size}
className={cn(
badgeBase(),
badgeBase({ size }),
badgeToneClasses(resolved.styleVariant, resolved.tone),
className
)}

View file

@ -19,23 +19,32 @@ import * as React from 'react';
import { cn } from '@/lib/utils';
import { checkboxTokenAliases, mergeAliasStyles } from './tokenAliases';
export type CheckboxProps = React.ComponentPropsWithoutRef<
typeof CheckboxPrimitives.Root
> & {
iconClassName?: string;
};
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitives.Root>
>(({ className, style, ...props }, ref) => (
CheckboxProps
>(({ className, style, iconClassName, ...props }, ref) => (
<CheckboxPrimitives.Root
ref={ref}
className={cn(
'focus-visible:ring-ds-ring-brand-default-focus peer h-4 w-4 rounded border-ds-border-neutral-default-default bg-ds-bg-neutral-default-default hover:border-ds-border-neutral-strong-default data-[state=checked]:border-ds-border-status-completed-default-default data-[state=checked]:bg-ds-bg-status-completed-default-default data-[state=checked]:text-ds-text-brand-inverse-default shrink-0 border border-solid transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
'group/checkbox focus-visible:ring-ds-ring-brand-default-focus peer h-4 w-4 rounded border-ds-border-neutral-default-default bg-ds-bg-neutral-default-default hover:border-ds-border-neutral-strong-default data-[state=checked]:border-ds-border-status-completed-default-default data-[state=checked]:bg-ds-bg-success-default-default shrink-0 border border-solid transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
className
)}
style={mergeAliasStyles(checkboxTokenAliases, style)}
{...props}
>
<CheckboxPrimitives.Indicator
className={cn('flex items-center justify-center text-current')}
>
<Check className="h-3.5 w-3.5" />
<CheckboxPrimitives.Indicator className="flex items-center justify-center">
<Check
className={cn(
'h-3.5 w-3.5 group-data-[state=checked]/checkbox:text-ds-text-brand-inverse-default shrink-0',
iconClassName
)}
/>
</CheckboxPrimitives.Indicator>
</CheckboxPrimitives.Root>
));

View file

@ -109,7 +109,7 @@ const Input = React.forwardRef<HTMLInputElement, BaseInputProps>(
<div
className={cn(
'rounded-lg shadow-sm relative flex items-center border border-solid transition-colors',
'rounded-xl shadow-sm relative flex items-center border border-solid transition-colors',
stateCls.field,
formFieldSizeClasses[size],
// After field base so hover / focus background wins; subtle surface on interaction

View file

@ -61,6 +61,8 @@ type SelectTriggerExtraProps = {
note?: string;
tooltip?: string;
required?: boolean;
/** Outer wrapper width; default `w-fit` keeps intrinsic width for inline selects. */
wrapperClassName?: string;
};
const SelectTrigger = React.forwardRef<
@ -80,6 +82,7 @@ const SelectTrigger = React.forwardRef<
disabled,
tooltip,
required = false,
wrapperClassName,
style,
...props
},
@ -87,7 +90,7 @@ const SelectTrigger = React.forwardRef<
) => {
const stateCls = formFieldSelectTriggerState(state, Boolean(disabled));
return (
<div className={cn('w-fit', stateCls.wrapper)}>
<div className={cn(wrapperClassName ?? 'w-fit', stateCls.wrapper)}>
{title ? (
<div className="mb-1.5 gap-1 text-body-sm font-bold text-ds-text-neutral-default-default flex items-center">
<span>{title}</span>

View file

@ -80,6 +80,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, BaseTextareaProps>(
return (
<>
<textarea
{...textareaProps}
data-scrollbar="ui-textarea"
className={cn(
'border-ds-border-neutral-default-default placeholder:text-ds-text-neutral-muted-default/20 focus-visible:ring-ds-ring-brand-default-focus rounded-lg py-2 pl-3 pr-3 text-body-sm shadow-sm flex min-h-[60px] w-full border bg-transparent [scrollbar-gutter:stable] focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
@ -101,7 +102,6 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, BaseTextareaProps>(
}
onKeyDown?.(e);
}}
{...textareaProps}
/>
<style>{`
/* Firefox */
@ -168,6 +168,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, BaseTextareaProps>(
) : null}
<textarea
{...textareaProps}
data-scrollbar="ui-textarea"
ref={ref}
disabled={disabled}
@ -190,7 +191,6 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, BaseTextareaProps>(
}
onKeyDown?.(e);
}}
{...textareaProps}
/>
{backIcon ? (

View file

@ -33,7 +33,7 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
'rounded-md border-ds-border-neutral-default-default bg-ds-bg-neutral-strong-default px-2 py-1.5 text-xs text-ds-text-neutral-default-default shadow-md backdrop-blur-sm animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 origin-[--radix-tooltip-content-transform-origin] overflow-hidden border',
'rounded-md border-ds-border-neutral-default-default bg-ds-bg-neutral-strong-default px-2 py-1.5 text-xs text-ds-text-neutral-default-default shadow-md backdrop-blur-sm animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[100] origin-[--radix-tooltip-content-transform-origin] overflow-hidden border',
className
)}
style={mergeAliasStyles(tooltipTokenAliases, style)}