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:
parent
21722c70fb
commit
116a184e2b
1 changed files with 75 additions and 10 deletions
|
|
@ -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 @@
|
|||
+ '<div class="cw-messages" role="log" aria-live="polite" aria-relevant="additions"></div>'
|
||||
+ '<div class="cw-quick" aria-label="Suggested questions"></div>'
|
||||
+ '<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>'
|
||||
+ '</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>'
|
||||
|
|
@ -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);
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
Loading…
Reference in a new issue