mtrl 0.2.9 → 0.3.1

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 (99) hide show
  1. package/CLAUDE.md +33 -0
  2. package/package.json +3 -1
  3. package/src/components/button/button.ts +34 -5
  4. package/src/components/navigation/index.ts +4 -1
  5. package/src/components/navigation/system/core.ts +302 -0
  6. package/src/components/navigation/system/events.ts +240 -0
  7. package/src/components/navigation/system/index.ts +184 -0
  8. package/src/components/navigation/system/mobile.ts +278 -0
  9. package/src/components/navigation/system/state.ts +77 -0
  10. package/src/components/navigation/system/types.ts +364 -0
  11. package/src/components/navigation/types.ts +33 -0
  12. package/src/components/slider/config.ts +2 -2
  13. package/src/components/slider/features/controller.ts +1 -25
  14. package/src/components/slider/features/handlers.ts +0 -1
  15. package/src/components/slider/features/range.ts +7 -7
  16. package/src/components/slider/{structure.ts → schema.ts} +2 -13
  17. package/src/components/slider/slider.ts +3 -2
  18. package/src/components/snackbar/index.ts +7 -1
  19. package/src/components/snackbar/types.ts +25 -0
  20. package/src/components/switch/api.ts +16 -0
  21. package/src/components/switch/config.ts +1 -18
  22. package/src/components/switch/features.ts +198 -0
  23. package/src/components/switch/index.ts +6 -1
  24. package/src/components/switch/switch.ts +3 -3
  25. package/src/components/switch/types.ts +27 -2
  26. package/src/components/textfield/index.ts +7 -1
  27. package/src/components/textfield/types.ts +36 -0
  28. package/src/core/composition/features/dom.ts +26 -14
  29. package/src/core/composition/features/icon.ts +18 -18
  30. package/src/core/composition/features/index.ts +3 -2
  31. package/src/core/composition/features/label.ts +16 -17
  32. package/src/core/composition/features/layout.ts +47 -0
  33. package/src/core/composition/index.ts +4 -4
  34. package/src/core/layout/README.md +350 -0
  35. package/src/core/layout/array.ts +181 -0
  36. package/src/core/layout/create.ts +55 -0
  37. package/src/core/layout/index.ts +26 -0
  38. package/src/core/layout/object.ts +124 -0
  39. package/src/core/layout/processor.ts +58 -0
  40. package/src/core/layout/result.ts +85 -0
  41. package/src/core/layout/types.ts +125 -0
  42. package/src/core/layout/utils.ts +136 -0
  43. package/src/styles/abstract/_variables.scss +28 -0
  44. package/src/styles/components/_switch.scss +133 -69
  45. package/src/styles/components/_textfield.scss +9 -16
  46. package/test/components/badge.test.ts +545 -0
  47. package/test/components/bottom-app-bar.test.ts +303 -0
  48. package/test/components/button.test.ts +233 -0
  49. package/test/components/card.test.ts +560 -0
  50. package/test/components/carousel.test.ts +951 -0
  51. package/test/components/checkbox.test.ts +462 -0
  52. package/test/components/chip.test.ts +692 -0
  53. package/test/components/datepicker.test.ts +1124 -0
  54. package/test/components/dialog.test.ts +990 -0
  55. package/test/components/divider.test.ts +412 -0
  56. package/test/components/extended-fab.test.ts +672 -0
  57. package/test/components/fab.test.ts +561 -0
  58. package/test/components/list.test.ts +365 -0
  59. package/test/components/menu.test.ts +718 -0
  60. package/test/components/navigation.test.ts +186 -0
  61. package/test/components/progress.test.ts +567 -0
  62. package/test/components/radios.test.ts +699 -0
  63. package/test/components/search.test.ts +1135 -0
  64. package/test/components/segmented-button.test.ts +732 -0
  65. package/test/components/sheet.test.ts +641 -0
  66. package/test/components/slider.test.ts +1220 -0
  67. package/test/components/snackbar.test.ts +461 -0
  68. package/test/components/switch.test.ts +452 -0
  69. package/test/components/tabs.test.ts +1369 -0
  70. package/test/components/textfield.test.ts +400 -0
  71. package/test/components/timepicker.test.ts +592 -0
  72. package/test/components/tooltip.test.ts +630 -0
  73. package/test/components/top-app-bar.test.ts +566 -0
  74. package/test/core/dom.attributes.test.ts +148 -0
  75. package/test/core/dom.classes.test.ts +152 -0
  76. package/test/core/dom.events.test.ts +243 -0
  77. package/test/core/emitter.test.ts +141 -0
  78. package/test/core/ripple.test.ts +99 -0
  79. package/test/core/state.store.test.ts +189 -0
  80. package/test/core/utils.normalize.test.ts +61 -0
  81. package/test/core/utils.object.test.ts +120 -0
  82. package/test/setup.ts +451 -0
  83. package/tsconfig.json +2 -2
  84. package/src/components/navigation/system-types.ts +0 -124
  85. package/src/components/navigation/system.ts +0 -776
  86. package/src/components/snackbar/constants.ts +0 -26
  87. package/src/core/composition/features/structure.ts +0 -22
  88. package/src/core/layout/index.js +0 -95
  89. package/src/core/structure.ts +0 -288
  90. package/test/components/button.test.js +0 -170
  91. package/test/components/checkbox.test.js +0 -238
  92. package/test/components/list.test.js +0 -105
  93. package/test/components/menu.test.js +0 -385
  94. package/test/components/navigation.test.js +0 -227
  95. package/test/components/snackbar.test.js +0 -234
  96. package/test/components/switch.test.js +0 -186
  97. package/test/components/textfield.test.js +0 -314
  98. package/test/core/emitter.test.js +0 -141
  99. package/test/core/ripple.test.js +0 -66
