hexo-theme-gnix 9.0.0 → 10.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 (66) hide show
  1. package/README.md +4 -2
  2. package/include/hexo/feed.js +5 -5
  3. package/include/hexo/filter.js +25 -1
  4. package/include/hexo/generator/archive.js +116 -0
  5. package/include/hexo/generator/home.js +64 -0
  6. package/include/hexo/generator/index.js +82 -0
  7. package/include/hexo/generator/md_generator.js +87 -0
  8. package/include/hexo/generator/page.js +55 -0
  9. package/include/hexo/generator/tag.js +84 -0
  10. package/include/hexo/helper.js +38 -0
  11. package/include/hexo/i18n.js +183 -0
  12. package/include/util/article_font.js +132 -0
  13. package/include/util/i18n.js +280 -0
  14. package/include/util/theme.js +84 -0
  15. package/languages/en.yml +28 -0
  16. package/languages/zh-CN.yml +28 -0
  17. package/layout/archive.jsx +131 -127
  18. package/layout/common/article.jsx +283 -16
  19. package/layout/common/article_info.jsx +339 -0
  20. package/layout/common/article_media.jsx +11 -4
  21. package/layout/common/comment.jsx +15 -7
  22. package/layout/common/footer.jsx +6 -5
  23. package/layout/common/head.jsx +121 -32
  24. package/layout/common/navbar.jsx +195 -65
  25. package/layout/common/theme_selector.jsx +16 -14
  26. package/layout/layout.jsx +43 -5
  27. package/layout/misc/open_graph.jsx +162 -66
  28. package/layout/misc/paginator.jsx +2 -8
  29. package/layout/plugin/cookie_consent.jsx +252 -53
  30. package/layout/plugin/swup.jsx +1 -1
  31. package/layout/search/insight.jsx +1 -1
  32. package/layout/tag.jsx +3 -2
  33. package/layout/tags.jsx +81 -73
  34. package/package.json +5 -5
  35. package/scripts/index.js +1 -0
  36. package/source/css/archive.css +225 -180
  37. package/source/css/default.css +1162 -98
  38. package/source/css/responsive.css +426 -0
  39. package/source/css/shiki/shiki.css +12 -2081
  40. package/source/css/tags.css +183 -0
  41. package/source/css/twikoo.css +1049 -1045
  42. package/source/img/favicon.svg +1 -6
  43. package/source/img/og_image.webp +0 -0
  44. package/source/js/article-font-utils.js +99 -0
  45. package/source/js/busuanzi.js +91 -24
  46. package/source/js/components/chat.js +169 -50
  47. package/source/js/components/image-carousel.js +152 -108
  48. package/source/js/components/sidenote.js +210 -0
  49. package/source/js/components/text-image-section.js +78 -90
  50. package/source/js/components/theme-stacked.js +65 -33
  51. package/source/js/components/tree.js +30 -16
  52. package/source/js/decrypt.js +7 -2
  53. package/source/js/main.js +428 -5
  54. package/source/js/swup.js +39 -0
  55. package/source/js/theme-selector.js +26 -16
  56. package/include/hexo/generator.js +0 -53
  57. package/layout/misc/article_licensing.jsx +0 -99
  58. package/source/css/responsive/desktop.css +0 -36
  59. package/source/css/responsive/mobile.css +0 -29
  60. package/source/css/responsive/tablet.css +0 -43
  61. package/source/css/responsive/touch.css +0 -155
  62. package/source/img/logo.svg +0 -9
  63. package/source/js/archive-breadcrumb.js +0 -132
  64. package/source/js/host/cookieconsent/3.1.1/build/cookieconsent.min.css +0 -6
  65. package/source/js/host/cookieconsent/3.1.1/build/cookieconsent.min.js +0 -1
  66. package/source/js/swup.bundle.js +0 -1
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * Attributes:
15
15
  * - image: Image URL (required)
16
- * - alt: Image alt text
16
+ * - alt: Image alt text (also used as figcaption)
17
17
  * - width: Image width (default: 300px)
