@rakeyshgidwani/roger-ui-bank-theme-stan-design 0.1.4 → 0.1.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/CHANGELOG.md +1 -1
- package/dist/index.d.ts +131 -131
- package/dist/index.esm.js +148 -148
- package/dist/index.js +148 -148
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/ui/accessibility-demo.tsx +271 -0
- package/src/components/ui/advanced-component-architecture-demo.tsx +916 -0
- package/src/components/ui/advanced-transition-system-demo.tsx +670 -0
- package/src/components/ui/advanced-transition-system.tsx +395 -0
- package/src/components/ui/animation/animated-container.tsx +166 -0
- package/src/components/ui/animation/index.ts +19 -0
- package/src/components/ui/animation/staggered-container.tsx +68 -0
- package/src/components/ui/animation-demo.tsx +250 -0
- package/src/components/ui/badge.tsx +33 -0
- package/src/components/ui/battery-conscious-animation-demo.tsx +568 -0
- package/src/components/ui/border-radius-shadow-demo.tsx +187 -0
- package/src/components/ui/button.tsx +36 -0
- package/src/components/ui/card.tsx +207 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/color-preview.tsx +411 -0
- package/src/components/ui/data-display/chart.tsx +653 -0
- package/src/components/ui/data-display/data-grid-simple.tsx +76 -0
- package/src/components/ui/data-display/data-grid.tsx +680 -0
- package/src/components/ui/data-display/list.tsx +456 -0
- package/src/components/ui/data-display/table.tsx +482 -0
- package/src/components/ui/data-display/timeline.tsx +441 -0
- package/src/components/ui/data-display/tree.tsx +602 -0
- package/src/components/ui/data-display/types.ts +536 -0
- package/src/components/ui/enterprise-mobile-experience-demo.tsx +749 -0
- package/src/components/ui/enterprise-mobile-experience.tsx +464 -0
- package/src/components/ui/feedback/alert.tsx +157 -0
- package/src/components/ui/feedback/progress.tsx +292 -0
- package/src/components/ui/feedback/skeleton.tsx +185 -0
- package/src/components/ui/feedback/toast.tsx +280 -0
- package/src/components/ui/feedback/types.ts +125 -0
- package/src/components/ui/font-preview.tsx +288 -0
- package/src/components/ui/form-demo.tsx +553 -0
- package/src/components/ui/hardware-acceleration-demo.tsx +547 -0
- package/src/components/ui/input.tsx +35 -0
- package/src/components/ui/label.tsx +16 -0
- package/src/components/ui/layout-demo.tsx +367 -0
- package/src/components/ui/layouts/adaptive-layout.tsx +139 -0
- package/src/components/ui/layouts/desktop-layout.tsx +224 -0
- package/src/components/ui/layouts/index.ts +10 -0
- package/src/components/ui/layouts/mobile-layout.tsx +162 -0
- package/src/components/ui/layouts/tablet-layout.tsx +197 -0
- package/src/components/ui/mobile-form-validation.tsx +451 -0
- package/src/components/ui/mobile-input-demo.tsx +201 -0
- package/src/components/ui/mobile-input.tsx +281 -0
- package/src/components/ui/mobile-skeleton-loading-demo.tsx +638 -0
- package/src/components/ui/navigation/breadcrumb.tsx +158 -0
- package/src/components/ui/navigation/index.ts +36 -0
- package/src/components/ui/navigation/menu.tsx +374 -0
- package/src/components/ui/navigation/navigation-demo.tsx +324 -0
- package/src/components/ui/navigation/pagination.tsx +272 -0
- package/src/components/ui/navigation/sidebar.tsx +383 -0
- package/src/components/ui/navigation/stepper.tsx +303 -0
- package/src/components/ui/navigation/tabs.tsx +205 -0
- package/src/components/ui/navigation/types.ts +299 -0
- package/src/components/ui/overlay/backdrop.tsx +81 -0
- package/src/components/ui/overlay/focus-manager.tsx +143 -0
- package/src/components/ui/overlay/index.ts +36 -0
- package/src/components/ui/overlay/modal.tsx +270 -0
- package/src/components/ui/overlay/overlay-manager.tsx +110 -0
- package/src/components/ui/overlay/popover.tsx +462 -0
- package/src/components/ui/overlay/portal.tsx +79 -0
- package/src/components/ui/overlay/tooltip.tsx +303 -0
- package/src/components/ui/overlay/types.ts +196 -0
- package/src/components/ui/performance-demo.tsx +596 -0
- package/src/components/ui/semantic-input-system-demo.tsx +502 -0
- package/src/components/ui/semantic-input-system-demo.tsx.disabled +873 -0
- package/src/components/ui/tablet-layout.tsx +192 -0
- package/src/components/ui/theme-customizer.tsx +386 -0
- package/src/components/ui/theme-preview.tsx +310 -0
- package/src/components/ui/theme-switcher.tsx +264 -0
- package/src/components/ui/theme-toggle.tsx +38 -0
- package/src/components/ui/token-demo.tsx +195 -0
- package/src/components/ui/touch-demo.tsx +462 -0
- package/src/components/ui/touch-friendly-interface-demo.tsx +519 -0
- package/src/components/ui/touch-friendly-interface.tsx +296 -0
- package/src/hooks/index.ts +190 -0
- package/src/hooks/use-accessibility-support.ts +518 -0
- package/src/hooks/use-adaptive-layout.ts +289 -0
- package/src/hooks/use-advanced-patterns.ts +294 -0
- package/src/hooks/use-advanced-transition-system.ts +393 -0
- package/src/hooks/use-animation-profile.ts +288 -0
- package/src/hooks/use-battery-animations.ts +384 -0
- package/src/hooks/use-battery-conscious-loading.ts +475 -0
- package/src/hooks/use-battery-optimization.ts +330 -0
- package/src/hooks/use-battery-status.ts +299 -0
- package/src/hooks/use-component-performance.ts +344 -0
- package/src/hooks/use-device-loading-states.ts +459 -0
- package/src/hooks/use-device.tsx +110 -0
- package/src/hooks/use-enterprise-mobile-experience.ts +488 -0
- package/src/hooks/use-form-feedback.ts +403 -0
- package/src/hooks/use-form-performance.ts +513 -0
- package/src/hooks/use-frame-rate.ts +251 -0
- package/src/hooks/use-gestures.ts +338 -0
- package/src/hooks/use-hardware-acceleration.ts +341 -0
- package/src/hooks/use-input-accessibility.ts +455 -0
- package/src/hooks/use-input-performance.ts +506 -0
- package/src/hooks/use-layout-performance.ts +319 -0
- package/src/hooks/use-loading-accessibility.ts +535 -0
- package/src/hooks/use-loading-performance.ts +473 -0
- package/src/hooks/use-memory-usage.ts +287 -0
- package/src/hooks/use-mobile-form-layout.ts +464 -0
- package/src/hooks/use-mobile-form-validation.ts +518 -0
- package/src/hooks/use-mobile-keyboard-optimization.ts +472 -0
- package/src/hooks/use-mobile-layout.ts +302 -0
- package/src/hooks/use-mobile-optimization.ts +406 -0
- package/src/hooks/use-mobile-skeleton.ts +402 -0
- package/src/hooks/use-mobile-touch.ts +414 -0
- package/src/hooks/use-performance-throttling.ts +348 -0
- package/src/hooks/use-performance.ts +316 -0
- package/src/hooks/use-reusable-architecture.ts +414 -0
- package/src/hooks/use-semantic-input-types.ts +357 -0
- package/src/hooks/use-semantic-input.ts +565 -0
- package/src/hooks/use-tablet-layout.ts +384 -0
- package/src/hooks/use-touch-friendly-input.ts +524 -0
- package/src/hooks/use-touch-friendly-interface.ts +331 -0
- package/src/hooks/use-touch-optimization.ts +375 -0
- package/src/index.ts +279 -279
- package/src/lib/utils.ts +6 -0
- package/src/themes/README.md +272 -0
- package/src/themes/ThemeContext.tsx +31 -0
- package/src/themes/ThemeProvider.tsx +232 -0
- package/src/themes/accessibility/index.ts +27 -0
- package/src/themes/accessibility.ts +259 -0
- package/src/themes/aria-patterns.ts +420 -0
- package/src/themes/base-themes.ts +55 -0
- package/src/themes/colorManager.ts +380 -0
- package/src/themes/examples/dark-theme.ts +154 -0
- package/src/themes/examples/minimal-theme.ts +108 -0
- package/src/themes/focus-management.ts +701 -0
- package/src/themes/fontLoader.ts +201 -0
- package/src/themes/high-contrast.ts +621 -0
- package/src/themes/index.ts +19 -0
- package/src/themes/inheritance.ts +227 -0
- package/src/themes/keyboard-navigation.ts +550 -0
- package/src/themes/motion-reduction.ts +662 -0
- package/src/themes/navigation.ts +238 -0
- package/src/themes/screen-reader.ts +645 -0
- package/src/themes/systemThemeDetector.ts +182 -0
- package/src/themes/themeCSSUpdater.ts +262 -0
- package/src/themes/themePersistence.ts +238 -0
- package/src/themes/themes/default.ts +586 -0
- package/src/themes/themes/harvey.ts +554 -0
- package/src/themes/themes/stan-design.ts +683 -0
- package/src/themes/types.ts +460 -0
- package/src/themes/useSystemTheme.ts +48 -0
- package/src/themes/useTheme.ts +87 -0
- package/src/themes/validation.ts +462 -0
- package/src/tokens/index.ts +34 -0
- package/src/tokens/tokenExporter.ts +397 -0
- package/src/tokens/tokenGenerator.ts +276 -0
- package/src/tokens/tokenManager.ts +248 -0
- package/src/tokens/tokenValidator.ts +543 -0
- package/src/tokens/types.ts +78 -0
- package/src/utils/bundle-analyzer.ts +260 -0
- package/src/utils/bundle-splitting.ts +483 -0
- package/src/utils/lazy-loading.ts +441 -0
- package/src/utils/performance-monitor.ts +513 -0
- package/src/utils/tree-shaking.ts +274 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Breadcrumb Component
|
|
3
|
+
* Provides navigation breadcrumbs with theme support and responsive design
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as React from 'react';
|
|
7
|
+
import { useState, useMemo } from 'react';
|
|
8
|
+
import { ChevronRightIcon, HomeIcon } from '@heroicons/react/24/outline';
|
|
9
|
+
import { BreadcrumbProps, BreadcrumbItem } from './types';
|
|
10
|
+
|
|
11
|
+
export const Breadcrumb: React.FC<BreadcrumbProps> = ({
|
|
12
|
+
items,
|
|
13
|
+
separator = <ChevronRightIcon className="w-4 h-4" />,
|
|
14
|
+
maxItems = 5,
|
|
15
|
+
showHome = true,
|
|
16
|
+
homeIcon = <HomeIcon className="w-4 h-4" />,
|
|
17
|
+
onItemClick,
|
|
18
|
+
truncate = true,
|
|
19
|
+
responsive = true,
|
|
20
|
+
className = '',
|
|
21
|
+
'data-testid': testId = 'breadcrumb',
|
|
22
|
+
}) => {
|
|
23
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
24
|
+
|
|
25
|
+
// Memoize processed items to avoid unnecessary recalculations
|
|
26
|
+
const processedItems = useMemo(() => {
|
|
27
|
+
if (!truncate || items.length <= maxItems) {
|
|
28
|
+
return items;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (isExpanded) {
|
|
32
|
+
return items;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Show first, last, and ellipsis in the middle
|
|
36
|
+
const firstItems = items.slice(0, 2);
|
|
37
|
+
const lastItems = items.slice(-2);
|
|
38
|
+
|
|
39
|
+
return [
|
|
40
|
+
...firstItems,
|
|
41
|
+
{ id: 'ellipsis', label: '...', disabled: true },
|
|
42
|
+
...lastItems,
|
|
43
|
+
];
|
|
44
|
+
}, [items, maxItems, truncate, isExpanded]);
|
|
45
|
+
|
|
46
|
+
const handleItemClick = (item: BreadcrumbItem) => {
|
|
47
|
+
if (item.disabled || item.id === 'ellipsis') return;
|
|
48
|
+
|
|
49
|
+
if (onItemClick) {
|
|
50
|
+
onItemClick(item);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleExpandToggle = () => {
|
|
55
|
+
setIsExpanded(!isExpanded);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const getThemeClasses = () => {
|
|
59
|
+
return 'breadcrumb breadcrumb--stan-design';
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const getItemClasses = (item: BreadcrumbItem, isLast: boolean) => {
|
|
63
|
+
const baseClasses = 'breadcrumb__item-button';
|
|
64
|
+
|
|
65
|
+
if (item.disabled) {
|
|
66
|
+
return `${baseClasses} breadcrumb__item-button--disabled`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (isLast) {
|
|
70
|
+
return `${baseClasses} breadcrumb__item-button--active`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return baseClasses;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const getSeparatorClasses = () => {
|
|
77
|
+
return 'breadcrumb__separator breadcrumb__separator--semantic';
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<nav
|
|
82
|
+
className={`${getThemeClasses()} ${className}`}
|
|
83
|
+
aria-label="Breadcrumb"
|
|
84
|
+
data-testid={testId}
|
|
85
|
+
>
|
|
86
|
+
{showHome && (
|
|
87
|
+
<>
|
|
88
|
+
<button
|
|
89
|
+
onClick={() => handleItemClick({ id: 'home', label: 'Home', href: '/' })}
|
|
90
|
+
className={`breadcrumb__home-button ${getItemClasses({ id: 'home', label: 'Home' } as BreadcrumbItem, false)}`}
|
|
91
|
+
aria-label="Go to home page"
|
|
92
|
+
>
|
|
93
|
+
<span className="breadcrumb__icon">{homeIcon}</span>
|
|
94
|
+
</button>
|
|
95
|
+
{separator && (
|
|
96
|
+
<span className={getSeparatorClasses()} aria-hidden="true">
|
|
97
|
+
{separator}
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
</>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<ol className="breadcrumb__list">
|
|
104
|
+
{processedItems.map((item, index) => {
|
|
105
|
+
const isLast = index === processedItems.length - 1;
|
|
106
|
+
const isEllipsis = item.id === 'ellipsis';
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<li key={item.id} className="breadcrumb__item">
|
|
110
|
+
{index > 0 && separator && (
|
|
111
|
+
<span className={getSeparatorClasses()} aria-hidden="true">
|
|
112
|
+
{separator}
|
|
113
|
+
</span>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{isEllipsis ? (
|
|
117
|
+
<button
|
|
118
|
+
onClick={handleExpandToggle}
|
|
119
|
+
className="breadcrumb__ellipsis"
|
|
120
|
+
aria-label={isExpanded ? 'Collapse breadcrumbs' : 'Expand breadcrumbs'}
|
|
121
|
+
>
|
|
122
|
+
{item.label}
|
|
123
|
+
</button>
|
|
124
|
+
) : (
|
|
125
|
+
<button
|
|
126
|
+
onClick={() => handleItemClick(item)}
|
|
127
|
+
className={getItemClasses(item, isLast)}
|
|
128
|
+
disabled={item.disabled}
|
|
129
|
+
aria-current={isLast ? 'page' : undefined}
|
|
130
|
+
aria-label={item.href ? `Navigate to ${item.label}` : item.label}
|
|
131
|
+
>
|
|
132
|
+
{item.icon && <span className="breadcrumb__icon">{item.icon}</span>}
|
|
133
|
+
{responsive && window.innerWidth < 640 ? (
|
|
134
|
+
<span className="breadcrumb__item-label">{item.label}</span>
|
|
135
|
+
) : (
|
|
136
|
+
<span className="breadcrumb__item-label">{item.label}</span>
|
|
137
|
+
)}
|
|
138
|
+
</button>
|
|
139
|
+
)}
|
|
140
|
+
</li>
|
|
141
|
+
);
|
|
142
|
+
})}
|
|
143
|
+
</ol>
|
|
144
|
+
|
|
145
|
+
{truncate && items.length > maxItems && (
|
|
146
|
+
<button
|
|
147
|
+
onClick={handleExpandToggle}
|
|
148
|
+
className={`breadcrumb__expand-toggle breadcrumb__expand-toggle--semantic`}
|
|
149
|
+
aria-label={isExpanded ? 'Show fewer breadcrumbs' : 'Show all breadcrumbs'}
|
|
150
|
+
>
|
|
151
|
+
{isExpanded ? 'Show Less' : 'Show All'}
|
|
152
|
+
</button>
|
|
153
|
+
)}
|
|
154
|
+
</nav>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export default Breadcrumb;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation Components Index
|
|
3
|
+
* Exports all navigation components for easy importing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { Breadcrumb } from './breadcrumb';
|
|
7
|
+
export { Pagination } from './pagination';
|
|
8
|
+
export { Tabs } from './tabs';
|
|
9
|
+
export { Stepper } from './stepper';
|
|
10
|
+
export { Menu } from './menu';
|
|
11
|
+
export { Sidebar } from './sidebar';
|
|
12
|
+
|
|
13
|
+
// Export types
|
|
14
|
+
export type {
|
|
15
|
+
NavigationBaseProps,
|
|
16
|
+
NavigationItem,
|
|
17
|
+
NavigationGroup,
|
|
18
|
+
BreadcrumbProps,
|
|
19
|
+
BreadcrumbItem,
|
|
20
|
+
PaginationProps,
|
|
21
|
+
PaginationItem,
|
|
22
|
+
TabsProps,
|
|
23
|
+
TabItem,
|
|
24
|
+
StepperProps,
|
|
25
|
+
StepItem,
|
|
26
|
+
StepAction,
|
|
27
|
+
MenuProps,
|
|
28
|
+
SidebarProps,
|
|
29
|
+
NavigationState,
|
|
30
|
+
NavigationContextValue,
|
|
31
|
+
NavigationAction,
|
|
32
|
+
NavigationBreakpoint,
|
|
33
|
+
NavigationSpacing,
|
|
34
|
+
NavigationAnimation,
|
|
35
|
+
NavigationAccessibility,
|
|
36
|
+
} from './types';
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Menu Component
|
|
3
|
+
* Provides comprehensive menu functionality with theme support and multiple variants
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as React from 'react';
|
|
7
|
+
import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
|
|
8
|
+
import { MenuProps, NavigationItem, NavigationGroup } from './types';
|
|
9
|
+
|
|
10
|
+
// Simple icon components
|
|
11
|
+
const ChevronDownIcon: React.FC<{ className?: string }> = ({ className = '' }) => (
|
|
12
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
13
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
14
|
+
</svg>
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const ChevronRightIcon: React.FC<{ className?: string }> = ({ className = '' }) => (
|
|
18
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
19
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
20
|
+
</svg>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const MagnifyingGlassIcon: React.FC<{ className?: string }> = ({ className = '' }) => (
|
|
24
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
25
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
26
|
+
</svg>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const FunnelIcon: React.FC<{ className?: string }> = ({ className = '' }) => (
|
|
30
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
31
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" />
|
|
32
|
+
</svg>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export const Menu: React.FC<MenuProps> = ({
|
|
36
|
+
items = [],
|
|
37
|
+
groups,
|
|
38
|
+
variant = 'dropdown',
|
|
39
|
+
size = 'md',
|
|
40
|
+
// TODO: Implement orientation, trigger, showArrow, and fullWidth in future version
|
|
41
|
+
// orientation = 'horizontal',
|
|
42
|
+
// trigger = 'click',
|
|
43
|
+
placement = 'bottom',
|
|
44
|
+
offset = 8,
|
|
45
|
+
// showArrow = false,
|
|
46
|
+
// fullWidth = false,
|
|
47
|
+
disabled = false,
|
|
48
|
+
loading = false,
|
|
49
|
+
searchable = false,
|
|
50
|
+
onSearch,
|
|
51
|
+
filterable = false,
|
|
52
|
+
onFilter,
|
|
53
|
+
selectable = false,
|
|
54
|
+
multiSelect = false,
|
|
55
|
+
selectedItems = [],
|
|
56
|
+
onSelectionChange,
|
|
57
|
+
className = '',
|
|
58
|
+
'data-testid': testId = 'menu',
|
|
59
|
+
}) => {
|
|
60
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
61
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
62
|
+
const [filterValue, setFilterValue] = useState('');
|
|
63
|
+
const [internalSelectedItems, setInternalSelectedItems] = useState<string[]>(selectedItems || []);
|
|
64
|
+
// TODO: Implement group expansion functionality in future version
|
|
65
|
+
// const [expandedGroups, setExpandedGroups] = useState<string[]>([]);
|
|
66
|
+
|
|
67
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
68
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
69
|
+
|
|
70
|
+
// Use controlled or uncontrolled selected items
|
|
71
|
+
const currentSelectedItems = selectedItems !== undefined ? selectedItems : internalSelectedItems;
|
|
72
|
+
|
|
73
|
+
// Handle click outside to close menu
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
76
|
+
if (menuRef.current && !menuRef.current.contains(event.target as Node) &&
|
|
77
|
+
triggerRef.current && !triggerRef.current.contains(event.target as Node)) {
|
|
78
|
+
setIsOpen(false);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (isOpen) {
|
|
83
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return () => {
|
|
87
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
88
|
+
};
|
|
89
|
+
}, [isOpen]);
|
|
90
|
+
|
|
91
|
+
// Handle escape key to close menu
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
94
|
+
if (event.key === 'Escape') {
|
|
95
|
+
setIsOpen(false);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (isOpen) {
|
|
100
|
+
document.addEventListener('keydown', handleEscape);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return () => {
|
|
104
|
+
document.removeEventListener('keydown', handleEscape);
|
|
105
|
+
};
|
|
106
|
+
}, [isOpen]);
|
|
107
|
+
|
|
108
|
+
const handleToggle = useCallback(() => {
|
|
109
|
+
if (disabled || loading) return;
|
|
110
|
+
setIsOpen(!isOpen);
|
|
111
|
+
}, [disabled, loading, isOpen]);
|
|
112
|
+
|
|
113
|
+
const handleItemClick = useCallback((item: NavigationItem) => {
|
|
114
|
+
if (item.disabled) return;
|
|
115
|
+
|
|
116
|
+
if (selectable) {
|
|
117
|
+
let newSelectedItems: string[];
|
|
118
|
+
|
|
119
|
+
if (multiSelect) {
|
|
120
|
+
if (currentSelectedItems.includes(item.id)) {
|
|
121
|
+
newSelectedItems = currentSelectedItems.filter(id => id !== item.id);
|
|
122
|
+
} else {
|
|
123
|
+
newSelectedItems = [...currentSelectedItems, item.id];
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
newSelectedItems = [item.id];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (selectedItems === undefined) {
|
|
130
|
+
setInternalSelectedItems(newSelectedItems);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (onSelectionChange) {
|
|
134
|
+
onSelectionChange(newSelectedItems);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (item.onClick) {
|
|
139
|
+
item.onClick();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Close menu for single select or non-selectable menus
|
|
143
|
+
if (!multiSelect || !selectable) {
|
|
144
|
+
setIsOpen(false);
|
|
145
|
+
}
|
|
146
|
+
}, [selectable, multiSelect, currentSelectedItems, selectedItems, onSelectionChange]);
|
|
147
|
+
|
|
148
|
+
// TODO: Implement group toggle functionality in future version
|
|
149
|
+
// const handleGroupToggle = useCallback((groupId: string) => {
|
|
150
|
+
// setExpandedGroups(prev =>
|
|
151
|
+
// prev.includes(groupId)
|
|
152
|
+
// ? prev.filter(id => id !== groupId)
|
|
153
|
+
// : [...prev, groupId]
|
|
154
|
+
// );
|
|
155
|
+
// }, []);
|
|
156
|
+
|
|
157
|
+
const handleSearch = useCallback((query: string) => {
|
|
158
|
+
setSearchQuery(query);
|
|
159
|
+
if (onSearch) {
|
|
160
|
+
onSearch(query);
|
|
161
|
+
}
|
|
162
|
+
}, [onSearch]);
|
|
163
|
+
|
|
164
|
+
const handleFilter = useCallback((filter: string) => {
|
|
165
|
+
setFilterValue(filter);
|
|
166
|
+
if (onFilter) {
|
|
167
|
+
onFilter(filter);
|
|
168
|
+
}
|
|
169
|
+
}, [onFilter]);
|
|
170
|
+
|
|
171
|
+
// Filter items based on search and filter
|
|
172
|
+
const filteredItems = useMemo(() => {
|
|
173
|
+
let filtered = items;
|
|
174
|
+
|
|
175
|
+
if (searchQuery) {
|
|
176
|
+
filtered = filtered.filter(item =>
|
|
177
|
+
item.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
178
|
+
item.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (filterValue) {
|
|
183
|
+
filtered = filtered.filter(item =>
|
|
184
|
+
item.label.toLowerCase().includes(filterValue.toLowerCase())
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return filtered;
|
|
189
|
+
}, [items, searchQuery, filterValue]);
|
|
190
|
+
|
|
191
|
+
const getSizeClasses = () => {
|
|
192
|
+
switch (size) {
|
|
193
|
+
case 'sm':
|
|
194
|
+
return 'menu__trigger--sm';
|
|
195
|
+
case 'lg':
|
|
196
|
+
return 'menu__trigger--lg';
|
|
197
|
+
default: // md
|
|
198
|
+
return 'menu__trigger--md';
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const getVariantClasses = () => {
|
|
203
|
+
switch (variant) {
|
|
204
|
+
case 'dropdown':
|
|
205
|
+
return 'menu__content--dropdown';
|
|
206
|
+
case 'context':
|
|
207
|
+
return 'menu__content--context';
|
|
208
|
+
case 'command':
|
|
209
|
+
return 'menu__content--command';
|
|
210
|
+
default: // default
|
|
211
|
+
return 'menu__content--default';
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const getThemeClasses = () => {
|
|
216
|
+
return 'menu menu--stan-design';
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const getItemClasses = (item: NavigationItem) => {
|
|
220
|
+
let classes = `menu__item ${getSizeClasses()}`;
|
|
221
|
+
|
|
222
|
+
if (item.disabled) {
|
|
223
|
+
classes += ' menu__item--disabled';
|
|
224
|
+
} else if (selectable && currentSelectedItems.includes(item.id)) {
|
|
225
|
+
classes += ' menu__item--selected';
|
|
226
|
+
} else {
|
|
227
|
+
classes += ' menu__item--default';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return classes;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const getPlacementClasses = () => {
|
|
234
|
+
switch (placement) {
|
|
235
|
+
case 'top':
|
|
236
|
+
return 'menu__content--placement-top';
|
|
237
|
+
case 'bottom':
|
|
238
|
+
return 'menu__content--placement-bottom';
|
|
239
|
+
case 'left':
|
|
240
|
+
return 'menu__content--placement-left';
|
|
241
|
+
case 'right':
|
|
242
|
+
return 'menu__content--placement-right';
|
|
243
|
+
default:
|
|
244
|
+
return 'menu__content--placement-bottom';
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const renderMenuItem = (item: NavigationItem) => (
|
|
249
|
+
<button
|
|
250
|
+
key={item.id}
|
|
251
|
+
onClick={() => handleItemClick(item)}
|
|
252
|
+
disabled={item.disabled}
|
|
253
|
+
className={getItemClasses(item)}
|
|
254
|
+
>
|
|
255
|
+
<div className="menu__item-content">
|
|
256
|
+
{item.icon && <span className="menu__item-icon">{item.icon}</span>}
|
|
257
|
+
<div className="menu__item-text">
|
|
258
|
+
<div className="menu__item-label">{item.label}</div>
|
|
259
|
+
{item.description && (
|
|
260
|
+
<div className="menu__item-description">{item.description}</div>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
{item.badge && <span className="menu__item-badge">{item.badge}</span>}
|
|
265
|
+
{item.children && item.children.length > 0 && (
|
|
266
|
+
<ChevronRightIcon className="menu__item-arrow" />
|
|
267
|
+
)}
|
|
268
|
+
</button>
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const renderMenuGroup = (group: NavigationGroup) => (
|
|
272
|
+
<div key={group.id} className="menu__group">
|
|
273
|
+
{group.title && (
|
|
274
|
+
<div className="menu__group-title">
|
|
275
|
+
{group.title}
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
{group.items.map(renderMenuItem)}
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<div className={`menu__container ${getThemeClasses()}`} data-testid={testId}>
|
|
284
|
+
{/* Trigger Button */}
|
|
285
|
+
<button
|
|
286
|
+
ref={triggerRef}
|
|
287
|
+
onClick={handleToggle}
|
|
288
|
+
disabled={disabled || loading}
|
|
289
|
+
className={`menu__trigger ${getSizeClasses()} ${className}`}
|
|
290
|
+
aria-expanded={isOpen}
|
|
291
|
+
aria-haspopup="true"
|
|
292
|
+
>
|
|
293
|
+
Menu
|
|
294
|
+
<ChevronDownIcon className="menu__trigger-icon" />
|
|
295
|
+
</button>
|
|
296
|
+
|
|
297
|
+
{/* Menu Dropdown */}
|
|
298
|
+
{isOpen && (
|
|
299
|
+
<div
|
|
300
|
+
ref={menuRef}
|
|
301
|
+
role="menu"
|
|
302
|
+
className={`menu__content ${getVariantClasses()} ${getPlacementClasses()}`}
|
|
303
|
+
style={{ marginTop: placement === 'bottom' ? offset : undefined }}
|
|
304
|
+
>
|
|
305
|
+
{/* Search Bar */}
|
|
306
|
+
{searchable && (
|
|
307
|
+
<div className="menu__search">
|
|
308
|
+
<div className="menu__search-container">
|
|
309
|
+
<MagnifyingGlassIcon className="menu__search-icon" />
|
|
310
|
+
<input
|
|
311
|
+
type="text"
|
|
312
|
+
placeholder="Search..."
|
|
313
|
+
value={searchQuery}
|
|
314
|
+
onChange={(e) => handleSearch(e.target.value)}
|
|
315
|
+
className="menu__search-input"
|
|
316
|
+
/>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
)}
|
|
320
|
+
|
|
321
|
+
{/* Filter Bar */}
|
|
322
|
+
{filterable && (
|
|
323
|
+
<div className="menu__filter">
|
|
324
|
+
<div className="menu__filter-container">
|
|
325
|
+
<FunnelIcon className="menu__filter-icon" />
|
|
326
|
+
<select
|
|
327
|
+
value={filterValue}
|
|
328
|
+
onChange={(e) => handleFilter(e.target.value)}
|
|
329
|
+
className="menu__filter-select"
|
|
330
|
+
>
|
|
331
|
+
<option value="">All</option>
|
|
332
|
+
<option value="featured">Featured</option>
|
|
333
|
+
<option value="recent">Recent</option>
|
|
334
|
+
</select>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
|
|
339
|
+
{/* Menu Items */}
|
|
340
|
+
<div className="menu__items">
|
|
341
|
+
{groups ? groups.map(renderMenuGroup) : filteredItems.map(renderMenuItem)}
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
{/* Selection Actions */}
|
|
345
|
+
{selectable && multiSelect && currentSelectedItems.length > 0 && (
|
|
346
|
+
<div className="menu__selection-actions">
|
|
347
|
+
<div className="menu__selection-info">
|
|
348
|
+
<span className="menu__selection-count">
|
|
349
|
+
{currentSelectedItems.length} selected
|
|
350
|
+
</span>
|
|
351
|
+
<button
|
|
352
|
+
onClick={() => {
|
|
353
|
+
const newSelectedItems: string[] = [];
|
|
354
|
+
if (selectedItems === undefined) {
|
|
355
|
+
setInternalSelectedItems(newSelectedItems);
|
|
356
|
+
}
|
|
357
|
+
if (onSelectionChange) {
|
|
358
|
+
onSelectionChange(newSelectedItems);
|
|
359
|
+
}
|
|
360
|
+
}}
|
|
361
|
+
className="menu__selection-clear"
|
|
362
|
+
>
|
|
363
|
+
Clear
|
|
364
|
+
</button>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
368
|
+
</div>
|
|
369
|
+
)}
|
|
370
|
+
</div>
|
|
371
|
+
);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
export default Menu;
|