ux4g-components-web 1.0.0 → 1.1.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.
- package/README.md +75 -0
- package/dist/runtime/bootstrap.cjs +16 -0
- package/dist/runtime/bootstrap.d.ts +1 -0
- package/dist/runtime/bootstrap.mjs +14 -0
- package/dist/runtime/index.cjs +300 -0
- package/dist/runtime/index.d.ts +40 -0
- package/dist/runtime/index.mjs +297 -0
- package/package.json +21 -2
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# ux4g-components-web
|
|
2
|
+
|
|
3
|
+
The CSS system, design tokens, utilities, and shared Class_Builder types for the UX4G Design System.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install ux4g-components-web
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### CSS Bundle
|
|
14
|
+
|
|
15
|
+
Import the full CSS bundle in your application entry point:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// JavaScript/TypeScript
|
|
19
|
+
import 'ux4g-components-web/styles.css';
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<!-- HTML -->
|
|
24
|
+
<link rel="stylesheet" href="node_modules/ux4g-components-web/styles/ux4g.css" />
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```css
|
|
28
|
+
/* CSS */
|
|
29
|
+
@import 'ux4g-components-web/styles.css';
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The bundle includes:
|
|
33
|
+
- Design tokens (CSS custom properties)
|
|
34
|
+
- Semantic layer (semantic aliases)
|
|
35
|
+
- All 27 utility modules
|
|
36
|
+
- Approved component CSS
|
|
37
|
+
- Layout utilities
|
|
38
|
+
- Cascade fixes
|
|
39
|
+
|
|
40
|
+
### Shared Types (Class_Builders)
|
|
41
|
+
|
|
42
|
+
Framework wrapper packages import Class_Builder functions from this package:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { buildButtonClasses, ButtonVariant, ButtonSize } from 'ux4g-components-web/types';
|
|
46
|
+
|
|
47
|
+
const classes = buildButtonClasses('primary', 'md', false, false);
|
|
48
|
+
// → "ux4g-btn-primary ux4g-btn-md"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Utility Classes
|
|
52
|
+
|
|
53
|
+
Use utility classes directly in HTML — no wrapper component needed:
|
|
54
|
+
|
|
55
|
+
```html
|
|
56
|
+
<div class="ux4g-d-flex ux4g-gap-4 ux4g-p-m ux4g-bg-primary-soft ux4g-radius-md">
|
|
57
|
+
<span class="ux4g-body-m-default ux4g-text-neutral-primary">Content</span>
|
|
58
|
+
</div>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## What's Included
|
|
62
|
+
|
|
63
|
+
| Export | Description |
|
|
64
|
+
|--------|-------------|
|
|
65
|
+
| `ux4g-components-web/styles.css` | Pre-built CSS bundle (~7MB, fonts embedded as base64) |
|
|
66
|
+
| `ux4g-components-web/types` | Class_Builder functions and TypeScript types |
|
|
67
|
+
|
|
68
|
+
## Related Packages
|
|
69
|
+
|
|
70
|
+
- [`ux4g-components-react`](https://www.npmjs.com/package/ux4g-components-react) — React wrapper components
|
|
71
|
+
- [`ux4g-components-angular`](https://www.npmjs.com/package/ux4g-components-angular) — Angular wrapper components
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var index = require('./index');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* UX4G Runtime Auto-Bootstrap
|
|
7
|
+
*
|
|
8
|
+
* Side-effect-only module that triggers runtime initialization on import.
|
|
9
|
+
* Framework packages use this as:
|
|
10
|
+
* import 'ux4g-components-web/runtime/bootstrap';
|
|
11
|
+
*
|
|
12
|
+
* Safe to import multiple times — initRuntime() uses a singleton guard
|
|
13
|
+
* (window.__UX4G_RUNTIME_INITIALIZED__) to ensure initialization happens
|
|
14
|
+
* exactly once.
|
|
15
|
+
*/
|
|
16
|
+
index.initRuntime();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { initRuntime } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UX4G Runtime Auto-Bootstrap
|
|
5
|
+
*
|
|
6
|
+
* Side-effect-only module that triggers runtime initialization on import.
|
|
7
|
+
* Framework packages use this as:
|
|
8
|
+
* import 'ux4g-components-web/runtime/bootstrap';
|
|
9
|
+
*
|
|
10
|
+
* Safe to import multiple times — initRuntime() uses a singleton guard
|
|
11
|
+
* (window.__UX4G_RUNTIME_INITIALIZED__) to ensure initialization happens
|
|
12
|
+
* exactly once.
|
|
13
|
+
*/
|
|
14
|
+
initRuntime();
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UX4G Runtime Module
|
|
5
|
+
*
|
|
6
|
+
* Framework-agnostic runtime that encapsulates all interactive behaviors
|
|
7
|
+
* previously delivered via CDN scripts (ux4g.js, ux4g-custom.js).
|
|
8
|
+
*
|
|
9
|
+
* Behaviors: dropdown toggling, modal management, tooltip positioning,
|
|
10
|
+
* accordion expand/collapse, carousel sliding, drawer open/close.
|
|
11
|
+
*
|
|
12
|
+
* Uses a singleton guard (window.__UX4G_RUNTIME_INITIALIZED__) to ensure
|
|
13
|
+
* initialization happens exactly once, even when multiple components import
|
|
14
|
+
* the bootstrap module.
|
|
15
|
+
*/
|
|
16
|
+
/** Registry of all active event listeners for cleanup */
|
|
17
|
+
let activeListeners = [];
|
|
18
|
+
/** Registry of active MutationObservers for cleanup */
|
|
19
|
+
let activeObservers = [];
|
|
20
|
+
// ─── Helper: register a listener for later cleanup ───────────────────────────
|
|
21
|
+
function addListener(target, event, handler) {
|
|
22
|
+
target.addEventListener(event, handler);
|
|
23
|
+
activeListeners.push({ target, event, handler });
|
|
24
|
+
}
|
|
25
|
+
// ─── Dropdown Toggling ───────────────────────────────────────────────────────
|
|
26
|
+
function initDropdowns() {
|
|
27
|
+
const handler = (e) => {
|
|
28
|
+
const target = e.target;
|
|
29
|
+
const trigger = target.closest('[data-ux4g-dropdown-toggle]');
|
|
30
|
+
if (trigger) {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
const dropdown = trigger.closest('.ux4g-dropdown');
|
|
33
|
+
if (dropdown) {
|
|
34
|
+
const isOpen = dropdown.classList.contains('is-open');
|
|
35
|
+
// Close all other open dropdowns
|
|
36
|
+
document.querySelectorAll('.ux4g-dropdown.is-open').forEach((el) => {
|
|
37
|
+
if (el !== dropdown)
|
|
38
|
+
el.classList.remove('is-open');
|
|
39
|
+
});
|
|
40
|
+
dropdown.classList.toggle('is-open', !isOpen);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Close dropdowns when clicking outside
|
|
45
|
+
if (!target.closest('.ux4g-dropdown')) {
|
|
46
|
+
document.querySelectorAll('.ux4g-dropdown.is-open').forEach((el) => {
|
|
47
|
+
el.classList.remove('is-open');
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
addListener(document, 'click', handler);
|
|
52
|
+
}
|
|
53
|
+
// ─── Modal Management ────────────────────────────────────────────────────────
|
|
54
|
+
function initModals() {
|
|
55
|
+
const openHandler = (e) => {
|
|
56
|
+
const target = e.target;
|
|
57
|
+
const trigger = target.closest('[data-ux4g-modal-open]');
|
|
58
|
+
if (trigger) {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
const modalId = trigger.getAttribute('data-ux4g-modal-open');
|
|
61
|
+
if (modalId) {
|
|
62
|
+
const backdrop = document.getElementById(modalId);
|
|
63
|
+
if (backdrop) {
|
|
64
|
+
backdrop.classList.add('is-open');
|
|
65
|
+
document.body.style.overflow = 'hidden';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const closeHandler = (e) => {
|
|
71
|
+
const target = e.target;
|
|
72
|
+
// Close button inside modal
|
|
73
|
+
const closeBtn = target.closest('[data-ux4g-modal-close]');
|
|
74
|
+
if (closeBtn) {
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
const backdrop = closeBtn.closest('.ux4g-modal-backdrop');
|
|
77
|
+
if (backdrop) {
|
|
78
|
+
backdrop.classList.remove('is-open');
|
|
79
|
+
document.body.style.overflow = '';
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Click on backdrop (outside modal box) closes modal
|
|
84
|
+
if (target.classList.contains('ux4g-modal-backdrop') && target.classList.contains('is-open')) {
|
|
85
|
+
target.classList.remove('is-open');
|
|
86
|
+
document.body.style.overflow = '';
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const keyHandler = (e) => {
|
|
90
|
+
if (e.key === 'Escape') {
|
|
91
|
+
const openModal = document.querySelector('.ux4g-modal-backdrop.is-open');
|
|
92
|
+
if (openModal) {
|
|
93
|
+
openModal.classList.remove('is-open');
|
|
94
|
+
document.body.style.overflow = '';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
addListener(document, 'click', openHandler);
|
|
99
|
+
addListener(document, 'click', closeHandler);
|
|
100
|
+
addListener(document, 'keydown', keyHandler);
|
|
101
|
+
}
|
|
102
|
+
// ─── Tooltip Positioning ─────────────────────────────────────────────────────
|
|
103
|
+
function initTooltips() {
|
|
104
|
+
const showHandler = (e) => {
|
|
105
|
+
const target = e.target;
|
|
106
|
+
const trigger = target.closest('[data-ux4g-tooltip]');
|
|
107
|
+
if (trigger) {
|
|
108
|
+
const tooltipId = trigger.getAttribute('data-ux4g-tooltip');
|
|
109
|
+
if (tooltipId) {
|
|
110
|
+
const tooltip = document.getElementById(tooltipId);
|
|
111
|
+
if (tooltip) {
|
|
112
|
+
tooltip.classList.add('show');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const hideHandler = (e) => {
|
|
118
|
+
const target = e.target;
|
|
119
|
+
const trigger = target.closest('[data-ux4g-tooltip]');
|
|
120
|
+
if (trigger) {
|
|
121
|
+
const tooltipId = trigger.getAttribute('data-ux4g-tooltip');
|
|
122
|
+
if (tooltipId) {
|
|
123
|
+
const tooltip = document.getElementById(tooltipId);
|
|
124
|
+
if (tooltip) {
|
|
125
|
+
tooltip.classList.remove('show');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
addListener(document, 'mouseenter', showHandler);
|
|
131
|
+
addListener(document, 'mouseleave', hideHandler);
|
|
132
|
+
addListener(document, 'focusin', showHandler);
|
|
133
|
+
addListener(document, 'focusout', hideHandler);
|
|
134
|
+
}
|
|
135
|
+
// ─── Accordion Expand/Collapse ───────────────────────────────────────────────
|
|
136
|
+
function initAccordions() {
|
|
137
|
+
const handler = (e) => {
|
|
138
|
+
const target = e.target;
|
|
139
|
+
const trigger = target.closest('[data-ux4g-accordion-toggle]');
|
|
140
|
+
if (trigger) {
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
const item = trigger.closest('.ux4g-accordion-item');
|
|
143
|
+
if (item) {
|
|
144
|
+
const isExpanded = item.classList.contains('is-expanded');
|
|
145
|
+
item.classList.toggle('is-expanded', !isExpanded);
|
|
146
|
+
// Update aria-expanded attribute
|
|
147
|
+
trigger.setAttribute('aria-expanded', String(!isExpanded));
|
|
148
|
+
// Toggle the panel visibility
|
|
149
|
+
const panel = item.querySelector('.ux4g-accordion-panel');
|
|
150
|
+
if (panel) {
|
|
151
|
+
panel.style.display = isExpanded ? 'none' : '';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
addListener(document, 'click', handler);
|
|
157
|
+
}
|
|
158
|
+
// ─── Carousel Sliding ────────────────────────────────────────────────────────
|
|
159
|
+
function initCarousels() {
|
|
160
|
+
const handler = (e) => {
|
|
161
|
+
const target = e.target;
|
|
162
|
+
// Next button
|
|
163
|
+
const nextBtn = target.closest('[data-ux4g-carousel-next]');
|
|
164
|
+
if (nextBtn) {
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
const carousel = nextBtn.closest('.ux4g-carousel');
|
|
167
|
+
if (carousel) {
|
|
168
|
+
const items = carousel.querySelectorAll('.ux4g-carousel-item');
|
|
169
|
+
const activeItem = carousel.querySelector('.ux4g-carousel-item.active');
|
|
170
|
+
if (activeItem && items.length > 0) {
|
|
171
|
+
const currentIndex = Array.from(items).indexOf(activeItem);
|
|
172
|
+
const nextIndex = (currentIndex + 1) % items.length;
|
|
173
|
+
activeItem.classList.remove('active');
|
|
174
|
+
items[nextIndex].classList.add('active');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Previous button
|
|
180
|
+
const prevBtn = target.closest('[data-ux4g-carousel-prev]');
|
|
181
|
+
if (prevBtn) {
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
const carousel = prevBtn.closest('.ux4g-carousel');
|
|
184
|
+
if (carousel) {
|
|
185
|
+
const items = carousel.querySelectorAll('.ux4g-carousel-item');
|
|
186
|
+
const activeItem = carousel.querySelector('.ux4g-carousel-item.active');
|
|
187
|
+
if (activeItem && items.length > 0) {
|
|
188
|
+
const currentIndex = Array.from(items).indexOf(activeItem);
|
|
189
|
+
const prevIndex = (currentIndex - 1 + items.length) % items.length;
|
|
190
|
+
activeItem.classList.remove('active');
|
|
191
|
+
items[prevIndex].classList.add('active');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
addListener(document, 'click', handler);
|
|
197
|
+
}
|
|
198
|
+
// ─── Drawer Open/Close ───────────────────────────────────────────────────────
|
|
199
|
+
function initDrawers() {
|
|
200
|
+
const openHandler = (e) => {
|
|
201
|
+
const target = e.target;
|
|
202
|
+
const trigger = target.closest('[data-ux4g-drawer-open]');
|
|
203
|
+
if (trigger) {
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
const drawerId = trigger.getAttribute('data-ux4g-drawer-open');
|
|
206
|
+
if (drawerId) {
|
|
207
|
+
const drawer = document.getElementById(drawerId);
|
|
208
|
+
if (drawer) {
|
|
209
|
+
drawer.classList.add('ux4g-drawer-open');
|
|
210
|
+
document.body.style.overflow = 'hidden';
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const closeHandler = (e) => {
|
|
216
|
+
const target = e.target;
|
|
217
|
+
const closeBtn = target.closest('[data-ux4g-drawer-close]');
|
|
218
|
+
if (closeBtn) {
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
const drawer = closeBtn.closest('.ux4g-drawer');
|
|
221
|
+
if (drawer) {
|
|
222
|
+
drawer.classList.remove('ux4g-drawer-open');
|
|
223
|
+
document.body.style.overflow = '';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const keyHandler = (e) => {
|
|
228
|
+
if (e.key === 'Escape') {
|
|
229
|
+
const openDrawer = document.querySelector('.ux4g-drawer.ux4g-drawer-open');
|
|
230
|
+
if (openDrawer) {
|
|
231
|
+
openDrawer.classList.remove('ux4g-drawer-open');
|
|
232
|
+
document.body.style.overflow = '';
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
addListener(document, 'click', openHandler);
|
|
237
|
+
addListener(document, 'click', closeHandler);
|
|
238
|
+
addListener(document, 'keydown', keyHandler);
|
|
239
|
+
}
|
|
240
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
241
|
+
/**
|
|
242
|
+
* Initialize the UX4G runtime.
|
|
243
|
+
*
|
|
244
|
+
* Encapsulates all interactive behaviors from the CDN scripts (ux4g.js, ux4g-custom.js):
|
|
245
|
+
* - Dropdown toggling
|
|
246
|
+
* - Modal management (open/close/escape/backdrop click)
|
|
247
|
+
* - Tooltip positioning (show/hide on hover/focus)
|
|
248
|
+
* - Accordion expand/collapse
|
|
249
|
+
* - Carousel sliding (next/prev)
|
|
250
|
+
* - Drawer open/close
|
|
251
|
+
*
|
|
252
|
+
* Uses a singleton guard to ensure initialization happens exactly once.
|
|
253
|
+
* Safe for SSR — no-ops when window/document are unavailable.
|
|
254
|
+
*/
|
|
255
|
+
function initRuntime() {
|
|
256
|
+
// Guard against non-browser environments (SSR/Node.js)
|
|
257
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// Singleton guard: prevent duplicate initialization
|
|
261
|
+
if (window.__UX4G_RUNTIME_INITIALIZED__) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
// Initialize all interactive behaviors
|
|
265
|
+
initDropdowns();
|
|
266
|
+
initModals();
|
|
267
|
+
initTooltips();
|
|
268
|
+
initAccordions();
|
|
269
|
+
initCarousels();
|
|
270
|
+
initDrawers();
|
|
271
|
+
// Mark as initialized
|
|
272
|
+
window.__UX4G_RUNTIME_INITIALIZED__ = true;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Destroy the UX4G runtime.
|
|
276
|
+
*
|
|
277
|
+
* Resets the singleton guard and removes all event listeners.
|
|
278
|
+
* Useful for testing and SSR cleanup.
|
|
279
|
+
*/
|
|
280
|
+
function destroyRuntime() {
|
|
281
|
+
// Guard against non-browser environments
|
|
282
|
+
if (typeof window === 'undefined') {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// Remove all registered event listeners
|
|
286
|
+
for (const { target, event, handler } of activeListeners) {
|
|
287
|
+
target.removeEventListener(event, handler);
|
|
288
|
+
}
|
|
289
|
+
activeListeners = [];
|
|
290
|
+
// Disconnect all observers
|
|
291
|
+
for (const observer of activeObservers) {
|
|
292
|
+
observer.disconnect();
|
|
293
|
+
}
|
|
294
|
+
activeObservers = [];
|
|
295
|
+
// Reset the singleton guard
|
|
296
|
+
window.__UX4G_RUNTIME_INITIALIZED__ = undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
exports.destroyRuntime = destroyRuntime;
|
|
300
|
+
exports.initRuntime = initRuntime;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UX4G Runtime Module
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic runtime that encapsulates all interactive behaviors
|
|
5
|
+
* previously delivered via CDN scripts (ux4g.js, ux4g-custom.js).
|
|
6
|
+
*
|
|
7
|
+
* Behaviors: dropdown toggling, modal management, tooltip positioning,
|
|
8
|
+
* accordion expand/collapse, carousel sliding, drawer open/close.
|
|
9
|
+
*
|
|
10
|
+
* Uses a singleton guard (window.__UX4G_RUNTIME_INITIALIZED__) to ensure
|
|
11
|
+
* initialization happens exactly once, even when multiple components import
|
|
12
|
+
* the bootstrap module.
|
|
13
|
+
*/
|
|
14
|
+
declare global {
|
|
15
|
+
interface Window {
|
|
16
|
+
__UX4G_RUNTIME_INITIALIZED__?: boolean;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Initialize the UX4G runtime.
|
|
21
|
+
*
|
|
22
|
+
* Encapsulates all interactive behaviors from the CDN scripts (ux4g.js, ux4g-custom.js):
|
|
23
|
+
* - Dropdown toggling
|
|
24
|
+
* - Modal management (open/close/escape/backdrop click)
|
|
25
|
+
* - Tooltip positioning (show/hide on hover/focus)
|
|
26
|
+
* - Accordion expand/collapse
|
|
27
|
+
* - Carousel sliding (next/prev)
|
|
28
|
+
* - Drawer open/close
|
|
29
|
+
*
|
|
30
|
+
* Uses a singleton guard to ensure initialization happens exactly once.
|
|
31
|
+
* Safe for SSR — no-ops when window/document are unavailable.
|
|
32
|
+
*/
|
|
33
|
+
export declare function initRuntime(): void;
|
|
34
|
+
/**
|
|
35
|
+
* Destroy the UX4G runtime.
|
|
36
|
+
*
|
|
37
|
+
* Resets the singleton guard and removes all event listeners.
|
|
38
|
+
* Useful for testing and SSR cleanup.
|
|
39
|
+
*/
|
|
40
|
+
export declare function destroyRuntime(): void;
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UX4G Runtime Module
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic runtime that encapsulates all interactive behaviors
|
|
5
|
+
* previously delivered via CDN scripts (ux4g.js, ux4g-custom.js).
|
|
6
|
+
*
|
|
7
|
+
* Behaviors: dropdown toggling, modal management, tooltip positioning,
|
|
8
|
+
* accordion expand/collapse, carousel sliding, drawer open/close.
|
|
9
|
+
*
|
|
10
|
+
* Uses a singleton guard (window.__UX4G_RUNTIME_INITIALIZED__) to ensure
|
|
11
|
+
* initialization happens exactly once, even when multiple components import
|
|
12
|
+
* the bootstrap module.
|
|
13
|
+
*/
|
|
14
|
+
/** Registry of all active event listeners for cleanup */
|
|
15
|
+
let activeListeners = [];
|
|
16
|
+
/** Registry of active MutationObservers for cleanup */
|
|
17
|
+
let activeObservers = [];
|
|
18
|
+
// ─── Helper: register a listener for later cleanup ───────────────────────────
|
|
19
|
+
function addListener(target, event, handler) {
|
|
20
|
+
target.addEventListener(event, handler);
|
|
21
|
+
activeListeners.push({ target, event, handler });
|
|
22
|
+
}
|
|
23
|
+
// ─── Dropdown Toggling ───────────────────────────────────────────────────────
|
|
24
|
+
function initDropdowns() {
|
|
25
|
+
const handler = (e) => {
|
|
26
|
+
const target = e.target;
|
|
27
|
+
const trigger = target.closest('[data-ux4g-dropdown-toggle]');
|
|
28
|
+
if (trigger) {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
const dropdown = trigger.closest('.ux4g-dropdown');
|
|
31
|
+
if (dropdown) {
|
|
32
|
+
const isOpen = dropdown.classList.contains('is-open');
|
|
33
|
+
// Close all other open dropdowns
|
|
34
|
+
document.querySelectorAll('.ux4g-dropdown.is-open').forEach((el) => {
|
|
35
|
+
if (el !== dropdown)
|
|
36
|
+
el.classList.remove('is-open');
|
|
37
|
+
});
|
|
38
|
+
dropdown.classList.toggle('is-open', !isOpen);
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Close dropdowns when clicking outside
|
|
43
|
+
if (!target.closest('.ux4g-dropdown')) {
|
|
44
|
+
document.querySelectorAll('.ux4g-dropdown.is-open').forEach((el) => {
|
|
45
|
+
el.classList.remove('is-open');
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
addListener(document, 'click', handler);
|
|
50
|
+
}
|
|
51
|
+
// ─── Modal Management ────────────────────────────────────────────────────────
|
|
52
|
+
function initModals() {
|
|
53
|
+
const openHandler = (e) => {
|
|
54
|
+
const target = e.target;
|
|
55
|
+
const trigger = target.closest('[data-ux4g-modal-open]');
|
|
56
|
+
if (trigger) {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
const modalId = trigger.getAttribute('data-ux4g-modal-open');
|
|
59
|
+
if (modalId) {
|
|
60
|
+
const backdrop = document.getElementById(modalId);
|
|
61
|
+
if (backdrop) {
|
|
62
|
+
backdrop.classList.add('is-open');
|
|
63
|
+
document.body.style.overflow = 'hidden';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const closeHandler = (e) => {
|
|
69
|
+
const target = e.target;
|
|
70
|
+
// Close button inside modal
|
|
71
|
+
const closeBtn = target.closest('[data-ux4g-modal-close]');
|
|
72
|
+
if (closeBtn) {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
const backdrop = closeBtn.closest('.ux4g-modal-backdrop');
|
|
75
|
+
if (backdrop) {
|
|
76
|
+
backdrop.classList.remove('is-open');
|
|
77
|
+
document.body.style.overflow = '';
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Click on backdrop (outside modal box) closes modal
|
|
82
|
+
if (target.classList.contains('ux4g-modal-backdrop') && target.classList.contains('is-open')) {
|
|
83
|
+
target.classList.remove('is-open');
|
|
84
|
+
document.body.style.overflow = '';
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const keyHandler = (e) => {
|
|
88
|
+
if (e.key === 'Escape') {
|
|
89
|
+
const openModal = document.querySelector('.ux4g-modal-backdrop.is-open');
|
|
90
|
+
if (openModal) {
|
|
91
|
+
openModal.classList.remove('is-open');
|
|
92
|
+
document.body.style.overflow = '';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
addListener(document, 'click', openHandler);
|
|
97
|
+
addListener(document, 'click', closeHandler);
|
|
98
|
+
addListener(document, 'keydown', keyHandler);
|
|
99
|
+
}
|
|
100
|
+
// ─── Tooltip Positioning ─────────────────────────────────────────────────────
|
|
101
|
+
function initTooltips() {
|
|
102
|
+
const showHandler = (e) => {
|
|
103
|
+
const target = e.target;
|
|
104
|
+
const trigger = target.closest('[data-ux4g-tooltip]');
|
|
105
|
+
if (trigger) {
|
|
106
|
+
const tooltipId = trigger.getAttribute('data-ux4g-tooltip');
|
|
107
|
+
if (tooltipId) {
|
|
108
|
+
const tooltip = document.getElementById(tooltipId);
|
|
109
|
+
if (tooltip) {
|
|
110
|
+
tooltip.classList.add('show');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const hideHandler = (e) => {
|
|
116
|
+
const target = e.target;
|
|
117
|
+
const trigger = target.closest('[data-ux4g-tooltip]');
|
|
118
|
+
if (trigger) {
|
|
119
|
+
const tooltipId = trigger.getAttribute('data-ux4g-tooltip');
|
|
120
|
+
if (tooltipId) {
|
|
121
|
+
const tooltip = document.getElementById(tooltipId);
|
|
122
|
+
if (tooltip) {
|
|
123
|
+
tooltip.classList.remove('show');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
addListener(document, 'mouseenter', showHandler);
|
|
129
|
+
addListener(document, 'mouseleave', hideHandler);
|
|
130
|
+
addListener(document, 'focusin', showHandler);
|
|
131
|
+
addListener(document, 'focusout', hideHandler);
|
|
132
|
+
}
|
|
133
|
+
// ─── Accordion Expand/Collapse ───────────────────────────────────────────────
|
|
134
|
+
function initAccordions() {
|
|
135
|
+
const handler = (e) => {
|
|
136
|
+
const target = e.target;
|
|
137
|
+
const trigger = target.closest('[data-ux4g-accordion-toggle]');
|
|
138
|
+
if (trigger) {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
const item = trigger.closest('.ux4g-accordion-item');
|
|
141
|
+
if (item) {
|
|
142
|
+
const isExpanded = item.classList.contains('is-expanded');
|
|
143
|
+
item.classList.toggle('is-expanded', !isExpanded);
|
|
144
|
+
// Update aria-expanded attribute
|
|
145
|
+
trigger.setAttribute('aria-expanded', String(!isExpanded));
|
|
146
|
+
// Toggle the panel visibility
|
|
147
|
+
const panel = item.querySelector('.ux4g-accordion-panel');
|
|
148
|
+
if (panel) {
|
|
149
|
+
panel.style.display = isExpanded ? 'none' : '';
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
addListener(document, 'click', handler);
|
|
155
|
+
}
|
|
156
|
+
// ─── Carousel Sliding ────────────────────────────────────────────────────────
|
|
157
|
+
function initCarousels() {
|
|
158
|
+
const handler = (e) => {
|
|
159
|
+
const target = e.target;
|
|
160
|
+
// Next button
|
|
161
|
+
const nextBtn = target.closest('[data-ux4g-carousel-next]');
|
|
162
|
+
if (nextBtn) {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
const carousel = nextBtn.closest('.ux4g-carousel');
|
|
165
|
+
if (carousel) {
|
|
166
|
+
const items = carousel.querySelectorAll('.ux4g-carousel-item');
|
|
167
|
+
const activeItem = carousel.querySelector('.ux4g-carousel-item.active');
|
|
168
|
+
if (activeItem && items.length > 0) {
|
|
169
|
+
const currentIndex = Array.from(items).indexOf(activeItem);
|
|
170
|
+
const nextIndex = (currentIndex + 1) % items.length;
|
|
171
|
+
activeItem.classList.remove('active');
|
|
172
|
+
items[nextIndex].classList.add('active');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Previous button
|
|
178
|
+
const prevBtn = target.closest('[data-ux4g-carousel-prev]');
|
|
179
|
+
if (prevBtn) {
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
const carousel = prevBtn.closest('.ux4g-carousel');
|
|
182
|
+
if (carousel) {
|
|
183
|
+
const items = carousel.querySelectorAll('.ux4g-carousel-item');
|
|
184
|
+
const activeItem = carousel.querySelector('.ux4g-carousel-item.active');
|
|
185
|
+
if (activeItem && items.length > 0) {
|
|
186
|
+
const currentIndex = Array.from(items).indexOf(activeItem);
|
|
187
|
+
const prevIndex = (currentIndex - 1 + items.length) % items.length;
|
|
188
|
+
activeItem.classList.remove('active');
|
|
189
|
+
items[prevIndex].classList.add('active');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
addListener(document, 'click', handler);
|
|
195
|
+
}
|
|
196
|
+
// ─── Drawer Open/Close ───────────────────────────────────────────────────────
|
|
197
|
+
function initDrawers() {
|
|
198
|
+
const openHandler = (e) => {
|
|
199
|
+
const target = e.target;
|
|
200
|
+
const trigger = target.closest('[data-ux4g-drawer-open]');
|
|
201
|
+
if (trigger) {
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
const drawerId = trigger.getAttribute('data-ux4g-drawer-open');
|
|
204
|
+
if (drawerId) {
|
|
205
|
+
const drawer = document.getElementById(drawerId);
|
|
206
|
+
if (drawer) {
|
|
207
|
+
drawer.classList.add('ux4g-drawer-open');
|
|
208
|
+
document.body.style.overflow = 'hidden';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const closeHandler = (e) => {
|
|
214
|
+
const target = e.target;
|
|
215
|
+
const closeBtn = target.closest('[data-ux4g-drawer-close]');
|
|
216
|
+
if (closeBtn) {
|
|
217
|
+
e.preventDefault();
|
|
218
|
+
const drawer = closeBtn.closest('.ux4g-drawer');
|
|
219
|
+
if (drawer) {
|
|
220
|
+
drawer.classList.remove('ux4g-drawer-open');
|
|
221
|
+
document.body.style.overflow = '';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const keyHandler = (e) => {
|
|
226
|
+
if (e.key === 'Escape') {
|
|
227
|
+
const openDrawer = document.querySelector('.ux4g-drawer.ux4g-drawer-open');
|
|
228
|
+
if (openDrawer) {
|
|
229
|
+
openDrawer.classList.remove('ux4g-drawer-open');
|
|
230
|
+
document.body.style.overflow = '';
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
addListener(document, 'click', openHandler);
|
|
235
|
+
addListener(document, 'click', closeHandler);
|
|
236
|
+
addListener(document, 'keydown', keyHandler);
|
|
237
|
+
}
|
|
238
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
239
|
+
/**
|
|
240
|
+
* Initialize the UX4G runtime.
|
|
241
|
+
*
|
|
242
|
+
* Encapsulates all interactive behaviors from the CDN scripts (ux4g.js, ux4g-custom.js):
|
|
243
|
+
* - Dropdown toggling
|
|
244
|
+
* - Modal management (open/close/escape/backdrop click)
|
|
245
|
+
* - Tooltip positioning (show/hide on hover/focus)
|
|
246
|
+
* - Accordion expand/collapse
|
|
247
|
+
* - Carousel sliding (next/prev)
|
|
248
|
+
* - Drawer open/close
|
|
249
|
+
*
|
|
250
|
+
* Uses a singleton guard to ensure initialization happens exactly once.
|
|
251
|
+
* Safe for SSR — no-ops when window/document are unavailable.
|
|
252
|
+
*/
|
|
253
|
+
function initRuntime() {
|
|
254
|
+
// Guard against non-browser environments (SSR/Node.js)
|
|
255
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// Singleton guard: prevent duplicate initialization
|
|
259
|
+
if (window.__UX4G_RUNTIME_INITIALIZED__) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// Initialize all interactive behaviors
|
|
263
|
+
initDropdowns();
|
|
264
|
+
initModals();
|
|
265
|
+
initTooltips();
|
|
266
|
+
initAccordions();
|
|
267
|
+
initCarousels();
|
|
268
|
+
initDrawers();
|
|
269
|
+
// Mark as initialized
|
|
270
|
+
window.__UX4G_RUNTIME_INITIALIZED__ = true;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Destroy the UX4G runtime.
|
|
274
|
+
*
|
|
275
|
+
* Resets the singleton guard and removes all event listeners.
|
|
276
|
+
* Useful for testing and SSR cleanup.
|
|
277
|
+
*/
|
|
278
|
+
function destroyRuntime() {
|
|
279
|
+
// Guard against non-browser environments
|
|
280
|
+
if (typeof window === 'undefined') {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
// Remove all registered event listeners
|
|
284
|
+
for (const { target, event, handler } of activeListeners) {
|
|
285
|
+
target.removeEventListener(event, handler);
|
|
286
|
+
}
|
|
287
|
+
activeListeners = [];
|
|
288
|
+
// Disconnect all observers
|
|
289
|
+
for (const observer of activeObservers) {
|
|
290
|
+
observer.disconnect();
|
|
291
|
+
}
|
|
292
|
+
activeObservers = [];
|
|
293
|
+
// Reset the singleton guard
|
|
294
|
+
window.__UX4G_RUNTIME_INITIALIZED__ = undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export { destroyRuntime, initRuntime };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ux4g-components-web",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "UX4G Design System — CSS bundle, design tokens, utilities, and shared Class_Builder types",
|
|
6
6
|
"scripts": {
|
|
@@ -19,19 +19,38 @@
|
|
|
19
19
|
"import": "./dist/types/index.mjs",
|
|
20
20
|
"require": "./dist/types/index.cjs",
|
|
21
21
|
"default": "./dist/types/index.mjs"
|
|
22
|
+
},
|
|
23
|
+
"./runtime": {
|
|
24
|
+
"types": "./dist/runtime/index.d.ts",
|
|
25
|
+
"import": "./dist/runtime/index.mjs",
|
|
26
|
+
"require": "./dist/runtime/index.cjs",
|
|
27
|
+
"default": "./dist/runtime/index.mjs"
|
|
28
|
+
},
|
|
29
|
+
"./runtime/bootstrap": {
|
|
30
|
+
"types": "./dist/runtime/bootstrap.d.ts",
|
|
31
|
+
"import": "./dist/runtime/bootstrap.mjs",
|
|
32
|
+
"require": "./dist/runtime/bootstrap.cjs",
|
|
33
|
+
"default": "./dist/runtime/bootstrap.mjs"
|
|
22
34
|
}
|
|
23
35
|
},
|
|
24
36
|
"typesVersions": {
|
|
25
37
|
"*": {
|
|
26
38
|
"types": [
|
|
27
39
|
"./dist/types/index.d.ts"
|
|
40
|
+
],
|
|
41
|
+
"runtime": [
|
|
42
|
+
"./dist/runtime/index.d.ts"
|
|
43
|
+
],
|
|
44
|
+
"runtime/bootstrap": [
|
|
45
|
+
"./dist/runtime/bootstrap.d.ts"
|
|
28
46
|
]
|
|
29
47
|
}
|
|
30
48
|
},
|
|
31
49
|
"files": [
|
|
32
50
|
"styles/ux4g.css",
|
|
33
51
|
"styles/cascade-fixes.css",
|
|
34
|
-
"dist/"
|
|
52
|
+
"dist/",
|
|
53
|
+
"dist/runtime/"
|
|
35
54
|
],
|
|
36
55
|
"devDependencies": {
|
|
37
56
|
"@rollup/plugin-typescript": "^12.0.0",
|