@pattern-stack/frontend-patterns 0.0.3 → 0.0.4

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 (154) hide show
  1. package/dist/index.es.js +1 -1
  2. package/dist/index.js +1 -0
  3. package/package.json +5 -3
  4. package/src/App.css +42 -0
  5. package/src/App.tsx +54 -0
  6. package/src/__tests__/README.md +221 -0
  7. package/src/__tests__/atoms/hooks/simple-hooks.test.ts +44 -0
  8. package/src/__tests__/atoms/ui/button.test.tsx +68 -0
  9. package/src/__tests__/atoms/utils/simple.test.ts +18 -0
  10. package/src/__tests__/atoms/utils/utils.test.ts +77 -0
  11. package/src/__tests__/features/auth/simple-auth.test.tsx +40 -0
  12. package/src/__tests__/molecules/layout/simple-layout.test.tsx +81 -0
  13. package/src/__tests__/organisms/showcase/simple-showcase.test.tsx +167 -0
  14. package/src/__tests__/setup.ts +51 -0
  15. package/src/__tests__/utils.tsx +123 -0
  16. package/src/atoms/composed/Accordion/Accordion.tsx +271 -0
  17. package/src/atoms/composed/Accordion/index.ts +1 -0
  18. package/src/atoms/composed/Alert/Alert.tsx +132 -0
  19. package/src/atoms/composed/Alert/index.ts +1 -0
  20. package/src/atoms/composed/Breadcrumb/Breadcrumb.tsx +83 -0
  21. package/src/atoms/composed/Breadcrumb/index.ts +1 -0
  22. package/src/atoms/composed/Chart/Chart.tsx +425 -0
  23. package/src/atoms/composed/Chart/index.ts +2 -0
  24. package/src/atoms/composed/ColorSwatch/ColorSwatch.tsx +72 -0
  25. package/src/atoms/composed/ColorSwatch/index.ts +1 -0
  26. package/src/atoms/composed/DarkModeToggle.tsx +66 -0
  27. package/src/atoms/composed/DataBadge/DataBadge.tsx +81 -0
  28. package/src/atoms/composed/DataBadge/index.ts +1 -0
  29. package/src/atoms/composed/DataTable/DataTable.tsx +394 -0
  30. package/src/atoms/composed/DataTable/TableCellWithTooltip.tsx +41 -0
  31. package/src/atoms/composed/DataTable/index.ts +2 -0
  32. package/src/atoms/composed/DateTimePicker/DateTimePicker.tsx +611 -0
  33. package/src/atoms/composed/DateTimePicker/index.ts +2 -0
  34. package/src/atoms/composed/DetailedCard/DetailedCard.tsx +181 -0
  35. package/src/atoms/composed/DetailedCard/index.ts +2 -0
  36. package/src/atoms/composed/EmptyState/EmptyState.tsx +90 -0
  37. package/src/atoms/composed/EmptyState/index.ts +1 -0
  38. package/src/atoms/composed/FileUpload/FileUpload.tsx +477 -0
  39. package/src/atoms/composed/FileUpload/index.ts +2 -0
  40. package/src/atoms/composed/FormField/FormField.tsx +92 -0
  41. package/src/atoms/composed/FormField/index.ts +1 -0
  42. package/src/atoms/composed/GlobalSearch/GlobalSearch.tsx +37 -0
  43. package/src/atoms/composed/GlobalSearch/index.ts +1 -0
  44. package/src/atoms/composed/IconBadge/IconBadge.tsx +95 -0
  45. package/src/atoms/composed/IconBadge/index.ts +2 -0
  46. package/src/atoms/composed/Modal/Modal.tsx +223 -0
  47. package/src/atoms/composed/Modal/index.ts +2 -0
  48. package/src/atoms/composed/PaletteSwitcher.tsx +386 -0
  49. package/src/atoms/composed/ProgressBar/ProgressBar.tsx +116 -0
  50. package/src/atoms/composed/ProgressBar/index.ts +1 -0
  51. package/src/atoms/composed/StatCard/StatCard.tsx +219 -0
  52. package/src/atoms/composed/StatCard/index.ts +1 -0
  53. package/src/atoms/composed/StyleGuide.tsx +717 -0
  54. package/src/atoms/composed/Toast/Toast.tsx +219 -0
  55. package/src/atoms/composed/Toast/index.ts +1 -0
  56. package/src/atoms/composed/Tooltip/Tooltip.tsx +213 -0
  57. package/src/atoms/composed/Tooltip/index.ts +1 -0
  58. package/src/atoms/composed/UserAvatar/UserAvatar.tsx +139 -0
  59. package/src/atoms/composed/UserAvatar/index.ts +1 -0
  60. package/src/atoms/composed/UserMenu/UserMenu.tsx +16 -0
  61. package/src/atoms/composed/UserMenu/index.ts +1 -0
  62. package/src/atoms/composed/index.ts +29 -0
  63. package/src/atoms/hooks/useApi.ts +80 -0
  64. package/src/atoms/hooks/useHealth.ts +17 -0
  65. package/src/atoms/index.ts +13 -0
  66. package/src/atoms/services/api/client.ts +134 -0
  67. package/src/atoms/services/auth-service.ts +248 -0
  68. package/src/atoms/services/health.ts +15 -0
  69. package/src/atoms/services/index.ts +3 -0
  70. package/src/atoms/shared/config/constants.ts +17 -0
  71. package/src/atoms/shared/config/dashboard-sizes.ts +111 -0
  72. package/src/atoms/shared/config/environment.ts +10 -0
  73. package/src/atoms/shared/index.ts +4 -0
  74. package/src/atoms/shared/styles/color-palettes.css +566 -0
  75. package/src/atoms/types/auth.ts +62 -0
  76. package/src/atoms/types/generated.ts +1469 -0
  77. package/src/atoms/types/index.ts +4 -0
  78. package/src/atoms/types/loading.ts +28 -0
  79. package/src/atoms/ui/Badge.tsx +30 -0
  80. package/src/atoms/ui/ErrorBoundary.tsx +59 -0
  81. package/src/atoms/ui/Select.tsx +53 -0
  82. package/src/atoms/ui/Switch.tsx +42 -0
  83. package/src/atoms/ui/Tabs.tsx +118 -0
  84. package/src/atoms/ui/avatar.tsx +48 -0
  85. package/src/atoms/ui/button.tsx +70 -0
  86. package/src/atoms/ui/card.tsx +76 -0
  87. package/src/atoms/ui/dropdown-menu.tsx +199 -0
  88. package/src/atoms/ui/index.ts +39 -0
  89. package/src/atoms/ui/input.tsx +23 -0
  90. package/src/atoms/ui/label.tsx +23 -0
  91. package/src/atoms/ui/skeleton.tsx +13 -0
  92. package/src/atoms/ui/spinner.tsx +49 -0
  93. package/src/atoms/ui/table.tsx +116 -0
  94. package/src/atoms/utils/animations.ts +135 -0
  95. package/src/atoms/utils/tooltip-helpers.ts +140 -0
  96. package/src/atoms/utils/utils.ts +9 -0
  97. package/src/features/auth/components/LoginForm.tsx +168 -0
  98. package/src/features/auth/components/LogoutButton.tsx +19 -0
  99. package/src/features/auth/components/ProtectedRoute.tsx +60 -0
  100. package/src/features/auth/components/index.ts +4 -0
  101. package/src/features/auth/hooks/index.ts +2 -0
  102. package/src/features/auth/hooks/useAuth.tsx +205 -0
  103. package/src/features/auth/hooks/usePermissions.ts +35 -0
  104. package/src/features/auth/index.ts +2 -0
  105. package/src/features/index.ts +2 -0
  106. package/src/index.css +704 -0
  107. package/src/index.ts +13 -0
  108. package/src/main.tsx +48 -0
  109. package/src/molecules/.gitkeep +0 -0
  110. package/src/molecules/forms/FormGroup.tsx +75 -0
  111. package/src/molecules/forms/SearchInput.tsx +259 -0
  112. package/src/molecules/forms/index.ts +4 -0
  113. package/src/molecules/index.ts +4 -0
  114. package/src/molecules/layout/AppHeader/AppHeader.tsx +42 -0
  115. package/src/molecules/layout/AppHeader/index.ts +1 -0
  116. package/src/molecules/layout/AppLayout.tsx +29 -0
  117. package/src/molecules/layout/PageTemplate.tsx +87 -0
  118. package/src/molecules/layout/SectionHeader/SectionHeader.tsx +87 -0
  119. package/src/molecules/layout/SectionHeader/index.ts +1 -0
  120. package/src/molecules/layout/ShowcaseSection.tsx +57 -0
  121. package/src/molecules/layout/Sidebar.tsx +144 -0
  122. package/src/molecules/layout/SidebarButton/SidebarButton.tsx +99 -0
  123. package/src/molecules/layout/SidebarButton/index.ts +1 -0
  124. package/src/molecules/layout/SidebarContext.tsx +31 -0
  125. package/src/molecules/layout/index.ts +7 -0
  126. package/src/molecules/navigation/NavMenu.tsx +188 -0
  127. package/src/molecules/navigation/Pagination.tsx +172 -0
  128. package/src/molecules/navigation/index.ts +4 -0
  129. package/src/organisms/index.ts +5 -0
  130. package/src/organisms/showcase/ComponentShowcasePage.tsx +2496 -0
  131. package/src/organisms/showcase/index.ts +1 -0
  132. package/src/pages/AdminShowcase/AdminCRUDShowcase.tsx +242 -0
  133. package/src/pages/AdminShowcase/AdminDashboardShowcase.tsx +171 -0
  134. package/src/pages/AdminShowcase/AdminDetailShowcase.tsx +385 -0
  135. package/src/pages/AdminShowcase/index.tsx +3 -0
  136. package/src/pages/ComponentShowcase/BadgesShowcase.tsx +188 -0
  137. package/src/pages/ComponentShowcase/CardsShowcase.tsx +392 -0
  138. package/src/pages/ComponentShowcase/PalettesShowcase.tsx +207 -0
  139. package/src/pages/ComponentShowcase/StatesShowcase.tsx +485 -0
  140. package/src/pages/ComponentShowcase/TablesShowcase.tsx +134 -0
  141. package/src/pages/ComponentShowcase/TypographyShowcase.tsx +255 -0
  142. package/src/pages/ComponentShowcase/index.tsx +188 -0
  143. package/src/pages/index.ts +2 -0
  144. package/src/templates/AuthTemplate.tsx +216 -0
  145. package/src/templates/ComponentShowcaseTemplate.tsx +173 -0
  146. package/src/templates/DashboardTemplate.tsx +232 -0
  147. package/src/templates/DataTemplate.tsx +319 -0
  148. package/src/templates/admin/AdminCRUDTemplate.tsx +630 -0
  149. package/src/templates/admin/AdminDashboardTemplate.tsx +351 -0
  150. package/src/templates/admin/AdminDetailTemplate.tsx +563 -0
  151. package/src/templates/admin/index.ts +29 -0
  152. package/src/templates/factory.tsx +169 -0
  153. package/src/templates/index.ts +37 -0
  154. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,630 @@
