@pattern-stack/frontend-patterns 0.0.1 → 0.0.3

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 (153) hide show
  1. package/README.md +6 -6
  2. package/package.json +3 -5
  3. package/src/App.css +0 -42
  4. package/src/App.tsx +0 -54
  5. package/src/__tests__/README.md +0 -221
  6. package/src/__tests__/atoms/hooks/simple-hooks.test.ts +0 -44
  7. package/src/__tests__/atoms/ui/button.test.tsx +0 -68
  8. package/src/__tests__/atoms/utils/simple.test.ts +0 -18
  9. package/src/__tests__/atoms/utils/utils.test.ts +0 -77
  10. package/src/__tests__/features/auth/simple-auth.test.tsx +0 -40
  11. package/src/__tests__/molecules/layout/simple-layout.test.tsx +0 -81
  12. package/src/__tests__/organisms/showcase/simple-showcase.test.tsx +0 -167
  13. package/src/__tests__/setup.ts +0 -51
  14. package/src/__tests__/utils.tsx +0 -123
  15. package/src/atoms/composed/Accordion/Accordion.tsx +0 -271
  16. package/src/atoms/composed/Accordion/index.ts +0 -1
  17. package/src/atoms/composed/Alert/Alert.tsx +0 -132
  18. package/src/atoms/composed/Alert/index.ts +0 -1
  19. package/src/atoms/composed/Breadcrumb/Breadcrumb.tsx +0 -83
  20. package/src/atoms/composed/Breadcrumb/index.ts +0 -1
  21. package/src/atoms/composed/Chart/Chart.tsx +0 -425
  22. package/src/atoms/composed/Chart/index.ts +0 -2
  23. package/src/atoms/composed/ColorSwatch/ColorSwatch.tsx +0 -72
  24. package/src/atoms/composed/ColorSwatch/index.ts +0 -1
  25. package/src/atoms/composed/DarkModeToggle.tsx +0 -66
  26. package/src/atoms/composed/DataBadge/DataBadge.tsx +0 -81
  27. package/src/atoms/composed/DataBadge/index.ts +0 -1
  28. package/src/atoms/composed/DataTable/DataTable.tsx +0 -394
  29. package/src/atoms/composed/DataTable/TableCellWithTooltip.tsx +0 -41
  30. package/src/atoms/composed/DataTable/index.ts +0 -2
  31. package/src/atoms/composed/DateTimePicker/DateTimePicker.tsx +0 -611
  32. package/src/atoms/composed/DateTimePicker/index.ts +0 -2
  33. package/src/atoms/composed/DetailedCard/DetailedCard.tsx +0 -181
  34. package/src/atoms/composed/DetailedCard/index.ts +0 -2
  35. package/src/atoms/composed/EmptyState/EmptyState.tsx +0 -90
  36. package/src/atoms/composed/EmptyState/index.ts +0 -1
  37. package/src/atoms/composed/FileUpload/FileUpload.tsx +0 -477
  38. package/src/atoms/composed/FileUpload/index.ts +0 -2
  39. package/src/atoms/composed/FormField/FormField.tsx +0 -92
  40. package/src/atoms/composed/FormField/index.ts +0 -1
  41. package/src/atoms/composed/GlobalSearch/GlobalSearch.tsx +0 -37
  42. package/src/atoms/composed/GlobalSearch/index.ts +0 -1
  43. package/src/atoms/composed/IconBadge/IconBadge.tsx +0 -95
  44. package/src/atoms/composed/IconBadge/index.ts +0 -2
  45. package/src/atoms/composed/Modal/Modal.tsx +0 -223
  46. package/src/atoms/composed/Modal/index.ts +0 -2
  47. package/src/atoms/composed/PaletteSwitcher.tsx +0 -386
  48. package/src/atoms/composed/ProgressBar/ProgressBar.tsx +0 -116
  49. package/src/atoms/composed/ProgressBar/index.ts +0 -1
  50. package/src/atoms/composed/StatCard/StatCard.tsx +0 -219
  51. package/src/atoms/composed/StatCard/index.ts +0 -1
  52. package/src/atoms/composed/StyleGuide.tsx +0 -717
  53. package/src/atoms/composed/Toast/Toast.tsx +0 -219
  54. package/src/atoms/composed/Toast/index.ts +0 -1
  55. package/src/atoms/composed/Tooltip/Tooltip.tsx +0 -213
  56. package/src/atoms/composed/Tooltip/index.ts +0 -1
  57. package/src/atoms/composed/UserAvatar/UserAvatar.tsx +0 -139
  58. package/src/atoms/composed/UserAvatar/index.ts +0 -1
  59. package/src/atoms/composed/UserMenu/UserMenu.tsx +0 -16
  60. package/src/atoms/composed/UserMenu/index.ts +0 -1
  61. package/src/atoms/composed/index.ts +0 -29
  62. package/src/atoms/hooks/useApi.ts +0 -80
  63. package/src/atoms/hooks/useHealth.ts +0 -17
  64. package/src/atoms/index.ts +0 -13
  65. package/src/atoms/services/api/client.ts +0 -134
  66. package/src/atoms/services/auth-service.ts +0 -248
  67. package/src/atoms/services/health.ts +0 -15
  68. package/src/atoms/services/index.ts +0 -3
  69. package/src/atoms/shared/config/constants.ts +0 -17
  70. package/src/atoms/shared/config/dashboard-sizes.ts +0 -111
  71. package/src/atoms/shared/config/environment.ts +0 -10
  72. package/src/atoms/shared/index.ts +0 -4
  73. package/src/atoms/shared/styles/color-palettes.css +0 -566
  74. package/src/atoms/types/auth.ts +0 -62
  75. package/src/atoms/types/generated.ts +0 -1469
  76. package/src/atoms/types/index.ts +0 -4
  77. package/src/atoms/types/loading.ts +0 -28
  78. package/src/atoms/ui/Badge.tsx +0 -30
  79. package/src/atoms/ui/ErrorBoundary.tsx +0 -59
  80. package/src/atoms/ui/Select.tsx +0 -53
  81. package/src/atoms/ui/Switch.tsx +0 -42
  82. package/src/atoms/ui/Tabs.tsx +0 -118
  83. package/src/atoms/ui/avatar.tsx +0 -48
  84. package/src/atoms/ui/button.tsx +0 -70
  85. package/src/atoms/ui/card.tsx +0 -76
  86. package/src/atoms/ui/dropdown-menu.tsx +0 -199
  87. package/src/atoms/ui/index.ts +0 -39
  88. package/src/atoms/ui/input.tsx +0 -23
  89. package/src/atoms/ui/label.tsx +0 -23
  90. package/src/atoms/ui/skeleton.tsx +0 -13
  91. package/src/atoms/ui/spinner.tsx +0 -49
  92. package/src/atoms/ui/table.tsx +0 -116
  93. package/src/atoms/utils/animations.ts +0 -135
  94. package/src/atoms/utils/tooltip-helpers.ts +0 -140
  95. package/src/atoms/utils/utils.ts +0 -9
  96. package/src/features/auth/components/LoginForm.tsx +0 -168
  97. package/src/features/auth/components/LogoutButton.tsx +0 -19
  98. package/src/features/auth/components/ProtectedRoute.tsx +0 -60
  99. package/src/features/auth/components/index.ts +0 -4
  100. package/src/features/auth/hooks/index.ts +0 -2
  101. package/src/features/auth/hooks/useAuth.tsx +0 -205
  102. package/src/features/auth/hooks/usePermissions.ts +0 -35
  103. package/src/features/auth/index.ts +0 -2
  104. package/src/features/index.ts +0 -2
  105. package/src/index.css +0 -704
  106. package/src/index.ts +0 -13
  107. package/src/main.tsx +0 -48
  108. package/src/molecules/.gitkeep +0 -0
  109. package/src/molecules/forms/FormGroup.tsx +0 -75
  110. package/src/molecules/forms/SearchInput.tsx +0 -259
  111. package/src/molecules/forms/index.ts +0 -4
  112. package/src/molecules/index.ts +0 -4
  113. package/src/molecules/layout/AppHeader/AppHeader.tsx +0 -42
  114. package/src/molecules/layout/AppHeader/index.ts +0 -1
  115. package/src/molecules/layout/AppLayout.tsx +0 -29
  116. package/src/molecules/layout/PageTemplate.tsx +0 -87
  117. package/src/molecules/layout/SectionHeader/SectionHeader.tsx +0 -87
  118. package/src/molecules/layout/SectionHeader/index.ts +0 -1
  119. package/src/molecules/layout/ShowcaseSection.tsx +0 -57
  120. package/src/molecules/layout/Sidebar.tsx +0 -144
  121. package/src/molecules/layout/SidebarButton/SidebarButton.tsx +0 -99
  122. package/src/molecules/layout/SidebarButton/index.ts +0 -1
  123. package/src/molecules/layout/SidebarContext.tsx +0 -31
  124. package/src/molecules/layout/index.ts +0 -7
  125. package/src/molecules/navigation/NavMenu.tsx +0 -188
  126. package/src/molecules/navigation/Pagination.tsx +0 -172
  127. package/src/molecules/navigation/index.ts +0 -4
  128. package/src/organisms/index.ts +0 -5
  129. package/src/organisms/showcase/ComponentShowcasePage.tsx +0 -2496
  130. package/src/organisms/showcase/index.ts +0 -1
  131. package/src/pages/AdminShowcase/AdminCRUDShowcase.tsx +0 -242
  132. package/src/pages/AdminShowcase/AdminDashboardShowcase.tsx +0 -171
  133. package/src/pages/AdminShowcase/AdminDetailShowcase.tsx +0 -385
  134. package/src/pages/AdminShowcase/index.tsx +0 -3
  135. package/src/pages/ComponentShowcase/BadgesShowcase.tsx +0 -188
  136. package/src/pages/ComponentShowcase/CardsShowcase.tsx +0 -392
  137. package/src/pages/ComponentShowcase/PalettesShowcase.tsx +0 -207
  138. package/src/pages/ComponentShowcase/StatesShowcase.tsx +0 -485
  139. package/src/pages/ComponentShowcase/TablesShowcase.tsx +0 -134
  140. package/src/pages/ComponentShowcase/TypographyShowcase.tsx +0 -255
  141. package/src/pages/ComponentShowcase/index.tsx +0 -188
  142. package/src/pages/index.ts +0 -2
  143. package/src/templates/AuthTemplate.tsx +0 -216
  144. package/src/templates/ComponentShowcaseTemplate.tsx +0 -173
  145. package/src/templates/DashboardTemplate.tsx +0 -232
  146. package/src/templates/DataTemplate.tsx +0 -319
  147. package/src/templates/admin/AdminCRUDTemplate.tsx +0 -630
  148. package/src/templates/admin/AdminDashboardTemplate.tsx +0 -351
  149. package/src/templates/admin/AdminDetailTemplate.tsx +0 -563
  150. package/src/templates/admin/index.ts +0 -29
  151. package/src/templates/factory.tsx +0 -169
  152. package/src/templates/index.ts +0 -37
  153. package/src/vite-env.d.ts +0 -1