package/CLAUDE.md ADDED
@@ -0,0 +1,33 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Build & Development Commands
6
+ - Build: `bun run build`
7
+ - Dev server: `bun run dev`
8
+ - Tests: `bun test`
9
+ - Single test: `bun test test/components/button.test.ts`
10
+ - Watch tests: `bun test --watch`
11
+ - Test with UI: `bun test --watch --ui`
12
+ - Test coverage: `bun test --coverage`
13
+ - Docs: `bun run docs` (uses TypeDoc)
14
+
15
+ ## Code Conventions
16
+ - Factory pattern: Use `createComponent` naming for component creators
17
+ - Functional composition with pipe pattern
18
+ - Follow BEM-style CSS naming: `mtrl-component__element--modifier`
19
+ - Use TypeDoc-compatible comments for documentation
20
+ - Use TypeScript with strict typing where possible
21
+ - Error handling: Try/catch blocks for component creation
22
+ - Import order: core modules first, then utility functions, local imports last
23
+ - Components follow a standard structure in their directories (api.ts, config.ts, etc.)
24
+ - Use ES6+ features with full browser compatibility
25
+
26
+ ## Test Conventions
27
+ - Tests live in `test/components/` with `.test.ts` extension
28
+ - Use JSDOM for DOM manipulation in tests
29
+ - Create mock implementations to avoid circular dependencies
30
+ - Test structure follows `describe/test` pattern
31
+ - Test component creation, options, events, and state changes
32
+ - Use `import type` to avoid circular dependencies in TypeScript tests
33
+ - Test directory structure mirrors src directory structure
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtrl",
3
- "version": "0.2.9",
3
+ "version": "0.3.1",
4
4
  "description": "A functional TypeScript/JavaScript component library with composable architecture based on Material Design 3",
5
5
  "author": "floor",
6
6
  "license": "MIT License",
@@ -42,6 +42,8 @@
42
42
  "url": "https://github.com/floor/mtrl.git"
43
43
  },
44
44
  "devDependencies": {
45
+ "@types/jsdom": "^21.1.7",
46
+ "jsdom": "^26.0.0",
45
47
  "sass": "^1.85.1",
46
48
  "typedoc": "^0.27.9"
47
49
  }
@@ -16,13 +16,43 @@ import { ButtonConfig } from './types';
16
16
  import { createBaseConfig, getElementConfig, getApiConfig } from './config';
17
17
 
