@kupola/kupola 1.2.0 → 1.4.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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +330 -286
  3. package/adapters/axios.d.ts +34 -0
  4. package/adapters/axios.js +122 -0
  5. package/adapters/navios-http.d.ts +110 -0
  6. package/adapters/navios-http.js +151 -0
  7. package/css/components-ext.css +157 -0
  8. package/css/components.css +69 -0
  9. package/css/theme-dark.css +5 -5
  10. package/css/utilities.css +119 -1
  11. package/dist/css/accessibility.css +119 -0
  12. package/dist/css/animations.css +224 -0
  13. package/dist/css/brand-themes.css +300 -0
  14. package/dist/css/colors_and_type.css +441 -0
  15. package/dist/css/components-ext.css +4165 -0
  16. package/dist/css/components.css +1483 -0
  17. package/dist/css/kupola.css +1 -12
  18. package/dist/css/kupola.min.css +1 -0
  19. package/dist/css/responsive.css +697 -0
  20. package/dist/css/scaffold.css +145 -0
  21. package/dist/css/states.css +316 -0
  22. package/dist/css/theme-dark.css +296 -0
  23. package/dist/css/theme-light.css +296 -0
  24. package/dist/css/utilities.css +171 -0
  25. package/dist/icons.svg +284 -0
  26. package/dist/kupola.cjs.js +17409 -257
  27. package/dist/kupola.cjs.js.map +1 -1
  28. package/dist/kupola.css +9000 -0
  29. package/dist/kupola.esm.js +17183 -7597
  30. package/dist/kupola.esm.js.map +1 -1
  31. package/dist/kupola.min.css +1 -0
  32. package/dist/kupola.min.js +2 -0
  33. package/dist/kupola.min.js.map +1 -0
  34. package/dist/kupola.umd.js +17415 -257
  35. package/dist/kupola.umd.js.map +1 -1
  36. package/dist/plugins/vite-plugin-kupola.js +120 -0
  37. package/dist/types/kupola.d.ts +421 -323
  38. package/js/calendar.js +334 -25
  39. package/js/carousel.js +182 -48
  40. package/js/collapse.js +148 -34
  41. package/js/color-picker.js +416 -108
  42. package/js/component.js +8 -19
  43. package/js/countdown.js +9 -8
  44. package/js/data-bind.js +73 -16
  45. package/js/datepicker.js +488 -110
  46. package/js/depends.js +710 -0
  47. package/js/dialog.js +4 -2
  48. package/js/drawer.js +172 -8
  49. package/js/dropdown.js +272 -17
  50. package/js/dynamic-tags.js +156 -40
  51. package/js/fileupload.js +9 -8
  52. package/js/form.js +280 -254
  53. package/js/global-events.js +281 -188
  54. package/js/heatmap.js +10 -7
  55. package/js/i18n.js +18 -10
  56. package/js/icons.js +141 -161
  57. package/js/image-preview.js +146 -2
  58. package/js/initializer.js +151 -71
  59. package/js/kupola-core.js +123 -45
  60. package/js/kupola-lifecycle.js +13 -11
  61. package/js/message.js +8 -1
  62. package/js/modal.js +207 -59
  63. package/js/notification.js +8 -1
  64. package/js/numberinput.js +9 -8
  65. package/js/pagination.js +263 -0
  66. package/js/registry.js +29 -12
  67. package/js/select.js +482 -27
  68. package/js/slide-captcha.js +11 -2
  69. package/js/slider.js +442 -25
  70. package/js/statcard.js +9 -7
  71. package/js/table.js +1210 -0
  72. package/js/tag.js +268 -14
  73. package/js/theme-standalone.js +88 -0
  74. package/js/theme.js +14 -43
  75. package/js/timepicker.js +335 -66
  76. package/js/tooltip.js +317 -86
  77. package/js/utils.js +6 -2
  78. package/js/validation.js +6 -2
  79. package/js/virtual-list.js +11 -7
  80. package/js/web-components.js +288 -0
  81. package/package.json +81 -67
  82. package/plugins/vite-plugin-kupola.js +120 -0
  83. package/scripts/build-css.cjs +113 -0
  84. package/scripts/build-svg-sprite.cjs +107 -0
  85. package/types/kupola.d.ts +421 -323
  86. package/CHANGELOG.md +0 -130
  87. package/INTEGRATION.md +0 -440
  88. package/PROJECT_SUMMARY.md +0 -312
  89. package/SKILL.md +0 -572
  90. package/dist/utils/utils/Kupola.cs +0 -77
  91. package/dist/utils/utils/Kupola.java +0 -104
  92. package/dist/utils/utils/kupola.go +0 -120
  93. package/dist/utils/utils/kupola.js +0 -63
  94. package/dist/utils/utils/kupola.py +0 -1392
  95. package/dist/utils/utils/kupola.rb +0 -69
  96. package/js/composition-api.js +0 -458
  97. package/js/error-handler.js +0 -181
  98. package/js/http.js +0 -419
  99. package/js/kupola-devtools.js +0 -598
  100. package/js/performance.js +0 -250
  101. package/js/router.js +0 -396
  102. package/js/security.js +0 -189
  103. package/js/test-utils.js +0 -251
  104. package/templates/base.html +0 -30
  105. package/templates/base_dashboard.html +0 -99
  106. package/utils/Kupola.cs +0 -77
  107. package/utils/Kupola.java +0 -104
  108. package/utils/kupola.go +0 -120
  109. package/utils/kupola.js +0 -63
  110. package/utils/kupola.py +0 -1392
  111. package/utils/kupola.rb +0 -69
