@tidecloak/ui-framework 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +377 -0
  2. package/dist/index.d.mts +2739 -0
  3. package/dist/index.d.ts +2739 -0
  4. package/dist/index.js +12869 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/index.mjs +12703 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +54 -0
  9. package/src/components/common/ActionButton.tsx +234 -0
  10. package/src/components/common/EmptyState.tsx +140 -0
  11. package/src/components/common/LoadingSkeleton.tsx +121 -0
  12. package/src/components/common/RefreshButton.tsx +127 -0
  13. package/src/components/common/StatusBadge.tsx +177 -0
  14. package/src/components/common/index.ts +31 -0
  15. package/src/components/data-table/DataTable.tsx +201 -0
  16. package/src/components/data-table/PaginatedTable.tsx +247 -0
  17. package/src/components/data-table/index.ts +2 -0
  18. package/src/components/dialogs/CollapsibleSection.tsx +184 -0
  19. package/src/components/dialogs/ConfirmDialog.tsx +264 -0
  20. package/src/components/dialogs/DetailDialog.tsx +228 -0
  21. package/src/components/dialogs/index.ts +3 -0
  22. package/src/components/index.ts +5 -0
  23. package/src/components/pages/base/ApprovalsPageBase.tsx +680 -0
  24. package/src/components/pages/base/LogsPageBase.tsx +581 -0
  25. package/src/components/pages/base/RolesPageBase.tsx +1470 -0
  26. package/src/components/pages/base/TemplatesPageBase.tsx +761 -0
  27. package/src/components/pages/base/UsersPageBase.tsx +843 -0
  28. package/src/components/pages/base/index.ts +58 -0
  29. package/src/components/pages/connected/ApprovalsPage.tsx +797 -0
  30. package/src/components/pages/connected/LogsPage.tsx +267 -0
  31. package/src/components/pages/connected/RolesPage.tsx +525 -0
  32. package/src/components/pages/connected/TemplatesPage.tsx +181 -0
  33. package/src/components/pages/connected/UsersPage.tsx +237 -0
  34. package/src/components/pages/connected/index.ts +36 -0
  35. package/src/components/pages/index.ts +5 -0
  36. package/src/components/tabs/TabsView.tsx +300 -0
  37. package/src/components/tabs/index.ts +1 -0
  38. package/src/components/ui/index.tsx +1001 -0
  39. package/src/hooks/index.ts +3 -0
  40. package/src/hooks/useAutoRefresh.ts +119 -0
  41. package/src/hooks/usePagination.ts +152 -0
  42. package/src/hooks/useSelection.ts +81 -0
  43. package/src/index.ts +256 -0
  44. package/src/theme.ts +185 -0
  45. package/src/tide/index.ts +19 -0
  46. package/src/tide/tidePolicy.ts +270 -0
  47. package/src/types/index.ts +484 -0
  48. package/src/utils/index.ts +121 -0