@@ -1,57 +0,0 @@
1
- import React from 'react';
2
- import { DataBadge } from '../../atoms/composed/DataBadge';
3
- import { cn } from '../../atoms/utils/utils';
4
-
5
- interface ShowcaseSectionProps {
6
- /** Section title */
7
- title: string;
8
- /** Section description */
9
- description: string;
10
- /** Badge configuration */
11
- badge: {
12
- text: string;
13
- variant?: 'status' | 'category';
14
- status?: 'success' | 'warning' | 'error' | 'info' | 'neutral';
15
- category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
16
- size?: 'sm' | 'md' | 'lg';
17
- };
18
- /** Section content */
19
- children: React.ReactNode;
20
- /** Additional CSS classes */
21
- className?: string;
22
- }
23
-
24
- export const ShowcaseSection: React.FC<ShowcaseSectionProps> = ({
25
- title,
26
- description,
27
- badge,
28
- children,
29
- className
30
- }) => {
31
- return (
32
- <div className={cn('space-y-6', className)} data-component-name="ShowcaseSection">
33
- {/* Title with inline badge */}
34
- <div className="space-y-4">
35
- <h3 className="text-lg font-medium flex items-center gap-2">
36
- {title}
37
- <DataBadge
38
- variant={badge.variant || 'status'}
39
- status={badge.status || 'neutral'}
40
- category={badge.category}
41
- size={badge.size || 'sm'}
42
- >
43
- {badge.text}
44
- </DataBadge>
45
- </h3>
46
- <p className="text-sm text-muted-foreground">
47
- {description}
48
- </p>
49
- </div>
50
-
51
- {/* Content */}
52
- <div>
53
- {children}
54
- </div>
55
- </div>
56
- );
57
- };
@@ -1,144 +0,0 @@
1
- import React from 'react';
2
- import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
3
- import { cn } from '../../atoms/utils/utils';
4
- import { useSidebar } from './SidebarContext';
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
- }
21
-
22
- interface SidebarProps {
23
- className?: string;
24
- }
25
-
26
- export const Sidebar = ({ className }: SidebarProps) => {
27
- const { isExpanded, toggleSidebar } = useSidebar();
28
- const location = useLocation();
29
- const navigate = useNavigate();
30
- const [searchParams] = useSearchParams();
31
-
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
- ];
37
-
38
- const handleNavigation = (path: string) => {
39
- if (path.includes('?')) {
40
- // Handle query parameters by parsing the URL
41
- const [basePath, query] = path.split('?');
42
- const searchParams = new URLSearchParams(query);
43
- navigate({ pathname: basePath, search: searchParams.toString() });
44
- } else {
45
- navigate(path);
46
- }
47
- };
48
-
49
- return (
50
- <aside
51
- className={cn(
52
- "fixed left-0 top-16 flex flex-col bg-background border-r border-border z-40",
53
- "transition-all duration-300 ease-in-out",
54
- "h-[calc(100vh-4rem)]", // Full height minus header (4rem = 64px)
55
- !isExpanded ? "w-16" : "w-64",
56
- className
57
- )}
58
- data-component-name="Sidebar"
59
- data-collapsed={!isExpanded}
60
- >
61
- {/* Toggle Button */}
62
- <div className="flex justify-center p-3 pt-4">
63
- <button
64
- onClick={toggleSidebar}
65
- className={cn(
66
- "w-7 h-7 rounded-md",
67
- "bg-muted/50 text-muted-foreground",
68
- "flex items-center justify-center",
69
- "hover:bg-muted hover:text-foreground",
70
- "active:scale-95",
71
- "transition-all duration-150 ease-out"
72
- )}
73
- data-component-name="SidebarToggle"
74
- title={!isExpanded ? "Expand sidebar" : "Collapse sidebar"}
75
- >
76
- {!isExpanded ? <Menu className="w-3.5 h-3.5" /> : <X className="w-3.5 h-3.5" />}
77
- </button>
78
- </div>
79
-
80
-
81
- {/* Navigation Items */}
82
- <nav className="flex-1 p-3 space-y-2" data-component-name="SidebarNav">
83
- {items.map((item) => {
84
- // Check if item path has query parameters
85
- const [itemBasePath, itemQuery] = item.path.split('?');
86
- const currentPath = location.pathname;
87
-
88
- let isActive;
89
- if (itemQuery) {
90
- // For items with query parameters, check both path and query
91
- const itemParams = new URLSearchParams(itemQuery);
92
- const tabParam = itemParams.get('tab');
93
- const currentTab = searchParams.get('tab');
94
- isActive = currentPath === itemBasePath && currentTab === tabParam;
95
- } else {
96
- // For simple paths, check exact match
97
- isActive = currentPath === item.path;
98
- }
99
-
100
- return (
101
- <SidebarButton
102
- key={item.value}
103
- icon={item.icon}
104
- label={item.label}
105
- active={isActive}
106
- category={item.category}
107
- expanded={isExpanded}
108
- onClick={() => handleNavigation(item.path)}
109
- />
110
- );
111
- })}
112
- </nav>
113
-
114
- {/* Footer */}
115
- <div
116
- className={cn(
117
- "p-4 border-t border-border",
118
- !isExpanded && "px-3"
119
- )}
120
- data-component-name="SidebarFooter"
121
- >
122
- <div className="relative">
123
- {/* Collapsed footer */}
124
- <div className={cn(
125
- "text-xs text-muted-foreground text-center",
126
- "absolute inset-0 transition-opacity duration-200 ease-out",
127
- !isExpanded ? "opacity-100" : "opacity-0"
128
- )}>
129
- v1.0
130
- </div>
131
-
132
- {/* Expanded footer */}
133
- <div className={cn(
134
- "text-xs text-muted-foreground whitespace-nowrap",
135
- "transition-opacity duration-200 ease-out",
136
- isExpanded ? "opacity-100" : "opacity-0"
137
- )}>
138
- AloeVera v1.0
139
- </div>
140
- </div>
141
- </div>
142
- </aside>
143
- );
144
- };
@@ -1,99 +0,0 @@
1
- import React from 'react';
2
- import { cn } from '../../../atoms/utils/utils';
3
- import { Button } from '../../../atoms/ui/button';
4
-
5
- interface SidebarButtonProps {
6
- icon: React.ReactNode;
7
- label: string;
8
- active?: boolean;
9
- category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
10
- expanded?: boolean;
11
- onClick?: () => void;
12
- className?: string;
13
- }
14
-
15
- export const SidebarButton: React.FC<SidebarButtonProps> = ({
16
- icon,
17
- label,
18
- active = false,
19
- category = 1,
20
- expanded = false,
21
- onClick,
22
- className
23
- }) => {
24
- return (
25
- <Button
26
- variant={active ? "secondary" : "ghost"}
27
- onClick={onClick}
28
- tooltip={!expanded ? label : undefined}
29
- className={cn(
30
- "relative w-full justify-start gap-3 h-12",
31
- "transition-all duration-200 ease-in-out",
32
-
33
- // Active state with category colors
34
- active && [
35
- `bg-category-${category}/15 hover:bg-category-${category}/25`,
36
- `border-category-${category}/30`,
37
- `text-category-${category}`,
38
- "shadow-sm"
39
- ],
40
-
41
- // Non-active hover effects
42
- !active && [
43
- "hover:bg-muted/80",
44
- `hover:text-category-${category}`,
45
- "hover:shadow-sm"
46
- ],
47
-
48
- // Collapsed state - ensure proper centering
49
- !expanded && "justify-center px-2",
50
- expanded && "px-3",
51
-
52
- className
53
- )}
54
- >
55
- {/* Icon Container - Always square */}
56
- <div className={cn(
57
- "flex items-center justify-center w-6 h-6 flex-shrink-0",
58
- "transition-colors duration-200",
59
- active ? `text-category-${category}` : "text-muted-foreground group-hover:text-foreground"
60
- )}>
61
- {React.isValidElement(icon)
62
- ? React.cloneElement(icon as React.ReactElement<{ className?: string }>, { className: "w-5 h-5" })
63
- : icon
64
- }
65
- </div>
66
-
67
- {/* Label - only show when expanded */}
68
- {expanded && (
69
- <>
70
- <span className={cn(
71
- "text-sm font-medium flex-1 text-left",
72
- active ? `text-category-${category}` : "text-foreground"
73
- )}>
74
- {label}
75
- </span>
76
-
77
- {/* Active indicator dot */}
78
- {active && (
79
- <div className={cn(
80
- "w-2 h-2 rounded-full flex-shrink-0",
81
- `bg-category-${category}`
82
- )} />
83
- )}
84
- </>
85
- )}
86
-
87
- {/* Collapsed active indicator */}
88
- {!expanded && active && (
89
- <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
- )} />
95
- </div>
96
- )}
97
- </Button>
98
- );
99
- };
@@ -1 +0,0 @@
1
- export { SidebarButton } from './SidebarButton';
@@ -1,31 +0,0 @@
1
- import React, { createContext, useContext, useState, type ReactNode } from 'react';
2
-
3
- interface SidebarContextType {
4
- isExpanded: boolean;
5
- setIsExpanded: (expanded: boolean) => void;
6
- toggleSidebar: () => void;
7
- }
8
-
9
- const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
10
-
11
- export const SidebarProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
12
- const [isExpanded, setIsExpanded] = useState(false);
13
-
14
- const toggleSidebar = () => {
15
- setIsExpanded(prev => !prev);
16
- };
17
-
18
- return (
19
- <SidebarContext.Provider value={{ isExpanded, setIsExpanded, toggleSidebar }}>
20
- {children}
21
- </SidebarContext.Provider>
22
- );
23
- };
24
-
25
- export const useSidebar = () => {
26
- const context = useContext(SidebarContext);
27
- if (!context) {
28
- throw new Error('useSidebar must be used within a SidebarProvider');
29
- }
30
- return context;
31
- };
@@ -1,7 +0,0 @@
1
- export { PageTemplate, type PageTemplateProps } from './PageTemplate';
2
- export { ShowcaseSection } from './ShowcaseSection';
3
- export { AppLayout } from './AppLayout';
4
- export { SectionHeader } from './SectionHeader';
5
- export { SidebarButton } from './SidebarButton';
6
- export { AppHeader } from './AppHeader';
7
- export { SidebarProvider, useSidebar } from './SidebarContext';
@@ -1,188 +0,0 @@
1
- import React, { useState, useRef, useEffect } from 'react';
2
- import { ChevronDown, ChevronRight } from 'lucide-react';
3
- import { cn } from '../../atoms/utils/utils';
4
- import { getAnimationClasses, animationPresets } from '../../atoms/utils/animations';
5
-
6
- export interface NavMenuItem {
7
- id: string;
8
- label: string;
9
- href?: string;
10
- icon?: React.ReactNode;
11
- category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
12
- children?: NavMenuItem[];
13
- disabled?: boolean;
14
- }
15
-
16
- interface NavMenuProps {
17
- items: NavMenuItem[];
18
- orientation?: 'horizontal' | 'vertical';
19
- className?: string;
20
- defaultCategory?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
21
- onItemClick?: (item: NavMenuItem) => void;
22
- }
23
-
24
- export const NavMenu: React.FC<NavMenuProps> = ({
25
- items,
26
- orientation = 'horizontal',
27
- className,
28
- defaultCategory = 1,
29
- onItemClick
30
- }) => {
31
- const [openMenus, setOpenMenus] = useState<Set<string>>(new Set());
32
- const timeoutRef = useRef<Record<string, NodeJS.Timeout>>({});
33
-
34
- const toggleMenu = (itemId: string) => {
35
- setOpenMenus(prev => {
36
- const newSet = new Set(prev);
37
- if (newSet.has(itemId)) {
38
- newSet.delete(itemId);
39
- } else {
40
- newSet.add(itemId);
41
- }
42
- return newSet;
43
- });
44
- };
45
-
46
- const handleMouseEnter = (itemId: string) => {
47
- if (orientation === 'horizontal') {
48
- // Clear any pending close timeout
49
- if (timeoutRef.current[itemId]) {
50
- clearTimeout(timeoutRef.current[itemId]);
51
- delete timeoutRef.current[itemId];
52
- }
53
-
54
- setOpenMenus(prev => new Set(prev).add(itemId));
55
- }
56
- };
57
-
58
- const handleMouseLeave = (itemId: string) => {
59
- if (orientation === 'horizontal') {
60
- // Delay closing to allow for mouse movement to submenu
61
- timeoutRef.current[itemId] = setTimeout(() => {
62
- setOpenMenus(prev => {
63
- const newSet = new Set(prev);
64
- newSet.delete(itemId);
65
- return newSet;
66
- });
67
- delete timeoutRef.current[itemId];
68
- }, 150);
69
- }
70
- };
71
-
72
- const handleItemClick = (item: NavMenuItem, event?: React.MouseEvent) => {
73
- if (item.disabled) {
74
- event?.preventDefault();
75
- return;
76
- }
77
-
78
- if (item.children && item.children.length > 0) {
79
- event?.preventDefault();
80
- toggleMenu(item.id);
81
- } else {
82
- onItemClick?.(item);
83
- }
84
- };
85
-
86
- useEffect(() => {
87
- const timeouts = timeoutRef.current;
88
- return () => {
89
- // Cleanup timeouts
90
- Object.values(timeouts).forEach(timeout => clearTimeout(timeout));
91
- };
92
- }, []);
93
-
94
- const renderMenuItem = (item: NavMenuItem, level = 0) => {
95
- const hasChildren = item.children && item.children.length > 0;
96
- const isOpen = openMenus.has(item.id);
97
- const category = item.category || defaultCategory;
98
-
99
- const itemContent = (
100
- <div
101
- className={cn(
102
- 'flex items-center gap-2 px-3 py-2 rounded-md transition-all duration-200',
103
- 'hover:bg-muted/50 cursor-pointer select-none',
104
- item.disabled && 'opacity-50 cursor-not-allowed',
105
- !item.disabled && [
106
- `hover:text-category-${category}`,
107
- `hover:bg-category-${category}/5`,
108
- getAnimationClasses(animationPresets.subtle)
109
- ],
110
- orientation === 'horizontal' && level === 0 && 'px-4 py-3'
111
- )}
112
- onClick={(e) => handleItemClick(item, e)}
113
- >
114
- {item.icon && (
115
- <span className="flex-shrink-0">
116
- {item.icon}
117
- </span>
118
- )}
119
-
120
- <span className="flex-1 text-sm font-medium">
121
- {item.label}
122
- </span>
123
-
124
- {hasChildren && (
125
- <span className="flex-shrink-0 transition-transform duration-200">
126
- {orientation === 'horizontal' ? (
127
- <ChevronDown className={cn(
128
- 'w-4 h-4 transition-transform duration-200',
129
- isOpen && 'rotate-180'
130
- )} />
131
- ) : (
132
- <ChevronRight className={cn(
133
- 'w-4 h-4 transition-transform duration-200',
134
- isOpen && 'rotate-90'
135
- )} />
136
- )}
137
- </span>
138
- )}
139
- </div>
140
- );
141
-
142
- return (
143
- <div
144
- key={item.id}
145
- className="relative"
146
- onMouseEnter={() => handleMouseEnter(item.id)}
147
- onMouseLeave={() => handleMouseLeave(item.id)}
148
- >
149
- {item.href && !hasChildren ? (
150
- <a href={item.href} className="block">
151
- {itemContent}
152
- </a>
153
- ) : (
154
- itemContent
155
- )}
156
-
157
- {/* Submenu */}
158
- {hasChildren && isOpen && (
159
- <div
160
- className={cn(
161
- 'bg-popover border border-border rounded-md shadow-lg py-1 min-w-48',
162
- 'z-50',
163
- orientation === 'horizontal'
164
- ? 'absolute top-full left-0 mt-1'
165
- : 'ml-4 mt-1',
166
- getAnimationClasses(animationPresets.card)
167
- )}
168
- >
169
- {item.children!.map(child => renderMenuItem(child, level + 1))}
170
- </div>
171
- )}
172
- </div>
173
- );
174
- };
175
-
176
- return (
177
- <nav
178
- className={cn(
179
- 'flex',
180
- orientation === 'horizontal' ? 'flex-row space-x-1' : 'flex-col space-y-1',
181
- className
182
- )}
183
- data-component-name="NavMenu"
184
- >
185
- {items.map(item => renderMenuItem(item))}
186
- </nav>
187
- );
188
- };
@@ -1,172 +0,0 @@
1
- import React from 'react';
2
- import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
3
- import { Button } from '../../atoms/ui/button';
4
- import { cn } from '../../atoms/utils/utils';
5
-
6
- interface PaginationProps {
7
- currentPage: number;
8
- totalPages: number;
9
- onPageChange: (page: number) => void;
10
- showFirstLast?: boolean;
11
- maxVisiblePages?: number;
12
- className?: string;
13
- category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
14
- size?: 'sm' | 'md' | 'lg';
15
- }
16
-
17
- export const Pagination: React.FC<PaginationProps> = ({
18
- currentPage,
19
- totalPages,
20
- onPageChange,
21
- showFirstLast = true,
22
- maxVisiblePages = 7,
23
- className,
24
- category = 1,
25
- size = 'md'
26
- }) => {
27
- const getVisiblePages = () => {
28
- if (totalPages <= maxVisiblePages) {
29
- return Array.from({ length: totalPages }, (_, i) => i + 1);
30
- }
31
-
32
- const halfVisible = Math.floor(maxVisiblePages / 2);
33
- let start = Math.max(1, currentPage - halfVisible);
34
- const end = Math.min(totalPages, start + maxVisiblePages - 1);
35
-
36
- if (end - start + 1 < maxVisiblePages) {
37
- start = Math.max(1, end - maxVisiblePages + 1);
38
- }
39
-
40
- const pages = [];
41
-
42
- // Add first page if not in range
43
- if (start > 1) {
44
- pages.push(1);
45
- if (start > 2) {
46
- pages.push('ellipsis-start');
47
- }
48
- }
49
-
50
- // Add visible range
51
- for (let i = start; i <= end; i++) {
52
- pages.push(i);
53
- }
54
-
55
- // Add last page if not in range
56
- if (end < totalPages) {
57
- if (end < totalPages - 1) {
58
- pages.push('ellipsis-end');
59
- }
60
- pages.push(totalPages);
61
- }
62
-
63
- return pages;
64
- };
65
-
66
- const buttonSizes = {
67
- sm: 'h-8 w-8 text-xs',
68
- md: 'h-9 w-9 text-sm',
69
- lg: 'h-10 w-10 text-sm'
70
- };
71
-
72
- const visiblePages = getVisiblePages();
73
-
74
- return (
75
- <nav
76
- className={cn('flex items-center space-x-1', className)}
77
- aria-label="Pagination"
78
- data-component-name="Pagination"
79
- >
80
- {/* First page button */}
81
- {showFirstLast && currentPage > 1 && (
82
- <Button
83
- variant="outline"
84
- size="sm"
85
- onClick={() => onPageChange(1)}
86
- className={cn(buttonSizes[size])}
87
- aria-label="Go to first page"
88
- >
89
- ««
90
- </Button>
91
- )}
92
-
93
- {/* Previous page button */}
94
- <Button
95
- variant="outline"
96
- size="sm"
97
- onClick={() => onPageChange(Math.max(1, currentPage - 1))}
98
- disabled={currentPage === 1}
99
- className={cn(buttonSizes[size])}
100
- aria-label="Go to previous page"
101
- >
102
- <ChevronLeft className="w-4 h-4" />
103
- </Button>
104
-
105
- {/* Page number buttons */}
106
- {visiblePages.map((page, index) => {
107
- if (typeof page === 'string') {
108
- return (
109
- <span
110
- key={index}
111
- className={cn(
112
- 'flex items-center justify-center text-muted-foreground',
113
- buttonSizes[size]
114
- )}
115
- >
116
- <MoreHorizontal className="w-4 h-4" />
117
- </span>
118
- );
119
- }
120
-
121
- const isActive = page === currentPage;
122
-
123
- return (
124
- <Button
125
- key={page}
126
- variant={isActive ? "default" : "outline"}
127
- size="sm"
128
- onClick={() => onPageChange(page)}
129
- className={cn(
130
- buttonSizes[size],
131
- isActive && [
132
- `bg-category-${category}/15 hover:bg-category-${category}/25`,
133
- `border-category-${category}/30`,
134
- `text-category-${category}`,
135
- 'shadow-sm'
136
- ]
137
- )}
138
- aria-label={`Go to page ${page}`}
139
- aria-current={isActive ? 'page' : undefined}
140
- >
141
- {page}
142
- </Button>
143
- );
144
- })}
145
-
146
- {/* Next page button */}
147
- <Button
148
- variant="outline"
149
- size="sm"
150
- onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
151
- disabled={currentPage === totalPages}
152
- className={cn(buttonSizes[size])}
153
- aria-label="Go to next page"
154
- >
155
- <ChevronRight className="w-4 h-4" />
156
- </Button>
157
-
158
- {/* Last page button */}
159
- {showFirstLast && currentPage < totalPages && (
160
- <Button
161
- variant="outline"
162
- size="sm"
163
- onClick={() => onPageChange(totalPages)}
164
- className={cn(buttonSizes[size])}
165
- aria-label="Go to last page"
166
- >
167
- »»
168
- </Button>
169
- )}
170
- </nav>
171
- );
172
- };