feat(a11y): contrast tokens, accent-solid, responsive drawer, keyboard menus
- Contrast: --fg-muted reaches 4.5:1 on every surface in all nine themes (dark default was 2.55:1); status chips and accent text clear 4.5:1 on their own tints. New --accent-solid(/-hover) + tailwind `accent-solid` is the fill behind white text (.btn-primary, Switch on, Checkbox tick); --accent is now a text colour. Switch off / unchecked Checkbox get a >=3:1 edge. - <html lang> follows the chosen language; a global prefers-reduced-motion rule; index.html body uses a token instead of text-gray-200. - Layout: the sidebar starts collapsed below lg and becomes an off-canvas drawer below md (menu button, backdrop, Escape, useDialog). - ContextMenu: focus moves in, arrows/Home/End, Escape/Tab close, focus returns, a keyboard-opened menu anchors to its target. Select is a real listbox button (aria-expanded, activedescendant, type-ahead). PageTabs implements APG tabs with a tabpanel. New .btn-secondary/.btn-danger/ .btn-icon and a .select class for native selects, all with focus rings. - StatusBar announces connection and "N need you" politely; Sidebar's connection modal uses useDialog; EmptyState/Skeleton/AiToolIcon a11y. - api.ts request(): a non-JSON 200 throws a translated EBADRESPONSE instead of "Unexpected token '<'".
This commit is contained in:
@@ -7,6 +7,17 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import i18n from "i18next";
|
||||
|
||||
describe("<html lang>", () => {
|
||||
it("follows the resolved language on every change", async () => {
|
||||
await i18n.changeLanguage("vi-VN");
|
||||
expect(document.documentElement.lang).toBe("vi");
|
||||
await i18n.changeLanguage("ko");
|
||||
expect(document.documentElement.lang).toBe("en");
|
||||
await i18n.changeLanguage("en");
|
||||
expect(document.documentElement.lang).toBe("en");
|
||||
});
|
||||
});
|
||||
|
||||
describe("i18n resources", () => {
|
||||
it("should provide Vietnamese translations for navigation keys", async () => {
|
||||
await i18n.changeLanguage("vi");
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
* Unknown regional variants (e.g. `en-US`) map to supported base languages via
|
||||
* `nonExplicitSupportedLngs`.
|
||||
*
|
||||
* `<html lang>` follows the resolved language on init and on every change.
|
||||
*
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
/* =============================================================================
|
||||
@@ -94,6 +96,15 @@ import splash_vi from "./locales/vi/splash.json";
|
||||
import lanes_en from "./locales/en/lanes.json";
|
||||
import lanes_vi from "./locales/vi/lanes.json";
|
||||
|
||||
/** Keep `<html lang>` on the active language, so a screen reader speaks the
|
||||
* page with the right voice and the browser hyphenates/translates correctly.
|
||||
* Registered before `init`, which fires the first `languageChanged`. */
|
||||
export function syncDocumentLang(): void {
|
||||
if (typeof document === "undefined") return;
|
||||
document.documentElement.lang = i18n.resolvedLanguage || i18n.language || "en";
|
||||
}
|
||||
i18n.on("languageChanged", syncDocumentLang);
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
@@ -169,5 +180,7 @@ i18n
|
||||
},
|
||||
});
|
||||
|
||||
syncDocumentLang();
|
||||
|
||||
/** Configured i18next instance — import side effects run {@link init}. */
|
||||
export default i18n;
|
||||
|
||||
@@ -28,5 +28,6 @@
|
||||
"glob": "Glob",
|
||||
"agent": "Agent",
|
||||
"other": "Other"
|
||||
}
|
||||
},
|
||||
"badResponse": "The server sent an unexpected response instead of data. The dashboard may be restarting, or a proxy is answering in its place — reload in a moment."
|
||||
}
|
||||
|
||||
@@ -68,5 +68,9 @@
|
||||
"workspace": "Workspace {{cost}}",
|
||||
"session": "Session {{cost}}"
|
||||
},
|
||||
"version": "Version"
|
||||
"version": "Version",
|
||||
"openMenu": "Open menu",
|
||||
"closeMenu": "Close menu",
|
||||
"sidebar": "Sidebar",
|
||||
"now": "now"
|
||||
}
|
||||
|
||||
@@ -28,5 +28,6 @@
|
||||
"glob": "Glob",
|
||||
"agent": "Agent",
|
||||
"other": "Khác"
|
||||
}
|
||||
},
|
||||
"badResponse": "Máy chủ trả về phản hồi không mong đợi thay vì dữ liệu. Dashboard có thể đang khởi động lại, hoặc một proxy đang trả lời thay nó — hãy tải lại sau giây lát."
|
||||
}
|
||||
|
||||
@@ -68,5 +68,9 @@
|
||||
"workspace": "Workspace {{cost}}",
|
||||
"session": "Session {{cost}}"
|
||||
},
|
||||
"version": "Phiên bản"
|
||||
"version": "Phiên bản",
|
||||
"openMenu": "Mở menu",
|
||||
"closeMenu": "Đóng menu",
|
||||
"sidebar": "Thanh bên",
|
||||
"now": "bây giờ"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user