hexo-theme-gnix 6.2.0 → 8.0.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.
Files changed (59) hide show
  1. package/README.md +6 -2
  2. package/include/hexo/encrypt.js +42 -0
  3. package/include/hexo/feed.js +329 -0
  4. package/include/util/common.js +7 -9
  5. package/languages/en.yml +6 -3
  6. package/languages/zh-CN.yml +6 -3
  7. package/layout/archive.jsx +86 -65
  8. package/layout/comment/twikoo.jsx +2 -11
  9. package/layout/comment/waline.jsx +2 -2
  10. package/layout/common/article.jsx +5 -8
  11. package/layout/common/article_cover.jsx +11 -1
  12. package/layout/common/article_media.jsx +2 -4
  13. package/layout/common/footer.jsx +11 -31
  14. package/layout/common/head.jsx +6 -14
  15. package/layout/common/navbar.jsx +4 -4
  16. package/layout/common/scripts.jsx +6 -6
  17. package/layout/common/theme_selector.jsx +5 -6
  18. package/layout/common/toc.jsx +8 -14
  19. package/layout/index.jsx +2 -4
  20. package/layout/misc/article_licensing.jsx +4 -2
  21. package/layout/misc/open_graph.jsx +4 -4
  22. package/layout/misc/paginator.jsx +10 -4
  23. package/layout/misc/structured_data.jsx +3 -4
  24. package/layout/plugin/busuanzi.jsx +1 -1
  25. package/layout/plugin/cookie_consent.jsx +40 -31
  26. package/layout/plugin/swup.jsx +2 -22
  27. package/layout/search/insight.jsx +16 -3
  28. package/package.json +12 -8
  29. package/scripts/hot-reload.js +92 -0
  30. package/scripts/index.js +2 -0
  31. package/source/css/archive.css +251 -0
  32. package/source/css/default.css +250 -284
  33. package/source/css/encrypt.css +55 -0
  34. package/source/css/responsive/desktop.css +0 -119
  35. package/source/css/responsive/mobile.css +7 -23
  36. package/source/css/responsive/touch.css +9 -103
  37. package/source/css/shiki/shiki.css +7 -22
  38. package/source/css/twikoo.css +290 -830
  39. package/source/img/og_image.webp +0 -0
  40. package/source/js/archive-breadcrumb.js +132 -0
  41. package/source/js/busuanzi.js +1 -12
  42. package/source/js/components/accordion.js +192 -0
  43. package/source/js/components/chat.js +239 -0
  44. package/source/js/components/device-carousel.js +260 -0
  45. package/source/js/components/image-carousel.js +410 -0
  46. package/source/js/components/text-image-section.js +180 -0
  47. package/source/js/components/theme-stacked.js +526 -0
  48. package/source/js/components/tree.js +437 -0
  49. package/source/js/decrypt.js +112 -0
  50. package/source/js/insight.js +75 -65
  51. package/source/js/main.js +192 -99
  52. package/source/js/mdit/mermaid.js +12 -4
  53. package/source/js/swup.bundle.js +1 -0
  54. package/source/js/theme-selector.js +94 -113
  55. package/source/img/og_image.png +0 -0
  56. package/source/js/host/swup/Swup.umd.min.js +0 -1
  57. package/source/js/host/swup/head-plugin.umd.min.js +0 -1
  58. package/source/js/host/swup/scripts-plugin.umd.min.js +0 -2
  59. package/source/js/mdit/shiki.js +0 -158
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Device Carousel Custom Element
3
+ * A seamless infinite scrolling carousel for showcasing devices
4
+ *
5
+ * Usage:
6
+ * <device-carousel></device-carousel>
7
+ *
8
+ * Or with custom devices:
9
+ * <device-carousel>
10
+ * <device-card
11
+ * name="Device Name"
12
+ * image="/path/to/image.png"
13
+ * specs="Spec 1<br>Spec 2"
14
+ * ></device-card>
15
+ * </device-carousel>
16
+ */
17
+
18
+ class DeviceCarousel extends HTMLElement {
19
+ constructor() {
20
+ super();
21
+ this.attachShadow({ mode: "open" });
22
+ }
23
+
24
+ connectedCallback() {
25
+ this.render();
26
+ this.setupIntersectionObserver();
27
+ }
28
+
29
+ get defaultDevices() {
30
+ return [
31
+ {
32
+ name: "Galaxy S24+",
33
+ image: "/assets/common/s24_plus.png",
34
+ alt: "Samsung Galaxy S24 Plus",
35
+ specs: "Snapdragon 8 Gen 3<br>12GB RAM + 512GB Storage",
36
+ },
37
+ {
38
+ name: "MacBook Air",
39
+ image: "/assets/common/MacBook_Air.png",
40
+ alt: "MacBook Air",
41
+ specs: "Apple M3 Chip<br>24GB Unified Memory<br>15-inch Liquid Retina",
42
+ },
43
+ {
44
+ name: "Legion R9000P",
45
+ image: "/assets/common/r9kp.webp",
46
+ alt: "Lenovo Legion R9000P 2021",
47
+ specs: 'AMD Ryzen 7 5800H<br>RTX 3060 / 16GB RAM<br>15.6" 165Hz Display',
48
+ },
49
+ {
50
+ name: "Galaxy Watch7",
51
+ image: "/assets/common/galaxy-watch7.webp",
52
+ alt: "Galaxy Watch 7",
53
+ specs: "44mm LTE Version<br>Exynos W1000<br>Health & Fitness Tracking",
54
+ },
55
+ {
56
+ name: "Beoplay H9i",
57
+ image: "/assets/common/beoplay-h9i.png",
58
+ alt: "Beoplay H9i",
59
+ specs: "Over-ear Wireless Headphones<br>Active Noise Cancellation",
60
+ },
61
+ {
62
+ name: "Galaxy Buds2 Pro",
63
+ image: "/assets/common/galaxy-buds2pro.webp",
64
+ alt: "Galaxy Buds2 Pro",
65
+ specs: "In-ear Wireless Earbuds<br>Active Noise Cancellation",
66
+ },
67
+ ];
68
+ }
69
+
70
+ render() {
71
+ const style = `
72
+ :host {
73
+ display: block;
74
+ --card-width: 280px;
75
+ --card-gap: 1.5rem;
76
+ --card-padding: 1.5rem;
77
+ --animation-duration: 40s;
78
+ }
79
+
80
+ .showcase-container {
81
+ position: relative;
82
+ overflow: hidden;
83
+ margin: 1.5rem 0;
84
+ mask-image: linear-gradient(to right, transparent, black 5%, black 95%, transparent);
85
+ -webkit-mask-image: linear-gradient(to right, transparent, black 5%, black 95%, transparent);
86
+ }
87
+
88
+ .showcase-track {
89
+ display: flex;
90
+ padding: 1.5rem 0;
91
+ width: max-content;
92
+ animation: scroll var(--animation-duration) linear infinite;
93
+ }
94
+
95
+ .showcase-track:hover {
96
+ animation-play-state: paused;
97
+ }
98
+
99
+ @keyframes scroll {
100
+ 0% { transform: translateX(0); }
101
+ 100% { transform: translateX(-50%); }
102
+ }
103
+
104
+ .showcase-card {
105
+ flex: 0 0 auto;
106
+ width: var(--card-width);
107
+ margin-right: var(--card-gap);
108
+ padding: var(--card-padding);
109
+ border-radius: 12px;
110
+ background: var(--crust, #1e1e2e);
111
+ border: 1px solid var(--surface0, #313244);
112
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
113
+ display: flex;
114
+ flex-direction: column;
115
+ align-items: center;
116
+ text-align: center;
117
+ box-sizing: border-box;
118
+ }
119
+
120
+ .showcase-card:hover {
121
+ transform: translateY(-2px);
122
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
123
+ }
124
+
125
+ .showcase-label {
126
+ font-size: 0.75rem;
127
+ font-weight: 600;
128
+ text-transform: uppercase;
129
+ letter-spacing: 0.1em;
130
+ color: var(--subtext1, #bac2de);
131
+ margin-bottom: 0.75rem;
132
+ }
133
+
134
+ .showcase-content {
135
+ margin-bottom: 0.75rem;
136
+ color: var(--text, #cdd6f4);
137
+ }
138
+
139
+ .device-image {
140
+ height: 120px;
141
+ width: auto;
142
+ max-width: 100%;
143
+ object-fit: contain;
144
+ transition: transform 0.3s ease;
145
+ display: block;
146
+ }
147
+
148
+ .showcase-card:hover .device-image {
149
+ transform: scale(1.1);
150
+ }
151
+
152
+ .showcase-meta {
153
+ font-family: var(--font-mono, 'Maple Mono', 'Fira Code', monospace);
154
+ font-size: 0.75rem;
155
+ color: var(--subtext0, #a6adc8);
156
+ padding: 0.5rem 0.75rem;
157
+ background: var(--mantle, #181825);
158
+ border-radius: 6px;
159
+ word-break: break-all;
160
+ line-height: 1.5;
161
+ }
162
+
163
+ /* Pause animation when not visible to save resources */
164
+ @media (prefers-reduced-motion: reduce) {
165
+ .showcase-track {
166
+ animation: none;
167
+ }
168
+ }
169
+ `;
170
+
171
+ // Get devices from slots or use defaults
172
+ const devices = this.getDevices();
173
+
174
+ // Duplicate for seamless infinite scroll
175
+ const allDevices = [...devices, ...devices];
176
+
177
+ const cardsHTML = allDevices
178
+ .map(
179
+ (device) => `
180
+ <div class="showcase-card">
181
+ <div class="showcase-label">${device.name}</div>
182
+ <div class="showcase-content">
183
+ <img src="${device.image}" alt="${device.alt}" class="device-image" loading="lazy"/>
184
+ </div>
185
+ <div class="showcase-meta">${device.specs}</div>
186
+ </div>
187
+ `,
188
+ )
189
+ .join("");
190
+
191
+ this.shadowRoot.innerHTML = `
192
+ <style>${style}</style>
193
+ <div class="showcase-container">
194
+ <div class="showcase-track">
195
+ ${cardsHTML}
196
+ </div>
197
+ </div>
198
+ <slot style="display: none;"></slot>
199
+ `;
200
+ }
201
+
202
+ getDevices() {
203
+ // Check if there are any device-card children in the slot
204
+ const slot = this.querySelectorAll("device-card");
205
+ if (slot.length > 0) {
206
+ return Array.from(slot).map((card) => ({
207
+ name: card.getAttribute("name") || "Device",
208
+ image: card.getAttribute("image") || "",
209
+ alt: card.getAttribute("alt") || card.getAttribute("name") || "Device",
210
+ specs: card.getAttribute("specs") || "",
211
+ }));
212
+ }
213
+ return this.defaultDevices;
214
+ }
215
+
216
+ // Pause animation when not visible
217
+ setupIntersectionObserver() {
218
+ const track = this.shadowRoot.querySelector(".showcase-track");
219
+ if (!track) return;
220
+
221
+ const observer = new IntersectionObserver(
222
+ (entries) => {
223
+ entries.forEach((entry) => {
224
+ if (entry.isIntersecting) {
225
+ track.style.animationPlayState = "running";
226
+ } else {
227
+ track.style.animationPlayState = "paused";
228
+ }
229
+ });
230
+ },
231
+ { threshold: 0.1 },
232
+ );
233
+
234
+ observer.observe(this);
235
+ }
236
+
237
+ static get observedAttributes() {
238
+ return ["speed", "card-width"];
239
+ }
240
+
241
+ attributeChangedCallback(name, oldValue, newValue) {
242
+ if (oldValue === newValue) return;
243
+
244
+ if (name === "speed" && this.shadowRoot) {
245
+ const track = this.shadowRoot.querySelector(".showcase-track");
246
+ if (track) {
247
+ track.style.setProperty("--animation-duration", `${newValue}s`);
248
+ }
249
+ }
250
+
251
+ if (name === "card-width" && this.shadowRoot) {
252
+ this.shadowRoot.host.style.setProperty("--card-width", newValue);
253
+ }
254
+ }
255
+ }
256
+
257
+ // Register custom elements
258
+ customElements.define("device-carousel", DeviceCarousel);
259
+
260
+ export { DeviceCarousel };
@@ -0,0 +1,410 @@
1
+ /**
2
+ * Image Carousel Custom Element
3
+ * A responsive image carousel with fade transitions and autoplay
4
+ *
5
+ * Usage:
6
+ * <image-carousel autoplay interval="4000">
7
+ * ![alt text](image-url)
8
+ * ![alt text](image-url)
9
+ * </image-carousel>
10
+ *
11
+ * Attributes:
12
+ * - autoplay: Enable automatic slide advancement
13
+ * - interval: Autoplay interval in ms (default: 3000)
14
+ * - ratio: Aspect ratio as CSS value (default: 3/2)
15
+ */
16
+
17
+ // Shared stylesheet — parsed once, reused across all carousel instances
18
+ let _sheet;
19
+
20
+ const STYLES = `
21
+ :host {
22
+ display: block;
23
+ margin: 1.5em 0;
24
+ }
25
+
26
+ .carousel {
27
+ outline: none;
28
+ }
29
+
30
+ .carousel:focus-visible {
31
+ outline: 2px solid var(--blue, #89b4fa);
32
+ outline-offset: 2px;
33
+ }
34
+
35
+ .stage {
36
+ position: relative;
37
+ border-radius: var(--radius, 12px);
38
+ overflow: hidden;
39
+ background: var(--crust, #11111b);
40
+ contain: content;
41
+ }
42
+
43
+ .slides {
44
+ position: relative;
45
+ width: 100%;
46
+ aspect-ratio: var(--carousel-ratio, 3/2);
47
+ }
48
+
49
+ .slide {
50
+ position: absolute;
51
+ inset: 0;
52
+ opacity: 0;
53
+ transition: opacity 0.7s cubic-bezier(0.25, 1, 0.5, 1);
54
+ pointer-events: none;
55
+ }
56
+
57
+ .slide.active {
58
+ opacity: 1;
59
+ pointer-events: auto;
60
+ }
61
+
62
+ .slide figure {
63
+ margin: 0;
64
+ width: 100%;
65
+ height: 100%;
66
+ }
67
+
68
+ .slide img {
69
+ width: 100%;
70
+ height: 100%;
71
+ object-fit: cover;
72
+ display: block;
73
+ }
74
+
75
+ .slide figcaption {
76
+ position: absolute;
77
+ bottom: 0;
78
+ left: 0;
79
+ right: 0;
80
+ padding: 2rem 1.25rem 0.875rem;
81
+ background: linear-gradient(transparent, rgba(0, 0, 0, 0.55));
82
+ color: #fff;
83
+ font-size: 0.875rem;
84
+ font-style: italic;
85
+ font-family: var(--font-serif, serif);
86
+ text-align: center;
87
+ pointer-events: none;
88
+ }
89
+
90
+ .nav {
91
+ position: absolute;
92
+ top: 50%;
93
+ transform: translateY(-50%);
94
+ background: rgba(0, 0, 0, 0.28);
95
+ backdrop-filter: blur(4px);
96
+ -webkit-backdrop-filter: blur(4px);
97
+ color: white;
98
+ border: none;
99
+ width: 2.5rem;
100
+ height: 2.5rem;
101
+ border-radius: 50%;
102
+ cursor: pointer;
103
+ display: flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+ opacity: 0;
107
+ transition: background 0.2s ease-out, opacity 0.2s ease-out;
108
+ z-index: 2;
109
+ padding: 0;
110
+ }
111
+
112
+ .nav svg {
113
+ width: 1.5rem;
114
+ height: 1.5rem;
115
+ }
116
+
117
+ .carousel:hover .nav,
118
+ .carousel:focus-within .nav {
119
+ opacity: 1;
120
+ }
121
+
122
+ .nav:hover {
123
+ background: rgba(0, 0, 0, 0.55);
124
+ }
125
+
126
+ .nav:focus-visible {
127
+ opacity: 1;
128
+ outline: 2px solid #fff;
129
+ outline-offset: 2px;
130
+ }
131
+
132
+ .nav:active {
133
+ transform: translateY(-50%) scale(0.92);
134
+ }
135
+
136
+ .prev { left: 0.75rem; }
137
+ .next { right: 0.75rem; }
138
+
139
+ .dots {
140
+ display: flex;
141
+ justify-content: center;
142
+ gap: 6px;
143
+ padding: 0.5rem 0 0;
144
+ }
145
+
146
+ .dot {
147
+ width: 8px;
148
+ height: 8px;
149
+ border-radius: 50%;
150
+ border: none;
151
+ background: var(--overlay0, rgba(127, 127, 127, 0.45));
152
+ cursor: pointer;
153
+ padding: 0;
154
+ transition: background 0.2s ease-out, transform 0.2s ease-out;
155
+ }
156
+
157
+ .dot:hover {
158
+ background: var(--overlay1);
159
+ }
160
+
161
+ .dot:focus-visible {
162
+ outline: 2px solid var(--blue, #89b4fa);
163
+ outline-offset: 2px;
164
+ }
165
+
166
+ .dot.active {
167
+ background: var(--text);
168
+ transform: scale(1.25);
169
+ }
170
+
171
+ @media (prefers-reduced-motion: reduce) {
172
+ .slide { transition: none; }
173
+ .nav, .dot { transition: none; }
174
+ }
175
+ `;
176
+
177
+ class ImageCarousel extends HTMLElement {
178
+ constructor() {
179
+ super();
180
+ this.attachShadow({ mode: "open" });
181
+ this._currentIndex = 0;
182
+ this._timer = null;
183
+ this._images = [];
184
+ this._slides = [];
185
+ this._dots = [];
186
+ this._touchStartX = 0;
187
+ this._observer = null;
188
+ this._isVisible = true;
189
+ this._handleVisibility = () => {
190
+ if (document.hidden) this._stopAutoplay();
191
+ else if (this._isVisible && this.hasAttribute("autoplay")) this._startAutoplay();
192
+ };
193
+ }
194
+
195
+ connectedCallback() {
196
+ this._images = this._collectImages();
197
+ this.render();
198
+ if (this._images.length > 1) {
199
+ this._setupListeners();
200
+ this._observeVisibility();
201
+ }
202
+ if (this.hasAttribute("autoplay") && this._images.length > 1) {
203
+ this._startAutoplay();
204
+ }
205
+ }
206
+
207
+ disconnectedCallback() {
208
+ this._stopAutoplay();
209
+ this._observer?.disconnect();
210
+ document.removeEventListener("visibilitychange", this._handleVisibility);
211
+ }
212
+
213
+ _collectImages() {
214
+ return Array.from(this.querySelectorAll("img")).map((img) => ({
215
+ src: img.src || img.getAttribute("src"),
216
+ alt: img.alt || "",
217
+ srcset: img.getAttribute("srcset") || "",
218
+ }));
219
+ }
220
+
221
+ _getInterval() {
222
+ return parseInt(this.getAttribute("interval") || "3000", 10);
223
+ }
224
+
225
+ _startAutoplay() {
226
+ this._stopAutoplay();
227
+ this._timer = setInterval(() => {
228
+ this._goTo((this._currentIndex + 1) % this._images.length);
229
+ }, this._getInterval());
230
+ }
231
+
232
+ _stopAutoplay() {
233
+ if (this._timer) {
234
+ clearInterval(this._timer);
235
+ this._timer = null;
236
+ }
237
+ }
238
+
239
+ _goTo(index) {
240
+ const prev = this._currentIndex;
241
+ this._currentIndex = index;
242
+ this._toggleSlide(prev, false);
243
+ this._toggleSlide(index, true);
244
+ }
245
+
246
+ _toggleSlide(i, active) {
247
+ this._slides[i]?.classList.toggle("active", active);
248
+ const dot = this._dots[i];
249
+ if (dot) {
250
+ dot.classList.toggle("active", active);
251
+ dot.setAttribute("aria-selected", String(active));
252
+ }
253
+ }
254
+
255
+ render() {
256
+ const images = this._images;
257
+ const ratio = this.getAttribute("ratio") || "3/2";
258
+
259
+ if (images.length === 0) {
260
+ this.shadowRoot.innerHTML = "";
261
+ return;
262
+ }
263
+
264
+ if (!_sheet) {
265
+ _sheet = new CSSStyleSheet();
266
+ _sheet.replaceSync(STYLES);
267
+ }
268
+ this.shadowRoot.adoptedStyleSheets = [_sheet];
269
+ this.style.setProperty("--carousel-ratio", ratio);
270
+
271
+ const slidesHTML = images
272
+ .map(
273
+ (img, i) => `
274
+ <div class="slide${i === 0 ? " active" : ""}" role="tabpanel" aria-label="${img.alt || `Slide ${i + 1}`}">
275
+ <figure>
276
+ <img src="${img.src}"${img.srcset ? ` srcset="${img.srcset}"` : ""} alt="${img.alt}" loading="${i === 0 ? "eager" : "lazy"}">
277
+ ${img.alt ? `<figcaption>${img.alt}</figcaption>` : ""}
278
+ </figure>
279
+ </div>
280
+ `,
281
+ )
282
+ .join("");
283
+
284
+ const navHTML =
285
+ images.length > 1
286
+ ? `<button class="nav prev" aria-label="Previous slide"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg></button>
287
+ <button class="nav next" aria-label="Next slide"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg></button>`
288
+ : "";
289
+
290
+ const dotsHTML =
291
+ images.length > 1
292
+ ? `<div class="dots" role="tablist" aria-label="Slide navigation">
293
+ ${images.map((_img, i) => `<button class="dot${i === 0 ? " active" : ""}" data-index="${i}" role="tab" aria-label="Slide ${i + 1}" aria-selected="${i === 0}"></button>`).join("")}
294
+ </div>`
295
+ : "";
296
+
297
+ this.shadowRoot.innerHTML = `
298
+ <div class="carousel" role="region" aria-label="Image carousel" tabindex="0">
299
+ <div class="stage">
300
+ <div class="slides">${slidesHTML}</div>
301
+ ${navHTML}
302
+ </div>
303
+ ${dotsHTML}
304
+ </div>
305
+ <slot style="display:none"></slot>
306
+ `;
307
+
308
+ this._slides = Array.from(this.shadowRoot.querySelectorAll(".slide"));
309
+ this._dots = Array.from(this.shadowRoot.querySelectorAll(".dot"));
310
+ }
311
+
312
+ _setupListeners() {
313
+ const root = this.shadowRoot;
314
+ const carousel = root.querySelector(".carousel");
315
+ const n = this._images.length;
316
+
317
+ root.querySelector(".prev")?.addEventListener("click", () => {
318
+ this._resetAutoplay();
319
+ this._goTo((this._currentIndex - 1 + n) % n);
320
+ });
321
+
322
+ root.querySelector(".next")?.addEventListener("click", () => {
323
+ this._resetAutoplay();
324
+ this._goTo((this._currentIndex + 1) % n);
325
+ });
326
+
327
+ root.querySelector(".dots")?.addEventListener("click", (e) => {
328
+ const dot = e.target.closest(".dot");
329
+ if (!dot) return;
330
+ this._resetAutoplay();
331
+ this._goTo(parseInt(dot.dataset.index, 10));
332
+ });
333
+
334
+ // Pause autoplay on hover
335
+ carousel.addEventListener("mouseenter", () => this._stopAutoplay());
336
+ carousel.addEventListener("mouseleave", () => {
337
+ if (this.hasAttribute("autoplay")) this._startAutoplay();
338
+ });
339
+
340
+ // Keyboard navigation
341
+ carousel.addEventListener("keydown", (e) => {
342
+ if (e.key === "ArrowLeft") {
343
+ this._resetAutoplay();
344
+ this._goTo((this._currentIndex - 1 + n) % n);
345
+ } else if (e.key === "ArrowRight") {
346
+ this._resetAutoplay();
347
+ this._goTo((this._currentIndex + 1) % n);
348
+ }
349
+ });
350
+
351
+ // Touch/swipe support
352
+ carousel.addEventListener(
353
+ "touchstart",
354
+ (e) => {
355
+ this._touchStartX = e.touches[0].clientX;
356
+ },
357
+ { passive: true },
358
+ );
359
+
360
+ carousel.addEventListener(
361
+ "touchend",
362
+ (e) => {
363
+ const dx = e.changedTouches[0].clientX - this._touchStartX;
364
+ if (Math.abs(dx) > 40) {
365
+ this._resetAutoplay();
366
+ this._goTo(dx < 0 ? (this._currentIndex + 1) % n : (this._currentIndex - 1 + n) % n);
367
+ }
368
+ },
369
+ { passive: true },
370
+ );
371
+ }
372
+
373
+ _observeVisibility() {
374
+ this._observer = new IntersectionObserver(([entry]) => {
375
+ this._isVisible = entry.isIntersecting;
376
+ if (!entry.isIntersecting) {
377
+ this._stopAutoplay();
378
+ } else if (this.hasAttribute("autoplay")) {
379
+ this._startAutoplay();
380
+ }
381
+ });
382
+ this._observer.observe(this);
383
+ document.addEventListener("visibilitychange", this._handleVisibility);
384
+ }
385
+
386
+ _resetAutoplay() {
387
+ if (this.hasAttribute("autoplay")) {
388
+ this._stopAutoplay();
389
+ this._startAutoplay();
390
+ }
391
+ }
392
+
393
+ static get observedAttributes() {
394
+ return ["autoplay", "interval"];
395
+ }
396
+
397
+ attributeChangedCallback(name, oldValue, newValue) {
398
+ if (oldValue === newValue || !this._images.length) return;
399
+ if (name === "autoplay") {
400
+ newValue !== null ? this._startAutoplay() : this._stopAutoplay();
401
+ }
402
+ if (name === "interval" && this._timer) {
403
+ this._startAutoplay();
404
+ }
405
+ }
406
+ }
407
+
408
+ customElements.define("image-carousel", ImageCarousel);
409
+
410
+ export { ImageCarousel };