mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 16:21:50 +03:00
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:
+20
-6
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
content="width=device-width, initial-scale=1.0, viewport-fit=auto"
|
||||||
/>
|
/>
|
||||||
<meta name="color-scheme" content="light dark" />
|
<meta name="color-scheme" content="light dark" />
|
||||||
<meta
|
<meta
|
||||||
@@ -12,8 +12,12 @@
|
|||||||
content="nanobot web UI — chat with your nanobot workspace."
|
content="nanobot web UI — chat with your nanobot workspace."
|
||||||
data-i18n-meta="description"
|
data-i18n-meta="description"
|
||||||
/>
|
/>
|
||||||
<meta name="theme-color" content="#fafaf9" media="(prefers-color-scheme: light)" />
|
<meta
|
||||||
<meta name="theme-color" content="#161618" media="(prefers-color-scheme: dark)" />
|
name="theme-color"
|
||||||
|
content="#ffffff"
|
||||||
|
data-theme-color-light="#ffffff"
|
||||||
|
data-theme-color-dark="#303030"
|
||||||
|
/>
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
@@ -49,7 +53,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
html.dark body {
|
html.dark body {
|
||||||
background: #1a1a1a;
|
background: #303030;
|
||||||
color: #fafafa;
|
color: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,13 +100,23 @@
|
|||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
try {
|
try {
|
||||||
var stored = localStorage.getItem("nanobot-webui.theme");
|
var stored = null;
|
||||||
|
try {
|
||||||
|
stored = localStorage.getItem("nanobot-webui.theme");
|
||||||
|
} catch {}
|
||||||
var dark =
|
var dark =
|
||||||
stored === "dark" ||
|
stored === "dark" ||
|
||||||
(!stored &&
|
(stored !== "light" &&
|
||||||
window.matchMedia &&
|
window.matchMedia &&
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||||
if (dark) document.documentElement.classList.add("dark");
|
if (dark) document.documentElement.classList.add("dark");
|
||||||
|
var themeColor = document.querySelector('meta[name="theme-color"]');
|
||||||
|
if (themeColor) {
|
||||||
|
var color = themeColor.getAttribute(
|
||||||
|
dark ? "data-theme-color-dark" : "data-theme-color-light"
|
||||||
|
);
|
||||||
|
if (color) themeColor.setAttribute("content", color);
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,8 +4,12 @@
|
|||||||
"description": "nanobot AI assistant",
|
"description": "nanobot AI assistant",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#161618",
|
"background_color": "#ffffff",
|
||||||
"theme_color": "#161618",
|
"theme_color": "#ffffff",
|
||||||
|
"color_scheme_dark": {
|
||||||
|
"background_color": "#303030",
|
||||||
|
"theme_color": "#303030"
|
||||||
|
},
|
||||||
"orientation": "any",
|
"orientation": "any",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ function applyTheme(theme: Theme): void {
|
|||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
if (theme === "dark") root.classList.add("dark");
|
if (theme === "dark") root.classList.add("dark");
|
||||||
else root.classList.remove("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(): {
|
export function useTheme(): {
|
||||||
|
|||||||
@@ -12,4 +12,39 @@ describe("index.html", () => {
|
|||||||
expect(viewport).not.toContain("user-scalable=no");
|
expect(viewport).not.toContain("user-scalable=no");
|
||||||
expect(viewport).not.toMatch(/maximum-scale\s*=\s*1(?:\.0)?(?:,|$)/);
|
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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user