@aereamart/design-system 0.3.0 → 0.3.2
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/package.json +43 -43
- package/src/Accordion.tsx +57 -57
- package/src/AlertBanner.tsx +61 -61
- package/src/Badge.tsx +27 -27
- package/src/BottomSheet.tsx +59 -59
- package/src/Button.tsx +54 -54
- package/src/Card.tsx +38 -38
- package/src/DataTable.tsx +63 -63
- package/src/DatePicker.tsx +43 -43
- package/src/Drawer.tsx +70 -70
- package/src/EmptyState.tsx +36 -36
- package/src/Input.tsx +30 -30
- package/src/Menu.tsx +72 -72
- package/src/Modal.tsx +62 -62
- package/src/NavigationBar.tsx +67 -67
- package/src/Pagination.tsx +68 -68
- package/src/Popover.tsx +33 -33
- package/src/SegmentedControl.tsx +62 -62
- package/src/Select.tsx +32 -32
- package/src/Sidebar.tsx +72 -72
- package/src/Snackbar.tsx +71 -71
- package/src/Spinner.tsx +19 -19
- package/src/Tabs.tsx +76 -76
- package/src/Textarea.tsx +30 -30
- package/src/Tooltip.tsx +24 -24
- package/src/TreeView.tsx +104 -104
- package/src/cn.ts +2 -2
package/src/SegmentedControl.tsx
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import { cn } from './cn';
|
|
3
|
-
|
|
4
|
-
export interface SegmentedOption<T extends string | number> {
|
|
5
|
-
value: T;
|
|
6
|
-
label: string;
|
|
7
|
-
disabled?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface SegmentedControlProps<T extends string | number> {
|
|
11
|
-
options: SegmentedOption<T>[];
|
|
12
|
-
value?: T;
|
|
13
|
-
defaultValue?: T;
|
|
14
|
-
onChange?: (value: T) => void;
|
|
15
|
-
className?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function SegmentedControl<T extends string | number>({
|
|
19
|
-
options,
|
|
20
|
-
value,
|
|
21
|
-
defaultValue,
|
|
22
|
-
onChange,
|
|
23
|
-
className,
|
|
24
|
-
}: SegmentedControlProps<T>) {
|
|
25
|
-
const isControlled = value !== undefined;
|
|
26
|
-
const [inner, setInner] = useState<T | undefined>(defaultValue);
|
|
27
|
-
const active = isControlled ? value : inner;
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<div
|
|
31
|
-
role="tablist"
|
|
32
|
-
className={cn(
|
|
33
|
-
'inline-flex items-center gap-1 rounded-lg border border-slate-200 bg-white p-1',
|
|
34
|
-
className
|
|
35
|
-
)}
|
|
36
|
-
>
|
|
37
|
-
{options.map(opt => {
|
|
38
|
-
const selected = active === opt.value;
|
|
39
|
-
return (
|
|
40
|
-
<button
|
|
41
|
-
key={String(opt.value)}
|
|
42
|
-
type="button"
|
|
43
|
-
role="tab"
|
|
44
|
-
aria-selected={selected}
|
|
45
|
-
disabled={opt.disabled}
|
|
46
|
-
onClick={() => {
|
|
47
|
-
if (!isControlled) setInner(opt.value);
|
|
48
|
-
onChange?.(opt.value);
|
|
49
|
-
}}
|
|
50
|
-
className={cn(
|
|
51
|
-
'rounded-md px-4 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-lime-500/40 disabled:opacity-50',
|
|
52
|
-
selected
|
|
53
|
-
? 'bg-lime-600 text-white'
|
|
54
|
-
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
|
55
|
-
)}
|
|
56
|
-
>
|
|
57
|
-
{opt.label}
|
|
58
|
-
</button>
|
|
59
|
-
);
|
|
60
|
-
})}
|
|
61
|
-
</div>
|
|
62
|
-
);
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { cn } from './cn';
|
|
3
|
+
|
|
4
|
+
export interface SegmentedOption<T extends string | number> {
|
|
5
|
+
value: T;
|
|
6
|
+
label: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SegmentedControlProps<T extends string | number> {
|
|
11
|
+
options: SegmentedOption<T>[];
|
|
12
|
+
value?: T;
|
|
13
|
+
defaultValue?: T;
|
|
14
|
+
onChange?: (value: T) => void;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function SegmentedControl<T extends string | number>({
|
|
19
|
+
options,
|
|
20
|
+
value,
|
|
21
|
+
defaultValue,
|
|
22
|
+
onChange,
|
|
23
|
+
className,
|
|
24
|
+
}: SegmentedControlProps<T>) {
|
|
25
|
+
const isControlled = value !== undefined;
|
|
26
|
+
const [inner, setInner] = useState<T | undefined>(defaultValue);
|
|
27
|
+
const active = isControlled ? value : inner;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
role="tablist"
|
|
32
|
+
className={cn(
|
|
33
|
+
'inline-flex items-center gap-1 rounded-lg border border-slate-200 bg-white p-1',
|
|
34
|
+
className
|
|
35
|
+
)}
|
|
36
|
+
>
|
|
37
|
+
{options.map(opt => {
|
|
38
|
+
const selected = active === opt.value;
|
|
39
|
+
return (
|
|
40
|
+
<button
|
|
41
|
+
key={String(opt.value)}
|
|
42
|
+
type="button"
|
|
43
|
+
role="tab"
|
|
44
|
+
aria-selected={selected}
|
|
45
|
+
disabled={opt.disabled}
|
|
46
|
+
onClick={() => {
|
|
47
|
+
if (!isControlled) setInner(opt.value);
|
|
48
|
+
onChange?.(opt.value);
|
|
49
|
+
}}
|
|
50
|
+
className={cn(
|
|
51
|
+
'rounded-md px-4 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-lime-500/40 disabled:opacity-50',
|
|
52
|
+
selected
|
|
53
|
+
? 'bg-lime-600 text-white'
|
|
54
|
+
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
{opt.label}
|
|
58
|
+
</button>
|
|
59
|
+
);
|
|
60
|
+
})}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
63
|
}
|
package/src/Select.tsx
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import { cn } from './cn';
|
|
2
|
-
|
|
3
|
-
export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
|
4
|
-
label?: string;
|
|
5
|
-
error?: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function Select({ label, error, className, id, children, ...props }: SelectProps) {
|
|
9
|
-
return (
|
|
10
|
-
<div className="w-full">
|
|
11
|
-
{label && (
|
|
12
|
-
<label htmlFor={id} className="mb-1 block text-sm font-medium text-slate-700">
|
|
13
|
-
{label}
|
|
14
|
-
</label>
|
|
15
|
-
)}
|
|
16
|
-
<select
|
|
17
|
-
id={id}
|
|
18
|
-
className={cn(
|
|
19
|
-
'w-full rounded-lg border bg-white px-3 py-2.5 text-sm text-slate-900 focus:outline-none focus:ring-2',
|
|
20
|
-
error
|
|
21
|
-
? 'border-red-600 focus:border-red-600 focus:ring-red-500/30'
|
|
22
|
-
: 'border-slate-500 focus:border-lime-600 focus:ring-lime-500/30',
|
|
23
|
-
className
|
|
24
|
-
)}
|
|
25
|
-
{...props}
|
|
26
|
-
>
|
|
27
|
-
{children}
|
|
28
|
-
</select>
|
|
29
|
-
{error && <p className="mt-1 text-xs text-red-600">{error}</p>}
|
|
30
|
-
</div>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
1
|
+
import { cn } from './cn';
|
|
2
|
+
|
|
3
|
+
export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
|
4
|
+
label?: string;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Select({ label, error, className, id, children, ...props }: SelectProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="w-full">
|
|
11
|
+
{label && (
|
|
12
|
+
<label htmlFor={id} className="mb-1 block text-sm font-medium text-slate-700">
|
|
13
|
+
{label}
|
|
14
|
+
</label>
|
|
15
|
+
)}
|
|
16
|
+
<select
|
|
17
|
+
id={id}
|
|
18
|
+
className={cn(
|
|
19
|
+
'w-full rounded-lg border bg-white px-3 py-2.5 text-sm text-slate-900 focus:outline-none focus:ring-2',
|
|
20
|
+
error
|
|
21
|
+
? 'border-red-600 focus:border-red-600 focus:ring-red-500/30'
|
|
22
|
+
: 'border-slate-500 focus:border-lime-600 focus:ring-lime-500/30',
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</select>
|
|
29
|
+
{error && <p className="mt-1 text-xs text-red-600">{error}</p>}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
package/src/Sidebar.tsx
CHANGED
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import { cn } from './cn';
|
|
3
|
-
|
|
4
|
-
export interface SidebarItem {
|
|
5
|
-
value: string;
|
|
6
|
-
label: string;
|
|
7
|
-
icon?: React.ReactNode;
|
|
8
|
-
badge?: React.ReactNode;
|
|
9
|
-
disabled?: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface SidebarProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange'> {
|
|
13
|
-
items: SidebarItem[];
|
|
14
|
-
value?: string;
|
|
15
|
-
defaultValue?: string;
|
|
16
|
-
onChange?: (value: string) => void;
|
|
17
|
-
header?: React.ReactNode;
|
|
18
|
-
footer?: React.ReactNode;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function Sidebar({
|
|
22
|
-
items,
|
|
23
|
-
value,
|
|
24
|
-
defaultValue,
|
|
25
|
-
onChange,
|
|
26
|
-
header,
|
|
27
|
-
footer,
|
|
28
|
-
className,
|
|
29
|
-
...props
|
|
30
|
-
}: SidebarProps) {
|
|
31
|
-
const controlled = value !== undefined;
|
|
32
|
-
const [active, setActive] = useState(defaultValue);
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<nav
|
|
36
|
-
className={cn('flex w-64 flex-col gap-2 rounded-xl border border-slate-200 bg-white p-3', className)}
|
|
37
|
-
{...props}
|
|
38
|
-
>
|
|
39
|
-
{header && <div className="border-b border-slate-100 pb-2">{header}</div>}
|
|
40
|
-
<ul className="flex-1 space-y-0.5">
|
|
41
|
-
{items.map(item => {
|
|
42
|
-
const isActive = controlled ? value === item.value : active === item.value;
|
|
43
|
-
return (
|
|
44
|
-
<li key={item.value}>
|
|
45
|
-
<button
|
|
46
|
-
type="button"
|
|
47
|
-
disabled={item.disabled}
|
|
48
|
-
onClick={() => {
|
|
49
|
-
if (!controlled) setActive(item.value);
|
|
50
|
-
onChange?.(item.value);
|
|
51
|
-
}}
|
|
52
|
-
className={cn(
|
|
53
|
-
'flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-left text-sm font-medium transition-colors',
|
|
54
|
-
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-lime-500/40 disabled:cursor-not-allowed',
|
|
55
|
-
item.disabled
|
|
56
|
-
? 'cursor-not-allowed text-slate-300'
|
|
57
|
-
: isActive
|
|
58
|
-
? 'bg-lime-600 text-white'
|
|
59
|
-
: 'text-slate-700 hover:bg-lime-50 hover:text-lime-800'
|
|
60
|
-
)}
|
|
61
|
-
>
|
|
62
|
-
{item.icon}
|
|
63
|
-
<span className="flex-1 truncate">{item.label}</span>
|
|
64
|
-
{item.badge}
|
|
65
|
-
</button>
|
|
66
|
-
</li>
|
|
67
|
-
);
|
|
68
|
-
})}
|
|
69
|
-
</ul>
|
|
70
|
-
{footer && <div className="border-t border-slate-100 pt-2">{footer}</div>}
|
|
71
|
-
</nav>
|
|
72
|
-
);
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { cn } from './cn';
|
|
3
|
+
|
|
4
|
+
export interface SidebarItem {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: React.ReactNode;
|
|
8
|
+
badge?: React.ReactNode;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SidebarProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange'> {
|
|
13
|
+
items: SidebarItem[];
|
|
14
|
+
value?: string;
|
|
15
|
+
defaultValue?: string;
|
|
16
|
+
onChange?: (value: string) => void;
|
|
17
|
+
header?: React.ReactNode;
|
|
18
|
+
footer?: React.ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function Sidebar({
|
|
22
|
+
items,
|
|
23
|
+
value,
|
|
24
|
+
defaultValue,
|
|
25
|
+
onChange,
|
|
26
|
+
header,
|
|
27
|
+
footer,
|
|
28
|
+
className,
|
|
29
|
+
...props
|
|
30
|
+
}: SidebarProps) {
|
|
31
|
+
const controlled = value !== undefined;
|
|
32
|
+
const [active, setActive] = useState(defaultValue);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<nav
|
|
36
|
+
className={cn('flex w-64 flex-col gap-2 rounded-xl border border-slate-200 bg-white p-3', className)}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
{header && <div className="border-b border-slate-100 pb-2">{header}</div>}
|
|
40
|
+
<ul className="flex-1 space-y-0.5">
|
|
41
|
+
{items.map(item => {
|
|
42
|
+
const isActive = controlled ? value === item.value : active === item.value;
|
|
43
|
+
return (
|
|
44
|
+
<li key={item.value}>
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
disabled={item.disabled}
|
|
48
|
+
onClick={() => {
|
|
49
|
+
if (!controlled) setActive(item.value);
|
|
50
|
+
onChange?.(item.value);
|
|
51
|
+
}}
|
|
52
|
+
className={cn(
|
|
53
|
+
'flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-left text-sm font-medium transition-colors',
|
|
54
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-lime-500/40 disabled:cursor-not-allowed',
|
|
55
|
+
item.disabled
|
|
56
|
+
? 'cursor-not-allowed text-slate-300'
|
|
57
|
+
: isActive
|
|
58
|
+
? 'bg-lime-600 text-white'
|
|
59
|
+
: 'text-slate-700 hover:bg-lime-50 hover:text-lime-800'
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
{item.icon}
|
|
63
|
+
<span className="flex-1 truncate">{item.label}</span>
|
|
64
|
+
{item.badge}
|
|
65
|
+
</button>
|
|
66
|
+
</li>
|
|
67
|
+
);
|
|
68
|
+
})}
|
|
69
|
+
</ul>
|
|
70
|
+
{footer && <div className="border-t border-slate-100 pt-2">{footer}</div>}
|
|
71
|
+
</nav>
|
|
72
|
+
);
|
|
73
73
|
}
|
package/src/Snackbar.tsx
CHANGED
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
import { CheckCircle2, XCircle, AlertTriangle, Info, X } from 'lucide-react';
|
|
2
|
-
import { cn } from './cn';
|
|
3
|
-
|
|
4
|
-
type SnackbarTone = 'success' | 'error' | 'warning' | 'info';
|
|
5
|
-
|
|
6
|
-
export interface SnackbarProps {
|
|
7
|
-
open?: boolean;
|
|
8
|
-
tone?: SnackbarTone;
|
|
9
|
-
message: string;
|
|
10
|
-
actionLabel?: string;
|
|
11
|
-
onAction?: () => void;
|
|
12
|
-
onClose?: () => void;
|
|
13
|
-
className?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const toneIcon: Record<SnackbarTone, React.ReactNode> = {
|
|
17
|
-
success: <CheckCircle2 className="h-5 w-5 shrink-0" />,
|
|
18
|
-
error: <XCircle className="h-5 w-5 shrink-0" />,
|
|
19
|
-
warning: <AlertTriangle className="h-5 w-5 shrink-0" />,
|
|
20
|
-
info: <Info className="h-5 w-5 shrink-0" />,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const toneText: Record<SnackbarTone, string> = {
|
|
24
|
-
success: 'text-lime-600',
|
|
25
|
-
error: 'text-red-600',
|
|
26
|
-
warning: 'text-purple-600',
|
|
27
|
-
info: 'text-sky-600',
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export function Snackbar({
|
|
31
|
-
open = true,
|
|
32
|
-
tone = 'info',
|
|
33
|
-
message,
|
|
34
|
-
actionLabel,
|
|
35
|
-
onAction,
|
|
36
|
-
onClose,
|
|
37
|
-
className,
|
|
38
|
-
}: SnackbarProps) {
|
|
39
|
-
if (!open) return null;
|
|
40
|
-
return (
|
|
41
|
-
<div
|
|
42
|
-
role="status"
|
|
43
|
-
aria-live="polite"
|
|
44
|
-
className={cn(
|
|
45
|
-
'pointer-events-auto flex items-center gap-3 rounded-xl border border-slate-200 bg-white px-4 py-3 shadow-lg',
|
|
46
|
-
className
|
|
47
|
-
)}
|
|
48
|
-
>
|
|
49
|
-
<span className={toneText[tone]}>{toneIcon[tone]}</span>
|
|
50
|
-
<p className="min-w-0 flex-1 text-sm font-medium text-slate-800">{message}</p>
|
|
51
|
-
{actionLabel && (
|
|
52
|
-
<button
|
|
53
|
-
type="button"
|
|
54
|
-
onClick={onAction}
|
|
55
|
-
className="shrink-0 text-sm font-semibold text-lime-700 hover:text-lime-800 hover:underline"
|
|
56
|
-
>
|
|
57
|
-
{actionLabel}
|
|
58
|
-
</button>
|
|
59
|
-
)}
|
|
60
|
-
{onClose && (
|
|
61
|
-
<button
|
|
62
|
-
type="button"
|
|
63
|
-
aria-label="Close"
|
|
64
|
-
onClick={onClose}
|
|
65
|
-
className="shrink-0 rounded-full p-1 text-slate-400 hover:bg-slate-100"
|
|
66
|
-
>
|
|
67
|
-
<X className="h-4 w-4" />
|
|
68
|
-
</button>
|
|
69
|
-
)}
|
|
70
|
-
</div>
|
|
71
|
-
);
|
|
1
|
+
import { CheckCircle2, XCircle, AlertTriangle, Info, X } from 'lucide-react';
|
|
2
|
+
import { cn } from './cn';
|
|
3
|
+
|
|
4
|
+
type SnackbarTone = 'success' | 'error' | 'warning' | 'info';
|
|
5
|
+
|
|
6
|
+
export interface SnackbarProps {
|
|
7
|
+
open?: boolean;
|
|
8
|
+
tone?: SnackbarTone;
|
|
9
|
+
message: string;
|
|
10
|
+
actionLabel?: string;
|
|
11
|
+
onAction?: () => void;
|
|
12
|
+
onClose?: () => void;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const toneIcon: Record<SnackbarTone, React.ReactNode> = {
|
|
17
|
+
success: <CheckCircle2 className="h-5 w-5 shrink-0" />,
|
|
18
|
+
error: <XCircle className="h-5 w-5 shrink-0" />,
|
|
19
|
+
warning: <AlertTriangle className="h-5 w-5 shrink-0" />,
|
|
20
|
+
info: <Info className="h-5 w-5 shrink-0" />,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const toneText: Record<SnackbarTone, string> = {
|
|
24
|
+
success: 'text-lime-600',
|
|
25
|
+
error: 'text-red-600',
|
|
26
|
+
warning: 'text-purple-600',
|
|
27
|
+
info: 'text-sky-600',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function Snackbar({
|
|
31
|
+
open = true,
|
|
32
|
+
tone = 'info',
|
|
33
|
+
message,
|
|
34
|
+
actionLabel,
|
|
35
|
+
onAction,
|
|
36
|
+
onClose,
|
|
37
|
+
className,
|
|
38
|
+
}: SnackbarProps) {
|
|
39
|
+
if (!open) return null;
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
role="status"
|
|
43
|
+
aria-live="polite"
|
|
44
|
+
className={cn(
|
|
45
|
+
'pointer-events-auto flex items-center gap-3 rounded-xl border border-slate-200 bg-white px-4 py-3 shadow-lg',
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
<span className={toneText[tone]}>{toneIcon[tone]}</span>
|
|
50
|
+
<p className="min-w-0 flex-1 text-sm font-medium text-slate-800">{message}</p>
|
|
51
|
+
{actionLabel && (
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
onClick={onAction}
|
|
55
|
+
className="shrink-0 text-sm font-semibold text-lime-700 hover:text-lime-800 hover:underline"
|
|
56
|
+
>
|
|
57
|
+
{actionLabel}
|
|
58
|
+
</button>
|
|
59
|
+
)}
|
|
60
|
+
{onClose && (
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
aria-label="Close"
|
|
64
|
+
onClick={onClose}
|
|
65
|
+
className="shrink-0 rounded-full p-1 text-slate-400 hover:bg-slate-100"
|
|
66
|
+
>
|
|
67
|
+
<X className="h-4 w-4" />
|
|
68
|
+
</button>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
72
|
}
|
package/src/Spinner.tsx
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { cn } from './cn';
|
|
2
|
-
|
|
3
|
-
export function Spinner({ className }: { className?: string }) {
|
|
4
|
-
return (
|
|
5
|
-
<svg
|
|
6
|
-
className={cn(
|
|
7
|
-
'h-5 w-5 animate-spin text-current',
|
|
8
|
-
'inline-flex shrink-0',
|
|
9
|
-
className
|
|
10
|
-
)}
|
|
11
|
-
viewBox="0 0 24 24"
|
|
12
|
-
fill="none"
|
|
13
|
-
aria-hidden="true"
|
|
14
|
-
>
|
|
15
|
-
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
16
|
-
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 0 1 8-8v4a4 4 0 0 0-4 4H4z" />
|
|
17
|
-
</svg>
|
|
18
|
-
);
|
|
19
|
-
}
|
|
1
|
+
import { cn } from './cn';
|
|
2
|
+
|
|
3
|
+
export function Spinner({ className }: { className?: string }) {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
className={cn(
|
|
7
|
+
'h-5 w-5 animate-spin text-current',
|
|
8
|
+
'inline-flex shrink-0',
|
|
9
|
+
className
|
|
10
|
+
)}
|
|
11
|
+
viewBox="0 0 24 24"
|
|
12
|
+
fill="none"
|
|
13
|
+
aria-hidden="true"
|
|
14
|
+
>
|
|
15
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
16
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 0 1 8-8v4a4 4 0 0 0-4 4H4z" />
|
|
17
|
+
</svg>
|
|
18
|
+
);
|
|
19
|
+
}
|
package/src/Tabs.tsx
CHANGED
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import { cn } from './cn';
|
|
3
|
-
|
|
4
|
-
export interface Tab<T extends string | number> {
|
|
5
|
-
value: T;
|
|
6
|
-
label: string;
|
|
7
|
-
disabled?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface TabsProps<T extends string | number> {
|
|
11
|
-
tabs: Tab<T>[];
|
|
12
|
-
value?: T;
|
|
13
|
-
defaultValue?: T;
|
|
14
|
-
onChange?: (value: T) => void;
|
|
15
|
-
variant?: 'underline' | 'pill';
|
|
16
|
-
className?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function Tabs<T extends string | number>({
|
|
20
|
-
tabs,
|
|
21
|
-
value,
|
|
22
|
-
defaultValue,
|
|
23
|
-
onChange,
|
|
24
|
-
variant = 'underline',
|
|
25
|
-
className,
|
|
26
|
-
}: TabsProps<T>) {
|
|
27
|
-
const isControlled = value !== undefined;
|
|
28
|
-
const [inner, setInner] = useState<T | undefined>(defaultValue);
|
|
29
|
-
const active = isControlled ? value : inner;
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<div
|
|
33
|
-
role="tablist"
|
|
34
|
-
className={cn(
|
|
35
|
-
variant === 'underline'
|
|
36
|
-
? 'flex gap-1 border-b border-slate-200'
|
|
37
|
-
: 'inline-flex items-center gap-1 rounded-lg border border-slate-200 bg-white p-1',
|
|
38
|
-
className
|
|
39
|
-
)}
|
|
40
|
-
>
|
|
41
|
-
{tabs.map(tab => {
|
|
42
|
-
const selected = active === tab.value;
|
|
43
|
-
return (
|
|
44
|
-
<button
|
|
45
|
-
key={String(tab.value)}
|
|
46
|
-
type="button"
|
|
47
|
-
role="tab"
|
|
48
|
-
aria-selected={selected}
|
|
49
|
-
disabled={tab.disabled}
|
|
50
|
-
onClick={() => {
|
|
51
|
-
if (!isControlled) setInner(tab.value);
|
|
52
|
-
onChange?.(tab.value);
|
|
53
|
-
}}
|
|
54
|
-
className={cn(
|
|
55
|
-
'px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-lime-500/40 disabled:opacity-50',
|
|
56
|
-
variant === 'underline'
|
|
57
|
-
? cn(
|
|
58
|
-
'-mb-px border-b-2',
|
|
59
|
-
selected
|
|
60
|
-
? 'border-lime-600 text-lime-700'
|
|
61
|
-
: 'border-transparent text-slate-500 hover:border-slate-300 hover:text-slate-900'
|
|
62
|
-
)
|
|
63
|
-
: cn(
|
|
64
|
-
'rounded-md',
|
|
65
|
-
selected
|
|
66
|
-
? 'bg-lime-600 text-white'
|
|
67
|
-
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
|
68
|
-
)
|
|
69
|
-
)}
|
|
70
|
-
>
|
|
71
|
-
{tab.label}
|
|
72
|
-
</button>
|
|
73
|
-
);
|
|
74
|
-
})}
|
|
75
|
-
</div>
|
|
76
|
-
);
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { cn } from './cn';
|
|
3
|
+
|
|
4
|
+
export interface Tab<T extends string | number> {
|
|
5
|
+
value: T;
|
|
6
|
+
label: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TabsProps<T extends string | number> {
|
|
11
|
+
tabs: Tab<T>[];
|
|
12
|
+
value?: T;
|
|
13
|
+
defaultValue?: T;
|
|
14
|
+
onChange?: (value: T) => void;
|
|
15
|
+
variant?: 'underline' | 'pill';
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Tabs<T extends string | number>({
|
|
20
|
+
tabs,
|
|
21
|
+
value,
|
|
22
|
+
defaultValue,
|
|
23
|
+
onChange,
|
|
24
|
+
variant = 'underline',
|
|
25
|
+
className,
|
|
26
|
+
}: TabsProps<T>) {
|
|
27
|
+
const isControlled = value !== undefined;
|
|
28
|
+
const [inner, setInner] = useState<T | undefined>(defaultValue);
|
|
29
|
+
const active = isControlled ? value : inner;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
role="tablist"
|
|
34
|
+
className={cn(
|
|
35
|
+
variant === 'underline'
|
|
36
|
+
? 'flex gap-1 border-b border-slate-200'
|
|
37
|
+
: 'inline-flex items-center gap-1 rounded-lg border border-slate-200 bg-white p-1',
|
|
38
|
+
className
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
{tabs.map(tab => {
|
|
42
|
+
const selected = active === tab.value;
|
|
43
|
+
return (
|
|
44
|
+
<button
|
|
45
|
+
key={String(tab.value)}
|
|
46
|
+
type="button"
|
|
47
|
+
role="tab"
|
|
48
|
+
aria-selected={selected}
|
|
49
|
+
disabled={tab.disabled}
|
|
50
|
+
onClick={() => {
|
|
51
|
+
if (!isControlled) setInner(tab.value);
|
|
52
|
+
onChange?.(tab.value);
|
|
53
|
+
}}
|
|
54
|
+
className={cn(
|
|
55
|
+
'px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-lime-500/40 disabled:opacity-50',
|
|
56
|
+
variant === 'underline'
|
|
57
|
+
? cn(
|
|
58
|
+
'-mb-px border-b-2',
|
|
59
|
+
selected
|
|
60
|
+
? 'border-lime-600 text-lime-700'
|
|
61
|
+
: 'border-transparent text-slate-500 hover:border-slate-300 hover:text-slate-900'
|
|
62
|
+
)
|
|
63
|
+
: cn(
|
|
64
|
+
'rounded-md',
|
|
65
|
+
selected
|
|
66
|
+
? 'bg-lime-600 text-white'
|
|
67
|
+
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
|
68
|
+
)
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
{tab.label}
|
|
72
|
+
</button>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
77
|
}
|