@kupola/kupola 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) 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/dist/css/accessibility.css +119 -0
  8. package/dist/css/animations.css +224 -0
  9. package/dist/css/brand-themes.css +300 -0
  10. package/dist/css/colors_and_type.css +441 -0
  11. package/dist/css/components-ext.css +4165 -0
  12. package/dist/css/components.css +1483 -0
  13. package/dist/css/responsive.css +697 -0
  14. package/dist/css/scaffold.css +145 -0
  15. package/dist/css/states.css +316 -0
  16. package/dist/css/theme-dark.css +296 -0
  17. package/dist/css/theme-light.css +296 -0
  18. package/dist/css/utilities.css +171 -0
  19. package/dist/kupola.cjs.js +219 -161
  20. package/dist/kupola.cjs.js.map +1 -1
  21. package/dist/kupola.esm.js +6643 -5709
  22. package/dist/kupola.esm.js.map +1 -1
  23. package/dist/kupola.umd.js +219 -161
  24. package/dist/kupola.umd.js.map +1 -1
  25. package/dist/plugins/vite-plugin-kupola.js +120 -0
  26. package/dist/types/kupola.d.ts +421 -323
  27. package/js/calendar.js +334 -25
  28. package/js/carousel.js +182 -48
  29. package/js/collapse.js +148 -34
  30. package/js/color-picker.js +416 -108
  31. package/js/component.js +8 -19
  32. package/js/countdown.js +9 -8
  33. package/js/data-bind.js +73 -16
  34. package/js/datepicker.js +488 -110
  35. package/js/depends.js +710 -0
  36. package/js/dialog.js +4 -2
  37. package/js/drawer.js +172 -8
  38. package/js/dropdown.js +272 -17
  39. package/js/dynamic-tags.js +156 -40
  40. package/js/fileupload.js +9 -8
  41. package/js/form.js +280 -254
  42. package/js/global-events.js +281 -188
  43. package/js/heatmap.js +10 -7
  44. package/js/i18n.js +18 -10
  45. package/js/icons.js +141 -161
  46. package/js/image-preview.js +146 -2
  47. package/js/initializer.js +113 -71
  48. package/js/kupola-core.js +123 -45
  49. package/js/kupola-lifecycle.js +13 -11
  50. package/js/message.js +8 -1
  51. package/js/modal.js +207 -59
  52. package/js/notification.js +8 -1
  53. package/js/numberinput.js +9 -8
  54. package/js/pagination.js +263 -0
  55. package/js/registry.js +29 -12
  56. package/js/select.js +482 -27
  57. package/js/slide-captcha.js +11 -2
  58. package/js/slider.js +442 -25
  59. package/js/statcard.js +9 -7
  60. package/js/table.js +1210 -0
  61. package/js/tag.js +268 -14
  62. package/js/theme.js +14 -43
  63. package/js/timepicker.js +335 -66
  64. package/js/tooltip.js +317 -86
  65. package/js/utils.js +6 -2
  66. package/js/validation.js +6 -2
  67. package/js/virtual-list.js +11 -7
  68. package/js/web-components.js +288 -0
  69. package/package.json +77 -67
  70. package/plugins/vite-plugin-kupola.js +120 -0
  71. package/types/kupola.d.ts +421 -323
  72. package/CHANGELOG.md +0 -130
  73. package/INTEGRATION.md +0 -440
  74. package/PROJECT_SUMMARY.md +0 -312
  75. package/SKILL.md +0 -572
  76. package/dist/utils/utils/Kupola.cs +0 -77
  77. package/dist/utils/utils/Kupola.java +0 -104
  78. package/dist/utils/utils/kupola.go +0 -120
  79. package/dist/utils/utils/kupola.js +0 -63
  80. package/dist/utils/utils/kupola.py +0 -1392
  81. package/dist/utils/utils/kupola.rb +0 -69
  82. package/js/composition-api.js +0 -458
  83. package/js/error-handler.js +0 -181
  84. package/js/http.js +0 -419
  85. package/js/kupola-devtools.js +0 -598
  86. package/js/performance.js +0 -250
  87. package/js/router.js +0 -396
  88. package/js/security.js +0 -189
  89. package/js/test-utils.js +0 -251
  90. package/templates/base.html +0 -30
  91. package/templates/base_dashboard.html +0 -99
  92. package/utils/Kupola.cs +0 -77
  93. package/utils/Kupola.java +0 -104
  94. package/utils/kupola.go +0 -120
  95. package/utils/kupola.js +0 -63
  96. package/utils/kupola.py +0 -1392
  97. package/utils/kupola.rb +0 -69
