cronixui 1.1.1 → 1.1.3

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 (76) hide show
  1. package/README.md +12 -8
  2. package/package.json +71 -71
  3. package/packages/flutter/.qwen/settings.json +7 -0
  4. package/packages/flutter/pubspec.yaml +20 -20
  5. package/packages/go/cronixui/cronixui.go +926 -926
  6. package/packages/python/README.md +142 -0
  7. package/packages/python/cronixui/__init__.py +15 -6
  8. package/packages/python/cronixui/__pycache__/__init__.cpython-314.pyc +0 -0
  9. package/packages/python/cronixui/__pycache__/accordion.cpython-314.pyc +0 -0
  10. package/packages/python/cronixui/__pycache__/alert.cpython-314.pyc +0 -0
  11. package/packages/python/cronixui/__pycache__/avatar.cpython-314.pyc +0 -0
  12. package/packages/python/cronixui/__pycache__/badge.cpython-314.pyc +0 -0
  13. package/packages/python/cronixui/__pycache__/button.cpython-314.pyc +0 -0
  14. package/packages/python/cronixui/__pycache__/card.cpython-314.pyc +0 -0
  15. package/packages/python/cronixui/__pycache__/command_palette.cpython-314.pyc +0 -0
  16. package/packages/python/cronixui/__pycache__/core.cpython-314.pyc +0 -0
  17. package/packages/python/cronixui/__pycache__/dropdown.cpython-314.pyc +0 -0
  18. package/packages/python/cronixui/__pycache__/form.cpython-314.pyc +0 -0
  19. package/packages/python/cronixui/__pycache__/layout.cpython-314.pyc +0 -0
  20. package/packages/python/cronixui/__pycache__/list.cpython-314.pyc +0 -0
  21. package/packages/python/cronixui/__pycache__/loading.cpython-314.pyc +0 -0
  22. package/packages/python/cronixui/__pycache__/modal.cpython-314.pyc +0 -0
  23. package/packages/python/cronixui/__pycache__/nav.cpython-314.pyc +0 -0
  24. package/packages/python/cronixui/__pycache__/pagination.cpython-314.pyc +0 -0
  25. package/packages/python/cronixui/__pycache__/progress.cpython-314.pyc +0 -0
  26. package/packages/python/cronixui/__pycache__/search.cpython-314.pyc +0 -0
  27. package/packages/python/cronixui/__pycache__/table.cpython-314.pyc +0 -0
  28. package/packages/python/cronixui/__pycache__/tabs.cpython-314.pyc +0 -0
  29. package/packages/python/cronixui/__pycache__/toast.cpython-314.pyc +0 -0
  30. package/packages/python/cronixui/__pycache__/toggle.cpython-314.pyc +0 -0
  31. package/packages/python/cronixui/__pycache__/tokens.cpython-314.pyc +0 -0
  32. package/packages/python/cronixui/__pycache__/tooltip.cpython-314.pyc +0 -0
  33. package/packages/python/cronixui/alert.py +119 -36
  34. package/packages/python/cronixui/avatar.py +129 -22
  35. package/packages/python/cronixui/badge.py +161 -24
  36. package/packages/python/cronixui/button.py +96 -27
  37. package/packages/python/cronixui/card.py +206 -33
  38. package/packages/python/cronixui/core.py +212 -23
  39. package/packages/python/cronixui/form.py +552 -141
  40. package/packages/python/cronixui/layout.py +358 -96
  41. package/packages/python/cronixui/list.py +140 -37
  42. package/packages/python/cronixui/loading.py +107 -17
  43. package/packages/python/cronixui/progress.py +189 -47
  44. package/packages/python/cronixui/table.py +118 -31
  45. package/packages/python/cronixui/tooltip.py +117 -15
  46. package/packages/react/src/components/Accordion.tsx +82 -82
  47. package/packages/react/src/components/Button.tsx +47 -47
  48. package/packages/react/src/components/Card.tsx +69 -69
  49. package/packages/react/src/components/CommandPalette.tsx +131 -131
  50. package/packages/react/src/components/Dropdown.tsx +88 -88
  51. package/packages/react/src/components/FileInput.tsx +86 -86
  52. package/packages/react/src/components/FormGroup.tsx +36 -36
  53. package/packages/react/src/components/List.tsx +55 -55
  54. package/packages/react/src/components/Pagination.tsx +107 -107
  55. package/packages/react/src/components/Progress.tsx +49 -49
  56. package/packages/react/src/components/Search.tsx +95 -95
  57. package/packages/react/src/components/Sidebar.tsx +64 -64
  58. package/packages/react/src/components/Stack.tsx +69 -69
  59. package/packages/react/src/components/Table.tsx +90 -90
  60. package/packages/react/src/components/Toast.tsx +134 -134
  61. package/packages/react/src/components/Typography.tsx +66 -66
  62. package/packages/react/src/index.ts +40 -40
  63. package/packages/react/src/styles.css +2039 -2039
  64. package/packages/rust/cronixui/src/components/avatar.rs +85 -85
  65. package/packages/rust/cronixui/src/components/breadcrumb.rs +58 -58
  66. package/packages/rust/cronixui/src/components/card.rs +259 -259
  67. package/packages/rust/cronixui/src/components/command_palette.rs +254 -254
  68. package/packages/rust/cronixui/src/components/dropdown.rs +179 -179
  69. package/packages/rust/cronixui/src/components/file_input.rs +74 -74
  70. package/packages/rust/cronixui/src/components/mod.rs +51 -51
  71. package/packages/rust/cronixui/src/components/search.rs +185 -185
  72. package/packages/rust/cronixui/src/components/skeleton.rs +63 -63
  73. package/packages/rust/cronixui/src/components/table.rs +56 -56
  74. package/packages/rust/cronixui/src/lib.rs +128 -128
  75. package/packages/web/dist/cronixui.css +97 -93
  76. package/packages/web/dist/cronixui.min.css +1 -1
