cronixui 1.0.5 → 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 (130) hide show
  1. package/README.md +35 -5
  2. package/package.json +21 -3
  3. package/packages/go/cronixui/cronixui.go +784 -237
  4. package/packages/go/cronixui/go.mod +34 -9
  5. package/packages/go/cronixui/go.sum +666 -0
  6. package/packages/python/cronixui/__init__.py +131 -1
  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/tokens.py +200 -0
  19. package/packages/python/cronixui/tooltip.py +28 -0
  20. package/packages/react/src/components/Accordion.tsx +82 -0
  21. package/packages/react/src/components/Alert.tsx +80 -0
  22. package/packages/react/src/components/Avatar.tsx +54 -0
  23. package/packages/react/src/components/Badge.tsx +32 -0
  24. package/packages/react/src/components/Breadcrumb.tsx +50 -0
  25. package/packages/react/src/components/Button.tsx +47 -0
  26. package/packages/react/src/components/Card.tsx +69 -0
  27. package/packages/react/src/components/Checkbox.tsx +30 -0
  28. package/packages/react/src/components/CommandPalette.tsx +131 -0
  29. package/packages/react/src/components/Container.tsx +26 -0
  30. package/packages/react/src/components/Dropdown.tsx +88 -0
  31. package/packages/react/src/components/FileInput.tsx +86 -0
  32. package/packages/react/src/components/Footer.tsx +36 -0
  33. package/packages/react/src/components/FormGroup.tsx +36 -0
  34. package/packages/react/src/components/Header.tsx +29 -0
  35. package/packages/react/src/components/Input.tsx +54 -0
  36. package/packages/react/src/components/List.tsx +55 -0
  37. package/packages/react/src/components/Modal.tsx +89 -0
  38. package/packages/react/src/components/Nav.tsx +63 -0
  39. package/packages/react/src/components/Pagination.tsx +107 -0
  40. package/packages/react/src/components/Progress.tsx +49 -0
  41. package/packages/react/src/components/Radio.tsx +64 -0
  42. package/packages/react/src/components/Search.tsx +95 -0
  43. package/packages/react/src/components/Select.tsx +41 -0
  44. package/packages/react/src/components/Sidebar.tsx +64 -0
  45. package/packages/react/src/components/Skeleton.tsx +39 -0
  46. package/packages/react/src/components/Slider.tsx +32 -0
  47. package/packages/react/src/components/Spinner.tsx +24 -0
  48. package/packages/react/src/components/Stack.tsx +69 -0
  49. package/packages/react/src/components/Stat.tsx +35 -0
  50. package/packages/react/src/components/Table.tsx +90 -0
  51. package/packages/react/src/components/Tabs.tsx +85 -0
  52. package/packages/react/src/components/Tag.tsx +30 -0
  53. package/packages/react/src/components/Textarea.tsx +21 -0
  54. package/packages/react/src/components/Toast.tsx +134 -0
  55. package/packages/react/src/components/Toggle.tsx +58 -0
  56. package/packages/react/src/components/Tooltip.tsx +31 -0
  57. package/packages/react/src/components/Typography.tsx +66 -0
  58. package/packages/react/src/index.ts +40 -0
  59. package/packages/react/src/styles.css +2039 -0
  60. package/packages/react/src/tokens/index.ts +94 -0
  61. package/packages/rust/cronixui/src/colors.rs +135 -0
  62. package/packages/rust/cronixui/src/components/accordion.rs +47 -0
  63. package/packages/rust/cronixui/src/components/alert.rs +95 -0
  64. package/packages/rust/cronixui/src/components/avatar.rs +85 -0
  65. package/packages/rust/cronixui/src/components/badge.rs +35 -0
  66. package/packages/rust/cronixui/src/components/breadcrumb.rs +58 -0
  67. package/packages/rust/cronixui/src/components/button.rs +70 -0
  68. package/packages/rust/cronixui/src/components/card.rs +259 -0
  69. package/packages/rust/cronixui/src/components/command_palette.rs +254 -0
  70. package/packages/rust/cronixui/src/components/dropdown.rs +179 -0
  71. package/packages/rust/cronixui/src/components/file_input.rs +74 -0
  72. package/packages/rust/cronixui/src/components/input.rs +21 -0
  73. package/packages/rust/cronixui/src/components/list.rs +38 -0
  74. package/packages/rust/cronixui/src/components/mod.rs +51 -0
  75. package/packages/rust/cronixui/src/{modal.rs → components/modal.rs} +15 -1
  76. package/packages/rust/cronixui/src/components/nav.rs +19 -0
  77. package/packages/rust/cronixui/src/{pagination.rs → components/pagination.rs} +14 -13
  78. package/packages/rust/cronixui/src/components/progress.rs +50 -0
  79. package/packages/rust/cronixui/src/components/search.rs +185 -0
  80. package/packages/rust/cronixui/src/components/skeleton.rs +63 -0
  81. package/packages/rust/cronixui/src/components/spinner.rs +21 -0
  82. package/packages/rust/cronixui/src/components/table.rs +56 -0
  83. package/packages/rust/cronixui/src/components/tabs.rs +43 -0
  84. package/packages/rust/cronixui/src/components/toast.rs +69 -0
  85. package/packages/rust/cronixui/src/{toggle.rs → components/toggle.rs} +7 -5
  86. package/packages/rust/cronixui/src/components/tooltip.rs +11 -0
  87. package/packages/rust/cronixui/src/lib.rs +111 -62
  88. package/packages/rust/cronixui/src/tokens.rs +107 -0
  89. package/packages/web/src/tokens.ts +120 -0
  90. package/packages/web/src/variables.css +81 -81
  91. package/packages/python/cronixui/pyproject.toml +0 -11
  92. package/packages/react/src/components/Accordion.jsx +0 -50
  93. package/packages/react/src/components/Alert.jsx +0 -62
  94. package/packages/react/src/components/Avatar.jsx +0 -34
  95. package/packages/react/src/components/Badge.jsx +0 -15
  96. package/packages/react/src/components/Breadcrumb.jsx +0 -27
  97. package/packages/react/src/components/Button.jsx +0 -21
  98. package/packages/react/src/components/Card.jsx +0 -23
  99. package/packages/react/src/components/Checkbox.jsx +0 -27
  100. package/packages/react/src/components/CommandPalette.jsx +0 -93
  101. package/packages/react/src/components/Dropdown.jsx +0 -48
  102. package/packages/react/src/components/FileInput.jsx +0 -44
  103. package/packages/react/src/components/Input.jsx +0 -22
  104. package/packages/react/src/components/List.jsx +0 -29
  105. package/packages/react/src/components/Modal.jsx +0 -65
  106. package/packages/react/src/components/Nav.jsx +0 -50
  107. package/packages/react/src/components/Pagination.jsx +0 -81
  108. package/packages/react/src/components/Progress.jsx +0 -23
  109. package/packages/react/src/components/Radio.jsx +0 -50
  110. package/packages/react/src/components/Search.jsx +0 -70
  111. package/packages/react/src/components/Select.jsx +0 -33
  112. package/packages/react/src/components/Skeleton.jsx +0 -15
  113. package/packages/react/src/components/Slider.jsx +0 -29
  114. package/packages/react/src/components/Spinner.jsx +0 -5
  115. package/packages/react/src/components/Stat.jsx +0 -19
  116. package/packages/react/src/components/Table.jsx +0 -48
  117. package/packages/react/src/components/Tabs.jsx +0 -65
  118. package/packages/react/src/components/Tag.jsx +0 -19
  119. package/packages/react/src/components/Textarea.jsx +0 -17
  120. package/packages/react/src/components/Toast.jsx +0 -78
  121. package/packages/react/src/components/Toggle.jsx +0 -34
  122. package/packages/react/src/components/Tooltip.jsx +0 -12
  123. package/packages/react/src/index.d.ts +0 -103
  124. package/packages/react/src/index.js +0 -33
  125. package/packages/rust/cronixui/src/accordion.rs +0 -49
  126. package/packages/rust/cronixui/src/command_palette.rs +0 -62
  127. package/packages/rust/cronixui/src/dropdown.rs +0 -31
  128. package/packages/rust/cronixui/src/search.rs +0 -49
  129. package/packages/rust/cronixui/src/tabs.rs +0 -23
  130. package/packages/rust/cronixui/src/toast.rs +0 -70