18
18
  * - left: Put image on left (default: image on right)
19
19
  * - font-family: Text font family
@@ -37,7 +37,7 @@ class TextImageSection extends HTMLElement {
37
37
 
38
38
  initZoom() {
39
39
  if (typeof mediumZoom !== "function") return;
40
- const img = this.querySelector(".ti-image img");
40
+ const img = this.querySelector(".ti-figure img");
41
41
  if (img) {
42
42
  mediumZoom(img, { background: "hsla(from var(--mantle) / 0.9)" });
43
43
  }
@@ -48,71 +48,66 @@ class TextImageSection extends HTMLElement {
48
48
 
49
49
  const style = `
50
50
  text-image-section {
51
- display: block;
51
+ /* flow-root establishes a BFC so floats are contained
52
+ and parent block formatting (e.g. blockquote borders)
53
+ don't visually bleed into the image area */
54
+ display: flow-root;
52
55
  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
56
  font-family: var(--ti-font-family, inherit);
68
- font-size: var(--ti-font-size, 1rem);
57
+ font-size: var(--ti-font-size);
69
58
  color: var(--ti-color, inherit);
59
+ line-height: 1.8;
70
60
  }
71
61
 
72
- .ti-image {
73
- position: relative;
74
- z-index: 1;
62
+ text-image-section > .ti-figure {
63
+ float: right;
75
64
  width: var(--ti-image-width, 300px);
76
- margin-bottom: 12px;
77
-
78
- .ti-container > & {
79
- float: right;
80
- margin-left: 24px;
81
- }
65
+ margin: 0 0 12px 24px;
66
+ }
82
67
 
83
- .image-left > & {
84
- float: left;
85
- margin-left: 0;
86
- margin-right: 24px;
87
- }
68
+ text-image-section[left] > .ti-figure {
69
+ float: left;
70
+ margin: 0 24px 12px 0;
71
+ }
88
72
 
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
- }
73
+ text-image-section .ti-figure img {
74
+ display: block;
75
+ width: 100%;
76
+ height: auto;
77
+ border-radius: 8px;
78
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
79
+ }
95
80
 
96
- figure {
97
- margin: 0;
98
- }
81
+ text-image-section .ti-figure figcaption {
82
+ font-family: var(--font-serif);
83
+ font-size: 0.875em;
84
+ color: var(--subtext0);
85
+ text-align: center;
86
+ margin-top: 8px;
87
+ font-style: italic;
88
+ }
99
89
 
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
- }
90
+ /* Tame nested blockquotes inside the text area so they
91
+ don't fight the image's float/stacked layout. */
92
+ text-image-section > blockquote {
93
+ margin: 0;
108
94
  }
109
95
 
110
96
  @media (max-width: 640px) {
111
- .ti-image {
97
+ text-image-section > .ti-figure,
98
+ text-image-section[left] > .ti-figure {
112
99
  float: none;
113
- width: 100%;
114
- margin: 0 0 16px 0;
115
- --ti-image-width: 100%;
100
+ width: auto;
101
+ /* Bottom margin separates the figure from any following
102
+ blockquote so its left border doesn't visually align
103
+ with the image edge */
104
+ margin: 0 0 20px 0;
105
+ }
106
+
107
+ /* When a blockquote follows the figure on mobile, give it
108
+ a touch more breathing room from the image above */
109
+ text-image-section > .ti-figure + blockquote {
110
+ margin-top: 4px;
116
111
  }
117
112
  }
118
113
  `;
@@ -130,48 +125,41 @@ class TextImageSection extends HTMLElement {
130
125
  const image = this.getAttribute("image");
131
126
  const alt = this.getAttribute("alt") || "";
132
127
  const imageWidth = this.getAttribute("width") || "300px";
133
- const imageLeft = this.hasAttribute("left");
134
128
  const fontFamily = this.getAttribute("font-family");
135
129
  const fontSize = this.getAttribute("font-size");
136
130
  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;
131
+
132
+ // Apply CSS variables directly to the host element so we no
133
+ // longer need a wrapping <div> just to hold inline styles.
134
+ this.style.setProperty("--ti-image-width", imageWidth);
135
+ if (fontFamily) this.style.setProperty("--ti-font-family", fontFamily);
136
+ if (fontSize) this.style.setProperty("--ti-font-size", fontSize);
137
+ if (color) this.style.setProperty("--ti-color", color);
138
+
139
+ // Without an image we have nothing extra to inject — leave the
140
+ // user's original markdown/HTML content in place.
141
+ if (!image) return;
142
+
143
+ const figure = document.createElement("figure");
144
+ figure.className = "ti-figure";
145
+
146
+ const img = document.createElement("img");
147
+ img.src = image;
148
+ img.alt = alt;
149
+ img.loading = "lazy";
150
+ figure.appendChild(img);
151
+
152
+ if (alt) {
153
+ const figcaption = document.createElement("figcaption");
154
+ figcaption.textContent = alt;
155
+ figure.appendChild(figcaption);
151
156
  }
152
157
 
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
- `;
158
+ // Prepend the <figure> so the existing user content (text,
159
+ // blockquotes, paragraphs, etc.) stays as direct children of
160
+ // the host element. This preserves semantic structure and
161
+ // avoids re-parsing innerHTML.
162
+ this.insertBefore(figure, this.firstChild);
175
163
  }
176
164
  }
177
165
 
@@ -29,14 +29,23 @@ const PREVIEW_COLORS = [
29
29
  "surface0",
30
30
  ];
31
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
- ];
32
+ const THEME_DATA_CACHE_PREFIX = "themeDataCache";
33
+
34
+ function getThemeOptions() {
35
+ const config = window.__GNIX_THEME_CONFIG__;
36
+
37
+ if (!Array.isArray(config?.themes)) return [];
38
+
39
+ return config.themes.filter((theme) => theme.value !== config.defaultTheme).map((theme) => ({ id: theme.value, name: theme.name || theme.label }));
40
+ }
41
+
42
+ function getThemeDataCacheKey(themes) {
43
+ return `${THEME_DATA_CACHE_PREFIX}:${themes.map((theme) => theme.id).join(",")}`;
44
+ }
45
+
46
+ function hasThemeDataForThemes(themeData, themes) {
47
+ return Boolean(themeData) && themes.every((theme) => themeData[theme.id]);
48
+ }
40
49
 
41
50
  class ThemeStackedElement extends HTMLElement {
42
51
  constructor() {
@@ -45,10 +54,14 @@ class ThemeStackedElement extends HTMLElement {
45
54
  this._cards = [];
46
55
  this._isVisible = false;
47
56
  this._themeData = {};
57
+ this._themes = [];
48
58
  this.attachShadow({ mode: "open" });
49
59
  }
50
60
 
51
61
  async connectedCallback() {
62
+ this._themes = getThemeOptions();
63
+ if (this._themes.length === 0) return;
64
+
52
65
  this._observer = new IntersectionObserver((e) => {
53
66
  this._isVisible = e[0].isIntersecting;
54
67
  });
@@ -65,15 +78,21 @@ class ThemeStackedElement extends HTMLElement {
65
78
  }
66
79
 
67
80
  async loadThemeData() {
68
- if (window.__cachedThemeData) {
81
+ if (hasThemeDataForThemes(window.__cachedThemeData, this._themes)) {
69
82
  this._themeData = window.__cachedThemeData;
70
83
  return;
71
84
  }
85
+
86
+ const cacheKey = getThemeDataCacheKey(this._themes);
87
+
72
88
  try {
73
- const cached = localStorage.getItem("themeDataCache");
89
+ const cached = localStorage.getItem(cacheKey);
74
90
  if (cached) {
75
- this._themeData = window.__cachedThemeData = JSON.parse(cached);
76
- return;
91
+ const cachedThemeData = JSON.parse(cached);
92
+ if (hasThemeDataForThemes(cachedThemeData, this._themes)) {
93
+ this._themeData = window.__cachedThemeData = cachedThemeData;
94
+ return;
95
+ }
77
96
  }
78
97
  } catch (_e) {}
79
98
 
@@ -81,7 +100,7 @@ class ThemeStackedElement extends HTMLElement {
81
100
  temp.style.cssText = "position:absolute;left:-9999px;width:0;height:0;overflow:hidden;";
82
101
  document.body.appendChild(temp);
83
102
 
84
- for (const theme of THEMES) {
103
+ for (const theme of this._themes) {
85
104
  temp.setAttribute("data-theme", theme.id);
86
105
  const computed = window.getComputedStyle(temp);
87
106
  this._themeData[theme.id] = Object.fromEntries(PREVIEW_COLORS.map((c) => [c, computed.getPropertyValue(`--${c}`).trim()]).filter(([, v]) => v));
@@ -90,7 +109,7 @@ class ThemeStackedElement extends HTMLElement {
90
109
  temp.remove();
91
110
  window.__cachedThemeData = this._themeData;
92
111
  try {
93
- localStorage.setItem("themeDataCache", JSON.stringify(this._themeData));
112
+ localStorage.setItem(cacheKey, JSON.stringify(this._themeData));
94
113
  } catch (_e) {}
95
114
  }
96
115
 
@@ -371,23 +390,24 @@ class ThemeStackedElement extends HTMLElement {
371
390
  this.renderDots();
372
391
  this.attachEvents();
373
392
 
374
- const idx = THEMES.findIndex((t) => t.id === this.getCurrentTheme());
393
+ const idx = this._themes.findIndex((t) => t.id === this.getCurrentTheme());
375
394
  this.goTo(idx !== -1 ? idx : 0, false);
376
395
  }
377
396
 
378
397
  renderCards() {
379
398
  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}">
399
+ this._cardStack.innerHTML = this._themes
400
+ .map((theme, i) => {
401
+ const colors = this._themeData[theme.id] || {};
402
+ const themeVars = Object.entries(colors)
403
+ .map(([k, v]) => `--${k}: ${v}`)
404
+ .join(";");
405
+ const swatches = PREVIEW_COLORS.map((c) => {
406
+ const v = colors[c] || "transparent";
407
+ return `<div class="color-swatch" data-color="${c}" data-value="${v}" style="--color:${v}"></div>`;
408
+ }).join("");
409
+ const active = current === theme.id;
410
+ return `<div class="theme-card" data-index="${i}" data-theme="${theme.id}" style="${themeVars}">
391
411
  <h4 class="card-title">${theme.name}</h4>
392
412
  <div class="color-grid">${swatches}</div>
393
413
  <div class="card-footer">
@@ -396,12 +416,13 @@ class ThemeStackedElement extends HTMLElement {
396
416
  </button>
397
417
  </div>
398
418
  </div>`;
399
- }).join("");
419
+ })
420
+ .join("");
400
421
  this._cards = [...this._cardStack.querySelectorAll(".theme-card")];
401
422
  }
402
423
 
403
424
  renderDots() {
404
- this._dotsContainer.innerHTML = THEMES.map((_, i) => `<button class="dot" data-index="${i}" aria-label="Go to theme ${i + 1}"></button>`).join("");
425
+ this._dotsContainer.innerHTML = this._themes.map((_, i) => `<button class="dot" data-index="${i}" aria-label="Go to theme ${i + 1}"></button>`).join("");
405
426
  this._dotsContainer.querySelectorAll(".dot").forEach((dot, i) => dot.addEventListener("click", () => this.goTo(i)));
406
427
  }
407
428
 
@@ -421,12 +442,14 @@ class ThemeStackedElement extends HTMLElement {
421
442
  }
422
443
 
423
444
  goTo(index, animate = true) {
424
- this._currentIndex = ((index % THEMES.length) + THEMES.length) % THEMES.length;
445
+ if (this._themes.length === 0) return;
446
+
447
+ this._currentIndex = ((index % this._themes.length) + this._themes.length) % this._themes.length;
425
448
  this.updateStack();
426
449
  if (animate)
427
450
  this.dispatchEvent(
428
451
  new CustomEvent("themeChange", {
429
- detail: { index: this._currentIndex, theme: THEMES[this._currentIndex] },
452
+ detail: { index: this._currentIndex, theme: this._themes[this._currentIndex] },
430
453
  }),
431
454
  );
432
455
  }
@@ -468,7 +491,7 @@ class ThemeStackedElement extends HTMLElement {
468
491
  } else if (e.key === "ArrowRight" || e.key === " ") {
469
492
  e.preventDefault();
470
493
  this.next();
471
- } else if (e.key === "Enter") this.applyTheme(THEMES[this._currentIndex].id);
494
+ } else if (e.key === "Enter") this.applyTheme(this._themes[this._currentIndex].id);
472
495
  };
473
496
  document.addEventListener("keydown", this._keyHandler);
474
497
 
@@ -501,7 +524,16 @@ class ThemeStackedElement extends HTMLElement {
501
524
  }
502
525
 
503
526
  getCurrentTheme() {
504
- return localStorage.getItem("themePreference") || document.documentElement.getAttribute("data-theme") || "mocha";
527
+ const config = window.__GNIX_THEME_CONFIG__;
528
+ let storedTheme = null;
529
+
530
+ try {
531
+ storedTheme = config?.storageKey ? localStorage.getItem(config.storageKey) : null;
532
+ } catch (_e) {}
533
+
534
+ if (storedTheme && storedTheme !== config?.defaultTheme) return storedTheme;
535
+
536
+ return document.documentElement.getAttribute("data-theme") || config?.systemTheme?.dark || this._themes[0]?.id;
505
537
  }
506
538
 
507
539
  applyTheme(themeId) {
@@ -509,7 +541,7 @@ class ThemeStackedElement extends HTMLElement {
509
541
  window.applyTheme(themeId, true);
510
542
  this._cards.forEach((card, i) => {
511
543
  const btn = card.querySelector(".apply-btn");
512
- const match = THEMES[i].id === themeId;
544
+ const match = this._themes[i].id === themeId;
513
545
  btn.classList.toggle("applied", match);
514
546
  btn.textContent = match ? "Applied ✓" : "Apply Theme";
515
547
  });
@@ -10,7 +10,7 @@
10
10
  * - components
11
11
  * - accordion.js
12
12
  * - tree.js
13
- * - styles
13
+ * - styles/
14
14
  * - main.css
15
15
  * - package.json
16
16
  * - README.md
@@ -285,11 +285,7 @@ class XTree extends HTMLElement {
285
285
  const childUL = li.querySelector(":scope > ul");
286
286
  const children = childUL ? this.parseUL(childUL) : [];
287
287
 
288
- result.push({
289
- label: text,
290
- children,
291
- hasChildren: children.length > 0,
292
- });
288
+ result.push(this.createNode(text, children));
293
289
  });
294
290
 
295
291
  return result;
@@ -313,7 +309,7 @@ class XTree extends HTMLElement {
313
309
  `;
314
310
 
315
311
  this.shadowRoot.querySelectorAll(".tree-children").forEach((el) => {
316
- el.style.maxHeight = el.scrollHeight + "px";
312
+ el.style.maxHeight = `${el.scrollHeight}px`;
317
313
  });
318
314
 
319
315
  this.shadowRoot.querySelectorAll(".tree-toggle:not(.collapsed)").forEach((btn) => {
@@ -333,23 +329,40 @@ class XTree extends HTMLElement {
333
329
  return ICON_FILE;
334
330
  }
335
331
 
332
+ createNode(label, children = []) {
333
+ const normalizedLabel = label.trim();
334
+ const isExplicitFolder = /\/+$/.test(normalizedLabel);
335
+ const displayLabel = isExplicitFolder ? normalizedLabel.replace(/\/+$/, "") : normalizedLabel;
336
+ const hasChildren = children.length > 0;
337
+
338
+ return {
339
+ label: normalizedLabel,
340
+ displayLabel: displayLabel || normalizedLabel,
341
+ children,
342
+ hasChildren,
343
+ isFolder: isExplicitFolder || hasChildren,
344
+ };
345
+ }
346
+
336
347
  renderTreeItems(items) {
337
348
  return items
338
349
  .map((item) => {
339
- const isFolder = item.hasChildren;
340
- const icon = this.getIcon(item.label, isFolder);
341
- const extClass = isFolder ? "folder" : this.getExtClass(item.label);
350
+ const isFolder = item.isFolder || item.hasChildren;
351
+ const canToggle = item.hasChildren;
352
+ const label = item.displayLabel || item.label;
353
+ const icon = this.getIcon(label, isFolder);
354
+ const extClass = isFolder ? "folder" : this.getExtClass(label);
342
355
 
343
- const toggleHTML = isFolder ? `<button class="tree-toggle" aria-expanded="true">${ICON_CHEVRON}</button>` : `<span class="tree-toggle-placeholder"></span>`;
356
+ const toggleHTML = canToggle ? `<button class="tree-toggle" aria-expanded="true">${ICON_CHEVRON}</button>` : `<span class="tree-toggle-placeholder"></span>`;
344
357
 
345
- const childrenHTML = isFolder ? `<ul class="tree-children">${this.renderTreeItems(item.children)}</ul>` : "";
358
+ const childrenHTML = canToggle ? `<ul class="tree-children">${this.renderTreeItems(item.children)}</ul>` : "";
346
359
 
347
360
  return `
348
361
  <li class="tree-item">
349
362
  <div class="tree-row">
350
363
  ${toggleHTML}
351
364
  <span class="tree-icon ${extClass}">${icon}</span>
352
- <span class="tree-label">${this.escapeHTML(item.label)}</span>
365
+ <span class="tree-label">${this.escapeHTML(label)}</span>
353
366
  </div>
354
367
  ${childrenHTML}
355
368
  </li>
@@ -374,11 +387,11 @@ class XTree extends HTMLElement {
374
387
  btn.classList.remove("collapsed");
375
388
  btn.setAttribute("aria-expanded", "true");
376
389
  children.classList.remove("collapsed");
377
- children.style.maxHeight = children.scrollHeight + "px";
390
+ children.style.maxHeight = `${children.scrollHeight}px`;
378
391
  } else {
379
392
  btn.classList.add("collapsed");
380
393
  btn.setAttribute("aria-expanded", "false");
381
- children.style.maxHeight = children.scrollHeight + "px";
394
+ children.style.maxHeight = `${children.scrollHeight}px`;
382
395
  children.offsetHeight;
383
396
  children.classList.add("collapsed");
384
397
  }
@@ -413,7 +426,7 @@ class XTree extends HTMLElement {
413
426
  const indent = match[1].length;
414
427
  const label = match[2].trim();
415
428
 
416
- const node = { label, children: [], hasChildren: false };
429
+ const node = this.createNode(label);
417
430
 
418
431
  while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
419
432
  stack.pop();
@@ -422,6 +435,7 @@ class XTree extends HTMLElement {
422
435
  const parent = stack[stack.length - 1].node;
423
436
  parent.children.push(node);
424
437
  parent.hasChildren = true;
438
+ parent.isFolder = true;
425
439
 
426
440
  stack.push({ indent, node });
427
441
  }
@@ -107,6 +107,11 @@ function init() {
107
107
 
108
108
  init();
109
109
 
110
- if (typeof swup !== "undefined") {
111
- swup.hooks.on("page:view", init);
110
+ function bindSwupDecryptHook(swupInstance) {
111
+ if (!swupInstance || swupInstance.gnixDecryptPageHookBound) return;
112
+ swupInstance.gnixDecryptPageHookBound = true;
113
+ swupInstance.hooks.on("page:view", init);
112
114
  }
115
+
116
+ bindSwupDecryptHook(window.swup);
117
+ document.addEventListener("gnix:swup-ready", (event) => bindSwupDecryptHook(event.detail?.swup), { once: true });