@pattern-stack/frontend-patterns 0.0.5 → 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/Accordion/Accordion.d.ts +20 -0
- package/dist/atoms/composed/Accordion/Accordion.d.ts.map +1 -0
- package/dist/atoms/composed/Accordion/index.d.ts +2 -0
- package/dist/atoms/composed/Accordion/index.d.ts.map +1 -0
- package/dist/atoms/composed/Alert/Alert.d.ts +25 -0
- package/dist/atoms/composed/Alert/Alert.d.ts.map +1 -0
- package/dist/atoms/composed/Alert/index.d.ts +2 -0
- package/dist/atoms/composed/Alert/index.d.ts.map +1 -0
- package/dist/atoms/composed/Breadcrumb/Breadcrumb.d.ts +17 -0
- package/dist/atoms/composed/Breadcrumb/Breadcrumb.d.ts.map +1 -0
- package/dist/atoms/composed/Breadcrumb/index.d.ts +2 -0
- package/dist/atoms/composed/Breadcrumb/index.d.ts.map +1 -0
- package/dist/atoms/composed/Chart/Chart.d.ts +37 -0
- package/dist/atoms/composed/Chart/Chart.d.ts.map +1 -0
- package/dist/atoms/composed/Chart/index.d.ts +3 -0
- package/dist/atoms/composed/Chart/index.d.ts.map +1 -0
- package/dist/atoms/composed/ColorSwatch/ColorSwatch.d.ts +19 -0
- package/dist/atoms/composed/ColorSwatch/ColorSwatch.d.ts.map +1 -0
- package/dist/atoms/composed/ColorSwatch/index.d.ts +2 -0
- package/dist/atoms/composed/ColorSwatch/index.d.ts.map +1 -0
- package/dist/atoms/composed/DarkModeToggle.d.ts +4 -0
- package/dist/atoms/composed/DarkModeToggle.d.ts.map +1 -0
- package/dist/atoms/composed/DataBadge/DataBadge.d.ts +13 -0
- package/dist/atoms/composed/DataBadge/DataBadge.d.ts.map +1 -0
- package/dist/atoms/composed/DataBadge/index.d.ts +2 -0
- package/dist/atoms/composed/DataBadge/index.d.ts.map +1 -0
- package/dist/atoms/composed/DataTable/DataTable.d.ts +28 -0
- package/dist/atoms/composed/DataTable/DataTable.d.ts.map +1 -0
- package/dist/atoms/composed/DataTable/TableCellWithTooltip.d.ts +10 -0
- package/dist/atoms/composed/DataTable/TableCellWithTooltip.d.ts.map +1 -0
- package/dist/atoms/composed/DataTable/index.d.ts +3 -0
- package/dist/atoms/composed/DataTable/index.d.ts.map +1 -0
- package/dist/atoms/composed/DateTimePicker/DateTimePicker.d.ts +45 -0
- package/dist/atoms/composed/DateTimePicker/DateTimePicker.d.ts.map +1 -0
- package/dist/atoms/composed/DateTimePicker/index.d.ts +3 -0
- package/dist/atoms/composed/DateTimePicker/index.d.ts.map +1 -0
- package/dist/atoms/composed/DetailedCard/DetailedCard.d.ts +30 -0
- package/dist/atoms/composed/DetailedCard/DetailedCard.d.ts.map +1 -0
- package/dist/atoms/composed/DetailedCard/index.d.ts +3 -0
- package/dist/atoms/composed/DetailedCard/index.d.ts.map +1 -0
- package/dist/atoms/composed/EmptyState/EmptyState.d.ts +18 -0
- package/dist/atoms/composed/EmptyState/EmptyState.d.ts.map +1 -0
- package/dist/atoms/composed/EmptyState/index.d.ts +2 -0
- package/dist/atoms/composed/EmptyState/index.d.ts.map +1 -0
- package/dist/atoms/composed/FileUpload/FileUpload.d.ts +46 -0
- package/dist/atoms/composed/FileUpload/FileUpload.d.ts.map +1 -0
- package/dist/atoms/composed/FileUpload/index.d.ts +3 -0
- package/dist/atoms/composed/FileUpload/index.d.ts.map +1 -0
- package/dist/atoms/composed/FormField/FormField.d.ts +23 -0
- package/dist/atoms/composed/FormField/FormField.d.ts.map +1 -0
- package/dist/atoms/composed/FormField/index.d.ts +2 -0
- package/dist/atoms/composed/FormField/index.d.ts.map +1 -0
- package/dist/atoms/composed/GlobalSearch/GlobalSearch.d.ts +8 -0
- package/dist/atoms/composed/GlobalSearch/GlobalSearch.d.ts.map +1 -0
- package/dist/atoms/composed/GlobalSearch/index.d.ts +2 -0
- package/dist/atoms/composed/GlobalSearch/index.d.ts.map +1 -0
- package/dist/atoms/composed/IconBadge/IconBadge.d.ts +16 -0
- package/dist/atoms/composed/IconBadge/IconBadge.d.ts.map +1 -0
- package/dist/atoms/composed/IconBadge/index.d.ts +3 -0
- package/dist/atoms/composed/IconBadge/index.d.ts.map +1 -0
- package/dist/atoms/composed/Modal/Modal.d.ts +18 -0
- package/dist/atoms/composed/Modal/Modal.d.ts.map +1 -0
- package/dist/atoms/composed/Modal/index.d.ts +3 -0
- package/dist/atoms/composed/Modal/index.d.ts.map +1 -0
- package/dist/atoms/composed/PaletteSwitcher.d.ts +7 -0
- package/dist/atoms/composed/PaletteSwitcher.d.ts.map +1 -0
- package/dist/atoms/composed/ProgressBar/ProgressBar.d.ts +25 -0
- package/dist/atoms/composed/ProgressBar/ProgressBar.d.ts.map +1 -0
- package/dist/atoms/composed/ProgressBar/index.d.ts +2 -0
- package/dist/atoms/composed/ProgressBar/index.d.ts.map +1 -0
- 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/StatCard/StatCard.d.ts +21 -0
- package/dist/atoms/composed/StatCard/StatCard.d.ts.map +1 -0
- package/dist/atoms/composed/StatCard/index.d.ts +2 -0
- package/dist/atoms/composed/StatCard/index.d.ts.map +1 -0
- package/dist/atoms/composed/StyleGuide.d.ts +3 -0
- package/dist/atoms/composed/StyleGuide.d.ts.map +1 -0
- package/dist/atoms/composed/Toast/Toast.d.ts +40 -0
- package/dist/atoms/composed/Toast/Toast.d.ts.map +1 -0
- package/dist/atoms/composed/Toast/index.d.ts +2 -0
- package/dist/atoms/composed/Toast/index.d.ts.map +1 -0
- package/dist/atoms/composed/Tooltip/Tooltip.d.ts +16 -0
- package/dist/atoms/composed/Tooltip/Tooltip.d.ts.map +1 -0
- package/dist/atoms/composed/Tooltip/index.d.ts +2 -0
- package/dist/atoms/composed/Tooltip/index.d.ts.map +1 -0
- package/dist/atoms/composed/UserAvatar/UserAvatar.d.ts +8 -0
- package/dist/atoms/composed/UserAvatar/UserAvatar.d.ts.map +1 -0
- package/dist/atoms/composed/UserAvatar/index.d.ts +2 -0
- package/dist/atoms/composed/UserAvatar/index.d.ts.map +1 -0
- package/dist/atoms/composed/UserMenu/UserMenu.d.ts +8 -0
- package/dist/atoms/composed/UserMenu/UserMenu.d.ts.map +1 -0
- package/dist/atoms/composed/UserMenu/index.d.ts +2 -0
- package/dist/atoms/composed/UserMenu/index.d.ts.map +1 -0
- package/dist/atoms/composed/index.d.ts +26 -0
- package/dist/atoms/composed/index.d.ts.map +1 -0
- package/dist/atoms/hooks/useApi.d.ts +25 -0
- package/dist/atoms/hooks/useApi.d.ts.map +1 -0
- package/dist/atoms/hooks/useHealth.d.ts +19 -0
- package/dist/atoms/hooks/useHealth.d.ts.map +1 -0
- package/dist/atoms/index.d.ts +9 -0
- package/dist/atoms/index.d.ts.map +1 -0
- package/dist/atoms/services/api/client.d.ts +20 -0
- package/dist/atoms/services/api/client.d.ts.map +1 -0
- package/dist/atoms/services/auth-service.d.ts +24 -0
- package/dist/atoms/services/auth-service.d.ts.map +1 -0
- package/dist/atoms/services/health.d.ts +7 -0
- package/dist/atoms/services/health.d.ts.map +1 -0
- package/dist/atoms/services/index.d.ts +4 -0
- package/dist/atoms/services/index.d.ts.map +1 -0
- package/dist/atoms/shared/config/constants.d.ts +15 -0
- package/dist/atoms/shared/config/constants.d.ts.map +1 -0
- package/dist/atoms/shared/config/dashboard-sizes.d.ts +83 -0
- package/dist/atoms/shared/config/dashboard-sizes.d.ts.map +1 -0
- package/dist/atoms/shared/config/environment.d.ts +10 -0
- package/dist/atoms/shared/config/environment.d.ts.map +1 -0
- package/dist/atoms/shared/index.d.ts +4 -0
- package/dist/atoms/shared/index.d.ts.map +1 -0
- package/dist/atoms/types/auth.d.ts +56 -0
- package/dist/atoms/types/auth.d.ts.map +1 -0
- 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/generated.d.ts +1469 -0
- package/dist/atoms/types/generated.d.ts.map +1 -0
- package/dist/atoms/types/index.d.ts +6 -0
- package/dist/atoms/types/index.d.ts.map +1 -0
- package/dist/atoms/types/loading.d.ts +26 -0
- package/dist/atoms/types/loading.d.ts.map +1 -0
- package/dist/atoms/types/navigation.d.ts +30 -0
- package/dist/atoms/types/navigation.d.ts.map +1 -0
- package/dist/atoms/ui/Badge.d.ts +10 -0
- package/dist/atoms/ui/Badge.d.ts.map +1 -0
- package/dist/atoms/ui/ErrorBoundary.d.ts +18 -0
- package/dist/atoms/ui/ErrorBoundary.d.ts.map +1 -0
- package/dist/atoms/ui/Select.d.ts +28 -0
- package/dist/atoms/ui/Select.d.ts.map +1 -0
- package/dist/atoms/ui/Switch.d.ts +9 -0
- package/dist/atoms/ui/Switch.d.ts.map +1 -0
- package/dist/atoms/ui/Tabs.d.ts +30 -0
- package/dist/atoms/ui/Tabs.d.ts.map +1 -0
- package/dist/atoms/ui/avatar.d.ts +7 -0
- package/dist/atoms/ui/avatar.d.ts.map +1 -0
- package/dist/atoms/ui/button.d.ts +14 -0
- package/dist/atoms/ui/button.d.ts.map +1 -0
- package/dist/atoms/ui/card.d.ts +12 -0
- package/dist/atoms/ui/card.d.ts.map +1 -0
- package/dist/atoms/ui/dropdown-menu.d.ts +28 -0
- package/dist/atoms/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/atoms/ui/index.d.ts +15 -0
- package/dist/atoms/ui/index.d.ts.map +1 -0
- package/dist/atoms/ui/input.d.ts +5 -0
- package/dist/atoms/ui/input.d.ts.map +1 -0
- package/dist/atoms/ui/label.d.ts +6 -0
- package/dist/atoms/ui/label.d.ts.map +1 -0
- package/dist/atoms/ui/skeleton.d.ts +3 -0
- package/dist/atoms/ui/skeleton.d.ts.map +1 -0
- package/dist/atoms/ui/spinner.d.ts +14 -0
- package/dist/atoms/ui/spinner.d.ts.map +1 -0
- package/dist/atoms/ui/table.d.ts +11 -0
- package/dist/atoms/ui/table.d.ts.map +1 -0
- package/dist/atoms/utils/animations.d.ts +65 -0
- package/dist/atoms/utils/animations.d.ts.map +1 -0
- 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/tooltip-helpers.d.ts +71 -0
- package/dist/atoms/utils/tooltip-helpers.d.ts.map +1 -0
- package/dist/atoms/utils/utils.d.ts +6 -0
- package/dist/atoms/utils/utils.d.ts.map +1 -0
- package/dist/features/auth/components/LoginForm.d.ts +2 -0
- package/dist/features/auth/components/LoginForm.d.ts.map +1 -0
- package/dist/features/auth/components/LogoutButton.d.ts +2 -0
- package/dist/features/auth/components/LogoutButton.d.ts.map +1 -0
- package/dist/features/auth/components/ProtectedRoute.d.ts +10 -0
- package/dist/features/auth/components/ProtectedRoute.d.ts.map +1 -0
- package/dist/features/auth/components/index.d.ts +4 -0
- package/dist/features/auth/components/index.d.ts.map +1 -0
- package/dist/features/auth/hooks/index.d.ts +3 -0
- package/dist/features/auth/hooks/index.d.ts.map +1 -0
- package/dist/features/auth/hooks/useAuth.d.ts +10 -0
- package/dist/features/auth/hooks/useAuth.d.ts.map +1 -0
- package/dist/features/auth/hooks/usePermissions.d.ts +13 -0
- package/dist/features/auth/hooks/usePermissions.d.ts.map +1 -0
- package/dist/features/auth/index.d.ts +3 -0
- package/dist/features/auth/index.d.ts.map +1 -0
- package/dist/features/index.d.ts +2 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/frontend-patterns.css +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.es.js +131 -1658
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +131 -1658
- package/dist/index.js.map +1 -1
- package/dist/molecules/forms/FormGroup.d.ts +17 -0
- package/dist/molecules/forms/FormGroup.d.ts.map +1 -0
- package/dist/molecules/forms/SearchInput.d.ts +36 -0
- package/dist/molecules/forms/SearchInput.d.ts.map +1 -0
- package/dist/molecules/forms/index.d.ts +3 -0
- package/dist/molecules/forms/index.d.ts.map +1 -0
- package/dist/molecules/index.d.ts +4 -0
- package/dist/molecules/index.d.ts.map +1 -0
- package/dist/molecules/layout/AppHeader/AppHeader.d.ts +7 -0
- package/dist/molecules/layout/AppHeader/AppHeader.d.ts.map +1 -0
- package/dist/molecules/layout/AppHeader/index.d.ts +2 -0
- package/dist/molecules/layout/AppHeader/index.d.ts.map +1 -0
- package/dist/molecules/layout/AppLayout.d.ts +2 -0
- package/dist/molecules/layout/AppLayout.d.ts.map +1 -0
- 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/PageTemplate.d.ts +19 -0
- package/dist/molecules/layout/PageTemplate.d.ts.map +1 -0
- package/dist/molecules/layout/SectionHeader/SectionHeader.d.ts +24 -0
- package/dist/molecules/layout/SectionHeader/SectionHeader.d.ts.map +1 -0
- package/dist/molecules/layout/SectionHeader/index.d.ts +2 -0
- package/dist/molecules/layout/SectionHeader/index.d.ts.map +1 -0
- package/dist/molecules/layout/ShowcaseSection.d.ts +22 -0
- package/dist/molecules/layout/ShowcaseSection.d.ts.map +1 -0
- package/dist/molecules/layout/Sidebar.d.ts +6 -0
- package/dist/molecules/layout/Sidebar.d.ts.map +1 -0
- package/dist/molecules/layout/SidebarButton/SidebarButton.d.ts +15 -0
- package/dist/molecules/layout/SidebarButton/SidebarButton.d.ts.map +1 -0
- package/dist/molecules/layout/SidebarButton/index.d.ts +2 -0
- package/dist/molecules/layout/SidebarButton/index.d.ts.map +1 -0
- package/dist/molecules/layout/SidebarContext.d.ts +12 -0
- package/dist/molecules/layout/SidebarContext.d.ts.map +1 -0
- package/dist/molecules/layout/index.d.ts +11 -0
- package/dist/molecules/layout/index.d.ts.map +1 -0
- package/dist/molecules/navigation/NavMenu.d.ts +20 -0
- package/dist/molecules/navigation/NavMenu.d.ts.map +1 -0
- package/dist/molecules/navigation/Pagination.d.ts +14 -0
- package/dist/molecules/navigation/Pagination.d.ts.map +1 -0
- package/dist/molecules/navigation/index.d.ts +3 -0
- package/dist/molecules/navigation/index.d.ts.map +1 -0
- package/dist/organisms/index.d.ts +2 -0
- package/dist/organisms/index.d.ts.map +1 -0
- package/dist/organisms/showcase/ComponentShowcasePage.d.ts +3 -0
- package/dist/organisms/showcase/ComponentShowcasePage.d.ts.map +1 -0
- package/dist/templates/AuthTemplate.d.ts +68 -0
- package/dist/templates/AuthTemplate.d.ts.map +1 -0
- package/dist/templates/ComponentShowcaseTemplate.d.ts +53 -0
- package/dist/templates/ComponentShowcaseTemplate.d.ts.map +1 -0
- package/dist/templates/DashboardTemplate.d.ts +62 -0
- package/dist/templates/DashboardTemplate.d.ts.map +1 -0
- package/dist/templates/DataTemplate.d.ts +78 -0
- package/dist/templates/DataTemplate.d.ts.map +1 -0
- package/dist/templates/admin/AdminCRUDTemplate.d.ts +105 -0
- package/dist/templates/admin/AdminCRUDTemplate.d.ts.map +1 -0
- package/dist/templates/admin/AdminDashboardTemplate.d.ts +89 -0
- package/dist/templates/admin/AdminDashboardTemplate.d.ts.map +1 -0
- package/dist/templates/admin/AdminDetailTemplate.d.ts +132 -0
- package/dist/templates/admin/AdminDetailTemplate.d.ts.map +1 -0
- package/dist/templates/admin/index.d.ts +4 -0
- package/dist/templates/admin/index.d.ts.map +1 -0
- package/dist/templates/factory.d.ts +29 -0
- package/dist/templates/factory.d.ts.map +1 -0
- package/dist/templates/index.d.ts +7 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/package.json +3 -2
- 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/types/index.ts +1 -0
- package/src/atoms/types/navigation.ts +43 -0
- package/src/atoms/utils/icon-resolver.tsx +54 -0
- package/src/atoms/utils/utils.ts +3 -2
- package/src/molecules/layout/NavigationContext.tsx +63 -0
- package/src/molecules/layout/Sidebar.tsx +10 -31
- package/src/molecules/layout/SidebarButton/SidebarButton.tsx +32 -10
- package/src/molecules/layout/index.ts +3 -1
- package/src/organisms/index.ts +2 -1
- package/src/templates/factory.tsx +14 -7
- package/src/templates/index.ts +2 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, userEvent } from '../../utils'
|
|
3
|
+
import { StatCard } from '../../../atoms/composed/StatCard'
|
|
4
|
+
|
|
5
|
+
describe('StatCard (Composed)', () => {
|
|
6
|
+
it('renders basic stat card with title and value', () => {
|
|
7
|
+
render(<StatCard title="Total Users" value="1,234" />)
|
|
8
|
+
|
|
9
|
+
expect(screen.getByText('Total Users')).toBeInTheDocument()
|
|
10
|
+
expect(screen.getByText('1,234')).toBeInTheDocument()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('renders with subtitle', () => {
|
|
14
|
+
render(
|
|
15
|
+
<StatCard
|
|
16
|
+
title="Revenue"
|
|
17
|
+
value="$45,678"
|
|
18
|
+
subtitle="+12% from last month"
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
expect(screen.getByText('Revenue')).toBeInTheDocument()
|
|
23
|
+
expect(screen.getByText('$45,678')).toBeInTheDocument()
|
|
24
|
+
expect(screen.getByText('+12% from last month')).toBeInTheDocument()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('renders with icon', () => {
|
|
28
|
+
render(
|
|
29
|
+
<StatCard
|
|
30
|
+
title="Orders"
|
|
31
|
+
value="567"
|
|
32
|
+
icon="ShoppingCart"
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
expect(screen.getByText('Orders')).toBeInTheDocument()
|
|
37
|
+
expect(screen.getByText('567')).toBeInTheDocument()
|
|
38
|
+
// Icon should be rendered (we can't easily test the actual icon, but we can test it doesn't crash)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('applies category theming', () => {
|
|
42
|
+
render(<StatCard title="Test" value="123" category={3} />)
|
|
43
|
+
|
|
44
|
+
const card = screen.getByText('Test').closest('[data-component-name="StatCard"]')
|
|
45
|
+
expect(card).toBeInTheDocument()
|
|
46
|
+
expect(card).toHaveAttribute('data-component-name', 'StatCard')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('handles click events', async () => {
|
|
50
|
+
const user = userEvent.setup()
|
|
51
|
+
const handleClick = vi.fn()
|
|
52
|
+
|
|
53
|
+
render(
|
|
54
|
+
<StatCard
|
|
55
|
+
title="Clickable Card"
|
|
56
|
+
value="999"
|
|
57
|
+
onClick={handleClick}
|
|
58
|
+
/>
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const card = screen.getByText('Clickable Card').closest('[role="button"]')
|
|
62
|
+
expect(card).toBeInTheDocument()
|
|
63
|
+
|
|
64
|
+
await user.click(card!)
|
|
65
|
+
expect(handleClick).toHaveBeenCalledOnce()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('applies hover effects when clickable', () => {
|
|
69
|
+
render(
|
|
70
|
+
<StatCard
|
|
71
|
+
title="Hoverable"
|
|
72
|
+
value="555"
|
|
73
|
+
onClick={() => {}}
|
|
74
|
+
/>
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const card = screen.getByText('Hoverable').closest('[role="button"]')
|
|
78
|
+
expect(card).toHaveClass('cursor-pointer')
|
|
79
|
+
expect(card).toHaveClass('hover:shadow-md')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('does not apply click styles when not clickable', () => {
|
|
83
|
+
render(<StatCard title="Static Card" value="111" />)
|
|
84
|
+
|
|
85
|
+
const card = screen.getByText('Static Card').closest('div')
|
|
86
|
+
expect(card).not.toHaveAttribute('role', 'button')
|
|
87
|
+
expect(card).not.toHaveClass('cursor-pointer')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('displays numbers as provided', () => {
|
|
91
|
+
render(<StatCard title="Big Number" value={1234567} />)
|
|
92
|
+
|
|
93
|
+
expect(screen.getByText('1234567')).toBeInTheDocument()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('handles string and number values', () => {
|
|
97
|
+
const { rerender } = render(<StatCard title="String" value="Custom Text" />)
|
|
98
|
+
expect(screen.getByText('Custom Text')).toBeInTheDocument()
|
|
99
|
+
|
|
100
|
+
rerender(<StatCard title="Number" value={42} />)
|
|
101
|
+
expect(screen.getByText('42')).toBeInTheDocument()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('applies custom className', () => {
|
|
105
|
+
render(
|
|
106
|
+
<StatCard
|
|
107
|
+
title="Custom"
|
|
108
|
+
value="123"
|
|
109
|
+
className="custom-stat-card"
|
|
110
|
+
/>
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const card = screen.getByText('Custom').closest('.custom-stat-card')
|
|
114
|
+
expect(card).toBeInTheDocument()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('renders loading state', () => {
|
|
118
|
+
render(<StatCard title="Loading" value="..." isLoading={true} />)
|
|
119
|
+
|
|
120
|
+
const card = screen.getByText('Loading').closest('[data-component-name="StatCard"]')
|
|
121
|
+
expect(card).toBeInTheDocument()
|
|
122
|
+
// Check for skeleton elements in loading state
|
|
123
|
+
const skeletons = card?.querySelectorAll('.animate-pulse')
|
|
124
|
+
expect(skeletons?.length).toBeGreaterThan(0)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('renders with data component name', () => {
|
|
128
|
+
render(<StatCard title="Test Card" value="123" />)
|
|
129
|
+
|
|
130
|
+
const card = screen.getByText('Test Card').closest('[data-component-name="StatCard"]')
|
|
131
|
+
expect(card).toHaveAttribute('data-component-name', 'StatCard')
|
|
132
|
+
})
|
|
133
|
+
})
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen } from '../../utils'
|
|
3
|
+
import { Icon, getIcon, iconMap } from '../../../atoms/utils/icon-resolver'
|
|
4
|
+
|
|
5
|
+
// Mock console.warn to avoid noise in tests
|
|
6
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
7
|
+
|
|
8
|
+
describe('Icon Resolver', () => {
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
consoleSpy.mockClear()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('Icon component', () => {
|
|
14
|
+
it('renders valid icon', () => {
|
|
15
|
+
render(<Icon name="Home" data-testid="home-icon" />)
|
|
16
|
+
|
|
17
|
+
const icon = screen.getByTestId('home-icon')
|
|
18
|
+
expect(icon).toBeInTheDocument()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('applies default className', () => {
|
|
22
|
+
render(<Icon name="Users" data-testid="users-icon" />)
|
|
23
|
+
|
|
24
|
+
const icon = screen.getByTestId('users-icon')
|
|
25
|
+
expect(icon).toHaveClass('w-5', 'h-5')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('applies custom className', () => {
|
|
29
|
+
render(<Icon name="Settings" className="w-8 h-8 text-blue-500" data-testid="settings-icon" />)
|
|
30
|
+
|
|
31
|
+
const icon = screen.getByTestId('settings-icon')
|
|
32
|
+
expect(icon).toHaveClass('w-8', 'h-8', 'text-blue-500')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('applies size prop', () => {
|
|
36
|
+
render(<Icon name="Search" size={24} data-testid="search-icon" />)
|
|
37
|
+
|
|
38
|
+
const icon = screen.getByTestId('search-icon')
|
|
39
|
+
expect(icon).toHaveAttribute('width', '24')
|
|
40
|
+
expect(icon).toHaveAttribute('height', '24')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('renders fallback icon for invalid name', () => {
|
|
44
|
+
render(<Icon name="InvalidIcon" data-testid="fallback-icon" />)
|
|
45
|
+
|
|
46
|
+
const icon = screen.getByTestId('fallback-icon')
|
|
47
|
+
expect(icon).toBeInTheDocument()
|
|
48
|
+
expect(consoleSpy).toHaveBeenCalledWith('Icon "InvalidIcon" not found. Using default Menu icon.')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('handles undefined icon name gracefully', () => {
|
|
52
|
+
render(<Icon name={undefined as any} data-testid="undefined-icon" />)
|
|
53
|
+
|
|
54
|
+
const icon = screen.getByTestId('undefined-icon')
|
|
55
|
+
expect(icon).toBeInTheDocument()
|
|
56
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('getIcon function', () => {
|
|
61
|
+
it('returns icon component for valid name', () => {
|
|
62
|
+
const HomeIcon = getIcon('Home')
|
|
63
|
+
expect(HomeIcon).toBeDefined()
|
|
64
|
+
expect(typeof HomeIcon).toBe('function')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('returns Menu icon for invalid name', () => {
|
|
68
|
+
const InvalidIcon = getIcon('InvalidIcon' as any)
|
|
69
|
+
expect(InvalidIcon).toBeDefined()
|
|
70
|
+
expect(typeof InvalidIcon).toBe('function')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('returns Menu icon for undefined name', () => {
|
|
74
|
+
const UndefinedIcon = getIcon(undefined as any)
|
|
75
|
+
expect(UndefinedIcon).toBeDefined()
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('iconMap', () => {
|
|
80
|
+
it('contains expected common icons', () => {
|
|
81
|
+
const expectedIcons = [
|
|
82
|
+
'Home', 'Users', 'Settings', 'Search', 'Menu',
|
|
83
|
+
'ChevronDown', 'ChevronRight', 'X', 'Plus',
|
|
84
|
+
'ShoppingCart', 'Calendar', 'Mail', 'Phone'
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
expectedIcons.forEach(iconName => {
|
|
88
|
+
expect(iconMap).toHaveProperty(iconName)
|
|
89
|
+
expect(typeof iconMap[iconName]).toBe('function')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('has reasonable number of icons', () => {
|
|
94
|
+
const iconCount = Object.keys(iconMap).length
|
|
95
|
+
expect(iconCount).toBeGreaterThan(30)
|
|
96
|
+
expect(iconCount).toBeLessThan(100) // Keep it manageable
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('all icon map values are functions', () => {
|
|
100
|
+
Object.values(iconMap).forEach(iconComponent => {
|
|
101
|
+
expect(typeof iconComponent).toBe('function')
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('Icon rendering variations', () => {
|
|
107
|
+
const testIcons = ['Home', 'Users', 'Settings', 'Search', 'Bell']
|
|
108
|
+
|
|
109
|
+
testIcons.forEach(iconName => {
|
|
110
|
+
it(`renders ${iconName} icon without errors`, () => {
|
|
111
|
+
render(<Icon name={iconName} data-testid={`${iconName.toLowerCase()}-icon`} />)
|
|
112
|
+
|
|
113
|
+
const icon = screen.getByTestId(`${iconName.toLowerCase()}-icon`)
|
|
114
|
+
expect(icon).toBeInTheDocument()
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe('Integration with real icon names', () => {
|
|
120
|
+
it('renders navigation icons correctly', () => {
|
|
121
|
+
const navIcons = ['Home', 'Users', 'Settings', 'FileText', 'BarChart']
|
|
122
|
+
|
|
123
|
+
navIcons.forEach(iconName => {
|
|
124
|
+
const { unmount } = render(<Icon name={iconName} />)
|
|
125
|
+
// If it renders without throwing, it's valid
|
|
126
|
+
unmount()
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('renders action icons correctly', () => {
|
|
131
|
+
const actionIcons = ['Plus', 'Edit', 'Trash2', 'Save', 'Download']
|
|
132
|
+
|
|
133
|
+
actionIcons.forEach(iconName => {
|
|
134
|
+
const { unmount } = render(<Icon name={iconName} />)
|
|
135
|
+
// If it renders without throwing, it's valid
|
|
136
|
+
unmount()
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
})
|
package/src/atoms/types/index.ts
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Navigation configuration types for createReactApp factory
|
|
2
|
+
|
|
3
|
+
export type IconName =
|
|
4
|
+
| 'Palette' | 'Menu' | 'X' | 'Shield' | 'Users' | 'BarChart3'
|
|
5
|
+
| 'Database' | 'TrendingUp' | 'Layout' | 'Home' | 'Settings'
|
|
6
|
+
| 'Bell' | 'Search' | 'Plus' | 'Edit' | 'Trash2' | 'Eye'
|
|
7
|
+
| 'Download' | 'Upload' | 'Share' | 'Lock' | 'Unlock'
|
|
8
|
+
| 'Mail' | 'Phone' | 'Calendar' | 'Clock' | 'MapPin'
|
|
9
|
+
| 'Star' | 'Heart' | 'Bookmark' | 'Tag' | 'Flag'
|
|
10
|
+
| 'File' | 'Folder' | 'Image' | 'Video' | 'Music'
|
|
11
|
+
| 'ChevronRight' | 'ChevronDown' | 'ChevronLeft' | 'ChevronUp'
|
|
12
|
+
| 'ArrowRight' | 'ArrowLeft' | 'ArrowUp' | 'ArrowDown'
|
|
13
|
+
| 'Check' | 'AlertCircle' | 'Info' | 'HelpCircle'
|
|
14
|
+
|
|
15
|
+
export interface NavigationItem {
|
|
16
|
+
/** Unique identifier for the navigation item */
|
|
17
|
+
value: string
|
|
18
|
+
/** Display label for the navigation item */
|
|
19
|
+
label: string
|
|
20
|
+
/** Path to navigate to when clicked */
|
|
21
|
+
path: string
|
|
22
|
+
/** Icon name from Lucide React icons */
|
|
23
|
+
icon: IconName
|
|
24
|
+
/** Optional category for color theming (1-8) */
|
|
25
|
+
category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
|
|
26
|
+
/** Optional badge text/count to display */
|
|
27
|
+
badge?: string | number
|
|
28
|
+
/** Whether this item is disabled */
|
|
29
|
+
disabled?: boolean
|
|
30
|
+
/** Child navigation items for nested menus */
|
|
31
|
+
children?: NavigationItem[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface NavigationConfig {
|
|
35
|
+
/** Main navigation items */
|
|
36
|
+
items: NavigationItem[]
|
|
37
|
+
/** Whether to show the default showcase navigation as fallback */
|
|
38
|
+
showDefaultNavigation?: boolean
|
|
39
|
+
/** Custom logo or title for the sidebar header */
|
|
40
|
+
logo?: string
|
|
41
|
+
/** Whether the sidebar starts expanded */
|
|
42
|
+
defaultExpanded?: boolean
|
|
43
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Palette, Menu, X, Shield, Users, BarChart3, Database, TrendingUp, Layout,
|
|
3
|
+
Home, Settings, Bell, Search, Plus, Edit, Trash2, Eye, Download, Upload,
|
|
4
|
+
Share, Lock, Unlock, Mail, Phone, Calendar, Clock, MapPin, Star, Heart,
|
|
5
|
+
Bookmark, Tag, Flag, File, Folder, Image, Video, Music, ChevronRight,
|
|
6
|
+
ChevronDown, ChevronLeft, ChevronUp, ArrowRight, ArrowLeft, ArrowUp,
|
|
7
|
+
ArrowDown, Check, AlertCircle, Info, HelpCircle
|
|
8
|
+
} from 'lucide-react'
|
|
9
|
+
import type { IconName } from '../types'
|
|
10
|
+
|
|
11
|
+
// Map of icon names to Lucide React components
|
|
12
|
+
export const iconMap = {
|
|
13
|
+
Palette, Menu, X, Shield, Users, BarChart3, Database, TrendingUp, Layout,
|
|
14
|
+
Home, Settings, Bell, Search, Plus, Edit, Trash2, Eye, Download, Upload,
|
|
15
|
+
Share, Lock, Unlock, Mail, Phone, Calendar, Clock, MapPin, Star, Heart,
|
|
16
|
+
Bookmark, Tag, Flag, File, Folder, Image, Video, Music, ChevronRight,
|
|
17
|
+
ChevronDown, ChevronLeft, ChevronUp, ArrowRight, ArrowLeft, ArrowUp,
|
|
18
|
+
ArrowDown, Check, AlertCircle, Info, HelpCircle
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface IconProps {
|
|
22
|
+
name: IconName
|
|
23
|
+
className?: string
|
|
24
|
+
size?: number
|
|
25
|
+
[key: string]: any // Allow any additional props to be passed through
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolves an icon name to the corresponding Lucide React component
|
|
30
|
+
*/
|
|
31
|
+
export const Icon = ({ name, className = "w-5 h-5", size, ...props }: IconProps) => {
|
|
32
|
+
const IconComponent = iconMap[name]
|
|
33
|
+
|
|
34
|
+
if (!IconComponent) {
|
|
35
|
+
console.warn(`Icon "${name}" not found. Using default Menu icon.`)
|
|
36
|
+
return <Menu className={className} size={size} {...props} />
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return <IconComponent className={className} size={size} {...props} />
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the icon component directly for custom rendering
|
|
44
|
+
*/
|
|
45
|
+
export const getIcon = (name: IconName) => {
|
|
46
|
+
return iconMap[name] || Menu
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if an icon name is valid
|
|
51
|
+
*/
|
|
52
|
+
export const isValidIcon = (name: string): name is IconName => {
|
|
53
|
+
return name in iconMap
|
|
54
|
+
}
|
package/src/atoms/utils/utils.ts
CHANGED
|
@@ -5,6 +5,7 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
5
5
|
return twMerge(clsx(inputs))
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
// Re-export
|
|
8
|
+
// Re-export utilities
|
|
9
9
|
export * from './tooltip-helpers';
|
|
10
|
-
export * from './metric-engine';
|
|
10
|
+
export * from './metric-engine';
|
|
11
|
+
export { Icon, getIcon, isValidIcon } from './icon-resolver';
|
|
@@ -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,27 +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
|
-
BarChart3,
|
|
13
|
-
Database,
|
|
14
|
-
TrendingUp,
|
|
15
|
-
Layout
|
|
16
|
-
} from 'lucide-react';
|
|
17
|
-
|
|
18
|
-
interface SidebarItem {
|
|
19
|
-
value: string;
|
|
20
|
-
label: string;
|
|
21
|
-
icon: React.ReactNode;
|
|
22
|
-
path: string;
|
|
23
|
-
category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
24
|
-
}
|
|
6
|
+
import { Menu, X } from 'lucide-react';
|
|
7
|
+
import type { NavigationItem } from '../../atoms/types';
|
|
25
8
|
|
|
26
9
|
interface SidebarProps {
|
|
27
10
|
className?: string;
|
|
@@ -29,19 +12,13 @@ interface SidebarProps {
|
|
|
29
12
|
|
|
30
13
|
export const Sidebar = ({ className }: SidebarProps) => {
|
|
31
14
|
const { isExpanded, toggleSidebar } = useSidebar();
|
|
15
|
+
const { navigation } = useNavigation();
|
|
32
16
|
const location = useLocation();
|
|
33
17
|
const navigate = useNavigate();
|
|
34
18
|
const [searchParams] = useSearchParams();
|
|
35
19
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
{ value: 'admin-dashboard', label: 'Admin Dashboard', icon: <Shield className="w-5 h-5" />, path: '/admin/dashboard', category: 2 },
|
|
39
|
-
{ value: 'admin-users', label: 'User Management', icon: <Users className="w-5 h-5" />, path: '/admin/users', category: 3 },
|
|
40
|
-
{ value: 'admin-sales', label: 'Sales Dashboard', icon: <TrendingUp className="w-5 h-5" />, path: '/admin/sales', category: 4 },
|
|
41
|
-
{ value: 'entity-performance', label: 'Performance Dashboard', icon: <BarChart3 className="w-5 h-5" />, path: '/entity/performance', category: 6 },
|
|
42
|
-
{ value: 'entity-management', label: 'Entity Management', icon: <Database className="w-5 h-5" />, path: '/entity/management', category: 7 },
|
|
43
|
-
{ value: 'entity-template', label: 'Template Example', icon: <Layout className="w-5 h-5" />, path: '/entity/template-example', category: 1 }
|
|
44
|
-
];
|
|
20
|
+
// Get navigation items from context
|
|
21
|
+
const items: NavigationItem[] = getNavigationItems(navigation);
|
|
45
22
|
|
|
46
23
|
const handleNavigation = (path: string) => {
|
|
47
24
|
if (path.includes('?')) {
|
|
@@ -108,12 +85,14 @@ export const Sidebar = ({ className }: SidebarProps) => {
|
|
|
108
85
|
return (
|
|
109
86
|
<SidebarButton
|
|
110
87
|
key={item.value}
|
|
111
|
-
icon={item.icon}
|
|
88
|
+
icon={<Icon name={item.icon} className="w-5 h-5" />}
|
|
112
89
|
label={item.label}
|
|
113
90
|
active={isActive}
|
|
114
91
|
category={item.category}
|
|
115
92
|
expanded={isExpanded}
|
|
116
93
|
onClick={() => handleNavigation(item.path)}
|
|
94
|
+
badge={item.badge}
|
|
95
|
+
disabled={item.disabled}
|
|
117
96
|
/>
|
|
118
97
|
);
|
|
119
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>
|
|
@@ -5,4 +5,6 @@ export { DashboardWithSidePanel } from './DashboardWithSidePanel';
|
|
|
5
5
|
export { SectionHeader } from './SectionHeader';
|
|
6
6
|
export { SidebarButton } from './SidebarButton';
|
|
7
7
|
export { AppHeader } from './AppHeader';
|
|
8
|
-
export { SidebarProvider, useSidebar } from './SidebarContext';
|
|
8
|
+
export { SidebarProvider, useSidebar } from './SidebarContext';
|
|
9
|
+
export { NavigationProvider, useNavigation, getNavigationItems } from './NavigationContext';
|
|
10
|
+
export { Sidebar } from './Sidebar';
|
package/src/organisms/index.ts
CHANGED
|
@@ -2,13 +2,12 @@ import React from 'react'
|
|
|
2
2
|
import type { ReactNode } from 'react'
|
|
3
3
|
import { createRoot } from 'react-dom/client'
|
|
4
4
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
5
|
-
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
|
6
5
|
import { BrowserRouter, Routes, Route } from 'react-router-dom'
|
|
7
6
|
import { AuthProvider } from '../features/auth'
|
|
8
|
-
import { SidebarProvider } from '../molecules/layout'
|
|
7
|
+
import { SidebarProvider, NavigationProvider } from '../molecules/layout'
|
|
9
8
|
import { AppLayout } from '../molecules/layout'
|
|
10
9
|
import { DashboardTemplate } from './DashboardTemplate'
|
|
11
|
-
import type { AuthConfig } from '../atoms/types'
|
|
10
|
+
import type { AuthConfig, NavigationConfig } from '../atoms/types'
|
|
12
11
|
|
|
13
12
|
// App configuration interface
|
|
14
13
|
export interface AppConfig {
|
|
@@ -24,6 +23,7 @@ export interface AppConfig {
|
|
|
24
23
|
theme?: string
|
|
25
24
|
darkMode?: boolean
|
|
26
25
|
auth?: AuthConfig
|
|
26
|
+
navigation?: NavigationConfig
|
|
27
27
|
customProviders?: React.ComponentType<{ children: ReactNode }>[]
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -50,6 +50,7 @@ export function createReactApp(config: AppConfig | string): AppInstance {
|
|
|
50
50
|
enableQuery = true,
|
|
51
51
|
enableRouting = true,
|
|
52
52
|
auth,
|
|
53
|
+
navigation,
|
|
53
54
|
customProviders = []
|
|
54
55
|
} = appConfig
|
|
55
56
|
|
|
@@ -94,9 +95,11 @@ export function createReactApp(config: AppConfig | string): AppInstance {
|
|
|
94
95
|
// Wrap with layout providers
|
|
95
96
|
if (enableRouting) {
|
|
96
97
|
tree = (
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
<NavigationProvider initialNavigation={navigation}>
|
|
99
|
+
<SidebarProvider>
|
|
100
|
+
{tree}
|
|
101
|
+
</SidebarProvider>
|
|
102
|
+
</NavigationProvider>
|
|
100
103
|
)
|
|
101
104
|
}
|
|
102
105
|
|
|
@@ -110,7 +113,11 @@ export function createReactApp(config: AppConfig | string): AppInstance {
|
|
|
110
113
|
tree = (
|
|
111
114
|
<QueryClientProvider client={queryClient}>
|
|
112
115
|
{tree}
|
|
113
|
-
{import.meta.env.DEV &&
|
|
116
|
+
{import.meta.env.DEV && (
|
|
117
|
+
import('@tanstack/react-query-devtools').then(({ ReactQueryDevtools }) => (
|
|
118
|
+
<ReactQueryDevtools initialIsOpen={false} />
|
|
119
|
+
))
|
|
120
|
+
)}
|
|
114
121
|
</QueryClientProvider>
|
|
115
122
|
)
|
|
116
123
|
}
|