mtrl 0.2.5 → 0.2.7

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 (196) hide show
  1. package/index.ts +18 -0
  2. package/package.json +1 -1
  3. package/src/components/badge/_styles.scss +123 -115
  4. package/src/components/badge/api.ts +57 -59
  5. package/src/components/badge/badge.ts +16 -2
  6. package/src/components/badge/config.ts +65 -11
  7. package/src/components/badge/constants.ts +22 -12
  8. package/src/components/badge/features.ts +44 -40
  9. package/src/components/badge/types.ts +42 -30
  10. package/src/components/bottom-app-bar/_styles.scss +103 -0
  11. package/src/components/bottom-app-bar/bottom-app-bar.ts +196 -0
  12. package/src/components/bottom-app-bar/config.ts +73 -0
  13. package/src/components/bottom-app-bar/index.ts +11 -0
  14. package/src/components/bottom-app-bar/types.ts +108 -0
  15. package/src/components/button/_styles.scss +0 -66
  16. package/src/components/button/api.ts +5 -0
  17. package/src/components/button/button.ts +0 -2
  18. package/src/components/button/config.ts +5 -0
  19. package/src/components/button/constants.ts +0 -6
  20. package/src/components/button/index.ts +2 -2
  21. package/src/components/button/types.ts +7 -7
  22. package/src/components/card/_styles.scss +67 -25
  23. package/src/components/card/api.ts +54 -3
  24. package/src/components/card/card.ts +25 -6
  25. package/src/components/card/config.ts +189 -22
  26. package/src/components/card/constants.ts +20 -19
  27. package/src/components/card/content.ts +299 -2
  28. package/src/components/card/features.ts +158 -4
  29. package/src/components/card/index.ts +31 -9
  30. package/src/components/card/types.ts +166 -15
  31. package/src/components/checkbox/_styles.scss +0 -2
  32. package/src/components/chip/chip.ts +1 -9
  33. package/src/components/chip/constants.ts +0 -10
  34. package/src/components/chip/index.ts +1 -1
  35. package/src/components/chip/types.ts +1 -4
  36. package/src/components/datepicker/_styles.scss +358 -0
  37. package/src/components/datepicker/api.ts +272 -0
  38. package/src/components/datepicker/config.ts +144 -0
  39. package/src/components/datepicker/constants.ts +98 -0
  40. package/src/components/datepicker/datepicker.ts +346 -0
  41. package/src/components/datepicker/index.ts +9 -0
  42. package/src/components/datepicker/render.ts +452 -0
  43. package/src/components/datepicker/types.ts +268 -0
  44. package/src/components/datepicker/utils.ts +290 -0
  45. package/src/components/dialog/_styles.scss +174 -128
  46. package/src/components/dialog/api.ts +48 -13
  47. package/src/components/dialog/config.ts +9 -5
  48. package/src/components/dialog/dialog.ts +6 -3
  49. package/src/components/dialog/features.ts +290 -130
  50. package/src/components/dialog/types.ts +7 -4
  51. package/src/components/divider/_styles.scss +57 -0
  52. package/src/components/divider/config.ts +81 -0
  53. package/src/components/divider/divider.ts +37 -0
  54. package/src/components/divider/features.ts +207 -0
  55. package/src/components/divider/index.ts +5 -0
  56. package/src/components/divider/types.ts +55 -0
  57. package/src/components/extended-fab/_styles.scss +267 -0
  58. package/src/components/extended-fab/api.ts +141 -0
  59. package/src/components/extended-fab/config.ts +108 -0
  60. package/src/components/extended-fab/constants.ts +36 -0
  61. package/src/components/extended-fab/extended-fab.ts +125 -0
  62. package/src/components/extended-fab/index.ts +4 -0
  63. package/src/components/extended-fab/types.ts +287 -0
  64. package/src/components/fab/_styles.scss +225 -0
  65. package/src/components/fab/api.ts +97 -0
  66. package/src/components/fab/config.ts +94 -0
  67. package/src/components/fab/constants.ts +41 -0
  68. package/src/components/fab/fab.ts +67 -0
  69. package/src/components/fab/index.ts +4 -0
  70. package/src/components/fab/types.ts +234 -0
  71. package/src/components/navigation/_styles.scss +1 -0
  72. package/src/components/navigation/api.ts +78 -50
  73. package/src/components/navigation/features/items.ts +280 -0
  74. package/src/components/navigation/nav-item.ts +72 -23
  75. package/src/components/navigation/navigation.ts +54 -2
  76. package/src/components/navigation/types.ts +210 -188
  77. package/src/components/progress/_styles.scss +0 -65
  78. package/src/components/progress/config.ts +1 -2
  79. package/src/components/progress/constants.ts +0 -14
  80. package/src/components/progress/index.ts +1 -1
  81. package/src/components/progress/progress.ts +1 -4
  82. package/src/components/progress/types.ts +1 -4
  83. package/src/components/radios/_styles.scss +0 -45
  84. package/src/components/radios/api.ts +85 -60
  85. package/src/components/radios/config.ts +1 -2
  86. package/src/components/radios/constants.ts +0 -9
  87. package/src/components/radios/index.ts +1 -1
  88. package/src/components/radios/radio.ts +34 -11
  89. package/src/components/radios/radios.ts +2 -1
  90. package/src/components/radios/types.ts +1 -7
  91. package/src/components/search/_styles.scss +306 -0
  92. package/src/components/search/api.ts +203 -0
  93. package/src/components/search/config.ts +87 -0
  94. package/src/components/search/constants.ts +21 -0
  95. package/src/components/search/features/index.ts +4 -0
  96. package/src/components/search/features/search.ts +718 -0
  97. package/src/components/search/features/states.ts +165 -0
  98. package/src/components/search/features/structure.ts +198 -0
  99. package/src/components/search/index.ts +10 -0
  100. package/src/components/search/search.ts +52 -0
  101. package/src/components/search/types.ts +163 -0
  102. package/src/components/segmented-button/_styles.scss +117 -0
  103. package/src/components/segmented-button/config.ts +67 -0
  104. package/src/components/segmented-button/constants.ts +42 -0
  105. package/src/components/segmented-button/index.ts +4 -0
  106. package/src/components/segmented-button/segment.ts +155 -0
  107. package/src/components/segmented-button/segmented-button.ts +250 -0
  108. package/src/components/segmented-button/types.ts +219 -0
  109. package/src/components/slider/_styles.scss +221 -168
  110. package/src/components/slider/accessibility.md +59 -0
  111. package/src/components/slider/api.ts +41 -120
  112. package/src/components/slider/config.ts +51 -49
  113. package/src/components/slider/features/handlers.ts +495 -0
  114. package/src/components/slider/features/index.ts +1 -2
  115. package/src/components/slider/features/slider.ts +66 -84
  116. package/src/components/slider/features/states.ts +195 -0
  117. package/src/components/slider/features/structure.ts +141 -184
  118. package/src/components/slider/features/ui.ts +150 -201
  119. package/src/components/slider/index.ts +2 -11
  120. package/src/components/slider/slider.ts +9 -12
  121. package/src/components/slider/types.ts +39 -24
  122. package/src/components/switch/_styles.scss +0 -2
  123. package/src/components/tabs/_styles.scss +346 -154
  124. package/src/components/tabs/api.ts +178 -400
  125. package/src/components/tabs/config.ts +46 -52
  126. package/src/components/tabs/constants.ts +85 -8
  127. package/src/components/tabs/features.ts +403 -0
  128. package/src/components/tabs/index.ts +60 -3
  129. package/src/components/tabs/indicator.ts +285 -0
  130. package/src/components/tabs/responsive.ts +144 -0
  131. package/src/components/tabs/scroll-indicators.ts +149 -0
  132. package/src/components/tabs/state.ts +186 -0
  133. package/src/components/tabs/tab-api.ts +258 -0
  134. package/src/components/tabs/tab.ts +255 -0
  135. package/src/components/tabs/tabs.ts +50 -31
  136. package/src/components/tabs/types.ts +332 -128
  137. package/src/components/tabs/utils.ts +107 -0
  138. package/src/components/textfield/_styles.scss +0 -98
  139. package/src/components/textfield/config.ts +2 -3
  140. package/src/components/textfield/constants.ts +0 -14
  141. package/src/components/textfield/index.ts +2 -2
  142. package/src/components/textfield/textfield.ts +0 -2
  143. package/src/components/textfield/types.ts +1 -4
  144. package/src/components/timepicker/README.md +277 -0
  145. package/src/components/timepicker/_styles.scss +451 -0
  146. package/src/components/timepicker/api.ts +632 -0
  147. package/src/components/timepicker/clockdial.ts +482 -0
  148. package/src/components/timepicker/config.ts +130 -0
  149. package/src/components/timepicker/constants.ts +138 -0
  150. package/src/components/timepicker/index.ts +8 -0
  151. package/src/components/timepicker/render.ts +613 -0
  152. package/src/components/timepicker/timepicker.ts +117 -0
  153. package/src/components/timepicker/types.ts +336 -0
  154. package/src/components/timepicker/utils.ts +241 -0
  155. package/src/components/top-app-bar/_styles.scss +225 -0
  156. package/src/components/top-app-bar/config.ts +83 -0
  157. package/src/components/top-app-bar/index.ts +11 -0
  158. package/src/components/top-app-bar/top-app-bar.ts +316 -0
  159. package/src/components/top-app-bar/types.ts +140 -0
  160. package/src/core/build/_ripple.scss +6 -6
  161. package/src/core/build/ripple.ts +72 -95
  162. package/src/core/compose/component.ts +1 -1
  163. package/src/core/compose/features/badge.ts +79 -0
  164. package/src/core/compose/features/icon.ts +3 -1
  165. package/src/core/compose/features/index.ts +3 -1
  166. package/src/core/compose/features/ripple.ts +4 -1
  167. package/src/core/compose/features/textlabel.ts +26 -2
  168. package/src/core/dom/create.ts +5 -0
  169. package/src/index.ts +9 -0
  170. package/src/styles/abstract/_theme.scss +115 -3
  171. package/src/styles/themes/_autumn.scss +21 -0
  172. package/src/styles/themes/_base-theme.scss +61 -0
  173. package/src/styles/themes/_baseline.scss +58 -0
  174. package/src/styles/themes/_bluekhaki.scss +125 -0
  175. package/src/styles/themes/_brownbeige.scss +125 -0
  176. package/src/styles/themes/_browngreen.scss +125 -0
  177. package/src/styles/themes/_forest.scss +6 -0
  178. package/src/styles/themes/_greenbeige.scss +125 -0
  179. package/src/styles/themes/_material.scss +125 -0
  180. package/src/styles/themes/_ocean.scss +6 -0
  181. package/src/styles/themes/_sageivory.scss +125 -0
  182. package/src/styles/themes/_spring.scss +6 -0
  183. package/src/styles/themes/_summer.scss +5 -0
  184. package/src/styles/themes/_sunset.scss +5 -0
  185. package/src/styles/themes/_tealcaramel.scss +125 -0
  186. package/src/styles/themes/_winter.scss +6 -0
  187. package/src/components/card/actions.ts +0 -48
  188. package/src/components/card/header.ts +0 -88
  189. package/src/components/card/media.ts +0 -52
  190. package/src/components/navigation/features/items.js +0 -192
  191. package/src/components/slider/features/appearance.ts +0 -94
  192. package/src/components/slider/features/disabled.ts +0 -43
  193. package/src/components/slider/features/events.ts +0 -164
  194. package/src/components/slider/features/interactions.ts +0 -261
  195. package/src/components/slider/features/keyboard.ts +0 -112
  196. package/src/core/collection/adapters/mongodb.js +0 -232