@@ -7,6 +7,12 @@ class ImagePreview {
7
7
  this.keyHandler = this.handleKeydown.bind(this);
8
8
  this.clickHandler = this.handleOverlayClick.bind(this);
9
9
 
10
+ this.zoom = 1;
11
+ this.rotation = 0;
12
+ this.zoomStep = options.zoomStep || 0.2;
13
+ this.minZoom = options.minZoom || 0.5;
14
+ this.maxZoom = options.maxZoom || 3;
15
+
10
16
  this.init();
11
17
  }
12
18
 
@@ -36,6 +42,38 @@ class ImagePreview {
36
42
  </svg>
37
43
  </button>
38
44
  </div>
45
+ <div class="ds-image-preview__toolbar">
46
+ <button class="ds-image-preview__toolbar-btn" type="button" aria-label="Zoom in" data-action="zoom-in">
47
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
48
+ <line x1="12" y1="5" x2="12" y2="19"/>
49
+ <line x1="5" y1="12" x2="19" y2="12"/>
50
+ </svg>
51
+ </button>
52
+ <button class="ds-image-preview__toolbar-btn" type="button" aria-label="Zoom out" data-action="zoom-out">
53
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
54
+ <line x1="5" y1="12" x2="19" y2="12"/>
55
+ </svg>
56
+ </button>
57
+ <button class="ds-image-preview__toolbar-btn" type="button" aria-label="Reset zoom" data-action="zoom-reset">
58
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
59
+ <path d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z"/>
60
+ <line x1="12" y1="5" x2="12" y2="19"/>
61
+ <line x1="5" y1="12" x2="19" y2="12"/>
62
+ </svg>
63
+ </button>
64
+ <button class="ds-image-preview__toolbar-btn" type="button" aria-label="Rotate left" data-action="rotate-left">
65
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
66
+ <polyline points="1 4 1 10 7 10"/>
67
+ <path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
68
+ </svg>
69
+ </button>
70
+ <button class="ds-image-preview__toolbar-btn" type="button" aria-label="Rotate right" data-action="rotate-right">
71
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
72
+ <polyline points="23 4 23 10 17 10"/>
73
+ <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
74
+ </svg>
75
+ </button>
76
+ </div>
39
77
  <div class="ds-image-preview__content">
40
78
  <img src="" alt="" />
41
79
  </div>
@@ -62,6 +100,82 @@ class ImagePreview {
62
100
  closeBtn.addEventListener('click', this.closeHandler);
63
101
  prevBtn.addEventListener('click', this._prevHandler);
64
102
  nextBtn.addEventListener('click', this._nextHandler);
103
+
104
+ const toolbarBtns = this.overlay.querySelectorAll('.ds-image-preview__toolbar-btn');
105
+ toolbarBtns.forEach(btn => {
106
+ btn.addEventListener('click', (e) => {
107
+ const action = btn.getAttribute('data-action');
108
+ this.handleToolbarAction(action);
109
+ });
110
+ });
111
+
112
+ const content = this.overlay.querySelector('.ds-image-preview__content');
113
+ content.addEventListener('wheel', (e) => {
114
+ e.preventDefault();
115
+ if (e.deltaY < 0) {
116
+ this.zoomIn();
117
+ } else {
118
+ this.zoomOut();
119
+ }
120
+ }, { passive: false });
121
+ }
122
+
123
+ handleToolbarAction(action) {
124
+ switch (action) {
125
+ case 'zoom-in':
126
+ this.zoomIn();
127
+ break;
128
+ case 'zoom-out':
129
+ this.zoomOut();
130
+ break;
131
+ case 'zoom-reset':
132
+ this.resetZoom();
133
+ break;
134
+ case 'rotate-left':
135
+ this.rotate(-90);
136
+ break;
137
+ case 'rotate-right':
138
+ this.rotate(90);
139
+ break;
140
+ }
141
+ }
142
+
143
+ zoomIn() {
144
+ this.zoom = Math.min(this.maxZoom, this.zoom + this.zoomStep);
145
+ this.updateTransform();
146
+ }
147
+
148
+ zoomOut() {
149
+ this.zoom = Math.max(this.minZoom, this.zoom - this.zoomStep);
150
+ this.updateTransform();
151
+ }
152
+
153
+ resetZoom() {
154
+ this.zoom = 1;
155
+ this.rotation = 0;
156
+ this.updateTransform();
157
+ }
158
+
159
+ rotate(degrees) {
160
+ this.rotation += degrees;
161
+ this.updateTransform();
162
+ }
163
+
164
+ setRotation(degrees) {
165
+ this.rotation = degrees;
166
+ this.updateTransform();
167
+ }
168
+
169
+ setZoom(zoom) {
170
+ this.zoom = Math.max(this.minZoom, Math.min(this.maxZoom, zoom));
171
+ this.updateTransform();
172
+ }
173
+
174
+ updateTransform() {
175
+ const img = this.overlay.querySelector('.ds-image-preview__content img');
176
+ if (img) {
177
+ img.style.transform = `scale(${this.zoom}) rotate(${this.rotation}deg)`;
178
+ }
65
179
  }
66
180
 
67
181
  handleKeydown(e) {
@@ -77,6 +191,28 @@ class ImagePreview {
77
191
  case 'ArrowRight':
78
192
  this.next();
79
193
  break;
194
+ case '+':
195
+ case '=':
196
+ e.preventDefault();
197
+ this.zoomIn();
198
+ break;
199
+ case '-':
200
+ case '_':
201
+ e.preventDefault();
202
+ this.zoomOut();
203
+ break;
204
+ case '0':
205
+ e.preventDefault();
206
+ this.resetZoom();
207
+ break;
208
+ case '[':
209
+ e.preventDefault();
210
+ this.rotate(-90);
211
+ break;
212
+ case ']':
213
+ e.preventDefault();
214
+ this.rotate(90);
215
+ break;
80
216
  }
81
217
  }
82
218
 
@@ -90,6 +226,7 @@ class ImagePreview {
90
226
  this.images = images;
91
227
  this.currentIndex = Math.min(Math.max(index, 0), images.length - 1);
92
228
 
229
+ this.resetZoom();
93
230
  this.render();
94
231
  this.overlay.classList.add('is-visible');
95
232
 
@@ -109,6 +246,7 @@ class ImagePreview {
109
246
  prev() {
110
247
  if (this.currentIndex > 0) {
111
248
  this.currentIndex--;
249
+ this.resetZoom();
112
250
  this.render();
113
251
  }
114
252
  }
@@ -116,6 +254,7 @@ class ImagePreview {
116
254
  next() {
117
255
  if (this.currentIndex < this.images.length - 1) {
118
256
  this.currentIndex++;
257
+ this.resetZoom();
119
258
  this.render();
120
259
  }
121
260
  }
@@ -123,6 +262,7 @@ class ImagePreview {
123
262
  goTo(index) {
124
263
  if (index >= 0 && index < this.images.length) {
125
264
  this.currentIndex = index;
265
+ this.resetZoom();
126
266
  this.render();
127
267
  }
128
268
  }
@@ -213,6 +353,10 @@ function showImagePreview(images, index = 0) {
213
353
  imagePreviewInstance.show(images, index);
214
354
  }
215
355
 
216
- if (typeof module !== 'undefined' && module.exports) {
217
- module.exports = { ImagePreview, initImagePreview, showImagePreview };
356
+ export { ImagePreview, initImagePreview, showImagePreview };
357
+
358
+ if (typeof window !== 'undefined') {
359
+ window.ImagePreview = ImagePreview;
360
+ window.initImagePreview = initImagePreview;
361
+ window.showImagePreview = showImagePreview;
218
362
  }
package/js/initializer.js CHANGED
@@ -1,15 +1,40 @@
1
+ /**
2
+ * Component Initializer Registry — auto-discovery and lifecycle management for declarative components.
3
+ * Scans the DOM for `data-*` attributes or CSS classes, calls init functions,
4
+ * and uses MutationObserver for dynamically added elements.
5
+ */
1
6
  class ComponentInitializerRegistry {
2
7
  constructor() {
3
8
  this.initializers = new Map();
4
9
  this.cleanupFunctions = new Map();
5
10
  this.processedElements = new WeakSet();
11
+ // Single source of truth for selectors
12
+ this._dataAttrs = ['data-component'];
13
+ this._cssClasses = [];
14
+ this._cachedSelector = null;
6
15
  }
7
16
 
8
- register(name, initFn, cleanupFn = null) {
17
+ /**
18
+ * Register a component initializer.
19
+ * @param {string} name - Component name
20
+ * @param {Function} initFn - Init function called with the DOM element
21
+ * @param {Function} [cleanupFn] - Optional cleanup/destroy function
22
+ * @param {Object} [options] - { dataAttribute, cssClass } for auto-discovery
23
+ */
24
+ register(name, initFn, cleanupFn = null, options = {}) {
9
25
  this.initializers.set(name, initFn);
10
26
  if (cleanupFn) {
11
27
  this.cleanupFunctions.set(name, cleanupFn);
12
28
  }
29
+ // Auto-register selectors
30
+ if (options.dataAttribute && !this._dataAttrs.includes(options.dataAttribute)) {
31
+ this._dataAttrs.push(options.dataAttribute);
32
+ this._cachedSelector = null;
33
+ }
34
+ if (options.cssClass && !this._cssClasses.includes(options.cssClass)) {
35
+ this._cssClasses.push(options.cssClass);
36
+ this._cachedSelector = null;
37
+ }
13
38
  }
14
39
 
15
40
  unregister(name) {
@@ -25,20 +50,26 @@ class ComponentInitializerRegistry {
25
50
  return this.initializers.get(name);
26
51
  }
27
52
 
53
+ _buildSelector() {
54
+ if (this._cachedSelector !== null) return this._cachedSelector;
55
+ const parts = this._dataAttrs.map(a => `[${a}]`);
56
+ for (const cls of this._cssClasses) {
57
+ parts.push(`.${cls}`);
58
+ }
59
+ this._cachedSelector = parts.join(', ');
60
+ return this._cachedSelector;
61
+ }
62
+
63
+ /** Initialize a single DOM element if it matches a registered component. */
28
64
  async initialize(element) {
29
65
  if (this.processedElements.has(element)) return;
30
66
 
31
- const dataAttrs = ['data-component', 'data-dropdown', 'data-select', 'data-datepicker',
32
- 'data-timepicker', 'data-slider', 'data-carousel', 'data-drawer',
33
- 'data-modal', 'data-dialog', 'data-color-picker', 'data-calendar',
34
- 'data-slide-captcha', 'data-heatmap'];
35
-
36
- for (const attr of dataAttrs) {
67
+ // Check data attributes
68
+ for (const attr of this._dataAttrs) {
37
69
  const value = element.getAttribute(attr);
38
- if (value) {
70
+ if (value !== null) {
39
71
  const name = value || attr.replace('data-', '');
40
72
  const initFn = this.initializers.get(name) || this.initializers.get(attr.replace('data-', ''));
41
-
42
73
  if (initFn) {
43
74
  try {
44
75
  await initFn(element);
@@ -51,46 +82,35 @@ class ComponentInitializerRegistry {
51
82
  }
52
83
  }
53
84
 
85
+ // Check CSS classes
54
86
  const className = element.className;
55
- const classSelectors = [
56
- '.ds-dropdown', '.ds-select', '.ds-datepicker', '.ds-timepicker',
57
- '.ds-slider', '.ds-carousel', '.ds-drawer', '.ds-modal', '.ds-dialog',
58
- '.ds-color-picker', '.ds-calendar', '.ds-slider-captcha', '.ds-heatmap',
59
- '.ds-tooltip', '.ds-tag', '.ds-statcard', '.ds-collapse', '.ds-fileupload',
60
- '.ds-notification', '.ds-message'
61
- ];
62
-
63
- for (const selector of classSelectors) {
64
- const classNamePart = selector.replace('.', '');
65
- if (className.includes(classNamePart)) {
66
- const name = classNamePart.replace('ds-', '');
67
- const initFn = this.initializers.get(name) || this.initializers.get(classNamePart);
68
-
69
- if (initFn) {
70
- try {
71
- await initFn(element);
72
- this.processedElements.add(element);
73
- } catch (error) {
74
- console.error(`[ComponentInitializerRegistry] Error initializing "${name}":`, error);
87
+ if (typeof className === 'string') {
88
+ for (const cls of this._cssClasses) {
89
+ if (className.includes(cls)) {
90
+ const name = cls.replace('ds-', '');
91
+ const initFn = this.initializers.get(name) || this.initializers.get(cls);
92
+ if (initFn) {
93
+ try {
94
+ await initFn(element);
95
+ this.processedElements.add(element);
96
+ } catch (error) {
97
+ console.error(`[ComponentInitializerRegistry] Error initializing "${name}":`, error);
98
+ }
99
+ return;
75
100
  }
76
- return;
77
101
  }
78
102
  }
79
103
  }
80
104
  }
81
105
 
106
+ /** Run cleanup/destroy for a single DOM element. */
82
107
  cleanup(element) {
83
- const dataAttrs = ['data-component', 'data-dropdown', 'data-select', 'data-datepicker',
84
- 'data-timepicker', 'data-slider', 'data-carousel', 'data-drawer',
85
- 'data-modal', 'data-dialog', 'data-color-picker', 'data-calendar',
86
- 'data-slide-captcha', 'data-heatmap'];
87
-
88
- for (const attr of dataAttrs) {
108
+ // Check data attributes
109
+ for (const attr of this._dataAttrs) {
89
110
  const value = element.getAttribute(attr);
90
- if (value) {
111
+ if (value !== null) {
91
112
  const name = value || attr.replace('data-', '');
92
113
  const cleanupFn = this.cleanupFunctions.get(name) || this.cleanupFunctions.get(attr.replace('data-', ''));
93
-
94
114
  if (cleanupFn) {
95
115
  try {
96
116
  cleanupFn(element);
@@ -103,45 +123,33 @@ class ComponentInitializerRegistry {
103
123
  }
104
124
  }
105
125
 
126
+ // Check CSS classes
106
127
  const className = element.className;
107
- const classSelectors = [
108
- '.ds-dropdown', '.ds-select', '.ds-datepicker', '.ds-timepicker',
109
- '.ds-slider', '.ds-carousel', '.ds-drawer', '.ds-modal', '.ds-dialog',
110
- '.ds-color-picker', '.ds-calendar', '.ds-slider-captcha', '.ds-heatmap',
111
- '.ds-tooltip', '.ds-tag', '.ds-statcard', '.ds-collapse', '.ds-fileupload',
112
- '.ds-notification', '.ds-message'
113
- ];
114
-
115
- for (const selector of classSelectors) {
116
- const classNamePart = selector.replace('.', '');
117
- if (className.includes(classNamePart)) {
118
- const name = classNamePart.replace('ds-', '');
119
- const cleanupFn = this.cleanupFunctions.get(name) || this.cleanupFunctions.get(classNamePart);
120
-
121
- if (cleanupFn) {
122
- try {
123
- cleanupFn(element);
124
- } catch (error) {
125
- console.error(`[ComponentInitializerRegistry] Error cleaning up "${name}":`, error);
128
+ if (typeof className === 'string') {
129
+ for (const cls of this._cssClasses) {
130
+ if (className.includes(cls)) {
131
+ const name = cls.replace('ds-', '');
132
+ const cleanupFn = this.cleanupFunctions.get(name) || this.cleanupFunctions.get(cls);
133
+ if (cleanupFn) {
134
+ try {
135
+ cleanupFn(element);
136
+ } catch (error) {
137
+ console.error(`[ComponentInitializerRegistry] Error cleaning up "${name}":`, error);
138
+ }
139
+ this.processedElements.delete(element);
140
+ return;
126
141
  }
127
- this.processedElements.delete(element);
128
- return;
129
142
  }
130
143
  }
131
144
  }
132
145
  }
133
146
 
147
+ /** Scan and initialize all matching components. Uses MutationObserver for dynamic content. */
134
148
  async initializeAll(root = document) {
135
- const elements = root.querySelectorAll('[data-component], [data-dropdown], [data-select], ' +
136
- '[data-datepicker], [data-timepicker], [data-slider], [data-carousel], ' +
137
- '[data-drawer], [data-modal], [data-dialog], [data-color-picker], ' +
138
- '[data-calendar], [data-slide-captcha], [data-heatmap], ' +
139
- '.ds-dropdown, .ds-select, .ds-datepicker, .ds-timepicker, ' +
140
- '.ds-slider, .ds-carousel, .ds-drawer, .ds-modal, .ds-dialog, ' +
141
- '.ds-color-picker, .ds-calendar, .ds-slider-captcha, .ds-heatmap, ' +
142
- '.ds-tooltip, .ds-tag, .ds-statcard, .ds-collapse, .ds-fileupload, ' +
143
- '.ds-notification, .ds-message');
149
+ const selector = this._buildSelector();
150
+ if (!selector) return;
144
151
 
152
+ const elements = root.querySelectorAll(selector);
145
153
  const promises = [];
146
154
  elements.forEach(element => {
147
155
  if (!this.processedElements.has(element)) {
@@ -152,11 +160,45 @@ class ComponentInitializerRegistry {
152
160
  }
153
161
  }
154
162
 
163
+ /** @type {ComponentInitializerRegistry} Global initializer instance */
155
164
  const kupolaInitializer = new ComponentInitializerRegistry();
156
165
 
157
- if (typeof module !== 'undefined' && module.exports) {
158
- module.exports = { ComponentInitializerRegistry, kupolaInitializer };
159
- } else {
166
+ // Pre-register known component selectors
167
+ const knownComponents = [
168
+ { attr: 'data-dropdown', cls: 'ds-dropdown' },
169
+ { attr: 'data-select', cls: 'ds-select' },
170
+ { attr: 'data-datepicker', cls: 'ds-datepicker' },
171
+ { attr: 'data-timepicker', cls: 'ds-timepicker' },
172
+ { attr: 'data-slider', cls: 'ds-slider' },
173
+ { attr: 'data-carousel', cls: 'ds-carousel' },
174
+ { attr: 'data-drawer', cls: 'ds-drawer' },
175
+ { attr: 'data-modal', cls: 'ds-modal' },
176
+ { attr: 'data-dialog', cls: 'ds-dialog' },
177
+ { attr: 'data-color-picker', cls: 'ds-color-picker' },
178
+ { attr: 'data-calendar', cls: 'ds-calendar' },
179
+ { attr: 'data-slide-captcha', cls: 'ds-slide-captcha' },
180
+ { attr: 'data-heatmap', cls: 'ds-heatmap' },
181
+ { cls: 'ds-tooltip' },
182
+ { cls: 'ds-tag' },
183
+ { cls: 'ds-statcard' },
184
+ { cls: 'ds-collapse' },
185
+ { cls: 'ds-fileupload' },
186
+ { cls: 'ds-notification' },
187
+ { cls: 'ds-message' }
188
+ ];
189
+
190
+ for (const comp of knownComponents) {
191
+ if (comp.attr && !kupolaInitializer._dataAttrs.includes(comp.attr)) {
192
+ kupolaInitializer._dataAttrs.push(comp.attr);
193
+ }
194
+ if (comp.cls && !kupolaInitializer._cssClasses.includes(comp.cls)) {
195
+ kupolaInitializer._cssClasses.push(comp.cls);
196
+ }
197
+ }
198
+
199
+ export { ComponentInitializerRegistry, kupolaInitializer };
200
+
201
+ if (typeof window !== 'undefined') {
160
202
  window.ComponentInitializerRegistry = ComponentInitializerRegistry;
161
203
  window.kupolaInitializer = kupolaInitializer;
162
204
  }
package/js/kupola-core.js CHANGED
@@ -1,66 +1,144 @@
1
- if (typeof window === 'undefined') {
2
- const { KupolaComponent, applyMixin } = require('./component.js');
3
- const { KupolaComponentRegistry } = require('./registry.js');
4
- const { KupolaRouter, createRouter } = require('./router.js');
5
- const { KupolaHttp, createHttp } = require('./http.js');
6
-
7
- module.exports = {
8
- KupolaComponent,
9
- applyMixin,
10
- KupolaComponentRegistry,
11
- KupolaRouter,
12
- KupolaHttp,
13
- createRouter,
14
- createHttp
15
- };
16
- }
1
+ /**
2
+ * Kupola Core — Component system bootstrap, registry, and lifecycle management.
3
+ * @module kupola-core
4
+ */
5
+
6
+ import { KupolaComponent, applyMixin } from './component.js';
7
+ import { KupolaComponentRegistry } from './registry.js';
8
+ import { KupolaLifecycle } from './kupola-lifecycle.js';
9
+ import { kupolaInitializer } from './initializer.js';
10
+ import { kupolaData } from './data-bind.js';
11
+ import { initTheme } from './theme.js';
12
+
13
+ let kupolaRegistry = null;
17
14
 
18
- if (window.KupolaComponentRegistry) {
19
- window.kupolaRegistry = new window.KupolaComponentRegistry();
15
+ if (typeof window !== 'undefined') {
16
+ kupolaRegistry = new KupolaComponentRegistry();
20
17
  }
21
18
 
19
+ /** Bootstrap the Kupola component system (data binding, theme, component discovery). */
22
20
  async function kupolaBootstrap() {
23
- if (window.kupolaData) {
24
- window.kupolaData.loadPersisted();
25
- }
26
- if (window.kupolaRegistry) {
27
- await window.kupolaRegistry.bootstrap();
21
+ if (typeof window !== 'undefined') {
22
+ // Load persisted data and bind data-bind elements
23
+ kupolaData.loadPersisted();
24
+ kupolaData.bind();
25
+ // Initialize theme
26
+ initTheme();
27
+ // Initialize function-based components first
28
+ await kupolaInitializer.initializeAll();
29
+ // Then bootstrap class-based components
30
+ if (kupolaRegistry) {
31
+ await kupolaRegistry.bootstrap();
32
+ }
28
33
  }
29
34
  }
30
35
 
31
- if (document.readyState === 'loading') {
36
+ if (typeof document !== 'undefined' && document.readyState === 'loading') {
32
37
  document.addEventListener('DOMContentLoaded', kupolaBootstrap);
33
- } else {
38
+ } else if (typeof window !== 'undefined') {
34
39
  setTimeout(kupolaBootstrap, 0);
35
40
  }
36
41
 
37
- window.registerComponent = function(name, componentClass) {
38
- if (window.kupolaRegistry) {
39
- window.kupolaRegistry.register(name, componentClass);
42
+ /** Register a class-based component. */
43
+ function registerComponent(name, componentClass) {
44
+ if (kupolaRegistry) {
45
+ kupolaRegistry.register(name, componentClass);
40
46
  }
41
- };
47
+ }
42
48
 
43
- window.registerLazyComponent = function(name, loader) {
44
- if (window.kupolaRegistry) {
45
- window.kupolaRegistry.registerLazy(name, loader);
49
+ /** Register a lazy-loaded component (loaded on first use). */
50
+ function registerLazyComponent(name, loader) {
51
+ if (kupolaRegistry) {
52
+ kupolaRegistry.registerLazy(name, loader);
46
53
  }
47
- };
54
+ }
48
55
 
49
- window.bootstrapComponents = function(root) {
50
- if (window.kupolaRegistry) {
51
- return window.kupolaRegistry.bootstrap(root);
56
+ /** Manually bootstrap components within a specific root element. */
57
+ function bootstrapComponents(root) {
58
+ if (kupolaRegistry) {
59
+ return kupolaRegistry.bootstrap(root);
52
60
  }
53
61
  return Promise.resolve();
54
- };
62
+ }
55
63
 
56
- window.defineMixin = function(name, mixin) {
57
- if (window.kupolaRegistry) {
58
- window.kupolaRegistry.defineMixin(name, mixin);
64
+ /** Define a reusable mixin for component classes. */
65
+ function defineMixin(name, mixin) {
66
+ if (kupolaRegistry) {
67
+ kupolaRegistry.defineMixin(name, mixin);
59
68
  }
60
- };
69
+ }
61
70
 
62
- window.useMixin = function(componentClass, ...mixinNames) {
63
- if (window.kupolaRegistry) {
64
- window.kupolaRegistry.useMixin(componentClass, ...mixinNames);
71
+ /** Apply named mixins to a component class. */
72
+ function useMixin(componentClass, ...mixinNames) {
73
+ if (kupolaRegistry) {
74
+ kupolaRegistry.useMixin(componentClass, ...mixinNames);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Unified component registration API.
80
+ * @param {string} name - Component name
81
+ * @param {object} options
82
+ * @param {Function} [options.init] - Init function (for function-based components)
83
+ * @param {Function} [options.cleanup] - Cleanup function
84
+ * @param {Function} [options.componentClass] - Component class (for class-based components)
85
+ * @param {Function} [options.lazy] - Lazy loader function
86
+ * @param {string} [options.dataAttribute] - Custom data attribute (e.g. 'data-my-component')
87
+ * @param {string} [options.cssClass] - CSS class selector (e.g. 'ds-my-component')
88
+ */
89
+ function defineComponent(name, options) {
90
+ if (!options || typeof options !== 'object') {
91
+ throw new Error(`defineComponent("${name}"): options must be an object`);
92
+ }
93
+ // Register with registry (class or lazy)
94
+ if (options.componentClass) {
95
+ if (kupolaRegistry) {
96
+ kupolaRegistry.register(name, options.componentClass);
97
+ }
98
+ } else if (options.lazy) {
99
+ if (kupolaRegistry) {
100
+ kupolaRegistry.registerLazy(name, options.lazy);
101
+ }
65
102
  }
66
- };
103
+ // Register with initializer (function-based)
104
+ if (options.init) {
105
+ kupolaInitializer.register(name, options.init, options.cleanup || null, {
106
+ dataAttribute: options.dataAttribute,
107
+ cssClass: options.cssClass
108
+ });
109
+ } else if (options.dataAttribute || options.cssClass) {
110
+ // Even without init, register selectors so MutationObserver can find them
111
+ kupolaInitializer.register(name, () => {}, null, {
112
+ dataAttribute: options.dataAttribute,
113
+ cssClass: options.cssClass
114
+ });
115
+ }
116
+ }
117
+
118
+ export {
119
+ KupolaComponent,
120
+ applyMixin,
121
+ KupolaComponentRegistry,
122
+ KupolaLifecycle,
123
+ kupolaRegistry,
124
+ kupolaInitializer,
125
+ kupolaBootstrap,
126
+ registerComponent,
127
+ registerLazyComponent,
128
+ bootstrapComponents,
129
+ defineMixin,
130
+ useMixin,
131
+ defineComponent
132
+ };
133
+
134
+ if (typeof window !== 'undefined') {
135
+ window.kupolaRegistry = kupolaRegistry;
136
+ window.kupolaInitializer = kupolaInitializer;
137
+ window.kupolaBootstrap = kupolaBootstrap;
138
+ window.registerComponent = registerComponent;
139
+ window.registerLazyComponent = registerLazyComponent;
140
+ window.bootstrapComponents = bootstrapComponents;
141
+ window.defineMixin = defineMixin;
142
+ window.useMixin = useMixin;
143
+ window.defineComponent = defineComponent;
144
+ }