@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,563 @@
1
+ import React, { useState } from 'react';
2
+ import { cn } from '../../atoms/utils/utils';
3
+ import { SectionHeader } from '../../molecules/layout/SectionHeader';
4
+ import { Card } from '../../atoms/ui/card';
5
+ import { Button } from '../../atoms/ui/button';
6
+ import { DataBadge } from '../../atoms/composed/DataBadge';
7
+ import { UserAvatar } from '../../atoms/composed/UserAvatar';
8
+ import { DataTable, type Column } from '../../atoms/composed/DataTable';
9
+ import {
10
+ ArrowLeft,
11
+ Edit,
12
+ Copy,
13
+ ExternalLink,
14
+ History,
15
+ Activity,
16
+ FileText,
17
+ Save,
18
+ X
19
+ } from 'lucide-react';
20
+
21
+ export interface TabConfig {
22
+ /** Tab ID */
23
+ id: string;
24
+ /** Tab label */
25
+ label: string;
26
+ /** Tab icon */
27
+ icon?: React.ReactNode;
28
+ /** Tab content */
29
+ content: React.ReactNode;
30
+ /** Whether tab is disabled */
31
+ disabled?: boolean;
32
+ /** Badge text for tab */
33
+ badge?: string | number;
34
+ }
35
+
36
+ export interface ActionConfig {
37
+ /** Action ID */
38
+ id: string;
39
+ /** Action label */
40
+ label: string;
41
+ /** Action icon */
42
+ icon?: React.ReactNode;
43
+ /** Action variant */
44
+ variant?: 'default' | 'outline' | 'destructive' | 'secondary';
45
+ /** Action handler */
46
+ onClick: () => void;
47
+ /** Whether action is disabled */
48
+ disabled?: boolean;
49
+ /** Action permission required */
50
+ permission?: string;
51
+ }
52
+
53
+ export interface FieldSection {
54
+ /** Section title */
55
+ title: string;
56
+ /** Section description */
57
+ description?: string;
58
+ /** Section fields */
59
+ fields: DetailField[];
60
+ /** Section category for styling */
61
+ category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
62
+ /** Whether section is collapsible */
63
+ collapsible?: boolean;
64
+ /** Whether section is initially collapsed */
65
+ defaultCollapsed?: boolean;
66
+ }
67
+
68
+ export interface DetailField {
69
+ /** Field key */
70
+ key: string;
71
+ /** Field label */
72
+ label: string;
73
+ /** Field type for rendering */
74
+ type?: 'text' | 'email' | 'phone' | 'url' | 'date' | 'datetime' | 'boolean' | 'badge' | 'avatar' | 'json' | 'custom';
75
+ /** Custom render function */
76
+ render?: (value: unknown, data: Record<string, unknown>) => React.ReactNode;
77
+ /** Whether field is editable inline */
78
+ editable?: boolean;
79
+ /** Field validation for editing */
80
+ validation?: Record<string, unknown>;
81
+ /** Copy to clipboard button */
82
+ copyable?: boolean;
83
+ }
84
+
85
+ export interface RelatedData {
86
+ /** Related data title */
87
+ title: string;
88
+ /** Related data description */
89
+ description?: string;
90
+ /** Related data items */
91
+ data: Record<string, unknown>[];
92
+ /** Columns for related data table */
93
+ columns: Column<Record<string, unknown>>[];
94
+ /** Actions for related data */
95
+ actions?: ActionConfig[];
96
+ /** Whether to show pagination */
97
+ showPagination?: boolean;
98
+ /** Category for styling */
99
+ category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
100
+ }
101
+
102
+ export interface AuditEntry {
103
+ /** Entry ID */
104
+ id: string;
105
+ /** Action performed */
106
+ action: string;
107
+ /** User who performed action */
108
+ user: string;
109
+ /** Timestamp */
110
+ timestamp: Date;
111
+ /** Fields changed */
112
+ changes?: Array<{
113
+ field: string;
114
+ oldValue?: unknown;
115
+ newValue?: unknown;
116
+ }>;
117
+ /** Additional metadata */
118
+ metadata?: Record<string, unknown>;
119
+ }
120
+
121
+ export interface AdminDetailTemplateProps {
122
+ /** Resource title */
123
+ title: string;
124
+ /** Resource subtitle */
125
+ subtitle?: string;
126
+ /** Resource type */
127
+ resourceType: string;
128
+ /** Main resource data */
129
+ data: Record<string, unknown>;
130
+ /** Field sections to display */
131
+ sections: FieldSection[];
132
+ /** Tab configuration */
133
+ tabs?: TabConfig[];
134
+ /** Available actions */
135
+ actions?: ActionConfig[];
136
+ /** Related data sections */
137
+ relatedData?: RelatedData[];
138
+ /** Audit trail */
139
+ auditTrail?: AuditEntry[];
140
+ /** Back navigation handler */
141
+ onBack?: () => void;
142
+ /** Data update handler */
143
+ onUpdate?: (field: string, value: unknown) => Promise<void>;
144
+ /** Whether data is loading */
145
+ isLoading?: boolean;
146
+ /** Additional CSS classes */
147
+ className?: string;
148
+ /** Category for styling */
149
+ category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
150
+ /** Custom sidebar content */
151
+ sidebar?: React.ReactNode;
152
+ /** Whether to show audit trail */
153
+ showAuditTrail?: boolean;
154
+ }
155
+
156
+ export const AdminDetailTemplate: React.FC<AdminDetailTemplateProps> = ({
157
+ title,
158
+ subtitle,
159
+ resourceType,
160
+ data,
161
+ sections,
162
+ tabs = [],
163
+ actions = [],
164
+ relatedData = [],
165
+ auditTrail = [],
166
+ onBack,
167
+ onUpdate,
168
+ isLoading = false,
169
+ className,
170
+ category = 1,
171
+ sidebar,
172
+ showAuditTrail = true
173
+ }) => {
174
+ const [activeTab, setActiveTab] = useState(tabs[0]?.id || 'overview');
175
+ const [editingField, setEditingField] = useState<string | null>(null);
176
+ const [editValue, setEditValue] = useState<unknown>('');
177
+
178
+ const handleEdit = (field: DetailField, currentValue: unknown) => {
179
+ setEditingField(field.key);
180
+ setEditValue(currentValue);
181
+ };
182
+
183
+ const handleSaveEdit = async () => {
184
+ if (editingField && onUpdate) {
185
+ try {
186
+ await onUpdate(editingField, editValue);
187
+ setEditingField(null);
188
+ setEditValue('');
189
+ } catch (error) {
190
+ console.error('Update failed:', error);
191
+ }
192
+ }
193
+ };
194
+
195
+ const handleCancelEdit = () => {
196
+ setEditingField(null);
197
+ setEditValue('');
198
+ };
199
+
200
+ const copyToClipboard = (value: string) => {
201
+ navigator.clipboard.writeText(value);
202
+ // You could show a toast notification here
203
+ };
204
+
205
+ const renderFieldValue = (field: DetailField, value: unknown) => {
206
+ // If currently editing this field
207
+ if (editingField === field.key) {
208
+ return (
209
+ <div className="flex items-center gap-2">
210
+ <input
211
+ type="text"
212
+ value={String(editValue)}
213
+ onChange={(e) => setEditValue(e.target.value)}
214
+ className="flex-1 px-2 py-1 text-sm border border-input rounded"
215
+ autoFocus
216
+ />
217
+ <Button size="sm" onClick={handleSaveEdit}>
218
+ <Save className="w-3 h-3" />
219
+ </Button>
220
+ <Button size="sm" variant="outline" onClick={handleCancelEdit}>
221
+ <X className="w-3 h-3" />
222
+ </Button>
223
+ </div>
224
+ );
225
+ }
226
+
227
+ // Custom render function
228
+ if (field.render) {
229
+ return field.render(value, data);
230
+ }
231
+
232
+ // Default rendering based on type
233
+ const stringValue = String(value || '');
234
+ let displayValue: React.ReactNode;
235
+
236
+ switch (field.type) {
237
+ case 'email':
238
+ displayValue = value ? (
239
+ <a href={`mailto:${stringValue}`} className="text-category-1 hover:underline">
240
+ {stringValue}
241
+ </a>
242
+ ) : '';
243
+ break;
244
+ case 'phone':
245
+ displayValue = value ? (
246
+ <a href={`tel:${stringValue}`} className="text-category-2 hover:underline">
247
+ {stringValue}
248
+ </a>
249
+ ) : '';
250
+ break;
251
+ case 'url':
252
+ displayValue = value ? (
253
+ <a href={stringValue} target="_blank" rel="noopener noreferrer" className="text-category-3 hover:underline inline-flex items-center">
254
+ {stringValue} <ExternalLink className="w-3 h-3 ml-1" />
255
+ </a>
256
+ ) : '';
257
+ break;
258
+ case 'date':
259
+ displayValue = value ? new Date(stringValue).toLocaleDateString() : '';
260
+ break;
261
+ case 'datetime':
262
+ displayValue = value ? new Date(stringValue).toLocaleString() : '';
263
+ break;
264
+ case 'boolean':
265
+ displayValue = (
266
+ <DataBadge
267
+ variant="status"
268
+ status={value ? 'success' : 'neutral'}
269
+ size="sm"
270
+ >
271
+ {value ? 'Yes' : 'No'}
272
+ </DataBadge>
273
+ );
274
+ break;
275
+ case 'badge':
276
+ displayValue = <DataBadge variant="category" category={1} size="sm">{stringValue}</DataBadge>;
277
+ break;
278
+ case 'avatar':
279
+ displayValue = <UserAvatar />;
280
+ break;
281
+ case 'json':
282
+ displayValue = (
283
+ <pre className="text-xs bg-muted p-2 rounded-md overflow-auto max-w-md">
284
+ {JSON.stringify(value, null, 2)}
285
+ </pre>
286
+ );
287
+ break;
288
+ default:
289
+ displayValue = stringValue;
290
+ break;
291
+ }
292
+
293
+ return (
294
+ <div className="flex items-center gap-2">
295
+ <span>{displayValue as React.ReactNode}</span>
296
+ {field.copyable && value != null && (
297
+ <Button
298
+ size="sm"
299
+ variant="ghost"
300
+ onClick={() => copyToClipboard(stringValue)}
301
+ className="h-6 w-6 p-0"
302
+ >
303
+ <Copy className="w-3 h-3" />
304
+ </Button>
305
+ )}
306
+ {field.editable && onUpdate && (
307
+ <Button
308
+ size="sm"
309
+ variant="ghost"
310
+ onClick={() => handleEdit(field, value)}
311
+ className="h-6 w-6 p-0"
312
+ >
313
+ <Edit className="w-3 h-3" />
314
+ </Button>
315
+ )}
316
+ </div>
317
+ );
318
+ };
319
+
320
+ const renderSection = (section: FieldSection) => (
321
+ <Card key={section.title} className="p-6">
322
+ <div className="mb-4">
323
+ <h3 className="text-lg font-semibold text-foreground">{section.title}</h3>
324
+ {section.description && (
325
+ <p className="text-sm text-muted-foreground mt-1">{section.description}</p>
326
+ )}
327
+ </div>
328
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
329
+ {section.fields.map((field) => (
330
+ <div key={field.key} className="space-y-1">
331
+ <label className="text-sm font-medium text-muted-foreground">
332
+ {field.label}
333
+ </label>
334
+ <div className="text-sm text-foreground">
335
+ {renderFieldValue(field, data[field.key])}
336
+ </div>
337
+ </div>
338
+ ))}
339
+ </div>
340
+ </Card>
341
+ );
342
+
343
+ const renderAuditTrail = () => (
344
+ <Card className="p-6">
345
+ <div className="mb-4">
346
+ <h3 className="text-lg font-semibold flex items-center">
347
+ <History className="w-5 h-5 mr-2" />
348
+ Audit Trail
349
+ </h3>
350
+ </div>
351
+ <div className="space-y-4">
352
+ {auditTrail.map((entry) => (
353
+ <div key={entry.id} className="flex items-start space-x-3 pb-4 border-b border-border last:border-b-0">
354
+ <div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0">
355
+ <Activity className="w-4 h-4 text-muted-foreground" />
356
+ </div>
357
+ <div className="flex-1 min-w-0">
358
+ <div className="flex items-center justify-between">
359
+ <p className="text-sm font-medium text-foreground">
360
+ {entry.action}
361
+ </p>
362
+ <span className="text-xs text-muted-foreground">
363
+ {entry.timestamp.toLocaleString()}
364
+ </span>
365
+ </div>
366
+ <p className="text-xs text-muted-foreground mt-1">
367
+ by {entry.user}
368
+ </p>
369
+ {entry.changes && entry.changes.length > 0 && (
370
+ <div className="mt-2 space-y-1">
371
+ {entry.changes.map((change, index) => (
372
+ <div key={index} className="text-xs bg-muted p-2 rounded-md">
373
+ <span className="font-medium">{change.field}:</span>{' '}
374
+ <span className="text-category-8">{String(change.oldValue || 'null')}</span>{' '}
375
+ →{' '}
376
+ <span className="text-category-2">{String(change.newValue || 'null')}</span>
377
+ </div>
378
+ ))}
379
+ </div>
380
+ )}
381
+ </div>
382
+ </div>
383
+ ))}
384
+ </div>
385
+ </Card>
386
+ );
387
+
388
+ const renderRelatedData = (related: RelatedData) => (
389
+ <Card key={related.title} className="p-6">
390
+ <div className="mb-4 flex items-center justify-between">
391
+ <div>
392
+ <h3 className="text-lg font-semibold">{related.title}</h3>
393
+ {related.description && (
394
+ <p className="text-sm text-muted-foreground mt-1">{related.description}</p>
395
+ )}
396
+ </div>
397
+ {related.actions && related.actions.length > 0 && (
398
+ <div className="flex gap-2">
399
+ {related.actions.map((action) => (
400
+ <Button
401
+ key={action.id}
402
+ variant={action.variant || 'outline'}
403
+ size="sm"
404
+ onClick={action.onClick}
405
+ disabled={action.disabled}
406
+ >
407
+ {action.icon}
408
+ {action.label}
409
+ </Button>
410
+ ))}
411
+ </div>
412
+ )}
413
+ </div>
414
+ <DataTable
415
+ data={related.data}
416
+ columns={related.columns}
417
+ isLoading={isLoading}
418
+ hover
419
+ />
420
+ </Card>
421
+ );
422
+
423
+ // Build tabs
424
+ const allTabs = [
425
+ {
426
+ id: 'overview',
427
+ label: 'Overview',
428
+ icon: <FileText className="w-4 h-4" />,
429
+ content: (
430
+ <div className="space-y-6">
431
+ {sections.map(renderSection)}
432
+ </div>
433
+ ),
434
+ },
435
+ ...tabs,
436
+ ...(relatedData.length > 0 ? [{
437
+ id: 'related',
438
+ label: 'Related Data',
439
+ icon: <Activity className="w-4 h-4" />,
440
+ content: (
441
+ <div className="space-y-6">
442
+ {relatedData.map(renderRelatedData)}
443
+ </div>
444
+ ),
445
+ }] : []),
446
+ ...(showAuditTrail && auditTrail.length > 0 ? [{
447
+ id: 'audit',
448
+ label: 'Audit Trail',
449
+ icon: <History className="w-4 h-4" />,
450
+ badge: auditTrail.length,
451
+ content: renderAuditTrail(),
452
+ }] : []),
453
+ ];
454
+
455
+ return (
456
+ <div className={cn('flex flex-col min-h-0 flex-1', className)}>
457
+ {/* Header Section */}
458
+ <div className="flex-shrink-0 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
459
+ <div className="container mx-auto px-6 py-6">
460
+ <div className="flex items-start justify-between">
461
+ <div className="flex-1 min-w-0">
462
+ {onBack && (
463
+ <Button
464
+ variant="ghost"
465
+ onClick={onBack}
466
+ className="mb-4 -ml-3"
467
+ >
468
+ <ArrowLeft className="w-4 h-4 mr-2" />
469
+ Back to {resourceType}
470
+ </Button>
471
+ )}
472
+ <SectionHeader
473
+ title={title}
474
+ subtitle={subtitle}
475
+ size="lg"
476
+ className="text-left"
477
+ />
478
+ </div>
479
+
480
+ {actions.length > 0 && (
481
+ <div className="ml-6 flex-shrink-0 flex items-center gap-2">
482
+ {actions.map((action) => (
483
+ <Button
484
+ key={action.id}
485
+ variant={action.variant || 'outline'}
486
+ onClick={action.onClick}
487
+ disabled={action.disabled}
488
+ >
489
+ {action.icon}
490
+ {action.label}
491
+ </Button>
492
+ ))}
493
+ </div>
494
+ )}
495
+ </div>
496
+ </div>
497
+ </div>
498
+
499
+ {/* Main Content */}
500
+ <div className="flex-1 min-h-0 overflow-auto">
501
+ <div className="container mx-auto px-6 py-6">
502
+ <div className={cn(
503
+ 'grid gap-6',
504
+ sidebar ? 'grid-cols-1 lg:grid-cols-4' : 'grid-cols-1'
505
+ )}>
506
+
507
+ {/* Main Content */}
508
+ <div className={sidebar ? 'lg:col-span-3' : 'col-span-1'}>
509
+ {allTabs.length > 1 ? (
510
+ <div className="space-y-6">
511
+ {/* Tab Navigation */}
512
+ <div className="border-b border-border">
513
+ <nav className="-mb-px flex space-x-8">
514
+ {allTabs.map((tab) => (
515
+ <button
516
+ key={tab.id}
517
+ onClick={() => setActiveTab(tab.id)}
518
+ className={cn(
519
+ 'py-2 px-1 border-b-2 font-medium text-sm whitespace-nowrap',
520
+ activeTab === tab.id
521
+ ? `border-category-${category} text-category-${category}`
522
+ : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
523
+ )}
524
+ >
525
+ <div className="flex items-center gap-2">
526
+ {tab.icon}
527
+ {tab.label}
528
+ {tab.badge && (
529
+ <span className={cn(
530
+ 'ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium',
531
+ `bg-category-${category}/20 text-category-${category}`
532
+ )}>
533
+ {tab.badge}
534
+ </span>
535
+ )}
536
+ </div>
537
+ </button>
538
+ ))}
539
+ </nav>
540
+ </div>
541
+
542
+ {/* Tab Content */}
543
+ <div>
544
+ {allTabs.find(tab => tab.id === activeTab)?.content}
545
+ </div>
546
+ </div>
547
+ ) : (
548
+ allTabs[0]?.content
549
+ )}
550
+ </div>
551
+
552
+ {/* Sidebar */}
553
+ {sidebar && (
554
+ <div className="lg:col-span-1">
555
+ {sidebar}
556
+ </div>
557
+ )}
558
+ </div>
559
+ </div>
560
+ </div>
561
+ </div>
562
+ );
563
+ };
@@ -0,0 +1,29 @@
1
+ // Admin template exports
2
+ export {
3
+ AdminDashboardTemplate,
4
+ type AdminDashboardTemplateProps,
5
+ type MetricCard,
6
+ type ChartConfig,
7
+ type ActivityItem,
8
+ type AlertItem
9
+ } from './AdminDashboardTemplate';
10
+
11
+ export {
12
+ AdminCRUDTemplate,
13
+ type AdminCRUDTemplateProps,
14
+ type ResourceSchema,
15
+ type ResourceField,
16
+ type CRUDPermissions,
17
+ type CRUDActions
18
+ } from './AdminCRUDTemplate';
19
+
20
+ export {
21
+ AdminDetailTemplate,
22
+ type AdminDetailTemplateProps,
23
+ type TabConfig,
24
+ type ActionConfig,
25
+ type FieldSection,
26
+ type DetailField,
27
+ type RelatedData,
28
+ type AuditEntry
29
+ } from './AdminDetailTemplate';