cronixui 1.0.0 → 1.0.1
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/LICENSE +674 -0
- package/README.md +582 -0
- package/package.json +10 -7
- package/packages/react/src/components/Accordion.jsx +50 -0
- package/packages/react/src/components/Alert.jsx +62 -0
- package/packages/react/src/components/Avatar.jsx +34 -0
- package/packages/react/src/components/Badge.jsx +15 -0
- package/packages/react/src/components/Breadcrumb.jsx +27 -0
- package/packages/react/src/components/Button.jsx +21 -0
- package/packages/react/src/components/Card.jsx +23 -0
- package/packages/react/src/components/Checkbox.jsx +27 -0
- package/packages/react/src/components/CommandPalette.jsx +93 -0
- package/packages/react/src/components/Dropdown.jsx +48 -0
- package/packages/react/src/components/FileInput.jsx +44 -0
- package/packages/react/src/components/Input.jsx +22 -0
- package/packages/react/src/components/List.jsx +29 -0
- package/packages/react/src/components/Modal.jsx +65 -0
- package/packages/react/src/components/Nav.jsx +50 -0
- package/packages/react/src/components/Pagination.jsx +81 -0
- package/packages/react/src/components/Progress.jsx +23 -0
- package/packages/react/src/components/Radio.jsx +50 -0
- package/packages/react/src/components/Search.jsx +70 -0
- package/packages/react/src/components/Select.jsx +33 -0
- package/packages/react/src/components/Skeleton.jsx +15 -0
- package/packages/react/src/components/Slider.jsx +29 -0
- package/packages/react/src/components/Spinner.jsx +5 -0
- package/packages/react/src/components/Stat.jsx +19 -0
- package/packages/react/src/components/Table.jsx +48 -0
- package/packages/react/src/components/Tabs.jsx +65 -0
- package/packages/react/src/components/Tag.jsx +19 -0
- package/packages/react/src/components/Textarea.jsx +17 -0
- package/packages/react/src/components/Toast.jsx +78 -0
- package/packages/react/src/components/Toggle.jsx +34 -0
- package/packages/react/src/components/Tooltip.jsx +12 -0
- package/packages/react/src/index.js +33 -0
- package/packages/win/CronixUI.WinUI/Controls/FlAvatar.cs +39 -0
- package/packages/win/CronixUI.WinUI/Controls/FlBadge.cs +21 -0
- package/packages/win/CronixUI.WinUI/Controls/FlButton.cs +30 -0
- package/packages/win/CronixUI.WinUI/Controls/FlCard.cs +21 -0
- package/packages/win/CronixUI.WinUI/Controls/FlCheckBox.cs +12 -0
- package/packages/win/CronixUI.WinUI/Controls/FlComboBox.cs +12 -0
- package/packages/win/CronixUI.WinUI/Controls/FlModal.cs +34 -0
- package/packages/win/CronixUI.WinUI/Controls/FlNavigation.cs +12 -0
- package/packages/win/CronixUI.WinUI/Controls/FlProgressBar.cs +21 -0
- package/packages/win/CronixUI.WinUI/Controls/FlRadioButton.cs +12 -0
- package/packages/win/CronixUI.WinUI/Controls/FlSlider.cs +12 -0
- package/packages/win/CronixUI.WinUI/Controls/FlSpinner.cs +21 -0
- package/packages/win/CronixUI.WinUI/Controls/FlTabs.cs +12 -0
- package/packages/win/CronixUI.WinUI/Controls/FlTextBox.cs +21 -0
- package/packages/win/CronixUI.WinUI/Controls/FlToggle.cs +12 -0
- package/packages/win/CronixUI.WinUI/Controls/FlTooltip.cs +21 -0
- package/packages/win/CronixUI.WinUI/CronixUI.WinUI.csproj +21 -0
- package/packages/win/CronixUI.WinUI/CronixUI.WinUI.sln +33 -0
- package/packages/win/CronixUI.WinUI/Themes/FlAvatar.xaml +39 -0
- package/packages/win/CronixUI.WinUI/Themes/FlBadge.xaml +30 -0
- package/packages/win/CronixUI.WinUI/Themes/FlButton.xaml +36 -0
- package/packages/win/CronixUI.WinUI/Themes/FlCard.xaml +28 -0
- package/packages/win/CronixUI.WinUI/Themes/FlCheckBox.xaml +45 -0
- package/packages/win/CronixUI.WinUI/Themes/FlComboBox.xaml +70 -0
- package/packages/win/CronixUI.WinUI/Themes/FlModal.xaml +47 -0
- package/packages/win/CronixUI.WinUI/Themes/FlProgressBar.xaml +27 -0
- package/packages/win/CronixUI.WinUI/Themes/FlRadioButton.xaml +42 -0
- package/packages/win/CronixUI.WinUI/Themes/FlSlider.xaml +38 -0
- package/packages/win/CronixUI.WinUI/Themes/FlSpinner.xaml +13 -0
- package/packages/win/CronixUI.WinUI/Themes/FlTextBox.xaml +39 -0
- package/packages/win/CronixUI.WinUI/Themes/FlToggle.xaml +45 -0
- package/packages/win/CronixUI.WinUI/Themes/FlTooltip.xaml +31 -0
- package/packages/win/CronixUI.WinUI/Themes/Generic.xaml +163 -0
- /package/{dist → packages/web/dist}/cronixui.css +0 -0
- /package/{dist → packages/web/dist}/cronixui.js +0 -0
- /package/{dist → packages/web/dist}/cronixui.min.css +0 -0
- /package/{dist → packages/web/dist}/cronixui.min.js +0 -0
- /package/{src → packages/web/src}/cronixui.css +0 -0
- /package/{src → packages/web/src}/cronixui.js +0 -0
- /package/{src → packages/web/src}/variables.css +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export default function Alert({
|
|
4
|
+
type = 'info',
|
|
5
|
+
title,
|
|
6
|
+
children,
|
|
7
|
+
closable = true,
|
|
8
|
+
onClose,
|
|
9
|
+
className = ''
|
|
10
|
+
}) {
|
|
11
|
+
const [visible, setVisible] = useState(true);
|
|
12
|
+
|
|
13
|
+
const handleClose = () => {
|
|
14
|
+
setVisible(false);
|
|
15
|
+
onClose?.();
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (!visible) return null;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className={`cn-alert cn-alert-${type} ${className}`}>
|
|
22
|
+
<div className="cn-alert-icon">
|
|
23
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
24
|
+
{type === 'success' && <polyline points="20 6 9 17 4 12"></polyline>}
|
|
25
|
+
{type === 'error' && (
|
|
26
|
+
<>
|
|
27
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
28
|
+
<line x1="15" y1="9" x2="9" y2="15"></line>
|
|
29
|
+
<line x1="9" y1="9" x2="15" y2="15"></line>
|
|
30
|
+
</>
|
|
31
|
+
)}
|
|
32
|
+
{type === 'warning' && (
|
|
33
|
+
<>
|
|
34
|
+
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
|
35
|
+
<line x1="12" y1="9" x2="12" y2="13"></line>
|
|
36
|
+
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
|
37
|
+
</>
|
|
38
|
+
)}
|
|
39
|
+
{type === 'info' && (
|
|
40
|
+
<>
|
|
41
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
42
|
+
<line x1="12" y1="16" x2="12" y2="12"></line>
|
|
43
|
+
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
|
44
|
+
</>
|
|
45
|
+
)}
|
|
46
|
+
</svg>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="cn-alert-content">
|
|
49
|
+
{title && <div className="cn-alert-title">{title}</div>}
|
|
50
|
+
<div className="cn-alert-message">{children}</div>
|
|
51
|
+
</div>
|
|
52
|
+
{closable && (
|
|
53
|
+
<button className="cn-alert-close" onClick={handleClose}>
|
|
54
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16">
|
|
55
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
56
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
57
|
+
</svg>
|
|
58
|
+
</button>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export default function Avatar({
|
|
2
|
+
src,
|
|
3
|
+
alt = '',
|
|
4
|
+
initials,
|
|
5
|
+
size = 'md',
|
|
6
|
+
className = ''
|
|
7
|
+
}) {
|
|
8
|
+
const sizeClass = size !== 'md' ? `cn-avatar-${size}` : '';
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className={`cn-avatar ${sizeClass} ${className}`}>
|
|
12
|
+
{src ? (
|
|
13
|
+
<img src={src} alt={alt} />
|
|
14
|
+
) : initials ? (
|
|
15
|
+
initials
|
|
16
|
+
) : null}
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function AvatarGroup({ children, max, className = '' }) {
|
|
22
|
+
const items = Array.isArray(children) ? children : [children];
|
|
23
|
+
const visible = max ? items.slice(0, max) : items;
|
|
24
|
+
const remaining = max ? items.length - max : 0;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className={`cn-avatar-group ${className}`}>
|
|
28
|
+
{visible}
|
|
29
|
+
{remaining > 0 && (
|
|
30
|
+
<div className="cn-avatar">+{remaining}</div>
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export default function Badge({
|
|
2
|
+
children,
|
|
3
|
+
variant = 'default',
|
|
4
|
+
size = 'md',
|
|
5
|
+
className = ''
|
|
6
|
+
}) {
|
|
7
|
+
const variantClass = variant !== 'default' ? `cn-badge-${variant}` : '';
|
|
8
|
+
const sizeClass = size !== 'md' ? `cn-badge-${size}` : '';
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<span className={`cn-badge ${variantClass} ${sizeClass} ${className}`}>
|
|
12
|
+
{children}
|
|
13
|
+
</span>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function Breadcrumb({ children, className = '' }) {
|
|
2
|
+
const items = Array.isArray(children) ? children : [children];
|
|
3
|
+
|
|
4
|
+
return (
|
|
5
|
+
<nav className={`cn-breadcrumb ${className}`}>
|
|
6
|
+
{items.map((child, idx) => (
|
|
7
|
+
<span key={idx}>
|
|
8
|
+
{child}
|
|
9
|
+
{idx < items.length - 1 && (
|
|
10
|
+
<span className="cn-breadcrumb-separator">/</span>
|
|
11
|
+
)}
|
|
12
|
+
</span>
|
|
13
|
+
))}
|
|
14
|
+
</nav>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function BreadcrumbItem({ children, href, active = false, className = '' }) {
|
|
19
|
+
if (active) {
|
|
20
|
+
return <span className={`cn-breadcrumb-current ${className}`}>{children}</span>;
|
|
21
|
+
}
|
|
22
|
+
return (
|
|
23
|
+
<a href={href} className={`cn-breadcrumb-item ${className}`}>
|
|
24
|
+
{children}
|
|
25
|
+
</a>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export default function Button({
|
|
2
|
+
children,
|
|
3
|
+
variant = 'default',
|
|
4
|
+
size = 'md',
|
|
5
|
+
disabled = false,
|
|
6
|
+
className = '',
|
|
7
|
+
...props
|
|
8
|
+
}) {
|
|
9
|
+
const variantClass = variant !== 'default' ? `cn-btn-${variant}` : '';
|
|
10
|
+
const sizeClass = size !== 'md' ? `cn-btn-${size}` : '';
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<button
|
|
14
|
+
className={`cn-btn ${variantClass} ${sizeClass} ${className}`}
|
|
15
|
+
disabled={disabled}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</button>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export default function Card({
|
|
2
|
+
children,
|
|
3
|
+
hoverable = false,
|
|
4
|
+
className = ''
|
|
5
|
+
}) {
|
|
6
|
+
return (
|
|
7
|
+
<div className={`cn-card ${hoverable ? 'cn-card-hoverable' : ''} ${className}`}>
|
|
8
|
+
{children}
|
|
9
|
+
</div>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
Card.Header = function CardHeader({ children, className = '' }) {
|
|
14
|
+
return <div className={`cn-card-header ${className}`}>{children}</div>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
Card.Body = function CardBody({ children, className = '' }) {
|
|
18
|
+
return <div className={`cn-card-body ${className}`}>{children}</div>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
Card.Footer = function CardFooter({ children, className = '' }) {
|
|
22
|
+
return <div className={`cn-card-footer ${className}`}>{children}</div>;
|
|
23
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { forwardRef } from 'react';
|
|
2
|
+
|
|
3
|
+
const Checkbox = forwardRef(function Checkbox({
|
|
4
|
+
checked = false,
|
|
5
|
+
onChange,
|
|
6
|
+
disabled = false,
|
|
7
|
+
children,
|
|
8
|
+
className = '',
|
|
9
|
+
...props
|
|
10
|
+
}, ref) {
|
|
11
|
+
return (
|
|
12
|
+
<label className={`cn-checkbox ${disabled ? 'disabled' : ''} ${className}`}>
|
|
13
|
+
<input
|
|
14
|
+
ref={ref}
|
|
15
|
+
type="checkbox"
|
|
16
|
+
checked={checked}
|
|
17
|
+
onChange={(e) => onChange?.(e.target.checked)}
|
|
18
|
+
disabled={disabled}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
<span className="cn-checkbox-box"></span>
|
|
22
|
+
{children && <span className="cn-checkbox-label">{children}</span>}
|
|
23
|
+
</label>
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export default Checkbox;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export default function CommandPalette({
|
|
4
|
+
items = [],
|
|
5
|
+
isOpen = false,
|
|
6
|
+
onClose,
|
|
7
|
+
onSelect,
|
|
8
|
+
placeholder = 'Search commands...',
|
|
9
|
+
className = ''
|
|
10
|
+
}) {
|
|
11
|
+
const [query, setQuery] = useState('');
|
|
12
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
13
|
+
const inputRef = useRef(null);
|
|
14
|
+
|
|
15
|
+
const filtered = items.filter(item =>
|
|
16
|
+
item.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
17
|
+
(item.subtitle && item.subtitle.toLowerCase().includes(query.toLowerCase()))
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (isOpen) {
|
|
22
|
+
setQuery('');
|
|
23
|
+
setActiveIndex(0);
|
|
24
|
+
setTimeout(() => inputRef.current?.focus(), 100);
|
|
25
|
+
}
|
|
26
|
+
}, [isOpen]);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const handleKeyDown = (e) => {
|
|
30
|
+
if (!isOpen) return;
|
|
31
|
+
|
|
32
|
+
if (e.key === 'Escape') {
|
|
33
|
+
onClose?.();
|
|
34
|
+
} else if (e.key === 'ArrowDown') {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
setActiveIndex(i => Math.min(i + 1, filtered.length - 1));
|
|
37
|
+
} else if (e.key === 'ArrowUp') {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
setActiveIndex(i => Math.max(i - 1, 0));
|
|
40
|
+
} else if (e.key === 'Enter' && filtered[activeIndex]) {
|
|
41
|
+
handleSelect(filtered[activeIndex]);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
46
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
47
|
+
}, [isOpen, filtered, activeIndex, onClose]);
|
|
48
|
+
|
|
49
|
+
const handleSelect = (item) => {
|
|
50
|
+
item.action?.();
|
|
51
|
+
onSelect?.(item);
|
|
52
|
+
onClose?.();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (!isOpen) return null;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className={`cn-command-palette cn-command-palette-open ${className}`} onClick={(e) => e.target === e.currentTarget && onClose?.()}>
|
|
59
|
+
<div className="cn-command-palette-inner">
|
|
60
|
+
<input
|
|
61
|
+
ref={inputRef}
|
|
62
|
+
type="text"
|
|
63
|
+
className="cn-command-palette-input"
|
|
64
|
+
placeholder={placeholder}
|
|
65
|
+
value={query}
|
|
66
|
+
onChange={(e) => {
|
|
67
|
+
setQuery(e.target.value);
|
|
68
|
+
setActiveIndex(0);
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
<div className="cn-command-palette-results">
|
|
72
|
+
{filtered.map((item, idx) => (
|
|
73
|
+
<div
|
|
74
|
+
key={idx}
|
|
75
|
+
className={`cn-command-item ${idx === activeIndex ? 'cn-command-item-active' : ''}`}
|
|
76
|
+
onClick={() => handleSelect(item)}
|
|
77
|
+
onMouseEnter={() => setActiveIndex(idx)}
|
|
78
|
+
>
|
|
79
|
+
{item.icon && <div className="cn-command-item-icon">{item.icon}</div>}
|
|
80
|
+
<div className="cn-command-item-content">
|
|
81
|
+
<div className="cn-command-item-title">{item.title}</div>
|
|
82
|
+
{item.subtitle && (
|
|
83
|
+
<div className="cn-command-item-subtitle">{item.subtitle}</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
{item.kbd && <div className="cn-command-item-kbd">{item.kbd}</div>}
|
|
87
|
+
</div>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export default function Dropdown({
|
|
4
|
+
trigger,
|
|
5
|
+
children,
|
|
6
|
+
className = ''
|
|
7
|
+
}) {
|
|
8
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
9
|
+
const dropdownRef = useRef(null);
|
|
10
|
+
|
|
11
|
+
const toggle = () => setIsOpen((prev) => !prev);
|
|
12
|
+
const close = () => setIsOpen(false);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const handleClickOutside = (e) => {
|
|
16
|
+
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
|
|
17
|
+
close();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (isOpen) {
|
|
22
|
+
document.addEventListener('click', handleClickOutside);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return () => {
|
|
26
|
+
document.removeEventListener('click', handleClickOutside);
|
|
27
|
+
};
|
|
28
|
+
}, [isOpen]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div ref={dropdownRef} className={`cn-dropdown ${isOpen ? 'cn-dropdown-open' : ''} ${className}`}>
|
|
32
|
+
<div className="cn-dropdown-trigger" onClick={toggle}>
|
|
33
|
+
{trigger}
|
|
34
|
+
</div>
|
|
35
|
+
<div className="cn-dropdown-menu" onClick={close}>
|
|
36
|
+
{children}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function DropdownItem({ children, onClick, className = '', ...props }) {
|
|
43
|
+
return (
|
|
44
|
+
<div className={`cn-dropdown-item ${className}`} onClick={onClick} {...props}>
|
|
45
|
+
{children}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useState, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export default function FileInput({
|
|
4
|
+
onFileSelect,
|
|
5
|
+
accept,
|
|
6
|
+
multiple = false,
|
|
7
|
+
label = 'Drag and drop files here, or click to browse',
|
|
8
|
+
className = ''
|
|
9
|
+
}) {
|
|
10
|
+
const [fileName, setFileName] = useState('');
|
|
11
|
+
const inputRef = useRef(null);
|
|
12
|
+
|
|
13
|
+
const handleChange = (e) => {
|
|
14
|
+
const files = Array.from(e.target.files);
|
|
15
|
+
if (files.length > 0) {
|
|
16
|
+
setFileName(files.map(f => f.name).join(', '));
|
|
17
|
+
onFileSelect?.(multiple ? files : files[0]);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className={`cn-file-input ${fileName ? 'cn-file-input-has-file' : ''} ${className}`}>
|
|
23
|
+
<input
|
|
24
|
+
ref={inputRef}
|
|
25
|
+
type="file"
|
|
26
|
+
accept={accept}
|
|
27
|
+
multiple={multiple}
|
|
28
|
+
onChange={handleChange}
|
|
29
|
+
/>
|
|
30
|
+
<div className="cn-file-input-label">
|
|
31
|
+
<div className="cn-file-input-icon">
|
|
32
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
33
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
34
|
+
<polyline points="17 8 12 3 7 8"></polyline>
|
|
35
|
+
<line x1="12" y1="3" x2="12" y2="15"></line>
|
|
36
|
+
</svg>
|
|
37
|
+
</div>
|
|
38
|
+
<div className="cn-file-input-text">
|
|
39
|
+
{fileName ? <span>{fileName}</span> : label}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { forwardRef } from 'react';
|
|
2
|
+
|
|
3
|
+
const Input = forwardRef(function Input({
|
|
4
|
+
type = 'text',
|
|
5
|
+
size = 'md',
|
|
6
|
+
error = false,
|
|
7
|
+
className = '',
|
|
8
|
+
...props
|
|
9
|
+
}, ref) {
|
|
10
|
+
const sizeClass = size !== 'md' ? `cn-input-${size}` : '';
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<input
|
|
14
|
+
ref={ref}
|
|
15
|
+
type={type}
|
|
16
|
+
className={`cn-input ${sizeClass} ${error ? 'cn-input-error' : ''} ${className}`}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export default Input;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function List({ children, className = '' }) {
|
|
2
|
+
return <div className={`cn-list ${className}`}>{children}</div>;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function ListItem({
|
|
6
|
+
children,
|
|
7
|
+
icon,
|
|
8
|
+
title,
|
|
9
|
+
subtitle,
|
|
10
|
+
actions,
|
|
11
|
+
clickable = false,
|
|
12
|
+
onClick,
|
|
13
|
+
className = ''
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
className={`cn-list-item ${clickable ? 'cn-list-item-clickable' : ''} ${className}`}
|
|
18
|
+
onClick={onClick}
|
|
19
|
+
>
|
|
20
|
+
{icon && <div className="cn-list-item-icon">{icon}</div>}
|
|
21
|
+
<div className="cn-list-item-content">
|
|
22
|
+
{title && <div className="cn-list-item-title">{title}</div>}
|
|
23
|
+
{subtitle && <div className="cn-list-item-subtitle">{subtitle}</div>}
|
|
24
|
+
{children}
|
|
25
|
+
</div>
|
|
26
|
+
{actions && <div className="cn-list-item-actions">{actions}</div>}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export default function Modal({
|
|
4
|
+
isOpen = false,
|
|
5
|
+
onClose,
|
|
6
|
+
children,
|
|
7
|
+
className = ''
|
|
8
|
+
}) {
|
|
9
|
+
const modalRef = useRef(null);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const handleEscape = (e) => {
|
|
13
|
+
if (e.key === 'Escape' && onClose) {
|
|
14
|
+
onClose();
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (isOpen) {
|
|
19
|
+
document.addEventListener('keydown', handleEscape);
|
|
20
|
+
document.body.style.overflow = 'hidden';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return () => {
|
|
24
|
+
document.removeEventListener('keydown', handleEscape);
|
|
25
|
+
document.body.style.overflow = '';
|
|
26
|
+
};
|
|
27
|
+
}, [isOpen, onClose]);
|
|
28
|
+
|
|
29
|
+
if (!isOpen) return null;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
className={`cn-modal-backdrop cn-modal-open ${className}`}
|
|
34
|
+
onClick={(e) => e.target === e.currentTarget && onClose?.()}
|
|
35
|
+
>
|
|
36
|
+
<div className="cn-modal" ref={modalRef}>
|
|
37
|
+
{children}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Modal.Header = function ModalHeader({ children, onClose, className = '' }) {
|
|
44
|
+
return (
|
|
45
|
+
<div className={`cn-modal-header ${className}`}>
|
|
46
|
+
<div className="cn-modal-title">{children}</div>
|
|
47
|
+
{onClose && (
|
|
48
|
+
<button className="cn-modal-close" onClick={onClose}>
|
|
49
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
50
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
51
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
52
|
+
</svg>
|
|
53
|
+
</button>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
Modal.Body = function ModalBody({ children, className = '' }) {
|
|
60
|
+
return <div className={`cn-modal-body ${className}`}>{children}</div>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
Modal.Footer = function ModalFooter({ children, className = '' }) {
|
|
64
|
+
return <div className={`cn-modal-footer ${className}`}>{children}</div>;
|
|
65
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export default function Nav({
|
|
4
|
+
defaultActive,
|
|
5
|
+
active: controlledActive,
|
|
6
|
+
onChange,
|
|
7
|
+
children,
|
|
8
|
+
className = ''
|
|
9
|
+
}) {
|
|
10
|
+
const [internalActive, setInternalActive] = useState(defaultActive);
|
|
11
|
+
const activeItem = controlledActive !== undefined ? controlledActive : internalActive;
|
|
12
|
+
|
|
13
|
+
const handleClick = (item) => {
|
|
14
|
+
if (onChange) {
|
|
15
|
+
onChange(item);
|
|
16
|
+
} else {
|
|
17
|
+
setInternalActive(item);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<nav className={`cn-nav ${className}`}>
|
|
23
|
+
{children.map((child, idx) => {
|
|
24
|
+
const isActive = child.props.active !== undefined
|
|
25
|
+
? child.props.active
|
|
26
|
+
: child.props.id === activeItem;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
...child,
|
|
30
|
+
props: {
|
|
31
|
+
...child.props,
|
|
32
|
+
active: isActive,
|
|
33
|
+
onClick: () => handleClick(child.props.id),
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
})}
|
|
37
|
+
</nav>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function NavItem({ children, active = false, onClick, className = '' }) {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
className={`cn-nav-item ${active ? 'cn-nav-active' : ''} ${className}`}
|
|
45
|
+
onClick={onClick}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export default function Pagination({
|
|
4
|
+
total,
|
|
5
|
+
current = 1,
|
|
6
|
+
onChange,
|
|
7
|
+
className = ''
|
|
8
|
+
}) {
|
|
9
|
+
const [page, setPage] = useState(current);
|
|
10
|
+
|
|
11
|
+
const activePage = onChange !== undefined ? current : page;
|
|
12
|
+
|
|
13
|
+
const getPages = () => {
|
|
14
|
+
const pages = [];
|
|
15
|
+
const maxVisible = 5;
|
|
16
|
+
|
|
17
|
+
if (total <= maxVisible) {
|
|
18
|
+
for (let i = 1; i <= total; i++) pages.push(i);
|
|
19
|
+
} else {
|
|
20
|
+
if (activePage <= 3) {
|
|
21
|
+
for (let i = 1; i <= 4; i++) pages.push(i);
|
|
22
|
+
pages.push('...');
|
|
23
|
+
pages.push(total);
|
|
24
|
+
} else if (activePage >= total - 2) {
|
|
25
|
+
pages.push(1);
|
|
26
|
+
pages.push('...');
|
|
27
|
+
for (let i = total - 3; i <= total; i++) pages.push(i);
|
|
28
|
+
} else {
|
|
29
|
+
pages.push(1);
|
|
30
|
+
pages.push('...');
|
|
31
|
+
for (let i = activePage - 1; i <= activePage + 1; i++) pages.push(i);
|
|
32
|
+
pages.push('...');
|
|
33
|
+
pages.push(total);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return pages;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const goTo = (p) => {
|
|
41
|
+
if (p < 1 || p > total || p === activePage) return;
|
|
42
|
+
if (onChange) {
|
|
43
|
+
onChange(p);
|
|
44
|
+
} else {
|
|
45
|
+
setPage(p);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className={`cn-pagination ${className}`}>
|
|
51
|
+
<button
|
|
52
|
+
className="cn-pagination-item"
|
|
53
|
+
onClick={() => goTo(activePage - 1)}
|
|
54
|
+
disabled={activePage === 1}
|
|
55
|
+
>
|
|
56
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16">
|
|
57
|
+
<polyline points="15 18 9 12 15 6"></polyline>
|
|
58
|
+
</svg>
|
|
59
|
+
</button>
|
|
60
|
+
{getPages().map((p, idx) => (
|
|
61
|
+
<button
|
|
62
|
+
key={idx}
|
|
63
|
+
className={`cn-pagination-item ${p === activePage ? 'cn-pagination-active' : ''}`}
|
|
64
|
+
onClick={() => typeof p === 'number' && goTo(p)}
|
|
65
|
+
disabled={p === '...'}
|
|
66
|
+
>
|
|
67
|
+
{p}
|
|
68
|
+
</button>
|
|
69
|
+
))}
|
|
70
|
+
<button
|
|
71
|
+
className="cn-pagination-item"
|
|
72
|
+
onClick={() => goTo(activePage + 1)}
|
|
73
|
+
disabled={activePage === total}
|
|
74
|
+
>
|
|
75
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16">
|
|
76
|
+
<polyline points="9 18 15 12 9 6"></polyline>
|
|
77
|
+
</svg>
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export default function Progress({
|
|
2
|
+
value = 0,
|
|
3
|
+
max = 100,
|
|
4
|
+
showLabel = false,
|
|
5
|
+
variant = 'default',
|
|
6
|
+
size = 'md',
|
|
7
|
+
className = ''
|
|
8
|
+
}) {
|
|
9
|
+
const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className={className}>
|
|
13
|
+
{showLabel && (
|
|
14
|
+
<div className="cn-progress-label">
|
|
15
|
+
<span>{percentage.toFixed(0)}%</span>
|
|
16
|
+
</div>
|
|
17
|
+
)}
|
|
18
|
+
<div className={`cn-progress ${size !== 'md' ? `cn-progress-${size}` : ''} ${variant !== 'default' ? `cn-progress-${variant}` : ''}`}>
|
|
19
|
+
<div className="cn-progress-bar" style={{ width: `${percentage}%` }}></div>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|