@thangnqdev/ui 0.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 (253) hide show
  1. package/README.md +196 -0
  2. package/dist/Label-DWOlgSNW.d.cts +14 -0
  3. package/dist/Label-DWOlgSNW.d.ts +14 -0
  4. package/dist/bento/index.cjs +140 -0
  5. package/dist/bento/index.cjs.map +1 -0
  6. package/dist/bento/index.d.cts +4 -0
  7. package/dist/bento/index.d.ts +4 -0
  8. package/dist/bento/index.js +4 -0
  9. package/dist/bento/index.js.map +1 -0
  10. package/dist/bento/styles.css +46 -0
  11. package/dist/buttons/index.cjs +365 -0
  12. package/dist/buttons/index.cjs.map +1 -0
  13. package/dist/buttons/index.d.cts +4 -0
  14. package/dist/buttons/index.d.ts +4 -0
  15. package/dist/buttons/index.js +5 -0
  16. package/dist/buttons/index.js.map +1 -0
  17. package/dist/buttons/styles.css +6 -0
  18. package/dist/chunk-2OXZK6IP.js +514 -0
  19. package/dist/chunk-2OXZK6IP.js.map +1 -0
  20. package/dist/chunk-4S6L5V7X.js +15 -0
  21. package/dist/chunk-4S6L5V7X.js.map +1 -0
  22. package/dist/chunk-5FZ6ZFII.js +728 -0
  23. package/dist/chunk-5FZ6ZFII.js.map +1 -0
  24. package/dist/chunk-66VWUJEI.js +86 -0
  25. package/dist/chunk-66VWUJEI.js.map +1 -0
  26. package/dist/chunk-A7P3O5Z5.js +163 -0
  27. package/dist/chunk-A7P3O5Z5.js.map +1 -0
  28. package/dist/chunk-ATYGDTMK.js +257 -0
  29. package/dist/chunk-ATYGDTMK.js.map +1 -0
  30. package/dist/chunk-C4SUXMI3.js +138 -0
  31. package/dist/chunk-C4SUXMI3.js.map +1 -0
  32. package/dist/chunk-EU2LPUYP.js +25 -0
  33. package/dist/chunk-EU2LPUYP.js.map +1 -0
  34. package/dist/chunk-F5Q7BTMH.js +1991 -0
  35. package/dist/chunk-F5Q7BTMH.js.map +1 -0
  36. package/dist/chunk-GXI2H5XA.js +46 -0
  37. package/dist/chunk-GXI2H5XA.js.map +1 -0
  38. package/dist/chunk-H3LCNPMC.js +101 -0
  39. package/dist/chunk-H3LCNPMC.js.map +1 -0
  40. package/dist/chunk-IABPUCAQ.js +19 -0
  41. package/dist/chunk-IABPUCAQ.js.map +1 -0
  42. package/dist/chunk-KJRBRTQE.js +101 -0
  43. package/dist/chunk-KJRBRTQE.js.map +1 -0
  44. package/dist/chunk-LBUBXUYQ.js +40 -0
  45. package/dist/chunk-LBUBXUYQ.js.map +1 -0
  46. package/dist/chunk-LWFRUJRU.js +537 -0
  47. package/dist/chunk-LWFRUJRU.js.map +1 -0
  48. package/dist/chunk-NFVONJUK.js +882 -0
  49. package/dist/chunk-NFVONJUK.js.map +1 -0
  50. package/dist/chunk-PF4ZHGVW.js +284 -0
  51. package/dist/chunk-PF4ZHGVW.js.map +1 -0
  52. package/dist/chunk-WAFHC4DQ.js +366 -0
  53. package/dist/chunk-WAFHC4DQ.js.map +1 -0
  54. package/dist/chunk-X76KBH4Y.js +71 -0
  55. package/dist/chunk-X76KBH4Y.js.map +1 -0
  56. package/dist/core/index.cjs +103 -0
  57. package/dist/core/index.cjs.map +1 -0
  58. package/dist/core/index.d.cts +3 -0
  59. package/dist/core/index.d.ts +3 -0
  60. package/dist/core/index.js +4 -0
  61. package/dist/core/index.js.map +1 -0
  62. package/dist/core/styles.css +128 -0
  63. package/dist/data-display/index.cjs +1023 -0
  64. package/dist/data-display/index.cjs.map +1 -0
  65. package/dist/data-display/index.d.cts +4 -0
  66. package/dist/data-display/index.d.ts +4 -0
  67. package/dist/data-display/index.js +6 -0
  68. package/dist/data-display/index.js.map +1 -0
  69. package/dist/data-display/styles.css +4 -0
  70. package/dist/feedback/index.cjs +368 -0
  71. package/dist/feedback/index.cjs.map +1 -0
  72. package/dist/feedback/index.d.cts +4 -0
  73. package/dist/feedback/index.d.ts +4 -0
  74. package/dist/feedback/index.js +4 -0
  75. package/dist/feedback/index.js.map +1 -0
  76. package/dist/feedback/styles.css +4 -0
  77. package/dist/forms/index.cjs +2287 -0
  78. package/dist/forms/index.cjs.map +1 -0
  79. package/dist/forms/index.d.cts +5 -0
  80. package/dist/forms/index.d.ts +5 -0
  81. package/dist/forms/index.js +10 -0
  82. package/dist/forms/index.js.map +1 -0
  83. package/dist/forms/styles.css +6 -0
  84. package/dist/glass/index.cjs +165 -0
  85. package/dist/glass/index.cjs.map +1 -0
  86. package/dist/glass/index.d.cts +4 -0
  87. package/dist/glass/index.d.ts +4 -0
  88. package/dist/glass/index.js +4 -0
  89. package/dist/glass/index.js.map +1 -0
  90. package/dist/glass/styles.css +81 -0
  91. package/dist/index-BTvkbCIj.d.cts +146 -0
  92. package/dist/index-BTvkbCIj.d.ts +146 -0
  93. package/dist/index-BkRqGNG1.d.cts +201 -0
  94. package/dist/index-BkRqGNG1.d.ts +201 -0
  95. package/dist/index-BlPcGeW-.d.cts +45 -0
  96. package/dist/index-BlPcGeW-.d.ts +45 -0
  97. package/dist/index-BzOCqvC6.d.cts +32 -0
  98. package/dist/index-BzOCqvC6.d.ts +32 -0
  99. package/dist/index-CCeWYAvn.d.cts +83 -0
  100. package/dist/index-COQ7uwt9.d.cts +44 -0
  101. package/dist/index-COQ7uwt9.d.ts +44 -0
  102. package/dist/index-CenuF_P2.d.cts +221 -0
  103. package/dist/index-CenuF_P2.d.ts +221 -0
  104. package/dist/index-Cle6QzQl.d.ts +371 -0
  105. package/dist/index-D6fE7w-b.d.cts +90 -0
  106. package/dist/index-D6fE7w-b.d.ts +90 -0
  107. package/dist/index-DR8cfJUG.d.ts +83 -0
  108. package/dist/index-DzcWh1Hs.d.cts +131 -0
  109. package/dist/index-DzcWh1Hs.d.ts +131 -0
  110. package/dist/index-TyK-Ldpy.d.cts +371 -0
  111. package/dist/index-ugj5s6a3.d.cts +95 -0
  112. package/dist/index-ugj5s6a3.d.ts +95 -0
  113. package/dist/index.cjs +6400 -0
  114. package/dist/index.cjs.map +1 -0
  115. package/dist/index.d.cts +16 -0
  116. package/dist/index.d.ts +16 -0
  117. package/dist/index.js +21 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/layout/index.cjs +516 -0
  120. package/dist/layout/index.cjs.map +1 -0
  121. package/dist/layout/index.d.cts +4 -0
  122. package/dist/layout/index.d.ts +4 -0
  123. package/dist/layout/index.js +4 -0
  124. package/dist/layout/index.js.map +1 -0
  125. package/dist/layout/styles.css +8 -0
  126. package/dist/navigation/index.cjs +673 -0
  127. package/dist/navigation/index.cjs.map +1 -0
  128. package/dist/navigation/index.d.cts +2 -0
  129. package/dist/navigation/index.d.ts +2 -0
  130. package/dist/navigation/index.js +8 -0
  131. package/dist/navigation/index.js.map +1 -0
  132. package/dist/navigation/styles.css +4 -0
  133. package/dist/overlays/index.cjs +954 -0
  134. package/dist/overlays/index.cjs.map +1 -0
  135. package/dist/overlays/index.d.cts +4 -0
  136. package/dist/overlays/index.d.ts +4 -0
  137. package/dist/overlays/index.js +9 -0
  138. package/dist/overlays/index.js.map +1 -0
  139. package/dist/overlays/styles.css +8 -0
  140. package/dist/styles.css +333 -0
  141. package/dist/typography/index.cjs +311 -0
  142. package/dist/typography/index.cjs.map +1 -0
  143. package/dist/typography/index.d.cts +5 -0
  144. package/dist/typography/index.d.ts +5 -0
  145. package/dist/typography/index.js +6 -0
  146. package/dist/typography/index.js.map +1 -0
  147. package/dist/typography/styles.css +5 -0
  148. package/llms.txt +113 -0
  149. package/package.json +116 -0
  150. package/src/bento/BentoCard.tsx +140 -0
  151. package/src/bento/BentoGrid.tsx +50 -0
  152. package/src/bento/index.ts +2 -0
  153. package/src/bento/tokens.css +46 -0
  154. package/src/buttons/Button.tsx +104 -0
  155. package/src/buttons/ButtonGroup.tsx +43 -0
  156. package/src/buttons/IconButton.tsx +79 -0
  157. package/src/buttons/SplitButton.tsx +86 -0
  158. package/src/buttons/ToggleButton.tsx +157 -0
  159. package/src/buttons/index.ts +5 -0
  160. package/src/buttons/tokens.css +6 -0
  161. package/src/core/ThemeProvider.tsx +123 -0
  162. package/src/core/index.ts +2 -0
  163. package/src/core/tokens.css +128 -0
  164. package/src/data-display/Accordion.tsx +147 -0
  165. package/src/data-display/Avatar.tsx +134 -0
  166. package/src/data-display/Card.tsx +112 -0
  167. package/src/data-display/Carousel.tsx +130 -0
  168. package/src/data-display/Chip.tsx +74 -0
  169. package/src/data-display/DataTablePro.tsx +356 -0
  170. package/src/data-display/StatCard.tsx +60 -0
  171. package/src/data-display/Table.tsx +74 -0
  172. package/src/data-display/Timeline.tsx +62 -0
  173. package/src/data-display/TreeView.tsx +140 -0
  174. package/src/data-display/index.ts +10 -0
  175. package/src/data-display/tokens.css +4 -0
  176. package/src/feedback/Alert.tsx +80 -0
  177. package/src/feedback/EmptyState.tsx +45 -0
  178. package/src/feedback/ProgressBar.tsx +174 -0
  179. package/src/feedback/Skeleton.tsx +66 -0
  180. package/src/feedback/Spinner.tsx +65 -0
  181. package/src/feedback/index.ts +5 -0
  182. package/src/feedback/tokens.css +4 -0
  183. package/src/forms/Checkbox.tsx +69 -0
  184. package/src/forms/CheckboxGroup.tsx +29 -0
  185. package/src/forms/Combobox.tsx +368 -0
  186. package/src/forms/DatePicker.tsx +480 -0
  187. package/src/forms/FileUpload.tsx +222 -0
  188. package/src/forms/FormField.tsx +115 -0
  189. package/src/forms/Input.tsx +151 -0
  190. package/src/forms/MaskedInput.tsx +97 -0
  191. package/src/forms/NumberInput.tsx +248 -0
  192. package/src/forms/PasswordInput.tsx +35 -0
  193. package/src/forms/PinInput.tsx +125 -0
  194. package/src/forms/Radio.tsx +142 -0
  195. package/src/forms/RangeSlider.tsx +111 -0
  196. package/src/forms/SearchInput.tsx +34 -0
  197. package/src/forms/Select.tsx +103 -0
  198. package/src/forms/Slider.tsx +85 -0
  199. package/src/forms/Switch.tsx +95 -0
  200. package/src/forms/Textarea.tsx +114 -0
  201. package/src/forms/index.ts +18 -0
  202. package/src/forms/tokens.css +6 -0
  203. package/src/glass/GlassBadge.tsx +61 -0
  204. package/src/glass/GlassButton.tsx +59 -0
  205. package/src/glass/GlassCard.tsx +71 -0
  206. package/src/glass/index.ts +3 -0
  207. package/src/glass/tokens.css +81 -0
  208. package/src/index.ts +26 -0
  209. package/src/layout/AspectRatio.tsx +39 -0
  210. package/src/layout/Container.tsx +53 -0
  211. package/src/layout/Divider.tsx +61 -0
  212. package/src/layout/Flex.tsx +79 -0
  213. package/src/layout/Grid.tsx +124 -0
  214. package/src/layout/PageLayout.tsx +59 -0
  215. package/src/layout/ScrollArea.tsx +43 -0
  216. package/src/layout/Section.tsx +47 -0
  217. package/src/layout/Spacer.tsx +30 -0
  218. package/src/layout/Stack.tsx +88 -0
  219. package/src/layout/index.ts +10 -0
  220. package/src/layout/tokens.css +8 -0
  221. package/src/navigation/Breadcrumb.tsx +73 -0
  222. package/src/navigation/CommandPalette.tsx +142 -0
  223. package/src/navigation/DropdownMenu.tsx +160 -0
  224. package/src/navigation/Pagination.tsx +114 -0
  225. package/src/navigation/Stepper.tsx +93 -0
  226. package/src/navigation/Tabs.tsx +133 -0
  227. package/src/navigation/index.ts +6 -0
  228. package/src/navigation/tokens.css +4 -0
  229. package/src/overlays/AlertDialog.tsx +54 -0
  230. package/src/overlays/BottomSheet.tsx +123 -0
  231. package/src/overlays/ContextMenu.tsx +119 -0
  232. package/src/overlays/Drawer.tsx +145 -0
  233. package/src/overlays/FocusTrap.tsx +49 -0
  234. package/src/overlays/HoverCard.tsx +72 -0
  235. package/src/overlays/Modal.tsx +219 -0
  236. package/src/overlays/Popover.tsx +126 -0
  237. package/src/overlays/Portal.tsx +23 -0
  238. package/src/overlays/Toast.tsx +137 -0
  239. package/src/overlays/Tooltip.tsx +100 -0
  240. package/src/overlays/index.ts +11 -0
  241. package/src/overlays/tokens.css +8 -0
  242. package/src/styles/tokens.css +2 -0
  243. package/src/typography/Blockquote.tsx +32 -0
  244. package/src/typography/Code.tsx +89 -0
  245. package/src/typography/Heading.tsx +56 -0
  246. package/src/typography/Kbd.tsx +25 -0
  247. package/src/typography/Label.tsx +47 -0
  248. package/src/typography/Link.tsx +56 -0
  249. package/src/typography/Text.tsx +62 -0
  250. package/src/typography/index.ts +7 -0
  251. package/src/typography/tokens.css +5 -0
  252. package/src/utils/cn.ts +6 -0
  253. package/ui-contract.json +62 -0
