diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 00000000000..c665f755e11 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,39 @@ +name: CD + +on: + push: + branches: [production] + tags-ignore: ["**"] + +jobs: + release: + if: ${{ ! startsWith(github.event.head_commit.message, 'chore(release)') }} + permissions: + contents: write + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.3 + bundler-cache: true + + - uses: actions/setup-node@v4 + with: + node-version: lts/* + + - run: npm install + - run: npx semantic-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GEM_HOST_API_KEY: ${{ secrets.GEM_HOST_API_KEY }} + + publish: + needs: release + uses: ./.github/workflows/publish.yml + secrets: + GH_PAT: ${{ secrets.GH_PAT }} + BUILDER: ${{ secrets.BUILDER }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000000..31b878632b1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI + +on: + push: + branches: + - master + - "hotfix/*" + paths-ignore: + - ".github/**" + - "!.github/workflows/ci.yml" + - .gitignore + - "docs/**" + - README.md + - LICENSE + pull_request: + paths-ignore: + - ".github/**" + - "!.github/workflows/ci.yml" + - .gitignore + - "docs/**" + - README.md + - LICENSE + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + ruby: ["3.1", "3.2", "3.3"] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # for posts's lastmod + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: lts/* + + - name: Build Assets + run: npm i && npm run build + + - name: Test Site + run: bash tools/test.sh diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 00000000000..58f1a3ff3d1 --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,15 @@ +name: Lint Commit Messages + +on: + push: + branches: + - master + - "hotfix/*" + pull_request: + +jobs: + commitlint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: wagoid/commitlint-github-action@v6 diff --git a/.github/workflows/pr-filter.yml b/.github/workflows/pr-filter.yml new file mode 100644 index 00000000000..8e9a18b736f --- /dev/null +++ b/.github/workflows/pr-filter.yml @@ -0,0 +1,25 @@ +name: PR Filter + +on: + pull_request_target: + types: [opened, reopened] + +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 + + 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 }} + script: | + const script = require('.github/workflows/scripts/pr-filter.js'); + await script({ github, context, core }); diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000000..b0f9713f2ef --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,23 @@ +name: Publish + +on: + push: + branches: + - docs + workflow_call: + secrets: + GH_PAT: + required: true + BUILDER: + required: true + workflow_dispatch: + +jobs: + launch: + runs-on: ubuntu-latest + steps: + - run: | + curl -X POST -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.GH_PAT }}" \ + https://api.github.com/repos/${{ secrets.BUILDER }}/dispatches \ + -d '{"event_type":"deploy", "client_payload":{"branch": "${{ github.ref_name }}"}}' diff --git a/.github/workflows/scripts/pr-filter.js b/.github/workflows/scripts/pr-filter.js new file mode 100644 index 00000000000..03f50dc5ca2 --- /dev/null +++ b/.github/workflows/scripts/pr-filter.js @@ -0,0 +1,36 @@ +function hasTypes(markdown) { + return /## Type of change/.test(markdown) && /-\s\[x\]/i.test(markdown); +} + +function hasDescription(markdown) { + return ( + /## Description/.test(markdown) && + !/## Description\s*\n\s*(##|\s*$)/.test(markdown) + ); +} + +module.exports = async ({ github, context, core }) => { + const pr = context.payload.pull_request; + const body = pr.body === null ? '' : pr.body; + const markdown = body.replace(//g, ''); + const action = context.payload.action; + + const isValid = + markdown !== '' && hasTypes(markdown) && hasDescription(markdown); + + if (!isValid) { + 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 ${action} an invalid pull request. No worries, we'll close it for you.` + }); + + core.setFailed('PR content does not meet template requirements.'); + } +}; diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000000..4f6e91cbf39 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,32 @@ +name: "Close stale issues and PRs" + +on: + schedule: + - cron: "0 0 * * *" # every day at 00:00 UTC + +permissions: + issues: write + pull-requests: write + +env: + 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. + It will be closed if no further activity occurs. + Thank you for your contributions. + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + # 60 days before marking issues/PRs stale + days-before-close: -1 # does not close automatically + stale-issue-label: ${{ env.STALE_LABEL }} + exempt-issue-labels: ${{ env.EXEMPT_LABELS }} + stale-issue-message: ${{ env.MESSAGE }} + stale-pr-label: ${{ env.STALE_LABEL }} + exempt-pr-labels: ${{ env.EXEMPT_LABELS }} + stale-pr-message: ${{ env.MESSAGE }} diff --git a/.github/workflows/style-lint.yml b/.github/workflows/style-lint.yml new file mode 100644 index 00000000000..5cb38a7a11a --- /dev/null +++ b/.github/workflows/style-lint.yml @@ -0,0 +1,25 @@ +name: Style Lint + +on: + push: + branches: + - master + - "hotfix/*" + paths: ["_sass/**/*.scss"] + pull_request: + paths: ["_sass/**/*.scss"] + +jobs: + stylelint: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: lts/* + - run: npm i + - run: npm test 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] diff --git a/_data/locales/hu-HU.yml b/_data/locales/hu-HU.yml index 53d88e96e52..be3a31b7d63 100644 --- a/_data/locales/hu-HU.yml +++ b/_data/locales/hu-HU.yml @@ -14,24 +14,23 @@ tabs: categories: Kategóriák tags: Címkék archives: Archívum - about: Rólam + about: Bemutatkozás # the text displayed in the search bar & search results search: hint: keresés cancel: Mégse - no_results: Oops! Nincs találat a keresésre. + no_results: Hoppá! Nincs találat a keresésre. panel: lastmod: Legutóbb frissítve trending_tags: Népszerű Címkék toc: Tartalom - links: Blog linkek copyright: # Shown at the bottom of the post license: - template: A bejegyzés :LICENSE_NAME licenccel rendelkezik. + template: A bejegyzést a szerző :LICENSE_NAME licenc alatt engedélyezte. name: CC BY 4.0 link: https://creativecommons.org/licenses/by/4.0/ @@ -42,7 +41,7 @@ copyright: Creative Commons Attribution 4.0 International (CC BY 4.0) licenccel rendelkeznek, hacsak másképp nincs jelezve. -meta: Készítve :PLATFORM motorral :THEME témával +meta: Készítve :THEME témával a :PLATFORM platformra. not_found: statement: Sajnáljuk, az URL-t rosszul helyeztük el, vagy valami nem létezőre mutat. @@ -73,7 +72,21 @@ post: title: Link másolása succeed: Link sikeresen másolva! +# Date time format. +# See: , +df: + post: + strftime: "%Y. %B. %e." + dayjs: "YYYY. MMMM D." + archives: + strftime: "%B" + dayjs: "MMM" + # categories page categories: - category_measure: kategória - post_measure: bejegyzés + category_measure: + singular: kategória + plural: kategória + post_measure: + singular: bejegyzés + plural: bejegyzés diff --git a/_includes/sidebar.html b/_includes/sidebar.html index 4f0bb8cad3d..569585f6f24 100644 --- a/_includes/sidebar.html +++ b/_includes/sidebar.html @@ -11,9 +11,7 @@ {%- endif -%} -

