mtrl 0.2.8 → 0.2.9

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 (42) hide show
  1. package/index.ts +2 -0
  2. package/package.json +1 -1
  3. package/src/components/navigation/api.ts +131 -96
  4. package/src/components/navigation/features/controller.ts +273 -0
  5. package/src/components/navigation/features/items.ts +133 -64
  6. package/src/components/navigation/navigation.ts +17 -2
  7. package/src/components/navigation/system-types.ts +124 -0
  8. package/src/components/navigation/system.ts +776 -0
  9. package/src/components/slider/config.ts +20 -2
  10. package/src/components/slider/features/controller.ts +761 -0
  11. package/src/components/slider/features/handlers.ts +18 -15
  12. package/src/components/slider/features/index.ts +3 -2
  13. package/src/components/slider/features/range.ts +104 -0
  14. package/src/components/slider/slider.ts +34 -14
  15. package/src/components/slider/structure.ts +152 -0
  16. package/src/components/textfield/api.ts +53 -0
  17. package/src/components/textfield/features.ts +322 -0
  18. package/src/components/textfield/textfield.ts +8 -0
  19. package/src/components/textfield/types.ts +12 -3
  20. package/src/components/timepicker/clockdial.ts +1 -4
  21. package/src/core/compose/features/textinput.ts +15 -2
  22. package/src/core/composition/features/dom.ts +33 -0
  23. package/src/core/composition/features/icon.ts +131 -0
  24. package/src/core/composition/features/index.ts +11 -0
  25. package/src/core/composition/features/label.ts +156 -0
  26. package/src/core/composition/features/structure.ts +22 -0
  27. package/src/core/composition/index.ts +26 -0
  28. package/src/core/index.ts +1 -1
  29. package/src/core/structure.ts +288 -0
  30. package/src/index.ts +1 -0
  31. package/src/styles/components/_navigation-mobile.scss +244 -0
  32. package/src/styles/components/_navigation-system.scss +151 -0
  33. package/src/styles/components/_textfield.scss +250 -11
  34. package/demo/build.ts +0 -349
  35. package/demo/index.html +0 -110
  36. package/demo/main.js +0 -448
  37. package/demo/styles.css +0 -239
  38. package/server.ts +0 -86
  39. package/src/components/slider/features/slider.ts +0 -318
  40. package/src/components/slider/features/structure.ts +0 -181
  41. package/src/components/slider/features/ui.ts +0 -388
  42. package/src/components/textfield/constants.ts +0 -100