18
18
  /**
19
- * Creates a new Button component
20
- * @param {ButtonConfig} config - Button configuration object
21
- * @returns {ButtonComponent} Button component instance
19
+ * Creates a new Button component with the specified configuration.
20
+ *
21
+ * The Button component is created using a functional composition pattern,
22
+ * applying various features through the pipe function. This approach allows
23
+ * for flexible and modular component construction.
24
+ *
25
+ * @param {ButtonConfig} config - Configuration options for the button
26
+ * This can include text content, icon options, variant styling, disabled state,
27
+ * and other button properties. See {@link ButtonConfig} for available options.
28
+ *
29
+ * @returns {ButtonComponent} A fully configured button component instance with
30
+ * all requested features applied. The returned component has methods for
31
+ * manipulation, event handling, and lifecycle management.
32
+ *
33
+ * @throws {Error} Throws an error if button creation fails for any reason
34
+ *
35
+ * @example
36
+ * // Create a simple text button
37
+ * const textButton = createButton({ text: 'Click me' });
38
+ *
39
+ * @example
40
+ * // Create a primary button with an icon
41
+ * const primaryButton = createButton({
42
+ * text: 'Submit',
43
+ * variant: 'primary',
44
+ * icon: 'send'
45
+ * });
46
+ *
47
+ * @example
48
+ * // Create a disabled button
49
+ * const disabledButton = createButton({
50
+ * text: 'Not available',
51
+ * disabled: true
52
+ * });
22
53
  */
