Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions internal/viewer/server_extra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,33 @@ func TestSurfaceCSS_LightSurfaceIsWhite(t *testing.T) {
}
}

func TestThemeToggle_IsAvailableInAppHeader(t *testing.T) {
head, err := assets.ReadFile("templates/app-header.html")
if err != nil {
t.Fatalf("read templates/app-header.html: %v", err)
}
if !strings.Contains(string(head), `data-theme-toggle`) {
t.Fatal("app-header.html is missing the theme toggle button hook")
}
if !strings.Contains(string(head), "☀") && !strings.Contains(string(head), "Toggle color theme") {
t.Fatal("app-header.html does not expose a visible theme toggle")
}
}

func TestThemeToggleCSS_UsesDataThemeOverride(t *testing.T) {
css, err := assets.ReadFile("static/style.css")
if err != nil {
t.Fatalf("read static/style.css: %v", err)
}
text := string(css)
if !strings.Contains(text, "body[data-theme=\"light\"]") || !strings.Contains(text, "body[data-theme=\"dark\"]") {
t.Fatal("style.css is missing body[data-theme=\"light\"] / body[data-theme=\"dark\"] overrides")
}
if !strings.Contains(text, "--bg: #ffffff;") || !strings.Contains(text, "--bg: #000000;") {
t.Fatal("style.css is missing the light/dark page background tokens used by the theme toggle")
}
}

// TestTextTokens_MeetWCAGAAOnPageBackground holds the muted and secondary
// text tokens to WCAG AA (4.5:1) against each theme's *page* background, so
// future palette tweaks cannot quietly drop the low-emphasis labels back
Expand Down
34 changes: 34 additions & 0 deletions internal/viewer/static/a11y.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,40 @@

// Small accessibility helpers shared by the viewer pages.
(() => {
const themeKey = "ocr-viewer-theme";
const applyTheme = (theme) => {
const nextTheme = theme === "light" ? "light" : "dark";
document.body.dataset.theme = nextTheme;
const button = document.querySelector("[data-theme-toggle]");
if (button) {
button.setAttribute("aria-pressed", String(nextTheme === "light"));
button.textContent = nextTheme === "light" ? "☀" : "☾";
button.title = nextTheme === "light" ? "Switch to dark mode" : "Switch to light mode";
}
try {
localStorage.setItem(themeKey, nextTheme);
} catch {
// Storage may be unavailable; the page still keeps the in-memory mode.
}
};

const storedTheme = (() => {
try {
return localStorage.getItem(themeKey);
} catch {
return null;
}
})();
applyTheme(storedTheme === "light" ? "light" : "dark");

const button = document.querySelector("[data-theme-toggle]");
if (button) {
button.addEventListener("click", () => {
const nextTheme = document.body.dataset.theme === "light" ? "dark" : "light";
applyTheme(nextTheme);
});
}

// The scrollable table wrappers are focusable regions, but not every
// browser scrolls a focused container with the arrow keys; route the
// keys through scrollBy so keyboard users can reach overflowing columns
Expand Down
Loading
Loading