@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.
Files changed (86) hide show
  1. package/dist/atoms/composed/SalesPanel/SalesPanel.d.ts +19 -0
  2. package/dist/atoms/composed/SalesPanel/SalesPanel.d.ts.map +1 -0
  3. package/dist/atoms/composed/SalesPanel/index.d.ts +2 -0
  4. package/dist/atoms/composed/SalesPanel/index.d.ts.map +1 -0
  5. package/dist/atoms/composed/SalesPanel/mockSalesData.d.ts +63 -0
  6. package/dist/atoms/composed/SalesPanel/mockSalesData.d.ts.map +1 -0
  7. package/dist/atoms/composed/index.d.ts +1 -0
  8. package/dist/atoms/composed/index.d.ts.map +1 -1
  9. package/dist/atoms/types/entity-config.d.ts +117 -0
  10. package/dist/atoms/types/entity-config.d.ts.map +1 -0
  11. package/dist/atoms/types/index.d.ts +2 -0
  12. package/dist/atoms/types/index.d.ts.map +1 -1
  13. package/dist/atoms/types/navigation.d.ts +30 -0
  14. package/dist/atoms/types/navigation.d.ts.map +1 -0
  15. package/dist/atoms/ui/ErrorBoundary.d.ts +1 -1
  16. package/dist/atoms/ui/button.d.ts +1 -1
  17. package/dist/atoms/utils/icon-resolver.d.ts +72 -0
  18. package/dist/atoms/utils/icon-resolver.d.ts.map +1 -0
  19. package/dist/atoms/utils/metric-engine.d.ts +30 -0
  20. package/dist/atoms/utils/metric-engine.d.ts.map +1 -0
  21. package/dist/atoms/utils/utils.d.ts +2 -0
  22. package/dist/atoms/utils/utils.d.ts.map +1 -1
  23. package/dist/features/auth/components/ProtectedRoute.d.ts +1 -1
  24. package/dist/frontend-patterns.css +1 -1
  25. package/dist/index.es.js +402 -14
  26. package/dist/index.es.js.map +1 -1
  27. package/dist/index.js +402 -14
  28. package/dist/index.js.map +1 -1
  29. package/dist/molecules/layout/DashboardWithSidePanel/DashboardWithSidePanel.d.ts +16 -0
  30. package/dist/molecules/layout/DashboardWithSidePanel/DashboardWithSidePanel.d.ts.map +1 -0
  31. package/dist/molecules/layout/DashboardWithSidePanel/index.d.ts +2 -0
  32. package/dist/molecules/layout/DashboardWithSidePanel/index.d.ts.map +1 -0
  33. package/dist/molecules/layout/NavigationContext.d.ts +15 -0
  34. package/dist/molecules/layout/NavigationContext.d.ts.map +1 -0
  35. package/dist/molecules/layout/Sidebar.d.ts.map +1 -1
  36. package/dist/molecules/layout/SidebarButton/SidebarButton.d.ts +2 -0
  37. package/dist/molecules/layout/SidebarButton/SidebarButton.d.ts.map +1 -1
  38. package/dist/molecules/layout/index.d.ts +3 -0
  39. package/dist/molecules/layout/index.d.ts.map +1 -1
  40. package/dist/templates/factory.d.ts +2 -1
  41. package/dist/templates/factory.d.ts.map +1 -1
  42. package/dist/templates/index.d.ts.map +1 -1
  43. package/package.json +7 -3
  44. package/src/App.tsx +11 -1
  45. package/src/__tests__/atoms/composed/databadge.test.tsx +106 -0
  46. package/src/__tests__/atoms/composed/statcard.test.tsx +133 -0
  47. package/src/__tests__/atoms/utils/icon-resolver.test.tsx +140 -0
  48. package/src/atoms/composed/SalesPanel/SalesPanel.tsx +116 -0
  49. package/src/atoms/composed/SalesPanel/index.ts +1 -0
  50. package/src/atoms/composed/SalesPanel/mockSalesData.ts +151 -0
  51. package/src/atoms/composed/index.ts +1 -0
  52. package/src/atoms/types/entity-config.ts +127 -0
  53. package/src/atoms/types/index.ts +3 -1
  54. package/src/atoms/types/navigation.ts +43 -0
  55. package/src/atoms/utils/icon-resolver.tsx +54 -0
  56. package/src/atoms/utils/metric-engine.ts +236 -0
  57. package/src/atoms/utils/utils.ts +4 -2
  58. package/src/molecules/layout/DashboardWithSidePanel/DashboardWithSidePanel.tsx +42 -0
  59. package/src/molecules/layout/DashboardWithSidePanel/index.ts +1 -0
  60. package/src/molecules/layout/NavigationContext.tsx +63 -0
  61. package/src/molecules/layout/Sidebar.tsx +10 -23
  62. package/src/molecules/layout/SidebarButton/SidebarButton.tsx +32 -10
  63. package/src/molecules/layout/index.ts +4 -1
  64. package/src/organisms/entity/CategoryBreakdownPanel.tsx +427 -0
  65. package/src/organisms/entity/EntityListPanel.tsx +339 -0
  66. package/src/organisms/entity/MetricsOverviewPanel.tsx +236 -0
  67. package/src/organisms/entity/TrendAnalysisPanel.tsx +337 -0
  68. package/src/organisms/entity/index.ts +4 -0
  69. package/src/organisms/index.ts +5 -1
  70. package/src/pages/AdminShowcase/AdminDashboardShowcase.tsx +77 -75
  71. package/src/pages/AdminShowcase/SalesPerformanceDashboard.tsx +158 -0
  72. package/src/pages/AdminShowcase/index.tsx +2 -1
  73. package/src/pages/EntityShowcase/EntityManagementShowcase.tsx +137 -0
  74. package/src/pages/EntityShowcase/EntityPerformanceShowcase.tsx +117 -0
  75. package/src/pages/EntityShowcase/index.ts +2 -0
  76. package/src/pages/EntityTemplateExample.tsx +229 -0
  77. package/src/pages/TestEntityTemplate.tsx +40 -0
  78. package/src/pages/index.ts +2 -1
  79. package/src/templates/entity/EntityManagementTemplate.tsx +430 -0
  80. package/src/templates/entity/EntityPerformanceDashboardTemplate.tsx +277 -0
  81. package/src/templates/entity/configs/financial-config.ts +141 -0
  82. package/src/templates/entity/configs/index.ts +1 -0
  83. package/src/templates/entity/index.ts +3 -0
  84. package/src/templates/factory.tsx +14 -7
  85. package/src/templates/financial/FinancialDashboardTemplate.tsx +326 -0
  86. 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
+ }
@@ -5,5 +5,7 @@ export function cn(...inputs: ClassValue[]) {
5
5
  return twMerge(clsx(inputs))
6
6
  }
7
7
 
8
- // Re-export tooltip helpers
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
- Palette,
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
- const items: SidebarItem[] = [
33
- { value: 'showcase', label: 'Showcase', icon: <Palette className="w-5 h-5" />, path: '/showcase', category: 5 },
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 dot */}
78
- {active && (
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 active indicator */}
88
- {!expanded && active && (
99
+ {/* Collapsed indicators */}
100
+ {!expanded && (badge || active) && (
89
101
  <div className="absolute -top-1 -right-1">
90
- <div className={cn(
91
- "w-2.5 h-2.5 rounded-full",
92
- `bg-category-${category}`,
93
- "ring-2 ring-background"
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';