@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/datepicker.js CHANGED
@@ -1,31 +1,79 @@
1
1
  class Datepicker {
2
- constructor(element) {
2
+ constructor(element, options = {}) {
3
3
  this.element = element;
4
4
  this.input = element.querySelector('input');
5
+ this.endInput = element.querySelector('.ds-datepicker__end-input');
5
6
  this.icon = element.querySelector('.ds-datepicker__icon');
6
7
  this.calendarEl = element.querySelector('.ds-datepicker__calendar');
7
8
  this.scope = `datepicker-${Math.random().toString(36).substr(2, 9)}`;
8
9
 
10
+ // Options
11
+ this.format = options.format || element.getAttribute('data-datepicker-format') || 'YYYY-MM-DD';
12
+ this.range = options.range || element.hasAttribute('data-datepicker-range');
13
+ this.minDate = options.minDate || element.getAttribute('data-datepicker-min') || null;
14
+ this.maxDate = options.maxDate || element.getAttribute('data-datepicker-max') || null;
15
+ this.disabledDate = options.disabledDate || null; // function(date) => boolean
16
+ this.weekStart = options.weekStart || parseInt(element.getAttribute('data-datepicker-week-start')) || 0; // 0=Sun, 1=Mon
17
+ this.placeholder = options.placeholder || element.getAttribute('data-datepicker-placeholder') || '';
18
+ this.showToday = options.showToday !== false;
19
+ this.showWeekNumber = options.showWeekNumber || element.hasAttribute('data-datepicker-week-number');
20
+ this.onChange = options.onChange || null;
21
+
22
+ // i18n
23
+ this.months = options.months || ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
24
+ this.weekDays = options.weekDays || ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
25
+ this.todayText = options.todayText || 'Today';
26
+ this.clearText = options.clearText || 'Clear';
27
+
28
+ // State
29
+ this.currentDate = new Date();
30
+ this.viewMode = 'days'; // days | months | years
31
+ this.selectedDate = null;
32
+ this.rangeStart = null;
33
+ this.rangeEnd = null;
34
+ this.isSelectingEnd = false;
35
+
9
36
  this._iconClickHandler = null;
10
37
  this._inputClickHandler = null;
11
38
  this._documentClickHandler = null;
12
39
  this._documentClickListener = null;
13
40
  this._resizeHandler = null;
14
41
  this._resizeListener = null;
42
+ this._keydownHandler = null;
15
43
  }
16
44
 
17
45
  init() {
18
46
  if (!this.calendarEl) return;
19
47
  if (this.element.__kupolaInitialized) return;
20
48
 
21
- this._iconClickHandler = (e) => this.toggleCalendar(e);
22
- this._inputClickHandler = (e) => this.toggleCalendar(e);
49
+ // Parse initial value
50
+ if (this.input && this.input.value) {
51
+ if (this.range) {
52
+ const parts = this.input.value.split(' ~ ');
53
+ if (parts.length === 2) {
54
+ this.rangeStart = this._parseDate(parts[0].trim());
55
+ this.rangeEnd = this._parseDate(parts[1].trim());
56
+ this.currentDate = new Date(this.rangeStart);
57
+ }
58
+ } else {
59
+ this.selectedDate = this._parseDate(this.input.value);
60
+ this.currentDate = new Date(this.selectedDate);
61
+ }
62
+ }
23
63
 
24
- if (this.icon) {
25
- this.icon.addEventListener('click', this._iconClickHandler);
64
+ if (this.placeholder && this.input) {
65
+ this.input.placeholder = this.placeholder;
26
66
  }
27
67
 
28
- this.input.addEventListener('click', this._inputClickHandler);
68
+ this._iconClickHandler = (e) => this.toggleCalendar(e);
69
+ this._inputClickHandler = (e) => this.toggleCalendar(e);
70
+
71
+ if (this.icon) this.icon.addEventListener('click', this._iconClickHandler);
72
+ if (this.input) this.input.addEventListener('click', this._inputClickHandler);
73
+ if (this.endInput) this.endInput.addEventListener('click', (e) => {
74
+ this.isSelectingEnd = true;
75
+ this.toggleCalendar(e);
76
+ });
29
77
 
30
78
  if (window.globalEvents) {
31
79
  this._documentClickListener = window.globalEvents.on(document, 'click', (e) => this.hideCalendar(e), { scope: this.scope });
@@ -37,8 +85,68 @@ class Datepicker {
37
85
  this._resizeHandler = () => this.resizeHandler();
38
86
  }
39
87
 
88
+ // Keyboard
89
+ this._keydownHandler = (e) => {
90
+ if (e.key === 'Escape' && this.calendarEl.style.display === 'block') {
91
+ this.hideCalendar(e);
92
+ }
93
+ };
94
+ document.addEventListener('keydown', this._keydownHandler);
95
+
40
96
  this.element.__kupolaInitialized = true;
41
- this.initCalendar();
97
+ this._renderCalendar();
98
+ }
99
+
100
+ _parseDate(str) {
101
+ if (!str) return null;
102
+ const parts = str.split('-');
103
+ if (parts.length === 3) return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
104
+ return null;
105
+ }
106
+
107
+ _formatDate(date) {
108
+ if (!date) return '';
109
+ const y = date.getFullYear();
110
+ const m = String(date.getMonth() + 1).padStart(2, '0');
111
+ const d = String(date.getDate()).padStart(2, '0');
112
+
113
+ return this.format
114
+ .replace('YYYY', y)
115
+ .replace('MM', m)
116
+ .replace('DD', d);
117
+ }
118
+
119
+ _isDateDisabled(date) {
120
+ if (this.minDate) {
121
+ const min = typeof this.minDate === 'string' ? this._parseDate(this.minDate) : this.minDate;
122
+ if (date < min) return true;
123
+ }
124
+ if (this.maxDate) {
125
+ const max = typeof this.maxDate === 'string' ? this._parseDate(this.maxDate) : this.maxDate;
126
+ if (date > max) return true;
127
+ }
128
+ if (this.disabledDate) {
129
+ return this.disabledDate(date);
130
+ }
131
+ return false;
132
+ }
133
+
134
+ _isToday(date) {
135
+ const today = new Date();
136
+ return date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth() && date.getDate() === today.getDate();
137
+ }
138
+
139
+ _isSameDay(d1, d2) {
140
+ if (!d1 || !d2) return false;
141
+ return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate();
142
+ }
143
+
144
+ _isInRange(date) {
145
+ if (!this.range || !this.rangeStart || !this.rangeEnd) return false;
146
+ const time = date.getTime();
147
+ const start = Math.min(this.rangeStart.getTime(), this.rangeEnd.getTime());
148
+ const end = Math.max(this.rangeStart.getTime(), this.rangeEnd.getTime());
149
+ return time >= start && time <= end;
42
150
  }
43
151
 
44
152
  calculatePosition() {
@@ -48,7 +156,7 @@ class Datepicker {
48
156
 
49
157
  const spaceBelow = viewportHeight - pickerRect.bottom;
50
158
  const spaceAbove = pickerRect.top;
51
- const calendarHeight = calendarRect.height || 280;
159
+ const calendarHeight = calendarRect.height || 320;
52
160
 
53
161
  if (spaceBelow >= calendarHeight) {
54
162
  this.calendarEl.style.top = 'calc(100% + 4px)';
@@ -68,9 +176,12 @@ class Datepicker {
68
176
 
69
177
  const isVisible = this.calendarEl.style.display === 'block';
70
178
 
179
+ // Close all other datepickers
71
180
  document.querySelectorAll('.ds-datepicker__calendar').forEach(c => {
72
- c.style.display = 'none';
73
- c.setAttribute('hidden', '');
181
+ if (c !== this.calendarEl) {
182
+ c.style.display = 'none';
183
+ c.setAttribute('hidden', '');
184
+ }
74
185
  });
75
186
 
76
187
  if (!isVisible) {
@@ -84,6 +195,7 @@ class Datepicker {
84
195
  if (!this.element.contains(e.target)) {
85
196
  this.calendarEl.style.display = 'none';
86
197
  this.calendarEl.setAttribute('hidden', '');
198
+ this.viewMode = 'days';
87
199
  }
88
200
  }
89
201
 
@@ -93,111 +205,379 @@ class Datepicker {
93
205
  }
94
206
  }
95
207
 
96
- initCalendar() {
208
+ _renderCalendar() {
97
209
  const calendar = this.calendarEl;
98
- const input = this.input;
99
- const titleEl = calendar.querySelector('.ds-datepicker__title');
100
- const daysEl = calendar.querySelector('.ds-datepicker__days');
101
- const prevBtn = calendar.querySelector('.ds-datepicker__nav--prev');
102
- const nextBtn = calendar.querySelector('.ds-datepicker__nav--next');
103
-
104
- let currentDate = new Date();
105
- if (input.value) {
106
- const parts = input.value.split('-');
107
- if (parts.length === 3) {
108
- currentDate = new Date(parts[0], parts[1] - 1, parts[2]);
109
- }
210
+ if (!calendar) return;
211
+
212
+ // Clean up old listeners
213
+ calendar.querySelectorAll('.ds-datepicker__day').forEach(dayEl => {
214
+ if (dayEl._dayClickHandler) dayEl.removeEventListener('click', dayEl._dayClickHandler);
215
+ });
216
+
217
+ if (this.viewMode === 'years') {
218
+ this._renderYearsView();
219
+ return;
220
+ }
221
+ if (this.viewMode === 'months') {
222
+ this._renderMonthsView();
223
+ return;
110
224
  }
111
225
 
112
- function renderCalendar() {
113
- daysEl.querySelectorAll('.ds-datepicker__day').forEach(dayEl => {
114
- if (dayEl._dayClickHandler) {
115
- dayEl.removeEventListener('click', dayEl._dayClickHandler);
226
+ const year = this.currentDate.getFullYear();
227
+ const month = this.currentDate.getMonth();
228
+
229
+ calendar.innerHTML = '';
230
+
231
+ // Header with navigation
232
+ const header = document.createElement('div');
233
+ header.className = 'ds-datepicker__header';
234
+
235
+ const prevBtn = document.createElement('button');
236
+ prevBtn.className = 'ds-datepicker__nav ds-datepicker__nav--prev';
237
+ prevBtn.type = 'button';
238
+ prevBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>';
239
+ prevBtn.addEventListener('click', (e) => { e.stopPropagation(); this._prevMonth(); });
240
+
241
+ const titleBtn = document.createElement('button');
242
+ titleBtn.className = 'ds-datepicker__title';
243
+ titleBtn.type = 'button';
244
+ titleBtn.textContent = `${year} ${this.months[month]}`;
245
+ titleBtn.addEventListener('click', (e) => { e.stopPropagation(); this.viewMode = 'months'; this._renderCalendar(); });
246
+
247
+ const nextBtn = document.createElement('button');
248
+ nextBtn.className = 'ds-datepicker__nav ds-datepicker__nav--next';
249
+ nextBtn.type = 'button';
250
+ nextBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>';
251
+ nextBtn.addEventListener('click', (e) => { e.stopPropagation(); this._nextMonth(); });
252
+
253
+ header.appendChild(prevBtn);
254
+ header.appendChild(titleBtn);
255
+ header.appendChild(nextBtn);
256
+ calendar.appendChild(header);
257
+
258
+ // Week day headers
259
+ const weekHeader = document.createElement('div');
260
+ weekHeader.className = 'ds-datepicker__weekdays';
261
+ const orderedDays = [...this.weekDays.slice(this.weekStart), ...this.weekDays.slice(0, this.weekStart)];
262
+ orderedDays.forEach(day => {
263
+ const span = document.createElement('span');
264
+ span.className = 'ds-datepicker__weekday';
265
+ span.textContent = day;
266
+ weekHeader.appendChild(span);
267
+ });
268
+ calendar.appendChild(weekHeader);
269
+
270
+ // Days grid
271
+ const daysEl = document.createElement('div');
272
+ daysEl.className = 'ds-datepicker__days';
273
+
274
+ const firstDay = new Date(year, month, 1).getDay();
275
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
276
+ const offset = (firstDay - this.weekStart + 7) % 7;
277
+
278
+ // Empty cells
279
+ for (let i = 0; i < offset; i++) {
280
+ const emptyDay = document.createElement('span');
281
+ emptyDay.className = 'ds-datepicker__day ds-datepicker__day--empty';
282
+ daysEl.appendChild(emptyDay);
283
+ }
284
+
285
+ // Day cells
286
+ for (let day = 1; day <= daysInMonth; day++) {
287
+ const date = new Date(year, month, day);
288
+ const dayEl = document.createElement('button');
289
+ dayEl.className = 'ds-datepicker__day';
290
+ dayEl.type = 'button';
291
+ dayEl.textContent = day;
292
+
293
+ const dateStr = this._formatDate(date);
294
+
295
+ if (this._isToday(date)) dayEl.classList.add('is-today');
296
+
297
+ // Selected state
298
+ if (this.range) {
299
+ if (this._isSameDay(date, this.rangeStart) || this._isSameDay(date, this.rangeEnd)) {
300
+ dayEl.classList.add('is-selected');
116
301
  }
117
- });
302
+ if (this._isInRange(date)) {
303
+ dayEl.classList.add('is-in-range');
304
+ }
305
+ } else {
306
+ if (this._isSameDay(date, this.selectedDate)) {
307
+ dayEl.classList.add('is-selected');
308
+ }
309
+ }
118
310
 
119
- daysEl.innerHTML = '';
311
+ // Disabled state
312
+ if (this._isDateDisabled(date)) {
313
+ dayEl.classList.add('is-disabled');
314
+ dayEl.disabled = true;
315
+ }
120
316
 
121
- const year = currentDate.getFullYear();
122
- const month = currentDate.getMonth();
317
+ const clickHandler = () => this._selectDate(date);
318
+ dayEl.addEventListener('click', clickHandler);
319
+ dayEl._dayClickHandler = clickHandler;
123
320
 
124
- titleEl.textContent = `${year} ${['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][month]}`;
321
+ daysEl.appendChild(dayEl);
322
+ }
323
+
324
+ calendar.appendChild(daysEl);
325
+
326
+ // Footer with Today / Clear buttons
327
+ if (this.showToday) {
328
+ const footer = document.createElement('div');
329
+ footer.className = 'ds-datepicker__footer';
125
330
 
126
- const firstDay = new Date(year, month, 1).getDay();
127
- const daysInMonth = new Date(year, month + 1, 0).getDate();
331
+ const todayBtn = document.createElement('button');
332
+ todayBtn.className = 'ds-datepicker__today-btn';
333
+ todayBtn.type = 'button';
334
+ todayBtn.textContent = this.todayText;
335
+ todayBtn.addEventListener('click', (e) => { e.stopPropagation(); this._goToToday(); });
128
336
 
129
- for (let i = 0; i < firstDay; i++) {
130
- const emptyDay = document.createElement('span');
131
- emptyDay.className = 'ds-datepicker__day--empty';
132
- daysEl.appendChild(emptyDay);
133
- }
337
+ const clearBtn = document.createElement('button');
338
+ clearBtn.className = 'ds-datepicker__clear-btn';
339
+ clearBtn.type = 'button';
340
+ clearBtn.textContent = this.clearText;
341
+ clearBtn.addEventListener('click', (e) => { e.stopPropagation(); this._clearDate(); });
134
342
 
135
- for (let day = 1; day <= daysInMonth; day++) {
136
- const dayEl = document.createElement('button');
137
- dayEl.className = 'ds-datepicker__day';
138
- dayEl.textContent = day;
139
-
140
- const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
141
- if (input.value === dateStr) {
142
- dayEl.classList.add('is-selected');
143
- }
144
-
145
- const today = new Date();
146
- const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
147
- if (dateStr === todayStr) {
148
- dayEl.classList.add('is-today');
343
+ footer.appendChild(todayBtn);
344
+ footer.appendChild(clearBtn);
345
+ calendar.appendChild(footer);
346
+ }
347
+ }
348
+
349
+ _renderYearsView() {
350
+ const calendar = this.calendarEl;
351
+ calendar.innerHTML = '';
352
+
353
+ const currentYear = this.currentDate.getFullYear();
354
+ const startYear = currentYear - 6;
355
+
356
+ const header = document.createElement('div');
357
+ header.className = 'ds-datepicker__header';
358
+
359
+ const prevBtn = document.createElement('button');
360
+ prevBtn.className = 'ds-datepicker__nav ds-datepicker__nav--prev';
361
+ prevBtn.type = 'button';
362
+ prevBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>';
363
+ prevBtn.addEventListener('click', (e) => { e.stopPropagation(); this.currentDate.setFullYear(this.currentDate.getFullYear() - 12); this._renderCalendar(); });
364
+
365
+ const titleBtn = document.createElement('button');
366
+ titleBtn.className = 'ds-datepicker__title';
367
+ titleBtn.type = 'button';
368
+ titleBtn.textContent = `${startYear} - ${startYear + 11}`;
369
+ titleBtn.addEventListener('click', (e) => { e.stopPropagation(); });
370
+
371
+ const nextBtn = document.createElement('button');
372
+ nextBtn.className = 'ds-datepicker__nav ds-datepicker__nav--next';
373
+ nextBtn.type = 'button';
374
+ nextBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>';
375
+ nextBtn.addEventListener('click', (e) => { e.stopPropagation(); this.currentDate.setFullYear(this.currentDate.getFullYear() + 12); this._renderCalendar(); });
376
+
377
+ header.appendChild(prevBtn);
378
+ header.appendChild(titleBtn);
379
+ header.appendChild(nextBtn);
380
+ calendar.appendChild(header);
381
+
382
+ const grid = document.createElement('div');
383
+ grid.className = 'ds-datepicker__years-grid';
384
+
385
+ for (let i = 0; i < 12; i++) {
386
+ const year = startYear + i;
387
+ const btn = document.createElement('button');
388
+ btn.className = 'ds-datepicker__year-cell';
389
+ btn.type = 'button';
390
+ btn.textContent = year;
391
+ if (year === currentYear) btn.classList.add('is-selected');
392
+ btn.addEventListener('click', (e) => {
393
+ e.stopPropagation();
394
+ this.currentDate.setFullYear(year);
395
+ this.viewMode = 'months';
396
+ this._renderCalendar();
397
+ });
398
+ grid.appendChild(btn);
399
+ }
400
+
401
+ calendar.appendChild(grid);
402
+ }
403
+
404
+ _renderMonthsView() {
405
+ const calendar = this.calendarEl;
406
+ calendar.innerHTML = '';
407
+
408
+ const year = this.currentDate.getFullYear();
409
+
410
+ const header = document.createElement('div');
411
+ header.className = 'ds-datepicker__header';
412
+
413
+ const prevBtn = document.createElement('button');
414
+ prevBtn.className = 'ds-datepicker__nav ds-datepicker__nav--prev';
415
+ prevBtn.type = 'button';
416
+ prevBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>';
417
+ prevBtn.addEventListener('click', (e) => { e.stopPropagation(); this.currentDate.setFullYear(this.currentDate.getFullYear() - 1); this._renderCalendar(); });
418
+
419
+ const titleBtn = document.createElement('button');
420
+ titleBtn.className = 'ds-datepicker__title';
421
+ titleBtn.type = 'button';
422
+ titleBtn.textContent = year;
423
+ titleBtn.addEventListener('click', (e) => { e.stopPropagation(); this.viewMode = 'years'; this._renderCalendar(); });
424
+
425
+ const nextBtn = document.createElement('button');
426
+ nextBtn.className = 'ds-datepicker__nav ds-datepicker__nav--next';
427
+ nextBtn.type = 'button';
428
+ nextBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>';
429
+ nextBtn.addEventListener('click', (e) => { e.stopPropagation(); this.currentDate.setFullYear(this.currentDate.getFullYear() + 1); this._renderCalendar(); });
430
+
431
+ header.appendChild(prevBtn);
432
+ header.appendChild(titleBtn);
433
+ header.appendChild(nextBtn);
434
+ calendar.appendChild(header);
435
+
436
+ const grid = document.createElement('div');
437
+ grid.className = 'ds-datepicker__months-grid';
438
+
439
+ this.months.forEach((monthName, idx) => {
440
+ const btn = document.createElement('button');
441
+ btn.className = 'ds-datepicker__month-cell';
442
+ btn.type = 'button';
443
+ btn.textContent = monthName;
444
+ if (idx === this.currentDate.getMonth()) btn.classList.add('is-selected');
445
+ btn.addEventListener('click', (e) => {
446
+ e.stopPropagation();
447
+ this.currentDate.setMonth(idx);
448
+ this.viewMode = 'days';
449
+ this._renderCalendar();
450
+ });
451
+ grid.appendChild(btn);
452
+ });
453
+
454
+ calendar.appendChild(grid);
455
+ }
456
+
457
+ _selectDate(date) {
458
+ if (this._isDateDisabled(date)) return;
459
+
460
+ if (this.range) {
461
+ if (!this.isSelectingEnd || !this.rangeStart) {
462
+ this.rangeStart = date;
463
+ this.rangeEnd = null;
464
+ this.isSelectingEnd = true;
465
+ } else {
466
+ this.rangeEnd = date;
467
+ // Ensure start <= end
468
+ if (this.rangeEnd < this.rangeStart) {
469
+ [this.rangeStart, this.rangeEnd] = [this.rangeEnd, this.rangeStart];
149
470
  }
471
+ this.isSelectingEnd = false;
150
472
 
151
- const dayClickHandler = () => {
152
- input.value = dateStr;
153
- calendar.style.display = 'none';
154
- calendar.setAttribute('hidden', '');
155
- input.dispatchEvent(new Event('change'));
156
- };
473
+ if (this.input) this.input.value = this._formatDate(this.rangeStart);
474
+ if (this.endInput) this.endInput.value = this._formatDate(this.rangeEnd);
157
475
 
158
- dayEl.addEventListener('click', dayClickHandler);
159
- dayEl._dayClickHandler = dayClickHandler;
160
-
161
- daysEl.appendChild(dayEl);
476
+ this.calendarEl.style.display = 'none';
477
+ this.calendarEl.setAttribute('hidden', '');
478
+ this._fireChange();
479
+ return;
480
+ }
481
+ } else {
482
+ this.selectedDate = date;
483
+ if (this.input) {
484
+ this.input.value = this._formatDate(date);
162
485
  }
486
+ this.calendarEl.style.display = 'none';
487
+ this.calendarEl.setAttribute('hidden', '');
488
+ this._fireChange();
489
+ return;
163
490
  }
164
491
 
165
- const prevClickHandler = (e) => {
166
- e.stopPropagation();
167
- currentDate.setMonth(currentDate.getMonth() - 1);
168
- renderCalendar();
169
- };
170
-
171
- const nextClickHandler = (e) => {
172
- e.stopPropagation();
173
- currentDate.setMonth(currentDate.getMonth() + 1);
174
- renderCalendar();
175
- };
176
-
177
- if (prevBtn) {
178
- prevBtn.addEventListener('click', prevClickHandler);
179
- prevBtn._prevClickHandler = prevClickHandler;
492
+ this._renderCalendar();
493
+ }
494
+
495
+ _fireChange() {
496
+ if (this.input) {
497
+ this.input.dispatchEvent(new Event('change', { bubbles: true }));
180
498
  }
181
499
 
182
- if (nextBtn) {
183
- nextBtn.addEventListener('click', nextClickHandler);
184
- nextBtn._nextClickHandler = nextClickHandler;
500
+ if (this.onChange) {
501
+ if (this.range) {
502
+ this.onChange({ start: this.rangeStart, end: this.rangeEnd, startStr: this._formatDate(this.rangeStart), endStr: this._formatDate(this.rangeEnd) });
503
+ } else {
504
+ this.onChange({ date: this.selectedDate, dateStr: this._formatDate(this.selectedDate) });
505
+ }
185
506
  }
186
507
 
187
- calendar._calendarInitialized = true;
188
- renderCalendar();
508
+ this.element.dispatchEvent(new CustomEvent('kupola:datepicker-change', {
509
+ detail: {
510
+ date: this.selectedDate,
511
+ dateStr: this._formatDate(this.selectedDate),
512
+ rangeStart: this.rangeStart,
513
+ rangeEnd: this.rangeEnd
514
+ },
515
+ bubbles: true
516
+ }));
189
517
  }
190
518
 
191
- destroy() {
192
- if (!this.element.__kupolaInitialized) return;
519
+ _prevMonth() {
520
+ this.currentDate.setMonth(this.currentDate.getMonth() - 1);
521
+ this._renderCalendar();
522
+ }
523
+
524
+ _nextMonth() {
525
+ this.currentDate.setMonth(this.currentDate.getMonth() + 1);
526
+ this._renderCalendar();
527
+ }
193
528
 
194
- if (this.icon && this._iconClickHandler) {
195
- this.icon.removeEventListener('click', this._iconClickHandler);
529
+ _goToToday() {
530
+ const today = new Date();
531
+ this.currentDate = new Date(today);
532
+
533
+ if (!this._isDateDisabled(today)) {
534
+ this._selectDate(today);
535
+ } else {
536
+ this._renderCalendar();
196
537
  }
538
+ }
539
+
540
+ _clearDate() {
541
+ this.selectedDate = null;
542
+ this.rangeStart = null;
543
+ this.rangeEnd = null;
544
+ this.isSelectingEnd = false;
545
+ if (this.input) this.input.value = '';
546
+ if (this.endInput) this.endInput.value = '';
547
+ this.calendarEl.style.display = 'none';
548
+ this.calendarEl.setAttribute('hidden', '');
549
+ this._fireChange();
550
+ }
197
551
 
198
- if (this.input && this._inputClickHandler) {
199
- this.input.removeEventListener('click', this._inputClickHandler);
552
+ // Public API
553
+ setDate(date) {
554
+ const d = typeof date === 'string' ? this._parseDate(date) : date;
555
+ if (d) {
556
+ this.selectedDate = d;
557
+ this.currentDate = new Date(d);
558
+ if (this.input) this.input.value = this._formatDate(d);
559
+ this._renderCalendar();
200
560
  }
561
+ }
562
+
563
+ getDate() {
564
+ return this.selectedDate;
565
+ }
566
+
567
+ setRange(start, end) {
568
+ this.rangeStart = typeof start === 'string' ? this._parseDate(start) : start;
569
+ this.rangeEnd = typeof end === 'string' ? this._parseDate(end) : end;
570
+ if (this.input) this.input.value = this._formatDate(this.rangeStart);
571
+ if (this.endInput) this.endInput.value = this._formatDate(this.rangeEnd);
572
+ this._renderCalendar();
573
+ }
574
+
575
+ destroy() {
576
+ if (!this.element.__kupolaInitialized) return;
577
+
578
+ if (this.icon && this._iconClickHandler) this.icon.removeEventListener('click', this._iconClickHandler);
579
+ if (this.input && this._inputClickHandler) this.input.removeEventListener('click', this._inputClickHandler);
580
+ if (this._keydownHandler) document.removeEventListener('keydown', this._keydownHandler);
201
581
 
202
582
  if (this._documentClickListener && this._documentClickListener.unsubscribe) {
203
583
  this._documentClickListener.unsubscribe();
@@ -212,22 +592,9 @@ class Datepicker {
212
592
  }
213
593
 
214
594
  if (this.calendarEl) {
215
- const daysEl = this.calendarEl.querySelector('.ds-datepicker__days');
216
- if (daysEl) {
217
- daysEl.querySelectorAll('.ds-datepicker__day').forEach(dayEl => {
218
- if (dayEl._dayClickHandler) {
219
- dayEl.removeEventListener('click', dayEl._dayClickHandler);
220
- }
221
- });
222
- }
223
- const prevBtn = this.calendarEl.querySelector('.ds-datepicker__nav--prev');
224
- const nextBtn = this.calendarEl.querySelector('.ds-datepicker__nav--next');
225
- if (prevBtn && prevBtn._prevClickHandler) {
226
- prevBtn.removeEventListener('click', prevBtn._prevClickHandler);
227
- }
228
- if (nextBtn && nextBtn._nextClickHandler) {
229
- nextBtn.removeEventListener('click', nextBtn._nextClickHandler);
230
- }
595
+ this.calendarEl.querySelectorAll('.ds-datepicker__day').forEach(dayEl => {
596
+ if (dayEl._dayClickHandler) dayEl.removeEventListener('click', dayEl._dayClickHandler);
597
+ });
231
598
  }
232
599
 
233
600
  this._documentClickHandler = null;
@@ -236,12 +603,13 @@ class Datepicker {
236
603
  this._resizeListener = null;
237
604
  this._iconClickHandler = null;
238
605
  this._inputClickHandler = null;
606
+ this._keydownHandler = null;
239
607
  this.element.__kupolaInitialized = false;
240
608
  }
241
609
  }
242
610
 
243
- function initDatepicker(element) {
244
- const picker = new Datepicker(element);
611
+ function initDatepicker(element, options) {
612
+ const picker = new Datepicker(element, options);
245
613
  picker.init();
246
614
  element._kupolaDatepicker = picker;
247
615
  }
@@ -265,6 +633,16 @@ function cleanupAllDatepickers() {
265
633
  });
266
634
  }
267
635
 
268
- if (window.kupolaInitializer) {
269
- window.kupolaInitializer.register('datepicker', initDatepicker, cleanupDatepicker);
270
- }
636
+ export { Datepicker, initDatepicker, initDatepickers, cleanupDatepicker, cleanupAllDatepickers };
637
+
638
+ if (typeof window !== 'undefined') {
639
+ window.Datepicker = Datepicker;
640
+ window.initDatepicker = initDatepicker;
641
+ window.initDatepickers = initDatepickers;
642
+ window.cleanupDatepicker = cleanupDatepicker;
643
+ window.cleanupAllDatepickers = cleanupAllDatepickers;
644
+
645
+ if (window.kupolaInitializer) {
646
+ window.kupolaInitializer.register('datepicker', initDatepicker, cleanupDatepicker);
647
+ }
648
+ }