feat(ui): create @bytelyst/ui with Button, Toast, Modal, ConfirmDialog, Badge, EmptyState
This commit is contained in:
parent
b3a1ee5db9
commit
252403f74e
@ -79,6 +79,31 @@ export default [
|
|||||||
ReactNode: 'readonly',
|
ReactNode: 'readonly',
|
||||||
React: 'readonly',
|
React: 'readonly',
|
||||||
JSX: '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: {
|
plugins: {
|
||||||
|
|||||||
18
packages/ui/eslint.config.js
Normal file
18
packages/ui/eslint.config.js
Normal file
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
30
packages/ui/package.json
Normal file
30
packages/ui/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
49
packages/ui/src/components/Badge.tsx
Normal file
49
packages/ui/src/components/Badge.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
|
||||||
|
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||||
|
variant?: 'success' | 'warning' | 'error' | 'info' | 'neutral';
|
||||||
|
size?: 'sm' | 'md';
|
||||||
|
dot?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantStyles: Record<string, string> = {
|
||||||
|
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<string, string> = {
|
||||||
|
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 (
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
'inline-flex items-center gap-1.5 rounded-full border font-medium',
|
||||||
|
size === 'sm' ? 'px-2 py-0.5 text-xs' : 'px-3 py-1 text-sm',
|
||||||
|
variantStyles[variant],
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{dot && <span className={clsx('h-1.5 w-1.5 rounded-full', dotColors[variant])} />}
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
packages/ui/src/components/Button.tsx
Normal file
54
packages/ui/src/components/Button.tsx
Normal file
@ -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<HTMLButtonElement> {
|
||||||
|
variant?: 'primary' | 'secondary' | 'ghost' | 'destructive' | 'outline';
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
loading?: boolean;
|
||||||
|
asChild?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
(
|
||||||
|
{ 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<string, string> = {
|
||||||
|
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<string, string> = {
|
||||||
|
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 (
|
||||||
|
<Comp
|
||||||
|
ref={ref}
|
||||||
|
className={clsx(baseStyles, variants[variant], sizes[size], className)}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{loading && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||||
|
{children}
|
||||||
|
</Comp>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Button.displayName = 'Button';
|
||||||
80
packages/ui/src/components/ConfirmDialog.tsx
Normal file
80
packages/ui/src/components/ConfirmDialog.tsx
Normal file
@ -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<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfirmDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
confirmLabel = 'Confirm',
|
||||||
|
cancelLabel = 'Cancel',
|
||||||
|
variant = 'destructive',
|
||||||
|
loading,
|
||||||
|
onConfirm,
|
||||||
|
}: ConfirmDialogProps) {
|
||||||
|
return (
|
||||||
|
<AlertDialog.Root open={open} onOpenChange={onOpenChange}>
|
||||||
|
<AlertDialog.Portal>
|
||||||
|
<AlertDialog.Overlay className="fixed inset-0 z-[9998] bg-black/60 backdrop-blur-sm" />
|
||||||
|
<AlertDialog.Content
|
||||||
|
className={clsx(
|
||||||
|
'fixed left-1/2 top-1/2 z-[9999] -translate-x-1/2 -translate-y-1/2',
|
||||||
|
'w-full max-w-md rounded-xl border p-6 shadow-xl',
|
||||||
|
'bg-[var(--bl-bg-elevated,#12151c)] border-[var(--bl-border,#2a2a4a)] text-[var(--bl-text-primary,#fff)]',
|
||||||
|
'focus:outline-none'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'mt-0.5 rounded-full p-2',
|
||||||
|
variant === 'destructive'
|
||||||
|
? 'bg-red-500/10 text-red-400'
|
||||||
|
: 'bg-yellow-500/10 text-yellow-400'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<AlertTriangle className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<AlertDialog.Title className="text-lg font-semibold">{title}</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description className="mt-2 text-sm text-[var(--bl-text-secondary,#a0a0b0)]">
|
||||||
|
{description}
|
||||||
|
</AlertDialog.Description>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flex justify-end gap-3">
|
||||||
|
<AlertDialog.Cancel asChild>
|
||||||
|
<Button variant="ghost">{cancelLabel}</Button>
|
||||||
|
</AlertDialog.Cancel>
|
||||||
|
<AlertDialog.Action asChild>
|
||||||
|
<Button
|
||||||
|
variant={variant === 'destructive' ? 'destructive' : 'primary'}
|
||||||
|
loading={loading}
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
{confirmLabel}
|
||||||
|
</Button>
|
||||||
|
</AlertDialog.Action>
|
||||||
|
</div>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Portal>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
packages/ui/src/components/EmptyState.tsx
Normal file
46
packages/ui/src/components/EmptyState.tsx
Normal file
@ -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 (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'flex flex-col items-center justify-center py-16 px-4 text-center',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="mb-4 text-[var(--bl-text-tertiary,#555)]">
|
||||||
|
{icon ?? <Inbox className="h-12 w-12" />}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium text-[var(--bl-text-primary,#fff)]">{title}</h3>
|
||||||
|
{description && (
|
||||||
|
<p className="mt-2 max-w-sm text-sm text-[var(--bl-text-secondary,#a0a0b0)]">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{actionLabel && onAction && (
|
||||||
|
<Button variant="primary" size="sm" className="mt-4" onClick={onAction}>
|
||||||
|
{actionLabel}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
packages/ui/src/components/Modal.tsx
Normal file
64
packages/ui/src/components/Modal.tsx
Normal file
@ -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<string, string> = {
|
||||||
|
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 (
|
||||||
|
<Dialog.Root open={open} onOpenChange={onOpenChange}>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay className="fixed inset-0 z-[9998] bg-black/60 backdrop-blur-sm" />
|
||||||
|
<Dialog.Content
|
||||||
|
className={clsx(
|
||||||
|
'fixed left-1/2 top-1/2 z-[9999] -translate-x-1/2 -translate-y-1/2',
|
||||||
|
'w-full rounded-xl border p-6 shadow-xl',
|
||||||
|
'bg-[var(--bl-bg-elevated,#12151c)] border-[var(--bl-border,#2a2a4a)] text-[var(--bl-text-primary,#fff)]',
|
||||||
|
'focus:outline-none',
|
||||||
|
sizes[size]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Dialog.Title className="text-lg font-semibold">{title}</Dialog.Title>
|
||||||
|
{description && (
|
||||||
|
<Dialog.Description className="mt-1 text-sm text-[var(--bl-text-secondary,#a0a0b0)]">
|
||||||
|
{description}
|
||||||
|
</Dialog.Description>
|
||||||
|
)}
|
||||||
|
<div className="mt-4">{children}</div>
|
||||||
|
<Dialog.Close asChild>
|
||||||
|
<button
|
||||||
|
className="absolute right-4 top-4 text-[var(--bl-text-tertiary,#666)] hover:text-[var(--bl-text-primary,#fff)]"
|
||||||
|
aria-label="Close dialog"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</Dialog.Close>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
packages/ui/src/components/Toast.tsx
Normal file
97
packages/ui/src/components/Toast.tsx
Normal file
@ -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<ToastListener>();
|
||||||
|
function notifyListeners() {
|
||||||
|
listeners.forEach(l => l([...globalToasts]));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toast(msg: Omit<ToastMessage, 'id'>) {
|
||||||
|
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<string, React.ReactNode> = {
|
||||||
|
success: <CheckCircle className="h-5 w-5 text-green-400" />,
|
||||||
|
error: <AlertCircle className="h-5 w-5 text-red-400" />,
|
||||||
|
warning: <AlertTriangle className="h-5 w-5 text-yellow-400" />,
|
||||||
|
info: <Info className="h-5 w-5 text-blue-400" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Toast({ message, onDismiss }: { message: ToastMessage; onDismiss: () => void }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
className={clsx(
|
||||||
|
'flex items-start gap-3 rounded-lg border p-4 shadow-lg backdrop-blur-sm',
|
||||||
|
'bg-[var(--bl-surface-card,#1a1a2e)] border-[var(--bl-border,#2a2a4a)] text-[var(--bl-text-primary,#fff)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{icons[message.type]}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium">{message.title}</p>
|
||||||
|
{message.description && (
|
||||||
|
<p className="mt-1 text-xs text-[var(--bl-text-secondary,#a0a0b0)]">
|
||||||
|
{message.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onDismiss}
|
||||||
|
className="text-[var(--bl-text-tertiary,#666)] hover:text-[var(--bl-text-primary,#fff)]"
|
||||||
|
aria-label="Dismiss notification"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [toasts, setToasts] = React.useState<ToastMessage[]>([]);
|
||||||
|
React.useEffect(() => {
|
||||||
|
listeners.add(setToasts);
|
||||||
|
return () => {
|
||||||
|
listeners.delete(setToasts);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<div
|
||||||
|
className="fixed bottom-4 right-4 z-[9999] flex flex-col gap-2 max-w-sm"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{toasts.map(t => (
|
||||||
|
<Toast key={t.id} message={t} onDismiss={() => dismissToast(t.id)} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
packages/ui/src/index.ts
Normal file
13
packages/ui/src/index.ts
Normal file
@ -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';
|
||||||
10
packages/ui/tsconfig.json
Normal file
10
packages/ui/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"declaration": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user