@whykusanagi/corrupted-theme 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/README.md +207 -42
- package/docs/COMPONENTS_REFERENCE.md +142 -35
- package/docs/governance/VERSION_MANAGEMENT.md +2 -2
- package/docs/governance/VERSION_REFERENCES.md +30 -32
- package/docs/platforms/NPM_PACKAGE.md +8 -7
- package/examples/basic/multi-gallery.html +155 -0
- package/examples/button.html +5 -2
- package/examples/card.html +5 -2
- package/examples/extensions-showcase.html +5 -2
- package/examples/form.html +5 -2
- package/examples/index.html +8 -5
- package/examples/interactive-components.html +223 -0
- package/examples/layout.html +5 -2
- package/examples/nikke-team-builder.html +6 -3
- package/examples/showcase-complete.html +14 -13
- package/examples/showcase.html +6 -3
- package/package.json +6 -5
- package/src/core/corrupted-text.js +25 -5
- package/src/core/event-tracker.js +46 -0
- package/src/core/timer-registry.js +94 -0
- package/src/core/typing-animation.js +36 -17
- package/src/css/components.css +108 -0
- package/src/lib/carousel.js +308 -0
- package/src/lib/celeste-widget.js +178 -47
- package/src/lib/character-corruption.js +33 -8
- package/src/lib/components.js +357 -25
- package/src/lib/corrupted-text.js +21 -5
- package/src/lib/corruption-loading.js +40 -10
- package/src/lib/countdown-widget.js +25 -6
- package/src/lib/gallery.js +420 -354
package/src/lib/components.js
CHANGED
|
@@ -4,13 +4,25 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Provides helper functions for:
|
|
6
6
|
* - Accordion/Collapse
|
|
7
|
+
* - Modal
|
|
8
|
+
* - Dropdown
|
|
9
|
+
* - Tabs
|
|
7
10
|
* - Toast Notifications
|
|
8
|
-
* - Auto-initialization
|
|
11
|
+
* - Auto-initialization via data-ct-* attributes
|
|
9
12
|
*
|
|
10
13
|
* @module components
|
|
11
|
-
* @version
|
|
14
|
+
* @version 2.0.0
|
|
12
15
|
*/
|
|
13
16
|
|
|
17
|
+
import { EventTracker } from '../core/event-tracker.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Shared tracker for listeners created during auto-initialization.
|
|
21
|
+
* Cleaned up by destroyComponents().
|
|
22
|
+
* @private
|
|
23
|
+
*/
|
|
24
|
+
const _initTracker = new EventTracker();
|
|
25
|
+
|
|
14
26
|
// ========== ACCORDION / COLLAPSE ==========
|
|
15
27
|
|
|
16
28
|
/**
|
|
@@ -28,7 +40,7 @@ export function initAccordions() {
|
|
|
28
40
|
const header = item.querySelector('.accordion-header');
|
|
29
41
|
if (!header) return;
|
|
30
42
|
|
|
31
|
-
|
|
43
|
+
_initTracker.add(header, 'click', () => {
|
|
32
44
|
const wasActive = item.classList.contains('active');
|
|
33
45
|
|
|
34
46
|
// Close all items in this accordion (unless it's already active)
|
|
@@ -137,32 +149,38 @@ class ToastManager {
|
|
|
137
149
|
const toast = document.createElement('div');
|
|
138
150
|
toast.className = `toast ${type}`;
|
|
139
151
|
|
|
140
|
-
// Create toast content
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
// Create toast content using safe DOM methods
|
|
153
|
+
if (title) {
|
|
154
|
+
const header = document.createElement('div');
|
|
155
|
+
header.className = 'toast-header';
|
|
156
|
+
const titleSpan = document.createElement('span');
|
|
157
|
+
titleSpan.textContent = title;
|
|
158
|
+
header.appendChild(titleSpan);
|
|
159
|
+
toast.appendChild(header);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const body = document.createElement('div');
|
|
163
|
+
body.className = 'toast-body';
|
|
164
|
+
body.textContent = message;
|
|
165
|
+
toast.appendChild(body);
|
|
166
|
+
|
|
167
|
+
const closeBtn = document.createElement('button');
|
|
168
|
+
closeBtn.className = 'toast-close';
|
|
169
|
+
closeBtn.setAttribute('aria-label', 'Close');
|
|
170
|
+
closeBtn.textContent = '\u00d7';
|
|
155
171
|
closeBtn.addEventListener('click', () => {
|
|
156
172
|
this.dismiss(toast, onClose);
|
|
157
173
|
});
|
|
174
|
+
toast.appendChild(closeBtn);
|
|
158
175
|
|
|
159
176
|
// Add to container
|
|
160
177
|
container.appendChild(toast);
|
|
161
178
|
this.toasts.push(toast);
|
|
162
179
|
|
|
163
|
-
// Auto-dismiss
|
|
180
|
+
// Auto-dismiss (tracked for cleanup on manual dismiss)
|
|
164
181
|
if (duration > 0) {
|
|
165
|
-
setTimeout(() => {
|
|
182
|
+
toast._autoDismissId = setTimeout(() => {
|
|
183
|
+
toast._autoDismissId = null;
|
|
166
184
|
this.dismiss(toast, onClose);
|
|
167
185
|
}, duration);
|
|
168
186
|
}
|
|
@@ -178,6 +196,12 @@ class ToastManager {
|
|
|
178
196
|
dismiss(toast, onClose = null) {
|
|
179
197
|
if (!toast || !document.contains(toast)) return;
|
|
180
198
|
|
|
199
|
+
// Clear auto-dismiss timer if dismissing early
|
|
200
|
+
if (toast._autoDismissId) {
|
|
201
|
+
clearTimeout(toast._autoDismissId);
|
|
202
|
+
toast._autoDismissId = null;
|
|
203
|
+
}
|
|
204
|
+
|
|
181
205
|
toast.classList.add('hiding');
|
|
182
206
|
|
|
183
207
|
setTimeout(() => {
|
|
@@ -201,7 +225,19 @@ class ToastManager {
|
|
|
201
225
|
* Dismiss all toasts
|
|
202
226
|
*/
|
|
203
227
|
dismissAll() {
|
|
204
|
-
this.toasts.forEach(toast => this.dismiss(toast));
|
|
228
|
+
[...this.toasts].forEach(toast => this.dismiss(toast));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Tear down the toast system: dismiss all toasts and remove container.
|
|
233
|
+
*/
|
|
234
|
+
destroy() {
|
|
235
|
+
this.dismissAll();
|
|
236
|
+
if (this.container && this.container.parentNode) {
|
|
237
|
+
this.container.parentNode.removeChild(this.container);
|
|
238
|
+
}
|
|
239
|
+
this.container = null;
|
|
240
|
+
this.toasts = [];
|
|
205
241
|
}
|
|
206
242
|
|
|
207
243
|
// Convenience methods for different toast types
|
|
@@ -247,16 +283,305 @@ export const toast = {
|
|
|
247
283
|
dismissAll: () => toastManager.dismissAll()
|
|
248
284
|
};
|
|
249
285
|
|
|
286
|
+
// ========== MODAL ==========
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Modal manager with lifecycle management
|
|
290
|
+
*
|
|
291
|
+
* Usage:
|
|
292
|
+
* ```html
|
|
293
|
+
* <button data-ct-toggle="modal" data-ct-target="#my-modal">Open</button>
|
|
294
|
+
* <div class="modal-overlay" id="my-modal">
|
|
295
|
+
* <div class="modal">
|
|
296
|
+
* <div class="modal-header">
|
|
297
|
+
* <h3 class="modal-title">Title</h3>
|
|
298
|
+
* <button class="modal-close">×</button>
|
|
299
|
+
* </div>
|
|
300
|
+
* <div class="modal-body">Content</div>
|
|
301
|
+
* </div>
|
|
302
|
+
* </div>
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
class ModalManager {
|
|
306
|
+
constructor() {
|
|
307
|
+
this._events = new EventTracker();
|
|
308
|
+
this._initialized = new Set();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Initialize a modal overlay
|
|
313
|
+
* @param {string|HTMLElement} selector - Modal overlay selector or element
|
|
314
|
+
*/
|
|
315
|
+
init(selector) {
|
|
316
|
+
const overlay = typeof selector === 'string'
|
|
317
|
+
? document.querySelector(selector) : selector;
|
|
318
|
+
if (!overlay || this._initialized.has(overlay)) return;
|
|
319
|
+
|
|
320
|
+
const closeBtn = overlay.querySelector('.modal-close');
|
|
321
|
+
if (closeBtn) {
|
|
322
|
+
this._events.add(closeBtn, 'click', () => this.close(overlay));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Close on overlay click (outside modal content)
|
|
326
|
+
this._events.add(overlay, 'click', (e) => {
|
|
327
|
+
if (e.target === overlay) this.close(overlay);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
this._initialized.add(overlay);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Open a modal
|
|
335
|
+
* @param {string|HTMLElement} selector - Modal overlay selector or element
|
|
336
|
+
*/
|
|
337
|
+
open(selector) {
|
|
338
|
+
const overlay = typeof selector === 'string'
|
|
339
|
+
? document.querySelector(selector) : selector;
|
|
340
|
+
if (!overlay) return;
|
|
341
|
+
|
|
342
|
+
overlay.classList.add('active');
|
|
343
|
+
document.body.style.overflow = 'hidden';
|
|
344
|
+
|
|
345
|
+
overlay.dispatchEvent(new CustomEvent('modal:open', { bubbles: true }));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Close a modal
|
|
350
|
+
* @param {string|HTMLElement} selector - Modal overlay selector or element
|
|
351
|
+
*/
|
|
352
|
+
close(selector) {
|
|
353
|
+
const overlay = typeof selector === 'string'
|
|
354
|
+
? document.querySelector(selector) : selector;
|
|
355
|
+
if (!overlay) return;
|
|
356
|
+
|
|
357
|
+
overlay.classList.remove('active');
|
|
358
|
+
document.body.style.overflow = '';
|
|
359
|
+
|
|
360
|
+
overlay.dispatchEvent(new CustomEvent('modal:close', { bubbles: true }));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Tear down all tracked listeners and state
|
|
365
|
+
*/
|
|
366
|
+
destroy() {
|
|
367
|
+
this._events.removeAll();
|
|
368
|
+
this._initialized.clear();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const modalManager = new ModalManager();
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Open a modal by selector
|
|
376
|
+
* @param {string|HTMLElement} selector
|
|
377
|
+
*/
|
|
378
|
+
export function openModal(selector) {
|
|
379
|
+
modalManager.open(selector);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Close a modal by selector
|
|
384
|
+
* @param {string|HTMLElement} selector
|
|
385
|
+
*/
|
|
386
|
+
export function closeModal(selector) {
|
|
387
|
+
modalManager.close(selector);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ========== DROPDOWN ==========
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Dropdown manager with click-outside-to-close
|
|
394
|
+
*
|
|
395
|
+
* Usage:
|
|
396
|
+
* ```html
|
|
397
|
+
* <div class="dropdown">
|
|
398
|
+
* <button class="dropdown-toggle" data-ct-toggle="dropdown">Menu</button>
|
|
399
|
+
* <div class="dropdown-menu">
|
|
400
|
+
* <a class="dropdown-item" href="#">Item 1</a>
|
|
401
|
+
* </div>
|
|
402
|
+
* </div>
|
|
403
|
+
* ```
|
|
404
|
+
*/
|
|
405
|
+
class DropdownManager {
|
|
406
|
+
constructor() {
|
|
407
|
+
this._events = new EventTracker();
|
|
408
|
+
this._initialized = false;
|
|
409
|
+
this._outsideClickBound = false;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Initialize dropdown toggle behavior
|
|
414
|
+
* @param {HTMLElement} toggle - The dropdown-toggle element
|
|
415
|
+
*/
|
|
416
|
+
init(toggle) {
|
|
417
|
+
const menu = toggle.parentElement?.querySelector('.dropdown-menu');
|
|
418
|
+
if (!menu) return;
|
|
419
|
+
|
|
420
|
+
this._events.add(toggle, 'click', (e) => {
|
|
421
|
+
e.stopPropagation();
|
|
422
|
+
const isOpen = menu.classList.contains('active');
|
|
423
|
+
|
|
424
|
+
// Close all other dropdowns first
|
|
425
|
+
this.closeAll();
|
|
426
|
+
|
|
427
|
+
if (!isOpen) {
|
|
428
|
+
menu.classList.add('active');
|
|
429
|
+
toggle.classList.add('active');
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// Setup global click-outside handler once
|
|
434
|
+
if (!this._outsideClickBound) {
|
|
435
|
+
this._events.add(document, 'click', () => this.closeAll());
|
|
436
|
+
this._outsideClickBound = true;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Close all open dropdowns
|
|
442
|
+
*/
|
|
443
|
+
closeAll() {
|
|
444
|
+
document.querySelectorAll('.dropdown-menu.active').forEach(menu => {
|
|
445
|
+
menu.classList.remove('active');
|
|
446
|
+
});
|
|
447
|
+
document.querySelectorAll('.dropdown-toggle.active').forEach(toggle => {
|
|
448
|
+
toggle.classList.remove('active');
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Tear down all tracked listeners
|
|
454
|
+
*/
|
|
455
|
+
destroy() {
|
|
456
|
+
this.closeAll();
|
|
457
|
+
this._events.removeAll();
|
|
458
|
+
this._outsideClickBound = false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const dropdownManager = new DropdownManager();
|
|
463
|
+
|
|
464
|
+
// ========== TABS ==========
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Tab manager
|
|
468
|
+
*
|
|
469
|
+
* Usage:
|
|
470
|
+
* ```html
|
|
471
|
+
* <div class="tabs">
|
|
472
|
+
* <button class="tab active" data-ct-target="#panel-1">Tab 1</button>
|
|
473
|
+
* <button class="tab" data-ct-target="#panel-2">Tab 2</button>
|
|
474
|
+
* </div>
|
|
475
|
+
* <div class="tab-content active" id="panel-1">Panel 1</div>
|
|
476
|
+
* <div class="tab-content" id="panel-2">Panel 2</div>
|
|
477
|
+
* ```
|
|
478
|
+
*/
|
|
479
|
+
class TabManager {
|
|
480
|
+
constructor() {
|
|
481
|
+
this._events = new EventTracker();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Initialize a tab container
|
|
486
|
+
* @param {HTMLElement} tabsContainer - The .tabs element
|
|
487
|
+
*/
|
|
488
|
+
init(tabsContainer) {
|
|
489
|
+
const tabs = tabsContainer.querySelectorAll('.tab[data-ct-target]');
|
|
490
|
+
|
|
491
|
+
tabs.forEach(tab => {
|
|
492
|
+
this._events.add(tab, 'click', () => {
|
|
493
|
+
// Deactivate all sibling tabs
|
|
494
|
+
tabsContainer.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
495
|
+
|
|
496
|
+
// Hide all associated panels
|
|
497
|
+
tabs.forEach(t => {
|
|
498
|
+
const panel = document.querySelector(t.dataset.ctTarget);
|
|
499
|
+
if (panel) panel.classList.remove('active');
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Activate clicked tab and its panel
|
|
503
|
+
tab.classList.add('active');
|
|
504
|
+
const panel = document.querySelector(tab.dataset.ctTarget);
|
|
505
|
+
if (panel) panel.classList.add('active');
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Tear down all tracked listeners
|
|
512
|
+
*/
|
|
513
|
+
destroy() {
|
|
514
|
+
this._events.removeAll();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const tabManager = new TabManager();
|
|
519
|
+
|
|
250
520
|
// ========== AUTO-INITIALIZATION ==========
|
|
251
521
|
|
|
252
522
|
/**
|
|
253
|
-
* Initialize all components on page load
|
|
523
|
+
* Initialize all components on page load.
|
|
524
|
+
* Scans for data-ct-* attributes and wires up behavior.
|
|
525
|
+
* All listeners are tracked via _initTracker or manager EventTrackers.
|
|
254
526
|
*/
|
|
255
527
|
function initComponents() {
|
|
256
|
-
//
|
|
528
|
+
// Accordions
|
|
257
529
|
if (document.querySelector('.accordion')) {
|
|
258
530
|
initAccordions();
|
|
259
531
|
}
|
|
532
|
+
|
|
533
|
+
// Modals — init overlays and wire triggers
|
|
534
|
+
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
|
535
|
+
modalManager.init(overlay);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
document.querySelectorAll('[data-ct-toggle="modal"]').forEach(trigger => {
|
|
539
|
+
const targetSel = trigger.dataset.ctTarget;
|
|
540
|
+
if (!targetSel) return;
|
|
541
|
+
_initTracker.add(trigger, 'click', () => modalManager.open(targetSel));
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Escape key closes active modals
|
|
545
|
+
if (document.querySelector('.modal-overlay')) {
|
|
546
|
+
_initTracker.add(document, 'keydown', (e) => {
|
|
547
|
+
if (e.key === 'Escape') {
|
|
548
|
+
document.querySelectorAll('.modal-overlay.active').forEach(overlay => {
|
|
549
|
+
modalManager.close(overlay);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Dropdowns
|
|
556
|
+
document.querySelectorAll('[data-ct-toggle="dropdown"]').forEach(toggle => {
|
|
557
|
+
dropdownManager.init(toggle);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// Tabs
|
|
561
|
+
document.querySelectorAll('.tabs').forEach(tabsContainer => {
|
|
562
|
+
if (tabsContainer.querySelector('.tab[data-ct-target]')) {
|
|
563
|
+
tabManager.init(tabsContainer);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Collapse triggers
|
|
568
|
+
document.querySelectorAll('[data-ct-toggle="collapse"]').forEach(trigger => {
|
|
569
|
+
const targetSel = trigger.dataset.ctTarget;
|
|
570
|
+
if (!targetSel) return;
|
|
571
|
+
_initTracker.add(trigger, 'click', () => toggleCollapse(targetSel));
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Destroy all component managers and clean up listeners.
|
|
577
|
+
* Removes all tracked listeners from auto-initialization and managers.
|
|
578
|
+
*/
|
|
579
|
+
export function destroyComponents() {
|
|
580
|
+
_initTracker.removeAll();
|
|
581
|
+
modalManager.destroy();
|
|
582
|
+
dropdownManager.destroy();
|
|
583
|
+
tabManager.destroy();
|
|
584
|
+
toastManager.destroy();
|
|
260
585
|
}
|
|
261
586
|
|
|
262
587
|
// Auto-initialize on DOM ready
|
|
@@ -271,13 +596,20 @@ if (typeof window !== 'undefined') {
|
|
|
271
596
|
// ========== EXPORTS ==========
|
|
272
597
|
|
|
273
598
|
export default {
|
|
274
|
-
// Accordion
|
|
599
|
+
// Accordion / Collapse
|
|
275
600
|
initAccordions,
|
|
276
601
|
toggleCollapse,
|
|
277
602
|
showCollapse,
|
|
278
603
|
hideCollapse,
|
|
279
604
|
|
|
605
|
+
// Modal
|
|
606
|
+
openModal,
|
|
607
|
+
closeModal,
|
|
608
|
+
|
|
280
609
|
// Toast
|
|
281
610
|
showToast,
|
|
282
|
-
toast
|
|
611
|
+
toast,
|
|
612
|
+
|
|
613
|
+
// Lifecycle
|
|
614
|
+
destroyComponents
|
|
283
615
|
};
|
|
@@ -46,6 +46,9 @@ class CorruptedText {
|
|
|
46
46
|
this.currentVariantIndex = 0;
|
|
47
47
|
this.isAnimating = false;
|
|
48
48
|
this.animationFrame = null;
|
|
49
|
+
this._startDelayId = null;
|
|
50
|
+
this._animateTimeoutId = null;
|
|
51
|
+
this._corruptTimeoutId = null;
|
|
49
52
|
|
|
50
53
|
this.init();
|
|
51
54
|
}
|
|
@@ -61,7 +64,7 @@ class CorruptedText {
|
|
|
61
64
|
|
|
62
65
|
// Start animation after delay
|
|
63
66
|
if (this.options.startDelay > 0) {
|
|
64
|
-
setTimeout(() => this.start(), this.options.startDelay);
|
|
67
|
+
this._startDelayId = setTimeout(() => this.start(), this.options.startDelay);
|
|
65
68
|
} else {
|
|
66
69
|
this.start();
|
|
67
70
|
}
|
|
@@ -75,9 +78,22 @@ class CorruptedText {
|
|
|
75
78
|
|
|
76
79
|
stop() {
|
|
77
80
|
this.isAnimating = false;
|
|
78
|
-
if (this.animationFrame)
|
|
79
|
-
|
|
81
|
+
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
|
|
82
|
+
if (this._startDelayId) clearTimeout(this._startDelayId);
|
|
83
|
+
if (this._animateTimeoutId) clearTimeout(this._animateTimeoutId);
|
|
84
|
+
if (this._corruptTimeoutId) clearTimeout(this._corruptTimeoutId);
|
|
85
|
+
this.animationFrame = null;
|
|
86
|
+
this._startDelayId = null;
|
|
87
|
+
this._animateTimeoutId = null;
|
|
88
|
+
this._corruptTimeoutId = null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
destroy() {
|
|
92
|
+
this.stop();
|
|
93
|
+
if (this.element && this.element.corruptedTextInstance === this) {
|
|
94
|
+
delete this.element.corruptedTextInstance;
|
|
80
95
|
}
|
|
96
|
+
this.element = null;
|
|
81
97
|
}
|
|
82
98
|
|
|
83
99
|
animate() {
|
|
@@ -99,7 +115,7 @@ class CorruptedText {
|
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
// Continue animation
|
|
102
|
-
setTimeout(() => {
|
|
118
|
+
this._animateTimeoutId = setTimeout(() => {
|
|
103
119
|
if (this.isAnimating) {
|
|
104
120
|
this.animate();
|
|
105
121
|
}
|
|
@@ -147,7 +163,7 @@ class CorruptedText {
|
|
|
147
163
|
step++;
|
|
148
164
|
|
|
149
165
|
this.animationFrame = requestAnimationFrame(() => {
|
|
150
|
-
setTimeout(corrupt, this.options.cycleDelay);
|
|
166
|
+
this._corruptTimeoutId = setTimeout(corrupt, this.options.cycleDelay);
|
|
151
167
|
});
|
|
152
168
|
};
|
|
153
169
|
|
|
@@ -31,6 +31,34 @@
|
|
|
31
31
|
localStorage.setItem("corruptionLoadingLastPlayed", Date.now().toString());
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Inline timer tracking (IIFE can't import ES modules)
|
|
35
|
+
const _timers = { _t: new Set(), _i: new Set() };
|
|
36
|
+
function _setTimeout(fn, delay) {
|
|
37
|
+
const id = setTimeout(() => { _timers._t.delete(id); fn(); }, delay);
|
|
38
|
+
_timers._t.add(id);
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
41
|
+
function _setInterval(fn, delay) {
|
|
42
|
+
const id = setInterval(fn, delay);
|
|
43
|
+
_timers._i.add(id);
|
|
44
|
+
return id;
|
|
45
|
+
}
|
|
46
|
+
function _clearAllTimers() {
|
|
47
|
+
_timers._t.forEach(id => clearTimeout(id));
|
|
48
|
+
_timers._i.forEach(id => clearInterval(id));
|
|
49
|
+
_timers._t.clear();
|
|
50
|
+
_timers._i.clear();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Cancel loading screen early
|
|
54
|
+
function cancelLoading() {
|
|
55
|
+
_clearAllTimers();
|
|
56
|
+
const screen = document.getElementById('corruption-loading');
|
|
57
|
+
if (screen) screen.remove();
|
|
58
|
+
const styles = document.getElementById('corruption-loading-styles');
|
|
59
|
+
if (styles) styles.remove();
|
|
60
|
+
}
|
|
61
|
+
|
|
34
62
|
// Main function to show loading screen
|
|
35
63
|
function showCorruptionLoading(options = {}) {
|
|
36
64
|
const config = {
|
|
@@ -51,7 +79,7 @@
|
|
|
51
79
|
// Inject styles
|
|
52
80
|
const style = document.createElement("style");
|
|
53
81
|
style.id = "corruption-loading-styles";
|
|
54
|
-
style.
|
|
82
|
+
style.textContent = `
|
|
55
83
|
@keyframes flicker {
|
|
56
84
|
0%, 100% { opacity: 1; }
|
|
57
85
|
50% { opacity: 0.4; }
|
|
@@ -235,6 +263,7 @@
|
|
|
235
263
|
const loadingScreen = document.createElement("div");
|
|
236
264
|
loadingScreen.id = "corruption-loading-screen";
|
|
237
265
|
|
|
266
|
+
// Static HTML only — no interpolated variables, safe from XSS
|
|
238
267
|
loadingScreen.innerHTML = `
|
|
239
268
|
<div class="corrupt-stream"></div>
|
|
240
269
|
<div class="crt-overlay"></div>
|
|
@@ -306,8 +335,8 @@
|
|
|
306
335
|
|
|
307
336
|
target.innerHTML = '';
|
|
308
337
|
let i = 0;
|
|
309
|
-
const interval =
|
|
310
|
-
if (i >= text.length)
|
|
338
|
+
const interval = _setInterval(() => {
|
|
339
|
+
if (i >= text.length) { clearInterval(interval); _timers._i.delete(interval); return; }
|
|
311
340
|
const span = document.createElement('span');
|
|
312
341
|
span.className = 'glyph';
|
|
313
342
|
span.textContent = text[i];
|
|
@@ -349,16 +378,16 @@
|
|
|
349
378
|
});
|
|
350
379
|
|
|
351
380
|
// Animate progress text
|
|
352
|
-
|
|
381
|
+
_setTimeout(() => {
|
|
353
382
|
[...progressText.children].forEach((span, idx) => {
|
|
354
|
-
|
|
383
|
+
_setTimeout(() => {
|
|
355
384
|
span.textContent = phrase.replace(/\s/g, '').charAt(idx);
|
|
356
385
|
}, 500 * (idx + 1));
|
|
357
386
|
});
|
|
358
387
|
}, 3600);
|
|
359
388
|
|
|
360
389
|
// Type grow text
|
|
361
|
-
|
|
390
|
+
_setTimeout(() => {
|
|
362
391
|
const growText = document.getElementById("grow-text");
|
|
363
392
|
if (growText) {
|
|
364
393
|
typeGlyphText("grow-text", "Initializing corruption protocols...", 80);
|
|
@@ -366,9 +395,10 @@
|
|
|
366
395
|
}, 2000);
|
|
367
396
|
|
|
368
397
|
// Remove loading screen
|
|
369
|
-
|
|
398
|
+
_setTimeout(() => {
|
|
370
399
|
loadingScreen.style.opacity = "0";
|
|
371
|
-
|
|
400
|
+
_setTimeout(() => {
|
|
401
|
+
_clearAllTimers();
|
|
372
402
|
loadingScreen.remove();
|
|
373
403
|
const styles = document.getElementById("corruption-loading-styles");
|
|
374
404
|
if (styles) styles.remove();
|
|
@@ -394,12 +424,12 @@
|
|
|
394
424
|
|
|
395
425
|
// Export for manual use
|
|
396
426
|
window.showCorruptionLoading = showCorruptionLoading;
|
|
397
|
-
window.CorruptionLoading = { show: showCorruptionLoading };
|
|
427
|
+
window.CorruptionLoading = { show: showCorruptionLoading, cancel: cancelLoading };
|
|
398
428
|
}
|
|
399
429
|
|
|
400
430
|
// Export for modules
|
|
401
431
|
if (typeof module !== 'undefined' && module.exports) {
|
|
402
|
-
module.exports = { showCorruptionLoading };
|
|
432
|
+
module.exports = { showCorruptionLoading, cancelLoading };
|
|
403
433
|
}
|
|
404
434
|
})();
|
|
405
435
|
|
|
@@ -134,6 +134,8 @@ let state = {
|
|
|
134
134
|
config: null,
|
|
135
135
|
countdownInterval: null,
|
|
136
136
|
popupInterval: null,
|
|
137
|
+
popupInitTimeout: null,
|
|
138
|
+
popupDurationTimeout: null,
|
|
137
139
|
isCompleted: false
|
|
138
140
|
};
|
|
139
141
|
|
|
@@ -314,7 +316,7 @@ function renderWidget(config) {
|
|
|
314
316
|
if (config.popup?.message) {
|
|
315
317
|
popup = document.createElement('div');
|
|
316
318
|
popup.className = 'countdown-popup';
|
|
317
|
-
popup.
|
|
319
|
+
popup.textContent = config.popup.message;
|
|
318
320
|
|
|
319
321
|
if (config.popup.colors) {
|
|
320
322
|
if (config.popup.colors.bg) popup.style.background = config.popup.colors.bg;
|
|
@@ -460,7 +462,8 @@ function startPopup(popupConfig, popupElement) {
|
|
|
460
462
|
const duration = popupConfig.duration || 5000;
|
|
461
463
|
|
|
462
464
|
// Show popup initially after a delay
|
|
463
|
-
setTimeout(() => {
|
|
465
|
+
state.popupInitTimeout = setTimeout(() => {
|
|
466
|
+
state.popupInitTimeout = null;
|
|
464
467
|
showPopup(popupElement, duration);
|
|
465
468
|
}, 2000);
|
|
466
469
|
|
|
@@ -478,8 +481,14 @@ function startPopup(popupConfig, popupElement) {
|
|
|
478
481
|
*/
|
|
479
482
|
function showPopup(popup, duration) {
|
|
480
483
|
popup.classList.add('active');
|
|
481
|
-
|
|
482
|
-
|
|
484
|
+
|
|
485
|
+
// Clear previous duration timeout if popup is re-shown before it hides
|
|
486
|
+
if (state.popupDurationTimeout) {
|
|
487
|
+
clearTimeout(state.popupDurationTimeout);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
state.popupDurationTimeout = setTimeout(() => {
|
|
491
|
+
state.popupDurationTimeout = null;
|
|
483
492
|
popup.classList.remove('active');
|
|
484
493
|
}, duration);
|
|
485
494
|
}
|
|
@@ -584,12 +593,22 @@ export function destroyCountdown() {
|
|
|
584
593
|
clearInterval(state.countdownInterval);
|
|
585
594
|
state.countdownInterval = null;
|
|
586
595
|
}
|
|
587
|
-
|
|
596
|
+
|
|
588
597
|
if (state.popupInterval) {
|
|
589
598
|
clearInterval(state.popupInterval);
|
|
590
599
|
state.popupInterval = null;
|
|
591
600
|
}
|
|
592
|
-
|
|
601
|
+
|
|
602
|
+
if (state.popupInitTimeout) {
|
|
603
|
+
clearTimeout(state.popupInitTimeout);
|
|
604
|
+
state.popupInitTimeout = null;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (state.popupDurationTimeout) {
|
|
608
|
+
clearTimeout(state.popupDurationTimeout);
|
|
609
|
+
state.popupDurationTimeout = null;
|
|
610
|
+
}
|
|
611
|
+
|
|
593
612
|
state.config = null;
|
|
594
613
|
state.isCompleted = false;
|
|
595
614
|
|