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,
"tabWidth": 4,
"trailingComma": "none",
"trailingComma": "all",
"overrides": [
{
"files": "*.yml",

View File

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

View File

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

View File

@@ -44,9 +44,53 @@ body {
}
body {
display: flex;
align-items: center;
display: flex;
flex-direction: column;
gap: 1rem;
justify-content: space-between;
}
.content {
display: flex;
flex: 1;
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 {
@@ -194,9 +238,10 @@ td:first-child {
white-space: nowrap;
}
/* Tip colors chosen for WCAG 2.2 AAA (≥7:1 contrast) */
.tip {
background: #eef2ff;
color: var(--accent-light);
color: #3730a3;
border-radius: 0.7em;
font-size: 0.98em;
padding: 0.48em 0.8em;
@@ -207,7 +252,7 @@ td:first-child {
@media (prefers-color-scheme: dark) {
.tip {
background: #232555;
color: #a5b4fc;
color: #c7d2fe;
}
}
@@ -295,14 +340,6 @@ td:first-child {
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) {
.container {
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 () {
var script = document.currentScript
var id = script && script.getAttribute('data-ga-id')
if (!id) return
window.dataLayer = window.dataLayer || []
(function () {
var script = document.currentScript;
var id = script && script.getAttribute('data-ga-id');
if (!id) return;
window.dataLayer = window.dataLayer || [];
function gtag() {
window.dataLayer.push(arguments)
window.dataLayer.push(arguments);
}
gtag('js', new Date())
gtag('config', id, { anonymize_ip: true })
})()
gtag('js', new Date());
gtag('config', id, { anonymize_ip: true });
})();

View File

@@ -95,7 +95,9 @@
</script>
</head>
<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">
<ol class="breadcrumb">
<li><a href="/">Home</a></li>
@@ -106,8 +108,8 @@
<div class="intro">
<strong>Let&apos;s get your inbox ready! 📬</strong><br />
<p>
Friendly help for setting up your email—works with Outlook,
Apple Mail, Thunderbird, phones, and more.
Friendly help for setting up your email—works with
Outlook, Apple Mail, Thunderbird, phones, and more.
</p>
</div>
@@ -175,8 +177,8 @@
<li>Go to <b>File → Add Account</b></li>
<li>Enter your full email address</li>
<li>
Choose <b>Advanced options</b> → check “Set up
manually”
Choose <b>Advanced options</b> → check “Set
up manually”
</li>
<li>Select <b>IMAP</b> (recommended) or POP</li>
<li>
@@ -187,7 +189,8 @@
<li>
Outgoing server:
<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>
Username: full email address; Password: your
@@ -196,8 +199,8 @@
<li>Click <b>Connect</b></li>
</ol>
<span class="tip"
>If sending fails, make sure “Require logon using
SPA” is <b>unchecked</b>.</span
>If sending fails, make sure “Require logon
using SPA” is <b>unchecked</b>.</span
>
</div>
</section>
@@ -233,7 +236,9 @@
</button>
<div class="accordion-content">
<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>
Click “Configure manually” and use settings
@@ -246,20 +251,21 @@
<!-- Mobile -->
<section class="accordion-section">
<button class="accordion-trigger" aria-expanded="false">
<span class="icon"></span> iOS / Android Mail / Gmail
App
<span class="icon"></span> iOS / Android Mail /
Gmail App
</button>
<div class="accordion-content">
<ul>
<li>Add Account → Other</li>
<li>Enter your email and password</li>
<li>
Manual setup: <code>mail.mifi.holdings</code>,
correct ports, SSL/TLS required
Manual setup:
<code>mail.mifi.holdings</code>, correct
ports, SSL/TLS required
</li>
<li>
Gmail app: tap profile → Add account → Other,
fill in details, use IMAP
Gmail app: tap profile → Add account →
Other, fill in details, use IMAP
</li>
</ul>
</div>
@@ -273,14 +279,14 @@
<div class="accordion-content">
<div class="faq-q">Q: My email wont send?</div>
<div class="faq-a">
Check that youre using your full email address for
both incoming and outgoing username, and that the
port is 587 or 465.
Check that youre using your full email address
for both incoming and outgoing username, and
that the port is 587 or 465.
</div>
<div class="faq-q">Q: SSL/TLS errors?</div>
<div class="faq-a">
Ensure SSL or STARTTLS is enabled for both incoming
and outgoing mail.
Ensure SSL or STARTTLS is enabled for both
incoming and outgoing mail.
</div>
<div class="faq-q">Q: Still stuck?</div>
<div class="faq-a">
@@ -288,8 +294,8 @@
<a href="mailto:postmaster@mifi.holdings"
>postmaster@mifi.holdings</a
>.<br />
Please include any error messages, your mail app,
and a screenshot if you can!
Please include any error messages, your mail
app, and a screenshot if you can!
</div>
</div>
</section>
@@ -302,16 +308,17 @@
<div class="accordion-content">
<ul>
<li>
<b>IMAP syncs</b> your mail everywhere—choose
IMAP unless you know you want POP3.
<b>IMAP syncs</b> your mail
everywhere—choose IMAP unless you know you
want POP3.
</li>
<li>
Your login is always your
<b>full email address</b>.
</li>
<li>
Check your Spam/Junk folder for misfiled good
emails.
Check your Spam/Junk folder for misfiled
good emails.
</li>
<li>
Advanced: IMAP path prefix =
@@ -322,56 +329,14 @@
</div>
</section>
</div>
<div class="footer">
Email from mifi Ventures &middot; Help Page &ndash; &copy; 2025
</main>
</div>
<footer class="footer">
Email from mifi Holdings &middot; Help Page &middot; &copy;
2025<span id="current-year"></span>
mifi Ventures, LLC
</div>
</div>
<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>
</footer>
<script defer src="/assets/js/current-year.js"></script>
<script defer src="/assets/js/accordion.js"></script>
</body>
</html>

View File

@@ -57,7 +57,9 @@
</script>
</head>
<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>
<h1>This is just a mailbox.</h1>
<p>
@@ -67,14 +69,26 @@
Looking for your messages?
</p>
<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
>
<a
class="button"
href="https://postmaster.mifi.holdings/users/login.php"
target="_blank"
>Change/Forgot Password</a
>
</main>
</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>
</html>

View File

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