package/js/dialog.js CHANGED
@@ -126,6 +126,8 @@ class Dialog {
126
126
  }
127
127
  }
128
128
 
129
- if (typeof module !== 'undefined' && module.exports) {
130
- module.exports = Dialog;
129
+ export { Dialog };
130
+
131
+ if (typeof window !== 'undefined') {
132
+ window.Dialog = Dialog;
131
133
  }
package/js/drawer.js CHANGED
@@ -1,8 +1,21 @@
1
1
  class Drawer {
2
- constructor(element) {
2
+ constructor(element, options = {}) {
3
3
  this.element = element;
4
4
  this.mask = element.querySelector('.ds-drawer-mask');
5
5
  this.drawerEl = element.querySelector('.ds-drawer');
6
+
7
+ // Options
8
+ this.placement = options.placement || element.getAttribute('data-drawer-placement') || 'right'; // right | left | top | bottom
9
+ this.width = options.width || element.getAttribute('data-drawer-width') || '400px';
10
+ this.height = options.height || element.getAttribute('data-drawer-height') || '400px';
11
+ this.escClose = options.escClose !== false;
12
+ this.maskClosable = options.maskClosable !== false;
13
+ this.showMask = options.showMask !== false;
14
+ this.onOpen = options.onOpen || null;
15
+ this.onClose = options.onClose || null;
16
+ this.onBeforeClose = options.onBeforeClose || null;
17
+
18
+ this._keydownHandler = null;
6
19
  this._bindEvents();
7
20
  }
8
21
 
@@ -12,16 +25,25 @@ class Drawer {
12
25
  const confirmBtn = this.mask?.querySelector('.ds-drawer__footer .ds-btn--brand');
13
26
 
14
27
  this.closeDrawer = () => {
28
+ if (this.onBeforeClose) {
29
+ const result = this.onBeforeClose();
30
+ if (result === false) return;
31
+ }
32
+
15
33
  if (this.mask) {
16
34
  this.mask.classList.remove('is-visible');
17
35
  }
18
36
  if (this.drawerEl) {
19
37
  this.drawerEl.classList.remove('is-visible');
20
38
  }
39
+ document.body.style.overflow = '';
40
+
41
+ if (this.onClose) this.onClose();
42
+ this.element.dispatchEvent(new CustomEvent('kupola:drawer-close', { bubbles: true }));
21
43
  };
22
44
 
23
45
  this.handleMaskClick = (e) => {
24
- if (e.target === this.mask) {
46
+ if (this.maskClosable && e.target === this.mask) {
25
47
  this.closeDrawer();
26
48
  }
27
49
  };
@@ -42,6 +64,16 @@ class Drawer {
42
64
  confirmBtn.addEventListener('click', this.closeDrawer);
43
65
  }
44
66
 
67
+ // ESC key
68
+ if (this.escClose) {
69
+ this._keydownHandler = (e) => {
70
+ if (e.key === 'Escape' && this.drawerEl?.classList.contains('is-visible')) {
71
+ this.closeDrawer();
72
+ }
73
+ };
74
+ document.addEventListener('keydown', this._keydownHandler);
75
+ }
76
+
45
77
  this._listeners = [
46
78
  { el: this.mask, event: 'click', handler: this.handleMaskClick },
47
79
  { el: closeBtn, event: 'click', handler: this.closeDrawer },
@@ -50,10 +82,35 @@ class Drawer {
50
82
  ].filter(item => item.el);
51
83
  }
52
84
 
85
+ _applyPlacement() {
86
+ if (!this.drawerEl) return;
87
+
88
+ // Set direction class
89
+ this.drawerEl.classList.remove('ds-drawer--right', 'ds-drawer--left', 'ds-drawer--top', 'ds-drawer--bottom');
90
+ this.drawerEl.classList.add(`ds-drawer--${this.placement}`);
91
+
92
+ // Set size
93
+ if (this.placement === 'left' || this.placement === 'right') {
94
+ this.drawerEl.style.width = this.width;
95
+ } else {
96
+ this.drawerEl.style.height = this.height;
97
+ }
98
+
99
+ // Hide mask if configured
100
+ if (!this.showMask && this.mask) {
101
+ this.mask.style.background = 'transparent';
102
+ this.mask.style.pointerEvents = 'none';
103
+ this.drawerEl.style.boxShadow = '0 0 24px rgba(0,0,0,0.15)';
104
+ }
105
+ }
106
+
53
107
  destroy() {
54
108
  this._listeners?.forEach(({ el, event, handler }) => {
55
109
  el.removeEventListener(event, handler);
56
110
  });
111
+ if (this._keydownHandler) {
112
+ document.removeEventListener('keydown', this._keydownHandler);
113
+ }
57
114
  this._listeners = null;
58
115
  this.mask = null;
59
116
  this.drawerEl = null;
@@ -61,23 +118,116 @@ class Drawer {
61
118
  }
62
119
 
63
120
  open() {
121
+ this._applyPlacement();
122
+
64
123
  if (this.mask) {
65
124
  this.mask.classList.add('is-visible');
66
125
  }
67
126
  if (this.drawerEl) {
68
127
  this.drawerEl.classList.add('is-visible');
69
128
  }
129
+ document.body.style.overflow = 'hidden';
130
+
131
+ if (this.onOpen) this.onOpen();
132
+ this.element.dispatchEvent(new CustomEvent('kupola:drawer-open', { bubbles: true }));
70
133
  }
71
134
 
72
135
  close() {
73
136
  this.closeDrawer();
74
137
  }
138
+
139
+ isOpen() {
140
+ return this.drawerEl?.classList.contains('is-visible') || false;
141
+ }
75
142
  }
76
143
 
77
- function initDrawer(element) {
144
+ function createDrawer(options = {}) {
145
+ const {
146
+ title = '',
147
+ content = '',
148
+ html = false,
149
+ placement = 'right',
150
+ width = '400px',
151
+ height = '400px',
152
+ closable = true,
153
+ showMask = true,
154
+ footer = null,
155
+ onClose,
156
+ onOpen
157
+ } = options;
158
+
159
+ const container = document.createElement('div');
160
+ container.className = 'ds-drawer-container';
161
+
162
+ let footerHTML = '';
163
+ if (footer !== null) {
164
+ if (typeof footer === 'string') {
165
+ footerHTML = `<div class="ds-drawer__footer">${footer}</div>`;
166
+ } else {
167
+ footerHTML = `<div class="ds-drawer__footer">
168
+ <button class="ds-btn ds-btn--ghost" data-drawer-cancel>Cancel</button>
169
+ <button class="ds-btn ds-btn--brand" data-drawer-confirm>OK</button>
170
+ </div>`;
171
+ }
172
+ }
173
+
174
+ container.innerHTML = `
175
+ <div class="ds-drawer-mask">
176
+ <div class="ds-drawer ds-drawer--${placement}" style="${placement === 'left' || placement === 'right' ? 'width:' + width : 'height:' + height}">
177
+ <div class="ds-drawer__header">
178
+ <span class="ds-drawer__title"></span>
179
+ ${closable ? `<button class="ds-drawer__close" aria-label="Close">
180
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
181
+ <path d="M18 6L6 18M6 6l12 12"/>
182
+ </svg>
183
+ </button>` : ''}
184
+ </div>
185
+ <div class="ds-drawer__body"></div>
186
+ ${footerHTML}
187
+ </div>
188
+ </div>
189
+ `;
190
+
191
+ document.body.appendChild(container);
192
+
193
+ const drawer = new Drawer(container, { placement, width, height, showMask, onClose, onOpen });
194
+
195
+ const titleEl = container.querySelector('.ds-drawer__title');
196
+ if (titleEl) titleEl.textContent = title;
197
+ const bodyEl = container.querySelector('.ds-drawer__body');
198
+ if (bodyEl) {
199
+ if (html) bodyEl.innerHTML = content;
200
+ else bodyEl.textContent = content;
201
+ }
202
+
203
+ const confirmBtn = container.querySelector('[data-drawer-confirm]');
204
+ const cancelBtn = container.querySelector('[data-drawer-cancel]');
205
+
206
+ if (cancelBtn) cancelBtn.addEventListener('click', () => drawer.close());
207
+ if (confirmBtn) {
208
+ confirmBtn.addEventListener('click', () => {
209
+ if (options.onConfirm) options.onConfirm();
210
+ drawer.close();
211
+ });
212
+ }
213
+
214
+ const origClose = drawer.closeDrawer;
215
+ drawer.closeDrawer = () => {
216
+ origClose();
217
+ setTimeout(() => {
218
+ drawer.destroy();
219
+ container.remove();
220
+ }, 300);
221
+ };
222
+
223
+ drawer.open();
224
+ return drawer;
225
+ }
226
+
227
+ function initDrawer(element, options) {
78
228
  if (element.__kupolaInitialized) return;
79
229
 
80
- const drawer = new Drawer(element);
230
+ const drawer = new Drawer(element, options);
81
231
  element.__kupolaInstance = drawer;
82
232
  element.__kupolaInitialized = true;
83
233
  }
@@ -100,7 +250,11 @@ function initDrawers() {
100
250
 
101
251
  if (!drawer) return;
102
252
 
103
- initDrawer(drawer);
253
+ initDrawer(drawer, {
254
+ placement: trigger.getAttribute('data-drawer-placement') || 'right',
255
+ width: trigger.getAttribute('data-drawer-width'),
256
+ height: trigger.getAttribute('data-drawer-height')
257
+ });
104
258
  drawer.__kupolaInstance?.open();
105
259
  });
106
260
  });
@@ -113,6 +267,16 @@ function initDrawers() {
113
267
  });
114
268
  }
115
269
 
116
- if (window.kupolaInitializer) {
117
- window.kupolaInitializer.register('drawer', initDrawer, cleanupDrawer);
118
- }
270
+ export { Drawer, initDrawer, initDrawers, cleanupDrawer, createDrawer };
271
+
272
+ if (typeof window !== 'undefined') {
273
+ window.Drawer = Drawer;
274
+ window.initDrawer = initDrawer;
275
+ window.initDrawers = initDrawers;
276
+ window.cleanupDrawer = cleanupDrawer;
277
+ window.createDrawer = createDrawer;
278
+
279
+ if (window.kupolaInitializer) {
280
+ window.kupolaInitializer.register('drawer', initDrawer, cleanupDrawer);
281
+ }
282
+ }
package/js/dropdown.js CHANGED
@@ -1,41 +1,155 @@
1
1
  class Dropdown {
2
- constructor(element) {
2
+ constructor(element, options = {}) {
3
3
  this.element = element;
4
4
  this.trigger = element.querySelector('.ds-dropdown__trigger');
5
5
  this.menu = element.querySelector('.ds-dropdown__menu');
6
6
  this.triggerText = this.trigger ? this.trigger.querySelector('span') : null;
7
7
  this.scope = `dropdown-${Math.random().toString(36).substr(2, 9)}`;
8
8
 
9
+ // Options
10
+ this.triggerMode = options.trigger || element.getAttribute('data-dropdown-trigger') || 'click'; // click | hover
11
+ this.hoverDelay = options.hoverDelay || parseInt(element.getAttribute('data-dropdown-hover-delay')) || 150;
12
+ this.disabled = options.disabled || element.hasAttribute('data-dropdown-disabled');
13
+ this.keyboardNav = options.keyboardNav !== false;
14
+ this.autoPosition = options.autoPosition !== false;
15
+ this.onSelect = options.onSelect || null;
16
+ this.onShow = options.onShow || null;
17
+ this.onHide = options.onHide || null;
18
+
19
+ this.isOpen = false;
20
+ this.focusIndex = -1;
21
+ this._hoverTimer = null;
22
+ this._hoverLeaveTimer = null;
23
+
9
24
  this._triggerClickHandler = null;
10
25
  this._documentClickHandler = null;
11
26
  this._documentClickListener = null;
12
27
  this._itemClickHandler = null;
28
+ this._keydownHandler = null;
29
+ this._mouseenterHandler = null;
30
+ this._mouseleaveHandler = null;
31
+ this._triggerMouseenterHandler = null;
32
+ this._triggerMouseleaveHandler = null;
13
33
  }
14
34
 
15
35
  init() {
16
36
  if (!this.trigger || !this.menu) return;
17
37
  if (this.element.__kupolaInitialized) return;
18
38
 
39
+ // Item click handler
19
40
  this._itemClickHandler = (e) => {
20
41
  const item = e.currentTarget;
21
- if (this.triggerText) {
42
+ if (item.classList.contains('is-disabled') || item.classList.contains('ds-dropdown__divider')) return;
43
+
44
+ if (this.triggerText && !item.hasAttribute('data-no-update-trigger')) {
22
45
  this.triggerText.textContent = item.textContent.trim();
23
46
  }
47
+
48
+ if (this.onSelect) {
49
+ this.onSelect({ item, value: item.getAttribute('data-value'), text: item.textContent.trim() });
50
+ }
51
+
24
52
  this.hideMenu();
53
+ this.trigger.focus();
25
54
  };
26
55
 
27
- this.menu.querySelectorAll('.ds-dropdown__item').forEach(item => {
28
- item.addEventListener('click', this._itemClickHandler);
29
- item._dropdownItemClickHandler = this._itemClickHandler;
30
- });
56
+ this._bindMenuItems();
31
57
 
58
+ // Trigger click
32
59
  this._triggerClickHandler = (e) => {
33
60
  e.stopPropagation();
61
+ if (this.disabled) return;
34
62
  this.toggleMenu();
35
63
  };
36
64
 
37
- this.trigger.addEventListener('click', this._triggerClickHandler);
65
+ // Hover trigger
66
+ this._triggerMouseenterHandler = () => {
67
+ if (this.disabled || this.triggerMode !== 'hover') return;
68
+ clearTimeout(this._hoverLeaveTimer);
69
+ this._hoverTimer = setTimeout(() => this.showMenu(), this.hoverDelay);
70
+ };
71
+
72
+ this._triggerMouseleaveHandler = () => {
73
+ if (this.triggerMode !== 'hover') return;
74
+ clearTimeout(this._hoverTimer);
75
+ this._hoverLeaveTimer = setTimeout(() => this.hideMenu(), this.hoverDelay);
76
+ };
77
+
78
+ this._mouseenterHandler = () => {
79
+ if (this.disabled || this.triggerMode !== 'hover') return;
80
+ clearTimeout(this._hoverLeaveTimer);
81
+ };
82
+
83
+ this._mouseleaveHandler = () => {
84
+ if (this.triggerMode !== 'hover') return;
85
+ this._hoverLeaveTimer = setTimeout(() => this.hideMenu(), this.hoverDelay);
86
+ };
87
+
88
+ // Keyboard navigation
89
+ this._keydownHandler = (e) => {
90
+ if (!this.isOpen || this.disabled) return;
91
+
92
+ const items = this._getNavigableItems();
93
+ if (!items.length) return;
94
+
95
+ switch (e.key) {
96
+ case 'ArrowDown':
97
+ e.preventDefault();
98
+ this.focusIndex = Math.min(this.focusIndex + 1, items.length - 1);
99
+ this._focusItem(items);
100
+ break;
101
+ case 'ArrowUp':
102
+ e.preventDefault();
103
+ this.focusIndex = Math.max(this.focusIndex - 1, 0);
104
+ this._focusItem(items);
105
+ break;
106
+ case 'Enter':
107
+ case ' ':
108
+ e.preventDefault();
109
+ if (this.focusIndex >= 0 && items[this.focusIndex]) {
110
+ items[this.focusIndex].click();
111
+ }
112
+ break;
113
+ case 'Escape':
114
+ e.preventDefault();
115
+ this.hideMenu();
116
+ this.trigger.focus();
117
+ break;
118
+ case 'Home':
119
+ e.preventDefault();
120
+ this.focusIndex = 0;
121
+ this._focusItem(items);
122
+ break;
123
+ case 'End':
124
+ e.preventDefault();
125
+ this.focusIndex = items.length - 1;
126
+ this._focusItem(items);
127
+ break;
128
+ }
129
+ };
130
+
131
+ // Bind trigger events
132
+ if (this.triggerMode === 'hover') {
133
+ this.trigger.addEventListener('mouseenter', this._triggerMouseenterHandler);
134
+ this.trigger.addEventListener('mouseleave', this._triggerMouseleaveHandler);
135
+ this.menu.addEventListener('mouseenter', this._mouseenterHandler);
136
+ this.menu.addEventListener('mouseleave', this._mouseleaveHandler);
137
+ } else {
138
+ this.trigger.addEventListener('click', this._triggerClickHandler);
139
+ }
38
140
 
141
+ // Keyboard on trigger
142
+ this.trigger.addEventListener('keydown', (e) => {
143
+ if (this.disabled) return;
144
+ if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
145
+ e.preventDefault();
146
+ this.showMenu();
147
+ }
148
+ });
149
+
150
+ document.addEventListener('keydown', this._keydownHandler);
151
+
152
+ // Document click to close
39
153
  this._documentClickHandler = (e) => {
40
154
  if (!this.element.contains(e.target)) {
41
155
  this.hideMenu();
@@ -52,22 +166,144 @@ class Dropdown {
52
166
  this.element.__kupolaInitialized = true;
53
167
  }
54
168
 
55
- toggleMenu() {
56
- const isOpen = this.menu.style.display === 'block';
57
- this.menu.style.display = isOpen ? 'none' : 'block';
58
- this.element.classList.toggle('is-open', !isOpen);
169
+ _bindMenuItems() {
170
+ this.menu.querySelectorAll('.ds-dropdown__item').forEach(item => {
171
+ item.addEventListener('click', this._itemClickHandler);
172
+ item._dropdownItemClickHandler = this._itemClickHandler;
173
+ });
174
+ }
175
+
176
+ _getNavigableItems() {
177
+ return Array.from(this.menu.querySelectorAll('.ds-dropdown__item'))
178
+ .filter(item => !item.classList.contains('is-disabled') && !item.classList.contains('ds-dropdown__divider'));
179
+ }
180
+
181
+ _focusItem(items) {
182
+ items.forEach(item => item.classList.remove('is-focused'));
183
+ if (items[this.focusIndex]) {
184
+ items[this.focusIndex].classList.add('is-focused');
185
+ items[this.focusIndex].scrollIntoView({ block: 'nearest' });
186
+ }
187
+ }
188
+
189
+ _calculatePosition() {
190
+ if (!this.autoPosition) return;
191
+
192
+ const triggerRect = this.element.getBoundingClientRect();
193
+ const menuRect = this.menu.getBoundingClientRect();
194
+ const viewportHeight = window.innerHeight;
195
+ const viewportWidth = window.innerWidth;
196
+
197
+ // Reset
198
+ this.menu.classList.remove('ds-dropdown--top', 'ds-dropdown--right', 'ds-dropdown--dropup');
199
+
200
+ // Vertical: prefer below, flip to above if needed
201
+ const spaceBelow = viewportHeight - triggerRect.bottom;
202
+ const spaceAbove = triggerRect.top;
203
+
204
+ if (spaceBelow < menuRect.height && spaceAbove > spaceBelow) {
205
+ this.menu.classList.add('ds-dropdown--dropup');
206
+ this.menu.style.top = 'auto';
207
+ this.menu.style.bottom = '100%';
208
+ this.menu.style.marginBottom = '4px';
209
+ } else {
210
+ this.menu.style.top = '100%';
211
+ this.menu.style.bottom = 'auto';
212
+ this.menu.style.marginBottom = '0';
213
+ }
214
+
215
+ // Horizontal: prevent overflow right
216
+ if (triggerRect.left + menuRect.width > viewportWidth) {
217
+ this.menu.style.left = 'auto';
218
+ this.menu.style.right = '0';
219
+ } else {
220
+ this.menu.style.left = '0';
221
+ this.menu.style.right = 'auto';
222
+ }
223
+ }
224
+
225
+ showMenu() {
226
+ if (this.disabled || this.isOpen) return;
227
+ this.isOpen = true;
228
+ this.focusIndex = -1;
229
+ this.menu.style.display = 'block';
230
+ this.element.classList.add('is-open');
231
+ this._calculatePosition();
232
+ if (this.onShow) this.onShow();
233
+ this.element.dispatchEvent(new CustomEvent('kupola:dropdown-show', { bubbles: true }));
59
234
  }
60
235
 
61
236
  hideMenu() {
237
+ if (!this.isOpen) return;
238
+ this.isOpen = false;
62
239
  this.menu.style.display = 'none';
63
240
  this.element.classList.remove('is-open');
241
+
242
+ // Clear focus
243
+ this.menu.querySelectorAll('.ds-dropdown__item').forEach(item => item.classList.remove('is-focused'));
244
+ if (this.onHide) this.onHide();
245
+ this.element.dispatchEvent(new CustomEvent('kupola:dropdown-hide', { bubbles: true }));
246
+ }
247
+
248
+ toggleMenu() {
249
+ if (this.isOpen) {
250
+ this.hideMenu();
251
+ } else {
252
+ this.showMenu();
253
+ }
254
+ }
255
+
256
+ enable() {
257
+ this.disabled = false;
258
+ this.element.removeAttribute('data-dropdown-disabled');
259
+ }
260
+
261
+ disable() {
262
+ this.disabled = true;
263
+ this.element.setAttribute('data-dropdown-disabled', '');
264
+ this.hideMenu();
265
+ }
266
+
267
+ // Update menu items dynamically
268
+ setItems(items) {
269
+ // Remove old item listeners
270
+ this.menu.querySelectorAll('.ds-dropdown__item').forEach(item => {
271
+ if (item._dropdownItemClickHandler) {
272
+ item.removeEventListener('click', item._dropdownItemClickHandler);
273
+ }
274
+ });
275
+
276
+ // Rebuild menu content
277
+ this.menu.innerHTML = '';
278
+ items.forEach(item => {
279
+ if (item.type === 'divider') {
280
+ const divider = document.createElement('div');
281
+ divider.className = 'ds-dropdown__divider';
282
+ this.menu.appendChild(divider);
283
+ } else {
284
+ const el = document.createElement('div');
285
+ el.className = 'ds-dropdown__item' + (item.disabled ? ' is-disabled' : '') + (item.active ? ' is-selected' : '');
286
+ el.textContent = item.text || item.label || '';
287
+ if (item.value !== undefined) el.setAttribute('data-value', item.value);
288
+ if (item.icon) el.innerHTML = item.icon + el.innerHTML;
289
+ if (item.disabled) el.classList.add('is-disabled');
290
+ this.menu.appendChild(el);
291
+ }
292
+ });
293
+
294
+ this._bindMenuItems();
64
295
  }
65
296
 
66
297
  destroy() {
67
298
  if (!this.element.__kupolaInitialized) return;
68
299
 
69
- if (this.trigger && this._triggerClickHandler) {
70
- this.trigger.removeEventListener('click', this._triggerClickHandler);
300
+ clearTimeout(this._hoverTimer);
301
+ clearTimeout(this._hoverLeaveTimer);
302
+
303
+ if (this.trigger) {
304
+ if (this._triggerClickHandler) this.trigger.removeEventListener('click', this._triggerClickHandler);
305
+ if (this._triggerMouseenterHandler) this.trigger.removeEventListener('mouseenter', this._triggerMouseenterHandler);
306
+ if (this._triggerMouseleaveHandler) this.trigger.removeEventListener('mouseleave', this._triggerMouseleaveHandler);
71
307
  }
72
308
 
73
309
  if (this.menu) {
@@ -76,8 +312,12 @@ class Dropdown {
76
312
  item.removeEventListener('click', item._dropdownItemClickHandler);
77
313
  }
78
314
  });
315
+ if (this._mouseenterHandler) this.menu.removeEventListener('mouseenter', this._mouseenterHandler);
316
+ if (this._mouseleaveHandler) this.menu.removeEventListener('mouseleave', this._mouseleaveHandler);
79
317
  }
80
318
 
319
+ if (this._keydownHandler) document.removeEventListener('keydown', this._keydownHandler);
320
+
81
321
  if (this._documentClickListener && this._documentClickListener.unsubscribe) {
82
322
  this._documentClickListener.unsubscribe();
83
323
  } else if (this._documentClickHandler) {
@@ -88,12 +328,17 @@ class Dropdown {
88
328
  this._documentClickListener = null;
89
329
  this._triggerClickHandler = null;
90
330
  this._itemClickHandler = null;
331
+ this._keydownHandler = null;
332
+ this._mouseenterHandler = null;
333
+ this._mouseleaveHandler = null;
334
+ this._triggerMouseenterHandler = null;
335
+ this._triggerMouseleaveHandler = null;
91
336
  this.element.__kupolaInitialized = false;
92
337
  }
93
338
  }
94
339
 
95
- function initDropdown(element) {
96
- const dropdown = new Dropdown(element);
340
+ function initDropdown(element, options) {
341
+ const dropdown = new Dropdown(element, options);
97
342
  dropdown.init();
98
343
  element._kupolaDropdown = dropdown;
99
344
  }
@@ -117,6 +362,16 @@ function cleanupAllDropdowns() {
117
362
  });
118
363
  }
119
364
 
120
- if (window.kupolaInitializer) {
121
- window.kupolaInitializer.register('dropdown', initDropdown, cleanupDropdown);
365
+ export { Dropdown, initDropdown, initDropdowns, cleanupDropdown, cleanupAllDropdowns };
366
+
367
+ if (typeof window !== 'undefined') {
368
+ window.Dropdown = Dropdown;
369
+ window.initDropdown = initDropdown;
370
+ window.initDropdowns = initDropdowns;
371
+ window.cleanupDropdown = cleanupDropdown;
372
+ window.cleanupAllDropdowns = cleanupAllDropdowns;
373
+
374
+ if (window.kupolaInitializer) {
375
+ window.kupolaInitializer.register('dropdown', initDropdown, cleanupDropdown);
376
+ }
122
377
  }