feat(ui): add product-safe primitive variants
This commit is contained in:
parent
9d5a31d559
commit
f37fd480fe
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||
variant?: 'success' | 'warning' | 'error' | 'info' | 'neutral';
|
||||
variant?: 'success' | 'warning' | 'error' | 'danger' | 'info' | 'neutral' | 'accent';
|
||||
size?: 'sm' | 'md';
|
||||
dot?: boolean;
|
||||
}
|
||||
@ -14,17 +14,23 @@ const variantStyles: Record<string, string> = {
|
||||
'bg-[var(--bl-warning-muted,var(--bl-surface-muted))] text-[var(--bl-warning)] border-[var(--bl-warning-border,var(--bl-warning))]',
|
||||
error:
|
||||
'bg-[var(--bl-danger-muted,var(--bl-surface-muted))] text-[var(--bl-danger)] border-[var(--bl-danger-border,var(--bl-danger))]',
|
||||
danger:
|
||||
'bg-[var(--bl-danger-muted,var(--bl-surface-muted))] text-[var(--bl-danger)] border-[var(--bl-danger-border,var(--bl-danger))]',
|
||||
info: 'bg-[var(--bl-info-muted,var(--bl-surface-muted))] text-[var(--bl-info,var(--bl-accent))] border-[var(--bl-info-border,var(--bl-info,var(--bl-accent)))]',
|
||||
neutral:
|
||||
'bg-[var(--bl-surface-muted,#252540)] text-[var(--bl-text-secondary,#a0a0b0)] border-[var(--bl-border,#2a2a4a)]',
|
||||
accent:
|
||||
'bg-[var(--bl-accent-muted,var(--bl-surface-muted))] text-[var(--bl-text-primary)] border-[var(--bl-accent)]',
|
||||
};
|
||||
|
||||
const dotColors: Record<string, string> = {
|
||||
success: 'bg-[var(--bl-success)]',
|
||||
error: 'bg-[var(--bl-danger)]',
|
||||
danger: 'bg-[var(--bl-danger)]',
|
||||
warning: 'bg-[var(--bl-warning)]',
|
||||
info: 'bg-[var(--bl-info,var(--bl-accent))]',
|
||||
neutral: 'bg-[var(--bl-text-tertiary,var(--bl-text-secondary))]',
|
||||
accent: 'bg-[var(--bl-accent)]',
|
||||
};
|
||||
|
||||
export function Badge({
|
||||
|
||||
@ -4,7 +4,7 @@ import { clsx } from 'clsx';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary' | 'ghost' | 'destructive' | 'outline';
|
||||
variant?: 'primary' | 'secondary' | 'ghost' | 'destructive' | 'outline' | 'subtle' | 'link';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
loading?: boolean;
|
||||
asChild?: boolean;
|
||||
@ -21,14 +21,19 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
'inline-flex items-center justify-center font-medium rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50';
|
||||
|
||||
const variants: Record<string, string> = {
|
||||
primary: 'bg-[var(--bl-accent,#5A8CFF)] text-white hover:opacity-90',
|
||||
primary:
|
||||
'bg-[var(--bl-accent,#5A8CFF)] text-[var(--bl-accent-foreground,var(--bl-bg-canvas,#0b0f17))] hover:opacity-90',
|
||||
secondary:
|
||||
'bg-[var(--bl-surface-card,#1a1a2e)] text-[var(--bl-text-primary,#fff)] border border-[var(--bl-border,#2a2a4a)] hover:bg-[var(--bl-surface-muted,#252540)]',
|
||||
ghost:
|
||||
'text-[var(--bl-text-secondary,#a0a0b0)] hover:bg-[var(--bl-surface-muted,#252540)] hover:text-[var(--bl-text-primary,#fff)]',
|
||||
destructive: 'bg-red-600 text-white hover:bg-red-700',
|
||||
destructive:
|
||||
'bg-[var(--bl-danger)] text-[var(--bl-danger-foreground,var(--bl-bg-canvas,#0b0f17))] hover:opacity-90',
|
||||
outline:
|
||||
'border border-[var(--bl-border,#2a2a4a)] text-[var(--bl-text-primary,#fff)] hover:bg-[var(--bl-surface-muted,#252540)]',
|
||||
subtle:
|
||||
'bg-[var(--bl-surface-muted,#252540)] text-[var(--bl-text-primary,#fff)] hover:bg-[var(--bl-surface-card,#1a1a2e)]',
|
||||
link: 'h-auto rounded-none p-0 text-[var(--bl-accent,#5A8CFF)] underline-offset-4 hover:underline',
|
||||
};
|
||||
|
||||
const sizes: Record<string, string> = {
|
||||
|
||||
@ -3,6 +3,7 @@ import { clsx } from 'clsx';
|
||||
|
||||
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
padding?: 'none' | 'sm' | 'md' | 'lg';
|
||||
variant?: 'default' | 'muted' | 'elevated' | 'outline';
|
||||
hover?: boolean;
|
||||
}
|
||||
|
||||
@ -13,12 +14,26 @@ const paddings: Record<string, string> = {
|
||||
lg: 'p-6',
|
||||
};
|
||||
|
||||
export function Card({ padding = 'md', hover, className, children, ...props }: CardProps) {
|
||||
export function Card({
|
||||
padding = 'md',
|
||||
variant = 'default',
|
||||
hover,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: CardProps) {
|
||||
const variants: Record<NonNullable<CardProps['variant']>, string> = {
|
||||
default: 'bg-[var(--bl-surface-card,#1a1a2e)] border-[var(--bl-border,#2a2a4a)]',
|
||||
muted: 'bg-[var(--bl-surface-muted,#252540)] border-[var(--bl-border,#2a2a4a)]',
|
||||
elevated: 'bg-[var(--bl-bg-elevated,#12151c)] border-[var(--bl-border,#2a2a4a)] shadow-sm',
|
||||
outline: 'bg-transparent border-[var(--bl-border,#2a2a4a)]',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'rounded-xl border',
|
||||
'bg-[var(--bl-surface-card,#1a1a2e)] border-[var(--bl-border,#2a2a4a)]',
|
||||
variants[variant],
|
||||
hover && 'transition-colors hover:border-[var(--bl-accent,#5A8CFF)]/40',
|
||||
paddings[padding],
|
||||
className
|
||||
|
||||
@ -5,11 +5,27 @@ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>
|
||||
label?: string;
|
||||
error?: string;
|
||||
hint?: string;
|
||||
controlSize?: 'sm' | 'md' | 'lg';
|
||||
variant?: 'surface' | 'muted' | 'ghost';
|
||||
}
|
||||
|
||||
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ label, error, hint, className, id, ...props }, ref) => {
|
||||
const inputId = id ?? (label ? `input-${label.toLowerCase().replace(/\s+/g, '-')}` : undefined);
|
||||
(
|
||||
{ label, error, hint, controlSize = 'md', variant = 'surface', className, id, ...props },
|
||||
ref
|
||||
) => {
|
||||
const generatedId = React.useId();
|
||||
const inputId = id ?? (label || error || hint ? `input-${generatedId}` : undefined);
|
||||
const sizes: Record<NonNullable<InputProps['controlSize']>, string> = {
|
||||
sm: 'h-8 px-2.5 text-xs',
|
||||
md: 'h-10 px-3 text-sm',
|
||||
lg: 'h-12 px-4 text-base',
|
||||
};
|
||||
const variants: Record<NonNullable<InputProps['variant']>, string> = {
|
||||
surface: 'bg-[var(--bl-surface-card,#1a1a2e)]',
|
||||
muted: 'bg-[var(--bl-surface-muted,#252540)]',
|
||||
ghost: 'bg-transparent',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
@ -25,11 +41,13 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
ref={ref}
|
||||
id={inputId}
|
||||
className={clsx(
|
||||
'w-full rounded-md border px-3 py-2 text-sm outline-none transition-colors',
|
||||
'bg-[var(--bl-surface-card,#1a1a2e)] text-[var(--bl-text-primary,#fff)]',
|
||||
'w-full rounded-md border outline-none transition-colors',
|
||||
variants[variant],
|
||||
sizes[controlSize],
|
||||
'text-[var(--bl-text-primary,#fff)]',
|
||||
'placeholder:text-[var(--bl-text-tertiary,#555)]',
|
||||
'focus:ring-2 focus:ring-[var(--bl-accent,#5A8CFF)] focus:ring-offset-0',
|
||||
error ? 'border-red-500' : 'border-[var(--bl-border,#2a2a4a)]',
|
||||
error ? 'border-[var(--bl-danger)]' : 'border-[var(--bl-border,#2a2a4a)]',
|
||||
className
|
||||
)}
|
||||
aria-invalid={error ? 'true' : undefined}
|
||||
@ -37,7 +55,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
{...props}
|
||||
/>
|
||||
{error && (
|
||||
<p id={`${inputId}-error`} className="text-xs text-red-400" role="alert">
|
||||
<p id={`${inputId}-error`} className="text-xs text-[var(--bl-danger)]" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -7,12 +7,37 @@ export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElemen
|
||||
error?: string;
|
||||
options: Array<{ value: string; label: string; disabled?: boolean }>;
|
||||
placeholder?: string;
|
||||
controlSize?: 'sm' | 'md' | 'lg';
|
||||
variant?: 'surface' | 'muted' | 'ghost';
|
||||
}
|
||||
|
||||
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||
({ label, error, options, placeholder, className, id, ...props }, ref) => {
|
||||
const selectId =
|
||||
id ?? (label ? `select-${label.toLowerCase().replace(/\s+/g, '-')}` : undefined);
|
||||
(
|
||||
{
|
||||
label,
|
||||
error,
|
||||
options,
|
||||
placeholder,
|
||||
controlSize = 'md',
|
||||
variant = 'surface',
|
||||
className,
|
||||
id,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const generatedId = React.useId();
|
||||
const selectId = id ?? (label || error ? `select-${generatedId}` : undefined);
|
||||
const sizes: Record<NonNullable<SelectProps['controlSize']>, string> = {
|
||||
sm: 'h-8 px-2.5 pr-8 text-xs',
|
||||
md: 'h-10 px-3 pr-8 text-sm',
|
||||
lg: 'h-12 px-4 pr-9 text-base',
|
||||
};
|
||||
const variants: Record<NonNullable<SelectProps['variant']>, string> = {
|
||||
surface: 'bg-[var(--bl-surface-card,#1a1a2e)]',
|
||||
muted: 'bg-[var(--bl-surface-muted,#252540)]',
|
||||
ghost: 'bg-transparent',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
@ -29,13 +54,16 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||
ref={ref}
|
||||
id={selectId}
|
||||
className={clsx(
|
||||
'w-full appearance-none rounded-md border px-3 py-2 pr-8 text-sm outline-none transition-colors',
|
||||
'bg-[var(--bl-surface-card,#1a1a2e)] text-[var(--bl-text-primary,#fff)]',
|
||||
'w-full appearance-none rounded-md border outline-none transition-colors',
|
||||
variants[variant],
|
||||
sizes[controlSize],
|
||||
'text-[var(--bl-text-primary,#fff)]',
|
||||
'focus:ring-2 focus:ring-[var(--bl-accent,#5A8CFF)] focus:ring-offset-0',
|
||||
error ? 'border-red-500' : 'border-[var(--bl-border,#2a2a4a)]',
|
||||
error ? 'border-[var(--bl-danger)]' : 'border-[var(--bl-border,#2a2a4a)]',
|
||||
className
|
||||
)}
|
||||
aria-invalid={error ? 'true' : undefined}
|
||||
aria-describedby={error ? `${selectId}-error` : undefined}
|
||||
{...props}
|
||||
>
|
||||
{placeholder && (
|
||||
@ -55,7 +83,7 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-xs text-red-400" role="alert">
|
||||
<p id={`${selectId}-error`} className="text-xs text-[var(--bl-danger)]" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -5,12 +5,27 @@ export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextArea
|
||||
label?: string;
|
||||
error?: string;
|
||||
hint?: string;
|
||||
controlSize?: 'sm' | 'md' | 'lg';
|
||||
variant?: 'surface' | 'muted' | 'ghost';
|
||||
}
|
||||
|
||||
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ label, error, hint, className, id, ...props }, ref) => {
|
||||
const textareaId =
|
||||
id ?? (label ? `textarea-${label.toLowerCase().replace(/\s+/g, '-')}` : undefined);
|
||||
(
|
||||
{ label, error, hint, controlSize = 'md', variant = 'surface', className, id, ...props },
|
||||
ref
|
||||
) => {
|
||||
const generatedId = React.useId();
|
||||
const textareaId = id ?? (label || error || hint ? `textarea-${generatedId}` : undefined);
|
||||
const sizes: Record<NonNullable<TextareaProps['controlSize']>, string> = {
|
||||
sm: 'min-h-20 px-2.5 py-2 text-xs',
|
||||
md: 'min-h-24 px-3 py-2 text-sm',
|
||||
lg: 'min-h-32 px-4 py-3 text-base',
|
||||
};
|
||||
const variants: Record<NonNullable<TextareaProps['variant']>, string> = {
|
||||
surface: 'bg-[var(--bl-surface-card,#1a1a2e)]',
|
||||
muted: 'bg-[var(--bl-surface-muted,#252540)]',
|
||||
ghost: 'bg-transparent',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
@ -26,11 +41,13 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
ref={ref}
|
||||
id={textareaId}
|
||||
className={clsx(
|
||||
'w-full rounded-md border px-3 py-2 text-sm outline-none transition-colors resize-y',
|
||||
'bg-[var(--bl-surface-card,#1a1a2e)] text-[var(--bl-text-primary,#fff)]',
|
||||
'w-full resize-y rounded-md border outline-none transition-colors',
|
||||
variants[variant],
|
||||
sizes[controlSize],
|
||||
'text-[var(--bl-text-primary,#fff)]',
|
||||
'placeholder:text-[var(--bl-text-tertiary,#555)]',
|
||||
'focus:ring-2 focus:ring-[var(--bl-accent,#5A8CFF)] focus:ring-offset-0',
|
||||
error ? 'border-red-500' : 'border-[var(--bl-border,#2a2a4a)]',
|
||||
error ? 'border-[var(--bl-danger)]' : 'border-[var(--bl-border,#2a2a4a)]',
|
||||
className
|
||||
)}
|
||||
aria-invalid={error ? 'true' : undefined}
|
||||
@ -38,7 +55,7 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
{...props}
|
||||
/>
|
||||
{error && (
|
||||
<p id={`${textareaId}-error`} className="text-xs text-red-400" role="alert">
|
||||
<p id={`${textareaId}-error`} className="text-xs text-[var(--bl-danger)]" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user