@@ -0,0 +1,86 @@
1
+ import * as React from 'react';
2
+
3
+ export interface FileInputProps {
4
+ onFileSelect?: (file: File | File[]) => void;
5
+ accept?: string;
6
+ multiple?: boolean;
7
+ label?: string;
8
+ className?: string;
9
+ }
10
+
11
+ export const FileInput: React.FC<FileInputProps> = ({
12
+ onFileSelect,
13
+ accept,
14
+ multiple = false,
15
+ label = 'Drag and drop files here, or click to browse',
16
+ className = '',
17
+ }) => {
18
+ const [fileName, setFileName] = React.useState('');
19
+ const [isDragging, setIsDragging] = React.useState(false);
20
+ const inputRef = React.useRef<HTMLInputElement>(null);
21
+
22
+ const processFiles = React.useCallback((files: File[]) => {
23
+ if (files.length > 0) {
24
+ setFileName(files.map(f => f.name).join(', '));
25
+ onFileSelect?.(multiple ? files : files[0]);
26
+ }
27
+ }, [multiple, onFileSelect]);
28
+
29
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
30
+ const files = Array.from(e.target.files || []);
31
+ processFiles(files);
32
+ };
33
+
34
+ const handleDragOver = (e: React.DragEvent<HTMLLabelElement>) => {
35
+ e.preventDefault();
36
+ e.stopPropagation();
37
+ setIsDragging(true);
38
+ };
39
+
40
+ const handleDragLeave = (e: React.DragEvent<HTMLLabelElement>) => {
41
+ e.preventDefault();
42
+ e.stopPropagation();
43
+ setIsDragging(false);
44
+ };
45
+
46
+ const handleDrop = (e: React.DragEvent<HTMLLabelElement>) => {
47
+ e.preventDefault();
48
+ e.stopPropagation();
49
+ setIsDragging(false);
50
+ const files = Array.from(e.dataTransfer.files);
51
+ processFiles(files);
52
+ };
53
+
54
+ return (
55
+ <div className={`cn-file-input ${fileName ? 'cn-file-input-has-file' : ''} ${isDragging ? 'cn-file-input-dragging' : ''} ${className}`.trim()}>
56
+ <label
57
+ className="cn-file-input-label"
58
+ onDragOver={handleDragOver}
59
+ onDragLeave={handleDragLeave}
60
+ onDrop={handleDrop}
61
+ >
62
+ <input
63
+ ref={inputRef}
64
+ type="file"
65
+ accept={accept}
66
+ multiple={multiple}
67
+ onChange={handleChange}
68
+ />
69
+ <div className="cn-file-input-icon">
70
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
71
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
72
+ <polyline points="17 8 12 3 7 8" />
73
+ <line x1="12" y1="3" x2="12" y2="15" />
74
+ </svg>
75
+ </div>
76
+ <div className="cn-file-input-text">
77
+ {fileName ? <span>{fileName}</span> : label}
78
+ </div>
79
+ </label>
80
+ </div>
81
+ );
82
+ };
83
+
84
+ FileInput.displayName = 'FileInput';
85
+
86
+ export default FileInput;
@@ -0,0 +1,36 @@
1
+ import * as React from 'react';
2
+
3
+ export interface FooterProps extends React.HTMLAttributes<HTMLElement> {
4
+ links?: Array<{ label: string; href: string }>;
5
+ copyright?: string;
6
+ }
7
+
8
+ export const Footer: React.FC<FooterProps> = ({
9
+ links,
10
+ copyright,
11
+ children,
12
+ className = '',
13
+ ...props
14
+ }) => {
15
+ return (
16
+ <footer className={`cn-footer ${className}`.trim()} {...props}>
17
+ <div className="cn-footer-content">
18
+ {links && (
19
+ <div className="cn-footer-links">
20
+ {links.map((link, idx) => (
21
+ <a key={idx} href={link.href} className="cn-footer-link">
22
+ {link.label}
23
+ </a>
24
+ ))}
25
+ </div>
26
+ )}
27
+ {copyright && <div className="cn-footer-copyright">{copyright}</div>}
28
+ {children}
29
+ </div>
30
+ </footer>
31
+ );
32
+ };
33
+
34
+ Footer.displayName = 'Footer';
35
+
36
+ export default Footer;
@@ -0,0 +1,36 @@
1
+ import * as React from 'react';
2
+
3
+ export interface FormGroupProps extends React.HTMLAttributes<HTMLDivElement> {
4
+ label?: React.ReactNode;
5
+ error?: React.ReactNode;
6
+ help?: React.ReactNode;
7
+ required?: boolean;
8
+ }
9
+
10
+ export const FormGroup: React.FC<FormGroupProps> = ({
11
+ children,
12
+ label,
13
+ error,
14
+ help,
15
+ required = false,
16
+ className = '',
17
+ ...props
18
+ }) => {
19
+ return (
20
+ <div className={`cn-form-group ${className}`.trim()} {...props}>
21
+ {label && (
22
+ <label className="cn-form-label">
23
+ {label}
24
+ {required && <span style={{ color: 'var(--cn-error-text)', marginLeft: 'var(--cn-space-1)' }}>*</span>}
25
+ </label>
26
+ )}
27
+ {children}
28
+ {error && <div className="cn-form-error">{error}</div>}
29
+ {!error && help && <div className="cn-form-help">{help}</div>}
30
+ </div>
31
+ );
32
+ };
33
+
34
+ FormGroup.displayName = 'FormGroup';
35
+
36
+ export default FormGroup;
@@ -0,0 +1,29 @@
1
+ import * as React from 'react';
2
+
3
+ export interface HeaderProps extends React.HTMLAttributes<HTMLElement> {
4
+ brand?: React.ReactNode;
5
+ nav?: React.ReactNode;
6
+ actions?: React.ReactNode;
7
+ }
8
+
9
+ export const Header: React.FC<HeaderProps> = ({
10
+ brand,
11
+ nav,
12
+ actions,
13
+ children,
14
+ className = '',
15
+ ...props
16
+ }) => {
17
+ return (
18
+ <header className={`cn-header ${className}`.trim()} {...props}>
19
+ {brand && <div className="cn-header-brand">{brand}</div>}
20
+ {nav && <div className="cn-header-nav">{nav}</div>}
21
+ {actions && <div className="cn-header-actions">{actions}</div>}
22
+ {children}
23
+ </header>
24
+ );
25
+ };
26
+
27
+ Header.displayName = 'Header';
28
+
29
+ export default Header;
@@ -0,0 +1,54 @@
1
+ import * as React from 'react';
2
+
3
+ export type InputSize = 'sm' | 'md' | 'lg';
4
+
5
+ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
6
+ size?: InputSize;
7
+ error?: boolean;
8
+ icon?: React.ReactNode;
9
+ action?: React.ReactNode;
10
+ }
11
+
12
+ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
13
+ ({ size = 'md', error = false, icon, action, className = '', ...props }, ref) => {
14
+ const sizeClass = size !== 'md' ? `cn-input-${size}` : '';
15
+
16
+ if (icon) {
17
+ return (
18
+ <div className="cn-input-icon-wrapper">
19
+ <div className="cn-input-icon">{icon}</div>
20
+ <input
21
+ ref={ref}
22
+ className={`cn-input ${sizeClass} ${error ? 'cn-input-error' : ''} ${className}`.trim()}
23
+ {...props}
24
+ />
25
+ </div>
26
+ );
27
+ }
28
+
29
+ if (action) {
30
+ return (
31
+ <div className="cn-input-action-wrapper">
32
+ <input
33
+ ref={ref}
34
+ className={`cn-input ${sizeClass} ${error ? 'cn-input-error' : ''} ${className}`.trim()}
35
+ {...props}
36
+ />
37
+ <div className="cn-input-action">{action}</div>
38
+ </div>
39
+ );
40
+ }
41
+
42
+ return (
43
+ <input
44
+ ref={ref}
45
+ className={`cn-input ${sizeClass} ${error ? 'cn-input-error' : ''} ${className}`.trim()}
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+ );
51
+
52
+ Input.displayName = 'Input';
53
+
54
+ export default Input;
@@ -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;