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,283 +95,248 @@
</script> </script>
</head> </head>
<body> <body>
<div class="container faq"> <a href="#main-content" class="skip-link">Skip to main content</a>
<nav aria-label="Breadcrumb"> <div class="content">
<ol class="breadcrumb"> <main id="main-content" class="container faq">
<li><a href="/">Home</a></li> <nav aria-label="Breadcrumb">
<li aria-current="page">Email Setup & Help</li> <ol class="breadcrumb">
</ol> <li><a href="/">Home</a></li>
</nav> <li aria-current="page">Email Setup & Help</li>
<h1 class="text-center">Welcome to Email from mifi Ventures</h1> </ol>
<div class="intro"> </nav>
<strong>Let&apos;s get your inbox ready! 📬</strong><br /> <h1 class="text-center">Welcome to Email from mifi Ventures</h1>
<p> <div class="intro">
Friendly help for setting up your email—works with Outlook, <strong>Let&apos;s get your inbox ready! 📬</strong><br />
Apple Mail, Thunderbird, phones, and more. <p>
</p> Friendly help for setting up your email—works with
</div> Outlook, Apple Mail, Thunderbird, phones, and more.
</p>
</div>
<section <section
class="general-settings" class="general-settings"
aria-label="General Email Settings" aria-label="General Email Settings"
>
<h2>General Settings (All Clients)</h2>
<table>
<tr>
<td>Email Address</td>
<td>your.name@yourdomain.com</td>
</tr>
<tr>
<td>Username</td>
<td>your.name@yourdomain.com</td>
</tr>
<tr>
<td>Password</td>
<td>(your email password)</td>
</tr>
<tr>
<td>Incoming Server</td>
<td><b>mail.mifi.holdings</b></td>
</tr>
<tr>
<td>Outgoing Server</td>
<td><b>mail.mifi.holdings</b></td>
</tr>
<tr>
<td>IMAP Port</td>
<td>993 (SSL/TLS)</td>
</tr>
<tr>
<td>POP3 Port</td>
<td>995 (SSL/TLS)</td>
</tr>
<tr>
<td>SMTP Port</td>
<td>587 (STARTTLS) or 465 (SSL/TLS)</td>
</tr>
<tr>
<td>Authentication</td>
<td>Required (use same as incoming)</td>
</tr>
<tr>
<td>Encryption</td>
<td>SSL/TLS or STARTTLS</td>
</tr>
</table>
<span class="tip"
>Tip: Always use your <b>full email address</b> as your
username!</span
> >
</section> <h2>General Settings (All Clients)</h2>
<table>
<div class="accordion" id="helpAccordion"> <tr>
<!-- Outlook --> <td>Email Address</td>
<section class="accordion-section"> <td>your.name@yourdomain.com</td>
<button class="accordion-trigger" aria-expanded="false"> </tr>
<span class="icon"></span> Microsoft Outlook <tr>
</button> <td>Username</td>
<div class="accordion-content"> <td>your.name@yourdomain.com</td>
<ol> </tr>
<li>Go to <b>File → Add Account</b></li> <tr>
<li>Enter your full email address</li> <td>Password</td>
<li> <td>(your email password)</td>
Choose <b>Advanced options</b> → check “Set up </tr>
manually” <tr>
</li> <td>Incoming Server</td>
<li>Select <b>IMAP</b> (recommended) or POP</li> <td><b>mail.mifi.holdings</b></td>
<li> </tr>
Incoming server: <tr>
<code>mail.mifi.holdings</code>, port <td>Outgoing Server</td>
<b>993</b> (SSL/TLS) <td><b>mail.mifi.holdings</b></td>
</li> </tr>
<li> <tr>
Outgoing server: <td>IMAP Port</td>
<code>mail.mifi.holdings</code>, port <td>993 (SSL/TLS)</td>
<b>587</b> (STARTTLS) or <b>465</b> (SSL/TLS) </tr>
</li> <tr>
<li> <td>POP3 Port</td>
Username: full email address; Password: your <td>995 (SSL/TLS)</td>
password </tr>
</li> <tr>
<li>Click <b>Connect</b></li> <td>SMTP Port</td>
</ol> <td>587 (STARTTLS) or 465 (SSL/TLS)</td>
<span class="tip" </tr>
>If sending fails, make sure “Require logon using <tr>
SPA” is <b>unchecked</b>.</span <td>Authentication</td>
> <td>Required (use same as incoming)</td>
</div> </tr>
<tr>
<td>Encryption</td>
<td>SSL/TLS or STARTTLS</td>
</tr>
</table>
<span class="tip"
>Tip: Always use your <b>full email address</b> as your
username!</span
>
</section> </section>
<!-- Apple Mail --> <div class="accordion" id="helpAccordion">
<section class="accordion-section"> <!-- Outlook -->
<button class="accordion-trigger" aria-expanded="false"> <section class="accordion-section">
<span class="icon"></span> Apple Mail (macOS, iOS) <button class="accordion-trigger" aria-expanded="false">
</button> <span class="icon"></span> Microsoft Outlook
<div class="accordion-content"> </button>
<ol> <div class="accordion-content">
<li> <ol>
Add Account &rarr; <b>Other Mail Account</b> <li>Go to <b>File → Add Account</b></li>
</li> <li>Enter your full email address</li>
<li>Enter your name, email, and password</li> <li>
<li> Choose <b>Advanced options</b> → check “Set
Incoming/Outgoing server: up manually”
<code>mail.mifi.holdings</code> </li>
</li> <li>Select <b>IMAP</b> (recommended) or POP</li>
<li> <li>
IMAP port: <b>993</b> (SSL); SMTP port: Incoming server:
<b>587</b> (STARTTLS) or <b>465</b> (SSL) <code>mail.mifi.holdings</code>, port
</li> <b>993</b> (SSL/TLS)
<li>Use full email address for username</li> </li>
</ol> <li>
</div> Outgoing server:
</section> <code>mail.mifi.holdings</code>, port
<b>587</b> (STARTTLS) or
<!-- Thunderbird --> <b>465</b> (SSL/TLS)
<section class="accordion-section"> </li>
<button class="accordion-trigger" aria-expanded="false"> <li>
<span class="icon"></span> Thunderbird Username: full email address; Password: your
</button> password
<div class="accordion-content"> </li>
<ol> <li>Click <b>Connect</b></li>
<li>Menu → Account Settings → Add Mail Account</li> </ol>
<li>Fill in your name, email, and password</li> <span class="tip"
<li> >If sending fails, make sure “Require logon
Click “Configure manually” and use settings using SPA” is <b>unchecked</b>.</span
above >
</li>
</ol>
</div>
</section>
<!-- Mobile -->
<section class="accordion-section">
<button class="accordion-trigger" aria-expanded="false">
<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
</li>
<li>
Gmail app: tap profile → Add account → Other,
fill in details, use IMAP
</li>
</ul>
</div>
</section>
<!-- FAQ -->
<section class="accordion-section">
<button class="accordion-trigger" aria-expanded="false">
<span class="icon"></span> FAQ / Troubleshooting
</button>
<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.
</div> </div>
<div class="faq-q">Q: SSL/TLS errors?</div> </section>
<div class="faq-a">
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">
Contact
<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!
</div>
</div>
</section>
<!-- Pro Tips --> <!-- Apple Mail -->
<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> Pro Tips & Advanced <span class="icon"></span> Apple Mail (macOS, iOS)
</button> </button>
<div class="accordion-content"> <div class="accordion-content">
<ul> <ol>
<li> <li>
<b>IMAP syncs</b> your mail everywhere—choose Add Account &rarr; <b>Other Mail Account</b>
IMAP unless you know you want POP3. </li>
</li> <li>Enter your name, email, and password</li>
<li> <li>
Your login is always your Incoming/Outgoing server:
<b>full email address</b>. <code>mail.mifi.holdings</code>
</li> </li>
<li> <li>
Check your Spam/Junk folder for misfiled good IMAP port: <b>993</b> (SSL); SMTP port:
emails. <b>587</b> (STARTTLS) or <b>465</b> (SSL)
</li> </li>
<li> <li>Use full email address for username</li>
Advanced: IMAP path prefix = </ol>
<b>(leave blank)</b>; SMTP authentication is </div>
always required. </section>
</li>
</ul> <!-- Thunderbird -->
</div> <section class="accordion-section">
</section> <button class="accordion-trigger" aria-expanded="false">
</div> <span class="icon"></span> Thunderbird
<div class="footer"> </button>
Email from mifi Ventures &middot; Help Page &ndash; &copy; 2025 <div class="accordion-content">
mifi Ventures, LLC <ol>
</div> <li>
Menu → Account Settings → Add Mail Account
</li>
<li>Fill in your name, email, and password</li>
<li>
Click “Configure manually” and use settings
above
</li>
</ol>
</div>
</section>
<!-- Mobile -->
<section class="accordion-section">
<button class="accordion-trigger" aria-expanded="false">
<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
</li>
<li>
Gmail app: tap profile → Add account →
Other, fill in details, use IMAP
</li>
</ul>
</div>
</section>
<!-- FAQ -->
<section class="accordion-section">
<button class="accordion-trigger" aria-expanded="false">
<span class="icon"></span> FAQ / Troubleshooting
</button>
<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.
</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.
</div>
<div class="faq-q">Q: Still stuck?</div>
<div class="faq-a">
Contact
<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!
</div>
</div>
</section>
<!-- Pro Tips -->
<section class="accordion-section">
<button class="accordion-trigger" aria-expanded="false">
<span class="icon"></span> Pro Tips & Advanced
</button>
<div class="accordion-content">
<ul>
<li>
<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.
</li>
<li>
Advanced: IMAP path prefix =
<b>(leave blank)</b>; SMTP authentication is
always required.
</li>
</ul>
</div>
</section>
</div>
</main>
</div> </div>
<script> <footer class="footer">
// Native accessible accordion Email from mifi Holdings &middot; Help Page &middot; &copy;
document.querySelectorAll('.accordion-trigger').forEach((btn) => { 2025<span id="current-year"></span>
btn.addEventListener('click', function () { mifi Ventures, LLC
const section = btn.closest('.accordion-section') </footer>
const expanded = <script defer src="/assets/js/current-year.js"></script>
btn.getAttribute('aria-expanded') === 'true' <script defer src="/assets/js/accordion.js"></script>
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,24 +57,38 @@
</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="emoji">📮</div> <div class="content">
<h1>This is just a mailbox.</h1> <main id="main-content" class="container text-center">
<p> <div class="emoji">📮</div>
You&apos;ve reached <b>mail.mifi.holdings</b>.<br /> <h1>This is just a mailbox.</h1>
There&apos;s nothing exciting here&apos;s nothing exciting <p>
here—just some gears whirring and mail being sorted.<br /> You&apos;ve reached <b>mail.mifi.holdings</b>.<br />
Looking for your messages? There&apos;s nothing exciting here&apos;s nothing exciting
</p> here—just some gears whirring and mail being sorted.<br />
<a class="button" href="/help">Email Setup Help</a> Looking for your messages?
<a class="button" href="https://webmail.mifi.holdings" </p>
>Go to Webmail</a <a class="button" href="/help">Email Setup Help</a>
> <a
<a class="button"
class="button" href="https://webmail.mifi.holdings"
href="https://postmaster.mifi.holdings/users/login.php" target="_blank"
>Change/Forgot Password</a >Go to Webmail</a
> >
<a
class="button"
href="https://postmaster.mifi.holdings/users/login.php"
target="_blank"
>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'],
} },
] ],
} };