From 55e500f2dc8591971a47b5b562a495b22c2c5f8f Mon Sep 17 00:00:00 2001 From: Krzysztof Wende Date: Tue, 24 Mar 2026 22:23:38 +0100 Subject: [PATCH 1/4] Preserve host backgrounds for embedded widget --- hooks/useAppMode.ts | 7 ++++++- lib/widgetContext.ts | 1 + widget.tsx | 13 ++++++++++--- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/hooks/useAppMode.ts b/hooks/useAppMode.ts index 6c18f55..8c04d18 100644 --- a/hooks/useAppMode.ts +++ b/hooks/useAppMode.ts @@ -73,9 +73,14 @@ export function useAppMode(): AppMode { isDetachedRef.current = resolvedMode.isDetached; isNativeRef.current = resolvedMode.isNative; - if (resolvedMode.isDetached) { + const shouldUseTransparentHostBackground = widgetCtx?.transparentHostBackground !== false + + if (resolvedMode.isDetached && shouldUseTransparentHostBackground) { document.body.style.background = "transparent"; document.documentElement.style.background = "transparent"; + } else if (resolvedMode.isDetached) { + document.body.style.background = ""; + document.documentElement.style.background = ""; } if (resolvedMode.isNative) { diff --git a/lib/widgetContext.ts b/lib/widgetContext.ts index fc42816..7cd9349 100644 --- a/lib/widgetContext.ts +++ b/lib/widgetContext.ts @@ -5,6 +5,7 @@ export interface WidgetContextValue { noBorder: boolean; wsUrl: string | null; demo?: boolean; + transparentHostBackground?: boolean; } const WidgetContext = createContext(null); diff --git a/widget.tsx b/widget.tsx index 1b4feab..63f4637 100644 --- a/widget.tsx +++ b/widget.tsx @@ -12,13 +12,20 @@ export interface ChatWidgetProps { wsUrl?: string className?: string demo?: boolean + transparentHostBackground?: boolean } export const ChatWidget = forwardRef( - function ChatWidget({ wsUrl, className, demo }, ref) { + function ChatWidget({ wsUrl, className, demo, transparentHostBackground = true }, ref) { const modeValue = useMemo( - () => ({ isDetached: true, noBorder: true, wsUrl: wsUrl ?? null, demo: demo ?? false }), - [wsUrl, demo], + () => ({ + isDetached: true, + noBorder: true, + wsUrl: wsUrl ?? null, + demo: demo ?? false, + transparentHostBackground, + }), + [wsUrl, demo, transparentHostBackground], ) return ( From f3fe3ae6fa68bd51af5ee6c9ee936eb1c2060421 Mon Sep 17 00:00:00 2001 From: Krzysztof Wende Date: Wed, 25 Mar 2026 01:15:24 +0100 Subject: [PATCH 2/4] Add detached mobile Safari document-scroll chat mode --- app/globals.css | 13 ++ app/layout.tsx | 21 ++- app/page.tsx | 53 +++++- components/ChatInput.tsx | 71 +++++--- components/chat/ChatComposerBar.tsx | 62 ++++++- components/chat/ChatViewport.tsx | 91 ++++++---- ...os-safari-detached-document-scroll-chat.md | 71 ++++++++ hooks/chat/useOpenClawRuntime.ts | 19 +- hooks/useAppMode.ts | 2 +- hooks/useIsMobileViewport.ts | 25 +++ hooks/useScrollManager.ts | 170 ++++++++++++------ lib/chat/layout.ts | 24 +++ tests/layout.test.ts | 38 +++- 13 files changed, 539 insertions(+), 121 deletions(-) create mode 100644 dev-notes/2026-03-25_ios-safari-detached-document-scroll-chat.md create mode 100644 hooks/useIsMobileViewport.ts diff --git a/app/globals.css b/app/globals.css index 4b616a9..ccac33b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -128,6 +128,16 @@ --color-sidebar-ring: var(--sidebar-ring); } +[data-mobileclaw-embedded] { + --primary: oklch(0.313 0 0); + --primary-foreground: oklch(0.985 0 0); +} + +.dark [data-mobileclaw-embedded] { + --primary: oklch(0.258 0 0); + --primary-foreground: oklch(0.911 0 0); +} + @theme { /* Font-size scale — boosted so text-sm = 16px body baseline */ --text-2xs: 0.75rem; /* 12px */ @@ -148,6 +158,9 @@ * { @apply border-border outline-ring/50; } + html { + @apply bg-background text-foreground; + } body { @apply bg-background text-foreground; overflow: hidden; diff --git a/app/layout.tsx b/app/layout.tsx index b9f52c8..d9a9301 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -32,9 +32,15 @@ export const metadata: Metadata = { // Also registers the service worker for PWA support const headScript = ` (function() { + var isDev = ${JSON.stringify(process.env.NODE_ENV !== 'production')}; var params = new URLSearchParams(location.search); var detached = params.has('detached'); var detachedMode = detached ? params.get('mode') : null; + var host = location.hostname; + var isIpV4 = /^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(host); + var isLocalHost = host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' || host.endsWith('.local'); + var isPrivateLanIp = /^10\\./.test(host) || /^192\\.168\\./.test(host) || /^172\\.(1[6-9]|2\\d|3[0-1])\\./.test(host); + var shouldDisableServiceWorker = isDev || isLocalHost || (isIpV4 && isPrivateLanIp); try { if (detachedMode === 'dark') { document.documentElement.classList.add('dark'); @@ -53,8 +59,19 @@ const headScript = ` if (detached) { document.documentElement.classList.add('detached-loading'); } - if ('serviceWorker' in navigator && location.hostname !== 'localhost' && !window.__nativeMode) { + if ('serviceWorker' in navigator && !window.__nativeMode) { window.addEventListener('load', function() { + if (shouldDisableServiceWorker) { + navigator.serviceWorker.getRegistrations().then(function(registrations) { + registrations.forEach(function(registration) { registration.unregister(); }); + }); + if (window.caches && caches.keys) { + caches.keys().then(function(keys) { + keys.forEach(function(key) { caches.delete(key); }); + }); + } + return; + } navigator.serviceWorker.register('/sw.js'); }); } @@ -67,7 +84,7 @@ export default function RootLayout({ children: React.ReactNode }>) { return ( - +