From 44011f4e273f45cec215eb79cb3f0d06180f9efe Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 22 Feb 2026 15:04:00 -0600 Subject: [PATCH 1/3] feat(ui): align design tokens and update component variants (MS15-UI-001, MS15-UI-002) Replace hardcoded Tailwind color classes with CSS custom property inline styles across all packages/ui components (Button, Card, Badge, Input, Select, Textarea, Avatar, Modal, Toast). Badge: add new variants (badge-teal, badge-amber, badge-blue, badge-purple, badge-pulse) with mono font and pill shape. Button: add success variant, hover states via React state. Card: flat design, no shadows, semantic border/surface tokens. New: Dot component (teal, blue, amber, red, muted) with glow effect. Co-Authored-By: Claude Opus 4.6 --- packages/ui/src/components/Avatar.tsx | 38 +++-- packages/ui/src/components/Badge.tsx | 193 ++++++++++++++++++++++-- packages/ui/src/components/Button.tsx | 119 ++++++++++++--- packages/ui/src/components/Card.tsx | 55 +++++-- packages/ui/src/components/Dot.tsx | 39 +++++ packages/ui/src/components/Input.tsx | 57 +++++-- packages/ui/src/components/Modal.tsx | 34 ++++- packages/ui/src/components/Select.tsx | 46 ++++-- packages/ui/src/components/Textarea.tsx | 49 ++++-- packages/ui/src/components/Toast.tsx | 45 +++++- packages/ui/src/index.ts | 4 + 11 files changed, 568 insertions(+), 111 deletions(-) create mode 100644 packages/ui/src/components/Dot.tsx diff --git a/packages/ui/src/components/Avatar.tsx b/packages/ui/src/components/Avatar.tsx index a6c0743..a0c02d8 100644 --- a/packages/ui/src/components/Avatar.tsx +++ b/packages/ui/src/components/Avatar.tsx @@ -15,41 +15,51 @@ export function Avatar({ fallback, initials, className = "", + style, ...props }: AvatarProps): ReactElement { - const sizeStyles = { + type AvatarSize = "sm" | "md" | "lg" | "xl"; + const sizeStyles: Record = { sm: "w-6 h-6 text-xs", md: "w-8 h-8 text-sm", lg: "w-12 h-12 text-base", xl: "w-16 h-16 text-xl", }; - const baseStyles = - "rounded-full overflow-hidden flex items-center justify-center bg-gray-200 font-medium text-gray-600"; + const baseClass = `rounded-full overflow-hidden flex items-center justify-center font-medium ${sizeStyles[size]} ${className}`; - const combinedClassName = [baseStyles, sizeStyles[size], className].filter(Boolean).join(" "); + const gradientStyle: React.CSSProperties = { + background: "linear-gradient(135deg, var(--ms-blue-500), var(--ms-purple-500))", + color: "#fff", + ...style, + }; if (src) { - return {alt}; + return ( + {alt} + ); } if (fallback) { - return
{fallback}
; + return ( +
+ {fallback} +
+ ); } if (initials) { - return
{initials}
; + return ( +
+ {initials} +
+ ); } // Default fallback with user icon return ( -
-
+ + + {def.pulse && ( + ); diff --git a/packages/ui/src/components/Button.tsx b/packages/ui/src/components/Button.tsx index 047e5a3..080380f 100644 --- a/packages/ui/src/components/Button.tsx +++ b/packages/ui/src/components/Button.tsx @@ -1,39 +1,120 @@ -import type { ButtonHTMLAttributes, ReactNode, ReactElement } from "react"; +import { useState, type ButtonHTMLAttributes, type ReactNode, type ReactElement } from "react"; export interface ButtonProps extends ButtonHTMLAttributes { - variant?: "primary" | "secondary" | "danger" | "ghost"; + variant?: "primary" | "secondary" | "ghost" | "danger" | "success"; size?: "sm" | "md" | "lg"; children: ReactNode; } +interface VariantStyle { + base: React.CSSProperties; + hover: React.CSSProperties; +} + +type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "success"; + +const variantStyles: Record = { + primary: { + base: { + background: "var(--ms-blue-500)", + color: "#fff", + border: "none", + }, + hover: { + background: "var(--ms-blue-400)", + boxShadow: "0 4px 16px rgba(47,128,255,0.3)", + }, + }, + secondary: { + base: { + background: "transparent", + border: "1px solid var(--border)", + color: "var(--text-2)", + }, + hover: { + background: "var(--surface)", + color: "var(--text)", + }, + }, + ghost: { + base: { + background: "transparent", + border: "1px solid var(--border)", + color: "var(--text-2)", + }, + hover: { + background: "var(--surface)", + color: "var(--text)", + }, + }, + danger: { + base: { + background: "rgba(229,72,77,0.12)", + border: "1px solid rgba(229,72,77,0.3)", + color: "var(--danger)", + }, + hover: { + background: "rgba(229,72,77,0.2)", + }, + }, + success: { + base: { + background: "rgba(20,184,166,0.12)", + border: "1px solid rgba(20,184,166,0.3)", + color: "var(--success)", + }, + hover: { + background: "rgba(20,184,166,0.2)", + }, + }, +}; + +type ButtonSize = "sm" | "md" | "lg"; + +const sizeStyles: Record = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-base", + lg: "px-6 py-3 text-lg", +}; + export function Button({ variant = "primary", size = "md", children, className = "", + style, + onMouseEnter, + onMouseLeave, + disabled, ...props }: ButtonProps): ReactElement { - const baseStyles = "inline-flex items-center justify-center font-medium rounded-md"; + const [isHovered, setIsHovered] = useState(false); - const variantStyles = { - primary: "bg-blue-600 text-white hover:bg-blue-700", - secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300", - danger: "bg-red-600 text-white hover:bg-red-700", - ghost: "bg-transparent text-gray-700 hover:bg-gray-100 border border-gray-300", + const vStyles = variantStyles[variant]; + const baseClass = `inline-flex items-center justify-center font-medium rounded-md transition-colors ${sizeStyles[size]} ${className}`; + + const computedStyle: React.CSSProperties = { + ...vStyles.base, + ...(isHovered && !disabled ? vStyles.hover : {}), + ...(disabled ? { opacity: 0.5, cursor: "not-allowed" } : { cursor: "pointer" }), + ...style, }; - const sizeStyles = { - sm: "px-3 py-1.5 text-sm", - md: "px-4 py-2 text-base", - lg: "px-6 py-3 text-lg", - }; - - const combinedClassName = [baseStyles, variantStyles[variant], sizeStyles[size], className] - .filter(Boolean) - .join(" "); - return ( - ); diff --git a/packages/ui/src/components/Card.tsx b/packages/ui/src/components/Card.tsx index 938a647..9c06e59 100644 --- a/packages/ui/src/components/Card.tsx +++ b/packages/ui/src/components/Card.tsx @@ -3,6 +3,7 @@ import type { ReactNode, ReactElement } from "react"; export interface CardProps { children: ReactNode; className?: string; + style?: React.CSSProperties; id?: string; onMouseEnter?: () => void; onMouseLeave?: () => void; @@ -11,21 +12,25 @@ export interface CardProps { export interface CardHeaderProps { children: ReactNode; className?: string; + style?: React.CSSProperties; } export interface CardContentProps { children: ReactNode; className?: string; + style?: React.CSSProperties; } export interface CardFooterProps { children: ReactNode; className?: string; + style?: React.CSSProperties; } export function Card({ children, className = "", + style, id, onMouseEnter, onMouseLeave, @@ -35,24 +40,52 @@ export function Card({ id={id} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} - className={`bg-white rounded-lg shadow-md border border-gray-200 ${className}`} + className={className} + style={{ + background: "var(--surface)", + border: "1px solid var(--border)", + borderRadius: "var(--r-lg)", + padding: "16px", + ...style, + }} > {children}
); } -export function CardHeader({ children, className = "" }: CardHeaderProps): ReactElement { - return
{children}
; -} - -export function CardContent({ children, className = "" }: CardContentProps): ReactElement { - return
{children}
; -} - -export function CardFooter({ children, className = "" }: CardFooterProps): ReactElement { +export function CardHeader({ children, className = "", style }: CardHeaderProps): ReactElement { return ( -
+
+ {children} +
+ ); +} + +export function CardContent({ children, className = "", style }: CardContentProps): ReactElement { + return ( +
+ {children} +
+ ); +} + +export function CardFooter({ children, className = "", style }: CardFooterProps): ReactElement { + return ( +
{children}
); diff --git a/packages/ui/src/components/Dot.tsx b/packages/ui/src/components/Dot.tsx new file mode 100644 index 0000000..df959a4 --- /dev/null +++ b/packages/ui/src/components/Dot.tsx @@ -0,0 +1,39 @@ +import type { ReactElement } from "react"; + +export type DotVariant = "teal" | "blue" | "amber" | "red" | "muted"; + +export interface DotProps { + variant?: DotVariant; + className?: string; +} + +interface DotColorDef { + bg: string; + shadow: string; +} + +export function Dot({ variant = "muted", className = "" }: DotProps): ReactElement { + const colors: Record = { + teal: { bg: "var(--success)", shadow: "0 0 5px var(--success)" }, + blue: { bg: "var(--primary)", shadow: "0 0 5px var(--primary)" }, + amber: { bg: "var(--warn)", shadow: "0 0 5px var(--warn)" }, + red: { bg: "var(--danger)", shadow: "0 0 5px var(--danger)" }, + muted: { bg: "var(--muted)", shadow: "none" }, + }; + + const { bg, shadow } = colors[variant]; + + return ( +