cronixui 1.1.2 → 1.1.3

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 (76) hide show
  1. package/README.md +1 -1
  2. package/package.json +71 -71
  3. package/packages/flutter/.qwen/settings.json +7 -0
  4. package/packages/flutter/pubspec.yaml +20 -20
  5. package/packages/go/cronixui/cronixui.go +926 -926
  6. package/packages/python/README.md +142 -0
  7. package/packages/python/cronixui/__init__.py +15 -6
  8. package/packages/python/cronixui/__pycache__/__init__.cpython-314.pyc +0 -0
  9. package/packages/python/cronixui/__pycache__/accordion.cpython-314.pyc +0 -0
  10. package/packages/python/cronixui/__pycache__/alert.cpython-314.pyc +0 -0
  11. package/packages/python/cronixui/__pycache__/avatar.cpython-314.pyc +0 -0
  12. package/packages/python/cronixui/__pycache__/badge.cpython-314.pyc +0 -0
  13. package/packages/python/cronixui/__pycache__/button.cpython-314.pyc +0 -0
  14. package/packages/python/cronixui/__pycache__/card.cpython-314.pyc +0 -0
  15. package/packages/python/cronixui/__pycache__/command_palette.cpython-314.pyc +0 -0
  16. package/packages/python/cronixui/__pycache__/core.cpython-314.pyc +0 -0
  17. package/packages/python/cronixui/__pycache__/dropdown.cpython-314.pyc +0 -0
  18. package/packages/python/cronixui/__pycache__/form.cpython-314.pyc +0 -0
  19. package/packages/python/cronixui/__pycache__/layout.cpython-314.pyc +0 -0
  20. package/packages/python/cronixui/__pycache__/list.cpython-314.pyc +0 -0
  21. package/packages/python/cronixui/__pycache__/loading.cpython-314.pyc +0 -0
  22. package/packages/python/cronixui/__pycache__/modal.cpython-314.pyc +0 -0
  23. package/packages/python/cronixui/__pycache__/nav.cpython-314.pyc +0 -0
  24. package/packages/python/cronixui/__pycache__/pagination.cpython-314.pyc +0 -0
  25. package/packages/python/cronixui/__pycache__/progress.cpython-314.pyc +0 -0
  26. package/packages/python/cronixui/__pycache__/search.cpython-314.pyc +0 -0
  27. package/packages/python/cronixui/__pycache__/table.cpython-314.pyc +0 -0
  28. package/packages/python/cronixui/__pycache__/tabs.cpython-314.pyc +0 -0
  29. package/packages/python/cronixui/__pycache__/toast.cpython-314.pyc +0 -0
  30. package/packages/python/cronixui/__pycache__/toggle.cpython-314.pyc +0 -0
  31. package/packages/python/cronixui/__pycache__/tokens.cpython-314.pyc +0 -0
  32. package/packages/python/cronixui/__pycache__/tooltip.cpython-314.pyc +0 -0
  33. package/packages/python/cronixui/alert.py +119 -36
  34. package/packages/python/cronixui/avatar.py +129 -22
  35. package/packages/python/cronixui/badge.py +161 -24
  36. package/packages/python/cronixui/button.py +96 -27
  37. package/packages/python/cronixui/card.py +206 -33
  38. package/packages/python/cronixui/core.py +212 -23
  39. package/packages/python/cronixui/form.py +552 -141
  40. package/packages/python/cronixui/layout.py +358 -96
  41. package/packages/python/cronixui/list.py +140 -37
  42. package/packages/python/cronixui/loading.py +107 -17
  43. package/packages/python/cronixui/progress.py +189 -47
  44. package/packages/python/cronixui/table.py +118 -31
  45. package/packages/python/cronixui/tooltip.py +117 -15
  46. package/packages/react/src/components/Accordion.tsx +82 -82
  47. package/packages/react/src/components/Button.tsx +47 -47
  48. package/packages/react/src/components/Card.tsx +69 -69
  49. package/packages/react/src/components/CommandPalette.tsx +131 -131
  50. package/packages/react/src/components/Dropdown.tsx +88 -88
  51. package/packages/react/src/components/FileInput.tsx +86 -86
  52. package/packages/react/src/components/FormGroup.tsx +36 -36
  53. package/packages/react/src/components/List.tsx +55 -55
  54. package/packages/react/src/components/Pagination.tsx +107 -107
  55. package/packages/react/src/components/Progress.tsx +49 -49
  56. package/packages/react/src/components/Search.tsx +95 -95
  57. package/packages/react/src/components/Sidebar.tsx +64 -64
  58. package/packages/react/src/components/Stack.tsx +69 -69
  59. package/packages/react/src/components/Table.tsx +90 -90
  60. package/packages/react/src/components/Toast.tsx +134 -134
  61. package/packages/react/src/components/Typography.tsx +66 -66
  62. package/packages/react/src/index.ts +40 -40
  63. package/packages/react/src/styles.css +2039 -2039
  64. package/packages/rust/cronixui/src/components/avatar.rs +85 -85
  65. package/packages/rust/cronixui/src/components/breadcrumb.rs +58 -58
  66. package/packages/rust/cronixui/src/components/card.rs +259 -259
  67. package/packages/rust/cronixui/src/components/command_palette.rs +254 -254
  68. package/packages/rust/cronixui/src/components/dropdown.rs +179 -179
  69. package/packages/rust/cronixui/src/components/file_input.rs +74 -74
  70. package/packages/rust/cronixui/src/components/mod.rs +51 -51
  71. package/packages/rust/cronixui/src/components/search.rs +185 -185
  72. package/packages/rust/cronixui/src/components/skeleton.rs +63 -63
  73. package/packages/rust/cronixui/src/components/table.rs +56 -56
  74. package/packages/rust/cronixui/src/lib.rs +128 -128
  75. package/packages/web/dist/cronixui.css +97 -93
  76. package/packages/web/dist/cronixui.min.css +1 -1
@@ -1,95 +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;
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;
@@ -1,64 +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;
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;
@@ -1,69 +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;
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;
@@ -1,90 +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;
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;