@apptimate/ui 1.0.0 → 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 (29) hide show
  1. package/CHANGELOG.md +7 -7
  2. package/package.json +21 -18
  3. package/src/{Button.tsx → base-components/Button.tsx} +21 -13
  4. package/src/{ChipInput.tsx → base-components/ChipInput.tsx} +1 -1
  5. package/src/base-components/DesktopFilterPopover.tsx +53 -0
  6. package/src/base-components/DesktopSearchPopover.tsx +51 -0
  7. package/src/{Dropdown.tsx → base-components/Dropdown.tsx} +11 -5
  8. package/src/{Input.tsx → base-components/Input.tsx} +8 -7
  9. package/src/base-components/Label.tsx +24 -0
  10. package/src/{Modal.tsx → base-components/Modal.tsx} +21 -7
  11. package/src/base-components/RecordCount.tsx +19 -0
  12. package/src/{SearchableSelect.tsx → base-components/SearchableSelect.tsx} +9 -9
  13. package/src/{Select.tsx → base-components/Select.tsx} +7 -6
  14. package/src/base-components/Table.tsx +83 -0
  15. package/src/base-components/TableMobileOptions.tsx +51 -0
  16. package/src/common-components/Charts.tsx +222 -0
  17. package/src/common-components/DashboardLayout.tsx +462 -0
  18. package/src/index.tsx +25 -18
  19. package/src/Table.tsx +0 -78
  20. /package/src/{Accordion.tsx → base-components/Accordion.tsx} +0 -0
  21. /package/src/{Badge.tsx → base-components/Badge.tsx} +0 -0
  22. /package/src/{ButtonGroup.tsx → base-components/ButtonGroup.tsx} +0 -0
  23. /package/src/{Card.tsx → base-components/Card.tsx} +0 -0
  24. /package/src/{Checkbox.tsx → base-components/Checkbox.tsx} +0 -0
  25. /package/src/{Divider.tsx → base-components/Divider.tsx} +0 -0
  26. /package/src/{EmptyState.tsx → base-components/EmptyState.tsx} +0 -0
  27. /package/src/{HintIcon.tsx → base-components/HintIcon.tsx} +0 -0
  28. /package/src/{Pagination.tsx → base-components/Pagination.tsx} +0 -0
  29. /package/src/{PopConfirm.tsx → base-components/PopConfirm.tsx} +0 -0
