mtrl 0.1.2 → 0.2.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.
Files changed (220) hide show
  1. package/README.md +70 -22
  2. package/index.ts +33 -0
  3. package/package.json +14 -5
  4. package/src/components/button/{styles.scss → _styles.scss} +2 -2
  5. package/src/components/button/api.ts +89 -0
  6. package/src/components/button/button.ts +50 -0
  7. package/src/components/button/config.ts +75 -0
  8. package/src/components/button/constants.ts +17 -0
  9. package/src/components/button/index.ts +4 -0
  10. package/src/components/button/types.ts +118 -0
  11. package/src/components/card/_styles.scss +359 -0
  12. package/src/components/card/actions.ts +48 -0
  13. package/src/components/card/api.ts +102 -0
  14. package/src/components/card/card.ts +41 -0
  15. package/src/components/card/config.ts +99 -0
  16. package/src/components/card/constants.ts +69 -0
  17. package/src/components/card/content.ts +48 -0
  18. package/src/components/card/features.ts +228 -0
  19. package/src/components/card/header.ts +88 -0
  20. package/src/components/card/index.ts +19 -0
  21. package/src/components/card/media.ts +52 -0
  22. package/src/components/card/types.ts +174 -0
  23. package/src/components/checkbox/api.ts +82 -0
  24. package/src/components/checkbox/checkbox.ts +75 -0
  25. package/src/components/checkbox/config.ts +90 -0
  26. package/src/components/checkbox/index.ts +4 -0
  27. package/src/components/checkbox/types.ts +146 -0
  28. package/src/components/chip/_styles.scss +372 -0
  29. package/src/components/chip/api.ts +115 -0
  30. package/src/components/chip/chip-set.ts +225 -0
  31. package/src/components/chip/chip.ts +82 -0
  32. package/src/components/chip/config.ts +92 -0
  33. package/src/components/chip/constants.ts +38 -0
  34. package/src/components/chip/index.ts +4 -0
  35. package/src/components/chip/types.ts +172 -0
  36. package/src/components/list/api.ts +72 -0
  37. package/src/components/list/config.ts +43 -0
  38. package/src/components/list/{constants.js → constants.ts} +34 -7
  39. package/src/components/list/features.ts +224 -0
  40. package/src/components/list/index.ts +14 -0
  41. package/src/components/list/list-item.ts +120 -0
  42. package/src/components/list/list.ts +37 -0
  43. package/src/components/list/types.ts +179 -0
  44. package/src/components/list/utils.ts +47 -0
  45. package/src/components/menu/api.ts +119 -0
  46. package/src/components/menu/config.ts +54 -0
  47. package/src/components/menu/constants.ts +154 -0
  48. package/src/components/menu/features/items-manager.ts +457 -0
  49. package/src/components/menu/features/keyboard-navigation.ts +133 -0
  50. package/src/components/menu/features/positioning.ts +127 -0
  51. package/src/components/menu/features/{visibility.js → visibility.ts} +66 -64
  52. package/src/components/menu/index.ts +14 -0
  53. package/src/components/menu/menu-item.ts +43 -0
  54. package/src/components/menu/menu.ts +53 -0
  55. package/src/components/menu/types.ts +178 -0
  56. package/src/components/navigation/api.ts +79 -0
  57. package/src/components/navigation/config.ts +61 -0
  58. package/src/components/navigation/{constants.js → constants.ts} +10 -10
  59. package/src/components/navigation/index.ts +14 -0
  60. package/src/components/navigation/nav-item.ts +148 -0
  61. package/src/components/navigation/navigation.ts +50 -0
  62. package/src/components/navigation/types.ts +212 -0
  63. package/src/components/progress/_styles.scss +204 -0
  64. package/src/components/progress/api.ts +179 -0
  65. package/src/components/progress/config.ts +124 -0
  66. package/src/components/progress/constants.ts +43 -0
  67. package/src/components/progress/index.ts +5 -0
  68. package/src/components/progress/progress.ts +163 -0
  69. package/src/components/progress/types.ts +102 -0
  70. package/src/components/snackbar/api.ts +162 -0
  71. package/src/components/snackbar/config.ts +62 -0
  72. package/src/components/snackbar/{constants.js → constants.ts} +21 -4
  73. package/src/components/snackbar/features.ts +76 -0
  74. package/src/components/snackbar/index.ts +4 -0
  75. package/src/components/snackbar/position.ts +71 -0
  76. package/src/components/snackbar/queue.ts +76 -0
  77. package/src/components/snackbar/snackbar.ts +60 -0
  78. package/src/components/snackbar/types.ts +58 -0
  79. package/src/components/switch/api.ts +77 -0
  80. package/src/components/switch/config.ts +74 -0
  81. package/src/components/switch/index.ts +4 -0
  82. package/src/components/switch/switch.ts +52 -0
  83. package/src/components/switch/types.ts +142 -0
  84. package/src/components/textfield/api.ts +72 -0
  85. package/src/components/textfield/config.ts +54 -0
  86. package/src/components/textfield/{constants.js → constants.ts} +38 -5
  87. package/src/components/textfield/index.ts +4 -0
  88. package/src/components/textfield/textfield.ts +50 -0
  89. package/src/components/textfield/types.ts +139 -0
  90. package/src/core/compose/base.ts +43 -0
  91. package/src/core/compose/component.ts +247 -0
  92. package/src/core/compose/features/checkable.ts +155 -0
  93. package/src/core/compose/features/disabled.ts +116 -0
  94. package/src/core/compose/features/events.ts +65 -0
  95. package/src/core/compose/features/icon.ts +67 -0
  96. package/src/core/compose/features/index.ts +35 -0
  97. package/src/core/compose/features/input.ts +174 -0
  98. package/src/core/compose/features/lifecycle.ts +139 -0
  99. package/src/core/compose/features/position.ts +94 -0
  100. package/src/core/compose/features/ripple.ts +55 -0
  101. package/src/core/compose/features/size.ts +29 -0
  102. package/src/core/compose/features/style.ts +31 -0
  103. package/src/core/compose/features/text.ts +44 -0
  104. package/src/core/compose/features/textinput.ts +225 -0
  105. package/src/core/compose/features/textlabel.ts +92 -0
  106. package/src/core/compose/features/track.ts +84 -0
  107. package/src/core/compose/features/variant.ts +29 -0
  108. package/src/core/compose/features/withEvents.ts +137 -0
  109. package/src/core/compose/index.ts +54 -0
  110. package/src/core/compose/{pipe.js → pipe.ts} +16 -11
  111. package/src/core/config/component-config.ts +136 -0
  112. package/src/core/config.ts +211 -0
  113. package/src/core/dom/{attributes.js → attributes.ts} +11 -11
  114. package/src/core/dom/classes.ts +60 -0
  115. package/src/core/dom/create.ts +188 -0
  116. package/src/core/dom/events.ts +209 -0
  117. package/src/core/dom/index.ts +10 -0
  118. package/src/core/dom/utils.ts +97 -0
  119. package/src/core/index.ts +111 -0
  120. package/src/core/state/disabled.ts +81 -0
  121. package/src/core/state/emitter.ts +94 -0
  122. package/src/core/state/events.ts +88 -0
  123. package/src/core/state/index.ts +16 -0
  124. package/src/core/state/lifecycle.ts +131 -0
  125. package/src/core/state/store.ts +197 -0
  126. package/src/core/utils/index.ts +45 -0
  127. package/src/core/utils/{mobile.js → mobile.ts} +48 -24
  128. package/src/core/utils/object.ts +41 -0
  129. package/src/core/utils/validate.ts +234 -0
  130. package/src/{index.js → index.ts} +4 -2
  131. package/index.js +0 -11
  132. package/src/components/button/api.js +0 -54
  133. package/src/components/button/button.js +0 -81
  134. package/src/components/button/config.js +0 -10
  135. package/src/components/button/constants.js +0 -63
  136. package/src/components/button/index.js +0 -2
  137. package/src/components/checkbox/api.js +0 -45
  138. package/src/components/checkbox/checkbox.js +0 -96
  139. package/src/components/checkbox/index.js +0 -2
  140. package/src/components/container/api.js +0 -42
  141. package/src/components/container/container.js +0 -45
  142. package/src/components/container/index.js +0 -2
  143. package/src/components/container/styles.scss +0 -66
  144. package/src/components/list/index.js +0 -2
  145. package/src/components/list/list-item.js +0 -147
  146. package/src/components/list/list.js +0 -267
  147. package/src/components/menu/api.js +0 -117
  148. package/src/components/menu/constants.js +0 -42
  149. package/src/components/menu/features/items-manager.js +0 -375
  150. package/src/components/menu/features/keyboard-navigation.js +0 -129
  151. package/src/components/menu/features/positioning.js +0 -125
  152. package/src/components/menu/index.js +0 -2
  153. package/src/components/menu/menu-item.js +0 -41
  154. package/src/components/menu/menu.js +0 -54
  155. package/src/components/navigation/api.js +0 -43
  156. package/src/components/navigation/index.js +0 -2
  157. package/src/components/navigation/nav-item.js +0 -137
  158. package/src/components/navigation/navigation.js +0 -55
  159. package/src/components/snackbar/api.js +0 -125
  160. package/src/components/snackbar/features.js +0 -69
  161. package/src/components/snackbar/index.js +0 -2
  162. package/src/components/snackbar/position.js +0 -63
  163. package/src/components/snackbar/queue.js +0 -74
  164. package/src/components/snackbar/snackbar.js +0 -70
  165. package/src/components/switch/api.js +0 -44
  166. package/src/components/switch/index.js +0 -2
  167. package/src/components/switch/switch.js +0 -71
  168. package/src/components/textfield/api.js +0 -49
  169. package/src/components/textfield/index.js +0 -2
  170. package/src/components/textfield/textfield.js +0 -68
  171. package/src/core/build/_ripple.scss +0 -79
  172. package/src/core/build/constants.js +0 -51
  173. package/src/core/build/icon.js +0 -78
  174. package/src/core/build/ripple.js +0 -159
  175. package/src/core/build/text.js +0 -54
  176. package/src/core/compose/base.js +0 -8
  177. package/src/core/compose/component.js +0 -225
  178. package/src/core/compose/features/checkable.js +0 -114
  179. package/src/core/compose/features/disabled.js +0 -64
  180. package/src/core/compose/features/events.js +0 -48
  181. package/src/core/compose/features/icon.js +0 -33
  182. package/src/core/compose/features/index.js +0 -20
  183. package/src/core/compose/features/input.js +0 -100
  184. package/src/core/compose/features/lifecycle.js +0 -69
  185. package/src/core/compose/features/position.js +0 -60
  186. package/src/core/compose/features/ripple.js +0 -32
  187. package/src/core/compose/features/size.js +0 -9
  188. package/src/core/compose/features/style.js +0 -12
  189. package/src/core/compose/features/text.js +0 -17
  190. package/src/core/compose/features/textinput.js +0 -114
  191. package/src/core/compose/features/textlabel.js +0 -28
  192. package/src/core/compose/features/track.js +0 -49
  193. package/src/core/compose/features/variant.js +0 -9
  194. package/src/core/compose/features/withEvents.js +0 -67
  195. package/src/core/compose/index.js +0 -16
  196. package/src/core/config.js +0 -140
  197. package/src/core/dom/classes.js +0 -70
  198. package/src/core/dom/create.js +0 -132
  199. package/src/core/dom/events.js +0 -175
  200. package/src/core/dom/index.js +0 -5
  201. package/src/core/dom/utils.js +0 -22
  202. package/src/core/index.js +0 -23
  203. package/src/core/state/disabled.js +0 -51
  204. package/src/core/state/emitter.js +0 -63
  205. package/src/core/state/events.js +0 -29
  206. package/src/core/state/index.js +0 -6
  207. package/src/core/state/lifecycle.js +0 -64
  208. package/src/core/state/store.js +0 -112
  209. package/src/core/utils/index.js +0 -39
  210. package/src/core/utils/object.js +0 -22
  211. package/src/core/utils/validate.js +0 -37
  212. /package/src/components/checkbox/{styles.scss → _styles.scss} +0 -0
  213. /package/src/components/checkbox/{constants.js → constants.ts} +0 -0
  214. /package/src/components/list/{styles.scss → _styles.scss} +0 -0
  215. /package/src/components/menu/{styles.scss → _styles.scss} +0 -0
  216. /package/src/components/navigation/{styles.scss → _styles.scss} +0 -0
  217. /package/src/components/snackbar/{styles.scss → _styles.scss} +0 -0
  218. /package/src/components/switch/{styles.scss → _styles.scss} +0 -0
  219. /package/src/components/switch/{constants.js → constants.ts} +0 -0
  220. /package/src/components/textfield/{styles.scss → _styles.scss} +0 -0
