@pattern-stack/frontend-patterns 0.0.1 → 0.0.3

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 (153) hide show
  1. package/README.md +6 -6
  2. package/package.json +3 -5
  3. package/src/App.css +0 -42
  4. package/src/App.tsx +0 -54
  5. package/src/__tests__/README.md +0 -221
  6. package/src/__tests__/atoms/hooks/simple-hooks.test.ts +0 -44
  7. package/src/__tests__/atoms/ui/button.test.tsx +0 -68
  8. package/src/__tests__/atoms/utils/simple.test.ts +0 -18
  9. package/src/__tests__/atoms/utils/utils.test.ts +0 -77
  10. package/src/__tests__/features/auth/simple-auth.test.tsx +0 -40
  11. package/src/__tests__/molecules/layout/simple-layout.test.tsx +0 -81
  12. package/src/__tests__/organisms/showcase/simple-showcase.test.tsx +0 -167
  13. package/src/__tests__/setup.ts +0 -51
  14. package/src/__tests__/utils.tsx +0 -123
  15. package/src/atoms/composed/Accordion/Accordion.tsx +0 -271
  16. package/src/atoms/composed/Accordion/index.ts +0 -1
  17. package/src/atoms/composed/Alert/Alert.tsx +0 -132
  18. package/src/atoms/composed/Alert/index.ts +0 -1
  19. package/src/atoms/composed/Breadcrumb/Breadcrumb.tsx +0 -83
  20. package/src/atoms/composed/Breadcrumb/index.ts +0 -1
  21. package/src/atoms/composed/Chart/Chart.tsx +0 -425
  22. package/src/atoms/composed/Chart/index.ts +0 -2
  23. package/src/atoms/composed/ColorSwatch/ColorSwatch.tsx +0 -72
  24. package/src/atoms/composed/ColorSwatch/index.ts +0 -1
  25. package/src/atoms/composed/DarkModeToggle.tsx +0 -66
  26. package/src/atoms/composed/DataBadge/DataBadge.tsx +0 -81
  27. package/src/atoms/composed/DataBadge/index.ts +0 -1
  28. package/src/atoms/composed/DataTable/DataTable.tsx +0 -394
  29. package/src/atoms/composed/DataTable/TableCellWithTooltip.tsx +0 -41
  30. package/src/atoms/composed/DataTable/index.ts +0 -2
  31. package/src/atoms/composed/DateTimePicker/DateTimePicker.tsx +0 -611
  32. package/src/atoms/composed/DateTimePicker/index.ts +0 -2
  33. package/src/atoms/composed/DetailedCard/DetailedCard.tsx +0 -181
  34. package/src/atoms/composed/DetailedCard/index.ts +0 -2
  35. package/src/atoms/composed/EmptyState/EmptyState.tsx +0 -90
  36. package/src/atoms/composed/EmptyState/index.ts +0 -1
  37. package/src/atoms/composed/FileUpload/FileUpload.tsx +0 -477
  38. package/src/atoms/composed/FileUpload/index.ts +0 -2
  39. package/src/atoms/composed/FormField/FormField.tsx +0 -92
  40. package/src/atoms/composed/FormField/index.ts +0 -1
  41. package/src/atoms/composed/GlobalSearch/GlobalSearch.tsx +0 -37
  42. package/src/atoms/composed/GlobalSearch/index.ts +0 -1
  43. package/src/atoms/composed/IconBadge/IconBadge.tsx +0 -95
  44. package/src/atoms/composed/IconBadge/index.ts +0 -2
  45. package/src/atoms/composed/Modal/Modal.tsx +0 -223
  46. package/src/atoms/composed/Modal/index.ts +0 -2
  47. package/src/atoms/composed/PaletteSwitcher.tsx +0 -386
  48. package/src/atoms/composed/ProgressBar/ProgressBar.tsx +0 -116
  49. package/src/atoms/composed/ProgressBar/index.ts +0 -1
  50. package/src/atoms/composed/StatCard/StatCard.tsx +0 -219
  51. package/src/atoms/composed/StatCard/index.ts +0 -1
  52. package/src/atoms/composed/StyleGuide.tsx +0 -717
  53. package/src/atoms/composed/Toast/Toast.tsx +0 -219
  54. package/src/atoms/composed/Toast/index.ts +0 -1
  55. package/src/atoms/composed/Tooltip/Tooltip.tsx +0 -213
  56. package/src/atoms/composed/Tooltip/index.ts +0 -1
  57. package/src/atoms/composed/UserAvatar/UserAvatar.tsx +0 -139
  58. package/src/atoms/composed/UserAvatar/index.ts +0 -1
  59. package/src/atoms/composed/UserMenu/UserMenu.tsx +0 -16
  60. package/src/atoms/composed/UserMenu/index.ts +0 -1
  61. package/src/atoms/composed/index.ts +0 -29
  62. package/src/atoms/hooks/useApi.ts +0 -80
  63. package/src/atoms/hooks/useHealth.ts +0 -17
  64. package/src/atoms/index.ts +0 -13
  65. package/src/atoms/services/api/client.ts +0 -134
  66. package/src/atoms/services/auth-service.ts +0 -248
  67. package/src/atoms/services/health.ts +0 -15
  68. package/src/atoms/services/index.ts +0 -3
  69. package/src/atoms/shared/config/constants.ts +0 -17
  70. package/src/atoms/shared/config/dashboard-sizes.ts +0 -111
  71. package/src/atoms/shared/config/environment.ts +0 -10
  72. package/src/atoms/shared/index.ts +0 -4
  73. package/src/atoms/shared/styles/color-palettes.css +0 -566
  74. package/src/atoms/types/auth.ts +0 -62
  75. package/src/atoms/types/generated.ts +0 -1469
  76. package/src/atoms/types/index.ts +0 -4
  77. package/src/atoms/types/loading.ts +0 -28
  78. package/src/atoms/ui/Badge.tsx +0 -30
  79. package/src/atoms/ui/ErrorBoundary.tsx +0 -59
  80. package/src/atoms/ui/Select.tsx +0 -53
  81. package/src/atoms/ui/Switch.tsx +0 -42
  82. package/src/atoms/ui/Tabs.tsx +0 -118
  83. package/src/atoms/ui/avatar.tsx +0 -48
  84. package/src/atoms/ui/button.tsx +0 -70
  85. package/src/atoms/ui/card.tsx +0 -76
  86. package/src/atoms/ui/dropdown-menu.tsx +0 -199
  87. package/src/atoms/ui/index.ts +0 -39
  88. package/src/atoms/ui/input.tsx +0 -23
  89. package/src/atoms/ui/label.tsx +0 -23
  90. package/src/atoms/ui/skeleton.tsx +0 -13
  91. package/src/atoms/ui/spinner.tsx +0 -49
  92. package/src/atoms/ui/table.tsx +0 -116
  93. package/src/atoms/utils/animations.ts +0 -135
  94. package/src/atoms/utils/tooltip-helpers.ts +0 -140
  95. package/src/atoms/utils/utils.ts +0 -9
  96. package/src/features/auth/components/LoginForm.tsx +0 -168
  97. package/src/features/auth/components/LogoutButton.tsx +0 -19
  98. package/src/features/auth/components/ProtectedRoute.tsx +0 -60
  99. package/src/features/auth/components/index.ts +0 -4
  100. package/src/features/auth/hooks/index.ts +0 -2
  101. package/src/features/auth/hooks/useAuth.tsx +0 -205
  102. package/src/features/auth/hooks/usePermissions.ts +0 -35
  103. package/src/features/auth/index.ts +0 -2
  104. package/src/features/index.ts +0 -2
  105. package/src/index.css +0 -704
  106. package/src/index.ts +0 -13
  107. package/src/main.tsx +0 -48
  108. package/src/molecules/.gitkeep +0 -0
  109. package/src/molecules/forms/FormGroup.tsx +0 -75
  110. package/src/molecules/forms/SearchInput.tsx +0 -259
  111. package/src/molecules/forms/index.ts +0 -4
  112. package/src/molecules/index.ts +0 -4
  113. package/src/molecules/layout/AppHeader/AppHeader.tsx +0 -42
  114. package/src/molecules/layout/AppHeader/index.ts +0 -1
  115. package/src/molecules/layout/AppLayout.tsx +0 -29
  116. package/src/molecules/layout/PageTemplate.tsx +0 -87
  117. package/src/molecules/layout/SectionHeader/SectionHeader.tsx +0 -87
  118. package/src/molecules/layout/SectionHeader/index.ts +0 -1
  119. package/src/molecules/layout/ShowcaseSection.tsx +0 -57
  120. package/src/molecules/layout/Sidebar.tsx +0 -144
  121. package/src/molecules/layout/SidebarButton/SidebarButton.tsx +0 -99
  122. package/src/molecules/layout/SidebarButton/index.ts +0 -1
  123. package/src/molecules/layout/SidebarContext.tsx +0 -31
  124. package/src/molecules/layout/index.ts +0 -7
  125. package/src/molecules/navigation/NavMenu.tsx +0 -188
  126. package/src/molecules/navigation/Pagination.tsx +0 -172
  127. package/src/molecules/navigation/index.ts +0 -4
  128. package/src/organisms/index.ts +0 -5
  129. package/src/organisms/showcase/ComponentShowcasePage.tsx +0 -2496
  130. package/src/organisms/showcase/index.ts +0 -1
  131. package/src/pages/AdminShowcase/AdminCRUDShowcase.tsx +0 -242
  132. package/src/pages/AdminShowcase/AdminDashboardShowcase.tsx +0 -171
  133. package/src/pages/AdminShowcase/AdminDetailShowcase.tsx +0 -385
  134. package/src/pages/AdminShowcase/index.tsx +0 -3
  135. package/src/pages/ComponentShowcase/BadgesShowcase.tsx +0 -188
  136. package/src/pages/ComponentShowcase/CardsShowcase.tsx +0 -392
  137. package/src/pages/ComponentShowcase/PalettesShowcase.tsx +0 -207
  138. package/src/pages/ComponentShowcase/StatesShowcase.tsx +0 -485
  139. package/src/pages/ComponentShowcase/TablesShowcase.tsx +0 -134
  140. package/src/pages/ComponentShowcase/TypographyShowcase.tsx +0 -255
  141. package/src/pages/ComponentShowcase/index.tsx +0 -188
  142. package/src/pages/index.ts +0 -2
  143. package/src/templates/AuthTemplate.tsx +0 -216
  144. package/src/templates/ComponentShowcaseTemplate.tsx +0 -173
  145. package/src/templates/DashboardTemplate.tsx +0 -232
  146. package/src/templates/DataTemplate.tsx +0 -319
  147. package/src/templates/admin/AdminCRUDTemplate.tsx +0 -630
  148. package/src/templates/admin/AdminDashboardTemplate.tsx +0 -351
  149. package/src/templates/admin/AdminDetailTemplate.tsx +0 -563
  150. package/src/templates/admin/index.ts +0 -29
  151. package/src/templates/factory.tsx +0 -169
  152. package/src/templates/index.ts +0 -37
  153. package/src/vite-env.d.ts +0 -1
@@ -1,630 +0,0 @@
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
- };