@@ -0,0 +1,718 @@
1
+ // src/components/search/features/search.ts
2
+ import { SEARCH_EVENTS, SEARCH_VARIANTS } from '../constants';
3
+ import { SearchConfig } from '../types';
4
+
5
+ /**
6
+ * Add main search functionality to component
7
+ * @param config Search configuration
8
+ * @returns Component enhancer with search functionality
9
+ */
10
+ export const withSearch = (config: SearchConfig) => component => {
11
+ // Ensure component has events capability
12
+ if (!component.emit) {
13
+ console.warn('Search component requires event emission capability');
14
+ }
15
+
16
+ // Ensure component structure exists
17
+ if (!component.structure) {
18
+ console.error('Search component missing structure');
19
+ return component;
20
+ }
21
+
22
+ // Get elements from structure
23
+ const {
24
+ container,
25
+ input,
26
+ inputWrapper,
27
+ leadingIcon,
28
+ clearButton,
29
+ trailingIcon,
30
+ trailingIcon2,
31
+ avatar,
32
+ divider,
33
+ suggestionsContainer
34
+ } = component.structure;
35
+
36
+ // Create state object
37
+ const state = {
38
+ value: config.value || '',
39
+ placeholder: config.placeholder || 'Search',
40
+ suggestions: config.suggestions || [],
41
+ isFocused: false,
42
+ isExpanded: config.variant === SEARCH_VARIANTS.VIEW,
43
+ component
44
+ };
45
+
46
+ // Create event helpers
47
+ const eventHelpers = {
48
+ triggerEvent(eventName, originalEvent = null) {
49
+ const eventData = {
50
+ search: state.component,
51
+ value: state.value,
52
+ originalEvent,
53
+ preventDefault: () => { eventData.defaultPrevented = true; },
54
+ defaultPrevented: false
55
+ };
56
+
57
+ if (component.emit) {
58
+ component.emit(eventName, eventData);
59
+ }
60
+
61
+ // Call onEvent handlers from config if they exist
62
+ const handlerName = `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`;
63
+ if (config[handlerName] && typeof config[handlerName] === 'function') {
64
+ config[handlerName](state.value);
65
+ }
66
+
67
+ return eventData;
68
+ }
69
+ };
70
+
71
+ /**
72
+ * Updates the value and UI
73
+ */
74
+ const updateValue = (newValue, triggerEvent = true) => {
75
+ // Update internal state
76
+ state.value = newValue;
77
+
78
+ // Update input value
79
+ if (input) {
80
+ input.value = newValue;
81
+ }
82
+
83
+ // Show/hide clear button
84
+ if (clearButton) {
85
+ if (newValue) {
86
+ clearButton.classList.remove(`${component.getClass('search-clear-button')}--hidden`);
87
+ if (!component.disabled?.isDisabled()) {
88
+ clearButton.tabIndex = 0;
89
+ }
90
+ } else {
91
+ clearButton.classList.add(`${component.getClass('search-clear-button')}--hidden`);
92
+ clearButton.tabIndex = -1;
93
+ }
94
+ }
95
+
96
+ // Trigger input event
97
+ if (triggerEvent) {
98
+ eventHelpers.triggerEvent(SEARCH_EVENTS.INPUT);
99
+ }
100
+ };
101
+
102
+ /**
103
+ * Submits the current search value
104
+ */
105
+ const submitSearch = () => {
106
+ if (state.value) {
107
+ eventHelpers.triggerEvent(SEARCH_EVENTS.SUBMIT);
108
+
109
+ // Hide suggestions if in bar mode
110
+ if (!state.isExpanded) {
111
+ hideSuggestions();
112
+ }
113
+ }
114
+ };
115
+
116
+ /**
117
+ * Clears the search input
118
+ */
119
+ const clearSearch = (triggerEvent = true) => {
120
+ updateValue('', triggerEvent);
121
+
122
+ if (input) {
123
+ input.focus();
124
+ }
125
+
126
+ if (triggerEvent) {
127
+ eventHelpers.triggerEvent(SEARCH_EVENTS.CLEAR);
128
+ }
129
+ };
130
+
131
+ /**
132
+ * Shows suggestions
133
+ */
134
+ const showSuggestions = () => {
135
+ if (suggestionsContainer) {
136
+ renderSuggestions();
137
+ }
138
+ };
139
+
140
+ /**
141
+ * Hides suggestions
142
+ */
143
+ const hideSuggestions = () => {
144
+ if (suggestionsContainer) {
145
+ suggestionsContainer.classList.remove(`${component.getClass('search-suggestions-container')}--visible`);
146
+ }
147
+ };
148
+
149
+ /**
150
+ * Expands search bar to view mode
151
+ */
152
+ const expandToView = () => {
153
+ if (state.isExpanded) return;
154
+
155
+ state.isExpanded = true;
156
+ component.element.classList.add(`${component.getClass('search')}--expanded`);
157
+
158
+ // Apply view variant class if not already present
159
+ if (!component.element.classList.contains(`${component.getClass('search')}--view`)) {
160
+ component.element.classList.remove(`${component.getClass('search')}--bar`);
161
+ component.element.classList.add(`${component.getClass('search')}--view`);
162
+ }
163
+
164
+ // Show suggestions
165
+ showSuggestions();
166
+
167
+ // Focus input
168
+ if (input) {
169
+ setTimeout(() => {
170
+ input.focus();
171
+ }, 50);
172
+ }
173
+ };
174
+
175
+ /**
176
+ * Collapses view mode to search bar
177
+ */
178
+ const collapseToBar = () => {
179
+ if (!state.isExpanded) return;
180
+
181
+ state.isExpanded = false;
182
+ component.element.classList.remove(`${component.getClass('search')}--expanded`);
183
+
184
+ // Apply bar variant class if not already present
185
+ if (!component.element.classList.contains(`${component.getClass('search')}--bar`)) {
186
+ component.element.classList.remove(`${component.getClass('search')}--view`);
187
+ component.element.classList.add(`${component.getClass('search')}--bar`);
188
+ }
189
+
190
+ // Hide suggestions
191
+ hideSuggestions();
192
+
193
+ // Blur input
194
+ if (input) {
195
+ input.blur();
196
+ }
197
+ };
198
+
199
+ /**
200
+ * Renders suggestions in the suggestions container
201
+ */
202
+ const renderSuggestions = () => {
203
+ if (!suggestionsContainer || !state.suggestions || !state.suggestions.length) {
204
+ return;
205
+ }
206
+
207
+ // Clear previous suggestions
208
+ suggestionsContainer.innerHTML = '';
209
+
210
+ // Create a list for suggestions
211
+ const list = document.createElement('ul');
212
+ list.className = component.getClass('search-suggestions-list');
213
+ list.setAttribute('role', 'listbox');
214
+
215
+ // Add suggestions
216
+ state.suggestions.forEach((suggestion, index) => {
217
+ const item = document.createElement('li');
218
+ item.className = component.getClass('search-suggestion-item');
219
+ item.setAttribute('role', 'option');
220
+ item.tabIndex = 0;
221
+
222
+ // Determine if suggestion is a string or object
223
+ if (typeof suggestion === 'string') {
224
+ item.textContent = suggestion;
225
+ // Highlight matched text if current input is a substring
226
+ if (state.value && suggestion.toLowerCase().includes(state.value.toLowerCase())) {
227
+ const matchedIndex = suggestion.toLowerCase().indexOf(state.value.toLowerCase());
228
+ const beforeMatch = suggestion.slice(0, matchedIndex);
229
+ const match = suggestion.slice(matchedIndex, matchedIndex + state.value.length);
230
+ const afterMatch = suggestion.slice(matchedIndex + state.value.length);
231
+
232
+ item.innerHTML = `${beforeMatch}<strong>${match}</strong>${afterMatch}`;
233
+ }
234
+ } else {
235
+ // Object with text, value, and optional icon
236
+ if (suggestion.icon) {
237
+ const iconElement = document.createElement('span');
238
+ iconElement.className = component.getClass('search-suggestion-icon');
239
+ iconElement.innerHTML = suggestion.icon;
240
+ item.appendChild(iconElement);
241
+ }
242
+
243
+ const textElement = document.createElement('span');
244
+ textElement.className = component.getClass('search-suggestion-text');
245
+ textElement.textContent = suggestion.text;
246
+
247
+ // Highlight matched text if current input is a substring
248
+ if (state.value && suggestion.text.toLowerCase().includes(state.value.toLowerCase())) {
249
+ const matchedIndex = suggestion.text.toLowerCase().indexOf(state.value.toLowerCase());
250
+ const beforeMatch = suggestion.text.slice(0, matchedIndex);
251
+ const match = suggestion.text.slice(matchedIndex, matchedIndex + state.value.length);
252
+ const afterMatch = suggestion.text.slice(matchedIndex + state.value.length);
253
+
254
+ textElement.innerHTML = `${beforeMatch}<strong>${match}</strong>${afterMatch}`;
255
+ }
256
+
257
+ item.appendChild(textElement);
258
+
259
+ // Store value as data attribute
260
+ if (suggestion.value) {
261
+ item.dataset.value = suggestion.value;
262
+ }
263
+ }
264
+
265
+ // Add click handler
266
+ item.addEventListener('click', (e) => {
267
+ e.preventDefault();
268
+ e.stopPropagation();
269
+ const selectedValue = item.dataset.value || (typeof suggestion === 'string' ? suggestion : suggestion.text);
270
+ updateValue(selectedValue);
271
+ submitSearch();
272
+ });
273
+
274
+ list.appendChild(item);
275
+ });
276
+
277
+ // Add divider if configured and not already present
278
+ if (config.showDividers && divider && divider.parentElement !== suggestionsContainer) {
279
+ const dividerClone = divider.cloneNode(true);
280
+ suggestionsContainer.appendChild(dividerClone);
281
+ }
282
+
283
+ suggestionsContainer.appendChild(list);
284
+
285
+ // Show suggestions container
286
+ suggestionsContainer.classList.add(`${component.getClass('search-suggestions-container')}--visible`);
287
+ };
288
+
289
+ /**
290
+ * Sets up all event listeners
291
+ */
292
+ const setupEventListeners = () => {
293
+ // Input events
294
+ if (input) {
295
+ // Input value change
296
+ input.addEventListener('input', (e) => {
297
+ updateValue(e.target.value);
298
+
299
+ // Show suggestions if expanded
300
+ if (state.isExpanded) {
301
+ showSuggestions();
302
+ }
303
+ });
304
+
305
+ // Focus event
306
+ input.addEventListener('focus', (e) => {
307
+ state.isFocused = true;
308
+ component.element.classList.add(`${component.getClass('search')}--focused`);
309
+
310
+ // Expand search bar to view if in bar mode
311
+ if (!state.isExpanded && config.variant === SEARCH_VARIANTS.BAR) {
312
+ expandToView();
313
+ }
314
+
315
+ eventHelpers.triggerEvent(SEARCH_EVENTS.FOCUS, e);
316
+ });
317
+
318
+ // Blur event
319
+ input.addEventListener('blur', (e) => {
320
+ // Don't blur if clicking inside the search component
321
+ if (component.element.contains(e.relatedTarget)) {
322
+ return;
323
+ }
324
+
325
+ state.isFocused = false;
326
+ component.element.classList.remove(`${component.getClass('search')}--focused`);
327
+
328
+ // Hide suggestions with slight delay to allow for clicks
329
+ setTimeout(() => {
330
+ if (!state.isFocused) {
331
+ hideSuggestions();
332
+
333
+ // Collapse to bar mode if in expanded state and originally a bar
334
+ if (state.isExpanded && config.variant === SEARCH_VARIANTS.BAR) {
335
+ collapseToBar();
336
+ }
337
+ }
338
+ }, 200);
339
+
340
+ eventHelpers.triggerEvent(SEARCH_EVENTS.BLUR, e);
341
+ });
342
+
343
+ // Enter key for submit
344
+ input.addEventListener('keydown', (e) => {
345
+ if (e.key === 'Enter') {
346
+ e.preventDefault();
347
+ submitSearch();
348
+ } else if (e.key === 'Escape') {
349
+ e.preventDefault();
350
+
351
+ // Clear if there's a value, otherwise collapse
352
+ if (state.value) {
353
+ clearSearch();
354
+ } else if (state.isExpanded && config.variant === SEARCH_VARIANTS.BAR) {
355
+ collapseToBar();
356
+ }
357
+ }
358
+ });
359
+ }
360
+
361
+ // Leading icon click
362
+ if (leadingIcon) {
363
+ leadingIcon.addEventListener('click', (e) => {
364
+ e.preventDefault();
365
+
366
+ // If disabled, do nothing
367
+ if (component.disabled?.isDisabled()) return;
368
+
369
+ // Toggle between expanded and collapsed
370
+ if (state.isExpanded) {
371
+ collapseToBar();
372
+ } else {
373
+ expandToView();
374
+ }
375
+
376
+ eventHelpers.triggerEvent(SEARCH_EVENTS.ICON_CLICK, e);
377
+ });
378
+
379
+ // Keyboard access
380
+ leadingIcon.addEventListener('keydown', (e) => {
381
+ if (e.key === 'Enter' || e.key === ' ') {
382
+ e.preventDefault();
383
+ leadingIcon.click();
384
+ }
385
+ });
386
+ }
387
+
388
+ // Clear button click
389
+ if (clearButton) {
390
+ clearButton.addEventListener('click', (e) => {
391
+ e.preventDefault();
392
+
393
+ // If disabled, do nothing
394
+ if (component.disabled?.isDisabled()) return;
395
+
396
+ clearSearch();
397
+ });
398
+
399
+ // Keyboard access
400
+ clearButton.addEventListener('keydown', (e) => {
401
+ if (e.key === 'Enter' || e.key === ' ') {
402
+ e.preventDefault();
403
+ clearButton.click();
404
+ }
405
+ });
406
+ }
407
+
408
+ // Trailing icon click
409
+ if (trailingIcon) {
410
+ trailingIcon.addEventListener('click', (e) => {
411
+ e.preventDefault();
412
+
413
+ // If disabled, do nothing
414
+ if (component.disabled?.isDisabled()) return;
415
+
416
+ eventHelpers.triggerEvent(SEARCH_EVENTS.ICON_CLICK, e);
417
+ });
418
+
419
+ // Keyboard access
420
+ trailingIcon.addEventListener('keydown', (e) => {
421
+ if (e.key === 'Enter' || e.key === ' ') {
422
+ e.preventDefault();
423
+ trailingIcon.click();
424
+ }
425
+ });
426
+ }
427
+
428
+ // Second trailing icon click
429
+ if (trailingIcon2) {
430
+ trailingIcon2.addEventListener('click', (e) => {
431
+ e.preventDefault();
432
+
433
+ // If disabled, do nothing
434
+ if (component.disabled?.isDisabled()) return;
435
+
436
+ eventHelpers.triggerEvent(SEARCH_EVENTS.ICON_CLICK, e);
437
+ });
438
+
439
+ // Keyboard access
440
+ trailingIcon2.addEventListener('keydown', (e) => {
441
+ if (e.key === 'Enter' || e.key === ' ') {
442
+ e.preventDefault();
443
+ trailingIcon2.click();
444
+ }
445
+ });
446
+ }
447
+
448
+ // Handle clicks outside to close suggestions
449
+ document.addEventListener('click', (e) => {
450
+ if (!component.element.contains(e.target) && state.isExpanded) {
451
+ hideSuggestions();
452
+
453
+ // Collapse to bar mode if in expanded state and originally a bar
454
+ if (state.isExpanded && config.variant === SEARCH_VARIANTS.BAR) {
455
+ collapseToBar();
456
+ }
457
+ }
458
+ });
459
+ };
460
+
461
+ /**
462
+ * Clean up all event listeners
463
+ */
464
+ const cleanupEventListeners = () => {
465
+ // Nothing to do if component is already destroyed
466
+ if (!component.element) return;
467
+
468
+ // Document click listener cleanup
469
+ document.removeEventListener('click', (e) => {
470
+ if (!component.element.contains(e.target) && state.isExpanded) {
471
+ hideSuggestions();
472
+
473
+ // Collapse to bar mode if in expanded state and originally a bar
474
+ if (state.isExpanded && config.variant === SEARCH_VARIANTS.BAR) {
475
+ collapseToBar();
476
+ }
477
+ }
478
+ });
479
+ };
480
+
481
+ // Initialize search component
482
+ const initSearch = () => {
483
+ // Set initial value if provided
484
+ if (config.value && input) {
485
+ input.value = config.value;
486
+
487
+ // Show clear button if value exists
488
+ if (clearButton && config.showClearButton !== false) {
489
+ clearButton.classList.remove(`${component.getClass('search-clear-button')}--hidden`);
490
+ }
491
+ }
492
+
493
+ // Set ARIA attributes
494
+ if (input) {
495
+ input.setAttribute('role', 'searchbox');
496
+ input.setAttribute('aria-label', state.placeholder || 'Search');
497
+ }
498
+
499
+ // Setup event listeners
500
+ setupEventListeners();
501
+
502
+ // If in view mode, show suggestions
503
+ if (state.isExpanded && config.suggestions && config.suggestions.length > 0) {
504
+ showSuggestions();
505
+ }
506
+ };
507
+
508
+ // Register with lifecycle if available
509
+ if (component.lifecycle) {
510
+ const originalDestroy = component.lifecycle.destroy || (() => {});
511
+ component.lifecycle.destroy = () => {
512
+ cleanupEventListeners();
513
+ originalDestroy();
514
+ };
515
+ }
516
+
517
+ // Initialize search
518
+ initSearch();
519
+
520
+ // Return enhanced component
521
+ return {
522
+ ...component,
523
+ search: {
524
+ /**
525
+ * Sets search value
526
+ * @param value New value
527
+ * @param triggerEvent Whether to trigger change event
528
+ * @returns Search controller for chaining
529
+ */
530
+ setValue(value, triggerEvent = true) {
531
+ updateValue(value, triggerEvent);
532
+ return this;
533
+ },
534
+
535
+ /**
536
+ * Gets search value
537
+ * @returns Current value
538
+ */
539
+ getValue() {
540
+ return state.value;
541
+ },
542
+
543
+ /**
544
+ * Sets placeholder text
545
+ * @param text New placeholder text
546
+ * @returns Search controller for chaining
547
+ */
548
+ setPlaceholder(text) {
549
+ state.placeholder = text;
550
+ if (input) {
551
+ input.placeholder = text;
552
+ input.setAttribute('aria-label', text);
553
+ }
554
+ return this;
555
+ },
556
+
557
+ /**
558
+ * Gets placeholder text
559
+ * @returns Current placeholder
560
+ */
561
+ getPlaceholder() {
562
+ return state.placeholder;
563
+ },
564
+
565
+ /**
566
+ * Focuses the search input
567
+ * @returns Search controller for chaining
568
+ */
569
+ focus() {
570
+ if (input && !component.disabled?.isDisabled()) {
571
+ input.focus();
572
+ }
573
+ return this;
574
+ },
575
+
576
+ /**
577
+ * Blurs the search input
578
+ * @returns Search controller for chaining
579
+ */
580
+ blur() {
581
+ if (input) {
582
+ input.blur();
583
+ }
584
+ return this;
585
+ },
586
+
587
+ /**
588
+ * Expands the search bar into view mode
589
+ * @returns Search controller for chaining
590
+ */
591
+ expand() {
592
+ expandToView();
593
+ return this;
594
+ },
595
+
596
+ /**
597
+ * Collapses the search view back to bar mode
598
+ * @returns Search controller for chaining
599
+ */
600
+ collapse() {
601
+ collapseToBar();
602
+ return this;
603
+ },
604
+
605
+ /**
606
+ * Clears the search input
607
+ * @returns Search controller for chaining
608
+ */
609
+ clear() {
610
+ clearSearch();
611
+ return this;
612
+ },
613
+
614
+ /**
615
+ * Submits the search
616
+ * @returns Search controller for chaining
617
+ */
618
+ submit() {
619
+ submitSearch();
620
+ return this;
621
+ },
622
+
623
+ /**
624
+ * Sets suggestions
625
+ * @param suggestions Array of suggestions
626
+ * @returns Search controller for chaining
627
+ */
628
+ setSuggestions(suggestions) {
629
+ state.suggestions = suggestions;
630
+ if (state.isExpanded) {
631
+ renderSuggestions();
632
+ }
633
+ return this;
634
+ },
635
+
636
+ /**
637
+ * Shows or hides suggestions
638
+ * @param show Whether to show suggestions
639
+ * @returns Search controller for chaining
640
+ */
641
+ showSuggestions(show) {
642
+ if (show) {
643
+ showSuggestions();
644
+ } else {
645
+ hideSuggestions();
646
+ }
647
+ return this;
648
+ }
649
+ },
650
+
651
+ // Icon management - separate from appearance to make API cleaner
652
+ icons: {
653
+ /**
654
+ * Sets leading icon
655
+ * @param iconHtml HTML content for icon
656
+ * @returns Icon manager for chaining
657
+ */
658
+ setLeadingIcon(iconHtml) {
659
+ if (leadingIcon) {
660
+ leadingIcon.innerHTML = iconHtml || '';
661
+ }
662
+ return this;
663
+ },
664
+
665
+ /**
666
+ * Sets trailing icon
667
+ * @param iconHtml HTML content for icon
668
+ * @returns Icon manager for chaining
669
+ */
670
+ setTrailingIcon(iconHtml) {
671
+ if (trailingIcon) {
672
+ trailingIcon.innerHTML = iconHtml || '';
673
+ }
674
+ return this;
675
+ },
676
+
677
+ /**
678
+ * Sets second trailing icon
679
+ * @param iconHtml HTML content for icon
680
+ * @returns Icon manager for chaining
681
+ */
682
+ setTrailingIcon2(iconHtml) {
683
+ if (trailingIcon2) {
684
+ trailingIcon2.innerHTML = iconHtml || '';
685
+ }
686
+ return this;
687
+ },
688
+
689
+ /**
690
+ * Sets avatar
691
+ * @param avatarHtml HTML content for avatar
692
+ * @returns Icon manager for chaining
693
+ */
694
+ setAvatar(avatarHtml) {
695
+ if (avatar) {
696
+ avatar.innerHTML = avatarHtml || '';
697
+ }
698
+ return this;
699
+ },
700
+
701
+ /**
702
+ * Shows or hides clear button
703
+ * @param show Whether to show clear button
704
+ * @returns Icon manager for chaining
705
+ */
706
+ showClearButton(show) {
707
+ if (clearButton) {
708
+ if (show) {
709
+ clearButton.classList.remove(`${component.getClass('search-clear-button')}--hidden`);
710
+ } else {
711
+ clearButton.classList.add(`${component.getClass('search-clear-button')}--hidden`);
712
+ }
713
+ }
714
+ return this;
715
+ }
716
+ }
717
+ };
718
+ };