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
@@ -11,12 +11,17 @@
11
11
  * Attributes:
12
12
  * - autoplay: Enable automatic slide advancement
13
13
  * - interval: Autoplay interval in ms (default: 3000)
14
- * - ratio: Aspect ratio as CSS value (default: 3/2)
14
+ * - ratio: Aspect ratio as CSS value (default: derived from first image,
15
+ * falls back to 3/2 while loading or if dimensions are unknown)
15
16
  */
16
17
 
17
18
  // Shared stylesheet — parsed once, reused across all carousel instances
18
19
  let _sheet;
19
20
 
21
+ const DEFAULT_INTERVAL = 3000;
22
+ const FALLBACK_RATIO = "3 / 2";
23
+ const SWIPE_THRESHOLD_PX = 40;
24
+
20
25
  const STYLES = `
21
26
  :host {
22
27
  display: block;
@@ -43,7 +48,7 @@ const STYLES = `
43
48
  .slides {
44
49
  position: relative;
45
50
  width: 100%;
46
- aspect-ratio: var(--carousel-ratio, 3/2);
51
+ aspect-ratio: var(--carousel-ratio, 3 / 2);
47
52
  }
48
53
 
49
54
  .slide {
@@ -174,6 +179,9 @@ const STYLES = `
174
179
  }
175
180
  `;
176
181
 
182
+ const CHEVRON_LEFT = `<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>`;
183
+ const CHEVRON_RIGHT = `<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>`;
184
+
177
185
  class ImageCarousel extends HTMLElement {
178
186
  constructor() {
179
187
  super();
@@ -188,19 +196,21 @@ class ImageCarousel extends HTMLElement {
188
196
  this._isVisible = true;
189
197
  this._handleVisibility = () => {
190
198
  if (document.hidden) this._stopAutoplay();
191
- else if (this._isVisible && this.hasAttribute("autoplay")) this._startAutoplay();
199
+ else if (this._isVisible) this._maybeStartAutoplay();
192
200
  };
193
201
  }
194
202
 
195
203
  connectedCallback() {
196
204
  this._images = this._collectImages();
197
- this.render();
205
+ if (!this._images.length) return;
206
+
207
+ this._resolveRatio();
208
+ this._render();
209
+
198
210
  if (this._images.length > 1) {
199
211
  this._setupListeners();
200
212
  this._observeVisibility();
201
- }
202
- if (this.hasAttribute("autoplay") && this._images.length > 1) {
203
- this._startAutoplay();
213
+ this._maybeStartAutoplay();
204
214
  }
205
215
  }
206
216
 
@@ -210,6 +220,23 @@ class ImageCarousel extends HTMLElement {
210
220
  document.removeEventListener("visibilitychange", this._handleVisibility);
211
221
  }
212
222
 
223
+ static get observedAttributes() {
224
+ return ["autoplay", "interval", "ratio"];
225
+ }
226
+
227
+ attributeChangedCallback(name, oldValue, newValue) {
228
+ if (oldValue === newValue || !this._images.length) return;
229
+ if (name === "autoplay") {
230
+ newValue !== null ? this._startAutoplay() : this._stopAutoplay();
231
+ } else if (name === "interval" && this._timer) {
232
+ this._startAutoplay();
233
+ } else if (name === "ratio") {
234
+ this._resolveRatio();
235
+ }
236
+ }
237
+
238
+ // ─── content & layout ──────────────────────────────────────────────
239
+
213
240
  _collectImages() {
214
241
  return Array.from(this.querySelectorAll("img")).map((img) => ({
215
242
  src: img.src || img.getAttribute("src"),
@@ -218,57 +245,53 @@ class ImageCarousel extends HTMLElement {
218
245
  }));
219
246
  }
220
247
 
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;
248
+ /**
249
+ * Resolve the carousel aspect ratio.
250
+ * Priority: explicit `ratio` attribute → first image's natural
251
+ * dimensions → fallback constant. The first image is probed via a
252
+ * detached Image() if the light-DOM <img> hasn't loaded yet.
253
+ */
254
+ _resolveRatio() {
255
+ const explicit = this.getAttribute("ratio");
256
+ if (explicit) {
257
+ this.style.setProperty("--carousel-ratio", explicit);
258
+ return;
236
259
  }
237
- }
238
260
 
239
- _goTo(index) {
240
- const prev = this._currentIndex;
241
- this._currentIndex = index;
242
- this._toggleSlide(prev, false);
243
- this._toggleSlide(index, true);
244
- }
261
+ // Set fallback first so the stage has dimensions while we wait.
262
+ this.style.setProperty("--carousel-ratio", FALLBACK_RATIO);
245
263
 
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
- }
264
+ const first = this._images[0];
265
+ if (!first?.src) return;
254
266
 
255
- render() {
256
- const images = this._images;
257
- const ratio = this.getAttribute("ratio") || "3/2";
267
+ const apply = (w, h) => {
268
+ if (w && h) this.style.setProperty("--carousel-ratio", `${w} / ${h}`);
269
+ };
258
270
 
259
- if (images.length === 0) {
260
- this.shadowRoot.innerHTML = "";
271
+ // Reuse the light-DOM <img> if it's already decoded — avoids a
272
+ // second network request when the browser cached the image.
273
+ const lightImg = this.querySelector("img");
274
+ if (lightImg?.complete && lightImg.naturalWidth) {
275
+ apply(lightImg.naturalWidth, lightImg.naturalHeight);
261
276
  return;
262
277
  }
263
278
 
279
+ const probe = new Image();
280
+ probe.onload = () => apply(probe.naturalWidth, probe.naturalHeight);
281
+ if (first.srcset) probe.srcset = first.srcset;
282
+ probe.src = first.src;
283
+ }
284
+
285
+ _render() {
264
286
  if (!_sheet) {
265
287
  _sheet = new CSSStyleSheet();
266
288
  _sheet.replaceSync(STYLES);
267
289
  }
268
290
  this.shadowRoot.adoptedStyleSheets = [_sheet];
269
- this.style.setProperty("--carousel-ratio", ratio);
270
291
 
271
- const slidesHTML = images
292
+ const multiSlide = this._images.length > 1;
293
+
294
+ const slidesHTML = this._images
272
295
  .map(
273
296
  (img, i) => `
274
297
  <div class="slide${i === 0 ? " active" : ""}" role="tabpanel" aria-label="${img.alt || `Slide ${i + 1}`}">
@@ -281,18 +304,21 @@ class ImageCarousel extends HTMLElement {
281
304
  )
282
305
  .join("");
283
306
 
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
- : "";
307
+ const navHTML = multiSlide
308
+ ? `<button class="nav prev" aria-label="Previous slide">${CHEVRON_LEFT}</button>
309
+ <button class="nav next" aria-label="Next slide">${CHEVRON_RIGHT}</button>`
310
+ : "";
311
+
312
+ const dotsHTML = multiSlide
313
+ ? `<div class="dots" role="tablist" aria-label="Slide navigation">
314
+ ${this._images
315
+ .map(
316
+ (_img, i) =>
317
+ `<button class="dot${i === 0 ? " active" : ""}" data-index="${i}" role="tab" aria-label="Slide ${i + 1}" aria-selected="${i === 0}"></button>`,
318
+ )
319
+ .join("")}
320
+ </div>`
321
+ : "";
296
322
 
297
323
  this.shadowRoot.innerHTML = `
298
324
  <div class="carousel" role="region" aria-label="Image carousel" tabindex="0">
@@ -309,46 +335,89 @@ class ImageCarousel extends HTMLElement {
309
335
  this._dots = Array.from(this.shadowRoot.querySelectorAll(".dot"));
310
336
  }
311
337
 
338
+ // ─── navigation ────────────────────────────────────────────────────
339
+
340
+ _next() {
341
+ this._goTo((this._currentIndex + 1) % this._images.length);
342
+ }
343
+
344
+ _prev() {
345
+ const n = this._images.length;
346
+ this._goTo((this._currentIndex - 1 + n) % n);
347
+ }
348
+
349
+ _goTo(index) {
350
+ const prev = this._currentIndex;
351
+ if (prev === index) return;
352
+ this._currentIndex = index;
353
+ this._toggleSlide(prev, false);
354
+ this._toggleSlide(index, true);
355
+ }
356
+
357
+ _toggleSlide(i, active) {
358
+ this._slides[i]?.classList.toggle("active", active);
359
+ const dot = this._dots[i];
360
+ if (dot) {
361
+ dot.classList.toggle("active", active);
362
+ dot.setAttribute("aria-selected", String(active));
363
+ }
364
+ }
365
+
366
+ /** User triggered a navigation — restart the autoplay clock so the
367
+ * next auto-advance doesn't fire immediately after their action. */
368
+ _userNav(direction) {
369
+ direction === "next" ? this._next() : this._prev();
370
+ this._maybeStartAutoplay();
371
+ }
372
+
373
+ // ─── autoplay ──────────────────────────────────────────────────────
374
+
375
+ _maybeStartAutoplay() {
376
+ if (this.hasAttribute("autoplay") && this._images.length > 1) {
377
+ this._startAutoplay();
378
+ }
379
+ }
380
+
381
+ _startAutoplay() {
382
+ this._stopAutoplay();
383
+ const interval = parseInt(this.getAttribute("interval") || "", 10) || DEFAULT_INTERVAL;
384
+ this._timer = setInterval(() => this._next(), interval);
385
+ }
386
+
387
+ _stopAutoplay() {
388
+ if (this._timer) {
389
+ clearInterval(this._timer);
390
+ this._timer = null;
391
+ }
392
+ }
393
+
394
+ // ─── listeners ─────────────────────────────────────────────────────
395
+
312
396
  _setupListeners() {
313
397
  const root = this.shadowRoot;
314
398
  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
399
 
322
- root.querySelector(".next")?.addEventListener("click", () => {
323
- this._resetAutoplay();
324
- this._goTo((this._currentIndex + 1) % n);
325
- });
400
+ root.querySelector(".prev")?.addEventListener("click", () => this._userNav("prev"));
401
+ root.querySelector(".next")?.addEventListener("click", () => this._userNav("next"));
326
402
 
327
403
  root.querySelector(".dots")?.addEventListener("click", (e) => {
328
404
  const dot = e.target.closest(".dot");
329
405
  if (!dot) return;
330
- this._resetAutoplay();
331
406
  this._goTo(parseInt(dot.dataset.index, 10));
407
+ this._maybeStartAutoplay();
332
408
  });
333
409
 
334
- // Pause autoplay on hover
410
+ // Pause autoplay while the cursor sits on the carousel
335
411
  carousel.addEventListener("mouseenter", () => this._stopAutoplay());
336
- carousel.addEventListener("mouseleave", () => {
337
- if (this.hasAttribute("autoplay")) this._startAutoplay();
338
- });
412
+ carousel.addEventListener("mouseleave", () => this._maybeStartAutoplay());
339
413
 
340
414
  // Keyboard navigation
341
415
  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
- }
416
+ if (e.key === "ArrowLeft") this._userNav("prev");
417
+ else if (e.key === "ArrowRight") this._userNav("next");
349
418
  });
350
419
 
351
- // Touch/swipe support
420
+ // Touch / swipe
352
421
  carousel.addEventListener(
353
422
  "touchstart",
354
423
  (e) => {
@@ -361,9 +430,8 @@ class ImageCarousel extends HTMLElement {
361
430
  "touchend",
362
431
  (e) => {
363
432
  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);
433
+ if (Math.abs(dx) > SWIPE_THRESHOLD_PX) {
434
+ this._userNav(dx < 0 ? "next" : "prev");
367
435
  }
368
436
  },
369
437
  { passive: true },
@@ -373,36 +441,12 @@ class ImageCarousel extends HTMLElement {
373
441
  _observeVisibility() {
374
442
  this._observer = new IntersectionObserver(([entry]) => {
375
443
  this._isVisible = entry.isIntersecting;
376
- if (!entry.isIntersecting) {
377
- this._stopAutoplay();
378
- } else if (this.hasAttribute("autoplay")) {
379
- this._startAutoplay();
380
- }
444
+ if (entry.isIntersecting) this._maybeStartAutoplay();
445
+ else this._stopAutoplay();
381
446
  });
382
447
  this._observer.observe(this);
383
448
  document.addEventListener("visibilitychange", this._handleVisibility);
384
449
  }
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
450
  }
407
451
 
408
452
  customElements.define("image-carousel", ImageCarousel);
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Side Note Custom Element
3
+ *
4
+ * Usage:
5
+ * <side-note>
6
+ * Extra context that should sit in the article margin on wide screens.
7
+ * </side-note>
8
+ */
9
+
10
+ let styleSheetInjected = false;
11
+ let layoutFrame = null;
12
+ let layoutEventsBound = false;
13
+
14
+ const boundImages = new WeakSet();
15
+ const SIDE_NOTE_MEDIA = "(min-width: 1280px)";
16
+
17
+ const STYLES = `
18
+ side-note {
19
+ display: block;
20
+ margin: 1.5rem 0;
21
+ padding: 0.4rem 0 0.4rem 1rem;
22
+ color: var(--subtext0);
23
+ font-size: 0.875em;
24
+ line-height: 1.55;
25
+ border-left: 2px solid var(--surface1);
26
+ }
27
+
28
+ side-note > :first-child {
29
+ margin-top: 0;
30
+ }
31
+
32
+ side-note > :last-child {
33
+ margin-bottom: 0;
34
+ }
35
+
36
+ @media screen and (min-width: 1280px) {
37
+ .card-content.article > .content {
38
+ overflow: visible;
39
+ display: flow-root;
40
+ }
41
+
42
+ .card-content.article > .content > side-note {
43
+ float: right;
44
+ clear: right;
45
+ box-sizing: border-box;
46
+ width: 11rem;
47
+ margin: 0 -12.5rem 1rem 1.5rem;
48
+ padding: 0 0 0 0.875rem;
49
+ font-size: 0.8em;
50
+ }
51
+
52
+ .card-content.article > .content.is-side-note-layout {
53
+ position: relative;
54
+ }
55
+
56
+ .card-content.article > .content.is-side-note-layout > side-note {
57
+ float: none;
58
+ clear: none;
59
+ position: absolute;
60
+ top: var(--side-note-top, 0);
61
+ right: -12.5rem;
62
+ margin: 0;
63
+ }
64
+ }
65
+ `;
66
+
67
+ function injectStyles() {
68
+ if (styleSheetInjected) return;
69
+
70
+ const styleEl = document.createElement("style");
71
+ styleEl.textContent = STYLES;
72
+ document.head.appendChild(styleEl);
73
+ styleSheetInjected = true;
74
+ }
75
+
76
+ function getArticleContentBlocks() {
77
+ return document.querySelectorAll(".card-content.article > .content");
78
+ }
79
+
80
+ function getSideNotes(content) {
81
+ return Array.from(content.children).filter((child) => child.tagName === "SIDE-NOTE");
82
+ }
83
+
84
+ function resetSideNoteLayout(content, sideNotes = getSideNotes(content)) {
85
+ content.classList.remove("is-side-note-layout");
86
+ content.style.removeProperty("min-height");
87
+ sideNotes.forEach((sideNote) => {
88
+ sideNote.style.removeProperty("--side-note-top");
89
+ });
90
+ }
91
+
92
+ function getSideNoteAnchor(sideNote) {
93
+ let anchor = sideNote.previousElementSibling;
94
+
95
+ while (anchor && anchor.tagName === "SIDE-NOTE") {
96
+ anchor = anchor.previousElementSibling;
97
+ }
98
+
99
+ return anchor || sideNote;
100
+ }
101
+
102
+ function getContentFlowBottom(content, sideNotes) {
103
+ const sideNoteSet = new Set(sideNotes);
104
+ let bottom = 0;
105
+
106
+ Array.from(content.children).forEach((child) => {
107
+ if (sideNoteSet.has(child)) return;
108
+ bottom = Math.max(bottom, child.offsetTop + child.offsetHeight);
109
+ });
110
+
111
+ return bottom;
112
+ }
113
+
114
+ function layoutSideNotes() {
115
+ const isWide = window.matchMedia(SIDE_NOTE_MEDIA).matches;
116
+
117
+ getArticleContentBlocks().forEach((content) => {
118
+ const sideNotes = getSideNotes(content);
119
+ if (sideNotes.length === 0) return;
120
+
121
+ if (!isWide) {
122
+ resetSideNoteLayout(content, sideNotes);
123
+ return;
124
+ }
125
+
126
+ content.style.removeProperty("min-height");
127
+ sideNotes.forEach((sideNote) => {
128
+ sideNote.style.removeProperty("--side-note-top");
129
+ });
130
+ content.classList.add("is-side-note-layout");
131
+
132
+ const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
133
+ const gap = rootFontSize;
134
+ let nextTop = 0;
135
+
136
+ sideNotes.forEach((sideNote) => {
137
+ const anchor = getSideNoteAnchor(sideNote);
138
+ const targetTop = anchor.offsetTop;
139
+ const top = Math.max(targetTop, nextTop);
140
+
141
+ sideNote.style.setProperty("--side-note-top", `${Math.round(top)}px`);
142
+ nextTop = top + sideNote.offsetHeight + gap;
143
+ });
144
+
145
+ const flowBottom = getContentFlowBottom(content, sideNotes);
146
+ const sideNoteBottom = Math.max(0, nextTop - gap);
147
+ content.style.minHeight = `${Math.ceil(Math.max(flowBottom, sideNoteBottom))}px`;
148
+ });
149
+ }
150
+
151
+ function scheduleSideNoteLayout() {
152
+ if (layoutFrame !== null) return;
153
+
154
+ layoutFrame = window.requestAnimationFrame(() => {
155
+ layoutFrame = null;
156
+ layoutSideNotes();
157
+ });
158
+ }
159
+
160
+ function bindImages(content) {
161
+ if (!content) return;
162
+
163
+ content.querySelectorAll("img").forEach((img) => {
164
+ if (boundImages.has(img)) return;
165
+ boundImages.add(img);
166
+ img.addEventListener("load", scheduleSideNoteLayout);
167
+ img.addEventListener("error", scheduleSideNoteLayout);
168
+ });
169
+ }
170
+
171
+ function bindLayoutEvents() {
172
+ if (layoutEventsBound) return;
173
+ layoutEventsBound = true;
174
+
175
+ window.addEventListener("resize", scheduleSideNoteLayout, { passive: true });
176
+ window.addEventListener("gnix:article-font-settings-change", scheduleSideNoteLayout);
177
+
178
+ if (document.fonts?.ready) {
179
+ document.fonts.ready.then(scheduleSideNoteLayout).catch(() => {});
180
+ }
181
+ }
182
+
183
+ class SideNote extends HTMLElement {
184
+ connectedCallback() {
185
+ injectStyles();
186
+ this.unwrapMarkdownParagraph();
187
+ bindLayoutEvents();
188
+ bindImages(this.closest(".content"));
189
+ scheduleSideNoteLayout();
190
+ }
191
+
192
+ unwrapMarkdownParagraph() {
193
+ const parent = this.parentElement;
194
+ if (!parent || parent.tagName !== "P") return;
195
+
196
+ const onlyContainsThisSideNote = Array.from(parent.childNodes).every((node) => {
197
+ return node === this || (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === "");
198
+ });
199
+ if (!onlyContainsThisSideNote) return;
200
+
201
+ parent.parentNode.insertBefore(this, parent);
202
+ parent.remove();
203
+ }
204
+ }
205
+
206
+ if (!customElements.get("side-note")) {
207
+ customElements.define("side-note", SideNote);
208
+ }
209
+
210
+ export { SideNote };