UNPKG

quala-widget

Version:

Smart feedback widget for SaaS trial optimization

1,299 lines (1,298 loc) 167 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { useState, useRef, useCallback, useEffect, createElement } from "react"; import { jsxs, jsx, Fragment } from "react/jsx-runtime"; const _ReactRenderer = class _ReactRenderer { constructor() { __publicField(this, "createRootFn", null); __publicField(this, "legacyRender", null); __publicField(this, "isReady", false); __publicField(this, "debug", false); } static getInstance() { if (!_ReactRenderer.instance) { _ReactRenderer.instance = new _ReactRenderer(); } return _ReactRenderer.instance; } setDebug(debug) { this.debug = debug; } log(message, ...args) { if (this.debug) { console.log(`[Quala Renderer] ${message}`, ...args); } } async initialize() { var _a, _b; if (this.isReady) return; try { const ReactDOMClient = require("react-dom/client"); if (ReactDOMClient && ReactDOMClient.createRoot) { this.createRootFn = ReactDOMClient.createRoot; this.isReady = true; this.log("Using direct require for react-dom/client"); return; } } catch (e) { } try { const ReactDOMClient = await import("./client-DFJurY5z.js").then((n) => n.c); this.createRootFn = ReactDOMClient.createRoot || ((_a = ReactDOMClient.default) == null ? void 0 : _a.createRoot); if (this.createRootFn) { this.isReady = true; this.log("Using dynamic import for react-dom/client"); return; } } catch (e) { } try { const ReactDOM = require("react-dom"); if (ReactDOM && ReactDOM.render) { this.legacyRender = ReactDOM; this.isReady = true; this.log("Using legacy ReactDOM.render"); return; } } catch (e) { } try { const ReactDOM = await import("react-dom"); const render = ReactDOM.render || ((_b = ReactDOM.default) == null ? void 0 : _b.render); if (render && typeof render === "function") { this.legacyRender = ReactDOM.default || ReactDOM; this.isReady = true; this.log("Using dynamic import for legacy ReactDOM"); return; } } catch (e) { console.error("[Quala] All React DOM loading methods failed"); } } createRoot(container) { if (!this.isReady) { throw new Error("[Quala] Renderer not ready. Call initialize() first."); } if (this.createRootFn) { return this.createRootFn(container); } else if (this.legacyRender) { return { render: (element) => { this.legacyRender.render(element, container); }, unmount: () => { if (this.legacyRender.unmountComponentAtNode) { this.legacyRender.unmountComponentAtNode(container); } } }; } else { throw new Error("[Quala] No React rendering method available"); } } isInitialized() { return this.isReady; } async ensureReady() { if (!this.isReady) { await this.initialize(); } } }; __publicField(_ReactRenderer, "instance"); let ReactRenderer = _ReactRenderer; const _TechnicalContextCollector = class _TechnicalContextCollector { constructor(debug = false) { __publicField(this, "consoleErrors", []); __publicField(this, "failedRequests", []); __publicField(this, "userActions", []); __publicField(this, "pageStartTime", Date.now()); __publicField(this, "debug", false); __publicField(this, "isCapturing", false); // ✅ SIMPLIFIED: Just container-level selectors __publicField(this, "qualaContainerSelectors", [ "[data-quala-widget]", // Main widget containers "[data-quala-micro-widget]", // Micro widget containers "[data-quala-container]", // Generic Quala container ".quala-widget", // CSS class for main widgets ".quala-micro-widget", // CSS class for micro widgets ".quala-container", // CSS class for containers ".quala-modal", // Modal containers ".quala-overlay" // Overlay/backdrop ]); this.debug = debug; } log(message, ...args) { if (this.debug) { console.log(`[Quala Context] ${message}`, ...args); } } /** * ✅ SIMPLIFIED: Check if element is inside any Quala container * Just mark top-level containers and ignore everything inside! */ isQualaElement(element) { var _a; if (!element || !(element instanceof Element)) return false; let currentElement = element; while (currentElement) { if (currentElement.hasAttribute("data-quala-widget") || currentElement.hasAttribute("data-quala-micro-widget") || currentElement.hasAttribute("data-quala-container")) { this.log("🎯 Quala element detected - inside container:", currentElement.tagName); return true; } if (currentElement.className && typeof currentElement.className === "string") { const classes = currentElement.className.split(" "); if (classes.some( (cls) => cls === "quala-widget" || cls === "quala-micro-widget" || cls === "quala-container" || cls === "quala-modal" || cls === "quala-overlay" )) { this.log("🎯 Quala element detected - inside CSS container:", currentElement.className); return true; } } if (currentElement.tagName === "DIV" || currentElement.tagName === "FORM") { const elementText = ((_a = currentElement.textContent) == null ? void 0 : _a.toLowerCase()) || ""; if ((elementText.includes("send feedback") || elementText.includes("submit feedback")) && elementText.length < 200) { this.log("🎯 Quala element detected - content pattern container"); return true; } const textarea = currentElement.querySelector('textarea[placeholder*="explain"], textarea[placeholder*="feedback"], textarea[placeholder*="improve"]'); if (textarea) { this.log("🎯 Quala element detected - feedback form container"); return true; } } currentElement = currentElement.parentElement; } return false; } /** * Check if a URL is a Quala API endpoint */ isQualaRequest(url) { try { const urlObj = new URL(url, window.location.origin); return urlObj.pathname.includes("/api/widget") || urlObj.hostname.includes("getquala.xyz") || urlObj.pathname.includes("quala"); } catch { return url.includes("quala") || url.includes("/api/widget"); } } initialize() { if (typeof window === "undefined" || this.isCapturing) return; this.isCapturing = true; this.captureConsoleErrors(); this.captureNetworkFailures(); this.captureUserActions(); this.trackPageVisibility(); this.log("Enhanced technical context capture initialized with improved Quala component filtering"); } captureConsoleErrors() { const originalError = console.error; const originalWarn = console.warn; console.error = (...args) => { const errorString = args.join(" ").toLowerCase(); if (!errorString.includes("[quala") && !errorString.includes("quala sdk")) { this.addConsoleError("error", args); } originalError.apply(console, args); }; console.warn = (...args) => { const warnString = args.join(" ").toLowerCase(); if (!warnString.includes("[quala") && !warnString.includes("quala sdk")) { this.addConsoleError("warn", args); } originalWarn.apply(console, args); }; window.addEventListener("error", (event) => { var _a, _b, _c; if (!this.isQualaElement(event.target) && !((_a = event.filename) == null ? void 0 : _a.includes("quala")) && !((_b = event.message) == null ? void 0 : _b.toLowerCase().includes("quala"))) { this.addConsoleError("error", [event.message], (_c = event.error) == null ? void 0 : _c.stack); this.addUserAction("error", "window", { message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno }); } }); window.addEventListener("unhandledrejection", (event) => { var _a; const reasonString = String(event.reason).toLowerCase(); if (!reasonString.includes("quala")) { this.addConsoleError("error", [event.reason], (_a = event.reason) == null ? void 0 : _a.stack); this.addUserAction("error", "promise", { reason: String(event.reason) }); } }); } captureNetworkFailures() { const originalFetch = window.fetch; window.fetch = async (...args) => { const url = this.getRequestUrl(args[0]); const isQualaRequest = this.isQualaRequest(url); try { const response = await originalFetch.apply(window, args); if (!response.ok && !isQualaRequest) { this.addFailedRequest( url, response.status, this.getRequestMethod(args[1]), (/* @__PURE__ */ new Date()).toISOString() ); } return response; } catch (error) { if (!isQualaRequest) { this.addFailedRequest( url, 0, this.getRequestMethod(args[1]), (/* @__PURE__ */ new Date()).toISOString() ); } throw error; } }; this.interceptXHR(); } interceptXHR() { const originalXHROpen = XMLHttpRequest.prototype.open; const originalXHRSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url) { this._quala_method = method; this._quala_url = url; this._quala_is_quala_request = _TechnicalContextCollector.getInstance().isQualaRequest(url); return originalXHROpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function() { const collector = _TechnicalContextCollector.getInstance(); const isQualaRequest = this._quala_is_quala_request; this.addEventListener("error", () => { if (this._quala_url && !isQualaRequest) { collector.addFailedRequest( this._quala_url, this.status || 0, this._quala_method || "GET", (/* @__PURE__ */ new Date()).toISOString() ); } }); this.addEventListener("load", () => { if (this.status >= 400 && this._quala_url && !isQualaRequest) { collector.addFailedRequest( this._quala_url, this.status, this._quala_method || "GET", (/* @__PURE__ */ new Date()).toISOString() ); } }); return originalXHRSend.apply(this, arguments); }; } getRequestUrl(input) { if (typeof input === "string") return input; if (input instanceof URL) return input.toString(); if (input instanceof Request) return input.url; return "unknown"; } getRequestMethod(init) { return (init == null ? void 0 : init.method) || "GET"; } captureUserActions() { let lastClickTime = 0; document.addEventListener("click", (event) => { const now = Date.now(); if (now - lastClickTime > 100 && !this.isQualaElement(event.target)) { const element = event.target; const elementDetails = this.getElementDetails(element); this.addUserAction("click", this.getElementSelector(element), { x: event.clientX, y: event.clientY, button: event.button }, elementDetails); lastClickTime = now; } }); document.addEventListener("input", (event) => { if (!this.isQualaElement(event.target)) { const element = event.target; if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") { const elementDetails = this.getElementDetails(element); this.addUserAction("input", this.getElementSelector(element), { inputType: element.type, valueLength: element.value.length // Don't capture actual value for privacy }, elementDetails); } } }); document.addEventListener("submit", (event) => { if (!this.isQualaElement(event.target)) { const form = event.target; const elementDetails = this.getElementDetails(form); this.addUserAction("form_submit", this.getElementSelector(form), { action: form.action, method: form.method, elementCount: form.elements.length }, elementDetails); } }); let scrollTimeout; document.addEventListener("scroll", () => { clearTimeout(scrollTimeout); scrollTimeout = setTimeout(() => { this.addUserAction("scroll", "window", { scrollY: window.scrollY, scrollX: window.scrollX, scrollPercentage: Math.round(window.scrollY / (document.documentElement.scrollHeight - window.innerHeight) * 100) }); }, 200); }); document.addEventListener("focusin", (event) => { if (!this.isQualaElement(event.target)) { const element = event.target; const elementDetails = this.getElementDetails(element); this.addUserAction("focus", this.getElementSelector(element), {}, elementDetails); } }); } trackPageVisibility() { document.addEventListener("visibilitychange", () => { this.addUserAction("navigation", "page", { visibility: document.visibilityState, timeOnPage: Date.now() - this.pageStartTime }); }); } // ✅ ENHANCED: Element details extraction with better ID and label detection getElementDetails(element) { var _a; if (!element) return { tagName: "unknown" }; const details = { tagName: element.tagName.toLowerCase(), id: element.id || void 0, className: element.className || void 0 }; const textContent = (_a = element.textContent) == null ? void 0 : _a.trim(); if (textContent && textContent.length > 0) { details.text = textContent.substring(0, 100); } if (element instanceof HTMLInputElement) { details.type = element.type; details.name = element.name || void 0; details.placeholder = element.placeholder || void 0; if (element.value && element.type !== "password") { details.value = element.type === "email" ? "[email]" : element.type === "tel" ? "[phone]" : element.type === "number" ? "[number]" : element.value.length > 0 ? `[${element.value.length} chars]` : void 0; } } else if (element instanceof HTMLTextAreaElement) { details.name = element.name || void 0; details.placeholder = element.placeholder || void 0; details.value = element.value.length > 0 ? `[${element.value.length} chars]` : void 0; } else if (element instanceof HTMLSelectElement) { details.name = element.name || void 0; details.value = element.value || void 0; } else if (element instanceof HTMLButtonElement) { details.type = element.type; details.name = element.name || void 0; } if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) { const label = this.getElementLabel(element); if (label && !details.text) { details.text = label; } } const ariaLabel = element.getAttribute("aria-label"); if (ariaLabel && !details.text) { details.ariaLabel = ariaLabel.substring(0, 50); details.text = ariaLabel.substring(0, 50); } const title = element.getAttribute("title"); if (title && !details.text) { details.title = title.substring(0, 50); details.text = title.substring(0, 50); } if (element instanceof HTMLButtonElement && !details.text) { const childText = Array.from(element.children).map((child) => { var _a2; return (_a2 = child.textContent) == null ? void 0 : _a2.trim(); }).filter((text) => text && text.length > 0).join(" "); if (childText) { details.text = childText.substring(0, 50); } } return details; } // ✅ ENHANCED: Better label detection getElementLabel(element) { var _a, _b, _c, _d, _e, _f, _g, _h; if (element.id) { const label = document.querySelector(`label[for="${element.id}"]`); if (label) { return (_a = label.textContent) == null ? void 0 : _a.trim().substring(0, 50); } } const parentLabel = element.closest("label"); if (parentLabel) { const labelText = Array.from(parentLabel.childNodes).filter((node) => node.nodeType === Node.TEXT_NODE).map((node) => { var _a2; return (_a2 = node.textContent) == null ? void 0 : _a2.trim(); }).filter((text) => text && text.length > 0).join(" "); if (labelText) { return labelText.substring(0, 50); } return (_b = parentLabel.textContent) == null ? void 0 : _b.trim().substring(0, 50); } if (element.getAttribute("aria-label")) { return (_c = element.getAttribute("aria-label")) == null ? void 0 : _c.substring(0, 50); } const labelledBy = element.getAttribute("aria-labelledby"); if (labelledBy) { const labelElement = document.getElementById(labelledBy); if (labelElement) { return (_d = labelElement.textContent) == null ? void 0 : _d.trim().substring(0, 50); } } const previousSibling = element.previousElementSibling; if (previousSibling && (previousSibling.tagName === "SPAN" || previousSibling.tagName === "DIV")) { const siblingText = (_e = previousSibling.textContent) == null ? void 0 : _e.trim(); if (siblingText && siblingText.length > 0 && siblingText.length < 100) { return siblingText.substring(0, 50); } } let current = element.previousSibling; while (current && current !== ((_f = element.parentElement) == null ? void 0 : _f.firstChild)) { if (current.nodeType === Node.TEXT_NODE) { const text = (_g = current.textContent) == null ? void 0 : _g.trim(); if (text && text.length > 0) { return text.substring(0, 50); } } else if (current.nodeType === Node.ELEMENT_NODE) { const elementText = (_h = current.textContent) == null ? void 0 : _h.trim(); if (elementText && elementText.length > 0 && elementText.length < 100) { return elementText.substring(0, 50); } } current = current.previousSibling; } return void 0; } getElementSelector(element) { if (!element) return "unknown"; const tag = element.tagName.toLowerCase(); const id = element.id ? `#${element.id}` : ""; const classes = element.className ? `.${element.className.toString().split(" ").filter((c) => c).slice(0, 2).join(".")}` : ""; return `${tag}${id}${classes}`.substring(0, 100); } addConsoleError(level, args, stack) { this.consoleErrors.push({ message: args.map((arg) => this.serializeArgument(arg)).join(" ").substring(0, 500), stack: stack == null ? void 0 : stack.substring(0, 1e3), timestamp: (/* @__PURE__ */ new Date()).toISOString(), level }); if (this.consoleErrors.length > 10) { this.consoleErrors = this.consoleErrors.slice(-10); } this.log(`Captured ${level}:`, args[0]); } /** * Properly serialize different types of arguments including Error objects */ serializeArgument(arg) { var _a; if (arg === null) return "null"; if (arg === void 0) return "undefined"; if (arg instanceof Error) { return `${arg.name}: ${arg.message}${arg.stack ? "\n" + arg.stack : ""}`; } if (typeof arg === "object") { try { if (arg.toString && arg.toString !== Object.prototype.toString) { return arg.toString(); } return JSON.stringify(arg, null, 2); } catch (e) { return `[Object: ${((_a = arg.constructor) == null ? void 0 : _a.name) || "Unknown"}]`; } } return String(arg); } addFailedRequest(url, status, method, timestamp) { const sanitizedUrl = this.sanitizeUrl(url); this.failedRequests.push({ url: sanitizedUrl, status, method, timestamp }); if (this.failedRequests.length > 5) { this.failedRequests = this.failedRequests.slice(-5); } this.log(`Captured failed request: ${method} ${sanitizedUrl} - ${status}`); } addUserAction(type, element, data, elementDetails) { this.userActions.push({ type, element, timestamp: (/* @__PURE__ */ new Date()).toISOString(), data, elementDetails }); if (this.userActions.length > 20) { this.userActions = this.userActions.slice(-20); } } sanitizeUrl(url) { try { const urlObj = new URL(url, window.location.origin); const sensitiveParams = [ "password", "token", "key", "secret", "auth", "session", "api_key", "access_token", "refresh_token", "jwt", "bearer", "credential", "ssn", "social_security", "credit_card", "ccn", "cvv" ]; sensitiveParams.forEach((param) => { if (urlObj.searchParams.has(param)) { urlObj.searchParams.set(param, "[REDACTED]"); } }); return urlObj.toString(); } catch { return url.length > 200 ? url.substring(0, 200) + "..." : url; } } getPerformanceMetrics() { if (typeof window === "undefined" || !window.performance) { return {}; } const navigation = window.performance.getEntriesByType("navigation")[0]; const paint = window.performance.getEntriesByType("paint"); const metrics = {}; if (navigation) { metrics.pageLoadTime = navigation.loadEventEnd - navigation.startTime; } paint.forEach((entry) => { if (entry.name === "first-contentful-paint") { metrics.firstContentfulPaint = entry.startTime; } }); try { if ("PerformanceObserver" in window) { const observer = new PerformanceObserver((list) => { const entries = list.getEntries(); entries.forEach((entry) => { if (entry.name === "largest-contentful-paint") { metrics.largestContentfulPaint = entry.startTime; } }); }); observer.observe({ entryTypes: ["largest-contentful-paint"] }); } } catch (e) { } return metrics; } // Enhanced device detection getDeviceInfo() { var _a; const userAgent = navigator.userAgent; const viewport = `${window.innerWidth}x${window.innerHeight}`; const screen = `${window.screen.width}x${window.screen.height}`; return { userAgent, viewport, screenResolution: screen, devicePixelRatio: window.devicePixelRatio || 1, connectionType: ((_a = navigator.connection) == null ? void 0 : _a.effectiveType) || "unknown", // Enhanced device detection deviceType: this.detectDeviceType(userAgent), operatingSystem: this.detectOperatingSystem(userAgent), browser: this.detectBrowser(userAgent).name, browserVersion: this.detectBrowser(userAgent).version, isTouchDevice: this.detectTouchDevice(), language: navigator.language || "unknown", timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "unknown" }; } detectDeviceType(userAgent) { if (/iPad|Android(?=.*Tablet)|Tablet/i.test(userAgent)) { return "tablet"; } if (/Mobile|Android|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent)) { return "mobile"; } if (window.screen.width <= 768) { return "mobile"; } else if (window.screen.width <= 1024) { return "tablet"; } return "desktop"; } detectOperatingSystem(userAgent) { if (/Windows NT 10.0/i.test(userAgent)) return "Windows 10/11"; if (/Windows NT 6.3/i.test(userAgent)) return "Windows 8.1"; if (/Windows NT 6.2/i.test(userAgent)) return "Windows 8"; if (/Windows NT 6.1/i.test(userAgent)) return "Windows 7"; if (/Windows/i.test(userAgent)) return "Windows"; if (/iPhone OS|iOS/i.test(userAgent)) { const match = userAgent.match(/OS (\d+_\d+)/); return match ? `iOS ${match[1].replace("_", ".")}` : "iOS"; } if (/Android/i.test(userAgent)) { const match = userAgent.match(/Android (\d+\.?\d*)/); return match ? `Android ${match[1]}` : "Android"; } if (/Mac OS X/i.test(userAgent)) { const match = userAgent.match(/Mac OS X (\d+_\d+_?\d*)/); return match ? `macOS ${match[1].replace(/_/g, ".")}` : "macOS"; } if (/Linux/i.test(userAgent)) return "Linux"; if (/CrOS/i.test(userAgent)) return "Chrome OS"; return "Unknown"; } detectBrowser(userAgent) { if (/Edg\//i.test(userAgent)) { const match = userAgent.match(/Edg\/(\d+\.?\d*)/); return { name: "Edge", version: (match == null ? void 0 : match[1]) || "Unknown" }; } if (/Chrome/i.test(userAgent) && !/Chromium/i.test(userAgent)) { const match = userAgent.match(/Chrome\/(\d+\.?\d*)/); return { name: "Chrome", version: (match == null ? void 0 : match[1]) || "Unknown" }; } if (/Safari/i.test(userAgent) && !/Chrome/i.test(userAgent)) { const match = userAgent.match(/Version\/(\d+\.?\d*)/); return { name: "Safari", version: (match == null ? void 0 : match[1]) || "Unknown" }; } if (/Firefox/i.test(userAgent)) { const match = userAgent.match(/Firefox\/(\d+\.?\d*)/); return { name: "Firefox", version: (match == null ? void 0 : match[1]) || "Unknown" }; } if (/OPR/i.test(userAgent)) { const match = userAgent.match(/OPR\/(\d+\.?\d*)/); return { name: "Opera", version: (match == null ? void 0 : match[1]) || "Unknown" }; } return { name: "Unknown", version: "Unknown" }; } detectTouchDevice() { return "ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; } getPageContext() { return { url: window.location.href, referrer: document.referrer, timestamp: (/* @__PURE__ */ new Date()).toISOString(), scrollPosition: window.scrollY, timeOnPage: Date.now() - this.pageStartTime }; } getContext() { return { consoleErrors: [...this.consoleErrors], failedRequests: [...this.failedRequests], performanceMetrics: this.getPerformanceMetrics(), deviceInfo: this.getDeviceInfo(), pageContext: this.getPageContext(), userActions: [...this.userActions.slice(-10)] }; } getRecentErrors(count = 5) { return this.consoleErrors.slice(-count); } getRecentFailures(count = 3) { return this.failedRequests.slice(-count); } hasRecentActivity() { const oneMinuteAgo = Date.now() - 6e4; return this.userActions.some( (action) => new Date(action.timestamp).getTime() > oneMinuteAgo ); } // Public method to add custom Quala container selectors addQualaContainerSelector(selector) { if (!this.qualaContainerSelectors.includes(selector)) { this.qualaContainerSelectors.push(selector); this.log(`Added custom Quala container selector: ${selector}`); } } // Public method to check if element is Quala component (for debugging) checkIsQualaElement(element) { return this.isQualaElement(element); } // Public method to get debug info about detection getDetectionInfo() { return { containerSelectors: [...this.qualaContainerSelectors], isCapturing: this.isCapturing }; } static getInstance() { if (!_TechnicalContextCollector.instance) { _TechnicalContextCollector.instance = new _TechnicalContextCollector(); } return _TechnicalContextCollector.instance; } static initialize(debug = false) { _TechnicalContextCollector.instance = new _TechnicalContextCollector(debug); _TechnicalContextCollector.instance.initialize(); return _TechnicalContextCollector.instance; } destroy() { this.isCapturing = false; this.log("Technical context collector destroyed"); } }; // Static instance management __publicField(_TechnicalContextCollector, "instance"); let TechnicalContextCollector = _TechnicalContextCollector; const QualaFonts = { // Base font stack - works everywhere // family: "system-ui, -apple-system, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif", family: "'Inter', system-ui, -apple-system, 'Segoe UI', 'Roboto', sans-serif", // Font sizes sizes: { xs: "10px", sm: "12px", base: "14px", lg: "16px", xl: "18px", "2xl": "20px" }, // Font weights weights: { normal: "400", medium: "500", semibold: "600", bold: "700" }, // Line heights lineHeights: { normal: "1.5" } }; const createTextStyle = (size, weight = "normal") => ({ fontFamily: QualaFonts.family, fontSize: QualaFonts.sizes[size], fontWeight: QualaFonts.weights[weight], lineHeight: QualaFonts.lineHeights.normal, letterSpacing: "-0.01em" }); createTextStyle("lg", "semibold"); createTextStyle("base", "normal"); createTextStyle("sm", "normal"); const themeColors$1 = { light: { background: "rgba(252, 252, 253, 0.98)", border: "rgba(226, 232, 240, 0.8)", text: { primary: "#0f172a", secondary: "#334155", tertiary: "#64748b" }, gradient: { from: "rgba(248, 250, 252, 0.8)", to: "rgba(241, 245, 249, 0.8)" }, button: { hover: "rgba(241, 245, 249, 0.8)", active: "rgba(226, 232, 240, 0.8)" }, input: { background: "rgba(248, 250, 252, 0.8)", border: "rgba(226, 232, 240, 0.8)", focus: { border: "#0EA5E9", shadow: "rgba(14, 165, 233, 0.15)" } } }, dark: { background: "rgba(15, 23, 42, 0.98)", border: "rgba(51, 65, 85, 0.8)", text: { primary: "#f8fafc", secondary: "#e2e8f0", tertiary: "#94a3b8" }, gradient: { from: "rgba(30, 41, 59, 0.8)", to: "rgba(15, 23, 42, 0.8)" }, button: { hover: "rgba(30, 41, 59, 0.8)", active: "rgba(51, 65, 85, 0.8)" }, input: { background: "rgba(30, 41, 59, 0.8)", border: "rgba(51, 65, 85, 0.8)", focus: { border: "#38BDF8", shadow: "rgba(56, 189, 248, 0.15)" } } } }; const QualaWidget = ({ triggerText = "Start Trial", title = "Before you start your trial...", questions, onSubmit, onClose, onCancel, open: controlledOpen, onComplete, modalStyle = {}, theme = "light", showPoweredBy = true }) => { const colors = themeColors$1[theme]; const isControlled = controlledOpen !== void 0; const [internalOpen, setInternalOpen] = useState(false); const open = isControlled ? controlledOpen : internalOpen; const [answers, setAnswers] = useState({}); const [errors, setErrors] = useState({}); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [currentStep, setCurrentStep] = useState(0); const inputRef = useRef(null); const textareaRef = useRef(null); const adjustTextareaHeight = useCallback(() => { if (textareaRef.current) { textareaRef.current.style.height = "auto"; textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; } }, []); useEffect(() => { var _a, _b; if (open) { const currentQuestion = questions[currentStep]; if ((currentQuestion == null ? void 0 : currentQuestion.type) === "textarea") { (_a = textareaRef.current) == null ? void 0 : _a.focus(); } else if ((currentQuestion == null ? void 0 : currentQuestion.type) === "text") { (_b = inputRef.current) == null ? void 0 : _b.focus(); } adjustTextareaHeight(); } }, [open, currentStep, adjustTextareaHeight]); useEffect(() => { adjustTextareaHeight(); }, [answers, adjustTextareaHeight]); useEffect(() => { const handleEscape = (e) => { if (e.key === "Escape" && open) handleClose(); }; window.addEventListener("keydown", handleEscape); return () => window.removeEventListener("keydown", handleEscape); }, [open]); const handleChange = (id, value) => { setAnswers((prev) => ({ ...prev, [id]: value })); setErrors((prev) => ({ ...prev, [id]: "" })); const currentQuestion = questions[currentStep]; if ((currentQuestion == null ? void 0 : currentQuestion.id) === id && (currentQuestion.type === "rating" || currentQuestion.type === "boolean")) { setTimeout(() => { if (currentStep < questions.length - 1) { setCurrentStep(currentStep + 1); } }, 800); } }; const validate = () => { const newErrors = {}; questions.forEach(({ id, text, required, type = "text" }) => { const answer = answers[id]; if (required && (!answer || (type === "text" || type === "textarea") && !answer.trim())) { newErrors[id] = `${text} is required`; } }); setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = async () => { if (!validate()) return; setLoading(true); try { await onSubmit(answers); setSuccess(true); } catch { setErrors({ form: "Submission failed. Please try again." }); } finally { setLoading(false); } }; const handleClose = () => { if (!isControlled) setInternalOpen(false); success ? onComplete == null ? void 0 : onComplete({ answers }) : onCancel == null ? void 0 : onCancel(); setAnswers({}); setErrors({}); setLoading(false); setSuccess(false); setCurrentStep(0); onClose == null ? void 0 : onClose(); }; const handleOpen = () => { if (!isControlled) setInternalOpen(true); }; const progress = (currentStep + 1) / questions.length * 100; const handleStepAdvance = useCallback( (stepIndex) => { const currentQuestion = questions[stepIndex]; const currentAnswer = answers[currentQuestion.id]; const questionType = currentQuestion.type || "text"; if (currentQuestion.required && (!currentAnswer || (questionType === "text" || questionType === "textarea") && !currentAnswer.trim())) { setErrors((prev) => ({ ...prev, [currentQuestion.id]: `${currentQuestion.text} is required` })); return; } setErrors((prev) => ({ ...prev, [currentQuestion.id]: "" })); setCurrentStep(stepIndex + 1); }, [questions, answers] ); return /* @__PURE__ */ jsxs("div", { style: { fontFamily: QualaFonts.family }, children: [ !isControlled && /* @__PURE__ */ jsx( "button", { onClick: handleOpen, style: { display: "inline-flex", alignItems: "center", justifyContent: "center", padding: "8px 16px", borderRadius: "8px", border: "none", fontSize: "14px", fontWeight: "500", cursor: "pointer", transition: "all 0.2s ease", backgroundColor: "#0EA5E9", color: "white", ...modalStyle.triggerColor && { backgroundColor: modalStyle.triggerColor } }, onMouseEnter: (e) => { e.currentTarget.style.backgroundColor = "#0369A1"; e.currentTarget.style.transform = "translateY(-1px)"; }, onMouseLeave: (e) => { e.currentTarget.style.backgroundColor = "#0EA5E9"; e.currentTarget.style.transform = "translateY(0)"; }, children: triggerText } ), /* @__PURE__ */ jsx( "div", { "data-quala-widget": true, className: "quala-widget", style: { position: "fixed", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: theme === "light" ? "rgba(0, 0, 0, 0.15)" : "rgba(0, 0, 0, 0.4)", backdropFilter: "blur(2px)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1e3, opacity: open ? 1 : 0, visibility: open ? "visible" : "hidden", transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)" }, onClick: (e) => { if (e.target === e.currentTarget) { handleClose(); } }, children: /* @__PURE__ */ jsxs( "div", { style: { backgroundColor: colors.background, border: `1px solid ${colors.border}`, borderRadius: "12px", padding: "16px", width: "100%", maxWidth: "360px", maxHeight: "90vh", overflowY: "auto", position: "relative", boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)", margin: "16px", transform: open ? "scale(1) translateY(0)" : "scale(0.95) translateY(10px)", transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)" }, children: [ /* @__PURE__ */ jsx( "button", { onClick: handleClose, style: { position: "absolute", top: "8px", right: "8px", width: "28px", height: "28px", display: "flex", alignItems: "center", justifyContent: "center", borderRadius: "50%", border: "none", backgroundColor: "transparent", cursor: "pointer", transition: "all 0.2s ease", color: colors.text.secondary }, onMouseEnter: (e) => { e.currentTarget.style.backgroundColor = "rgba(14, 165, 233, 0.1)"; e.currentTarget.style.color = "#0EA5E9"; }, onMouseLeave: (e) => { e.currentTarget.style.backgroundColor = "transparent"; e.currentTarget.style.color = colors.text.secondary; }, "aria-label": "Close modal", children: /* @__PURE__ */ jsx("svg", { style: { width: "14px", height: "14px" }, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) } ), success ? /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: "24px 0" }, children: [ /* @__PURE__ */ jsxs("div", { style: { marginBottom: "24px" }, children: [ /* @__PURE__ */ jsx("div", { style: { width: "48px", height: "48px", backgroundColor: "rgba(14, 165, 233, 0.1)", color: "#0EA5E9", borderRadius: "50%", display: "flex", alignItems: "center", justifyContent: "center", margin: "0 auto 16px", animation: "scaleIn 0.3s cubic-bezier(0.4, 0, 0.2, 1)" }, children: /* @__PURE__ */ jsx("svg", { style: { width: "24px", height: "24px" }, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M5 13l4 4L19 7" }) }) }), /* @__PURE__ */ jsx("h3", { style: { fontSize: "18px", fontWeight: "600", color: colors.text.primary, margin: 0, marginBottom: "8px", animation: "fadeInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1)" }, children: "Thank you for your feedback!" }), /* @__PURE__ */ jsx("p", { style: { fontSize: "14px", color: colors.text.secondary, margin: 0, lineHeight: "1.5", animation: "fadeInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1) 0.1s both" }, children: "We appreciate you taking the time to share your thoughts with us." }) ] }), /* @__PURE__ */ jsxs( "button", { onClick: handleClose, style: { padding: "10px 20px", fontSize: "14px", backgroundColor: "#0EA5E9", color: "white", border: "none", borderRadius: "8px", cursor: "pointer", fontWeight: "500", transition: "all 0.2s ease", display: "inline-flex", alignItems: "center", gap: "8px" }, onMouseEnter: (e) => { e.currentTarget.style.backgroundColor = "#0369A1"; e.currentTarget.style.transform = "translateY(-1px)"; }, onMouseLeave: (e) => { e.currentTarget.style.backgroundColor = "#0EA5E9"; e.currentTarget.style.transform = "translateY(0)"; }, children: [ "Close", /* @__PURE__ */ jsx("svg", { style: { width: "16px", height: "16px" }, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) ] } ) ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsxs("div", { style: { marginBottom: "16px" }, children: [ /* @__PURE__ */ jsx("h3", { style: { fontSize: "16px", fontWeight: "600", color: colors.text.primary, margin: 0, marginBottom: "8px" }, children: title }), /* @__PURE__ */ jsx( "div", { style: { width: "100%", height: "4px", backgroundColor: colors.input.border, borderRadius: "2px", overflow: "hidden" }, children: /* @__PURE__ */ jsx( "div", { style: { height: "100%", background: "linear-gradient(90deg, #0EA5E9 0%, #22D3EE 100%)", borderRadius: "2px", transition: "width 0.5s cubic-bezier(0.4, 0, 0.2, 1)", width: `${progress}%`, animation: "gradientMove 2s ease infinite" } } ) } ), /* @__PURE__ */ jsxs("p", { style: { fontSize: "11px", color: colors.text.secondary, margin: "6px 0 0 0" }, children: [ "Step ", currentStep + 1, " of ", questions.length ] }) ] }), /* @__PURE__ */ jsxs( "form", { onSubmit: (e) => { e.preventDefault(); handleSubmit(); }, style: { display: "flex", flexDirection: "column", gap: "16px" }, children: [ questions.map(({ id, text, required, type = "text", options, placeholder }, index) => /* @__PURE__ */ jsxs( "div", { style: { opacity: index === currentStep ? 1 : 0, display: index === currentStep ? "block" : "none", transition: "opacity 0.3s ease" }, children: [ /* @__PURE__ */ jsxs("label", { style: { fontSize: "13px", fontWeight: "500", color: colors.text.primary, marginBottom: "6px", display: "block" }, children: [ text, " ", required && /* @__PURE__ */ jsx("span", { style: { color: "#ef4444" }, children: "*" }) ] }), type === "text" && /* @__PURE__ */ jsx( "input", { ref: index === currentStep ? inputRef : null, type: "text", placeholder: placeholder || "Your answer...", style: { width: "100%", boxSizing: "border-box", padding: "10px 12px", fontSize: "13px", border: `1px solid ${colors.input.border}`, borderRadius: "8px", backgroundColor: colors.input.background, color: colors.text.primary, transition: "all 0.2s ease", outline: "none", ...errors[id] && { borderColor: "#ef4444", boxShadow: "0 0 0 3px rgba(239, 68, 68, 0.15)" }, fontFamily: QualaFonts.family }, value: answers[id] || "", onChange: (e) => handleChange(id, e.target.value), onFocus: (e) => { e.target.style.borderColor = "#0EA5E9"; e.target.style.boxShadow = "0 0 0 3px rgba(14, 165, 233, 0.15)"; }, onBlur: (e) => { if (!errors[id]) { e.target.style.borderColor = colors.input.border; e.target.style.boxShadow = "none"; } }, onKeyDown: (e) => { if (e.key === "Enter") { e.preventDefault(); if (currentStep < questions.length - 1) { handleStepAdvance(index); } else { handleSubmit(); } } }, required } ), type === "textarea" && /* @__PURE__ */ jsx( "textarea", { ref: index === currentStep ? textareaRef : null, placeholder: placeholder || "Tell us more...", style: { width: "100%", boxSizing: "border-box", padding: "10px 12px", fontSize: "13px", border: `1px solid ${colors.input.border}`, borderRadius: "8px", backgroundColor: colors.input.background, color: colors.text.primary, transition: "all 0.2s ease", outline: "none", resize: "none", minHeight: "80px", fontFamily: QualaFonts.family, ...errors[id] && { borderColor: "#ef4444", boxShadow: "0 0 0 3px rgba(239, 68, 68, 0.15)" } }, value: answers[id] || "", onChange: (e) => { handleChange(id, e.target.value); adjustTextareaHeight(); }, onFocus: (e) => { e.target.style.borderColor = "#0EA5E9";