@pattern-stack/frontend-patterns 0.0.4 → 0.0.6
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.
- package/dist/atoms/composed/SalesPanel/SalesPanel.d.ts +19 -0
- package/dist/atoms/composed/SalesPanel/SalesPanel.d.ts.map +1 -0
- package/dist/atoms/composed/SalesPanel/index.d.ts +2 -0
- package/dist/atoms/composed/SalesPanel/index.d.ts.map +1 -0
- package/dist/atoms/composed/SalesPanel/mockSalesData.d.ts +63 -0
- package/dist/atoms/composed/SalesPanel/mockSalesData.d.ts.map +1 -0
- package/dist/atoms/composed/index.d.ts +1 -0
- package/dist/atoms/composed/index.d.ts.map +1 -1
- package/dist/atoms/types/entity-config.d.ts +117 -0
- package/dist/atoms/types/entity-config.d.ts.map +1 -0
- package/dist/atoms/types/index.d.ts +2 -0
- package/dist/atoms/types/index.d.ts.map +1 -1
- package/dist/atoms/types/navigation.d.ts +30 -0
- package/dist/atoms/types/navigation.d.ts.map +1 -0
- package/dist/atoms/ui/ErrorBoundary.d.ts +1 -1
- package/dist/atoms/ui/button.d.ts +1 -1
- package/dist/atoms/utils/icon-resolver.d.ts +72 -0
- package/dist/atoms/utils/icon-resolver.d.ts.map +1 -0
- package/dist/atoms/utils/metric-engine.d.ts +30 -0
- package/dist/atoms/utils/metric-engine.d.ts.map +1 -0
- package/dist/atoms/utils/utils.d.ts +2 -0
- package/dist/atoms/utils/utils.d.ts.map +1 -1
- package/dist/features/auth/components/ProtectedRoute.d.ts +1 -1
- package/dist/frontend-patterns.css +1 -1
- package/dist/index.es.js +402 -14
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +402 -14
- package/dist/index.js.map +1 -1
- package/dist/molecules/layout/DashboardWithSidePanel/DashboardWithSidePanel.d.ts +16 -0
- package/dist/molecules/layout/DashboardWithSidePanel/DashboardWithSidePanel.d.ts.map +1 -0
- package/dist/molecules/layout/DashboardWithSidePanel/index.d.ts +2 -0
- package/dist/molecules/layout/DashboardWithSidePanel/index.d.ts.map +1 -0
- package/dist/molecules/layout/NavigationContext.d.ts +15 -0
- package/dist/molecules/layout/NavigationContext.d.ts.map +1 -0
- package/dist/molecules/layout/Sidebar.d.ts.map +1 -1
- package/dist/molecules/layout/SidebarButton/SidebarButton.d.ts +2 -0
- package/dist/molecules/layout/SidebarButton/SidebarButton.d.ts.map +1 -1
- package/dist/molecules/layout/index.d.ts +3 -0
- package/dist/molecules/layout/index.d.ts.map +1 -1
- package/dist/templates/factory.d.ts +2 -1
- package/dist/templates/factory.d.ts.map +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/package.json +7 -3
- package/src/App.tsx +11 -1
- package/src/__tests__/atoms/composed/databadge.test.tsx +106 -0
- package/src/__tests__/atoms/composed/statcard.test.tsx +133 -0
- package/src/__tests__/atoms/utils/icon-resolver.test.tsx +140 -0
- package/src/atoms/composed/SalesPanel/SalesPanel.tsx +116 -0
- package/src/atoms/composed/SalesPanel/index.ts +1 -0
- package/src/atoms/composed/SalesPanel/mockSalesData.ts +151 -0
- package/src/atoms/composed/index.ts +1 -0
- package/src/atoms/types/entity-config.ts +127 -0
- package/src/atoms/types/index.ts +3 -1
- package/src/atoms/types/navigation.ts +43 -0
- package/src/atoms/utils/icon-resolver.tsx +54 -0
- package/src/atoms/utils/metric-engine.ts +236 -0
- package/src/atoms/utils/utils.ts +4 -2
- package/src/molecules/layout/DashboardWithSidePanel/DashboardWithSidePanel.tsx +42 -0
- package/src/molecules/layout/DashboardWithSidePanel/index.ts +1 -0
- package/src/molecules/layout/NavigationContext.tsx +63 -0
- package/src/molecules/layout/Sidebar.tsx +10 -23
- package/src/molecules/layout/SidebarButton/SidebarButton.tsx +32 -10
- package/src/molecules/layout/index.ts +4 -1
- package/src/organisms/entity/CategoryBreakdownPanel.tsx +427 -0
- package/src/organisms/entity/EntityListPanel.tsx +339 -0
- package/src/organisms/entity/MetricsOverviewPanel.tsx +236 -0
- package/src/organisms/entity/TrendAnalysisPanel.tsx +337 -0
- package/src/organisms/entity/index.ts +4 -0
- package/src/organisms/index.ts +5 -1
- package/src/pages/AdminShowcase/AdminDashboardShowcase.tsx +77 -75
- package/src/pages/AdminShowcase/SalesPerformanceDashboard.tsx +158 -0
- package/src/pages/AdminShowcase/index.tsx +2 -1
- package/src/pages/EntityShowcase/EntityManagementShowcase.tsx +137 -0
- package/src/pages/EntityShowcase/EntityPerformanceShowcase.tsx +117 -0
- package/src/pages/EntityShowcase/index.ts +2 -0
- package/src/pages/EntityTemplateExample.tsx +229 -0
- package/src/pages/TestEntityTemplate.tsx +40 -0
- package/src/pages/index.ts +2 -1
- package/src/templates/entity/EntityManagementTemplate.tsx +430 -0
- package/src/templates/entity/EntityPerformanceDashboardTemplate.tsx +277 -0
- package/src/templates/entity/configs/financial-config.ts +141 -0
- package/src/templates/entity/configs/index.ts +1 -0
- package/src/templates/entity/index.ts +3 -0
- package/src/templates/factory.tsx +14 -7
- package/src/templates/financial/FinancialDashboardTemplate.tsx +326 -0
- package/src/templates/index.ts +4 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { MetricConfig, MetricValue, EntityData, FormatConfig, TrendDataPoint } from '../types';
|
|
2
|
+
|
|
3
|
+
export class MetricCalculationEngine {
|
|
4
|
+
static calculateMetric(
|
|
5
|
+
config: MetricConfig,
|
|
6
|
+
data: EntityData[],
|
|
7
|
+
previousData?: EntityData[]
|
|
8
|
+
): MetricValue {
|
|
9
|
+
const currentValue = this.aggregateValue(config, data);
|
|
10
|
+
const previousValue = previousData ? this.aggregateValue(config, previousData) : undefined;
|
|
11
|
+
|
|
12
|
+
const trend = this.calculateTrend(currentValue, previousValue);
|
|
13
|
+
const formattedValue = this.formatValue(currentValue, config.format, config.type);
|
|
14
|
+
|
|
15
|
+
const target = typeof config.target === 'function'
|
|
16
|
+
? config.target(data)
|
|
17
|
+
: config.target;
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
current: currentValue,
|
|
21
|
+
previous: previousValue,
|
|
22
|
+
trend,
|
|
23
|
+
target,
|
|
24
|
+
formattedValue
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private static aggregateValue(config: MetricConfig, data: EntityData[]): number {
|
|
29
|
+
if (!data.length) return 0;
|
|
30
|
+
|
|
31
|
+
const values = data
|
|
32
|
+
.map(item => {
|
|
33
|
+
const value = this.extractValue(item, config.key);
|
|
34
|
+
return typeof value === 'number' ? value : 0;
|
|
35
|
+
})
|
|
36
|
+
.filter(value => !isNaN(value));
|
|
37
|
+
|
|
38
|
+
if (!values.length) return 0;
|
|
39
|
+
|
|
40
|
+
switch (config.aggregation || 'sum') {
|
|
41
|
+
case 'sum':
|
|
42
|
+
return values.reduce((sum, value) => sum + value, 0);
|
|
43
|
+
case 'avg':
|
|
44
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
45
|
+
case 'count':
|
|
46
|
+
return values.length;
|
|
47
|
+
case 'min':
|
|
48
|
+
return Math.min(...values);
|
|
49
|
+
case 'max':
|
|
50
|
+
return Math.max(...values);
|
|
51
|
+
default:
|
|
52
|
+
return values.reduce((sum, value) => sum + value, 0);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private static extractValue(item: EntityData, key: string): unknown {
|
|
57
|
+
return key.split('.').reduce((obj: any, k: string) => obj?.[k], item);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private static calculateTrend(current: number, previous?: number): 'up' | 'down' | 'neutral' {
|
|
61
|
+
if (previous === undefined || previous === 0) return 'neutral';
|
|
62
|
+
|
|
63
|
+
const change = ((current - previous) / Math.abs(previous)) * 100;
|
|
64
|
+
|
|
65
|
+
if (change > 1) return 'up';
|
|
66
|
+
if (change < -1) return 'down';
|
|
67
|
+
return 'neutral';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private static formatValue(value: number, format?: FormatConfig, type?: string): string {
|
|
71
|
+
let formatted = value;
|
|
72
|
+
|
|
73
|
+
if (format?.decimals !== undefined) {
|
|
74
|
+
formatted = Number(value.toFixed(format.decimals));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let result = formatted.toString();
|
|
78
|
+
|
|
79
|
+
if (format?.thousands !== false && Math.abs(formatted) >= 1000) {
|
|
80
|
+
result = formatted.toLocaleString();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
switch (type) {
|
|
84
|
+
case 'currency':
|
|
85
|
+
result = new Intl.NumberFormat('en-US', {
|
|
86
|
+
style: 'currency',
|
|
87
|
+
currency: 'USD',
|
|
88
|
+
minimumFractionDigits: format?.decimals ?? 2,
|
|
89
|
+
maximumFractionDigits: format?.decimals ?? 2
|
|
90
|
+
}).format(formatted);
|
|
91
|
+
break;
|
|
92
|
+
case 'percentage':
|
|
93
|
+
result = `${formatted.toFixed(format?.decimals ?? 1)}%`;
|
|
94
|
+
break;
|
|
95
|
+
case 'duration':
|
|
96
|
+
result = this.formatDuration(formatted);
|
|
97
|
+
break;
|
|
98
|
+
case 'ratio':
|
|
99
|
+
result = `${formatted.toFixed(format?.decimals ?? 2)}:1`;
|
|
100
|
+
break;
|
|
101
|
+
default:
|
|
102
|
+
if (format?.prefix) result = format.prefix + result;
|
|
103
|
+
if (format?.suffix) result = result + format.suffix;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private static formatDuration(minutes: number): string {
|
|
110
|
+
const hours = Math.floor(minutes / 60);
|
|
111
|
+
const mins = Math.floor(minutes % 60);
|
|
112
|
+
|
|
113
|
+
if (hours > 0) {
|
|
114
|
+
return `${hours}h ${mins}m`;
|
|
115
|
+
}
|
|
116
|
+
return `${mins}m`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static calculateTrendData(
|
|
120
|
+
config: MetricConfig,
|
|
121
|
+
data: EntityData[],
|
|
122
|
+
dateField: string = 'date',
|
|
123
|
+
periods: number = 12
|
|
124
|
+
): TrendDataPoint[] {
|
|
125
|
+
const groupedData = this.groupByPeriod(data, dateField);
|
|
126
|
+
const sortedDates = Object.keys(groupedData).sort();
|
|
127
|
+
|
|
128
|
+
return sortedDates.slice(-periods).map(date => ({
|
|
129
|
+
date,
|
|
130
|
+
value: this.aggregateValue(config, groupedData[date]),
|
|
131
|
+
label: this.formatDateLabel(date)
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private static groupByPeriod(data: EntityData[], dateField: string): Record<string, EntityData[]> {
|
|
136
|
+
return data.reduce((groups, item) => {
|
|
137
|
+
const date = this.extractValue(item, dateField);
|
|
138
|
+
if (!date) return groups;
|
|
139
|
+
|
|
140
|
+
const period = new Date(date as string | number | Date).toISOString().split('T')[0];
|
|
141
|
+
|
|
142
|
+
if (!groups[period]) groups[period] = [];
|
|
143
|
+
groups[period].push(item);
|
|
144
|
+
|
|
145
|
+
return groups;
|
|
146
|
+
}, {} as Record<string, EntityData[]>);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private static formatDateLabel(dateString: string): string {
|
|
150
|
+
const date = new Date(dateString);
|
|
151
|
+
return date.toLocaleDateString('en-US', {
|
|
152
|
+
month: 'short',
|
|
153
|
+
day: 'numeric'
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
static calculateCategoryBreakdown(
|
|
158
|
+
data: EntityData[],
|
|
159
|
+
categoryField: string,
|
|
160
|
+
valueField: string,
|
|
161
|
+
maxCategories: number = 8
|
|
162
|
+
) {
|
|
163
|
+
const groups = data.reduce((acc, item) => {
|
|
164
|
+
const category = String(this.extractValue(item, categoryField) || 'Unknown');
|
|
165
|
+
const value = this.extractValue(item, valueField) || 0;
|
|
166
|
+
|
|
167
|
+
if (!acc[category]) acc[category] = 0;
|
|
168
|
+
acc[category] += typeof value === 'number' ? value : 0;
|
|
169
|
+
|
|
170
|
+
return acc;
|
|
171
|
+
}, {} as Record<string, number>);
|
|
172
|
+
|
|
173
|
+
const total = Object.values(groups).reduce((sum, value) => sum + value, 0);
|
|
174
|
+
|
|
175
|
+
let categories = Object.entries(groups)
|
|
176
|
+
.map(([category, value]) => ({
|
|
177
|
+
category,
|
|
178
|
+
value,
|
|
179
|
+
percentage: total > 0 ? (value / total) * 100 : 0
|
|
180
|
+
}))
|
|
181
|
+
.sort((a, b) => b.value - a.value);
|
|
182
|
+
|
|
183
|
+
if (categories.length > maxCategories) {
|
|
184
|
+
const topCategories = categories.slice(0, maxCategories - 1);
|
|
185
|
+
const otherValue = categories.slice(maxCategories - 1)
|
|
186
|
+
.reduce((sum, cat) => sum + cat.value, 0);
|
|
187
|
+
|
|
188
|
+
topCategories.push({
|
|
189
|
+
category: 'Other',
|
|
190
|
+
value: otherValue,
|
|
191
|
+
percentage: total > 0 ? (otherValue / total) * 100 : 0
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
categories = topCategories;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return categories;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
static detectInsights(
|
|
201
|
+
metrics: Record<string, MetricValue>,
|
|
202
|
+
thresholds: Record<string, { warning: number; critical: number }> = {}
|
|
203
|
+
) {
|
|
204
|
+
const insights = [];
|
|
205
|
+
|
|
206
|
+
for (const [key, metric] of Object.entries(metrics)) {
|
|
207
|
+
const threshold = thresholds[key];
|
|
208
|
+
|
|
209
|
+
if (metric.target && metric.current < metric.target * 0.8) {
|
|
210
|
+
insights.push({
|
|
211
|
+
type: 'negative' as const,
|
|
212
|
+
message: `${key} is significantly below target`,
|
|
213
|
+
value: metric.current
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (metric.trend === 'up' && metric.previous && metric.current > metric.previous * 1.2) {
|
|
218
|
+
insights.push({
|
|
219
|
+
type: 'positive' as const,
|
|
220
|
+
message: `${key} showing strong growth`,
|
|
221
|
+
value: metric.current
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (threshold && metric.current >= threshold.critical) {
|
|
226
|
+
insights.push({
|
|
227
|
+
type: 'negative' as const,
|
|
228
|
+
message: `${key} has reached critical threshold`,
|
|
229
|
+
value: metric.current
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return insights.slice(0, 5);
|
|
235
|
+
}
|
|
236
|
+
}
|
package/src/atoms/utils/utils.ts
CHANGED
|
@@ -5,5 +5,7 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
5
5
|
return twMerge(clsx(inputs))
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
// Re-export
|
|
9
|
-
export * from './tooltip-helpers';
|
|
8
|
+
// Re-export utilities
|
|
9
|
+
export * from './tooltip-helpers';
|
|
10
|
+
export * from './metric-engine';
|
|
11
|
+
export { Icon, getIcon, isValidIcon } from './icon-resolver';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { cn } from '../../../atoms/utils/utils';
|
|
3
|
+
|
|
4
|
+
interface DashboardWithSidePanelProps {
|
|
5
|
+
/** Main dashboard content */
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
/** Side panel component to display */
|
|
8
|
+
sidePanel?: React.ReactNode;
|
|
9
|
+
/** Whether to show the side panel */
|
|
10
|
+
showSidePanel?: boolean;
|
|
11
|
+
/** Side panel width in Tailwind units (e.g., 72, 80) */
|
|
12
|
+
sidePanelWidth?: number;
|
|
13
|
+
/** Additional CSS classes */
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const DashboardWithSidePanel: React.FC<DashboardWithSidePanelProps> = ({
|
|
18
|
+
children,
|
|
19
|
+
sidePanel,
|
|
20
|
+
showSidePanel = false,
|
|
21
|
+
sidePanelWidth = 72,
|
|
22
|
+
className
|
|
23
|
+
}) => {
|
|
24
|
+
const marginClass = `pr-${sidePanelWidth}`;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className={cn('relative h-full', className)}>
|
|
28
|
+
{/* Main Dashboard Content */}
|
|
29
|
+
<div
|
|
30
|
+
className={cn(
|
|
31
|
+
'transition-all duration-300',
|
|
32
|
+
showSidePanel ? marginClass : ''
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{/* Side Panel */}
|
|
39
|
+
{showSidePanel && sidePanel}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DashboardWithSidePanel } from './DashboardWithSidePanel';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React, { createContext, useContext, type ReactNode } from 'react'
|
|
2
|
+
import type { NavigationConfig, NavigationItem } from '../../atoms/types'
|
|
3
|
+
|
|
4
|
+
interface NavigationContextType {
|
|
5
|
+
navigation: NavigationConfig
|
|
6
|
+
setNavigation: (config: NavigationConfig) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const NavigationContext = createContext<NavigationContextType | undefined>(undefined)
|
|
10
|
+
|
|
11
|
+
interface NavigationProviderProps {
|
|
12
|
+
children: ReactNode
|
|
13
|
+
initialNavigation?: NavigationConfig
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Default showcase navigation for backwards compatibility
|
|
17
|
+
const defaultShowcaseNavigation: NavigationItem[] = [
|
|
18
|
+
{ value: 'showcase', label: 'Showcase', icon: 'Palette', path: '/showcase', category: 5 },
|
|
19
|
+
{ value: 'admin-dashboard', label: 'Admin Dashboard', icon: 'Shield', path: '/admin/dashboard', category: 2 },
|
|
20
|
+
{ value: 'admin-users', label: 'User Management', icon: 'Users', path: '/admin/users', category: 3 },
|
|
21
|
+
{ value: 'admin-sales', label: 'Sales Dashboard', icon: 'TrendingUp', path: '/admin/sales', category: 4 },
|
|
22
|
+
{ value: 'entity-performance', label: 'Performance Dashboard', icon: 'BarChart3', path: '/entity/performance', category: 6 },
|
|
23
|
+
{ value: 'entity-management', label: 'Entity Management', icon: 'Database', path: '/entity/management', category: 7 },
|
|
24
|
+
{ value: 'entity-template', label: 'Template Example', icon: 'Layout', path: '/entity/template-example', category: 1 }
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
export const NavigationProvider = ({ children, initialNavigation }: NavigationProviderProps) => {
|
|
28
|
+
const [navigation, setNavigation] = React.useState<NavigationConfig>(
|
|
29
|
+
initialNavigation || {
|
|
30
|
+
items: defaultShowcaseNavigation,
|
|
31
|
+
showDefaultNavigation: true,
|
|
32
|
+
defaultExpanded: true
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<NavigationContext.Provider value={{ navigation, setNavigation }}>
|
|
38
|
+
{children}
|
|
39
|
+
</NavigationContext.Provider>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const useNavigation = () => {
|
|
44
|
+
const context = useContext(NavigationContext)
|
|
45
|
+
if (context === undefined) {
|
|
46
|
+
throw new Error('useNavigation must be used within a NavigationProvider')
|
|
47
|
+
}
|
|
48
|
+
return context
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Helper to get navigation items with fallback
|
|
52
|
+
export const getNavigationItems = (config: NavigationConfig): NavigationItem[] => {
|
|
53
|
+
if (config.items.length > 0) {
|
|
54
|
+
return config.items
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Fallback to showcase navigation if no items provided
|
|
58
|
+
if (config.showDefaultNavigation !== false) {
|
|
59
|
+
return defaultShowcaseNavigation
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return []
|
|
63
|
+
}
|
|
@@ -1,23 +1,10 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
1
|
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
|
3
|
-
import { cn } from '../../atoms/utils/utils';
|
|
2
|
+
import { cn, Icon } from '../../atoms/utils/utils';
|
|
4
3
|
import { useSidebar } from './SidebarContext';
|
|
4
|
+
import { useNavigation, getNavigationItems } from './NavigationContext';
|
|
5
5
|
import { SidebarButton } from './SidebarButton';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
Menu,
|
|
9
|
-
X,
|
|
10
|
-
Shield,
|
|
11
|
-
Users
|
|
12
|
-
} from 'lucide-react';
|
|
13
|
-
|
|
14
|
-
interface SidebarItem {
|
|
15
|
-
value: string;
|
|
16
|
-
label: string;
|
|
17
|
-
icon: React.ReactNode;
|
|
18
|
-
path: string;
|
|
19
|
-
category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
20
|
-
}
|
|
6
|
+
import { Menu, X } from 'lucide-react';
|
|
7
|
+
import type { NavigationItem } from '../../atoms/types';
|
|
21
8
|
|
|
22
9
|
interface SidebarProps {
|
|
23
10
|
className?: string;
|
|
@@ -25,15 +12,13 @@ interface SidebarProps {
|
|
|
25
12
|
|
|
26
13
|
export const Sidebar = ({ className }: SidebarProps) => {
|
|
27
14
|
const { isExpanded, toggleSidebar } = useSidebar();
|
|
15
|
+
const { navigation } = useNavigation();
|
|
28
16
|
const location = useLocation();
|
|
29
17
|
const navigate = useNavigate();
|
|
30
18
|
const [searchParams] = useSearchParams();
|
|
31
19
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
{ value: 'admin-dashboard', label: 'Admin Dashboard', icon: <Shield className="w-5 h-5" />, path: '/admin/dashboard', category: 2 },
|
|
35
|
-
{ value: 'admin-users', label: 'User Management', icon: <Users className="w-5 h-5" />, path: '/admin/users', category: 3 }
|
|
36
|
-
];
|
|
20
|
+
// Get navigation items from context
|
|
21
|
+
const items: NavigationItem[] = getNavigationItems(navigation);
|
|
37
22
|
|
|
38
23
|
const handleNavigation = (path: string) => {
|
|
39
24
|
if (path.includes('?')) {
|
|
@@ -100,12 +85,14 @@ export const Sidebar = ({ className }: SidebarProps) => {
|
|
|
100
85
|
return (
|
|
101
86
|
<SidebarButton
|
|
102
87
|
key={item.value}
|
|
103
|
-
icon={item.icon}
|
|
88
|
+
icon={<Icon name={item.icon} className="w-5 h-5" />}
|
|
104
89
|
label={item.label}
|
|
105
90
|
active={isActive}
|
|
106
91
|
category={item.category}
|
|
107
92
|
expanded={isExpanded}
|
|
108
93
|
onClick={() => handleNavigation(item.path)}
|
|
94
|
+
badge={item.badge}
|
|
95
|
+
disabled={item.disabled}
|
|
109
96
|
/>
|
|
110
97
|
);
|
|
111
98
|
})}
|
|
@@ -10,6 +10,8 @@ interface SidebarButtonProps {
|
|
|
10
10
|
expanded?: boolean;
|
|
11
11
|
onClick?: () => void;
|
|
12
12
|
className?: string;
|
|
13
|
+
badge?: string | number;
|
|
14
|
+
disabled?: boolean;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export const SidebarButton: React.FC<SidebarButtonProps> = ({
|
|
@@ -19,12 +21,15 @@ export const SidebarButton: React.FC<SidebarButtonProps> = ({
|
|
|
19
21
|
category = 1,
|
|
20
22
|
expanded = false,
|
|
21
23
|
onClick,
|
|
22
|
-
className
|
|
24
|
+
className,
|
|
25
|
+
badge,
|
|
26
|
+
disabled = false
|
|
23
27
|
}) => {
|
|
24
28
|
return (
|
|
25
29
|
<Button
|
|
26
30
|
variant={active ? "secondary" : "ghost"}
|
|
27
31
|
onClick={onClick}
|
|
32
|
+
disabled={disabled}
|
|
28
33
|
tooltip={!expanded ? label : undefined}
|
|
29
34
|
className={cn(
|
|
30
35
|
"relative w-full justify-start gap-3 h-12",
|
|
@@ -74,8 +79,15 @@ export const SidebarButton: React.FC<SidebarButtonProps> = ({
|
|
|
74
79
|
{label}
|
|
75
80
|
</span>
|
|
76
81
|
|
|
77
|
-
{/* Active indicator
|
|
78
|
-
{
|
|
82
|
+
{/* Badge or Active indicator */}
|
|
83
|
+
{badge ? (
|
|
84
|
+
<span className={cn(
|
|
85
|
+
"px-2 py-0.5 text-xs font-medium rounded-full flex-shrink-0",
|
|
86
|
+
"bg-primary/10 text-primary"
|
|
87
|
+
)}>
|
|
88
|
+
{badge}
|
|
89
|
+
</span>
|
|
90
|
+
) : active && (
|
|
79
91
|
<div className={cn(
|
|
80
92
|
"w-2 h-2 rounded-full flex-shrink-0",
|
|
81
93
|
`bg-category-${category}`
|
|
@@ -84,14 +96,24 @@ export const SidebarButton: React.FC<SidebarButtonProps> = ({
|
|
|
84
96
|
</>
|
|
85
97
|
)}
|
|
86
98
|
|
|
87
|
-
{/* Collapsed
|
|
88
|
-
{!expanded && active && (
|
|
99
|
+
{/* Collapsed indicators */}
|
|
100
|
+
{!expanded && (badge || active) && (
|
|
89
101
|
<div className="absolute -top-1 -right-1">
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
{badge ? (
|
|
103
|
+
<span className={cn(
|
|
104
|
+
"px-1.5 py-0.5 text-xs font-bold rounded-full",
|
|
105
|
+
"bg-primary text-primary-foreground",
|
|
106
|
+
"ring-2 ring-background"
|
|
107
|
+
)}>
|
|
108
|
+
{badge}
|
|
109
|
+
</span>
|
|
110
|
+
) : active && (
|
|
111
|
+
<div className={cn(
|
|
112
|
+
"w-2.5 h-2.5 rounded-full",
|
|
113
|
+
`bg-category-${category}`,
|
|
114
|
+
"ring-2 ring-background"
|
|
115
|
+
)} />
|
|
116
|
+
)}
|
|
95
117
|
</div>
|
|
96
118
|
)}
|
|
97
119
|
</Button>
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export { PageTemplate, type PageTemplateProps } from './PageTemplate';
|
|
2
2
|
export { ShowcaseSection } from './ShowcaseSection';
|
|
3
3
|
export { AppLayout } from './AppLayout';
|
|
4
|
+
export { DashboardWithSidePanel } from './DashboardWithSidePanel';
|
|
4
5
|
export { SectionHeader } from './SectionHeader';
|
|
5
6
|
export { SidebarButton } from './SidebarButton';
|
|
6
7
|
export { AppHeader } from './AppHeader';
|
|
7
|
-
export { SidebarProvider, useSidebar } from './SidebarContext';
|
|
8
|
+
export { SidebarProvider, useSidebar } from './SidebarContext';
|
|
9
|
+
export { NavigationProvider, useNavigation, getNavigationItems } from './NavigationContext';
|
|
10
|
+
export { Sidebar } from './Sidebar';
|