@whykusanagi/corrupted-theme 0.1.6 → 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 +38 -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
|
@@ -284,31 +284,40 @@ class TypingAnimation {
|
|
|
284
284
|
// Color for phrase corruption (SFW vs NSFW)
|
|
285
285
|
const phraseColor = this.options.nsfw ? '#8b5cf6' : '#d94f90';
|
|
286
286
|
|
|
287
|
+
let text;
|
|
288
|
+
let color;
|
|
289
|
+
|
|
287
290
|
if (r < 0.30) {
|
|
288
291
|
// Japanese phrase buffer corruption
|
|
289
|
-
|
|
290
|
-
|
|
292
|
+
text = japaneseSet[Math.floor(Math.random() * japaneseSet.length)];
|
|
293
|
+
color = phraseColor;
|
|
291
294
|
} else if (r < 0.50) {
|
|
292
295
|
// English phrase buffer corruption
|
|
293
|
-
|
|
294
|
-
|
|
296
|
+
text = englishSet[Math.floor(Math.random() * englishSet.length)];
|
|
297
|
+
color = phraseColor;
|
|
295
298
|
} else if (r < 0.65) {
|
|
296
299
|
// Romaji buffer corruption
|
|
297
|
-
|
|
298
|
-
|
|
300
|
+
text = romajiSet[Math.floor(Math.random() * romajiSet.length)];
|
|
301
|
+
color = phraseColor;
|
|
299
302
|
} else if (r < 0.80) {
|
|
300
303
|
// Symbols - decorative corruption (always SFW)
|
|
301
|
-
|
|
304
|
+
text = TypingAnimation.SYMBOLS[
|
|
302
305
|
Math.floor(Math.random() * TypingAnimation.SYMBOLS.length)
|
|
303
306
|
];
|
|
304
|
-
|
|
307
|
+
color = '#d94f90';
|
|
305
308
|
} else {
|
|
306
309
|
// Block chars - terminal/critical state (always SFW)
|
|
307
|
-
|
|
310
|
+
text = TypingAnimation.BLOCKS[
|
|
308
311
|
Math.floor(Math.random() * TypingAnimation.BLOCKS.length)
|
|
309
312
|
];
|
|
310
|
-
|
|
313
|
+
color = '#ff0000';
|
|
311
314
|
}
|
|
315
|
+
|
|
316
|
+
// Return DOM element instead of HTML string (XSS-safe)
|
|
317
|
+
const span = document.createElement('span');
|
|
318
|
+
span.style.color = color;
|
|
319
|
+
span.textContent = text;
|
|
320
|
+
return span;
|
|
312
321
|
}
|
|
313
322
|
|
|
314
323
|
/**
|
|
@@ -320,15 +329,21 @@ class TypingAnimation {
|
|
|
320
329
|
* @private
|
|
321
330
|
*/
|
|
322
331
|
render() {
|
|
323
|
-
|
|
332
|
+
const displayed = this.getDisplayed();
|
|
333
|
+
|
|
334
|
+
// Clear and rebuild using safe DOM methods (no innerHTML)
|
|
335
|
+
this.element.textContent = '';
|
|
324
336
|
|
|
325
|
-
//
|
|
337
|
+
// Stable revealed text (white)
|
|
338
|
+
const textSpan = document.createElement('span');
|
|
339
|
+
textSpan.style.color = '#ffffff';
|
|
340
|
+
textSpan.textContent = displayed;
|
|
341
|
+
this.element.appendChild(textSpan);
|
|
342
|
+
|
|
343
|
+
// Add buffer corruption element at the "cursor" position
|
|
326
344
|
if (!this.isDone() && Math.random() < this.options.glitchChance) {
|
|
327
|
-
|
|
345
|
+
this.element.appendChild(this.getRandomCorruption());
|
|
328
346
|
}
|
|
329
|
-
|
|
330
|
-
// Rendered text: white for stable, corruption colors for buffer glitches
|
|
331
|
-
this.element.innerHTML = `<span style="color: #ffffff;">${displayed}</span>`;
|
|
332
347
|
}
|
|
333
348
|
|
|
334
349
|
/**
|
|
@@ -339,7 +354,11 @@ class TypingAnimation {
|
|
|
339
354
|
*/
|
|
340
355
|
settle(finalText) {
|
|
341
356
|
this.stop();
|
|
342
|
-
|
|
357
|
+
const span = document.createElement('span');
|
|
358
|
+
span.style.color = '#ffffff';
|
|
359
|
+
span.textContent = finalText || this.content;
|
|
360
|
+
this.element.textContent = '';
|
|
361
|
+
this.element.appendChild(span);
|
|
343
362
|
}
|
|
344
363
|
|
|
345
364
|
/**
|
package/src/css/components.css
CHANGED
|
@@ -2214,3 +2214,111 @@ nav.navbar {
|
|
|
2214
2214
|
.ratio-21x9 {
|
|
2215
2215
|
--aspect-ratio: calc(9 / 21 * 100%);
|
|
2216
2216
|
}
|
|
2217
|
+
|
|
2218
|
+
/* ========== CAROUSEL / SLIDESHOW ========== */
|
|
2219
|
+
|
|
2220
|
+
.carousel {
|
|
2221
|
+
position: relative;
|
|
2222
|
+
overflow: hidden;
|
|
2223
|
+
border-radius: var(--radius-lg);
|
|
2224
|
+
background: var(--glass);
|
|
2225
|
+
border: 1px solid var(--border);
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
.carousel:focus {
|
|
2229
|
+
outline: 2px solid var(--accent);
|
|
2230
|
+
outline-offset: 2px;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
.carousel-inner {
|
|
2234
|
+
position: relative;
|
|
2235
|
+
width: 100%;
|
|
2236
|
+
overflow: hidden;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
.carousel-slide {
|
|
2240
|
+
display: none;
|
|
2241
|
+
width: 100%;
|
|
2242
|
+
opacity: 0;
|
|
2243
|
+
transition: opacity var(--transition-normal) var(--transition-easing);
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
.carousel-slide.active {
|
|
2247
|
+
display: block;
|
|
2248
|
+
opacity: 1;
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
.carousel-slide img {
|
|
2252
|
+
width: 100%;
|
|
2253
|
+
height: auto;
|
|
2254
|
+
display: block;
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
/* Controls (prev/next) */
|
|
2258
|
+
.carousel-control {
|
|
2259
|
+
position: absolute;
|
|
2260
|
+
top: 50%;
|
|
2261
|
+
transform: translateY(-50%);
|
|
2262
|
+
z-index: 2;
|
|
2263
|
+
display: flex;
|
|
2264
|
+
align-items: center;
|
|
2265
|
+
justify-content: center;
|
|
2266
|
+
width: 36px;
|
|
2267
|
+
height: 36px;
|
|
2268
|
+
border: 1px solid var(--border);
|
|
2269
|
+
border-radius: 50%;
|
|
2270
|
+
background: var(--glass);
|
|
2271
|
+
color: var(--text);
|
|
2272
|
+
cursor: pointer;
|
|
2273
|
+
transition: all var(--transition-fast) var(--transition-easing);
|
|
2274
|
+
backdrop-filter: blur(8px);
|
|
2275
|
+
-webkit-backdrop-filter: blur(8px);
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
.carousel-control:hover {
|
|
2279
|
+
background: var(--accent);
|
|
2280
|
+
color: var(--bg);
|
|
2281
|
+
border-color: var(--accent);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
.carousel-prev {
|
|
2285
|
+
left: 0.75rem;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
.carousel-next {
|
|
2289
|
+
right: 0.75rem;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
/* Dot indicators */
|
|
2293
|
+
.carousel-indicators {
|
|
2294
|
+
display: flex;
|
|
2295
|
+
justify-content: center;
|
|
2296
|
+
gap: 0.5rem;
|
|
2297
|
+
padding: 0.75rem 0;
|
|
2298
|
+
position: absolute;
|
|
2299
|
+
bottom: 0;
|
|
2300
|
+
left: 0;
|
|
2301
|
+
right: 0;
|
|
2302
|
+
z-index: 2;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
.carousel-dot {
|
|
2306
|
+
width: 10px;
|
|
2307
|
+
height: 10px;
|
|
2308
|
+
border-radius: 50%;
|
|
2309
|
+
border: 1px solid var(--border-light);
|
|
2310
|
+
background: transparent;
|
|
2311
|
+
cursor: pointer;
|
|
2312
|
+
padding: 0;
|
|
2313
|
+
transition: all var(--transition-fast) var(--transition-easing);
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
.carousel-dot:hover {
|
|
2317
|
+
border-color: var(--accent);
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
.carousel-dot.active {
|
|
2321
|
+
background: var(--accent);
|
|
2322
|
+
border-color: var(--accent);
|
|
2323
|
+
box-shadow: 0 0 6px var(--accent);
|
|
2324
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carousel / Slideshow Component
|
|
3
|
+
*
|
|
4
|
+
* A lightweight carousel with autoplay, touch/swipe, keyboard navigation,
|
|
5
|
+
* and dot indicators. Integrates with the Corrupted Theme design system.
|
|
6
|
+
*
|
|
7
|
+
* @module carousel
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
* @license MIT
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* ```html
|
|
13
|
+
* <div class="carousel" data-ct-autoplay data-ct-interval="5000">
|
|
14
|
+
* <div class="carousel-inner">
|
|
15
|
+
* <div class="carousel-slide active">
|
|
16
|
+
* <img src="slide1.jpg" alt="Slide 1">
|
|
17
|
+
* </div>
|
|
18
|
+
* <div class="carousel-slide">
|
|
19
|
+
* <img src="slide2.jpg" alt="Slide 2">
|
|
20
|
+
* </div>
|
|
21
|
+
* </div>
|
|
22
|
+
* </div>
|
|
23
|
+
*
|
|
24
|
+
* <script type="module">
|
|
25
|
+
* import { initCarousel } from '@whykusanagi/corrupted-theme/carousel';
|
|
26
|
+
* const carousel = initCarousel('.carousel');
|
|
27
|
+
* // carousel.destroy() when done
|
|
28
|
+
* </script>
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { TimerRegistry } from '../core/timer-registry.js';
|
|
33
|
+
import { EventTracker } from '../core/event-tracker.js';
|
|
34
|
+
|
|
35
|
+
// Instance counter for unique IDs
|
|
36
|
+
let instanceCounter = 0;
|
|
37
|
+
|
|
38
|
+
class Carousel {
|
|
39
|
+
/**
|
|
40
|
+
* @param {string|HTMLElement} selector - Carousel container selector or element
|
|
41
|
+
* @param {Object} [options={}] - Configuration options
|
|
42
|
+
* @param {boolean} [options.autoplay=false] - Auto-advance slides
|
|
43
|
+
* @param {number} [options.interval=5000] - Autoplay interval in ms
|
|
44
|
+
* @param {boolean} [options.indicators=true] - Show dot indicators
|
|
45
|
+
* @param {boolean} [options.controls=true] - Show prev/next controls
|
|
46
|
+
* @param {boolean} [options.keyboard=true] - Enable keyboard navigation
|
|
47
|
+
* @param {boolean} [options.touch=true] - Enable touch/swipe
|
|
48
|
+
* @param {boolean} [options.pauseOnHover=true] - Pause autoplay on hover
|
|
49
|
+
*/
|
|
50
|
+
constructor(selector, options = {}) {
|
|
51
|
+
this._id = ++instanceCounter;
|
|
52
|
+
this._events = new EventTracker();
|
|
53
|
+
this._timers = new TimerRegistry();
|
|
54
|
+
|
|
55
|
+
const el = typeof selector === 'string'
|
|
56
|
+
? document.querySelector(selector) : selector;
|
|
57
|
+
if (!el) {
|
|
58
|
+
console.warn('[Carousel] Element not found:', selector);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.container = el;
|
|
63
|
+
this.inner = el.querySelector('.carousel-inner');
|
|
64
|
+
if (!this.inner) {
|
|
65
|
+
console.warn('[Carousel] Missing .carousel-inner');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Merge options with data attributes and defaults
|
|
70
|
+
this.config = {
|
|
71
|
+
autoplay: el.hasAttribute('data-ct-autoplay') || options.autoplay || false,
|
|
72
|
+
interval: parseInt(el.dataset.ctInterval) || options.interval || 5000,
|
|
73
|
+
indicators: options.indicators !== false,
|
|
74
|
+
controls: options.controls !== false,
|
|
75
|
+
keyboard: options.keyboard !== false,
|
|
76
|
+
touch: options.touch !== false,
|
|
77
|
+
pauseOnHover: options.pauseOnHover !== false
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
this.slides = Array.from(this.inner.querySelectorAll('.carousel-slide'));
|
|
81
|
+
this.currentIndex = this.slides.findIndex(s => s.classList.contains('active'));
|
|
82
|
+
if (this.currentIndex === -1) this.currentIndex = 0;
|
|
83
|
+
|
|
84
|
+
this._autoplayIntervalId = null;
|
|
85
|
+
|
|
86
|
+
this._init();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @private */
|
|
90
|
+
_init() {
|
|
91
|
+
// Ensure first slide is active
|
|
92
|
+
this.slides.forEach((slide, i) => {
|
|
93
|
+
slide.classList.toggle('active', i === this.currentIndex);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (this.config.controls) this._createControls();
|
|
97
|
+
if (this.config.indicators) this._createIndicators();
|
|
98
|
+
if (this.config.touch) this._initTouch();
|
|
99
|
+
|
|
100
|
+
if (this.config.keyboard) {
|
|
101
|
+
this._events.add(document, 'keydown', (e) => {
|
|
102
|
+
// Only respond when carousel or its children are focused
|
|
103
|
+
if (!this.container.contains(document.activeElement) &&
|
|
104
|
+
document.activeElement !== document.body) return;
|
|
105
|
+
if (e.key === 'ArrowLeft') this.prev();
|
|
106
|
+
else if (e.key === 'ArrowRight') this.next();
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (this.config.pauseOnHover) {
|
|
111
|
+
this._events.add(this.container, 'mouseenter', () => this._pauseAutoplay());
|
|
112
|
+
this._events.add(this.container, 'mouseleave', () => {
|
|
113
|
+
if (this.config.autoplay) this._startAutoplay();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (this.config.autoplay) this._startAutoplay();
|
|
118
|
+
|
|
119
|
+
// Make container focusable for keyboard nav
|
|
120
|
+
if (!this.container.hasAttribute('tabindex')) {
|
|
121
|
+
this.container.setAttribute('tabindex', '0');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** @private */
|
|
126
|
+
_createControls() {
|
|
127
|
+
const prevBtn = document.createElement('button');
|
|
128
|
+
prevBtn.className = 'carousel-control carousel-prev';
|
|
129
|
+
prevBtn.setAttribute('aria-label', 'Previous slide');
|
|
130
|
+
prevBtn.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>';
|
|
131
|
+
|
|
132
|
+
const nextBtn = document.createElement('button');
|
|
133
|
+
nextBtn.className = 'carousel-control carousel-next';
|
|
134
|
+
nextBtn.setAttribute('aria-label', 'Next slide');
|
|
135
|
+
nextBtn.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>';
|
|
136
|
+
|
|
137
|
+
this.container.appendChild(prevBtn);
|
|
138
|
+
this.container.appendChild(nextBtn);
|
|
139
|
+
|
|
140
|
+
this._events.add(prevBtn, 'click', () => this.prev());
|
|
141
|
+
this._events.add(nextBtn, 'click', () => this.next());
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** @private */
|
|
145
|
+
_createIndicators() {
|
|
146
|
+
const dots = document.createElement('div');
|
|
147
|
+
dots.className = 'carousel-indicators';
|
|
148
|
+
|
|
149
|
+
this.slides.forEach((_, i) => {
|
|
150
|
+
const dot = document.createElement('button');
|
|
151
|
+
dot.className = 'carousel-dot';
|
|
152
|
+
dot.setAttribute('aria-label', `Go to slide ${i + 1}`);
|
|
153
|
+
if (i === this.currentIndex) dot.classList.add('active');
|
|
154
|
+
this._events.add(dot, 'click', () => this.goTo(i));
|
|
155
|
+
dots.appendChild(dot);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
this.container.appendChild(dots);
|
|
159
|
+
this._dotsContainer = dots;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** @private */
|
|
163
|
+
_initTouch() {
|
|
164
|
+
let startX = 0;
|
|
165
|
+
let startY = 0;
|
|
166
|
+
|
|
167
|
+
this._events.add(this.inner, 'touchstart', (e) => {
|
|
168
|
+
startX = e.touches[0].clientX;
|
|
169
|
+
startY = e.touches[0].clientY;
|
|
170
|
+
}, { passive: true });
|
|
171
|
+
|
|
172
|
+
this._events.add(this.inner, 'touchend', (e) => {
|
|
173
|
+
const dx = startX - e.changedTouches[0].clientX;
|
|
174
|
+
const dy = startY - e.changedTouches[0].clientY;
|
|
175
|
+
// Only swipe if horizontal movement > vertical and > threshold
|
|
176
|
+
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 50) {
|
|
177
|
+
if (dx > 0) this.next();
|
|
178
|
+
else this.prev();
|
|
179
|
+
}
|
|
180
|
+
}, { passive: true });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** @private */
|
|
184
|
+
_startAutoplay() {
|
|
185
|
+
this._pauseAutoplay();
|
|
186
|
+
this._autoplayIntervalId = this._timers.setInterval(
|
|
187
|
+
() => this.next(),
|
|
188
|
+
this.config.interval
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** @private */
|
|
193
|
+
_pauseAutoplay() {
|
|
194
|
+
if (this._autoplayIntervalId != null) {
|
|
195
|
+
this._timers.clearInterval(this._autoplayIntervalId);
|
|
196
|
+
this._autoplayIntervalId = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Go to a specific slide
|
|
202
|
+
* @param {number} index - Slide index (0-based)
|
|
203
|
+
*/
|
|
204
|
+
goTo(index) {
|
|
205
|
+
if (index < 0 || index >= this.slides.length || index === this.currentIndex) return;
|
|
206
|
+
|
|
207
|
+
this.slides[this.currentIndex].classList.remove('active');
|
|
208
|
+
this.currentIndex = index;
|
|
209
|
+
this.slides[this.currentIndex].classList.add('active');
|
|
210
|
+
|
|
211
|
+
this._updateIndicators();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Go to the next slide (wraps around)
|
|
216
|
+
*/
|
|
217
|
+
next() {
|
|
218
|
+
this.goTo((this.currentIndex + 1) % this.slides.length);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Go to the previous slide (wraps around)
|
|
223
|
+
*/
|
|
224
|
+
prev() {
|
|
225
|
+
this.goTo((this.currentIndex - 1 + this.slides.length) % this.slides.length);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** @private */
|
|
229
|
+
_updateIndicators() {
|
|
230
|
+
if (!this._dotsContainer) return;
|
|
231
|
+
const dots = this._dotsContainer.querySelectorAll('.carousel-dot');
|
|
232
|
+
dots.forEach((dot, i) => {
|
|
233
|
+
dot.classList.toggle('active', i === this.currentIndex);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Tear down this carousel instance
|
|
239
|
+
*/
|
|
240
|
+
destroy() {
|
|
241
|
+
this._pauseAutoplay();
|
|
242
|
+
this._events.removeAll();
|
|
243
|
+
this._timers.clearAll();
|
|
244
|
+
|
|
245
|
+
// Remove generated controls and indicators
|
|
246
|
+
this.container.querySelectorAll('.carousel-control, .carousel-indicators').forEach(el => el.remove());
|
|
247
|
+
|
|
248
|
+
this._dotsContainer = null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ============================================================================
|
|
253
|
+
// PUBLIC API
|
|
254
|
+
// ============================================================================
|
|
255
|
+
|
|
256
|
+
/** @type {Carousel|null} Default auto-initialized instance */
|
|
257
|
+
let _defaultInstance = null;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Initialize a carousel instance
|
|
261
|
+
* @param {string|HTMLElement} [selector='.carousel'] - Carousel container
|
|
262
|
+
* @param {Object} [options={}] - Configuration options
|
|
263
|
+
* @returns {Carousel} Carousel instance
|
|
264
|
+
*/
|
|
265
|
+
export function initCarousel(selector = '.carousel', options = {}) {
|
|
266
|
+
const instance = new Carousel(selector, options);
|
|
267
|
+
if (!_defaultInstance) {
|
|
268
|
+
_defaultInstance = instance;
|
|
269
|
+
}
|
|
270
|
+
return instance;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Destroy a carousel instance
|
|
275
|
+
* @param {Carousel} [instance] - Instance to destroy (default: first created)
|
|
276
|
+
*/
|
|
277
|
+
export function destroyCarousel(instance) {
|
|
278
|
+
const target = instance || _defaultInstance;
|
|
279
|
+
if (target) {
|
|
280
|
+
target.destroy();
|
|
281
|
+
if (target === _defaultInstance) {
|
|
282
|
+
_defaultInstance = null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Auto-initialize carousels with data-ct-autoplay
|
|
288
|
+
if (typeof document !== 'undefined') {
|
|
289
|
+
const autoInit = () => {
|
|
290
|
+
document.querySelectorAll('.carousel[data-ct-autoplay]').forEach(el => {
|
|
291
|
+
initCarousel(el);
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
if (document.readyState === 'loading') {
|
|
296
|
+
document.addEventListener('DOMContentLoaded', autoInit);
|
|
297
|
+
} else {
|
|
298
|
+
autoInit();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Global export
|
|
303
|
+
if (typeof window !== 'undefined') {
|
|
304
|
+
window.CorruptedCarousel = {
|
|
305
|
+
init: initCarousel,
|
|
306
|
+
destroy: destroyCarousel
|
|
307
|
+
};
|
|
308
|
+
}
|