@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.
@@ -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
  }