From 8a064a5e5a95cd22aa654f7c80da09d107262508 Mon Sep 17 00:00:00 2001 From: Alexander Fuks Date: Fri, 11 Oct 2024 18:32:10 +0400 Subject: [PATCH 01/13] feat: show toc on mobile screens (#1964) --- _includes/toc-status.html | 10 + _includes/toc.html | 9 +- _javascript/modules/components/toc.js | 41 ++-- .../modules/components/toc/toc-desktop.js | 22 ++ .../modules/components/toc/toc-mobile.js | 117 ++++++++++ _javascript/modules/plugins.js | 2 +- _javascript/post.js | 7 +- _layouts/post.html | 28 ++- _sass/addon/commons.scss | 4 +- _sass/addon/module.scss | 7 + _sass/colors/typography-dark.scss | 2 +- _sass/colors/typography-light.scss | 2 +- _sass/layout/post.scss | 209 ++++++++++++++++++ 13 files changed, 430 insertions(+), 30 deletions(-) create mode 100644 _includes/toc-status.html create mode 100644 _javascript/modules/components/toc/toc-desktop.js create mode 100644 _javascript/modules/components/toc/toc-mobile.js diff --git a/_includes/toc-status.html b/_includes/toc-status.html new file mode 100644 index 00000000000..4b71caeefcc --- /dev/null +++ b/_includes/toc-status.html @@ -0,0 +1,10 @@ +{% comment %} + Determine TOC state and return it through variable "enable_toc" +{% endcomment %} + +{% assign enable_toc = false %} +{% if site.toc and page.toc %} + {% if page.content contains ' +

{{- site.data.locales[include.lang].panel.toc -}}

diff --git a/_javascript/modules/components/toc.js b/_javascript/modules/components/toc.js index 56ce26fac50..765336ac2db 100644 --- a/_javascript/modules/components/toc.js +++ b/_javascript/modules/components/toc.js @@ -1,15 +1,30 @@ -export function toc() { - if (document.querySelector('main h2, main h3')) { - // see: https://github.com/tscanlin/tocbot#usage - tocbot.init({ - tocSelector: '#toc', - contentSelector: '.content', - ignoreSelector: '[data-toc-skip]', - headingSelector: 'h2, h3, h4', - orderedList: false, - scrollSmooth: false - }); - - document.getElementById('toc-wrapper').classList.remove('d-none'); +import { TocMobile as mobile } from './toc/toc-mobile'; +import { TocDesktop as desktop } from './toc/toc-desktop'; + +const desktopMode = matchMedia('(min-width: 1200px)'); + +function refresh(e) { + if (e.matches) { + mobile.hidePopup(); + desktop.refresh(); + } else { + mobile.refresh(); } } + +function init() { + if (document.querySelector('main>article[data-toc="true"]') === null) { + return; + } + + // Avoid create multiple instances of Tocbot. Ref: + if (desktopMode.matches) { + desktop.init(); + } else { + mobile.init(); + } + + desktopMode.onchange = refresh; +} + +export { init as initToc }; diff --git a/_javascript/modules/components/toc/toc-desktop.js b/_javascript/modules/components/toc/toc-desktop.js new file mode 100644 index 00000000000..5021a72a082 --- /dev/null +++ b/_javascript/modules/components/toc/toc-desktop.js @@ -0,0 +1,22 @@ +export class TocDesktop { + /* Tocbot options Ref: https://github.com/tscanlin/tocbot#usage */ + static options = { + tocSelector: '#toc', + contentSelector: '.content', + ignoreSelector: '[data-toc-skip]', + headingSelector: 'h2, h3, h4', + orderedList: false, + scrollSmooth: false, + headingsOffset: 16 * 2 // 2rem + }; + + static refresh() { + tocbot.refresh(this.options); + } + + static init() { + if (document.getElementById('toc-wrapper')) { + tocbot.init(this.options); + } + } +} diff --git a/_javascript/modules/components/toc/toc-mobile.js b/_javascript/modules/components/toc/toc-mobile.js new file mode 100644 index 00000000000..48b372df443 --- /dev/null +++ b/_javascript/modules/components/toc/toc-mobile.js @@ -0,0 +1,117 @@ +/** + * TOC button, topbar and popup for mobile devices + */ + +const $tocBar = document.getElementById('toc-bar'); +const $soloTrigger = document.getElementById('toc-solo-trigger'); +const $triggers = document.getElementsByClassName('toc-trigger'); +const $popup = document.getElementById('toc-popup'); +const $btnClose = document.getElementById('toc-popup-close'); + +const SCROLL_LOCK = 'overflow-hidden'; +const CLOSING = 'closing'; + +export class TocMobile { + static invisible = true; + static barHeight = 16 * 3; // 3rem + + static options = { + tocSelector: '#toc-popup-content', + contentSelector: '.content', + ignoreSelector: '[data-toc-skip]', + headingSelector: 'h2, h3, h4', + orderedList: false, + scrollSmooth: false, + collapseDepth: 4, + headingsOffset: this.barHeight + }; + + static initBar() { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + $tocBar.classList.toggle('invisible', entry.isIntersecting); + }); + }, + { rootMargin: `-${this.barHeight}px 0px 0px 0px` } + ); + + observer.observe($soloTrigger); + this.invisible = false; + } + + static listenAnchors() { + const $anchors = document.getElementsByClassName('toc-link'); + [...$anchors].forEach((anchor) => { + anchor.onclick = this.hidePopup; + }); + } + + static refresh() { + if (this.invisible) { + this.initComponents(); + } + tocbot.refresh(this.options); + this.listenAnchors(); + } + + static showPopup() { + TocMobile.lockScroll(true); + $popup.showModal(); + const activeItem = $popup.querySelector('li.is-active-li'); + activeItem.scrollIntoView({ block: 'center' }); + } + + static hidePopup() { + if (!$popup.open) { + return; + } + + $popup.toggleAttribute(CLOSING); + + $popup.addEventListener( + 'animationend', + () => { + $popup.toggleAttribute(CLOSING); + $popup.close(); + }, + { once: true } + ); + + TocMobile.lockScroll(false); + } + + static lockScroll(enable) { + document.documentElement.classList.toggle(SCROLL_LOCK, enable); + document.body.classList.toggle(SCROLL_LOCK, enable); + } + + static clickBackdrop(event) { + const rect = event.target.getBoundingClientRect(); + if ( + event.clientX < rect.left || + event.clientX > rect.right || + event.clientY < rect.top || + event.clientY > rect.bottom + ) { + TocMobile.hidePopup(); + } + } + + static initComponents() { + this.initBar(); + + [...$triggers].forEach((trigger) => { + trigger.onclick = this.showPopup; + }); + + $popup.onclick = this.clickBackdrop; + $btnClose.onclick = $popup.oncancel = this.hidePopup; + } + + static init() { + tocbot.init(this.options); + this.listenAnchors(); + this.initComponents(); + } +} diff --git a/_javascript/modules/plugins.js b/_javascript/modules/plugins.js index fb892e25bf5..cc95c1bc393 100644 --- a/_javascript/modules/plugins.js +++ b/_javascript/modules/plugins.js @@ -3,4 +3,4 @@ export { initClipboard } from './components/clipboard'; export { loadImg } from './components/img-loading'; export { imgPopup } from './components/img-popup'; export { initLocaleDatetime } from './components/locale-datetime'; -export { toc } from './components/toc'; +export { initToc } from './components/toc'; diff --git a/_javascript/post.js b/_javascript/post.js index 9340f05e70d..1c616ecd0a6 100644 --- a/_javascript/post.js +++ b/_javascript/post.js @@ -1,14 +1,15 @@ -import { basic, initSidebar, initTopbar } from './modules/layouts'; +import { basic, initTopbar, initSidebar } from './modules/layouts'; + import { loadImg, imgPopup, initLocaleDatetime, initClipboard, - toc + initToc } from './modules/plugins'; loadImg(); -toc(); +initToc(); imgPopup(); initSidebar(); initLocaleDatetime(); diff --git a/_layouts/post.html b/_layouts/post.html index f17ceea8641..bcc133f8559 100644 --- a/_layouts/post.html +++ b/_layouts/post.html @@ -11,7 +11,9 @@ {% include lang.html %} -
+{% include toc-status.html %} + +

{{ page.title }}

{% if page.description %} @@ -95,6 +97,30 @@

{{ page.title }}

+ {% if enable_toc %} + + + + + +
+
{{- page.title -}}
+ +
+
+
+ {% endif %} +
{{ content }}
diff --git a/_sass/addon/commons.scss b/_sass/addon/commons.scss index e2a0e6154b5..e332f21336a 100644 --- a/_sass/addon/commons.scss +++ b/_sass/addon/commons.scss @@ -908,9 +908,7 @@ $btn-mb: 0.5rem; } #topbar { - button i { - color: #999999; - } + @extend %btn-color; #breadcrumb { font-size: 1rem; diff --git a/_sass/addon/module.scss b/_sass/addon/module.scss index 42db4e2d991..34ac67b95ea 100644 --- a/_sass/addon/module.scss +++ b/_sass/addon/module.scss @@ -8,6 +8,7 @@ color: var(--heading-color); font-weight: 400; font-family: $font-family-heading; + scroll-margin-top: 3.5rem; } %anchor { @@ -134,6 +135,12 @@ } } +%btn-color { + button i { + color: #999999; + } +} + /* ---------- scss mixin --------- */ @mixin mt-mb($value) { diff --git a/_sass/colors/typography-dark.scss b/_sass/colors/typography-dark.scss index 12427ec494a..664c93653e6 100644 --- a/_sass/colors/typography-dark.scss +++ b/_sass/colors/typography-dark.scss @@ -22,7 +22,6 @@ --btn-border-color: #2e2f31; --btn-backtotop-color: var(--text-color); --btn-backtotop-border-color: #212122; - --btn-box-shadow: var(--main-bg); --card-header-bg: #292929; --checkbox-color: rgb(118, 120, 121); --checkbox-checked-color: var(--link-color); @@ -60,6 +59,7 @@ /* Posts */ --toc-highlight: rgb(116, 178, 243); + --toc-popup-border-color: #373737; --tag-hover: rgb(43, 56, 62); --tb-odd-bg: #252526; /* odd rows of the posts' table */ --tb-even-bg: rgb(31, 31, 34); /* even rows of the posts' table */ diff --git a/_sass/colors/typography-light.scss b/_sass/colors/typography-light.scss index 78000746925..b6fc5618ad5 100644 --- a/_sass/colors/typography-light.scss +++ b/_sass/colors/typography-light.scss @@ -22,7 +22,6 @@ --btn-border-color: #e9ecef; --btn-backtotop-color: #686868; --btn-backtotop-border-color: #f1f1f1; - --btn-box-shadow: #eaeaea; --checkbox-color: #c5c5c5; --checkbox-checked-color: #07a8f7; --img-bg: radial-gradient( @@ -63,6 +62,7 @@ /* Posts */ --toc-highlight: #0550ae; + --toc-popup-border-color: lightgray; --btn-share-color: gray; --btn-share-hover-color: #0d6efd; --card-bg: white; diff --git a/_sass/layout/post.scss b/_sass/layout/post.scss index 815db933198..be727006b62 100644 --- a/_sass/layout/post.scss +++ b/_sass/layout/post.scss @@ -228,6 +228,7 @@ header { } } +/* TOC panel */ #toc-wrapper { border-left: 1px solid rgba(158, 158, 158, 0.17); position: -webkit-sticky; @@ -290,6 +291,201 @@ header { } } +/* --- TOC button, bar and popup in mobile/tablet --- */ + +#toc-bar { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1; + margin: 0 -1rem; + height: $topbar-height; + background: var(--main-bg); + border-bottom: 1px solid var(--main-border-color); + transition: all 0.2s ease-in-out; + + @extend %btn-color; + + .label { + @extend %heading; + + margin-left: 0.25rem; + padding: 0 0.75rem; + color: inherit; + } + + &.invisible { + top: -$topbar-height; + transition: none; + } +} + +#toc-solo-trigger { + color: var(--text-muted-color); + border-color: var(--btn-border-color); + border-radius: $radius-lg; + + .label { + font-size: 1rem; + font-family: $font-family-heading; + } + + &:hover { + box-shadow: none; + background: none; + } +} + +@mixin slide-in { + from { + opacity: 0.7; + transform: translateY(-$topbar-height); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@mixin slide-out { + 0% { + transform: translateY(0); + opacity: 1; + } + + 100% { + transform: translateY(-$topbar-height); + opacity: 0; + } +} + +@-webkit-keyframes slide-in { + @include slide-in; +} + +@keyframes slide-in { + @include slide-in; +} + +@-webkit-keyframes slide-out { + @include slide-out; +} + +@keyframes slide-out { + @include slide-out; +} + +#toc-popup { + $slide-in: slide-in 0.3s ease-out; + $slide-out: slide-out 0.3s ease-out; + $curtain-height: 2rem; + + border-color: var(--toc-popup-border-color); + border-width: 1px; + border-radius: $radius-lg; + color: var(--text-color); + background: var(--main-bg); + margin-top: $topbar-height; + min-width: 20rem; + font-size: 1.05rem; + + @media all and (min-width: 576px) { + max-width: 32rem; + } + + &[open] { + -webkit-animation: $slide-in; + animation: $slide-in; + } + + &[closing] { + -webkit-animation: $slide-out; + animation: $slide-out; + } + + @media all and (min-width: 850px) { + left: $sidebar-width; + } + + .header { + @extend %btn-color; + + position: -webkit-sticky; + position: sticky; + top: 0; + background-color: inherit; + border-bottom: 1px solid var(--main-border-color); + + .label { + font-family: $font-family-heading; + } + } + + button:focus-visible { + box-shadow: none; + } + + ul { + list-style-type: none; + padding-left: 0; + + li { + ul, + & + li { + margin-top: 0.25rem; + } + + a { + display: flex; + line-height: 1.5; + padding: 0.375rem 0; + padding-right: 1.125rem; + + &.toc-link::before { + display: none; + } + } + } + } + + @for $i from 2 through 4 { + .node-name--H#{$i} { + padding-left: 1.125rem * ($i - 1); + } + } + + .is-active-link { + color: var(--toc-highlight) !important; + font-weight: 600; + } + + &::-webkit-backdrop { + -webkit-backdrop-filter: blur(5px); + backdrop-filter: blur(5px); + } + + &::backdrop { + -webkit-backdrop-filter: blur(5px); + backdrop-filter: blur(5px); + } + + &::after { + display: flex; + content: ''; + position: relative; + background: linear-gradient(transparent, var(--main-bg) 70%); + height: $curtain-height; + } + + #toc-popup-content { + overflow: auto; + max-height: calc(100vh - 4 * $topbar-height); + font-family: $font-family-heading; + margin-bottom: -$curtain-height; + } +} + /* --- Related Posts --- */ #related-posts { @@ -368,3 +564,16 @@ header { margin-right: -0.5rem; } } + +@media all and (min-width: 1200px) { + h2, + h3, + h4 { + scroll-margin-top: 2rem; + } + + #toc-bar, + #toc-solo-trigger { + display: none !important; + } +} From 03e302cbf68cc502a2b6bef0726186a0b3ef321c Mon Sep 17 00:00:00 2001 From: Alexander Fuks Date: Sun, 13 Oct 2024 21:16:25 +0400 Subject: [PATCH 02/13] chore: close toc-popup gracefully with Esc key (#1990) --- _javascript/modules/components/toc/toc-mobile.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/_javascript/modules/components/toc/toc-mobile.js b/_javascript/modules/components/toc/toc-mobile.js index 48b372df443..8d717dd006e 100644 --- a/_javascript/modules/components/toc/toc-mobile.js +++ b/_javascript/modules/components/toc/toc-mobile.js @@ -62,7 +62,11 @@ export class TocMobile { activeItem.scrollIntoView({ block: 'center' }); } - static hidePopup() { + static hidePopup(event) { + if (event?.type === 'cancel') { + event.preventDefault(); + } + if (!$popup.open) { return; } From 6f461132c00c7f7d557e0b865530bb256c1fe5be Mon Sep 17 00:00:00 2001 From: Cotes Chung <11371340+cotes2020@users.noreply.github.com> Date: Sat, 19 Oct 2024 21:13:21 +0800 Subject: [PATCH 03/13] refactor: improve toc popup module --- _javascript/modules/components/toc.js | 5 +- .../modules/components/toc/toc-mobile.js | 48 ++++++++++--------- _layouts/post.html | 6 +-- _sass/layout/post.scss | 24 ++++++---- 4 files changed, 49 insertions(+), 34 deletions(-) diff --git a/_javascript/modules/components/toc.js b/_javascript/modules/components/toc.js index 765336ac2db..e9086eedf9a 100644 --- a/_javascript/modules/components/toc.js +++ b/_javascript/modules/components/toc.js @@ -5,7 +5,10 @@ const desktopMode = matchMedia('(min-width: 1200px)'); function refresh(e) { if (e.matches) { - mobile.hidePopup(); + if (mobile.popupOpened) { + mobile.hidePopup(); + } + desktop.refresh(); } else { mobile.refresh(); diff --git a/_javascript/modules/components/toc/toc-mobile.js b/_javascript/modules/components/toc/toc-mobile.js index 8d717dd006e..20e24a73c9f 100644 --- a/_javascript/modules/components/toc/toc-mobile.js +++ b/_javascript/modules/components/toc/toc-mobile.js @@ -12,8 +12,8 @@ const SCROLL_LOCK = 'overflow-hidden'; const CLOSING = 'closing'; export class TocMobile { - static invisible = true; - static barHeight = 16 * 3; // 3rem + static #invisible = true; + static #barHeight = 16 * 3; // 3rem static options = { tocSelector: '#toc-popup-content', @@ -23,7 +23,7 @@ export class TocMobile { orderedList: false, scrollSmooth: false, collapseDepth: 4, - headingsOffset: this.barHeight + headingsOffset: this.#barHeight }; static initBar() { @@ -33,44 +33,40 @@ export class TocMobile { $tocBar.classList.toggle('invisible', entry.isIntersecting); }); }, - { rootMargin: `-${this.barHeight}px 0px 0px 0px` } + { rootMargin: `-${this.#barHeight}px 0px 0px 0px` } ); observer.observe($soloTrigger); - this.invisible = false; + this.#invisible = false; } static listenAnchors() { const $anchors = document.getElementsByClassName('toc-link'); [...$anchors].forEach((anchor) => { - anchor.onclick = this.hidePopup; + anchor.onclick = () => this.hidePopup(); }); } static refresh() { - if (this.invisible) { + if (this.#invisible) { this.initComponents(); } tocbot.refresh(this.options); this.listenAnchors(); } + static get popupOpened() { + return $popup.open; + } + static showPopup() { - TocMobile.lockScroll(true); + this.lockScroll(true); $popup.showModal(); const activeItem = $popup.querySelector('li.is-active-li'); activeItem.scrollIntoView({ block: 'center' }); } - static hidePopup(event) { - if (event?.type === 'cancel') { - event.preventDefault(); - } - - if (!$popup.open) { - return; - } - + static hidePopup() { $popup.toggleAttribute(CLOSING); $popup.addEventListener( @@ -82,7 +78,7 @@ export class TocMobile { { once: true } ); - TocMobile.lockScroll(false); + this.lockScroll(false); } static lockScroll(enable) { @@ -91,6 +87,10 @@ export class TocMobile { } static clickBackdrop(event) { + if ($popup.hasAttribute(CLOSING)) { + return; + } + const rect = event.target.getBoundingClientRect(); if ( event.clientX < rect.left || @@ -98,7 +98,7 @@ export class TocMobile { event.clientY < rect.top || event.clientY > rect.bottom ) { - TocMobile.hidePopup(); + this.hidePopup(); } } @@ -106,11 +106,15 @@ export class TocMobile { this.initBar(); [...$triggers].forEach((trigger) => { - trigger.onclick = this.showPopup; + trigger.onclick = () => this.showPopup(); }); - $popup.onclick = this.clickBackdrop; - $btnClose.onclick = $popup.oncancel = this.hidePopup; + $popup.onclick = (e) => this.clickBackdrop(e); + $btnClose.onclick = () => this.hidePopup(); + $popup.oncancel = (e) => { + e.preventDefault(); + this.hidePopup(); + }; } static init() { diff --git a/_layouts/post.html b/_layouts/post.html index bcc133f8559..6a2deff7be6 100644 --- a/_layouts/post.html +++ b/_layouts/post.html @@ -100,7 +100,7 @@

{{ page.title }}

{% if enable_toc %} @@ -113,8 +113,8 @@

{{ page.title }}

{{- page.title -}}
-
diff --git a/_sass/layout/post.scss b/_sass/layout/post.scss index be727006b62..01021f3f615 100644 --- a/_sass/layout/post.scss +++ b/_sass/layout/post.scss @@ -380,12 +380,13 @@ header { $slide-in: slide-in 0.3s ease-out; $slide-out: slide-out 0.3s ease-out; $curtain-height: 2rem; + $backdrop: blur(5px); border-color: var(--toc-popup-border-color); border-width: 1px; border-radius: $radius-lg; color: var(--text-color); - background: var(--main-bg); + background: var(--card-bg); margin-top: $topbar-height; min-width: 20rem; font-size: 1.05rem; @@ -422,8 +423,15 @@ header { } } - button:focus-visible { - box-shadow: none; + button { + > i { + font-size: 1.25rem; + vertical-align: middle; + } + + &:focus-visible { + box-shadow: none; + } } ul { @@ -461,20 +469,20 @@ header { } &::-webkit-backdrop { - -webkit-backdrop-filter: blur(5px); - backdrop-filter: blur(5px); + -webkit-backdrop-filter: $backdrop; + backdrop-filter: $backdrop; } &::backdrop { - -webkit-backdrop-filter: blur(5px); - backdrop-filter: blur(5px); + -webkit-backdrop-filter: $backdrop; + backdrop-filter: $backdrop; } &::after { display: flex; content: ''; position: relative; - background: linear-gradient(transparent, var(--main-bg) 70%); + background: linear-gradient(transparent, var(--card-bg) 70%); height: $curtain-height; } From c1bd9eb9eece4cf3b7e76d750bcc4108aec63361 Mon Sep 17 00:00:00 2001 From: Cotes Chung <11371340+cotes2020@users.noreply.github.com> Date: Sun, 20 Oct 2024 13:47:54 +0800 Subject: [PATCH 04/13] refactor: reduce duplicate scss --- _sass/addon/commons.scss | 22 ++++++++++------------ _sass/addon/module.scss | 21 ++++++++++++++++++--- _sass/layout/archives.scss | 5 ++--- _sass/layout/category-tag.scss | 4 +--- _sass/layout/home.scss | 5 ++--- _sass/layout/post.scss | 26 ++++++++++++-------------- 6 files changed, 45 insertions(+), 38 deletions(-) diff --git a/_sass/addon/commons.scss b/_sass/addon/commons.scss index e332f21336a..2bf99b85b71 100644 --- a/_sass/addon/commons.scss +++ b/_sass/addon/commons.scss @@ -251,8 +251,8 @@ i { > p { margin-left: 0.25em; - margin-top: 0; - margin-bottom: 0; + + @include mt-mb(0); } } } @@ -769,8 +769,8 @@ $btn-mb: 0.5rem; li.nav-item { opacity: 0.9; width: 100%; - padding-left: 1.5rem; - padding-right: 1.5rem; + + @include pl-pr(1.5rem); a.nav-link { @include pt-pb(0.6rem); @@ -1043,7 +1043,7 @@ search { a { font-size: 1.4rem; - line-height: 2.5rem; + line-height: 1.5rem; &:hover { @extend %link-hover; @@ -1069,8 +1069,9 @@ search { } > p { - overflow: hidden; - text-overflow: ellipsis; + @extend %text-ellipsis; + + white-space: break-spaces; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; @@ -1086,10 +1087,7 @@ search { color: var(--topbar-text-color); text-align: center; width: 70%; - overflow: hidden; - text-overflow: ellipsis; word-break: keep-all; - white-space: nowrap; } #mask { @@ -1492,8 +1490,8 @@ search { #main-wrapper > .container { max-width: $main-content-max-width; - padding-left: 1.75rem !important; - padding-right: 1.75rem !important; + + @include pl-pr(1.75rem, true); } main.col-12, diff --git a/_sass/addon/module.scss b/_sass/addon/module.scss index 34ac67b95ea..1dfb735fd01 100644 --- a/_sass/addon/module.scss +++ b/_sass/addon/module.scss @@ -112,6 +112,16 @@ -webkit-box-orient: vertical; } +@mixin text-ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +%text-ellipsis { + @include text-ellipsis; +} + %text-highlight { color: var(--text-muted-highlight-color); font-weight: 600; @@ -158,9 +168,14 @@ padding-bottom: $val; } -@mixin pl-pr($val) { - padding-left: $val; - padding-right: $val; +@mixin pl-pr($val, $important: false) { + @if $important { + padding-left: $val !important; + padding-right: $val !important; + } @else { + padding-left: $val; + padding-right: $val; + } } @mixin placeholder { diff --git a/_sass/layout/archives.scss b/_sass/layout/archives.scss index 3a2e86b1191..fd1979b8f91 100644 --- a/_sass/layout/archives.scss +++ b/_sass/layout/archives.scss @@ -58,9 +58,8 @@ li { font-size: 1.1rem; line-height: 3rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + + @extend %text-ellipsis; &:nth-child(odd) { background-color: var(--main-bg, #ffffff); diff --git a/_sass/layout/category-tag.scss b/_sass/layout/category-tag.scss index 9e43a911ace..fe7d99cec25 100644 --- a/_sass/layout/category-tag.scss +++ b/_sass/layout/category-tag.scss @@ -63,9 +63,7 @@ } > a { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + @include text-ellipsis; } } } diff --git a/_sass/layout/home.scss b/_sass/layout/home.scss index 0d95d7ba6a6..7fff3ba1627 100644 --- a/_sass/layout/home.scss +++ b/_sass/layout/home.scss @@ -74,9 +74,8 @@ > div:first-child { display: block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + + @extend %text-ellipsis; } } } diff --git a/_sass/layout/post.scss b/_sass/layout/post.scss index 01021f3f615..b66e906c87f 100644 --- a/_sass/layout/post.scss +++ b/_sass/layout/post.scss @@ -1,6 +1,6 @@ -/* - Post-specific style -*/ +/** + * Post-specific styles + */ %btn-post-nav { width: 50%; @@ -97,7 +97,7 @@ header { &:hover { i { - @extend %btn-share-hovor; + @extend %btn-share-hover; } } } @@ -258,9 +258,8 @@ header { .toc-link { display: block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + + @extend %text-ellipsis; &:hover { color: var(--toc-highlight); @@ -509,10 +508,11 @@ header { } p { + @extend %text-ellipsis; + font-size: 0.9rem; margin-bottom: 0.5rem; - overflow: hidden; - text-overflow: ellipsis; + white-space: break-spaces; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; @@ -534,7 +534,7 @@ header { max-width: 100%; } -%btn-share-hovor { +%btn-share-hover { color: var(--btn-share-hover-color) !important; } @@ -566,10 +566,8 @@ header { /* Hide SideBar and TOC */ @media all and (max-width: 849px) { .post-navigation { - padding-left: 0; - padding-right: 0; - margin-left: -0.5rem; - margin-right: -0.5rem; + @include pl-pr(0); + @include ml-mr(-0.5rem); } } From d4f7f39ece2d8feaca963b8bfba331268b28811e Mon Sep 17 00:00:00 2001 From: Cotes Chung <11371340+cotes2020@users.noreply.github.com> Date: Tue, 22 Oct 2024 10:42:07 +0800 Subject: [PATCH 05/13] refactor: simplify sidebar animation --- _javascript/modules/components/sidebar.js | 23 +++++++++-------------- _layouts/default.html | 2 +- _sass/addon/commons.scss | 10 ---------- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/_javascript/modules/components/sidebar.js b/_javascript/modules/components/sidebar.js index 6b562d84147..aed759ed57d 100644 --- a/_javascript/modules/components/sidebar.js +++ b/_javascript/modules/components/sidebar.js @@ -2,26 +2,21 @@ * Expand or close the sidebar in mobile screens. */ -const ATTR_DISPLAY = 'sidebar-display'; +const $sidebar = document.getElementById('sidebar'); +const $trigger = document.getElementById('sidebar-trigger'); +const $mask = document.getElementById('mask'); class SidebarUtil { - static isExpanded = false; + static #isExpanded = false; static toggle() { - if (SidebarUtil.isExpanded === false) { - document.body.setAttribute(ATTR_DISPLAY, ''); - } else { - document.body.removeAttribute(ATTR_DISPLAY); - } - - SidebarUtil.isExpanded = !SidebarUtil.isExpanded; + this.#isExpanded = !this.#isExpanded; + document.body.toggleAttribute('sidebar-display', this.#isExpanded); + $sidebar.classList.toggle('z-2', this.#isExpanded); + $mask.classList.toggle('d-none', !this.#isExpanded); } } export function sidebarExpand() { - document - .getElementById('sidebar-trigger') - .addEventListener('click', SidebarUtil.toggle); - - document.getElementById('mask').addEventListener('click', SidebarUtil.toggle); + $trigger.onclick = $mask.onclick = () => SidebarUtil.toggle(); } diff --git a/_layouts/default.html b/_layouts/default.html index ea438fe7abb..1590ef62430 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -68,7 +68,7 @@ -
+
{% if site.pwa.enabled %} {% include_cached notification.html lang=lang %} diff --git a/_sass/addon/commons.scss b/_sass/addon/commons.scss index 2bf99b85b71..5e8aceaa262 100644 --- a/_sass/addon/commons.scss +++ b/_sass/addon/commons.scss @@ -688,7 +688,6 @@ $btn-mb: 0.5rem; height: 100%; overflow-y: auto; width: $sidebar-width; - z-index: 99; background: var(--sidebar-bg); border-right: 1px solid var(--sidebar-border-color); @@ -1091,16 +1090,7 @@ search { } #mask { - display: none; - position: fixed; inset: 0 0 0 0; - height: 100%; - width: 100%; - z-index: 1; - - @at-root [#{$sidebar-display}] & { - display: block !important; - } } /* --- basic wrappers --- */ From 74ed06321c1730dc788af3306b80a0f1739510d4 Mon Sep 17 00:00:00 2001 From: Cotes Chung <11371340+cotes2020@users.noreply.github.com> Date: Fri, 25 Oct 2024 19:48:11 +0800 Subject: [PATCH 06/13] ci: block invalid pull requests (#2010) --- .github/workflows/pr-filter.yml | 22 ++++++++++++++ .github/workflows/scripts/pr-filter.js | 40 ++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 .github/workflows/pr-filter.yml create mode 100644 .github/workflows/scripts/pr-filter.js diff --git a/.github/workflows/pr-filter.yml b/.github/workflows/pr-filter.yml new file mode 100644 index 00000000000..b6bcd00a3ca --- /dev/null +++ b/.github/workflows/pr-filter.yml @@ -0,0 +1,22 @@ +name: Block Invalid PR + +on: + pull_request_target: + types: [opened, reopened, edited] + +jobs: + check-template: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Check PR Content + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const script = require('.github/workflows/scripts/pr-filter.js'); + await script({ github, context }); diff --git a/.github/workflows/scripts/pr-filter.js b/.github/workflows/scripts/pr-filter.js new file mode 100644 index 00000000000..e7206301ead --- /dev/null +++ b/.github/workflows/scripts/pr-filter.js @@ -0,0 +1,40 @@ +function noTypes(markdown) { + if (/## Type of change/.test(markdown) && /- \[x\]/i.test(markdown)) { + return false; + } + return true; +} + +function noDescription(markdown) { + return ( + /## Description/.test(markdown) === false || + /## Description\s*\n\s*## \w+/.test(markdown) || + /## Description\s*\n\s*$/.test(markdown) + ); +} + +module.exports = async ({ github, context }) => { + const pr = context.payload.pull_request; + + if (pr.labels.length > 0) { + // Skip if the PR is already labeled (typically created by a deps-bot.) + return; + } + + const body = pr.body === null ? '' : pr.body.trim(); + const markdown = body.replace(//g, ''); + + if (body === '' || noTypes(markdown) || noDescription(markdown)) { + await github.rest.pulls.update({ + ...context.repo, + pull_number: pr.number, + state: 'closed' + }); + + await github.rest.issues.createComment({ + ...context.repo, + issue_number: pr.number, + body: "Oops, it seems you've submitted an invalid pull request. No worries, we'll close it for you." + }); + } +}; From c7f967529c294f9622f142470406f2be43ea4d35 Mon Sep 17 00:00:00 2001 From: Cotes Chung <11371340+cotes2020@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:58:07 +0800 Subject: [PATCH 07/13] ci: skip test for invalid PRs (#2013) --- .github/workflows/ci.yml | 7 +++++- .github/workflows/commitlint.yml | 3 ++- .github/workflows/pr-filter.yml | 17 +++++++++++--- .github/workflows/scripts/pr-filter.js | 31 ++++++++++++-------------- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50a158b10a9..f909b7e6bb4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,5 @@ name: "CI" + on: push: branches: @@ -11,7 +12,7 @@ on: - "docs/**" - "README.md" - "LICENSE" - pull_request: + workflow_call: jobs: build: @@ -43,3 +44,7 @@ jobs: - name: Test Site run: bash tools/test.sh + + check-commit: + needs: build + uses: ./.github/workflows/commitlint.yml diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index c9c48c33f97..ac8726f3111 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -1,5 +1,6 @@ name: Lint Commit Messages -on: pull_request + +on: workflow_call jobs: commitlint: diff --git a/.github/workflows/pr-filter.yml b/.github/workflows/pr-filter.yml index b6bcd00a3ca..fe393a29ea5 100644 --- a/.github/workflows/pr-filter.yml +++ b/.github/workflows/pr-filter.yml @@ -1,22 +1,33 @@ -name: Block Invalid PR +name: PR Filter on: pull_request_target: - types: [opened, reopened, edited] + types: [opened, reopened] jobs: check-template: runs-on: ubuntu-latest permissions: pull-requests: write + steps: - name: Checkout Code uses: actions/checkout@v4 - name: Check PR Content + id: intercept uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} + result-encoding: string script: | const script = require('.github/workflows/scripts/pr-filter.js'); - await script({ github, context }); + return await script({ github, context }); + + - name: Abort due to invalid PR + if: ${{ steps.intercept.outputs.result != 'true' }} + run: exit 1 + + test: + needs: check-template + uses: ./.github/workflows/ci.yml diff --git a/.github/workflows/scripts/pr-filter.js b/.github/workflows/scripts/pr-filter.js index e7206301ead..f4190ddf1f4 100644 --- a/.github/workflows/scripts/pr-filter.js +++ b/.github/workflows/scripts/pr-filter.js @@ -1,30 +1,25 @@ -function noTypes(markdown) { - if (/## Type of change/.test(markdown) && /- \[x\]/i.test(markdown)) { - return false; - } - return true; +function hasTypes(markdown) { + return /## Type of change/.test(markdown) && /-\s*\[x\]/i.test(markdown); } -function noDescription(markdown) { +function hasDescription(markdown) { return ( - /## Description/.test(markdown) === false || - /## Description\s*\n\s*## \w+/.test(markdown) || - /## Description\s*\n\s*$/.test(markdown) + /## Description/.test(markdown) && + !/## Description\s*\n\s*(##|\s*$)/.test(markdown) ); } module.exports = async ({ github, context }) => { const pr = context.payload.pull_request; - - if (pr.labels.length > 0) { - // Skip if the PR is already labeled (typically created by a deps-bot.) - return; - } - const body = pr.body === null ? '' : pr.body.trim(); const markdown = body.replace(//g, ''); + const action = context.payload.action; + + const isValid = + pr.labels.length > 0 || // PR create by Dependabot would have labels + (markdown !== '' && hasTypes(markdown) && hasDescription(markdown)); - if (body === '' || noTypes(markdown) || noDescription(markdown)) { + if (!isValid) { await github.rest.pulls.update({ ...context.repo, pull_number: pr.number, @@ -34,7 +29,9 @@ module.exports = async ({ github, context }) => { await github.rest.issues.createComment({ ...context.repo, issue_number: pr.number, - body: "Oops, it seems you've submitted an invalid pull request. No worries, we'll close it for you." + body: `Oops, it seems you've ${action} an invalid pull request. No worries, we'll close it for you.` }); } + + return isValid; }; From 4ef3cd8efc90518cc56b4e92306f81aa36feee70 Mon Sep 17 00:00:00 2001 From: Cotes Chung <11371340+cotes2020@users.noreply.github.com> Date: Tue, 29 Oct 2024 22:56:32 +0800 Subject: [PATCH 08/13] ci: improve workflow triggers (#2017) - Unchain commit-lint and CI - Even if a commit does not meet the CI path filter, it still needs to lint the commit message. - Unchain PR filter and CI - The CI workflow needs to be triggered when the commits in a pull request are modified. - Allow manual publishing - Sometimes `semantic-release` will error out due to commit messages referencing discussions, but this does not affect the final RubyGems/GitHub Release. In such cases, manual triggering of the publish process is needed to complete the remaining publishing steps. --- .github/workflows/cd.yml | 7 +++---- .github/workflows/ci.yml | 25 ++++++++++++++----------- .github/workflows/commitlint.yml | 7 ++++++- .github/workflows/pr-filter.yml | 11 +---------- .github/workflows/publish.yml | 1 + .github/workflows/scripts/pr-filter.js | 10 +++++----- .github/workflows/style-lint.yml | 6 ++++-- 7 files changed, 34 insertions(+), 33 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 4f2da0e3683..c665f755e11 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -2,13 +2,12 @@ name: CD on: push: - branches: - - production - tags-ignore: - - "**" + branches: [production] + tags-ignore: ["**"] jobs: release: + if: ${{ ! startsWith(github.event.head_commit.message, 'chore(release)') }} permissions: contents: write issues: write diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f909b7e6bb4..31b878632b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,18 +1,25 @@ -name: "CI" +name: CI on: push: branches: - - "master" - - "hotfix/**" + - master + - "hotfix/*" paths-ignore: - ".github/**" - "!.github/workflows/ci.yml" - - ".gitignore" + - .gitignore - "docs/**" - - "README.md" - - "LICENSE" - workflow_call: + - README.md + - LICENSE + pull_request: + paths-ignore: + - ".github/**" + - "!.github/workflows/ci.yml" + - .gitignore + - "docs/**" + - README.md + - LICENSE jobs: build: @@ -44,7 +51,3 @@ jobs: - name: Test Site run: bash tools/test.sh - - check-commit: - needs: build - uses: ./.github/workflows/commitlint.yml diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index ac8726f3111..58f1a3ff3d1 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -1,6 +1,11 @@ name: Lint Commit Messages -on: workflow_call +on: + push: + branches: + - master + - "hotfix/*" + pull_request: jobs: commitlint: diff --git a/.github/workflows/pr-filter.yml b/.github/workflows/pr-filter.yml index fe393a29ea5..42e1d640089 100644 --- a/.github/workflows/pr-filter.yml +++ b/.github/workflows/pr-filter.yml @@ -19,15 +19,6 @@ jobs: uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} - result-encoding: string script: | const script = require('.github/workflows/scripts/pr-filter.js'); - return await script({ github, context }); - - - name: Abort due to invalid PR - if: ${{ steps.intercept.outputs.result != 'true' }} - run: exit 1 - - test: - needs: check-template - uses: ./.github/workflows/ci.yml + await script({ github, context, core }); diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 99114ea3cb6..b0f9713f2ef 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,6 +10,7 @@ on: required: true BUILDER: required: true + workflow_dispatch: jobs: launch: diff --git a/.github/workflows/scripts/pr-filter.js b/.github/workflows/scripts/pr-filter.js index f4190ddf1f4..97242016b22 100644 --- a/.github/workflows/scripts/pr-filter.js +++ b/.github/workflows/scripts/pr-filter.js @@ -1,5 +1,5 @@ function hasTypes(markdown) { - return /## Type of change/.test(markdown) && /-\s*\[x\]/i.test(markdown); + return /## Type of change/.test(markdown) && /-\s\[x\]/i.test(markdown); } function hasDescription(markdown) { @@ -9,9 +9,9 @@ function hasDescription(markdown) { ); } -module.exports = async ({ github, context }) => { +module.exports = async ({ github, context, core }) => { const pr = context.payload.pull_request; - const body = pr.body === null ? '' : pr.body.trim(); + const body = pr.body === null ? '' : pr.body; const markdown = body.replace(//g, ''); const action = context.payload.action; @@ -31,7 +31,7 @@ module.exports = async ({ github, context }) => { issue_number: pr.number, body: `Oops, it seems you've ${action} an invalid pull request. No worries, we'll close it for you.` }); - } - return isValid; + core.setFailed('PR content does not meet template requirements.'); + } }; diff --git a/.github/workflows/style-lint.yml b/.github/workflows/style-lint.yml index f84f3bcc432..5cb38a7a11a 100644 --- a/.github/workflows/style-lint.yml +++ b/.github/workflows/style-lint.yml @@ -1,8 +1,10 @@ -name: "Style Lint" +name: Style Lint on: push: - branches: ["master", "hotfix/**"] + branches: + - master + - "hotfix/*" paths: ["_sass/**/*.scss"] pull_request: paths: ["_sass/**/*.scss"] From 86b13c917f8f9477b10766fb621cbe987c025203 Mon Sep 17 00:00:00 2001 From: Alexander Fuks Date: Fri, 1 Nov 2024 14:39:03 +0400 Subject: [PATCH 09/13] chore: improve feed interoperability (#2024) --- assets/feed.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/feed.xml b/assets/feed.xml index 0ab20e3be6a..d2aad4db1c1 100644 --- a/assets/feed.xml +++ b/assets/feed.xml @@ -34,7 +34,7 @@ permalink: /feed.xml {{ post.date | date_to_xmlschema }} {% endif %} {{ post_absolute_url }} - + {{ post.author | default: site.social.name }} From 42dea8ee2986ee609ca6f2643a03bd29bd5456a3 Mon Sep 17 00:00:00 2001 From: Supreeth Mysore Venkatesh Date: Sun, 3 Nov 2024 17:45:59 +0100 Subject: [PATCH 10/13] build(deps): update `wdm` gem version for compatibility (#2028) --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 66f9337db44..e5415748ad5 100644 --- a/Gemfile +++ b/Gemfile @@ -11,4 +11,4 @@ platforms :mingw, :x64_mingw, :mswin, :jruby do gem "tzinfo-data" end -gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] +gem "wdm", "~> 0.2.0", :platforms => [:mingw, :x64_mingw, :mswin] From 2f844978aac587a8631c16d6907f24a8af6035a3 Mon Sep 17 00:00:00 2001 From: Cotes Chung <11371340+cotes2020@users.noreply.github.com> Date: Fri, 8 Nov 2024 22:15:31 +0800 Subject: [PATCH 11/13] chore: change stale label to `inactive` --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index bcf425ae604..4f6e91cbf39 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -9,7 +9,7 @@ permissions: pull-requests: write env: - STALE_LABEL: stale + STALE_LABEL: inactive EXEMPT_LABELS: "pending,planning,in progress" MESSAGE: > This conversation has been automatically marked as stale because it has not had recent activity. From d51345e29754ba92a0c2fd6534bbd248ff298331 Mon Sep 17 00:00:00 2001 From: Cotes Chung <11371340+cotes2020@users.noreply.github.com> Date: Fri, 8 Nov 2024 22:35:18 +0800 Subject: [PATCH 12/13] ci: reduce unnecessary pr-filter runs (#2033) - Checking the repository of the PR is more effective than checking the label to identify bot-initiated PRs - This change also allows more flexible PR body definitions for developers with write access to the repository --- .github/workflows/pr-filter.yml | 1 + .github/workflows/scripts/pr-filter.js | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-filter.yml b/.github/workflows/pr-filter.yml index 42e1d640089..8e9a18b736f 100644 --- a/.github/workflows/pr-filter.yml +++ b/.github/workflows/pr-filter.yml @@ -6,6 +6,7 @@ on: jobs: check-template: + if: github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name runs-on: ubuntu-latest permissions: pull-requests: write diff --git a/.github/workflows/scripts/pr-filter.js b/.github/workflows/scripts/pr-filter.js index 97242016b22..03f50dc5ca2 100644 --- a/.github/workflows/scripts/pr-filter.js +++ b/.github/workflows/scripts/pr-filter.js @@ -16,8 +16,7 @@ module.exports = async ({ github, context, core }) => { const action = context.payload.action; const isValid = - pr.labels.length > 0 || // PR create by Dependabot would have labels - (markdown !== '' && hasTypes(markdown) && hasDescription(markdown)); + markdown !== '' && hasTypes(markdown) && hasDescription(markdown); if (!isValid) { await github.rest.pulls.update({ From 65f960c31a734b5306a8b919040c3aae9b783efd Mon Sep 17 00:00:00 2001 From: Cotes Chung <11371340+cotes2020@users.noreply.github.com> Date: Sat, 16 Nov 2024 22:49:55 +0800 Subject: [PATCH 13/13] perf: speed up page rendering and jekyll build process (#2034) - Ensure inline scripts execute after the DOM has fully loaded. - Use Rollup to bundle the theme-mode and Mermaid scripts, reducing the number of Jekyll include snippets. --- _includes/analytics/cloudflare.html | 1 - _includes/analytics/fathom.html | 5 +- _includes/analytics/google.html | 2 +- _includes/analytics/matomo.html | 13 +- _includes/{comments.html => comment.html} | 0 _includes/comments/disqus.html | 65 +++++---- _includes/comments/giscus.html | 44 ++---- _includes/comments/utterances.html | 63 ++++---- _includes/head.html | 25 +++- _includes/js-selector.html | 29 +--- _includes/jsdelivr-combine.html | 8 +- _includes/mermaid.html | 62 -------- _includes/mode-toggle.html | 116 --------------- _includes/search-loader.html | 42 +++--- _javascript/categories.js | 2 +- _javascript/home.js | 2 +- _javascript/misc.js | 2 +- .../modules/{plugins.js => components.js} | 4 + _javascript/modules/components/img-popup.js | 16 +-- _javascript/modules/components/mermaid.js | 60 ++++++++ _javascript/modules/components/mode-toggle.js | 15 ++ .../modules/components/mode-watcher.js | 14 -- _javascript/modules/components/sidebar.js | 22 --- _javascript/modules/layouts/basic.js | 4 +- _javascript/modules/layouts/sidebar.js | 20 ++- _javascript/modules/theme.js | 135 ++++++++++++++++++ _javascript/page.js | 8 +- _javascript/post.js | 6 +- _layouts/default.html | 8 +- _layouts/post.html | 3 +- _sass/layout/post.scss | 1 + package.json | 1 + rollup.config.js | 19 ++- 33 files changed, 410 insertions(+), 407 deletions(-) rename _includes/{comments.html => comment.html} (100%) delete mode 100644 _includes/mermaid.html delete mode 100644 _includes/mode-toggle.html rename _javascript/modules/{plugins.js => components.js} (60%) create mode 100644 _javascript/modules/components/mermaid.js create mode 100644 _javascript/modules/components/mode-toggle.js delete mode 100644 _javascript/modules/components/mode-watcher.js delete mode 100644 _javascript/modules/components/sidebar.js create mode 100644 _javascript/modules/theme.js diff --git a/_includes/analytics/cloudflare.html b/_includes/analytics/cloudflare.html index 1eeb1a924ff..9faa11ee3b4 100644 --- a/_includes/analytics/cloudflare.html +++ b/_includes/analytics/cloudflare.html @@ -4,4 +4,3 @@ src="https://static.cloudflareinsights.com/beacon.min.js" data-cf-beacon='{"token": "{{ site.analytics.cloudflare.id }}"}' > - diff --git a/_includes/analytics/fathom.html b/_includes/analytics/fathom.html index 4b603d3aed5..216bb140c00 100644 --- a/_includes/analytics/fathom.html +++ b/_includes/analytics/fathom.html @@ -2,6 +2,5 @@ - + defer +> diff --git a/_includes/analytics/google.html b/_includes/analytics/google.html index d0aac651c22..dfe4828c5f0 100644 --- a/_includes/analytics/google.html +++ b/_includes/analytics/google.html @@ -1,7 +1,7 @@ - diff --git a/_includes/comments.html b/_includes/comment.html similarity index 100% rename from _includes/comments.html rename to _includes/comment.html diff --git a/_includes/comments/disqus.html b/_includes/comments/disqus.html index 2b889a4e051..fd12a3c724e 100644 --- a/_includes/comments/disqus.html +++ b/_includes/comments/disqus.html @@ -1,38 +1,25 @@ - - -
-

Comments powered by Disqus.

-
- - diff --git a/_includes/comments/giscus.html b/_includes/comments/giscus.html index f9becfe9631..8058472028f 100644 --- a/_includes/comments/giscus.html +++ b/_includes/comments/giscus.html @@ -1,21 +1,8 @@ - - - diff --git a/_includes/head.html b/_includes/head.html index af3acdb5301..310f52eb995 100644 --- a/_includes/head.html +++ b/_includes/head.html @@ -97,11 +97,32 @@ {% endif %} - + {% unless site.theme_mode %} - {% include mode-toggle.html %} + {% endunless %} + {% include js-selector.html lang=lang %} + + {% if jekyll.environment == 'production' %} + + {% if site.pwa.enabled %} + + {% endif %} + + + {% for analytics in site.analytics %} + {% capture str %}{{ analytics }}{% endcapture %} + {% assign platform = str | split: '{' | first %} + {% if site.analytics[platform].id and site.analytics[platform].id != empty %} + {% include analytics/{{ platform }}.html %} + {% endif %} + {% endfor %} + {% endif %} + {% include metadata-hook.html %} diff --git a/_includes/js-selector.html b/_includes/js-selector.html index 4d77d06b590..fd4acca8afb 100644 --- a/_includes/js-selector.html +++ b/_includes/js-selector.html @@ -62,12 +62,12 @@ {% capture script %}/assets/js/dist/{{ js }}.min.js{% endcapture %} - + {% if page.math %} - - + + {% endif %} @@ -84,26 +84,3 @@ {% endcase %} {% endif %} {% endif %} - -{% if page.mermaid %} - {% include mermaid.html %} -{% endif %} - -{% if jekyll.environment == 'production' %} - - {% if site.pwa.enabled %} - - {% endif %} - - - {% for analytics in site.analytics %} - {% capture str %}{{ analytics }}{% endcapture %} - {% assign type = str | split: '{' | first %} - {% if site.analytics[type].id and site.analytics[type].id != empty %} - {% include analytics/{{ type }}.html %} - {% endif %} - {% endfor %} -{% endif %} diff --git a/_includes/jsdelivr-combine.html b/_includes/jsdelivr-combine.html index cffa6995bf9..0611213b8f7 100644 --- a/_includes/jsdelivr-combine.html +++ b/_includes/jsdelivr-combine.html @@ -1,6 +1,6 @@ {% assign urls = include.urls | split: ',' %} -{% assign combined_urls = nil %} +{% assign combined_urls = null %} {% assign domain = 'https://cdn.jsdelivr.net/' %} @@ -15,12 +15,12 @@ {% endif %} {% elsif url contains '//' %} - + {% else %} - + {% endif %} {% endfor %} {% if combined_urls %} - + {% endif %} diff --git a/_includes/mermaid.html b/_includes/mermaid.html deleted file mode 100644 index a3a83edbdc4..00000000000 --- a/_includes/mermaid.html +++ /dev/null @@ -1,62 +0,0 @@ - - diff --git a/_includes/mode-toggle.html b/_includes/mode-toggle.html deleted file mode 100644 index 113ec375467..00000000000 --- a/_includes/mode-toggle.html +++ /dev/null @@ -1,116 +0,0 @@ - - - diff --git a/_includes/search-loader.html b/_includes/search-loader.html index 2582580a91b..7fd065d8f59 100644 --- a/_includes/search-loader.html +++ b/_includes/search-loader.html @@ -19,29 +19,31 @@

{title}

{% capture not_found %}

{{ site.data.locales[include.lang].search.no_results }}

{% endcapture %} diff --git a/_javascript/categories.js b/_javascript/categories.js index 15d82513178..ce87d671ba2 100644 --- a/_javascript/categories.js +++ b/_javascript/categories.js @@ -1,5 +1,5 @@ import { basic, initSidebar, initTopbar } from './modules/layouts'; -import { categoryCollapse } from './modules/plugins'; +import { categoryCollapse } from './modules/components'; basic(); initSidebar(); diff --git a/_javascript/home.js b/_javascript/home.js index ef22cb97c1f..7f628a17ad5 100644 --- a/_javascript/home.js +++ b/_javascript/home.js @@ -1,5 +1,5 @@ import { basic, initSidebar, initTopbar } from './modules/layouts'; -import { initLocaleDatetime, loadImg } from './modules/plugins'; +import { initLocaleDatetime, loadImg } from './modules/components'; loadImg(); initLocaleDatetime(); diff --git a/_javascript/misc.js b/_javascript/misc.js index 52b40438c89..37130da14b8 100644 --- a/_javascript/misc.js +++ b/_javascript/misc.js @@ -1,5 +1,5 @@ import { basic, initSidebar, initTopbar } from './modules/layouts'; -import { initLocaleDatetime } from './modules/plugins'; +import { initLocaleDatetime } from './modules/components'; initSidebar(); initTopbar(); diff --git a/_javascript/modules/plugins.js b/_javascript/modules/components.js similarity index 60% rename from _javascript/modules/plugins.js rename to _javascript/modules/components.js index cc95c1bc393..95791a69fcf 100644 --- a/_javascript/modules/plugins.js +++ b/_javascript/modules/components.js @@ -4,3 +4,7 @@ export { loadImg } from './components/img-loading'; export { imgPopup } from './components/img-popup'; export { initLocaleDatetime } from './components/locale-datetime'; export { initToc } from './components/toc'; +export { loadMermaid } from './components/mermaid'; +export { modeWatcher } from './components/mode-toggle'; +export { back2top } from './components/back-to-top'; +export { loadTooptip } from './components/tooltip-loader'; diff --git a/_javascript/modules/components/img-popup.js b/_javascript/modules/components/img-popup.js index ac120435505..420a2265fa5 100644 --- a/_javascript/modules/components/img-popup.js +++ b/_javascript/modules/components/img-popup.js @@ -4,7 +4,6 @@ * Dependencies: https://github.com/biati-digital/glightbox */ -const html = document.documentElement; const lightImages = '.popup:not(.dark)'; const darkImages = '.popup:not(.light)'; let selector = lightImages; @@ -33,26 +32,17 @@ export function imgPopup() { document.querySelector('.popup.dark') === null ); - if ( - (html.hasAttribute('data-mode') && - html.getAttribute('data-mode') === 'dark') || - (!html.hasAttribute('data-mode') && - window.matchMedia('(prefers-color-scheme: dark)').matches) - ) { + if (Theme.visualState === Theme.DARK) { selector = darkImages; } let current = GLightbox({ selector: `${selector}` }); - if (hasDualImages && document.getElementById('mode-toggle')) { + if (hasDualImages && Theme.switchable) { let reverse = null; window.addEventListener('message', (event) => { - if ( - event.source === window && - event.data && - event.data.direction === ModeToggle.ID - ) { + if (event.source === window && event.data && event.data.id === Theme.ID) { updateImages(current, reverse); } }); diff --git a/_javascript/modules/components/mermaid.js b/_javascript/modules/components/mermaid.js new file mode 100644 index 00000000000..2b4759f4726 --- /dev/null +++ b/_javascript/modules/components/mermaid.js @@ -0,0 +1,60 @@ +/** + * Mermaid-js loader + */ + +const MERMAID = 'mermaid'; +const themeMapper = Theme.getThemeMapper('default', 'dark'); + +function refreshTheme(event) { + if (event.source === window && event.data && event.data.id === Theme.ID) { + // Re-render the SVG › + const mermaidList = document.getElementsByClassName(MERMAID); + + [...mermaidList].forEach((elem) => { + const svgCode = elem.previousSibling.children.item(0).innerHTML; + elem.textContent = svgCode; + elem.removeAttribute('data-processed'); + }); + + const newTheme = themeMapper[Theme.visualState]; + + mermaid.initialize({ theme: newTheme }); + mermaid.init(null, `.${MERMAID}`); + } +} + +function setNode(elem) { + const svgCode = elem.textContent; + const backup = elem.parentElement; + backup.classList.add('d-none'); + // Create mermaid node + const mermaid = document.createElement('pre'); + mermaid.classList.add(MERMAID); + const text = document.createTextNode(svgCode); + mermaid.appendChild(text); + backup.after(mermaid); +} + +export function loadMermaid() { + if ( + typeof mermaid === 'undefined' || + typeof mermaid.initialize !== 'function' + ) { + return; + } + + const initTheme = themeMapper[Theme.visualState]; + + let mermaidConf = { + theme: initTheme + }; + + const basicList = document.getElementsByClassName('language-mermaid'); + [...basicList].forEach(setNode); + + mermaid.initialize(mermaidConf); + + if (Theme.switchable) { + window.addEventListener('message', refreshTheme); + } +} diff --git a/_javascript/modules/components/mode-toggle.js b/_javascript/modules/components/mode-toggle.js new file mode 100644 index 00000000000..455ff0a02dc --- /dev/null +++ b/_javascript/modules/components/mode-toggle.js @@ -0,0 +1,15 @@ +/** + * Add listener for theme mode toggle + */ + +const $toggle = document.getElementById('mode-toggle'); + +export function modeWatcher() { + if (!$toggle) { + return; + } + + $toggle.addEventListener('click', () => { + Theme.flip(); + }); +} diff --git a/_javascript/modules/components/mode-watcher.js b/_javascript/modules/components/mode-watcher.js deleted file mode 100644 index 9eecd0961ef..00000000000 --- a/_javascript/modules/components/mode-watcher.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Add listener for theme mode toggle - */ -const toggle = document.getElementById('mode-toggle'); - -export function modeWatcher() { - if (!toggle) { - return; - } - - toggle.addEventListener('click', () => { - modeToggle.flipMode(); - }); -} diff --git a/_javascript/modules/components/sidebar.js b/_javascript/modules/components/sidebar.js deleted file mode 100644 index aed759ed57d..00000000000 --- a/_javascript/modules/components/sidebar.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Expand or close the sidebar in mobile screens. - */ - -const $sidebar = document.getElementById('sidebar'); -const $trigger = document.getElementById('sidebar-trigger'); -const $mask = document.getElementById('mask'); - -class SidebarUtil { - static #isExpanded = false; - - static toggle() { - this.#isExpanded = !this.#isExpanded; - document.body.toggleAttribute('sidebar-display', this.#isExpanded); - $sidebar.classList.toggle('z-2', this.#isExpanded); - $mask.classList.toggle('d-none', !this.#isExpanded); - } -} - -export function sidebarExpand() { - $trigger.onclick = $mask.onclick = () => SidebarUtil.toggle(); -} diff --git a/_javascript/modules/layouts/basic.js b/_javascript/modules/layouts/basic.js index fb36a8b8a0c..b8eddf61588 100644 --- a/_javascript/modules/layouts/basic.js +++ b/_javascript/modules/layouts/basic.js @@ -1,7 +1,7 @@ -import { back2top } from '../components/back-to-top'; -import { loadTooptip } from '../components/tooltip-loader'; +import { back2top, loadTooptip, modeWatcher } from '../components'; export function basic() { + modeWatcher(); back2top(); loadTooptip(); } diff --git a/_javascript/modules/layouts/sidebar.js b/_javascript/modules/layouts/sidebar.js index 8795693c105..bbf5e7dab60 100644 --- a/_javascript/modules/layouts/sidebar.js +++ b/_javascript/modules/layouts/sidebar.js @@ -1,7 +1,19 @@ -import { modeWatcher } from '../components/mode-watcher'; -import { sidebarExpand } from '../components/sidebar'; +const ATTR_DISPLAY = 'sidebar-display'; +const $sidebar = document.getElementById('sidebar'); +const $trigger = document.getElementById('sidebar-trigger'); +const $mask = document.getElementById('mask'); + +class SidebarUtil { + static #isExpanded = false; + + static toggle() { + this.#isExpanded = !this.#isExpanded; + document.body.toggleAttribute(ATTR_DISPLAY, this.#isExpanded); + $sidebar.classList.toggle('z-2', this.#isExpanded); + $mask.classList.toggle('d-none', !this.#isExpanded); + } +} export function initSidebar() { - modeWatcher(); - sidebarExpand(); + $trigger.onclick = $mask.onclick = () => SidebarUtil.toggle(); } diff --git a/_javascript/modules/theme.js b/_javascript/modules/theme.js new file mode 100644 index 00000000000..f9ebf20246d --- /dev/null +++ b/_javascript/modules/theme.js @@ -0,0 +1,135 @@ +/** + * Theme management class + * + * To reduce flickering during page load, this script should be loaded synchronously. + */ +class Theme { + static #modeKey = 'mode'; + static #modeAttr = 'data-mode'; + static #darkMedia = window.matchMedia('(prefers-color-scheme: dark)'); + static switchable = !document.documentElement.hasAttribute(this.#modeAttr); + + static get DARK() { + return 'dark'; + } + + static get LIGHT() { + return 'light'; + } + + /** + * @returns {string} Theme mode identifier + */ + static get ID() { + return 'theme-mode'; + } + + /** + * Gets the current visual state of the theme. + * + * @returns {string} The current visual state, either the mode if it exists, + * or the system dark mode state ('dark' or 'light'). + */ + static get visualState() { + if (this.#hasMode) { + return this.#mode; + } else { + return this.#sysDark ? this.DARK : this.LIGHT; + } + } + + static get #mode() { + return sessionStorage.getItem(this.#modeKey); + } + + static get #isDarkMode() { + return this.#mode === this.DARK; + } + + static get #hasMode() { + return this.#mode !== null; + } + + static get #sysDark() { + return this.#darkMedia.matches; + } + + /** + * Maps theme modes to provided values + * @param {string} light Value for light mode + * @param {string} dark Value for dark mode + * @returns {Object} Mapped values + */ + static getThemeMapper(light, dark) { + return { + [this.LIGHT]: light, + [this.DARK]: dark + }; + } + + /** + * Initializes the theme based on system preferences or stored mode + */ + static init() { + if (!this.switchable) { + return; + } + + this.#darkMedia.addEventListener('change', () => { + const lastMode = this.#mode; + this.#clearMode(); + + if (lastMode !== this.visualState) { + this.#notify(); + } + }); + + if (!this.#hasMode) { + return; + } + + if (this.#isDarkMode) { + this.#setDark(); + } else { + this.#setLight(); + } + } + + /** + * Flips the current theme mode + */ + static flip() { + if (this.#hasMode) { + this.#clearMode(); + } else { + this.#sysDark ? this.#setLight() : this.#setDark(); + } + this.#notify(); + } + + static #setDark() { + document.documentElement.setAttribute(this.#modeAttr, this.DARK); + sessionStorage.setItem(this.#modeKey, this.DARK); + } + + static #setLight() { + document.documentElement.setAttribute(this.#modeAttr, this.LIGHT); + sessionStorage.setItem(this.#modeKey, this.LIGHT); + } + + static #clearMode() { + document.documentElement.removeAttribute(this.#modeAttr); + sessionStorage.removeItem(this.#modeKey); + } + + /** + * Notifies other plugins that the theme mode has changed + */ + static #notify() { + window.postMessage({ id: this.ID }, '*'); + } +} + +Theme.init(); + +export default Theme; diff --git a/_javascript/page.js b/_javascript/page.js index 76e8ce97c10..4b03b790d14 100644 --- a/_javascript/page.js +++ b/_javascript/page.js @@ -1,9 +1,15 @@ import { basic, initSidebar, initTopbar } from './modules/layouts'; -import { loadImg, imgPopup, initClipboard } from './modules/plugins'; +import { + loadImg, + imgPopup, + initClipboard, + loadMermaid +} from './modules/components'; loadImg(); imgPopup(); initSidebar(); initTopbar(); initClipboard(); +loadMermaid(); basic(); diff --git a/_javascript/post.js b/_javascript/post.js index 1c616ecd0a6..dc472b42978 100644 --- a/_javascript/post.js +++ b/_javascript/post.js @@ -5,8 +5,9 @@ import { imgPopup, initLocaleDatetime, initClipboard, - initToc -} from './modules/plugins'; + initToc, + loadMermaid +} from './modules/components'; loadImg(); initToc(); @@ -15,4 +16,5 @@ initSidebar(); initLocaleDatetime(); initClipboard(); initTopbar(); +loadMermaid(); basic(); diff --git a/_layouts/default.html b/_layouts/default.html index 1590ef62430..c83c561c242 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -74,8 +74,12 @@ {% include_cached notification.html lang=lang %} {% endif %} - - {% include js-selector.html lang=lang %} + + + {% for _include in layout.script_includes %} + {% assign _include_path = _include | append: '.html' %} + {% include {{ _include_path }} %} + {% endfor %} {% include_cached search-loader.html lang=lang %} diff --git a/_layouts/post.html b/_layouts/post.html index 6a2deff7be6..c8c21ef7986 100644 --- a/_layouts/post.html +++ b/_layouts/post.html @@ -6,7 +6,8 @@ tail_includes: - related-posts - post-nav - - comments +script_includes: + - comment --- {% include lang.html %} diff --git a/_sass/layout/post.scss b/_sass/layout/post.scss index b66e906c87f..891479e6784 100644 --- a/_sass/layout/post.scss +++ b/_sass/layout/post.scss @@ -532,6 +532,7 @@ header { .utterances { max-width: 100%; + min-height: 269px; } %btn-share-hover { diff --git a/package.json b/package.json index f93e76b3d23..34477845612 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "devDependencies": { "@babel/core": "^7.25.2", "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-private-methods": "^7.25.7", "@babel/preset-env": "^7.25.4", "@commitlint/cli": "^19.5.0", "@commitlint/config-conventional": "^19.5.0", diff --git a/rollup.config.js b/rollup.config.js index 19ba4da098e..3a1ae29ada6 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -34,24 +34,32 @@ function insertFrontmatter() { }; } -function build(filename, { src = SRC_DEFAULT, jekyll = false } = {}) { +function build( + filename, + { src = SRC_DEFAULT, jekyll = false, outputName = null } = {} +) { + const input = `${src}/${filename}.js`; + return { - input: `${src}/${filename}.js`, + input, output: { file: `${DIST}/${filename}.min.js`, format: 'iife', - name: 'Chirpy', + ...(outputName !== null && { name: outputName }), banner, sourcemap: !isProd && !jekyll }, watch: { - include: `${src}/**` + include: input }, plugins: [ babel({ babelHelpers: 'bundled', presets: ['@babel/env'], - plugins: ['@babel/plugin-transform-class-properties'] + plugins: [ + '@babel/plugin-transform-class-properties', + '@babel/plugin-transform-private-methods' + ] }), nodeResolve(), isProd && terser(), @@ -69,6 +77,7 @@ export default [ build('page'), build('post'), build('misc'), + build('theme', { src: `${SRC_DEFAULT}/modules`, outputName: 'Theme' }), build('app', { src: SRC_PWA, jekyll: true }), build('sw', { src: SRC_PWA, jekyll: true }) ];