From 252403f74eb44fcf94ea5cad74a262189341a2ee Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Fri, 27 Mar 2026 16:29:37 -0700 Subject: [PATCH] feat(ui): create @bytelyst/ui with Button, Toast, Modal, ConfirmDialog, Badge, EmptyState --- eslint.config.js | 25 +++++ packages/ui/eslint.config.js | 18 ++++ packages/ui/package.json | 30 ++++++ packages/ui/src/components/Badge.tsx | 49 ++++++++++ packages/ui/src/components/Button.tsx | 54 +++++++++++ packages/ui/src/components/ConfirmDialog.tsx | 80 ++++++++++++++++ packages/ui/src/components/EmptyState.tsx | 46 ++++++++++ packages/ui/src/components/Modal.tsx | 64 +++++++++++++ packages/ui/src/components/Toast.tsx | 97 ++++++++++++++++++++ packages/ui/src/index.ts | 13 +++ packages/ui/tsconfig.json | 10 ++ 11 files changed, 486 insertions(+) create mode 100644 packages/ui/eslint.config.js create mode 100644 packages/ui/package.json create mode 100644 packages/ui/src/components/Badge.tsx create mode 100644 packages/ui/src/components/Button.tsx create mode 100644 packages/ui/src/components/ConfirmDialog.tsx create mode 100644 packages/ui/src/components/EmptyState.tsx create mode 100644 packages/ui/src/components/Modal.tsx create mode 100644 packages/ui/src/components/Toast.tsx create mode 100644 packages/ui/src/index.ts create mode 100644 packages/ui/tsconfig.json diff --git a/eslint.config.js b/eslint.config.js index 9b9fcb49..c2bff88e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -79,6 +79,31 @@ export default [ ReactNode: 'readonly', React: 'readonly', JSX: 'readonly', + HTMLElement: 'readonly', + HTMLButtonElement: 'readonly', + HTMLSpanElement: 'readonly', + HTMLDivElement: 'readonly', + HTMLInputElement: 'readonly', + HTMLFormElement: 'readonly', + HTMLAnchorElement: 'readonly', + HTMLImageElement: 'readonly', + HTMLTextAreaElement: 'readonly', + HTMLSelectElement: 'readonly', + HTMLLabelElement: 'readonly', + Element: 'readonly', + Event: 'readonly', + KeyboardEvent: 'readonly', + MouseEvent: 'readonly', + FocusEvent: 'readonly', + CustomEvent: 'readonly', + MediaQueryList: 'readonly', + MutationObserver: 'readonly', + ResizeObserver: 'readonly', + IntersectionObserver: 'readonly', + EventTarget: 'readonly', + NodeList: 'readonly', + DOMRect: 'readonly', + SVGElement: 'readonly', }, }, plugins: { diff --git a/packages/ui/eslint.config.js b/packages/ui/eslint.config.js new file mode 100644 index 00000000..fc017d43 --- /dev/null +++ b/packages/ui/eslint.config.js @@ -0,0 +1,18 @@ +export default [ + { + files: ['src/**/*.{ts,tsx}'], + languageOptions: { + globals: { + HTMLButtonElement: 'readonly', + HTMLSpanElement: 'readonly', + HTMLDivElement: 'readonly', + HTMLInputElement: 'readonly', + React: 'readonly', + setTimeout: 'readonly', + Date: 'readonly', + Math: 'readonly', + console: 'readonly', + }, + }, + }, +]; diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 00000000..7e6c7c00 --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,30 @@ +{ + "name": "@bytelyst/ui", + "version": "0.1.0", + "type": "module", + "exports": { + ".": "./src/index.ts", + "./button": "./src/components/Button.tsx", + "./toast": "./src/components/Toast.tsx", + "./modal": "./src/components/Modal.tsx", + "./confirm-dialog": "./src/components/ConfirmDialog.tsx", + "./badge": "./src/components/Badge.tsx", + "./empty-state": "./src/components/EmptyState.tsx" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.0", + "@radix-ui/react-alert-dialog": "^1.1.0", + "@radix-ui/react-slot": "^1.1.0", + "lucide-react": "^0.460.0", + "clsx": "^2.1.0" + }, + "devDependencies": { + "typescript": "^5.7.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0" + } +} diff --git a/packages/ui/src/components/Badge.tsx b/packages/ui/src/components/Badge.tsx new file mode 100644 index 00000000..e3582b32 --- /dev/null +++ b/packages/ui/src/components/Badge.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { clsx } from 'clsx'; + +export interface BadgeProps extends React.HTMLAttributes { + variant?: 'success' | 'warning' | 'error' | 'info' | 'neutral'; + size?: 'sm' | 'md'; + dot?: boolean; +} + +const variantStyles: Record = { + success: 'bg-green-500/10 text-green-400 border-green-500/20', + warning: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20', + error: 'bg-red-500/10 text-red-400 border-red-500/20', + info: 'bg-blue-500/10 text-blue-400 border-blue-500/20', + neutral: + 'bg-[var(--bl-surface-muted,#252540)] text-[var(--bl-text-secondary,#a0a0b0)] border-[var(--bl-border,#2a2a4a)]', +}; + +const dotColors: Record = { + success: 'bg-green-400', + error: 'bg-red-400', + warning: 'bg-yellow-400', + info: 'bg-blue-400', + neutral: 'bg-gray-400', +}; + +export function Badge({ + variant = 'neutral', + size = 'sm', + dot, + className, + children, + ...props +}: BadgeProps) { + return ( + + {dot && } + {children} + + ); +} diff --git a/packages/ui/src/components/Button.tsx b/packages/ui/src/components/Button.tsx new file mode 100644 index 00000000..a9a7e2b8 --- /dev/null +++ b/packages/ui/src/components/Button.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { clsx } from 'clsx'; +import { Loader2 } from 'lucide-react'; + +export interface ButtonProps extends React.ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'ghost' | 'destructive' | 'outline'; + size?: 'sm' | 'md' | 'lg'; + loading?: boolean; + asChild?: boolean; +} + +export const Button = React.forwardRef( + ( + { variant = 'primary', size = 'md', loading, asChild, className, children, disabled, ...props }, + ref + ) => { + const Comp = asChild ? Slot : 'button'; + + const baseStyles = + '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', + 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', + outline: + 'border border-[var(--bl-border,#2a2a4a)] text-[var(--bl-text-primary,#fff)] hover:bg-[var(--bl-surface-muted,#252540)]', + }; + + const sizes: Record = { + sm: 'h-8 px-3 text-xs gap-1.5', + md: 'h-10 px-4 text-sm gap-2', + lg: 'h-12 px-6 text-base gap-2.5', + }; + + return ( + + {loading && } + {children} + + ); + } +); + +Button.displayName = 'Button'; diff --git a/packages/ui/src/components/ConfirmDialog.tsx b/packages/ui/src/components/ConfirmDialog.tsx new file mode 100644 index 00000000..abedb317 --- /dev/null +++ b/packages/ui/src/components/ConfirmDialog.tsx @@ -0,0 +1,80 @@ +'use client'; + +import * as React from 'react'; +import * as AlertDialog from '@radix-ui/react-alert-dialog'; +import { clsx } from 'clsx'; +import { AlertTriangle } from 'lucide-react'; +import { Button } from './Button.js'; + +export interface ConfirmDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + description: string; + confirmLabel?: string; + cancelLabel?: string; + variant?: 'destructive' | 'warning'; + loading?: boolean; + onConfirm: () => void | Promise; +} + +export function ConfirmDialog({ + open, + onOpenChange, + title, + description, + confirmLabel = 'Confirm', + cancelLabel = 'Cancel', + variant = 'destructive', + loading, + onConfirm, +}: ConfirmDialogProps) { + return ( + + + + +
+
+ +
+
+ {title} + + {description} + +
+
+
+ + + + + + +
+
+
+
+ ); +} diff --git a/packages/ui/src/components/EmptyState.tsx b/packages/ui/src/components/EmptyState.tsx new file mode 100644 index 00000000..a82d4b15 --- /dev/null +++ b/packages/ui/src/components/EmptyState.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { clsx } from 'clsx'; +import { Inbox } from 'lucide-react'; +import { Button } from './Button.js'; + +export interface EmptyStateProps { + icon?: React.ReactNode; + title: string; + description?: string; + actionLabel?: string; + onAction?: () => void; + className?: string; +} + +export function EmptyState({ + icon, + title, + description, + actionLabel, + onAction, + className, +}: EmptyStateProps) { + return ( +
+
+ {icon ?? } +
+

{title}

+ {description && ( +

+ {description} +

+ )} + {actionLabel && onAction && ( + + )} +
+ ); +} diff --git a/packages/ui/src/components/Modal.tsx b/packages/ui/src/components/Modal.tsx new file mode 100644 index 00000000..c1fe06cf --- /dev/null +++ b/packages/ui/src/components/Modal.tsx @@ -0,0 +1,64 @@ +'use client'; + +import * as React from 'react'; +import * as Dialog from '@radix-ui/react-dialog'; +import { clsx } from 'clsx'; +import { X } from 'lucide-react'; + +export interface ModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + description?: string; + size?: 'sm' | 'md' | 'lg' | 'full'; + children: React.ReactNode; +} + +const sizes: Record = { + sm: 'max-w-sm', + md: 'max-w-lg', + lg: 'max-w-2xl', + full: 'max-w-[90vw]', +}; + +export function Modal({ + open, + onOpenChange, + title, + description, + size = 'md', + children, +}: ModalProps) { + return ( + + + + + {title} + {description && ( + + {description} + + )} +
{children}
+ + + +
+
+
+ ); +} diff --git a/packages/ui/src/components/Toast.tsx b/packages/ui/src/components/Toast.tsx new file mode 100644 index 00000000..8a912749 --- /dev/null +++ b/packages/ui/src/components/Toast.tsx @@ -0,0 +1,97 @@ +'use client'; + +import * as React from 'react'; +import { clsx } from 'clsx'; +import { X, CheckCircle, AlertTriangle, Info, AlertCircle } from 'lucide-react'; + +export interface ToastMessage { + id: string; + type: 'success' | 'error' | 'warning' | 'info'; + title: string; + description?: string; + duration?: number; +} + +type ToastListener = (toasts: ToastMessage[]) => void; +let globalToasts: ToastMessage[] = []; +const listeners = new Set(); +function notifyListeners() { + listeners.forEach(l => l([...globalToasts])); +} + +export function toast(msg: Omit) { + const id = `toast-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; + globalToasts = [...globalToasts, { ...msg, id }]; + notifyListeners(); + const duration = msg.duration ?? 5000; + if (duration > 0) setTimeout(() => dismissToast(id), duration); +} + +export function dismissToast(id: string) { + globalToasts = globalToasts.filter(t => t.id !== id); + notifyListeners(); +} + +export function useToast() { + return { toast, dismiss: dismissToast }; +} + +const icons: Record = { + success: , + error: , + warning: , + info: , +}; + +export function Toast({ message, onDismiss }: { message: ToastMessage; onDismiss: () => void }) { + return ( +
+ {icons[message.type]} +
+

{message.title}

+ {message.description && ( +

+ {message.description} +

+ )} +
+ +
+ ); +} + +export function ToastProvider({ children }: { children: React.ReactNode }) { + const [toasts, setToasts] = React.useState([]); + React.useEffect(() => { + listeners.add(setToasts); + return () => { + listeners.delete(setToasts); + }; + }, []); + + return ( + <> + {children} +
+ {toasts.map(t => ( + dismissToast(t.id)} /> + ))} +
+ + ); +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts new file mode 100644 index 00000000..0efcf9b4 --- /dev/null +++ b/packages/ui/src/index.ts @@ -0,0 +1,13 @@ +export { Button, type ButtonProps } from './components/Button.js'; +export { + Toast, + ToastProvider, + useToast, + toast, + dismissToast, + type ToastMessage, +} from './components/Toast.js'; +export { Modal, type ModalProps } from './components/Modal.js'; +export { ConfirmDialog, type ConfirmDialogProps } from './components/ConfirmDialog.js'; +export { Badge, type BadgeProps } from './components/Badge.js'; +export { EmptyState, type EmptyStateProps } from './components/EmptyState.js'; diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json new file mode 100644 index 00000000..b4e95a7c --- /dev/null +++ b/packages/ui/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "outDir": "dist", + "rootDir": "src", + "declaration": true + }, + "include": ["src"] +}