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.
- package/README.md +35 -5
- package/package.json +21 -3
- package/packages/go/cronixui/cronixui.go +784 -237
- package/packages/go/cronixui/go.mod +34 -9
- package/packages/go/cronixui/go.sum +666 -0
- package/packages/python/cronixui/__init__.py +131 -1
- package/packages/python/cronixui/alert.py +61 -0
- package/packages/python/cronixui/avatar.py +50 -0
- package/packages/python/cronixui/badge.py +46 -0
- package/packages/python/cronixui/button.py +64 -0
- package/packages/python/cronixui/card.py +62 -0
- package/packages/python/cronixui/form.py +255 -0
- package/packages/python/cronixui/layout.py +143 -0
- package/packages/python/cronixui/list.py +51 -0
- package/packages/python/cronixui/loading.py +36 -0
- package/packages/python/cronixui/progress.py +90 -0
- package/packages/python/cronixui/table.py +48 -0
- package/packages/python/cronixui/tokens.py +200 -0
- package/packages/python/cronixui/tooltip.py +28 -0
- package/packages/react/src/components/Accordion.tsx +82 -0
- package/packages/react/src/components/Alert.tsx +80 -0
- package/packages/react/src/components/Avatar.tsx +54 -0
- package/packages/react/src/components/Badge.tsx +32 -0
- package/packages/react/src/components/Breadcrumb.tsx +50 -0
- package/packages/react/src/components/Button.tsx +47 -0
- package/packages/react/src/components/Card.tsx +69 -0
- package/packages/react/src/components/Checkbox.tsx +30 -0
- package/packages/react/src/components/CommandPalette.tsx +131 -0
- package/packages/react/src/components/Container.tsx +26 -0
- package/packages/react/src/components/Dropdown.tsx +88 -0
- package/packages/react/src/components/FileInput.tsx +86 -0
- package/packages/react/src/components/Footer.tsx +36 -0
- package/packages/react/src/components/FormGroup.tsx +36 -0
- package/packages/react/src/components/Header.tsx +29 -0
- package/packages/react/src/components/Input.tsx +54 -0
- package/packages/react/src/components/List.tsx +55 -0
- package/packages/react/src/components/Modal.tsx +89 -0
- package/packages/react/src/components/Nav.tsx +63 -0
- package/packages/react/src/components/Pagination.tsx +107 -0
- package/packages/react/src/components/Progress.tsx +49 -0
- package/packages/react/src/components/Radio.tsx +64 -0
- package/packages/react/src/components/Search.tsx +95 -0
- package/packages/react/src/components/Select.tsx +41 -0
- package/packages/react/src/components/Sidebar.tsx +64 -0
- package/packages/react/src/components/Skeleton.tsx +39 -0
- package/packages/react/src/components/Slider.tsx +32 -0
- package/packages/react/src/components/Spinner.tsx +24 -0
- package/packages/react/src/components/Stack.tsx +69 -0
- package/packages/react/src/components/Stat.tsx +35 -0
- package/packages/react/src/components/Table.tsx +90 -0
- package/packages/react/src/components/Tabs.tsx +85 -0
- package/packages/react/src/components/Tag.tsx +30 -0
- package/packages/react/src/components/Textarea.tsx +21 -0
- package/packages/react/src/components/Toast.tsx +134 -0
- package/packages/react/src/components/Toggle.tsx +58 -0
- package/packages/react/src/components/Tooltip.tsx +31 -0
- package/packages/react/src/components/Typography.tsx +66 -0
- package/packages/react/src/index.ts +40 -0
- package/packages/react/src/styles.css +2039 -0
- package/packages/react/src/tokens/index.ts +94 -0
- package/packages/rust/cronixui/src/colors.rs +135 -0
- package/packages/rust/cronixui/src/components/accordion.rs +47 -0
- package/packages/rust/cronixui/src/components/alert.rs +95 -0
- package/packages/rust/cronixui/src/components/avatar.rs +85 -0
- package/packages/rust/cronixui/src/components/badge.rs +35 -0
- package/packages/rust/cronixui/src/components/breadcrumb.rs +58 -0
- package/packages/rust/cronixui/src/components/button.rs +70 -0
- package/packages/rust/cronixui/src/components/card.rs +259 -0
- package/packages/rust/cronixui/src/components/command_palette.rs +254 -0
- package/packages/rust/cronixui/src/components/dropdown.rs +179 -0
- package/packages/rust/cronixui/src/components/file_input.rs +74 -0
- package/packages/rust/cronixui/src/components/input.rs +21 -0
- package/packages/rust/cronixui/src/components/list.rs +38 -0
- package/packages/rust/cronixui/src/components/mod.rs +51 -0
- package/packages/rust/cronixui/src/{modal.rs → components/modal.rs} +15 -1
- package/packages/rust/cronixui/src/components/nav.rs +19 -0
- package/packages/rust/cronixui/src/{pagination.rs → components/pagination.rs} +14 -13
- package/packages/rust/cronixui/src/components/progress.rs +50 -0
- package/packages/rust/cronixui/src/components/search.rs +185 -0
- package/packages/rust/cronixui/src/components/skeleton.rs +63 -0
- package/packages/rust/cronixui/src/components/spinner.rs +21 -0
- package/packages/rust/cronixui/src/components/table.rs +56 -0
- package/packages/rust/cronixui/src/components/tabs.rs +43 -0
- package/packages/rust/cronixui/src/components/toast.rs +69 -0
- package/packages/rust/cronixui/src/{toggle.rs → components/toggle.rs} +7 -5
- package/packages/rust/cronixui/src/components/tooltip.rs +11 -0
- package/packages/rust/cronixui/src/lib.rs +111 -62
- package/packages/rust/cronixui/src/tokens.rs +107 -0
- package/packages/web/src/tokens.ts +120 -0
- package/packages/web/src/variables.css +81 -81
- package/packages/python/cronixui/pyproject.toml +0 -11
- package/packages/react/src/components/Accordion.jsx +0 -50
- package/packages/react/src/components/Alert.jsx +0 -62
- package/packages/react/src/components/Avatar.jsx +0 -34
- package/packages/react/src/components/Badge.jsx +0 -15
- package/packages/react/src/components/Breadcrumb.jsx +0 -27
- package/packages/react/src/components/Button.jsx +0 -21
- package/packages/react/src/components/Card.jsx +0 -23
- package/packages/react/src/components/Checkbox.jsx +0 -27
- package/packages/react/src/components/CommandPalette.jsx +0 -93
- package/packages/react/src/components/Dropdown.jsx +0 -48
- package/packages/react/src/components/FileInput.jsx +0 -44
- package/packages/react/src/components/Input.jsx +0 -22
- package/packages/react/src/components/List.jsx +0 -29
- package/packages/react/src/components/Modal.jsx +0 -65
- package/packages/react/src/components/Nav.jsx +0 -50
- package/packages/react/src/components/Pagination.jsx +0 -81
- package/packages/react/src/components/Progress.jsx +0 -23
- package/packages/react/src/components/Radio.jsx +0 -50
- package/packages/react/src/components/Search.jsx +0 -70
- package/packages/react/src/components/Select.jsx +0 -33
- package/packages/react/src/components/Skeleton.jsx +0 -15
- package/packages/react/src/components/Slider.jsx +0 -29
- package/packages/react/src/components/Spinner.jsx +0 -5
- package/packages/react/src/components/Stat.jsx +0 -19
- package/packages/react/src/components/Table.jsx +0 -48
- package/packages/react/src/components/Tabs.jsx +0 -65
- package/packages/react/src/components/Tag.jsx +0 -19
- package/packages/react/src/components/Textarea.jsx +0 -17
- package/packages/react/src/components/Toast.jsx +0 -78
- package/packages/react/src/components/Toggle.jsx +0 -34
- package/packages/react/src/components/Tooltip.jsx +0 -12
- package/packages/react/src/index.d.ts +0 -103
- package/packages/react/src/index.js +0 -33
- package/packages/rust/cronixui/src/accordion.rs +0 -49
- package/packages/rust/cronixui/src/command_palette.rs +0 -62
- package/packages/rust/cronixui/src/dropdown.rs +0 -31
- package/packages/rust/cronixui/src/search.rs +0 -49
- package/packages/rust/cronixui/src/tabs.rs +0 -23
- 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;
|