@qwickapps/react-framework 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +326 -0
  2. package/dist/components/AccessibilityProvider.d.ts +64 -0
  3. package/dist/components/AccessibilityProvider.d.ts.map +1 -0
  4. package/dist/components/Breadcrumbs.d.ts +39 -0
  5. package/dist/components/Breadcrumbs.d.ts.map +1 -0
  6. package/dist/components/ErrorBoundary.d.ts +39 -0
  7. package/dist/components/ErrorBoundary.d.ts.map +1 -0
  8. package/dist/components/QwickApp.d.ts.map +1 -1
  9. package/dist/components/forms/FormBlock.d.ts +1 -1
  10. package/dist/components/forms/FormBlock.d.ts.map +1 -1
  11. package/dist/components/index.d.ts +3 -0
  12. package/dist/components/index.d.ts.map +1 -1
  13. package/dist/components/input/SwitchInputField.d.ts +28 -0
  14. package/dist/components/input/SwitchInputField.d.ts.map +1 -0
  15. package/dist/components/input/index.d.ts +2 -0
  16. package/dist/components/input/index.d.ts.map +1 -1
  17. package/dist/components/layout/CollapsibleLayout/CollapsibleLayout.d.ts +34 -0
  18. package/dist/components/layout/CollapsibleLayout/CollapsibleLayout.d.ts.map +1 -0
  19. package/dist/components/layout/CollapsibleLayout/index.d.ts +9 -0
  20. package/dist/components/layout/CollapsibleLayout/index.d.ts.map +1 -0
  21. package/dist/components/layout/index.d.ts +2 -0
  22. package/dist/components/layout/index.d.ts.map +1 -1
  23. package/dist/index.bundled.css +12 -0
  24. package/dist/index.esm.js +1678 -25
  25. package/dist/index.js +1689 -21
  26. package/dist/schemas/CollapsibleLayoutSchema.d.ts +31 -0
  27. package/dist/schemas/CollapsibleLayoutSchema.d.ts.map +1 -0
  28. package/dist/schemas/SwitchInputFieldSchema.d.ts +18 -0
  29. package/dist/schemas/SwitchInputFieldSchema.d.ts.map +1 -0
  30. package/dist/types/CollapsibleLayout.d.ts +142 -0
  31. package/dist/types/CollapsibleLayout.d.ts.map +1 -0
  32. package/dist/types/index.d.ts +1 -0
  33. package/dist/types/index.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/components/AccessibilityProvider.tsx +466 -0
  36. package/src/components/Breadcrumbs.tsx +223 -0
  37. package/src/components/ErrorBoundary.tsx +216 -0
  38. package/src/components/QwickApp.tsx +17 -11
  39. package/src/components/__tests__/AccessibilityProvider.test.tsx +330 -0
  40. package/src/components/__tests__/Breadcrumbs.test.tsx +268 -0
  41. package/src/components/__tests__/ErrorBoundary.test.tsx +163 -0
  42. package/src/components/forms/FormBlock.tsx +2 -2
  43. package/src/components/index.ts +3 -0
  44. package/src/components/input/SwitchInputField.tsx +165 -0
  45. package/src/components/input/index.ts +2 -0
  46. package/src/components/layout/CollapsibleLayout/CollapsibleLayout.tsx +554 -0
  47. package/src/components/layout/CollapsibleLayout/__tests__/CollapsibleLayout.test.tsx +1469 -0
  48. package/src/components/layout/CollapsibleLayout/index.tsx +17 -0
  49. package/src/components/layout/index.ts +4 -1
  50. package/src/components/pages/FormPage.tsx +1 -1
  51. package/src/schemas/CollapsibleLayoutSchema.ts +276 -0
  52. package/src/schemas/SwitchInputFieldSchema.ts +99 -0
  53. package/src/stories/AccessibilityProvider.stories.tsx +284 -0
  54. package/src/stories/Breadcrumbs.stories.tsx +304 -0
  55. package/src/stories/CollapsibleLayout.stories.tsx +1566 -0
  56. package/src/stories/ErrorBoundary.stories.tsx +159 -0
  57. package/src/types/CollapsibleLayout.ts +231 -0
  58. package/src/types/index.ts +1 -0
  59. package/dist/schemas/Builders.d.ts +0 -7
  60. package/dist/schemas/Builders.d.ts.map +0 -1
  61. package/dist/schemas/types.d.ts +0 -7
  62. package/dist/schemas/types.d.ts.map +0 -1
  63. package/dist/types/DataBinding.d.ts +0 -7
  64. package/dist/types/DataBinding.d.ts.map +0 -1
  65. package/dist/types/DataProvider.d.ts +0 -7
  66. package/dist/types/DataProvider.d.ts.map +0 -1
