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.
Files changed (75) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +582 -0
  3. package/package.json +10 -7
  4. package/packages/react/src/components/Accordion.jsx +50 -0
  5. package/packages/react/src/components/Alert.jsx +62 -0
  6. package/packages/react/src/components/Avatar.jsx +34 -0
  7. package/packages/react/src/components/Badge.jsx +15 -0
  8. package/packages/react/src/components/Breadcrumb.jsx +27 -0
  9. package/packages/react/src/components/Button.jsx +21 -0
  10. package/packages/react/src/components/Card.jsx +23 -0
  11. package/packages/react/src/components/Checkbox.jsx +27 -0
  12. package/packages/react/src/components/CommandPalette.jsx +93 -0
  13. package/packages/react/src/components/Dropdown.jsx +48 -0
  14. package/packages/react/src/components/FileInput.jsx +44 -0
  15. package/packages/react/src/components/Input.jsx +22 -0
  16. package/packages/react/src/components/List.jsx +29 -0
  17. package/packages/react/src/components/Modal.jsx +65 -0
  18. package/packages/react/src/components/Nav.jsx +50 -0
  19. package/packages/react/src/components/Pagination.jsx +81 -0
  20. package/packages/react/src/components/Progress.jsx +23 -0
  21. package/packages/react/src/components/Radio.jsx +50 -0
  22. package/packages/react/src/components/Search.jsx +70 -0
  23. package/packages/react/src/components/Select.jsx +33 -0
  24. package/packages/react/src/components/Skeleton.jsx +15 -0
  25. package/packages/react/src/components/Slider.jsx +29 -0
  26. package/packages/react/src/components/Spinner.jsx +5 -0
  27. package/packages/react/src/components/Stat.jsx +19 -0
  28. package/packages/react/src/components/Table.jsx +48 -0
  29. package/packages/react/src/components/Tabs.jsx +65 -0
  30. package/packages/react/src/components/Tag.jsx +19 -0
  31. package/packages/react/src/components/Textarea.jsx +17 -0
  32. package/packages/react/src/components/Toast.jsx +78 -0
  33. package/packages/react/src/components/Toggle.jsx +34 -0
  34. package/packages/react/src/components/Tooltip.jsx +12 -0
  35. package/packages/react/src/index.js +33 -0
  36. package/packages/win/CronixUI.WinUI/Controls/FlAvatar.cs +39 -0
  37. package/packages/win/CronixUI.WinUI/Controls/FlBadge.cs +21 -0
  38. package/packages/win/CronixUI.WinUI/Controls/FlButton.cs +30 -0
  39. package/packages/win/CronixUI.WinUI/Controls/FlCard.cs +21 -0
  40. package/packages/win/CronixUI.WinUI/Controls/FlCheckBox.cs +12 -0
  41. package/packages/win/CronixUI.WinUI/Controls/FlComboBox.cs +12 -0
  42. package/packages/win/CronixUI.WinUI/Controls/FlModal.cs +34 -0
  43. package/packages/win/CronixUI.WinUI/Controls/FlNavigation.cs +12 -0
  44. package/packages/win/CronixUI.WinUI/Controls/FlProgressBar.cs +21 -0
  45. package/packages/win/CronixUI.WinUI/Controls/FlRadioButton.cs +12 -0
  46. package/packages/win/CronixUI.WinUI/Controls/FlSlider.cs +12 -0
  47. package/packages/win/CronixUI.WinUI/Controls/FlSpinner.cs +21 -0
  48. package/packages/win/CronixUI.WinUI/Controls/FlTabs.cs +12 -0
  49. package/packages/win/CronixUI.WinUI/Controls/FlTextBox.cs +21 -0
  50. package/packages/win/CronixUI.WinUI/Controls/FlToggle.cs +12 -0
  51. package/packages/win/CronixUI.WinUI/Controls/FlTooltip.cs +21 -0
  52. package/packages/win/CronixUI.WinUI/CronixUI.WinUI.csproj +21 -0
  53. package/packages/win/CronixUI.WinUI/CronixUI.WinUI.sln +33 -0
  54. package/packages/win/CronixUI.WinUI/Themes/FlAvatar.xaml +39 -0
  55. package/packages/win/CronixUI.WinUI/Themes/FlBadge.xaml +30 -0
  56. package/packages/win/CronixUI.WinUI/Themes/FlButton.xaml +36 -0
  57. package/packages/win/CronixUI.WinUI/Themes/FlCard.xaml +28 -0
  58. package/packages/win/CronixUI.WinUI/Themes/FlCheckBox.xaml +45 -0
  59. package/packages/win/CronixUI.WinUI/Themes/FlComboBox.xaml +70 -0
  60. package/packages/win/CronixUI.WinUI/Themes/FlModal.xaml +47 -0
  61. package/packages/win/CronixUI.WinUI/Themes/FlProgressBar.xaml +27 -0
  62. package/packages/win/CronixUI.WinUI/Themes/FlRadioButton.xaml +42 -0
  63. package/packages/win/CronixUI.WinUI/Themes/FlSlider.xaml +38 -0
  64. package/packages/win/CronixUI.WinUI/Themes/FlSpinner.xaml +13 -0
  65. package/packages/win/CronixUI.WinUI/Themes/FlTextBox.xaml +39 -0
  66. package/packages/win/CronixUI.WinUI/Themes/FlToggle.xaml +45 -0
  67. package/packages/win/CronixUI.WinUI/Themes/FlTooltip.xaml +31 -0
  68. package/packages/win/CronixUI.WinUI/Themes/Generic.xaml +163 -0
  69. /package/{dist → packages/web/dist}/cronixui.css +0 -0
  70. /package/{dist → packages/web/dist}/cronixui.js +0 -0
  71. /package/{dist → packages/web/dist}/cronixui.min.css +0 -0
  72. /package/{dist → packages/web/dist}/cronixui.min.js +0 -0
  73. /package/{src → packages/web/src}/cronixui.css +0 -0
  74. /package/{src → packages/web/src}/cronixui.js +0 -0
  75. /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
+ }