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,180 @@
1
+ /**
2
+ * Text-Image Section Custom Element
3
+ * A responsive text-image layout component
4
+ *
5
+ * Usage:
6
+ * <text-image-section
7
+ * image="https://example.com/image.jpg"
8
+ * alt="Image description"
9
+ * width="300px"
10
+ * >
11
+ * Your text content here...
12
+ * </text-image-section>
13
+ *
14
+ * Attributes:
15
+ * - image: Image URL (required)
16
+ * - alt: Image alt text
17
+ * - width: Image width (default: 300px)
18
+ * - left: Put image on left (default: image on right)
19
+ * - font-family: Text font family
20
+ * - font-size: Text font size (default: 0.8rem)
21
+ * - color: Text color
22
+ */
23
+
24
+ let styleSheetInjected = false;
25
+
26
+ class TextImageSection extends HTMLElement {
27
+ constructor() {
28
+ super();
29
+ this._rendered = false;
30
+ }
31
+
32
+ connectedCallback() {
33
+ this.injectStyles();
34
+ this.render();
35
+ this.initZoom();
36
+ }
37
+
38
+ initZoom() {
39
+ if (typeof mediumZoom !== "function") return;
40
+ const img = this.querySelector(".ti-image img");
41
+ if (img) {
42
+ mediumZoom(img, { background: "hsla(from var(--mantle) / 0.9)" });
43
+ }
44
+ }
45
+
46
+ injectStyles() {
47
+ if (styleSheetInjected) return;
48
+
49
+ const style = `
50
+ text-image-section {
51
+ display: block;
52
+ margin: 1em 0;
53
+ }
54
+
55
+ .ti-container {
56
+ overflow: hidden;
57
+
58
+ &::after {
59
+ content: "";
60
+ display: table;
61
+ clear: both;
62
+ }
63
+ }
64
+
65
+ .ti-text {
66
+ line-height: 1.8;
67
+ font-family: var(--ti-font-family, inherit);
68
+ font-size: var(--ti-font-size, 1rem);
69
+ color: var(--ti-color, inherit);
70
+ }
71
+
72
+ .ti-image {
73
+ position: relative;
74
+ z-index: 1;
75
+ width: var(--ti-image-width, 300px);
76
+ margin-bottom: 12px;
77
+
78
+ .ti-container > & {
79
+ float: right;
80
+ margin-left: 24px;
81
+ }
82
+
83
+ .image-left > & {
84
+ float: left;
85
+ margin-left: 0;
86
+ margin-right: 24px;
87
+ }
88
+
89
+ img {
90
+ width: 100%;
91
+ height: auto;
92
+ border-radius: 8px;
93
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
94
+ }
95
+
96
+ figure {
97
+ margin: 0;
98
+ }
99
+
100
+ figcaption {
101
+ font-family: var(--font-serif);
102
+ font-size: 0.875em;
103
+ color: var(--subtext0);
104
+ text-align: center;
105
+ margin-top: 8px;
106
+ font-style: italic;
107
+ }
108
+ }
109
+
110
+ @media (max-width: 640px) {
111
+ .ti-image {
112
+ float: none;
113
+ width: 100%;
114
+ margin: 0 0 16px 0;
115
+ --ti-image-width: 100%;
116
+ }
117
+ }
118
+ `;
119
+
120
+ const styleEl = document.createElement("style");
121
+ styleEl.textContent = style;
122
+ document.head.appendChild(styleEl);
123
+ styleSheetInjected = true;
124
+ }
125
+
126
+ render() {
127
+ if (this._rendered) return;
128
+ this._rendered = true;
129
+
130
+ const image = this.getAttribute("image");
131
+ const alt = this.getAttribute("alt") || "";
132
+ const imageWidth = this.getAttribute("width") || "300px";
133
+ const imageLeft = this.hasAttribute("left");
134
+ const fontFamily = this.getAttribute("font-family");
135
+ const fontSize = this.getAttribute("font-size");
136
+ const color = this.getAttribute("color");
137
+ const contentNodes = Array.from(this.childNodes).filter((node) => {
138
+ return node.nodeType !== Node.ELEMENT_NODE || node.tagName.toLowerCase() !== "text-image-section";
139
+ });
140
+
141
+ const content = contentNodes
142
+ .map((node) => {
143
+ return node.nodeType === Node.TEXT_NODE ? node.textContent : node.outerHTML;
144
+ })
145
+ .join("")
146
+ .trim();
147
+
148
+ if (!image) {
149
+ this.innerHTML = `<div class="ti-container"><div class="ti-text">${content}</div></div>`;
150
+ return;
151
+ }
152
+
153
+ const containerClass = imageLeft ? "ti-container image-left" : "ti-container";
154
+
155
+ const figureHtml = alt
156
+ ? `<figure>
157
+ <img src="${image}" alt="${alt}" loading="lazy">
158
+ <figcaption>${alt}</figcaption>
159
+ </figure>`
160
+ : `<img src="${image}" alt="${alt}" loading="lazy">`;
161
+
162
+ const styleAttrs = [];
163
+ styleAttrs.push(`--ti-image-width: ${imageWidth};`);
164
+ if (fontFamily) styleAttrs.push(`--ti-font-family: ${fontFamily};`);
165
+ if (fontSize) styleAttrs.push(`--ti-font-size: ${fontSize};`);
166
+ if (color) styleAttrs.push(`--ti-color: ${color};`);
167
+ this.innerHTML = `
168
+ <div class="${containerClass}" style="${styleAttrs.join(" ")}">
169
+ <div class="ti-image">
170
+ ${figureHtml}
171
+ </div>
172
+ <div class="ti-text">${content}</div>
173
+ </div>
174
+ `;
175
+ }
176
+ }
177
+
178
+ customElements.define("text-image-section", TextImageSection);
179
+
180
+ export { TextImageSection };
@@ -0,0 +1,526 @@
1
+ /**
2
+ * Theme Stacked Card Selector Component
3
+ * Displays themes as a stack of interactive cards with circular navigation
4
+ */
5
+
6
+ const PREVIEW_COLORS = [
7
+ "rosewater",
8
+ "flamingo",
9
+ "pink",
10
+ "mauve",
11
+ "red",
12
+ "maroon",
13
+ "peach",
14
+ "yellow",
15
+ "green",
16
+ "teal",
17
+ "sky",
18
+ "sapphire",
19
+ "blue",
20
+ "lavender",
21
+ "text",
22
+ "subtext1",
23
+ "subtext0",
24
+ "overlay2",
25
+ "overlay1",
26
+ "overlay0",
27
+ "surface2",
28
+ "surface1",
29
+ "surface0",
30
+ ];
31
+
32
+ const THEMES = [
33
+ { id: "latte", name: "Catppuccin Latte" },
34
+ { id: "nord", name: "Nord Light" },
35
+ { id: "nord_night", name: "Nord Night" },
36
+ { id: "rose_pine", name: "Rosé Pine" },
37
+ { id: "mocha", name: "Catppuccin Mocha" },
38
+ { id: "tokyo_night", name: "Tokyo Night" },
39
+ ];
40
+
41
+ class ThemeStackedElement extends HTMLElement {
42
+ constructor() {
43
+ super();
44
+ this._currentIndex = 0;
45
+ this._cards = [];
46
+ this._isVisible = false;
47
+ this._themeData = {};
48
+ this.attachShadow({ mode: "open" });
49
+ }
50
+
51
+ async connectedCallback() {
52
+ this._observer = new IntersectionObserver((e) => {
53
+ this._isVisible = e[0].isIntersecting;
54
+ });
55
+ this._observer.observe(this);
56
+ await this.loadThemeData();
57
+ this.render();
58
+ this.init();
59
+ }
60
+
61
+ disconnectedCallback() {
62
+ this._observer?.disconnect();
63
+ document.removeEventListener("keydown", this._keyHandler);
64
+ document.removeEventListener("mouseup", this._mouseUpHandler);
65
+ }
66
+
67
+ async loadThemeData() {
68
+ if (window.__cachedThemeData) {
69
+ this._themeData = window.__cachedThemeData;
70
+ return;
71
+ }
72
+ try {
73
+ const cached = localStorage.getItem("themeDataCache");
74
+ if (cached) {
75
+ this._themeData = window.__cachedThemeData = JSON.parse(cached);
76
+ return;
77
+ }
78
+ } catch (_e) {}
79
+
80
+ const temp = document.createElement("div");
81
+ temp.style.cssText = "position:absolute;left:-9999px;width:0;height:0;overflow:hidden;";
82
+ document.body.appendChild(temp);
83
+
84
+ for (const theme of THEMES) {
85
+ temp.setAttribute("data-theme", theme.id);
86
+ const computed = window.getComputedStyle(temp);
87
+ this._themeData[theme.id] = Object.fromEntries(PREVIEW_COLORS.map((c) => [c, computed.getPropertyValue(`--${c}`).trim()]).filter(([, v]) => v));
88
+ }
89
+
90
+ temp.remove();
91
+ window.__cachedThemeData = this._themeData;
92
+ try {
93
+ localStorage.setItem("themeDataCache", JSON.stringify(this._themeData));
94
+ } catch (_e) {}
95
+ }
96
+
97
+ render() {
98
+ this.shadowRoot.innerHTML = `
99
+ <style>
100
+ :host {
101
+ display: block;
102
+ width: 100%;
103
+ padding: 2rem 0;
104
+ }
105
+
106
+ * {
107
+ box-sizing: border-box;
108
+ }
109
+
110
+ .stacked-container {
111
+ display: flex;
112
+ flex-direction: column;
113
+ align-items: center;
114
+ gap: 1rem;
115
+ width: 100%;
116
+ max-width: 1100px;
117
+ margin: 0 auto;
118
+ }
119
+
120
+ .card-stack {
121
+ position: relative;
122
+ width: 100%;
123
+ padding: 40px 0;
124
+ perspective: 1200px;
125
+ overflow: hidden;
126
+ display: grid;
127
+ place-items: center;
128
+ }
129
+
130
+ .theme-card {
131
+ grid-area: 1 / 1;
132
+ position: relative;
133
+ width: min(550px, 90%);
134
+ background: var(--base);
135
+ border: 2px solid color-mix(in oklch, var(--base) 80%, var(--text));
136
+ border-radius: 16px;
137
+ padding: 1.5rem;
138
+ cursor: grab;
139
+ transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1);
140
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
141
+ user-select: none;
142
+ touch-action: pan-y;
143
+ opacity: 0;
144
+ transform: scale(0.8) translateX(200px);
145
+ pointer-events: none;
146
+ contain: layout style;
147
+
148
+ &.active {
149
+ opacity: 1;
150
+ transform: scale(1) translateX(0) translateZ(0);
151
+ z-index: 10;
152
+ pointer-events: auto;
153
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
154
+ }
155
+
156
+ &.prev {
157
+ opacity: 0.6;
158
+ transform: scale(0.85) translateX(-150px) rotateY(10deg);
159
+ z-index: 5;
160
+ pointer-events: auto;
161
+ }
162
+
163
+ &.next {
164
+ opacity: 0.6;
165
+ transform: scale(0.85) translateX(150px) rotateY(-10deg);
166
+ z-index: 5;
167
+ pointer-events: auto;
168
+ }
169
+
170
+ &.hidden {
171
+ opacity: 0;
172
+ transform: scale(0.7) translateX(0);
173
+ z-index: 0;
174
+ }
175
+ }
176
+
177
+ @media (max-width: 640px) {
178
+ .theme-card {
179
+ width: min(340px, 90%);
180
+ &.prev {
181
+ transform: scale(0.9) translateY(-15%)translateZ(-50px);
182
+ }
183
+
184
+ &.next {
185
+ transform: scale(0.9) translateY(15%)translateZ(-100px);
186
+ }
187
+ }
188
+
189
+ }
190
+ .card-title {
191
+ font-family: var(--font-handwriting);
192
+ font-size: 2em;
193
+ font-weight: 600;
194
+ color: var(--lavender);
195
+ margin: 0 0 0.75rem;
196
+ text-align: center;
197
+ }
198
+
199
+ .color-grid {
200
+ display: grid;
201
+ grid-template-columns: repeat(9, 1fr);
202
+ gap: 6px;
203
+ margin-bottom: 1rem;
204
+
205
+ @media (max-width: 640px) {
206
+ gap: 4px;
207
+ }
208
+ }
209
+
210
+ .color-swatch {
211
+ aspect-ratio: 1 / 1;
212
+ border-radius: 6px;
213
+ cursor: pointer;
214
+ transition: transform 0.2s ease;
215
+ border: 2px solid hsl(from var(--color) h s calc(l - 10) / 0.7);
216
+ background-color: var(--color);
217
+ position: relative;
218
+
219
+ &:hover {
220
+ transform: scale(1.15);
221
+ z-index: 10;
222
+
223
+ &::before,
224
+ &::after {
225
+ opacity: 1;
226
+ visibility: visible;
227
+ }
228
+ }
229
+
230
+ &::before {
231
+ content: attr(data-color) "\\A" attr(data-value);
232
+ text-transform: uppercase;
233
+ position: absolute;
234
+ bottom: 100%;
235
+ left: 50%;
236
+ transform: translateX(-50%) translateY(-6px);
237
+ background: var(--crust);
238
+ color: var(--text);
239
+ padding: 0.4rem 0.6rem;
240
+ border-radius: 6px;
241
+ font-size: 0.7rem;
242
+ font-family: var(--font-mono);
243
+ white-space: pre;
244
+ text-align: center;
245
+ line-height: 1.4;
246
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
247
+ border: 1px solid var(--surface0);
248
+ z-index: 100;
249
+ pointer-events: none;
250
+ opacity: 0;
251
+ visibility: hidden;
252
+ transition: opacity 0.15s ease, visibility 0.15s ease;
253
+ }
254
+ }
255
+
256
+ .card-footer {
257
+ display: flex;
258
+ }
259
+
260
+ .apply-btn {
261
+ flex: 1;
262
+ padding: 0.6rem 1rem;
263
+ background: var(--blue);
264
+ color: var(--crust);
265
+ border: none;
266
+ border-radius: 8px;
267
+ font-family: var(--font-mono);
268
+ font-size: 0.85rem;
269
+ font-weight: 600;
270
+ cursor: pointer;
271
+ transition: all 0.2s ease;
272
+
273
+ &:hover {
274
+ background: var(--blue);
275
+ transform: translateY(-1px);
276
+ }
277
+
278
+ &.applied {
279
+ background: var(--green);
280
+ }
281
+ }
282
+
283
+ .controls {
284
+ display: flex;
285
+ align-items: center;
286
+ gap: 1.5rem;
287
+ }
288
+
289
+ .nav-btn {
290
+ width: 40px;
291
+ height: 40px;
292
+ border: 1px solid var(--surface0);
293
+ border-radius: 8px;
294
+ background: var(--base);
295
+ color: var(--text);
296
+ cursor: pointer;
297
+ display: flex;
298
+ align-items: center;
299
+ justify-content: center;
300
+ transition: all 0.2s ease;
301
+
302
+ &:hover {
303
+ border-color: var(--overlay0);
304
+ color: var(--mauve);
305
+ }
306
+
307
+ &:active {
308
+ transform: scale(0.95);
309
+ }
310
+ }
311
+
312
+ .dots {
313
+ display: flex;
314
+ gap: 8px;
315
+ }
316
+
317
+ .dot {
318
+ width: 8px;
319
+ height: 8px;
320
+ border-radius: 50%;
321
+ background: var(--surface0);
322
+ border: none;
323
+ cursor: pointer;
324
+ transition: all 0.2s ease;
325
+ padding: 0;
326
+ margin: 0;
327
+ display: inline-block;
328
+ aspect-ratio: 1;
329
+
330
+ &.active {
331
+ background: var(--mauve);
332
+ width: 24px;
333
+ border-radius: 4px;
334
+ }
335
+
336
+ &:hover {
337
+ background: var(--overlay0);
338
+ }
339
+ }
340
+ </style>
341
+
342
+ <div class="stacked-container">
343
+ <div class="card-stack" id="card-stack"></div>
344
+
345
+ <div class="controls">
346
+ <button class="nav-btn" id="prev-btn" aria-label="Previous theme">
347
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
348
+ <path d="M15 18l-6-6 6-6"/>
349
+ </svg>
350
+ </button>
351
+
352
+ <div class="dots" id="dots"></div>
353
+
354
+ <button class="nav-btn" id="next-btn" aria-label="Next theme">
355
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
356
+ <path d="M9 18l6-6-6-6"/>
357
+ </svg>
358
+ </button>
359
+ </div>
360
+ </div>
361
+ `;
362
+ }
363
+
364
+ init() {
365
+ this._cardStack = this.shadowRoot.querySelector("#card-stack");
366
+ this._dotsContainer = this.shadowRoot.querySelector("#dots");
367
+ this._prevBtn = this.shadowRoot.querySelector("#prev-btn");
368
+ this._nextBtn = this.shadowRoot.querySelector("#next-btn");
369
+
370
+ this.renderCards();
371
+ this.renderDots();
372
+ this.attachEvents();
373
+
374
+ const idx = THEMES.findIndex((t) => t.id === this.getCurrentTheme());
375
+ this.goTo(idx !== -1 ? idx : 0, false);
376
+ }
377
+
378
+ renderCards() {
379
+ const current = this.getCurrentTheme();
380
+ this._cardStack.innerHTML = THEMES.map((theme, i) => {
381
+ const colors = this._themeData[theme.id] || {};
382
+ const themeVars = Object.entries(colors)
383
+ .map(([k, v]) => `--${k}: ${v}`)
384
+ .join(";");
385
+ const swatches = PREVIEW_COLORS.map((c) => {
386
+ const v = colors[c] || "transparent";
387
+ return `<div class="color-swatch" data-color="${c}" data-value="${v}" style="--color:${v}"></div>`;
388
+ }).join("");
389
+ const active = current === theme.id;
390
+ return `<div class="theme-card" data-index="${i}" data-theme="${theme.id}" style="${themeVars}">
391
+ <h4 class="card-title">${theme.name}</h4>
392
+ <div class="color-grid">${swatches}</div>
393
+ <div class="card-footer">
394
+ <button class="apply-btn${active ? " applied" : ""}" data-theme="${theme.id}">
395
+ ${active ? "Applied ✓" : "Apply"}
396
+ </button>
397
+ </div>
398
+ </div>`;
399
+ }).join("");
400
+ this._cards = [...this._cardStack.querySelectorAll(".theme-card")];
401
+ }
402
+
403
+ renderDots() {
404
+ this._dotsContainer.innerHTML = THEMES.map((_, i) => `<button class="dot" data-index="${i}" aria-label="Go to theme ${i + 1}"></button>`).join("");
405
+ this._dotsContainer.querySelectorAll(".dot").forEach((dot, i) => dot.addEventListener("click", () => this.goTo(i)));
406
+ }
407
+
408
+ updateStack() {
409
+ const total = this._cards.length;
410
+ const distClass = { 0: "active", "-1": "prev", 1: "next" };
411
+ this._cards.forEach((card, i) => {
412
+ const cls = distClass[this.getDistance(i, this._currentIndex, total)] ?? "hidden";
413
+ card.className = `theme-card ${cls}`;
414
+ });
415
+ this._dotsContainer.querySelectorAll(".dot").forEach((dot, i) => dot.classList.toggle("active", i === this._currentIndex));
416
+ }
417
+
418
+ getDistance(index, current, total) {
419
+ const d = (index - current + total) % total;
420
+ return d === 0 ? 0 : d === 1 ? 1 : d === total - 1 ? -1 : 2;
421
+ }
422
+
423
+ goTo(index, animate = true) {
424
+ this._currentIndex = ((index % THEMES.length) + THEMES.length) % THEMES.length;
425
+ this.updateStack();
426
+ if (animate)
427
+ this.dispatchEvent(
428
+ new CustomEvent("themeChange", {
429
+ detail: { index: this._currentIndex, theme: THEMES[this._currentIndex] },
430
+ }),
431
+ );
432
+ }
433
+
434
+ next() {
435
+ this.goTo(this._currentIndex + 1);
436
+ }
437
+ prev() {
438
+ this.goTo(this._currentIndex - 1);
439
+ }
440
+
441
+ attachEvents() {
442
+ this._prevBtn.addEventListener("click", () => this.prev());
443
+ this._nextBtn.addEventListener("click", () => this.next());
444
+
445
+ this._cardStack.addEventListener("click", (e) => {
446
+ const swatch = e.target.closest(".color-swatch");
447
+ if (swatch) {
448
+ e.stopPropagation();
449
+ const hex = (swatch.dataset.value || "").replace("#", "");
450
+ if (hex && hex !== "transparent") window.open(`https://www.colorhexa.com/${hex}`, "_blank");
451
+ return;
452
+ }
453
+ const applyBtn = e.target.closest(".apply-btn");
454
+ if (applyBtn) {
455
+ e.stopPropagation();
456
+ this.applyTheme(applyBtn.dataset.theme);
457
+ return;
458
+ }
459
+ const card = e.target.closest(".theme-card");
460
+ if (card && !card.classList.contains("active")) this.goTo(Number(card.dataset.index));
461
+ });
462
+
463
+ this._keyHandler = (e) => {
464
+ if (!this._isVisible) return;
465
+ if (e.key === "ArrowLeft") {
466
+ e.preventDefault();
467
+ this.prev();
468
+ } else if (e.key === "ArrowRight" || e.key === " ") {
469
+ e.preventDefault();
470
+ this.next();
471
+ } else if (e.key === "Enter") this.applyTheme(THEMES[this._currentIndex].id);
472
+ };
473
+ document.addEventListener("keydown", this._keyHandler);
474
+
475
+ let startX = 0,
476
+ dragging = false;
477
+ const onStart = (x) => {
478
+ if (!this._isVisible) return;
479
+ startX = x;
480
+ dragging = true;
481
+ };
482
+ const onEnd = (x) => {
483
+ if (!dragging) return;
484
+ dragging = false;
485
+ const diff = startX - x;
486
+ if (Math.abs(diff) > 50) diff > 0 ? this.next() : this.prev();
487
+ };
488
+
489
+ this._cardStack.addEventListener("touchstart", (e) => onStart(e.touches[0].clientX), { passive: true });
490
+ this._cardStack.addEventListener("touchend", (e) => onEnd(e.changedTouches[0].clientX));
491
+ this._cardStack.addEventListener("mousedown", (e) => {
492
+ onStart(e.clientX);
493
+ this._cardStack.style.cursor = "grabbing";
494
+ });
495
+ this._mouseUpHandler = (e) => {
496
+ if (!this._isVisible) return;
497
+ onEnd(e.clientX);
498
+ this._cardStack.style.cursor = "";
499
+ };
500
+ document.addEventListener("mouseup", this._mouseUpHandler, { passive: true });
501
+ }
502
+
503
+ getCurrentTheme() {
504
+ return localStorage.getItem("themePreference") || document.documentElement.getAttribute("data-theme") || "mocha";
505
+ }
506
+
507
+ applyTheme(themeId) {
508
+ if (!window.applyTheme) return;
509
+ window.applyTheme(themeId, true);
510
+ this._cards.forEach((card, i) => {
511
+ const btn = card.querySelector(".apply-btn");
512
+ const match = THEMES[i].id === themeId;
513
+ btn.classList.toggle("applied", match);
514
+ btn.textContent = match ? "Applied ✓" : "Apply Theme";
515
+ });
516
+ const btn = this._cards[this._currentIndex].querySelector(".apply-btn");
517
+ btn.style.transform = "scale(0.95)";
518
+ setTimeout(() => {
519
+ btn.style.transform = "";
520
+ }, 150);
521
+ }
522
+ }
523
+
524
+ if (!customElements.get("theme-stacked")) {
525
+ customElements.define("theme-stacked", ThemeStackedElement);
526
+ }