Improve accessibility across core flows (#86)
First incremental pass at issue #86, focused on the universal entry points and primary navigation. All changes verified in-browser with the axe-core engine (0 violations on the surfaces below) plus manual keyboard testing, on both desktop (1280px) and mobile (390px). Login / first-run setup (static/login.html) - Add a real <h1>, wrap content in <main> + <footer> landmarks. - Mark the decorative boat SVG aria-hidden. - Errors now use role="alert" so screen readers announce them. - "Remember me" checkbox is keyboard-focusable (was display:none) with an accessible name and a focus ring; dynamic 2FA field gets a linked label. - Darken the brand-red submit button so white text clears WCAG AA 4.5:1 (was ~3.2:1); add visible :focus-visible rings. App shell (static/index.html, static/style.css) - Remove invalid role="region" from the <main> chat container (it was overriding the implicit main landmark). - Add a persistent, visually-hidden <h1> inside <main> so the page always exposes one logical level-1 heading — works even on mobile where the sidebar (with the visible brand) is hidden off-canvas. - Add a reusable .a11y-visually-hidden utility. - Raise chat-title, model-picker, settings-helper and notes text contrast above 4.5:1 (were 2.8-3.9:1). Keyboard nav + dialogs (static/js/a11y.js - new) - Make the click-only <div> sidebar navigation (New Chat, Search, Brain, Calendar, Compare, Cookbook, Deep Research, Gallery, Library, Notes, Tasks, Theme, account) focusable and Enter/Space-activatable, announced as buttons (skipping role=button where a nested control would create a nested-interactive violation). Visible focus ring reused from existing .list-item:focus-visible. - Upgrade modals (.modal-content and the docked .notes-pane) to labelled role="dialog" + aria-modal, and normalise their title to heading level 2 so heading order stays valid. A MutationObserver covers runtime-rendered rows and modals. Decorative background canvases (static/js/theme.js) - Mark all 7 bg-effect canvases aria-hidden. Notes & Tasks (static/js/notes.js, static/js/tasks.js) - Label the icon-only Note/To-do toggle pills (fixes a critical button-name issue) and track aria-pressed state. - Improve Notes header-button + empty-state contrast. - Give the Tasks sort <select> an accessible name (fixes a critical select-name issue). Remaining data-dense tool modals (Tasks cards, Calendar, Gallery, Email, Cookbook, Compare, Deep Research) still have muted-text contrast to polish and are the next incremental step, per the issue's own guidance.
This commit is contained in:
@@ -150,16 +150,23 @@
|
||||
color: var(--fg); font-size: 0.95rem; font-family: 'Fira Code', monospace;
|
||||
}
|
||||
input:focus { outline: none; border-color: var(--red); }
|
||||
/* Clear, visible focus ring for keyboard users on every focusable control. */
|
||||
input:focus-visible, a:focus-visible, button:focus-visible {
|
||||
outline: 2px solid var(--red);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
/* Asymmetric vertical padding nudges the label 1px down while keeping
|
||||
the button's total height the same as 0.7rem all-around. */
|
||||
padding: calc(0.7rem + 1px) 0.7rem calc(0.7rem - 1px);
|
||||
border: none; border-radius: 6px;
|
||||
background: var(--red); color: #fff; font-size: 1rem; cursor: pointer;
|
||||
/* Darken the brand red slightly so #fff label text clears the WCAG AA
|
||||
4.5:1 contrast threshold (plain --red #e06c75 only reaches ~3.2:1). */
|
||||
background: color-mix(in srgb, var(--red) 78%, #000); color: #fff; font-size: 1rem; cursor: pointer;
|
||||
font-weight: 600; font-family: 'Fira Code', monospace;
|
||||
}
|
||||
button:hover { background: color-mix(in srgb, var(--red) 85%, black); }
|
||||
button:hover { background: color-mix(in srgb, var(--red) 66%, black); }
|
||||
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.error { color: #e55; font-size: 0.85rem; margin-bottom: 0.75rem; display: none; }
|
||||
.toggle { text-align: center; margin-top: calc(1rem + 4px); font-size: 0.85rem; color: color-mix(in srgb, var(--fg) 50%, transparent); }
|
||||
@@ -185,7 +192,17 @@
|
||||
align-items: center; justify-content: center;
|
||||
font-size: 0; margin: 0; color: transparent;
|
||||
}
|
||||
.remember-toggle .remember-check { display: none; }
|
||||
/* Visually hide the native checkbox but keep it in the accessibility tree
|
||||
and keyboard-focusable (display:none would drop it from tab order). It
|
||||
overlays the dot so a click/tap still toggles it. */
|
||||
.remember-toggle .remember-check {
|
||||
position: absolute; top: 0; left: 0;
|
||||
width: 100%; height: 100%; margin: 0;
|
||||
opacity: 0; cursor: pointer;
|
||||
}
|
||||
.remember-toggle .remember-check:focus-visible + .remember-dot {
|
||||
outline: 2px solid var(--red); outline-offset: 2px;
|
||||
}
|
||||
.remember-toggle .remember-dot {
|
||||
display: block; width: 10px; height: 10px; min-width: 10px; min-height: 10px;
|
||||
border-radius: 50%;
|
||||
@@ -223,21 +240,21 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="logo">
|
||||
<svg class="logo-boat" viewBox="0 0 32 32"><path d="M16 4L16 22L6 22Z" fill="currentColor"/><path d="M16 8L16 22L24 22Z" fill="currentColor" opacity="0.6"/><path d="M4 24Q10 20 16 24Q22 28 28 24" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round"/></svg><span>Odysseus</span>
|
||||
</div>
|
||||
<main class="card">
|
||||
<h1 class="logo">
|
||||
<svg class="logo-boat" viewBox="0 0 32 32" aria-hidden="true" focusable="false"><path d="M16 4L16 22L6 22Z" fill="currentColor"/><path d="M16 8L16 22L24 22Z" fill="currentColor" opacity="0.6"/><path d="M4 24Q10 20 16 24Q22 28 28 24" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round"/></svg><span>Odysseus</span>
|
||||
</h1>
|
||||
<p class="setup-note" id="setupNote" style="display:none"></p>
|
||||
|
||||
<div class="error" id="error"></div>
|
||||
<div class="error" id="error" role="alert" aria-live="assertive"></div>
|
||||
|
||||
<form id="authForm" autocomplete="on">
|
||||
<label for="username">Username</label>
|
||||
<div class="pw-wrapper">
|
||||
<input id="username" name="username" type="text" required autofocus autocomplete="username">
|
||||
<label class="remember-toggle" id="rememberToggle" title="Remember me">
|
||||
<input type="checkbox" class="remember-check" id="remember" checked>
|
||||
<span class="remember-dot"></span>
|
||||
<input type="checkbox" class="remember-check" id="remember" checked aria-label="Remember me">
|
||||
<span class="remember-dot" aria-hidden="true"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -266,9 +283,9 @@
|
||||
<span id="toggleText">Don't have an account? </span>
|
||||
<a id="toggleLink" href="#">Sign up</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="version-label" id="version-label"></div>
|
||||
<footer class="version-label" id="version-label"></footer>
|
||||
|
||||
<script nonce="{{CSP_NONCE}}">
|
||||
(async () => {
|
||||
@@ -468,7 +485,7 @@
|
||||
form._totpMode = true;
|
||||
const totpWrap = document.createElement('div');
|
||||
totpWrap.style.cssText = 'margin-top:12px;';
|
||||
totpWrap.innerHTML = '<label style="font-size:0.85em;opacity:0.7;display:block;margin-bottom:4px;">2FA Code</label><input type="text" id="totp-input" placeholder="Enter 6-digit code" autocomplete="one-time-code" inputmode="numeric" maxlength="8" style="width:100%;padding:10px 12px;background:var(--bg);color:var(--fg);border:1px solid var(--border);border-radius:8px;font-size:14px;box-sizing:border-box;text-align:center;letter-spacing:4px;">';
|
||||
totpWrap.innerHTML = '<label for="totp-input" style="font-size:0.85em;opacity:0.7;display:block;margin-bottom:4px;">2FA Code</label><input type="text" id="totp-input" placeholder="Enter 6-digit code" aria-label="Two-factor authentication code" autocomplete="one-time-code" inputmode="numeric" maxlength="8" style="width:100%;padding:10px 12px;background:var(--bg);color:var(--fg);border:1px solid var(--border);border-radius:8px;font-size:14px;box-sizing:border-box;text-align:center;letter-spacing:4px;">';
|
||||
const formEl = submitBtn.parentElement;
|
||||
formEl.insertBefore(totpWrap, submitBtn);
|
||||
const totpInput = document.getElementById('totp-input');
|
||||
|
||||
Reference in New Issue
Block a user