@aereamart/design-system 0.2.0 → 0.3.0

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.
Files changed (106) hide show
  1. package/dist/Accordion.d.ts +10 -0
  2. package/dist/Accordion.js +12 -0
  3. package/dist/AlertBanner.d.ts +9 -0
  4. package/dist/AlertBanner.js +21 -0
  5. package/dist/AppBar.d.ts +7 -0
  6. package/dist/AppBar.js +5 -0
  7. package/dist/Avatar.d.ts +6 -0
  8. package/dist/Avatar.js +20 -0
  9. package/dist/Badge.js +4 -4
  10. package/dist/BottomSheet.d.ts +8 -0
  11. package/dist/BottomSheet.js +24 -0
  12. package/dist/Breadcrumb.d.ts +8 -0
  13. package/dist/Breadcrumb.js +9 -0
  14. package/dist/Button.js +3 -3
  15. package/dist/Card.d.ts +7 -0
  16. package/dist/Card.js +6 -0
  17. package/dist/Checkbox.d.ts +5 -0
  18. package/dist/Checkbox.js +5 -0
  19. package/dist/Chip.d.ts +7 -0
  20. package/dist/Chip.js +15 -0
  21. package/dist/DataTable.d.ts +15 -0
  22. package/dist/DataTable.js +7 -0
  23. package/dist/DatePicker.d.ts +5 -0
  24. package/dist/DatePicker.js +11 -0
  25. package/dist/Divider.d.ts +5 -0
  26. package/dist/Divider.js +11 -0
  27. package/dist/Drawer.d.ts +10 -0
  28. package/dist/Drawer.js +24 -0
  29. package/dist/EmptyState.d.ts +7 -0
  30. package/dist/EmptyState.js +6 -0
  31. package/dist/Input.js +3 -3
  32. package/dist/Link.d.ts +4 -0
  33. package/dist/Link.js +6 -0
  34. package/dist/Menu.d.ts +14 -0
  35. package/dist/Menu.js +29 -0
  36. package/dist/Modal.js +1 -1
  37. package/dist/NavigationBar.d.ts +14 -0
  38. package/dist/NavigationBar.js +22 -0
  39. package/dist/Pagination.d.ts +7 -0
  40. package/dist/Pagination.js +24 -0
  41. package/dist/Popover.d.ts +7 -0
  42. package/dist/Popover.js +9 -0
  43. package/dist/Progress.d.ts +8 -0
  44. package/dist/Progress.js +15 -0
  45. package/dist/Radio.d.ts +15 -0
  46. package/dist/Radio.js +8 -0
  47. package/dist/SegmentedControl.d.ts +13 -0
  48. package/dist/SegmentedControl.js +18 -0
  49. package/dist/Select.js +3 -3
  50. package/dist/Sidebar.d.ts +16 -0
  51. package/dist/Sidebar.js +19 -0
  52. package/dist/Slider.d.ts +5 -0
  53. package/dist/Slider.js +5 -0
  54. package/dist/Snackbar.d.ts +12 -0
  55. package/dist/Snackbar.js +20 -0
  56. package/dist/Switch.d.ts +4 -0
  57. package/dist/Switch.js +5 -0
  58. package/dist/Tabs.d.ts +14 -0
  59. package/dist/Tabs.js +24 -0
  60. package/dist/Tag.d.ts +6 -0
  61. package/dist/Tag.js +11 -0
  62. package/dist/Textarea.js +3 -3
  63. package/dist/Tooltip.d.ts +7 -0
  64. package/dist/Tooltip.js +5 -0
  65. package/dist/TreeView.d.ts +12 -0
  66. package/dist/TreeView.js +34 -0
  67. package/dist/index.d.ts +32 -2
  68. package/dist/index.js +32 -2
  69. package/package.json +1 -1
  70. package/src/Accordion.tsx +58 -0
  71. package/src/AlertBanner.tsx +62 -0
  72. package/src/AppBar.tsx +29 -0
  73. package/src/Avatar.tsx +42 -0
  74. package/src/Badge.tsx +4 -4
  75. package/src/BottomSheet.tsx +60 -0
  76. package/src/Breadcrumb.tsx +48 -0
  77. package/src/Button.tsx +3 -3
  78. package/src/Card.tsx +39 -0
  79. package/src/Checkbox.tsx +31 -0
  80. package/src/Chip.tsx +44 -0
  81. package/src/DataTable.tsx +64 -0
  82. package/src/DatePicker.tsx +44 -0
  83. package/src/Divider.tsx +33 -0
  84. package/src/Drawer.tsx +71 -0
  85. package/src/EmptyState.tsx +37 -0
  86. package/src/Input.tsx +4 -4
  87. package/src/Link.tsx +22 -0
  88. package/src/Menu.tsx +73 -0
  89. package/src/Modal.tsx +3 -3
  90. package/src/NavigationBar.tsx +68 -0
  91. package/src/Pagination.tsx +69 -0
  92. package/src/Popover.tsx +34 -0
  93. package/src/Progress.tsx +53 -0
  94. package/src/Radio.tsx +49 -0
  95. package/src/SegmentedControl.tsx +63 -0
  96. package/src/Select.tsx +4 -4
  97. package/src/Sidebar.tsx +73 -0
  98. package/src/Slider.tsx +33 -0
  99. package/src/Snackbar.tsx +72 -0
  100. package/src/Switch.tsx +40 -0
  101. package/src/Tabs.tsx +77 -0
  102. package/src/Tag.tsx +29 -0
  103. package/src/Textarea.tsx +4 -4
  104. package/src/Tooltip.tsx +25 -0
  105. package/src/TreeView.tsx +105 -0
  106. package/src/index.ts +38 -8
