@aereamart/design-system 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Accordion.d.ts +10 -0
- package/dist/Accordion.js +12 -0
- package/dist/AlertBanner.d.ts +9 -0
- package/dist/AlertBanner.js +21 -0
- package/dist/AppBar.d.ts +7 -0
- package/dist/AppBar.js +5 -0
- package/dist/Avatar.d.ts +6 -0
- package/dist/Avatar.js +20 -0
- package/dist/Badge.js +4 -4
- package/dist/BottomSheet.d.ts +8 -0
- package/dist/BottomSheet.js +24 -0
- package/dist/Breadcrumb.d.ts +8 -0
- package/dist/Breadcrumb.js +9 -0
- package/dist/Button.js +3 -3
- package/dist/Card.d.ts +7 -0
- package/dist/Card.js +6 -0
- package/dist/Checkbox.d.ts +5 -0
- package/dist/Checkbox.js +5 -0
- package/dist/Chip.d.ts +7 -0
- package/dist/Chip.js +15 -0
- package/dist/DataTable.d.ts +15 -0
- package/dist/DataTable.js +7 -0
- package/dist/DatePicker.d.ts +5 -0
- package/dist/DatePicker.js +11 -0
- package/dist/Divider.d.ts +5 -0
- package/dist/Divider.js +11 -0
- package/dist/Drawer.d.ts +10 -0
- package/dist/Drawer.js +24 -0
- package/dist/EmptyState.d.ts +7 -0
- package/dist/EmptyState.js +6 -0
- package/dist/Input.js +3 -3
- package/dist/Link.d.ts +4 -0
- package/dist/Link.js +6 -0
- package/dist/Menu.d.ts +14 -0
- package/dist/Menu.js +29 -0
- package/dist/Modal.js +1 -1
- package/dist/NavigationBar.d.ts +14 -0
- package/dist/NavigationBar.js +22 -0
- package/dist/Pagination.d.ts +7 -0
- package/dist/Pagination.js +24 -0
- package/dist/Popover.d.ts +7 -0
- package/dist/Popover.js +9 -0
- package/dist/Progress.d.ts +8 -0
- package/dist/Progress.js +15 -0
- package/dist/Radio.d.ts +15 -0
- package/dist/Radio.js +8 -0
- package/dist/SegmentedControl.d.ts +13 -0
- package/dist/SegmentedControl.js +18 -0
- package/dist/Select.js +3 -3
- package/dist/Sidebar.d.ts +16 -0
- package/dist/Sidebar.js +19 -0
- package/dist/Slider.d.ts +5 -0
- package/dist/Slider.js +5 -0
- package/dist/Snackbar.d.ts +12 -0
- package/dist/Snackbar.js +20 -0
- package/dist/Switch.d.ts +4 -0
- package/dist/Switch.js +5 -0
- package/dist/Tabs.d.ts +14 -0
- package/dist/Tabs.js +24 -0
- package/dist/Tag.d.ts +6 -0
- package/dist/Tag.js +11 -0
- package/dist/Textarea.js +3 -3
- package/dist/Tooltip.d.ts +7 -0
- package/dist/Tooltip.js +5 -0
- package/dist/TreeView.d.ts +12 -0
- package/dist/TreeView.js +34 -0
- package/dist/index.d.ts +32 -2
- package/dist/index.js +32 -2
- package/package.json +43 -43
- package/src/Accordion.tsx +58 -0
- package/src/AlertBanner.tsx +62 -0
- package/src/AppBar.tsx +29 -0
- package/src/Avatar.tsx +42 -0
- package/src/Badge.tsx +27 -27
- package/src/BottomSheet.tsx +60 -0
- package/src/Breadcrumb.tsx +48 -0
- package/src/Button.tsx +54 -54
- package/src/Card.tsx +39 -0
- package/src/Checkbox.tsx +31 -0
- package/src/Chip.tsx +44 -0
- package/src/DataTable.tsx +64 -0
- package/src/DatePicker.tsx +44 -0
- package/src/Divider.tsx +33 -0
- package/src/Drawer.tsx +71 -0
- package/src/EmptyState.tsx +37 -0
- package/src/Input.tsx +30 -30
- package/src/Link.tsx +22 -0
- package/src/Menu.tsx +73 -0
- package/src/Modal.tsx +62 -62
- package/src/NavigationBar.tsx +68 -0
- package/src/Pagination.tsx +69 -0
- package/src/Popover.tsx +34 -0
- package/src/Progress.tsx +53 -0
- package/src/Radio.tsx +49 -0
- package/src/SegmentedControl.tsx +63 -0
- package/src/Select.tsx +32 -32
- package/src/Sidebar.tsx +73 -0
- package/src/Slider.tsx +33 -0
- package/src/Snackbar.tsx +72 -0
- package/src/Spinner.tsx +19 -19
- package/src/Switch.tsx +40 -0
- package/src/Tabs.tsx +77 -0
- package/src/Tag.tsx +29 -0
- package/src/Textarea.tsx +30 -30
- package/src/Tooltip.tsx +25 -0
- package/src/TreeView.tsx +105 -0
- package/src/cn.ts +2 -2
- package/src/index.ts +38 -8
package/src/Avatar.tsx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { cn } from './cn';
|
|
2
|
+
|
|
3
|
+
export interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
src?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
size?: 'sm' | 'md' | 'lg';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const sizeClasses: Record<NonNullable<AvatarProps['size']>, string> = {
|
|
10
|
+
sm: 'h-8 w-8 text-xs',
|
|
11
|
+
md: 'h-10 w-10 text-sm',
|
|
12
|
+
lg: 'h-14 w-14 text-lg',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function initials(name?: string): string {
|
|
16
|
+
if (!name) return '?';
|
|
17
|
+
return name
|
|
18
|
+
.split(/\s+/)
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.slice(0, 2)
|
|
21
|
+
.map(w => w[0]?.toUpperCase() ?? '')
|
|
22
|
+
.join('') || '?';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function Avatar({ src, name, size = 'md', className, ...props }: AvatarProps) {
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={cn(
|
|
29
|
+
'inline-flex shrink-0 select-none items-center justify-center overflow-hidden rounded-full bg-lime-100 font-semibold text-lime-800',
|
|
30
|
+
sizeClasses[size],
|
|
31
|
+
className
|
|
32
|
+
)}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
{src ? (
|
|
36
|
+
<img src={src} alt={name ?? ''} className="h-full w-full object-cover" />
|
|
37
|
+
) : (
|
|
38
|
+
<span aria-hidden="true">{initials(name)}</span>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
package/src/Badge.tsx
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import { cn } from './cn';
|
|
2
|
-
|
|
3
|
-
type BadgeVariant = 'danger' | 'primary' | 'accent' | 'neutral';
|
|
4
|
-
|
|
5
|
-
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
6
|
-
variant?: BadgeVariant;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const variantClasses: Record<BadgeVariant, string> = {
|
|
10
|
-
danger: 'bg-red-
|
|
11
|
-
primary: 'bg-
|
|
12
|
-
accent: 'bg-
|
|
13
|
-
neutral: 'bg-
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export function Badge({ variant = 'danger', className, ...props }: BadgeProps) {
|
|
17
|
-
return (
|
|
18
|
-
<span
|
|
19
|
-
className={cn(
|
|
20
|
-
'inline-flex items-center justify-center rounded-full text-[10px] font-bold leading-none',
|
|
21
|
-
variantClasses[variant],
|
|
22
|
-
className
|
|
23
|
-
)}
|
|
24
|
-
{...props}
|
|
25
|
-
/>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
1
|
+
import { cn } from './cn';
|
|
2
|
+
|
|
3
|
+
type BadgeVariant = 'danger' | 'primary' | 'accent' | 'neutral';
|
|
4
|
+
|
|
5
|
+
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
6
|
+
variant?: BadgeVariant;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const variantClasses: Record<BadgeVariant, string> = {
|
|
10
|
+
danger: 'bg-red-100 text-red-800',
|
|
11
|
+
primary: 'bg-lime-100 text-lime-800',
|
|
12
|
+
accent: 'bg-purple-100 text-purple-900',
|
|
13
|
+
neutral: 'bg-slate-200 text-slate-700',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function Badge({ variant = 'danger', className, ...props }: BadgeProps) {
|
|
17
|
+
return (
|
|
18
|
+
<span
|
|
19
|
+
className={cn(
|
|
20
|
+
'inline-flex items-center justify-center rounded-full text-[10px] font-bold leading-none',
|
|
21
|
+
variantClasses[variant],
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { X } from 'lucide-react';
|
|
4
|
+
import { cn } from './cn';
|
|
5
|
+
|
|
6
|
+
export interface BottomSheetProps {
|
|
7
|
+
open: boolean;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
title?: string;
|
|
10
|
+
children?: React.ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function BottomSheet({ open, onClose, title, children, className }: BottomSheetProps) {
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!open) return;
|
|
17
|
+
const onKey = (e: KeyboardEvent) => {
|
|
18
|
+
if (e.key === 'Escape') onClose();
|
|
19
|
+
};
|
|
20
|
+
document.body.style.overflow = 'hidden';
|
|
21
|
+
document.addEventListener('keydown', onKey);
|
|
22
|
+
return () => {
|
|
23
|
+
document.body.style.overflow = '';
|
|
24
|
+
document.removeEventListener('keydown', onKey);
|
|
25
|
+
};
|
|
26
|
+
}, [open, onClose]);
|
|
27
|
+
|
|
28
|
+
if (!open) return null;
|
|
29
|
+
|
|
30
|
+
return createPortal(
|
|
31
|
+
<div className="fixed inset-0 z-50 flex items-end justify-center">
|
|
32
|
+
<div className="absolute inset-0 bg-black/50" onClick={onClose} aria-hidden="true" />
|
|
33
|
+
<section
|
|
34
|
+
role="dialog"
|
|
35
|
+
aria-modal="true"
|
|
36
|
+
aria-label={title}
|
|
37
|
+
className={cn(
|
|
38
|
+
'relative w-full max-w-lg rounded-t-2xl bg-white p-6 shadow-xl',
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
<div aria-hidden="true" className="mx-auto mb-4 h-1 w-10 rounded-full bg-slate-200" />
|
|
43
|
+
{title && (
|
|
44
|
+
<div className="mb-2 flex items-center justify-between">
|
|
45
|
+
<h2 className="text-lg font-bold text-slate-900">{title}</h2>
|
|
46
|
+
<button
|
|
47
|
+
onClick={onClose}
|
|
48
|
+
className="rounded-lg p-1 text-slate-400 hover:bg-slate-100"
|
|
49
|
+
aria-label="Close"
|
|
50
|
+
>
|
|
51
|
+
<X className="h-5 w-5" />
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
{children}
|
|
56
|
+
</section>
|
|
57
|
+
</div>,
|
|
58
|
+
document.body
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ChevronRight } from 'lucide-react';
|
|
2
|
+
import { cn } from './cn';
|
|
3
|
+
|
|
4
|
+
export interface Crumb {
|
|
5
|
+
label: string;
|
|
6
|
+
href?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface BreadcrumbProps extends React.HTMLAttributes<HTMLElement> {
|
|
10
|
+
items: Crumb[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Breadcrumb({ items, className, ...props }: BreadcrumbProps) {
|
|
14
|
+
return (
|
|
15
|
+
<nav aria-label="Breadcrumb" className={className} {...props}>
|
|
16
|
+
<ol className="flex flex-wrap items-center gap-1.5 text-sm">
|
|
17
|
+
{items.map((item, i) => {
|
|
18
|
+
const last = i === items.length - 1;
|
|
19
|
+
return (
|
|
20
|
+
<li key={`${item.label}-${i}`} className="flex items-center gap-1.5">
|
|
21
|
+
{i > 0 && (
|
|
22
|
+
<ChevronRight aria-hidden="true" className="h-3.5 w-3.5 text-slate-400" />
|
|
23
|
+
)}
|
|
24
|
+
{last || !item.href ? (
|
|
25
|
+
<span
|
|
26
|
+
aria-current={last ? 'page' : undefined}
|
|
27
|
+
className={cn(
|
|
28
|
+
'font-medium',
|
|
29
|
+
last ? 'text-slate-900' : 'text-slate-500'
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
{item.label}
|
|
33
|
+
</span>
|
|
34
|
+
) : (
|
|
35
|
+
<a
|
|
36
|
+
href={item.href}
|
|
37
|
+
className="font-medium text-lime-700 hover:text-lime-800 hover:underline"
|
|
38
|
+
>
|
|
39
|
+
{item.label}
|
|
40
|
+
</a>
|
|
41
|
+
)}
|
|
42
|
+
</li>
|
|
43
|
+
);
|
|
44
|
+
})}
|
|
45
|
+
</ol>
|
|
46
|
+
</nav>
|
|
47
|
+
);
|
|
48
|
+
}
|
package/src/Button.tsx
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
import { cn } from './cn';
|
|
2
|
-
import { Spinner } from './Spinner';
|
|
3
|
-
|
|
4
|
-
type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';
|
|
5
|
-
type ButtonSize = 'sm' | 'md' | 'lg' | 'full';
|
|
6
|
-
|
|
7
|
-
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
8
|
-
variant?: ButtonVariant;
|
|
9
|
-
size?: ButtonSize;
|
|
10
|
-
loading?: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const variantClasses: Record<ButtonVariant, string> = {
|
|
14
|
-
primary:
|
|
15
|
-
'bg-
|
|
16
|
-
secondary:
|
|
17
|
-
'border border-
|
|
18
|
-
danger: 'bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-500/40',
|
|
19
|
-
ghost:
|
|
20
|
-
'bg-transparent text-
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const sizeClasses: Record<ButtonSize, string> = {
|
|
24
|
-
sm: 'px-3 py-1.5 text-xs',
|
|
25
|
-
md: 'px-4 py-2.5 text-sm',
|
|
26
|
-
lg: 'px-6 py-3 text-base',
|
|
27
|
-
full: 'w-full px-4 py-2.5 text-sm',
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export function Button({
|
|
31
|
-
variant = 'primary',
|
|
32
|
-
size = 'md',
|
|
33
|
-
loading = false,
|
|
34
|
-
className,
|
|
35
|
-
disabled,
|
|
36
|
-
children,
|
|
37
|
-
...props
|
|
38
|
-
}: ButtonProps) {
|
|
39
|
-
return (
|
|
40
|
-
<button
|
|
41
|
-
className={cn(
|
|
42
|
-
'inline-flex items-center justify-center gap-2 rounded-lg font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
43
|
-
variantClasses[variant],
|
|
44
|
-
sizeClasses[size],
|
|
45
|
-
className
|
|
46
|
-
)}
|
|
47
|
-
disabled={disabled || loading}
|
|
48
|
-
{...props}
|
|
49
|
-
>
|
|
50
|
-
{loading && <Spinner className="h-4 w-4" />}
|
|
51
|
-
{children}
|
|
52
|
-
</button>
|
|
53
|
-
);
|
|
54
|
-
}
|
|
1
|
+
import { cn } from './cn';
|
|
2
|
+
import { Spinner } from './Spinner';
|
|
3
|
+
|
|
4
|
+
type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';
|
|
5
|
+
type ButtonSize = 'sm' | 'md' | 'lg' | 'full';
|
|
6
|
+
|
|
7
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
8
|
+
variant?: ButtonVariant;
|
|
9
|
+
size?: ButtonSize;
|
|
10
|
+
loading?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const variantClasses: Record<ButtonVariant, string> = {
|
|
14
|
+
primary:
|
|
15
|
+
'bg-lime-600 text-white hover:bg-lime-700 focus-visible:ring-lime-500/40',
|
|
16
|
+
secondary:
|
|
17
|
+
'border border-slate-300 bg-white text-slate-700 hover:bg-slate-50 focus-visible:ring-slate-400/40',
|
|
18
|
+
danger: 'bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-500/40',
|
|
19
|
+
ghost:
|
|
20
|
+
'bg-transparent text-slate-600 hover:bg-slate-100 focus-visible:ring-slate-400/40',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const sizeClasses: Record<ButtonSize, string> = {
|
|
24
|
+
sm: 'px-3 py-1.5 text-xs',
|
|
25
|
+
md: 'px-4 py-2.5 text-sm',
|
|
26
|
+
lg: 'px-6 py-3 text-base',
|
|
27
|
+
full: 'w-full px-4 py-2.5 text-sm',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function Button({
|
|
31
|
+
variant = 'primary',
|
|
32
|
+
size = 'md',
|
|
33
|
+
loading = false,
|
|
34
|
+
className,
|
|
35
|
+
disabled,
|
|
36
|
+
children,
|
|
37
|
+
...props
|
|
38
|
+
}: ButtonProps) {
|
|
39
|
+
return (
|
|
40
|
+
<button
|
|
41
|
+
className={cn(
|
|
42
|
+
'inline-flex items-center justify-center gap-2 rounded-lg font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
43
|
+
variantClasses[variant],
|
|
44
|
+
sizeClasses[size],
|
|
45
|
+
className
|
|
46
|
+
)}
|
|
47
|
+
disabled={disabled || loading}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
{loading && <Spinner className="h-4 w-4" />}
|
|
51
|
+
{children}
|
|
52
|
+
</button>
|
|
53
|
+
);
|
|
54
|
+
}
|
package/src/Card.tsx
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { cn } from './cn';
|
|
2
|
+
|
|
3
|
+
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
footer?: React.ReactNode;
|
|
7
|
+
hoverable?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Card({
|
|
11
|
+
title,
|
|
12
|
+
description,
|
|
13
|
+
footer,
|
|
14
|
+
hoverable = false,
|
|
15
|
+
className,
|
|
16
|
+
children,
|
|
17
|
+
...props
|
|
18
|
+
}: CardProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
className={cn(
|
|
22
|
+
'rounded-xl border border-slate-200 bg-white',
|
|
23
|
+
hoverable &&
|
|
24
|
+
'transition-all hover:-translate-y-0.5 hover:border-lime-300 hover:shadow-md',
|
|
25
|
+
className
|
|
26
|
+
)}
|
|
27
|
+
{...props}
|
|
28
|
+
>
|
|
29
|
+
{(title || description) && (
|
|
30
|
+
<div className="border-b border-slate-100 px-5 py-4">
|
|
31
|
+
{title && <h3 className="text-base font-bold text-slate-900">{title}</h3>}
|
|
32
|
+
{description && <p className="mt-0.5 text-sm text-slate-500">{description}</p>}
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
<div className="px-5 py-4">{children}</div>
|
|
36
|
+
{footer && <div className="border-t border-slate-100 px-5 py-3">{footer}</div>}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
package/src/Checkbox.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { cn } from './cn';
|
|
2
|
+
|
|
3
|
+
export interface CheckboxProps
|
|
4
|
+
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
5
|
+
label?: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Checkbox({ label, error, id, className, ...props }: CheckboxProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="grid gap-1.5">
|
|
12
|
+
<label
|
|
13
|
+
htmlFor={id}
|
|
14
|
+
className={cn('inline-flex cursor-pointer items-start gap-2.5 text-sm', className)}
|
|
15
|
+
>
|
|
16
|
+
<input
|
|
17
|
+
id={id}
|
|
18
|
+
type="checkbox"
|
|
19
|
+
className="mt-0.5 h-4 w-4 shrink-0 cursor-pointer rounded border-slate-500 text-lime-600 accent-lime-600 focus:outline-none focus:ring-2 focus:ring-lime-500/40"
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
{label && (
|
|
23
|
+
<span className={cn('font-medium', error ? 'text-red-700' : 'text-slate-700')}>
|
|
24
|
+
{label}
|
|
25
|
+
</span>
|
|
26
|
+
)}
|
|
27
|
+
</label>
|
|
28
|
+
{error && <p className="pl-6.5 text-xs text-red-600">{error}</p>}
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
package/src/Chip.tsx
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { X } from 'lucide-react';
|
|
2
|
+
import { cn } from './cn';
|
|
3
|
+
|
|
4
|
+
type ChipVariant = 'primary' | 'accent' | 'neutral' | 'danger';
|
|
5
|
+
|
|
6
|
+
export interface ChipProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
7
|
+
variant?: ChipVariant;
|
|
8
|
+
onRemove?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const variantClasses: Record<ChipVariant, string> = {
|
|
12
|
+
primary: 'border-lime-300 bg-lime-50 text-lime-800',
|
|
13
|
+
accent: 'border-purple-300 bg-purple-50 text-purple-900',
|
|
14
|
+
neutral: 'border-slate-300 bg-slate-50 text-slate-700',
|
|
15
|
+
danger: 'border-red-300 bg-red-50 text-red-800',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function Chip({ variant = 'neutral', onRemove, className, children, ...props }: ChipProps) {
|
|
19
|
+
return (
|
|
20
|
+
<span
|
|
21
|
+
className={cn(
|
|
22
|
+
'inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-sm font-medium',
|
|
23
|
+
variantClasses[variant],
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
{onRemove && (
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
onClick={e => {
|
|
33
|
+
e.stopPropagation();
|
|
34
|
+
onRemove();
|
|
35
|
+
}}
|
|
36
|
+
aria-label={`Remove ${typeof children === 'string' ? children : 'chip'}`}
|
|
37
|
+
className="-mr-1 rounded-full p-0.5 hover:bg-black/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current"
|
|
38
|
+
>
|
|
39
|
+
<X aria-hidden="true" className="h-3 w-3" />
|
|
40
|
+
</button>
|
|
41
|
+
)}
|
|
42
|
+
</span>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useId } from 'react';
|
|
2
|
+
import { cn } from './cn';
|
|
3
|
+
|
|
4
|
+
export interface Column<T> {
|
|
5
|
+
key: string;
|
|
6
|
+
label: string;
|
|
7
|
+
render?: (row: T) => React.ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DataTableProps<T> {
|
|
12
|
+
columns: Column<T>[];
|
|
13
|
+
rows: T[];
|
|
14
|
+
rowKey: (row: T) => string;
|
|
15
|
+
caption?: string;
|
|
16
|
+
emptyText?: string;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function DataTable<T>({
|
|
21
|
+
columns,
|
|
22
|
+
rows,
|
|
23
|
+
rowKey,
|
|
24
|
+
caption,
|
|
25
|
+
emptyText = 'No rows to show.',
|
|
26
|
+
className,
|
|
27
|
+
}: DataTableProps<T>) {
|
|
28
|
+
const captionId = useId();
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className={cn('overflow-x-auto rounded-xl border border-slate-200 bg-white', className)}>
|
|
32
|
+
<table className="w-full min-w-[480px] border-collapse text-left text-sm">
|
|
33
|
+
{caption && <caption className="sr-only" id={captionId}>{caption}</caption>}
|
|
34
|
+
<thead>
|
|
35
|
+
<tr className="border-b border-slate-200 bg-slate-50 text-xs uppercase tracking-wider text-slate-500">
|
|
36
|
+
{columns.map(col => (
|
|
37
|
+
<th key={col.key} scope="col" className="px-4 py-3 font-semibold">
|
|
38
|
+
{col.label}
|
|
39
|
+
</th>
|
|
40
|
+
))}
|
|
41
|
+
</tr>
|
|
42
|
+
</thead>
|
|
43
|
+
<tbody className="divide-y divide-slate-100">
|
|
44
|
+
{rows.map(row => (
|
|
45
|
+
<tr key={rowKey(row)} className="transition-colors hover:bg-slate-50">
|
|
46
|
+
{columns.map(col => (
|
|
47
|
+
<td key={col.key} className={cn('px-4 py-3 align-middle text-slate-700', col.className)}>
|
|
48
|
+
{col.render ? col.render(row) : String((row as Record<string, unknown>)[col.key] ?? '')}
|
|
49
|
+
</td>
|
|
50
|
+
))}
|
|
51
|
+
</tr>
|
|
52
|
+
))}
|
|
53
|
+
{rows.length === 0 && (
|
|
54
|
+
<tr>
|
|
55
|
+
<td colSpan={columns.length} className="px-4 py-10 text-center text-sm text-slate-400">
|
|
56
|
+
{emptyText}
|
|
57
|
+
</td>
|
|
58
|
+
</tr>
|
|
59
|
+
)}
|
|
60
|
+
</tbody>
|
|
61
|
+
</table>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useId } from 'react';
|
|
2
|
+
import { CalendarDays } from 'lucide-react';
|
|
3
|
+
import { cn } from './cn';
|
|
4
|
+
|
|
5
|
+
export interface DatePickerProps
|
|
6
|
+
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
7
|
+
label?: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function DatePicker({ label, error, id, className, ...props }: DatePickerProps) {
|
|
12
|
+
const autoId = useId();
|
|
13
|
+
const inputId = id ?? autoId;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div>
|
|
17
|
+
{label && (
|
|
18
|
+
<label htmlFor={inputId} className="mb-1 block text-sm font-medium text-slate-700">
|
|
19
|
+
{label}
|
|
20
|
+
</label>
|
|
21
|
+
)}
|
|
22
|
+
<div className="relative">
|
|
23
|
+
<CalendarDays
|
|
24
|
+
aria-hidden="true"
|
|
25
|
+
className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400"
|
|
26
|
+
/>
|
|
27
|
+
<input
|
|
28
|
+
id={inputId}
|
|
29
|
+
type="date"
|
|
30
|
+
className={cn(
|
|
31
|
+
'w-full rounded-lg border bg-white py-2.5 pl-9 pr-4 text-sm text-slate-700 shadow-xs',
|
|
32
|
+
'focus:outline-none focus:ring-2 focus:ring-lime-500/40 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:text-slate-400',
|
|
33
|
+
error
|
|
34
|
+
? 'border-red-400 focus:border-red-500'
|
|
35
|
+
: 'border-slate-300 hover:border-slate-400',
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
{error && <p className="mt-1 text-xs text-red-600">{error}</p>}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
package/src/Divider.tsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { cn } from './cn';
|
|
2
|
+
|
|
3
|
+
export interface DividerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
orientation?: 'horizontal' | 'vertical';
|
|
5
|
+
label?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Divider({ orientation = 'horizontal', label, className, ...props }: DividerProps) {
|
|
9
|
+
if (orientation === 'vertical') {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
role="separator"
|
|
13
|
+
aria-orientation="vertical"
|
|
14
|
+
className={cn('mx-3 my-0 h-auto w-px shrink-0 self-stretch bg-slate-200', className)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
if (label) {
|
|
20
|
+
return (
|
|
21
|
+
<div className={cn('flex items-center gap-3', className)} {...props}>
|
|
22
|
+
<div className="h-px flex-1 bg-slate-200" />
|
|
23
|
+
<span className="text-xs font-medium uppercase tracking-wider text-slate-400">
|
|
24
|
+
{label}
|
|
25
|
+
</span>
|
|
26
|
+
<div className="h-px flex-1 bg-slate-200" />
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return (
|
|
31
|
+
<div role="separator" className={cn('h-px w-full bg-slate-200', className)} {...props} />
|
|
32
|
+
);
|
|
33
|
+
}
|
package/src/Drawer.tsx
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { X } from 'lucide-react';
|
|
4
|
+
import { cn } from './cn';
|
|
5
|
+
|
|
6
|
+
export interface DrawerProps {
|
|
7
|
+
open: boolean;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
title?: string;
|
|
10
|
+
side?: 'right' | 'left';
|
|
11
|
+
children?: React.ReactNode;
|
|
12
|
+
footer?: React.ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Drawer({
|
|
17
|
+
open,
|
|
18
|
+
onClose,
|
|
19
|
+
title,
|
|
20
|
+
side = 'right',
|
|
21
|
+
children,
|
|
22
|
+
footer,
|
|
23
|
+
className,
|
|
24
|
+
}: DrawerProps) {
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!open) return;
|
|
27
|
+
const onKey = (e: KeyboardEvent) => {
|
|
28
|
+
if (e.key === 'Escape') onClose();
|
|
29
|
+
};
|
|
30
|
+
document.body.style.overflow = 'hidden';
|
|
31
|
+
document.addEventListener('keydown', onKey);
|
|
32
|
+
return () => {
|
|
33
|
+
document.body.style.overflow = '';
|
|
34
|
+
document.removeEventListener('keydown', onKey);
|
|
35
|
+
};
|
|
36
|
+
}, [open, onClose]);
|
|
37
|
+
|
|
38
|
+
if (!open) return null;
|
|
39
|
+
|
|
40
|
+
return createPortal(
|
|
41
|
+
<div className="fixed inset-0 z-50">
|
|
42
|
+
<div className="absolute inset-0 bg-black/50" onClick={onClose} aria-hidden="true" />
|
|
43
|
+
<section
|
|
44
|
+
role="dialog"
|
|
45
|
+
aria-modal="true"
|
|
46
|
+
aria-label={title}
|
|
47
|
+
className={cn(
|
|
48
|
+
'absolute inset-y-0 flex w-full max-w-sm flex-col bg-white shadow-xl',
|
|
49
|
+
side === 'right' ? 'right-0' : 'left-0',
|
|
50
|
+
className
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
{title && (
|
|
54
|
+
<div className="flex items-center justify-between border-b border-slate-100 px-6 py-4">
|
|
55
|
+
<h2 className="text-lg font-bold text-slate-900">{title}</h2>
|
|
56
|
+
<button
|
|
57
|
+
onClick={onClose}
|
|
58
|
+
className="rounded-lg p-1 text-slate-400 hover:bg-slate-100"
|
|
59
|
+
aria-label="Close"
|
|
60
|
+
>
|
|
61
|
+
<X className="h-5 w-5" />
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
<div className="flex-1 overflow-y-auto px-6 py-4">{children}</div>
|
|
66
|
+
{footer && <div className="border-t border-slate-100 px-6 py-4">{footer}</div>}
|
|
67
|
+
</section>
|
|
68
|
+
</div>,
|
|
69
|
+
document.body
|
|
70
|
+
);
|
|
71
|
+
}
|