From a8f073678c715014eea9e982621124c2dc8e0aec Mon Sep 17 00:00:00 2001 From: PhilW Date: Fri, 9 Oct 2026 14:49:54 +0100 Subject: [PATCH 1/2] remove inline JS and various housekeeping tasks * remove inline JS * reformat _config.yml * add `make js` to minify new JS files * various minor CS fixes * remove unnecessary HTTPS check and `enforce_ssl` site variable * remove unnecessary site variable `hostname` --- Makefile | 29 ++++++- README.md | 4 + _config.yml | 77 ++++++++++------- _includes/footer.html | 105 ++++------------------- _includes/head.html | 14 +--- _includes/viewer.html | 20 +++++ download/index.html | 13 +-- google2ae4ba011e8a69d4.html | 7 +- index.html | 54 +----------- js/banner.js | 24 ++++++ js/banner.min.js | 1 + js/copy.js | 118 ++++++++++++++++++++++++++ js/copy.min.js | 1 + js/theme.js | 34 ++++++++ js/theme.min.js | 1 + js/viewer.js | 162 ++++++++++++++++++++++++++++++++++++ js/viewer.min.js | 1 + package.json | 3 +- screens/index.html | 146 +------------------------------- sitemap.xml | 4 +- styles/styles.less | 26 +++--- styles/styles.min.css | 2 +- y_key_f7f60b3952eaecd0.html | 16 ++-- 23 files changed, 504 insertions(+), 358 deletions(-) create mode 100644 _includes/viewer.html create mode 100644 js/banner.js create mode 100644 js/banner.min.js create mode 100644 js/copy.js create mode 100644 js/copy.min.js create mode 100644 js/theme.js create mode 100644 js/theme.min.js create mode 100644 js/viewer.js create mode 100644 js/viewer.min.js diff --git a/Makefile b/Makefile index 72929ec..e36a222 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,14 @@ -.PHONY: all css serve-dev +.PHONY: all css js serve-dev + +define update_cache_buster + hash=$$(sha256sum "$(1)" | cut -c1-8); \ + perl -0pi -e "s|(^[[:space:]]*$(2):.*?url: '/)[^']*(')|\$$1$(1)?v=$$hash\$$2|ms" _config.yml +endef + +define update_sri + hash=$$(openssl dgst -sha384 -binary "$(1)" | openssl base64 -A); \ + perl -0pi -e "s|(^[[:space:]]*$(2):.*?sri: ')[^']*(')|\$$1sha384-$$hash\$$2|ms" _config.yml +endef all: @echo '# These are the available make targets' @@ -6,7 +16,22 @@ all: css: npm install - npx less --clean-css="--s1 --advanced" styles/styles.less > styles/styles.min.css + npx lessc --clean-css="--s1 --advanced" styles/styles.less > styles/styles.min.css + $(call update_cache_buster,styles/styles.min.css,css) + $(call update_sri,styles/styles.min.css,css) + +js: + npm install + @for file in js/*.js; do \ + case "$$file" in \ + *.min.js) continue ;; \ + esac; \ + minified="$${file%.js}.min.js"; \ + name="$$(basename "$$file" .js)"; \ + npx uglifyjs "$$file" -c -m -o "$$minified"; \ + $(call update_cache_buster,$$minified,$$name); \ + $(call update_sri,$$minified,$$name); \ + done serve-dev: docker run -it -p 4000:4000 -v $(PWD):/site -w /site library/ruby:3 bash -c 'bundle install && bundle exec jekyll serve -H 0.0.0.0' diff --git a/README.md b/README.md index 02d9161..3e9ee7e 100644 --- a/README.md +++ b/README.md @@ -12,3 +12,7 @@ The pages are rendered with Jekyll and kindly hosted by github. ## Build CSS To build the stylesheets run `make css`. + +## Build JS + +To build the stylesheets run `make js`. diff --git a/_config.yml b/_config.yml index 09d8338..2747711 100644 --- a/_config.yml +++ b/_config.yml @@ -1,38 +1,55 @@ safe: true encoding: UTF-8 url: https://roundcube.net -enforce_ssl: roundcube.net -hostname: "https://roundcube.net" permalink: news/:year/:month/:day/:title exclude: [vendor] plugins: [jekyll-paginate] paginate: 10 -paginate_path: "/news/page:num/" -environment: "production" +paginate_path: '/news/page:num/' +environment: 'production' num_related_items: 5 -libs: { - fonts: { - roboto: 'https://fonts.googleapis.com/css?family=Roboto&display=swap', - }, - less: { - url: 'https://cdn.jsdelivr.net/npm/less@4.6.4/dist/less.min.js', - sri: 'sha384-WR+iiQT+XSq8wDUutASKAXx1pZTJCLRyHfpwqBiL1ji4qd2BsJIPhn8k/s72NkPj' - }, - css: '/styles/styles.min.css?v=2026051000', - bootstrap: { - css: { - url: 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css', - sri: 'sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB' - }, - js: { - url: 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js', - sri: 'sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI' - } - }, - fontawesome: { - css: { - url: 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7.2.0/css/all.min.css', - sri: 'sha384-EXatlQyrOJgDaM9/a74ArMzy7/2bTMSrZj8ID1IPeVmc3GncfCugefCFWSLj8JL/' - }, - } -} +exclude: + - Gemfile + - Gemfile.lock + - node_modules/ + - Makefile + - package.json + - package-lock.json + - README.md + - stylelint.config.mjs +libs: + css: + url: '/styles/styles.min.css?v=9bf32423' + sri: 'sha384-yb8/IfPlNNl8SDmdqLJ/CA5xbgQHXyHb2ehtoAamh+bLbMmu/tW+EBvF2BdzkzLv' + js: + banner: + url: '/js/banner.min.js?v=f44c2f56' + sri: 'sha384-zF0W4VZ06nGXgt2nFDiPoMhn5+UK+WaAIX1cprNGHkcURqccwGD+fXRZiu77TZ+b' + copy: + url: '/js/copy.min.js?v=ebb9b310' + sri: 'sha384-9TlbtUpnP3OoI5QMwNcfEMbkzjHOvlpeDEjVJCPWHJPxwCA5K2dXqlKZrb07rYVa' + theme: + url: '/js/theme.min.js?v=17d30fbf' + sri: 'sha384-VhNYv2z6f3ZriTjr7a9xelQKHZilrphb/xj36CtPT1McEdLSIPrMG4L+IJkgC1+T' + viewer: + url: '/js/viewer.min.js?v=df6dc21d' + sri: 'sha384-Gn/Zio1YIE6O7P0MXgTN8GECQMnPDd8MYeXQbJV5hk4+6O6aWhkBHcFUFs+DpNDL' + less: + js: + url: 'https://cdn.jsdelivr.net/npm/less@4.9.1/dist/less.min.js' + sri: 'sha256-QoOydTeDccOLjcHMHS1oOUe0QaqHXh2XFuki4s/zSXc=' + roboto: + css: + url: 'https://cdn.jsdelivr.net/npm/@fontsource/roboto@5.3.0/400.css' + sri: 'sha256-aYPEXwxOBquazL7uNzcM57whskXEvU0X84EZ2lI41yk=' + bootstrap: + css: + url: 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css' + sri: 'sha256-2FMn2Zx6PuH5tdBQDRNwrOo60ts5wWPC9R8jK67b3t4=' + js: + url: 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js' + sri: 'sha256-5P1JGBOIxI7FBAvT/mb1fCnI5n/NhQKzNUuW7Hq0fMc=' + fontawesome: + css: + url: 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7.3.1/css/all.min.css' + sri: 'sha256-4Lad8m4ZWW1Lgb9+sMVLYEfnIh7BjV1NQMEe79Pviks=' diff --git a/_includes/footer.html b/_includes/footer.html index 9e7e268..bda5fad 100644 --- a/_includes/footer.html +++ b/_includes/footer.html @@ -13,9 +13,9 @@ Switch theme @@ -41,91 +41,14 @@ - \ No newline at end of file +{% assign scripts = "theme" | split: "," %} +{% if page.scripts %}{% assign scripts = scripts | concat: page.scripts %}{% endif %} +{% for script in scripts %} + {% assign js = site.libs.js[script] %} + + {% if site.environment == "dev" %} + + {% else %} + + {% endif %} +{% endfor %} diff --git a/_includes/head.html b/_includes/head.html index 6c5fcb5..bd5c82b 100644 --- a/_includes/head.html +++ b/_includes/head.html @@ -5,20 +5,14 @@ - + {% if site.environment == "dev" %} - - + + {% else %} - + {% endif %} - diff --git a/_includes/viewer.html b/_includes/viewer.html new file mode 100644 index 0000000..15353b5 --- /dev/null +++ b/_includes/viewer.html @@ -0,0 +1,20 @@ + diff --git a/download/index.html b/download/index.html index eeba2a6..f390944 100644 --- a/download/index.html +++ b/download/index.html @@ -3,6 +3,8 @@ title: Roundcube Webmail Downloads navtitle: Download lastmod: 2019-08-28T12:32:00Z +scripts: + - copy ---

Download your version of Roundcube

@@ -37,9 +39,9 @@

{{ params.title }}{% if params.titleVersion %} - {{ params.pac {{ package.size }} - {{ package.checksum }} + {{ package.checksum }} - + {% endfor %} @@ -115,7 +117,7 @@

PGP signatures for package verification pubkey and the signature file, verify packages with

- gpg --verify roundcubemail-<version>.tar.gz{.asc,} + gpg --verify roundcubemail-<version>.tar.gz{.asc,}

Installation and upgrading

@@ -141,13 +143,12 @@

Integrations

- + Nextcloud webmail The Nextcloud app "Roundcube Mail" integrates an existing Roundcube standalone email-webapp into Nextcloud, with optional SSO.

- @@ -161,7 +162,7 @@

Complete email server packages with Roundcube

Roundcube for Enterprises - is a stand-alone, all-in-one email solution for service providers by Nextcloud. Your customers need a reliable, dependable and performant email service. The Nextcloud Roundcube Enterprise subscription helps you deliver exactly that. + is a stand-alone, all-in-one email solution for service providers by Nextcloud. Your customers need a reliable, dependable and performant email service. The Nextcloud Roundcube Enterprise subscription helps you deliver exactly that.

diff --git a/google2ae4ba011e8a69d4.html b/google2ae4ba011e8a69d4.html index e046b08..d24ab5a 100644 --- a/google2ae4ba011e8a69d4.html +++ b/google2ae4ba011e8a69d4.html @@ -1,4 +1,7 @@ + - + +roundcube + - \ No newline at end of file + diff --git a/index.html b/index.html index 83df609..cdfe49a 100644 --- a/index.html +++ b/index.html @@ -2,6 +2,8 @@ layout: home title: Roundcube - Free and Open Source Webmail Software date: 2020-04-29T21:10:00Z +scripts: + - banner --- - - diff --git a/js/banner.js b/js/banner.js new file mode 100644 index 0000000..4c19087 --- /dev/null +++ b/js/banner.js @@ -0,0 +1,24 @@ +const currentDate = new Date(); + +document.querySelectorAll('.rc-announcement').forEach((announcement) => { + const expiry = announcement.dataset.rcExpiry; + + if (!expiry || currentDate < new Date(expiry)) { + announcement.classList.remove('d-none'); + } +}); + +const switchLogo = () => { + const isDark = document.documentElement.dataset.bsTheme === 'dark'; + + document.querySelectorAll('img[data-rc-dark]').forEach((img) => { + if (!img.dataset.rcLight) { + img.dataset.rcLight = img.src; + } + + img.src = isDark ? img.dataset.rcDark : img.dataset.rcLight; + }); +}; + +switchLogo(); +document.addEventListener('switch-color-mode', switchLogo, false); diff --git a/js/banner.min.js b/js/banner.min.js new file mode 100644 index 0000000..0e93722 --- /dev/null +++ b/js/banner.min.js @@ -0,0 +1 @@ +let currentDate=new Date,switchLogo=(document.querySelectorAll(".rc-announcement").forEach(e=>{var t=e.dataset.rcExpiry;(!t||currentDate{let t="dark"===document.documentElement.dataset.bsTheme;document.querySelectorAll("img[data-rc-dark]").forEach(e=>{e.dataset.rcLight||(e.dataset.rcLight=e.src),e.src=t?e.dataset.rcDark:e.dataset.rcLight})});switchLogo(),document.addEventListener("switch-color-mode",switchLogo,!1); \ No newline at end of file diff --git a/js/copy.js b/js/copy.js new file mode 100644 index 0000000..9b6af14 --- /dev/null +++ b/js/copy.js @@ -0,0 +1,118 @@ +const copyText = 'Copy to clipboard'; +const copyComplete = 'Copied'; +const copyError = 'Copy failed'; +const copyPopoverLinks = document.querySelectorAll('button.copy-popover-link'); + +copyPopoverLinks.forEach((button) => { + const target = button.closest('td')?.querySelector('.copy-popover-target'); + + if (!target) { + return; + } + + // eslint-disable-next-line no-new + new bootstrap.Popover(button, { + content: target.innerHTML, + html: true, + allowList: { ...bootstrap.Popover.Default.allowList, button: [] }, + customClass: 'copy-popover', + }); + + button.addEventListener('click', (event) => { + event.preventDefault(); + }); + + button.addEventListener('shown.bs.popover', () => { + initialisePopoverCopyButtons(button); + }); +}); + +function initialisePopoverCopyButtons(popoverButton) { + const popoverId = popoverButton.getAttribute('aria-describedby'); + + if (!popoverId) { + return; + } + + const popover = document.getElementById(popoverId); + + if (!popover) { + return; + } + + popover.querySelectorAll('button.copy-link').forEach((copyButton) => { + initialiseCopyButton(copyButton); + }); +} + +if (copyPopoverLinks.length > 0) { + document.addEventListener('click', (event) => { + if (event.target.closest('.popover.show')) { + return; + } + + copyPopoverLinks.forEach((button) => { + const popover = bootstrap.Popover.getInstance(button); + + if (popover?.tip?.classList.contains('show')) { + popover.hide(); + } + }); + }); +} + +document.querySelectorAll('button.copy-link').forEach((copyButton) => { + initialiseCopyButton(copyButton); +}); + +function initialiseCopyButton(copyButton) { + if (copyButton.dataset.copyInitialised === 'true') { + return; + } + + copyButton.dataset.copyInitialised = 'true'; + + const tooltip = bootstrap.Tooltip.getOrCreateInstance(copyButton, { + title: copyText, + }); + + copyButton.addEventListener('click', (event) => { + copyToClipboard(copyButton, tooltip, event); + }); +} + +async function copyToClipboard(copyButton, tooltip, event) { + event.preventDefault(); + + const container = copyButton.closest('p, td, div'); + const target = container?.querySelector('.copy-target'); + + if (!target) { + return; + } + + const payload = target.textContent.trimEnd(); + + try { + await navigator.clipboard.writeText(payload); + updateCopyButton(copyButton, tooltip, 'complete', copyComplete); + } catch { + updateCopyButton(copyButton, tooltip, 'error', copyError); + } +} + +function updateCopyButton(copyButton, tooltip, cssClass, text) { + tooltip.setContent({ + '.tooltip-inner': text, + }); + + copyButton.classList.add(cssClass); + + window.setTimeout(() => { + tooltip.setContent({ + '.tooltip-inner': copyText, + }); + + copyButton.classList.remove(cssClass); + }, 2500); +} diff --git a/js/copy.min.js b/js/copy.min.js new file mode 100644 index 0000000..026af17 --- /dev/null +++ b/js/copy.min.js @@ -0,0 +1 @@ +let copyText="Copy to clipboard",copyComplete="Copied",copyError="Copy failed",copyPopoverLinks=document.querySelectorAll("button.copy-popover-link");function initialisePopoverCopyButtons(t){var t=t.getAttribute("aria-describedby");t&&(t=document.getElementById(t))&&t.querySelectorAll("button.copy-link").forEach(t=>{initialiseCopyButton(t)})}function initialiseCopyButton(e){if("true"!==e.dataset.copyInitialised){e.dataset.copyInitialised="true";let o=bootstrap.Tooltip.getOrCreateInstance(e,{title:copyText});e.addEventListener("click",t=>{copyToClipboard(e,o,t)})}}async function copyToClipboard(t,o,e){e.preventDefault();e=t.closest("p, td, div")?.querySelector(".copy-target");if(e){e=e.textContent.trimEnd();try{await navigator.clipboard.writeText(e),updateCopyButton(t,o,"complete",copyComplete)}catch{updateCopyButton(t,o,"error",copyError)}}}function updateCopyButton(t,o,e,i){o.setContent({".tooltip-inner":i}),t.classList.add(e),window.setTimeout(()=>{o.setContent({".tooltip-inner":copyText}),t.classList.remove(e)},2500)}copyPopoverLinks.forEach(t=>{var o=t.closest("td")?.querySelector(".copy-popover-target");o&&(new bootstrap.Popover(t,{content:o.innerHTML,html:!0,allowList:{...bootstrap.Popover.Default.allowList,button:[]},customClass:"copy-popover"}),t.addEventListener("click",t=>{t.preventDefault()}),t.addEventListener("shown.bs.popover",()=>{initialisePopoverCopyButtons(t)}))}),0{t.target.closest(".popover.show")||copyPopoverLinks.forEach(t=>{t=bootstrap.Popover.getInstance(t);t?.tip?.classList.contains("show")&&t.hide()})}),document.querySelectorAll("button.copy-link").forEach(t=>{initialiseCopyButton(t)}); \ No newline at end of file diff --git a/js/theme.js b/js/theme.js new file mode 100644 index 0000000..8695797 --- /dev/null +++ b/js/theme.js @@ -0,0 +1,34 @@ +const html = document.documentElement; +const storageAvailable = typeof Storage !== 'undefined'; +const savedColorMode = storageAvailable ? localStorage.getItem('color-mode') : undefined; + +switchColorMode(savedColorMode || 'auto', true); + +document.querySelectorAll('div.color-modes a[data-rc-color-mode]').forEach((link) => { + link.addEventListener('click', (event) => { + event.preventDefault(); + switchColorMode(link.dataset.rcColorMode); + }); +}); + +function switchColorMode(mode, onload = false) { + const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false; + const colorMode = mode === 'auto' ? (prefersDark ? 'dark' : 'light') : mode; + html.dataset.bsTheme = colorMode; + + const currentTheme = document.querySelector('div.color-modes > ul.dropdown-menu a.current-theme'); + const selectedTheme = document.querySelector(`div.color-modes > ul.dropdown-menu a.${mode}-mode`); + + currentTheme?.classList.remove('current-theme'); + selectedTheme?.classList.add('current-theme'); + + if (!onload && storageAvailable) { + if (mode === 'auto') { + localStorage.removeItem('color-mode'); + } else { + localStorage.setItem('color-mode', mode); + } + } + + document.dispatchEvent(new Event('switch-color-mode')); +} diff --git a/js/theme.min.js b/js/theme.min.js new file mode 100644 index 0000000..e05bdf1 --- /dev/null +++ b/js/theme.min.js @@ -0,0 +1 @@ +let html=document.documentElement,storageAvailable="undefined"!=typeof Storage,savedColorMode=storageAvailable?localStorage.getItem("color-mode"):void 0;function switchColorMode(e,o=!1){var t=window.matchMedia?.("(prefers-color-scheme: dark)").matches??!1,t="auto"===e?t?"dark":"light":e,t=(html.dataset.bsTheme=t,document.querySelector("div.color-modes > ul.dropdown-menu a.current-theme")),r=document.querySelector(`div.color-modes > ul.dropdown-menu a.${e}-mode`);t?.classList.remove("current-theme"),r?.classList.add("current-theme"),!o&&storageAvailable&&("auto"===e?localStorage.removeItem("color-mode"):localStorage.setItem("color-mode",e)),document.dispatchEvent(new Event("switch-color-mode"))}switchColorMode(savedColorMode||"auto",!0),document.querySelectorAll("div.color-modes a[data-rc-color-mode]").forEach(o=>{o.addEventListener("click",e=>{e.preventDefault(),switchColorMode(o.dataset.rcColorMode)})}); \ No newline at end of file diff --git a/js/viewer.js b/js/viewer.js new file mode 100644 index 0000000..1361b42 --- /dev/null +++ b/js/viewer.js @@ -0,0 +1,162 @@ +const viewer = document.getElementById('viewer'); +const viewerBody = viewer.querySelector('.modal-body'); +const viewerImage = viewerBody.querySelector('img'); +const viewerDialog = viewer.querySelector('.modal-dialog'); +const viewerCaption = viewer.querySelector('.modal-footer > p'); + +viewer.querySelectorAll('button[data-rc-navigation]').forEach((link) => { + link.addEventListener('click', (event) => { + event.preventDefault(); + moveScreen(link.dataset.rcNavigation === 'previous'); + }); +}); + +viewer.addEventListener('show.bs.modal', (event) => { + const source = event.relatedTarget; + const img = source.querySelector('img'); + + const src = img.src; + const alt = img.alt || ''; + + viewerImage.src = src.replace('/thumbs/', '/screens/'); + viewerImage.alt = alt; + viewerCaption.textContent = alt; + + // Extract the current source and index once. + const id = source.id; + viewer.dataset.rcCurIndex = id.replace(/^screenshots-[a-z]+-[a-z]+-/, ''); + viewer.dataset.rcCurSource = id.replace(/-\d+$/, ''); + + updateNavigation(); +}); + +viewer.addEventListener('hidden.bs.modal', () => { + viewerImage.src = 'data:,'; + viewerImage.alt = ''; + viewerImage.style.removeProperty('height'); + + if (viewer.dataset.rcNoResize !== 'true') { + viewerDialog.style.removeProperty('width'); + } + + viewerCaption.textContent = ''; + viewer.dataset.rcCurIndex = ''; + viewer.dataset.rcCurSource = ''; + viewerBody.classList.add('loading'); +}); + +// Keyboard navigation +document.addEventListener('keyup', (event) => { + if (!viewer.classList.contains('show')) { + return; + } + + if (event.key === 'ArrowLeft') { + moveScreen(true); + } else if (event.key === 'ArrowRight') { + moveScreen(); + } +}); + +// Swipe navigation +let touchStartX = 0; +let touchStartY = 0; + +viewerBody.addEventListener('touchstart', (event) => { + const touch = event.changedTouches[0]; + + touchStartX = touch.screenX; + touchStartY = touch.screenY; +}, { passive: true }); + +viewerBody.addEventListener('touchend', (event) => { + if (!viewer.classList.contains('show')) { + return; + } + + const touch = event.changedTouches[0]; + const deltaX = touch.screenX - touchStartX; + const deltaY = touch.screenY - touchStartY; + + // Ignore predominantly vertical swipes. + if (Math.abs(deltaY) > 100) { + return; + } + + if (deltaX > 200) { + moveScreen(true); + } else if (deltaX < -200) { + moveScreen(); + } +}, { passive: true }); + +viewerImage.addEventListener('load', () => { + screenLoaded(viewerImage); +}); + +function screenLoaded(img) { + viewerDialog.style.removeProperty('width'); + + const maxWidth = viewerBody.offsetWidth; + const maxHeight = window.innerHeight * 0.75; + + let height = Math.min( + (img.height / img.width) * maxWidth, + img.height + ); + + height = Math.min(height, maxHeight); + + const width = (img.width / img.height) * height; + + img.style.height = `${height}px`; + viewerDialog.style.width = `${width}px`; + + viewerBody.classList.remove('loading'); +} + +function moveScreen(reverse = false) { + const source = viewer.dataset.rcCurSource; + const target = getNextScreenId(reverse); + + if (target <= 0) { + return; + } + + viewer.dataset.rcNoResize = 'true'; + + bootstrap.Modal.getInstance(viewer).hide(); + + const targetElement = document.getElementById(`${source}-${target}`); + + if (targetElement) { + targetElement.click(); + } + + viewer.dataset.rcNoResize = 'false'; + document.activeElement.blur(); +} + +function getNextScreenId(reverse = false) { + const source = viewer.dataset.rcCurSource; + const current = Number(viewer.dataset.rcCurIndex); + + const sourceElement = document.getElementById(source); + + if (!sourceElement || !current) { + return 0; + } + + const total = sourceElement.querySelectorAll('li').length; + + if (reverse) { + return current === 1 ? 0 : current - 1; + } + + return current === total ? 0 : current + 1; +} + +function updateNavigation() { + viewer.querySelector('.modal-footer > .nav > .nav-link.previous').classList.toggle('disabled', getNextScreenId(true) === 0); + viewer.querySelector('.modal-footer > .nav > .nav-link.next').classList.toggle('disabled', getNextScreenId() === 0); +} diff --git a/js/viewer.min.js b/js/viewer.min.js new file mode 100644 index 0000000..78b552d --- /dev/null +++ b/js/viewer.min.js @@ -0,0 +1 @@ +let viewer=document.getElementById("viewer"),viewerBody=viewer.querySelector(".modal-body"),viewerImage=viewerBody.querySelector("img"),viewerDialog=viewer.querySelector(".modal-dialog"),viewerCaption=viewer.querySelector(".modal-footer > p"),touchStartX=(viewer.querySelectorAll("button[data-rc-navigation]").forEach(t=>{t.addEventListener("click",e=>{e.preventDefault(),moveScreen("previous"===t.dataset.rcNavigation)})}),viewer.addEventListener("show.bs.modal",e=>{var e=e.relatedTarget,t=e.querySelector("img"),r=t.src,t=t.alt||"",r=(viewerImage.src=r.replace("/thumbs/","/screens/"),viewerImage.alt=t,viewerCaption.textContent=t,e.id);viewer.dataset.rcCurIndex=r.replace(/^screenshots-[a-z]+-[a-z]+-/,""),viewer.dataset.rcCurSource=r.replace(/-\d+$/,""),updateNavigation()}),viewer.addEventListener("hidden.bs.modal",()=>{viewerImage.src="data:,",viewerImage.alt="",viewerImage.style.removeProperty("height"),"true"!==viewer.dataset.rcNoResize&&viewerDialog.style.removeProperty("width"),viewerCaption.textContent="",viewer.dataset.rcCurIndex="",viewer.dataset.rcCurSource="",viewerBody.classList.add("loading")}),document.addEventListener("keyup",e=>{viewer.classList.contains("show")&&("ArrowLeft"===e.key?moveScreen(!0):"ArrowRight"===e.key&&moveScreen())}),0),touchStartY=0;function screenLoaded(e){viewerDialog.style.removeProperty("width");var t=viewerBody.offsetWidth,r=.75*window.innerHeight,t=Math.min(e.height/e.width*t,e.height),t=Math.min(t,r),r=e.width/e.height*t;e.style.height=t+"px",viewerDialog.style.width=r+"px",viewerBody.classList.remove("loading")}function moveScreen(e=!1){var t=viewer.dataset.rcCurSource,e=getNextScreenId(e);e<=0||(viewer.dataset.rcNoResize="true",bootstrap.Modal.getInstance(viewer).hide(),(t=document.getElementById(t+"-"+e))&&t.click(),viewer.dataset.rcNoResize="false",document.activeElement.blur())}function getNextScreenId(e=!1){var t=viewer.dataset.rcCurSource,r=Number(viewer.dataset.rcCurIndex),t=document.getElementById(t);return t&&r?(t=t.querySelectorAll("li").length,e?1===r?0:r-1:r===t?0:r+1):0}function updateNavigation(){viewer.querySelector(".modal-footer > .nav > .nav-link.previous").classList.toggle("disabled",0===getNextScreenId(!0)),viewer.querySelector(".modal-footer > .nav > .nav-link.next").classList.toggle("disabled",0===getNextScreenId())}viewerBody.addEventListener("touchstart",e=>{e=e.changedTouches[0];touchStartX=e.screenX,touchStartY=e.screenY},{passive:!0}),viewerBody.addEventListener("touchend",e=>{var t;viewer.classList.contains("show")&&(t=(e=e.changedTouches[0]).screenX-touchStartX,e=e.screenY-touchStartY,100{screenLoaded(viewerImage)}); \ No newline at end of file diff --git a/package.json b/package.json index 4897939..f05d0fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "dependencies": { "less": "^4.4.0", - "less-plugin-clean-css": "^1.6.0" + "less-plugin-clean-css": "^1.6.0", + "uglify-js": "^3.19.3" } } diff --git a/screens/index.html b/screens/index.html index d05c37d..21b5b2b 100644 --- a/screens/index.html +++ b/screens/index.html @@ -2,6 +2,8 @@ layout: default title: Roundcube Webmail Screenshots navtitle: Screenshots +scripts: + - viewer ---

Impressions of Roundcube's User Interface

@@ -40,146 +42,4 @@

{{ params.name }} Note: The following plugins, distributed with Roundcube, were active when the screenshots where taken: acl, archive, enigma, help, managesieve, markasjunk, password, and vcard_attachments.

-
- - \ No newline at end of file +{% include viewer.html %} diff --git a/sitemap.xml b/sitemap.xml index 8d5c207..a18beef 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -5,7 +5,7 @@ {% for page in site.pages %} {% if page.layout != 'nil' %} - {{site.hostname }}{{ page.url | remove:'index.html' }} + {{site.url}}{{ page.url | remove:'index.html' }} {% if page.lastmod != nil %} {{ page.lastmod | date_to_xmlschema }} {% endif %} @@ -15,7 +15,7 @@ {% endfor %} {% for post in site.posts %} - {{site.hostname }}{{ post.url | remove:'index.html' }} + {{site.url}}{{ post.url | remove:'index.html' }} {% if post.lastmod == nil %} {{ post.date | date_to_xmlschema }} {% else %} diff --git a/styles/styles.less b/styles/styles.less index fb3e1d3..1ef8820 100644 --- a/styles/styles.less +++ b/styles/styles.less @@ -27,7 +27,7 @@ @color-dark-footer: lighten(@color-footer, 30%); :root { - --bs-font-sans-serif: -apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; + --bs-font-sans-serif: -apple-system,"Segoe UI",roboto,"Helvetica Neue",arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; --rc-color-primary: @color-primary; --rc-color-header-background: @color-header-background; --rc-color-banner-background: @color-banner-background; @@ -74,8 +74,8 @@ html[data-bs-theme="dark"] { background-color: var(--rc-color-header-background) !important; } -a:not(.btn):not(.nav-link):not(.dropdown-item), -.btn-link { +a:not(.btn, .nav-link, .dropdown-item), +a.btn-link { text-decoration: none; &.anchor { @@ -207,7 +207,7 @@ a.anchor { text-decoration: none !important; } - &:after { + &::after { font: var(--fa-font-solid); content: @fa-var-link; } @@ -217,7 +217,7 @@ a.anchor { } } -a.copy-link { +button.copy-link { color: var(--bs-secondary-color); font-size: .75rem; display: inline-block; @@ -229,29 +229,29 @@ a.copy-link { text-decoration: none !important; } - &:after { + &::after { font: var(--fa-font-regular); content: @fa-var-clipboard; } - &.complete:after { + &.complete::after { font: var(--fa-font-solid); content: @fa-var-check; color: var(--rc-color-success); } - &.error:after { + &.error::after { font: var(--fa-font-solid); content: @fa-var-triangle-exclamation; color: var(--rc-color-error); } } -a.copy-popover-link { +button.copy-popover-link { color: var(--bs-body-color); text-decoration: none !important; - &.checksum:after { + &.checksum::after { font: var(--fa-font-solid); content: @fa-var-file-shield; } @@ -329,11 +329,13 @@ a.mail-list-link::before { font: var(--fa-font-regular); content: @fa-var-envelope; } + /* a.asc-link::after { content: @fa-var-key; } */ + a.irc-link::after { content: @fa-var-comments; } @@ -426,10 +428,14 @@ div.color-modes > ul.dropdown-menu { .modal-arrow { position: absolute; + display: block; top: 0; bottom: 0; height: 100%; width: 30%; + cursor: default; + background: none; + border: 0; &.right-arrow { right: 0; diff --git a/styles/styles.min.css b/styles/styles.min.css index 89dc5e1..04e183e 100644 --- a/styles/styles.min.css +++ b/styles/styles.min.css @@ -1 +1 @@ -:root{--bs-font-sans-serif:-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--rc-color-primary:#37beff;--rc-color-header-background:#212529;--rc-color-banner-background:rgba(189, 234, 255, 0.5);--rc-color-background-alt:rgba(0, 0, 0, 0.05);--rc-color-download-bg:#1eb6ff;--rc-color-download-border:#13b2ff;--rc-color-thumbnail-background:rgba(0, 0, 0, 0.05);--rc-color-footer:#495057;--rc-color-footer-hover:#262a2d;--rc-color-success:#3fb950;--rc-color-error:#f85149}html[data-bs-theme=dark]{--rc-color-primary:#006a9d;--rc-color-header-background:#000;--rc-color-banner-background:rgba(0, 35, 52, 0.5);--rc-color-background-alt:rgba(255, 255, 255, 0.05);--rc-color-download-bg:#005984;--rc-color-download-border:#005279;--rc-color-thumbnail-background:rgba(255, 255, 255, 0.05);--rc-color-footer:#949da5;--rc-color-footer-hover:#bdc3c8}.navbar{--bs-navbar-toggler-focus-width:0;--bs-navbar-toggler-border-color:transparent;--bs-navbar-toggler-padding-x:0}.table{--bs-table-striped-bg:var(--rc-color-background-alt)}.bg-badge,.bg-quote{background-color:var(--rc-color-background-alt)!important}.bg-header,.bg-navbar,.modal-footer.bg-footer{background-color:var(--rc-color-header-background)!important}.btn-link,a:not(.btn):not(.nav-link):not(.dropdown-item){text-decoration:none}.btn-link.anchor,a:not(.btn):not(.nav-link):not(.dropdown-item).anchor{top:-6rem}.btn-link:hover,a:not(.btn):not(.nav-link):not(.dropdown-item):hover{text-decoration:underline}.card-header{padding:.75rem 1.25rem}.card-body{padding:1.25rem}.list-group-item{padding:.75rem 1.25rem}#banner{margin-top:66px;background-color:var(--rc-color-banner-background)}#banner>.container>.grid{display:grid;grid-template-columns:auto;grid-template-rows:auto}#banner>.container>.grid>.logo{grid-column:1;grid-row:1}#banner>.container>.grid>.image{grid-column:1;grid-row:2}#banner>.container>.grid>.download{grid-column:1;grid-row:3}@media (min-width:768px){#banner>.container>.grid{grid-template-columns:1fr 1.5fr}#banner>.container>.grid>.logo{grid-column:1/3;grid-row:1}#banner>.container>.grid>.image{grid-column:2;grid-row:2}#banner>.container>.grid>.download{grid-column:1;grid-row:2}}@media (min-width:992px){#banner>.container>.grid{grid-template-columns:1fr 1fr;grid-template-rows:auto 1fr}#banner>.container>.grid>.logo{grid-column:1;grid-row:1}#banner>.container>.grid>.image{grid-column:2;grid-row:1/3}#banner>.container>.grid>.download{grid-column:1;grid-row:2}}main{padding-top:6rem}main .h1,main h1{font-size:2.2rem;margin-bottom:1rem}main .h2,main h2{font-size:1.8rem;margin-top:1.5rem;font-weight:700}main .h3,main h3{font-size:1.4rem;margin-top:1rem}blockquote{padding:.6rem 1rem;background-color:var(--rc-color-background-alt)}blockquote>p{margin:0}code.block{white-space:pre}a.anchor{color:var(--bs-secondary-color);font-size:.75rem;vertical-align:middle;opacity:0}a.anchor:hover{text-decoration:none!important}a.anchor:after{font:var(--fa-font-solid);content:"\f0c1"}:hover>a.anchor{opacity:1}a.copy-link{color:var(--bs-secondary-color);font-size:.75rem;display:inline-block;width:1.25em;text-align:center;margin-left:.5rem}a.copy-link:hover{text-decoration:none!important}a.copy-link:after{font:var(--fa-font-regular);content:"\f328"}a.copy-link.complete:after{font:var(--fa-font-solid);content:"\f00c";color:var(--rc-color-success)}a.copy-link.error:after{font:var(--fa-font-solid);content:"\f071";color:var(--rc-color-error)}a.copy-popover-link{color:var(--bs-body-color);text-decoration:none!important}a.copy-popover-link.checksum:after{font:var(--fa-font-solid);content:"\e4f0"}div.popover.copy-popover>div.popover-body{padding:0 var(--bs-popover-body-padding-x);display:flex}h1,h2,h3,p{scroll-margin-top:80px}.artical-list>.artical:nth-of-type(odd),ul.ul-striped li:nth-of-type(odd){background-color:var(--rc-color-background-alt)}.artical-list+nav{margin-top:2rem}footer ul,ul.screens{list-style:none}.artical>div.snippet>p{display:inline}footer a{font-weight:500;color:var(--rc-color-footer)}footer a:focus,footer a:hover{color:var(--rc-color-footer-hover)}.rc-icon::after,.rc-icon::before{font:var(--fa-font-solid)}.rc-icon::before{margin-right:.25rem}.rc-icon::after{margin-left:.25rem}a.back-link::before{content:"\f100"}a.read-link::after{content:"\f101"}a.external-link::after{content:"\f35d"}a.rss-link::after{content:"\f09e"}a.mail-list-link::before{font:var(--fa-font-regular);content:"\f0e0"}a.irc-link::after{content:"\f086"}a.dark-mode::before{content:"\f186"}a.light-mode::before{content:"\f185"}a.auto-mode::before{content:"\f72b"}a.current-theme::after{content:"\f00c"}.btn-rc-download{color:var(--bs-white)!important;background-color:var(--rc-color-primary);border-color:var(--rc-color-primary)}.btn-rc-download:hover{color:var(--bs-white);background-color:var(--rc-color-download-bg);border-color:var(--rc-color-download-border)}.btn-rc-download::before{margin-right:0;content:"\f019"}.btn-rc-download>span{margin-left:.5rem}.btn-rc-download.btn-sm>span{display:none}@media (min-width:992px){.btn-rc-download.btn-sm>span{display:inline}}div.color-modes>ul.dropdown-menu{min-width:auto}div.color-modes>ul.dropdown-menu>li>a{font-size:.8rem}div.color-modes>ul.dropdown-menu>li>a::before{text-align:center;width:1.25em;margin-right:.5em;display:inline-block}div.color-modes>ul.dropdown-menu>li>a::after{margin-left:.75em}.img-thumbnail>a{width:140px;background-color:var(--rc-color-thumbnail-background)}#viewer .modal-body{user-select:none}#viewer .modal-body>img{position:relative}#viewer .modal-body .modal-loading span{font-size:10em;opacity:.5}#viewer .modal-body .modal-arrow{position:absolute;top:0;bottom:0;height:100%;width:30%}#viewer .modal-body .modal-arrow.right-arrow{right:0}#viewer .modal-body.loading>img{display:none}#viewer .modal-body:not(.loading)>.modal-loading{display:none}#viewer .modal-footer{color:var(--bs-white)}#viewer .modal-footer .nav{user-select:none}#viewer .modal-footer .nav>.nav-link:not(.disabled){color:var(--bs-white)}table.download-table tbody td,table.download-table tbody th{vertical-align:middle}table.download-table td.link{width:60px}table.download-table td.size{width:80px}table.download-table td.checksum{font-size:.9em;width:40px}@media (min-width:768px){table.download-table th.package{width:255px}table.download-table td.checksum{width:auto}}@media (min-width:992px){table.download-table td.link{width:170px}table.download-table td.size{width:120px}}@media (min-width:768px){table.download-table td.checksum>span>span.copy-target{width:254px}}@media (min-width:992px){table.download-table td.checksum>span>span.copy-target{width:344px}}@media (min-width:1200px){table.download-table td.checksum>span>span.copy-target{width:auto}} \ No newline at end of file +:root{--bs-font-sans-serif:-apple-system,"Segoe UI",roboto,"Helvetica Neue",arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--rc-color-primary:#37beff;--rc-color-header-background:#212529;--rc-color-banner-background:rgba(189, 234, 255, 0.5);--rc-color-background-alt:rgba(0, 0, 0, 0.05);--rc-color-download-bg:#1eb6ff;--rc-color-download-border:#13b2ff;--rc-color-thumbnail-background:rgba(0, 0, 0, 0.05);--rc-color-footer:#495057;--rc-color-footer-hover:#262a2d;--rc-color-success:#3fb950;--rc-color-error:#f85149}html[data-bs-theme=dark]{--rc-color-primary:#006a9d;--rc-color-header-background:#000;--rc-color-banner-background:rgba(0, 35, 52, 0.5);--rc-color-background-alt:rgba(255, 255, 255, 0.05);--rc-color-download-bg:#005984;--rc-color-download-border:#005279;--rc-color-thumbnail-background:rgba(255, 255, 255, 0.05);--rc-color-footer:#949da5;--rc-color-footer-hover:#bdc3c8}.navbar{--bs-navbar-toggler-focus-width:0;--bs-navbar-toggler-border-color:transparent;--bs-navbar-toggler-padding-x:0}.table{--bs-table-striped-bg:var(--rc-color-background-alt)}.bg-badge,.bg-quote{background-color:var(--rc-color-background-alt)!important}.bg-header,.bg-navbar,.modal-footer.bg-footer{background-color:var(--rc-color-header-background)!important}a.btn-link,a:not(.btn,.nav-link,.dropdown-item){text-decoration:none}a.btn-link.anchor,a:not(.btn,.nav-link,.dropdown-item).anchor{top:-6rem}a.btn-link:hover,a:not(.btn,.nav-link,.dropdown-item):hover{text-decoration:underline}.card-header{padding:.75rem 1.25rem}.card-body{padding:1.25rem}.list-group-item{padding:.75rem 1.25rem}#banner{margin-top:66px;background-color:var(--rc-color-banner-background)}#banner>.container>.grid{display:grid;grid-template-columns:auto;grid-template-rows:auto}#banner>.container>.grid>.logo{grid-column:1;grid-row:1}#banner>.container>.grid>.image{grid-column:1;grid-row:2}#banner>.container>.grid>.download{grid-column:1;grid-row:3}@media (min-width:768px){#banner>.container>.grid{grid-template-columns:1fr 1.5fr}#banner>.container>.grid>.logo{grid-column:1/3;grid-row:1}#banner>.container>.grid>.image{grid-column:2;grid-row:2}#banner>.container>.grid>.download{grid-column:1;grid-row:2}}@media (min-width:992px){#banner>.container>.grid{grid-template-columns:1fr 1fr;grid-template-rows:auto 1fr}#banner>.container>.grid>.logo{grid-column:1;grid-row:1}#banner>.container>.grid>.image{grid-column:2;grid-row:1/3}#banner>.container>.grid>.download{grid-column:1;grid-row:2}}main{padding-top:6rem}main .h1,main h1{font-size:2.2rem;margin-bottom:1rem}main .h2,main h2{font-size:1.8rem;margin-top:1.5rem;font-weight:700}main .h3,main h3{font-size:1.4rem;margin-top:1rem}blockquote{padding:.6rem 1rem;background-color:var(--rc-color-background-alt)}blockquote>p{margin:0}code.block{white-space:pre}a.anchor{color:var(--bs-secondary-color);font-size:.75rem;vertical-align:middle;opacity:0}a.anchor:hover{text-decoration:none!important}a.anchor::after{font:var(--fa-font-solid);content:"\f0c1"}:hover>a.anchor{opacity:1}button.copy-link{color:var(--bs-secondary-color);font-size:.75rem;display:inline-block;width:1.25em;text-align:center;margin-left:.5rem}button.copy-link:hover{text-decoration:none!important}button.copy-link::after{font:var(--fa-font-regular);content:"\f328"}button.copy-link.complete::after{font:var(--fa-font-solid);content:"\f00c";color:var(--rc-color-success)}button.copy-link.error::after{font:var(--fa-font-solid);content:"\f071";color:var(--rc-color-error)}button.copy-popover-link{color:var(--bs-body-color);text-decoration:none!important}button.copy-popover-link.checksum::after{font:var(--fa-font-solid);content:"\e4f0"}div.popover.copy-popover>div.popover-body{padding:0 var(--bs-popover-body-padding-x);display:flex}h1,h2,h3,p{scroll-margin-top:80px}.artical-list>.artical:nth-of-type(odd),ul.ul-striped li:nth-of-type(odd){background-color:var(--rc-color-background-alt)}.artical-list+nav{margin-top:2rem}footer ul,ul.screens{list-style:none}.artical>div.snippet>p{display:inline}footer a{font-weight:500;color:var(--rc-color-footer)}footer a:focus,footer a:hover{color:var(--rc-color-footer-hover)}.rc-icon::after,.rc-icon::before{font:var(--fa-font-solid)}.rc-icon::before{margin-right:.25rem}.rc-icon::after{margin-left:.25rem}a.back-link::before{content:"\f100"}a.read-link::after{content:"\f101"}a.external-link::after{content:"\f35d"}a.rss-link::after{content:"\f09e"}a.mail-list-link::before{font:var(--fa-font-regular);content:"\f0e0"}a.irc-link::after{content:"\f086"}a.dark-mode::before{content:"\f186"}a.light-mode::before{content:"\f185"}a.auto-mode::before{content:"\f72b"}a.current-theme::after{content:"\f00c"}.btn-rc-download{color:var(--bs-white)!important;background-color:var(--rc-color-primary);border-color:var(--rc-color-primary)}.btn-rc-download:hover{color:var(--bs-white);background-color:var(--rc-color-download-bg);border-color:var(--rc-color-download-border)}.btn-rc-download::before{margin-right:0;content:"\f019"}.btn-rc-download>span{margin-left:.5rem}.btn-rc-download.btn-sm>span{display:none}@media (min-width:992px){.btn-rc-download.btn-sm>span{display:inline}}div.color-modes>ul.dropdown-menu{min-width:auto}div.color-modes>ul.dropdown-menu>li>a{font-size:.8rem}div.color-modes>ul.dropdown-menu>li>a::before{text-align:center;width:1.25em;margin-right:.5em;display:inline-block}div.color-modes>ul.dropdown-menu>li>a::after{margin-left:.75em}.img-thumbnail>a{width:140px;background-color:var(--rc-color-thumbnail-background)}#viewer .modal-body{user-select:none}#viewer .modal-body>img{position:relative}#viewer .modal-body .modal-loading span{font-size:10em;opacity:.5}#viewer .modal-body .modal-arrow{position:absolute;display:block;top:0;bottom:0;height:100%;width:30%;cursor:default;background:0 0;border:0}#viewer .modal-body .modal-arrow.right-arrow{right:0}#viewer .modal-body.loading>img{display:none}#viewer .modal-body:not(.loading)>.modal-loading{display:none}#viewer .modal-footer{color:var(--bs-white)}#viewer .modal-footer .nav{user-select:none}#viewer .modal-footer .nav>.nav-link:not(.disabled){color:var(--bs-white)}table.download-table tbody td,table.download-table tbody th{vertical-align:middle}table.download-table td.link{width:60px}table.download-table td.size{width:80px}table.download-table td.checksum{font-size:.9em;width:40px}@media (min-width:768px){table.download-table th.package{width:255px}table.download-table td.checksum{width:auto}}@media (min-width:992px){table.download-table td.link{width:170px}table.download-table td.size{width:120px}}@media (min-width:768px){table.download-table td.checksum>span>span.copy-target{width:254px}}@media (min-width:992px){table.download-table td.checksum>span>span.copy-target{width:344px}}@media (min-width:1200px){table.download-table td.checksum>span>span.copy-target{width:auto}} \ No newline at end of file diff --git a/y_key_f7f60b3952eaecd0.html b/y_key_f7f60b3952eaecd0.html index 9cae5be..1573ff3 100644 --- a/y_key_f7f60b3952eaecd0.html +++ b/y_key_f7f60b3952eaecd0.html @@ -1,9 +1,9 @@ - - - - - - + + + +roundcube + + - - \ No newline at end of file + + From 6810494fa5bb2d35cbcdcb6508e147795c3bf7f1 Mon Sep 17 00:00:00 2001 From: PhilW Date: Fri, 9 Oct 2026 15:04:29 +0100 Subject: [PATCH 2/2] add github action to lint js/less/html --- .eslintrc.js | 70 +++++++++++++++++++++++++++++++++ .github/workflows/lintcheck.yml | 45 +++++++++++++++++++++ .htmlhintrc | 26 ++++++++++++ _config.yml | 1 + package.json | 9 +++++ stylelint.config.mjs | 8 ++++ 6 files changed, 159 insertions(+) create mode 100644 .eslintrc.js create mode 100644 .github/workflows/lintcheck.yml create mode 100644 .htmlhintrc create mode 100644 stylelint.config.mjs diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..c8758cb --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,70 @@ +module.exports = { + env: { + browser: true, + es6: true, + }, + extends: [ + 'airbnb-base', + 'plugin:unicorn/recommended', + ], + parserOptions: { + ecmaVersion: '2022', + sourceType: 'module', + }, + ignorePatterns: [ + '!.*', + '*.min.js', + ], + rules: { + 'brace-style': ['error', '1tbs'], + 'comma-dangle': ['error', { + arrays: 'always-multiline', + exports: 'always-multiline', + functions: 'never', + imports: 'always-multiline', + objects: 'always-multiline', + }], + curly: ['error', 'all'], + indent: ['error', 4, { + SwitchCase: 1, + }], + 'linebreak-style': ['error', 'unix'], + 'no-multi-spaces': ['error', { + exceptions: { + Property: true, + VariableDeclarator: true, + }, + }], + 'object-shorthand': ['error', 'never'], + 'padding-line-between-statements': ['error', { + blankLine: 'always', + next: ['continue', 'break', 'export', 'return', 'throw'], + prev: '*', + }], + 'spaced-comment': ['error', 'always', { + block: { + balanced: true, + exceptions: ['*'], + markers: ['!'], + }, + line: { + exceptions: ['-', '+'], + markers: ['/'], + }, + }], + strict: 'off', + 'wrap-iife': ['error', 'inside'], + 'func-names': 'off', + 'no-param-reassign': 'off', + 'no-use-before-define': 'off', + 'prefer-destructuring': 'off', + 'unicorn/prefer-module': 'off', + 'unicorn/prefer-query-selector': 'off', + 'unicorn/no-array-for-each': 'off', + 'unicorn/prevent-abbreviations': 'off', + }, + reportUnusedDisableDirectives: true, + globals: { + bootstrap: true, + }, +}; diff --git a/.github/workflows/lintcheck.yml b/.github/workflows/lintcheck.yml new file mode 100644 index 0000000..6fc4335 --- /dev/null +++ b/.github/workflows/lintcheck.yml @@ -0,0 +1,45 @@ +name: LintCheck + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + validate: + if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name)" + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version: 22 + + - name: Install Node dependencies + run: npm install --omit=optional + + - name: Lint JavaScript + run: npx eslint --ext .js . + + - name: Lint Less + run: npx stylelint "./styles/styles.less" + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4" + bundler-cache: true + + - name: Build site + run: bundle exec jekyll build + + - name: Lint HTML + # Find generated HTML, excluding news pages as then contain HTML4 + run: find _site -name '*.html' ! -path '_site/news/*/*.html' -print0 | xargs -0 -r npx htmlhint -c .htmlhintrc.js + diff --git a/.htmlhintrc b/.htmlhintrc new file mode 100644 index 0000000..c42a29a --- /dev/null +++ b/.htmlhintrc @@ -0,0 +1,26 @@ +{ + "doctype-first": true, + "doctype-html5": true, + + "tagname-lowercase": true, + "attr-lowercase": true, + "attr-value-double-quotes": true, + "attr-no-duplication": true, + "attr-value-no-duplication": true, + + "tag-pair": true, + "empty-tag-not-self-closed": true, + "tag-no-obsolete": true, + + "spec-char-escape": true, + "src-not-empty": true, + "alt-require": true, + "id-unique": true, + + "html-lang-require": true, + "title-require": true, + + "button-type-require": true, + "input-requires-label": true, + "frame-title-require": true, +} diff --git a/_config.yml b/_config.yml index 2747711..2d6f445 100644 --- a/_config.yml +++ b/_config.yml @@ -17,6 +17,7 @@ exclude: - package-lock.json - README.md - stylelint.config.mjs + - vendor libs: css: url: '/styles/styles.min.css?v=9bf32423' diff --git a/package.json b/package.json index f05d0fe..df29d6d 100644 --- a/package.json +++ b/package.json @@ -3,5 +3,14 @@ "less": "^4.4.0", "less-plugin-clean-css": "^1.6.0", "uglify-js": "^3.19.3" + }, + "devDependencies": { + "stylelint": "^17.16.0", + "stylelint-config-standard-less": "^4.1.0", + "eslint": "^8.57.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-unicorn": "^54.0.0", + "htmlhint": "^1.1.4" } } diff --git a/stylelint.config.mjs b/stylelint.config.mjs new file mode 100644 index 0000000..1b7472e --- /dev/null +++ b/stylelint.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('stylelint').Config} */ +export default { + extends: ["stylelint-config-standard-less"], + + rules: { + "no-descending-specificity": null + } +};