diff --git a/packages/ui/src/components/Badge.tsx b/packages/ui/src/components/Badge.tsx index e06ac456..a615941c 100644 --- a/packages/ui/src/components/Badge.tsx +++ b/packages/ui/src/components/Badge.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { clsx } from 'clsx'; export interface BadgeProps extends React.HTMLAttributes { - 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 = { '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 = { 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({ diff --git a/packages/ui/src/components/Button.tsx b/packages/ui/src/components/Button.tsx index a9a7e2b8..73f69cbd 100644 --- a/packages/ui/src/components/Button.tsx +++ b/packages/ui/src/components/Button.tsx @@ -4,7 +4,7 @@ import { clsx } from 'clsx'; import { Loader2 } from 'lucide-react'; export interface ButtonProps extends React.ButtonHTMLAttributes { - 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( '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 = { - 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 = { diff --git a/packages/ui/src/components/Card.tsx b/packages/ui/src/components/Card.tsx index bf20f525..c6cc3e80 100644 --- a/packages/ui/src/components/Card.tsx +++ b/packages/ui/src/components/Card.tsx @@ -3,6 +3,7 @@ import { clsx } from 'clsx'; export interface CardProps extends React.HTMLAttributes { padding?: 'none' | 'sm' | 'md' | 'lg'; + variant?: 'default' | 'muted' | 'elevated' | 'outline'; hover?: boolean; } @@ -13,12 +14,26 @@ const paddings: Record = { 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, 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 (
label?: string; error?: string; hint?: string; + controlSize?: 'sm' | 'md' | 'lg'; + variant?: 'surface' | 'muted' | 'ghost'; } export const Input = React.forwardRef( - ({ 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, 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, string> = { + surface: 'bg-[var(--bl-surface-card,#1a1a2e)]', + muted: 'bg-[var(--bl-surface-muted,#252540)]', + ghost: 'bg-transparent', + }; return (
@@ -25,11 +41,13 @@ export const Input = React.forwardRef( 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( {...props} /> {error && ( - )} diff --git a/packages/ui/src/components/Select.tsx b/packages/ui/src/components/Select.tsx index 4f0124e6..01d69aef 100644 --- a/packages/ui/src/components/Select.tsx +++ b/packages/ui/src/components/Select.tsx @@ -7,12 +7,37 @@ export interface SelectProps extends React.SelectHTMLAttributes; placeholder?: string; + controlSize?: 'sm' | 'md' | 'lg'; + variant?: 'surface' | 'muted' | 'ghost'; } export const Select = React.forwardRef( - ({ 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, 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, string> = { + surface: 'bg-[var(--bl-surface-card,#1a1a2e)]', + muted: 'bg-[var(--bl-surface-muted,#252540)]', + ghost: 'bg-transparent', + }; return (
@@ -29,13 +54,16 @@ export const Select = React.forwardRef( 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( />
{error && ( -

+

)} diff --git a/packages/ui/src/components/Textarea.tsx b/packages/ui/src/components/Textarea.tsx index da0d198c..87076a12 100644 --- a/packages/ui/src/components/Textarea.tsx +++ b/packages/ui/src/components/Textarea.tsx @@ -5,12 +5,27 @@ export interface TextareaProps extends React.TextareaHTMLAttributes( - ({ 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, 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, string> = { + surface: 'bg-[var(--bl-surface-card,#1a1a2e)]', + muted: 'bg-[var(--bl-surface-muted,#252540)]', + ghost: 'bg-transparent', + }; return (
@@ -26,11 +41,13 @@ export const Textarea = React.forwardRef( 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( {...props} /> {error && ( - )}