@@ -1,69 +1,69 @@
1
- import * as React from 'react';
2
-
3
- export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
4
- clickable?: boolean;
5
- }
6
-
7
- export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
8
-
9
- export interface CardBodyProps extends React.HTMLAttributes<HTMLDivElement> {}
10
-
11
- export interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> {}
12
-
13
- export interface CardIconProps extends React.HTMLAttributes<HTMLDivElement> {}
14
-
15
- export interface CardTitleProps extends React.HTMLAttributes<HTMLDivElement> {}
16
-
17
- export interface CardSubtitleProps extends React.HTMLAttributes<HTMLDivElement> {}
18
-
19
- export const Card: React.FC<CardProps> & {
20
- Header: React.FC<CardHeaderProps>;
21
- Body: React.FC<CardBodyProps>;
22
- Footer: React.FC<CardFooterProps>;
23
- Icon: React.FC<CardIconProps>;
24
- Title: React.FC<CardTitleProps>;
25
- Subtitle: React.FC<CardSubtitleProps>;
26
- } = Object.assign(
27
- ({ children, clickable = false, className = '', ...props }: CardProps) => {
28
- return (
29
- <div
30
- className={`cn-card ${clickable ? 'cn-card-clickable' : ''} ${className}`.trim()}
31
- {...props}
32
- >
33
- {children}
34
- </div>
35
- );
36
- },
37
- {
38
- Header: ({ children, className = '', ...props }: CardHeaderProps) => (
39
- <div className={`cn-card-header ${className}`.trim()} {...props}>{children}</div>
40
- ),
41
- Body: ({ children, className = '', ...props }: CardBodyProps) => (
42
- <div className={`cn-card-body ${className}`.trim()} {...props}>{children}</div>
43
- ),
44
- Footer: ({ children, className = '', ...props }: CardFooterProps) => (
45
- <div className={`cn-card-footer ${className}`.trim()} {...props}>{children}</div>
46
- ),
47
- Icon: ({ children, className = '', ...props }: CardIconProps) => (
48
- <div className={`cn-card-icon ${className}`.trim()} {...props}>
49
- {children}
50
- </div>
51
- ),
52
- Title: ({ children, className = '', ...props }: CardTitleProps) => (
53
- <div className={`cn-card-title ${className}`.trim()} {...props}>{children}</div>
54
- ),
55
- Subtitle: ({ children, className = '', ...props }: CardSubtitleProps) => (
56
- <div className={`cn-card-subtitle ${className}`.trim()} {...props}>{children}</div>
57
- ),
58
- }
59
- );
60
-
61
- Card.displayName = 'Card';
62
- Card.Header.displayName = 'Card.Header';
63
- Card.Body.displayName = 'Card.Body';
64
- Card.Footer.displayName = 'Card.Footer';
65
- Card.Icon.displayName = 'Card.Icon';
66
- Card.Title.displayName = 'Card.Title';
67
- Card.Subtitle.displayName = 'Card.Subtitle';
68
-
69
- export default Card;
1
+ import * as React from 'react';
2
+
3
+ export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
4
+ clickable?: boolean;
5
+ }
6
+
7
+ export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
8
+
9
+ export interface CardBodyProps extends React.HTMLAttributes<HTMLDivElement> {}
10
+
11
+ export interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> {}
12
+
13
+ export interface CardIconProps extends React.HTMLAttributes<HTMLDivElement> {}
14
+
15
+ export interface CardTitleProps extends React.HTMLAttributes<HTMLDivElement> {}
16
+
17
+ export interface CardSubtitleProps extends React.HTMLAttributes<HTMLDivElement> {}
18
+
19
+ export const Card: React.FC<CardProps> & {
20
+ Header: React.FC<CardHeaderProps>;
21
+ Body: React.FC<CardBodyProps>;
22
+ Footer: React.FC<CardFooterProps>;
23
+ Icon: React.FC<CardIconProps>;
24
+ Title: React.FC<CardTitleProps>;
25
+ Subtitle: React.FC<CardSubtitleProps>;
26
+ } = Object.assign(
27
+ ({ children, clickable = false, className = '', ...props }: CardProps) => {
28
+ return (
29
+ <div
30
+ className={`cn-card ${clickable ? 'cn-card-clickable' : ''} ${className}`.trim()}
31
+ {...props}
32
+ >
33
+ {children}
34
+ </div>
35
+ );
36
+ },
37
+ {
38
+ Header: ({ children, className = '', ...props }: CardHeaderProps) => (
39
+ <div className={`cn-card-header ${className}`.trim()} {...props}>{children}</div>
40
+ ),
41
+ Body: ({ children, className = '', ...props }: CardBodyProps) => (
42
+ <div className={`cn-card-body ${className}`.trim()} {...props}>{children}</div>
43
+ ),
44
+ Footer: ({ children, className = '', ...props }: CardFooterProps) => (
45
+ <div className={`cn-card-footer ${className}`.trim()} {...props}>{children}</div>
46
+ ),
47
+ Icon: ({ children, className = '', ...props }: CardIconProps) => (
48
+ <div className={`cn-card-icon ${className}`.trim()} {...props}>
49
+ {children}
50
+ </div>
51
+ ),
52
+ Title: ({ children, className = '', ...props }: CardTitleProps) => (
53
+ <div className={`cn-card-title ${className}`.trim()} {...props}>{children}</div>
54
+ ),
55
+ Subtitle: ({ children, className = '', ...props }: CardSubtitleProps) => (
56
+ <div className={`cn-card-subtitle ${className}`.trim()} {...props}>{children}</div>
57
+ ),
58
+ }
59
+ );
60
+
61
+ Card.displayName = 'Card';
62
+ Card.Header.displayName = 'Card.Header';
63
+ Card.Body.displayName = 'Card.Body';
64
+ Card.Footer.displayName = 'Card.Footer';
65
+ Card.Icon.displayName = 'Card.Icon';
66
+ Card.Title.displayName = 'Card.Title';
67
+ Card.Subtitle.displayName = 'Card.Subtitle';
68
+
69
+ export default Card;
@@ -1,131 +1,131 @@
1
- import * as React from 'react';
2
-
3
- export interface CommandPaletteItem {
4
- title: string;
5
- subtitle?: string;
6
- icon?: React.ReactNode;
7
- kbd?: string;
8
- action?: () => void;
9
- }
10
-
11
- export interface CommandPaletteProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect'> {
12
- items?: CommandPaletteItem[];
13
- isOpen?: boolean;
14
- onClose?: () => void;
15
- onSelect?: (item: CommandPaletteItem) => void;
16
- placeholder?: string;
17
- }
18
-
19
- export const CommandPalette: React.FC<CommandPaletteProps> = ({
20
- items = [],
21
- isOpen = false,
22
- onClose,
23
- onSelect,
24
- placeholder = 'Search commands...',
25
- className = '',
26
- ...props
27
- }) => {
28
- const [query, setQuery] = React.useState('');
29
- const [activeIndex, setActiveIndex] = React.useState(0);
30
- const inputRef = React.useRef<HTMLInputElement>(null);
31
-
32
- const filtered = items.filter(
33
- (item) =>
34
- item.title.toLowerCase().includes(query.toLowerCase()) ||
35
- (item.subtitle && item.subtitle.toLowerCase().includes(query.toLowerCase()))
36
- );
37
-
38
- React.useEffect(() => {
39
- if (isOpen) {
40
- setQuery('');
41
- setActiveIndex(0);
42
- setTimeout(() => inputRef.current?.focus(), 100);
43
- }
44
- }, [isOpen]);
45
-
46
- React.useEffect(() => {
47
- const handleKeyDown = (e: KeyboardEvent) => {
48
- if (!isOpen) return;
49
-
50
- if (e.key === 'Escape') {
51
- onClose?.();
52
- } else if (e.key === 'ArrowDown') {
53
- e.preventDefault();
54
- setActiveIndex((i) => Math.min(i + 1, filtered.length - 1));
55
- } else if (e.key === 'ArrowUp') {
56
- e.preventDefault();
57
- setActiveIndex((i) => Math.max(i - 1, 0));
58
- } else if (e.key === 'Enter' && filtered[activeIndex]) {
59
- handleSelect(filtered[activeIndex]);
60
- }
61
- };
62
-
63
- document.addEventListener('keydown', handleKeyDown);
64
- return () => document.removeEventListener('keydown', handleKeyDown);
65
- }, [isOpen, filtered, activeIndex, onClose]);
66
-
67
- const handleSelect = (item: CommandPaletteItem) => {
68
- item.action?.();
69
- onSelect?.(item);
70
- onClose?.();
71
- };
72
-
73
- if (!isOpen) return null;
74
-
75
- return (
76
- <div
77
- className={`cn-command-palette cn-command-palette-open ${className}`.trim()}
78
- onClick={(e) => e.target === e.currentTarget && onClose?.()}
79
- role="dialog"
80
- aria-modal="true"
81
- aria-label="Command palette"
82
- {...props}
83
- >
84
- <div className="cn-command-palette-inner">
85
- <input
86
- ref={inputRef}
87
- type="text"
88
- className="cn-command-palette-input"
89
- placeholder={placeholder}
90
- value={query}
91
- onChange={(e) => {
92
- setQuery(e.target.value);
93
- setActiveIndex(0);
94
- }}
95
- role="combobox"
96
- aria-autocomplete="list"
97
- aria-expanded={filtered.length > 0}
98
- />
99
- <div className="cn-command-palette-results" role="listbox">
100
- {filtered.length > 0 ? (
101
- filtered.map((item, idx) => (
102
- <div
103
- key={idx}
104
- className={`cn-command-item ${idx === activeIndex ? 'cn-command-item-active' : ''}`.trim()}
105
- onClick={() => handleSelect(item)}
106
- onMouseEnter={() => setActiveIndex(idx)}
107
- role="option"
108
- aria-selected={idx === activeIndex}
109
- >
110
- {item.icon && <div className="cn-command-item-icon">{item.icon}</div>}
111
- <div className="cn-command-item-content">
112
- <div className="cn-command-item-title">{item.title}</div>
113
- {item.subtitle && (
114
- <div className="cn-command-item-subtitle">{item.subtitle}</div>
115
- )}
116
- </div>
117
- {item.kbd && <div className="cn-command-item-kbd">{item.kbd}</div>}
118
- </div>
119
- ))
120
- ) : (
121
- <div className="cn-command-empty">No commands found</div>
122
- )}
123
- </div>
124
- </div>
125
- </div>
126
- );
127
- };
128
-
129
- CommandPalette.displayName = 'CommandPalette';
130
-
131
- export default CommandPalette;
1
+ import * as React from 'react';
2
+
3
+ export interface CommandPaletteItem {
4
+ title: string;
5
+ subtitle?: string;
6
+ icon?: React.ReactNode;
7
+ kbd?: string;
8
+ action?: () => void;
9
+ }
10
+
11
+ export interface CommandPaletteProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect'> {
12
+ items?: CommandPaletteItem[];
13
+ isOpen?: boolean;
14
+ onClose?: () => void;
15
+ onSelect?: (item: CommandPaletteItem) => void;
16
+ placeholder?: string;
17
+ }
18
+
19
+ export const CommandPalette: React.FC<CommandPaletteProps> = ({
20
+ items = [],
21
+ isOpen = false,
22
+ onClose,
23
+ onSelect,
24
+ placeholder = 'Search commands...',
25
+ className = '',
26
+ ...props
27
+ }) => {
28
+ const [query, setQuery] = React.useState('');
29
+ const [activeIndex, setActiveIndex] = React.useState(0);
30
+ const inputRef = React.useRef<HTMLInputElement>(null);
31
+
32
+ const filtered = items.filter(
33
+ (item) =>
34
+ item.title.toLowerCase().includes(query.toLowerCase()) ||
35
+ (item.subtitle && item.subtitle.toLowerCase().includes(query.toLowerCase()))
36
+ );
37
+
38
+ React.useEffect(() => {
39
+ if (isOpen) {
40
+ setQuery('');
41
+ setActiveIndex(0);
42
+ setTimeout(() => inputRef.current?.focus(), 100);
43
+ }
44
+ }, [isOpen]);
45
+
46
+ React.useEffect(() => {
47
+ const handleKeyDown = (e: KeyboardEvent) => {
48
+ if (!isOpen) return;
49
+
50
+ if (e.key === 'Escape') {
51
+ onClose?.();
52
+ } else if (e.key === 'ArrowDown') {
53
+ e.preventDefault();
54
+ setActiveIndex((i) => Math.min(i + 1, filtered.length - 1));
55
+ } else if (e.key === 'ArrowUp') {
56
+ e.preventDefault();
57
+ setActiveIndex((i) => Math.max(i - 1, 0));
58
+ } else if (e.key === 'Enter' && filtered[activeIndex]) {
59
+ handleSelect(filtered[activeIndex]);
60
+ }
61
+ };
62
+
63
+ document.addEventListener('keydown', handleKeyDown);
64
+ return () => document.removeEventListener('keydown', handleKeyDown);
65
+ }, [isOpen, filtered, activeIndex, onClose]);
66
+
67
+ const handleSelect = (item: CommandPaletteItem) => {
68
+ item.action?.();
69
+ onSelect?.(item);
70
+ onClose?.();
71
+ };
72
+
73
+ if (!isOpen) return null;
74
+
75
+ return (
76
+ <div
77
+ className={`cn-command-palette cn-command-palette-open ${className}`.trim()}
78
+ onClick={(e) => e.target === e.currentTarget && onClose?.()}
79
+ role="dialog"
80
+ aria-modal="true"
81
+ aria-label="Command palette"
82
+ {...props}
83
+ >
84
+ <div className="cn-command-palette-inner">
85
+ <input
86
+ ref={inputRef}
87
+ type="text"
88
+ className="cn-command-palette-input"
89
+ placeholder={placeholder}
90
+ value={query}
91
+ onChange={(e) => {
92
+ setQuery(e.target.value);
93
+ setActiveIndex(0);
94
+ }}
95
+ role="combobox"
96
+ aria-autocomplete="list"
97
+ aria-expanded={filtered.length > 0}
98
+ />
99
+ <div className="cn-command-palette-results" role="listbox">
100
+ {filtered.length > 0 ? (
101
+ filtered.map((item, idx) => (
102
+ <div
103
+ key={idx}
104
+ className={`cn-command-item ${idx === activeIndex ? 'cn-command-item-active' : ''}`.trim()}
105
+ onClick={() => handleSelect(item)}
106
+ onMouseEnter={() => setActiveIndex(idx)}
107
+ role="option"
108
+ aria-selected={idx === activeIndex}
109
+ >
110
+ {item.icon && <div className="cn-command-item-icon">{item.icon}</div>}
111
+ <div className="cn-command-item-content">
112
+ <div className="cn-command-item-title">{item.title}</div>
113
+ {item.subtitle && (
114
+ <div className="cn-command-item-subtitle">{item.subtitle}</div>
115
+ )}
116
+ </div>
117
+ {item.kbd && <div className="cn-command-item-kbd">{item.kbd}</div>}
118
+ </div>
119
+ ))
120
+ ) : (
121
+ <div className="cn-command-empty">No commands found</div>
122
+ )}
123
+ </div>
124
+ </div>
125
+ </div>
126
+ );
127
+ };
128
+
129
+ CommandPalette.displayName = 'CommandPalette';
130
+
131
+ export default CommandPalette;
@@ -1,88 +1,88 @@
1
- import * as React from 'react';
2
-
3
- export interface DropdownProps extends React.HTMLAttributes<HTMLDivElement> {
4
- trigger: React.ReactNode;
5
- }
6
-
7
- export interface DropdownItemProps extends React.HTMLAttributes<HTMLDivElement | HTMLButtonElement> {
8
- icon?: React.ReactNode;
9
- divider?: boolean;
10
- }
11
-
12
- export const Dropdown: React.FC<DropdownProps> & {
13
- Item: React.FC<DropdownItemProps>;
14
- Divider: React.FC;
15
- } = Object.assign(
16
- ({ trigger, children, className = '', ...props }: DropdownProps) => {
17
- const [isOpen, setIsOpen] = React.useState(false);
18
- const dropdownRef = React.useRef<HTMLDivElement>(null);
19
-
20
- const toggle = () => setIsOpen((prev) => !prev);
21
- const close = () => setIsOpen(false);
22
-
23
- React.useEffect(() => {
24
- const handleClickOutside = (e: MouseEvent) => {
25
- if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
26
- close();
27
- }
28
- };
29
-
30
- if (isOpen) {
31
- document.addEventListener('click', handleClickOutside);
32
- }
33
-
34
- return () => {
35
- document.removeEventListener('click', handleClickOutside);
36
- };
37
- }, [isOpen]);
38
-
39
- return (
40
- <div
41
- ref={dropdownRef}
42
- className={`cn-dropdown ${isOpen ? 'cn-dropdown-open' : ''} ${className}`.trim()}
43
- {...props}
44
- >
45
- <div
46
- className="cn-dropdown-trigger"
47
- onClick={toggle}
48
- role="button"
49
- aria-haspopup="true"
50
- aria-expanded={isOpen}
51
- tabIndex={0}
52
- onKeyDown={(e) => {
53
- if (e.key === 'Enter' || e.key === ' ') {
54
- e.preventDefault();
55
- toggle();
56
- }
57
- }}
58
- >
59
- {trigger}
60
- </div>
61
- <div className="cn-dropdown-menu" role="menu" onClick={close}>
62
- {children}
63
- </div>
64
- </div>
65
- );
66
- },
67
- {
68
- Item: ({ children, icon, onClick, className = '', ...props }: DropdownItemProps) => (
69
- <div
70
- className={`cn-dropdown-item ${className}`.trim()}
71
- onClick={onClick}
72
- role="menuitem"
73
- tabIndex={0}
74
- {...props}
75
- >
76
- {icon && <span className="cn-dropdown-item-icon">{icon}</span>}
77
- {children}
78
- </div>
79
- ),
80
- Divider: () => <div className="cn-dropdown-divider" role="separator" />,
81
- }
82
- );
83
-
84
- Dropdown.displayName = 'Dropdown';
85
- Dropdown.Item.displayName = 'Dropdown.Item';
86
- Dropdown.Divider.displayName = 'Dropdown.Divider';
87
-
88
- export default Dropdown;
1
+ import * as React from 'react';
2
+
3
+ export interface DropdownProps extends React.HTMLAttributes<HTMLDivElement> {
4
+ trigger: React.ReactNode;
5
+ }
6
+
7
+ export interface DropdownItemProps extends React.HTMLAttributes<HTMLDivElement | HTMLButtonElement> {
8
+ icon?: React.ReactNode;
9
+ divider?: boolean;
10
+ }
11
+
12
+ export const Dropdown: React.FC<DropdownProps> & {
13
+ Item: React.FC<DropdownItemProps>;
14
+ Divider: React.FC;
15
+ } = Object.assign(
16
+ ({ trigger, children, className = '', ...props }: DropdownProps) => {
17
+ const [isOpen, setIsOpen] = React.useState(false);
18
+ const dropdownRef = React.useRef<HTMLDivElement>(null);
19
+
20
+ const toggle = () => setIsOpen((prev) => !prev);
21
+ const close = () => setIsOpen(false);
22
+
23
+ React.useEffect(() => {
24
+ const handleClickOutside = (e: MouseEvent) => {
25
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
26
+ close();
27
+ }
28
+ };
29
+
30
+ if (isOpen) {
31
+ document.addEventListener('click', handleClickOutside);
32
+ }
33
+
34
+ return () => {
35
+ document.removeEventListener('click', handleClickOutside);
36
+ };
37
+ }, [isOpen]);
38
+
39
+ return (
40
+ <div
41
+ ref={dropdownRef}
42
+ className={`cn-dropdown ${isOpen ? 'cn-dropdown-open' : ''} ${className}`.trim()}
43
+ {...props}
44
+ >
45
+ <div
46
+ className="cn-dropdown-trigger"
47
+ onClick={toggle}
48
+ role="button"
49
+ aria-haspopup="true"
50
+ aria-expanded={isOpen}
51
+ tabIndex={0}
52
+ onKeyDown={(e) => {
53
+ if (e.key === 'Enter' || e.key === ' ') {
54
+ e.preventDefault();
55
+ toggle();
56
+ }
57
+ }}
58
+ >
59
+ {trigger}
60
+ </div>
61
+ <div className="cn-dropdown-menu" role="menu" onClick={close}>
62
+ {children}
63
+ </div>
64
+ </div>
65
+ );
66
+ },
67
+ {
68
+ Item: ({ children, icon, onClick, className = '', ...props }: DropdownItemProps) => (
69
+ <div
70
+ className={`cn-dropdown-item ${className}`.trim()}
71
+ onClick={onClick}
72
+ role="menuitem"
73
+ tabIndex={0}
74
+ {...props}
75
+ >
76
+ {icon && <span className="cn-dropdown-item-icon">{icon}</span>}
77
+ {children}
78
+ </div>
79
+ ),
80
+ Divider: () => <div className="cn-dropdown-divider" role="separator" />,
81
+ }
82
+ );
83
+
84
+ Dropdown.displayName = 'Dropdown';
85
+ Dropdown.Item.displayName = 'Dropdown.Item';
86
+ Dropdown.Divider.displayName = 'Dropdown.Divider';
87
+
88
+ export default Dropdown;