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.

This commit is contained in:
SJCoetz 2026-06-19 01:12:10 +02:00
parent 21722c70fb
commit 116a184e2b

View file

@ -31,7 +31,7 @@
// ---- styles (scoped under .cw-*) ---- // ---- styles (scoped under .cw-*) ----
var css = '' 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-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: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:focus-visible{outline:2px solid ' + BRAND.teal + ';outline-offset:3px;}'
+ '.cw-launcher svg{width:24px;height:24px;}' + '.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{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-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{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:focus{border-color:' + BRAND.teal + ';box-shadow:0 0 0 3px rgba(29,184,151,.15);}'
+ '.cw-form textarea::placeholder{color:' + BRAND.muted + ';}' + '.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:hover{background:#e25645;}'
+ '.cw-send:active{transform:translateY(1px);}' + '.cw-send:active{transform:translateY(1px);}'
+ '.cw-send:disabled{opacity:.5;cursor:not-allowed;}' + '.cw-send:disabled{opacity:.5;cursor:not-allowed;}'
+ '.cw-send svg{width:18px;height:18px;}' + '.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{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;}' + '.cw-foot a{color:' + BRAND.ink + ';text-decoration:underline;text-underline-offset:2px;}'
+ '@media (max-width:520px){' + '@media (max-width:640px){'
+ '.cw-root{right:12px;bottom:12px;}' + '.cw-root{right:12px;bottom:calc(12px + env(safe-area-inset-bottom,0px));}'
+ '.cw-panel{right:12px;left:12px;bottom:84px;width:auto;height:calc(100vh - 100px);border-radius:18px;}' + '.cw-launcher{width:56px;height:56px;}'
+ '.cw-launcher{width:54px;height:54px;}' + '.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){' + '@media (prefers-reduced-motion:reduce){'
+ '.cw-launcher,.cw-panel,.cw-msg{transition:none;animation:none;}' + '.cw-launcher,.cw-panel,.cw-msg{transition:none;animation:none;}'
@ -126,7 +134,7 @@
+ '<div class="cw-messages" role="log" aria-live="polite" aria-relevant="additions"></div>' + '<div class="cw-messages" role="log" aria-live="polite" aria-relevant="additions"></div>'
+ '<div class="cw-quick" aria-label="Suggested questions"></div>' + '<div class="cw-quick" aria-label="Suggested questions"></div>'
+ '<form class="cw-form" novalidate>' + '<form class="cw-form" novalidate>'
+ '<textarea class="cw-input" rows="1" placeholder="Ask about websites, AI agents, pricing…" aria-label="Your message"></textarea>' + '<textarea class="cw-input" rows="1" inputmode="text" enterkeyhint="send" autocapitalize="sentences" autocorrect="off" autocomplete="off" spellcheck="true" placeholder="Ask about websites, AI agents, pricing…" aria-label="Your message"></textarea>'
+ '<button type="submit" class="cw-send" aria-label="Send">' + ICON_SEND + '</button>' + '<button type="submit" class="cw-send" aria-label="Send">' + ICON_SEND + '</button>'
+ '</form>' + '</form>'
+ '<div class="cw-foot">powered by <a href="https://stackless.co.za">stackless</a> · <a href="contact.html">talk to a human</a></div>' + '<div class="cw-foot">powered by <a href="https://stackless.co.za">stackless</a> · <a href="contact.html">talk to a human</a></div>'
@ -237,8 +245,24 @@
panel.setAttribute('aria-hidden', 'false'); panel.setAttribute('aria-hidden', 'false');
launcher.setAttribute('aria-expanded', 'true'); launcher.setAttribute('aria-expanded', 'true');
launcher.style.transform = 'scale(.9)'; launcher.style.transform = 'scale(.9)';
setTimeout(function () { input.focus(); }, 200);
try { localStorage.setItem(POS_KEY, '1'); } catch (_) {} 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() { function close() {
@ -249,6 +273,21 @@
launcher.setAttribute('aria-expanded', 'false'); launcher.setAttribute('aria-expanded', 'false');
launcher.style.transform = ''; launcher.style.transform = '';
try { localStorage.setItem(POS_KEY, '0'); } catch (_) {} 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(); } function toggle() { isOpen ? close() : open(); }
@ -372,5 +411,31 @@
// ---- boot ---- // ---- boot ----
renderHistory(); renderHistory();
renderQuick(); 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);
}
})(); })();