@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.
@@ -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
- const phrase = japaneseSet[Math.floor(Math.random() * japaneseSet.length)];
290
- return `<span style="color: ${phraseColor};">${phrase}</span>`;
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
- const phrase = englishSet[Math.floor(Math.random() * englishSet.length)];
294
- return `<span style="color: ${phraseColor};">${phrase}</span>`;
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
- const phrase = romajiSet[Math.floor(Math.random() * romajiSet.length)];
298
- return `<span style="color: ${phraseColor};">${phrase}</span>`;
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
- const symbol = TypingAnimation.SYMBOLS[
304
+ text = TypingAnimation.SYMBOLS[
302
305
  Math.floor(Math.random() * TypingAnimation.SYMBOLS.length)
303
306
  ];
304
- return `<span style="color: #d94f90;">${symbol}</span>`;
307
+ color = '#d94f90';
305
308
  } else {
306
309
  // Block chars - terminal/critical state (always SFW)
307
- const char = TypingAnimation.BLOCKS[
310
+ text = TypingAnimation.BLOCKS[
308
311
  Math.floor(Math.random() * TypingAnimation.BLOCKS.length)
309
312
  ];
310
- return `<span style="color: #ff0000;">${char}</span>`;
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
- let displayed = this.getDisplayed();
332
+ const displayed = this.getDisplayed();
333
+
334
+ // Clear and rebuild using safe DOM methods (no innerHTML)
335
+ this.element.textContent = '';
324
336
 
325
- // Add buffer corruption at the "cursor" position
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
- displayed += this.getRandomCorruption();
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
- this.element.innerHTML = `<span style="color: #ffffff;">${finalText || this.content}</span>`;
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
  /**
@@ -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
+ }