cronixui 1.0.6 → 1.1.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 (129) hide show
  1. package/README.md +35 -5
  2. package/package.json +19 -5
  3. package/packages/go/cronixui/cronixui.go +784 -237
  4. package/packages/go/cronixui/go.mod +32 -0
  5. package/packages/go/cronixui/go.sum +666 -0
  6. package/packages/python/cronixui/__init__.py +59 -3
  7. package/packages/python/cronixui/alert.py +61 -0
  8. package/packages/python/cronixui/avatar.py +50 -0
  9. package/packages/python/cronixui/badge.py +46 -0
  10. package/packages/python/cronixui/button.py +64 -0
  11. package/packages/python/cronixui/card.py +62 -0
  12. package/packages/python/cronixui/form.py +255 -0
  13. package/packages/python/cronixui/layout.py +143 -0
  14. package/packages/python/cronixui/list.py +51 -0
  15. package/packages/python/cronixui/loading.py +36 -0
  16. package/packages/python/cronixui/progress.py +90 -0
  17. package/packages/python/cronixui/table.py +48 -0
  18. package/packages/python/cronixui/tooltip.py +28 -0
  19. package/packages/react/src/components/Accordion.tsx +82 -0
  20. package/packages/react/src/components/Alert.tsx +80 -0
  21. package/packages/react/src/components/Avatar.tsx +54 -0
  22. package/packages/react/src/components/Badge.tsx +32 -0
  23. package/packages/react/src/components/Breadcrumb.tsx +50 -0
  24. package/packages/react/src/components/Button.tsx +47 -0
  25. package/packages/react/src/components/Card.tsx +69 -0
  26. package/packages/react/src/components/Checkbox.tsx +30 -0
  27. package/packages/react/src/components/CommandPalette.tsx +131 -0
  28. package/packages/react/src/components/Container.tsx +26 -0
  29. package/packages/react/src/components/Dropdown.tsx +88 -0
  30. package/packages/react/src/components/FileInput.tsx +86 -0
  31. package/packages/react/src/components/Footer.tsx +36 -0
  32. package/packages/react/src/components/FormGroup.tsx +36 -0
  33. package/packages/react/src/components/Header.tsx +29 -0
  34. package/packages/react/src/components/Input.tsx +54 -0
  35. package/packages/react/src/components/List.tsx +55 -0
  36. package/packages/react/src/components/Modal.tsx +89 -0
  37. package/packages/react/src/components/Nav.tsx +63 -0
  38. package/packages/react/src/components/Pagination.tsx +107 -0
  39. package/packages/react/src/components/Progress.tsx +49 -0
  40. package/packages/react/src/components/Radio.tsx +64 -0
  41. package/packages/react/src/components/Search.tsx +95 -0
  42. package/packages/react/src/components/Select.tsx +41 -0
  43. package/packages/react/src/components/Sidebar.tsx +64 -0
  44. package/packages/react/src/components/Skeleton.tsx +39 -0
  45. package/packages/react/src/components/Slider.tsx +32 -0
  46. package/packages/react/src/components/Spinner.tsx +24 -0
  47. package/packages/react/src/components/Stack.tsx +69 -0
  48. package/packages/react/src/components/Stat.tsx +35 -0
  49. package/packages/react/src/components/Table.tsx +90 -0
  50. package/packages/react/src/components/Tabs.tsx +85 -0
  51. package/packages/react/src/components/Tag.tsx +30 -0
  52. package/packages/react/src/components/Textarea.tsx +21 -0
  53. package/packages/react/src/components/Toast.tsx +134 -0
  54. package/packages/react/src/components/Toggle.tsx +58 -0
  55. package/packages/react/src/components/Tooltip.tsx +31 -0
  56. package/packages/react/src/components/Typography.tsx +66 -0
  57. package/packages/react/src/index.ts +40 -0
  58. package/packages/react/src/styles.css +2039 -0
  59. package/packages/react/src/tokens/index.ts +94 -0
  60. package/packages/rust/cronixui/src/colors.rs +135 -0
  61. package/packages/rust/cronixui/src/components/accordion.rs +47 -0
  62. package/packages/rust/cronixui/src/components/alert.rs +95 -0
  63. package/packages/rust/cronixui/src/components/avatar.rs +85 -0
  64. package/packages/rust/cronixui/src/components/badge.rs +35 -0
  65. package/packages/rust/cronixui/src/components/breadcrumb.rs +58 -0
  66. package/packages/rust/cronixui/src/components/button.rs +70 -0
  67. package/packages/rust/cronixui/src/components/card.rs +259 -0
  68. package/packages/rust/cronixui/src/components/command_palette.rs +254 -0
  69. package/packages/rust/cronixui/src/components/dropdown.rs +179 -0
  70. package/packages/rust/cronixui/src/components/file_input.rs +74 -0
  71. package/packages/rust/cronixui/src/components/input.rs +21 -0
  72. package/packages/rust/cronixui/src/components/list.rs +38 -0
  73. package/packages/rust/cronixui/src/components/mod.rs +51 -0
  74. package/packages/rust/cronixui/src/{modal.rs → components/modal.rs} +15 -1
  75. package/packages/rust/cronixui/src/components/nav.rs +19 -0
  76. package/packages/rust/cronixui/src/{pagination.rs → components/pagination.rs} +14 -13
  77. package/packages/rust/cronixui/src/components/progress.rs +50 -0
  78. package/packages/rust/cronixui/src/components/search.rs +185 -0
  79. package/packages/rust/cronixui/src/components/skeleton.rs +63 -0
  80. package/packages/rust/cronixui/src/components/spinner.rs +21 -0
  81. package/packages/rust/cronixui/src/components/table.rs +56 -0
  82. package/packages/rust/cronixui/src/components/tabs.rs +43 -0
  83. package/packages/rust/cronixui/src/components/toast.rs +69 -0
  84. package/packages/rust/cronixui/src/{toggle.rs → components/toggle.rs} +7 -5
  85. package/packages/rust/cronixui/src/components/tooltip.rs +11 -0
  86. package/packages/rust/cronixui/src/lib.rs +111 -64
  87. package/packages/rust/cronixui/src/tokens.rs +97 -127
  88. package/packages/web/src/variables.css +81 -81
  89. package/packages/go/cronixui/tokens.go +0 -129
  90. package/packages/python/cronixui/pyproject.toml +0 -11
  91. package/packages/react/src/components/Accordion.jsx +0 -50
  92. package/packages/react/src/components/Alert.jsx +0 -62
  93. package/packages/react/src/components/Avatar.jsx +0 -34
  94. package/packages/react/src/components/Badge.jsx +0 -15
  95. package/packages/react/src/components/Breadcrumb.jsx +0 -27
  96. package/packages/react/src/components/Button.jsx +0 -21
  97. package/packages/react/src/components/Card.jsx +0 -23
  98. package/packages/react/src/components/Checkbox.jsx +0 -27
  99. package/packages/react/src/components/CommandPalette.jsx +0 -93
  100. package/packages/react/src/components/Dropdown.jsx +0 -48
  101. package/packages/react/src/components/FileInput.jsx +0 -44
  102. package/packages/react/src/components/Input.jsx +0 -22
  103. package/packages/react/src/components/List.jsx +0 -29
  104. package/packages/react/src/components/Modal.jsx +0 -65
  105. package/packages/react/src/components/Nav.jsx +0 -50
  106. package/packages/react/src/components/Pagination.jsx +0 -81
  107. package/packages/react/src/components/Progress.jsx +0 -23
  108. package/packages/react/src/components/Radio.jsx +0 -50
  109. package/packages/react/src/components/Search.jsx +0 -70
  110. package/packages/react/src/components/Select.jsx +0 -33
  111. package/packages/react/src/components/Skeleton.jsx +0 -15
  112. package/packages/react/src/components/Slider.jsx +0 -29
  113. package/packages/react/src/components/Spinner.jsx +0 -5
  114. package/packages/react/src/components/Stat.jsx +0 -19
  115. package/packages/react/src/components/Table.jsx +0 -48
  116. package/packages/react/src/components/Tabs.jsx +0 -65
  117. package/packages/react/src/components/Tag.jsx +0 -19
  118. package/packages/react/src/components/Textarea.jsx +0 -17
  119. package/packages/react/src/components/Toast.jsx +0 -78
  120. package/packages/react/src/components/Toggle.jsx +0 -34
  121. package/packages/react/src/components/Tooltip.jsx +0 -12
  122. package/packages/react/src/index.d.ts +0 -103
  123. package/packages/react/src/index.js +0 -33
  124. package/packages/rust/cronixui/src/accordion.rs +0 -49
  125. package/packages/rust/cronixui/src/command_palette.rs +0 -62
  126. package/packages/rust/cronixui/src/dropdown.rs +0 -31
  127. package/packages/rust/cronixui/src/search.rs +0 -49
  128. package/packages/rust/cronixui/src/tabs.rs +0 -23
  129. package/packages/rust/cronixui/src/toast.rs +0 -70
