From 5b376cc116ec4272db59a450d37483ad6c7fb690 Mon Sep 17 00:00:00 2001 From: ZhenShuo Leo <98386542+ZhenShuo2021@users.noreply.github.com> Date: Mon, 21 Jul 2025 21:11:22 +0800 Subject: [PATCH] feat: add a11y panel --- assets/css/compiled/main.css | 150 ++++++++++++++++++++---- assets/css/components/a11y.css | 77 +++++++++++++ assets/css/main.css | 1 + assets/js/background-blur.js | 176 ++++++++++++++++++++++++----- layouts/partials/header/basic.html | 100 ++++++++++++---- 5 files changed, 433 insertions(+), 71 deletions(-) create mode 100644 assets/css/components/a11y.css diff --git a/assets/css/compiled/main.css b/assets/css/compiled/main.css index d9788bf9..e9f48c51 100644 --- a/assets/css/compiled/main.css +++ b/assets/css/compiled/main.css @@ -174,6 +174,69 @@ body.zen-mode-enable { .chroma .gl { text-decoration-line: underline; } +.a11y-panel-enter-active { + animation: slideInFromTop 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; +} +.a11y-panel-leave-active { + animation: slideOutToTop 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; +} +.ios-toggle { + position: relative; + width: 42px; + height: 24px; + background: #e5e5e5; + border-radius: 12px; + cursor: pointer; + transition: background-color 0.3s ease; + pointer-events: auto; +} +.ios-toggle input { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; + cursor: pointer; + z-index: 1; +} +.ios-toggle::after { + content: ""; + position: absolute; + top: 2px; + left: 2px; + width: 20px; + height: 20px; + background: white; + border-radius: 50%; + transition: transform 0.3s ease, background-color 0.3s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + z-index: 0; +} +.ios-toggle input:checked + .toggle-track::after { + transform: translateX(18px); +} +.ios-toggle input:checked + .toggle-track { + background: rgba(var(--color-primary-500), 1); +} +.ios-toggle input:checked ~ .ios-toggle-ball { + transform: translateX(18px); +} +.ios-toggle.is-checked { + background: rgba(var(--color-primary-500), 1); +} +.ios-toggle:has(input:checked) { + background: rgba(var(--color-primary-500), 1); +} +.ios-toggle:has(input:checked)::after { + transform: translateX(18px); +} +.dark .ios-toggle { + background: #404040; +} +.dark .ios-toggle::after { + background: #f5f5f5; +} @layer theme, base, components, utilities; @layer theme { :root, :host { @@ -428,6 +491,9 @@ body.zen-mode-enable { .top-0 { top: calc(var(--spacing) * 0); } + .top-1\/2 { + top: calc(1/2 * 100%); + } .top-20 { top: calc(var(--spacing) * 20); } @@ -446,6 +512,9 @@ body.zen-mode-enable { .left-0 { left: calc(var(--spacing) * 0); } + .left-1\/2 { + left: calc(1/2 * 100%); + } .left-\[calc\(max\(-50vw\,-800px\)\+50\%\)\] { left: calc(max(-50vw, -800px) + 50%); } @@ -1245,6 +1314,9 @@ body.zen-mode-enable { .h-3 { height: calc(var(--spacing) * 3); } + .h-5 { + height: calc(var(--spacing) * 5); + } .h-6 { height: calc(var(--spacing) * 6); } @@ -1320,6 +1392,9 @@ body.zen-mode-enable { .w-3 { width: calc(var(--spacing) * 3); } + .w-5 { + width: calc(var(--spacing) * 5); + } .w-6 { width: calc(var(--spacing) * 6); } @@ -1335,6 +1410,9 @@ body.zen-mode-enable { .w-36 { width: calc(var(--spacing) * 36); } + .w-80 { + width: calc(var(--spacing) * 80); + } .w-\[15\%\] { width: 15%; } @@ -1434,6 +1512,10 @@ body.zen-mode-enable { .border-collapse { border-collapse: collapse; } + .-translate-x-1\/2 { + --tw-translate-x: calc(calc(1/2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } .-translate-x-full { --tw-translate-x: -100%; translate: var(--tw-translate-x) var(--tw-translate-y); @@ -1442,6 +1524,10 @@ body.zen-mode-enable { --tw-translate-x: 100%; translate: var(--tw-translate-x) var(--tw-translate-y); } + .-translate-y-1\/2 { + --tw-translate-y: calc(calc(1/2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } .-translate-y-8 { --tw-translate-y: calc(var(--spacing) * -8); translate: var(--tw-translate-x) var(--tw-translate-y); @@ -1533,6 +1619,13 @@ body.zen-mode-enable { margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse))); } } + .space-y-5 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse))); + } + } .space-y-10 { :where(& > :not(:last-child)) { --tw-space-y-reverse: 0; @@ -1755,6 +1848,9 @@ body.zen-mode-enable { .bg-neutral { background-color: rgba(var(--color-neutral), 1); } + .bg-neutral-50 { + background-color: rgba(var(--color-neutral-50), 1); + } .bg-neutral-100 { background-color: rgba(var(--color-neutral-100), 1); } @@ -1956,6 +2052,9 @@ body.zen-mode-enable { .pr-0 { padding-right: calc(var(--spacing) * 0); } + .pr-8 { + padding-right: calc(var(--spacing) * 8); + } .pr-\[24px\] { padding-right: 24px; } @@ -2563,6 +2662,13 @@ body.zen-mode-enable { } } } + .hover\:text-neutral-700 { + &:hover { + @media (hover: hover) { + color: rgba(var(--color-neutral-700), 1); + } + } + } .hover\:text-primary-400 { &:hover { @media (hover: hover) { @@ -2661,6 +2767,11 @@ body.zen-mode-enable { translate: var(--tw-translate-x) var(--tw-translate-y); } } + .focus\:border-primary-500 { + &:focus { + border-color: rgba(var(--color-primary-500), 1); + } + } .focus\:bg-primary-100 { &:focus { background-color: rgba(var(--color-primary-100), 1); @@ -2676,6 +2787,11 @@ body.zen-mode-enable { opacity: 90%; } } + .focus\:ring-primary-500 { + &:focus { + --tw-ring-color: rgba(var(--color-primary-500), 1); + } + } .focus\:outline-2 { &:focus { outline-style: var(--tw-outline-style); @@ -3513,6 +3629,15 @@ body.zen-mode-enable { } } } + .dark\:hover\:text-neutral-200 { + &:is(.dark *) { + &:hover { + @media (hover: hover) { + color: rgba(var(--color-neutral-200), 1); + } + } + } + } .dark\:hover\:text-neutral-800 { &:is(.dark *) { &:hover { @@ -3721,44 +3846,29 @@ a { pre { text-align: left; } +.thumbnail, .thumbnail_card, .thumbnail_card_related, .thumbnail_card_term, .single_hero_basic, .single_hero_background { + background-repeat: no-repeat; + background-size: cover; + background-position: center; +} .thumbnail { min-width: 300px; height: 180px; - background-repeat: no-repeat; - background-size: cover; - background-position: center; } .thumbnail_card { height: 200px; - background-repeat: no-repeat; - background-size: cover; - background-position: center; } .thumbnail_card_related { height: 150px; - background-repeat: no-repeat; - background-size: cover; - background-position: center; } .thumbnail_card_term { height: 150px; - background-repeat: no-repeat; - background-size: cover; - background-position: center; -} -.single_hero_basic { - background-repeat: no-repeat; - background-size: cover; - background-position: center; } .single_hero_round { max-height: 50vh; object-fit: cover; } .single_hero_background { - background-repeat: no-repeat; - background-size: cover; - background-position: center; z-index: -10; } .hero_gradient { diff --git a/assets/css/components/a11y.css b/assets/css/components/a11y.css new file mode 100644 index 00000000..330561ed --- /dev/null +++ b/assets/css/components/a11y.css @@ -0,0 +1,77 @@ +.a11y-panel-enter-active { + animation: slideInFromTop 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; +} + +.a11y-panel-leave-active { + animation: slideOutToTop 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; +} + +.ios-toggle { + position: relative; + width: 42px; + height: 24px; + background: #e5e5e5; + border-radius: 12px; + cursor: pointer; + transition: background-color 0.3s ease; + pointer-events: auto; +} + +.ios-toggle input { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; + cursor: pointer; + z-index: 1; +} + +.ios-toggle::after { + content: ""; + position: absolute; + top: 2px; + left: 2px; + width: 20px; + height: 20px; + background: white; + border-radius: 50%; + transition: + transform 0.3s ease, + background-color 0.3s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + z-index: 0; +} + +.ios-toggle input:checked + .toggle-track::after { + transform: translateX(18px); +} + +.ios-toggle input:checked + .toggle-track { + background: rgba(var(--color-primary-500), 1); +} + +.ios-toggle input:checked ~ .ios-toggle-ball { + transform: translateX(18px); +} + +.ios-toggle.is-checked { + background: rgba(var(--color-primary-500), 1); +} + +.ios-toggle:has(input:checked) { + background: rgba(var(--color-primary-500), 1); +} + +.ios-toggle:has(input:checked)::after { + transform: translateX(18px); +} + +.dark .ios-toggle { + background: #404040; +} + +.dark .ios-toggle::after { + background: #f5f5f5; +} diff --git a/assets/css/main.css b/assets/css/main.css index c1e6e7de..db602162 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -2,6 +2,7 @@ @import "./components/zen-mode.css"; @import "./components/chroma.css"; +@import "./components/a11y.css"; @import "tailwindcss"; @config "../../tailwind.config.js"; diff --git a/assets/js/background-blur.js b/assets/js/background-blur.js index dc8d491b..34686fcd 100644 --- a/assets/js/background-blur.js +++ b/assets/js/background-blur.js @@ -1,8 +1,20 @@ (() => { - const scripts = document.querySelectorAll('script[data-target-id]'); - - const isA11yMode = () => { - return localStorage.getItem("a11yMode") === "true"; + const scripts = document.querySelectorAll("script[data-target-id]"); + + const getA11ySettings = () => { + const settings = localStorage.getItem("a11ySettings"); + return settings + ? JSON.parse(settings) + : { + disableBlur: false, + disableImages: false, + fontSize: "16px", + underlineLinks: false, + }; + }; + + const saveA11ySettings = (settings) => { + localStorage.setItem("a11ySettings", JSON.stringify(settings)); }; const getScrollOpacity = (scrollDivisor) => { @@ -12,9 +24,10 @@ const applyBlurState = (blurElement, scrollDivisor, targetId, imageElement, imageUrl) => { if (!blurElement) return; + const settings = getA11ySettings(); const isMenuBlur = targetId === "menu-blur"; - if (isA11yMode()) { + if (settings.disableBlur) { blurElement.setAttribute("aria-hidden", "true"); if (!isMenuBlur) { blurElement.style.display = "none"; @@ -23,30 +36,139 @@ blurElement.style.display = ""; blurElement.style.opacity = getScrollOpacity(scrollDivisor); } - // Hide image in a11y mode - if (imageElement) { - imageElement.style.display = "none"; - } } else { blurElement.style.display = ""; blurElement.style.opacity = getScrollOpacity(scrollDivisor); blurElement.removeAttribute("aria-hidden"); - // Show image in normal mode - if (imageElement) { + } + + if (imageElement) { + if (settings.disableImages) { + imageElement.style.display = "none"; + } else { imageElement.style.display = ""; - // Re-apply image source if it was removed - if (imageUrl && !imageElement.getAttribute('src')) { - imageElement.setAttribute('src', imageUrl); + if (imageUrl && !imageElement.getAttribute("src")) { + imageElement.setAttribute("src", imageUrl); } } } }; - scripts.forEach(script => { + const applyFontSize = (fontSizePx) => { + document.documentElement.style.fontSize = fontSizePx; + }; + + const applyUnderlineLinks = (enabled) => { + let styleElement = document.getElementById("a11y-underline-links"); + if (enabled) { + if (!styleElement) { + styleElement = document.createElement("style"); + styleElement.id = "a11y-underline-links"; + styleElement.textContent = "a { text-decoration: underline !important; }"; + document.head.appendChild(styleElement); + } + } else { + if (styleElement) { + styleElement.remove(); + } + } + }; + + const applyA11ySettings = () => { + const settings = getA11ySettings(); + + scripts.forEach((script) => { + const targetId = script.getAttribute("data-target-id"); + const scrollDivisor = Number(script.getAttribute("data-scroll-divisor") || 300); + const imageId = script.getAttribute("data-image-id"); + const imageUrl = script.getAttribute("data-image-url"); + + const blurElement = document.getElementById(targetId); + const imageElement = imageId ? document.getElementById(imageId) : null; + applyBlurState(blurElement, scrollDivisor, targetId, imageElement, imageUrl); + }); + + applyFontSize(settings.fontSize); + applyUnderlineLinks(settings.underlineLinks); + }; + + const updateA11ySetting = (key, value) => { + const settings = getA11ySettings(); + settings[key] = value; + saveA11ySettings(settings); + applyA11ySettings(); + }; + + const toggleA11yPanel = () => { + const panel = document.getElementById("a11y-panel"); + const overlay = document.getElementById("a11y-overlay"); + const button = document.getElementById("a11y-toggle"); + + if (overlay.classList.contains("hidden")) { + overlay.classList.remove("hidden"); + panel.classList.remove("hidden"); + button.setAttribute("aria-pressed", "true"); + button.setAttribute("aria-expanded", "true"); + } else { + overlay.classList.add("hidden"); + panel.classList.add("hidden"); + button.setAttribute("aria-pressed", "false"); + button.setAttribute("aria-expanded", "false"); + } + }; + + const initA11yPanel = () => { + const settings = getA11ySettings(); + + document.getElementById("disable-blur").checked = settings.disableBlur; + document.getElementById("disable-images").checked = settings.disableImages; + + const fontSizeSelect = document.getElementById("font-size-select"); + fontSizeSelect.value = settings.fontSize; + + document.getElementById("underline-links").checked = settings.underlineLinks; + + document.getElementById("disable-blur").addEventListener("change", (e) => { + updateA11ySetting("disableBlur", e.target.checked); + }); + + document.getElementById("disable-images").addEventListener("change", (e) => { + updateA11ySetting("disableImages", e.target.checked); + }); + + document.getElementById("font-size-select").addEventListener("change", (e) => { + updateA11ySetting("fontSize", e.target.value); + }); + + document.getElementById("underline-links").addEventListener("change", (e) => { + updateA11ySetting("underlineLinks", e.target.checked); + }); + + document.querySelectorAll(".ios-toggle").forEach((toggle) => { + toggle.addEventListener("click", () => { + const checkbox = toggle.querySelector('input[type="checkbox"]'); + checkbox.checked = !checkbox.checked; + checkbox.dispatchEvent(new Event("change")); + }); + }); + + document.addEventListener("click", (e) => { + const overlay = document.getElementById("a11y-overlay"); + const button = document.getElementById("a11y-toggle"); + if (e.target === overlay) { + overlay.classList.add("hidden"); + document.getElementById("a11y-panel").classList.add("hidden"); + button.setAttribute("aria-pressed", "false"); + button.setAttribute("aria-expanded", "false"); + } + }); + }; + + scripts.forEach((script) => { const targetId = script.getAttribute("data-target-id"); const scrollDivisor = Number(script.getAttribute("data-scroll-divisor") || 300); const imageId = script.getAttribute("data-image-id"); - const imageUrl = script.getAttribute("data-image-url"); // Get image URL from data attribute + const imageUrl = script.getAttribute("data-image-url"); const blurElement = document.getElementById(targetId); const imageElement = imageId ? document.getElementById(imageId) : null; @@ -57,24 +179,20 @@ applyBlurState(blurElement, scrollDivisor, targetId, imageElement, imageUrl); window.addEventListener("scroll", () => { - if (!isA11yMode() || targetId === "menu-blur") { + const settings = getA11ySettings(); + if (!settings.disableBlur || targetId === "menu-blur") { blurElement.style.opacity = getScrollOpacity(scrollDivisor); } }); } }); - window.toggleA11yMode = function() { - localStorage.setItem("a11yMode", String(!isA11yMode())); - scripts.forEach(script => { - const targetId = script.getAttribute("data-target-id"); - const scrollDivisor = Number(script.getAttribute("data-scroll-divisor") || 300); - const imageId = script.getAttribute("data-image-id"); - const imageUrl = script.getAttribute("data-image-url"); // Get image URL from data attribute + window.toggleA11yPanel = toggleA11yPanel; - const blurElement = document.getElementById(targetId); - const imageElement = imageId ? document.getElementById(imageId) : null; - applyBlurState(blurElement, scrollDivisor, targetId, imageElement, imageUrl); - }); - }; + document.addEventListener("DOMContentLoaded", () => { + applyA11ySettings(); + if (document.getElementById("a11y-panel")) { + initA11yPanel(); + } + }); })(); diff --git a/layouts/partials/header/basic.html b/layouts/partials/header/basic.html index 80acaffc..b411cad3 100644 --- a/layouts/partials/header/basic.html +++ b/layouts/partials/header/basic.html @@ -35,16 +35,7 @@ {{ partial "translations.html" . }} {{ if .Site.Params.enableA11y | default false }} - + {{ template "HeaderA11y" . }} {{ end }} {{ if .Site.Params.enableSearch | default false }} @@ -82,18 +73,8 @@ {{ partial "translations.html" . }} - {{ if .Site.Params.enableA11y | default false }} - + {{ template "HeaderA11y" . }} {{ end }} {{ if .Site.Params.enableSearch | default false }} @@ -178,6 +159,79 @@ {{ end }} +{{ define "HeaderA11y" }} +
+ + + + + +
+{{ end }} + {{/* ========== Render HTML ========== */}}