diff --git a/CHANGELOG.md b/CHANGELOG.md
index b8ef443e..d871e8d8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
# Changelog
-## Unreleased
+## 19.2.0
+Surveys 2.0: `Gleap.showSurvey(surveyId, options)` takes `{ format: 'card' | 'full' | 'page', fields, personalToken, container, resume }`; the old format strings (`'survey'`, `'survey_full'`, `'survey_web'`) keep working. A survey shown before the SDK is ready now opens once it is.
+New survey containers: the card is a 348px corner popover with an enter animation and becomes a bottom sheet (handle, scrim, home-indicator inset) on phones; full screen covers the whole viewport and the survey draws its own background and close button (no more centred 640px card over a blurred backdrop); `page` renders inline in `container` (or `document.body`) and grows with the survey. Card and page surveys only appear once their height is known. Reduced motion is respected. Legacy surveys (not yet migrated) keep the previous card and full-screen look.
+New events `survey-shown`, `survey-answered`, `survey-completed` and `survey-closed` (with `surveyId`, `version`, `responseId`, …); `outbound-sent` keeps firing as before.
+New standalone `survey-embed.js` (~3 KB): `
` loads the SDK on demand and shows the survey inline, as a popup or behind a side tab.
Fixed: inline banners did not show on pages whose `body` clips its overflow, the usual app-shell layout (`html, body { height: 100%; overflow: hidden }`). The banner sits in the body's top margin, which the body cut off, so only an empty strip appeared above the page. On such pages the banner now goes into the body's top padding (added to the page's own padding), which the body does not clip; with `box-sizing: border-box` the app also keeps fitting the viewport instead of losing the banner's height at the bottom. Pages that scroll normally and floating banners are unchanged.
## 19.1.1
diff --git a/build/browser/index.js b/build/browser/index.js
index 4a2d5006..ba417028 100644
--- a/build/browser/index.js
+++ b/build/browser/index.js
@@ -1,2 +1,2 @@
-(()=>{var e={d:(t,n)=>{for(var r in n)e.o(n,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:n[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};(()=>{"use strict";function n(e){return n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},n(e)}function r(){r=function(){return t};var e,t={},o=Object.prototype,i=o.hasOwnProperty,a=Object.defineProperty||function(e,t,n){e[t]=n.value},s="function"==typeof Symbol?Symbol:{},c=s.iterator||"@@iterator",l=s.asyncIterator||"@@asyncIterator",u=s.toStringTag||"@@toStringTag";function d(e,t,n){return Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}),e[t]}try{d({},"")}catch(e){d=function(e,t,n){return e[t]=n}}function p(e,t,n,r){var o=t&&t.prototype instanceof b?t:b,i=Object.create(o.prototype),s=new R(r||[]);return a(i,"_invoke",{value:L(e,n,s)}),i}function h(e,t,n){try{return{type:"normal",arg:e.call(t,n)}}catch(e){return{type:"throw",arg:e}}}t.wrap=p;var f="suspendedStart",g="suspendedYield",m="executing",v="completed",y={};function b(){}function w(){}function k(){}var x={};d(x,c,(function(){return this}));var S=Object.getPrototypeOf,C=S&&S(S(P([])));C&&C!==o&&i.call(C,c)&&(x=C);var E=k.prototype=b.prototype=Object.create(x);function I(e){["next","throw","return"].forEach((function(t){d(e,t,(function(e){return this._invoke(t,e)}))}))}function T(e,t){function r(o,a,s,c){var l=h(e[o],e,a);if("throw"!==l.type){var u=l.arg,d=u.value;return d&&"object"==n(d)&&i.call(d,"__await")?t.resolve(d.__await).then((function(e){r("next",e,s,c)}),(function(e){r("throw",e,s,c)})):t.resolve(d).then((function(e){u.value=e,s(u)}),(function(e){return r("throw",e,s,c)}))}c(l.arg)}var o;a(this,"_invoke",{value:function(e,n){function i(){return new t((function(t,o){r(e,n,t,o)}))}return o=o?o.then(i,i):i()}})}function L(t,n,r){var o=f;return function(i,a){if(o===m)throw new Error("Generator is already running");if(o===v){if("throw"===i)throw a;return{value:e,done:!0}}for(r.method=i,r.arg=a;;){var s=r.delegate;if(s){var c=O(s,r);if(c){if(c===y)continue;return c}}if("next"===r.method)r.sent=r._sent=r.arg;else if("throw"===r.method){if(o===f)throw o=v,r.arg;r.dispatchException(r.arg)}else"return"===r.method&&r.abrupt("return",r.arg);o=m;var l=h(t,n,r);if("normal"===l.type){if(o=r.done?v:g,l.arg===y)continue;return{value:l.arg,done:r.done}}"throw"===l.type&&(o=v,r.method="throw",r.arg=l.arg)}}}function O(t,n){var r=n.method,o=t.iterator[r];if(o===e)return n.delegate=null,"throw"===r&&t.iterator.return&&(n.method="return",n.arg=e,O(t,n),"throw"===n.method)||"return"!==r&&(n.method="throw",n.arg=new TypeError("The iterator does not provide a '"+r+"' method")),y;var i=h(o,t.iterator,n.arg);if("throw"===i.type)return n.method="throw",n.arg=i.arg,n.delegate=null,y;var a=i.arg;return a?a.done?(n[t.resultName]=a.value,n.next=t.nextLoc,"return"!==n.method&&(n.method="next",n.arg=e),n.delegate=null,y):a:(n.method="throw",n.arg=new TypeError("iterator result is not an object"),n.delegate=null,y)}function A(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function M(e){var t=e.completion||{};t.type="normal",delete t.arg,e.completion=t}function R(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(A,this),this.reset(!0)}function P(t){if(t||""===t){var r=t[c];if(r)return r.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var o=-1,a=function n(){for(;++o
=0;--o){var a=this.tryEntries[o],s=a.completion;if("root"===a.tryLoc)return r("end");if(a.tryLoc<=this.prev){var c=i.call(a,"catchLoc"),l=i.call(a,"finallyLoc");if(c&&l){if(this.prev=0;--n){var r=this.tryEntries[n];if(r.tryLoc<=this.prev&&i.call(r,"finallyLoc")&&this.prev=0;--t){var n=this.tryEntries[t];if(n.finallyLoc===e)return this.complete(n.completion,n.afterLoc),M(n),y}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.tryLoc===e){var r=n.completion;if("throw"===r.type){var o=r.arg;M(n)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,n,r){return this.delegate={iterator:P(t),resultName:n,nextLoc:r},"next"===this.method&&(this.arg=e),y}},t}function o(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!n){if(Array.isArray(e)||(n=function(e,t){if(!e)return;if("string"==typeof e)return i(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return i(e,t)}(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var r=0,o=function(){};return{s:o,n:function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,s=!0,c=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return s=e.done,e},e:function(e){c=!0,a=e},f:function(){try{s||null==n.return||n.return()}finally{if(c)throw a}}}}function i(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);ney});var s=function(e,t,n){var r,o,i,a,s,c,l;switch(e){case"meta":null===(r=n.onMeta)||void 0===r||r.call(n,t);break;case"token":null===(o=n.onToken)||void 0===o||o.call(n,t);break;case"tool_start":null===(i=n.onToolStart)||void 0===i||i.call(n,t);break;case"tool_end":null===(a=n.onToolEnd)||void 0===a||a.call(n,t);break;case"done":null===(s=n.onDone)||void 0===s||s.call(n,t);break;case"error":null===(c=n.onError)||void 0===c||c.call(n,t);break;case"handoff":null===(l=n.onHandoff)||void 0===l||l.call(n,t)}},c=function(){var e,t=(e=r().mark((function e(t,n){var i,a,c,l,u,d,p,h,f,g,m,v,y,b,w,k,x,S;return r().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(t.body){e.next=3;break}return null===(i=n.onError)||void 0===i||i.call(n,{message:"No response body."}),e.abrupt("return");case 3:a=t.body.getReader(),c=new TextDecoder,l="",u="",d="",e.prev=8;case 9:return e.next=12,a.read();case 12:if(h=e.sent,f=h.done,g=h.value,!f){e.next=17;break}return e.abrupt("break",24);case 17:l+=c.decode(g,{stream:!0}),m=l.split("\n"),l=null!==(p=m.pop())&&void 0!==p?p:"",v=o(m);try{for(v.s();!(y=v.n()).done;)if(b=y.value,(w=b.replace(/\r$/,"")).startsWith("event: "))u=w.slice(7).trim();else if(w.startsWith("data: "))d+=(d?"\n":"")+w.slice(6);else if(""===w){if(u&&d)try{k=JSON.parse(d),s(u,k,n)}catch(r){}u="",d=""}}catch(C){v.e(C)}finally{v.f()}e.next=9;break;case 24:if(u&&d)try{x=JSON.parse(d),s(u,x,n)}catch(E){}e.next=30;break;case 27:e.prev=27,e.t0=e.catch(8),"AbortError"!==(null===e.t0||void 0===e.t0?void 0:e.t0.name)&&(null===(S=n.onError)||void 0===S||S.call(n,{message:"Stream interrupted."}));case 30:case"end":return e.stop()}}),e,null,[[8,27]])})),function(){var t=this,n=arguments;return new Promise((function(r,o){var i=e.apply(t,n);function s(e){a(i,r,o,s,c,"next",e)}function c(e){a(i,r,o,s,c,"throw",e)}s(void 0)}))});return function(e,n){return t.apply(this,arguments)}}();function l(e){return l="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},l(e)}function u(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!n){if(Array.isArray(e)||(n=function(e,t){if(!e)return;if("string"==typeof e)return d(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return d(e,t)}(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var r=0,o=function(){};return{s:o,n:function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,a=!0,s=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return a=e.done,e},e:function(e){s=!0,i=e},f:function(){try{a||null==n.return||n.return()}finally{if(s)throw i}}}}function d(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0;--i){var a=this.tryEntries[i],s=a.completion;if("root"===a.tryLoc)return o("end");if(a.tryLoc<=this.prev){var c=r.call(a,"catchLoc"),l=r.call(a,"finallyLoc");if(c&&l){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--t){var n=this.tryEntries[t];if(n.finallyLoc===e)return this.complete(n.completion,n.afterLoc),M(n),y}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.tryLoc===e){var r=n.completion;if("throw"===r.type){var o=r.arg;M(n)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,n,r){return this.delegate={iterator:P(t),resultName:n,nextLoc:r},"next"===this.method&&(this.arg=e),y}},t}function h(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function f(e){for(var t=1;t=200&&i.status<300)try{r(i.responseText?JSON.parse(i.responseText):null)}catch(e){o({status:i.status,statusText:"JSON Parse Error",error:e})}else o({status:i.status,statusText:i.statusText,responseText:i.responseText})},i.onerror=function(){return o({status:0,statusText:"Network Error"})},i.send(n?JSON.stringify(n):null)}))}},{key:"streamAgent",value:(o=p().mark((function e(t,n,r,o){var i,a,s,l,u,d,h;return p().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(!this.executeInFlight.get(t)){e.next=2;break}return e.abrupt("return");case 2:if(this.executeInFlight.set(t,!0),i=this._getApiUrl()){e.next=8;break}return this.executeInFlight.delete(t),null===(a=r.onError)||void 0===a||a.call(r,{message:"Gleap API URL not configured."}),e.abrupt("return");case 8:return s="".concat(i,"/v3/shared/agents/").concat(t,"/stream"),l=f(f({},this._getHeaders()),{},{Accept:"text/event-stream"}),e.prev=10,e.next=13,fetch(s,{method:"POST",headers:l,body:JSON.stringify(n),signal:o});case 13:u=e.sent,e.next=23;break;case 16:if(e.prev=16,e.t0=e.catch(10),this.executeInFlight.delete(t),"AbortError"!==(null===e.t0||void 0===e.t0?void 0:e.t0.name)){e.next=21;break}return e.abrupt("return");case 21:return null===(d=r.onError)||void 0===d||d.call(r,{message:"Failed to connect to agent."}),e.abrupt("return");case 23:if(u.ok){e.next=27;break}return this.executeInFlight.delete(t),null===(h=r.onError)||void 0===h||h.call(r,{message:"Server error: ".concat(u.status)}),e.abrupt("return");case 27:return e.next=29,c(u,r);case 29:n.conversationId&&this._invalidateMessagesCache(n.conversationId),this.executeInFlight.delete(t);case 31:case"end":return e.stop()}}),e,this,[[10,16]])})),i=function(){var e=this,t=arguments;return new Promise((function(n,r){var i=o.apply(e,t);function a(e){g(i,n,r,a,s,"next",e)}function s(e){g(i,n,r,a,s,"throw",e)}a(void 0)}))},function(e,t,n,r){return i.apply(this,arguments)})},{key:"executeAgent",value:function(e,t){var n=this;if(this.executeInFlight.get(e))return Promise.reject(new Error("Agent execution already in progress."));var r=this._getApiUrl();if(!r)return Promise.reject(new Error("AgentNetworkManager: Gleap API URL not configured."));this.executeInFlight.set(e,!0);var o="".concat(r,"/v3/shared/agents/").concat(e,"/execute");return this._makeRequest("POST",o,t).then((function(e){return t.conversationId&&n._invalidateMessagesCache(t.conversationId),null!=e&&e.conversationId&&n._invalidateMessagesCache(e.conversationId),e})).finally((function(){n.executeInFlight.delete(e)}))}},{key:"getConversationMessages",value:function(e,t){var n=this,r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:50,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:0;if("kai"===e)return Promise.resolve({messages:[]});var i="".concat(t,":").concat(r,":").concat(o),a=this.messagesCache.get(i);if(a)return a.status===b?Promise.resolve(JSON.parse(JSON.stringify(a.data))):Promise.reject(a.error);if(this.messagesRequests.has(i))return this.messagesRequests.get(i).then((function(e){return JSON.parse(JSON.stringify(e))}));var s=this._getApiUrl();if(!s)return Promise.reject(new Error("AgentNetworkManager: Gleap API URL not configured."));var c="".concat(s,"/v3/shared/agents/").concat(e,"/conversations/").concat(t,"/messages?limit=").concat(r,"&skip=").concat(o),l=this._makeRequest("GET",c,null).then((function(e){return n.messagesCache.set(i,{status:b,data:e}),JSON.parse(JSON.stringify(e))})).catch((function(e){throw n.messagesCache.set(i,{status:w,error:e}),e})).finally((function(){n.messagesRequests.delete(i)}));return this.messagesRequests.set(i,l),l.then((function(e){return JSON.parse(JSON.stringify(e))}))}},{key:"getConversations",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:10,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;if("kai"===e)return Promise.resolve({conversations:[]});var r=this._getApiUrl();return r?this._makeRequest("GET","".concat(r,"/v3/shared/agents/").concat(e,"/conversations?limit=").concat(t,"&skip=").concat(n),null):Promise.reject(new Error("AgentNetworkManager: Gleap API URL not configured."))}},{key:"_invalidateMessagesCache",value:function(e){var t,n=u(this.messagesCache.keys());try{for(n.s();!(t=n.n()).done;){var r=t.value;r.startsWith(e+":")&&this.messagesCache.delete(r)}}catch(s){n.e(s)}finally{n.f()}var o,i=u(this.messagesRequests.keys());try{for(i.s();!(o=i.n()).done;){var a=o.value;a.startsWith(e+":")&&this.messagesRequests.delete(a)}}catch(s){i.e(s)}finally{i.f()}}}],r=[{key:"getInstance",value:function(){return e.instance||(e.instance=new e),e.instance}}],n&&m(t.prototype,n),r&&m(t,r),Object.defineProperty(t,"prototype",{writable:!1}),e}();v(k,"instance",null);const x=k;function S(e){return S="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},S(e)}function C(e,t){for(var n=0;n=200&&i.status<300)try{var e=i.responseText?JSON.parse(i.responseText):null;r(e)}catch(t){o({status:i.status,statusText:"JSON Parse Error",responseText:i.responseText,error:t})}else o({status:i.status,statusText:i.statusText,responseText:i.responseText})},i.onerror=function(){o({status:0,statusText:"Network Error",responseText:null})},i.send(n?JSON.stringify(n):null)}))}},{key:"_createCacheKey",value:function(e,t){return t?"".concat(e,"::").concat(t):e}},{key:"validateChecklist",value:function(e,t){var n=this,r=this._createCacheKey(e,t),o=this.validationCache.get(r);if(o)return o.status===T?Promise.resolve(o.internalId):Promise.reject(o.error);if(this.validationRequests.has(r))return this.validationRequests.get(r);var i=this._getApiUrl();if(!i){var a=new Error("ChecklistNetworkManager: Gleap API URL not configured.");return this.validationCache.set(r,{status:L,error:a}),Promise.reject(a)}var s="".concat(i,"/outbound/checklists?").concat(this._getQueryParams()),c=this._makeRequest("POST",s,{outboundId:e,sharedKey:t}).then((function(e){if(e&&e.id)return n.validationCache.set(r,{status:T,internalId:e.id}),e.id;var t=new Error("Validation response missing checklist ID.");throw n.validationCache.set(r,{status:L,error:e||t}),t})).catch((function(e){throw n.validationCache.set(r,{status:L,error:e}),e})).finally((function(){n.validationRequests.delete(r)}));return this.validationRequests.set(r,c),c}},{key:"fetchChecklist",value:function(e){var t=this,n=this.fetchCache.get(e);if(n)return n.status===T?Promise.resolve(JSON.parse(JSON.stringify(n.data))):Promise.reject(n.error);if(this.fetchRequests.has(e))return this.fetchRequests.get(e).then((function(e){return JSON.parse(JSON.stringify(e))}));var r=this._getApiUrl();if(!r){var o=new Error("ChecklistNetworkManager: Gleap API URL not configured.");return this.fetchCache.set(e,{status:L,error:o}),Promise.reject(o)}var i="".concat(r,"/outbound/checklists/").concat(e,"?convertTipTap=true&").concat(this._getQueryParams()),a=this._makeRequest("GET",i,null).then((function(n){if(n)return t.fetchCache.set(e,{status:T,data:n}),JSON.parse(JSON.stringify(n));var r=new Error("Empty response received for checklist fetch.");throw t.fetchCache.set(e,{status:L,error:n||r}),r})).catch((function(n){throw t.fetchCache.set(e,{status:L,error:n}),n})).finally((function(){t.fetchRequests.delete(e)}));return this.fetchRequests.set(e,a),a.then((function(e){return JSON.parse(JSON.stringify(e))}))}}])&&C(t.prototype,n),r&&C(t,r),Object.defineProperty(t,"prototype",{writable:!1}),e}();E(O,"instance",null);const A=O;function M(e){return M="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},M(e)}function R(e,t){for(var n=0;n0&&!Zf.getInstance().isOpened()?(t.classList.remove(n),t.innerText=e):t.classList.add(n)}}},{key:"refresh",value:function(){document.querySelector(".bb-feedback-button")?(this.updateFeedbackButtonText(),this.updateFeedbackButtonState()):(this.injectedFeedbackButton=!1,this.feedbackButton=null,this.buttonHidden=null,this.lastButtonIcon=null,this.injectFeedbackButton())}},{key:"updateFeedbackButtonText",value:function(){var t=Xu.getInstance().getFlowConfig();if(t.feedbackButtonPosition===e.FEEDBACK_BUTTON_CLASSIC||t.feedbackButtonPosition===e.FEEDBACK_BUTTON_CLASSIC_BOTTOM||t.feedbackButtonPosition===e.FEEDBACK_BUTTON_CLASSIC_LEFT){var n=document.querySelector(".bb-feedback-button-classic");n&&(n.innerText=t.widgetButtonText)}}},{key:"updateFeedbackButtonState",value:function(){var t=this;if(Nn.getInstance().updateUIVisibility(),null!==this.feedbackButton){var n=Xu.getInstance().getFlowConfig(),r="";r=n.buttonLogo?'
'):he("button","#fff");var o=["bb-feedback-button--pressed","bb-feedback-button--springing"].filter((function(e){return t.feedbackButton.classList.contains(e)}));this.feedbackButton.className="bb-feedback-button gleap-font rr-block",o.forEach((function(e){return t.feedbackButton.classList.add(e)})),this.feedbackButton.setAttribute("dir",Hv.getInstance().isRTLLayout?"rtl":"ltr"),n.feedbackButtonPosition===e.FEEDBACK_BUTTON_CLASSIC||n.feedbackButtonPosition===e.FEEDBACK_BUTTON_CLASSIC_BOTTOM||n.feedbackButtonPosition===e.FEEDBACK_BUTTON_CLASSIC_LEFT?(this.feedbackButton.classList.add("bb-feedback-button--classic-button-style"),this.feedbackButton.innerHTML='').concat(n.widgetButtonText,"
")):r!==this.lastButtonIcon&&(this.feedbackButton.innerHTML=''.concat(r).concat(he("arrowdown","#fff"),'
')),this.lastButtonIcon=r;var i=!1;null===e.getInstance().buttonHidden?n.feedbackButtonPosition===e.FEEDBACK_BUTTON_NONE&&(i=!0):e.getInstance().buttonHidden&&(i=!0),i&&this.feedbackButton.classList.add("bb-feedback-button--disabled"),n.feedbackButtonPosition===e.FEEDBACK_BUTTON_BOTTOM_LEFT&&this.feedbackButton.classList.add("bb-feedback-button--bottomleft"),Zf.getInstance().isOpened()&&this.feedbackButton.classList.add("bb-feedback-button--open");var a=Zf.getInstance().appMode;"survey"!==a&&"survey_full"!==a&&"survey_web"!==a||this.feedbackButton.classList.add("bb-feedback-button--survey"),!0!==n.hideForGuests||Tf.getInstance().isUser()||this.feedbackButton.classList.add("bb-feedback-button--hidden")}}}])&&R(t.prototype,n),r&&R(t,r),Object.defineProperty(t,"prototype",{writable:!1}),e}();function j(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function _(e,t,n){return t=function(e){var t=function(e,t){if("object"!=F(e)||!e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var r=n.call(e,t||"default");if("object"!=F(r))return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==F(t)?t:String(t)}(t),t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function F(e){return F="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},F(e)}P(N,"FEEDBACK_BUTTON_BOTTOM_RIGHT","BOTTOM_RIGHT"),P(N,"FEEDBACK_BUTTON_BOTTOM_LEFT","BOTTOM_LEFT"),P(N,"FEEDBACK_BUTTON_CLASSIC","BUTTON_CLASSIC"),P(N,"FEEDBACK_BUTTON_CLASSIC_LEFT","BUTTON_CLASSIC_LEFT"),P(N,"FEEDBACK_BUTTON_CLASSIC_BOTTOM","BUTTON_CLASSIC_BOTTOM"),P(N,"FEEDBACK_BUTTON_NONE","BUTTON_NONE"),P(N,"instance",void 0);var B=/^[A-Za-z0-9+/=_-]{1,256}$/,U=function(e){if("string"!=typeof e)return null;var t=e.trim();return B.test(t)?t:null},H=function(){try{if("undefined"==typeof document)return null;var e=document.currentScript;if(e){var t=U(e.nonce||e.getAttribute("nonce"));if(t)return t}var n=document.querySelector('script[src*="sdk.gleap.io"]');if(n)return U(n.nonce||n.getAttribute("nonce"))}catch(r){}return null}(),q=null,z=function(){return q||H},W=function(e){var t=z();if(!t||!e)return e;try{e.setAttribute("nonce",t),e.nonce=t}catch(n){}return e},G="https://docs.gleap.ai/documentation/javascript/content-security-policy",V=new Set,$="gleap-frame-route-failed",X=function(e){return"string"==typeof e&&(0===e.indexOf("script-src")||0===e.indexOf("style-src"))},J=function(e,t){if(e&&t){var n,r=!1,o=function(){if(!r){r=!0;try{e.src=t}catch(n){}}};if(!function(){try{var e,t=null===(e=window.location)||void 0===e?void 0:e.protocol;return"http:"===t||"https:"===t}catch(n){return!1}}())return o();try{if(!e.contentDocument)return o()}catch(a){return o()}try{var i=new URL(t);n=i.origin+"/"}catch(a){return o()}fetch(t,{mode:"cors",credentials:"omit"}).then((function(e){if(!e.ok)throw new Error("Bootstrap fetch failed: "+e.status);return e.text()})).then((function(r){var i=r.replace(/(\s(?:href|src)\s*=\s*["'])\/([^"'/][^"']*)/g,"$1"+n+"$2"),s=z(),c=function(e,t){return t?e.replace(/<(script|style)\b(?![^>]*\snonce\s*=)/gi,'<$1 nonce="'+t+'"'):e}(i,s),l=s?' nonce="'+s+'"':"",u="/",d="/";try{var p=new URL(t);u=(d=p.pathname)+p.search+p.hash}catch(a){}var h,f,g="';\n\n // Inject the route-setter script BEFORE the first existing ';\n\n // Inject the route-setter script BEFORE the first existing ';\n\n // Inject the route-setter script BEFORE the first existing ';\n\n // Inject the route-setter script BEFORE the first existing ';\n\n // Inject the route-setter script BEFORE the first existing ';\n\n // Inject the route-setter script BEFORE the first existing ';\n\n // Inject the route-setter script BEFORE the first existing ';\n\n // Inject the route-setter script BEFORE the first existing ';\n\n // Inject the route-setter script BEFORE the first existing
+//
+//
+// data-mode: inline (default) — renders in the element, as tall as the survey
+// popup — opens as a card (data-format="full" for full screen), after data-delay seconds
+// tab — a side tab (data-label, data-color, data-position="left|right") opening the survey
+// data-api-key / data-region / data-language: used when this script boots the SDK itself. A page that
+// already runs the Gleap SDK keeps its own setup.
+// data-field-="value": hidden field values.
+//
+// The SDK keeps one messenger frame per page, so one inline survey per page.
+
+const SDK_SRC = 'https://sdk.gleap.io/latest/index.js';
+const PROCESSED_ATTR = 'data-gleap-survey-ready';
+
+const currentScript = typeof document !== 'undefined' ? document.currentScript : null;
+const scriptData = (currentScript && currentScript.dataset) || {};
+
+let sdkPromise = null;
+let inlineMounted = false;
+
+const readFields = (element) => {
+ const fields = {};
+ const data = element.dataset || {};
+ Object.keys(data).forEach((key) => {
+ if (key.indexOf('field') === 0 && key.length > 5) {
+ const fieldKey = key.charAt(5).toLowerCase() + key.slice(6);
+ fields[fieldKey] = String(data[key]);
+ }
+ });
+ return fields;
+};
+
+const loadSdk = () => {
+ if (window.Gleap && typeof window.Gleap.initialize === 'function') {
+ return Promise.resolve(window.Gleap);
+ }
+ if (sdkPromise) {
+ return sdkPromise;
+ }
+ sdkPromise = new Promise((resolve, reject) => {
+ const script = document.createElement('script');
+ script.src = scriptData.sdkSrc || SDK_SRC;
+ script.async = true;
+ if (currentScript && currentScript.nonce) {
+ script.nonce = currentScript.nonce;
+ }
+ script.onload = () => (window.Gleap ? resolve(window.Gleap) : reject(new Error('Gleap SDK missing')));
+ script.onerror = () => reject(new Error('Gleap SDK failed to load'));
+ (document.head || document.body).appendChild(script);
+ });
+ return sdkPromise;
+};
+
+const bootSdk = (Gleap, element) => {
+ const instance = typeof Gleap.getInstance === 'function' ? Gleap.getInstance() : null;
+ if (instance && instance.initialized) {
+ return true;
+ }
+
+ const apiKey = element.getAttribute('data-api-key') || scriptData.apiKey;
+ if (!apiKey) {
+ console.warn('[Gleap] survey embed: data-api-key is missing.');
+ return false;
+ }
+
+ const region = element.getAttribute('data-region') || scriptData.region;
+ if (region && region !== 'eu' && typeof Gleap.setRegion === 'function') {
+ Gleap.setRegion(region);
+ }
+ const language = element.getAttribute('data-language') || scriptData.language;
+ if (language) {
+ Gleap.setLanguage(language);
+ }
+
+ // Minimal mode: the survey only — no launcher, no in-app messages.
+ Gleap.showFeedbackButton(false);
+ Gleap.setDisableInAppNotifications(true);
+ Gleap.initialize(apiKey);
+ return true;
+};
+
+const createTab = (Gleap, element, surveyId, options) => {
+ const tab = document.createElement('button');
+ tab.type = 'button';
+ tab.textContent = element.getAttribute('data-label') || 'Feedback';
+ tab.setAttribute('aria-label', tab.textContent);
+
+ const left = element.getAttribute('data-position') === 'left';
+ const style = tab.style;
+ style.position = 'fixed';
+ style.top = '50%';
+ style[left ? 'left' : 'right'] = '0';
+ style.transform = 'translateY(-50%) rotate(180deg)';
+ style.writingMode = 'vertical-rl';
+ style.zIndex = '2147483000';
+ style.border = '0';
+ style.margin = '0';
+ style.cursor = 'pointer';
+ style.padding = '14px 9px';
+ style.borderRadius = left ? '0 10px 10px 0' : '10px 0 0 10px';
+ style.background = element.getAttribute('data-color') || '#0f7b6c';
+ style.color = element.getAttribute('data-text-color') || '#ffffff';
+ style.font = '600 13px/1 system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
+ style.letterSpacing = '0.01em';
+ style.boxShadow = '0 6px 20px rgba(16, 24, 40, 0.18)';
+
+ tab.addEventListener('click', () => {
+ Gleap.showSurvey(surveyId, options);
+ });
+ document.body.appendChild(tab);
+};
+
+const mount = (element) => {
+ if (element.hasAttribute(PROCESSED_ATTR)) {
+ return;
+ }
+ element.setAttribute(PROCESSED_ATTR, 'true');
+
+ const surveyId = element.getAttribute('data-gleap-survey');
+ if (!surveyId) {
+ return;
+ }
+ const mode = element.getAttribute('data-mode') || 'inline';
+ const fields = readFields(element);
+
+ if (mode === 'inline') {
+ if (inlineMounted) {
+ console.warn('[Gleap] survey embed: only one inline survey per page.');
+ return;
+ }
+ inlineMounted = true;
+ }
+
+ loadSdk()
+ .then((Gleap) => {
+ if (!bootSdk(Gleap, element)) {
+ return;
+ }
+
+ if (mode === 'inline') {
+ Gleap.showSurvey(surveyId, { format: 'page', container: element, fields });
+ return;
+ }
+
+ const options = { format: element.getAttribute('data-format') === 'full' ? 'full' : 'card', fields };
+ if (mode === 'tab') {
+ createTab(Gleap, element, surveyId, options);
+ return;
+ }
+
+ const delay = parseFloat(element.getAttribute('data-delay') || '0');
+ setTimeout(
+ () => {
+ Gleap.showSurvey(surveyId, options);
+ },
+ isNaN(delay) ? 0 : Math.max(0, delay) * 1000
+ );
+ })
+ .catch((error) => {
+ console.warn('[Gleap] survey embed:', error && error.message);
+ });
+};
+
+const scan = () => {
+ const elements = document.querySelectorAll('[data-gleap-survey]');
+ for (let i = 0; i < elements.length; i++) {
+ mount(elements[i]);
+ }
+};
+
+if (typeof window !== 'undefined' && typeof document !== 'undefined') {
+ if (!window.GleapSurveyEmbed) {
+ window.GleapSurveyEmbed = { scan };
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', scan);
+ } else {
+ scan();
+ }
+ } else {
+ window.GleapSurveyEmbed.scan();
+ }
+}
diff --git a/src/GleapSurveyOptions.js b/src/GleapSurveyOptions.js
new file mode 100644
index 00000000..27e8d2b9
--- /dev/null
+++ b/src/GleapSurveyOptions.js
@@ -0,0 +1,102 @@
+// Normalises the second argument of Gleap.showSurvey: a legacy format string ('survey',
+// 'survey_full', 'survey_web') or an options object ({ format, fields, personalToken,
+// container, resume }).
+//
+// format — what the survey looks like: 'card' (corner popover / mobile bottom sheet),
+// 'full' (full-viewport, Typeform style) or 'page' (inline in a container).
+// appMode — how the messenger frame is mounted (sent as `format` in start-survey, which is
+// what Messenger-App builds before Surveys 2.0 read).
+
+const APP_MODE_BY_FORMAT = {
+ card: 'survey',
+ full: 'survey_full',
+ page: 'survey_page',
+};
+
+const FORMAT_ALIASES = {
+ card: 'card',
+ survey: 'card',
+ full: 'full',
+ survey_full: 'full',
+ survey_web: 'full',
+ page: 'page',
+ survey_page: 'page',
+};
+
+const MAX_FIELDS = 50;
+const MAX_FIELD_KEY_LENGTH = 100;
+const MAX_FIELD_VALUE_LENGTH = 1000;
+
+const resolveContainer = (container) => {
+ if (!container || typeof document === 'undefined') {
+ return null;
+ }
+ if (typeof container === 'string') {
+ try {
+ return document.querySelector(container);
+ } catch (e) {
+ return null;
+ }
+ }
+ if (typeof container === 'object' && container.nodeType === 1) {
+ return container;
+ }
+ return null;
+};
+
+// Hidden field values are strings (spec §2 SurveyHiddenField); anything else is dropped.
+export const sanitizeSurveyFields = (fields) => {
+ const result = {};
+ if (!fields || typeof fields !== 'object' || Array.isArray(fields)) {
+ return result;
+ }
+ const keys = Object.keys(fields).slice(0, MAX_FIELDS);
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i];
+ const value = fields[key];
+ if (!key || key.length > MAX_FIELD_KEY_LENGTH) {
+ continue;
+ }
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
+ result[key] = String(value).slice(0, MAX_FIELD_VALUE_LENGTH);
+ }
+ }
+ return result;
+};
+
+export const normalizeSurveyOptions = (formatOrOptions) => {
+ const options =
+ formatOrOptions && typeof formatOrOptions === 'object' ? formatOrOptions : { format: formatOrOptions };
+
+ const legacyFormat = typeof options.format === 'string' ? options.format : '';
+ const format = FORMAT_ALIASES[legacyFormat] || 'card';
+
+ // 'survey_web' (the old standalone survey page) keeps its own app mode: it can't be closed.
+ const appMode = legacyFormat === 'survey_web' ? 'survey_web' : APP_MODE_BY_FORMAT[format];
+
+ const normalized = {
+ format,
+ appMode,
+ fields: sanitizeSurveyFields(options.fields),
+ resume: options.resume === true,
+ container: format === 'page' ? resolveContainer(options.container) : null,
+ };
+
+ // Page format only: answer with the keyboard while focus is on the host page
+ // (the survey page sets it; inline embeds on content pages leave it off).
+ if (options.keyboard === true) {
+ normalized.keyboard = true;
+ }
+ if (typeof options.personalToken === 'string' && options.personalToken.length > 0) {
+ normalized.personalToken = options.personalToken;
+ }
+ if (typeof options.outboundAction === 'string' && options.outboundAction.length > 0) {
+ normalized.outboundAction = options.outboundAction;
+ }
+ // Server-side resume state for a reminder delivery (answers so far, lastBlockId).
+ if (options.resumeData && typeof options.resumeData === 'object' && !Array.isArray(options.resumeData)) {
+ normalized.resumeData = options.resumeData;
+ }
+
+ return normalized;
+};
diff --git a/src/UI.js b/src/UI.js
index a5757654..ea01b9b2 100644
--- a/src/UI.js
+++ b/src/UI.js
@@ -774,8 +774,31 @@ export const injectStyledCSS = (
object-fit: cover;
}
+ /* Surveys 2.0 — card: a compact popover in the launcher's corner. */
.gleap-frame-container--survey {
bottom: ${buttonY}px !important;
+ bottom: calc(${buttonY}px + env(safe-area-inset-bottom, 0px)) !important;
+ max-width: 348px;
+ border-radius: 24px;
+ box-shadow: 0 14px 44px rgba(16, 24, 40, 0.18), 0 0 0 1px rgba(16, 24, 40, 0.06);
+ animation: gleapSurveyCardIn 0.42s cubic-bezier(0.16, 1, 0.3, 1) both, gleapWidgetFade 0.18s ease-out both;
+ transition: none;
+ }
+
+ /* The survey's corners (survey-theme from the messenger); rounded is the 24px above. */
+ .gleap-frame-container--survey.gleap-frame-container--corners-sharp {
+ border-radius: 10px;
+ }
+
+ .gleap-frame-container--survey.gleap-frame-container--corners-pill {
+ border-radius: 32px;
+ }
+
+ /* Card and page surveys stay invisible until the messenger reports their height. */
+ .gleap-frame-container--measuring {
+ visibility: hidden !important;
+ animation: none !important;
+ pointer-events: none !important;
}
.gleap-frame-container--extended {
@@ -795,34 +818,108 @@ export const injectStyledCSS = (
}
}
- .gleap-frame-container--survey-full {
+ /* Surveys 2.0 — full screen: the frame covers the viewport and the messenger draws the
+ brand-tinted, Typeform-style background and the close button itself. */
+ .gleap-frame-container--survey-full,
+ [dir=rtl].gleap-frame-container--survey-full {
position: fixed;
top: 0 !important;
left: 0 !important;
bottom: 0 !important;
right: 0 !important;
- width: 100vw !important;
- max-width: 100vw !important;
+ width: 100% !important;
+ max-width: none !important;
height: 100vh;
height: 100dvh !important;
- background-color: rgba(0, 0, 0, 0.5);
- backdrop-filter: blur(6px);
+ min-height: 0 !important;
+ max-height: none !important;
+ background-color: transparent;
+ border-radius: 0 !important;
+ box-shadow: none !important;
+ transform: none;
+ animation: gleapWidgetFade 0.28s ease-out both !important;
+ }
+
+ .gleap-frame-container--survey-full .gleap-frame-container-inner {
+ width: 100% !important;
+ height: 100% !important;
+ max-width: none !important;
+ max-height: none !important;
+ border-radius: 0;
+ }
+
+ .gleap-frame-container--survey-full .gleap-frame-container-inner:before {
+ display: none;
+ }
+
+ .gleap-frame-container--survey-full.gleap-frame-container--closing {
+ animation: gleapWidgetFadeOut 0.22s ease-in both !important;
+ }
+
+ /* Legacy surveys (the messenger fell back to the pre-2.0 flow): the old survey chrome. */
+ .gleap-frame-container--survey.gleap-frame-container--survey-legacy {
+ max-width: 410px;
+ border-radius: ${containerRadius * 1.5}px;
+ box-shadow: 0px 5px 40px 0px rgba(9, 14, 21, 0.16);
+ }
+
+ .gleap-frame-container--survey-full.gleap-frame-container--survey-legacy {
display: flex !important;
justify-content: center !important;
align-items: center !important;
- max-height: 100vh;
- max-height: 100dvh !important;
- border-radius: 0 !important;
- animation-name: none !important;
+ background-color: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(6px);
+ -webkit-backdrop-filter: blur(6px);
}
- .gleap-frame-container--survey-full .gleap-frame-container-inner {
+ .gleap-frame-container--survey-full.gleap-frame-container--survey-legacy .gleap-frame-container-inner {
+ width: calc(100% - 24px) !important;
max-width: 640px !important;
- width: calc(100% - 24px);
+ height: calc(100% - 40px) !important;
border-radius: ${containerRadius}px;
overflow: hidden;
}
+ .gleap-frame-container--survey-full.gleap-frame-container--survey-legacy .gleap-frame-container-inner:before {
+ display: block;
+ }
+
+ /* Surveys 2.0 — page: an inline frame in the host's container, as tall as the survey. */
+ .gleap-frame-container.gleap-frame-container--survey-page,
+ [dir=rtl].gleap-frame-container.gleap-frame-container--survey-page {
+ position: relative !important;
+ top: auto !important;
+ right: auto !important;
+ bottom: auto !important;
+ left: auto !important;
+ width: 100% !important;
+ max-width: none !important;
+ height: auto !important;
+ min-height: 0 !important;
+ max-height: none !important;
+ z-index: auto;
+ box-shadow: none !important;
+ border-radius: 0 !important;
+ background-color: transparent;
+ transform: none !important;
+ animation: gleapWidgetFade 0.3s ease-out both !important;
+ }
+
+ .gleap-frame-container--survey-page .gleap-frame-container-inner {
+ width: 100% !important;
+ height: 360px;
+ max-height: none !important;
+ }
+
+ .gleap-frame-container--survey-page.gleap-frame-container--animate .gleap-frame-container-inner {
+ transition: height 0.28s cubic-bezier(0.16, 1, 0.3, 1);
+ }
+
+ .gleap-frame-container--survey-page .gleap-frame-container-inner:before,
+ .gleap-frame-container--survey-page .gleap-frame-loader {
+ display: none;
+ }
+
.gleap-frame-container--classic {
right: ${buttonX}px;
bottom: ${buttonY}px;
@@ -904,6 +1001,16 @@ export const injectStyledCSS = (
to { opacity: 0; transform: scale(0.96); }
}
+ @keyframes gleapWidgetFadeOut {
+ from { opacity: 1; }
+ to { opacity: 0; }
+ }
+
+ @keyframes gleapSurveyCardIn {
+ from { transform: translateY(14px) scale(0.98); }
+ to { transform: translateY(0) scale(1); }
+ }
+
@keyframes gleapWidgetSlideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
@@ -1407,7 +1514,28 @@ export const injectStyledCSS = (
}
.gleap-frame-container--survey .gleap-frame-container-inner {
- height: calc(100vh - 40px);
+ height: calc(100vh - ${buttonY + 24}px);
+ height: calc(100dvh - ${buttonY + 24}px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
+ }
+
+ /* Behind the survey card the frame shows the survey's surface, not the widget background. */
+ .gleap-frame-container--survey .gleap-frame-container-inner:before {
+ background-color: var(--gleap-survey-surface, ${backgroundColor});
+ }
+
+ /* The page frame takes the survey's color-scheme (the messenger sets the same on its document),
+ so the browser keeps the iframe transparent on the host's backdrop, whatever the host's scheme. */
+ .gleap-frame-container--survey-page {
+ color-scheme: light;
+ }
+
+ .gleap-frame-container--survey-page.gleap-frame-container--survey-dark {
+ color-scheme: dark;
+ }
+
+ /* Smooth resizes between questions (not on the first measure, which happens before --animate). */
+ .gleap-frame-container--survey.gleap-frame-container--animate .gleap-frame-container-inner {
+ transition: max-height 0.28s cubic-bezier(0.16, 1, 0.3, 1);
}
.gleap-frame-container-inner:before {
@@ -1531,6 +1659,15 @@ export const injectStyledCSS = (
.bb-feedback-button--hidden {
display: none !important;
}
+
+ /* A survey takes the launcher's corner: the launcher steps aside while it is open (a pill or
+ sharp card would otherwise show the launcher peeking out behind its corner). */
+ .bb-feedback-button--open.bb-feedback-button--survey {
+ opacity: 0;
+ visibility: hidden;
+ pointer-events: none;
+ transition: opacity 0.15s ease, visibility 0s linear 0.15s;
+ }
.bb-feedback-button-text {
padding: 8px 12px;
@@ -2359,19 +2496,91 @@ export const injectStyledCSS = (
height: 100%;
}
- .gleap-frame-container:not(.gleap-frame-container--survey):not(.gleap-frame-container--survey-full) .gleap-frame-container-inner {
+ .gleap-frame-container:not(.gleap-frame-container--survey):not(.gleap-frame-container--survey-full):not(.gleap-frame-container--survey-page) .gleap-frame-container-inner {
max-height: calc(var(--glvh, 1vh) * 100) !important;
}
- .gleap-frame-container--survey {
+ /* Card surveys become a floating sheet (iOS 26 style): inset from the edges and above the
+ home indicator, rounded on all corners, layered shadow, dimmed page behind. */
+ .gleap-frame-container--survey,
+ [dir=rtl].gleap-frame-container--survey {
+ top: auto !important;
+ bottom: calc(8px + env(safe-area-inset-bottom, 0px)) !important;
+ left: 8px !important;
+ right: 8px !important;
+ width: auto !important;
+ max-width: none !important;
height: auto !important;
- top: initial !important;
- bottom: 0px !important;
- min-height: initial !important;
+ min-height: 0 !important;
+ max-height: calc(100vh - 24px) !important;
+ max-height: calc(100dvh - 24px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px)) !important;
+ box-sizing: border-box;
+ padding-top: 15px;
+ padding-bottom: 0;
+ /* The survey's surface (survey-theme), so a dark survey has no light handle strip. */
+ background-color: var(--gleap-survey-surface, ${backgroundColor});
+ border-radius: 32px 32px 40px 40px !important;
+ overflow: hidden;
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.18), 0 1px 3px rgba(0, 0, 0, 0.08), 0 0 0 100vmax rgba(10, 12, 16, 0.25);
+ transform-origin: center bottom;
+ transition: bottom 0.25s cubic-bezier(0.16, 1, 0.3, 1), max-height 0.25s cubic-bezier(0.16, 1, 0.3, 1);
+ animation: gleapWidgetSlideUp 0.5s cubic-bezier(0.22, 1.2, 0.36, 1) both, gleapWidgetFade 0.2s ease-out both;
+ }
+
+ .gleap-frame-container--survey.gleap-frame-container--corners-sharp {
+ border-radius: 14px !important;
+ }
+
+ .gleap-frame-container--survey.gleap-frame-container--corners-pill {
+ border-radius: 40px 40px 46px 46px !important;
+ }
+
+ .gleap-frame-container--survey:after {
+ content: "";
+ position: absolute;
+ top: 8px;
+ left: 50%;
+ width: 36px;
+ height: 5px;
+ margin-left: -18px;
+ border-radius: 5px;
+ background-color: ${isDarkMode ? 'rgba(255, 255, 255, 0.28)' : 'rgba(16, 24, 40, 0.16)'};
+ pointer-events: none;
+ }
+
+ .gleap-frame-container--survey.gleap-frame-container--survey-dark:after {
+ background-color: rgba(255, 255, 255, 0.28);
+ }
+
+ .gleap-frame-container--survey.gleap-frame-container--survey-light:after {
+ background-color: rgba(16, 24, 40, 0.16);
}
.gleap-frame-container--survey .gleap-frame-container-inner {
- height: 100vh !important;
+ width: 100% !important;
+ height: calc(100vh - 39px) !important;
+ height: calc(100dvh - 39px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px)) !important;
+ }
+
+ .gleap-frame-container--survey-page .gleap-frame-container-inner {
+ width: 100% !important;
+ }
+
+ /* Legacy card surveys: the old full-width panel at the bottom (no sheet chrome). */
+ .gleap-frame-container--survey.gleap-frame-container--survey-legacy,
+ [dir=rtl].gleap-frame-container--survey.gleap-frame-container--survey-legacy {
+ padding: 0;
+ border-radius: 28px !important;
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.18), 0 1px 3px rgba(0, 0, 0, 0.08);
+ }
+
+ .gleap-frame-container--survey.gleap-frame-container--survey-legacy:after {
+ display: none;
+ }
+
+ .gleap-frame-container--survey.gleap-frame-container--survey-legacy .gleap-frame-container-inner {
+ height: calc(100vh - 12px) !important;
+ height: calc(100dvh - 12px - env(safe-area-inset-top, 0px)) !important;
}
.bb-tooltip {
@@ -3056,6 +3265,24 @@ export const injectStyledCSS = (
}
}
+ @media (prefers-reduced-motion: reduce) {
+ .gleap-frame-container--survey,
+ .gleap-frame-container--survey-page,
+ .gleap-frame-container--survey-full {
+ animation: gleapWidgetFade 0.15s ease-out both !important;
+ }
+
+ .gleap-frame-container--survey.gleap-frame-container--closing,
+ .gleap-frame-container--survey-full.gleap-frame-container--closing {
+ animation: gleapWidgetFadeOut 0.15s ease-in both !important;
+ }
+
+ .gleap-frame-container--survey .gleap-frame-container-inner,
+ .gleap-frame-container--survey-page .gleap-frame-container-inner {
+ transition: none !important;
+ }
+ }
+
.gleap-admin-frame-container,
.gleap-frame-container,
.bb-feedback-button,
diff --git a/webpack.config.js b/webpack.config.js
index 194b4ed1..dbc0da11 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -140,6 +140,35 @@ const browserConfig = {
},
};
+// survey-embed.js: a small standalone loader for embeds (loads the SDK on demand).
+const copyEmbedPlugin = {
+ apply: (compiler) => {
+ compiler.hooks.afterEmit.tap("AfterEmitEmbedPlugin", () => {
+ const nodeVersion = process.env.npm_package_version;
+ return exec(
+ `mkdir -p published/${nodeVersion} published/latest && cp ./build/embed/survey-embed.js published/${nodeVersion}/survey-embed.js && cp ./build/embed/survey-embed.js published/latest/survey-embed.js`,
+ (err, stdout, stderr) => {
+ if (stdout) process.stdout.write(stdout);
+ if (stderr) process.stderr.write(stderr);
+ }
+ );
+ });
+ },
+};
+
+const embedConfig = {
+ ...commonConfig(false, [copyEmbedPlugin]),
+ entry: {
+ "survey-embed": "./src/GleapSurveyEmbed.js",
+ },
+ devtool: false,
+ output: {
+ path: path.resolve(__dirname, "build/embed"),
+ filename: "[name].js",
+ clean: true,
+ },
+};
+
const developmentConfig = {
...commonConfig(true, []),
// ... additional development-specific settings ...
@@ -149,6 +178,6 @@ module.exports = (env) => {
if (env && env.development) {
return developmentConfig;
} else {
- return [esmConfig, cjsConfig, browserConfig];
+ return [esmConfig, cjsConfig, browserConfig, embedConfig];
}
};