@@ -0,0 +1,356 @@
1
+ import React, { useState, useMemo, forwardRef } from 'react';
2
+ import {
3
+ ChevronUp,
4
+ ChevronDown,
5
+ ChevronsUpDown,
6
+ Search,
7
+ Download,
8
+ Trash2,
9
+ ChevronLeft,
10
+ ChevronRight,
11
+ Filter,
12
+ } from 'lucide-react';
13
+ import { cn } from '../utils/cn';
14
+ import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from './Table';
15
+ import { Button } from '../buttons/Button';
16
+ import { Input } from '../forms/Input';
17
+ import { Checkbox } from '../forms/Checkbox';
18
+ import { Chip } from './Chip';
19
+
20
+ export interface ColumnDef<T> {
21
+ key: string;
22
+ header: string | React.ReactNode;
23
+ accessor?: (row: T) => any;
24
+ render?: (row: T, index: number) => React.ReactNode;
25
+ sortable?: boolean;
26
+ width?: string;
27
+ }
28
+
29
+ export interface DataTableProProps<T> {
30
+ data: T[];
31
+ columns: ColumnDef<T>[];
32
+ keyExtractor: (row: T) => string;
33
+ searchable?: boolean;
34
+ searchPlaceholder?: string;
35
+ selectable?: boolean;
36
+ onSelectionChange?: (selectedKeys: string[]) => void;
37
+ pageSize?: number;
38
+ pageSizeOptions?: number[];
39
+ title?: string;
40
+ description?: string;
41
+ actions?: React.ReactNode;
42
+ className?: string;
43
+ }
44
+
45
+ export function DataTablePro<T extends Record<string, any>>({
46
+ data,
47
+ columns,
48
+ keyExtractor,
49
+ searchable = true,
50
+ searchPlaceholder = 'Search all records...',
51
+ selectable = true,
52
+ onSelectionChange,
53
+ pageSize: initialPageSize = 10,
54
+ pageSizeOptions = [5, 10, 25, 50],
55
+ title,
56
+ description,
57
+ actions,
58
+ className,
59
+ }: DataTableProProps<T>) {
60
+ const [search, setSearch] = useState('');
61
+ const [sortKey, setSortKey] = useState<string | null>(null);
62
+ const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
63
+ const [currentPage, setCurrentPage] = useState(1);
64
+ const [pageSize, setPageSize] = useState(initialPageSize);
65
+ const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
66
+
67
+ // 1. Search filter
68
+ const filteredData = useMemo(() => {
69
+ if (!search.trim()) return data;
70
+ const q = search.toLowerCase();
71
+ return data.filter((row) => {
72
+ return Object.values(row).some((val) => {
73
+ if (val === null || val === undefined) return false;
74
+ return String(val).toLowerCase().includes(q);
75
+ });
76
+ });
77
+ }, [data, search]);
78
+
79
+ // 2. Sorting
80
+ const sortedData = useMemo(() => {
81
+ if (!sortKey) return filteredData;
82
+ const col = columns.find((c) => c.key === sortKey);
83
+
84
+ return [...filteredData].sort((a, b) => {
85
+ const valA = col?.accessor ? col.accessor(a) : a[sortKey];
86
+ const valB = col?.accessor ? col.accessor(b) : b[sortKey];
87
+
88
+ if (valA === valB) return 0;
89
+ if (valA === null || valA === undefined) return 1;
90
+ if (valB === null || valB === undefined) return -1;
91
+
92
+ const comp = valA < valB ? -1 : 1;
93
+ return sortDirection === 'asc' ? comp : -comp;
94
+ });
95
+ }, [filteredData, sortKey, sortDirection, columns]);
96
+
97
+ // 3. Pagination
98
+ const totalPages = Math.max(1, Math.ceil(sortedData.length / pageSize));
99
+ const paginatedData = useMemo(() => {
100
+ const start = (currentPage - 1) * pageSize;
101
+ return sortedData.slice(start, start + pageSize);
102
+ }, [sortedData, currentPage, pageSize]);
103
+
104
+ const handleSort = (key: string) => {
105
+ if (sortKey === key) {
106
+ if (sortDirection === 'asc') {
107
+ setSortDirection('desc');
108
+ } else {
109
+ setSortKey(null);
110
+ setSortDirection('asc');
111
+ }
112
+ } else {
113
+ setSortKey(key);
114
+ setSortDirection('asc');
115
+ }
116
+ };
117
+
118
+ const toggleSelectAll = () => {
119
+ if (selectedKeys.length === paginatedData.length) {
120
+ setSelectedKeys([]);
121
+ onSelectionChange?.([]);
122
+ } else {
123
+ const allKeys = paginatedData.map(keyExtractor);
124
+ setSelectedKeys(allKeys);
125
+ onSelectionChange?.(allKeys);
126
+ }
127
+ };
128
+
129
+ const toggleSelectRow = (key: string) => {
130
+ const next = selectedKeys.includes(key)
131
+ ? selectedKeys.filter((k) => k !== key)
132
+ : [...selectedKeys, key];
133
+ setSelectedKeys(next);
134
+ onSelectionChange?.(next);
135
+ };
136
+
137
+ const exportCSV = () => {
138
+ const headers = columns.map((c) => (typeof c.header === 'string' ? c.header : c.key));
139
+ const rows = sortedData.map((row) =>
140
+ columns.map((c) => {
141
+ const val = c.accessor ? c.accessor(row) : row[c.key];
142
+ return `"${String(val ?? '').replace(/"/g, '""')}"`;
143
+ }).join(',')
144
+ );
145
+ const csvContent = 'data:text/csv;charset=utf-8,' + [headers.join(','), ...rows].join('\n');
146
+ const encodedUri = encodeURI(csvContent);
147
+ const link = document.createElement('a');
148
+ link.setAttribute('href', encodedUri);
149
+ link.setAttribute('download', `export_${Date.now()}.csv`);
150
+ document.body.appendChild(link);
151
+ link.click();
152
+ document.body.removeChild(link);
153
+ };
154
+
155
+ const isAllSelected =
156
+ paginatedData.length > 0 && selectedKeys.length === paginatedData.length;
157
+
158
+ return (
159
+ <div className={cn('w-full space-y-4', className)}>
160
+ {/* Header Toolbar */}
161
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
162
+ {(title || description) && (
163
+ <div>
164
+ {title && <h3 className="text-base font-bold tracking-tight text-[var(--ui-text-primary)]">{title}</h3>}
165
+ {description && <p className="text-xs text-[var(--ui-text-muted)]">{description}</p>}
166
+ </div>
167
+ )}
168
+
169
+ <div className="flex flex-wrap items-center gap-2.5">
170
+ {searchable && (
171
+ <div className="relative w-full sm:w-64">
172
+ <Search className="absolute left-3 w-3.5 h-3.5 text-[var(--ui-text-muted)] pointer-events-none" />
173
+ <input
174
+ type="text"
175
+ placeholder={searchPlaceholder}
176
+ value={search}
177
+ onChange={(e) => {
178
+ setSearch(e.target.value);
179
+ setCurrentPage(1);
180
+ }}
181
+ className="w-full h-8 pl-8 pr-3 text-xs rounded-xl bg-[var(--ui-bg-surface)] border border-[var(--ui-border-default)] hover:border-[var(--ui-border-strong)] focus:outline-none focus:ring-2 focus:ring-[var(--ui-primary)]/40 text-[var(--ui-text-primary)] placeholder:text-[var(--ui-text-muted)] transition-all shadow-inner"
182
+ />
183
+ </div>
184
+ )}
185
+
186
+ <Button size="xs" variant="outline" leftIcon={<Download className="w-3 h-3" />} onClick={exportCSV}>
187
+ Export CSV
188
+ </Button>
189
+
190
+ {actions}
191
+ </div>
192
+ </div>
193
+
194
+ {/* Bulk selection pill */}
195
+ {selectedKeys.length > 0 && (
196
+ <div className="px-4 py-2 rounded-xl bg-[var(--ui-primary)]/15 border border-[var(--ui-primary)]/30 flex items-center justify-between animate-in fade-in duration-200">
197
+ <div className="flex items-center gap-2 text-xs font-semibold text-[var(--ui-primary)]">
198
+ <span>Selected {selectedKeys.length} of {sortedData.length} records</span>
199
+ </div>
200
+ <Button
201
+ size="xs"
202
+ variant="ghost"
203
+ className="text-xs text-rose-500 hover:text-rose-600 hover:bg-rose-500/10"
204
+ onClick={() => {
205
+ setSelectedKeys([]);
206
+ onSelectionChange?.([]);
207
+ }}
208
+ >
209
+ Clear Selection
210
+ </Button>
211
+ </div>
212
+ )}
213
+
214
+ {/* Table Container */}
215
+ <div className="relative w-full overflow-x-auto rounded-2xl border border-[var(--ui-border-default)] bg-[var(--ui-bg-surface)] shadow-sm">
216
+ <Table>
217
+ <TableHeader>
218
+ <TableRow>
219
+ {selectable && (
220
+ <TableHead className="w-10 px-4">
221
+ <input
222
+ type="checkbox"
223
+ checked={isAllSelected}
224
+ onChange={toggleSelectAll}
225
+ className="rounded border-[var(--ui-border-default)] text-[var(--ui-primary)] focus:ring-[var(--ui-primary)] cursor-pointer"
226
+ />
227
+ </TableHead>
228
+ )}
229
+
230
+ {columns.map((col) => (
231
+ <TableHead
232
+ key={col.key}
233
+ style={{ width: col.width }}
234
+ onClick={() => col.sortable !== false && handleSort(col.key)}
235
+ className={cn(
236
+ col.sortable !== false && 'cursor-pointer select-none hover:text-[var(--ui-text-primary)] transition-colors'
237
+ )}
238
+ >
239
+ <div className="flex items-center gap-1.5">
240
+ <span>{col.header}</span>
241
+ {col.sortable !== false && (
242
+ <span className="text-[var(--ui-text-muted)]">
243
+ {sortKey === col.key ? (
244
+ sortDirection === 'asc' ? (
245
+ <ChevronUp className="w-3.5 h-3.5 text-[var(--ui-primary)] font-bold" />
246
+ ) : (
247
+ <ChevronDown className="w-3.5 h-3.5 text-[var(--ui-primary)] font-bold" />
248
+ )
249
+ ) : (
250
+ <ChevronsUpDown className="w-3 h-3 opacity-40" />
251
+ )}
252
+ </span>
253
+ )}
254
+ </div>
255
+ </TableHead>
256
+ ))}
257
+ </TableRow>
258
+ </TableHeader>
259
+
260
+ <TableBody>
261
+ {paginatedData.length === 0 ? (
262
+ <TableRow>
263
+ <TableCell colSpan={columns.length + (selectable ? 1 : 0)} className="h-32 text-center text-xs text-[var(--ui-text-muted)]">
264
+ No matching records found.
265
+ </TableCell>
266
+ </TableRow>
267
+ ) : (
268
+ paginatedData.map((row, rowIdx) => {
269
+ const key = keyExtractor(row);
270
+ const isSelected = selectedKeys.includes(key);
271
+
272
+ return (
273
+ <TableRow
274
+ key={key}
275
+ data-state={isSelected ? 'selected' : undefined}
276
+ className={cn(isSelected && 'bg-[var(--ui-primary)]/10')}
277
+ >
278
+ {selectable && (
279
+ <TableCell className="w-10 px-4">
280
+ <input
281
+ type="checkbox"
282
+ checked={isSelected}
283
+ onChange={() => toggleSelectRow(key)}
284
+ className="rounded border-[var(--ui-border-default)] text-[var(--ui-primary)] focus:ring-[var(--ui-primary)] cursor-pointer"
285
+ />
286
+ </TableCell>
287
+ )}
288
+
289
+ {columns.map((col) => {
290
+ const cellContent = col.render
291
+ ? col.render(row, rowIdx)
292
+ : col.accessor
293
+ ? col.accessor(row)
294
+ : row[col.key];
295
+
296
+ return <TableCell key={col.key}>{cellContent}</TableCell>;
297
+ })}
298
+ </TableRow>
299
+ );
300
+ })
301
+ )}
302
+ </TableBody>
303
+ </Table>
304
+ </div>
305
+
306
+ {/* Pagination Footer */}
307
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 px-1 text-xs text-[var(--ui-text-muted)] font-mono">
308
+ <div className="flex items-center gap-2">
309
+ <span>Rows per page:</span>
310
+ <select
311
+ value={pageSize}
312
+ onChange={(e) => {
313
+ setPageSize(Number(e.target.value));
314
+ setCurrentPage(1);
315
+ }}
316
+ className="px-2 py-1 rounded-lg bg-[var(--ui-bg-surface)] border border-[var(--ui-border-default)] text-[var(--ui-text-primary)] font-mono text-xs focus:outline-none"
317
+ >
318
+ {pageSizeOptions.map((opt) => (
319
+ <option key={opt} value={opt}>
320
+ {opt}
321
+ </option>
322
+ ))}
323
+ </select>
324
+ <span className="text-[var(--ui-text-muted)]">
325
+ Showing {Math.min((currentPage - 1) * pageSize + 1, sortedData.length)}–
326
+ {Math.min(currentPage * pageSize, sortedData.length)} of {sortedData.length}
327
+ </span>
328
+ </div>
329
+
330
+ <div className="flex items-center gap-1.5">
331
+ <Button
332
+ size="xs"
333
+ variant="outline"
334
+ disabled={currentPage === 1}
335
+ onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
336
+ >
337
+ <ChevronLeft className="w-3.5 h-3.5" />
338
+ </Button>
339
+
340
+ <span className="px-2 font-bold text-[var(--ui-text-primary)]">
341
+ {currentPage} / {totalPages}
342
+ </span>
343
+
344
+ <Button
345
+ size="xs"
346
+ variant="outline"
347
+ disabled={currentPage === totalPages}
348
+ onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
349
+ >
350
+ <ChevronRight className="w-3.5 h-3.5" />
351
+ </Button>
352
+ </div>
353
+ </div>
354
+ </div>
355
+ );
356
+ }
@@ -0,0 +1,60 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { ArrowUpRight, ArrowDownRight, Minus } from 'lucide-react';
3
+ import { Card } from './Card';
4
+ import { cn } from '../utils/cn';
5
+
6
+ export interface StatCardProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ label: string;
8
+ value: string | number;
9
+ change?: string | number;
10
+ trend?: 'up' | 'down' | 'neutral';
11
+ trendLabel?: string;
12
+ icon?: React.ReactNode;
13
+ }
14
+
15
+ export const StatCard = forwardRef<HTMLDivElement, StatCardProps>(
16
+ ({ className, label, value, change, trend, trendLabel, icon, ...props }, ref) => {
17
+ return (
18
+ <Card ref={ref} className={cn('p-6', className)} {...props}>
19
+ <div className="flex items-center justify-between">
20
+ <span className="text-xs font-semibold uppercase tracking-wider text-[var(--ui-text-muted)]">
21
+ {label}
22
+ </span>
23
+ {icon && (
24
+ <div className="p-2 rounded-[var(--ui-radius-md)] bg-[var(--ui-bg-secondary)] text-[var(--ui-text-secondary)]">
25
+ {icon}
26
+ </div>
27
+ )}
28
+ </div>
29
+
30
+ <div className="mt-3 flex items-baseline gap-2">
31
+ <span className="text-2xl sm:text-3xl font-extrabold tracking-tight text-[var(--ui-text-primary)]">
32
+ {value}
33
+ </span>
34
+
35
+ {change !== undefined && (
36
+ <div
37
+ className={cn(
38
+ 'inline-flex items-center gap-0.5 text-xs font-bold px-1.5 py-0.5 rounded-full',
39
+ trend === 'up' && 'text-[var(--ui-success)] bg-[var(--ui-success-subtle)]',
40
+ trend === 'down' && 'text-[var(--ui-error)] bg-[var(--ui-error-subtle)]',
41
+ trend === 'neutral' && 'text-[var(--ui-text-muted)] bg-[var(--ui-bg-tertiary)]'
42
+ )}
43
+ >
44
+ {trend === 'up' && <ArrowUpRight className="w-3.5 h-3.5" />}
45
+ {trend === 'down' && <ArrowDownRight className="w-3.5 h-3.5" />}
46
+ {trend === 'neutral' && <Minus className="w-3.5 h-3.5" />}
47
+ <span>{change}</span>
48
+ </div>
49
+ )}
50
+ </div>
51
+
52
+ {trendLabel && (
53
+ <p className="mt-2 text-xs text-[var(--ui-text-muted)]">{trendLabel}</p>
54
+ )}
55
+ </Card>
56
+ );
57
+ }
58
+ );
59
+
60
+ StatCard.displayName = 'StatCard';
@@ -0,0 +1,74 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { cn } from '../utils/cn';
3
+
4
+ export const Table = forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
5
+ ({ className, ...props }, ref) => (
6
+ <div className="relative w-full overflow-auto rounded-[var(--ui-radius-lg)] border border-[var(--ui-border-default)] bg-[var(--ui-bg-surface)]">
7
+ <table ref={ref} className={cn('w-full caption-bottom text-sm text-left', className)} {...props} />
8
+ </div>
9
+ )
10
+ );
11
+ Table.displayName = 'Table';
12
+
13
+ export const TableHeader = forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
14
+ ({ className, ...props }, ref) => (
15
+ <thead ref={ref} className={cn('bg-[var(--ui-bg-secondary)] border-b border-[var(--ui-border-default)]', className)} {...props} />
16
+ )
17
+ );
18
+ TableHeader.displayName = 'TableHeader';
19
+
20
+ export const TableBody = forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
21
+ ({ className, ...props }, ref) => (
22
+ <tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
23
+ )
24
+ );
25
+ TableBody.displayName = 'TableBody';
26
+
27
+ export const TableFooter = forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
28
+ ({ className, ...props }, ref) => (
29
+ <tfoot ref={ref} className={cn('bg-[var(--ui-bg-secondary)] font-medium border-t border-[var(--ui-border-default)]', className)} {...props} />
30
+ )
31
+ );
32
+ TableFooter.displayName = 'TableFooter';
33
+
34
+ export const TableRow = forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
35
+ ({ className, ...props }, ref) => (
36
+ <tr
37
+ ref={ref}
38
+ className={cn(
39
+ 'border-b border-[var(--ui-border-subtle)] transition-colors hover:bg-[var(--ui-bg-tertiary)]/50 data-[state=selected]:bg-[var(--ui-bg-tertiary)]',
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ )
45
+ );
46
+ TableRow.displayName = 'TableRow';
47
+
48
+ export const TableHead = forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
49
+ ({ className, ...props }, ref) => (
50
+ <th
51
+ ref={ref}
52
+ className={cn(
53
+ 'h-11 px-4 text-left align-middle font-semibold text-xs text-[var(--ui-text-muted)] uppercase tracking-wider',
54
+ className
55
+ )}
56
+ {...props}
57
+ />
58
+ )
59
+ );
60
+ TableHead.displayName = 'TableHead';
61
+
62
+ export const TableCell = forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
63
+ ({ className, ...props }, ref) => (
64
+ <td ref={ref} className={cn('p-4 align-middle text-sm text-[var(--ui-text-primary)]', className)} {...props} />
65
+ )
66
+ );
67
+ TableCell.displayName = 'TableCell';
68
+
69
+ export const TableCaption = forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
70
+ ({ className, ...props }, ref) => (
71
+ <caption ref={ref} className={cn('mt-4 text-xs text-[var(--ui-text-muted)]', className)} {...props} />
72
+ )
73
+ );
74
+ TableCaption.displayName = 'TableCaption';
@@ -0,0 +1,62 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { cn } from '../utils/cn';
3
+
4
+ export interface TimelineItem {
5
+ id: string | number;
6
+ title: React.ReactNode;
7
+ description?: React.ReactNode;
8
+ timestamp?: string;
9
+ icon?: React.ReactNode;
10
+ status?: 'completed' | 'active' | 'pending';
11
+ }
12
+
13
+ export interface TimelineProps extends React.HTMLAttributes<HTMLDivElement> {
14
+ items: TimelineItem[];
15
+ }
16
+
17
+ export const Timeline = forwardRef<HTMLDivElement, TimelineProps>(
18
+ ({ className, items, ...props }, ref) => {
19
+ return (
20
+ <div ref={ref} className={cn('relative pl-6 space-y-6', className)} {...props}>
21
+ {/* Vertical track line */}
22
+ <div className="absolute left-[11px] top-2 bottom-2 w-0.5 bg-[var(--ui-border-subtle)]" />
23
+
24
+ {items.map((item) => {
25
+ const isCompleted = item.status === 'completed';
26
+ const isActive = item.status === 'active';
27
+
28
+ return (
29
+ <div key={item.id} className="relative flex items-start gap-4">
30
+ {/* Dot */}
31
+ <div
32
+ className={cn(
33
+ 'absolute -left-6 flex items-center justify-center w-6 h-6 rounded-full ring-4 ring-[var(--ui-bg-surface)] text-xs font-bold transition-all',
34
+ isCompleted && 'bg-[var(--ui-primary)] text-[var(--ui-primary-fg)]',
35
+ isActive && 'bg-[var(--ui-bg-surface)] border-2 border-[var(--ui-primary)] text-[var(--ui-primary)]',
36
+ (!item.status || item.status === 'pending') && 'bg-[var(--ui-bg-tertiary)] border border-[var(--ui-border-default)] text-[var(--ui-text-muted)]'
37
+ )}
38
+ >
39
+ {item.icon || <span className="w-1.5 h-1.5 rounded-full bg-current" />}
40
+ </div>
41
+
42
+ {/* Content */}
43
+ <div className="flex-1 space-y-1 pt-0.5">
44
+ <div className="flex items-center justify-between gap-2">
45
+ <h4 className="text-sm font-semibold text-[var(--ui-text-primary)]">{item.title}</h4>
46
+ {item.timestamp && (
47
+ <span className="text-xs text-[var(--ui-text-muted)] font-mono">{item.timestamp}</span>
48
+ )}
49
+ </div>
50
+ {item.description && (
51
+ <p className="text-xs text-[var(--ui-text-secondary)] leading-relaxed">{item.description}</p>
52
+ )}
53
+ </div>
54
+ </div>
55
+ );
56
+ })}
57
+ </div>
58
+ );
59
+ }
60
+ );
61
+
62
+ Timeline.displayName = 'Timeline';
@@ -0,0 +1,140 @@
1
+ import React, { useState, forwardRef } from 'react';
2
+ import { ChevronRight, Folder, FolderOpen, File, FileCode, FileText, Image as ImageIcon } from 'lucide-react';
3
+ import { cn } from '../utils/cn';
4
+
5
+ export interface TreeItem {
6
+ id: string;
7
+ label: string;
8
+ icon?: React.ReactNode;
9
+ badge?: string | number;
10
+ children?: TreeItem[];
11
+ defaultExpanded?: boolean;
12
+ }
13
+
14
+ export interface TreeViewProps {
15
+ data: TreeItem[];
16
+ selectedId?: string;
17
+ onSelect?: (item: TreeItem) => void;
18
+ className?: string;
19
+ }
20
+
21
+ interface TreeNodeProps {
22
+ item: TreeItem;
23
+ level: number;
24
+ selectedId?: string;
25
+ onSelect?: (item: TreeItem) => void;
26
+ }
27
+
28
+ function TreeNode({ item, level, selectedId, onSelect }: TreeNodeProps) {
29
+ const [isExpanded, setIsExpanded] = useState(Boolean(item.defaultExpanded));
30
+ const hasChildren = Boolean(item.children && item.children.length > 0);
31
+ const isSelected = selectedId === item.id;
32
+
33
+ const handleClick = (e: React.MouseEvent) => {
34
+ e.stopPropagation();
35
+ if (hasChildren) {
36
+ setIsExpanded(!isExpanded);
37
+ }
38
+ onSelect?.(item);
39
+ };
40
+
41
+ const getIcon = () => {
42
+ if (item.icon) return item.icon;
43
+ if (hasChildren) {
44
+ return isExpanded ? (
45
+ <FolderOpen className="w-4 h-4 text-amber-500 shrink-0" />
46
+ ) : (
47
+ <Folder className="w-4 h-4 text-amber-500 shrink-0" />
48
+ );
49
+ }
50
+ if (item.label.endsWith('.tsx') || item.label.endsWith('.ts') || item.label.endsWith('.js')) {
51
+ return <FileCode className="w-4 h-4 text-blue-500 shrink-0" />;
52
+ }
53
+ if (item.label.endsWith('.png') || item.label.endsWith('.jpg') || item.label.endsWith('.svg')) {
54
+ return <ImageIcon className="w-4 h-4 text-purple-500 shrink-0" />;
55
+ }
56
+ return <FileText className="w-4 h-4 text-[var(--ui-text-muted)] shrink-0" />;
57
+ };
58
+
59
+ return (
60
+ <div className="select-none text-xs">
61
+ <div
62
+ onClick={handleClick}
63
+ style={{ paddingLeft: `${level * 16 + 8}px` }}
64
+ className={cn(
65
+ 'flex items-center justify-between py-1.5 pr-2.5 rounded-xl cursor-pointer transition-colors group',
66
+ isSelected
67
+ ? 'bg-[var(--ui-primary)] text-white font-semibold shadow-xs'
68
+ : 'text-[var(--ui-text-primary)] hover:bg-[var(--ui-bg-tertiary)]'
69
+ )}
70
+ >
71
+ <div className="flex items-center gap-2 truncate">
72
+ {hasChildren ? (
73
+ <span className={cn('p-0.5 rounded transition-transform duration-200', isExpanded && 'rotate-90')}>
74
+ <ChevronRight className={cn('w-3 h-3', isSelected ? 'text-white' : 'text-[var(--ui-text-muted)]')} />
75
+ </span>
76
+ ) : (
77
+ <span className="w-4" />
78
+ )}
79
+
80
+ {getIcon()}
81
+ <span className="truncate">{item.label}</span>
82
+ </div>
83
+
84
+ {item.badge !== undefined && (
85
+ <span
86
+ className={cn(
87
+ 'px-1.5 py-0.5 rounded-md text-[10px] font-mono',
88
+ isSelected ? 'bg-white/20 text-white' : 'bg-[var(--ui-bg-secondary)] text-[var(--ui-text-muted)]'
89
+ )}
90
+ >
91
+ {item.badge}
92
+ </span>
93
+ )}
94
+ </div>
95
+
96
+ {hasChildren && isExpanded && (
97
+ <div className="relative space-y-0.5 animate-in fade-in duration-150">
98
+ <div
99
+ className="absolute left-[15px] top-0 bottom-0 w-[1px] bg-[var(--ui-border-subtle)]"
100
+ style={{ left: `${level * 16 + 15}px` }}
101
+ />
102
+ {item.children!.map((child) => (
103
+ <TreeNode
104
+ key={child.id}
105
+ item={child}
106
+ level={level + 1}
107
+ selectedId={selectedId}
108
+ onSelect={onSelect}
109
+ />
110
+ ))}
111
+ </div>
112
+ )}
113
+ </div>
114
+ );
115
+ }
116
+
117
+ export const TreeView = forwardRef<HTMLDivElement, TreeViewProps>(
118
+ ({ data, selectedId, onSelect, className }, ref) => {
119
+ return (
120
+ <div
121
+ ref={ref}
122
+ className={cn(
123
+ 'w-full p-2 rounded-2xl bg-[var(--ui-bg-surface)] border border-[var(--ui-border-default)] space-y-0.5 shadow-sm overflow-hidden',
124
+ className
125
+ )}
126
+ >
127
+ {data.map((item) => (
128
+ <TreeNode
129
+ key={item.id}
130
+ item={item}
131
+ level={0}
132
+ selectedId={selectedId}
133
+ onSelect={onSelect}
134
+ />
135
+ ))}
136
+ </div>
137
+ );
138
+ }
139
+ );
140
+ TreeView.displayName = 'TreeView';