diff --git a/assets/css/_core/_base.scss b/assets/css/_core/_base.scss index c3cb8792..b512607d 100644 --- a/assets/css/_core/_base.scss +++ b/assets/css/_core/_base.scss @@ -63,6 +63,10 @@ img,video { height: auto; } +dialog { + padding: 0; +} + details { &.center { summary { diff --git a/assets/css/_core/_media.scss b/assets/css/_core/_media.scss index 5c29f081..bf55c170 100644 --- a/assets/css/_core/_media.scss +++ b/assets/css/_core/_media.scss @@ -40,6 +40,12 @@ } } +@media only screen and (min-width: 961px) { + #toc-drawer-button { + display: none; + } +} + @media only screen and (max-width: 960px) { %page-style { width: ROUND(80%, 2px) !important; diff --git a/assets/css/_partials/_single/_toc.scss b/assets/css/_partials/_single/_toc.scss index a35227fe..c0f39d48 100644 --- a/assets/css/_partials/_single/_toc.scss +++ b/assets/css/_partials/_single/_toc.scss @@ -218,14 +218,14 @@ &::before { color: $single-link-hover-color; - + [data-theme='dark'] & { color: $single-link-hover-color-dark; } } } } - + > ol { margin: 0; padding: 0.4em 1em 0.4em 2.25em; @@ -252,3 +252,72 @@ } } } + +#toc-dialog { + transition: + display 0.2s allow-discrete, + overlay 0.2s allow-discrete, + translate 0.2s, + opacity 0.2s 0.4s; + opacity: 0; + translate: 100vw 0; + width: 100%; + height: 100%; + max-width: 100%; + max-height: 100%; + margin-left: 4rem; + border: none; + + .toc { + max-width: 100%; + margin-inline: 1rem; + + .toc-title { + font-size: $toc-title-font-size * 1.5; + line-height: $toc-title-font-size * 2; + margin-block: $toc-title-font-size; + } + + .toc-content { + font-size: max($toc-content-font-size, 1rem); + } + } + + &::backdrop { + transition: + display 0.5s allow-discrete, + overlay 0.5s allow-discrete, + opacity 0.2s 0.4s; + opacity: 0; + } + + &[open], + &[open]::backdrop { + opacity: 1; + transition: + display 0.2s allow-discrete, + overlay 0.2s allow-discrete, + translate 0.2s, + opacity 0.2s; + } + + &[open] { + translate: 0 0; + } + + @starting-style { + + &[open], + &[open]::backdrop { + opacity: 0; + } + + &[open] { + translate: 100vw 0; + } + } +} + +html:has(#toc-dialog[open]) { + overflow: hidden; +} diff --git a/assets/css/_partials/_widgets/_fixed-button.scss b/assets/css/_partials/_widgets/_fixed-button.scss index 9f955bcd..ef799b26 100644 --- a/assets/css/_partials/_widgets/_fixed-button.scss +++ b/assets/css/_partials/_widgets/_fixed-button.scss @@ -1,12 +1,19 @@ -.fixed-button { +.fixed-buttons { position: fixed; right: 1.5rem; + bottom: 1.5rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + @include z-index(1); +} + +.fixed-button { font-size: 1rem; line-height: 1.5rem; padding: 0.5rem 0.625rem; - color: $global-font-secondary-color; + color: $global-font-color; background-color: $header-background-color; - @include z-index(1); @include border-radius(1.25rem); @include transition(color 0.4s ease); @include blur; @@ -25,7 +32,7 @@ } [data-theme='dark'] & { - color: $global-font-secondary-color-dark; + color: $global-font-color-dark; background-color: $header-background-color-dark; &:hover, @@ -36,9 +43,47 @@ } .back-to-top { - bottom: 1.5rem; -} + overflow: hidden; + position: relative; + // Ring stroke width + --b2t-stroke: 0.5rem; + // Ring background color + --b2t-bg: #{$global-border-color}; + // Progress color + --b2t-prog: #FF7359; + // 2π×50 ≈ 314 (radius 50 for viewBox 100) + --b2t-circumference: 314; -.view-comments { - bottom: 4.5rem; + [data-theme='dark'] & { + --b2t-bg: #{$global-border-color-dark}; + --b2t-prog: #{$global-font-color-dark}; + } + + // SVG circular progress ring + svg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + // Rotate to start from top + transform: rotate(-90deg); + } + + .bg { + fill: none; + stroke: var(--b2t-bg); + stroke-width: var(--b2t-stroke); + } + + // Progress ring + .progress { + fill: none; + stroke: var(--b2t-prog); + stroke-width: var(--b2t-stroke); + stroke-linecap: round; + stroke-dasharray: var(--b2t-circumference); + stroke-dashoffset: calc(var(--b2t-circumference) - var(--b2t-circumference) * var(--scroll-percent, 0) / 100); + transition: stroke-dashoffset .1s linear; + } } diff --git a/assets/js/head/color-scheme.js b/assets/js/head/color-scheme.js index 34578579..234fa34c 100644 --- a/assets/js/head/color-scheme.js +++ b/assets/js/head/color-scheme.js @@ -18,4 +18,5 @@ import params from '@params'; params.defaultTheme === 'dark'; } isDark && (document.documentElement.dataset.theme = 'dark'); + document.documentElement.style.setProperty('color-scheme', isDark ? 'dark' : 'light'); })(); diff --git a/assets/js/theme.js b/assets/js/theme.js index ee714841..ab1c9de4 100644 --- a/assets/js/theme.js +++ b/assets/js/theme.js @@ -91,6 +91,7 @@ class FixIt { this.util.forEach(document.getElementsByClassName('theme-switch'), ($themeSwitch) => { $themeSwitch.addEventListener('click', () => { document.documentElement.dataset.theme = this.isDark ? 'light' : 'dark'; + document.documentElement.style.setProperty('color-scheme', this.isDark ? 'light' : 'dark'); this.isDark = !this.isDark; window.localStorage?.setItem('theme', this.isDark ? 'dark' : 'light'); for (let event of this.switchThemeEventSet) { @@ -650,12 +651,40 @@ class FixIt { const animation = ['animate__faster']; const tocHidden = $toc.classList.contains('toc-hidden'); animation.push(tocHidden ? 'animate__fadeIn' : 'animate__fadeOut'); - $tocContentAuto.classList.remove(tocHidden ? 'animate__fadeOut' : 'animate__fadeIn'); - this.util.animateCSS($tocContentAuto, animation, true); + if (tocHidden) { + $tocContentAuto.classList.remove('d-none', 'animate__fadeOut'); + } else { + $tocContentAuto.classList.remove('animate__fadeIn'); + } + this.util.animateCSS($tocContentAuto, animation, true, () => { + $tocContentAuto.classList.contains('animate__fadeOut') && $tocContentAuto.classList.add('d-none'); + }); $toc.classList.toggle('toc-hidden'); }, false); } + initTocDialog() { + // HTMLDialogElement + const dialog = document.querySelector("#toc-dialog"); + const openButton = document.querySelector("#toc-drawer-button"); + if (!dialog || !openButton) { + return; + } + openButton.addEventListener("click", () => { + dialog.showModal(); + document.activeElement?.blur(); + }); + dialog.addEventListener("click", (e) => { + dialog.close(); + }); + dialog.addEventListener("keydown", (e) => { + // ensure Escape key closes the dialog (for robustness) + if (e.key === "Escape" && dialog.open) { + dialog.close(); + } + }); + } + /** * It's a dirty hack to fix the bug of APlayer and smoothScroll. * see https://github.com/hugo-fixit/FixIt/issues/292 @@ -1134,6 +1163,7 @@ class FixIt { this.fixTocScroll(); this.initToc(); this.initTocListener(); + this.initTocDialog(); } this.initPangu(); this.initMathJax(); @@ -1237,7 +1267,6 @@ class FixIt { onScroll() { const $headers = []; const ACCURACY = 20; - const $fixedButtons = document.querySelector('.fixed-buttons'); const $backToTop = document.querySelector('.back-to-top'); const $readingProgressBar = document.querySelector('.reading-progress-bar'); if (document.body.dataset.headerDesktop === 'auto') { @@ -1275,20 +1304,19 @@ class FixIt { if ($readingProgressBar) { $readingProgressBar.style.setProperty('--progress', `${scrollPercent.toFixed(2)}%`); } - // whether to show fixed buttons - if ($fixedButtons) { + // whether to show back to top button + if ($backToTop) { if (scrollPercent > 1) { - $fixedButtons.classList.remove('d-none', 'animate__fadeOut'); - this.util.animateCSS($fixedButtons, ['animate__fadeIn'], true); + $backToTop.classList.remove('d-none', 'animate__fadeOut'); + this.util.animateCSS($backToTop, ['animate__fadeIn'], true); } else { - $fixedButtons.classList.remove('animate__fadeIn'); - this.util.animateCSS($fixedButtons, ['animate__fadeOut'], true, () => { - $fixedButtons.classList.contains('animate__fadeOut') && $fixedButtons.classList.add('d-none'); + $backToTop.classList.remove('animate__fadeIn'); + this.util.animateCSS($backToTop, ['animate__fadeOut'], true, () => { + $backToTop.classList.contains('animate__fadeOut') && $backToTop.classList.add('d-none'); }); } - if ($backToTop) { - $backToTop.querySelector('span').innerText = `${Math.round(scrollPercent)}%`; - } + // [todo] Shares the scrollPercent variable with readingProgressBar + $backToTop.style.setProperty('--scroll-percent', scrollPercent.toFixed(2)); } for (let event of this.scrollEventSet) { event(); @@ -1372,6 +1400,7 @@ class FixIt { this.fixTocScroll(); this.initToc(); this.initTocListener(); + this.initTocDialog(); } this.onScroll(); this.onResize(); diff --git a/hugo.toml b/hugo.toml index 69c69340..a2d4ba67 100644 --- a/hugo.toml +++ b/hugo.toml @@ -988,7 +988,7 @@ style = "" # ["", "mp", "identicon", "monsterid", "wavatar", "retro", "blank", " # FixIt 0.2.16 | NEW Back to top [params.backToTop] enable = true -# Scroll percent label in b2t button +# FixIt 0.4.0 | CHANGED whether to show the scroll percent indicator around the back to top button scrollpercent = false # FixIt 0.2.16 | NEW Reading progress bar diff --git a/layouts/_partials/widgets.html b/layouts/_partials/widgets.html index 71dadef7..12ab82c0 100644 --- a/layouts/_partials/widgets.html +++ b/layouts/_partials/widgets.html @@ -1,13 +1,25 @@