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:
Zeus-Deus
2026-06-01 21:05:43 +02:00
parent 70a71f603c
commit ad445a1b30
7 changed files with 260 additions and 29 deletions
+25 -5
View File
@@ -265,7 +265,9 @@ body.bg-pattern-sparkles {
transform: translateY(calc(-50% - 2px));
font-size: 0.75em;
line-height: 1;
color: color-mix(in srgb, var(--fg) 40%, transparent);
/* 70% mix keeps the chat title clearly above the WCAG AA 4.5:1
contrast threshold (40% only reached ~2.8:1). */
color: color-mix(in srgb, var(--fg) 70%, transparent);
white-space: nowrap;
display: flex;
align-items: center;
@@ -2550,7 +2552,9 @@ body.bg-pattern-sparkles {
background: none;
border: 1px solid transparent;
border-radius: 4px;
color: color-mix(in srgb, var(--fg) 40%, transparent);
/* 65% mix lifts the model label above the WCAG AA 4.5:1 threshold
against the dark chat-bar (40% only reached ~2.9:1). */
color: color-mix(in srgb, var(--fg) 65%, transparent);
cursor: pointer;
white-space: nowrap;
transition: background 0.15s, color 0.15s, border-color 0.15s;
@@ -9610,7 +9614,8 @@ details a:hover {
margin: 0;
font-size: 11px;
line-height: 1.5;
color: color-mix(in srgb, var(--fg) 50%, transparent);
/* 65% keeps this description text above WCAG AA 4.5:1 (50% was ~3.9:1). */
color: color-mix(in srgb, var(--fg) 65%, transparent);
}
.memory-add-row {
@@ -11152,6 +11157,17 @@ textarea.memory-add-input {
#doc-language-icon:empty { display: none; }
#doc-language-icon svg { display: block; }
/* Visually hidden but available to assistive tech (screen readers, axe).
Use for content that should be announced/structural but not painted
e.g. the persistent page <h1>. */
.a11y-visually-hidden {
position: absolute !important;
width: 1px !important; height: 1px !important;
padding: 0 !important; margin: -1px !important;
overflow: hidden !important; clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important; border: 0 !important;
}
/* Custom language type picker (replaces visible chrome of native <select>
<option>s can't render SVG). Hidden select stays as the source of truth. */
.doc-langpicker-native-hidden {
@@ -12929,7 +12945,9 @@ body:has(.doc-version-panel:not(.hidden)) .hamburger-btn {
font-weight: 500;
}
.admin-toggle-sub {
color: color-mix(in srgb, var(--fg) 50%, transparent);
/* 65% mix keeps this helper text above WCAG AA 4.5:1 on the dark panel
(50% only reached ~3.9:1). */
color: color-mix(in srgb, var(--fg) 65%, transparent);
font-size: 11px;
margin-top: 2px;
}
@@ -29699,7 +29717,9 @@ body.notes-mobile-mode.notes-drag-mode .note-card-pin.active {
}
.notes-empty-msg {
text-align: center;
opacity: 0.4;
/* 0.4 dropped this empty-state text to ~2.8:1; 0.65 keeps it readable
(WCAG AA) while staying visibly secondary. */
opacity: 0.65;
padding: 30px 20px;
font-size: 11px;
}