@whykusanagi/corrupted-theme 0.1.1 → 0.1.2

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.
@@ -0,0 +1,481 @@
1
+ /**
2
+ * gallery.js — Gallery System with Lightbox and NSFW Support
3
+ *
4
+ * A complete gallery system with filtering, lightbox viewer, and NSFW content handling.
5
+ * Integrates with the Corrupted Theme design system.
6
+ *
7
+ * @module gallery
8
+ * @version 1.0.0
9
+ * @license MIT
10
+ *
11
+ * Features:
12
+ * - Responsive gallery grid
13
+ * - Category filtering with animated transitions
14
+ * - Fullscreen lightbox with keyboard navigation
15
+ * - NSFW content blur with click-to-reveal
16
+ * - Lazy loading support
17
+ * - Touch gesture support for mobile
18
+ *
19
+ * Usage:
20
+ * ```html
21
+ * <div class="filter-bar">
22
+ * <button class="filter-btn active" data-filter="all">All</button>
23
+ * <button class="filter-btn" data-filter="photos">Photos</button>
24
+ * </div>
25
+ * <div class="gallery-container" id="gallery">
26
+ * <div class="gallery-item" data-tags="photos">
27
+ * <img src="image.jpg" alt="Description">
28
+ * <div class="gallery-caption">Caption</div>
29
+ * </div>
30
+ * </div>
31
+ *
32
+ * <script type="module">
33
+ * import { initGallery } from '@whykusanagi/corrupted-theme/src/lib/gallery.js';
34
+ * initGallery('#gallery');
35
+ * </script>
36
+ * ```
37
+ */
38
+
39
+ // ============================================================================
40
+ // CONFIGURATION
41
+ // ============================================================================
42
+
43
+ const DEFAULT_CONFIG = {
44
+ // Gallery selectors
45
+ gallerySelector: '.gallery-container',
46
+ itemSelector: '.gallery-item',
47
+ filterBarSelector: '.filter-bar',
48
+ filterBtnSelector: '.filter-btn',
49
+
50
+ // Lightbox
51
+ enableLightbox: true,
52
+ lightboxId: 'corrupted-lightbox',
53
+
54
+ // NSFW
55
+ enableNsfw: true,
56
+ nsfwSelector: '.nsfw-content',
57
+ nsfwWarning: '18+ Click to View',
58
+
59
+ // Animation
60
+ filterAnimation: true,
61
+ animationDuration: 300,
62
+
63
+ // Keyboard
64
+ enableKeyboard: true,
65
+
66
+ // Callbacks
67
+ onFilter: null,
68
+ onLightboxOpen: null,
69
+ onLightboxClose: null,
70
+ onNsfwReveal: null
71
+ };
72
+
73
+ // ============================================================================
74
+ // STATE
75
+ // ============================================================================
76
+
77
+ let state = {
78
+ currentFilter: 'all',
79
+ lightboxOpen: false,
80
+ currentImageIndex: 0,
81
+ galleryImages: [],
82
+ config: { ...DEFAULT_CONFIG }
83
+ };
84
+
85
+ // ============================================================================
86
+ // LIGHTBOX
87
+ // ============================================================================
88
+
89
+ /**
90
+ * Creates and injects the lightbox HTML into the DOM
91
+ * @private
92
+ */
93
+ function createLightbox() {
94
+ if (document.getElementById(state.config.lightboxId)) return;
95
+
96
+ const lightbox = document.createElement('div');
97
+ lightbox.id = state.config.lightboxId;
98
+ lightbox.className = 'lightbox';
99
+ lightbox.innerHTML = `
100
+ <button class="lightbox-close" aria-label="Close lightbox">&times;</button>
101
+ <button class="lightbox-prev" aria-label="Previous image">
102
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
103
+ <path d="M15 18l-6-6 6-6"/>
104
+ </svg>
105
+ </button>
106
+ <img class="lightbox-image" src="" alt="">
107
+ <button class="lightbox-next" aria-label="Next image">
108
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
109
+ <path d="M9 18l6-6-6-6"/>
110
+ </svg>
111
+ </button>
112
+ <div class="lightbox-caption"></div>
113
+ <div class="lightbox-counter"></div>
114
+ `;
115
+
116
+ document.body.appendChild(lightbox);
117
+
118
+ // Event listeners
119
+ lightbox.querySelector('.lightbox-close').addEventListener('click', closeLightbox);
120
+ lightbox.querySelector('.lightbox-prev').addEventListener('click', () => navigateLightbox(-1));
121
+ lightbox.querySelector('.lightbox-next').addEventListener('click', () => navigateLightbox(1));
122
+ lightbox.addEventListener('click', (e) => {
123
+ if (e.target === lightbox) closeLightbox();
124
+ });
125
+
126
+ // Touch gestures
127
+ let touchStartX = 0;
128
+ lightbox.addEventListener('touchstart', (e) => {
129
+ touchStartX = e.touches[0].clientX;
130
+ }, { passive: true });
131
+
132
+ lightbox.addEventListener('touchend', (e) => {
133
+ const touchEndX = e.changedTouches[0].clientX;
134
+ const diff = touchStartX - touchEndX;
135
+
136
+ if (Math.abs(diff) > 50) {
137
+ navigateLightbox(diff > 0 ? 1 : -1);
138
+ }
139
+ }, { passive: true });
140
+ }
141
+
142
+ /**
143
+ * Opens the lightbox with specified image
144
+ * @param {number} index - Index of image in gallery
145
+ */
146
+ function openLightbox(index) {
147
+ const lightbox = document.getElementById(state.config.lightboxId);
148
+ if (!lightbox || !state.galleryImages[index]) return;
149
+
150
+ state.currentImageIndex = index;
151
+ state.lightboxOpen = true;
152
+
153
+ const imageData = state.galleryImages[index];
154
+ const img = lightbox.querySelector('.lightbox-image');
155
+ const caption = lightbox.querySelector('.lightbox-caption');
156
+ const counter = lightbox.querySelector('.lightbox-counter');
157
+
158
+ img.src = imageData.src;
159
+ img.alt = imageData.alt || '';
160
+
161
+ // Handle NSFW class
162
+ if (imageData.isNsfw) {
163
+ img.classList.add('nsfw-revealed');
164
+ } else {
165
+ img.classList.remove('nsfw-revealed');
166
+ }
167
+
168
+ caption.textContent = imageData.caption || '';
169
+ caption.style.display = imageData.caption ? 'block' : 'none';
170
+
171
+ counter.textContent = `${index + 1} / ${state.galleryImages.length}`;
172
+
173
+ // Update navigation buttons
174
+ lightbox.querySelector('.lightbox-prev').disabled = index === 0;
175
+ lightbox.querySelector('.lightbox-next').disabled = index === state.galleryImages.length - 1;
176
+
177
+ lightbox.classList.add('active');
178
+ document.body.style.overflow = 'hidden';
179
+
180
+ if (state.config.onLightboxOpen) {
181
+ state.config.onLightboxOpen(imageData, index);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Closes the lightbox
187
+ */
188
+ function closeLightbox() {
189
+ const lightbox = document.getElementById(state.config.lightboxId);
190
+ if (!lightbox) return;
191
+
192
+ lightbox.classList.remove('active');
193
+ document.body.style.overflow = '';
194
+ state.lightboxOpen = false;
195
+
196
+ if (state.config.onLightboxClose) {
197
+ state.config.onLightboxClose();
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Navigates to next/previous image in lightbox
203
+ * @param {number} direction - 1 for next, -1 for previous
204
+ */
205
+ function navigateLightbox(direction) {
206
+ const newIndex = state.currentImageIndex + direction;
207
+
208
+ if (newIndex >= 0 && newIndex < state.galleryImages.length) {
209
+ openLightbox(newIndex);
210
+ }
211
+ }
212
+
213
+ // ============================================================================
214
+ // FILTERING
215
+ // ============================================================================
216
+
217
+ /**
218
+ * Filters gallery items by category
219
+ * @param {string} filter - Filter value (tag name or 'all')
220
+ */
221
+ function filterGallery(filter) {
222
+ const galleries = document.querySelectorAll(state.config.gallerySelector);
223
+
224
+ galleries.forEach(gallery => {
225
+ const items = gallery.querySelectorAll(state.config.itemSelector);
226
+
227
+ items.forEach(item => {
228
+ const tags = (item.dataset.tags || '').split(',').map(t => t.trim());
229
+ const shouldShow = filter === 'all' || tags.includes(filter);
230
+
231
+ if (state.config.filterAnimation) {
232
+ item.style.transition = `opacity ${state.config.animationDuration}ms ease, transform ${state.config.animationDuration}ms ease`;
233
+
234
+ if (shouldShow) {
235
+ item.style.opacity = '0';
236
+ item.style.transform = 'scale(0.9)';
237
+ item.style.display = '';
238
+
239
+ requestAnimationFrame(() => {
240
+ item.style.opacity = '1';
241
+ item.style.transform = 'scale(1)';
242
+ });
243
+ } else {
244
+ item.style.opacity = '0';
245
+ item.style.transform = 'scale(0.9)';
246
+
247
+ setTimeout(() => {
248
+ item.style.display = 'none';
249
+ }, state.config.animationDuration);
250
+ }
251
+ } else {
252
+ item.style.display = shouldShow ? '' : 'none';
253
+ }
254
+ });
255
+ });
256
+
257
+ state.currentFilter = filter;
258
+ updateGalleryImages();
259
+
260
+ // Update filter button active state
261
+ document.querySelectorAll(state.config.filterBtnSelector).forEach(btn => {
262
+ btn.classList.toggle('active', btn.dataset.filter === filter);
263
+ });
264
+
265
+ if (state.config.onFilter) {
266
+ state.config.onFilter(filter);
267
+ }
268
+ }
269
+
270
+ // ============================================================================
271
+ // NSFW HANDLING
272
+ // ============================================================================
273
+
274
+ /**
275
+ * Reveals NSFW content on click
276
+ * @param {HTMLElement} element - NSFW content element
277
+ */
278
+ function revealNsfwContent(element) {
279
+ if (element.classList.contains('revealed')) return;
280
+
281
+ element.classList.add('revealed');
282
+
283
+ if (state.config.onNsfwReveal) {
284
+ state.config.onNsfwReveal(element);
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Initializes NSFW content handling
290
+ * @private
291
+ */
292
+ function initNsfwContent() {
293
+ document.querySelectorAll(state.config.nsfwSelector).forEach(item => {
294
+ if (!item.dataset.warning) {
295
+ item.dataset.warning = state.config.nsfwWarning;
296
+ }
297
+
298
+ item.addEventListener('click', (e) => {
299
+ if (!item.classList.contains('revealed')) {
300
+ e.preventDefault();
301
+ e.stopPropagation();
302
+ revealNsfwContent(item);
303
+ }
304
+ });
305
+ });
306
+ }
307
+
308
+ // ============================================================================
309
+ // KEYBOARD NAVIGATION
310
+ // ============================================================================
311
+
312
+ /**
313
+ * Handles keyboard events for lightbox navigation
314
+ * @private
315
+ * @param {KeyboardEvent} e - Keyboard event
316
+ */
317
+ function handleKeyboard(e) {
318
+ if (!state.lightboxOpen) return;
319
+
320
+ switch (e.key) {
321
+ case 'Escape':
322
+ closeLightbox();
323
+ break;
324
+ case 'ArrowLeft':
325
+ navigateLightbox(-1);
326
+ break;
327
+ case 'ArrowRight':
328
+ navigateLightbox(1);
329
+ break;
330
+ }
331
+ }
332
+
333
+ // ============================================================================
334
+ // UTILITY
335
+ // ============================================================================
336
+
337
+ /**
338
+ * Updates the list of gallery images for lightbox navigation
339
+ * @private
340
+ */
341
+ function updateGalleryImages() {
342
+ state.galleryImages = [];
343
+
344
+ document.querySelectorAll(`${state.config.gallerySelector} ${state.config.itemSelector}`).forEach((item, index) => {
345
+ if (item.style.display === 'none') return;
346
+
347
+ const img = item.querySelector('img');
348
+ const caption = item.querySelector('.gallery-caption');
349
+
350
+ if (img) {
351
+ state.galleryImages.push({
352
+ src: img.dataset.fullSrc || img.src,
353
+ alt: img.alt,
354
+ caption: caption ? caption.textContent : '',
355
+ isNsfw: item.classList.contains('nsfw-content'),
356
+ element: item,
357
+ originalIndex: index
358
+ });
359
+ }
360
+ });
361
+ }
362
+
363
+ // ============================================================================
364
+ // PUBLIC API
365
+ // ============================================================================
366
+
367
+ /**
368
+ * Initializes the gallery system
369
+ * @param {string|HTMLElement} selector - Gallery container selector or element
370
+ * @param {Object} options - Configuration options
371
+ * @returns {Object} Gallery API
372
+ */
373
+ export function initGallery(selector = '.gallery-container', options = {}) {
374
+ // Merge config
375
+ state.config = { ...DEFAULT_CONFIG, ...options };
376
+
377
+ if (typeof selector === 'string') {
378
+ state.config.gallerySelector = selector;
379
+ }
380
+
381
+ // Create lightbox
382
+ if (state.config.enableLightbox) {
383
+ createLightbox();
384
+ }
385
+
386
+ // Initialize filter buttons
387
+ document.querySelectorAll(state.config.filterBtnSelector).forEach(btn => {
388
+ btn.addEventListener('click', () => {
389
+ filterGallery(btn.dataset.filter || 'all');
390
+ });
391
+ });
392
+
393
+ // Initialize gallery items
394
+ document.querySelectorAll(`${state.config.gallerySelector} ${state.config.itemSelector}`).forEach((item, index) => {
395
+ const img = item.querySelector('img');
396
+
397
+ if (img && state.config.enableLightbox) {
398
+ img.style.cursor = 'pointer';
399
+ img.addEventListener('click', (e) => {
400
+ // Don't open lightbox for unrevealed NSFW content
401
+ if (item.classList.contains('nsfw-content') && !item.classList.contains('revealed')) {
402
+ return;
403
+ }
404
+
405
+ e.stopPropagation();
406
+ const visibleIndex = state.galleryImages.findIndex(i => i.element === item);
407
+ if (visibleIndex !== -1) {
408
+ openLightbox(visibleIndex);
409
+ }
410
+ });
411
+ }
412
+ });
413
+
414
+ // Initialize NSFW handling
415
+ if (state.config.enableNsfw) {
416
+ initNsfwContent();
417
+ }
418
+
419
+ // Initialize keyboard navigation
420
+ if (state.config.enableKeyboard) {
421
+ document.addEventListener('keydown', handleKeyboard);
422
+ }
423
+
424
+ // Build initial image list
425
+ updateGalleryImages();
426
+
427
+ // Return public API
428
+ return {
429
+ filter: filterGallery,
430
+ openLightbox,
431
+ closeLightbox,
432
+ revealNsfw: revealNsfwContent,
433
+ refresh: updateGalleryImages,
434
+ getImages: () => [...state.galleryImages],
435
+ getCurrentFilter: () => state.currentFilter
436
+ };
437
+ }
438
+
439
+ /**
440
+ * Destroys the gallery instance and removes event listeners
441
+ */
442
+ export function destroyGallery() {
443
+ const lightbox = document.getElementById(state.config.lightboxId);
444
+ if (lightbox) {
445
+ lightbox.remove();
446
+ }
447
+
448
+ document.removeEventListener('keydown', handleKeyboard);
449
+
450
+ state = {
451
+ currentFilter: 'all',
452
+ lightboxOpen: false,
453
+ currentImageIndex: 0,
454
+ galleryImages: [],
455
+ config: { ...DEFAULT_CONFIG }
456
+ };
457
+ }
458
+
459
+ // Auto-initialize if DOM is ready and elements exist
460
+ if (typeof document !== 'undefined') {
461
+ if (document.readyState === 'loading') {
462
+ document.addEventListener('DOMContentLoaded', () => {
463
+ if (document.querySelector('.gallery-container')) {
464
+ initGallery();
465
+ }
466
+ });
467
+ } else {
468
+ if (document.querySelector('.gallery-container')) {
469
+ initGallery();
470
+ }
471
+ }
472
+ }
473
+
474
+ // Export for global usage
475
+ if (typeof window !== 'undefined') {
476
+ window.CorruptedGallery = {
477
+ init: initGallery,
478
+ destroy: destroyGallery
479
+ };
480
+ }
481
+