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,95 @@
1
+ import * as React from 'react';
2
+
3
+ export interface SearchItem {
4
+ title: string;
5
+ subtitle?: string;
6
+ }
7
+
8
+ export interface SearchProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect'> {
9
+ items?: SearchItem[];
10
+ placeholder?: string;
11
+ onSelect?: (item: SearchItem) => void;
12
+ }
13
+
14
+ export const Search: React.FC<SearchProps> = ({
15
+ items = [],
16
+ placeholder = 'Search...',
17
+ onSelect,
18
+ className = '',
19
+ ...props
20
+ }) => {
21
+ const [query, setQuery] = React.useState('');
22
+ const [isOpen, setIsOpen] = React.useState(false);
23
+ const ref = React.useRef<HTMLDivElement>(null);
24
+
25
+ const filtered = items.filter(
26
+ (item) =>
27
+ item.title.toLowerCase().includes(query.toLowerCase()) ||
28
+ (item.subtitle && item.subtitle.toLowerCase().includes(query.toLowerCase()))
29
+ );
30
+
31
+ React.useEffect(() => {
32
+ const handleClickOutside = (e: MouseEvent) => {
33
+ if (ref.current && !ref.current.contains(e.target as Node)) {
34
+ setIsOpen(false);
35
+ }
36
+ };
37
+
38
+ document.addEventListener('click', handleClickOutside);
39
+ return () => document.removeEventListener('click', handleClickOutside);
40
+ }, []);
41
+
42
+ const handleSelect = (item: SearchItem) => {
43
+ setQuery(item.title);
44
+ setIsOpen(false);
45
+ onSelect?.(item);
46
+ };
47
+
48
+ return (
49
+ <div ref={ref} className={`cn-search ${isOpen ? 'cn-search-open' : ''} ${className}`.trim()} {...props}>
50
+ <svg className="cn-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
51
+ <circle cx="11" cy="11" r="8" />
52
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
53
+ </svg>
54
+ <input
55
+ type="search"
56
+ className="cn-input cn-search-input"
57
+ placeholder={placeholder}
58
+ value={query}
59
+ onChange={(e) => {
60
+ setQuery(e.target.value);
61
+ setIsOpen(true);
62
+ }}
63
+ onFocus={() => setIsOpen(true)}
64
+ role="combobox"
65
+ aria-autocomplete="list"
66
+ aria-expanded={isOpen}
67
+ />
68
+ {isOpen && (
69
+ <div className="cn-search-results" role="listbox">
70
+ {filtered.length > 0 ? (
71
+ filtered.map((item, idx) => (
72
+ <div
73
+ key={idx}
74
+ className="cn-search-result"
75
+ onClick={() => handleSelect(item)}
76
+ role="option"
77
+ >
78
+ <div className="cn-search-result-title">{item.title}</div>
79
+ {item.subtitle && (
80
+ <div className="cn-search-result-subtitle">{item.subtitle}</div>
81
+ )}
82
+ </div>
83
+ ))
84
+ ) : (
85
+ <div className="cn-search-empty">No results found</div>
86
+ )}
87
+ </div>
88
+ )}
89
+ </div>
90
+ );
91
+ };
92
+
93
+ Search.displayName = 'Search';
94
+
95
+ export default Search;
@@ -0,0 +1,41 @@
1
+ import * as React from 'react';
2
+
3
+ export interface SelectOption {
4
+ value: string;
5
+ label: string;
6
+ }
7
+
8
+ export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'value' | 'onChange'> {
9
+ options: SelectOption[];
10
+ value?: string;
11
+ onChange?: (value: string) => void;
12
+ placeholder?: string;
13
+ }
14
+
15
+ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
16
+ ({ options = [], value, onChange, placeholder = 'Select...', disabled = false, className = '', ...props }, ref) => {
17
+ return (
18
+ <div className={`cn-select-wrapper ${className}`.trim()}>
19
+ <select
20
+ ref={ref}
21
+ className="cn-select"
22
+ value={value}
23
+ onChange={(e) => onChange?.(e.target.value)}
24
+ disabled={disabled}
25
+ {...props}
26
+ >
27
+ {placeholder && <option value="" disabled>{placeholder}</option>}
28
+ {options.map((opt, idx) => (
29
+ <option key={idx} value={opt.value}>
30
+ {opt.label}
31
+ </option>
32
+ ))}
33
+ </select>
34
+ </div>
35
+ );
36
+ }
37
+ );
38
+
39
+ Select.displayName = 'Select';
40
+
41
+ export default Select;
@@ -0,0 +1,64 @@
1
+ import * as React from 'react';
2
+
3
+ export interface SidebarProps extends React.HTMLAttributes<HTMLElement> {
4
+ header?: React.ReactNode;
5
+ footer?: React.ReactNode;
6
+ }
7
+
8
+ export interface SidebarItemProps extends React.HTMLAttributes<HTMLAnchorElement | HTMLDivElement> {
9
+ active?: boolean;
10
+ icon?: React.ReactNode;
11
+ href?: string;
12
+ }
13
+
14
+ export const Sidebar: React.FC<SidebarProps> & {
15
+ Item: React.FC<SidebarItemProps>;
16
+ } = Object.assign(
17
+ ({ header, footer, children, className = '', ...props }: SidebarProps) => {
18
+ return (
19
+ <aside className={`cn-sidebar ${className}`.trim()} {...props}>
20
+ {header && <div className="cn-sidebar-header">{header}</div>}
21
+ <div className="cn-sidebar-nav">{children}</div>
22
+ {footer && <div className="cn-sidebar-footer">{footer}</div>}
23
+ </aside>
24
+ );
25
+ },
26
+ {
27
+ Item: ({ children, active = false, icon, href, className = '', ...props }: SidebarItemProps) => {
28
+ const content = (
29
+ <>
30
+ {icon && <span className="cn-sidebar-item-icon">{icon}</span>}
31
+ {children}
32
+ </>
33
+ );
34
+
35
+ if (href) {
36
+ return (
37
+ <a
38
+ href={href}
39
+ className={`cn-sidebar-item ${active ? 'cn-sidebar-active' : ''} ${className}`.trim()}
40
+ role="menuitem"
41
+ {...(props as React.AnchorHTMLAttributes<HTMLAnchorElement>)}
42
+ >
43
+ {content}
44
+ </a>
45
+ );
46
+ }
47
+
48
+ return (
49
+ <div
50
+ className={`cn-sidebar-item ${active ? 'cn-sidebar-active' : ''} ${className}`.trim()}
51
+ role="menuitem"
52
+ {...(props as React.HTMLAttributes<HTMLDivElement>)}
53
+ >
54
+ {content}
55
+ </div>
56
+ );
57
+ },
58
+ }
59
+ );
60
+
61
+ Sidebar.displayName = 'Sidebar';
62
+ (Sidebar.Item as React.FC<SidebarItemProps> & { displayName?: string }).displayName = 'SidebarItem';
63
+
64
+ export default Sidebar;
@@ -0,0 +1,39 @@
1
+ import * as React from 'react';
2
+
3
+ export type SkeletonVariant = 'text' | 'title' | 'avatar' | 'rect';
4
+
5
+ export interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ variant?: SkeletonVariant;
7
+ width?: string | number;
8
+ height?: string | number;
9
+ }
10
+
11
+ export const Skeleton: React.FC<SkeletonProps> = ({
12
+ variant = 'text',
13
+ width,
14
+ height,
15
+ className = '',
16
+ style,
17
+ ...props
18
+ }) => {
19
+ const variantClass = variant !== 'text' ? `cn-skeleton-${variant}` : 'cn-skeleton';
20
+
21
+ const mergedStyle: React.CSSProperties = {
22
+ width: width,
23
+ height: height,
24
+ ...style,
25
+ };
26
+
27
+ return (
28
+ <div
29
+ className={`${variantClass} ${className}`.trim()}
30
+ style={mergedStyle}
31
+ aria-hidden="true"
32
+ {...props}
33
+ />
34
+ );
35
+ };
36
+
37
+ Skeleton.displayName = 'Skeleton';
38
+
39
+ export default Skeleton;
@@ -0,0 +1,32 @@
1
+ import * as React from 'react';
2
+
3
+ export interface SliderProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'value' | 'onChange'> {
4
+ value?: number;
5
+ min?: number;
6
+ max?: number;
7
+ step?: number;
8
+ onChange?: (value: number) => void;
9
+ }
10
+
11
+ export const Slider = React.forwardRef<HTMLInputElement, SliderProps>(
12
+ ({ value = 0, min = 0, max = 100, step = 1, onChange, disabled = false, className = '', ...props }, ref) => {
13
+ return (
14
+ <input
15
+ ref={ref}
16
+ type="range"
17
+ className={`cn-slider ${className}`.trim()}
18
+ value={value}
19
+ min={min}
20
+ max={max}
21
+ step={step}
22
+ onChange={(e) => onChange?.(Number(e.target.value))}
23
+ disabled={disabled}
24
+ {...props}
25
+ />
26
+ );
27
+ }
28
+ );
29
+
30
+ Slider.displayName = 'Slider';
31
+
32
+ export default Slider;
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+
3
+ export type SpinnerSize = 'sm' | 'md' | 'lg';
4
+
5
+ export interface SpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ size?: SpinnerSize;
7
+ }
8
+
9
+ export const Spinner: React.FC<SpinnerProps> = ({ size = 'md', className = '', ...props }) => {
10
+ const sizeClass = size !== 'md' ? `cn-spinner-${size}` : '';
11
+
12
+ return (
13
+ <div
14
+ className={`cn-spinner ${sizeClass} ${className}`.trim()}
15
+ role="status"
16
+ aria-label="Loading"
17
+ {...props}
18
+ />
19
+ );
20
+ };
21
+
22
+ Spinner.displayName = 'Spinner';
23
+
24
+ export default Spinner;
@@ -0,0 +1,69 @@
1
+ import * as React from 'react';
2
+
3
+ export interface StackProps extends React.HTMLAttributes<HTMLDivElement> {
4
+ spacing?: '1' | '2' | '4' | '6';
5
+ }
6
+
7
+ export const Stack: React.FC<StackProps> = ({
8
+ children,
9
+ spacing = '4',
10
+ className = '',
11
+ ...props
12
+ }) => {
13
+ return (
14
+ <div
15
+ className={`cn-stack cn-stack-${spacing} ${className}`.trim()}
16
+ {...props}
17
+ >
18
+ {children}
19
+ </div>
20
+ );
21
+ };
22
+
23
+ Stack.displayName = 'Stack';
24
+
25
+ export const HStack: React.FC<StackProps> = ({
26
+ children,
27
+ spacing = '4',
28
+ className = '',
29
+ ...props
30
+ }) => {
31
+ return (
32
+ <div
33
+ className={`cn-hstack cn-hstack-${spacing} ${className}`.trim()}
34
+ {...props}
35
+ >
36
+ {children}
37
+ </div>
38
+ );
39
+ };
40
+
41
+ HStack.displayName = 'HStack';
42
+
43
+ export interface DividerProps extends React.HTMLAttributes<HTMLHRElement> {
44
+ orientation?: 'horizontal' | 'vertical';
45
+ }
46
+
47
+ export const Divider: React.FC<DividerProps> = ({
48
+ className = '',
49
+ orientation = 'horizontal',
50
+ style,
51
+ ...props
52
+ }) => {
53
+ const dividerStyle: React.CSSProperties = orientation === 'vertical'
54
+ ? { width: '1px', height: 'auto', minHeight: '100%', borderRight: 'none', borderBottom: '1px solid var(--cn-border)', ...style }
55
+ : { height: '1px', border: 'none', backgroundColor: 'var(--cn-border)', ...style };
56
+
57
+ return (
58
+ <hr
59
+ className={`cn-divider ${className}`.trim()}
60
+ style={dividerStyle}
61
+ role="separator"
62
+ {...props}
63
+ />
64
+ );
65
+ };
66
+
67
+ Divider.displayName = 'Divider';
68
+
69
+ export default Stack;
@@ -0,0 +1,35 @@
1
+ import * as React from 'react';
2
+
3
+ export type StatDeltaType = 'up' | 'down';
4
+
5
+ export interface StatProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ value: React.ReactNode;
7
+ label?: string;
8
+ delta?: string;
9
+ deltaType?: StatDeltaType;
10
+ }
11
+
12
+ export const Stat: React.FC<StatProps> = ({
13
+ value,
14
+ label,
15
+ delta,
16
+ deltaType = 'up',
17
+ className = '',
18
+ ...props
19
+ }) => {
20
+ return (
21
+ <div className={`cn-stat ${className}`.trim()} {...props}>
22
+ <div className="cn-stat-value">{value}</div>
23
+ {label && <div className="cn-stat-label">{label}</div>}
24
+ {delta && (
25
+ <div className={`cn-stat-delta cn-stat-delta-${deltaType}`.trim()}>
26
+ {deltaType === 'up' ? '↑' : '↓'} {delta}
27
+ </div>
28
+ )}
29
+ </div>
30
+ );
31
+ };
32
+
33
+ Stat.displayName = 'Stat';
34
+
35
+ export default Stat;
@@ -0,0 +1,90 @@
1
+ import * as React from 'react';
2
+
3
+ export interface TableColumn<T = Record<string, unknown>> {
4
+ key: string;
5
+ header: string;
6
+ sortable?: boolean;
7
+ render?: (row: T) => React.ReactNode;
8
+ }
9
+
10
+ export interface TableProps<T = Record<string, unknown>> extends React.HTMLAttributes<HTMLDivElement> {
11
+ columns: TableColumn<T>[];
12
+ data: T[];
13
+ sortable?: boolean;
14
+ onSort?: (key: string) => void;
15
+ }
16
+
17
+ export function Table<T extends Record<string, unknown> = Record<string, unknown>>({
18
+ columns = [],
19
+ data = [],
20
+ sortable = false,
21
+ onSort,
22
+ className = '',
23
+ ...props
24
+ }: TableProps<T>): React.ReactElement {
25
+ const [sortState, setSortState] = React.useState<Record<string, 'none' | 'ascending' | 'descending'>>(
26
+ Object.fromEntries(columns.map((col) => [col.key, 'none']))
27
+ );
28
+
29
+ const handleSort = (column: TableColumn<T>) => {
30
+ if (!sortable || !column.sortable) return;
31
+
32
+ const next: Record<string, 'none' | 'ascending' | 'descending'> = {};
33
+ columns.forEach((col) => {
34
+ next[col.key] = 'none';
35
+ });
36
+
37
+ const current = sortState[column.key] ?? 'none';
38
+ if (current === 'none') {
39
+ next[column.key] = 'ascending';
40
+ } else if (current === 'ascending') {
41
+ next[column.key] = 'descending';
42
+ } else {
43
+ next[column.key] = 'none';
44
+ }
45
+
46
+ setSortState(next);
47
+ onSort?.(column.key);
48
+ };
49
+
50
+ return (
51
+ <div className={`cn-table-wrapper ${sortable ? 'cn-table-sortable' : ''} ${className}`.trim()} {...props}>
52
+ <table className="cn-table">
53
+ <thead>
54
+ <tr>
55
+ {columns.map((col, idx) => {
56
+ const sortDirection = sortState[col.key] ?? 'none';
57
+ const sortIndicator = sortDirection === 'ascending' ? ' ↑' : sortDirection === 'descending' ? ' ↓' : col.sortable ? ' ↕' : '';
58
+ return (
59
+ <th
60
+ key={idx}
61
+ onClick={() => handleSort(col)}
62
+ data-sort={col.key}
63
+ aria-sort={col.sortable ? sortDirection : undefined}
64
+ >
65
+ {col.header}
66
+ {sortIndicator}
67
+ </th>
68
+ );
69
+ })}
70
+ </tr>
71
+ </thead>
72
+ <tbody>
73
+ {data.map((row, idx) => (
74
+ <tr key={idx}>
75
+ {columns.map((col, colIdx) => (
76
+ <td key={colIdx}>
77
+ {col.render ? col.render(row) : (row as Record<string, unknown>)[col.key] as React.ReactNode}
78
+ </td>
79
+ ))}
80
+ </tr>
81
+ ))}
82
+ </tbody>
83
+ </table>
84
+ </div>
85
+ );
86
+ }
87
+
88
+ Table.displayName = 'Table';
89
+
90
+ export default Table;
@@ -0,0 +1,85 @@
1
+ import * as React from 'react';
2
+
3
+ export interface TabsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
4
+ defaultIndex?: number;
5
+ index?: number;
6
+ onChange?: (index: number) => void;
7
+ }
8
+
9
+ export interface TabProps extends React.HTMLAttributes<HTMLDivElement> {
10
+ children: React.ReactNode;
11
+ }
12
+
13
+ export interface TabPanelProps extends React.HTMLAttributes<HTMLDivElement> {
14
+ children: React.ReactNode;
15
+ }
16
+
17
+ export const Tabs: React.FC<TabsProps> & {
18
+ Tab: React.FC<TabProps>;
19
+ Panel: React.FC<TabPanelProps>;
20
+ } = Object.assign(
21
+ ({ defaultIndex = 0, index: controlledIndex, onChange, children, className = '', ...props }: TabsProps) => {
22
+ const [internalIndex, setInternalIndex] = React.useState(defaultIndex);
23
+ const activeIndex = controlledIndex !== undefined ? controlledIndex : internalIndex;
24
+
25
+ const handleTabClick = (idx: number) => {
26
+ if (onChange) {
27
+ onChange(idx);
28
+ } else {
29
+ setInternalIndex(idx);
30
+ }
31
+ };
32
+
33
+ const tabs: React.ReactElement<TabProps>[] = [];
34
+ const panels: React.ReactElement<TabPanelProps>[] = [];
35
+
36
+ React.Children.forEach(children, (child) => {
37
+ if (React.isValidElement(child)) {
38
+ if ((child.type as React.FC).displayName === 'Tab') {
39
+ tabs.push(child as React.ReactElement<TabProps>);
40
+ } else if ((child.type as React.FC).displayName === 'TabPanel') {
41
+ panels.push(child as React.ReactElement<TabPanelProps>);
42
+ }
43
+ }
44
+ });
45
+
46
+ return (
47
+ <div className={className} {...props}>
48
+ <div className="cn-tabs">
49
+ {tabs.map((tab, idx) => (
50
+ <div
51
+ key={idx}
52
+ className={`cn-tab ${activeIndex === idx ? 'cn-tab-active' : ''}`.trim()}
53
+ onClick={() => handleTabClick(idx)}
54
+ role="tab"
55
+ aria-selected={activeIndex === idx}
56
+ tabIndex={0}
57
+ >
58
+ {tab.props.children}
59
+ </div>
60
+ ))}
61
+ </div>
62
+ {panels.map((panel, idx) => (
63
+ <div
64
+ key={idx}
65
+ className={`cn-tab-panel ${activeIndex === idx ? 'cn-tab-panel-active' : ''}`.trim()}
66
+ role="tabpanel"
67
+ hidden={activeIndex !== idx}
68
+ >
69
+ {panel.props.children}
70
+ </div>
71
+ ))}
72
+ </div>
73
+ );
74
+ },
75
+ {
76
+ Tab: ({ children }: TabProps) => <>{children}</>,
77
+ Panel: ({ children }: TabPanelProps) => <>{children}</>,
78
+ }
79
+ );
80
+
81
+ Tabs.displayName = 'Tabs';
82
+ (Tabs.Tab as React.FC<TabProps> & { displayName?: string }).displayName = 'Tab';
83
+ (Tabs.Panel as React.FC<TabPanelProps> & { displayName?: string }).displayName = 'TabPanel';
84
+
85
+ export default Tabs;
@@ -0,0 +1,30 @@
1
+ import * as React from 'react';
2
+
3
+ export interface TagProps extends React.HTMLAttributes<HTMLDivElement> {
4
+ onRemove?: () => void;
5
+ }
6
+
7
+ export const Tag: React.FC<TagProps> = ({
8
+ children,
9
+ onRemove,
10
+ className = '',
11
+ ...props
12
+ }) => {
13
+ return (
14
+ <div className={`cn-tag ${className}`.trim()} {...props}>
15
+ <span>{children}</span>
16
+ {onRemove && (
17
+ <button className="cn-tag-remove" onClick={onRemove} aria-label="Remove">
18
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
19
+ <line x1="18" y1="6" x2="6" y2="18" />
20
+ <line x1="6" y1="6" x2="18" y2="18" />
21
+ </svg>
22
+ </button>
23
+ )}
24
+ </div>
25
+ );
26
+ };
27
+
28
+ Tag.displayName = 'Tag';
29
+
30
+ export default Tag;
@@ -0,0 +1,21 @@
1
+ import * as React from 'react';
2
+
3
+ export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
4
+ error?: boolean;
5
+ }
6
+
7
+ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
8
+ ({ error = false, className = '', ...props }, ref) => {
9
+ return (
10
+ <textarea
11
+ ref={ref}
12
+ className={`cn-input cn-textarea ${error ? 'cn-input-error' : ''} ${className}`.trim()}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+ );
18
+
19
+ Textarea.displayName = 'Textarea';
20
+
21
+ export default Textarea;