@@ -0,0 +1,466 @@
1
+ import React, { createContext, useContext, useEffect, useReducer, ReactNode } from 'react';
2
+
3
+ // Accessibility State
4
+ export interface AccessibilityState {
5
+ highContrast: boolean;
6
+ reducedMotion: boolean;
7
+ largeText: boolean;
8
+ focusVisible: boolean;
9
+ isKeyboardUser: boolean;
10
+ issues: AccessibilityIssue[];
11
+ lastAnnouncement: Announcement | null;
12
+ preferences: Record<string, any>;
13
+ }
14
+
15
+ export interface AccessibilityIssue {
16
+ type: string;
17
+ message: string;
18
+ level: 'error' | 'warning' | 'info';
19
+ element?: Element;
20
+ }
21
+
22
+ export interface Announcement {
23
+ message: string;
24
+ level: 'polite' | 'assertive';
25
+ timestamp: number;
26
+ }
27
+
28
+ // Actions
29
+ type AccessibilityAction =
30
+ | { type: 'SET_HIGH_CONTRAST'; payload: boolean }
31
+ | { type: 'SET_REDUCED_MOTION'; payload: boolean }
32
+ | { type: 'SET_LARGE_TEXT'; payload: boolean }
33
+ | { type: 'SET_FOCUS_VISIBLE'; payload: boolean }
34
+ | { type: 'SET_KEYBOARD_USER'; payload: boolean }
35
+ | { type: 'ADD_ISSUE'; payload: AccessibilityIssue }
36
+ | { type: 'CLEAR_ISSUES' }
37
+ | { type: 'SET_ANNOUNCEMENT'; payload: Announcement };
38
+
39
+ // Context
40
+ export interface AccessibilityContextValue extends AccessibilityState {
41
+ setHighContrast: (enabled: boolean) => void;
42
+ setReducedMotion: (enabled: boolean) => void;
43
+ setLargeText: (enabled: boolean) => void;
44
+ setFocusVisible: (enabled: boolean) => void;
45
+ announce: (message: string, level?: 'polite' | 'assertive') => void;
46
+ announcePolite: (message: string) => void;
47
+ announceAssertive: (message: string) => void;
48
+ addIssue: (issue: AccessibilityIssue) => void;
49
+ clearIssues: () => void;
50
+ runAudit: () => void;
51
+ }
52
+
53
+ const AccessibilityContext = createContext<AccessibilityContextValue | null>(null);
54
+
55
+ // Reducer
56
+ const accessibilityReducer = (state: AccessibilityState, action: AccessibilityAction): AccessibilityState => {
57
+ switch (action.type) {
58
+ case 'SET_HIGH_CONTRAST':
59
+ return { ...state, highContrast: action.payload };
60
+ case 'SET_REDUCED_MOTION':
61
+ return { ...state, reducedMotion: action.payload };
62
+ case 'SET_LARGE_TEXT':
63
+ return { ...state, largeText: action.payload };
64
+ case 'SET_FOCUS_VISIBLE':
65
+ return { ...state, focusVisible: action.payload };
66
+ case 'SET_KEYBOARD_USER':
67
+ return { ...state, isKeyboardUser: action.payload };
68
+ case 'ADD_ISSUE':
69
+ return { ...state, issues: [...state.issues, action.payload] };
70
+ case 'CLEAR_ISSUES':
71
+ return { ...state, issues: [] };
72
+ case 'SET_ANNOUNCEMENT':
73
+ return { ...state, lastAnnouncement: action.payload };
74
+ default:
75
+ return state;
76
+ }
77
+ };
78
+
79
+ // Initial state
80
+ const initialState: AccessibilityState = {
81
+ highContrast: false,
82
+ reducedMotion: false,
83
+ largeText: false,
84
+ focusVisible: true,
85
+ isKeyboardUser: false,
86
+ issues: [],
87
+ lastAnnouncement: null,
88
+ preferences: {}
89
+ };
90
+
91
+ // ARIA Live Manager
92
+ class AriaLiveManager {
93
+ private politeRegion: HTMLElement | null = null;
94
+ private assertiveRegion: HTMLElement | null = null;
95
+
96
+ constructor() {
97
+ this.createLiveRegions();
98
+ }
99
+
100
+ private createLiveRegions() {
101
+ if (typeof document === 'undefined') return;
102
+
103
+ // Polite announcements
104
+ this.politeRegion = document.createElement('div');
105
+ this.politeRegion.setAttribute('aria-live', 'polite');
106
+ this.politeRegion.setAttribute('aria-atomic', 'true');
107
+ this.politeRegion.setAttribute('id', 'qwickapps-aria-live-polite');
108
+ this.politeRegion.style.cssText = `
109
+ position: absolute !important;
110
+ left: -10000px !important;
111
+ width: 1px !important;
112
+ height: 1px !important;
113
+ overflow: hidden !important;
114
+ `;
115
+ document.body.appendChild(this.politeRegion);
116
+
117
+ // Assertive announcements
118
+ this.assertiveRegion = document.createElement('div');
119
+ this.assertiveRegion.setAttribute('aria-live', 'assertive');
120
+ this.assertiveRegion.setAttribute('aria-atomic', 'true');
121
+ this.assertiveRegion.setAttribute('id', 'qwickapps-aria-live-assertive');
122
+ this.assertiveRegion.style.cssText = `
123
+ position: absolute !important;
124
+ left: -10000px !important;
125
+ width: 1px !important;
126
+ height: 1px !important;
127
+ overflow: hidden !important;
128
+ `;
129
+ document.body.appendChild(this.assertiveRegion);
130
+ }
131
+
132
+ announce(message: string, level: 'polite' | 'assertive' = 'polite') {
133
+ if (level === 'assertive') {
134
+ this.announceAssertive(message);
135
+ } else {
136
+ this.announcePolite(message);
137
+ }
138
+ }
139
+
140
+ announcePolite(message: string) {
141
+ if (!this.politeRegion) return;
142
+
143
+ this.politeRegion.textContent = '';
144
+ // Small delay ensures screen readers detect the change
145
+ setTimeout(() => {
146
+ if (this.politeRegion) {
147
+ this.politeRegion.textContent = message;
148
+ }
149
+ }, 100);
150
+ }
151
+
152
+ announceAssertive(message: string) {
153
+ if (!this.assertiveRegion) return;
154
+
155
+ this.assertiveRegion.textContent = '';
156
+ // Small delay ensures screen readers detect the change
157
+ setTimeout(() => {
158
+ if (this.assertiveRegion) {
159
+ this.assertiveRegion.textContent = message;
160
+ }
161
+ }, 100);
162
+ }
163
+ }
164
+
165
+ const ariaLiveManager = new AriaLiveManager();
166
+
167
+ // Props
168
+ export interface AccessibilityProviderProps {
169
+ children: ReactNode;
170
+ enableAudit?: boolean;
171
+ }
172
+
173
+ /**
174
+ * Accessibility Provider Component
175
+ * Provides comprehensive accessibility context and utilities
176
+ *
177
+ * Features:
178
+ * - System preference detection (high contrast, reduced motion)
179
+ * - Keyboard navigation detection
180
+ * - ARIA live announcements
181
+ * - Focus management
182
+ * - Accessibility auditing
183
+ * - Settings persistence
184
+ */
185
+ export const AccessibilityProvider: React.FC<AccessibilityProviderProps> = ({
186
+ children,
187
+ enableAudit = process.env.NODE_ENV === 'development'
188
+ }) => {
189
+ const [state, dispatch] = useReducer(accessibilityReducer, initialState);
190
+
191
+ useEffect(() => {
192
+ // Detect user preferences from system
193
+ detectUserPreferences();
194
+
195
+ // Set up keyboard detection
196
+ const keyboardCleanup = setupKeyboardDetection();
197
+
198
+ // Initialize focus management
199
+ initializeFocusManagement();
200
+
201
+ // Run initial accessibility audit
202
+ if (enableAudit) {
203
+ runAccessibilityAudit();
204
+ }
205
+
206
+ // Cleanup
207
+ return () => {
208
+ if (keyboardCleanup) keyboardCleanup();
209
+ };
210
+ }, [enableAudit]);
211
+
212
+ const detectUserPreferences = () => {
213
+ if (typeof window === 'undefined') return;
214
+
215
+ // High contrast mode
216
+ if (window.matchMedia && window.matchMedia('(prefers-contrast: high)').matches) {
217
+ dispatch({ type: 'SET_HIGH_CONTRAST', payload: true });
218
+ }
219
+
220
+ // Reduced motion
221
+ if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
222
+ dispatch({ type: 'SET_REDUCED_MOTION', payload: true });
223
+ }
224
+
225
+ // Listen for changes
226
+ if (window.matchMedia) {
227
+ const contrastMedia = window.matchMedia('(prefers-contrast: high)');
228
+ const motionMedia = window.matchMedia('(prefers-reduced-motion: reduce)');
229
+
230
+ const contrastHandler = (e: MediaQueryListEvent) => {
231
+ dispatch({ type: 'SET_HIGH_CONTRAST', payload: e.matches });
232
+ };
233
+
234
+ const motionHandler = (e: MediaQueryListEvent) => {
235
+ dispatch({ type: 'SET_REDUCED_MOTION', payload: e.matches });
236
+ };
237
+
238
+ contrastMedia.addEventListener('change', contrastHandler);
239
+ motionMedia.addEventListener('change', motionHandler);
240
+
241
+ // Return cleanup function
242
+ return () => {
243
+ contrastMedia.removeEventListener('change', contrastHandler);
244
+ motionMedia.removeEventListener('change', motionHandler);
245
+ };
246
+ }
247
+ };
248
+
249
+ const setupKeyboardDetection = () => {
250
+ if (typeof document === 'undefined') return;
251
+
252
+ let keyboardUser = false;
253
+
254
+ const handleKeyDown = (e: KeyboardEvent) => {
255
+ if (e.key === 'Tab') {
256
+ keyboardUser = true;
257
+ dispatch({ type: 'SET_KEYBOARD_USER', payload: true });
258
+ document.body.classList.add('keyboard-user');
259
+ }
260
+ };
261
+
262
+ const handleMouseDown = () => {
263
+ if (keyboardUser) {
264
+ keyboardUser = false;
265
+ dispatch({ type: 'SET_KEYBOARD_USER', payload: false });
266
+ document.body.classList.remove('keyboard-user');
267
+ }
268
+ };
269
+
270
+ document.addEventListener('keydown', handleKeyDown);
271
+ document.addEventListener('mousedown', handleMouseDown);
272
+
273
+ return () => {
274
+ document.removeEventListener('keydown', handleKeyDown);
275
+ document.removeEventListener('mousedown', handleMouseDown);
276
+ };
277
+ };
278
+
279
+ const initializeFocusManagement = () => {
280
+ if (typeof document === 'undefined') return;
281
+
282
+ // Enhanced focus indicators for keyboard users
283
+ const style = document.createElement('style');
284
+ style.textContent = `
285
+ .keyboard-user *:focus {
286
+ outline: 3px solid #005cee !important;
287
+ outline-offset: 2px !important;
288
+ }
289
+
290
+ .high-contrast *:focus {
291
+ outline: 3px solid #ffffff !important;
292
+ outline-offset: 2px !important;
293
+ box-shadow: 0 0 0 1px #000000 !important;
294
+ }
295
+
296
+ .reduced-motion * {
297
+ animation-duration: 0.01ms !important;
298
+ animation-iteration-count: 1 !important;
299
+ transition-duration: 0.01ms !important;
300
+ }
301
+
302
+ .large-text {
303
+ font-size: 1.2em !important;
304
+ }
305
+ `;
306
+ document.head.appendChild(style);
307
+ };
308
+
309
+ const runAccessibilityAudit = () => {
310
+ if (typeof document === 'undefined') return;
311
+
312
+ setTimeout(() => {
313
+ const issues: AccessibilityIssue[] = [];
314
+
315
+ // Check for images without alt text
316
+ const images = document.querySelectorAll('img:not([alt])');
317
+ images.forEach(img => {
318
+ issues.push({
319
+ type: 'missing-alt-text',
320
+ message: 'Image missing alt attribute',
321
+ level: 'error',
322
+ element: img
323
+ });
324
+ });
325
+
326
+ // Check for buttons without accessible names
327
+ const buttons = document.querySelectorAll('button:not([aria-label]):not([title])');
328
+ buttons.forEach(button => {
329
+ if (!button.textContent?.trim()) {
330
+ issues.push({
331
+ type: 'unnamed-button',
332
+ message: 'Button missing accessible name',
333
+ level: 'error',
334
+ element: button
335
+ });
336
+ }
337
+ });
338
+
339
+ // Check for form inputs without labels
340
+ const inputs = document.querySelectorAll('input:not([aria-label]):not([title])');
341
+ inputs.forEach(input => {
342
+ const id = input.getAttribute('id');
343
+ if (id) {
344
+ const label = document.querySelector(`label[for="${id}"]`);
345
+ if (!label) {
346
+ issues.push({
347
+ type: 'unlabeled-input',
348
+ message: 'Form input missing label',
349
+ level: 'error',
350
+ element: input
351
+ });
352
+ }
353
+ } else {
354
+ issues.push({
355
+ type: 'unlabeled-input',
356
+ message: 'Form input missing label',
357
+ level: 'error',
358
+ element: input
359
+ });
360
+ }
361
+ });
362
+
363
+ dispatch({ type: 'CLEAR_ISSUES' });
364
+
365
+ issues.forEach(issue => {
366
+ dispatch({ type: 'ADD_ISSUE', payload: issue });
367
+ });
368
+
369
+ if (issues.length > 0) {
370
+ console.group('🔍 Accessibility Issues Found');
371
+ issues.forEach(issue => {
372
+ const logMethod = issue.level === 'error' ? console.error : console.warn;
373
+ logMethod(`${issue.type}: ${issue.message}`);
374
+ });
375
+ console.groupEnd();
376
+ }
377
+ }, 1000);
378
+ };
379
+
380
+ // Context value
381
+ const contextValue: AccessibilityContextValue = {
382
+ ...state,
383
+
384
+ // Actions
385
+ setHighContrast: (enabled: boolean) => dispatch({ type: 'SET_HIGH_CONTRAST', payload: enabled }),
386
+ setReducedMotion: (enabled: boolean) => dispatch({ type: 'SET_REDUCED_MOTION', payload: enabled }),
387
+ setLargeText: (enabled: boolean) => dispatch({ type: 'SET_LARGE_TEXT', payload: enabled }),
388
+ setFocusVisible: (enabled: boolean) => dispatch({ type: 'SET_FOCUS_VISIBLE', payload: enabled }),
389
+
390
+ // Utilities
391
+ announce: (message: string, level: 'polite' | 'assertive' = 'polite') => {
392
+ ariaLiveManager.announce(message, level);
393
+ dispatch({ type: 'SET_ANNOUNCEMENT', payload: { message, level, timestamp: Date.now() } });
394
+ },
395
+
396
+ announcePolite: (message: string) => {
397
+ ariaLiveManager.announcePolite(message);
398
+ dispatch({ type: 'SET_ANNOUNCEMENT', payload: { message, level: 'polite', timestamp: Date.now() } });
399
+ },
400
+
401
+ announceAssertive: (message: string) => {
402
+ ariaLiveManager.announceAssertive(message);
403
+ dispatch({ type: 'SET_ANNOUNCEMENT', payload: { message, level: 'assertive', timestamp: Date.now() } });
404
+ },
405
+
406
+ addIssue: (issue: AccessibilityIssue) => dispatch({ type: 'ADD_ISSUE', payload: issue }),
407
+ clearIssues: () => dispatch({ type: 'CLEAR_ISSUES' }),
408
+
409
+ // Audit function
410
+ runAudit: runAccessibilityAudit
411
+ };
412
+
413
+ // Apply CSS classes based on preferences
414
+ useEffect(() => {
415
+ if (typeof document === 'undefined') return;
416
+
417
+ const { highContrast, reducedMotion, largeText } = state;
418
+
419
+ document.body.classList.toggle('high-contrast', highContrast);
420
+ document.body.classList.toggle('reduced-motion', reducedMotion);
421
+ document.body.classList.toggle('large-text', largeText);
422
+ }, [state.highContrast, state.reducedMotion, state.largeText]);
423
+
424
+ return (
425
+ <AccessibilityContext.Provider value={contextValue}>
426
+ {children}
427
+ </AccessibilityContext.Provider>
428
+ );
429
+ };
430
+
431
+ /**
432
+ * Hook to access accessibility context
433
+ */
434
+ export const useAccessibility = (): AccessibilityContextValue => {
435
+ const context = useContext(AccessibilityContext);
436
+
437
+ if (!context) {
438
+ throw new Error('useAccessibility must be used within an AccessibilityProvider');
439
+ }
440
+
441
+ return context;
442
+ };
443
+
444
+ /**
445
+ * Higher-Order Component for accessibility enhancements
446
+ */
447
+ export const withAccessibility = <P extends object>(
448
+ WrappedComponent: React.ComponentType<P>
449
+ ) => {
450
+ const AccessibilityEnhancedComponent = (props: P) => {
451
+ const accessibility = useAccessibility();
452
+
453
+ return (
454
+ <WrappedComponent
455
+ {...props}
456
+ accessibility={accessibility}
457
+ />
458
+ );
459
+ };
460
+
461
+ AccessibilityEnhancedComponent.displayName = `withAccessibility(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`;
462
+
463
+ return AccessibilityEnhancedComponent;
464
+ };
465
+
466
+ export default AccessibilityProvider;
@@ -0,0 +1,223 @@
1
+ import React from 'react';
2
+
3
+ export interface BreadcrumbItem {
4
+ label: string;
5
+ href?: string;
6
+ icon?: React.ReactNode;
7
+ current?: boolean;
8
+ }
9
+
10
+ export interface BreadcrumbsProps {
11
+ items: BreadcrumbItem[];
12
+ separator?: React.ReactNode;
13
+ className?: string;
14
+ onNavigate?: (item: BreadcrumbItem, index: number) => void;
15
+ maxItems?: number;
16
+ showRoot?: boolean;
17
+ }
18
+
19
+ /**
20
+ * Generic Breadcrumbs component for navigation hierarchy
21
+ *
22
+ * Features:
23
+ * - Accessible navigation with proper ARIA labels
24
+ * - Customizable separators and icons
25
+ * - Responsive design with item truncation
26
+ * - Support for custom navigation handlers
27
+ * - Keyboard navigation support
28
+ * - Screen reader friendly
29
+ */
30
+ export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
31
+ items,
32
+ separator = '/',
33
+ className = '',
34
+ onNavigate,
35
+ maxItems,
36
+ showRoot = true
37
+ }) => {
38
+ // Filter and prepare items
39
+ let displayItems = showRoot ? items : items.slice(1);
40
+
41
+ // Handle max items with ellipsis
42
+ if (maxItems && displayItems.length > maxItems) {
43
+ const firstItems = displayItems.slice(0, 1);
44
+ const lastItems = displayItems.slice(-Math.max(1, maxItems - 2));
45
+ displayItems = [
46
+ ...firstItems,
47
+ { label: '...', href: undefined, current: false },
48
+ ...lastItems
49
+ ];
50
+ }
51
+
52
+ const handleItemClick = (e: React.MouseEvent, item: BreadcrumbItem, index: number) => {
53
+ if (onNavigate) {
54
+ e.preventDefault();
55
+ onNavigate(item, index);
56
+ }
57
+ };
58
+
59
+ const handleKeyDown = (e: React.KeyboardEvent, item: BreadcrumbItem, index: number) => {
60
+ if (e.key === 'Enter' || e.key === ' ') {
61
+ e.preventDefault();
62
+ if (onNavigate) {
63
+ onNavigate(item, index);
64
+ } else if (item.href) {
65
+ window.location.href = item.href;
66
+ }
67
+ }
68
+ };
69
+
70
+ if (displayItems.length <= 1) {
71
+ return null;
72
+ }
73
+
74
+ return (
75
+ <nav
76
+ className={`breadcrumbs ${className}`}
77
+ role="navigation"
78
+ aria-label="Breadcrumb navigation"
79
+ style={{
80
+ display: 'flex',
81
+ alignItems: 'center',
82
+ fontSize: '14px',
83
+ color: '#6b7280',
84
+ ...defaultStyles.nav
85
+ }}
86
+ >
87
+ <ol
88
+ style={{
89
+ display: 'flex',
90
+ alignItems: 'center',
91
+ listStyle: 'none',
92
+ margin: 0,
93
+ padding: 0,
94
+ gap: '8px'
95
+ }}
96
+ >
97
+ {displayItems.map((item, index) => {
98
+ const isLast = index === displayItems.length - 1;
99
+ const isClickable = (item.href || onNavigate) && !item.current && item.label !== '...';
100
+
101
+ return (
102
+ <li key={`${item.label}-${index}`} style={{ display: 'flex', alignItems: 'center' }}>
103
+ {isClickable ? (
104
+ <a
105
+ href={item.href}
106
+ onClick={(e) => handleItemClick(e, item, index)}
107
+ onKeyDown={(e) => handleKeyDown(e, item, index)}
108
+ style={{
109
+ ...defaultStyles.link,
110
+ color: isLast ? '#374151' : '#6b7280',
111
+ textDecoration: 'none',
112
+ display: 'flex',
113
+ alignItems: 'center',
114
+ gap: '4px'
115
+ }}
116
+ tabIndex={0}
117
+ aria-current={item.current ? 'page' : undefined}
118
+ >
119
+ {item.icon && (
120
+ <span
121
+ style={{ display: 'flex', alignItems: 'center' }}
122
+ aria-hidden="true"
123
+ >
124
+ {item.icon}
125
+ </span>
126
+ )}
127
+ <span>{item.label}</span>
128
+ </a>
129
+ ) : (
130
+ <span
131
+ style={{
132
+ ...defaultStyles.current,
133
+ color: isLast ? '#111827' : '#6b7280',
134
+ fontWeight: isLast ? 600 : 400,
135
+ display: 'flex',
136
+ alignItems: 'center',
137
+ gap: '4px'
138
+ }}
139
+ aria-current={item.current ? 'page' : undefined}
140
+ >
141
+ {item.icon && (
142
+ <span
143
+ style={{ display: 'flex', alignItems: 'center' }}
144
+ aria-hidden="true"
145
+ >
146
+ {item.icon}
147
+ </span>
148
+ )}
149
+ <span>{item.label}</span>
150
+ </span>
151
+ )}
152
+
153
+ {!isLast && (
154
+ <span
155
+ style={{
156
+ display: 'flex',
157
+ alignItems: 'center',
158
+ marginLeft: '8px',
159
+ color: '#d1d5db',
160
+ fontSize: '12px'
161
+ }}
162
+ aria-hidden="true"
163
+ >
164
+ {separator}
165
+ </span>
166
+ )}
167
+ </li>
168
+ );
169
+ })}
170
+ </ol>
171
+ </nav>
172
+ );
173
+ };
174
+
175
+ // Default styles
176
+ const defaultStyles = {
177
+ nav: {
178
+ padding: '8px 0'
179
+ },
180
+ link: {
181
+ transition: 'color 0.2s ease',
182
+ cursor: 'pointer',
183
+ borderRadius: '4px',
184
+ padding: '4px',
185
+ margin: '-4px'
186
+ },
187
+ current: {
188
+ padding: '4px'
189
+ }
190
+ } as const;
191
+
192
+ /**
193
+ * Hook for managing breadcrumb state
194
+ */
195
+ export const useBreadcrumbs = () => {
196
+ const [breadcrumbs, setBreadcrumbs] = React.useState<BreadcrumbItem[]>([]);
197
+
198
+ const addBreadcrumb = React.useCallback((item: BreadcrumbItem) => {
199
+ setBreadcrumbs(prev => [...prev, item]);
200
+ }, []);
201
+
202
+ const removeBreadcrumb = React.useCallback((index: number) => {
203
+ setBreadcrumbs(prev => prev.filter((_, i) => i !== index));
204
+ }, []);
205
+
206
+ const setBreadcrumbsCurrent = React.useCallback((items: BreadcrumbItem[]) => {
207
+ setBreadcrumbs(items);
208
+ }, []);
209
+
210
+ const clearBreadcrumbs = React.useCallback(() => {
211
+ setBreadcrumbs([]);
212
+ }, []);
213
+
214
+ return {
215
+ breadcrumbs,
216
+ addBreadcrumb,
217
+ removeBreadcrumb,
218
+ setBreadcrumbs: setBreadcrumbsCurrent,
219
+ clearBreadcrumbs
220
+ };
221
+ };
222
+
223
+ export default Breadcrumbs;