@@ -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
@@ -12,12 +12,12 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
12
12
 
13
13
  const variantClasses: Record<ButtonVariant, string> = {
14
14
  primary:
15
- 'bg-primary-600 text-white hover:bg-primary-700 focus-visible:ring-primary-500/40',
15
+ 'bg-lime-600 text-white hover:bg-lime-700 focus-visible:ring-lime-500/40',
16
16
  secondary:
17
- 'border border-neutral-300 bg-white text-neutral-700 hover:bg-neutral-50 focus-visible:ring-neutral-400/40',
17
+ 'border border-slate-300 bg-white text-slate-700 hover:bg-slate-50 focus-visible:ring-slate-400/40',
18
18
  danger: 'bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-500/40',
19
19
  ghost:
20
- 'bg-transparent text-neutral-600 hover:bg-neutral-100 focus-visible:ring-neutral-400/40',
20
+ 'bg-transparent text-slate-600 hover:bg-slate-100 focus-visible:ring-slate-400/40',
21
21
  };
22
22
 
23
23
  const sizeClasses: Record<ButtonSize, string> = {
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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,37 @@
1
+ import { Inbox } from 'lucide-react';
2
+ import { cn } from './cn';
3
+
4
+ export interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ title: string;
6
+ description?: string;
7
+ icon?: React.ReactNode;
8
+ action?: React.ReactNode;
9
+ }
10
+
11
+ export function EmptyState({
12
+ title,
13
+ description,
14
+ icon,
15
+ action,
16
+ className,
17
+ ...props
18
+ }: EmptyStateProps) {
19
+ return (
20
+ <div
21
+ className={cn(
22
+ 'flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-slate-200 bg-white px-8 py-12 text-center',
23
+ className
24
+ )}
25
+ {...props}
26
+ >
27
+ <div className="mb-3 rounded-full bg-lime-50 p-3 text-lime-600">
28
+ {icon ?? <Inbox aria-hidden="true" className="h-8 w-8" />}
29
+ </div>
30
+ <h3 className="text-base font-bold text-slate-900">{title}</h3>
31
+ {description && (
32
+ <p className="mt-1 max-w-sm text-sm text-slate-500">{description}</p>
33
+ )}
34
+ {action && <div className="mt-5">{action}</div>}
35
+ </div>
36
+ );
37
+ }
package/src/Input.tsx CHANGED
@@ -9,17 +9,17 @@ export function Input({ label, error, className, id, ...props }: InputProps) {
9
9
  return (
10
10
  <div className="w-full">
11
11
  {label && (
12
- <label htmlFor={id} className="mb-1 block text-sm font-medium text-neutral-700">
12
+ <label htmlFor={id} className="mb-1 block text-sm font-medium text-slate-700">
13
13
  {label}
14
14
  </label>
15
15
  )}
16
16
  <input
17
17
  id={id}
18
18
  className={cn(
19
- 'w-full rounded-lg border bg-white px-4 py-2.5 text-sm text-neutral-900 placeholder:text-neutral-400 focus:outline-none focus:ring-2',
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
20
  error
21
- ? 'border-red-500 focus:border-red-500 focus:ring-red-500/30'
22
- : 'border-neutral-300 focus:border-primary-600 focus:ring-primary-500/30',
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
23
  className
24
24
  )}
25
25
  {...props}
package/src/Link.tsx ADDED
@@ -0,0 +1,22 @@
1
+ import { ExternalLink } from 'lucide-react';
2
+ import { cn } from './cn';
3
+
4
+ export interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
5
+ external?: boolean;
6
+ }
7
+
8
+ export function Link({ external = false, className, children, ...props }: LinkProps) {
9
+ return (
10
+ <a
11
+ className={cn(
12
+ 'inline-flex items-center gap-1 font-medium text-lime-700 hover:text-lime-800 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-lime-500/40 rounded-sm',
13
+ className
14
+ )}
15
+ {...(external ? { target: '_blank', rel: 'noreferrer' } : {})}
16
+ {...props}
17
+ >
18
+ {children}
19
+ {external && <ExternalLink aria-hidden="true" className="h-3.5 w-3.5" />}
20
+ </a>
21
+ );
22
+ }
package/src/Menu.tsx ADDED
@@ -0,0 +1,73 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { cn } from './cn';
3
+
4
+ export interface MenuItem {
5
+ label: string;
6
+ onSelect?: () => void;
7
+ danger?: boolean;
8
+ disabled?: boolean;
9
+ icon?: React.ReactNode;
10
+ }
11
+
12
+ export interface MenuProps {
13
+ trigger: React.ReactNode;
14
+ items: MenuItem[];
15
+ align?: 'start' | 'end';
16
+ className?: string;
17
+ }
18
+
19
+ export function Menu({ trigger, items, align = 'end', className }: MenuProps) {
20
+ const [open, setOpen] = useState(false);
21
+ const ref = useRef<HTMLDivElement>(null);
22
+
23
+ useEffect(() => {
24
+ if (!open) return;
25
+ const onDown = (e: MouseEvent) => {
26
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
27
+ };
28
+ const onKey = (e: KeyboardEvent) => {
29
+ if (e.key === 'Escape') setOpen(false);
30
+ };
31
+ document.addEventListener('mousedown', onDown);
32
+ document.addEventListener('keydown', onKey);
33
+ return () => {
34
+ document.removeEventListener('mousedown', onDown);
35
+ document.removeEventListener('keydown', onKey);
36
+ };
37
+ }, [open]);
38
+
39
+ return (
40
+ <div ref={ref} className={cn('relative inline-flex', className)}>
41
+ <div onClick={() => setOpen(v => !v)}>{trigger}</div>
42
+ {open && (
43
+ <div
44
+ role="menu"
45
+ className={cn(
46
+ 'absolute z-50 mt-1.5 min-w-40 overflow-hidden rounded-lg border border-slate-200 bg-white py-1 shadow-lg',
47
+ align === 'end' ? 'right-0' : 'left-0'
48
+ )}
49
+ >
50
+ {items.map((item, i) => (
51
+ <button
52
+ key={`${item.label}-${i}`}
53
+ type="button"
54
+ role="menuitem"
55
+ disabled={item.disabled}
56
+ onClick={() => {
57
+ setOpen(false);
58
+ item.onSelect?.();
59
+ }}
60
+ className={cn(
61
+ 'flex w-full items-center gap-2.5 px-4 py-2 text-left text-sm transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:bg-slate-100',
62
+ item.danger ? 'text-red-700 hover:bg-red-50' : 'text-slate-700 hover:bg-slate-50'
63
+ )}
64
+ >
65
+ {item.icon}
66
+ <span className="flex-1">{item.label}</span>
67
+ </button>
68
+ ))}
69
+ </div>
70
+ )}
71
+ </div>
72
+ );
73
+ }
package/src/Modal.tsx CHANGED
@@ -43,11 +43,11 @@ export function Modal({
43
43
  aria-modal="true"
44
44
  >
45
45
  {title && (
46
- <div className="flex items-center justify-between border-b border-neutral-100 px-6 py-4">
47
- <h2 className="text-lg font-bold text-neutral-900">{title}</h2>
46
+ <div className="flex items-center justify-between border-b border-slate-100 px-6 py-4">
47
+ <h2 className="text-lg font-bold text-slate-900">{title}</h2>
48
48
  <button
49
49
  onClick={onClose}
50
- className="rounded-lg p-1 text-neutral-400 hover:bg-neutral-100"
50
+ className="rounded-lg p-1 text-slate-400 hover:bg-slate-100"
51
51
  aria-label="Close"
52
52
  >
53
53
  <X className="h-5 w-5" />