@@ -0,0 +1,55 @@
1
+ import * as React from 'react';
2
+
3
+ export interface ListProps extends React.HTMLAttributes<HTMLDivElement> {}
4
+
5
+ export interface ListItemProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ icon?: React.ReactNode;
7
+ title?: string;
8
+ subtitle?: string;
9
+ actions?: React.ReactNode;
10
+ clickable?: boolean;
11
+ }
12
+
13
+ export const List: React.FC<ListProps> = ({ children, className = '', ...props }) => {
14
+ return (
15
+ <div className={`cn-list ${className}`.trim()} role="list" {...props}>
16
+ {children}
17
+ </div>
18
+ );
19
+ };
20
+
21
+ List.displayName = 'List';
22
+
23
+ export const ListItem: React.FC<ListItemProps> = ({
24
+ children,
25
+ icon,
26
+ title,
27
+ subtitle,
28
+ actions,
29
+ clickable = false,
30
+ onClick,
31
+ className = '',
32
+ ...props
33
+ }) => {
34
+ return (
35
+ <div
36
+ className={`cn-list-item ${clickable ? 'cn-list-item-clickable' : ''} ${className}`.trim()}
37
+ onClick={onClick}
38
+ role={clickable ? 'button' : 'listitem'}
39
+ tabIndex={clickable ? 0 : undefined}
40
+ {...props}
41
+ >
42
+ {icon && <div className="cn-list-item-icon">{icon}</div>}
43
+ <div className="cn-list-item-content">
44
+ {title && <div className="cn-list-item-title">{title}</div>}
45
+ {subtitle && <div className="cn-list-item-subtitle">{subtitle}</div>}
46
+ {children}
47
+ </div>
48
+ {actions && <div className="cn-list-item-actions">{actions}</div>}
49
+ </div>
50
+ );
51
+ };
52
+
53
+ ListItem.displayName = 'ListItem';
54
+
55
+ export default List;
@@ -0,0 +1,89 @@
1
+ import * as React from 'react';
2
+
3
+ export type ModalSize = 'sm' | 'md' | 'lg' | 'xl';
4
+
5
+ export interface ModalProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ isOpen?: boolean;
7
+ onClose?: () => void;
8
+ size?: ModalSize;
9
+ }
10
+
11
+ export interface ModalHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
12
+ onClose?: () => void;
13
+ }
14
+
15
+ export interface ModalBodyProps extends React.HTMLAttributes<HTMLDivElement> {}
16
+
17
+ export interface ModalFooterProps extends React.HTMLAttributes<HTMLDivElement> {}
18
+
19
+ export const Modal: React.FC<ModalProps> & {
20
+ Header: React.FC<ModalHeaderProps>;
21
+ Body: React.FC<ModalBodyProps>;
22
+ Footer: React.FC<ModalFooterProps>;
23
+ } = Object.assign(
24
+ ({ isOpen = false, onClose, size = 'md', children, className = '', ...props }: ModalProps) => {
25
+ React.useEffect(() => {
26
+ const handleEscape = (e: KeyboardEvent) => {
27
+ if (e.key === 'Escape' && onClose) {
28
+ onClose();
29
+ }
30
+ };
31
+
32
+ if (isOpen) {
33
+ document.addEventListener('keydown', handleEscape);
34
+ document.body.style.overflow = 'hidden';
35
+ }
36
+
37
+ return () => {
38
+ document.removeEventListener('keydown', handleEscape);
39
+ document.body.style.overflow = '';
40
+ };
41
+ }, [isOpen, onClose]);
42
+
43
+ if (!isOpen) return null;
44
+
45
+ const sizeClass = size !== 'md' ? `cn-modal-${size}` : '';
46
+
47
+ return (
48
+ <div
49
+ className={`cn-modal-backdrop cn-modal-open ${className}`.trim()}
50
+ onClick={(e) => e.target === e.currentTarget && onClose?.()}
51
+ role="dialog"
52
+ aria-modal="true"
53
+ {...props}
54
+ >
55
+ <div className={`cn-modal ${sizeClass}`.trim()}>
56
+ {children}
57
+ </div>
58
+ </div>
59
+ );
60
+ },
61
+ {
62
+ Header: ({ children, onClose, className = '', ...props }: ModalHeaderProps) => (
63
+ <div className={`cn-modal-header ${className}`.trim()} {...props}>
64
+ <div className="cn-modal-title">{children}</div>
65
+ {onClose && (
66
+ <button className="cn-modal-close" onClick={onClose} aria-label="Close">
67
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
68
+ <line x1="18" y1="6" x2="6" y2="18" />
69
+ <line x1="6" y1="6" x2="18" y2="18" />
70
+ </svg>
71
+ </button>
72
+ )}
73
+ </div>
74
+ ),
75
+ Body: ({ children, className = '', ...props }: ModalBodyProps) => (
76
+ <div className={`cn-modal-body ${className}`.trim()} {...props}>{children}</div>
77
+ ),
78
+ Footer: ({ children, className = '', ...props }: ModalFooterProps) => (
79
+ <div className={`cn-modal-footer ${className}`.trim()} {...props}>{children}</div>
80
+ ),
81
+ }
82
+ );
83
+
84
+ Modal.displayName = 'Modal';
85
+ Modal.Header.displayName = 'Modal.Header';
86
+ Modal.Body.displayName = 'Modal.Body';
87
+ Modal.Footer.displayName = 'Modal.Footer';
88
+
89
+ export default Modal;
@@ -0,0 +1,63 @@
1
+ import * as React from 'react';
2
+
3
+ export interface NavProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange'> {
4
+ defaultActive?: string;
5
+ active?: string;
6
+ onChange?: (id: string) => void;
7
+ }
8
+
9
+ export interface NavItemProps extends React.HTMLAttributes<HTMLDivElement> {
10
+ id: string;
11
+ active?: boolean;
12
+ }
13
+
14
+ export const Nav: React.FC<NavProps> & {
15
+ Item: React.FC<NavItemProps>;
16
+ } = Object.assign(
17
+ ({ defaultActive, active: controlledActive, onChange, children, className = '', ...props }: NavProps) => {
18
+ const [internalActive, setInternalActive] = React.useState(defaultActive);
19
+ const activeItem = controlledActive !== undefined ? controlledActive : internalActive;
20
+
21
+ const handleClick = (id: string) => {
22
+ if (onChange) {
23
+ onChange(id);
24
+ } else {
25
+ setInternalActive(id);
26
+ }
27
+ };
28
+
29
+ return (
30
+ <nav className={`cn-nav ${className}`.trim()} {...props}>
31
+ {React.Children.map(children, (child) => {
32
+ if (React.isValidElement<NavItemProps>(child)) {
33
+ const isActive = child.props.active !== undefined ? child.props.active : child.props.id === activeItem;
34
+ return React.cloneElement(child, {
35
+ active: isActive,
36
+ onClick: () => handleClick(child.props.id),
37
+ } as Partial<NavItemProps>);
38
+ }
39
+ return child;
40
+ })}
41
+ </nav>
42
+ );
43
+ },
44
+ {
45
+ Item: ({ children, active = false, onClick, className = '', ...props }: NavItemProps) => (
46
+ <div
47
+ className={`cn-nav-item ${active ? 'cn-nav-active' : ''} ${className}`.trim()}
48
+ onClick={onClick}
49
+ role="tab"
50
+ aria-selected={active}
51
+ tabIndex={0}
52
+ {...props}
53
+ >
54
+ {children}
55
+ </div>
56
+ ),
57
+ }
58
+ );
59
+
60
+ Nav.displayName = 'Nav';
61
+ (Nav.Item as React.FC<NavItemProps> & { displayName?: string }).displayName = 'NavItem';
62
+
63
+ export default Nav;
@@ -0,0 +1,107 @@
1
+ import * as React from 'react';
2
+
3
+ export interface PaginationProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
4
+ total: number;
5
+ current?: number;
6
+ onChange?: (page: number) => void;
7
+ }
8
+
9
+ export const Pagination: React.FC<PaginationProps> = ({
10
+ total,
11
+ current = 1,
12
+ onChange,
13
+ className = '',
14
+ ...props
15
+ }) => {
16
+ const [page, setPage] = React.useState(current);
17
+
18
+ const activePage = onChange !== undefined ? current : page;
19
+
20
+ const getPages = (): (number | string)[] => {
21
+ const pages: (number | string)[] = [];
22
+ const maxVisible = 5;
23
+
24
+ if (total <= maxVisible) {
25
+ for (let i = 1; i <= total; i++) pages.push(i);
26
+ } else {
27
+ if (activePage <= 3) {
28
+ for (let i = 1; i <= 4; i++) pages.push(i);
29
+ pages.push('...');
30
+ pages.push(total);
31
+ } else if (activePage >= total - 2) {
32
+ pages.push(1);
33
+ pages.push('...');
34
+ for (let i = total - 3; i <= total; i++) pages.push(i);
35
+ } else {
36
+ pages.push(1);
37
+ pages.push('...');
38
+ for (let i = activePage - 1; i <= activePage + 1; i++) pages.push(i);
39
+ pages.push('...');
40
+ pages.push(total);
41
+ }
42
+ }
43
+
44
+ return pages;
45
+ };
46
+
47
+ const goTo = (p: number) => {
48
+ if (p < 1 || p > total || p === activePage) return;
49
+ if (onChange) {
50
+ onChange(p);
51
+ } else {
52
+ setPage(p);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <nav className={`cn-pagination ${className}`.trim()} aria-label="Pagination" {...props}>
58
+ <button
59
+ className="cn-pagination-item"
60
+ onClick={() => goTo(activePage - 1)}
61
+ disabled={activePage === 1}
62
+ aria-label="Previous page"
63
+ >
64
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16">
65
+ <polyline points="15 18 9 12 15 6" />
66
+ </svg>
67
+ </button>
68
+ {getPages().map((p, idx) =>
69
+ p === '...' ? (
70
+ <button
71
+ key={idx}
72
+ className="cn-pagination-item cn-pagination-ellipsis"
73
+ disabled
74
+ aria-label="More pages"
75
+ aria-hidden="true"
76
+ tabIndex={-1}
77
+ >
78
+ {p}
79
+ </button>
80
+ ) : (
81
+ <button
82
+ key={idx}
83
+ className={`cn-pagination-item ${p === activePage ? 'cn-pagination-active' : ''}`.trim()}
84
+ onClick={() => goTo(p)}
85
+ aria-current={p === activePage ? 'page' : undefined}
86
+ >
87
+ {p}
88
+ </button>
89
+ )
90
+ )}
91
+ <button
92
+ className="cn-pagination-item"
93
+ onClick={() => goTo(activePage + 1)}
94
+ disabled={activePage === total}
95
+ aria-label="Next page"
96
+ >
97
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16">
98
+ <polyline points="9 18 15 12 9 6" />
99
+ </svg>
100
+ </button>
101
+ </nav>
102
+ );
103
+ };
104
+
105
+ Pagination.displayName = 'Pagination';
106
+
107
+ export default Pagination;
@@ -0,0 +1,49 @@
1
+ import * as React from 'react';
2
+
3
+ export type ProgressVariant = 'default' | 'success' | 'warning' | 'error';
4
+ export type ProgressSize = 'sm' | 'md' | 'lg';
5
+
6
+ export interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ value?: number;
8
+ max?: number;
9
+ showLabel?: boolean;
10
+ label?: string;
11
+ variant?: ProgressVariant;
12
+ size?: ProgressSize;
13
+ }
14
+
15
+ export const Progress: React.FC<ProgressProps> = ({
16
+ value = 0,
17
+ max = 100,
18
+ showLabel = false,
19
+ label,
20
+ variant = 'default',
21
+ size = 'md',
22
+ className = '',
23
+ ...props
24
+ }) => {
25
+ const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
26
+
27
+ return (
28
+ <div className={className} {...props}>
29
+ {showLabel && (
30
+ <div className="cn-progress-label">
31
+ <span>{label ?? `${percentage.toFixed(0)}%`}</span>
32
+ </div>
33
+ )}
34
+ <div
35
+ className={`cn-progress ${size !== 'md' ? `cn-progress-${size}` : ''} ${variant !== 'default' ? `cn-progress-${variant}` : ''}`.trim()}
36
+ role="progressbar"
37
+ aria-valuenow={percentage}
38
+ aria-valuemin={0}
39
+ aria-valuemax={100}
40
+ >
41
+ <div className="cn-progress-bar" style={{ width: `${percentage}%` }} />
42
+ </div>
43
+ </div>
44
+ );
45
+ };
46
+
47
+ Progress.displayName = 'Progress';
48
+
49
+ export default Progress;
@@ -0,0 +1,64 @@
1
+ import * as React from 'react';
2
+
3
+ export interface RadioProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'checked' | 'onChange'> {
4
+ checked?: boolean;
5
+ onChange?: (checked: boolean) => void;
6
+ value: string;
7
+ }
8
+
9
+ export interface RadioGroupProps {
10
+ name: string;
11
+ value?: string;
12
+ onChange?: (value: string) => void;
13
+ children: React.ReactNode;
14
+ className?: string;
15
+ }
16
+
17
+ export const Radio = React.forwardRef<HTMLInputElement, RadioProps>(
18
+ ({ checked = false, onChange, disabled = false, children, name, className = '', ...props }, ref) => {
19
+ return (
20
+ <label className={`cn-radio ${disabled ? 'disabled' : ''} ${className}`.trim()}>
21
+ <input
22
+ ref={ref}
23
+ type="radio"
24
+ checked={checked}
25
+ onChange={(e) => onChange?.(e.target.checked)}
26
+ disabled={disabled}
27
+ name={name}
28
+ {...props}
29
+ />
30
+ <span className="cn-radio-box" />
31
+ {children && <span className="cn-radio-label">{children}</span>}
32
+ </label>
33
+ );
34
+ }
35
+ );
36
+
37
+ Radio.displayName = 'Radio';
38
+
39
+ export const RadioGroup: React.FC<RadioGroupProps> = ({
40
+ children,
41
+ name,
42
+ value,
43
+ onChange,
44
+ className = '',
45
+ }) => {
46
+ return (
47
+ <div className={className} role="radiogroup">
48
+ {React.Children.map(children, (child) => {
49
+ if (React.isValidElement<RadioProps>(child) && child.type === Radio) {
50
+ return React.cloneElement(child, {
51
+ name,
52
+ checked: child.props.value === value,
53
+ onChange: () => onChange?.(child.props.value),
54
+ });
55
+ }
56
+ return child;
57
+ })}
58
+ </div>
59
+ );
60
+ };
61
+
62
+ RadioGroup.displayName = 'RadioGroup';
63
+
64
+ export default Radio;
@@ -0,0 +1,95 @@
1
+ import * as React from 'react';
2
+
3
+ export interface SearchItem {
4
+ title: string;
5
+ subtitle?: string;
6
+ }
7
+
8
+ export interface SearchProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect'> {
9
+ items?: SearchItem[];
10
+ placeholder?: string;
11
+ onSelect?: (item: SearchItem) => void;
12
+ }
13
+
14
+ export const Search: React.FC<SearchProps> = ({
15
+ items = [],
16
+ placeholder = 'Search...',
17
+ onSelect,
18
+ className = '',
19
+ ...props
20
+ }) => {
21
+ const [query, setQuery] = React.useState('');
22
+ const [isOpen, setIsOpen] = React.useState(false);
23
+ const ref = React.useRef<HTMLDivElement>(null);
24
+
25
+ const filtered = items.filter(
26
+ (item) =>
27
+ item.title.toLowerCase().includes(query.toLowerCase()) ||
28
+ (item.subtitle && item.subtitle.toLowerCase().includes(query.toLowerCase()))
29
+ );
30
+
31
+ React.useEffect(() => {
32
+ const handleClickOutside = (e: MouseEvent) => {
33
+ if (ref.current && !ref.current.contains(e.target as Node)) {
34
+ setIsOpen(false);
35
+ }
36
+ };
37
+
38
+ document.addEventListener('click', handleClickOutside);
39
+ return () => document.removeEventListener('click', handleClickOutside);
40
+ }, []);
41
+
42
+ const handleSelect = (item: SearchItem) => {
43
+ setQuery(item.title);
44
+ setIsOpen(false);
45
+ onSelect?.(item);
46
+ };
47
+
48
+ return (
49
+ <div ref={ref} className={`cn-search ${isOpen ? 'cn-search-open' : ''} ${className}`.trim()} {...props}>
50
+ <svg className="cn-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
51
+ <circle cx="11" cy="11" r="8" />
52
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
53
+ </svg>
54
+ <input
55
+ type="search"
56
+ className="cn-input cn-search-input"
57
+ placeholder={placeholder}
58
+ value={query}
59
+ onChange={(e) => {
60
+ setQuery(e.target.value);
61
+ setIsOpen(true);
62
+ }}
63
+ onFocus={() => setIsOpen(true)}
64
+ role="combobox"
65
+ aria-autocomplete="list"
66
+ aria-expanded={isOpen}
67
+ />
68
+ {isOpen && (
69
+ <div className="cn-search-results" role="listbox">
70
+ {filtered.length > 0 ? (
71
+ filtered.map((item, idx) => (
72
+ <div
73
+ key={idx}
74
+ className="cn-search-result"
75
+ onClick={() => handleSelect(item)}
76
+ role="option"
77
+ >
78
+ <div className="cn-search-result-title">{item.title}</div>
79
+ {item.subtitle && (
80
+ <div className="cn-search-result-subtitle">{item.subtitle}</div>
81
+ )}
82
+ </div>
83
+ ))
84
+ ) : (
85
+ <div className="cn-search-empty">No results found</div>
86
+ )}
87
+ </div>
88
+ )}
89
+ </div>
90
+ );
91
+ };
92
+
93
+ Search.displayName = 'Search';
94
+
95
+ export default Search;
@@ -0,0 +1,41 @@
1
+ import * as React from 'react';
2
+
3
+ export interface SelectOption {
4
+ value: string;
5
+ label: string;
6
+ }
7
+
8
+ export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'value' | 'onChange'> {
9
+ options: SelectOption[];
10
+ value?: string;
11
+ onChange?: (value: string) => void;
12
+ placeholder?: string;
13
+ }
14
+
15
+ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
16
+ ({ options = [], value, onChange, placeholder = 'Select...', disabled = false, className = '', ...props }, ref) => {
17
+ return (
18
+ <div className={`cn-select-wrapper ${className}`.trim()}>
19
+ <select
20
+ ref={ref}
21
+ className="cn-select"
22
+ value={value}
23
+ onChange={(e) => onChange?.(e.target.value)}
24
+ disabled={disabled}
25
+ {...props}
26
+ >
27
+ {placeholder && <option value="" disabled>{placeholder}</option>}
28
+ {options.map((opt, idx) => (
29
+ <option key={idx} value={opt.value}>
30
+ {opt.label}
31
+ </option>
32
+ ))}
33
+ </select>
34
+ </div>
35
+ );
36
+ }
37
+ );
38
+
39
+ Select.displayName = 'Select';
40
+
41
+ export default Select;
@@ -0,0 +1,64 @@
1
+ import * as React from 'react';
2
+
3
+ export interface SidebarProps extends React.HTMLAttributes<HTMLElement> {
4
+ header?: React.ReactNode;
5
+ footer?: React.ReactNode;
6
+ }
7
+
8
+ export interface SidebarItemProps extends React.HTMLAttributes<HTMLAnchorElement | HTMLDivElement> {
9
+ active?: boolean;
10
+ icon?: React.ReactNode;
11
+ href?: string;
12
+ }
13
+
14
+ export const Sidebar: React.FC<SidebarProps> & {
15
+ Item: React.FC<SidebarItemProps>;
16
+ } = Object.assign(
17
+ ({ header, footer, children, className = '', ...props }: SidebarProps) => {
18
+ return (
19
+ <aside className={`cn-sidebar ${className}`.trim()} {...props}>
20
+ {header && <div className="cn-sidebar-header">{header}</div>}
21
+ <div className="cn-sidebar-nav">{children}</div>
22
+ {footer && <div className="cn-sidebar-footer">{footer}</div>}
23
+ </aside>
24
+ );
25
+ },
26
+ {
27
+ Item: ({ children, active = false, icon, href, className = '', ...props }: SidebarItemProps) => {
28
+ const content = (
29
+ <>
30
+ {icon && <span className="cn-sidebar-item-icon">{icon}</span>}
31
+ {children}
32
+ </>
33
+ );
34
+
35
+ if (href) {
36
+ return (
37
+ <a
38
+ href={href}
39
+ className={`cn-sidebar-item ${active ? 'cn-sidebar-active' : ''} ${className}`.trim()}
40
+ role="menuitem"
41
+ {...(props as React.AnchorHTMLAttributes<HTMLAnchorElement>)}
42
+ >
43
+ {content}
44
+ </a>
45
+ );
46
+ }
47
+
48
+ return (
49
+ <div
50
+ className={`cn-sidebar-item ${active ? 'cn-sidebar-active' : ''} ${className}`.trim()}
51
+ role="menuitem"
52
+ {...(props as React.HTMLAttributes<HTMLDivElement>)}
53
+ >
54
+ {content}
55
+ </div>
56
+ );
57
+ },
58
+ }
59
+ );
60
+
61
+ Sidebar.displayName = 'Sidebar';
62
+ (Sidebar.Item as React.FC<SidebarItemProps> & { displayName?: string }).displayName = 'SidebarItem';
63
+
64
+ export default Sidebar;
@@ -0,0 +1,39 @@
1
+ import * as React from 'react';
2
+
3
+ export type SkeletonVariant = 'text' | 'title' | 'avatar' | 'rect';
4
+
5
+ export interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ variant?: SkeletonVariant;
7
+ width?: string | number;
8
+ height?: string | number;
9
+ }
10
+
11
+ export const Skeleton: React.FC<SkeletonProps> = ({
12
+ variant = 'text',
13
+ width,
14
+ height,
15
+ className = '',
16
+ style,
17
+ ...props
18
+ }) => {
19
+ const variantClass = variant !== 'text' ? `cn-skeleton-${variant}` : 'cn-skeleton';
20
+
21
+ const mergedStyle: React.CSSProperties = {
22
+ width: width,
23
+ height: height,
24
+ ...style,
25
+ };
26
+
27
+ return (
28
+ <div
29
+ className={`${variantClass} ${className}`.trim()}
30
+ style={mergedStyle}
31
+ aria-hidden="true"
32
+ {...props}
33
+ />
34
+ );
35
+ };
36
+
37
+ Skeleton.displayName = 'Skeleton';
38
+
39
+ export default Skeleton;