Merge pull request #2365 from ZhenShuo2021/feat/a11y-panel

 Feat: add setting panel for a11y
This commit is contained in:
Nuno C.
2025-08-03 01:37:56 +01:00
committed by GitHub
32 changed files with 703 additions and 140 deletions
+125
View File
@@ -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;
}
@@ -2570,6 +2669,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) {
@@ -2668,6 +2774,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);
@@ -2683,6 +2794,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);
@@ -3520,6 +3636,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 {
+77
View File
@@ -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;
}
+1
View File
@@ -2,6 +2,7 @@
@import "./components/zen-mode.css";
@import "./components/chroma.css";
@import "./components/a11y.css";
@import "tailwindcss";
@config "../../tailwind.config.js";
+172
View File
@@ -0,0 +1,172 @@
const getA11ySettings = () => {
const settings = localStorage.getItem("a11ySettings");
return settings
? JSON.parse(settings)
: {
disableBlur: false,
disableImages: false,
fontSize: "default",
underlineLinks: false,
};
};
const saveA11ySettings = (settings) => {
localStorage.setItem("a11ySettings", JSON.stringify(settings));
};
const applyImageState = (imageElement, imageUrl, disableImages) => {
if (!imageElement) return;
if (disableImages) {
imageElement.style.display = "none";
} else {
imageElement.style.display = "";
if (imageUrl && !imageElement.getAttribute("src")) {
imageElement.setAttribute("src", imageUrl);
}
}
};
const applyFontSize = (fontSizePx) => {
const isDefaultSettings = localStorage.getItem("a11ySettings") === null;
if (!isDefaultSettings && fontSizePx !== "default") {
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();
document.querySelectorAll("script[data-target-id]").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 isMenuBlur = targetId === "menu-blur";
setBackgroundBlur(targetId, scrollDivisor, settings.disableBlur, isMenuBlur);
applyImageState(document.getElementById(imageId), imageUrl, settings.disableImages);
});
applyFontSize(settings.fontSize);
applyUnderlineLinks(settings.underlineLinks);
};
const updateA11ySetting = (key, value) => {
const settings = getA11ySettings();
settings[key] = value;
saveA11ySettings(settings);
applyA11ySettings();
};
const toggleA11yPanel = (prefix = "") => {
const panel = document.getElementById(`${prefix}a11y-panel`);
const overlay = document.getElementById(`${prefix}a11y-overlay`);
const button = document.getElementById(`${prefix}a11y-toggle`);
if (!panel || !overlay || !button) return;
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 = (prefix = "") => {
const settings = getA11ySettings();
const checkboxBlur = document.getElementById(`${prefix}disable-blur`);
const checkboxImages = document.getElementById(`${prefix}disable-images`);
const checkboxUnderline = document.getElementById(`${prefix}underline-links`);
const fontSizeSelect = document.getElementById(`${prefix}font-size-select`);
const toggleButton = document.getElementById(`${prefix}a11y-toggle`);
const closeButton = document.getElementById(`${prefix}a11y-close`);
const overlay = document.getElementById(`${prefix}a11y-overlay`);
if (
!checkboxBlur ||
!checkboxImages ||
!checkboxUnderline ||
!fontSizeSelect ||
!toggleButton ||
!closeButton ||
!overlay
) {
console.warn(`One or more a11y elements not found for prefix: ${prefix}`);
return;
}
checkboxBlur.checked = settings.disableBlur;
checkboxImages.checked = settings.disableImages;
fontSizeSelect.value = settings.fontSize;
checkboxUnderline.checked = settings.underlineLinks;
checkboxBlur.addEventListener("change", (e) => updateA11ySetting("disableBlur", e.target.checked));
checkboxImages.addEventListener("change", (e) => updateA11ySetting("disableImages", e.target.checked));
checkboxUnderline.addEventListener("change", (e) => updateA11ySetting("underlineLinks", e.target.checked));
fontSizeSelect.addEventListener("change", (e) => {
// Remove fontSize from localStorage when default is selected
if (e.target.value === "default") {
const settings = getA11ySettings();
delete settings.fontSize;
saveA11ySettings(settings);
document.documentElement.style.fontSize = "";
} else {
updateA11ySetting("fontSize", e.target.value);
}
});
toggleButton.addEventListener("click", () => toggleA11yPanel(prefix));
closeButton.addEventListener("click", () => toggleA11yPanel(prefix));
overlay.addEventListener("click", (e) => {
if (e.target === overlay) {
toggleA11yPanel(prefix);
}
});
document.querySelectorAll(`.ios-toggle${prefix ? `[id^="${prefix}"]` : ""}`).forEach((toggle) => {
const checkbox = toggle.querySelector('input[type="checkbox"]');
if (!checkbox) return;
const newToggle = toggle.cloneNode(true);
toggle.parentNode.replaceChild(newToggle, toggle);
newToggle.addEventListener("click", () => {
const newCheckbox = newToggle.querySelector('input[type="checkbox"]');
if (newCheckbox) {
newCheckbox.checked = !newCheckbox.checked;
newCheckbox.dispatchEvent(new Event("change", { bubbles: true }));
}
});
});
};
document.querySelectorAll("script[data-target-id]").forEach((script) => {
const imageId = script.getAttribute("data-image-id");
const imageUrl = script.getAttribute("data-image-url");
const settings = getA11ySettings();
applyImageState(document.getElementById(imageId), imageUrl, settings.disableImages);
});
document.addEventListener("DOMContentLoaded", () => {
applyA11ySettings();
const allPanels = document.querySelectorAll('[id$="a11y-panel"]');
allPanels.forEach((panel) => {
const prefix = panel.id.replace("a11y-panel", "");
initA11yPanel(prefix);
});
});
+43 -71
View File
@@ -1,80 +1,52 @@
(() => {
const scripts = document.querySelectorAll('script[data-target-id]');
const isA11yMode = () => {
return localStorage.getItem("a11yMode") === "true";
};
const getScrollOpacity = (scrollDivisor) => {
const scrollY = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
return scrollY / scrollDivisor;
};
const applyBlurState = (blurElement, scrollDivisor, targetId, imageElement, imageUrl) => {
if (!blurElement) return;
const isMenuBlur = targetId === "menu-blur";
if (isA11yMode()) {
blurElement.setAttribute("aria-hidden", "true");
if (!isMenuBlur) {
blurElement.style.display = "none";
blurElement.style.opacity = "0";
} else {
blurElement.style.display = "";
blurElement.style.opacity = getScrollOpacity(scrollDivisor);
}
// Hide image in a11y mode
if (imageElement) {
imageElement.style.display = "none";
}
function setBackgroundBlur(targetId, scrollDivisor = 300, disableBlur = false, isMenuBlur = false) {
if (!targetId) {
console.error("data-target-id is null");
return;
}
const blurElement = document.getElementById(targetId);
if (!blurElement) return;
if (disableBlur) {
blurElement.setAttribute("aria-hidden", "true");
if (!isMenuBlur) {
blurElement.style.display = "none";
blurElement.style.opacity = "0";
} else {
blurElement.style.display = "";
blurElement.style.opacity = getScrollOpacity(scrollDivisor);
blurElement.removeAttribute("aria-hidden");
// Show image in normal mode
if (imageElement) {
imageElement.style.display = "";
// Re-apply image source if it was removed
if (imageUrl && !imageElement.getAttribute('src')) {
imageElement.setAttribute('src', imageUrl);
}
}
}
} else {
blurElement.style.display = "";
blurElement.removeAttribute("aria-hidden");
}
const updateBlur = () => {
if (!disableBlur || isMenuBlur) {
const scroll = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
blurElement.style.opacity = scroll / scrollDivisor;
}
};
blurElement.setAttribute("role", "presentation");
blurElement.setAttribute("tabindex", "-1");
window.addEventListener("scroll", updateBlur);
updateBlur();
}
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
document.querySelectorAll("script[data-target-id]").forEach((script) => {
const targetId = script.getAttribute("data-target-id");
const scrollDivisor = Number(script.getAttribute("data-scroll-divisor") || 300);
const isMenuBlur = targetId === "menu-blur";
const settings = JSON.parse(localStorage.getItem("a11ySettings") || "{}");
const disableBlur = settings.disableBlur || false;
setBackgroundBlur(targetId, scrollDivisor, disableBlur, isMenuBlur);
});
const blurElement = document.getElementById(targetId);
const imageElement = imageId ? document.getElementById(imageId) : null;
if (blurElement) {
blurElement.setAttribute("role", "presentation");
blurElement.setAttribute("tabindex", "-1");
applyBlurState(blurElement, scrollDivisor, targetId, imageElement, imageUrl);
window.addEventListener("scroll", () => {
if (!isA11yMode() || 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);
// Prevent disableImages FOUC
// Note: I tried putting this in a11y.js but it did not work, and placing it here prevents FOUC
(() => {
const settings = JSON.parse(localStorage.getItem("a11ySettings") || "{}");
if (settings.disableImages) {
document.querySelectorAll("script[data-image-id]").forEach((script) => {
const imageId = script.getAttribute("data-image-id");
const imageUrl = script.getAttribute("data-image-url"); // Get image URL from data attribute
const blurElement = document.getElementById(targetId);
const imageElement = imageId ? document.getElementById(imageId) : null;
applyBlurState(blurElement, scrollDivisor, targetId, imageElement, imageUrl);
const image = imageId && document.getElementById(imageId);
if (image) image.style.display = "none";
});
};
}
})();