@@ -0,0 +1,177 @@
1
+ import React from "react";
2
+ import {
3
+ Clock,
4
+ CheckCircle2,
5
+ XCircle,
6
+ Check,
7
+ Ban,
8
+ AlertCircle,
9
+ Info,
10
+ AlertTriangle,
11
+ } from "lucide-react";
12
+ import type { StatusType, StatusConfig } from "../../types";
13
+
14
+ /**
15
+ * Status color configurations as inline styles
16
+ */
17
+ export const STATUS_STYLES: Record<StatusType, React.CSSProperties> = {
18
+ pending: { backgroundColor: "#fefce8", color: "#a16207", borderColor: "#fef08a" },
19
+ approved: { backgroundColor: "#f0fdf4", color: "#15803d", borderColor: "#bbf7d0" },
20
+ rejected: { backgroundColor: "#fef2f2", color: "#b91c1c", borderColor: "#fecaca" },
21
+ committed: { backgroundColor: "#eff6ff", color: "#1d4ed8", borderColor: "#bfdbfe" },
22
+ cancelled: { backgroundColor: "#f9fafb", color: "#374151", borderColor: "#e5e7eb" },
23
+ mixed: { backgroundColor: "#faf5ff", color: "#7e22ce", borderColor: "#e9d5ff" },
24
+ ready: { backgroundColor: "#f0fdf4", color: "#15803d", borderColor: "#bbf7d0" },
25
+ error: { backgroundColor: "#fef2f2", color: "#b91c1c", borderColor: "#fecaca" },
26
+ warning: { backgroundColor: "#fff7ed", color: "#c2410c", borderColor: "#fed7aa" },
27
+ info: { backgroundColor: "#eff6ff", color: "#1d4ed8", borderColor: "#bfdbfe" },
28
+ success: { backgroundColor: "#f0fdf4", color: "#15803d", borderColor: "#bbf7d0" },
29
+ };
30
+
31
+ // Keep class names for backwards compatibility with custom Badge components
32
+ export const STATUS_COLORS: Record<StatusType, string> = {
33
+ pending: "bg-yellow-50 text-yellow-700 border-yellow-200",
34
+ approved: "bg-green-50 text-green-700 border-green-200",
35
+ rejected: "bg-red-50 text-red-700 border-red-200",
36
+ committed: "bg-blue-50 text-blue-700 border-blue-200",
37
+ cancelled: "bg-gray-50 text-gray-700 border-gray-200",
38
+ mixed: "bg-purple-50 text-purple-700 border-purple-200",
39
+ ready: "bg-green-50 text-green-700 border-green-200",
40
+ error: "bg-red-50 text-red-700 border-red-200",
41
+ warning: "bg-orange-50 text-orange-700 border-orange-200",
42
+ info: "bg-blue-50 text-blue-700 border-blue-200",
43
+ success: "bg-green-50 text-green-700 border-green-200",
44
+ };
45
+
46
+ /**
47
+ * Default status icons
48
+ */
49
+ export const STATUS_ICONS: Record<StatusType, React.ComponentType<{ style?: React.CSSProperties }>> = {
50
+ pending: Clock,
51
+ approved: CheckCircle2,
52
+ rejected: XCircle,
53
+ committed: Check,
54
+ cancelled: Ban,
55
+ mixed: AlertCircle,
56
+ ready: CheckCircle2,
57
+ error: XCircle,
58
+ warning: AlertTriangle,
59
+ info: Info,
60
+ success: CheckCircle2,
61
+ };
62
+
63
+ export interface StatusBadgeProps {
64
+ /** Status type */
65
+ status: StatusType | string;
66
+ /** Custom label (defaults to capitalized status) */
67
+ label?: string;
68
+ /** Show icon */
69
+ showIcon?: boolean;
70
+ /** Custom icon override */
71
+ icon?: React.ReactNode;
72
+ /** Additional class name */
73
+ className?: string;
74
+ /** Custom status configurations for non-standard statuses */
75
+ customStatuses?: Record<string, { colorClass?: string; style?: React.CSSProperties; icon?: React.ComponentType<{ style?: React.CSSProperties }> }>;
76
+ /** Custom badge component */
77
+ BadgeComponent?: React.ComponentType<{
78
+ variant?: string;
79
+ className?: string;
80
+ style?: React.CSSProperties;
81
+ children: React.ReactNode;
82
+ }>;
83
+ }
84
+
85
+ /**
86
+ * StatusBadge - Displays a colored badge based on status
87
+ *
88
+ * @example
89
+ * ```tsx
90
+ * <StatusBadge status="pending" />
91
+ * <StatusBadge status="approved" showIcon />
92
+ * <StatusBadge status="custom" customStatuses={{ custom: { style: { backgroundColor: "#fdf4ff", color: "#a21caf" } } }} />
93
+ * ```
94
+ */
95
+ export function StatusBadge({
96
+ status,
97
+ label,
98
+ showIcon = true,
99
+ icon,
100
+ className,
101
+ customStatuses = {},
102
+ BadgeComponent,
103
+ }: StatusBadgeProps) {
104
+ const normalizedStatus = status.toLowerCase() as StatusType;
105
+
106
+ // Get style from built-in statuses or custom statuses
107
+ const statusStyle =
108
+ STATUS_STYLES[normalizedStatus] ||
109
+ customStatuses[normalizedStatus]?.style ||
110
+ STATUS_STYLES.info;
111
+
112
+ // Get color class for custom Badge components
113
+ const colorClass =
114
+ STATUS_COLORS[normalizedStatus] ||
115
+ customStatuses[normalizedStatus]?.colorClass ||
116
+ STATUS_COLORS.info;
117
+
118
+ // Get icon
119
+ const IconComponent =
120
+ STATUS_ICONS[normalizedStatus] ||
121
+ customStatuses[normalizedStatus]?.icon ||
122
+ Info;
123
+
124
+ const displayLabel = label || status.charAt(0).toUpperCase() + status.slice(1);
125
+
126
+ const iconStyle: React.CSSProperties = { height: "0.75rem", width: "0.75rem", marginRight: "0.25rem" };
127
+
128
+ const badgeContent = (
129
+ <>
130
+ {showIcon && (
131
+ icon || <IconComponent style={iconStyle} />
132
+ )}
133
+ {displayLabel}
134
+ </>
135
+ );
136
+
137
+ // If custom badge component provided, use it
138
+ if (BadgeComponent) {
139
+ return (
140
+ <BadgeComponent variant="outline" className={className} style={statusStyle}>
141
+ {badgeContent}
142
+ </BadgeComponent>
143
+ );
144
+ }
145
+
146
+ // Default badge implementation with inline styles
147
+ const badgeStyle: React.CSSProperties = {
148
+ display: "inline-flex",
149
+ alignItems: "center",
150
+ borderRadius: "9999px",
151
+ border: "1px solid",
152
+ padding: "0.125rem 0.625rem",
153
+ fontSize: "0.75rem",
154
+ fontWeight: 600,
155
+ ...statusStyle,
156
+ };
157
+
158
+ return (
159
+ <span style={badgeStyle} className={className}>
160
+ {badgeContent}
161
+ </span>
162
+ );
163
+ }
164
+
165
+ /**
166
+ * Helper to get status badge configuration
167
+ */
168
+ export function getStatusConfig(status: string): StatusConfig {
169
+ const normalizedStatus = status.toLowerCase() as StatusType;
170
+ const Icon = STATUS_ICONS[normalizedStatus] || Info;
171
+
172
+ return {
173
+ label: status.charAt(0).toUpperCase() + status.slice(1),
174
+ variant: STATUS_COLORS[normalizedStatus] ? normalizedStatus : "info",
175
+ icon: <Icon style={{ height: "0.75rem", width: "0.75rem", marginRight: "0.25rem" }} />,
176
+ };
177
+ }
@@ -0,0 +1,31 @@
1
+ export { RefreshButton, type RefreshButtonProps } from "./RefreshButton";
2
+ export {
3
+ StatusBadge,
4
+ STATUS_COLORS,
5
+ STATUS_ICONS,
6
+ getStatusConfig,
7
+ type StatusBadgeProps,
8
+ } from "./StatusBadge";
9
+ export {
10
+ ActionButton,
11
+ ActionButtonGroup,
12
+ ACTION_COLORS,
13
+ ACTION_ICONS,
14
+ type ActionButtonProps,
15
+ type ActionButtonGroupProps,
16
+ type ActionType,
17
+ } from "./ActionButton";
18
+ export {
19
+ EmptyState,
20
+ EmptyStateNoData,
21
+ EmptyStateNoResults,
22
+ EmptyStateNoFiles,
23
+ type EmptyStateProps,
24
+ } from "./EmptyState";
25
+ export {
26
+ Skeleton,
27
+ LoadingSkeleton,
28
+ TableRowSkeleton,
29
+ type SkeletonProps,
30
+ type LoadingSkeletonProps,
31
+ } from "./LoadingSkeleton";
@@ -0,0 +1,201 @@
1
+ import React from "react";
2
+ import { LoadingSkeleton } from "../common/LoadingSkeleton";
3
+ import { EmptyState } from "../common/EmptyState";
4
+ import type { BaseDataItem, ColumnDef, EmptyStateConfig } from "../../types";
5
+
6
+ export interface DataTableProps<T extends BaseDataItem> {
7
+ /** Data items to display */
8
+ data: T[];
9
+ /** Column definitions */
10
+ columns: ColumnDef<T>[];
11
+ /** Loading state */
12
+ isLoading?: boolean;
13
+ /** Row key getter (defaults to item.id) */
14
+ getRowKey?: (item: T) => string;
15
+ /** Row click handler */
16
+ onRowClick?: (item: T) => void;
17
+ /** Selected row IDs for highlighting */
18
+ selectedIds?: string[];
19
+ /** Empty state configuration */
20
+ emptyState?: EmptyStateConfig;
21
+ /** Additional class name */
22
+ className?: string;
23
+ /** Custom table components */
24
+ components?: {
25
+ Table?: React.ComponentType<{ children: React.ReactNode; className?: string; style?: React.CSSProperties }>;
26
+ TableHeader?: React.ComponentType<{ children: React.ReactNode }>;
27
+ TableBody?: React.ComponentType<{ children: React.ReactNode }>;
28
+ TableRow?: React.ComponentType<{ children: React.ReactNode; className?: string; style?: React.CSSProperties; onClick?: () => void }>;
29
+ TableHead?: React.ComponentType<{ children: React.ReactNode; className?: string; style?: React.CSSProperties }>;
30
+ TableCell?: React.ComponentType<{ children: React.ReactNode; className?: string; style?: React.CSSProperties }>;
31
+ Skeleton?: React.ComponentType<{ className?: string; style?: React.CSSProperties }>;
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Alignment style mapping
37
+ */
38
+ const ALIGN_STYLES: Record<string, React.CSSProperties> = {
39
+ left: { textAlign: "left" },
40
+ center: { textAlign: "center" },
41
+ right: { textAlign: "right" },
42
+ };
43
+
44
+ /**
45
+ * DataTable - Generic data table component
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * <DataTable
50
+ * data={items}
51
+ * columns={[
52
+ * { key: 'name', header: 'Name', cell: (item) => item.name },
53
+ * { key: 'status', header: 'Status', cell: (item) => <StatusBadge status={item.status} /> },
54
+ * { key: 'actions', header: 'Actions', cell: (item) => <ActionButton action="view" onClick={() => view(item)} /> },
55
+ * ]}
56
+ * isLoading={isLoading}
57
+ * emptyState={{ title: 'No items', description: 'Create an item to get started.' }}
58
+ * />
59
+ * ```
60
+ */
61
+ export function DataTable<T extends BaseDataItem>({
62
+ data,
63
+ columns,
64
+ isLoading = false,
65
+ getRowKey = (item) => item.id,
66
+ onRowClick,
67
+ selectedIds = [],
68
+ emptyState = { title: "No data found" },
69
+ className,
70
+ components = {},
71
+ }: DataTableProps<T>) {
72
+ // Default components (can be overridden with shadcn/ui components)
73
+ const Table = components.Table || DefaultTable;
74
+ const TableHeader = components.TableHeader || DefaultTableHeader;
75
+ const TableBody = components.TableBody || DefaultTableBody;
76
+ const TableRow = components.TableRow || DefaultTableRow;
77
+ const TableHead = components.TableHead || DefaultTableHead;
78
+ const TableCell = components.TableCell || DefaultTableCell;
79
+ const Skeleton = components.Skeleton;
80
+
81
+ // Show loading skeleton
82
+ if (isLoading && data.length === 0) {
83
+ return <LoadingSkeleton rows={5} type="table" SkeletonComponent={Skeleton} />;
84
+ }
85
+
86
+ // Show empty state
87
+ if (!data || data.length === 0) {
88
+ return <EmptyState {...emptyState} />;
89
+ }
90
+
91
+ return (
92
+ <div style={{ overflowX: "auto" }} className={className}>
93
+ <Table>
94
+ <TableHeader>
95
+ <TableRow>
96
+ {columns.map((column) => (
97
+ <TableHead
98
+ key={column.key}
99
+ style={column.align ? ALIGN_STYLES[column.align] : undefined}
100
+ >
101
+ {column.header}
102
+ </TableHead>
103
+ ))}
104
+ </TableRow>
105
+ </TableHeader>
106
+ <TableBody>
107
+ {data.map((item, index) => {
108
+ const rowKey = getRowKey(item);
109
+ const isSelected = selectedIds.includes(rowKey);
110
+
111
+ return (
112
+ <TableRow
113
+ key={rowKey}
114
+ onClick={onRowClick ? () => onRowClick(item) : undefined}
115
+ style={{
116
+ cursor: onRowClick ? "pointer" : undefined,
117
+ backgroundColor: isSelected ? "rgba(59, 130, 246, 0.05)" : undefined,
118
+ }}
119
+ >
120
+ {columns.map((column) => (
121
+ <TableCell
122
+ key={column.key}
123
+ style={column.align ? ALIGN_STYLES[column.align] : undefined}
124
+ >
125
+ {column.cell(item, index)}
126
+ </TableCell>
127
+ ))}
128
+ </TableRow>
129
+ );
130
+ })}
131
+ </TableBody>
132
+ </Table>
133
+ </div>
134
+ );
135
+ }
136
+
137
+ // Default table component implementations with inline styles
138
+ function DefaultTable({ children, className, style }: { children: React.ReactNode; className?: string; style?: React.CSSProperties }) {
139
+ return (
140
+ <table style={{ width: "100%", fontSize: "0.875rem", borderCollapse: "collapse", ...style }} className={className}>
141
+ {children}
142
+ </table>
143
+ );
144
+ }
145
+
146
+ function DefaultTableHeader({ children }: { children: React.ReactNode }) {
147
+ return <thead>{children}</thead>;
148
+ }
149
+
150
+ function DefaultTableBody({ children }: { children: React.ReactNode }) {
151
+ return <tbody>{children}</tbody>;
152
+ }
153
+
154
+ function DefaultTableRow({
155
+ children,
156
+ className,
157
+ style,
158
+ onClick,
159
+ }: {
160
+ children: React.ReactNode;
161
+ className?: string;
162
+ style?: React.CSSProperties;
163
+ onClick?: () => void;
164
+ }) {
165
+ return (
166
+ <tr
167
+ style={{ borderBottom: "1px solid #e5e7eb", ...style }}
168
+ className={className}
169
+ onClick={onClick}
170
+ >
171
+ {children}
172
+ </tr>
173
+ );
174
+ }
175
+
176
+ function DefaultTableHead({ children, className, style }: { children: React.ReactNode; className?: string; style?: React.CSSProperties }) {
177
+ return (
178
+ <th
179
+ style={{
180
+ height: "3rem",
181
+ padding: "0 1rem",
182
+ textAlign: "left",
183
+ verticalAlign: "middle",
184
+ fontWeight: 500,
185
+ color: "#6b7280",
186
+ ...style,
187
+ }}
188
+ className={className}
189
+ >
190
+ {children}
191
+ </th>
192
+ );
193
+ }
194
+
195
+ function DefaultTableCell({ children, className, style }: { children: React.ReactNode; className?: string; style?: React.CSSProperties }) {
196
+ return (
197
+ <td style={{ padding: "1rem", verticalAlign: "middle", ...style }} className={className}>
198
+ {children}
199
+ </td>
200
+ );
201
+ }
@@ -0,0 +1,247 @@
1
+ import React, { useRef } from "react";
2
+ import { cn } from "../../utils";
3
+ import { DataTable, type DataTableProps } from "./DataTable";
4
+ import { usePagination, type UsePaginationOptions } from "../../hooks/usePagination";
5
+ import type { BaseDataItem } from "../../types";
6
+
7
+ export interface PaginatedTableProps<T extends BaseDataItem> extends Omit<DataTableProps<T>, 'data'> {
8
+ /** Data items (all or paginated from server) */
9
+ data: T[];
10
+ /** Total items count (for server-side pagination) */
11
+ totalItems?: number;
12
+ /** Whether pagination is server-side */
13
+ serverSide?: boolean;
14
+ /** Current page (controlled) */
15
+ page?: number;
16
+ /** Page size (controlled) */
17
+ pageSize?: number;
18
+ /** Page change callback */
19
+ onPageChange?: (page: number) => void;
20
+ /** Page size change callback */
21
+ onPageSizeChange?: (pageSize: number) => void;
22
+ /** Whether to show pagination controls */
23
+ showPagination?: boolean;
24
+ /** Available page sizes */
25
+ pageSizes?: number[];
26
+ /** Whether to enable auto page size */
27
+ autoPageSize?: boolean;
28
+ /** Header content (rendered above table) */
29
+ headerContent?: React.ReactNode;
30
+ /** Custom components */
31
+ components?: DataTableProps<T>['components'] & {
32
+ Select?: React.ComponentType<{
33
+ value: string;
34
+ onValueChange: (value: string) => void;
35
+ children: React.ReactNode;
36
+ }>;
37
+ SelectTrigger?: React.ComponentType<{ className?: string; children: React.ReactNode }>;
38
+ SelectValue?: React.ComponentType<Record<string, unknown>>;
39
+ SelectContent?: React.ComponentType<{ children: React.ReactNode }>;
40
+ SelectItem?: React.ComponentType<{ value: string; children: React.ReactNode }>;
41
+ Button?: React.ComponentType<{
42
+ size?: string;
43
+ variant?: string;
44
+ onClick?: () => void;
45
+ disabled?: boolean;
46
+ className?: string;
47
+ children: React.ReactNode;
48
+ }>;
49
+ };
50
+ }
51
+
52
+ /**
53
+ * PaginatedTable - DataTable with built-in pagination
54
+ *
55
+ * @example
56
+ * ```tsx
57
+ * // Client-side pagination
58
+ * <PaginatedTable
59
+ * data={items}
60
+ * columns={columns}
61
+ * showPagination
62
+ * />
63
+ *
64
+ * // Server-side pagination
65
+ * <PaginatedTable
66
+ * data={pageData}
67
+ * columns={columns}
68
+ * serverSide
69
+ * totalItems={totalCount}
70
+ * page={page}
71
+ * pageSize={pageSize}
72
+ * onPageChange={setPage}
73
+ * onPageSizeChange={setPageSize}
74
+ * />
75
+ * ```
76
+ */
77
+ export function PaginatedTable<T extends BaseDataItem>({
78
+ data,
79
+ totalItems,
80
+ serverSide = false,
81
+ page: controlledPage,
82
+ pageSize: controlledPageSize,
83
+ onPageChange,
84
+ onPageSizeChange,
85
+ showPagination = true,
86
+ pageSizes = [25, 50, 100, 200],
87
+ autoPageSize = false,
88
+ headerContent,
89
+ components = {},
90
+ className,
91
+ ...tableProps
92
+ }: PaginatedTableProps<T>) {
93
+ const viewportRef = useRef<HTMLDivElement>(null);
94
+
95
+ // Use internal pagination hook for client-side pagination
96
+ const pagination = usePagination({
97
+ initialPage: controlledPage ?? 0,
98
+ initialPageSize: controlledPageSize ?? 25,
99
+ totalItems: serverSide ? totalItems : data.length,
100
+ autoPageSize,
101
+ viewportRef: autoPageSize ? viewportRef : undefined,
102
+ onPageChange,
103
+ onPageSizeChange,
104
+ });
105
+
106
+ // Use controlled values if provided
107
+ const page = controlledPage ?? pagination.page;
108
+ const pageSize = controlledPageSize ?? pagination.pageSize;
109
+
110
+ // Compute visible data for client-side pagination
111
+ const visibleData = serverSide
112
+ ? data
113
+ : data.slice(page * pageSize, (page + 1) * pageSize);
114
+
115
+ const hasNextPage = serverSide
116
+ ? data.length === pageSize
117
+ : (page + 1) * pageSize < data.length;
118
+
119
+ const canPrevPage = page > 0;
120
+
121
+ // Components
122
+ const Button = components.Button || DefaultButton;
123
+ const Select = components.Select;
124
+ const SelectTrigger = components.SelectTrigger;
125
+ const SelectValue = components.SelectValue;
126
+ const SelectContent = components.SelectContent;
127
+ const SelectItem = components.SelectItem;
128
+
129
+ const hasSelectComponents = Select && SelectTrigger && SelectValue && SelectContent && SelectItem;
130
+
131
+ return (
132
+ <div className={cn("flex flex-col", className)}>
133
+ {/* Pagination Header */}
134
+ {showPagination && (
135
+ <div className="flex items-center justify-between gap-2 px-3 sm:px-4 py-2 sm:py-3 border-b border-border">
136
+ <div className="text-xs sm:text-sm text-muted-foreground">
137
+ Page <span className="font-medium text-foreground">{page + 1}</span>
138
+ </div>
139
+ <div className="flex items-center gap-2 sm:gap-3">
140
+ {/* Page Size Selector */}
141
+ {hasSelectComponents && (
142
+ <div className="hidden sm:flex items-center gap-2 text-sm text-muted-foreground">
143
+ Page size
144
+ <Select
145
+ value={pagination.pageSizeSelectValue}
146
+ onValueChange={pagination.onPageSizeSelect}
147
+ >
148
+ <SelectTrigger className="h-8 w-[92px]">
149
+ <SelectValue />
150
+ </SelectTrigger>
151
+ <SelectContent>
152
+ {autoPageSize && <SelectItem value="auto">Auto</SelectItem>}
153
+ {pageSizes.map((size) => (
154
+ <SelectItem key={size} value={String(size)}>
155
+ {size}
156
+ </SelectItem>
157
+ ))}
158
+ </SelectContent>
159
+ </Select>
160
+ </div>
161
+ )}
162
+
163
+ {/* Page Navigation */}
164
+ <div className="flex items-center gap-1 sm:gap-2">
165
+ <Button
166
+ size="sm"
167
+ variant="outline"
168
+ onClick={() => {
169
+ const newPage = Math.max(0, page - 1);
170
+ if (onPageChange) onPageChange(newPage);
171
+ else pagination.setPage(newPage);
172
+ }}
173
+ disabled={!canPrevPage}
174
+ className="px-2 sm:px-3"
175
+ >
176
+ <span className="hidden sm:inline">Previous</span>
177
+ <span className="sm:hidden">Prev</span>
178
+ </Button>
179
+ <Button
180
+ size="sm"
181
+ variant="outline"
182
+ onClick={() => {
183
+ const newPage = page + 1;
184
+ if (onPageChange) onPageChange(newPage);
185
+ else pagination.setPage(newPage);
186
+ }}
187
+ disabled={!hasNextPage}
188
+ className="px-2 sm:px-3"
189
+ >
190
+ Next
191
+ </Button>
192
+ </div>
193
+ </div>
194
+ </div>
195
+ )}
196
+
197
+ {/* Header Content */}
198
+ {headerContent}
199
+
200
+ {/* Table */}
201
+ <div
202
+ ref={viewportRef}
203
+ className={cn(
204
+ autoPageSize && "h-[max(200px,calc(100vh-480px))] sm:h-[max(240px,calc(100vh-420px))] md:h-[max(320px,calc(100vh-360px))] overflow-auto"
205
+ )}
206
+ >
207
+ <DataTable
208
+ {...tableProps}
209
+ data={visibleData}
210
+ components={components}
211
+ />
212
+ </div>
213
+ </div>
214
+ );
215
+ }
216
+
217
+ // Default button
218
+ function DefaultButton({
219
+ children,
220
+ className,
221
+ disabled,
222
+ onClick,
223
+ }: {
224
+ size?: string;
225
+ variant?: string;
226
+ onClick?: () => void;
227
+ disabled?: boolean;
228
+ className?: string;
229
+ children: React.ReactNode;
230
+ }) {
231
+ return (
232
+ <button
233
+ type="button"
234
+ onClick={onClick}
235
+ disabled={disabled}
236
+ className={cn(
237
+ "inline-flex items-center justify-center rounded-md text-sm font-medium",
238
+ "h-8 px-3",
239
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
240
+ "disabled:pointer-events-none disabled:opacity-50",
241
+ className
242
+ )}
243
+ >
244
+ {children}
245
+ </button>
246
+ );
247
+ }
@@ -0,0 +1,2 @@
1
+ export { DataTable, type DataTableProps } from "./DataTable";
2
+ export { PaginatedTable, type PaginatedTableProps } from "./PaginatedTable";