@@ -1,179 +1,181 @@
1
- // src/components/menu/features/visibility.js
2
- import { MENU_EVENTS } from '../constants'
1
+ // src/components/menu/features/visibility.ts
2
+ import { BaseComponent, MenuConfig } from '../types';
3
+ import { MENU_EVENTS } from '../constants';
3
4
 
4
5
  /**
5
6
  * Adds visibility management functionality to a menu component
6
- * @param {Object} config - Menu configuration
7
- * @param {string} config.prefix - CSS class prefix
7
+ * @param {MenuConfig} config - Menu configuration
8
8
  * @returns {Function} Component enhancer
9
9
  */
10
- export const withVisibility = (config) => (component) => {
11
- let isVisible = false
12
- let outsideClickHandler = null
13
- let keydownHandler = null
10
+ export const withVisibility = (config: MenuConfig) => (component: BaseComponent): BaseComponent => {
11
+ let isVisible = false;
12
+ let outsideClickHandler: ((event: MouseEvent) => void) | null = null;
13
+ let keydownHandler: ((event: KeyboardEvent) => void) | null = null;
14
+ const prefix = config.prefix || 'mtrl';
14
15
 
15
16
  // Create the component interface with hide/show methods first
16
- const enhancedComponent = {
17
+ const enhancedComponent: BaseComponent = {
17
18
  ...component,
18
19
 
19
20
  /**
20
21
  * Shows the menu
21
22
  */
22
- show () {
23
- if (isVisible) return this
23
+ show() {
24
+ if (isVisible) return this;
24
25
 
25
26
  // First set visibility to true to prevent multiple calls
26
- isVisible = true
27
+ isVisible = true;
27
28
 
28
29
  // Make sure the element is in the DOM
29
30
  if (!component.element.parentNode) {
30
- document.body.appendChild(component.element)
31
+ document.body.appendChild(component.element);
31
32
  }
32
33
 
33
34
  // Always clean up previous handlers before adding new ones
34
35
  if (outsideClickHandler) {
35
- document.removeEventListener('mousedown', outsideClickHandler)
36
+ document.removeEventListener('mousedown', outsideClickHandler);
36
37
  }
37
38
 
38
39
  // Setup outside click handler for closing
39
- outsideClickHandler = handleOutsideClick
40
+ outsideClickHandler = handleOutsideClick;
40
41
 
41
42
  // Use setTimeout to ensure the handler is not triggered immediately
42
43
  setTimeout(() => {
43
- document.addEventListener('mousedown', outsideClickHandler)
44
- }, 0)
44
+ document.addEventListener('mousedown', outsideClickHandler!);
45
+ }, 0);
45
46
 
46
47
  // Setup keyboard navigation
47
48
  if (!keydownHandler) {
48
- keydownHandler = handleKeydown
49
- document.addEventListener('keydown', keydownHandler)
49
+ keydownHandler = handleKeydown;
50
+ document.addEventListener('keydown', keydownHandler);
50
51
  }
51
52
 
52
53
  // Add display block first for transition to work
53
- component.element.style.display = 'block'
54
+ component.element.style.display = 'block';
54
55
 
55
56
  // Force a reflow before adding the visible class for animation
56
- component.element.offsetHeight
57
- component.element.classList.add(`${config.prefix}-menu--visible`)
58
- component.element.setAttribute('aria-hidden', 'false')
57
+ // eslint-disable-next-line no-void
58
+ void component.element.offsetHeight;
59
+ component.element.classList.add(`${prefix}-menu--visible`);
60
+ component.element.setAttribute('aria-hidden', 'false');
59
61
 
60
62
  // Emit open event
61
- component.emit(MENU_EVENTS.OPEN)
63
+ component.emit?.(MENU_EVENTS.OPEN, {});
62
64
 
63
- return this
65
+ return this;
64
66
  },
65
67
 
66
68
  /**
67
69
  * Hides the menu
68
70
  */
69
- hide () {
71
+ hide() {
70
72
  // Return early if already hidden
71
- if (!isVisible) return this
73
+ if (!isVisible) return this;
72
74
 
73
75
  // First set the visibility flag to false
74
- isVisible = false
76
+ isVisible = false;
75
77
 
76
78
  // Close any open submenus first
77
79
  if (component.closeSubmenus) {
78
- component.closeSubmenus()
80
+ component.closeSubmenus();
79
81
  }
80
82
 
81
83
  // Remove ALL event listeners
82
84
  if (outsideClickHandler) {
83
- document.removeEventListener('mousedown', outsideClickHandler)
84
- outsideClickHandler = null
85
+ document.removeEventListener('mousedown', outsideClickHandler);
86
+ outsideClickHandler = null;
85
87
  }
86
88
 
87
89
  if (keydownHandler) {
88
- document.removeEventListener('keydown', keydownHandler)
89
- keydownHandler = null
90
+ document.removeEventListener('keydown', keydownHandler);
91
+ keydownHandler = null;
90
92
  }
91
93
 
92
94
  // Hide the menu with visual indication first
93
- component.element.classList.remove(`${config.prefix}-menu--visible`)
94
- component.element.setAttribute('aria-hidden', 'true')
95
+ component.element.classList.remove(`${prefix}-menu--visible`);
96
+ component.element.setAttribute('aria-hidden', 'true');
95
97
 
96
98
  // Define a reliable cleanup function
97
99
  const cleanupElement = () => {
98
100
  // Safety check to prevent errors
99
101
  if (component.element) {
100
- component.element.style.display = 'none'
102
+ component.element.style.display = 'none';
101
103
 
102
104
  // Remove from DOM if still attached
103
105
  if (component.element.parentNode) {
104
- component.element.remove()
106
+ component.element.remove();
105
107
  }
106
108
  }
107
- }
109
+ };
108
110
 
109
111
  // Try to use transition end for smooth animation
110
- const handleTransitionEnd = (e) => {
112
+ const handleTransitionEnd = (e: TransitionEvent) => {
111
113
  if (e.propertyName === 'opacity' || e.propertyName === 'transform') {
112
- component.element.removeEventListener('transitionend', handleTransitionEnd)
113
- cleanupElement()
114
+ component.element.removeEventListener('transitionend', handleTransitionEnd);
115
+ cleanupElement();
114
116
  }
115
- }
117
+ };
116
118
 
117
- component.element.addEventListener('transitionend', handleTransitionEnd)
119
+ component.element.addEventListener('transitionend', handleTransitionEnd);
118
120
 
119
121
  // Fallback timeout in case transition events don't fire
120
122
  // This ensures the menu always gets removed
121
- setTimeout(cleanupElement, 300)
123
+ setTimeout(cleanupElement, 300);
122
124
 
123
125
  // Emit close event
124
- component.emit(MENU_EVENTS.CLOSE)
126
+ component.emit?.(MENU_EVENTS.CLOSE, {});
125
127
 
126
- return this
128
+ return this;
127
129
  },
128
130
 
129
131
  /**
130
132
  * Returns whether the menu is currently visible
131
133
  * @returns {boolean} Visibility state
132
134
  */
133
- isVisible () {
134
- return isVisible
135
+ isVisible() {
136
+ return isVisible;
135
137
  }
136
- }
138
+ };
137
139
 
138
140
  /**
139
141
  * Handles clicks outside the menu
140
142
  * @param {MouseEvent} event - Mouse event
141
143
  */
142
- const handleOutsideClick = (event) => {
143
- if (!isVisible) return
144
+ const handleOutsideClick = (event: MouseEvent) => {
145
+ if (!isVisible) return;
144
146
 
145
147
  // Store the opening button if available
146
- const openingButton = config.openingButton?.element
148
+ const openingButton = config.openingButton?.element;
147
149
 
148
150
  // Check if click is outside the menu but not on the opening button
149
- const clickedElement = event.target
151
+ const clickedElement = event.target as Node;
150
152
 
151
153
  // Don't close if the click is inside the menu
152
154
  if (component.element.contains(clickedElement)) {
153
- return
155
+ return;
154
156
  }
155
157
 
156
158
  // Don't close if the click is on the opening button (it will handle opening/closing)
157
159
  if (openingButton && (openingButton === clickedElement || openingButton.contains(clickedElement))) {
158
- return
160
+ return;
159
161
  }
160
162
 
161
163
  // If we got here, close the menu
162
- enhancedComponent.hide()
163
- }
164
+ enhancedComponent.hide?.();
165
+ };
164
166
 
165
167
  /**
166
168
  * Handles keyboard events
167
169
  * @param {KeyboardEvent} event - Keyboard event
168
170
  */
169
- const handleKeydown = (event) => {
170
- if (!isVisible) return
171
+ const handleKeydown = (event: KeyboardEvent) => {
172
+ if (!isVisible) return;
171
173
 
172
174
  if (event.key === 'Escape') {
173
- event.preventDefault()
174
- enhancedComponent.hide()
175
+ event.preventDefault();
176
+ enhancedComponent.hide?.();
175
177
  }
176
- }
178
+ };
177
179
 
178
- return enhancedComponent
179
- }
180
+ return enhancedComponent;
181
+ };
@@ -0,0 +1,14 @@
1
+ // src/components/menu/index.ts
2
+ export { default } from './menu'
3
+ export {
4
+ MENU_ALIGN,
5
+ MENU_VERTICAL_ALIGN,
6
+ MENU_ITEM_TYPES,
7
+ MENU_EVENTS
8
+ } from './constants'
9
+ export {
10
+ MenuConfig,
11
+ MenuComponent,
12
+ MenuItemConfig,
13
+ MenuPositionConfig
14
+ } from './types'
@@ -0,0 +1,43 @@
1
+ // src/components/menu/menu-item.ts
2
+ import { MenuItemConfig } from './types';
3
+ import { MENU_ITEM_TYPES } from './constants';
4
+
5
+ /**
6
+ * Creates a menu item element
7
+ * @param {MenuItemConfig} itemConfig - Item configuration
8
+ * @param {string} prefix - CSS class prefix
9
+ * @returns {HTMLElement} Menu item element
10
+ */
11
+ export const createMenuItem = (itemConfig: MenuItemConfig, prefix: string): HTMLElement => {
12
+ const item = document.createElement('li');
13
+ item.className = `${prefix}-menu-item`;
14
+
15
+ if (itemConfig.type === MENU_ITEM_TYPES.DIVIDER) {
16
+ item.className = `${prefix}-menu-divider`;
17
+ return item;
18
+ }
19
+
20
+ if (itemConfig.class) {
21
+ item.className += ` ${itemConfig.class}`;
22
+ }
23
+
24
+ if (itemConfig.disabled) {
25
+ item.setAttribute('aria-disabled', 'true');
26
+ item.className += ` ${prefix}-menu-item--disabled`;
27
+ }
28
+
29
+ if (itemConfig.name) {
30
+ item.setAttribute('data-name', itemConfig.name);
31
+ }
32
+
33
+ item.textContent = itemConfig.text || '';
34
+
35
+ if (itemConfig.items?.length) {
36
+ item.className += ` ${prefix}-menu-item--submenu`;
37
+ item.setAttribute('aria-haspopup', 'true');
38
+ item.setAttribute('aria-expanded', 'false');
39
+ // We don't need to add a submenu indicator as it's handled by CSS ::after
40
+ }
41
+
42
+ return item;
43
+ }
@@ -0,0 +1,53 @@
1
+ // src/components/menu/menu.ts
2
+ import { pipe } from '../../core/compose';
3
+ import { createBase, withElement } from '../../core/compose/component';
4
+ import { withEvents, withLifecycle } from '../../core/compose/features';
5
+ import { withAPI } from './api';
6
+ import { withVisibility } from './features/visibility';
7
+ import { withItemsManager } from './features/items-manager';
8
+ import { withPositioning } from './features/positioning';
9
+ import { withKeyboardNavigation } from './features/keyboard-navigation';
10
+ import { MenuConfig, MenuComponent } from './types';
11
+ import {
12
+ createBaseConfig,
13
+ getElementConfig,
14
+ getApiConfig
15
+ } from './config';
16
+
17
+ /**
18
+ * Creates a new Menu component
19
+ * @param {MenuConfig} config - Menu configuration
20
+ * @returns {MenuComponent} Menu component instance
21
+ */
22
+ const createMenu = (config: MenuConfig = {}): MenuComponent => {
23
+ const baseConfig = createBaseConfig(config);
24
+
25
+ try {
26
+ // Create menu component
27
+ const menu = pipe(
28
+ createBase,
29
+ withEvents(),
30
+ withElement(getElementConfig(baseConfig)),
31
+ withLifecycle(),
32
+ withItemsManager(baseConfig),
33
+ withVisibility(baseConfig),
34
+ withPositioning,
35
+ withKeyboardNavigation(baseConfig),
36
+ comp => withAPI(getApiConfig(comp))(comp)
37
+ )(baseConfig);
38
+
39
+ // Handle circular dependency for submenus
40
+ // This is needed because we need the complete menu factory function
41
+ // to create submenus, but we can't import it directly in items-manager
42
+ if (menu.setCreateSubmenuFunction) {
43
+ menu.setCreateSubmenuFunction(createMenu);
44
+ }
45
+
46
+ return menu as MenuComponent;
47
+ } catch (error) {
48
+ console.error('Menu creation error:', error instanceof Error ? error.message : String(error));
49
+ throw new Error(`Failed to create menu: ${error instanceof Error ? error.message : String(error)}`);
50
+ }
51
+ };
52
+
53
+ export default createMenu;
@@ -0,0 +1,178 @@
1
+ // src/components/menu/types.ts
2
+ import {
3
+ MENU_ALIGN,
4
+ MENU_VERTICAL_ALIGN,
5
+ MENU_ITEM_TYPES
6
+ } from './constants';
7
+
8
+ /**
9
+ * Menu item configuration
10
+ */
11
+ export interface MenuItemConfig {
12
+ /** Unique identifier for the item */
13
+ name: string;
14
+
15
+ /** Text content for the item */
16
+ text: string;
17
+
18
+ /** Type of menu item */
19
+ type?: keyof typeof MENU_ITEM_TYPES | string;
20
+
21
+ /** Whether the item is disabled */
22
+ disabled?: boolean;
23
+
24
+ /** Additional CSS classes */
25
+ class?: string;
26
+
27
+ /** Submenu items */
28
+ items?: MenuItemConfig[];
29
+ }
30
+
31
+ /**
32
+ * Position configuration
33
+ */
34
+ export interface MenuPositionConfig {
35
+ /** Horizontal alignment */
36
+ align?: keyof typeof MENU_ALIGN | string;
37
+
38
+ /** Vertical alignment */
39
+ vAlign?: keyof typeof MENU_VERTICAL_ALIGN | string;
40
+
41
+ /** Horizontal offset in pixels */
42
+ offsetX?: number;
43
+
44
+ /** Vertical offset in pixels */
45
+ offsetY?: number;
46
+ }
47
+
48
+ /**
49
+ * Position result
50
+ */
51
+ export interface MenuPosition {
52
+ /** Left position in pixels */
53
+ left: number;
54
+
55
+ /** Top position in pixels */
56
+ top: number;
57
+ }
58
+
59
+ /**
60
+ * Stored item data
61
+ */
62
+ export interface MenuItemData {
63
+ /** DOM element for the item */
64
+ element: HTMLElement;
65
+
66
+ /** Item configuration */
67
+ config: MenuItemConfig;
68
+ }
69
+
70
+ /**
71
+ * Menu selection event data
72
+ */
73
+ export interface MenuSelectEvent {
74
+ /** Name of the selected item */
75
+ name: string;
76
+
77
+ /** Text content of the selected item */
78
+ text: string;
79
+
80
+ /** Path of parent item names (for submenus) */
81
+ path?: string[];
82
+ }
83
+
84
+ /**
85
+ * Configuration interface for the Menu component
86
+ */
87
+ export interface MenuConfig {
88
+ /** Initial menu items */
89
+ items?: MenuItemConfig[];
90
+
91
+ /** Additional CSS classes */
92
+ class?: string;
93
+
94
+ /** Whether to keep menu open after selection */
95
+ stayOpenOnSelect?: boolean;
96
+
97
+ /** Button element that opens the menu */
98
+ openingButton?: HTMLElement | { element: HTMLElement };
99
+
100
+ /** Parent item element (for submenus) */
101
+ parentItem?: HTMLElement;
102
+
103
+ /** Prefix for class names */
104
+ prefix?: string;
105
+
106
+ /** Component name */
107
+ componentName?: string;
108
+ }
109
+
110
+ /**
111
+ * Menu component interface
112
+ */
113
+ export interface MenuComponent {
114
+ /** The root element of the menu */
115
+ element: HTMLElement;
116
+
117
+ /** Shows the menu */
118
+ show: () => MenuComponent;
119
+
120
+ /** Hides the menu */
121
+ hide: () => MenuComponent;
122
+
123
+ /** Checks if the menu is visible */
124
+ isVisible: () => boolean;
125
+
126
+ /** Positions the menu relative to a target */
127
+ position: (target: HTMLElement, options?: MenuPositionConfig) => MenuComponent;
128
+
129
+ /** Adds a menu item */
130
+ addItem: (config: MenuItemConfig) => MenuComponent;
131
+
132
+ /** Removes a menu item by name */
133
+ removeItem: (name: string) => MenuComponent;
134
+
135
+ /** Gets all menu items */
136
+ getItems: () => Map<string, MenuItemData>;
137
+
138
+ /** Adds event listener */
139
+ on: (event: string, handler: Function) => MenuComponent;
140
+
141
+ /** Removes event listener */
142
+ off: (event: string, handler: Function) => MenuComponent;
143
+
144
+ /** Destroys the menu component and cleans up resources */
145
+ destroy: () => MenuComponent;
146
+ }
147
+
148
+ /**
149
+ * Base component interface
150
+ */
151
+ export interface BaseComponent {
152
+ element: HTMLElement;
153
+ emit?: (event: string, data: any) => void;
154
+ on?: (event: string, handler: Function) => any;
155
+ off?: (event: string, handler: Function) => any;
156
+ show?: () => any;
157
+ hide?: () => any;
158
+ isVisible?: () => boolean;
159
+ position?: (target: HTMLElement, options?: MenuPositionConfig) => any;
160
+ addItem?: (config: MenuItemConfig) => any;
161
+ removeItem?: (name: string) => any;
162
+ getItems?: () => Map<string, MenuItemData>;
163
+ closeSubmenus?: () => any;
164
+ refreshHoverHandlers?: () => any;
165
+ lifecycle?: {
166
+ destroy: () => void;
167
+ };
168
+ [key: string]: any;
169
+ }
170
+
171
+ /**
172
+ * API options interface
173
+ */
174
+ export interface ApiOptions {
175
+ lifecycle: {
176
+ destroy: () => void;
177
+ };
178
+ }
@@ -0,0 +1,79 @@
1
+ // src/components/navigation/api.ts
2
+ import {
3
+ BaseComponent,
4
+ NavigationComponent,
5
+ ApiOptions,
6
+ NavItemConfig,
7
+ NavItemData
8
+ } from './types';
9
+
10
+ /**
11
+ * Enhances navigation component with API methods
12
+ * @param {ApiOptions} options - API configuration
13
+ * @returns {Function} Higher-order function that adds API methods to component
14
+ */
15
+ export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
16
+ (component: BaseComponent): NavigationComponent => ({
17
+ ...component as any,
18
+ element: component.element,
19
+ items: component.items as Map<string, NavItemData>,
20
+
21
+ // Item management
22
+ addItem(config: NavItemConfig): NavigationComponent {
23
+ component.addItem?.(config);
24
+ return this;
25
+ },
26
+
27
+ removeItem(id: string): NavigationComponent {
28
+ component.removeItem?.(id);
29
+ return this;
30
+ },
31
+
32
+ getItem(id: string): NavItemData | undefined {
33
+ return component.getItem?.(id);
34
+ },
35
+
36
+ getAllItems(): NavItemData[] {
37
+ return component.getAllItems?.() || [];
38
+ },
39
+
40
+ getItemPath(id: string): string[] {
41
+ return component.getItemPath?.(id) || [];
42
+ },
43
+
44
+ // Active state management
45
+ setActive(id: string): NavigationComponent {
46
+ component.setActive?.(id);
47
+ return this;
48
+ },
49
+
50
+ getActive(): NavItemData | null {
51
+ return component.getActive?.() || null;
52
+ },
53
+
54
+ // Event handling
55
+ on(event: string, handler: Function): NavigationComponent {
56
+ component.on?.(event, handler);
57
+ return this;
58
+ },
59
+
60
+ off(event: string, handler: Function): NavigationComponent {
61
+ component.off?.(event, handler);
62
+ return this;
63
+ },
64
+
65
+ // State management
66
+ enable(): NavigationComponent {
67
+ disabled.enable();
68
+ return this;
69
+ },
70
+
71
+ disable(): NavigationComponent {
72
+ disabled.disable();
73
+ return this;
74
+ },
75
+
76
+ destroy(): void {
77
+ lifecycle.destroy();
78
+ }
79
+ });
@@ -0,0 +1,61 @@
1
+ // src/components/navigation/config.ts
2
+ import {
3
+ createComponentConfig,
4
+ createElementConfig,
5
+ BaseComponentConfig
6
+ } from '../../core/config/component-config';
7
+ import { NavigationConfig, BaseComponent, ApiOptions } from './types';
8
+ import { NAV_VARIANTS, NAV_POSITIONS, NAV_BEHAVIORS } from './constants';
9
+
10
+ /**
11
+ * Default configuration for the Navigation component
12
+ */
13
+ export const defaultConfig: NavigationConfig = {
14
+ variant: NAV_VARIANTS.RAIL,
15
+ position: NAV_POSITIONS.LEFT,
16
+ behavior: NAV_BEHAVIORS.FIXED,
17
+ items: [],
18
+ showLabels: true,
19
+ scrimEnabled: false
20
+ };
21
+
22
+ /**
23
+ * Creates the base configuration for Navigation component
24
+ * @param {NavigationConfig} config - User provided configuration
25
+ * @returns {NavigationConfig} Complete configuration with defaults applied
26
+ */
27
+ export const createBaseConfig = (config: NavigationConfig = {}): NavigationConfig =>
28
+ createComponentConfig(defaultConfig, config, 'nav') as NavigationConfig;
29
+
30
+ /**
31
+ * Generates element configuration for the Navigation component
32
+ * @param {NavigationConfig} config - Navigation configuration
33
+ * @returns {Object} Element configuration object for withElement
34
+ */
35
+ export const getElementConfig = (config: NavigationConfig) =>
36
+ createElementConfig(config, {
37
+ tag: 'nav',
38
+ componentName: 'nav',
39
+ attrs: {
40
+ role: 'navigation',
41
+ 'aria-label': config.ariaLabel || 'Main Navigation'
42
+ },
43
+ className: config.class
44
+ });
45
+
46
+ /**
47
+ * Creates API configuration for the Navigation component
48
+ * @param {BaseComponent} comp - Component with disabled and lifecycle features
49
+ * @returns {ApiOptions} API configuration object
50
+ */
51
+ export const getApiConfig = (comp: BaseComponent): ApiOptions => ({
52
+ disabled: {
53
+ enable: comp.disabled?.enable,
54
+ disable: comp.disabled?.disable
55
+ },
56
+ lifecycle: {
57
+ destroy: comp.lifecycle?.destroy
58
+ }
59
+ });
60
+
61
+ export default defaultConfig;