@@ -0,0 +1,776 @@
1
+ // src/components/navigation/system.ts
2
+
3
+ import createNavigation from './navigation';
4
+ import {
5
+ isMobileDevice,
6
+ hasTouchSupport,
7
+ TOUCH_CONFIG,
8
+ TOUCH_TARGETS,
9
+ normalizeEvent
10
+ } from '../../core/utils/mobile';
11
+
12
+ /**
13
+ * Creates a complete navigation system with synchronized rail and drawer components
14
+ *
15
+ * @param {Object} options - System configuration options
16
+ * @returns {Object} Navigation system API
17
+ */
18
+ export const createNavigationSystem = (options = {}) => {
19
+ // Determine mobile configuration
20
+ const mobileConfig = {
21
+ breakpoint: options.breakpoint || 960,
22
+ lockBodyScroll: options.lockBodyScroll !== false,
23
+ hideOnClickOutside: options.hideOnClickOutside !== false,
24
+ enableSwipeGestures: options.enableSwipeGestures !== false,
25
+ optimizeForTouch: options.optimizeForTouch !== false,
26
+ overlayClass: options.overlayClass || 'mtrl-nav-overlay',
27
+ closeButtonClass: options.closeButtonClass || 'mtrl-nav-close-btn',
28
+ bodyLockClass: options.bodyLockClass || 'mtrl-body-drawer-open'
29
+ };
30
+
31
+ // Internal state
32
+ const state = {
33
+ rail: null,
34
+ drawer: null,
35
+ activeSection: options.activeSection || null,
36
+ activeSubsection: options.activeSubsection || null,
37
+ items: options.items || {},
38
+ mouseInDrawer: false,
39
+ mouseInRail: false,
40
+ hoverTimer: null,
41
+ closeTimer: null,
42
+ processingChange: false,
43
+ isMobile: false,
44
+ overlayElement: null,
45
+ closeButtonElement: null,
46
+ resizeObserver: null,
47
+ outsideClickHandler: null,
48
+ outsideClickHandlerSet: false
49
+ };
50
+
51
+ // Config with defaults
52
+ const config = {
53
+ // Display options
54
+ animateDrawer: true,
55
+ showLabelsOnRail: options.showLabelsOnRail !== false,
56
+ hideDrawerOnClick: options.hideDrawerOnClick || false,
57
+ expanded: options.expanded === true,
58
+
59
+ // Timing options (ms)
60
+ hoverDelay: options.hoverDelay || 200,
61
+ closeDelay: options.closeDelay || 100,
62
+
63
+ // Component options
64
+ railOptions: options.railOptions || {},
65
+ drawerOptions: options.drawerOptions || {}
66
+ };
67
+
68
+ /**
69
+ * Update drawer content for a specific section WITHOUT changing visibility
70
+ */
71
+ const updateDrawerContent = (sectionId) => {
72
+ if (!state.drawer || !sectionId || !state.items[sectionId]) {
73
+ return;
74
+ }
75
+
76
+ // Get section items
77
+ const sectionData = state.items[sectionId];
78
+ const items = sectionData.items || [];
79
+
80
+ // If no items, hide drawer and exit
81
+ if (items.length === 0) {
82
+ hideDrawer();
83
+ return;
84
+ }
85
+
86
+ // Clear existing drawer items first using the API
87
+ const currentItems = state.drawer.getAllItems();
88
+ if (currentItems?.length > 0) {
89
+ currentItems.forEach(item => {
90
+ state.drawer.removeItem(item.config.id);
91
+ });
92
+ }
93
+
94
+ // Add new items to drawer through the API
95
+ items.forEach(item => {
96
+ state.drawer.addItem(item);
97
+ });
98
+
99
+ // Show the drawer
100
+ showDrawer();
101
+ };
102
+
103
+ /**
104
+ * Create the rail navigation component
105
+ */
106
+ const createRailNavigation = () => {
107
+ // Build rail items from sections
108
+ const railItems = Object.keys(state.items || {}).map(sectionId => ({
109
+ id: sectionId,
110
+ label: state.items[sectionId]?.label || sectionId,
111
+ icon: state.items[sectionId]?.icon || '',
112
+ active: sectionId === state.activeSection
113
+ }));
114
+
115
+ // Create the rail component
116
+ const rail = createNavigation({
117
+ variant: 'rail',
118
+ position: 'left',
119
+ showLabels: config.showLabelsOnRail,
120
+ items: railItems,
121
+ ...config.railOptions
122
+ });
123
+
124
+ document.body.appendChild(rail.element);
125
+
126
+ // Register for change events - will listen for when rail items are clicked
127
+ rail.on('change', (event) => {
128
+ console.log('rail.on change', event)
129
+ // Extract ID from event data
130
+ const id = event?.id;
131
+
132
+ if (!id || state.processingChange) {
133
+ return;
134
+ }
135
+
136
+ // Check if this is a user action
137
+ const isUserAction = event?.source === 'userAction';
138
+
139
+ // Set processing flag to prevent loops
140
+ state.processingChange = true;
141
+
142
+ // Update active section
143
+ state.activeSection = id;
144
+
145
+ // Handle internally first - update drawer content
146
+ updateDrawerContent(id);
147
+
148
+ // Then notify external handlers
149
+ if (system.onSectionChange && isUserAction) {
150
+ system.onSectionChange(id, { source: isUserAction ? 'userClick' : 'programmatic' });
151
+ }
152
+
153
+ // Clear the processing flag after a delay
154
+ setTimeout(() => {
155
+ state.processingChange = false;
156
+ }, 50);
157
+ });
158
+
159
+ rail.on('mouseover', (event) => {
160
+ const id = event?.id;
161
+
162
+ // Set rail mouse state
163
+ state.mouseInRail = true;
164
+
165
+ // Clear any existing hover timer
166
+ clearTimeout(state.hoverTimer);
167
+ state.hoverTimer = null;
168
+
169
+ // Only schedule drawer operations if there's an ID
170
+ if (id) {
171
+ // Check if this section has items
172
+ if (state.items[id]?.items?.length > 0) {
173
+ // Has items - schedule drawer opening
174
+ state.hoverTimer = setTimeout(() => {
175
+ updateDrawerContent(id);
176
+ }, config.hoverDelay);
177
+ } else {
178
+ // No items - hide drawer after a delay to prevent flickering
179
+ state.closeTimer = setTimeout(() => {
180
+ // Only hide if we're still in the rail but not in the drawer
181
+ if (state.mouseInRail && !state.mouseInDrawer) {
182
+ hideDrawer();
183
+ }
184
+ }, config.hoverDelay);
185
+ }
186
+ }
187
+ });
188
+
189
+ rail.on('mouseenter', () => {
190
+ state.mouseInRail = true;
191
+
192
+ // Clear any pending drawer close timer when entering rail
193
+ clearTimeout(state.closeTimer);
194
+ state.closeTimer = null;
195
+ });
196
+
197
+ rail.on('mouseleave', () => {
198
+ state.mouseInRail = false;
199
+
200
+ // Clear any existing hover timer
201
+ clearTimeout(state.hoverTimer);
202
+ state.hoverTimer = null;
203
+
204
+ // Only set timer to hide drawer if we're not in drawer either
205
+ if (!state.mouseInDrawer) {
206
+ state.closeTimer = setTimeout(() => {
207
+ // Double-check we're still not in rail or drawer before hiding
208
+ if (!state.mouseInRail && !state.mouseInDrawer) {
209
+ hideDrawer();
210
+ }
211
+ }, config.closeDelay);
212
+ }
213
+ });
214
+
215
+ return rail;
216
+ };
217
+
218
+ /**
219
+ * Create the drawer navigation component
220
+ */
221
+ const createDrawerNavigation = () => {
222
+ // Create the drawer component (initially empty)
223
+ const drawer = createNavigation({
224
+ variant: 'drawer',
225
+ position: 'left',
226
+ items: [], // Start empty
227
+ ...config.drawerOptions
228
+ });
229
+
230
+ document.body.appendChild(drawer.element);
231
+
232
+ // Mark drawer with identifier
233
+ drawer.element.dataset.id = 'drawer';
234
+
235
+ // IMPORTANT: Make drawer initially hidden unless explicitly expanded
236
+ if (!config.expanded) {
237
+ drawer.element.classList.add('mtrl-nav--hidden');
238
+ drawer.element.setAttribute('aria-hidden', 'true');
239
+ }
240
+
241
+ // Use the component's native event system
242
+ if (typeof drawer.on === 'function') {
243
+ // Handle item selection
244
+ drawer.on('change', (event) => {
245
+ const id = event.id;
246
+
247
+ state.activeSubsection = id;
248
+
249
+ // If configuration specifies to hide drawer on click, do so
250
+ if (config.hideDrawerOnClick) {
251
+ hideDrawer();
252
+ }
253
+
254
+ // Emit item selection event
255
+ if (system.onItemSelect) {
256
+ system.onItemSelect(event);
257
+ }
258
+ });
259
+
260
+ // Handle mouseenter/mouseleave for drawer
261
+ drawer.on('mouseenter', () => {
262
+ state.mouseInDrawer = true;
263
+
264
+ // Clear any hover and close timers
265
+ clearTimeout(state.hoverTimer);
266
+ clearTimeout(state.closeTimer);
267
+ state.hoverTimer = null;
268
+ state.closeTimer = null;
269
+ });
270
+
271
+ drawer.on('mouseleave', () => {
272
+ state.mouseInDrawer = false;
273
+
274
+ // Only set timer to hide drawer if we're not in rail
275
+ if (!state.mouseInRail) {
276
+ state.closeTimer = setTimeout(() => {
277
+ // Double-check we're still not in drawer or rail before hiding
278
+ if (!state.mouseInDrawer && !state.mouseInRail) {
279
+ hideDrawer();
280
+ }
281
+ }, config.closeDelay);
282
+ }
283
+ });
284
+ }
285
+
286
+ return drawer;
287
+ };
288
+
289
+ /**
290
+ * Checks and updates the mobile state
291
+ */
292
+ const checkMobileState = () => {
293
+ const prevState = state.isMobile;
294
+ state.isMobile = window.innerWidth <= mobileConfig.breakpoint || isMobileDevice();
295
+
296
+ // If state changed, adjust UI
297
+ if (prevState !== state.isMobile) {
298
+ if (state.isMobile) {
299
+ // Switched to mobile mode
300
+ setupMobileMode();
301
+ } else {
302
+ // Switched to desktop mode
303
+ teardownMobileMode();
304
+ }
305
+
306
+ // Emit a view change event
307
+ if (system.onViewChange) {
308
+ system.onViewChange({
309
+ mobile: state.isMobile,
310
+ previousMobile: prevState,
311
+ width: window.innerWidth
312
+ });
313
+ }
314
+ }
315
+ };
316
+
317
+ /**
318
+ * Creates and appends overlay element for mobile
319
+ */
320
+ const createOverlay = () => {
321
+ if (state.overlayElement) return state.overlayElement;
322
+
323
+ state.overlayElement = document.createElement('div');
324
+ state.overlayElement.className = mobileConfig.overlayClass;
325
+ state.overlayElement.setAttribute('aria-hidden', 'true');
326
+ document.body.appendChild(state.overlayElement);
327
+
328
+ state.overlayElement.addEventListener('click', (event) => {
329
+ if (event.target === state.overlayElement) {
330
+ hideDrawer();
331
+ }
332
+ });
333
+
334
+ return state.overlayElement;
335
+ };
336
+
337
+ /**
338
+ * Creates and adds close button to the drawer
339
+ */
340
+ const createCloseButton = () => {
341
+ if (!state.drawer || state.closeButtonElement) return null;
342
+
343
+ state.closeButtonElement = document.createElement('button');
344
+ state.closeButtonElement.className = mobileConfig.closeButtonClass;
345
+ state.closeButtonElement.setAttribute('aria-label', 'Close navigation');
346
+ state.closeButtonElement.innerHTML = `
347
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
348
+ <line x1="18" y1="6" x2="6" y2="18"></line>
349
+ <line x1="6" y1="6" x2="18" y2="18"></line>
350
+ </svg>
351
+ `;
352
+
353
+ // Handle click event
354
+ state.closeButtonElement.addEventListener('click', () => {
355
+ hideDrawer();
356
+ });
357
+
358
+ // Apply touch-friendly styles if needed
359
+ if (hasTouchSupport() && mobileConfig.optimizeForTouch) {
360
+ state.closeButtonElement.style.minWidth = `${TOUCH_TARGETS.COMFORTABLE}px`;
361
+ state.closeButtonElement.style.minHeight = `${TOUCH_TARGETS.COMFORTABLE}px`;
362
+
363
+ // Add touch feedback
364
+ state.closeButtonElement.addEventListener('touchstart', () => {
365
+ state.closeButtonElement.classList.add('active');
366
+ }, { passive: true });
367
+
368
+ state.closeButtonElement.addEventListener('touchend', () => {
369
+ setTimeout(() => {
370
+ state.closeButtonElement.classList.remove('active');
371
+ }, TOUCH_CONFIG.FEEDBACK_DURATION);
372
+ }, { passive: true });
373
+ }
374
+
375
+ state.drawer.element.appendChild(state.closeButtonElement);
376
+ return state.closeButtonElement;
377
+ };
378
+
379
+ /**
380
+ * Sets up mobile mode features
381
+ */
382
+ const setupMobileMode = () => {
383
+ const drawer = state.drawer;
384
+ const rail = state.rail;
385
+
386
+ if (!drawer || !rail) return;
387
+
388
+ // Create mobile UI elements
389
+ createOverlay();
390
+ createCloseButton();
391
+
392
+ // Setup outside click handling
393
+ setupOutsideClickHandling();
394
+
395
+ // Setup touch gestures if enabled
396
+ if (mobileConfig.enableSwipeGestures && hasTouchSupport()) {
397
+ setupTouchGestures();
398
+ }
399
+
400
+ // Hide drawer initially in mobile mode
401
+ hideDrawer();
402
+ };
403
+
404
+ /**
405
+ * Tears down mobile-specific features
406
+ */
407
+ const teardownMobileMode = () => {
408
+ // Hide overlay
409
+ if (state.overlayElement) {
410
+ state.overlayElement.classList.remove('active');
411
+ state.overlayElement.setAttribute('aria-hidden', 'true');
412
+ }
413
+
414
+ // Hide close button
415
+ if (state.closeButtonElement) {
416
+ state.closeButtonElement.style.display = 'none';
417
+ }
418
+
419
+ // Remove body scroll lock if applied
420
+ document.body.classList.remove(mobileConfig.bodyLockClass);
421
+ };
422
+
423
+ /**
424
+ * Sets up outside click handling for mobile
425
+ */
426
+ const setupOutsideClickHandling = () => {
427
+ if (!mobileConfig.hideOnClickOutside) return;
428
+
429
+ // Only set up once
430
+ if (state.outsideClickHandlerSet) return;
431
+ state.outsideClickHandlerSet = true;
432
+
433
+ // Use either click or touchend event depending on device capability
434
+ const eventType = hasTouchSupport() ? 'touchend' : 'click';
435
+
436
+ // The handler function
437
+ const handleOutsideClick = (event) => {
438
+ if (!state.isMobile || !isDrawerVisible()) return;
439
+
440
+ const normalizedEvent = normalizeEvent(event);
441
+ const target = normalizedEvent.target as HTMLElement;
442
+
443
+ // Don't close if clicking on drawer, rail, or excluded elements
444
+ if (state.drawer.element.contains(target) ||
445
+ state.rail.element.contains(target)) {
446
+ return;
447
+ }
448
+
449
+ // Close drawer - it's an outside click/touch
450
+ hideDrawer();
451
+ };
452
+
453
+ // Store handler for cleanup
454
+ state.outsideClickHandler = handleOutsideClick;
455
+
456
+ // Add listener
457
+ document.addEventListener(eventType, handleOutsideClick,
458
+ hasTouchSupport() ? { passive: true } : false);
459
+ };
460
+
461
+ /**
462
+ * Sets up touch gestures for mobile
463
+ */
464
+ const setupTouchGestures = () => {
465
+ const drawer = state.drawer;
466
+ const rail = state.rail;
467
+
468
+ if (!drawer || !rail) return;
469
+
470
+ let touchStartX = 0;
471
+ let touchStartY = 0;
472
+
473
+ // Rail swipe right to open drawer
474
+ rail.element.addEventListener('touchstart', (event) => {
475
+ const touch = event.touches[0];
476
+ touchStartX = touch.clientX;
477
+ touchStartY = touch.clientY;
478
+ }, { passive: true });
479
+
480
+ rail.element.addEventListener('touchmove', (event) => {
481
+ if (!state.isMobile || isDrawerVisible()) return;
482
+
483
+ const touch = event.touches[0];
484
+ const deltaX = touch.clientX - touchStartX;
485
+ const deltaY = touch.clientY - touchStartY;
486
+
487
+ // Only consider horizontal swipes
488
+ if (Math.abs(deltaX) > Math.abs(deltaY) &&
489
+ deltaX > TOUCH_CONFIG.SWIPE_THRESHOLD) {
490
+ showDrawer();
491
+ }
492
+ }, { passive: true });
493
+
494
+ // Drawer swipe left to close
495
+ drawer.element.addEventListener('touchstart', (event) => {
496
+ const touch = event.touches[0];
497
+ touchStartX = touch.clientX;
498
+ touchStartY = touch.clientY;
499
+ }, { passive: true });
500
+
501
+ // Use touchmove with transform for visual feedback
502
+ drawer.element.addEventListener('touchmove', (event) => {
503
+ if (!state.isMobile || !isDrawerVisible()) return;
504
+
505
+ const touch = event.touches[0];
506
+ const deltaX = touch.clientX - touchStartX;
507
+
508
+ // Only apply transform for leftward swipes
509
+ if (deltaX < 0) {
510
+ // Apply transform with resistance
511
+ drawer.element.style.transform = `translateX(${deltaX / 2}px)`;
512
+
513
+ // Close if threshold reached
514
+ if (deltaX < -TOUCH_CONFIG.SWIPE_THRESHOLD) {
515
+ hideDrawer();
516
+ }
517
+ }
518
+ }, { passive: true });
519
+
520
+ // Reset transforms when touch ends
521
+ drawer.element.addEventListener('touchend', () => {
522
+ if (drawer.element.style.transform) {
523
+ drawer.element.style.transition = 'transform 0.2s ease';
524
+ drawer.element.style.transform = '';
525
+
526
+ setTimeout(() => {
527
+ drawer.element.style.transition = '';
528
+ }, 200);
529
+ }
530
+ }, { passive: true });
531
+ };
532
+
533
+ /**
534
+ * Show the drawer with mobile-specific behaviors
535
+ */
536
+ const showDrawer = () => {
537
+ if (!state.drawer) return;
538
+
539
+ state.drawer.element.classList.remove('mtrl-nav--hidden');
540
+ state.drawer.element.setAttribute('aria-hidden', 'false');
541
+
542
+ // Apply mobile-specific behaviors
543
+ if (state.isMobile) {
544
+ if (state.overlayElement) {
545
+ state.overlayElement.classList.add('active');
546
+ state.overlayElement.setAttribute('aria-hidden', 'false');
547
+ }
548
+
549
+ // Lock body scroll if enabled
550
+ if (mobileConfig.lockBodyScroll) {
551
+ document.body.classList.add(mobileConfig.bodyLockClass);
552
+ }
553
+
554
+ // Ensure close button is visible
555
+ if (state.closeButtonElement) {
556
+ state.closeButtonElement.style.display = 'flex';
557
+ }
558
+ }
559
+ };
560
+
561
+ /**
562
+ * Hide the drawer with mobile-specific behaviors
563
+ */
564
+ const hideDrawer = () => {
565
+ if (!state.drawer) return;
566
+
567
+ state.drawer.element.classList.add('mtrl-nav--hidden');
568
+ state.drawer.element.setAttribute('aria-hidden', 'true');
569
+
570
+ // Remove mobile-specific effects
571
+ if (state.overlayElement) {
572
+ state.overlayElement.classList.remove('active');
573
+ state.overlayElement.setAttribute('aria-hidden', 'true');
574
+ }
575
+
576
+ // Unlock body scroll
577
+ if (mobileConfig.lockBodyScroll) {
578
+ document.body.classList.remove(mobileConfig.bodyLockClass);
579
+ }
580
+ };
581
+
582
+ /**
583
+ * Check if drawer is visible
584
+ */
585
+ const isDrawerVisible = () => {
586
+ if (!state.drawer) return false;
587
+ return !state.drawer.element.classList.contains('mtrl-nav--hidden');
588
+ };
589
+
590
+ /**
591
+ * Initialize the navigation system
592
+ */
593
+ const initialize = () => {
594
+ // Create rail component
595
+ state.rail = createRailNavigation();
596
+
597
+ // Create drawer component
598
+ state.drawer = createDrawerNavigation();
599
+
600
+ // Setup responsive behavior
601
+ if (window.ResizeObserver) {
602
+ // Use ResizeObserver for better performance
603
+ state.resizeObserver = new ResizeObserver(() => {
604
+ checkMobileState();
605
+ });
606
+ state.resizeObserver.observe(document.body);
607
+ } else {
608
+ // Fallback to window resize event
609
+ window.addEventListener('resize', checkMobileState);
610
+ }
611
+
612
+ // Listen for orientation changes on mobile
613
+ window.addEventListener('orientationchange', () => {
614
+ // Small delay to ensure dimensions have updated
615
+ setTimeout(checkMobileState, 100);
616
+ });
617
+
618
+ // Set active section if specified
619
+ if (options.activeSection && state.items[options.activeSection]) {
620
+ state.activeSection = options.activeSection;
621
+
622
+ if (state.rail) {
623
+ state.rail.setActive(options.activeSection);
624
+ }
625
+
626
+ // Update drawer content without showing it
627
+ updateDrawerContent(options.activeSection);
628
+
629
+ // Only show drawer if expanded is explicitly true
630
+ if (options.expanded === true) {
631
+ showDrawer();
632
+ } else {
633
+ // Explicitly ensure drawer is hidden
634
+ hideDrawer();
635
+ }
636
+ }
637
+
638
+ // Check initial mobile state
639
+ checkMobileState();
640
+
641
+ return system;
642
+ };
643
+
644
+ /**
645
+ * Clean up resources
646
+ */
647
+ const cleanup = () => {
648
+ // Clean up resize observer
649
+ if (state.resizeObserver) {
650
+ state.resizeObserver.disconnect();
651
+ state.resizeObserver = null;
652
+ } else {
653
+ window.removeEventListener('resize', checkMobileState);
654
+ }
655
+
656
+ // Remove orientation change listener
657
+ window.removeEventListener('orientationchange', checkMobileState);
658
+
659
+ // Remove outside click handler
660
+ if (state.outsideClickHandler) {
661
+ const eventType = hasTouchSupport() ? 'touchend' : 'click';
662
+ document.removeEventListener(eventType, state.outsideClickHandler);
663
+ }
664
+
665
+ // Clean up overlay
666
+ if (state.overlayElement && state.overlayElement.parentNode) {
667
+ state.overlayElement.parentNode.removeChild(state.overlayElement);
668
+ state.overlayElement = null;
669
+ }
670
+
671
+ // Clear timers
672
+ clearTimeout(state.hoverTimer);
673
+ clearTimeout(state.closeTimer);
674
+ state.hoverTimer = null;
675
+ state.closeTimer = null;
676
+
677
+ // Destroy components
678
+ if (state.rail) {
679
+ state.rail.destroy();
680
+ state.rail = null;
681
+ }
682
+
683
+ if (state.drawer) {
684
+ state.drawer.destroy();
685
+ state.drawer = null;
686
+ }
687
+
688
+ // Reset state
689
+ state.activeSection = null;
690
+ state.activeSubsection = null;
691
+ state.mouseInDrawer = false;
692
+ state.mouseInRail = false;
693
+ state.processingChange = false;
694
+ state.isMobile = false;
695
+ };
696
+
697
+ /**
698
+ * Navigate to a specific section and subsection
699
+ */
700
+ const navigateTo = (section, subsection, silent) => {
701
+ console.error('navigateTo', section, subsection, silent)
702
+ // Skip if section doesn't exist
703
+ if (!section || !state.items[section]) {
704
+ return;
705
+ }
706
+
707
+ // Check if we're already on this section and subsection
708
+ if (state.activeSection === section && state.activeSubsection === subsection) {
709
+ return;
710
+ }
711
+
712
+ // Update active section
713
+ state.activeSection = section;
714
+
715
+ // Update rail if it exists
716
+ if (state.rail) {
717
+ state.rail.setActive(section, silent);
718
+ }
719
+
720
+ // Update drawer content
721
+ // updateDrawerContent(section);
722
+
723
+ // Update active subsection if specified
724
+ if (subsection && state.drawer) {
725
+ state.activeSubsection = subsection;
726
+ state.drawer.setActive(subsection, silent);
727
+ }
728
+ };
729
+
730
+ // Create the public API
731
+ const system = {
732
+ initialize,
733
+ cleanup,
734
+ navigateTo,
735
+
736
+ // Component access
737
+ getRail: () => state.rail,
738
+ getDrawer: () => state.drawer,
739
+
740
+ // State getters
741
+ getActiveSection: () => state.activeSection,
742
+ getActiveSubsection: () => state.activeSubsection,
743
+
744
+ // Drawer control
745
+ showDrawer,
746
+ hideDrawer,
747
+ isDrawerVisible,
748
+
749
+ // Configure
750
+ configure: (newConfig) => {
751
+ Object.assign(options, newConfig);
752
+ return system;
753
+ },
754
+
755
+ // State processing management
756
+ setProcessingChange: (isProcessing) => {
757
+ state.processingChange = isProcessing;
758
+ },
759
+
760
+ isProcessingChange: () => state.processingChange,
761
+
762
+ // Mobile-specific methods
763
+ isMobile: () => state.isMobile,
764
+ checkMobileState,
765
+
766
+ // Event handlers (to be set by user)
767
+ onSectionChange: null,
768
+ onItemSelect: null,
769
+ onViewChange: null
770
+ };
771
+
772
+ // Return the uninitialized system
773
+ return system;
774
+ };
775
+
776
+ export default createNavigationSystem;