@rakeyshgidwani/roger-ui-bank-theme-stan-design 0.1.3 → 0.1.5
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,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar Component
|
|
3
|
+
* Provides comprehensive sidebar functionality with theme support and multiple variants
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as React from 'react';
|
|
7
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
8
|
+
import { SidebarProps, NavigationItem, NavigationGroup } from './types';
|
|
9
|
+
|
|
10
|
+
// Simple icon components
|
|
11
|
+
const ChevronLeftIcon: 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="M15 19l-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
|
+
// TODO: Implement close icon functionality in future version
|
|
24
|
+
// const XMarkIcon: React.FC<{ className?: string }> = ({ className = '' }) => (
|
|
25
|
+
// <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
26
|
+
// <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
27
|
+
// </svg>
|
|
28
|
+
// );
|
|
29
|
+
|
|
30
|
+
export const Sidebar: React.FC<SidebarProps> = ({
|
|
31
|
+
items = [],
|
|
32
|
+
groups = [],
|
|
33
|
+
variant = 'default',
|
|
34
|
+
size = 'md',
|
|
35
|
+
position = 'left',
|
|
36
|
+
collapsed = false,
|
|
37
|
+
defaultCollapsed = false,
|
|
38
|
+
onCollapseChange,
|
|
39
|
+
collapsible = true,
|
|
40
|
+
showToggle = true,
|
|
41
|
+
toggleIcon,
|
|
42
|
+
header,
|
|
43
|
+
footer,
|
|
44
|
+
overlay = false,
|
|
45
|
+
overlayCloseOnClick = true,
|
|
46
|
+
overlayCloseOnEscape = true,
|
|
47
|
+
overlayCloseOnOutsideClick = true,
|
|
48
|
+
responsive = true,
|
|
49
|
+
breakpoint = 'lg',
|
|
50
|
+
onItemClick,
|
|
51
|
+
onGroupToggle,
|
|
52
|
+
expandedGroups = [],
|
|
53
|
+
defaultExpandedGroups = [],
|
|
54
|
+
// TODO: Implement group icons in future version
|
|
55
|
+
// showGroupIcons = true,
|
|
56
|
+
showItemIcons = true,
|
|
57
|
+
showItemBadges = true,
|
|
58
|
+
showItemDescriptions = true,
|
|
59
|
+
sticky = false,
|
|
60
|
+
zIndex = 1000,
|
|
61
|
+
className = '',
|
|
62
|
+
'data-testid': testId = 'sidebar',
|
|
63
|
+
}) => {
|
|
64
|
+
const [internalCollapsed, setInternalCollapsed] = useState<boolean>(
|
|
65
|
+
collapsed !== undefined ? collapsed : defaultCollapsed
|
|
66
|
+
);
|
|
67
|
+
const [internalExpandedGroups, setInternalExpandedGroups] = useState<string[]>(
|
|
68
|
+
expandedGroups !== undefined ? expandedGroups : defaultExpandedGroups
|
|
69
|
+
);
|
|
70
|
+
const [isOverlayOpen, setIsOverlayOpen] = useState(false);
|
|
71
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
72
|
+
|
|
73
|
+
const sidebarRef = useRef<HTMLDivElement>(null);
|
|
74
|
+
|
|
75
|
+
// Use controlled or uncontrolled collapsed state
|
|
76
|
+
const currentCollapsed = collapsed !== undefined ? collapsed : internalCollapsed;
|
|
77
|
+
const currentExpandedGroups = expandedGroups !== undefined ? expandedGroups : internalExpandedGroups;
|
|
78
|
+
|
|
79
|
+
// Handle responsive behavior
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const handleResize = () => {
|
|
82
|
+
const breakpointWidth = breakpoint === 'sm' ? 640 : breakpoint === 'md' ? 768 : breakpoint === 'lg' ? 1024 : 1280;
|
|
83
|
+
setIsMobile(window.innerWidth < breakpointWidth);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (responsive) {
|
|
87
|
+
handleResize();
|
|
88
|
+
window.addEventListener('resize', handleResize);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return () => {
|
|
92
|
+
if (responsive) {
|
|
93
|
+
window.removeEventListener('resize', handleResize);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}, [responsive, breakpoint]);
|
|
97
|
+
|
|
98
|
+
// Handle escape key for overlay
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
101
|
+
if (event.key === 'Escape' && isOverlayOpen && overlayCloseOnEscape) {
|
|
102
|
+
setIsOverlayOpen(false);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (isOverlayOpen) {
|
|
107
|
+
document.addEventListener('keydown', handleEscape);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return () => {
|
|
111
|
+
document.removeEventListener('keydown', handleEscape);
|
|
112
|
+
};
|
|
113
|
+
}, [isOverlayOpen, overlayCloseOnEscape]);
|
|
114
|
+
|
|
115
|
+
const handleCollapseToggle = useCallback(() => {
|
|
116
|
+
if (!collapsible) return;
|
|
117
|
+
|
|
118
|
+
const newCollapsed = !currentCollapsed;
|
|
119
|
+
|
|
120
|
+
if (collapsed === undefined) {
|
|
121
|
+
setInternalCollapsed(newCollapsed);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (onCollapseChange) {
|
|
125
|
+
onCollapseChange(newCollapsed);
|
|
126
|
+
}
|
|
127
|
+
}, [collapsed, currentCollapsed, collapsible, onCollapseChange]);
|
|
128
|
+
|
|
129
|
+
const handleGroupToggle = useCallback((groupId: string) => {
|
|
130
|
+
const newExpandedGroups = currentExpandedGroups.includes(groupId)
|
|
131
|
+
? currentExpandedGroups.filter(id => id !== groupId)
|
|
132
|
+
: [...currentExpandedGroups, groupId];
|
|
133
|
+
|
|
134
|
+
if (expandedGroups === undefined) {
|
|
135
|
+
setInternalExpandedGroups(newExpandedGroups);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (onGroupToggle) {
|
|
139
|
+
onGroupToggle(groupId, newExpandedGroups.includes(groupId));
|
|
140
|
+
}
|
|
141
|
+
}, [currentExpandedGroups, expandedGroups, onGroupToggle]);
|
|
142
|
+
|
|
143
|
+
const handleItemClick = useCallback((item: NavigationItem) => {
|
|
144
|
+
if (item.disabled) return;
|
|
145
|
+
|
|
146
|
+
if (onItemClick) {
|
|
147
|
+
onItemClick(item);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (item.onClick) {
|
|
151
|
+
item.onClick();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Close overlay on mobile after item click
|
|
155
|
+
if (isMobile && overlay && overlayCloseOnClick) {
|
|
156
|
+
setIsOverlayOpen(false);
|
|
157
|
+
}
|
|
158
|
+
}, [onItemClick, isMobile, overlay, overlayCloseOnClick]);
|
|
159
|
+
|
|
160
|
+
const handleOverlayClose = useCallback(() => {
|
|
161
|
+
setIsOverlayOpen(false);
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
const getSizeClasses = () => {
|
|
165
|
+
if (currentCollapsed) return 'sidebar--collapsed';
|
|
166
|
+
|
|
167
|
+
switch (size) {
|
|
168
|
+
case 'sm':
|
|
169
|
+
return 'sidebar--size-sm';
|
|
170
|
+
case 'lg':
|
|
171
|
+
return 'sidebar--size-lg';
|
|
172
|
+
case 'xl':
|
|
173
|
+
return 'sidebar--size-xl';
|
|
174
|
+
default: // md
|
|
175
|
+
return 'sidebar--size-md';
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const getVariantClasses = () => {
|
|
180
|
+
switch (variant) {
|
|
181
|
+
case 'collapsible':
|
|
182
|
+
return 'sidebar--collapsible';
|
|
183
|
+
case 'overlay':
|
|
184
|
+
return 'sidebar--overlay';
|
|
185
|
+
case 'drawer':
|
|
186
|
+
return 'sidebar--drawer';
|
|
187
|
+
default: // default
|
|
188
|
+
return 'sidebar--default';
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const getThemeClasses = () => {
|
|
193
|
+
return 'sidebar sidebar--stan-design';
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const getPositionClasses = () => {
|
|
197
|
+
if (variant === 'overlay' || variant === 'drawer') {
|
|
198
|
+
return position === 'right' ? 'sidebar--position-right' : 'sidebar--position-left';
|
|
199
|
+
}
|
|
200
|
+
return '';
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const getTransformClasses = () => {
|
|
204
|
+
if (variant === 'overlay' || variant === 'drawer' || (isMobile && overlay)) {
|
|
205
|
+
if (position === 'right') {
|
|
206
|
+
return isOverlayOpen ? 'sidebar--open' : 'sidebar--closed-right';
|
|
207
|
+
}
|
|
208
|
+
return isOverlayOpen ? 'sidebar--open' : 'sidebar--closed-left';
|
|
209
|
+
}
|
|
210
|
+
return '';
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const getItemClasses = (item: NavigationItem) => {
|
|
214
|
+
let classes = `sidebar__item ${
|
|
215
|
+
currentCollapsed ? 'sidebar__item--collapsed' : 'sidebar__item--expanded'
|
|
216
|
+
}`;
|
|
217
|
+
|
|
218
|
+
if (item.disabled) {
|
|
219
|
+
classes += ' sidebar__item--disabled';
|
|
220
|
+
} else if (item.active) {
|
|
221
|
+
classes += ' sidebar__item--active';
|
|
222
|
+
} else {
|
|
223
|
+
classes += ' sidebar__item--default';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return classes;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const getGroupClasses = (groupId: string) => {
|
|
230
|
+
const isExpanded = currentExpandedGroups.includes(groupId);
|
|
231
|
+
let classes = 'sidebar__group';
|
|
232
|
+
|
|
233
|
+
if (isExpanded) {
|
|
234
|
+
classes += ' sidebar__group--expanded';
|
|
235
|
+
} else {
|
|
236
|
+
classes += ' sidebar__group--collapsed';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return classes;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const renderMenuItem = (item: NavigationItem) => (
|
|
243
|
+
<button
|
|
244
|
+
key={item.id}
|
|
245
|
+
onClick={() => handleItemClick(item)}
|
|
246
|
+
disabled={item.disabled}
|
|
247
|
+
className={getItemClasses(item)}
|
|
248
|
+
title={currentCollapsed ? item.label : undefined}
|
|
249
|
+
>
|
|
250
|
+
{showItemIcons && item.icon && (
|
|
251
|
+
<span className={`sidebar__item-icon ${currentCollapsed ? 'sidebar__item-icon--collapsed' : 'sidebar__item-icon--expanded'}`}>
|
|
252
|
+
{item.icon}
|
|
253
|
+
</span>
|
|
254
|
+
)}
|
|
255
|
+
|
|
256
|
+
{!currentCollapsed && (
|
|
257
|
+
<div className="sidebar__item-content">
|
|
258
|
+
<div className="sidebar__item-label">{item.label}</div>
|
|
259
|
+
{showItemDescriptions && item.description && (
|
|
260
|
+
<div className="sidebar__item-description">{item.description}</div>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{!currentCollapsed && showItemBadges && item.badge && (
|
|
266
|
+
<span className="sidebar__item-badge">{item.badge}</span>
|
|
267
|
+
)}
|
|
268
|
+
</button>
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const renderMenuGroup = (group: NavigationGroup) => (
|
|
272
|
+
<div key={group.id}>
|
|
273
|
+
<button
|
|
274
|
+
onClick={() => handleGroupToggle(group.id)}
|
|
275
|
+
className={getGroupClasses(group.id)}
|
|
276
|
+
title={currentCollapsed ? group.title : undefined}
|
|
277
|
+
>
|
|
278
|
+
{/* TODO: Add icon support to NavigationGroup in future version */}
|
|
279
|
+
{/* {showGroupIcons && group.icon && (
|
|
280
|
+
<span className={currentCollapsed ? '' : 'mr-3'}>
|
|
281
|
+
{group.icon}
|
|
282
|
+
</span>
|
|
283
|
+
)} */}
|
|
284
|
+
|
|
285
|
+
{!currentCollapsed && (
|
|
286
|
+
<>
|
|
287
|
+
<div className="sidebar__group-title">{group.title}</div>
|
|
288
|
+
<ChevronRightIcon
|
|
289
|
+
className={`sidebar__group-arrow ${
|
|
290
|
+
currentExpandedGroups.includes(group.id) ? 'sidebar__group-arrow--expanded' : ''
|
|
291
|
+
}`}
|
|
292
|
+
/>
|
|
293
|
+
</>
|
|
294
|
+
)}
|
|
295
|
+
</button>
|
|
296
|
+
|
|
297
|
+
{!currentCollapsed && currentExpandedGroups.includes(group.id) && (
|
|
298
|
+
<div className="sidebar__group-items">
|
|
299
|
+
{group.items.map(renderMenuItem)}
|
|
300
|
+
</div>
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const sidebarContent = (
|
|
306
|
+
<div
|
|
307
|
+
ref={sidebarRef}
|
|
308
|
+
className={`${getSizeClasses()} ${getVariantClasses()} ${getThemeClasses()} ${getPositionClasses()} ${getTransformClasses()} ${className}`}
|
|
309
|
+
data-testid={testId}
|
|
310
|
+
style={{ zIndex }}
|
|
311
|
+
>
|
|
312
|
+
{/* Header */}
|
|
313
|
+
{header && (
|
|
314
|
+
<div className="sidebar__header">
|
|
315
|
+
{header}
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
|
|
319
|
+
{/* Toggle Button */}
|
|
320
|
+
{showToggle && collapsible && (
|
|
321
|
+
<div className="sidebar__toggle-container">
|
|
322
|
+
<button
|
|
323
|
+
onClick={handleCollapseToggle}
|
|
324
|
+
className="sidebar__toggle-button"
|
|
325
|
+
aria-label={currentCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
326
|
+
>
|
|
327
|
+
{toggleIcon || (currentCollapsed ? <ChevronRightIcon className="sidebar__toggle-icon" /> : <ChevronLeftIcon className="sidebar__toggle-icon" />)}
|
|
328
|
+
</button>
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
331
|
+
|
|
332
|
+
{/* Navigation Items */}
|
|
333
|
+
<nav className="sidebar__navigation">
|
|
334
|
+
{groups && groups.length > 0 ? groups.map(renderMenuGroup) : items.map(renderMenuItem)}
|
|
335
|
+
</nav>
|
|
336
|
+
|
|
337
|
+
{/* Footer */}
|
|
338
|
+
{footer && (
|
|
339
|
+
<div className="sidebar__footer">
|
|
340
|
+
{footer}
|
|
341
|
+
</div>
|
|
342
|
+
)}
|
|
343
|
+
</div>
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// Mobile overlay
|
|
347
|
+
if (isMobile && overlay) {
|
|
348
|
+
return (
|
|
349
|
+
<>
|
|
350
|
+
{/* Overlay Backdrop */}
|
|
351
|
+
{isOverlayOpen && (
|
|
352
|
+
<div
|
|
353
|
+
className="sidebar__overlay-backdrop"
|
|
354
|
+
onClick={overlayCloseOnOutsideClick ? handleOverlayClose : undefined}
|
|
355
|
+
/>
|
|
356
|
+
)}
|
|
357
|
+
|
|
358
|
+
{/* Sidebar */}
|
|
359
|
+
{sidebarContent}
|
|
360
|
+
|
|
361
|
+
{/* Mobile Toggle Button */}
|
|
362
|
+
<button
|
|
363
|
+
onClick={() => setIsOverlayOpen(!isOverlayOpen)}
|
|
364
|
+
className="sidebar__mobile-toggle"
|
|
365
|
+
aria-label="Toggle sidebar"
|
|
366
|
+
>
|
|
367
|
+
<svg className="sidebar__mobile-toggle-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
368
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16" />
|
|
369
|
+
</svg>
|
|
370
|
+
</button>
|
|
371
|
+
</>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Desktop sidebar
|
|
376
|
+
return (
|
|
377
|
+
<div className={`sidebar__wrapper ${sticky ? 'sidebar__wrapper--sticky' : ''}`}>
|
|
378
|
+
{sidebarContent}
|
|
379
|
+
</div>
|
|
380
|
+
);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
export default Sidebar;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stepper Component
|
|
3
|
+
* Provides comprehensive stepper functionality with theme support and multiple variants
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as React from 'react';
|
|
7
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
8
|
+
import { CheckIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline';
|
|
9
|
+
import { StepperProps, StepItem } from './types';
|
|
10
|
+
|
|
11
|
+
// Simple icon components
|
|
12
|
+
const ExclamationCircleIcon: React.FC<{ className?: string }> = ({ className = '' }) => (
|
|
13
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
14
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
15
|
+
</svg>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export const Stepper: React.FC<StepperProps> = ({
|
|
19
|
+
steps,
|
|
20
|
+
currentStep = 0,
|
|
21
|
+
defaultCurrentStep = 0,
|
|
22
|
+
onStepChange,
|
|
23
|
+
variant = 'default',
|
|
24
|
+
size = 'md',
|
|
25
|
+
showStepNumbers = true,
|
|
26
|
+
showStepLabels = true,
|
|
27
|
+
showStepDescriptions = true,
|
|
28
|
+
showStepIcons = true,
|
|
29
|
+
allowStepClick = true,
|
|
30
|
+
allowStepNavigation = true,
|
|
31
|
+
showProgress = true,
|
|
32
|
+
// TODO: Implement step status display in future version
|
|
33
|
+
// showStepStatus = true,
|
|
34
|
+
onStepComplete,
|
|
35
|
+
onStepError,
|
|
36
|
+
onFinish,
|
|
37
|
+
onCancel,
|
|
38
|
+
className = '',
|
|
39
|
+
'data-testid': testId = 'stepper',
|
|
40
|
+
}) => {
|
|
41
|
+
const [internalCurrentStep, setInternalCurrentStep] = useState<number>(
|
|
42
|
+
currentStep !== undefined ? currentStep : defaultCurrentStep
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Use controlled or uncontrolled current step
|
|
46
|
+
const currentStepIndex = currentStep !== undefined ? currentStep : internalCurrentStep;
|
|
47
|
+
|
|
48
|
+
const handleStepClick = useCallback((stepIndex: number) => {
|
|
49
|
+
if (!allowStepClick || !allowStepNavigation) return;
|
|
50
|
+
|
|
51
|
+
const step = steps[stepIndex];
|
|
52
|
+
if (step.disabled) return;
|
|
53
|
+
|
|
54
|
+
if (currentStep === undefined) {
|
|
55
|
+
setInternalCurrentStep(stepIndex);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (onStepChange) {
|
|
59
|
+
onStepChange(stepIndex);
|
|
60
|
+
}
|
|
61
|
+
}, [currentStep, onStepChange, allowStepClick, allowStepNavigation, steps]);
|
|
62
|
+
|
|
63
|
+
const handleStepComplete = useCallback(async (stepIndex: number) => {
|
|
64
|
+
const step = steps[stepIndex];
|
|
65
|
+
if (!step.validation) return;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const isValid = await step.validation();
|
|
69
|
+
if (isValid) {
|
|
70
|
+
if (onStepComplete) {
|
|
71
|
+
onStepComplete(stepIndex);
|
|
72
|
+
}
|
|
73
|
+
// Move to next step if available
|
|
74
|
+
if (stepIndex < steps.length - 1) {
|
|
75
|
+
handleStepClick(stepIndex + 1);
|
|
76
|
+
} else if (onFinish) {
|
|
77
|
+
onFinish();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (onStepError) {
|
|
82
|
+
onStepError(stepIndex);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}, [steps, onStepComplete, onStepError, onFinish, handleStepClick]);
|
|
86
|
+
|
|
87
|
+
const handleFinish = useCallback(() => {
|
|
88
|
+
if (onFinish) {
|
|
89
|
+
onFinish();
|
|
90
|
+
}
|
|
91
|
+
}, [onFinish]);
|
|
92
|
+
|
|
93
|
+
const handleCancel = useCallback(() => {
|
|
94
|
+
if (onCancel) {
|
|
95
|
+
onCancel();
|
|
96
|
+
}
|
|
97
|
+
}, [onCancel]);
|
|
98
|
+
|
|
99
|
+
// Calculate progress percentage
|
|
100
|
+
const progressPercentage = useMemo(() => {
|
|
101
|
+
return ((currentStepIndex + 1) / steps.length) * 100;
|
|
102
|
+
}, [currentStepIndex, steps.length]);
|
|
103
|
+
|
|
104
|
+
const getSizeClasses = () => {
|
|
105
|
+
switch (size) {
|
|
106
|
+
case 'sm':
|
|
107
|
+
return 'stepper--size-sm';
|
|
108
|
+
case 'lg':
|
|
109
|
+
return 'stepper--size-lg';
|
|
110
|
+
default: // md
|
|
111
|
+
return 'stepper--size-md';
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const getVariantClasses = () => {
|
|
116
|
+
switch (variant) {
|
|
117
|
+
case 'vertical':
|
|
118
|
+
return 'stepper--vertical';
|
|
119
|
+
case 'horizontal':
|
|
120
|
+
return 'stepper--horizontal';
|
|
121
|
+
case 'wizard':
|
|
122
|
+
return 'stepper--wizard';
|
|
123
|
+
default:
|
|
124
|
+
return 'stepper--default';
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const getThemeClasses = () => {
|
|
129
|
+
return 'stepper stepper--stan-design';
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const getStepStatusIcon = (step: StepItem, stepIndex: number) => {
|
|
133
|
+
if (!showStepIcons) return null;
|
|
134
|
+
|
|
135
|
+
if (step.status === 'completed') {
|
|
136
|
+
return <CheckIcon className="stepper__step-icon stepper__step-icon--completed" />;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (step.status === 'error') {
|
|
140
|
+
return <ExclamationCircleIcon className="stepper__step-icon stepper__step-icon--error" />;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (step.status === 'warning') {
|
|
144
|
+
return <ExclamationTriangleIcon className="stepper__step-icon stepper__step-icon--warning" />;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (stepIndex === currentStepIndex) {
|
|
148
|
+
return <div className="stepper__step-number stepper__step-number--current">
|
|
149
|
+
{stepIndex + 1}
|
|
150
|
+
</div>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (showStepNumbers) {
|
|
154
|
+
return <div className="stepper__step-number stepper__step-number--default">
|
|
155
|
+
{stepIndex + 1}
|
|
156
|
+
</div>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const getStepClasses = (step: StepItem, stepIndex: number) => {
|
|
163
|
+
let classes = 'stepper__step';
|
|
164
|
+
|
|
165
|
+
if (step.disabled) {
|
|
166
|
+
classes += ' stepper__step--disabled';
|
|
167
|
+
} else if (stepIndex === currentStepIndex) {
|
|
168
|
+
classes += ' stepper__step--current';
|
|
169
|
+
} else if (step.status === 'completed') {
|
|
170
|
+
classes += ' stepper__step--completed';
|
|
171
|
+
} else if (step.status === 'error') {
|
|
172
|
+
classes += ' stepper__step--error';
|
|
173
|
+
} else if (step.status === 'warning') {
|
|
174
|
+
classes += ' stepper__step--warning';
|
|
175
|
+
} else {
|
|
176
|
+
classes += ' stepper__step--default';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return classes;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (steps.length === 0) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div className={`stepper__container ${getThemeClasses()} ${getSizeClasses()} ${className}`} data-testid={testId}>
|
|
188
|
+
{/* Progress Bar */}
|
|
189
|
+
{showProgress && (
|
|
190
|
+
<div className="stepper__progress">
|
|
191
|
+
<div className="stepper__progress-header">
|
|
192
|
+
<span className="stepper__progress-label">Progress</span>
|
|
193
|
+
<span className="stepper__progress-percentage">{Math.round(progressPercentage)}%</span>
|
|
194
|
+
</div>
|
|
195
|
+
<div className="stepper__progress-track">
|
|
196
|
+
<div
|
|
197
|
+
className="stepper__progress-bar"
|
|
198
|
+
style={{
|
|
199
|
+
width: `${progressPercentage}%`
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{/* Steps */}
|
|
207
|
+
<div className={`stepper__steps ${getVariantClasses()}`}>
|
|
208
|
+
{steps.map((step, stepIndex) => (
|
|
209
|
+
<div
|
|
210
|
+
key={step.id}
|
|
211
|
+
className={`stepper__step-container ${variant === 'vertical' ? 'stepper__step-container--vertical' : ''}`}
|
|
212
|
+
>
|
|
213
|
+
<div
|
|
214
|
+
className={`${getStepClasses(step, stepIndex)} ${variant === 'vertical' ? 'stepper__step--vertical' : ''}`}
|
|
215
|
+
onClick={() => handleStepClick(stepIndex)}
|
|
216
|
+
>
|
|
217
|
+
{getStepStatusIcon(step, stepIndex)}
|
|
218
|
+
|
|
219
|
+
<div className={`stepper__step-content ${variant === 'vertical' ? 'stepper__step-content--vertical' : ''}`}>
|
|
220
|
+
{showStepLabels && (
|
|
221
|
+
<div className="stepper__step-label">
|
|
222
|
+
{step.label}
|
|
223
|
+
{step.required && <span className="stepper__step-required">*</span>}
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
{showStepDescriptions && step.description && (
|
|
228
|
+
<div className="stepper__step-description">
|
|
229
|
+
{step.description}
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
{/* Step Content for Wizard variant */}
|
|
236
|
+
{variant === 'wizard' && stepIndex === currentStepIndex && step.content && (
|
|
237
|
+
<div className="stepper__step-panel">
|
|
238
|
+
{step.content}
|
|
239
|
+
|
|
240
|
+
{/* Step Actions */}
|
|
241
|
+
{step.actions && (
|
|
242
|
+
<div className="stepper__step-actions">
|
|
243
|
+
{step.actions.map((action, actionIndex) => (
|
|
244
|
+
<button
|
|
245
|
+
key={actionIndex}
|
|
246
|
+
onClick={action.onClick}
|
|
247
|
+
disabled={action.disabled}
|
|
248
|
+
className={`stepper__action stepper__action--${
|
|
249
|
+
action.variant || 'default'
|
|
250
|
+
}`}
|
|
251
|
+
>
|
|
252
|
+
{action.loading ? 'Loading...' : action.label}
|
|
253
|
+
</button>
|
|
254
|
+
))}
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
))}
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
{/* Global Actions */}
|
|
264
|
+
<div className="stepper__navigation">
|
|
265
|
+
<button
|
|
266
|
+
onClick={handleCancel}
|
|
267
|
+
className="stepper__nav-button stepper__nav-button--cancel"
|
|
268
|
+
>
|
|
269
|
+
Cancel
|
|
270
|
+
</button>
|
|
271
|
+
|
|
272
|
+
<div className="stepper__nav-actions">
|
|
273
|
+
{currentStepIndex > 0 && (
|
|
274
|
+
<button
|
|
275
|
+
onClick={() => handleStepClick(currentStepIndex - 1)}
|
|
276
|
+
className="stepper__nav-button stepper__nav-button--previous"
|
|
277
|
+
>
|
|
278
|
+
Previous
|
|
279
|
+
</button>
|
|
280
|
+
)}
|
|
281
|
+
|
|
282
|
+
{currentStepIndex < steps.length - 1 && !steps.slice(currentStepIndex + 1).every(step => step.disabled) ? (
|
|
283
|
+
<button
|
|
284
|
+
onClick={() => handleStepComplete(currentStepIndex)}
|
|
285
|
+
className="stepper__nav-button stepper__nav-button--next"
|
|
286
|
+
>
|
|
287
|
+
Next
|
|
288
|
+
</button>
|
|
289
|
+
) : (
|
|
290
|
+
<button
|
|
291
|
+
onClick={handleFinish}
|
|
292
|
+
className="stepper__nav-button stepper__nav-button--finish"
|
|
293
|
+
>
|
|
294
|
+
Finish
|
|
295
|
+
</button>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
export default Stepper;
|