Accessibility fixes
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful

This commit is contained in:
2026-02-16 12:57:44 -03:00
parent 635594aea8
commit c71ec612bb
10 changed files with 418 additions and 361 deletions

View File

@@ -1,8 +1,8 @@
{ {
"semi": false, "semi": true,
"singleQuote": true, "singleQuote": true,
"tabWidth": 4, "tabWidth": 4,
"trailingComma": "none", "trailingComma": "all",
"overrides": [ "overrides": [
{ {
"files": "*.yml", "files": "*.yml",

View File

@@ -1,4 +1,4 @@
import prettierConfig from 'eslint-config-prettier/flat' import prettierConfig from 'eslint-config-prettier/flat';
export default [ export default [
{ {
@@ -9,12 +9,12 @@ export default [
globals: { globals: {
window: 'readonly', window: 'readonly',
document: 'readonly', document: 'readonly',
dataLayer: 'writable' dataLayer: 'writable',
} },
}, },
rules: { rules: {
'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }] 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
}
}, },
prettierConfig },
] prettierConfig,
];

View File

@@ -8,71 +8,71 @@ import {
readFileSync, readFileSync,
writeFileSync, writeFileSync,
cpSync, cpSync,
readdirSync readdirSync,
} from 'fs' } from 'fs';
import { join, dirname, extname } from 'path' import { join, dirname, extname } from 'path';
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url';
import Beasties from 'beasties' import Beasties from 'beasties';
import { minify as minifyJs } from 'terser' import { minify as minifyJs } from 'terser';
import CleanCSS from 'clean-css' import CleanCSS from 'clean-css';
const __dirname = dirname(fileURLToPath(import.meta.url)) const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..') const root = join(__dirname, '..');
const srcDir = join(root, 'src') const srcDir = join(root, 'src');
const distDir = join(root, 'dist') const distDir = join(root, 'dist');
function getFiles(dir, files = []) { function getFiles(dir, files = []) {
const entries = readdirSync(dir, { withFileTypes: true }) const entries = readdirSync(dir, { withFileTypes: true });
for (const e of entries) { for (const e of entries) {
const full = join(dir, e.name) const full = join(dir, e.name);
if (e.isDirectory()) getFiles(full, files) if (e.isDirectory()) getFiles(full, files);
else files.push(full) else files.push(full);
} }
return files return files;
} }
async function main() { async function main() {
// 1. Clean and copy src → dist // 1. Clean and copy src → dist
rmSync(distDir, { recursive: true, force: true }) rmSync(distDir, { recursive: true, force: true });
mkdirSync(distDir, { recursive: true }) mkdirSync(distDir, { recursive: true });
cpSync(srcDir, distDir, { recursive: true }) cpSync(srcDir, distDir, { recursive: true });
const distFiles = getFiles(distDir) const distFiles = getFiles(distDir);
// 2. Minify JS // 2. Minify JS
const jsFiles = distFiles.filter((f) => extname(f) === '.js') const jsFiles = distFiles.filter((f) => extname(f) === '.js');
for (const f of jsFiles) { for (const f of jsFiles) {
const code = readFileSync(f, 'utf8') const code = readFileSync(f, 'utf8');
const result = await minifyJs(code, { format: { comments: false } }) const result = await minifyJs(code, { format: { comments: false } });
if (result.code) writeFileSync(f, result.code) if (result.code) writeFileSync(f, result.code);
} }
// 3. Minify CSS // 3. Minify CSS
const cleanCss = new CleanCSS({ level: 2 }) const cleanCss = new CleanCSS({ level: 2 });
const cssFiles = distFiles.filter((f) => extname(f) === '.css') const cssFiles = distFiles.filter((f) => extname(f) === '.css');
for (const f of cssFiles) { for (const f of cssFiles) {
const code = readFileSync(f, 'utf8') const code = readFileSync(f, 'utf8');
const result = cleanCss.minify(code) const result = cleanCss.minify(code);
if (!result.errors.length) writeFileSync(f, result.styles) if (!result.errors.length) writeFileSync(f, result.styles);
} }
// 4. Inline critical CSS with Beasties for all HTML files (no browser; works in CI) // 4. Inline critical CSS with Beasties for all HTML files (no browser; works in CI)
const htmlFiles = distFiles.filter((f) => extname(f) === '.html') const htmlFiles = distFiles.filter((f) => extname(f) === '.html');
const beasties = new Beasties({ const beasties = new Beasties({
path: distDir, path: distDir,
preload: 'default', preload: 'default',
logLevel: 'warn' logLevel: 'warn',
}) });
for (const htmlFile of htmlFiles) { for (const htmlFile of htmlFiles) {
const html = readFileSync(htmlFile, 'utf8') const html = readFileSync(htmlFile, 'utf8');
const inlined = await beasties.process(html) const inlined = await beasties.process(html);
writeFileSync(htmlFile, inlined) writeFileSync(htmlFile, inlined);
} }
console.log('Build complete: dist/') console.log('Build complete: dist/');
} }
main().catch((err) => { main().catch((err) => {
console.error(err) console.error(err);
process.exit(1) process.exit(1);
}) });

View File

@@ -44,9 +44,53 @@ body {
} }
body { body {
display: flex;
align-items: center; align-items: center;
display: flex;
flex-direction: column;
gap: 1rem;
justify-content: space-between;
}
.content {
display: flex;
flex: 1;
justify-content: center; justify-content: center;
align-items: center;
}
.footer {
color: #374151;
font-size: 0.94em;
letter-spacing: 0.01em;
padding: 1rem;
text-align: center;
width: 100%;
}
@media (prefers-color-scheme: dark) {
.footer {
color: #aab2bd;
}
}
.skip-link {
position: absolute;
top: 0;
left: 0;
padding: 0.75rem 1.25rem;
background: var(--accent);
color: var(--button-text);
font-weight: 600;
text-decoration: none;
z-index: 100;
transform: translateY(-100%);
transition: transform 0.2s;
}
.skip-link:focus {
transform: translateY(0);
outline: 2px solid var(--accent-light);
outline-offset: 2px;
} }
h1 { h1 {
@@ -194,9 +238,10 @@ td:first-child {
white-space: nowrap; white-space: nowrap;
} }
/* Tip colors chosen for WCAG 2.2 AAA (≥7:1 contrast) */
.tip { .tip {
background: #eef2ff; background: #eef2ff;
color: var(--accent-light); color: #3730a3;
border-radius: 0.7em; border-radius: 0.7em;
font-size: 0.98em; font-size: 0.98em;
padding: 0.48em 0.8em; padding: 0.48em 0.8em;
@@ -207,7 +252,7 @@ td:first-child {
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.tip { .tip {
background: #232555; background: #232555;
color: #a5b4fc; color: #c7d2fe;
} }
} }
@@ -295,14 +340,6 @@ td:first-child {
color: var(--faq-a); color: var(--faq-a);
} }
.footer {
margin-top: 2.5em;
text-align: center;
color: #bbb;
font-size: 0.94em;
letter-spacing: 0.01em;
}
@media (width <= 600px) { @media (width <= 600px) {
.container { .container {
padding: 1.1rem 0.5rem 1rem; padding: 1.1rem 0.5rem 1rem;

View File

@@ -0,0 +1,37 @@
// Native accessible accordion
document.querySelectorAll('.accordion-trigger').forEach((btn) => {
btn.addEventListener('click', function () {
const section = btn.closest('.accordion-section');
const expanded = btn.getAttribute('aria-expanded') === 'true';
document.querySelectorAll('.accordion-section').forEach((s) => {
if (s === section) {
s.classList.toggle('open', !expanded);
btn.setAttribute('aria-expanded', String(!expanded));
const content = btn.nextElementSibling;
content.style.maxHeight = !expanded
? content.scrollHeight + 40 + 'px'
: '0px';
} else {
s.classList.remove('open');
s.querySelector('.accordion-trigger').setAttribute(
'aria-expanded',
'false',
);
s.querySelector('.accordion-content').style.maxHeight = '0px';
}
});
});
// Allow arrow navigation
btn.addEventListener('keydown', function (e) {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
const triggers = Array.from(
document.querySelectorAll('.accordion-trigger'),
);
let idx = triggers.indexOf(e.target);
if (e.key === 'ArrowDown') idx = (idx + 1) % triggers.length;
else idx = (idx - 1 + triggers.length) % triggers.length;
triggers[idx].focus();
}
});
});

View File

@@ -0,0 +1,4 @@
(function () {
const year = new Date().getFullYear();
document.getElementById('current-year').textContent = `${year}`;
})();

View File

@@ -1,11 +1,11 @@
;(function () { (function () {
var script = document.currentScript var script = document.currentScript;
var id = script && script.getAttribute('data-ga-id') var id = script && script.getAttribute('data-ga-id');
if (!id) return if (!id) return;
window.dataLayer = window.dataLayer || [] window.dataLayer = window.dataLayer || [];
function gtag() { function gtag() {
window.dataLayer.push(arguments) window.dataLayer.push(arguments);
} }
gtag('js', new Date()) gtag('js', new Date());
gtag('config', id, { anonymize_ip: true }) gtag('config', id, { anonymize_ip: true });
})() })();

View File

@@ -95,7 +95,9 @@
</script> </script>
</head> </head>
<body> <body>
<div class="container faq"> <a href="#main-content" class="skip-link">Skip to main content</a>
<div class="content">
<main id="main-content" class="container faq">
<nav aria-label="Breadcrumb"> <nav aria-label="Breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="/">Home</a></li> <li><a href="/">Home</a></li>
@@ -106,8 +108,8 @@
<div class="intro"> <div class="intro">
<strong>Let&apos;s get your inbox ready! 📬</strong><br /> <strong>Let&apos;s get your inbox ready! 📬</strong><br />
<p> <p>
Friendly help for setting up your email—works with Outlook, Friendly help for setting up your email—works with
Apple Mail, Thunderbird, phones, and more. Outlook, Apple Mail, Thunderbird, phones, and more.
</p> </p>
</div> </div>
@@ -175,8 +177,8 @@
<li>Go to <b>File → Add Account</b></li> <li>Go to <b>File → Add Account</b></li>
<li>Enter your full email address</li> <li>Enter your full email address</li>
<li> <li>
Choose <b>Advanced options</b> → check “Set up Choose <b>Advanced options</b> → check “Set
manually” up manually”
</li> </li>
<li>Select <b>IMAP</b> (recommended) or POP</li> <li>Select <b>IMAP</b> (recommended) or POP</li>
<li> <li>
@@ -187,7 +189,8 @@
<li> <li>
Outgoing server: Outgoing server:
<code>mail.mifi.holdings</code>, port <code>mail.mifi.holdings</code>, port
<b>587</b> (STARTTLS) or <b>465</b> (SSL/TLS) <b>587</b> (STARTTLS) or
<b>465</b> (SSL/TLS)
</li> </li>
<li> <li>
Username: full email address; Password: your Username: full email address; Password: your
@@ -196,8 +199,8 @@
<li>Click <b>Connect</b></li> <li>Click <b>Connect</b></li>
</ol> </ol>
<span class="tip" <span class="tip"
>If sending fails, make sure “Require logon using >If sending fails, make sure “Require logon
SPA” is <b>unchecked</b>.</span using SPA” is <b>unchecked</b>.</span
> >
</div> </div>
</section> </section>
@@ -233,7 +236,9 @@
</button> </button>
<div class="accordion-content"> <div class="accordion-content">
<ol> <ol>
<li>Menu → Account Settings → Add Mail Account</li> <li>
Menu → Account Settings → Add Mail Account
</li>
<li>Fill in your name, email, and password</li> <li>Fill in your name, email, and password</li>
<li> <li>
Click “Configure manually” and use settings Click “Configure manually” and use settings
@@ -246,20 +251,21 @@
<!-- Mobile --> <!-- Mobile -->
<section class="accordion-section"> <section class="accordion-section">
<button class="accordion-trigger" aria-expanded="false"> <button class="accordion-trigger" aria-expanded="false">
<span class="icon"></span> iOS / Android Mail / Gmail <span class="icon"></span> iOS / Android Mail /
App Gmail App
</button> </button>
<div class="accordion-content"> <div class="accordion-content">
<ul> <ul>
<li>Add Account → Other</li> <li>Add Account → Other</li>
<li>Enter your email and password</li> <li>Enter your email and password</li>
<li> <li>
Manual setup: <code>mail.mifi.holdings</code>, Manual setup:
correct ports, SSL/TLS required <code>mail.mifi.holdings</code>, correct
ports, SSL/TLS required
</li> </li>
<li> <li>
Gmail app: tap profile → Add account → Other, Gmail app: tap profile → Add account →
fill in details, use IMAP Other, fill in details, use IMAP
</li> </li>
</ul> </ul>
</div> </div>
@@ -273,14 +279,14 @@
<div class="accordion-content"> <div class="accordion-content">
<div class="faq-q">Q: My email wont send?</div> <div class="faq-q">Q: My email wont send?</div>
<div class="faq-a"> <div class="faq-a">
Check that youre using your full email address for Check that youre using your full email address
both incoming and outgoing username, and that the for both incoming and outgoing username, and
port is 587 or 465. that the port is 587 or 465.
</div> </div>
<div class="faq-q">Q: SSL/TLS errors?</div> <div class="faq-q">Q: SSL/TLS errors?</div>
<div class="faq-a"> <div class="faq-a">
Ensure SSL or STARTTLS is enabled for both incoming Ensure SSL or STARTTLS is enabled for both
and outgoing mail. incoming and outgoing mail.
</div> </div>
<div class="faq-q">Q: Still stuck?</div> <div class="faq-q">Q: Still stuck?</div>
<div class="faq-a"> <div class="faq-a">
@@ -288,8 +294,8 @@
<a href="mailto:postmaster@mifi.holdings" <a href="mailto:postmaster@mifi.holdings"
>postmaster@mifi.holdings</a >postmaster@mifi.holdings</a
>.<br /> >.<br />
Please include any error messages, your mail app, Please include any error messages, your mail
and a screenshot if you can! app, and a screenshot if you can!
</div> </div>
</div> </div>
</section> </section>
@@ -302,16 +308,17 @@
<div class="accordion-content"> <div class="accordion-content">
<ul> <ul>
<li> <li>
<b>IMAP syncs</b> your mail everywhere—choose <b>IMAP syncs</b> your mail
IMAP unless you know you want POP3. everywhere—choose IMAP unless you know you
want POP3.
</li> </li>
<li> <li>
Your login is always your Your login is always your
<b>full email address</b>. <b>full email address</b>.
</li> </li>
<li> <li>
Check your Spam/Junk folder for misfiled good Check your Spam/Junk folder for misfiled
emails. good emails.
</li> </li>
<li> <li>
Advanced: IMAP path prefix = Advanced: IMAP path prefix =
@@ -322,56 +329,14 @@
</div> </div>
</section> </section>
</div> </div>
<div class="footer"> </main>
Email from mifi Ventures &middot; Help Page &ndash; &copy; 2025 </div>
<footer class="footer">
Email from mifi Holdings &middot; Help Page &middot; &copy;
2025<span id="current-year"></span>
mifi Ventures, LLC mifi Ventures, LLC
</div> </footer>
</div> <script defer src="/assets/js/current-year.js"></script>
<script> <script defer src="/assets/js/accordion.js"></script>
// Native accessible accordion
document.querySelectorAll('.accordion-trigger').forEach((btn) => {
btn.addEventListener('click', function () {
const section = btn.closest('.accordion-section')
const expanded =
btn.getAttribute('aria-expanded') === 'true'
document
.querySelectorAll('.accordion-section')
.forEach((s) => {
if (s === section) {
s.classList.toggle('open', !expanded)
btn.setAttribute(
'aria-expanded',
String(!expanded)
)
const content = btn.nextElementSibling
content.style.maxHeight = !expanded
? content.scrollHeight + 40 + 'px'
: '0px'
} else {
s.classList.remove('open')
s.querySelector(
'.accordion-trigger'
).setAttribute('aria-expanded', 'false')
s.querySelector(
'.accordion-content'
).style.maxHeight = '0px'
}
})
})
// Allow arrow navigation
btn.addEventListener('keydown', function (e) {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
const triggers = Array.from(
document.querySelectorAll('.accordion-trigger')
)
let idx = triggers.indexOf(e.target)
if (e.key === 'ArrowDown')
idx = (idx + 1) % triggers.length
else idx = (idx - 1 + triggers.length) % triggers.length
triggers[idx].focus()
}
})
})
</script>
</body> </body>
</html> </html>

View File

@@ -57,7 +57,9 @@
</script> </script>
</head> </head>
<body> <body>
<div class="container text-center"> <a href="#main-content" class="skip-link">Skip to main content</a>
<div class="content">
<main id="main-content" class="container text-center">
<div class="emoji">📮</div> <div class="emoji">📮</div>
<h1>This is just a mailbox.</h1> <h1>This is just a mailbox.</h1>
<p> <p>
@@ -67,14 +69,26 @@
Looking for your messages? Looking for your messages?
</p> </p>
<a class="button" href="/help">Email Setup Help</a> <a class="button" href="/help">Email Setup Help</a>
<a class="button" href="https://webmail.mifi.holdings" <a
class="button"
href="https://webmail.mifi.holdings"
target="_blank"
>Go to Webmail</a >Go to Webmail</a
> >
<a <a
class="button" class="button"
href="https://postmaster.mifi.holdings/users/login.php" href="https://postmaster.mifi.holdings/users/login.php"
target="_blank"
>Change/Forgot Password</a >Change/Forgot Password</a
> >
</main>
</div> </div>
<footer class="footer">
Email from mifi Holdings &middot; &copy; 2025<span
id="current-year"
></span>
mifi Ventures, LLC
</footer>
<script defer src="/assets/js/current-year.js"></script>
</body> </body>
</html> </html>

View File

@@ -2,7 +2,7 @@ export default {
extends: ['stylelint-config-standard'], extends: ['stylelint-config-standard'],
overrides: [ overrides: [
{ {
files: ['src/**/*.css'] files: ['src/**/*.css'],
} },
] ],
} };