@superdangerous/app-framework 4.14.0 → 4.15.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 (38) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/middleware/validation.d.ts +12 -12
  6. package/dist/services/emailService.d.ts +146 -0
  7. package/dist/services/emailService.d.ts.map +1 -0
  8. package/dist/services/emailService.js +649 -0
  9. package/dist/services/emailService.js.map +1 -0
  10. package/dist/services/index.d.ts +2 -0
  11. package/dist/services/index.d.ts.map +1 -1
  12. package/dist/services/index.js +2 -0
  13. package/dist/services/index.js.map +1 -1
  14. package/package.json +9 -1
  15. package/src/index.ts +14 -0
  16. package/src/services/emailService.ts +812 -0
  17. package/src/services/index.ts +14 -0
  18. package/ui/data-table/components/BatchActionsBar.tsx +53 -0
  19. package/ui/data-table/components/ColumnVisibility.tsx +111 -0
  20. package/ui/data-table/components/DataTable.tsx +492 -0
  21. package/ui/data-table/components/DataTablePage.tsx +238 -0
  22. package/ui/data-table/components/Pagination.tsx +203 -0
  23. package/ui/data-table/components/PaginationControls.tsx +122 -0
  24. package/ui/data-table/components/TableFilters.tsx +139 -0
  25. package/ui/data-table/components/index.ts +41 -0
  26. package/ui/data-table/components/types.ts +181 -0
  27. package/ui/data-table/hooks/index.ts +17 -0
  28. package/ui/data-table/hooks/useColumnOrder.ts +233 -0
  29. package/ui/data-table/hooks/useColumnVisibility.ts +128 -0
  30. package/ui/data-table/hooks/usePagination.ts +160 -0
  31. package/ui/data-table/hooks/useResizableColumns.ts +280 -0
  32. package/ui/data-table/index.ts +84 -0
  33. package/ui/dist/index.d.mts +207 -5
  34. package/ui/dist/index.d.ts +207 -5
  35. package/ui/dist/index.js +36 -43
  36. package/ui/dist/index.js.map +1 -1
  37. package/ui/dist/index.mjs +36 -43
  38. package/ui/dist/index.mjs.map +1 -1
