@elsapiens/cli 0.1.0 → 0.1.2

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.
@@ -1,191 +1,662 @@
1
+ import { useState, useMemo, useCallback, useEffect } from 'react';
2
+ import { useNavigate, useSearchParams, Link } from 'react-router-dom';
3
+ import { cn } from '@elsapiens/utils';
4
+ import { usePageHeader } from '@elsapiens/providers';
5
+ import { PageHeaderWithBreadcrumbs } from '@elsapiens/layout';
1
6
  import {
2
7
  Button,
3
- Input,
4
- Select,
5
- Card,
6
- CardHeader,
7
- CardTitle,
8
- CardContent,
9
- Table,
10
8
  Badge,
11
- DonutChart,
9
+ Table,
10
+ FilterBar,
11
+ Skeleton,
12
+ Modal,
13
+ Summary,
12
14
  } from '@elsapiens/ui';
13
- import { Search, Filter, Plus, FileText, MoreVertical } from 'lucide-react';
14
-
15
- // Sample data - replace with your data source
16
- const items = [
17
- { id: '1', name: 'Item One', type: 'Type A', status: 'active', date: '2024-01-15' },
18
- { id: '2', name: 'Item Two', type: 'Type B', status: 'pending', date: '2024-01-14' },
19
- { id: '3', name: 'Item Three', type: 'Type A', status: 'active', date: '2024-01-13' },
20
- { id: '4', name: 'Item Four', type: 'Type C', status: 'inactive', date: '2024-01-12' },
21
- { id: '5', name: 'Item Five', type: 'Type B', status: 'active', date: '2024-01-11' },
22
- { id: '6', name: 'Item Six', type: 'Type A', status: 'pending', date: '2024-01-10' },
23
- { id: '7', name: 'Item Seven', type: 'Type C', status: 'active', date: '2024-01-09' },
24
- { id: '8', name: 'Item Eight', type: 'Type B', status: 'inactive', date: '2024-01-08' },
25
- ];
15
+ import type { TableColumn, FilterBarTab, FilterBarField, FilterBarCondition } from '@elsapiens/ui';
16
+ import { Plus, Edit, Trash2, SearchX, <%= icon %>, Info, ChevronDown, ChevronUp, Tag, BarChart3, Layers, Settings, TrendingUp, AlertTriangle } from 'lucide-react';
26
17
 
