@rakeyshgidwani/roger-ui-bank-theme-harvey 0.2.51 → 0.3.0
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/components/ui/button.d.ts +3 -1
- package/dist/components/ui/button.d.ts.map +1 -1
- package/dist/components/ui/button.esm.js +3 -2
- package/dist/components/ui/button.js +3 -2
- package/dist/components/ui/layout/container.d.ts +57 -0
- package/dist/components/ui/layout/container.d.ts.map +1 -0
- package/dist/components/ui/layout/container.esm.js +173 -0
- package/dist/components/ui/layout/container.js +173 -0
- package/dist/components/ui/layout/index.d.ts +9 -0
- package/dist/components/ui/layout/index.d.ts.map +1 -0
- package/dist/components/ui/layout/index.esm.js +6 -0
- package/dist/components/ui/layout/index.js +6 -0
- package/dist/components/ui/layout/responsive-grid.d.ts +93 -0
- package/dist/components/ui/layout/responsive-grid.d.ts.map +1 -0
- package/dist/components/ui/layout/responsive-grid.esm.js +124 -0
- package/dist/components/ui/layout/responsive-grid.js +124 -0
- package/dist/components/ui/navigation/index.d.ts +2 -1
- package/dist/components/ui/navigation/index.d.ts.map +1 -1
- package/dist/components/ui/navigation/index.esm.js +1 -0
- package/dist/components/ui/navigation/index.js +1 -0
- package/dist/components/ui/navigation/progressive-navigation.d.ts +37 -0
- package/dist/components/ui/navigation/progressive-navigation.d.ts.map +1 -0
- package/dist/components/ui/navigation/progressive-navigation.esm.js +145 -0
- package/dist/components/ui/navigation/progressive-navigation.js +145 -0
- package/dist/components/ui/navigation/types.d.ts +21 -0
- package/dist/components/ui/navigation/types.d.ts.map +1 -1
- package/dist/components/ui/theme-toggle.esm.js +1 -1
- package/dist/components/ui/theme-toggle.js +1 -1
- package/dist/hooks/use-adaptive-layout.d.ts +2 -1
- package/dist/hooks/use-adaptive-layout.d.ts.map +1 -1
- package/dist/hooks/use-adaptive-layout.esm.js +13 -8
- package/dist/hooks/use-adaptive-layout.js +13 -8
- package/dist/hooks/use-device.d.ts +3 -1
- package/dist/hooks/use-device.d.ts.map +1 -1
- package/dist/hooks/use-device.esm.js +14 -7
- package/dist/hooks/use-device.js +14 -7
- package/dist/index.d.ts +19 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +9 -4
- package/dist/index.js +9 -4
- package/dist/plugins/css-purge-optimizer.d.ts +25 -0
- package/dist/plugins/css-purge-optimizer.d.ts.map +1 -0
- package/dist/plugins/css-purge-optimizer.esm.js +414 -0
- package/dist/plugins/css-purge-optimizer.js +414 -0
- package/dist/plugins/performance-monitor.d.ts +29 -0
- package/dist/plugins/performance-monitor.d.ts.map +1 -0
- package/dist/plugins/performance-monitor.esm.js +221 -0
- package/dist/plugins/performance-monitor.js +221 -0
- package/dist/plugins/progressive-css-loader.d.ts +21 -0
- package/dist/plugins/progressive-css-loader.d.ts.map +1 -0
- package/dist/plugins/progressive-css-loader.esm.js +227 -0
- package/dist/plugins/progressive-css-loader.js +227 -0
- package/dist/plugins/theme-css-generator.d.ts.map +1 -1
- package/dist/plugins/theme-css-generator.esm.js +19 -6
- package/dist/plugins/theme-css-generator.js +19 -6
- package/dist/styles.css +1025 -110
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.esm.js +4 -1
- package/dist/theme.js +4 -1
- package/dist/themes/phase1-constants.d.ts +23 -0
- package/dist/themes/phase1-constants.d.ts.map +1 -0
- package/dist/themes/phase1-constants.esm.js +180 -0
- package/dist/themes/phase1-constants.js +180 -0
- package/dist/themes/themes/default.d.ts.map +1 -1
- package/dist/themes/themes/default.esm.js +4 -1
- package/dist/themes/themes/default.js +4 -1
- package/dist/themes/themes/harvey.d.ts.map +1 -1
- package/dist/themes/themes/harvey.esm.js +4 -1
- package/dist/themes/themes/harvey.js +4 -1
- package/dist/themes/types.d.ts +62 -0
- package/dist/themes/types.d.ts.map +1 -1
- package/dist/themes/validation.d.ts +17 -0
- package/dist/themes/validation.d.ts.map +1 -1
- package/dist/themes/validation.esm.js +218 -0
- package/dist/themes/validation.js +218 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/progressive-css-injector.d.ts +80 -0
- package/dist/utils/progressive-css-injector.d.ts.map +1 -0
- package/dist/utils/progressive-css-injector.esm.js +217 -0
- package/dist/utils/progressive-css-injector.js +217 -0
- package/package.json +1 -1
- package/src/components/ui/button.tsx +9 -6
- package/src/components/ui/layout/container.tsx +312 -0
- package/src/components/ui/layout/index.ts +10 -0
- package/src/components/ui/layout/responsive-grid.tsx +286 -0
- package/src/components/ui/navigation/index.ts +2 -0
- package/src/components/ui/navigation/progressive-navigation.tsx +453 -0
- package/src/components/ui/navigation/types.ts +41 -0
- package/src/components/ui/theme-toggle.tsx +4 -4
- package/src/hooks/use-adaptive-layout.ts +13 -9
- package/src/hooks/use-device.tsx +17 -10
- package/src/index.ts +19 -4
- package/src/plugins/css-purge-optimizer.ts +491 -0
- package/src/plugins/performance-monitor.ts +292 -0
- package/src/plugins/progressive-css-loader.ts +269 -0
- package/src/plugins/theme-css-generator.ts +22 -6
- package/src/styles/components/base/badge.css +2 -2
- package/src/styles/components/base/button.css +238 -35
- package/src/styles/components/base/card.css +2 -2
- package/src/styles/components/base/checkbox.css +3 -3
- package/src/styles/components/base/label.css +3 -3
- package/src/styles/components/feedback/skeleton.css +1 -1
- package/src/styles/components/feedback/toast.css +1 -1
- package/src/styles/components/index.css +3 -0
- package/src/styles/components/layout/container.css +466 -0
- package/src/styles/components/layout/index.css +5 -0
- package/src/styles/components/layout/responsive-grid.css +422 -0
- package/src/styles/components/navigation/breadcrumb.css +1 -1
- package/src/styles/components/navigation/index.css +1 -0
- package/src/styles/components/navigation/menu.css +2 -2
- package/src/styles/components/navigation/pagination.css +4 -4
- package/src/styles/components/navigation/progressive-navigation.css +633 -0
- package/src/styles/components/navigation/sidebar.css +4 -4
- package/src/styles/components/navigation/stepper.css +2 -2
- package/src/styles/components/navigation/tabs.css +1 -1
- package/src/styles/progressive.css +17 -0
- package/src/styles/themes/harvey.css +103 -19
- package/src/styles/utilities/semantic-input-system.css +7 -13
- package/src/theme.ts +5 -1
- package/src/themes/phase1-constants.ts +189 -0
- package/src/themes/themes/default.ts +5 -1
- package/src/themes/themes/harvey.ts +5 -1
- package/src/themes/types.ts +77 -1
- package/src/themes/validation.ts +249 -0
- package/src/types.ts +77 -1
- package/src/utils/progressive-css-injector.ts +254 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progressive Navigation Component
|
|
3
|
+
* Implements Phase 2 progressive navigation patterns as defined in the implementation plan
|
|
4
|
+
*
|
|
5
|
+
* Navigation Patterns:
|
|
6
|
+
* - mobile (xs-sm): hamburger menu pattern with 56px height
|
|
7
|
+
* - tablet (md): horizontal-compact pattern with 64px height
|
|
8
|
+
* - desktop (lg+): horizontal-full pattern with 72px height
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use client'
|
|
12
|
+
|
|
13
|
+
import * as React from 'react';
|
|
14
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
15
|
+
import { NavigationItem, NavigationGroup } from './types.js';
|
|
16
|
+
|
|
17
|
+
// Simple icon components for navigation
|
|
18
|
+
const HamburgerIcon: React.FC<{ className?: string }> = ({ className = '' }) => (
|
|
19
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
20
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
21
|
+
</svg>
|
|
22
|
+
);
|
|
23
|
+
|
|
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
|
+
const ChevronDownIcon: React.FC<{ className?: string }> = ({ className = '' }) => (
|
|
31
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
32
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
33
|
+
</svg>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export interface ProgressiveNavigationProps {
|
|
37
|
+
// Navigation content
|
|
38
|
+
items?: NavigationItem[];
|
|
39
|
+
groups?: NavigationGroup[];
|
|
40
|
+
|
|
41
|
+
// Logo configuration
|
|
42
|
+
logo?: React.ReactNode;
|
|
43
|
+
logoHref?: string;
|
|
44
|
+
|
|
45
|
+
// Mobile hamburger configuration
|
|
46
|
+
hamburgerHeight?: string; // Default: 56px
|
|
47
|
+
mobileLogoSize?: 'sm' | 'md' | 'lg'; // Default: sm
|
|
48
|
+
mobileTouchTarget?: string; // Default: 48px
|
|
49
|
+
|
|
50
|
+
// Tablet horizontal-compact configuration
|
|
51
|
+
tabletHeight?: string; // Default: 64px
|
|
52
|
+
tabletLogoSize?: 'sm' | 'md' | 'lg'; // Default: md
|
|
53
|
+
|
|
54
|
+
// Desktop horizontal-full configuration
|
|
55
|
+
desktopHeight?: string; // Default: 72px
|
|
56
|
+
desktopLogoSize?: 'sm' | 'md' | 'lg'; // Default: lg
|
|
57
|
+
showSecondaryActions?: boolean; // Default: true
|
|
58
|
+
secondaryActions?: React.ReactNode[];
|
|
59
|
+
|
|
60
|
+
// Responsive behavior
|
|
61
|
+
mobileBreakpoint?: string; // Default: 640px (sm) - Controls mobile/tablet cutoff
|
|
62
|
+
tabletBreakpoint?: string; // Default: 768px (md) - Reserved for future tablet sub-patterns
|
|
63
|
+
desktopBreakpoint?: string; // Default: 1024px (lg) - Controls tablet/desktop cutoff
|
|
64
|
+
|
|
65
|
+
// Event handlers
|
|
66
|
+
onItemClick?: (item: NavigationItem) => void;
|
|
67
|
+
onLogoClick?: () => void;
|
|
68
|
+
|
|
69
|
+
// Accessibility
|
|
70
|
+
'aria-label'?: string;
|
|
71
|
+
'data-testid'?: string;
|
|
72
|
+
|
|
73
|
+
// Styling
|
|
74
|
+
className?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const ProgressiveNavigation: React.FC<ProgressiveNavigationProps> = ({
|
|
78
|
+
items = [],
|
|
79
|
+
groups = [],
|
|
80
|
+
logo,
|
|
81
|
+
logoHref,
|
|
82
|
+
hamburgerHeight = '56px',
|
|
83
|
+
mobileLogoSize = 'sm',
|
|
84
|
+
mobileTouchTarget = '48px',
|
|
85
|
+
tabletHeight = '64px',
|
|
86
|
+
tabletLogoSize = 'md',
|
|
87
|
+
desktopHeight = '72px',
|
|
88
|
+
desktopLogoSize = 'lg',
|
|
89
|
+
showSecondaryActions = true,
|
|
90
|
+
secondaryActions = [],
|
|
91
|
+
mobileBreakpoint = '640px',
|
|
92
|
+
tabletBreakpoint = '768px', // Currently unused - reserved for future tablet sub-patterns
|
|
93
|
+
desktopBreakpoint = '1024px',
|
|
94
|
+
onItemClick,
|
|
95
|
+
onLogoClick,
|
|
96
|
+
'aria-label': ariaLabel = 'Main navigation',
|
|
97
|
+
'data-testid': testId = 'progressive-navigation',
|
|
98
|
+
className = '',
|
|
99
|
+
}) => {
|
|
100
|
+
// Suppress unused variable warning for tabletBreakpoint (reserved for future use)
|
|
101
|
+
void tabletBreakpoint;
|
|
102
|
+
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
103
|
+
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
|
|
104
|
+
const [screenSize, setScreenSize] = useState<'mobile' | 'tablet' | 'desktop'>('desktop');
|
|
105
|
+
const navRef = useRef<HTMLElement>(null);
|
|
106
|
+
|
|
107
|
+
// Determine current screen size based on configurable breakpoints
|
|
108
|
+
// Default: mobile < 640px, tablet 640-1023px, desktop >= 1024px
|
|
109
|
+
// This ensures sm range (640-767px) uses tablet-compact pattern, not hamburger
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
const handleResize = () => {
|
|
112
|
+
const width = window.innerWidth;
|
|
113
|
+
if (width < parseInt(mobileBreakpoint)) {
|
|
114
|
+
setScreenSize('mobile'); // < 640px: hamburger menu
|
|
115
|
+
} else if (width < parseInt(desktopBreakpoint)) {
|
|
116
|
+
setScreenSize('tablet'); // 640-1023px: horizontal-compact
|
|
117
|
+
} else {
|
|
118
|
+
setScreenSize('desktop'); // >= 1024px: horizontal-full
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
handleResize();
|
|
123
|
+
window.addEventListener('resize', handleResize);
|
|
124
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
125
|
+
}, [mobileBreakpoint, desktopBreakpoint]);
|
|
126
|
+
|
|
127
|
+
// Close mobile menu on escape key
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
130
|
+
if (event.key === 'Escape' && isMobileMenuOpen) {
|
|
131
|
+
setIsMobileMenuOpen(false);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (isMobileMenuOpen) {
|
|
136
|
+
document.addEventListener('keydown', handleEscape);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return () => {
|
|
140
|
+
document.removeEventListener('keydown', handleEscape);
|
|
141
|
+
};
|
|
142
|
+
}, [isMobileMenuOpen]);
|
|
143
|
+
|
|
144
|
+
const handleItemClick = useCallback((item: NavigationItem) => {
|
|
145
|
+
if (item.disabled) return;
|
|
146
|
+
|
|
147
|
+
if (onItemClick) {
|
|
148
|
+
onItemClick(item);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (item.onClick) {
|
|
152
|
+
item.onClick();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Close mobile menu after item click
|
|
156
|
+
if (screenSize === 'mobile') {
|
|
157
|
+
setIsMobileMenuOpen(false);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Close dropdown if open
|
|
161
|
+
setActiveDropdown(null);
|
|
162
|
+
}, [onItemClick, screenSize]);
|
|
163
|
+
|
|
164
|
+
const handleDropdownToggle = useCallback((groupId: string) => {
|
|
165
|
+
setActiveDropdown(activeDropdown === groupId ? null : groupId);
|
|
166
|
+
}, [activeDropdown]);
|
|
167
|
+
|
|
168
|
+
const handleLogoClick = useCallback(() => {
|
|
169
|
+
if (onLogoClick) {
|
|
170
|
+
onLogoClick();
|
|
171
|
+
}
|
|
172
|
+
}, [onLogoClick]);
|
|
173
|
+
|
|
174
|
+
const getNavigationHeight = () => {
|
|
175
|
+
switch (screenSize) {
|
|
176
|
+
case 'mobile':
|
|
177
|
+
return hamburgerHeight;
|
|
178
|
+
case 'tablet':
|
|
179
|
+
return tabletHeight;
|
|
180
|
+
case 'desktop':
|
|
181
|
+
return desktopHeight;
|
|
182
|
+
default:
|
|
183
|
+
return desktopHeight;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const getLogoSize = () => {
|
|
188
|
+
switch (screenSize) {
|
|
189
|
+
case 'mobile':
|
|
190
|
+
return mobileLogoSize;
|
|
191
|
+
case 'tablet':
|
|
192
|
+
return tabletLogoSize;
|
|
193
|
+
case 'desktop':
|
|
194
|
+
return desktopLogoSize;
|
|
195
|
+
default:
|
|
196
|
+
return desktopLogoSize;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const renderLogo = () => {
|
|
201
|
+
const logoContent = (
|
|
202
|
+
<div className={`progressive-nav__logo progressive-nav__logo--${getLogoSize()}`}>
|
|
203
|
+
{logo}
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (logoHref) {
|
|
208
|
+
return (
|
|
209
|
+
<a
|
|
210
|
+
href={logoHref}
|
|
211
|
+
className="progressive-nav__logo-link"
|
|
212
|
+
onClick={handleLogoClick}
|
|
213
|
+
>
|
|
214
|
+
{logoContent}
|
|
215
|
+
</a>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return logoContent;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const renderNavigationItems = () => {
|
|
223
|
+
const allItems = groups.length > 0
|
|
224
|
+
? groups.flatMap(group => group.items)
|
|
225
|
+
: items;
|
|
226
|
+
|
|
227
|
+
return allItems.map((item) => (
|
|
228
|
+
<button
|
|
229
|
+
key={item.id}
|
|
230
|
+
onClick={() => handleItemClick(item)}
|
|
231
|
+
disabled={item.disabled}
|
|
232
|
+
className={`progressive-nav__item ${
|
|
233
|
+
item.active ? 'progressive-nav__item--active' : ''
|
|
234
|
+
} ${item.disabled ? 'progressive-nav__item--disabled' : ''}`}
|
|
235
|
+
>
|
|
236
|
+
{item.icon && (
|
|
237
|
+
<span className="progressive-nav__item-icon">
|
|
238
|
+
{item.icon}
|
|
239
|
+
</span>
|
|
240
|
+
)}
|
|
241
|
+
<span className="progressive-nav__item-label">{item.label}</span>
|
|
242
|
+
{item.badge && (
|
|
243
|
+
<span className="progressive-nav__item-badge">{item.badge}</span>
|
|
244
|
+
)}
|
|
245
|
+
</button>
|
|
246
|
+
));
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const renderDropdownItems = (groupItems: NavigationItem[]) => {
|
|
250
|
+
return groupItems.map((item) => (
|
|
251
|
+
<button
|
|
252
|
+
key={item.id}
|
|
253
|
+
onClick={() => handleItemClick(item)}
|
|
254
|
+
disabled={item.disabled}
|
|
255
|
+
className={`progressive-nav__dropdown-item ${
|
|
256
|
+
item.active ? 'progressive-nav__dropdown-item--active' : ''
|
|
257
|
+
} ${item.disabled ? 'progressive-nav__dropdown-item--disabled' : ''}`}
|
|
258
|
+
>
|
|
259
|
+
{item.icon && (
|
|
260
|
+
<span className="progressive-nav__dropdown-item-icon">
|
|
261
|
+
{item.icon}
|
|
262
|
+
</span>
|
|
263
|
+
)}
|
|
264
|
+
<span className="progressive-nav__dropdown-item-label">{item.label}</span>
|
|
265
|
+
{item.badge && (
|
|
266
|
+
<span className="progressive-nav__dropdown-item-badge">{item.badge}</span>
|
|
267
|
+
)}
|
|
268
|
+
</button>
|
|
269
|
+
));
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const renderNavigationGroups = () => {
|
|
273
|
+
return groups.map((group) => (
|
|
274
|
+
<div key={group.id} className="progressive-nav__group">
|
|
275
|
+
<button
|
|
276
|
+
onClick={() => handleDropdownToggle(group.id)}
|
|
277
|
+
className={`progressive-nav__group-trigger ${
|
|
278
|
+
activeDropdown === group.id ? 'progressive-nav__group-trigger--active' : ''
|
|
279
|
+
}`}
|
|
280
|
+
>
|
|
281
|
+
<span className="progressive-nav__group-title">{group.title}</span>
|
|
282
|
+
<ChevronDownIcon
|
|
283
|
+
className={`progressive-nav__group-arrow ${
|
|
284
|
+
activeDropdown === group.id ? 'progressive-nav__group-arrow--expanded' : ''
|
|
285
|
+
}`}
|
|
286
|
+
/>
|
|
287
|
+
</button>
|
|
288
|
+
|
|
289
|
+
{activeDropdown === group.id && (
|
|
290
|
+
<div className="progressive-nav__dropdown">
|
|
291
|
+
{renderDropdownItems(group.items)}
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
294
|
+
</div>
|
|
295
|
+
));
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const renderSecondaryActions = () => {
|
|
299
|
+
if (!showSecondaryActions || !secondaryActions.length) return null;
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<div className="progressive-nav__secondary-actions">
|
|
303
|
+
{secondaryActions.map((action, index) => (
|
|
304
|
+
<div key={index} className="progressive-nav__secondary-action">
|
|
305
|
+
{action}
|
|
306
|
+
</div>
|
|
307
|
+
))}
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// Mobile hamburger menu pattern
|
|
313
|
+
if (screenSize === 'mobile') {
|
|
314
|
+
return (
|
|
315
|
+
<nav
|
|
316
|
+
ref={navRef}
|
|
317
|
+
className={`progressive-nav progressive-nav--mobile ${className}`}
|
|
318
|
+
style={{ height: getNavigationHeight() }}
|
|
319
|
+
aria-label={ariaLabel}
|
|
320
|
+
data-testid={testId}
|
|
321
|
+
>
|
|
322
|
+
<div className="progressive-nav__mobile-container">
|
|
323
|
+
{/* Mobile header */}
|
|
324
|
+
<div className="progressive-nav__mobile-header">
|
|
325
|
+
{renderLogo()}
|
|
326
|
+
|
|
327
|
+
<button
|
|
328
|
+
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
|
329
|
+
className="progressive-nav__mobile-toggle"
|
|
330
|
+
style={{
|
|
331
|
+
minHeight: mobileTouchTarget,
|
|
332
|
+
minWidth: mobileTouchTarget
|
|
333
|
+
}}
|
|
334
|
+
aria-label={isMobileMenuOpen ? 'Close menu' : 'Open menu'}
|
|
335
|
+
aria-expanded={isMobileMenuOpen}
|
|
336
|
+
>
|
|
337
|
+
{isMobileMenuOpen ? (
|
|
338
|
+
<XMarkIcon className="progressive-nav__mobile-toggle-icon" />
|
|
339
|
+
) : (
|
|
340
|
+
<HamburgerIcon className="progressive-nav__mobile-toggle-icon" />
|
|
341
|
+
)}
|
|
342
|
+
</button>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
{/* Mobile menu overlay */}
|
|
346
|
+
{isMobileMenuOpen && (
|
|
347
|
+
<>
|
|
348
|
+
<div
|
|
349
|
+
className="progressive-nav__mobile-overlay"
|
|
350
|
+
onClick={() => setIsMobileMenuOpen(false)}
|
|
351
|
+
/>
|
|
352
|
+
<div className="progressive-nav__mobile-menu">
|
|
353
|
+
<div className="progressive-nav__mobile-content">
|
|
354
|
+
{groups.length > 0 ? (
|
|
355
|
+
<div className="progressive-nav__mobile-groups">
|
|
356
|
+
{groups.map((group) => (
|
|
357
|
+
<div key={group.id} className="progressive-nav__mobile-group">
|
|
358
|
+
{group.title && (
|
|
359
|
+
<div className="progressive-nav__mobile-group-title">
|
|
360
|
+
{group.title}
|
|
361
|
+
</div>
|
|
362
|
+
)}
|
|
363
|
+
<div className="progressive-nav__mobile-group-items">
|
|
364
|
+
{group.items.map((item) => (
|
|
365
|
+
<button
|
|
366
|
+
key={item.id}
|
|
367
|
+
onClick={() => handleItemClick(item)}
|
|
368
|
+
disabled={item.disabled}
|
|
369
|
+
className={`progressive-nav__mobile-item ${
|
|
370
|
+
item.active ? 'progressive-nav__mobile-item--active' : ''
|
|
371
|
+
} ${item.disabled ? 'progressive-nav__mobile-item--disabled' : ''}`}
|
|
372
|
+
>
|
|
373
|
+
{item.icon && (
|
|
374
|
+
<span className="progressive-nav__mobile-item-icon">
|
|
375
|
+
{item.icon}
|
|
376
|
+
</span>
|
|
377
|
+
)}
|
|
378
|
+
<span className="progressive-nav__mobile-item-label">{item.label}</span>
|
|
379
|
+
{item.badge && (
|
|
380
|
+
<span className="progressive-nav__mobile-item-badge">{item.badge}</span>
|
|
381
|
+
)}
|
|
382
|
+
</button>
|
|
383
|
+
))}
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
))}
|
|
387
|
+
</div>
|
|
388
|
+
) : (
|
|
389
|
+
<div className="progressive-nav__mobile-items">
|
|
390
|
+
{renderNavigationItems()}
|
|
391
|
+
</div>
|
|
392
|
+
)}
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
</>
|
|
396
|
+
)}
|
|
397
|
+
</div>
|
|
398
|
+
</nav>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Tablet horizontal-compact pattern
|
|
403
|
+
if (screenSize === 'tablet') {
|
|
404
|
+
return (
|
|
405
|
+
<nav
|
|
406
|
+
ref={navRef}
|
|
407
|
+
className={`progressive-nav progressive-nav--tablet ${className}`}
|
|
408
|
+
style={{ height: getNavigationHeight() }}
|
|
409
|
+
aria-label={ariaLabel}
|
|
410
|
+
data-testid={testId}
|
|
411
|
+
>
|
|
412
|
+
<div className="progressive-nav__tablet-container">
|
|
413
|
+
{renderLogo()}
|
|
414
|
+
|
|
415
|
+
<div className="progressive-nav__tablet-content">
|
|
416
|
+
{groups.length > 0 ? renderNavigationGroups() : (
|
|
417
|
+
<div className="progressive-nav__tablet-items">
|
|
418
|
+
{renderNavigationItems()}
|
|
419
|
+
</div>
|
|
420
|
+
)}
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
</nav>
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Desktop horizontal-full pattern
|
|
428
|
+
return (
|
|
429
|
+
<nav
|
|
430
|
+
ref={navRef}
|
|
431
|
+
className={`progressive-nav progressive-nav--desktop ${className}`}
|
|
432
|
+
style={{ height: getNavigationHeight() }}
|
|
433
|
+
aria-label={ariaLabel}
|
|
434
|
+
data-testid={testId}
|
|
435
|
+
>
|
|
436
|
+
<div className="progressive-nav__desktop-container">
|
|
437
|
+
{renderLogo()}
|
|
438
|
+
|
|
439
|
+
<div className="progressive-nav__desktop-content">
|
|
440
|
+
{groups.length > 0 ? renderNavigationGroups() : (
|
|
441
|
+
<div className="progressive-nav__desktop-items">
|
|
442
|
+
{renderNavigationItems()}
|
|
443
|
+
</div>
|
|
444
|
+
)}
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
{renderSecondaryActions()}
|
|
448
|
+
</div>
|
|
449
|
+
</nav>
|
|
450
|
+
);
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
export default ProgressiveNavigation;
|
|
@@ -215,6 +215,47 @@ export interface MenuProps extends NavigationBaseProps {
|
|
|
215
215
|
onSelectionChange?: (selectedItems: string[]) => void;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
// ============================================================================
|
|
219
|
+
// PROGRESSIVE NAVIGATION COMPONENT TYPES (Phase 2 Enhancement)
|
|
220
|
+
// ============================================================================
|
|
221
|
+
|
|
222
|
+
export interface ProgressiveNavigationProps extends NavigationBaseProps {
|
|
223
|
+
// Navigation content
|
|
224
|
+
items?: NavigationItem[];
|
|
225
|
+
groups?: NavigationGroup[];
|
|
226
|
+
|
|
227
|
+
// Logo configuration
|
|
228
|
+
logo?: React.ReactNode;
|
|
229
|
+
logoHref?: string;
|
|
230
|
+
|
|
231
|
+
// Mobile hamburger configuration
|
|
232
|
+
hamburgerHeight?: string; // Default: 56px
|
|
233
|
+
mobileLogoSize?: 'sm' | 'md' | 'lg'; // Default: sm
|
|
234
|
+
mobileTouchTarget?: string; // Default: 48px
|
|
235
|
+
|
|
236
|
+
// Tablet horizontal-compact configuration
|
|
237
|
+
tabletHeight?: string; // Default: 64px
|
|
238
|
+
tabletLogoSize?: 'sm' | 'md' | 'lg'; // Default: md
|
|
239
|
+
|
|
240
|
+
// Desktop horizontal-full configuration
|
|
241
|
+
desktopHeight?: string; // Default: 72px
|
|
242
|
+
desktopLogoSize?: 'sm' | 'md' | 'lg'; // Default: lg
|
|
243
|
+
showSecondaryActions?: boolean; // Default: true
|
|
244
|
+
secondaryActions?: React.ReactNode[];
|
|
245
|
+
|
|
246
|
+
// Responsive behavior
|
|
247
|
+
mobileBreakpoint?: string; // Default: 640px (sm)
|
|
248
|
+
tabletBreakpoint?: string; // Default: 768px (md)
|
|
249
|
+
desktopBreakpoint?: string; // Default: 1024px (lg)
|
|
250
|
+
|
|
251
|
+
// Event handlers
|
|
252
|
+
onItemClick?: (item: NavigationItem) => void;
|
|
253
|
+
onLogoClick?: () => void;
|
|
254
|
+
|
|
255
|
+
// Accessibility
|
|
256
|
+
'aria-label'?: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
218
259
|
// ============================================================================
|
|
219
260
|
// SIDEBAR COMPONENT TYPES
|
|
220
261
|
// ============================================================================
|
|
@@ -40,16 +40,16 @@ export const ThemeToggle: React.FC = () => {
|
|
|
40
40
|
return (
|
|
41
41
|
<Button
|
|
42
42
|
variant="ghost"
|
|
43
|
-
size="
|
|
43
|
+
size="icon"
|
|
44
44
|
onClick={toggleTheme}
|
|
45
|
-
|
|
45
|
+
style={{ borderRadius: 'var(--cs-border-radius-full)' }}
|
|
46
46
|
aria-label={`Switch to ${isDark ? 'light' : 'dark'} mode`}
|
|
47
47
|
aria-pressed={isDark}
|
|
48
48
|
>
|
|
49
49
|
{isDark ? (
|
|
50
|
-
<Sun
|
|
50
|
+
<Sun style={{ width: 'var(--button-icon-size)', height: 'var(--button-icon-size)' }} />
|
|
51
51
|
) : (
|
|
52
|
-
<Moon
|
|
52
|
+
<Moon style={{ width: 'var(--button-icon-size)', height: 'var(--button-icon-size)' }} />
|
|
53
53
|
)}
|
|
54
54
|
</Button>
|
|
55
55
|
);
|
|
@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
|
2
2
|
|
|
3
3
|
export type DeviceType = 'mobile' | 'tablet' | 'desktop'
|
|
4
4
|
export type Orientation = 'portrait' | 'landscape'
|
|
5
|
-
export type ScreenSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
|
|
5
|
+
export type ScreenSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl'
|
|
6
6
|
|
|
7
7
|
export interface AdaptiveLayoutState {
|
|
8
8
|
deviceType: DeviceType
|
|
@@ -28,6 +28,7 @@ export interface AdaptiveLayoutConfig {
|
|
|
28
28
|
lg: number
|
|
29
29
|
xl: number
|
|
30
30
|
'2xl': number
|
|
31
|
+
'3xl': number
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -49,12 +50,13 @@ export const useAdaptiveLayout = (
|
|
|
49
50
|
enableBatteryOptimization = true,
|
|
50
51
|
enableMemoryOptimization = true,
|
|
51
52
|
breakpoints = {
|
|
52
|
-
xs:
|
|
53
|
-
sm: 640,
|
|
54
|
-
md: 768,
|
|
55
|
-
lg: 1024,
|
|
56
|
-
xl: 1280,
|
|
57
|
-
'2xl': 1536
|
|
53
|
+
xs: 475, // Phase 1: Large phones (iPhone 14 Plus, etc.)
|
|
54
|
+
sm: 640, // Small tablets, large phones landscape
|
|
55
|
+
md: 768, // Tablets (iPad Mini, etc.)
|
|
56
|
+
lg: 1024, // Desktop, large tablets
|
|
57
|
+
xl: 1280, // Large desktop (MacBook Pro 13")
|
|
58
|
+
'2xl': 1536, // Ultra-wide (MacBook Pro 16", external monitors)
|
|
59
|
+
'3xl': 1920 // Large external monitors, TV displays
|
|
58
60
|
}
|
|
59
61
|
} = config
|
|
60
62
|
|
|
@@ -79,12 +81,14 @@ export const useAdaptiveLayout = (
|
|
|
79
81
|
|
|
80
82
|
// Screen size detection
|
|
81
83
|
const detectScreenSize = useCallback((width: number): ScreenSize => {
|
|
82
|
-
if (width < breakpoints.
|
|
84
|
+
if (width < breakpoints.xs) return 'xs' // Small phones (< 475px)
|
|
85
|
+
if (width < breakpoints.sm) return 'xs' // Still xs for 475-640px range
|
|
83
86
|
if (width < breakpoints.md) return 'sm'
|
|
84
87
|
if (width < breakpoints.lg) return 'md'
|
|
85
88
|
if (width < breakpoints.xl) return 'lg'
|
|
86
89
|
if (width < breakpoints['2xl']) return 'xl'
|
|
87
|
-
return '2xl'
|
|
90
|
+
if (width < breakpoints['3xl']) return '2xl'
|
|
91
|
+
return '3xl'
|
|
88
92
|
}, [breakpoints])
|
|
89
93
|
|
|
90
94
|
// Touch device detection
|
package/src/hooks/use-device.tsx
CHANGED
|
@@ -6,16 +6,18 @@ export interface DeviceInfo {
|
|
|
6
6
|
isMobile: boolean
|
|
7
7
|
isTablet: boolean
|
|
8
8
|
isDesktop: boolean
|
|
9
|
-
|
|
9
|
+
isLargeDisplay: boolean
|
|
10
|
+
screenSize: 'mobile' | 'tablet' | 'desktop' | 'largeDisplay'
|
|
10
11
|
orientation: 'portrait' | 'landscape'
|
|
11
12
|
touchDevice: boolean
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
//
|
|
15
|
+
// Updated breakpoints to align with Phase 1 Enhanced Breakpoint System
|
|
16
|
+
// xs: 475px, sm: 640px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px, 3xl: 1920px
|
|
15
17
|
const BREAKPOINTS = {
|
|
16
|
-
mobile: 768,
|
|
17
|
-
tablet: 1024,
|
|
18
|
-
desktop:
|
|
18
|
+
mobile: 768, // xs-sm range (475-768px)
|
|
19
|
+
tablet: 1024, // md range (768-1024px)
|
|
20
|
+
desktop: 1536 // lg-xl range (1024-1536px), largeDisplay starts at 1536px+ (2xl-3xl)
|
|
19
21
|
} as const
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -27,6 +29,7 @@ export const useDevice = (): DeviceInfo => {
|
|
|
27
29
|
isMobile: false,
|
|
28
30
|
isTablet: false,
|
|
29
31
|
isDesktop: false,
|
|
32
|
+
isLargeDisplay: false,
|
|
30
33
|
screenSize: 'desktop',
|
|
31
34
|
orientation: 'landscape',
|
|
32
35
|
touchDevice: false
|
|
@@ -37,16 +40,18 @@ export const useDevice = (): DeviceInfo => {
|
|
|
37
40
|
const width = window.innerWidth
|
|
38
41
|
const height = window.innerHeight
|
|
39
42
|
|
|
40
|
-
// Determine device type based on width
|
|
43
|
+
// Determine device type based on width (aligned with new breakpoint system)
|
|
41
44
|
const isMobile = width < BREAKPOINTS.mobile
|
|
42
45
|
const isTablet = width >= BREAKPOINTS.mobile && width < BREAKPOINTS.tablet
|
|
43
|
-
const isDesktop = width >= BREAKPOINTS.tablet
|
|
44
|
-
|
|
46
|
+
const isDesktop = width >= BREAKPOINTS.tablet && width < BREAKPOINTS.desktop
|
|
47
|
+
const isLargeDisplay = width >= BREAKPOINTS.desktop
|
|
48
|
+
|
|
45
49
|
// Determine screen size category
|
|
46
50
|
let screenSize: DeviceInfo['screenSize']
|
|
47
51
|
if (isMobile) screenSize = 'mobile'
|
|
48
52
|
else if (isTablet) screenSize = 'tablet'
|
|
49
|
-
else screenSize = 'desktop'
|
|
53
|
+
else if (isDesktop) screenSize = 'desktop'
|
|
54
|
+
else screenSize = 'largeDisplay'
|
|
50
55
|
|
|
51
56
|
// Determine orientation
|
|
52
57
|
const orientation: DeviceInfo['orientation'] = width > height ? 'landscape' : 'portrait'
|
|
@@ -61,6 +66,7 @@ export const useDevice = (): DeviceInfo => {
|
|
|
61
66
|
isMobile,
|
|
62
67
|
isTablet,
|
|
63
68
|
isDesktop,
|
|
69
|
+
isLargeDisplay,
|
|
64
70
|
screenSize,
|
|
65
71
|
orientation,
|
|
66
72
|
touchDevice
|
|
@@ -92,8 +98,9 @@ export const useDevice = (): DeviceInfo => {
|
|
|
92
98
|
|
|
93
99
|
// Convenience hooks for specific use cases
|
|
94
100
|
export const useIsMobile = () => useDevice().isMobile
|
|
95
|
-
export const useIsTablet = () => useDevice().isTablet
|
|
101
|
+
export const useIsTablet = () => useDevice().isTablet
|
|
96
102
|
export const useIsDesktop = () => useDevice().isDesktop
|
|
103
|
+
export const useIsLargeDisplay = () => useDevice().isLargeDisplay
|
|
97
104
|
export const useOrientation = () => useDevice().orientation
|
|
98
105
|
export const useIsTouchDevice = () => useDevice().touchDevice
|
|
99
106
|
|