@@ -0,0 +1,462 @@
1
+ "use client";
2
+
3
+ import React, { useState } from 'react';
4
+ import { Briefcase, CreditCard, Settings, Menu, X, LogOut, User as UserIcon, Building2, ChevronRight, Check } from 'lucide-react';
5
+ import { Dropdown, DropdownItem } from '../base-components/Dropdown';
6
+
7
+ import Link from 'next/link';
8
+ import { usePathname } from 'next/navigation';
9
+ import { cn } from '@apptimate/core-lib';
10
+
11
+ export type DashboardMenuConfig = {
12
+ id: string;
13
+ label: string;
14
+ icon: React.ReactNode;
15
+ groups: {
16
+ id: string;
17
+ label: string;
18
+ items: {
19
+ id: string;
20
+ label: string;
21
+ path: string;
22
+ }[]
23
+ }[]
24
+ };
25
+
26
+ export type UserConfig = {
27
+ name: string;
28
+ avatar?: string;
29
+ role?: string;
30
+ };
31
+
32
+ export type OrganizationConfig = {
33
+ id: number;
34
+ name: string;
35
+ code?: string;
36
+ organization_type?: string;
37
+ };
38
+
39
+ export function DashboardLayout({
40
+ children,
41
+ logo,
42
+ menus = [],
43
+ basePath = "",
44
+ externalPaths = [],
45
+ user,
46
+ onLogout,
47
+ onProfile,
48
+ organizations = [],
49
+ selectedOrganization,
50
+ onOrganizationChange
51
+ }: {
52
+ children: React.ReactNode;
53
+ logo?: React.ReactNode;
54
+ menus?: DashboardMenuConfig[];
55
+ basePath?: string;
56
+ externalPaths?: string[];
57
+ user?: UserConfig;
58
+ onLogout?: () => void;
59
+ onProfile?: () => void;
60
+ organizations?: OrganizationConfig[];
61
+ selectedOrganization?: OrganizationConfig | null;
62
+ onOrganizationChange?: (org: OrganizationConfig) => void;
63
+ }) {
64
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
65
+ const [isOrgModalOpen, setIsOrgModalOpen] = useState(false);
66
+ const pathname = usePathname() || "";
67
+
68
+ // Find which main menu should be active based on current path, or default to first
69
+ const fullPath = basePath + (pathname === "/" && basePath ? "" : pathname);
70
+
71
+ const currentMainMenu = menus.find(menu =>
72
+ menu.groups.some(group => group.items.some(item => fullPath.startsWith(item.path)))
73
+ ) || menus[0];
74
+
75
+ const [activeMenuId, setActiveMenuId] = useState(currentMainMenu?.id || "");
76
+
77
+ // Keep it updated if route changes and matches another main menu
78
+ React.useEffect(() => {
79
+ if (currentMainMenu) {
80
+ setActiveMenuId(currentMainMenu.id);
81
+ }
82
+ }, [currentMainMenu?.id]);
83
+
84
+ const activeMenu = menus.find(m => m.id === activeMenuId) || menus[0];
85
+
86
+ const handleSelectOrg = (org: OrganizationConfig) => {
87
+ onOrganizationChange?.(org);
88
+ setIsOrgModalOpen(false);
89
+ };
90
+
91
+ // Truncated org name for display
92
+ const orgDisplayName = selectedOrganization?.name
93
+ ? (selectedOrganization.name.length > 22 ? selectedOrganization.name.substring(0, 20) + '…' : selectedOrganization.name)
94
+ : 'Select Org';
95
+
96
+ return (
97
+ <div className="flex h-screen bg-[#F4F5F7] overflow-hidden font-sans">
98
+ {/* Desktop Global Sidebar */}
99
+ <aside className="hidden md:flex w-[80px] bg-white border-r border-gray-200 flex-col items-center py-6 shrink-0 z-20">
100
+ <div className="w-12 h-12 flex items-center justify-center mb-8">
101
+ {logo}
102
+ </div>
103
+
104
+ <nav className="flex flex-col gap-6 flex-1 w-full">
105
+ {menus.map((menu) => {
106
+ const isActive = activeMenuId === menu.id;
107
+ return (
108
+ <div
109
+ key={menu.id}
110
+ onClick={() => setActiveMenuId(menu.id)}
111
+ className={`flex flex-col items-center justify-center gap-1.5 cursor-pointer transition-colors ${isActive ? "text-[#2D3142]" : "text-gray-400 hover:text-[#2D3142]"
112
+ }`}
113
+ >
114
+ {/* Clone the icon to dynamically apply styling based on active state */}
115
+ {React.cloneElement(menu.icon as React.ReactElement, {
116
+ size: 22,
117
+ className: isActive ? "stroke-[2.5]" : "stroke-[2]"
118
+ })}
119
+ <span className={`text-[11px] ${isActive ? "font-bold" : "font-medium"}`}>{menu.label}</span>
120
+ </div>
121
+ );
122
+ })}
123
+ </nav>
124
+
125
+ <div className="mt-auto flex flex-col items-center">
126
+ <Dropdown
127
+ align="left"
128
+ valign="top"
129
+ className="w-full flex justify-center"
130
+ trigger={
131
+ <div className="w-10 h-10 rounded-full bg-[#2D3142] flex items-center justify-center text-white text-[10px] font-bold shadow-md cursor-pointer hover:bg-[#3d4258] transition-colors overflow-hidden border-2 border-white">
132
+ {user?.avatar ? (
133
+ <img src={user.avatar} alt={user.name} className="w-full h-full object-cover" />
134
+ ) : (
135
+ user?.name ? user.name.substring(0, 2).toUpperCase() : "User"
136
+ )}
137
+ </div>
138
+ }
139
+ >
140
+ <div className="px-3 py-2 border-b border-gray-50 mb-1">
141
+ <p className="text-[13px] font-bold text-[#2D3142] truncate">{user?.name || "Guest"}</p>
142
+ <p className="text-[11px] text-gray-400 font-medium truncate">{user?.role || "Member"}</p>
143
+ </div>
144
+
145
+ <div className="my-1 h-px bg-gray-50" />
146
+
147
+ <DropdownItem onClick={onLogout} className="text-danger-600 hover:bg-danger-50 hover:text-danger-700">
148
+ <LogOut size={16} />
149
+ <span>Log out</span>
150
+ </DropdownItem>
151
+ </Dropdown>
152
+ </div>
153
+ </aside>
154
+
155
+ {/* Desktop Secondary Sidebar */}
156
+ {activeMenu && activeMenu.groups.length > 0 && (
157
+ <aside className="hidden md:flex w-[240px] bg-white border-gray-200 flex-col py-8 shrink-0 z-10">
158
+ <h2 className="px-6 text-xl font-bold text-[#2D3142]/80 mb-6">{activeMenu.label}</h2>
159
+ <div className="flex-1 overflow-y-auto px-4 space-y-6">
160
+ {activeMenu.groups.map((group) => (
161
+ <div key={group.id}>
162
+ <h3 className="px-2 text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">{group.label}</h3>
163
+ <nav className="flex flex-col gap-1">
164
+ {group.items.map((item) => {
165
+ const isItemActive = fullPath === item.path || fullPath.startsWith(`${item.path}/`);
166
+ const isInternal = basePath
167
+ ? item.path.startsWith(basePath)
168
+ : !externalPaths.some(ext => item.path.startsWith(ext));
169
+ const href = basePath && isInternal ? item.path.replace(basePath, "") || "/" : item.path;
170
+
171
+ const className = `px-3 py-2 rounded-lg text-sm transition-colors block ${isItemActive
172
+ ? "bg-[#F4F5F7] text-[#2D3142] font-semibold"
173
+ : "text-gray-500 font-medium hover:bg-gray-50"
174
+ }`;
175
+
176
+ return isInternal ? (
177
+ <Link key={item.id} href={href} className={className}>
178
+ {item.label}
179
+ </Link>
180
+ ) : (
181
+ <a key={item.id} href={href} className={className}>
182
+ {item.label}
183
+ </a>
184
+ );
185
+ })}
186
+ </nav>
187
+ </div>
188
+ ))}
189
+ </div>
190
+
191
+ {/* Organization Selector - Bottom of Secondary Sidebar */}
192
+ {organizations.length > 0 && (
193
+ <div className="px-4 pt-4 mt-2 border-t border-gray-100">
194
+ <button
195
+ id="org-selector-desktop"
196
+ onClick={() => setIsOrgModalOpen(true)}
197
+ className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl bg-[#F4F5F7] hover:bg-gray-200/70 transition-all duration-200 group cursor-pointer"
198
+ >
199
+ <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center shrink-0 shadow-sm">
200
+ <Building2 size={14} className="text-white" />
201
+ </div>
202
+ <div className="flex-1 min-w-0 text-left">
203
+ <p className="text-[11px] font-semibold text-gray-400 uppercase tracking-wider leading-none mb-0.5">Organization</p>
204
+ <p className="text-[13px] font-bold text-[#2D3142] truncate leading-tight">{orgDisplayName}</p>
205
+ </div>
206
+ <ChevronRight size={14} className="text-gray-400 group-hover:text-[#2D3142] transition-colors shrink-0" />
207
+ </button>
208
+ </div>
209
+ )}
210
+ </aside>
211
+ )}
212
+
213
+ {/* Main Content Area & Mobile Header */}
214
+ <div className="flex-1 flex flex-col min-w-0 overflow-hidden">
215
+ {/* Mobile Header */}
216
+ <header className="md:hidden flex items-center justify-between p-4 bg-white border-b border-gray-200 shrink-0 z-10">
217
+ <div className="flex items-center justify-center w-8 h-8">
218
+ {logo}
219
+ </div>
220
+
221
+ <div className="flex items-center gap-2">
222
+ {/* Organization Selector - Mobile Header */}
223
+ {organizations.length > 0 && selectedOrganization && (
224
+ <button
225
+ id="org-selector-mobile"
226
+ onClick={() => setIsOrgModalOpen(true)}
227
+ className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-[#F4F5F7] hover:bg-gray-200/70 transition-all cursor-pointer"
228
+ >
229
+ <div className="w-5 h-5 rounded-md bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center shrink-0">
230
+ <Building2 size={10} className="text-white" />
231
+ </div>
232
+ <span className="text-[12px] font-semibold text-[#2D3142] max-w-[100px] truncate">
233
+ {selectedOrganization.code || selectedOrganization.name?.substring(0, 10)}
234
+ </span>
235
+ </button>
236
+ )}
237
+ <button
238
+ className="p-1.5 rounded-md hover:bg-gray-100 text-[#2D3142]"
239
+ onClick={() => setIsMobileMenuOpen(true)}
240
+ >
241
+ <Menu size={24} />
242
+ </button>
243
+ </div>
244
+ </header>
245
+
246
+ {/* Main Content Scrollable Area */}
247
+ <main className="flex-1 p-4 md:p-6 lg:p-8 overflow-y-auto">
248
+ <div className="bg-white rounded-2xl min-h-[calc(100vh-2rem)] md:min-h-[calc(100vh-4rem)] p-4 md:p-8">
249
+ {children}
250
+ </div>
251
+ </main>
252
+ </div>
253
+
254
+ {/* Mobile Drawer Overlay */}
255
+ {isMobileMenuOpen && (
256
+ <div className="fixed inset-0 z-50 flex md:hidden">
257
+ {/* Backdrop */}
258
+ <div
259
+ className="fixed inset-0 bg-[#1a1a2e]/50 backdrop-blur-sm transition-opacity"
260
+ onClick={() => setIsMobileMenuOpen(false)}
261
+ />
262
+
263
+ {/* Drawer Content */}
264
+ <div className="relative flex w-[80%] max-w-[300px] flex-col bg-white h-full shadow-2xl animate-in slide-in-from-left-full duration-300">
265
+ <div className="flex items-center justify-between p-5 border-b border-gray-100">
266
+ <div className="flex items-center gap-3">
267
+ <div className="flex items-center justify-center w-8 h-8">
268
+ {logo}
269
+ </div>
270
+ <span className="font-bold text-[#2D3142] tracking-tight">Apptimate</span>
271
+ </div>
272
+ <button
273
+ className="p-1.5 rounded-md hover:bg-gray-100 text-gray-500"
274
+ onClick={() => setIsMobileMenuOpen(false)}
275
+ >
276
+ <X size={20} />
277
+ </button>
278
+ </div>
279
+
280
+ <div className="flex-1 overflow-y-auto py-6 px-4">
281
+ <nav className="flex flex-col gap-8">
282
+ {menus.map((menu) => (
283
+ <div key={menu.id}>
284
+ <div className="flex items-center gap-3 text-[#2D3142] font-bold mb-3 px-2">
285
+ {React.cloneElement(menu.icon as React.ReactElement, {
286
+ size: 20,
287
+ className: "stroke-[2.5]"
288
+ })}
289
+ <span>{menu.label}</span>
290
+ </div>
291
+
292
+ <div className="flex flex-col pl-4 border-l-2 border-gray-100 ml-4 gap-4">
293
+ {menu.groups.map(group => (
294
+ <div key={group.id} className="flex flex-col gap-1">
295
+ <span className="text-xs font-bold text-gray-400 uppercase tracking-wider mb-1 px-3">
296
+ {group.label}
297
+ </span>
298
+ {group.items.map(item => {
299
+ const isItemActive = fullPath === item.path || fullPath.startsWith(`${item.path}/`);
300
+ const isInternal = basePath
301
+ ? item.path.startsWith(basePath)
302
+ : !externalPaths.some(ext => item.path.startsWith(ext));
303
+ const href = basePath && isInternal ? item.path.replace(basePath, "") || "/" : item.path;
304
+
305
+ const className = `px-3 py-2 rounded-lg text-sm transition-colors block ${isItemActive
306
+ ? "bg-[#F4F5F7] text-[#2D3142] font-semibold"
307
+ : "text-gray-500 font-medium hover:bg-gray-50"
308
+ }`;
309
+
310
+ return isInternal ? (
311
+ <Link
312
+ key={item.id}
313
+ href={href}
314
+ onClick={() => setIsMobileMenuOpen(false)}
315
+ className={className}
316
+ >
317
+ {item.label}
318
+ </Link>
319
+ ) : (
320
+ <a
321
+ key={item.id}
322
+ href={href}
323
+ onClick={() => setIsMobileMenuOpen(false)}
324
+ className={className}
325
+ >
326
+ {item.label}
327
+ </a>
328
+ );
329
+ })}
330
+ </div>
331
+ ))}
332
+ </div>
333
+ </div>
334
+ ))}
335
+ </nav>
336
+ </div>
337
+
338
+ <div className="p-4 border-t border-gray-100 flex items-center gap-3">
339
+ <div className="w-10 h-10 rounded-full bg-[#2D3142] flex items-center justify-center text-white text-[10px] font-bold shrink-0 overflow-hidden">
340
+ {user?.avatar ? (
341
+ <img src={user.avatar} alt={user?.name || 'User'} className="w-full h-full object-cover" />
342
+ ) : (
343
+ user?.name ? user.name.substring(0, 2).toUpperCase() : "User"
344
+ )}
345
+ </div>
346
+ <div className="flex flex-col">
347
+ <span className="text-sm font-semibold text-[#2D3142]">{user?.name || "Guest"}</span>
348
+ <span className="text-xs text-gray-500">{user?.role || "Member"}</span>
349
+ </div>
350
+ </div>
351
+ </div>
352
+ </div>
353
+ )}
354
+
355
+ {/* Organization Selector Modal */}
356
+ {isOrgModalOpen && (
357
+ <div className="fixed inset-0 z-[60] flex items-center justify-center p-4">
358
+ {/* Backdrop */}
359
+ <div
360
+ className="absolute inset-0 bg-[#1a1a2e]/60 backdrop-blur-sm"
361
+ onClick={() => setIsOrgModalOpen(false)}
362
+ />
363
+
364
+ {/* Modal */}
365
+ <div
366
+ className="relative w-full max-w-[440px] bg-white rounded-2xl shadow-[0_20px_60px_rgba(0,0,0,0.2)] overflow-hidden"
367
+ style={{ animation: 'orgModalIn 0.25s cubic-bezier(0.16, 1, 0.3, 1)' }}
368
+ >
369
+ {/* Header */}
370
+ <div className="flex items-center justify-between px-6 py-5 border-b border-gray-100">
371
+ <div className="flex items-center gap-3">
372
+ <div className="w-9 h-9 rounded-xl bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center shadow-sm">
373
+ <Building2 size={16} className="text-white" />
374
+ </div>
375
+ <div>
376
+ <h3 className="text-[16px] font-bold text-[#2D3142] tracking-tight">Switch Organization</h3>
377
+ <p className="text-[12px] text-gray-400 font-medium">Select the organization to work with</p>
378
+ </div>
379
+ </div>
380
+ <button
381
+ onClick={() => setIsOrgModalOpen(false)}
382
+ className="w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:bg-gray-100 hover:text-[#2D3142] transition-all"
383
+ >
384
+ <X size={18} />
385
+ </button>
386
+ </div>
387
+
388
+ {/* Organization List */}
389
+ <div className="px-4 py-3 max-h-[360px] overflow-y-auto">
390
+ <div className="flex flex-col gap-1">
391
+ {organizations.map((org) => {
392
+ const isSelected = selectedOrganization?.id === org.id;
393
+ return (
394
+ <button
395
+ key={org.id}
396
+ id={`org-option-${org.id}`}
397
+ onClick={() => handleSelectOrg(org)}
398
+ className={cn(
399
+ "w-full flex items-center gap-3 px-4 py-3 rounded-xl text-left transition-all duration-150 cursor-pointer",
400
+ isSelected
401
+ ? "bg-indigo-50 border border-indigo-200 shadow-sm"
402
+ : "hover:bg-gray-50 border border-transparent"
403
+ )}
404
+ >
405
+ <div className={cn(
406
+ "w-9 h-9 rounded-lg flex items-center justify-center shrink-0 transition-colors",
407
+ isSelected
408
+ ? "bg-gradient-to-br from-indigo-500 to-purple-600 shadow-sm"
409
+ : "bg-gray-100"
410
+ )}>
411
+ <Building2 size={14} className={isSelected ? "text-white" : "text-gray-500"} />
412
+ </div>
413
+ <div className="flex-1 min-w-0">
414
+ <p className={cn(
415
+ "text-[14px] font-semibold truncate leading-tight",
416
+ isSelected ? "text-indigo-700" : "text-[#2D3142]"
417
+ )}>
418
+ {org.name}
419
+ </p>
420
+ <p className="text-[11px] text-gray-400 font-medium truncate">
421
+ {org.code && <span className="uppercase">{org.code}</span>}
422
+ {org.code && org.organization_type && <span className="mx-1">·</span>}
423
+ {org.organization_type && <span className="capitalize">{org.organization_type}</span>}
424
+ {!org.code && !org.organization_type && <span>ID: {org.id}</span>}
425
+ </p>
426
+ </div>
427
+ {isSelected && (
428
+ <div className="w-6 h-6 rounded-full bg-indigo-500 flex items-center justify-center shrink-0">
429
+ <Check size={12} className="text-white stroke-[3]" />
430
+ </div>
431
+ )}
432
+ </button>
433
+ );
434
+ })}
435
+ </div>
436
+ </div>
437
+
438
+ {/* Footer */}
439
+ <div className="px-6 py-4 border-t border-gray-100 bg-gray-50/50">
440
+ <p className="text-[11px] text-gray-400 font-medium text-center">
441
+ All data and actions will be scoped to the selected organization
442
+ </p>
443
+ </div>
444
+ </div>
445
+
446
+ <style dangerouslySetInnerHTML={{ __html: `
447
+ @keyframes orgModalIn {
448
+ from {
449
+ opacity: 0;
450
+ transform: scale(0.95) translateY(10px);
451
+ }
452
+ to {
453
+ opacity: 1;
454
+ transform: scale(1) translateY(0);
455
+ }
456
+ }
457
+ `}} />
458
+ </div>
459
+ )}
460
+ </div>
461
+ );
462
+ }
package/src/index.tsx CHANGED
@@ -1,20 +1,27 @@
1
1
  "use client";
2
2
 
3
- export * from './Accordion';
4
- export * from './Badge';
5
- export * from './Button';
6
- export * from './ButtonGroup';
7
- export * from './Card';
8
- export * from './Checkbox';
9
- export * from './ChipInput';
10
- export * from './Divider';
11
- export * from './Dropdown';
12
- export * from './EmptyState';
13
- export * from './HintIcon';
14
- export * from './Input';
15
- export * from './Modal';
16
- export * from './Pagination';
17
- export * from './PopConfirm';
18
- export * from './SearchableSelect';
19
- export * from './Select';
20
- export * from './Table';
3
+ export * from './base-components/Accordion';
4
+ export * from './base-components/Badge';
5
+ export * from './base-components/Button';
6
+ export * from './base-components/ButtonGroup';
7
+ export * from './base-components/Card';
8
+ export * from './base-components/Checkbox';
9
+ export * from './base-components/ChipInput';
10
+ export * from './base-components/Divider';
11
+ export * from './base-components/Dropdown';
12
+ export * from './base-components/EmptyState';
13
+ export * from './base-components/HintIcon';
14
+ export * from './base-components/RecordCount';
15
+ export * from './base-components/TableMobileOptions';
16
+ export * from './base-components/DesktopSearchPopover';
17
+ export * from './base-components/DesktopFilterPopover';
18
+ export * from './base-components/Input';
19
+ export * from './base-components/Label';
20
+ export * from './base-components/Modal';
21
+ export * from './base-components/Pagination';
22
+ export * from './base-components/PopConfirm';
23
+ export * from './base-components/SearchableSelect';
24
+ export * from './base-components/Select';
25
+ export * from './base-components/Table';
26
+ export * from './common-components/DashboardLayout';
27
+ export * from './common-components/Charts';
package/src/Table.tsx DELETED
@@ -1,78 +0,0 @@
1
- "use client";
2
-
3
- import React from 'react';
4
- import { cn } from '@apptimate/core-lib';
5
-
6
- export interface TableProps {
7
- children?: React.ReactNode;
8
- className?: string;
9
- }
10
-
11
- export const Table = ({ children, className }: TableProps) => {
12
- return (
13
- <div className={cn("w-full overflow-x-auto", className)}>
14
- <table className="w-full border-separate border-spacing-0 text-left min-w-[600px]">
15
- {children}
16
- </table>
17
- </div>
18
- );
19
- };
20
-
21
- export const THeader = ({ children, className }: TableProps) => (
22
- <thead className={cn(className)}>
23
- {children}
24
- </thead>
25
- );
26
-
27
- export const TBody = ({ children, className }: TableProps) => (
28
- <tbody className={cn(className)}>
29
- {children}
30
- </tbody>
31
- );
32
-
33
- export const TRow = ({ children, className, onClick }: TableProps & { onClick?: () => void }) => (
34
- <tr
35
- className={cn(
36
- "group transition-colors",
37
- onClick && "cursor-pointer hover:bg-surface-hover-table",
38
- className
39
- )}
40
- onClick={onClick}
41
- >
42
- {children}
43
- </tr>
44
- );
45
-
46
- export const TCell = ({ children, className, isHeader = false, colSpan, rowSpan }: TableProps & { isHeader?: boolean; colSpan?: number; rowSpan?: number }) => {
47
- const Tag = isHeader ? 'th' : 'td';
48
- return (
49
- <Tag
50
- colSpan={colSpan}
51
- rowSpan={rowSpan}
52
- className={cn(
53
- "transition-all",
54
- isHeader
55
- ? "bg-surface-0 py-[10px] px-[14px] text-[11px] font-semibold text-foreground-subtle uppercase tracking-wider first:rounded-l-[8px] last:rounded-r-[8px]"
56
- : "py-3 px-2 text-[13px] font-medium text-foreground-1 border-b border-surface-0",
57
- className
58
- )}>
59
- {children}
60
- </Tag>
61
- );
62
- };
63
-
64
- export const TLoader = ({ rows = 8, cols = 4 }: { rows?: number; cols?: number }) => {
65
- return (
66
- <>
67
- {Array.from({ length: rows }).map((_, i) => (
68
- <TRow key={i}>
69
- {Array.from({ length: cols }).map((_, j) => (
70
- <TCell key={j}>
71
- <div className="h-4 bg-surface-0 animate-pulse rounded w-full" />
72
- </TCell>
73
- ))}
74
- </TRow>
75
- ))}
76
- </>
77
- );
78
- };
File without changes
File without changes