27
- const itemStats = [
28
- { name: 'Type A', value: 3, color: 'primary' as const },
29
- { name: 'Type B', value: 3, color: 'success' as const },
30
- { name: 'Type C', value: 2, color: 'warning' as const },
18
+ interface <%= pascalName %>ListPageProps {
19
+ className?: string;
20
+ }
21
+
22
+ // Table row type - customize based on your data
23
+ interface <%= pascalName %>Row extends Record<string, unknown> {
24
+ id: string;
25
+ name: string;
26
+ status: 'active' | 'draft' | 'archived';
27
+ category: string;
28
+ createdAt: string;
29
+ }
30
+
31
+ // Sample data - replace with your data source (e.g., GraphQL query)
32
+ const sampleData: <%= pascalName %>Row[] = [
33
+ { id: '1', name: '<%= singularTitle %> One', status: 'active', category: 'Category A', createdAt: '2024-01-15' },
34
+ { id: '2', name: '<%= singularTitle %> Two', status: 'active', category: 'Category B', createdAt: '2024-01-14' },
35
+ { id: '3', name: '<%= singularTitle %> Three', status: 'draft', category: 'Category A', createdAt: '2024-01-13' },
36
+ { id: '4', name: '<%= singularTitle %> Four', status: 'archived', category: 'Category C', createdAt: '2024-01-12' },
37
+ { id: '5', name: '<%= singularTitle %> Five', status: 'active', category: 'Category B', createdAt: '2024-01-11' },
31
38
  ];
32
39
 
33
- const getStatusBadgeVariant = (status: string): 'default' | 'success' | 'warning' | 'info' => {
34
- switch (status) {
35
- case 'active':
36
- return 'success';
37
- case 'pending':
38
- return 'warning';
39
- case 'inactive':
40
- return 'default';
41
- default:
42
- return 'default';
43
- }
40
+ const statusLabels: Record<string, string> = {
41
+ active: 'Active',
42
+ draft: 'Draft',
43
+ archived: 'Archived',
44
44
  };
45
45
 
46
- export default function <%= pascalName %>() {
47
- return (
48
- <div className="p-6">
49
- {/* Page Header - pl-6 to align with table spacer */}
50
- <div className="flex items-center justify-between mb-8 pl-6">
51
- <div>
52
- <h1 className="text-2xl font-bold text-foreground"><%= title %></h1>
53
- <p className="text-muted-foreground"><%= description %></p>
46
+ const statusColors: Record<string, string> = {
47
+ active: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
48
+ draft: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
49
+ archived: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
50
+ };
51
+
52
+ const categoryOptions = [
53
+ { value: 'Category A', label: 'Category A' },
54
+ { value: 'Category B', label: 'Category B' },
55
+ { value: 'Category C', label: 'Category C' },
56
+ ];
57
+
58
+ export function <%= pascalName %>ListPage({ className }: <%= pascalName %>ListPageProps) {
59
+ const navigate = useNavigate();
60
+ const [searchParams, setSearchParams] = useSearchParams();
61
+
62
+ // URL-based state
63
+ const activeTab = searchParams.get('tab') || 'all';
64
+ const searchValue = searchParams.get('search') || '';
65
+ const page = parseInt(searchParams.get('page') || '1', 10);
66
+ const sortBy = searchParams.get('sortBy') || 'name';
67
+ const sortOrder = (searchParams.get('sortOrder') || 'asc') as 'asc' | 'desc';
68
+
69
+ // Filter conditions state (synced with URL)
70
+ const [conditions, setConditions] = useState<FilterBarCondition[]>([]);
71
+
72
+ // Initialize conditions from URL params on mount
73
+ useEffect(() => {
74
+ const categoryParam = searchParams.get('category');
75
+ if (categoryParam) {
76
+ // Support comma-separated values for multi-select
77
+ const categoryValues = categoryParam.split(',').filter(Boolean);
78
+ setConditions([{
79
+ id: 'category-filter',
80
+ fieldId: 'category',
81
+ operator: 'equals',
82
+ value: categoryValues,
83
+ }]);
84
+ }
85
+ }, []); // Only run on mount
86
+
87
+ // Modal state
88
+ const [deleteModalOpen, setDeleteModalOpen] = useState(false);
89
+ const [itemToDelete, setItemToDelete] = useState<<%= pascalName %>Row | null>(null);
90
+
91
+ // Loading state - set to true when fetching data
92
+ const [loading] = useState(false);
93
+
94
+ // Collapsible info state (persisted in localStorage)
95
+ const [isInfoCollapsed, setIsInfoCollapsed] = useState(() => {
96
+ if (typeof window !== 'undefined') {
97
+ return localStorage.getItem('<%= kebabName %>-list-info-collapsed') === 'true';
98
+ }
99
+ return false;
100
+ });
101
+
102
+ const toggleInfoCollapsed = useCallback(() => {
103
+ setIsInfoCollapsed(prev => {
104
+ const newValue = !prev;
105
+ localStorage.setItem('<%= kebabName %>-list-info-collapsed', String(newValue));
106
+ return newValue;
107
+ });
108
+ }, []);
109
+
110
+ // Handle conditions change - sync to URL params
111
+ const handleConditionsChange = useCallback((newConditions: FilterBarCondition[]) => {
112
+ setConditions(newConditions);
113
+ const params = new URLSearchParams(searchParams);
114
+
115
+ // Clear old filter params
116
+ params.delete('category');
117
+
118
+ // Add new filter params from conditions (support multiple values)
119
+ newConditions.forEach(condition => {
120
+ if (condition.fieldId === 'category' && Array.isArray(condition.value) && condition.value.length > 0) {
121
+ // Store multiple values as comma-separated string
122
+ params.set('category', condition.value.join(','));
123
+ }
124
+ });
125
+
126
+ params.delete('page'); // Reset to first page when filters change
127
+ setSearchParams(params);
128
+ }, [searchParams, setSearchParams]);
129
+
130
+ // Extract category filter from conditions (returns array for multi-select)
131
+ const categoryFilters = useMemo(() => {
132
+ const categoryCondition = conditions.find(c => c.fieldId === 'category');
133
+ if (categoryCondition && Array.isArray(categoryCondition.value) && categoryCondition.value.length > 0) {
134
+ return categoryCondition.value;
135
+ }
136
+ return [];
137
+ }, [conditions]);
138
+
139
+ // Filter data based on URL params and conditions
140
+ const filteredData = useMemo(() => {
141
+ let result = [...sampleData];
142
+
143
+ // Filter by search
144
+ if (searchValue) {
145
+ const search = searchValue.toLowerCase();
146
+ result = result.filter(item =>
147
+ item.name.toLowerCase().includes(search)
148
+ );
149
+ }
150
+
151
+ // Filter by category (from conditions - supports multiple selections)
152
+ if (categoryFilters.length > 0) {
153
+ result = result.filter(item => categoryFilters.includes(item.category));
154
+ }
155
+
156
+ // Filter by tab/status
157
+ if (activeTab === 'active') {
158
+ result = result.filter(item => item.status === 'active');
159
+ } else if (activeTab === 'draft') {
160
+ result = result.filter(item => item.status === 'draft');
161
+ } else if (activeTab === 'archived') {
162
+ result = result.filter(item => item.status === 'archived');
163
+ }
164
+
165
+ // Sort
166
+ result.sort((a, b) => {
167
+ const aVal = String(a[sortBy] || '');
168
+ const bVal = String(b[sortBy] || '');
169
+ return sortOrder === 'asc'
170
+ ? aVal.localeCompare(bVal)
171
+ : bVal.localeCompare(aVal);
172
+ });
173
+
174
+ return result;
175
+ }, [searchValue, categoryFilters, activeTab, sortBy, sortOrder]);
176
+
177
+ // Pagination
178
+ const perPage = 20;
179
+ const totalPages = Math.ceil(filteredData.length / perPage);
180
+ const paginatedData = filteredData.slice((page - 1) * perPage, page * perPage);
181
+
182
+ // Check if any filters are active
183
+ const hasActiveFilters = Boolean(searchValue || conditions.length > 0 || activeTab !== 'all');
184
+
185
+ // Summary stats
186
+ const summaryStats = useMemo(() => {
187
+ const total = sampleData.length;
188
+ const active = sampleData.filter(i => i.status === 'active').length;
189
+ const draft = sampleData.filter(i => i.status === 'draft').length;
190
+ const archived = sampleData.filter(i => i.status === 'archived').length;
191
+ return { total, active, draft, archived };
192
+ }, []);
193
+
194
+ // Helper to clear all filters
195
+ const clearFilters = useCallback(() => {
196
+ setConditions([]);
197
+ setSearchParams(new URLSearchParams());
198
+ }, [setSearchParams]);
199
+
200
+ // Set page header
201
+ usePageHeader({
202
+ title: '<%= title %>',
203
+ description: '<%= description %>',
204
+ breadcrumbs: [
205
+ { label: 'Home', href: '/', as: Link },
206
+ { label: '<%= title %>' },
207
+ ],
208
+ });
209
+
210
+ // Filter tabs
211
+ const filterTabs: FilterBarTab[] = [
212
+ { id: 'all', label: 'All', count: sampleData.length },
213
+ { id: 'active', label: 'Active', count: sampleData.filter(i => i.status === 'active').length },
214
+ { id: 'draft', label: 'Draft', count: sampleData.filter(i => i.status === 'draft').length },
215
+ { id: 'archived', label: 'Archived', count: sampleData.filter(i => i.status === 'archived').length },
216
+ ];
217
+
218
+ // Filter fields
219
+ const filterFields: FilterBarField[] = [
220
+ {
221
+ id: 'category',
222
+ label: 'Category',
223
+ type: 'multi-select',
224
+ options: categoryOptions,
225
+ },
226
+ ];
227
+
228
+ // Handlers
229
+ const handleTabChange = useCallback((tabId: string) => {
230
+ const params = new URLSearchParams(searchParams);
231
+ params.set('tab', tabId);
232
+ params.delete('page');
233
+ setSearchParams(params);
234
+ }, [searchParams, setSearchParams]);
235
+
236
+ const handleSearchChange = useCallback((value: string) => {
237
+ const params = new URLSearchParams(searchParams);
238
+ if (value) {
239
+ params.set('search', value);
240
+ } else {
241
+ params.delete('search');
242
+ }
243
+ params.delete('page');
244
+ setSearchParams(params);
245
+ }, [searchParams, setSearchParams]);
246
+
247
+ const handlePageChange = useCallback((newPage: number) => {
248
+ const params = new URLSearchParams(searchParams);
249
+ params.set('page', newPage.toString());
250
+ setSearchParams(params);
251
+ }, [searchParams, setSearchParams]);
252
+
253
+ const handleDelete = useCallback((item: <%= pascalName %>Row) => {
254
+ setItemToDelete(item);
255
+ setDeleteModalOpen(true);
256
+ }, []);
257
+
258
+ const confirmDelete = useCallback(async () => {
259
+ if (!itemToDelete) return;
260
+ // TODO: Call your delete mutation/API here
261
+ console.log('Deleting:', itemToDelete.id);
262
+ setDeleteModalOpen(false);
263
+ setItemToDelete(null);
264
+ }, [itemToDelete]);
265
+
266
+ // Table columns
267
+ const columns: TableColumn<<%= pascalName %>Row>[] = [
268
+ {
269
+ key: 'name',
270
+ header: 'Name',
271
+ render: (item: <%= pascalName %>Row) => (
272
+ <div className="flex items-center el-gap-sm">
273
+ <div className="w-8 h-8 rounded bg-primary/10 flex items-center justify-center flex-shrink-0">
274
+ <<%= icon %> className="w-4 h-4 text-primary" />
275
+ </div>
276
+ <div className="min-w-0">
277
+ <div className="font-medium truncate">{item.name}</div>
278
+ </div>
279
+ </div>
280
+ ),
281
+ },
282
+ {
283
+ key: 'category',
284
+ header: 'Category',
285
+ render: (item: <%= pascalName %>Row) => (
286
+ <Badge variant="outline" size="sm">
287
+ {item.category}
288
+ </Badge>
289
+ ),
290
+ width: '120px',
291
+ },
292
+ {
293
+ key: 'status',
294
+ header: 'Status',
295
+ render: (item: <%= pascalName %>Row) => (
296
+ <Badge className={cn('text-xs', statusColors[item.status])}>
297
+ {statusLabels[item.status]}
298
+ </Badge>
299
+ ),
300
+ width: '100px',
301
+ },
302
+ {
303
+ key: 'createdAt',
304
+ header: 'Created',
305
+ render: (item: <%= pascalName %>Row) => (
306
+ <span className="text-sm text-muted-foreground">{item.createdAt}</span>
307
+ ),
308
+ width: '120px',
309
+ },
310
+ {
311
+ key: 'actions',
312
+ header: '',
313
+ render: (item: <%= pascalName %>Row) => (
314
+ <div className="flex items-center el-gap-xs justify-end">
315
+ <Button
316
+ variant="ghost"
317
+ size="sm"
318
+ onClick={(e) => {
319
+ e.stopPropagation();
320
+ navigate(`/<%= routePath %>/${item.id}/edit`);
321
+ }}
322
+ >
323
+ <Edit className="w-4 h-4" />
324
+ </Button>
325
+ <Button
326
+ variant="ghost"
327
+ size="sm"
328
+ onClick={(e) => {
329
+ e.stopPropagation();
330
+ handleDelete(item);
331
+ }}
332
+ >
333
+ <Trash2 className="w-4 h-4 text-destructive" />
334
+ </Button>
54
335
  </div>
55
- <Button>
336
+ ),
337
+ width: '100px',
338
+ },
339
+ ];
340
+
341
+ return (
342
+ <div className={cn('flex flex-col flex-1', className)}>
343
+ <PageHeaderWithBreadcrumbs variant="card" linkComponent={Link}>
344
+ <Button onClick={() => navigate('/<%= routePath %>/new')}>
56
345
  <Plus className="w-4 h-4 mr-2" />
57
- Add New
346
+ Add <%= singularTitle %>
58
347
  </Button>
59
- </div>
348
+ </PageHeaderWithBreadcrumbs>
349
+
350
+ <div className="flex-1 flex flex-col el-p-lg el-gap-lg" style={{ overflowX: 'clip' }}>
351
+ {/* Summary Bar */}
352
+ <Summary
353
+ items={[
354
+ {
355
+ label: 'Total <%= title %>',
356
+ value: summaryStats.total,
357
+ icon: <<%= icon %> className="w-4 h-4" />,
358
+ },
359
+ {
360
+ label: 'Active',
361
+ value: summaryStats.active,
362
+ icon: <TrendingUp className="w-4 h-4" />,
363
+ },
364
+ {
365
+ label: 'Draft',
366
+ value: summaryStats.draft,
367
+ icon: <AlertTriangle className="w-4 h-4" />,
368
+ },
369
+ {
370
+ label: 'Archived',
371
+ value: summaryStats.archived,
372
+ icon: <Layers className="w-4 h-4" />,
373
+ },
374
+ ]}
375
+ />
376
+
377
+ {/* Main Content Card */}
378
+ <div className="flex-1 flex flex-col border rounded-lg el-p-md" style={{ '--el-container-px': 'var(--el-layout-gap-md)' } as React.CSSProperties}>
379
+ <FilterBar
380
+ tabs={filterTabs}
381
+ activeTab={activeTab}
382
+ onTabChange={handleTabChange}
383
+ fields={filterFields}
384
+ conditions={conditions}
385
+ onConditionsChange={handleConditionsChange}
386
+ search={{
387
+ value: searchValue,
388
+ onChange: handleSearchChange,
389
+ placeholder: 'Search <%= title.toLowerCase() %>...',
390
+ }}
391
+ sort={{
392
+ options: [
393
+ { id: 'name', label: 'Name' },
394
+ { id: 'category', label: 'Category' },
395
+ { id: 'createdAt', label: 'Created Date' },
396
+ ],
397
+ value: sortBy,
398
+ direction: sortOrder,
399
+ onChange: (sortId, direction) => {
400
+ const params = new URLSearchParams(searchParams);
401
+ params.set('sortBy', sortId);
402
+ params.set('sortOrder', direction);
403
+ setSearchParams(params);
404
+ },
405
+ }}
406
+ inCard
407
+ firstInCard
408
+ />
60
409
 
61
- {/* Stats Row */}
62
- <div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mb-8">
63
- {/* Quick Stats */}
64
- <Card padding="none">
65
- <CardContent className="p-6">
66
- <div className="text-center">
67
- <p className="text-3xl font-bold text-foreground">{items.length}</p>
68
- <p className="text-sm text-muted-foreground">Total Items</p>
410
+ {/* Table */}
411
+ {loading && paginatedData.length === 0 ? (
412
+ <div className="el-p-md el-space-y-md">
413
+ {Array.from({ length: 5 }).map((_, i) => (
414
+ <div key={i} className="flex items-center el-gap-md">
415
+ <Skeleton className="h-8 w-8 rounded" />
416
+ <Skeleton className="h-4 w-40 flex-1" />
417
+ <Skeleton className="h-4 w-24" />
418
+ <Skeleton className="h-4 w-20" />
419
+ <Skeleton className="h-4 w-24" />
420
+ </div>
421
+ ))}
69
422
  </div>
70
- </CardContent>
71
- </Card>
72
- <Card padding="none">
73
- <CardContent className="p-6">
74
- <div className="text-center">
75
- <p className="text-3xl font-bold text-foreground">
76
- {items.filter(i => i.status === 'active').length}
77
- </p>
78
- <p className="text-sm text-muted-foreground">Active</p>
423
+ ) : paginatedData.length === 0 && hasActiveFilters ? (
424
+ /* Filtered empty state - no results for current filters */
425
+ <div className="flex-1 flex flex-col items-center justify-center el-gap-md el-py-xl">
426
+ <div className="w-16 h-16 rounded-full bg-muted/50 flex items-center justify-center">
427
+ <SearchX className="w-8 h-8 text-muted-foreground" />
428
+ </div>
429
+ <div className="text-center el-space-y-xs">
430
+ <h3 className="text-lg font-medium text-foreground">No results found</h3>
431
+ <p className="text-sm text-muted-foreground max-w-md">
432
+ No <%= title.toLowerCase() %> match your current filters. Try adjusting your search or filters.
433
+ </p>
434
+ </div>
435
+ <Button variant="outline" onClick={clearFilters}>
436
+ Clear filters
437
+ </Button>
79
438
  </div>
80
- </CardContent>
81
- </Card>
82
- <Card padding="none">
83
- <CardContent className="p-6">
84
- <div className="text-center">
85
- <p className="text-3xl font-bold text-foreground">
86
- {items.filter(i => i.status === 'pending').length}
87
- </p>
88
- <p className="text-sm text-muted-foreground">Pending</p>
439
+ ) : paginatedData.length === 0 ? (
440
+ /* True empty state - no data at all */
441
+ <div className="flex-1 flex flex-col items-center justify-center el-gap-md el-py-xl">
442
+ <div className="w-16 h-16 rounded-full bg-muted/50 flex items-center justify-center">
443
+ <<%= icon %> className="w-8 h-8 text-muted-foreground" />
444
+ </div>
445
+ <div className="text-center el-space-y-xs">
446
+ <h3 className="text-lg font-medium text-foreground">No <%= title.toLowerCase() %> yet</h3>
447
+ <p className="text-sm text-muted-foreground max-w-md">
448
+ Get started by creating your first <%= singularTitle.toLowerCase() %>.
449
+ </p>
450
+ </div>
451
+ <Button onClick={() => navigate('/<%= routePath %>/new')}>
452
+ <Plus className="w-4 h-4 mr-2" />
453
+ Add <%= singularTitle %>
454
+ </Button>
89
455
  </div>
90
- </CardContent>
91
- </Card>
92
- {/* Distribution Chart */}
93
- <Card padding="none">
94
- <CardContent className="p-6">
95
- <DonutChart
96
- data={itemStats}
97
- height={100}
98
- showLegend={false}
99
- centerValue={String(items.length)}
100
- centerSubtext="Items"
456
+ ) : (
457
+ <Table
458
+ data={paginatedData}
459
+ columns={columns}
460
+ onRowClick={(item) => navigate(`/<%= routePath %>/${item.id}`)}
461
+ inCard="last"
101
462
  />
102
- </CardContent>
103
- </Card>
104
- </div>
463
+ )}
464
+ </div>
105
465
 
106
- {/* Filters - pl-6 to align with table spacer */}
107
- <div className="flex items-center gap-4 mb-6 pl-6">
108
- <div className="flex-1 max-w-md">
109
- <div className="relative">
110
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
111
- <Input placeholder="Search..." className="pl-10" hideMessage />
466
+ {/* Pagination */}
467
+ {totalPages > 1 && (
468
+ <div className="flex items-center justify-between border rounded-lg el-px-lg el-py-md bg-muted/20">
469
+ <p className="text-sm text-muted-foreground">
470
+ Showing {(page - 1) * perPage + 1} to{' '}
471
+ {Math.min(page * perPage, filteredData.length)} of{' '}
472
+ {filteredData.length} <%= title.toLowerCase() %>
473
+ </p>
474
+ <div className="flex items-center el-gap-field">
475
+ <Button
476
+ variant="outline"
477
+ size="sm"
478
+ disabled={page <= 1}
479
+ onClick={() => handlePageChange(page - 1)}
480
+ >
481
+ Previous
482
+ </Button>
483
+ <span className="text-sm text-muted-foreground">
484
+ Page {page} of {totalPages}
485
+ </span>
486
+ <Button
487
+ variant="outline"
488
+ size="sm"
489
+ disabled={page >= totalPages}
490
+ onClick={() => handlePageChange(page + 1)}
491
+ >
492
+ Next
493
+ </Button>
494
+ </div>
112
495
  </div>
113
- </div>
114
- <Select
115
- options={[
116
- { value: 'all', label: 'All Types' },
117
- { value: 'type-a', label: 'Type A' },
118
- { value: 'type-b', label: 'Type B' },
119
- { value: 'type-c', label: 'Type C' },
120
- ]}
121
- placeholder="Filter by type"
122
- hideMessage
123
- />
124
- <Button variant="outline">
125
- <Filter className="w-4 h-4 mr-2" />
126
- Filters
127
- </Button>
128
- </div>
496
+ )}
129
497
 
130
- {/* Table */}
131
- <div>
132
- <Card padding="none">
133
- <CardHeader className="p-6">
134
- <CardTitle>All Items</CardTitle>
135
- </CardHeader>
136
- <CardContent className="p-6">
137
- <Table
138
- data={items}
139
- columns={[
140
- {
141
- key: 'name',
142
- header: 'Name',
143
- render: (item) => (
144
- <div className="flex items-center gap-3">
145
- <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
146
- <FileText className="w-5 h-5 text-primary" />
498
+ {/* Collapsible Page Info - shows when items exist */}
499
+ {paginatedData.length > 0 && (
500
+ <div className="relative">
501
+ {isInfoCollapsed ? (
502
+ <button
503
+ onClick={toggleInfoCollapsed}
504
+ className={cn(
505
+ 'w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg border',
506
+ 'bg-gradient-to-r from-primary/[0.02] to-primary/[0.04]',
507
+ 'border-border/50',
508
+ 'text-muted-foreground text-sm',
509
+ 'hover:bg-primary/[0.05] hover:border-primary/30 transition-colors',
510
+ 'cursor-pointer'
511
+ )}
512
+ >
513
+ <Info className="w-4 h-4" />
514
+ <span className="font-medium">About <%= title %></span>
515
+ <ChevronDown className="w-4 h-4" />
516
+ </button>
517
+ ) : (
518
+ <div className="relative">
519
+ <div className={cn(
520
+ 'flex flex-col lg:flex-row items-stretch w-full',
521
+ 'bg-gradient-to-br from-primary/[0.02] via-transparent to-primary/[0.04]',
522
+ 'rounded-xl border border-border/50',
523
+ 'overflow-hidden'
524
+ )}>
525
+ {/* Left Column - Illustration and What Is It */}
526
+ <div className={cn(
527
+ 'flex flex-col items-center justify-center',
528
+ 'lg:w-[40%] lg:min-w-[280px] lg:max-w-[380px]',
529
+ 'bg-gradient-to-br from-primary/5 to-primary/10',
530
+ 'border-b lg:border-b-0 lg:border-r border-border/30',
531
+ 'p-6 lg:p-8'
532
+ )}>
533
+ <div className="relative mb-6">
534
+ <div className="absolute -inset-4 bg-primary/5 rounded-full blur-2xl" />
535
+ <div className="absolute top-0 right-0 w-16 h-16 bg-primary/10 rounded-full blur-xl" />
536
+ <div className="absolute bottom-0 left-0 w-12 h-12 bg-primary/10 rounded-full blur-lg" />
537
+ <div className="relative w-24 h-24 rounded-full bg-primary/10 flex items-center justify-center">
538
+ <<%= icon %> className="w-12 h-12 text-primary" />
147
539
  </div>
148
- <span className="font-medium text-foreground">{item.name}</span>
149
540
  </div>
150
- ),
151
- },
152
- {
153
- key: 'type',
154
- header: 'Type',
155
- render: (item) => (
156
- <Badge variant="info" size="sm">
157
- {item.type}
158
- </Badge>
159
- ),
160
- },
161
- {
162
- key: 'status',
163
- header: 'Status',
164
- render: (item) => (
165
- <Badge variant={getStatusBadgeVariant(item.status)} size="sm">
166
- {item.status}
167
- </Badge>
168
- ),
169
- },
170
- { key: 'date', header: 'Date' },
171
- {
172
- key: 'actions',
173
- header: 'Actions',
174
- align: 'right',
175
- render: () => (
176
- <button className="p-2 rounded-md hover:bg-muted transition-colors">
177
- <MoreVertical className="w-4 h-4 text-muted-foreground" />
178
- </button>
179
- ),
180
- },
181
- ]}
182
- striped={false}
183
- inCard="full"
184
- cardPadding="lg"
185
- />
186
- </CardContent>
187
- </Card>
541
+ <div className="w-full bg-background/60 rounded-lg p-4 border border-primary/20">
542
+ <h4 className="font-medium text-sm text-primary mb-1">What are <%= title %>?</h4>
543
+ <p className="text-sm text-muted-foreground leading-relaxed">
544
+ <%= description %>
545
+ </p>
546
+ </div>
547
+ </div>
548
+
549
+ {/* Right Column - Content */}
550
+ <div className="flex-1 p-6 lg:p-8 el-space-y-lg">
551
+ <div className="el-space-y-sm">
552
+ <h3 className="text-xl lg:text-2xl font-semibold text-foreground">About <%= title %></h3>
553
+ <p className="text-muted-foreground text-base leading-relaxed max-w-lg">
554
+ <%= description %>
555
+ </p>
556
+ </div>
557
+
558
+ {/* Features Grid */}
559
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
560
+ <div className={cn(
561
+ 'flex items-start gap-3 p-3',
562
+ 'bg-background/60 rounded-lg border border-border/40',
563
+ 'transition-colors hover:border-primary/30 hover:bg-primary/[0.02]'
564
+ )}>
565
+ <div className="shrink-0 w-8 h-8 rounded-md bg-primary/10 text-primary flex items-center justify-center">
566
+ <Tag className="w-4 h-4" />
567
+ </div>
568
+ <div className="min-w-0">
569
+ <p className="font-medium text-sm text-foreground">Categories</p>
570
+ <p className="text-xs text-muted-foreground mt-0.5">Organize by category</p>
571
+ </div>
572
+ </div>
573
+ <div className={cn(
574
+ 'flex items-start gap-3 p-3',
575
+ 'bg-background/60 rounded-lg border border-border/40',
576
+ 'transition-colors hover:border-primary/30 hover:bg-primary/[0.02]'
577
+ )}>
578
+ <div className="shrink-0 w-8 h-8 rounded-md bg-primary/10 text-primary flex items-center justify-center">
579
+ <BarChart3 className="w-4 h-4" />
580
+ </div>
581
+ <div className="min-w-0">
582
+ <p className="font-medium text-sm text-foreground">Analytics</p>
583
+ <p className="text-xs text-muted-foreground mt-0.5">Track and analyze data</p>
584
+ </div>
585
+ </div>
586
+ <div className={cn(
587
+ 'flex items-start gap-3 p-3',
588
+ 'bg-background/60 rounded-lg border border-border/40',
589
+ 'transition-colors hover:border-primary/30 hover:bg-primary/[0.02]'
590
+ )}>
591
+ <div className="shrink-0 w-8 h-8 rounded-md bg-primary/10 text-primary flex items-center justify-center">
592
+ <Layers className="w-4 h-4" />
593
+ </div>
594
+ <div className="min-w-0">
595
+ <p className="font-medium text-sm text-foreground">Organization</p>
596
+ <p className="text-xs text-muted-foreground mt-0.5">Keep everything organized</p>
597
+ </div>
598
+ </div>
599
+ <div className={cn(
600
+ 'flex items-start gap-3 p-3',
601
+ 'bg-background/60 rounded-lg border border-border/40',
602
+ 'transition-colors hover:border-primary/30 hover:bg-primary/[0.02]'
603
+ )}>
604
+ <div className="shrink-0 w-8 h-8 rounded-md bg-primary/10 text-primary flex items-center justify-center">
605
+ <Settings className="w-4 h-4" />
606
+ </div>
607
+ <div className="min-w-0">
608
+ <p className="font-medium text-sm text-foreground">Status</p>
609
+ <p className="text-xs text-muted-foreground mt-0.5">Active, draft, or archived</p>
610
+ </div>
611
+ </div>
612
+ </div>
613
+ </div>
614
+ </div>
615
+
616
+ {/* Collapse toggle at bottom-center */}
617
+ <button
618
+ onClick={toggleInfoCollapsed}
619
+ className={cn(
620
+ 'absolute -bottom-px left-1/2 -translate-x-1/2',
621
+ 'flex items-center gap-1.5 px-3 py-1',
622
+ 'bg-background border border-t-0 border-border/50 rounded-b-lg',
623
+ 'text-xs text-muted-foreground',
624
+ 'hover:text-foreground hover:bg-muted/50 transition-colors',
625
+ 'cursor-pointer'
626
+ )}
627
+ >
628
+ <span>Hide</span>
629
+ <ChevronUp className="w-3.5 h-3.5" />
630
+ </button>
631
+ </div>
632
+ )}
633
+ </div>
634
+ )}
188
635
  </div>
636
+
637
+ {/* Delete Confirmation Modal */}
638
+ <Modal
639
+ isOpen={deleteModalOpen}
640
+ onClose={() => setDeleteModalOpen(false)}
641
+ title="Delete <%= singularTitle %>"
642
+ >
643
+ <div className="el-space-y-md">
644
+ <p>
645
+ Are you sure you want to delete <strong>{itemToDelete?.name}</strong>?
646
+ This action cannot be undone.
647
+ </p>
648
+ <div className="flex justify-end el-gap-field">
649
+ <Button variant="outline" onClick={() => setDeleteModalOpen(false)}>
650
+ Cancel
651
+ </Button>
652
+ <Button variant="destructive" onClick={confirmDelete}>
653
+ Delete
654
+ </Button>
655
+ </div>
656
+ </div>
657
+ </Modal>
189
658
  </div>
190
659
  );
191
660
  }
661
+
662
+ export default <%= pascalName %>ListPage;