diff --git a/static/index.html b/static/index.html
index 404c370..9077425 100644
--- a/static/index.html
+++ b/static/index.html
@@ -264,8 +264,8 @@
-
-Loading…
+
+Loading…
diff --git a/static/js/utils.js b/static/js/utils.js
index 431fd7a..954972e 100644
--- a/static/js/utils.js
+++ b/static/js/utils.js
@@ -139,6 +139,79 @@ async function clientAutoTrimBounds(fid) {
};
}
+
+// ── Accessibility helpers ────────────────────────────────────────────────
+function _a11yText(el) {
+ return String(el?.textContent || '').replace(/\s+/g, ' ').trim();
+}
+
+function _a11yLabel(el) {
+ return el?.getAttribute('aria-label') || el?.getAttribute('title') || _a11yText(el) || '';
+}
+
+function enhanceAccessibility(root = document) {
+ const scope = root && root.querySelectorAll ? root : document;
+ const statusBar = $('status-bar');
+ if (statusBar) {
+ statusBar.setAttribute('role', 'status');
+ statusBar.setAttribute('aria-live', 'polite');
+ statusBar.setAttribute('aria-atomic', 'true');
+ }
+ const toastEl = $('toast');
+ if (toastEl) {
+ toastEl.setAttribute('aria-live', 'polite');
+ toastEl.setAttribute('aria-atomic', 'true');
+ }
+
+ scope.querySelectorAll('.nav-item, .nav-tree-item, .tab, [onclick]').forEach(el => {
+ if (/^(A|BUTTON|INPUT|SELECT|TEXTAREA|SUMMARY)$/i.test(el.tagName)) return;
+ if (!el.hasAttribute('role')) el.setAttribute('role', 'button');
+ if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '0');
+ const label = _a11yLabel(el);
+ if (label && !el.getAttribute('aria-label')) el.setAttribute('aria-label', label);
+ if (label && !el.getAttribute('title')) el.setAttribute('title', label);
+ });
+
+ scope.querySelectorAll('button, [role=button], a, input, select, textarea').forEach(el => {
+ const label = _a11yLabel(el);
+ if (label && !el.getAttribute('title') && el.matches('button, [role=button]')) el.setAttribute('title', label);
+ if (label && !el.getAttribute('aria-label') && !_a11yText(el) && el.matches('button, [role=button]')) el.setAttribute('aria-label', label);
+ });
+
+ scope.querySelectorAll('button .mdi, [role=button] .mdi, .nav-icon .mdi').forEach(icon => {
+ icon.setAttribute('aria-hidden', 'true');
+ });
+
+ document.querySelectorAll('.nav-item, .nav-tree-item').forEach(el => {
+ const active = el.classList.contains('active') || el.classList.contains('is-active');
+ if (active && el.getAttribute('aria-current') !== 'page') el.setAttribute('aria-current', 'page');
+ else if (!active && el.hasAttribute('aria-current')) el.removeAttribute('aria-current');
+ });
+}
+
+function initAccessibilityEnhancements() {
+ if (window.__ttsvcA11yReady) { enhanceAccessibility(document); return; }
+ window.__ttsvcA11yReady = true;
+ enhanceAccessibility(document);
+ document.addEventListener('keydown', (e) => {
+ if (e.key !== 'Enter' && e.key !== ' ') return;
+ const el = e.target?.closest?.('[role=button]');
+ if (!el || /^(BUTTON|A|INPUT|SELECT|TEXTAREA)$/i.test(el.tagName)) return;
+ e.preventDefault();
+ el.click();
+ });
+ const obs = new MutationObserver((mutations) => {
+ for (const m of mutations) {
+ if (m.type === 'childList') {
+ m.addedNodes.forEach(n => { if (n.nodeType === 1) enhanceAccessibility(n); });
+ } else if (m.type === 'attributes') {
+ enhanceAccessibility(document);
+ }
+ }
+ });
+ obs.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
+}
+
// ── Light / dark theme ────────────────────────────────────────────────────
function applyTheme(t) {
diff --git a/static/loader.js b/static/loader.js
index e74425a..af80c29 100644
--- a/static/loader.js
+++ b/static/loader.js
@@ -149,6 +149,7 @@
// ── i18n: add the language picker and translate the (now fully-rendered) UI ──
if (window.initLangPicker) window.initLangPicker();
if (window.applyI18n) window.applyI18n(document);
+ if (window.initAccessibilityEnhancements) window.initAccessibilityEnhancements();
// E — post-init modules load in background AFTER the UI is already visible.
// Engines, AI backends, generation, and conversation sections load while