mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-14 06:33:09 -06:00
32ced3053d
* fix(ui): scope the Control UI cursor convention to app-like display modes
The app-chrome cursor convention from #103357/#103411 was applied
unconditionally, so an ordinary browser tab lost the pointer hand on
buttons, menus, tabs, rails, selects and accordion summaries — the only
hover affordance a page owns. The convention is correct for the installed
window the manifest declares ("display": "standalone"), not for every
window the same bundle is served into.
base.css now owns one policy token, --cursor-action, selected by display
mode: pointer by default, the desktop arrow under standalone, minimal-ui
and window-controls-overlay. A low-specificity rule maps generic
actionable controls onto that token, restoring the affordance on the
surfaces #103411 stripped bare, while every component rule that owns a
semantic cursor (not-allowed, disabled, grab, resize, zoom-in, text,
wait) keeps winning without !important.
The 92 cursor: pointer declarations that had drifted back into ui/src
since July now consume the token instead of hardcoding the hand, so they
stop contradicting the policy in an installed window. Real hyperlinks
keep the pointer in every mode. The pre-boot mount fallback repeats the
policy locally because it must render when the bundle fails to load.
Closes #121242
* test(ui): run the cursor policy browser test in the node-driven project
ui/vitest.config.ts routes Playwright-from-Node .browser.test.ts files to the
unit-node project; without registering the new cursor policy test there, the
in-browser chromium project tried to import it and failed on node:fs/playwright.
* fix(ui): keep the desktop arrow in the native app hosts
The macOS dashboard embeds the Control UI in a plain web view, which
reports `display-mode: browser`, so the display-mode-only policy would
have handed it the browser-tab pointer. It already announces itself with
`openclaw-native-macos`/`-nav`/`-web-chrome` on `<html>`, the same markers
`ui/src/styles/layout.css` matches on, so the policy reads those too.
474 lines
15 KiB
HTML
474 lines
15 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta
|
|
name="viewport"
|
|
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content"
|
|
/>
|
|
<title>OpenClaw Control</title>
|
|
<meta name="color-scheme" content="dark light" />
|
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
<link rel="manifest" href="/manifest.webmanifest" crossorigin="use-credentials" />
|
|
<script>
|
|
// Configure Zod before module evaluation so its JIT probe cannot violate strict CSP.
|
|
globalThis.__zod_globalConfig ??= {};
|
|
globalThis.__zod_globalConfig.jitless = true;
|
|
|
|
(function () {
|
|
var THEMES = { claw: 1, knot: 1, dash: 1 };
|
|
var MODES = { system: 1, light: 1, dark: 1 };
|
|
var LEGACY = {
|
|
dark: "claw:dark",
|
|
light: "claw:light",
|
|
openknot: "knot:dark",
|
|
fieldmanual: "dash:dark",
|
|
clawdash: "dash:light",
|
|
system: "claw:system",
|
|
};
|
|
try {
|
|
var keys = Object.keys(localStorage);
|
|
var raw;
|
|
for (var i = 0; i < keys.length; i++) {
|
|
if (keys[i].indexOf("openclaw.control.settings.v1") === 0) {
|
|
raw = localStorage.getItem(keys[i]);
|
|
if (raw) break;
|
|
}
|
|
}
|
|
if (!raw) return;
|
|
var s = JSON.parse(raw);
|
|
var t = s && s.theme;
|
|
var m = s && s.themeMode;
|
|
if (typeof t !== "string") t = "";
|
|
if (typeof m !== "string") m = "";
|
|
var legacy = LEGACY[t];
|
|
var theme = THEMES[t] ? t : legacy ? legacy.split(":")[0] : "claw";
|
|
var mode = MODES[m] ? m : legacy ? legacy.split(":")[1] : "system";
|
|
if (mode === "system") {
|
|
mode = window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
|
|
}
|
|
var resolved =
|
|
theme === "knot"
|
|
? mode === "light"
|
|
? "openknot-light"
|
|
: "openknot"
|
|
: theme === "dash"
|
|
? mode === "light"
|
|
? "dash-light"
|
|
: "dash"
|
|
: mode === "light"
|
|
? "light"
|
|
: "dark";
|
|
document.documentElement.setAttribute("data-theme", resolved);
|
|
var resolvedMode = resolved.includes("light") ? "light" : "dark";
|
|
document.documentElement.setAttribute("data-theme-mode", resolvedMode);
|
|
// Carapace CSS selects on [data-theme-resolved]; keep in lockstep.
|
|
document.documentElement.setAttribute("data-theme-resolved", resolvedMode);
|
|
} catch (e) {}
|
|
})();
|
|
</script>
|
|
<style>
|
|
/* Paint before the app stylesheet arrives so WebKit never exposes a white
|
|
document between its native dark canvas and the persisted UI theme. */
|
|
html,
|
|
body {
|
|
min-height: 100%;
|
|
}
|
|
|
|
html {
|
|
background: #0e1015;
|
|
color-scheme: dark;
|
|
}
|
|
|
|
html[data-theme-mode="light"] {
|
|
background: #faf9f7;
|
|
color-scheme: light;
|
|
}
|
|
|
|
html[data-theme="openknot"] {
|
|
background: #080808;
|
|
}
|
|
|
|
html[data-theme="openknot-light"] {
|
|
background: #f9f9fb;
|
|
}
|
|
|
|
html[data-theme="dash"] {
|
|
background: #1a1210;
|
|
}
|
|
|
|
html[data-theme="dash-light"] {
|
|
background: #f7f2ec;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
min-height: 100%;
|
|
background: inherit;
|
|
color-scheme: inherit;
|
|
}
|
|
|
|
body.openclaw-mount-fallback-active {
|
|
margin: 0;
|
|
min-width: 320px;
|
|
color: #eef4f8;
|
|
background: #101418;
|
|
font-family:
|
|
Inter,
|
|
ui-sans-serif,
|
|
system-ui,
|
|
-apple-system,
|
|
BlinkMacSystemFont,
|
|
"Segoe UI",
|
|
sans-serif;
|
|
}
|
|
|
|
body.openclaw-mount-fallback-active openclaw-app {
|
|
display: none;
|
|
}
|
|
|
|
.mount-fallback {
|
|
box-sizing: border-box;
|
|
/* The app stylesheet clips the root scrollers, so this pre-app error
|
|
surface must own its scrolling on short windows. dvh keeps the
|
|
scroll range inside the visible mobile viewport; vh is the fallback. */
|
|
height: 100vh;
|
|
height: 100dvh;
|
|
overflow-y: auto;
|
|
padding: 24px;
|
|
place-items: center;
|
|
place-items: safe center;
|
|
}
|
|
|
|
.mount-fallback:not([hidden]) {
|
|
display: grid;
|
|
}
|
|
|
|
.mount-fallback__panel {
|
|
box-sizing: border-box;
|
|
width: min(100%, 640px);
|
|
border: 1px solid rgba(148, 163, 184, 0.28);
|
|
border-radius: 8px;
|
|
background: #151b21;
|
|
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.36);
|
|
padding: 28px;
|
|
}
|
|
|
|
.mount-fallback__eyebrow {
|
|
margin: 0 0 10px;
|
|
color: #9fb0bd;
|
|
font-size: 0.78rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.mount-fallback__panel h1 {
|
|
margin: 0;
|
|
color: inherit;
|
|
font-size: clamp(1.5rem, 3vw, 2rem);
|
|
line-height: 1.15;
|
|
}
|
|
|
|
.mount-fallback__panel p {
|
|
margin: 16px 0 0;
|
|
color: #c8d3da;
|
|
font-size: 1rem;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.mount-fallback__panel ul {
|
|
margin: 18px 0 0;
|
|
padding-left: 1.2rem;
|
|
color: #c8d3da;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.mount-fallback__panel a {
|
|
color: #8bd3ff;
|
|
text-decoration-thickness: 0.08em;
|
|
text-underline-offset: 0.18em;
|
|
}
|
|
|
|
.mount-fallback__actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
margin-top: 24px;
|
|
}
|
|
|
|
/* The fallback renders when the bundle fails to load, so it cannot read
|
|
base.css tokens; it repeats the cursor policy locally for that reason. */
|
|
.mount-fallback__button {
|
|
cursor: pointer;
|
|
min-height: 42px;
|
|
border: 1px solid rgba(148, 163, 184, 0.36);
|
|
border-radius: 6px;
|
|
padding: 0 16px;
|
|
color: inherit;
|
|
background: transparent;
|
|
font: inherit;
|
|
font-weight: 700;
|
|
}
|
|
|
|
@media (display-mode: standalone),
|
|
(display-mode: minimal-ui),
|
|
(display-mode: window-controls-overlay) {
|
|
.mount-fallback__button {
|
|
cursor: default;
|
|
}
|
|
}
|
|
|
|
html:is(.openclaw-native-macos, .openclaw-native-nav, .openclaw-native-web-chrome)
|
|
.mount-fallback__button {
|
|
cursor: default;
|
|
}
|
|
|
|
.mount-fallback__button--primary {
|
|
border-color: #66c2ff;
|
|
color: #061019;
|
|
background: #8bd3ff;
|
|
}
|
|
|
|
.mount-fallback__panel:focus {
|
|
outline: none;
|
|
}
|
|
|
|
.mount-fallback__button:focus-visible,
|
|
.mount-fallback__panel a:focus-visible {
|
|
outline: 3px solid #f9c74f;
|
|
outline-offset: 3px;
|
|
}
|
|
|
|
html[data-theme-mode="light"] body.openclaw-mount-fallback-active {
|
|
color: #151b21;
|
|
background: #f5f7fa;
|
|
}
|
|
|
|
html[data-theme-mode="light"] .mount-fallback__panel {
|
|
border-color: rgba(71, 85, 105, 0.22);
|
|
background: #ffffff;
|
|
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.14);
|
|
}
|
|
|
|
html[data-theme-mode="light"] .mount-fallback__eyebrow,
|
|
html[data-theme-mode="light"] .mount-fallback__panel p,
|
|
html[data-theme-mode="light"] .mount-fallback__panel ul {
|
|
color: #4b5963;
|
|
}
|
|
|
|
html[data-theme-mode="light"] .mount-fallback__panel a {
|
|
color: #0369a1;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<openclaw-app></openclaw-app>
|
|
<section
|
|
id="openclaw-mount-fallback"
|
|
class="mount-fallback"
|
|
data-openclaw-mount-timeout-ms="12000"
|
|
role="alert"
|
|
aria-labelledby="openclaw-mount-fallback-title"
|
|
hidden
|
|
>
|
|
<div class="mount-fallback__panel" tabindex="-1">
|
|
<p class="mount-fallback__eyebrow">OpenClaw Control UI</p>
|
|
<h1 id="openclaw-mount-fallback-title">Control UI did not start</h1>
|
|
<p id="openclaw-mount-fallback-summary">
|
|
The browser loaded the static page, but the app bundle did not start. The gateway may be
|
|
restarting, or this page may reference assets from a different OpenClaw version.
|
|
</p>
|
|
<ul>
|
|
<li>OpenClaw will retry the current app bundle automatically.</li>
|
|
<li>If this persists, reload or try a clean browser profile.</li>
|
|
<li>
|
|
See
|
|
<a
|
|
href="https://docs.openclaw.ai/web/control-ui#blank-control-ui-page"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>Control UI troubleshooting</a
|
|
>.
|
|
</li>
|
|
</ul>
|
|
<div class="mount-fallback__actions">
|
|
<button
|
|
type="button"
|
|
id="openclaw-mount-retry"
|
|
class="mount-fallback__button mount-fallback__button--primary"
|
|
>
|
|
Try again
|
|
</button>
|
|
<button type="button" id="openclaw-mount-wait" class="mount-fallback__button">
|
|
Keep waiting
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<script>
|
|
(function () {
|
|
var tagName = "openclaw-app";
|
|
var app = document.querySelector(tagName);
|
|
var fallback = document.getElementById("openclaw-mount-fallback");
|
|
if (!app || !fallback) return;
|
|
|
|
var panel = fallback.querySelector(".mount-fallback__panel");
|
|
var summary = document.getElementById("openclaw-mount-fallback-summary");
|
|
var retry = document.getElementById("openclaw-mount-retry");
|
|
var wait = document.getElementById("openclaw-mount-wait");
|
|
var rawDelay = Number(fallback.getAttribute("data-openclaw-mount-timeout-ms"));
|
|
var delay = Number.isFinite(rawDelay) && rawDelay > 0 ? rawDelay : 12000;
|
|
var maxRecoveryAttempts = 6;
|
|
var timer;
|
|
var recoveryTimer;
|
|
var recoveryAttempt = 0;
|
|
var recoveryInFlight = false;
|
|
var recoveryNavigation = false;
|
|
|
|
try {
|
|
var initialUrl = new URL(window.location.href);
|
|
recoveryNavigation = initialUrl.searchParams.has("openclaw_mount_recovery");
|
|
if (recoveryNavigation) {
|
|
initialUrl.searchParams.delete("openclaw_mount_recovery");
|
|
window.history.replaceState(null, "", initialUrl.href);
|
|
}
|
|
} catch (e) {}
|
|
|
|
function appMounted() {
|
|
try {
|
|
return Boolean(
|
|
app.isConnected &&
|
|
window.customElements &&
|
|
typeof window.customElements.get === "function" &&
|
|
window.customElements.get(tagName),
|
|
);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function hideFallback() {
|
|
fallback.hidden = true;
|
|
document.body.classList.remove("openclaw-mount-fallback-active");
|
|
}
|
|
|
|
function setSummary(text) {
|
|
if (summary) summary.textContent = text;
|
|
}
|
|
|
|
function scheduleRecovery() {
|
|
window.clearTimeout(recoveryTimer);
|
|
if (appMounted() || recoveryAttempt >= maxRecoveryAttempts) return;
|
|
var retryDelay = Math.min(delay, 1000 * Math.pow(2, recoveryAttempt));
|
|
recoveryTimer = window.setTimeout(retryCurrentDocument, retryDelay);
|
|
}
|
|
|
|
function finishRecoveryAttempt() {
|
|
recoveryInFlight = false;
|
|
if (appMounted()) return;
|
|
if (recoveryAttempt >= maxRecoveryAttempts) {
|
|
setSummary(
|
|
"The gateway is still unavailable. Try again, then check the troubleshooting guide if the problem persists.",
|
|
);
|
|
return;
|
|
}
|
|
setSummary(
|
|
"The gateway is not reachable yet. OpenClaw will keep retrying while it restarts.",
|
|
);
|
|
scheduleRecovery();
|
|
}
|
|
|
|
function retryCurrentDocument() {
|
|
if (
|
|
appMounted() ||
|
|
recoveryInFlight ||
|
|
recoveryAttempt >= maxRecoveryAttempts ||
|
|
typeof window.fetch !== "function"
|
|
)
|
|
return;
|
|
var documentUrl = new URL(window.location.href);
|
|
if (recoveryNavigation) {
|
|
setSummary(
|
|
"A fresh page still could not start the Control UI. Try again, then check the troubleshooting guide if the problem persists.",
|
|
);
|
|
return;
|
|
}
|
|
recoveryInFlight = true;
|
|
recoveryAttempt += 1;
|
|
documentUrl.searchParams.set("openclaw_mount_recovery", String(Date.now()));
|
|
var recoveryController = new AbortController();
|
|
var requestTimer = window.setTimeout(function () {
|
|
recoveryController.abort();
|
|
}, delay);
|
|
window
|
|
.fetch(documentUrl.href, {
|
|
cache: "no-store",
|
|
credentials: "same-origin",
|
|
signal: recoveryController.signal,
|
|
})
|
|
.then(function (response) {
|
|
if (!response.ok) throw new Error("gateway unavailable");
|
|
window.location.replace(documentUrl.href);
|
|
})
|
|
.catch(function () {
|
|
finishRecoveryAttempt();
|
|
})
|
|
.finally(function () {
|
|
window.clearTimeout(requestTimer);
|
|
});
|
|
}
|
|
|
|
function showFallback() {
|
|
if (appMounted()) return;
|
|
retryCurrentDocument();
|
|
fallback.hidden = false;
|
|
document.body.classList.add("openclaw-mount-fallback-active");
|
|
if (panel && typeof panel.focus === "function") {
|
|
try {
|
|
panel.focus({ preventScroll: true });
|
|
} catch (e) {
|
|
panel.focus();
|
|
}
|
|
}
|
|
}
|
|
|
|
function armFallbackTimer() {
|
|
window.clearTimeout(timer);
|
|
timer = window.setTimeout(showFallback, delay);
|
|
}
|
|
|
|
armFallbackTimer();
|
|
|
|
if (window.customElements && typeof window.customElements.whenDefined === "function") {
|
|
window.customElements.whenDefined(tagName).then(
|
|
function () {
|
|
window.clearTimeout(timer);
|
|
window.clearTimeout(recoveryTimer);
|
|
hideFallback();
|
|
},
|
|
function () {},
|
|
);
|
|
}
|
|
|
|
if (retry) {
|
|
retry.addEventListener("click", function () {
|
|
window.location.reload();
|
|
});
|
|
}
|
|
if (wait) {
|
|
wait.addEventListener("click", function () {
|
|
hideFallback();
|
|
recoveryAttempt = 0;
|
|
retryCurrentDocument();
|
|
armFallbackTimer();
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
<script type="module" src="/src/main.ts"></script>
|
|
</body>
|
|
</html>
|