@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/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-
|
|
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-
|
|
20
|
-
error
|
|
21
|
-
? 'border-red-
|
|
22
|
-
: 'border-
|
|
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
ADDED
|
@@ -0,0 +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
|
+
);
|
|
73
|
+
}
|
package/src/Slider.tsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { cn } from './cn';
|
|
2
|
+
|
|
3
|
+
export interface SliderProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
4
|
+
label?: string;
|
|
5
|
+
valueLabel?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Slider({ label, valueLabel, id, className, ...props }: SliderProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className={cn('w-full max-w-md', className)}>
|
|
11
|
+
{(label || valueLabel) && (
|
|
12
|
+
<div className="mb-2 flex items-center justify-between">
|
|
13
|
+
{label && (
|
|
14
|
+
<label htmlFor={id} className="text-sm font-medium text-slate-700">
|
|
15
|
+
{label}
|
|
16
|
+
</label>
|
|
17
|
+
)}
|
|
18
|
+
{valueLabel && (
|
|
19
|
+
<span className="text-sm font-semibold tabular-nums text-slate-600">
|
|
20
|
+
{valueLabel}
|
|
21
|
+
</span>
|
|
22
|
+
)}
|
|
23
|
+
</div>
|
|
24
|
+
)}
|
|
25
|
+
<input
|
|
26
|
+
id={id}
|
|
27
|
+
type="range"
|
|
28
|
+
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-slate-200 accent-lime-600 focus:outline-none focus:ring-2 focus:ring-lime-500/40"
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
package/src/Snackbar.tsx
ADDED
|
@@ -0,0 +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
|
+
);
|
|
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/Switch.tsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { cn } from './cn';
|
|
2
|
+
|
|
3
|
+
export interface SwitchProps
|
|
4
|
+
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
5
|
+
label?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Switch({ label, id, className, checked, onChange, ...props }: SwitchProps) {
|
|
9
|
+
return (
|
|
10
|
+
<label
|
|
11
|
+
htmlFor={id}
|
|
12
|
+
className={cn('inline-flex cursor-pointer items-center gap-3', className)}
|
|
13
|
+
>
|
|
14
|
+
<input
|
|
15
|
+
id={id}
|
|
16
|
+
type="checkbox"
|
|
17
|
+
role="switch"
|
|
18
|
+
checked={checked}
|
|
19
|
+
onChange={onChange}
|
|
20
|
+
className="sr-only"
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
<span
|
|
24
|
+
aria-hidden="true"
|
|
25
|
+
className={cn(
|
|
26
|
+
'relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors',
|
|
27
|
+
checked ? 'bg-lime-600' : 'bg-slate-300'
|
|
28
|
+
)}
|
|
29
|
+
>
|
|
30
|
+
<span
|
|
31
|
+
className={cn(
|
|
32
|
+
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
|
|
33
|
+
checked ? 'translate-x-6' : 'translate-x-1'
|
|
34
|
+
)}
|
|
35
|
+
/>
|
|
36
|
+
</span>
|
|
37
|
+
{label && <span className="text-sm font-medium text-slate-700">{label}</span>}
|
|
38
|
+
</label>
|
|
39
|
+
);
|
|
40
|
+
}
|
package/src/Tabs.tsx
ADDED
|
@@ -0,0 +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
|
+
);
|
|
77
|
+
}
|
package/src/Tag.tsx
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { cn } from './cn';
|
|
2
|
+
|
|
3
|
+
type TagTone = 'primary' | 'accent' | 'neutral' | 'danger';
|
|
4
|
+
|
|
5
|
+
export interface TagProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
6
|
+
tone?: TagTone;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const toneClasses: Record<TagTone, string> = {
|
|
10
|
+
primary: 'border-lime-300 bg-white text-lime-800',
|
|
11
|
+
accent: 'border-purple-300 bg-white text-purple-900',
|
|
12
|
+
neutral: 'border-slate-300 bg-white text-slate-700',
|
|
13
|
+
danger: 'border-red-300 bg-white text-red-800',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function Tag({ tone = 'neutral', className, children, ...props }: TagProps) {
|
|
17
|
+
return (
|
|
18
|
+
<span
|
|
19
|
+
className={cn(
|
|
20
|
+
'inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-xs font-semibold',
|
|
21
|
+
toneClasses[tone],
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
{children}
|
|
27
|
+
</span>
|
|
28
|
+
);
|
|
29
|
+
}
|
package/src/Textarea.tsx
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import { cn } from './cn';
|
|
2
|
-
|
|
3
|
-
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
4
|
-
label?: string;
|
|
5
|
-
error?: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function Textarea({ label, error, className, id, ...props }: TextareaProps) {
|
|
9
|
-
return (
|
|
10
|
-
<div className="w-full">
|
|
11
|
-
{label && (
|
|
12
|
-
<label htmlFor={id} className="mb-1 block text-sm font-medium text-
|
|
13
|
-
{label}
|
|
14
|
-
</label>
|
|
15
|
-
)}
|
|
16
|
-
<textarea
|
|
17
|
-
id={id}
|
|
18
|
-
className={cn(
|
|
19
|
-
'w-full rounded-lg border bg-white px-4 py-2.5 text-sm text-
|
|
20
|
-
error
|
|
21
|
-
? 'border-red-
|
|
22
|
-
: 'border-
|
|
23
|
-
className
|
|
24
|
-
)}
|
|
25
|
-
{...props}
|
|
26
|
-
/>
|
|
27
|
-
{error && <p className="mt-1 text-xs text-red-600">{error}</p>}
|
|
28
|
-
</div>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
1
|
+
import { cn } from './cn';
|
|
2
|
+
|
|
3
|
+
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
4
|
+
label?: string;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Textarea({ label, error, className, id, ...props }: TextareaProps) {
|
|
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
|
+
<textarea
|
|
17
|
+
id={id}
|
|
18
|
+
className={cn(
|
|
19
|
+
'w-full rounded-lg border bg-white px-4 py-2.5 text-sm text-slate-900 placeholder:text-slate-500 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
|
+
{error && <p className="mt-1 text-xs text-red-600">{error}</p>}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
package/src/Tooltip.tsx
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cn } from './cn';
|
|
2
|
+
|
|
3
|
+
export interface TooltipProps {
|
|
4
|
+
content: React.ReactNode;
|
|
5
|
+
side?: 'top' | 'bottom';
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Tooltip({ content, side = 'top', children, className }: TooltipProps) {
|
|
11
|
+
return (
|
|
12
|
+
<span className={cn('group relative inline-flex', className)}>
|
|
13
|
+
{children}
|
|
14
|
+
<span
|
|
15
|
+
role="tooltip"
|
|
16
|
+
className={cn(
|
|
17
|
+
'pointer-events-none absolute left-1/2 z-50 w-max max-w-xs -translate-x-1/2 rounded-lg bg-slate-900 px-3 py-1.5 text-xs font-medium text-white opacity-0 shadow-lg transition-opacity duration-200 group-hover:opacity-100 group-focus-within:opacity-100',
|
|
18
|
+
side === 'top' ? 'bottom-full mb-2' : 'top-full mt-2'
|
|
19
|
+
)}
|
|
20
|
+
>
|
|
21
|
+
{content}
|
|
22
|
+
</span>
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
25
|
+
}
|
package/src/TreeView.tsx
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { useId, useState } from 'react';
|
|
2
|
+
import { ChevronRight } from 'lucide-react';
|
|
3
|
+
import { cn } from './cn';
|
|
4
|
+
|
|
5
|
+
export interface TreeNode {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
children?: TreeNode[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TreeViewProps {
|
|
12
|
+
nodes: TreeNode[];
|
|
13
|
+
defaultExpanded?: string[];
|
|
14
|
+
defaultAllExpanded?: boolean;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Node({
|
|
19
|
+
node,
|
|
20
|
+
depth,
|
|
21
|
+
expanded,
|
|
22
|
+
onToggle,
|
|
23
|
+
}: {
|
|
24
|
+
node: TreeNode;
|
|
25
|
+
depth: number;
|
|
26
|
+
expanded: Set<string>;
|
|
27
|
+
onToggle: (id: string) => void;
|
|
28
|
+
}) {
|
|
29
|
+
const hasChildren = !!node.children?.length;
|
|
30
|
+
const isOpen = expanded.has(node.id);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<li>
|
|
34
|
+
<div className={cn('flex items-center gap-1.5 rounded-lg py-1.5 pr-2')}>
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
aria-expanded={hasChildren ? isOpen : undefined}
|
|
38
|
+
onClick={() => hasChildren && onToggle(node.id)}
|
|
39
|
+
className={cn(
|
|
40
|
+
'inline-flex w-full items-center gap-1.5 rounded-lg py-1.5 pr-2 text-left text-sm font-medium transition-colors',
|
|
41
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-lime-500/40',
|
|
42
|
+
'hover:bg-slate-100'
|
|
43
|
+
)}
|
|
44
|
+
style={{ paddingLeft: `${8 + depth * 24}px` }}
|
|
45
|
+
>
|
|
46
|
+
{hasChildren ? (
|
|
47
|
+
<ChevronRight
|
|
48
|
+
className={cn('h-4 w-4 shrink-0 text-slate-400 transition-transform', isOpen && 'rotate-90')}
|
|
49
|
+
/>
|
|
50
|
+
) : (
|
|
51
|
+
<span className="ml-4 h-4 w-4 shrink-0" aria-hidden="true" />
|
|
52
|
+
)}
|
|
53
|
+
<span className={cn(isOpen && 'font-semibold text-slate-900')}>{node.label}</span>
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
{hasChildren && isOpen && (
|
|
57
|
+
<ul role="group">
|
|
58
|
+
{node.children!.map(child => (
|
|
59
|
+
<Node key={child.id} node={child} depth={depth + 1} expanded={expanded} onToggle={onToggle} />
|
|
60
|
+
))}
|
|
61
|
+
</ul>
|
|
62
|
+
)}
|
|
63
|
+
</li>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function TreeView({ nodes, defaultExpanded = [], defaultAllExpanded = false, className }: TreeViewProps) {
|
|
68
|
+
const [expanded, setExpanded] = useState<Set<string>>(() => {
|
|
69
|
+
if (defaultAllExpanded) {
|
|
70
|
+
const all = new Set<string>();
|
|
71
|
+
const walk = (list: TreeNode[]) => list.forEach(n => {
|
|
72
|
+
all.add(n.id);
|
|
73
|
+
if (n.children) walk(n.children);
|
|
74
|
+
});
|
|
75
|
+
walk(nodes);
|
|
76
|
+
return all;
|
|
77
|
+
}
|
|
78
|
+
return new Set(defaultExpanded);
|
|
79
|
+
});
|
|
80
|
+
const labelId = useId();
|
|
81
|
+
|
|
82
|
+
const toggle = (id: string) =>
|
|
83
|
+
setExpanded(prev => {
|
|
84
|
+
const next = new Set(prev);
|
|
85
|
+
if (next.has(id)) next.delete(id);
|
|
86
|
+
else next.add(id);
|
|
87
|
+
return next;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<ul
|
|
92
|
+
role="tree"
|
|
93
|
+
aria-label="Tree"
|
|
94
|
+
aria-labelledby={labelId}
|
|
95
|
+
className={cn('space-y-0.5', className)}
|
|
96
|
+
>
|
|
97
|
+
<span id={labelId} className="sr-only">
|
|
98
|
+
Tree view
|
|
99
|
+
</span>
|
|
100
|
+
{nodes.map(node => (
|
|
101
|
+
<Node key={node.id} node={node} depth={0} expanded={expanded} onToggle={toggle} />
|
|
102
|
+
))}
|
|
103
|
+
</ul>
|
|
104
|
+
);
|
|
105
|
+
}
|
package/src/cn.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export function cn(...parts: Array<string | false | null | undefined>): string {
|
|
2
|
-
return parts.filter(Boolean).join(' ');
|
|
1
|
+
export function cn(...parts: Array<string | false | null | undefined>): string {
|
|
2
|
+
return parts.filter(Boolean).join(' ');
|
|
3
3
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,38 @@
|
|
|
1
|
-
export { cn } from './cn';
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
1
|
+
export { cn } from './cn';
|
|
2
|
+
export { Accordion, type AccordionProps, type AccordionItem } from './Accordion';
|
|
3
|
+
export { AlertBanner, type AlertBannerProps } from './AlertBanner';
|
|
4
|
+
export { AppBar, type AppBarProps } from './AppBar';
|
|
5
|
+
export { Avatar, type AvatarProps } from './Avatar';
|
|
6
|
+
export { Badge, type BadgeProps } from './Badge';
|
|
7
|
+
export { BottomSheet, type BottomSheetProps } from './BottomSheet';
|
|
8
|
+
export { Breadcrumb, type BreadcrumbProps, type Crumb } from './Breadcrumb';
|
|
9
|
+
export { Button, type ButtonProps } from './Button';
|
|
10
|
+
export { Card, type CardProps } from './Card';
|
|
11
|
+
export { Checkbox, type CheckboxProps } from './Checkbox';
|
|
12
|
+
export { Chip, type ChipProps } from './Chip';
|
|
13
|
+
export { DataTable, type DataTableProps, type Column } from './DataTable';
|
|
14
|
+
export { DatePicker, type DatePickerProps } from './DatePicker';
|
|
15
|
+
export { Divider, type DividerProps } from './Divider';
|
|
16
|
+
export { Drawer, type DrawerProps } from './Drawer';
|
|
17
|
+
export { EmptyState, type EmptyStateProps } from './EmptyState';
|
|
18
|
+
export { Input, type InputProps } from './Input';
|
|
19
|
+
export { Link, type LinkProps } from './Link';
|
|
20
|
+
export { Menu, type MenuProps, type MenuItem } from './Menu';
|
|
21
|
+
export { Modal, type ModalProps } from './Modal';
|
|
22
|
+
export { NavigationBar, type NavigationBarProps, type NavItem } from './NavigationBar';
|
|
23
|
+
export { Pagination, type PaginationProps } from './Pagination';
|
|
24
|
+
export { Popover, type PopoverProps } from './Popover';
|
|
25
|
+
export { Progress, type ProgressProps } from './Progress';
|
|
26
|
+
export { Radio, RadioGroup, type RadioProps, type RadioGroupProps } from './Radio';
|
|
27
|
+
export { SegmentedControl, type SegmentedControlProps, type SegmentedOption } from './SegmentedControl';
|
|
28
|
+
export { Select, type SelectProps } from './Select';
|
|
29
|
+
export { Sidebar, type SidebarProps, type SidebarItem } from './Sidebar';
|
|
30
|
+
export { Slider, type SliderProps } from './Slider';
|
|
31
|
+
export { Snackbar, type SnackbarProps } from './Snackbar';
|
|
32
|
+
export { Spinner } from './Spinner';
|
|
33
|
+
export { Switch, type SwitchProps } from './Switch';
|
|
34
|
+
export { Tabs, type TabsProps, type Tab } from './Tabs';
|
|
35
|
+
export { Tag, type TagProps } from './Tag';
|
|
36
|
+
export { Textarea, type TextareaProps } from './Textarea';
|
|
37
|
+
export { Tooltip, type TooltipProps } from './Tooltip';
|
|
38
|
+
export { TreeView, type TreeViewProps, type TreeNode } from './TreeView';
|