@@ -0,0 +1,238 @@
1
+ /**
2
+ * DataTablePage - Full-height data table layout with integrated header
3
+ *
4
+ * This component provides a desktop-app style layout where:
5
+ * - The header contains search, filters, action buttons, AND pagination controls
6
+ * - The data table fills the available vertical space between header and footer
7
+ * - Pagination controls appear inline in the header (right side)
8
+ *
9
+ * Usage:
10
+ * ```tsx
11
+ * <DataTablePage
12
+ * title="Issues"
13
+ * description="Review and manage detected code issues"
14
+ * search={searchTerm}
15
+ * onSearchChange={setSearchTerm}
16
+ * searchPlaceholder="Search issues..."
17
+ * filters={filterOptions}
18
+ * activeFilterCount={countActiveFilters}
19
+ * onClearFilters={clearFilters}
20
+ * pagination={pagination}
21
+ * actions={<>
22
+ * <Button>Refresh</Button>
23
+ * <Button>Export</Button>
24
+ * </>}
25
+ * >
26
+ * <DataTable ... hidePagination />
27
+ * </DataTablePage>
28
+ * ```
29
+ */
30
+
31
+ import React from 'react';
32
+ import { Search, Filter, X } from 'lucide-react';
33
+ import { Input } from '../../components/base/input';
34
+ import { Button } from '../../components/base/button';
35
+ import { Popover, PopoverContent, PopoverTrigger } from '../../components/base/popover';
36
+ import { cn } from '../../src/utils/cn';
37
+ import { PaginationControls, type PaginationControlsProps } from './PaginationControls';
38
+
39
+ export interface FilterOption {
40
+ id: string;
41
+ label: string;
42
+ render: () => React.ReactNode;
43
+ }
44
+
45
+ export interface DataTablePageProps {
46
+ /** Page title */
47
+ title: string;
48
+ /** Page description */
49
+ description?: string;
50
+ /** Search term */
51
+ search?: string;
52
+ /** Search change handler */
53
+ onSearchChange?: (value: string) => void;
54
+ /** Search placeholder text */
55
+ searchPlaceholder?: string;
56
+ /** Filter options for popover */
57
+ filters?: FilterOption[];
58
+ /** Number of active filters */
59
+ activeFilterCount?: number;
60
+ /** Clear all filters handler */
61
+ onClearFilters?: () => void;
62
+ /** Pagination props from usePagination hook */
63
+ pagination?: PaginationControlsProps;
64
+ /** Action buttons to show in the header */
65
+ actions?: React.ReactNode;
66
+ /** Content before the table (e.g., BatchActionsBar) */
67
+ beforeTable?: React.ReactNode;
68
+ /** The DataTable component */
69
+ children: React.ReactNode;
70
+ /** Additional class for the container */
71
+ className?: string;
72
+ /** Whether to show a loading state */
73
+ loading?: boolean;
74
+ /** Loading component to show */
75
+ loadingComponent?: React.ReactNode;
76
+ }
77
+
78
+ export function DataTablePage({
79
+ title,
80
+ description,
81
+ search,
82
+ onSearchChange,
83
+ searchPlaceholder = 'Search...',
84
+ filters,
85
+ activeFilterCount = 0,
86
+ onClearFilters,
87
+ pagination,
88
+ actions,
89
+ beforeTable,
90
+ children,
91
+ className,
92
+ loading,
93
+ loadingComponent,
94
+ }: DataTablePageProps) {
95
+ // Always show pagination controls when pagination is provided (for row count selector)
96
+ const showPagination = pagination && pagination.totalItems > 0;
97
+
98
+ return (
99
+ <div className={cn('flex flex-col h-full', className)}>
100
+ {/* Page Header - has horizontal padding */}
101
+ <div className="data-table-page-header flex-shrink-0 space-y-4 pb-4">
102
+ {/* Title */}
103
+ <div>
104
+ <h1 className="text-3xl font-bold">{title}</h1>
105
+ {description && (
106
+ <p className="text-muted-foreground">{description}</p>
107
+ )}
108
+ </div>
109
+
110
+ {/* Controls Row: Search | Filters | Pagination | Spacer | Actions */}
111
+ <div className="flex items-center gap-3 flex-wrap">
112
+ {/* Search Input - responsive width */}
113
+ {onSearchChange !== undefined && (
114
+ <div className="relative w-full sm:w-auto sm:min-w-[200px] sm:max-w-xs">
115
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none z-10" />
116
+ <Input
117
+ placeholder={searchPlaceholder}
118
+ value={search || ''}
119
+ onChange={(e) => onSearchChange(e.target.value)}
120
+ className="pl-9 h-9"
121
+ />
122
+ {search && (
123
+ <button
124
+ type="button"
125
+ onClick={() => onSearchChange('')}
126
+ className="absolute right-2 top-1/2 transform -translate-y-1/2 p-1 rounded-sm hover:bg-muted"
127
+ >
128
+ <X className="h-3 w-3 text-muted-foreground" />
129
+ </button>
130
+ )}
131
+ </div>
132
+ )}
133
+
134
+ {/* Filters Popover */}
135
+ {filters && filters.length > 0 && (
136
+ <Popover>
137
+ <PopoverTrigger asChild>
138
+ <Button variant="outline" size="sm" className="gap-2 h-9">
139
+ <Filter className="h-4 w-4" />
140
+ <span className="hidden sm:inline">Filters</span>
141
+ {activeFilterCount > 0 && (
142
+ <span className="rounded-full bg-primary text-primary-foreground px-2 py-0.5 text-xs font-medium">
143
+ {activeFilterCount}
144
+ </span>
145
+ )}
146
+ </Button>
147
+ </PopoverTrigger>
148
+ <PopoverContent
149
+ className="w-80 overflow-y-auto"
150
+ align="start"
151
+ collisionPadding={16}
152
+ style={{ maxHeight: 'var(--radix-popover-content-available-height)' }}
153
+ >
154
+ <div className="space-y-4">
155
+ <div className="flex items-center justify-between">
156
+ <h4 className="font-medium text-sm">Filters</h4>
157
+ {activeFilterCount > 0 && onClearFilters && (
158
+ <Button
159
+ variant="ghost"
160
+ size="sm"
161
+ onClick={onClearFilters}
162
+ className="h-auto p-0 text-xs text-destructive hover:text-destructive"
163
+ >
164
+ Clear all
165
+ </Button>
166
+ )}
167
+ </div>
168
+
169
+ <div className="space-y-3">
170
+ {filters.map((filter) => (
171
+ <div key={filter.id} className="space-y-1.5">
172
+ <label className="text-xs font-medium text-muted-foreground">
173
+ {filter.label}
174
+ </label>
175
+ {filter.render()}
176
+ </div>
177
+ ))}
178
+ </div>
179
+ </div>
180
+ </PopoverContent>
181
+ </Popover>
182
+ )}
183
+
184
+ {/* Clear filters button (visible when filters are active) */}
185
+ {activeFilterCount > 0 && onClearFilters && (
186
+ <Button
187
+ variant="ghost"
188
+ size="sm"
189
+ onClick={onClearFilters}
190
+ className="h-9 gap-1.5 text-muted-foreground hover:text-foreground"
191
+ title="Clear filters"
192
+ >
193
+ <X className="h-4 w-4" />
194
+ <span className="hidden sm:inline">Clear</span>
195
+ </Button>
196
+ )}
197
+
198
+ {/* Pagination Controls (after filters) */}
199
+ {showPagination && (
200
+ <PaginationControls {...pagination} />
201
+ )}
202
+
203
+ {/* Spacer */}
204
+ <div className="flex-1" />
205
+
206
+ {/* Action buttons (right side) - never wrap */}
207
+ {actions && (
208
+ <div className="flex items-center gap-2 flex-shrink-0">
209
+ {actions}
210
+ </div>
211
+ )}
212
+ </div>
213
+ </div>
214
+
215
+ {/* Before Table Content (e.g., BatchActionsBar) - with padding */}
216
+ {beforeTable && (
217
+ <div className="px-6 pb-2">
218
+ {beforeTable}
219
+ </div>
220
+ )}
221
+
222
+ {/* Table Container - edge to edge, scrolls both directions */}
223
+ <div className="relative flex-1 min-h-0">
224
+ <div className="data-table-scroll-container h-full">
225
+ {loading ? (
226
+ loadingComponent || (
227
+ <div className="flex items-center justify-center h-full">
228
+ <div className="text-muted-foreground">Loading...</div>
229
+ </div>
230
+ )
231
+ ) : (
232
+ children
233
+ )}
234
+ </div>
235
+ </div>
236
+ </div>
237
+ );
238
+ }
@@ -0,0 +1,203 @@
1
+ import {
2
+ ChevronLeft,
3
+ ChevronRight,
4
+ ChevronsLeft,
5
+ ChevronsRight,
6
+ } from 'lucide-react';
7
+ import { Button } from '../../components/base/button';
8
+ import {
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ } from '../../components/base/select';
15
+ import { cn } from '../../src/utils/cn';
16
+
17
+ interface PaginationProps {
18
+ page: number;
19
+ pageSize: number;
20
+ totalPages: number;
21
+ totalItems: number;
22
+ startIndex: number;
23
+ endIndex: number;
24
+ canGoNext: boolean;
25
+ canGoPrev: boolean;
26
+ pageSizeOptions: number[];
27
+ onPageChange: (page: number) => void;
28
+ onPageSizeChange: (size: number) => void;
29
+ onNextPage: () => void;
30
+ onPrevPage: () => void;
31
+ onFirstPage: () => void;
32
+ onLastPage: () => void;
33
+ className?: string;
34
+ }
35
+
36
+ export function Pagination({
37
+ page,
38
+ pageSize,
39
+ totalPages,
40
+ totalItems,
41
+ startIndex,
42
+ endIndex,
43
+ canGoNext,
44
+ canGoPrev,
45
+ pageSizeOptions,
46
+ onPageChange,
47
+ onPageSizeChange,
48
+ onNextPage,
49
+ onPrevPage,
50
+ onFirstPage,
51
+ onLastPage,
52
+ className,
53
+ }: PaginationProps) {
54
+ // Generate page numbers to show
55
+ const getPageNumbers = () => {
56
+ const pages: (number | 'ellipsis')[] = [];
57
+ const showPages = 5; // Max pages to show
58
+ const halfShow = Math.floor(showPages / 2);
59
+
60
+ let startPage = Math.max(1, page - halfShow);
61
+ let endPage = Math.min(totalPages, page + halfShow);
62
+
63
+ // Adjust range if near edges
64
+ if (page <= halfShow) {
65
+ endPage = Math.min(totalPages, showPages);
66
+ }
67
+ if (page > totalPages - halfShow) {
68
+ startPage = Math.max(1, totalPages - showPages + 1);
69
+ }
70
+
71
+ // Add first page and ellipsis if needed
72
+ if (startPage > 1) {
73
+ pages.push(1);
74
+ if (startPage > 2) {
75
+ pages.push('ellipsis');
76
+ }
77
+ }
78
+
79
+ // Add middle pages
80
+ for (let i = startPage; i <= endPage; i++) {
81
+ if (i !== 1 && i !== totalPages) {
82
+ pages.push(i);
83
+ }
84
+ }
85
+
86
+ // Add ellipsis and last page if needed
87
+ if (endPage < totalPages) {
88
+ if (endPage < totalPages - 1) {
89
+ pages.push('ellipsis');
90
+ }
91
+ pages.push(totalPages);
92
+ }
93
+
94
+ return pages;
95
+ };
96
+
97
+ if (totalItems === 0) {
98
+ return null;
99
+ }
100
+
101
+ return (
102
+ <div
103
+ className={cn(
104
+ 'flex flex-col sm:flex-row items-center justify-between gap-4 px-2 py-3',
105
+ className
106
+ )}
107
+ >
108
+ {/* Items info */}
109
+ <div className="text-sm text-muted-foreground">
110
+ Showing {startIndex} to {endIndex} of {totalItems} items
111
+ </div>
112
+
113
+ {/* Controls */}
114
+ <div className="flex items-center gap-4">
115
+ {/* Page size selector */}
116
+ <div className="flex items-center gap-2">
117
+ <span className="text-sm text-muted-foreground">Per page:</span>
118
+ <Select
119
+ value={String(pageSize)}
120
+ onValueChange={(v) => onPageSizeChange(Number(v))}
121
+ >
122
+ <SelectTrigger className="w-20 h-8">
123
+ <SelectValue />
124
+ </SelectTrigger>
125
+ <SelectContent>
126
+ {pageSizeOptions.map((size) => (
127
+ <SelectItem key={size} value={String(size)}>
128
+ {size}
129
+ </SelectItem>
130
+ ))}
131
+ </SelectContent>
132
+ </Select>
133
+ </div>
134
+
135
+ {/* Page navigation */}
136
+ <div className="flex items-center gap-1">
137
+ <Button
138
+ variant="outline"
139
+ size="icon"
140
+ className="h-8 w-8"
141
+ onClick={onFirstPage}
142
+ disabled={!canGoPrev}
143
+ title="First page"
144
+ >
145
+ <ChevronsLeft className="h-4 w-4" />
146
+ </Button>
147
+ <Button
148
+ variant="outline"
149
+ size="icon"
150
+ className="h-8 w-8"
151
+ onClick={onPrevPage}
152
+ disabled={!canGoPrev}
153
+ title="Previous page"
154
+ >
155
+ <ChevronLeft className="h-4 w-4" />
156
+ </Button>
157
+
158
+ {/* Page numbers */}
159
+ <div className="flex items-center gap-1">
160
+ {getPageNumbers().map((pageNum, idx) =>
161
+ pageNum === 'ellipsis' ? (
162
+ <span key={`ellipsis-${idx}`} className="px-2 text-muted-foreground">
163
+ ...
164
+ </span>
165
+ ) : (
166
+ <Button
167
+ key={pageNum}
168
+ variant={page === pageNum ? 'default' : 'outline'}
169
+ size="icon"
170
+ className="h-8 w-8"
171
+ onClick={() => onPageChange(pageNum)}
172
+ >
173
+ {pageNum}
174
+ </Button>
175
+ )
176
+ )}
177
+ </div>
178
+
179
+ <Button
180
+ variant="outline"
181
+ size="icon"
182
+ className="h-8 w-8"
183
+ onClick={onNextPage}
184
+ disabled={!canGoNext}
185
+ title="Next page"
186
+ >
187
+ <ChevronRight className="h-4 w-4" />
188
+ </Button>
189
+ <Button
190
+ variant="outline"
191
+ size="icon"
192
+ className="h-8 w-8"
193
+ onClick={onLastPage}
194
+ disabled={!canGoNext}
195
+ title="Last page"
196
+ >
197
+ <ChevronsRight className="h-4 w-4" />
198
+ </Button>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ );
203
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * PaginationControls - Compact inline pagination for table headers
3
+ *
4
+ * A condensed version of pagination controls designed to sit alongside
5
+ * search/filter controls in a table header bar.
6
+ */
7
+
8
+ import {
9
+ ChevronLeft,
10
+ ChevronRight,
11
+ } from 'lucide-react';
12
+ import { Button } from '../../components/base/button';
13
+ import {
14
+ Select,
15
+ SelectContent,
16
+ SelectItem,
17
+ SelectTrigger,
18
+ SelectValue,
19
+ } from '../../components/base/select';
20
+ import { cn } from '../../src/utils/cn';
21
+
22
+ export interface PaginationControlsProps {
23
+ page: number;
24
+ pageSize: number;
25
+ totalPages: number;
26
+ totalItems: number;
27
+ startIndex: number;
28
+ endIndex: number;
29
+ canGoNext: boolean;
30
+ canGoPrev: boolean;
31
+ pageSizeOptions: number[];
32
+ setPage: (page: number) => void;
33
+ setPageSize: (size: number) => void;
34
+ nextPage: () => void;
35
+ prevPage: () => void;
36
+ className?: string;
37
+ }
38
+
39
+ export function PaginationControls({
40
+ page,
41
+ pageSize,
42
+ totalPages,
43
+ totalItems,
44
+ startIndex,
45
+ endIndex,
46
+ canGoNext,
47
+ canGoPrev,
48
+ pageSizeOptions,
49
+ setPage: _setPage,
50
+ setPageSize,
51
+ nextPage,
52
+ prevPage,
53
+ className,
54
+ }: PaginationControlsProps) {
55
+ // Note: setPage is included for API compatibility with usePagination but not used
56
+ // as this component only provides prev/next navigation
57
+ void _setPage;
58
+ if (totalItems === 0) {
59
+ return null;
60
+ }
61
+
62
+ return (
63
+ <div className={cn('flex items-center gap-3 text-sm', className)}>
64
+ {/* Page size selector - hidden on small screens */}
65
+ <div className="hidden md:flex items-center gap-1.5">
66
+ <span className="text-muted-foreground hidden lg:inline">Rows:</span>
67
+ <Select
68
+ value={String(pageSize)}
69
+ onValueChange={(v) => setPageSize(Number(v))}
70
+ >
71
+ <SelectTrigger className="w-16 h-8 text-xs">
72
+ <SelectValue />
73
+ </SelectTrigger>
74
+ <SelectContent>
75
+ {pageSizeOptions.map((size) => (
76
+ <SelectItem key={size} value={String(size)}>
77
+ {size}
78
+ </SelectItem>
79
+ ))}
80
+ </SelectContent>
81
+ </Select>
82
+ </div>
83
+
84
+ {/* Page navigation - always visible when there are multiple pages */}
85
+ {totalPages > 1 && (
86
+ <div className="flex items-center gap-1">
87
+ <Button
88
+ variant="outline"
89
+ size="icon"
90
+ className="h-8 w-8"
91
+ onClick={prevPage}
92
+ disabled={!canGoPrev}
93
+ title="Previous page"
94
+ >
95
+ <ChevronLeft className="h-4 w-4" />
96
+ </Button>
97
+
98
+ {/* Page indicator - hidden on very small screens */}
99
+ <span className="hidden sm:inline px-2 text-muted-foreground tabular-nums min-w-[60px] text-center">
100
+ {page} / {totalPages}
101
+ </span>
102
+
103
+ <Button
104
+ variant="outline"
105
+ size="icon"
106
+ className="h-8 w-8"
107
+ onClick={nextPage}
108
+ disabled={!canGoNext}
109
+ title="Next page"
110
+ >
111
+ <ChevronRight className="h-4 w-4" />
112
+ </Button>
113
+ </div>
114
+ )}
115
+
116
+ {/* Items info - hidden on smaller screens */}
117
+ <span className="text-muted-foreground whitespace-nowrap hidden lg:inline">
118
+ Showing {startIndex}–{endIndex} of {totalItems}
119
+ </span>
120
+ </div>
121
+ );
122
+ }
@@ -0,0 +1,139 @@
1
+ import React from 'react';
2
+ import { Search, Filter, X } from 'lucide-react';
3
+ import { Input } from '../../components/base/input';
4
+ import { Button } from '../../components/base/button';
5
+ import { Popover, PopoverContent, PopoverTrigger } from '../../components/base/popover';
6
+ import { cn } from '../../src/utils/cn';
7
+
8
+ export interface FilterOption {
9
+ id: string;
10
+ label: string;
11
+ render: () => React.ReactNode;
12
+ }
13
+
14
+ export interface TableFiltersProps {
15
+ search?: string;
16
+ onSearchChange?: (value: string) => void;
17
+ searchPlaceholder?: string;
18
+ filters?: FilterOption[];
19
+ activeFilterCount?: number;
20
+ onClearFilters?: () => void;
21
+ className?: string;
22
+ children?: React.ReactNode;
23
+ }
24
+
25
+ /**
26
+ * TableFilters - Compact filter controls for data tables
27
+ *
28
+ * Features:
29
+ * - Search input
30
+ * - Popover filter menu with badge showing active count
31
+ * - Clear all filters button
32
+ * - Custom filter components via render prop
33
+ * - Children slot for additional action buttons
34
+ */
35
+ export function TableFilters({
36
+ search,
37
+ onSearchChange,
38
+ searchPlaceholder = 'Search...',
39
+ filters,
40
+ activeFilterCount = 0,
41
+ onClearFilters,
42
+ className,
43
+ children,
44
+ }: TableFiltersProps) {
45
+ return (
46
+ <div className={cn('flex items-center gap-3 flex-wrap', className)}>
47
+ {/* Search Input */}
48
+ {onSearchChange !== undefined && (
49
+ <div className="relative flex-1 min-w-[200px] max-w-xs">
50
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none z-10" />
51
+ <Input
52
+ placeholder={searchPlaceholder}
53
+ value={search || ''}
54
+ onChange={(e) => onSearchChange(e.target.value)}
55
+ className="pl-9 h-9"
56
+ />
57
+ {search && (
58
+ <button
59
+ type="button"
60
+ onClick={() => onSearchChange('')}
61
+ className="absolute right-2 top-1/2 transform -translate-y-1/2 p-1 rounded-sm hover:bg-muted"
62
+ >
63
+ <X className="h-3 w-3 text-muted-foreground" />
64
+ </button>
65
+ )}
66
+ </div>
67
+ )}
68
+
69
+ {/* Filters Popover */}
70
+ {filters && filters.length > 0 && (
71
+ <Popover>
72
+ <PopoverTrigger asChild>
73
+ <Button variant="outline" size="sm" className="gap-2 h-9">
74
+ <Filter className="h-4 w-4" />
75
+ Filters
76
+ {activeFilterCount > 0 && (
77
+ <span className="ml-1 rounded-full bg-primary text-primary-foreground px-2 py-0.5 text-xs font-medium">
78
+ {activeFilterCount}
79
+ </span>
80
+ )}
81
+ </Button>
82
+ </PopoverTrigger>
83
+ <PopoverContent
84
+ className="w-80 overflow-y-auto"
85
+ align="start"
86
+ collisionPadding={16}
87
+ style={{ maxHeight: 'var(--radix-popover-content-available-height)' }}
88
+ >
89
+ <div className="space-y-4">
90
+ <div className="flex items-center justify-between">
91
+ <h4 className="font-medium text-sm">Filters</h4>
92
+ {activeFilterCount > 0 && onClearFilters && (
93
+ <Button
94
+ variant="ghost"
95
+ size="sm"
96
+ onClick={onClearFilters}
97
+ className="h-auto p-0 text-xs text-destructive hover:text-destructive"
98
+ >
99
+ Clear all
100
+ </Button>
101
+ )}
102
+ </div>
103
+
104
+ <div className="space-y-3">
105
+ {filters.map((filter) => (
106
+ <div key={filter.id} className="space-y-1.5">
107
+ <label className="text-xs font-medium text-muted-foreground">
108
+ {filter.label}
109
+ </label>
110
+ {filter.render()}
111
+ </div>
112
+ ))}
113
+ </div>
114
+ </div>
115
+ </PopoverContent>
116
+ </Popover>
117
+ )}
118
+
119
+ {/* Clear filters button (visible when filters are active, outside popover) */}
120
+ {activeFilterCount > 0 && onClearFilters && (
121
+ <Button
122
+ variant="ghost"
123
+ size="sm"
124
+ onClick={onClearFilters}
125
+ className="h-9 gap-1.5 text-muted-foreground hover:text-foreground"
126
+ >
127
+ <X className="h-4 w-4" />
128
+ Clear
129
+ </Button>
130
+ )}
131
+
132
+ {/* Spacer to push children to the right */}
133
+ <div className="flex-1" />
134
+
135
+ {/* Custom children (e.g., export button, create button, etc.) */}
136
+ {children}
137
+ </div>
138
+ );
139
+ }