From 116a184e2bc26b02da39c5c5c8f9604249821948 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Fri, 19 Jun 2026 01:12:10 +0200 Subject: [PATCH] Enhance chat widget styles and functionality for better mobile experience. Added touch-action properties, improved textarea input attributes, and implemented background scroll locking when the chat panel is open. --- chat-widget.js | 85 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 75 insertions(+), 10 deletions(-) diff --git a/chat-widget.js b/chat-widget.js index b600911..77bf25c 100644 --- a/chat-widget.js +++ b/chat-widget.js @@ -31,7 +31,7 @@ // ---- styles (scoped under .cw-*) ---- var css = '' + '.cw-root{position:fixed;right:20px;bottom:20px;z-index:1000;font-family:Inter,-apple-system,BlinkMacSystemFont,system-ui,sans-serif;color:' + BRAND.ink + ';}' - + '.cw-launcher{width:58px;height:58px;border-radius:50%;background:' + BRAND.ink + ';border:0;box-shadow:0 12px 30px -10px rgba(28,36,57,.45),0 0 0 1px rgba(255,255,255,.04) inset;cursor:pointer;display:grid;place-items:center;transition:transform .2s ease,background .2s ease,box-shadow .2s ease;color:#fff;}' + + '.cw-launcher{width:58px;height:58px;border-radius:50%;background:' + BRAND.ink + ';border:0;box-shadow:0 12px 30px -10px rgba(28,36,57,.45),0 0 0 1px rgba(255,255,255,.04) inset;cursor:pointer;display:grid;place-items:center;transition:transform .2s ease,background .2s ease,box-shadow .2s ease;color:#fff;touch-action:manipulation;-webkit-tap-highlight-color:transparent;}' + '.cw-launcher:hover{transform:translateY(-2px);box-shadow:0 18px 38px -10px rgba(28,36,57,.55);}' + '.cw-launcher:focus-visible{outline:2px solid ' + BRAND.teal + ';outline-offset:3px;}' + '.cw-launcher svg{width:24px;height:24px;}' @@ -68,20 +68,28 @@ + '.cw-quick button{background:' + BRAND.surface + ';border:1px solid ' + BRAND.border + ';border-radius:999px;padding:7px 12px;font:500 12.5px/1 Inter,system-ui,sans-serif;color:' + BRAND.ink + ';cursor:pointer;transition:background .15s,border-color .15s,color .15s;}' + '.cw-quick button:hover{background:' + BRAND.ink + ';color:#fff;border-color:' + BRAND.ink + ';}' + '.cw-form{display:flex;gap:8px;align-items:flex-end;padding:12px 14px;border-top:1px solid ' + BRAND.border + ';background:' + BRAND.surface + ';}' - + '.cw-form textarea{flex:1;resize:none;border:1px solid ' + BRAND.border + ';border-radius:14px;padding:10px 12px;font:400 14px/1.5 Inter,system-ui,sans-serif;color:' + BRAND.ink + ';background:' + BRAND.surface + ';outline:none;min-height:42px;max-height:120px;transition:border-color .15s,box-shadow .15s;}' + + '.cw-form textarea{flex:1;resize:none;border:1px solid ' + BRAND.border + ';border-radius:14px;padding:10px 12px;font:400 16px/1.5 Inter,system-ui,sans-serif;color:' + BRAND.ink + ';background:' + BRAND.surface + ';outline:none;min-height:42px;max-height:120px;transition:border-color .15s,box-shadow .15s;touch-action:manipulation;}' + '.cw-form textarea:focus{border-color:' + BRAND.teal + ';box-shadow:0 0 0 3px rgba(29,184,151,.15);}' + '.cw-form textarea::placeholder{color:' + BRAND.muted + ';}' - + '.cw-send{width:42px;height:42px;border-radius:12px;background:' + BRAND.coral + ';color:#fff;border:0;cursor:pointer;display:grid;place-items:center;flex:none;transition:background .15s,transform .12s;}' + + '.cw-send{width:44px;height:44px;border-radius:12px;background:' + BRAND.coral + ';color:#fff;border:0;cursor:pointer;display:grid;place-items:center;flex:none;transition:background .15s,transform .12s;touch-action:manipulation;-webkit-tap-highlight-color:transparent;}' + '.cw-send:hover{background:#e25645;}' + '.cw-send:active{transform:translateY(1px);}' + '.cw-send:disabled{opacity:.5;cursor:not-allowed;}' + '.cw-send svg{width:18px;height:18px;}' + '.cw-foot{padding:8px 16px 12px;background:' + BRAND.surface + ';border-top:1px solid ' + BRAND.border + ';font:500 11px/1.4 "JetBrains Mono",ui-monospace,Menlo,monospace;letter-spacing:.04em;color:' + BRAND.muted + ';text-align:center;}' + '.cw-foot a{color:' + BRAND.ink + ';text-decoration:underline;text-underline-offset:2px;}' - + '@media (max-width:520px){' - + '.cw-root{right:12px;bottom:12px;}' - + '.cw-panel{right:12px;left:12px;bottom:84px;width:auto;height:calc(100vh - 100px);border-radius:18px;}' - + '.cw-launcher{width:54px;height:54px;}' + + '@media (max-width:640px){' + + '.cw-root{right:12px;bottom:calc(12px + env(safe-area-inset-bottom,0px));}' + + '.cw-launcher{width:56px;height:56px;}' + + '.cw-panel{right:0;left:0;top:0;bottom:0;width:100%;height:100vh;height:100dvh;max-height:100dvh;border-radius:0;transform:translateY(100%);transform-origin:bottom center;}' + + '.cw-panel.is-open{transform:translateY(0);}' + + '.cw-panel::before{content:"";position:absolute;top:max(8px,env(safe-area-inset-top,0px));left:50%;transform:translateX(-50%);width:36px;height:4px;background:rgba(255,255,255,.32);border-radius:2px;z-index:2;pointer-events:none;}' + + '.cw-head{padding-top:calc(16px + env(safe-area-inset-top,0px));}' + + '.cw-form{padding-bottom:calc(10px + env(safe-area-inset-bottom,0px));}' + + '.cw-foot{padding-bottom:calc(10px + env(safe-area-inset-bottom,0px));}' + + '.cw-title{font-size:15.5px;}' + + '.cw-msg{font-size:15px;}' + + '.cw-quick button{padding:9px 14px;font-size:13px;}' + '}' + '@media (prefers-reduced-motion:reduce){' + '.cw-launcher,.cw-panel,.cw-msg{transition:none;animation:none;}' @@ -126,7 +134,7 @@ + '
' + '
' + '
' - + '' + + '' + '' + '
' + '
powered by stackless · talk to a human
' @@ -237,8 +245,24 @@ panel.setAttribute('aria-hidden', 'false'); launcher.setAttribute('aria-expanded', 'true'); launcher.style.transform = 'scale(.9)'; - setTimeout(function () { input.focus(); }, 200); try { localStorage.setItem(POS_KEY, '1'); } catch (_) {} + // Lock background scroll so the page behind the panel doesn't move + try { + var scrollY = window.scrollY || window.pageYOffset || 0; + panel.dataset.lockedScroll = String(scrollY); + document.body.style.overflow = 'hidden'; + document.documentElement.style.overflow = 'hidden'; + document.body.style.position = 'fixed'; + document.body.style.top = '-' + scrollY + 'px'; + document.body.style.left = '0'; + document.body.style.right = '0'; + } catch (_) {} + // Only auto-focus on desktop — on mobile that would summon the keyboard + // before the user has even seen the panel. + var isMobile = window.matchMedia('(max-width: 640px)').matches; + if (!isMobile) { + setTimeout(function () { input.focus(); }, 200); + } } function close() { @@ -249,6 +273,21 @@ launcher.setAttribute('aria-expanded', 'false'); launcher.style.transform = ''; try { localStorage.setItem(POS_KEY, '0'); } catch (_) {} + // Release background scroll and restore the scroll position + try { + var y = parseInt(panel.dataset.lockedScroll || '0', 10) || 0; + document.body.style.overflow = ''; + document.documentElement.style.overflow = ''; + document.body.style.position = ''; + document.body.style.top = ''; + document.body.style.left = ''; + document.body.style.right = ''; + window.scrollTo(0, y); + delete panel.dataset.lockedScroll; + } catch (_) {} + // Clear any keyboard-driven panel resizing + panel.style.height = ''; + panel.style.top = ''; } function toggle() { isOpen ? close() : open(); } @@ -372,5 +411,31 @@ // ---- boot ---- renderHistory(); renderQuick(); - try { if (localStorage.getItem(POS_KEY) === '1') open(); } catch (_) {} + // Only auto-reopen the panel on desktop — on mobile this would slam a + // fullscreen overlay over the page on every reload. + try { + if (localStorage.getItem(POS_KEY) === '1' && !window.matchMedia('(max-width: 640px)').matches) open(); + } catch (_) {} + + // ---- visual viewport (mobile keyboard handling) ---- + // When the soft keyboard opens on mobile, shrink the panel to the visible + // viewport so the input stays above the keyboard. The panel is full-screen + // on mobile, so we just resize it to the visible height. + if (window.visualViewport) { + var vv = window.visualViewport; + function handleVV() { + if (!isOpen) return; + if (!window.matchMedia('(max-width: 640px)').matches) return; + // vv.height shrinks when the keyboard appears; vv.offsetTop rises. + panel.style.height = vv.height + 'px'; + panel.style.top = vv.offsetTop + 'px'; + // Keep the latest message visible above the keyboard. + var wasAtBottom = messages.scrollHeight - messages.scrollTop - messages.clientHeight < 80; + if (wasAtBottom) { + requestAnimationFrame(function () { messages.scrollTop = messages.scrollHeight; }); + } + } + vv.addEventListener('resize', handleVV); + vv.addEventListener('scroll', handleVV); + } })();