diff --git a/dashboard/web/package.json b/dashboard/web/package.json index 356e366..d29aa9f 100644 --- a/dashboard/web/package.json +++ b/dashboard/web/package.json @@ -15,8 +15,6 @@ "test:e2e:ui": "playwright test --ui" }, "dependencies": { - "@bytelyst/design-tokens": "file:../../../learning_ai_common_plat/packages/design-tokens", - "@bytelyst/ui": "file:../../../learning_ai_common_plat/packages/ui", "clsx": "^2.1.1", "lucide-react": "^0.562.0", "next": "16.0.0", diff --git a/dashboard/web/src/app/globals.css b/dashboard/web/src/app/globals.css index 96dd454..3993748 100644 --- a/dashboard/web/src/app/globals.css +++ b/dashboard/web/src/app/globals.css @@ -1,4 +1,4 @@ -@import "@bytelyst/design-tokens/css"; +@import "../styles/tokens.css"; @tailwind base; @tailwind components; diff --git a/dashboard/web/src/components/ui/Primitives.tsx b/dashboard/web/src/components/ui/Primitives.tsx index 7deb1cd..a728388 100644 --- a/dashboard/web/src/components/ui/Primitives.tsx +++ b/dashboard/web/src/components/ui/Primitives.tsx @@ -1,226 +1,95 @@ import * as React from 'react'; -import { - Badge as CommonBadge, - Button as CommonButton, - Input as CommonInput, - Select as CommonSelect, - Textarea as CommonTextarea, - type BadgeProps as CommonBadgeProps, - type ButtonProps as CommonButtonProps, - type InputProps as CommonInputProps, - type SelectProps as CommonSelectProps, - type TextareaProps as CommonTextareaProps, -} from '@bytelyst/ui'; import { cn } from '@/lib/utils'; -// Re-export all shared primitives from @bytelyst/ui -export { - ActionMenu, - AlertBanner, - DataList, - DataTable, - Drawer, - EmptyState, - EntityCard, - Field, - FieldContent, - FieldDescription, - FieldError, - FieldGroup, - FieldLabel, - FieldTitle, - FilterBar, - FormSection, - MetricCard, - Modal, - PageHeader, - Panel, - PanelBody, - PanelDescription, - PanelHeader, - PanelTitle, - Skeleton, - Timeline, - Toolbar, - // Add other @bytelyst/ui components as needed -} from '@bytelyst/ui'; - -// Define product-specific variants -type ProductButtonVariant = NonNullable | 'link'; -type ProductButtonSize = NonNullable | 'icon'; -type ProductFieldVariant = 'surface' | 'muted'; -type ProductFieldSize = 'sm' | 'md'; -type ProductBadgeVariant = NonNullable | 'danger'; -type ProductStatusTone = 'success' | 'warning' | 'error' | 'info' | 'neutral'; - -// Extend interfaces with product-specific props -export interface ButtonProps extends Omit { - variant?: ProductButtonVariant; - size?: ProductButtonSize; +// Basic button component using design tokens +export interface ButtonProps extends React.ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'ghost' | 'link'; + size?: 'sm' | 'md' | 'lg'; } -export interface IconButtonProps extends Omit { - icon: React.ReactNode; - label: string; -} - -export interface InputProps extends CommonInputProps { - controlSize?: ProductFieldSize; - variant?: ProductFieldVariant; -} - -export interface SelectProps extends CommonSelectProps { - controlSize?: ProductFieldSize; - variant?: ProductFieldVariant; -} - -export interface TextareaProps extends CommonTextareaProps { - controlSize?: ProductFieldSize; - variant?: ProductFieldVariant; -} - -export interface BadgeProps extends Omit { - variant?: ProductBadgeVariant; -} - -// Product status mapping for badges (devops-specific statuses) -export type ProductStatus = - | 'active' | 'approved' | 'blocked' | 'cancelled' - | 'connected' | 'danger' | 'degraded' | 'disabled' | 'error' - | 'failed' | 'idle' | 'info' | 'live' | 'neutral' | 'off' - | 'ok' | 'paper' | 'pending' | 'rejected' | 'success' - | 'synced' | 'warning' | 'healthy' | 'unhealthy' | 'maintenance'; - -const productStatusTone: Record = { - active: 'success', - approved: 'success', - blocked: 'error', - cancelled: 'neutral', - connected: 'success', - danger: 'error', - degraded: 'warning', - disabled: 'neutral', - error: 'error', - failed: 'error', - healthy: 'success', - idle: 'neutral', - info: 'info', - live: 'warning', - maintenance: 'warning', - neutral: 'neutral', - off: 'neutral', - ok: 'success', - paper: 'info', - pending: 'warning', - rejected: 'error', - success: 'success', - synced: 'success', - unhealthy: 'error', - warning: 'warning', -}; - -// Helper function to map product status to tone -export function statusToneFor(status: ProductStatus | string | null | undefined): ProductStatusTone { - if (!status) return 'neutral'; - const normalized = status.trim().toLowerCase().replace(/[\s_]+/g, '-') as ProductStatus; - return productStatusTone[normalized] ?? 'neutral'; -} - -// Product-specific component implementations export const Button = React.forwardRef( - ({ variant = 'primary', size = 'md', className, ...props }, ref) => ( - - ), + ({ variant = 'primary', size = 'md', className, ...props }, ref) => { + const baseStyles = 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50'; + + const variantStyles = { + primary: 'bg-[var(--bl-primary)] text-white hover:bg-[var(--bl-primary-hover)] focus-visible:ring-[var(--bl-primary)]', + secondary: 'bg-[var(--bl-surface)] text-[var(--bl-fg)] hover:bg-[var(--bl-surface-hover)] focus-visible:ring-[var(--bl-fg)]', + ghost: 'hover:bg-[var(--bl-surface-hover)] text-[var(--bl-fg)] focus-visible:ring-[var(--bl-fg)]', + link: 'text-[var(--bl-primary)] hover:underline focus-visible:ring-[var(--bl-primary)]', + }; + + const sizeStyles = { + sm: 'h-9 px-3 text-sm', + md: 'h-10 px-4 text-sm', + lg: 'h-11 px-8 text-base', + }; + + return ( + - ), -); +// Basic badge component using design tokens +export interface BadgeProps extends React.HTMLAttributes { + variant?: 'neutral' | 'success' | 'warning' | 'error' | 'info'; + dot?: boolean; +} -IconButton.displayName = 'IconButton'; +export function Badge({ variant = 'neutral', dot = false, className, children, ...props }: BadgeProps) { + const variantStyles = { + neutral: 'bg-[var(--bl-surface-muted)] text-[var(--bl-fg-muted)]', + success: 'bg-[var(--bl-success-bg)] text-[var(--bl-success-fg)]', + warning: 'bg-[var(--bl-warning-bg)] text-[var(--bl-warning-fg)]', + error: 'bg-[var(--bl-danger-bg)] text-[var(--bl-danger-fg)]', + info: 'bg-[var(--bl-info-bg)] text-[var(--bl-info-fg)]', + }; -export const Input = React.forwardRef( - ({ controlSize = 'md', variant = 'surface', className, ...props }, ref) => ( - - ), + > + {dot && } + {children} + + ); +} + +// Input component using design tokens +export interface InputProps extends React.InputHTMLAttributes { + variant?: 'surface' | 'muted'; +} + +export const Input = React.forwardRef( + ({ variant = 'surface', className, ...props }, ref) => { + const variantStyles = { + surface: 'bg-[var(--bl-input)] border-[var(--bl-border)]', + muted: 'bg-[var(--bl-surface-muted)] border-[var(--bl-border)]', + }; + + return ( + + ); + }, ); Input.displayName = 'Input'; - -export const Select = React.forwardRef( - ({ controlSize = 'md', variant = 'surface', className, ...props }, ref) => ( - - ), -); - -Select.displayName = 'Select'; - -export const Textarea = React.forwardRef( - ({ controlSize = 'md', variant = 'surface', className, ...props }, ref) => ( - - ), -); - -Textarea.displayName = 'Textarea'; - -export function Badge({ variant = 'neutral', ...props }: BadgeProps) { - return ; -} - -export function ProductStatusBadge({ - status, - children, -}: { - status: ProductStatus | string | null | undefined; - children?: React.ReactNode; -}) { - return ( - - {children ?? status ?? 'Unknown'} - - ); -} diff --git a/dashboard/web/src/styles/tokens.css b/dashboard/web/src/styles/tokens.css new file mode 100644 index 0000000..ea76353 --- /dev/null +++ b/dashboard/web/src/styles/tokens.css @@ -0,0 +1,116 @@ +/* Auto-generated from bytelyst.tokens.json — do not edit manually */ + +:root, +[data-theme="dark"] { + --ml-bg-canvas: #06070A; + --ml-bg-elevated: #0E1118; + --ml-surface-card: #121725; + --ml-surface-muted: #1A2335; + --ml-border-default: rgba(255,255,255,0.12); + --ml-border-strong: rgba(255,255,255,0.22); + --ml-text-primary: #EFF4FF; + --ml-text-secondary: #A5B1C7; + --ml-text-tertiary: #6C7C98; + --ml-accent-primary: #5A8CFF; + --ml-accent-secondary: #2EE6D6; + --ml-success: #34D399; + --ml-warning: #F59E0B; + --ml-danger: #FF6E6E; + --ml-focus-ring: rgba(90,140,255,0.45); + --ml-overlay-scrim: rgba(5,8,18,0.72); + + --bl-bg-canvas: var(--ml-bg-canvas); + --bl-bg-elevated: var(--ml-bg-elevated); + --bl-surface-card: var(--ml-surface-card); + --bl-surface-muted: var(--ml-surface-muted); + --bl-surface-highlight: color-mix(in oklab, var(--ml-surface-muted) 82%, white); + --bl-surface-overlay: color-mix(in oklab, var(--ml-bg-canvas) 88%, transparent); + --bl-input: color-mix(in oklab, var(--ml-surface-muted) 76%, var(--ml-bg-canvas)); + --bl-border: var(--ml-border-default); + --bl-border-strong: var(--ml-border-strong); + --bl-border-subtle: color-mix(in oklab, var(--ml-border-default) 62%, transparent); + --bl-text-primary: var(--ml-text-primary); + --bl-text-secondary: var(--ml-text-secondary); + --bl-text-tertiary: var(--ml-text-tertiary); + --bl-text-quiet: color-mix(in oklab, var(--ml-text-secondary) 78%, var(--ml-bg-canvas)); + --bl-accent: var(--ml-accent-primary); + --bl-accent-foreground: var(--ml-bg-canvas); + --bl-accent-muted: color-mix(in oklab, var(--ml-accent-primary) 16%, transparent); + --bl-info: var(--ml-accent-primary); + --bl-info-muted: color-mix(in oklab, var(--ml-accent-primary) 14%, transparent); + --bl-success: var(--ml-success); + --bl-success-muted: color-mix(in oklab, var(--ml-success) 14%, transparent); + --bl-warning: var(--ml-warning); + --bl-warning-muted: color-mix(in oklab, var(--ml-warning) 14%, transparent); + --bl-danger: var(--ml-danger); + --bl-danger-muted: color-mix(in oklab, var(--ml-danger) 14%, transparent); + --bl-danger-foreground: var(--ml-bg-canvas); + --bl-focus-ring: var(--ml-focus-ring); + --bl-focus-ring-muted: color-mix(in oklab, var(--ml-accent-primary) 18%, transparent); + --bl-overlay-scrim: var(--ml-overlay-scrim); + + --ml-font-display: "Space Grotesk", "SF Pro Display", sans-serif; + --ml-font-body: "DM Sans", "SF Pro Text", sans-serif; + --ml-font-mono: "IBM Plex Mono", "SF Mono", monospace; + + --ml-fs-xs: 12px; + --ml-fs-sm: 14px; + --ml-fs-md: 16px; + --ml-fs-lg: 18px; + --ml-fs-xl: 22px; + --ml-fs-2xl: 28px; + --ml-fs-3xl: 36px; + + --ml-space-0: 0; + --ml-space-1: 4px; + --ml-space-2: 8px; + --ml-space-3: 12px; + --ml-space-4: 16px; + --ml-space-5: 20px; + --ml-space-6: 24px; + --ml-space-7: 28px; + --ml-space-8: 32px; + --ml-space-10: 40px; + --ml-space-12: 48px; + --ml-space-16: 64px; + + --ml-radius-xs: 8px; + --ml-radius-sm: 12px; + --ml-radius-md: 16px; + --ml-radius-lg: 20px; + --ml-radius-xl: 24px; + --ml-radius-pill: 999px; + --bl-radius-control: var(--ml-radius-xs); + --bl-radius-surface: var(--ml-radius-sm); + --bl-radius-card: var(--ml-radius-md); + --bl-radius-panel: var(--ml-radius-lg); + --bl-radius-pill: var(--ml-radius-pill); + + --ml-elevation-sm: 0 4px 12px rgba(0,0,0,0.12); + --ml-elevation-md: 0 12px 28px rgba(0,0,0,0.18); + --ml-elevation-lg: 0 20px 48px rgba(0,0,0,0.24); + --bl-shadow-sm: var(--ml-elevation-sm); + --bl-shadow-md: var(--ml-elevation-md); + --bl-shadow-lg: var(--ml-elevation-lg); + + --ml-motion-fast: 140ms; + --ml-motion-base: 220ms; + --ml-motion-slow: 320ms; + --ml-easing-standard: cubic-bezier(0.2, 0.0, 0.2, 1); +} + +[data-theme="light"] { + --ml-bg-canvas: #F6F8FC; + --ml-bg-elevated: #EEF2FA; + --ml-surface-card: #FFFFFF; + --ml-surface-muted: #F3F5FA; + --ml-border-default: rgba(14,19,32,0.12); + --ml-border-strong: rgba(14,19,32,0.24); + --ml-text-primary: #0E1320; + --ml-text-secondary: #55637A; + --ml-success: #13956A; + --ml-warning: #B87504; + --ml-danger: #D24242; + --ml-focus-ring: rgba(90,140,255,0.35); + --ml-overlay-scrim: rgba(10,13,23,0.5); +}