1
+ import React, { useState } from 'react';
2
+ import { cn } from '../../atoms/utils/utils';
3
+ import { IconBadge } from '../../atoms/composed';
4
+ import { DataTable, type Column } from '../../atoms/composed/DataTable';
5
+ import { DataBadge } from '../../atoms/composed/DataBadge';
6
+ import { Card } from '../../atoms/ui/card';
7
+ import { Button } from '../../atoms/ui/button';
8
+ import { Modal } from '../../atoms/composed/Modal';
9
+ import { FormField } from '../../atoms/composed/FormField';
10
+ import { EmptyState } from '../../atoms/composed/EmptyState';
11
+ import {
12
+ Plus,
13
+ Download,
14
+ Filter,
15
+ Edit,
16
+ Trash2,
17
+ Eye,
18
+ Upload,
19
+ RefreshCw
20
+ } from 'lucide-react';
21
+
22
+ export interface ResourceSchema {
23
+ /** Field definitions for the resource */
24
+ fields: ResourceField[];
25
+ /** Primary key field name */
26
+ primaryKey: string;
27
+ /** Display name for single resource */
28
+ displayName: string;
29
+ /** Display name for multiple resources */
30
+ displayNamePlural: string;
31
+ }
32
+
33
+ export interface ResourceField {
34
+ /** Field name/key */
35
+ name: string;
36
+ /** Display label */
37
+ label: string;
38
+ /** Field type */
39
+ type: 'text' | 'email' | 'number' | 'boolean' | 'date' | 'select' | 'textarea' | 'file';
40
+ /** Whether field is required */
41
+ required?: boolean;
42
+ /** Field validation rules */
43
+ validation?: Record<string, unknown>;
44
+ /** Options for select fields */
45
+ options?: Array<{ value: string; label: string }>;
46
+ /** Whether field is searchable */
47
+ searchable?: boolean;
48
+ /** Whether field should be shown in table */
49
+ showInTable?: boolean;
50
+ /** Column width for table display */
51
+ width?: number;
52
+ /** Custom render function for table cells */
53
+ render?: (value: unknown, item: Record<string, unknown>) => React.ReactNode;
54
+ }
55
+
56
+ export interface CRUDPermissions {
57
+ /** Can view resources */
58
+ view: boolean;
59
+ /** Can create new resources */
60
+ create: boolean;
61
+ /** Can edit existing resources */
62
+ edit: boolean;
63
+ /** Can delete resources */
64
+ delete: boolean;
65
+ /** Can export data */
66
+ export: boolean;
67
+ /** Can import data */
68
+ import: boolean;
69
+ /** Can bulk edit */
70
+ bulkEdit: boolean;
71
+ }
72
+
73
+ export interface CRUDActions {
74
+ /** Create new resource */
75
+ onCreate?: (data: Record<string, unknown>) => Promise<void>;
76
+ /** Update existing resource */
77
+ onUpdate?: (id: string, data: Record<string, unknown>) => Promise<void>;
78
+ /** Delete resource */
79
+ onDelete?: (id: string) => Promise<void>;
80
+ /** Bulk delete resources */
81
+ onBulkDelete?: (ids: string[]) => Promise<void>;
82
+ /** Export data */
83
+ onExport?: (format: 'csv' | 'xlsx' | 'json') => Promise<void>;
84
+ /** Import data */
85
+ onImport?: (file: File) => Promise<void>;
86
+ /** Refresh data */
87
+ onRefresh?: () => Promise<void>;
88
+ }
89
+
90
+ export interface AdminCRUDTemplateProps {
91
+ /** Resource schema definition */
92
+ schema: ResourceSchema;
93
+ /** Current data */
94
+ data: Record<string, unknown>[];
95
+ /** User permissions */
96
+ permissions: CRUDPermissions;
97
+ /** CRUD action handlers */
98
+ actions: CRUDActions;
99
+ /** Whether data is loading */
100
+ isLoading?: boolean;
101
+ /** Filter configuration */
102
+ filterConfig?: {
103
+ activeFilters?: Array<{ key: string; label: string; value: string }>;
104
+ onFilter?: () => void;
105
+ onClearFilters?: () => void;
106
+ };
107
+ /** Additional custom actions */
108
+ customActions?: Array<{
109
+ label: string;
110
+ icon?: React.ReactNode;
111
+ onClick: () => void;
112
+ variant?: 'default' | 'outline' | 'destructive';
113
+ }>;
114
+ /** Additional CSS classes */
115
+ className?: string;
116
+ /** Category for styling */
117
+ category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
118
+ /** Custom empty state */
119
+ emptyState?: React.ReactNode;
120
+ }
121
+
122
+ export const AdminCRUDTemplate: React.FC<AdminCRUDTemplateProps> = ({
123
+ schema,
124
+ data,
125
+ permissions,
126
+ actions,
127
+ isLoading = false,
128
+ filterConfig,
129
+ customActions = [],
130
+ className,
131
+ category = 1,
132
+ emptyState
133
+ }) => {
134
+ const [selectedItems, setSelectedItems] = useState<string[]>([]);
135
+ const [showCreateModal, setShowCreateModal] = useState(false);
136
+ const [showEditModal, setShowEditModal] = useState(false);
137
+ const [editingItem, setEditingItem] = useState<Record<string, unknown> | null>(null);
138
+ const [formData, setFormData] = useState<Record<string, unknown>>({});
139
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
140
+ const [deletingId, setDeletingId] = useState<string | null>(null);
141
+
142
+ const hasData = data && data.length > 0;
143
+ const hasSelection = selectedItems.length > 0;
144
+ const activeFilterCount = filterConfig?.activeFilters?.length || 0;
145
+
146
+ // Generate table columns from schema
147
+ const columns: Column<Record<string, unknown>>[] = [
148
+ // Selection column
149
+ {
150
+ key: 'select',
151
+ header: (
152
+ <input
153
+ type="checkbox"
154
+ checked={selectedItems.length > 0 && selectedItems.length === data.length}
155
+ onChange={(e) => {
156
+ if (e.target.checked) {
157
+ setSelectedItems(data.map(item => String(item[schema.primaryKey])));
158
+ } else {
159
+ setSelectedItems([]);
160
+ }
161
+ }}
162
+ className="rounded border-input"
163
+ />
164
+ ),
165
+ cell: (item) => (
166
+ <input
167
+ type="checkbox"
168
+ checked={selectedItems.includes(String(item[schema.primaryKey]))}
169
+ onChange={(e) => {
170
+ const id = String(item[schema.primaryKey]);
171
+ if (e.target.checked) {
172
+ setSelectedItems(prev => [...prev, id]);
173
+ } else {
174
+ setSelectedItems(prev => prev.filter(existingId => existingId !== id));
175
+ }
176
+ }}
177
+ className="rounded border-input"
178
+ />
179
+ ),
180
+ sortable: false,
181
+ width: '50px',
182
+ },
183
+ // Data columns
184
+ ...schema.fields
185
+ .filter(field => field.showInTable !== false)
186
+ .map(field => {
187
+ // Determine column type for automatic badge rendering
188
+ let columnType: 'status' | 'category' | 'default' = 'default';
189
+ if (field.type === 'boolean' || field.name.toLowerCase().includes('status')) {
190
+ columnType = 'status';
191
+ } else if (field.name.toLowerCase().includes('category')) {
192
+ columnType = 'category';
193
+ }
194
+
195
+ return {
196
+ key: field.name,
197
+ header: field.label,
198
+ type: columnType,
199
+ cell: field.render ? (item: Record<string, unknown>) => field.render!(item[field.name], item) : undefined,
200
+ sortable: true,
201
+ width: field.width ? `${field.width}px` : undefined,
202
+ };
203
+ }),
204
+ // Actions column
205
+ {
206
+ key: 'actions',
207
+ header: 'Actions',
208
+ cell: (item) => (
209
+ <div className="flex items-center gap-1">
210
+ <Button
211
+ variant="ghost"
212
+ size="sm"
213
+ onClick={(e) => {
214
+ e.stopPropagation();
215
+ handleView(item);
216
+ }}
217
+ tooltip="View details"
218
+ >
219
+ <Eye className="w-4 h-4" />
220
+ </Button>
221
+ {permissions.edit && (
222
+ <Button
223
+ variant="ghost"
224
+ size="sm"
225
+ onClick={(e) => {
226
+ e.stopPropagation();
227
+ handleEdit(item);
228
+ }}
229
+ tooltip="Edit"
230
+ >
231
+ <Edit className="w-4 h-4" />
232
+ </Button>
233
+ )}
234
+ {permissions.delete && (
235
+ <Button
236
+ variant="ghost"
237
+ size="sm"
238
+ onClick={(e) => {
239
+ e.stopPropagation();
240
+ handleDelete(String(item[schema.primaryKey]));
241
+ }}
242
+ tooltip="Delete"
243
+ >
244
+ <Trash2 className="w-4 h-4" />
245
+ </Button>
246
+ )}
247
+ </div>
248
+ ),
249
+ sortable: false,
250
+ width: '120px',
251
+ },
252
+ ];
253
+
254
+ const handleCreate = () => {
255
+ setFormData({});
256
+ setShowCreateModal(true);
257
+ };
258
+
259
+ const handleEdit = (item: Record<string, unknown>) => {
260
+ setEditingItem(item);
261
+ setFormData(item);
262
+ setShowEditModal(true);
263
+ };
264
+
265
+ const handleView = (item: Record<string, unknown>) => {
266
+ // Navigate to detail view - this would be handled by the parent component
267
+ console.log('View item:', item);
268
+ };
269
+
270
+ const handleDelete = (id: string) => {
271
+ setDeletingId(id);
272
+ setShowDeleteConfirm(true);
273
+ };
274
+
275
+ const handleBulkDelete = () => {
276
+ if (actions.onBulkDelete) {
277
+ actions.onBulkDelete(selectedItems);
278
+ setSelectedItems([]);
279
+ }
280
+ };
281
+
282
+ const confirmDelete = async () => {
283
+ if (deletingId && actions.onDelete) {
284
+ await actions.onDelete(deletingId);
285
+ setShowDeleteConfirm(false);
286
+ setDeletingId(null);
287
+ }
288
+ };
289
+
290
+ const handleSubmit = async (isEdit: boolean) => {
291
+ try {
292
+ if (isEdit && editingItem && actions.onUpdate) {
293
+ await actions.onUpdate(String(editingItem[schema.primaryKey]), formData);
294
+ setShowEditModal(false);
295
+ } else if (!isEdit && actions.onCreate) {
296
+ await actions.onCreate(formData);
297
+ setShowCreateModal(false);
298
+ }
299
+ setFormData({});
300
+ setEditingItem(null);
301
+ } catch (error) {
302
+ console.error('Form submission error:', error);
303
+ }
304
+ };
305
+
306
+ const renderFormField = (field: ResourceField) => {
307
+ const value = formData[field.name];
308
+
309
+ return (
310
+ <FormField
311
+ key={field.name}
312
+ label={field.label}
313
+ required={field.required}
314
+ error={undefined} // Add validation error handling here
315
+ >
316
+ {field.type === 'select' ? (
317
+ <select
318
+ value={String(value || '')}
319
+ onChange={(e) => setFormData(prev => ({ ...prev, [field.name]: e.target.value }))}
320
+ className="w-full px-3 py-2 border border-input rounded-md"
321
+ >
322
+ <option value="">Select {field.label}</option>
323
+ {field.options?.map(option => (
324
+ <option key={option.value} value={option.value}>
325
+ {option.label}
326
+ </option>
327
+ ))}
328
+ </select>
329
+ ) : field.type === 'textarea' ? (
330
+ <textarea
331
+ value={String(value || '')}
332
+ onChange={(e) => setFormData(prev => ({ ...prev, [field.name]: e.target.value }))}
333
+ className="w-full px-3 py-2 border border-input rounded-md"
334
+ rows={3}
335
+ />
336
+ ) : field.type === 'boolean' ? (
337
+ <input
338
+ type="checkbox"
339
+ checked={Boolean(value)}
340
+ onChange={(e) => setFormData(prev => ({ ...prev, [field.name]: e.target.checked }))}
341
+ className="rounded border-input"
342
+ />
343
+ ) : (
344
+ <input
345
+ type={field.type === 'email' ? 'email' : field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'}
346
+ value={String(value || '')}
347
+ onChange={(e) => setFormData(prev => ({ ...prev, [field.name]: e.target.value }))}
348
+ className="w-full px-3 py-2 border border-input rounded-md"
349
+ />
350
+ )}
351
+ </FormField>
352
+ );
353
+ };
354
+
355
+ return (
356
+ <div className={cn('flex flex-col min-h-0 flex-1', className)}>
357
+ {/* Header Section */}
358
+ <div className="bg-gradient-to-br from-muted/50 via-muted/30 to-background border-b border-border/50">
359
+ <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-12">
360
+ <div className="flex items-center justify-between">
361
+ <div className="flex items-center space-x-4">
362
+ <IconBadge
363
+ variant="category"
364
+ category={category}
365
+ size="lg"
366
+ icon={<Plus className="w-5 h-5" />}
367
+ />
368
+ <div>
369
+ <h1 className="text-4xl font-bold text-foreground">{schema.displayNamePlural}</h1>
370
+ <p className="text-lg text-muted-foreground mt-2">
371
+ Manage {schema.displayNamePlural.toLowerCase()} with comprehensive CRUD operations
372
+ </p>
373
+ </div>
374
+ </div>
375
+
376
+ <div className="flex items-center gap-2">
377
+ {actions.onRefresh && (
378
+ <Button
379
+ variant="outline"
380
+ onClick={actions.onRefresh}
381
+ disabled={isLoading}
382
+ >
383
+ <RefreshCw className={cn("w-4 h-4 mr-2", isLoading && "animate-spin")} />
384
+ Refresh
385
+ </Button>
386
+ )}
387
+
388
+ {permissions.create && (
389
+ <Button
390
+ onClick={handleCreate}
391
+ variant="default"
392
+ >
393
+ <Plus className="w-4 h-4 mr-2" />
394
+ Create {schema.displayName}
395
+ </Button>
396
+ )}
397
+ </div>
398
+ </div>
399
+ </div>
400
+ </div>
401
+
402
+ {/* Controls Section */}
403
+ <div className="flex-shrink-0 border-b border-border bg-muted/30">
404
+ <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4">
405
+ <div className="flex items-center justify-between">
406
+ {/* Left side - Filters */}
407
+ <div className="flex items-center gap-4">
408
+ {filterConfig && (
409
+ <Button
410
+ variant="outline"
411
+ onClick={filterConfig.onFilter}
412
+ className="relative"
413
+ >
414
+ <Filter className="w-4 h-4 mr-2" />
415
+ Filters
416
+ {activeFilterCount > 0 && (
417
+ <span className="absolute -top-2 -right-2 min-w-5 h-5 rounded-full text-xs bg-destructive text-destructive-foreground flex items-center justify-center">
418
+ {activeFilterCount}
419
+ </span>
420
+ )}
421
+ </Button>
422
+ )}
423
+
424
+ {/* Active Filters */}
425
+ {filterConfig?.activeFilters && filterConfig.activeFilters.length > 0 && (
426
+ <div className="flex items-center gap-2">
427
+ {filterConfig.activeFilters.map((filter, index) => (
428
+ <DataBadge key={index} variant="category" category={category} size="sm">
429
+ {filter.label}: {filter.value}
430
+ </DataBadge>
431
+ ))}
432
+ <Button
433
+ variant="ghost"
434
+ size="sm"
435
+ onClick={filterConfig.onClearFilters}
436
+ >
437
+ Clear all
438
+ </Button>
439
+ </div>
440
+ )}
441
+ </div>
442
+
443
+ {/* Right side - Actions */}
444
+ <div className="flex items-center gap-2">
445
+ {/* Bulk actions */}
446
+ {hasSelection && (
447
+ <div className="flex items-center gap-2 mr-4">
448
+ <span className="text-sm text-muted-foreground">
449
+ {selectedItems.length} selected
450
+ </span>
451
+ {permissions.delete && (
452
+ <Button
453
+ variant="outline"
454
+ size="sm"
455
+ onClick={handleBulkDelete}
456
+ >
457
+ <Trash2 className="w-4 h-4 mr-2" />
458
+ Delete
459
+ </Button>
460
+ )}
461
+ </div>
462
+ )}
463
+
464
+ {/* Export/Import */}
465
+ {permissions.export && (
466
+ <Button
467
+ variant="outline"
468
+ onClick={() => actions.onExport?.('csv')}
469
+ >
470
+ <Download className="w-4 h-4 mr-2" />
471
+ Export
472
+ </Button>
473
+ )}
474
+
475
+ {permissions.import && (
476
+ <Button
477
+ variant="outline"
478
+ onClick={() => {
479
+ const input = document.createElement('input');
480
+ input.type = 'file';
481
+ input.accept = '.csv,.xlsx,.json';
482
+ input.onchange = (e) => {
483
+ const file = (e.target as HTMLInputElement).files?.[0];
484
+ if (file && actions.onImport) {
485
+ actions.onImport(file);
486
+ }
487
+ };
488
+ input.click();
489
+ }}
490
+ >
491
+ <Upload className="w-4 h-4 mr-2" />
492
+ Import
493
+ </Button>
494
+ )}
495
+
496
+ {/* Custom Actions */}
497
+ {customActions.map((action, index) => (
498
+ <Button
499
+ key={index}
500
+ variant={action.variant || 'outline'}
501
+ onClick={action.onClick}
502
+ >
503
+ {action.icon}
504
+ {action.label}
505
+ </Button>
506
+ ))}
507
+ </div>
508
+ </div>
509
+ </div>
510
+ </div>
511
+
512
+ {/* Main Content */}
513
+ <div className="flex-1 min-h-0 overflow-auto">
514
+ <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
515
+ {hasData ? (
516
+ <Card className="p-6" category={category}>
517
+ <DataTable
518
+ data={data}
519
+ columns={columns}
520
+ searchPlaceholder={`Search ${schema.displayNamePlural.toLowerCase()}...`}
521
+ isLoading={isLoading}
522
+ hover={true}
523
+ showSearch={true}
524
+ showPagination={true}
525
+ pageSize={10}
526
+ onRowClick={permissions.view ? handleView : undefined}
527
+ />
528
+ </Card>
529
+ ) : (
530
+ <div className="flex items-center justify-center min-h-96">
531
+ {emptyState || (
532
+ <EmptyState
533
+ title={`No ${schema.displayNamePlural.toLowerCase()} found`}
534
+ description={`Get started by creating your first ${schema.displayName.toLowerCase()}.`}
535
+ action={
536
+ permissions.create ? {
537
+ label: `Create ${schema.displayName}`,
538
+ onClick: handleCreate
539
+ } : undefined
540
+ }
541
+ />
542
+ )}
543
+ </div>
544
+ )}
545
+ </div>
546
+ </div>
547
+
548
+ {/* Create Modal */}
549
+ <Modal
550
+ isOpen={showCreateModal}
551
+ onClose={() => setShowCreateModal(false)}
552
+ title={`Create ${schema.displayName}`}
553
+ size="lg"
554
+ >
555
+ <div className="space-y-4">
556
+ {schema.fields.map(renderFormField)}
557
+
558
+ <div className="flex justify-end gap-2 pt-4">
559
+ <Button
560
+ variant="outline"
561
+ onClick={() => setShowCreateModal(false)}
562
+ >
563
+ Cancel
564
+ </Button>
565
+ <Button
566
+ onClick={() => handleSubmit(false)}
567
+ variant="default"
568
+ >
569
+ Create
570
+ </Button>
571
+ </div>
572
+ </div>
573
+ </Modal>
574
+
575
+ {/* Edit Modal */}
576
+ <Modal
577
+ isOpen={showEditModal}
578
+ onClose={() => setShowEditModal(false)}
579
+ title={`Edit ${schema.displayName}`}
580
+ size="lg"
581
+ >
582
+ <div className="space-y-4">
583
+ {schema.fields.map(renderFormField)}
584
+
585
+ <div className="flex justify-end gap-2 pt-4">
586
+ <Button
587
+ variant="outline"
588
+ onClick={() => setShowEditModal(false)}
589
+ >
590
+ Cancel
591
+ </Button>
592
+ <Button
593
+ onClick={() => handleSubmit(true)}
594
+ variant="default"
595
+ >
596
+ Update
597
+ </Button>
598
+ </div>
599
+ </div>
600
+ </Modal>
601
+
602
+ {/* Delete Confirmation Modal */}
603
+ <Modal
604
+ isOpen={showDeleteConfirm}
605
+ onClose={() => setShowDeleteConfirm(false)}
606
+ title="Confirm Delete"
607
+ size="sm"
608
+ >
609
+ <div className="space-y-4">
610
+ <p>Are you sure you want to delete this {schema.displayName.toLowerCase()}? This action cannot be undone.</p>
611
+
612
+ <div className="flex justify-end gap-2">
613
+ <Button
614
+ variant="outline"
615
+ onClick={() => setShowDeleteConfirm(false)}
616
+ >
617
+ Cancel
618
+ </Button>
619
+ <Button
620
+ variant="destructive"
621
+ onClick={confirmDelete}
622
+ >
623
+ Delete
624
+ </Button>
625
+ </div>
626
+ </div>
627
+ </Modal>
628
+ </div>
629
+ );
630
+ };