fix(webui): keep iOS PWA controls inside safe area (#5477)

* fix(webui): keep iOS PWA controls inside safe area

* fix(webui): blend iOS PWA chrome into dark canvas

* fix(webui): sync PWA chrome with app theme
This commit is contained in:
chengyongru
2026-08-22 01:11:20 +08:00
committed by GitHub
parent 9ae3ae17da
commit 7604dc47f9
5 changed files with 107 additions and 8 deletions
+7
View File
@@ -25,6 +25,13 @@ function applyTheme(theme: Theme): void {
const root = document.documentElement;
if (theme === "dark") root.classList.add("dark");
else root.classList.remove("dark");
const themeColor = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]');
const color =
theme === "dark"
? themeColor?.dataset.themeColorDark
: themeColor?.dataset.themeColorLight;
if (themeColor && color) themeColor.content = color;
}
export function useTheme(): {
+35
View File
@@ -12,4 +12,39 @@ describe("index.html", () => {
expect(viewport).not.toContain("user-scalable=no");
expect(viewport).not.toMatch(/maximum-scale\s*=\s*1(?:\.0)?(?:,|$)/);
});
it("lets iOS keep standalone content inside the safe area", () => {
const html = readFileSync(resolve(process.cwd(), "index.html"), "utf8");
const viewport = html.match(/<meta\s+name="viewport"\s+content="([^"]+)"/i)?.[1];
expect(viewport).toContain("viewport-fit=auto");
expect(viewport).not.toContain("viewport-fit=cover");
});
it("provides light and dark PWA chrome colors", () => {
const html = readFileSync(resolve(process.cwd(), "index.html"), "utf8");
const manifest = JSON.parse(
readFileSync(resolve(process.cwd(), "public/manifest.json"), "utf8"),
) as {
background_color?: string;
theme_color?: string;
color_scheme_dark?: { background_color?: string; theme_color?: string };
};
const document = new DOMParser().parseFromString(html, "text/html");
const themeColor = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]');
const lightBodyBackground = html.match(/body\s*{[^}]*background:\s*([^;]+);/s)?.[1]?.trim();
const darkBodyBackground = html.match(
/html\.dark body\s*{[^}]*background:\s*([^;]+);/s,
)?.[1]?.trim();
expect(themeColor?.content).toBe("#ffffff");
expect(themeColor?.dataset.themeColorLight).toBe("#ffffff");
expect(themeColor?.dataset.themeColorDark).toBe("#303030");
expect(lightBodyBackground).toBe("#ffffff");
expect(darkBodyBackground).toBe("#303030");
expect(manifest.background_color).toBe("#ffffff");
expect(manifest.theme_color).toBe("#ffffff");
expect(manifest.color_scheme_dark?.background_color).toBe("#303030");
expect(manifest.color_scheme_dark?.theme_color).toBe("#303030");
});
});
+39
View File
@@ -0,0 +1,39 @@
import { act, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { useTheme } from "@/hooks/useTheme";
describe("useTheme", () => {
beforeEach(() => {
localStorage.removeItem("nanobot-webui.theme");
document.documentElement.classList.remove("dark");
const themeColor = document.createElement("meta");
themeColor.name = "theme-color";
themeColor.content = "#ffffff";
themeColor.dataset.themeColorLight = "#ffffff";
themeColor.dataset.themeColorDark = "#303030";
document.head.append(themeColor);
});
afterEach(() => {
document.querySelector('meta[name="theme-color"]')?.remove();
document.documentElement.classList.remove("dark");
localStorage.removeItem("nanobot-webui.theme");
});
it("keeps browser chrome in sync with the selected app theme", () => {
localStorage.setItem("nanobot-webui.theme", "dark");
const { result } = renderHook(useTheme);
const themeColor = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]');
expect(document.documentElement).toHaveClass("dark");
expect(themeColor?.content).toBe("#303030");
act(() => result.current.setTheme("light"));
expect(document.documentElement).not.toHaveClass("dark");
expect(themeColor?.content).toBe("#ffffff");
expect(localStorage.getItem("nanobot-webui.theme")).toBe("light");
});
});