- {{ site.title }} -

+ {{ site.title }}

{{ site.tagline }}

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/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/_javascript/modules/components/toc.js b/_javascript/modules/components/toc.js index 56ce26fac50..e9086eedf9a 100644 --- a/_javascript/modules/components/toc.js +++ b/_javascript/modules/components/toc.js @@ -1,15 +1,33 @@ -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) { + if (mobile.popupOpened) { + 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..20e24a73c9f --- /dev/null +++ b/_javascript/modules/components/toc/toc-mobile.js @@ -0,0 +1,125 @@ +/** + * 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 get popupOpened() { + return $popup.open; + } + + static showPopup() { + this.lockScroll(true); + $popup.showModal(); + const activeItem = $popup.querySelector('li.is-active-li'); + activeItem.scrollIntoView({ block: 'center' }); + } + + static hidePopup() { + $popup.toggleAttribute(CLOSING); + + $popup.addEventListener( + 'animationend', + () => { + $popup.toggleAttribute(CLOSING); + $popup.close(); + }, + { once: true } + ); + + this.lockScroll(false); + } + + static lockScroll(enable) { + document.documentElement.classList.toggle(SCROLL_LOCK, enable); + document.body.classList.toggle(SCROLL_LOCK, enable); + } + + static clickBackdrop(event) { + if ($popup.hasAttribute(CLOSING)) { + return; + } + + const rect = event.target.getBoundingClientRect(); + if ( + event.clientX < rect.left || + event.clientX > rect.right || + event.clientY < rect.top || + event.clientY > rect.bottom + ) { + this.hidePopup(); + } + } + + static initComponents() { + this.initBar(); + + [...$triggers].forEach((trigger) => { + trigger.onclick = () => this.showPopup(); + }); + + $popup.onclick = (e) => this.clickBackdrop(e); + $btnClose.onclick = () => this.hidePopup(); + $popup.oncancel = (e) => { + e.preventDefault(); + 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/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/_layouts/home.html b/_layouts/home.html index e44efe8f4cb..451e391cd6d 100644 --- a/_layouts/home.html +++ b/_layouts/home.html @@ -5,38 +5,45 @@ {% include lang.html %} -{% assign pinned = site.posts | where: 'pin', 'true' %} -{% assign default = site.posts | where_exp: 'item', 'item.pin != true and item.hidden != true' %} +{% assign all_pinned = site.posts | where: 'pin', 'true' %} +{% assign all_normal = site.posts | where_exp: 'item', 'item.pin != true and item.hidden != true' %} {% assign posts = '' | split: '' %} - + -{% assign offset = paginator.page | minus: 1 | times: paginator.per_page %} -{% assign pinned_num = pinned.size | minus: offset %} +{% assign visible_start = paginator.page | minus: 1 | times: paginator.per_page %} +{% assign visible_end = visible_start | plus: paginator.per_page %} -{% if pinned_num > 0 %} - {% for i in (offset..pinned.size) limit: pinned_num %} - {% assign posts = posts | push: pinned[i] %} +{% if all_pinned.size > visible_start %} + {% if all_pinned.size > visible_end %} + {% assign pinned_size = paginator.per_page %} + {% else %} + {% assign pinned_size = all_pinned.size | minus: visible_start %} + {% endif %} + + {% for i in (visible_start..all_pinned.size) limit: pinned_size %} + {% assign posts = posts | push: all_pinned[i] %} {% endfor %} {% else %} - {% assign pinned_num = 0 %} + {% assign pinned_size = 0 %} {% endif %} - + -{% assign default_beg = offset | minus: pinned.size %} +{% assign normal_size = paginator.posts | size | minus: pinned_size %} -{% if default_beg < 0 %} - {% assign default_beg = 0 %} -{% endif %} +{% if normal_size > 0 %} + {% if pinned_size > 0 %} + {% assign normal_start = 0 %} + {% else %} + {% assign normal_start = visible_start | minus: all_pinned.size %} + {% endif %} -{% assign default_num = paginator.posts | size | minus: pinned_num %} -{% assign default_end = default_beg | plus: default_num | minus: 1 %} + {% assign normal_end = normal_start | plus: normal_size | minus: 1 %} -{% if default_num > 0 %} - {% for i in (default_beg..default_end) %} - {% assign posts = posts | push: default[i] %} + {% for i in (normal_start..normal_end) %} + {% assign posts = posts | push: all_normal[i] %} {% endfor %} {% endif %} diff --git a/_layouts/post.html b/_layouts/post.html index f17ceea8641..6a2deff7be6 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 465cb085905..5e8aceaa262 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); } } } @@ -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); @@ -738,6 +737,9 @@ $btn-mb: 0.5rem; } .site-title { + @extend %clickable-transition; + @extend %sidebar-link-hover; + font-family: inherit; font-weight: 900; font-size: 1.75rem; @@ -745,13 +747,8 @@ $btn-mb: 0.5rem; letter-spacing: 0.25px; margin-top: 1.25rem; margin-bottom: 0.5rem; - - a { - @extend %clickable-transition; - @extend %sidebar-link-hover; - - color: var(--site-title-color); - } + width: fit-content; + color: var(--site-title-color); } .site-subtitle { @@ -771,8 +768,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); @@ -910,9 +907,7 @@ $btn-mb: 0.5rem; } #topbar { - button i { - color: #999999; - } + @extend %btn-color; #breadcrumb { font-size: 1rem; @@ -1047,7 +1042,7 @@ search { a { font-size: 1.4rem; - line-height: 2.5rem; + line-height: 1.5rem; &:hover { @extend %link-hover; @@ -1073,8 +1068,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; @@ -1090,23 +1086,11 @@ search { color: var(--topbar-text-color); text-align: center; width: 70%; - overflow: hidden; - text-overflow: ellipsis; word-break: keep-all; - white-space: nowrap; } #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 --- */ @@ -1496,8 +1480,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 42db4e2d991..1dfb735fd01 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 { @@ -111,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; @@ -134,6 +145,12 @@ } } +%btn-color { + button i { + color: #999999; + } +} + /* ---------- scss mixin --------- */ @mixin mt-mb($value) { @@ -151,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/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/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 815db933198..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; } } } @@ -228,6 +228,7 @@ header { } } +/* TOC panel */ #toc-wrapper { border-left: 1px solid rgba(158, 158, 158, 0.17); position: -webkit-sticky; @@ -257,9 +258,8 @@ header { .toc-link { display: block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + + @extend %text-ellipsis; &:hover { color: var(--toc-highlight); @@ -290,6 +290,209 @@ 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; + $backdrop: blur(5px); + + border-color: var(--toc-popup-border-color); + border-width: 1px; + border-radius: $radius-lg; + color: var(--text-color); + background: var(--card-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 { + > i { + font-size: 1.25rem; + vertical-align: middle; + } + + &: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: $backdrop; + backdrop-filter: $backdrop; + } + + &::backdrop { + -webkit-backdrop-filter: $backdrop; + backdrop-filter: $backdrop; + } + + &::after { + display: flex; + content: ''; + position: relative; + background: linear-gradient(transparent, var(--card-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 { @@ -305,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; @@ -330,7 +534,7 @@ header { max-width: 100%; } -%btn-share-hovor { +%btn-share-hover { color: var(--btn-share-hover-color) !important; } @@ -362,9 +566,20 @@ 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); + } +} + +@media all and (min-width: 1200px) { + h2, + h3, + h4 { + scroll-margin-top: 2rem; + } + + #toc-bar, + #toc-solo-trigger { + display: none !important; } } 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 }}