23
54
  const createButton = (config: ButtonConfig = {}) => {
24
55
  const baseConfig = createBaseConfig(config);
25
-
26
56
  try {
27
57
  const button = pipe(
28
58
  createBase,
@@ -36,7 +66,6 @@ const createButton = (config: ButtonConfig = {}) => {
36
66
  withLifecycle(),
37
67
  comp => withAPI(getApiConfig(comp))(comp)
38
68
  )(baseConfig);
39
-
40
69
  return button;
41
70
  } catch (error) {
42
71
  console.error('Button creation error:', error);
@@ -4,5 +4,8 @@ export {
4
4
  NavigationConfig,
5
5
  NavigationComponent,
6
6
  NavItemConfig,
7
- NavGroupConfig
7
+ NavGroupConfig,
8
+ NAV_VARIANTS,
9
+ NAV_POSITIONS,
10
+ NAV_BEHAVIORS
8
11
  } from './types'
@@ -0,0 +1,302 @@
1
+ // src/components/navigation/system/core.ts
2
+
3
+ import { NavigationSystemState, NavigationSystemConfig, NavigationItem } from './types';
4
+ import { isMobileDevice } from '../../../core/utils/mobile';
5
+ import createNavigation from '../navigation';
6
+
7
+ /**
8
+ * Update drawer content for a specific section WITHOUT changing visibility
9
+ *
10
+ * @param state - System state
11
+ * @param sectionId - Section ID to display
12
+ * @param showDrawer - Function to show the drawer
13
+ * @param hideDrawer - Function to hide the drawer
14
+ */
15
+ export const updateDrawerContent = (
16
+ state: NavigationSystemState,
17
+ sectionId: string,
18
+ showDrawer: () => void,
19
+ hideDrawer: () => void
20
+ ): void => {
21
+ if (!state.drawer || !sectionId || !state.items[sectionId]) {
22
+ return;
23
+ }
24
+
25
+ // Get section items
26
+ const sectionData = state.items[sectionId];
27
+ const items = sectionData.items || [];
28
+
29
+ // If no items, hide drawer and exit
30
+ if (items.length === 0) {
31
+ hideDrawer();
32
+ return;
33
+ }
34
+
35
+ // Clear existing drawer items first using the API
36
+ const currentItems = state.drawer.getAllItems();
37
+ if (currentItems?.length > 0) {
38
+ currentItems.forEach((item: any) => {
39
+ state.drawer.removeItem(item.config.id);
40
+ });
41
+ }
42
+
43
+ // Add new items to drawer through the API
44
+ items.forEach((item: NavigationItem) => {
45
+ state.drawer.addItem(item);
46
+ });
47
+
48
+ // Show the drawer
49
+ showDrawer();
50
+ };
51
+
52
+ /**
53
+ * Creates the rail navigation component
54
+ *
55
+ * @param state - System state
56
+ * @param config - System configuration
57
+ * @returns Rail navigation component
58
+ */
59
+ export const createRailNavigation = (
60
+ state: NavigationSystemState,
61
+ config: any
62
+ ): any => {
63
+ // Build rail items from sections
64
+ const railItems = Object.keys(state.items || {}).map(sectionId => ({
65
+ id: sectionId,
66
+ label: state.items[sectionId]?.label || sectionId,
67
+ icon: state.items[sectionId]?.icon || '',
68
+ active: sectionId === state.activeSection
69
+ }));
70
+
71
+ // Create the rail component
72
+ const rail = createNavigation({
73
+ variant: 'rail',
74
+ position: 'left',
75
+ showLabels: config.showLabelsOnRail,
76
+ items: railItems,
77
+ ...config.railOptions
78
+ });
79
+
80
+ document.body.appendChild(rail.element);
81
+ return rail;
82
+ };
83
+
84
+ /**
85
+ * Creates the drawer navigation component
86
+ *
87
+ * @param state - System state
88
+ * @param config - System configuration
89
+ * @returns Drawer navigation component
90
+ */
91
+ export const createDrawerNavigation = (
92
+ state: NavigationSystemState,
93
+ config: any
94
+ ): any => {
95
+ // Create the drawer component (initially empty)
96
+ const drawer = createNavigation({
97
+ variant: 'drawer',
98
+ position: 'left',
99
+ items: [], // Start empty
100
+ ...config.drawerOptions
101
+ });
102
+
103
+ document.body.appendChild(drawer.element);
104
+
105
+ // Mark drawer with identifier
106
+ drawer.element.dataset.id = 'drawer';
107
+
108
+ // IMPORTANT: Make drawer initially hidden unless explicitly expanded
109
+ if (!config.expanded) {
110
+ drawer.element.classList.add('mtrl-nav--hidden');
111
+ drawer.element.setAttribute('aria-hidden', 'true');
112
+ }
113
+
114
+ return drawer;
115
+ };
116
+
117
+ /**
118
+ * Shows the drawer with mobile-specific behaviors
119
+ *
120
+ * @param state - System state
121
+ * @param mobileConfig - Mobile configuration
122
+ */
123
+ export const showDrawer = (
124
+ state: NavigationSystemState,
125
+ mobileConfig: any
126
+ ): void => {
127
+ if (!state.drawer) return;
128
+
129
+ state.drawer.element.classList.remove('mtrl-nav--hidden');
130
+ state.drawer.element.setAttribute('aria-hidden', 'false');
131
+
132
+ // Apply mobile-specific behaviors
133
+ if (state.isMobile) {
134
+ if (state.overlayElement) {
135
+ state.overlayElement.classList.add('active');
136
+ state.overlayElement.setAttribute('aria-hidden', 'false');
137
+ }
138
+
139
+ // Lock body scroll if enabled
140
+ if (mobileConfig.lockBodyScroll) {
141
+ document.body.classList.add(mobileConfig.bodyLockClass);
142
+ }
143
+
144
+ // Ensure close button is visible
145
+ if (state.closeButtonElement) {
146
+ state.closeButtonElement.style.display = 'flex';
147
+ }
148
+ }
149
+ };
150
+
151
+ /**
152
+ * Hides the drawer with mobile-specific behaviors
153
+ *
154
+ * @param state - System state
155
+ * @param mobileConfig - Mobile configuration
156
+ */
157
+ export const hideDrawer = (
158
+ state: NavigationSystemState,
159
+ mobileConfig: any
160
+ ): void => {
161
+ if (!state.drawer) return;
162
+
163
+ state.drawer.element.classList.add('mtrl-nav--hidden');
164
+ state.drawer.element.setAttribute('aria-hidden', 'true');
165
+
166
+ // Remove mobile-specific effects
167
+ if (state.overlayElement) {
168
+ state.overlayElement.classList.remove('active');
169
+ state.overlayElement.setAttribute('aria-hidden', 'true');
170
+ }
171
+
172
+ // Unlock body scroll
173
+ if (mobileConfig.lockBodyScroll) {
174
+ document.body.classList.remove(mobileConfig.bodyLockClass);
175
+ }
176
+ };
177
+
178
+ /**
179
+ * Checks if drawer is visible
180
+ *
181
+ * @param state - System state
182
+ * @returns true if drawer is visible
183
+ */
184
+ export const isDrawerVisible = (
185
+ state: NavigationSystemState
186
+ ): boolean => {
187
+ if (!state.drawer) return false;
188
+ return !state.drawer.element.classList.contains('mtrl-nav--hidden');
189
+ };
190
+
191
+ /**
192
+ * Checks and updates the mobile state
193
+ *
194
+ * @param state - System state
195
+ * @param mobileConfig - Mobile configuration
196
+ * @param setupMobileMode - Function to set up mobile mode
197
+ * @param teardownMobileMode - Function to tear down mobile mode
198
+ * @param systemApi - Reference to the public system API
199
+ */
200
+ export const checkMobileState = (
201
+ state: NavigationSystemState,
202
+ mobileConfig: any,
203
+ setupMobileMode: () => void,
204
+ teardownMobileMode: () => void,
205
+ systemApi: any
206
+ ): void => {
207
+ const prevState = state.isMobile;
208
+ state.isMobile = window.innerWidth <= mobileConfig.breakpoint || isMobileDevice();
209
+
210
+ // If state changed, adjust UI
211
+ if (prevState !== state.isMobile) {
212
+ if (state.isMobile) {
213
+ // Switched to mobile mode
214
+ setupMobileMode();
215
+ } else {
216
+ // Switched to desktop mode
217
+ teardownMobileMode();
218
+ }
219
+
220
+ // Emit a view change event
221
+ if (systemApi.onViewChange) {
222
+ systemApi.onViewChange({
223
+ mobile: state.isMobile,
224
+ previousMobile: prevState,
225
+ width: window.innerWidth
226
+ });
227
+ }
228
+ }
229
+ };
230
+
231
+ /**
232
+ * Clean up resources when the system is destroyed
233
+ *
234
+ * @param state - System state
235
+ */
236
+ export const cleanupResources = (
237
+ state: NavigationSystemState
238
+ ): void => {
239
+ // Clean up overlay
240
+ if (state.overlayElement && state.overlayElement.parentNode) {
241
+ state.overlayElement.parentNode.removeChild(state.overlayElement);
242
+ state.overlayElement = null;
243
+ }
244
+
245
+ // Destroy components
246
+ if (state.rail) {
247
+ state.rail.destroy();
248
+ state.rail = null;
249
+ }
250
+
251
+ if (state.drawer) {
252
+ state.drawer.destroy();
253
+ state.drawer = null;
254
+ }
255
+
256
+ // Reset state
257
+ state.activeSection = null;
258
+ state.activeSubsection = null;
259
+ state.mouseInDrawer = false;
260
+ state.mouseInRail = false;
261
+ state.processingChange = false;
262
+ state.isMobile = false;
263
+ };
264
+
265
+ /**
266
+ * Navigate to a specific section and subsection
267
+ *
268
+ * @param state - System state
269
+ * @param section - Section ID
270
+ * @param subsection - Subsection ID (optional)
271
+ * @param silent - Whether to suppress change events
272
+ */
273
+ export const navigateTo = (
274
+ state: NavigationSystemState,
275
+ section: string,
276
+ subsection?: string,
277
+ silent?: boolean
278
+ ): void => {
279
+ // Skip if section doesn't exist
280
+ if (!section || !state.items[section]) {
281
+ return;
282
+ }
283
+
284
+ // Check if we're already on this section and subsection
285
+ if (state.activeSection === section && state.activeSubsection === subsection) {
286
+ return;
287
+ }
288
+
289
+ // Update active section
290
+ state.activeSection = section;
291
+
292
+ // Update rail if it exists
293
+ if (state.rail) {
294
+ state.rail.setActive(section, silent);
295
+ }
296
+
297
+ // Update active subsection if specified
298
+ if (subsection && state.drawer) {
299
+ state.activeSubsection = subsection;
300
+ state.drawer.setActive(subsection, silent);
301
+ }
302
+ };
@@ -0,0 +1,240 @@
1
+ // src/components/navigation/system/events.ts
2
+
3
+ import { NavigationSystemState } from './types';
4
+ import { NavigationItem, NavigationSection } from './types';
5
+
6
+ /**
7
+ * Registers rail navigation event handlers
8
+ *
9
+ * @param state - System state
10
+ * @param config - System configuration
11
+ * @param updateDrawerContent - Function to update drawer content
12
+ * @param showDrawer - Function to show the drawer
13
+ * @param hideDrawer - Function to hide the drawer
14
+ * @param systemApi - Reference to the public system API
15
+ */
16
+ export const registerRailEvents = (
17
+ state: NavigationSystemState,
18
+ config: any,
19
+ updateDrawerContent: (sectionId: string) => void,
20
+ showDrawer: () => void,
21
+ hideDrawer: () => void,
22
+ systemApi: any
23
+ ): void => {
24
+ const rail = state.rail;
25
+ if (!rail) return;
26
+
27
+ // Register for change events - will listen for when rail items are clicked
28
+ rail.on('change', (event: any) => {
29
+ // Extract ID from event data
30
+ const id = event?.id;
31
+
32
+ if (!id || state.processingChange) {
33
+ return;
34
+ }
35
+
36
+ // Check if this is a user action
37
+ const isUserAction = event?.source === 'userAction';
38
+
39
+ // Set processing flag to prevent loops
40
+ state.processingChange = true;
41
+
42
+ // Update active section
43
+ state.activeSection = id;
44
+
45
+ // Handle internally first - update drawer content
46
+ updateDrawerContent(id);
47
+
48
+ // Then notify external handlers
49
+ if (systemApi.onSectionChange && isUserAction) {
50
+ systemApi.onSectionChange(id, { source: isUserAction ? 'userClick' : 'programmatic' });
51
+ }
52
+
53
+ // Clear the processing flag after a delay
54
+ setTimeout(() => {
55
+ state.processingChange = false;
56
+ }, 50);
57
+ });
58
+
59
+ rail.on('mouseover', (event: any) => {
60
+ const id = event?.id;
61
+
62
+ // Set rail mouse state
63
+ state.mouseInRail = true;
64
+
65
+ // Clear any existing hover timer
66
+ clearTimeout(state.hoverTimer as number);
67
+ state.hoverTimer = null;
68
+
69
+ // Only schedule drawer operations if there's an ID
70
+ if (id) {
71
+ // Check if this section has items
72
+ if (state.items[id]?.items?.length > 0) {
73
+ // Has items - schedule drawer opening
74
+ state.hoverTimer = window.setTimeout(() => {
75
+ updateDrawerContent(id);
76
+ }, config.hoverDelay) as unknown as number;
77
+ } else {
78
+ // No items - hide drawer after a delay to prevent flickering
79
+ state.closeTimer = window.setTimeout(() => {
80
+ // Only hide if we're still in the rail but not in the drawer
81
+ if (state.mouseInRail && !state.mouseInDrawer) {
82
+ hideDrawer();
83
+ }
84
+ }, config.hoverDelay) as unknown as number;
85
+ }
86
+ }
87
+ });
88
+
89
+ rail.on('mouseenter', () => {
90
+ state.mouseInRail = true;
91
+
92
+ // Clear any pending drawer close timer when entering rail
93
+ clearTimeout(state.closeTimer as number);
94
+ state.closeTimer = null;
95
+ });
96
+
97
+ rail.on('mouseleave', () => {
98
+ state.mouseInRail = false;
99
+
100
+ // Clear any existing hover timer
101
+ clearTimeout(state.hoverTimer as number);
102
+ state.hoverTimer = null;
103
+
104
+ // Only set timer to hide drawer if we're not in drawer either
105
+ if (!state.mouseInDrawer) {
106
+ state.closeTimer = window.setTimeout(() => {
107
+ // Double-check we're still not in rail or drawer before hiding
108
+ if (!state.mouseInRail && !state.mouseInDrawer) {
109
+ hideDrawer();
110
+ }
111
+ }, config.closeDelay) as unknown as number;
112
+ }
113
+ });
114
+ };
115
+
116
+ /**
117
+ * Registers drawer navigation event handlers
118
+ *
119
+ * @param state - System state
120
+ * @param config - System configuration
121
+ * @param hideDrawer - Function to hide the drawer
122
+ * @param systemApi - Reference to the public system API
123
+ */
124
+ export const registerDrawerEvents = (
125
+ state: NavigationSystemState,
126
+ config: any,
127
+ hideDrawer: () => void,
128
+ systemApi: any
129
+ ): void => {
130
+ const drawer = state.drawer;
131
+ if (!drawer) return;
132
+
133
+ // Use the component's native event system
134
+ if (typeof drawer.on === 'function') {
135
+ // Handle item selection
136
+ drawer.on('change', (event: any) => {
137
+ const id = event.id;
138
+
139
+ state.activeSubsection = id;
140
+
141
+ // If configuration specifies to hide drawer on click, do so
142
+ if (config.hideDrawerOnClick) {
143
+ hideDrawer();
144
+ }
145
+
146
+ // Emit item selection event
147
+ if (systemApi.onItemSelect) {
148
+ systemApi.onItemSelect(event);
149
+ }
150
+ });
151
+
152
+ // Handle mouseenter/mouseleave for drawer
153
+ drawer.on('mouseenter', () => {
154
+ state.mouseInDrawer = true;
155
+
156
+ // Clear any hover and close timers
157
+ clearTimeout(state.hoverTimer as number);
158
+ clearTimeout(state.closeTimer as number);
159
+ state.hoverTimer = null;
160
+ state.closeTimer = null;
161
+ });
162
+
163
+ drawer.on('mouseleave', () => {
164
+ state.mouseInDrawer = false;
165
+
166
+ // Only set timer to hide drawer if we're not in rail
167
+ if (!state.mouseInRail) {
168
+ state.closeTimer = window.setTimeout(() => {
169
+ // Double-check we're still not in drawer or rail before hiding
170
+ if (!state.mouseInDrawer && !state.mouseInRail) {
171
+ hideDrawer();
172
+ }
173
+ }, config.closeDelay) as unknown as number;
174
+ }
175
+ });
176
+ }
177
+ };
178
+
179
+ /**
180
+ * Sets up window resize and orientation change handling
181
+ *
182
+ * @param state - System state
183
+ * @param checkMobileState - Function to check and update mobile state
184
+ */
185
+ export const setupResponsiveHandling = (
186
+ state: NavigationSystemState,
187
+ checkMobileState: () => void
188
+ ): void => {
189
+ // Setup responsive behavior
190
+ if (window.ResizeObserver) {
191
+ // Use ResizeObserver for better performance
192
+ state.resizeObserver = new ResizeObserver(() => {
193
+ checkMobileState();
194
+ });
195
+ state.resizeObserver.observe(document.body);
196
+ } else {
197
+ // Fallback to window resize event
198
+ window.addEventListener('resize', checkMobileState);
199
+ }
200
+
201
+ // Listen for orientation changes on mobile
202
+ window.addEventListener('orientationchange', () => {
203
+ // Small delay to ensure dimensions have updated
204
+ setTimeout(checkMobileState, 100);
205
+ });
206
+ };
207
+
208
+ /**
209
+ * Cleans up all event handlers and resources
210
+ *
211
+ * @param state - System state
212
+ * @param checkMobileState - Function reference to remove event handlers
213
+ */
214
+ export const cleanupEvents = (
215
+ state: NavigationSystemState,
216
+ checkMobileState: () => void
217
+ ): void => {
218
+ // Clean up resize observer
219
+ if (state.resizeObserver) {
220
+ state.resizeObserver.disconnect();
221
+ state.resizeObserver = null;
222
+ } else {
223
+ window.removeEventListener('resize', checkMobileState);
224
+ }
225
+
226
+ // Remove orientation change listener
227
+ window.removeEventListener('orientationchange', checkMobileState);
228
+
229
+ // Remove outside click handler
230
+ if (state.outsideClickHandler) {
231
+ const eventType = ('ontouchend' in window) ? 'touchend' : 'click';
232
+ document.removeEventListener(eventType, state.outsideClickHandler);
233
+ }
234
+
235
+ // Clear timers
236
+ clearTimeout(state.hoverTimer as number);
237
+ clearTimeout(state.closeTimer as number);
238
+ state.hoverTimer = null;
239
+ state.closeTimer = null;
240
+ };