@perspective-ai/sdk 1.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +333 -0
  2. package/dist/browser.cjs +1939 -0
  3. package/dist/browser.cjs.map +1 -0
  4. package/dist/browser.d.cts +213 -0
  5. package/dist/browser.d.ts +213 -0
  6. package/dist/browser.js +1900 -0
  7. package/dist/browser.js.map +1 -0
  8. package/dist/cdn/perspective.global.js +406 -0
  9. package/dist/cdn/perspective.global.js.map +1 -0
  10. package/dist/constants.cjs +142 -0
  11. package/dist/constants.cjs.map +1 -0
  12. package/dist/constants.d.cts +104 -0
  13. package/dist/constants.d.ts +104 -0
  14. package/dist/constants.js +127 -0
  15. package/dist/constants.js.map +1 -0
  16. package/dist/index.cjs +1596 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.d.cts +155 -0
  19. package/dist/index.d.ts +155 -0
  20. package/dist/index.js +1579 -0
  21. package/dist/index.js.map +1 -0
  22. package/package.json +83 -0
  23. package/src/browser.test.ts +388 -0
  24. package/src/browser.ts +509 -0
  25. package/src/config.test.ts +81 -0
  26. package/src/config.ts +95 -0
  27. package/src/constants.ts +214 -0
  28. package/src/float.test.ts +332 -0
  29. package/src/float.ts +231 -0
  30. package/src/fullpage.test.ts +224 -0
  31. package/src/fullpage.ts +126 -0
  32. package/src/iframe.test.ts +1037 -0
  33. package/src/iframe.ts +421 -0
  34. package/src/index.ts +61 -0
  35. package/src/loading.ts +90 -0
  36. package/src/popup.test.ts +344 -0
  37. package/src/popup.ts +157 -0
  38. package/src/slider.test.ts +277 -0
  39. package/src/slider.ts +158 -0
  40. package/src/styles.ts +395 -0
  41. package/src/types.ts +148 -0
  42. package/src/utils.test.ts +162 -0
  43. package/src/utils.ts +86 -0
  44. package/src/widget.test.ts +375 -0
  45. package/src/widget.ts +195 -0
@@ -0,0 +1,1900 @@
1
+ // src/constants.ts
2
+ var SDK_VERSION = "1.0.0";
3
+ var FEATURES = {
4
+ RESIZE: 1 << 0,
5
+ // 0b0001
6
+ THEME_SYNC: 1 << 1,
7
+ // 0b0010
8
+ ANON_ID: 1 << 2,
9
+ // 0b0100
10
+ SCROLLBAR_STYLES: 1 << 3
11
+ // 0b1000
12
+ };
13
+ var CURRENT_FEATURES = FEATURES.RESIZE | FEATURES.THEME_SYNC | FEATURES.ANON_ID | FEATURES.SCROLLBAR_STYLES;
14
+ var PARAM_KEYS = {
15
+ // User identification
16
+ email: "email",
17
+ name: "name",
18
+ // Navigation
19
+ returnUrl: "returnUrl",
20
+ // Interview behavior
21
+ voice: "voice",
22
+ scroll: "scroll",
23
+ hideProgress: "hideProgress",
24
+ hideGreeting: "hideGreeting",
25
+ hideBranding: "hideBranding",
26
+ // Interview mode & auth
27
+ mode: "mode",
28
+ invite: "invite",
29
+ // System (internal)
30
+ embed: "embed",
31
+ embedType: "embed_type",
32
+ theme: "theme"
33
+ };
34
+ var BRAND_KEYS = {
35
+ // Light mode
36
+ primary: "brand.primary",
37
+ secondary: "brand.secondary",
38
+ bg: "brand.bg",
39
+ text: "brand.text",
40
+ // Dark mode
41
+ darkPrimary: "brand.dark.primary",
42
+ darkSecondary: "brand.dark.secondary",
43
+ darkBg: "brand.dark.bg",
44
+ darkText: "brand.dark.text"
45
+ };
46
+ var UTM_PARAMS = [
47
+ "utm_source",
48
+ "utm_medium",
49
+ "utm_campaign",
50
+ "utm_term",
51
+ "utm_content"
52
+ ];
53
+ var RESERVED_PARAMS = /* @__PURE__ */ new Set([
54
+ PARAM_KEYS.embed,
55
+ PARAM_KEYS.embedType,
56
+ PARAM_KEYS.theme,
57
+ BRAND_KEYS.primary,
58
+ BRAND_KEYS.secondary,
59
+ BRAND_KEYS.bg,
60
+ BRAND_KEYS.text,
61
+ BRAND_KEYS.darkPrimary,
62
+ BRAND_KEYS.darkSecondary,
63
+ BRAND_KEYS.darkBg,
64
+ BRAND_KEYS.darkText,
65
+ ...UTM_PARAMS
66
+ ]);
67
+ var DATA_ATTRS = {
68
+ widget: "data-perspective-widget",
69
+ popup: "data-perspective-popup",
70
+ slider: "data-perspective-slider",
71
+ float: "data-perspective-float",
72
+ // Primary name
73
+ chat: "data-perspective-chat",
74
+ // Legacy alias
75
+ fullpage: "data-perspective-fullpage",
76
+ params: "data-perspective-params",
77
+ brand: "data-perspective-brand",
78
+ brandDark: "data-perspective-brand-dark",
79
+ theme: "data-perspective-theme",
80
+ noStyle: "data-perspective-no-style"
81
+ };
82
+ var MESSAGE_TYPES = {
83
+ // SDK -> Iframe (initialization)
84
+ init: "perspective:init",
85
+ // Iframe -> SDK
86
+ ready: "perspective:ready",
87
+ resize: "perspective:resize",
88
+ submit: "perspective:submit",
89
+ close: "perspective:close",
90
+ error: "perspective:error",
91
+ redirect: "perspective:redirect",
92
+ // SDK -> Iframe (internal)
93
+ anonId: "perspective:anon-id",
94
+ injectStyles: "perspective:inject-styles",
95
+ themeChange: "perspective:theme-change",
96
+ // Iframe -> SDK (internal)
97
+ requestScrollbarStyles: "perspective:request-scrollbar-styles"
98
+ };
99
+ var PARAM_VALUES = {
100
+ disabled: "0",
101
+ enabled: "1",
102
+ true: "true",
103
+ false: "false"
104
+ };
105
+ var THEME_VALUES = {
106
+ dark: "dark",
107
+ light: "light",
108
+ system: "system"
109
+ };
110
+ var STORAGE_KEYS = {
111
+ anonId: "perspective-anon-id"
112
+ };
113
+
114
+ // src/config.ts
115
+ var DEFAULT_HOST = "https://getperspective.ai";
116
+ var globalConfig = {};
117
+ function configure(config) {
118
+ globalConfig = { ...globalConfig, ...config };
119
+ }
120
+ function getConfig() {
121
+ return { ...globalConfig };
122
+ }
123
+ function hasDom() {
124
+ return typeof window !== "undefined" && typeof document !== "undefined";
125
+ }
126
+ function normalizeToOrigin(host) {
127
+ try {
128
+ return new URL(host).origin;
129
+ } catch {
130
+ return host;
131
+ }
132
+ }
133
+ function getHost(instanceHost) {
134
+ if (instanceHost) {
135
+ return normalizeToOrigin(instanceHost);
136
+ }
137
+ if (globalConfig.host) {
138
+ return normalizeToOrigin(globalConfig.host);
139
+ }
140
+ if (hasDom()) {
141
+ const scriptHost = getScriptHost();
142
+ if (scriptHost) {
143
+ return scriptHost;
144
+ }
145
+ }
146
+ return DEFAULT_HOST;
147
+ }
148
+ var capturedScriptHost = null;
149
+ function getScriptHost() {
150
+ if (capturedScriptHost !== null) {
151
+ return capturedScriptHost;
152
+ }
153
+ if (!hasDom()) {
154
+ return null;
155
+ }
156
+ const currentScript = document.currentScript;
157
+ if (currentScript?.src) {
158
+ try {
159
+ capturedScriptHost = new URL(currentScript.src).origin;
160
+ return capturedScriptHost;
161
+ } catch {
162
+ }
163
+ }
164
+ capturedScriptHost = "";
165
+ return null;
166
+ }
167
+ if (hasDom()) {
168
+ getScriptHost();
169
+ }
170
+
171
+ // src/utils.ts
172
+ function cn(...classes) {
173
+ return classes.map((c) => (c || "").split(" ")).flat().filter(Boolean).join(" ");
174
+ }
175
+ function getThemeClass(theme) {
176
+ return theme && theme !== THEME_VALUES.system ? `perspective-${theme}-theme` : void 0;
177
+ }
178
+ function resolveIsDark(theme) {
179
+ if (theme === THEME_VALUES.dark) return true;
180
+ if (theme === THEME_VALUES.light) return false;
181
+ if (!hasDom()) return false;
182
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
183
+ }
184
+ function resolveTheme(themeOverride) {
185
+ return resolveIsDark(themeOverride) ? "dark" : "light";
186
+ }
187
+ function normalizeHex(color) {
188
+ const trimmed = color.trim();
189
+ if (!trimmed) return void 0;
190
+ const normalized = trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
191
+ if (/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(normalized)) {
192
+ return normalized;
193
+ }
194
+ const hexChars = normalized.slice(1).replace(/[^0-9a-fA-F]/g, "");
195
+ if (hexChars.length >= 6) return `#${hexChars.slice(0, 6)}`;
196
+ if (hexChars.length >= 3) return `#${hexChars.slice(0, 3)}`;
197
+ return void 0;
198
+ }
199
+ function hexToRgba(hex, alpha) {
200
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
201
+ if (!result || !result[1] || !result[2] || !result[3]) {
202
+ return `rgba(118, 41, 200, ${alpha})`;
203
+ }
204
+ const r = parseInt(result[1], 16);
205
+ const g = parseInt(result[2], 16);
206
+ const b = parseInt(result[3], 16);
207
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
208
+ }
209
+
210
+ // src/iframe.ts
211
+ function isAllowedRedirectUrl(url) {
212
+ if (!url || typeof url !== "string") return false;
213
+ try {
214
+ const parsed = new URL(url, window.location.origin);
215
+ const protocol = parsed.protocol.toLowerCase();
216
+ const hostname = parsed.hostname.toLowerCase();
217
+ if (protocol === "https:") return true;
218
+ if (protocol === "http:" && (hostname === "localhost" || hostname === "127.0.0.1"))
219
+ return true;
220
+ return false;
221
+ } catch {
222
+ return false;
223
+ }
224
+ }
225
+ function getOrCreateAnonId() {
226
+ if (!hasDom()) return "";
227
+ try {
228
+ let id = localStorage.getItem(STORAGE_KEYS.anonId);
229
+ if (!id) {
230
+ id = crypto.randomUUID();
231
+ localStorage.setItem(STORAGE_KEYS.anonId, id);
232
+ }
233
+ return id;
234
+ } catch {
235
+ return crypto.randomUUID();
236
+ }
237
+ }
238
+ function getUtmParams() {
239
+ if (!hasDom()) return {};
240
+ const params = {};
241
+ const searchParams = new URLSearchParams(window.location.search);
242
+ for (const key of UTM_PARAMS) {
243
+ const value = searchParams.get(key);
244
+ if (value) {
245
+ params[key] = value;
246
+ }
247
+ }
248
+ return params;
249
+ }
250
+ function buildIframeUrl(researchId, type, host, customParams, brand, themeOverride) {
251
+ const url = new URL(`${host}/interview/${researchId}`);
252
+ url.searchParams.set(PARAM_KEYS.embed, PARAM_VALUES.true);
253
+ url.searchParams.set(PARAM_KEYS.embedType, type === "float" ? "chat" : type);
254
+ if (hasDom()) {
255
+ const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
256
+ if (themeOverride && themeOverride !== THEME_VALUES.system) {
257
+ url.searchParams.set(PARAM_KEYS.theme, themeOverride);
258
+ } else {
259
+ url.searchParams.set(
260
+ PARAM_KEYS.theme,
261
+ isDark ? THEME_VALUES.dark : THEME_VALUES.light
262
+ );
263
+ }
264
+ } else {
265
+ url.searchParams.set(PARAM_KEYS.theme, themeOverride || THEME_VALUES.light);
266
+ }
267
+ const utmParams = getUtmParams();
268
+ for (const [key, value] of Object.entries(utmParams)) {
269
+ url.searchParams.set(key, value);
270
+ }
271
+ const setColor = (key, color) => {
272
+ if (!color) return;
273
+ const normalized = normalizeHex(color);
274
+ if (normalized) url.searchParams.set(key, normalized);
275
+ };
276
+ if (brand?.light) {
277
+ setColor(BRAND_KEYS.primary, brand.light.primary);
278
+ setColor(BRAND_KEYS.secondary, brand.light.secondary);
279
+ setColor(BRAND_KEYS.bg, brand.light.bg);
280
+ setColor(BRAND_KEYS.text, brand.light.text);
281
+ }
282
+ if (brand?.dark) {
283
+ setColor(BRAND_KEYS.darkPrimary, brand.dark.primary);
284
+ setColor(BRAND_KEYS.darkSecondary, brand.dark.secondary);
285
+ setColor(BRAND_KEYS.darkBg, brand.dark.bg);
286
+ setColor(BRAND_KEYS.darkText, brand.dark.text);
287
+ }
288
+ if (customParams) {
289
+ for (const [key, value] of Object.entries(customParams)) {
290
+ if (!RESERVED_PARAMS.has(key)) {
291
+ url.searchParams.set(key, value);
292
+ }
293
+ }
294
+ }
295
+ return url.toString();
296
+ }
297
+ function createIframe(researchId, type, host, params, brand, themeOverride) {
298
+ if (!hasDom()) {
299
+ return {};
300
+ }
301
+ const iframe = document.createElement("iframe");
302
+ iframe.src = buildIframeUrl(
303
+ researchId,
304
+ type,
305
+ host,
306
+ params,
307
+ brand,
308
+ themeOverride
309
+ );
310
+ iframe.setAttribute("allow", "microphone; camera");
311
+ iframe.setAttribute("allowfullscreen", "true");
312
+ iframe.setAttribute(
313
+ "sandbox",
314
+ "allow-scripts allow-same-origin allow-forms allow-popups allow-modals allow-top-navigation"
315
+ );
316
+ iframe.setAttribute("data-perspective", "true");
317
+ iframe.style.cssText = "border:none;";
318
+ return iframe;
319
+ }
320
+ function setupMessageListener(researchId, config, iframe, host, options) {
321
+ if (!hasDom()) {
322
+ return () => {
323
+ };
324
+ }
325
+ const handler = (event) => {
326
+ if (event.origin !== host) return;
327
+ if (event.source !== iframe.contentWindow) return;
328
+ if (typeof event.data?.type !== "string") return;
329
+ if (!event.data.type.startsWith("perspective:")) return;
330
+ if (event.data.researchId !== researchId) return;
331
+ switch (event.data.type) {
332
+ case MESSAGE_TYPES.ready:
333
+ sendScrollbarStyles(iframe, host);
334
+ sendMessage(iframe, host, {
335
+ type: MESSAGE_TYPES.anonId,
336
+ anonId: getOrCreateAnonId()
337
+ });
338
+ sendMessage(iframe, host, {
339
+ type: MESSAGE_TYPES.init,
340
+ version: SDK_VERSION,
341
+ features: CURRENT_FEATURES,
342
+ researchId
343
+ });
344
+ config.onReady?.();
345
+ break;
346
+ case MESSAGE_TYPES.resize:
347
+ if (!options?.skipResize) {
348
+ iframe.style.height = `${event.data.height}px`;
349
+ }
350
+ break;
351
+ case MESSAGE_TYPES.submit:
352
+ config.onSubmit?.({ researchId });
353
+ break;
354
+ case MESSAGE_TYPES.close:
355
+ config.onClose?.();
356
+ break;
357
+ case MESSAGE_TYPES.error:
358
+ const error = new Error(
359
+ event.data.error
360
+ );
361
+ error.code = event.data.code || "UNKNOWN";
362
+ config.onError?.(error);
363
+ break;
364
+ case MESSAGE_TYPES.redirect:
365
+ const redirectUrl = event.data.url;
366
+ if (!isAllowedRedirectUrl(redirectUrl)) {
367
+ console.warn(
368
+ "[Perspective] Blocked unsafe redirect URL:",
369
+ redirectUrl
370
+ );
371
+ return;
372
+ }
373
+ if (config.onNavigate) {
374
+ config.onNavigate(redirectUrl);
375
+ } else {
376
+ window.location.href = redirectUrl;
377
+ }
378
+ break;
379
+ }
380
+ };
381
+ window.addEventListener("message", handler);
382
+ return () => window.removeEventListener("message", handler);
383
+ }
384
+ function sendMessage(iframe, host, message) {
385
+ if (!hasDom()) return;
386
+ iframe.contentWindow?.postMessage(message, host);
387
+ }
388
+ var activeIframes = /* @__PURE__ */ new Map();
389
+ function registerIframe(iframe, host) {
390
+ activeIframes.set(iframe, host);
391
+ return () => {
392
+ activeIframes.delete(iframe);
393
+ if (activeIframes.size === 0) {
394
+ teardownGlobalListeners();
395
+ }
396
+ };
397
+ }
398
+ function getScrollbarStyles() {
399
+ if (!hasDom()) return "";
400
+ const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
401
+ const borderColor = isDark ? "hsl(217 33% 17%)" : "hsl(240 6% 90%)";
402
+ return `
403
+ * {
404
+ scrollbar-width: thin;
405
+ scrollbar-color: ${borderColor} transparent;
406
+ }
407
+ *::-webkit-scrollbar {
408
+ width: 10px;
409
+ height: 10px;
410
+ }
411
+ *::-webkit-scrollbar-track {
412
+ background: transparent;
413
+ }
414
+ *::-webkit-scrollbar-thumb {
415
+ background-color: ${borderColor};
416
+ border-radius: 9999px;
417
+ border: 2px solid transparent;
418
+ background-clip: padding-box;
419
+ }
420
+ *::-webkit-scrollbar-thumb:hover {
421
+ background-color: color-mix(in srgb, ${borderColor} 80%, currentColor);
422
+ }
423
+ `;
424
+ }
425
+ function sendScrollbarStyles(iframe, host) {
426
+ const styles = getScrollbarStyles();
427
+ sendMessage(iframe, host, {
428
+ type: MESSAGE_TYPES.injectStyles,
429
+ styles
430
+ });
431
+ }
432
+ function notifyThemeChange() {
433
+ if (!hasDom()) return;
434
+ const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
435
+ activeIframes.forEach((host, iframe) => {
436
+ const message = {
437
+ type: MESSAGE_TYPES.themeChange,
438
+ theme: isDark ? THEME_VALUES.dark : THEME_VALUES.light
439
+ };
440
+ sendMessage(iframe, host, message);
441
+ sendScrollbarStyles(iframe, host);
442
+ });
443
+ }
444
+ var themeListener = null;
445
+ var themeMediaQuery = null;
446
+ var globalMessageHandler = null;
447
+ var globalListenersInitialized = false;
448
+ function setupThemeListener() {
449
+ if (themeListener || !hasDom()) return;
450
+ themeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
451
+ themeListener = () => notifyThemeChange();
452
+ themeMediaQuery.addEventListener("change", themeListener);
453
+ }
454
+ function teardownThemeListener() {
455
+ if (themeListener && themeMediaQuery) {
456
+ themeMediaQuery.removeEventListener("change", themeListener);
457
+ themeListener = null;
458
+ themeMediaQuery = null;
459
+ }
460
+ }
461
+ function setupGlobalListeners() {
462
+ if (!hasDom() || globalMessageHandler) return;
463
+ setupThemeListener();
464
+ globalMessageHandler = (event) => {
465
+ if (!event.data?.type?.startsWith("perspective:")) return;
466
+ if (event.data.type === MESSAGE_TYPES.requestScrollbarStyles) {
467
+ const iframes = Array.from(
468
+ document.querySelectorAll("iframe[data-perspective]")
469
+ );
470
+ const sourceIframe = iframes.find(
471
+ (iframe) => iframe.contentWindow === event.source
472
+ );
473
+ if (sourceIframe) {
474
+ const host = activeIframes.get(sourceIframe);
475
+ if (host && event.origin === host) {
476
+ sendScrollbarStyles(sourceIframe, host);
477
+ }
478
+ }
479
+ }
480
+ };
481
+ window.addEventListener("message", globalMessageHandler);
482
+ }
483
+ function teardownGlobalListeners() {
484
+ if (globalMessageHandler) {
485
+ window.removeEventListener("message", globalMessageHandler);
486
+ globalMessageHandler = null;
487
+ }
488
+ teardownThemeListener();
489
+ globalListenersInitialized = false;
490
+ }
491
+ function ensureGlobalListeners() {
492
+ if (globalListenersInitialized) return;
493
+ globalListenersInitialized = true;
494
+ setupGlobalListeners();
495
+ }
496
+
497
+ // src/loading.ts
498
+ var DEFAULT_COLORS = {
499
+ light: {
500
+ bg: "#ffffff",
501
+ primary: "#7629C8"
502
+ },
503
+ dark: {
504
+ bg: "#02040a",
505
+ primary: "#B170FF"
506
+ }
507
+ };
508
+ function getLoadingColors(options) {
509
+ const theme = resolveTheme(options?.theme);
510
+ const isDark = theme === "dark";
511
+ const brandColors = isDark ? options?.brand?.dark : options?.brand?.light;
512
+ return {
513
+ bg: brandColors?.bg || (isDark ? DEFAULT_COLORS.dark.bg : DEFAULT_COLORS.light.bg),
514
+ primary: brandColors?.primary || (isDark ? DEFAULT_COLORS.dark.primary : DEFAULT_COLORS.light.primary)
515
+ };
516
+ }
517
+ function createLoadingIndicator(options) {
518
+ if (!hasDom()) {
519
+ return { remove: () => {
520
+ }, style: {} };
521
+ }
522
+ const colors = getLoadingColors(options);
523
+ const container = document.createElement("div");
524
+ container.className = "perspective-loading";
525
+ container.style.cssText = `
526
+ position: absolute;
527
+ top: 0;
528
+ left: 0;
529
+ right: 0;
530
+ bottom: 0;
531
+ display: flex;
532
+ align-items: center;
533
+ justify-content: center;
534
+ background: ${colors.bg};
535
+ transition: opacity 0.3s ease;
536
+ z-index: 1;
537
+ `;
538
+ const spinner = document.createElement("div");
539
+ spinner.style.cssText = `
540
+ width: 2.5rem;
541
+ height: 2.5rem;
542
+ border: 3px solid ${hexToRgba(colors.primary, 0.15)};
543
+ border-top-color: ${colors.primary};
544
+ border-radius: 50%;
545
+ animation: perspective-spin 0.8s linear infinite;
546
+ `;
547
+ container.appendChild(spinner);
548
+ return container;
549
+ }
550
+
551
+ // src/styles.ts
552
+ var stylesInjected = false;
553
+ var LIGHT_THEME = `
554
+ --perspective-overlay-bg: rgba(0, 0, 0, 0.5);
555
+ --perspective-modal-bg: #ffffff;
556
+ --perspective-modal-text: #151B23;
557
+ --perspective-close-bg: rgba(0, 0, 0, 0.1);
558
+ --perspective-close-text: #666666;
559
+ --perspective-close-hover-bg: rgba(0, 0, 0, 0.2);
560
+ --perspective-close-hover-text: #333333;
561
+ --perspective-border: hsl(240 6% 90%);
562
+ `;
563
+ var DARK_THEME = `
564
+ --perspective-overlay-bg: rgba(0, 0, 0, 0.7);
565
+ --perspective-modal-bg: #02040a;
566
+ --perspective-modal-text: #ffffff;
567
+ --perspective-close-bg: rgba(255, 255, 255, 0.1);
568
+ --perspective-close-text: #a0a0a0;
569
+ --perspective-close-hover-bg: rgba(255, 255, 255, 0.2);
570
+ --perspective-close-hover-text: #ffffff;
571
+ --perspective-border: hsl(217 33% 17%);
572
+ `;
573
+ function injectStyles() {
574
+ if (!hasDom()) return;
575
+ if (stylesInjected) return;
576
+ stylesInjected = true;
577
+ const style = document.createElement("style");
578
+ style.id = "perspective-embed-styles";
579
+ style.textContent = `
580
+ /* Theme-aware color variables */
581
+ .perspective-embed-root, .perspective-light-theme {
582
+ ${LIGHT_THEME}
583
+ --perspective-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
584
+ --perspective-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
585
+ --perspective-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
586
+ --perspective-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
587
+ --perspective-radius: 1.2rem;
588
+ --perspective-radius-sm: calc(var(--perspective-radius) - 4px);
589
+ }
590
+
591
+ /* Dark theme */
592
+ .perspective-dark-theme {
593
+ ${DARK_THEME}
594
+ }
595
+
596
+ /* System dark mode support */
597
+ @media (prefers-color-scheme: dark) {
598
+ .perspective-embed-root:not(.perspective-light-theme) {
599
+ ${DARK_THEME}
600
+ }
601
+ }
602
+
603
+ /* Scrollbar styling */
604
+ .perspective-modal,
605
+ .perspective-slider,
606
+ .perspective-float-window,
607
+ .perspective-chat-window {
608
+ scrollbar-width: thin;
609
+ scrollbar-color: var(--perspective-border) transparent;
610
+ }
611
+
612
+ .perspective-modal::-webkit-scrollbar,
613
+ .perspective-slider::-webkit-scrollbar,
614
+ .perspective-float-window::-webkit-scrollbar,
615
+ .perspective-chat-window::-webkit-scrollbar {
616
+ width: 10px;
617
+ height: 10px;
618
+ }
619
+
620
+ .perspective-modal::-webkit-scrollbar-track,
621
+ .perspective-slider::-webkit-scrollbar-track,
622
+ .perspective-float-window::-webkit-scrollbar-track,
623
+ .perspective-chat-window::-webkit-scrollbar-track {
624
+ background: transparent;
625
+ }
626
+
627
+ .perspective-modal::-webkit-scrollbar-thumb,
628
+ .perspective-slider::-webkit-scrollbar-thumb,
629
+ .perspective-float-window::-webkit-scrollbar-thumb,
630
+ .perspective-chat-window::-webkit-scrollbar-thumb {
631
+ background-color: var(--perspective-border);
632
+ border-radius: 9999px;
633
+ border: 2px solid transparent;
634
+ background-clip: padding-box;
635
+ }
636
+
637
+ .perspective-modal::-webkit-scrollbar-thumb:hover,
638
+ .perspective-slider::-webkit-scrollbar-thumb:hover,
639
+ .perspective-float-window::-webkit-scrollbar-thumb:hover,
640
+ .perspective-chat-window::-webkit-scrollbar-thumb:hover {
641
+ background-color: color-mix(in srgb, var(--perspective-border) 80%, currentColor);
642
+ }
643
+
644
+ /* Overlay for popup/modal */
645
+ .perspective-overlay {
646
+ position: fixed;
647
+ inset: 0;
648
+ background: var(--perspective-overlay-bg);
649
+ display: flex;
650
+ align-items: center;
651
+ justify-content: center;
652
+ z-index: 9999;
653
+ animation: perspective-fade-in 0.2s ease-out;
654
+ }
655
+
656
+ @keyframes perspective-fade-in {
657
+ from { opacity: 0; }
658
+ to { opacity: 1; }
659
+ }
660
+
661
+ @keyframes perspective-spin {
662
+ to { transform: rotate(360deg); }
663
+ }
664
+
665
+ /* Modal container */
666
+ .perspective-modal {
667
+ position: relative;
668
+ width: 90%;
669
+ max-width: 600px;
670
+ height: 80vh;
671
+ max-height: 700px;
672
+ background: var(--perspective-modal-bg);
673
+ color: var(--perspective-modal-text);
674
+ border-radius: var(--perspective-radius);
675
+ overflow: hidden;
676
+ box-shadow: var(--perspective-shadow-xl);
677
+ animation: perspective-slide-up 0.3s ease-out;
678
+ }
679
+
680
+ @keyframes perspective-slide-up {
681
+ from {
682
+ opacity: 0;
683
+ transform: translateY(20px) scale(0.95);
684
+ }
685
+ to {
686
+ opacity: 1;
687
+ transform: translateY(0) scale(1);
688
+ }
689
+ }
690
+
691
+ .perspective-modal iframe {
692
+ width: 100%;
693
+ height: 100%;
694
+ border: none;
695
+ }
696
+
697
+ /* Close button */
698
+ .perspective-close {
699
+ position: absolute;
700
+ top: 1rem;
701
+ right: 1.5rem;
702
+ width: 2rem;
703
+ height: 2rem;
704
+ border: none;
705
+ background: var(--perspective-close-bg);
706
+ color: var(--perspective-close-text);
707
+ border-radius: 50%;
708
+ cursor: pointer;
709
+ display: flex;
710
+ align-items: center;
711
+ justify-content: center;
712
+ font-size: 1rem;
713
+ z-index: 10;
714
+ transition: background-color 0.2s ease, color 0.2s ease;
715
+ }
716
+
717
+ .perspective-close:hover {
718
+ background: var(--perspective-close-hover-bg);
719
+ color: var(--perspective-close-hover-text);
720
+ }
721
+
722
+ .perspective-close:focus-visible {
723
+ outline: 2px solid currentColor;
724
+ outline-offset: 2px;
725
+ }
726
+
727
+ .perspective-close svg {
728
+ width: 1rem;
729
+ height: 1rem;
730
+ stroke-width: 2;
731
+ }
732
+
733
+ /* Slider drawer */
734
+ .perspective-slider {
735
+ position: fixed;
736
+ top: 0;
737
+ right: 0;
738
+ width: 100%;
739
+ max-width: 450px;
740
+ height: 100%;
741
+ background: var(--perspective-modal-bg);
742
+ color: var(--perspective-modal-text);
743
+ box-shadow: var(--perspective-shadow-xl);
744
+ z-index: 9999;
745
+ animation: perspective-slide-in 0.3s ease-out;
746
+ }
747
+
748
+ @keyframes perspective-slide-in {
749
+ from { transform: translateX(100%); }
750
+ to { transform: translateX(0); }
751
+ }
752
+
753
+ .perspective-slider iframe {
754
+ width: 100%;
755
+ height: 100%;
756
+ border: none;
757
+ }
758
+
759
+ .perspective-slider .perspective-close {
760
+ top: 1rem;
761
+ right: 2rem;
762
+ }
763
+
764
+ /* Slider backdrop */
765
+ .perspective-slider-backdrop {
766
+ position: fixed;
767
+ inset: 0;
768
+ background: var(--perspective-overlay-bg);
769
+ z-index: 9998;
770
+ animation: perspective-fade-in 0.2s ease-out;
771
+ }
772
+
773
+ /* Float bubble (and legacy chat-bubble alias) */
774
+ .perspective-float-bubble,
775
+ .perspective-chat-bubble {
776
+ position: fixed;
777
+ bottom: 1.5rem;
778
+ right: 1.5rem;
779
+ width: 3.75rem;
780
+ height: 3.75rem;
781
+ border-radius: 50%;
782
+ background: var(--perspective-float-bg, var(--perspective-chat-bg, #7629C8));
783
+ color: white;
784
+ border: none;
785
+ cursor: pointer;
786
+ box-shadow: var(--perspective-float-shadow, var(--perspective-chat-shadow, 0 4px 12px rgba(118, 41, 200, 0.4)));
787
+ z-index: 9996;
788
+ display: flex;
789
+ align-items: center;
790
+ justify-content: center;
791
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
792
+ }
793
+
794
+ .perspective-float-bubble:hover,
795
+ .perspective-chat-bubble:hover {
796
+ transform: scale(1.05);
797
+ box-shadow: var(--perspective-float-shadow-hover, var(--perspective-chat-shadow-hover, 0 6px 16px rgba(118, 41, 200, 0.5)));
798
+ }
799
+
800
+ .perspective-float-bubble:focus-visible,
801
+ .perspective-chat-bubble:focus-visible {
802
+ outline: 2px solid currentColor;
803
+ outline-offset: 2px;
804
+ }
805
+
806
+ .perspective-float-bubble svg,
807
+ .perspective-chat-bubble svg {
808
+ width: 1.75rem;
809
+ height: 1.75rem;
810
+ stroke-width: 2;
811
+ }
812
+
813
+ /* Float window (and legacy chat-window alias) */
814
+ .perspective-float-window,
815
+ .perspective-chat-window {
816
+ position: fixed;
817
+ bottom: 6.25rem;
818
+ right: 1.5rem;
819
+ width: 380px;
820
+ height: calc(100vh - 8.75rem);
821
+ max-height: 600px;
822
+ background: var(--perspective-modal-bg);
823
+ color: var(--perspective-modal-text);
824
+ border-radius: var(--perspective-radius);
825
+ overflow: hidden;
826
+ box-shadow: var(--perspective-shadow-xl);
827
+ z-index: 9997;
828
+ animation: perspective-float-open 0.3s ease-out;
829
+ }
830
+
831
+ @keyframes perspective-float-open {
832
+ from {
833
+ opacity: 0;
834
+ transform: translateY(20px) scale(0.9);
835
+ }
836
+ to {
837
+ opacity: 1;
838
+ transform: translateY(0) scale(1);
839
+ }
840
+ }
841
+
842
+ .perspective-float-window iframe,
843
+ .perspective-chat-window iframe {
844
+ width: 100%;
845
+ height: 100%;
846
+ border: none;
847
+ }
848
+
849
+ .perspective-float-window .perspective-close,
850
+ .perspective-chat-window .perspective-close {
851
+ top: 1rem;
852
+ right: 1.5rem;
853
+ }
854
+
855
+ /* Fullpage */
856
+ .perspective-fullpage {
857
+ position: fixed;
858
+ inset: 0;
859
+ z-index: 9999;
860
+ background: var(--perspective-modal-bg);
861
+ }
862
+
863
+ .perspective-fullpage iframe {
864
+ width: 100%;
865
+ height: 100%;
866
+ border: none;
867
+ }
868
+
869
+ /* Responsive */
870
+ @media (max-width: 640px) {
871
+ .perspective-modal {
872
+ width: 100%;
873
+ height: 100%;
874
+ max-width: none;
875
+ max-height: none;
876
+ border-radius: 0;
877
+ }
878
+
879
+ .perspective-slider {
880
+ max-width: 100%;
881
+ }
882
+
883
+ .perspective-float-window,
884
+ .perspective-chat-window {
885
+ width: calc(100% - 2rem);
886
+ right: 1rem;
887
+ bottom: 5.625rem;
888
+ height: calc(100vh - 7.5rem);
889
+ }
890
+
891
+ .perspective-float-bubble,
892
+ .perspective-chat-bubble {
893
+ bottom: 1rem;
894
+ right: 1rem;
895
+ }
896
+ }
897
+
898
+ @media (max-width: 450px) {
899
+ .perspective-float-window,
900
+ .perspective-chat-window {
901
+ width: calc(100% - 1rem);
902
+ right: 0.5rem;
903
+ bottom: 5rem;
904
+ height: calc(100vh - 6.5rem);
905
+ }
906
+
907
+ .perspective-float-bubble,
908
+ .perspective-chat-bubble {
909
+ bottom: 0.75rem;
910
+ right: 0.75rem;
911
+ width: 3.5rem;
912
+ height: 3.5rem;
913
+ }
914
+
915
+ .perspective-float-bubble svg,
916
+ .perspective-chat-bubble svg {
917
+ width: 1.5rem;
918
+ height: 1.5rem;
919
+ }
920
+ }
921
+ `;
922
+ document.head.appendChild(style);
923
+ }
924
+ var MIC_ICON = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
925
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z" />
926
+ </svg>`;
927
+ var CLOSE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
928
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
929
+ </svg>`;
930
+
931
+ // src/widget.ts
932
+ var widgetResources = /* @__PURE__ */ new WeakMap();
933
+ function createNoOpHandle(researchId, type) {
934
+ return {
935
+ unmount: () => {
936
+ },
937
+ update: () => {
938
+ },
939
+ destroy: () => {
940
+ },
941
+ researchId,
942
+ type,
943
+ iframe: null,
944
+ container: null
945
+ };
946
+ }
947
+ function createExistingWidgetHandle(container, researchId) {
948
+ const existingWrapper = container.querySelector(
949
+ ".perspective-embed-root"
950
+ );
951
+ const existingIframe = container.querySelector(
952
+ "iframe[data-perspective]"
953
+ );
954
+ let destroyed = false;
955
+ const unmount = () => {
956
+ if (destroyed) return;
957
+ destroyed = true;
958
+ if (existingIframe) {
959
+ const resources = widgetResources.get(existingIframe);
960
+ if (resources) {
961
+ resources.cleanup();
962
+ resources.unregister();
963
+ widgetResources.delete(existingIframe);
964
+ }
965
+ }
966
+ existingWrapper?.remove();
967
+ };
968
+ return {
969
+ unmount,
970
+ update: () => {
971
+ },
972
+ destroy: unmount,
973
+ researchId,
974
+ type: "widget",
975
+ iframe: existingIframe,
976
+ container
977
+ };
978
+ }
979
+ function createWidget(container, config) {
980
+ const { researchId } = config;
981
+ if (!hasDom() || !container) {
982
+ return createNoOpHandle(researchId, "widget");
983
+ }
984
+ if (container.querySelector("iframe[data-perspective]")) {
985
+ return createExistingWidgetHandle(container, researchId);
986
+ }
987
+ const host = getHost(config.host);
988
+ injectStyles();
989
+ ensureGlobalListeners();
990
+ const wrapper = document.createElement("div");
991
+ wrapper.className = cn("perspective-embed-root", getThemeClass(config.theme));
992
+ wrapper.style.cssText = "position:relative;width:100%;height:100%;min-height:500px;";
993
+ const loading = createLoadingIndicator({
994
+ theme: config.theme,
995
+ brand: config.brand
996
+ });
997
+ wrapper.appendChild(loading);
998
+ const iframe = createIframe(
999
+ researchId,
1000
+ "widget",
1001
+ host,
1002
+ config.params,
1003
+ config.brand,
1004
+ config.theme
1005
+ );
1006
+ iframe.style.width = "100%";
1007
+ iframe.style.height = "100%";
1008
+ iframe.style.minHeight = "500px";
1009
+ iframe.style.opacity = "0";
1010
+ iframe.style.transition = "opacity 0.3s ease";
1011
+ wrapper.appendChild(iframe);
1012
+ container.appendChild(wrapper);
1013
+ let currentConfig = { ...config };
1014
+ const cleanup = setupMessageListener(
1015
+ researchId,
1016
+ {
1017
+ get onReady() {
1018
+ return () => {
1019
+ loading.style.opacity = "0";
1020
+ iframe.style.opacity = "1";
1021
+ setTimeout(() => loading.remove(), 300);
1022
+ currentConfig.onReady?.();
1023
+ };
1024
+ },
1025
+ get onSubmit() {
1026
+ return currentConfig.onSubmit;
1027
+ },
1028
+ get onNavigate() {
1029
+ return currentConfig.onNavigate;
1030
+ },
1031
+ get onClose() {
1032
+ return currentConfig.onClose;
1033
+ },
1034
+ get onError() {
1035
+ return currentConfig.onError;
1036
+ }
1037
+ },
1038
+ iframe,
1039
+ host,
1040
+ { skipResize: true }
1041
+ );
1042
+ const unregisterIframe = registerIframe(iframe, host);
1043
+ widgetResources.set(iframe, {
1044
+ cleanup,
1045
+ unregister: unregisterIframe,
1046
+ wrapper
1047
+ });
1048
+ let destroyed = false;
1049
+ const unmount = () => {
1050
+ if (destroyed) return;
1051
+ destroyed = true;
1052
+ cleanup();
1053
+ unregisterIframe();
1054
+ widgetResources.delete(iframe);
1055
+ wrapper.remove();
1056
+ };
1057
+ return {
1058
+ unmount,
1059
+ update: (options) => {
1060
+ currentConfig = { ...currentConfig, ...options };
1061
+ },
1062
+ destroy: unmount,
1063
+ researchId,
1064
+ type: "widget",
1065
+ iframe,
1066
+ container
1067
+ };
1068
+ }
1069
+
1070
+ // src/popup.ts
1071
+ function createNoOpHandle2(researchId) {
1072
+ return {
1073
+ unmount: () => {
1074
+ },
1075
+ update: () => {
1076
+ },
1077
+ destroy: () => {
1078
+ },
1079
+ researchId,
1080
+ type: "popup",
1081
+ iframe: null,
1082
+ container: null
1083
+ };
1084
+ }
1085
+ function openPopup(config) {
1086
+ const { researchId } = config;
1087
+ if (!hasDom()) {
1088
+ return createNoOpHandle2(researchId);
1089
+ }
1090
+ const host = getHost(config.host);
1091
+ injectStyles();
1092
+ ensureGlobalListeners();
1093
+ const overlay = document.createElement("div");
1094
+ overlay.className = cn(
1095
+ "perspective-overlay perspective-embed-root",
1096
+ getThemeClass(config.theme)
1097
+ );
1098
+ const modal = document.createElement("div");
1099
+ modal.className = "perspective-modal";
1100
+ const closeBtn = document.createElement("button");
1101
+ closeBtn.className = "perspective-close";
1102
+ closeBtn.innerHTML = CLOSE_ICON;
1103
+ closeBtn.setAttribute("aria-label", "Close");
1104
+ const loading = createLoadingIndicator({
1105
+ theme: config.theme,
1106
+ brand: config.brand
1107
+ });
1108
+ loading.style.borderRadius = "16px";
1109
+ const iframe = createIframe(
1110
+ researchId,
1111
+ "popup",
1112
+ host,
1113
+ config.params,
1114
+ config.brand,
1115
+ config.theme
1116
+ );
1117
+ iframe.style.opacity = "0";
1118
+ iframe.style.transition = "opacity 0.3s ease";
1119
+ modal.appendChild(closeBtn);
1120
+ modal.appendChild(loading);
1121
+ modal.appendChild(iframe);
1122
+ overlay.appendChild(modal);
1123
+ document.body.appendChild(overlay);
1124
+ let currentConfig = { ...config };
1125
+ let isOpen = true;
1126
+ let messageCleanup = null;
1127
+ const unregisterIframe = registerIframe(iframe, host);
1128
+ const destroy2 = () => {
1129
+ if (!isOpen) return;
1130
+ isOpen = false;
1131
+ messageCleanup?.();
1132
+ unregisterIframe();
1133
+ overlay.remove();
1134
+ document.removeEventListener("keydown", escHandler);
1135
+ currentConfig.onClose?.();
1136
+ };
1137
+ messageCleanup = setupMessageListener(
1138
+ researchId,
1139
+ {
1140
+ get onReady() {
1141
+ return () => {
1142
+ loading.style.opacity = "0";
1143
+ iframe.style.opacity = "1";
1144
+ setTimeout(() => loading.remove(), 300);
1145
+ currentConfig.onReady?.();
1146
+ };
1147
+ },
1148
+ get onSubmit() {
1149
+ return currentConfig.onSubmit;
1150
+ },
1151
+ get onNavigate() {
1152
+ return currentConfig.onNavigate;
1153
+ },
1154
+ get onClose() {
1155
+ return destroy2;
1156
+ },
1157
+ get onError() {
1158
+ return currentConfig.onError;
1159
+ }
1160
+ },
1161
+ iframe,
1162
+ host,
1163
+ { skipResize: true }
1164
+ );
1165
+ closeBtn.addEventListener("click", destroy2);
1166
+ overlay.addEventListener("click", (e) => {
1167
+ if (e.target === overlay) destroy2();
1168
+ });
1169
+ const escHandler = (e) => {
1170
+ if (e.key === "Escape") {
1171
+ destroy2();
1172
+ }
1173
+ };
1174
+ document.addEventListener("keydown", escHandler);
1175
+ return {
1176
+ unmount: destroy2,
1177
+ update: (options) => {
1178
+ currentConfig = { ...currentConfig, ...options };
1179
+ },
1180
+ destroy: destroy2,
1181
+ researchId,
1182
+ type: "popup",
1183
+ iframe,
1184
+ container: overlay
1185
+ };
1186
+ }
1187
+
1188
+ // src/slider.ts
1189
+ function createNoOpHandle3(researchId) {
1190
+ return {
1191
+ unmount: () => {
1192
+ },
1193
+ update: () => {
1194
+ },
1195
+ destroy: () => {
1196
+ },
1197
+ researchId,
1198
+ type: "slider",
1199
+ iframe: null,
1200
+ container: null
1201
+ };
1202
+ }
1203
+ function openSlider(config) {
1204
+ const { researchId } = config;
1205
+ if (!hasDom()) {
1206
+ return createNoOpHandle3(researchId);
1207
+ }
1208
+ const host = getHost(config.host);
1209
+ injectStyles();
1210
+ ensureGlobalListeners();
1211
+ const backdrop = document.createElement("div");
1212
+ backdrop.className = cn(
1213
+ "perspective-slider-backdrop perspective-embed-root",
1214
+ getThemeClass(config.theme)
1215
+ );
1216
+ const slider = document.createElement("div");
1217
+ slider.className = cn(
1218
+ "perspective-slider perspective-embed-root",
1219
+ getThemeClass(config.theme)
1220
+ );
1221
+ const closeBtn = document.createElement("button");
1222
+ closeBtn.className = "perspective-close";
1223
+ closeBtn.innerHTML = CLOSE_ICON;
1224
+ closeBtn.setAttribute("aria-label", "Close");
1225
+ const loading = createLoadingIndicator({
1226
+ theme: config.theme,
1227
+ brand: config.brand
1228
+ });
1229
+ const iframe = createIframe(
1230
+ researchId,
1231
+ "slider",
1232
+ host,
1233
+ config.params,
1234
+ config.brand,
1235
+ config.theme
1236
+ );
1237
+ iframe.style.opacity = "0";
1238
+ iframe.style.transition = "opacity 0.3s ease";
1239
+ slider.appendChild(closeBtn);
1240
+ slider.appendChild(loading);
1241
+ slider.appendChild(iframe);
1242
+ document.body.appendChild(backdrop);
1243
+ document.body.appendChild(slider);
1244
+ let currentConfig = { ...config };
1245
+ let isOpen = true;
1246
+ let messageCleanup = null;
1247
+ const unregisterIframe = registerIframe(iframe, host);
1248
+ const destroy2 = () => {
1249
+ if (!isOpen) return;
1250
+ isOpen = false;
1251
+ messageCleanup?.();
1252
+ unregisterIframe();
1253
+ slider.remove();
1254
+ backdrop.remove();
1255
+ document.removeEventListener("keydown", escHandler);
1256
+ currentConfig.onClose?.();
1257
+ };
1258
+ messageCleanup = setupMessageListener(
1259
+ researchId,
1260
+ {
1261
+ get onReady() {
1262
+ return () => {
1263
+ loading.style.opacity = "0";
1264
+ iframe.style.opacity = "1";
1265
+ setTimeout(() => loading.remove(), 300);
1266
+ currentConfig.onReady?.();
1267
+ };
1268
+ },
1269
+ get onSubmit() {
1270
+ return currentConfig.onSubmit;
1271
+ },
1272
+ get onNavigate() {
1273
+ return currentConfig.onNavigate;
1274
+ },
1275
+ get onClose() {
1276
+ return destroy2;
1277
+ },
1278
+ get onError() {
1279
+ return currentConfig.onError;
1280
+ }
1281
+ },
1282
+ iframe,
1283
+ host,
1284
+ { skipResize: true }
1285
+ );
1286
+ closeBtn.addEventListener("click", destroy2);
1287
+ backdrop.addEventListener("click", destroy2);
1288
+ const escHandler = (e) => {
1289
+ if (e.key === "Escape") {
1290
+ destroy2();
1291
+ }
1292
+ };
1293
+ document.addEventListener("keydown", escHandler);
1294
+ return {
1295
+ unmount: destroy2,
1296
+ update: (options) => {
1297
+ currentConfig = { ...currentConfig, ...options };
1298
+ },
1299
+ destroy: destroy2,
1300
+ researchId,
1301
+ type: "slider",
1302
+ iframe,
1303
+ container: slider
1304
+ };
1305
+ }
1306
+
1307
+ // src/float.ts
1308
+ function createNoOpHandle4(researchId) {
1309
+ return {
1310
+ unmount: () => {
1311
+ },
1312
+ update: () => {
1313
+ },
1314
+ destroy: () => {
1315
+ },
1316
+ open: () => {
1317
+ },
1318
+ close: () => {
1319
+ },
1320
+ toggle: () => {
1321
+ },
1322
+ isOpen: false,
1323
+ researchId,
1324
+ type: "float",
1325
+ iframe: null,
1326
+ container: null
1327
+ };
1328
+ }
1329
+ function createFloatBubble(config) {
1330
+ const { researchId, _themeConfig, theme, brand } = config;
1331
+ if (!hasDom()) {
1332
+ return createNoOpHandle4(researchId);
1333
+ }
1334
+ const host = getHost(config.host);
1335
+ injectStyles();
1336
+ ensureGlobalListeners();
1337
+ const bubble = document.createElement("button");
1338
+ bubble.className = cn(
1339
+ "perspective-float-bubble perspective-embed-root",
1340
+ getThemeClass(config.theme)
1341
+ );
1342
+ bubble.innerHTML = MIC_ICON;
1343
+ bubble.setAttribute("aria-label", "Open chat");
1344
+ bubble.setAttribute("data-perspective", "float-bubble");
1345
+ if (_themeConfig || brand) {
1346
+ const isDark = resolveIsDark(theme);
1347
+ const bg = isDark ? brand?.dark?.primary ?? _themeConfig?.darkPrimaryColor ?? "#a78bfa" : brand?.light?.primary ?? _themeConfig?.primaryColor ?? "#7c3aed";
1348
+ bubble.style.setProperty("--perspective-float-bg", bg);
1349
+ bubble.style.setProperty(
1350
+ "--perspective-float-shadow",
1351
+ `0 4px 12px ${bg}66`
1352
+ );
1353
+ bubble.style.setProperty(
1354
+ "--perspective-float-shadow-hover",
1355
+ `0 6px 16px ${bg}80`
1356
+ );
1357
+ bubble.style.backgroundColor = bg;
1358
+ bubble.style.boxShadow = `0 4px 12px ${bg}66`;
1359
+ }
1360
+ document.body.appendChild(bubble);
1361
+ let floatWindow = null;
1362
+ let iframe = null;
1363
+ let cleanup = null;
1364
+ let unregisterIframe = null;
1365
+ let isOpen = false;
1366
+ let currentConfig = { ...config };
1367
+ const openFloat = () => {
1368
+ if (isOpen) return;
1369
+ isOpen = true;
1370
+ floatWindow = document.createElement("div");
1371
+ floatWindow.className = cn(
1372
+ "perspective-float-window perspective-embed-root",
1373
+ getThemeClass(currentConfig.theme)
1374
+ );
1375
+ const closeBtn = document.createElement("button");
1376
+ closeBtn.className = "perspective-close";
1377
+ closeBtn.innerHTML = CLOSE_ICON;
1378
+ closeBtn.setAttribute("aria-label", "Close chat");
1379
+ closeBtn.addEventListener("click", closeFloat);
1380
+ const loading = createLoadingIndicator({
1381
+ theme: currentConfig.theme,
1382
+ brand: currentConfig.brand
1383
+ });
1384
+ loading.style.borderRadius = "16px";
1385
+ iframe = createIframe(
1386
+ researchId,
1387
+ "float",
1388
+ host,
1389
+ currentConfig.params,
1390
+ currentConfig.brand,
1391
+ currentConfig.theme
1392
+ );
1393
+ iframe.style.opacity = "0";
1394
+ iframe.style.transition = "opacity 0.3s ease";
1395
+ floatWindow.appendChild(closeBtn);
1396
+ floatWindow.appendChild(loading);
1397
+ floatWindow.appendChild(iframe);
1398
+ document.body.appendChild(floatWindow);
1399
+ cleanup = setupMessageListener(
1400
+ researchId,
1401
+ {
1402
+ get onReady() {
1403
+ return () => {
1404
+ loading.style.opacity = "0";
1405
+ iframe.style.opacity = "1";
1406
+ setTimeout(() => loading.remove(), 300);
1407
+ currentConfig.onReady?.();
1408
+ };
1409
+ },
1410
+ get onSubmit() {
1411
+ return currentConfig.onSubmit;
1412
+ },
1413
+ get onNavigate() {
1414
+ return currentConfig.onNavigate;
1415
+ },
1416
+ get onClose() {
1417
+ return closeFloat;
1418
+ },
1419
+ get onError() {
1420
+ return currentConfig.onError;
1421
+ }
1422
+ },
1423
+ iframe,
1424
+ host,
1425
+ { skipResize: true }
1426
+ );
1427
+ if (iframe) {
1428
+ unregisterIframe = registerIframe(iframe, host);
1429
+ }
1430
+ bubble.innerHTML = CLOSE_ICON;
1431
+ bubble.setAttribute("aria-label", "Close chat");
1432
+ };
1433
+ const closeFloat = () => {
1434
+ if (!isOpen) return;
1435
+ isOpen = false;
1436
+ cleanup?.();
1437
+ unregisterIframe?.();
1438
+ floatWindow?.remove();
1439
+ floatWindow = null;
1440
+ iframe = null;
1441
+ cleanup = null;
1442
+ unregisterIframe = null;
1443
+ bubble.innerHTML = MIC_ICON;
1444
+ bubble.setAttribute("aria-label", "Open chat");
1445
+ currentConfig.onClose?.();
1446
+ };
1447
+ bubble.addEventListener("click", () => {
1448
+ if (isOpen) {
1449
+ closeFloat();
1450
+ } else {
1451
+ openFloat();
1452
+ }
1453
+ });
1454
+ const unmount = () => {
1455
+ closeFloat();
1456
+ bubble.remove();
1457
+ };
1458
+ return {
1459
+ unmount,
1460
+ update: (options) => {
1461
+ currentConfig = { ...currentConfig, ...options };
1462
+ },
1463
+ destroy: unmount,
1464
+ open: openFloat,
1465
+ close: closeFloat,
1466
+ toggle: () => {
1467
+ if (isOpen) {
1468
+ closeFloat();
1469
+ } else {
1470
+ openFloat();
1471
+ }
1472
+ },
1473
+ get isOpen() {
1474
+ return isOpen;
1475
+ },
1476
+ researchId,
1477
+ type: "float",
1478
+ get iframe() {
1479
+ return iframe;
1480
+ },
1481
+ container: bubble
1482
+ };
1483
+ }
1484
+ var createChatBubble = createFloatBubble;
1485
+
1486
+ // src/fullpage.ts
1487
+ function createNoOpHandle5(researchId) {
1488
+ return {
1489
+ unmount: () => {
1490
+ },
1491
+ update: () => {
1492
+ },
1493
+ destroy: () => {
1494
+ },
1495
+ researchId,
1496
+ type: "fullpage",
1497
+ iframe: null,
1498
+ container: null
1499
+ };
1500
+ }
1501
+ function createFullpage(config) {
1502
+ const { researchId } = config;
1503
+ if (!hasDom()) {
1504
+ return createNoOpHandle5(researchId);
1505
+ }
1506
+ const host = getHost(config.host);
1507
+ injectStyles();
1508
+ ensureGlobalListeners();
1509
+ const container = document.createElement("div");
1510
+ container.className = cn(
1511
+ "perspective-embed-root perspective-fullpage",
1512
+ getThemeClass(config.theme)
1513
+ );
1514
+ const loading = createLoadingIndicator({
1515
+ theme: config.theme,
1516
+ brand: config.brand
1517
+ });
1518
+ container.appendChild(loading);
1519
+ const iframe = createIframe(
1520
+ researchId,
1521
+ "fullpage",
1522
+ host,
1523
+ config.params,
1524
+ config.brand,
1525
+ config.theme
1526
+ );
1527
+ iframe.style.opacity = "0";
1528
+ iframe.style.transition = "opacity 0.3s ease";
1529
+ container.appendChild(iframe);
1530
+ document.body.appendChild(container);
1531
+ let currentConfig = { ...config };
1532
+ let messageCleanup = null;
1533
+ const unregisterIframe = registerIframe(iframe, host);
1534
+ const unmount = () => {
1535
+ messageCleanup?.();
1536
+ unregisterIframe();
1537
+ container.remove();
1538
+ currentConfig.onClose?.();
1539
+ };
1540
+ messageCleanup = setupMessageListener(
1541
+ researchId,
1542
+ {
1543
+ get onReady() {
1544
+ return () => {
1545
+ loading.style.opacity = "0";
1546
+ iframe.style.opacity = "1";
1547
+ setTimeout(() => loading.remove(), 300);
1548
+ currentConfig.onReady?.();
1549
+ };
1550
+ },
1551
+ get onSubmit() {
1552
+ return currentConfig.onSubmit;
1553
+ },
1554
+ get onNavigate() {
1555
+ return currentConfig.onNavigate;
1556
+ },
1557
+ get onClose() {
1558
+ return unmount;
1559
+ },
1560
+ get onError() {
1561
+ return currentConfig.onError;
1562
+ }
1563
+ },
1564
+ iframe,
1565
+ host,
1566
+ { skipResize: true }
1567
+ );
1568
+ return {
1569
+ unmount,
1570
+ update: (options) => {
1571
+ currentConfig = { ...currentConfig, ...options };
1572
+ },
1573
+ destroy: unmount,
1574
+ researchId,
1575
+ type: "fullpage",
1576
+ iframe,
1577
+ container
1578
+ };
1579
+ }
1580
+
1581
+ // src/browser.ts
1582
+ var instances = /* @__PURE__ */ new Map();
1583
+ var configCache = /* @__PURE__ */ new Map();
1584
+ var styledButtons = /* @__PURE__ */ new Map();
1585
+ var buttonThemeMediaQuery = null;
1586
+ var DEFAULT_THEME = {
1587
+ primaryColor: "#7c3aed",
1588
+ textColor: "#ffffff",
1589
+ darkPrimaryColor: "#a78bfa",
1590
+ darkTextColor: "#ffffff"
1591
+ };
1592
+ async function fetchConfig(researchId) {
1593
+ if (configCache.has(researchId)) {
1594
+ return configCache.get(researchId);
1595
+ }
1596
+ try {
1597
+ const host = getHost();
1598
+ const res = await fetch(`${host}/api/v1/embed/config/${researchId}`);
1599
+ if (!res.ok) return DEFAULT_THEME;
1600
+ const config = await res.json();
1601
+ configCache.set(researchId, config);
1602
+ return config;
1603
+ } catch {
1604
+ return DEFAULT_THEME;
1605
+ }
1606
+ }
1607
+ function styleButton(el, themeConfig, options) {
1608
+ if (el.hasAttribute(DATA_ATTRS.noStyle)) return;
1609
+ styledButtons.set(el, {
1610
+ themeConfig,
1611
+ theme: options?.theme,
1612
+ brand: options?.brand
1613
+ });
1614
+ updateButtonTheme(el, {
1615
+ themeConfig,
1616
+ theme: options?.theme,
1617
+ brand: options?.brand
1618
+ });
1619
+ }
1620
+ function updateButtonTheme(el, config) {
1621
+ const { themeConfig, theme, brand } = config;
1622
+ const isDark = resolveIsDark(theme);
1623
+ const bg = isDark ? brand?.dark?.primary ?? themeConfig.darkPrimaryColor : brand?.light?.primary ?? themeConfig.primaryColor;
1624
+ const text = isDark ? brand?.dark?.text ?? themeConfig.darkTextColor : brand?.light?.text ?? themeConfig.textColor;
1625
+ el.style.backgroundColor = bg;
1626
+ el.style.color = text;
1627
+ el.style.padding = "10px 20px";
1628
+ el.style.border = "none";
1629
+ el.style.borderRadius = "8px";
1630
+ el.style.fontWeight = "500";
1631
+ el.style.cursor = "pointer";
1632
+ }
1633
+ function updateAllButtonThemes() {
1634
+ styledButtons.forEach((config, el) => {
1635
+ if (document.contains(el)) {
1636
+ updateButtonTheme(el, config);
1637
+ } else {
1638
+ styledButtons.delete(el);
1639
+ }
1640
+ });
1641
+ }
1642
+ var buttonThemeListener = null;
1643
+ function setupButtonThemeListener() {
1644
+ if (buttonThemeListener || !hasDom()) return;
1645
+ buttonThemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
1646
+ buttonThemeListener = () => updateAllButtonThemes();
1647
+ buttonThemeMediaQuery.addEventListener("change", buttonThemeListener);
1648
+ }
1649
+ function teardownButtonThemeListener() {
1650
+ if (buttonThemeListener && buttonThemeMediaQuery) {
1651
+ buttonThemeMediaQuery.removeEventListener("change", buttonThemeListener);
1652
+ buttonThemeListener = null;
1653
+ buttonThemeMediaQuery = null;
1654
+ }
1655
+ }
1656
+ function parseParamsAttr(el) {
1657
+ const paramsStr = el.getAttribute(DATA_ATTRS.params);
1658
+ if (!paramsStr) return void 0;
1659
+ const params = {};
1660
+ for (const pair of paramsStr.split(",")) {
1661
+ const [key, ...valueParts] = pair.trim().split("=");
1662
+ if (key) {
1663
+ params[key.trim()] = valueParts.join("=").trim();
1664
+ }
1665
+ }
1666
+ return Object.keys(params).length > 0 ? params : void 0;
1667
+ }
1668
+ function parseBrandAttr(attrValue) {
1669
+ if (!attrValue) return void 0;
1670
+ const colors = {};
1671
+ for (const pair of attrValue.split(",")) {
1672
+ const [key, ...valueParts] = pair.trim().split("=");
1673
+ if (key && valueParts.length > 0) {
1674
+ const value = valueParts.join("=").trim();
1675
+ if (value) {
1676
+ const k = key.trim();
1677
+ if (k === "primary" || k === "secondary" || k === "bg" || k === "text") {
1678
+ colors[k] = value;
1679
+ }
1680
+ }
1681
+ }
1682
+ }
1683
+ return Object.keys(colors).length > 0 ? colors : void 0;
1684
+ }
1685
+ function extractBrandConfig(el) {
1686
+ const light = parseBrandAttr(el.getAttribute(DATA_ATTRS.brand));
1687
+ const dark = parseBrandAttr(el.getAttribute(DATA_ATTRS.brandDark));
1688
+ const themeAttr = el.getAttribute(DATA_ATTRS.theme);
1689
+ const config = {};
1690
+ if (light || dark) {
1691
+ config.brand = {};
1692
+ if (light) config.brand.light = light;
1693
+ if (dark) config.brand.dark = dark;
1694
+ }
1695
+ if (themeAttr === THEME_VALUES.dark || themeAttr === THEME_VALUES.light || themeAttr === THEME_VALUES.system) {
1696
+ config.theme = themeAttr;
1697
+ }
1698
+ return config;
1699
+ }
1700
+ function init(config) {
1701
+ const { researchId } = config;
1702
+ const type = config.type === "chat" ? "float" : config.type ?? "widget";
1703
+ if (instances.has(researchId)) {
1704
+ instances.get(researchId).unmount();
1705
+ instances.delete(researchId);
1706
+ }
1707
+ let instance;
1708
+ switch (type) {
1709
+ case "popup":
1710
+ instance = openPopup(config);
1711
+ break;
1712
+ case "slider":
1713
+ instance = openSlider(config);
1714
+ break;
1715
+ case "float":
1716
+ instance = createFloatBubble(config);
1717
+ break;
1718
+ case "fullpage":
1719
+ instance = createFullpage(config);
1720
+ break;
1721
+ default:
1722
+ throw new Error(
1723
+ `Unknown embed type "${type}". Valid types: popup, slider, float, fullpage (use init()), or widget (use mount()).`
1724
+ );
1725
+ }
1726
+ instances.set(researchId, instance);
1727
+ return instance;
1728
+ }
1729
+ function mount(container, config) {
1730
+ const { researchId } = config;
1731
+ const type = config.type === "chat" ? "float" : config.type ?? "widget";
1732
+ const el = typeof container === "string" ? document.querySelector(container) : container;
1733
+ if (!el) {
1734
+ throw new Error(`Container not found: ${container}`);
1735
+ }
1736
+ if (instances.has(researchId)) {
1737
+ instances.get(researchId).unmount();
1738
+ instances.delete(researchId);
1739
+ }
1740
+ let instance;
1741
+ switch (type) {
1742
+ case "widget":
1743
+ instance = createWidget(el, config);
1744
+ break;
1745
+ default:
1746
+ instance = init({ ...config, type });
1747
+ return instance;
1748
+ }
1749
+ instances.set(researchId, instance);
1750
+ return instance;
1751
+ }
1752
+ function destroy(researchId) {
1753
+ const instance = instances.get(researchId);
1754
+ if (instance) {
1755
+ instance.unmount();
1756
+ instances.delete(researchId);
1757
+ }
1758
+ }
1759
+ function destroyAll() {
1760
+ instances.forEach((instance) => instance.unmount());
1761
+ instances.clear();
1762
+ styledButtons.clear();
1763
+ teardownButtonThemeListener();
1764
+ }
1765
+ function autoInit() {
1766
+ if (!hasDom()) return;
1767
+ setupButtonThemeListener();
1768
+ document.querySelectorAll(`[${DATA_ATTRS.widget}]`).forEach((el) => {
1769
+ const researchId = el.getAttribute(DATA_ATTRS.widget);
1770
+ if (researchId && !instances.has(researchId)) {
1771
+ const params = parseParamsAttr(el);
1772
+ const brandConfig = extractBrandConfig(el);
1773
+ mount(el, { researchId, type: "widget", params, ...brandConfig });
1774
+ }
1775
+ });
1776
+ document.querySelectorAll(`[${DATA_ATTRS.fullpage}]`).forEach((el) => {
1777
+ const researchId = el.getAttribute(DATA_ATTRS.fullpage);
1778
+ if (researchId && !instances.has(researchId)) {
1779
+ const params = parseParamsAttr(el);
1780
+ const brandConfig = extractBrandConfig(el);
1781
+ init({ researchId, type: "fullpage", params, ...brandConfig });
1782
+ }
1783
+ });
1784
+ document.querySelectorAll(`[${DATA_ATTRS.popup}]`).forEach((el) => {
1785
+ if (el.hasAttribute("data-perspective-initialized")) return;
1786
+ el.setAttribute("data-perspective-initialized", "true");
1787
+ const researchId = el.getAttribute(DATA_ATTRS.popup);
1788
+ if (researchId) {
1789
+ const params = parseParamsAttr(el);
1790
+ const brandConfig = extractBrandConfig(el);
1791
+ styleButton(el, DEFAULT_THEME, brandConfig);
1792
+ el.addEventListener("click", (e) => {
1793
+ e.preventDefault();
1794
+ init({ researchId, type: "popup", params, ...brandConfig });
1795
+ });
1796
+ fetchConfig(researchId).then((config) => {
1797
+ styleButton(el, config, brandConfig);
1798
+ });
1799
+ }
1800
+ });
1801
+ document.querySelectorAll(`[${DATA_ATTRS.slider}]`).forEach((el) => {
1802
+ if (el.hasAttribute("data-perspective-initialized")) return;
1803
+ el.setAttribute("data-perspective-initialized", "true");
1804
+ const researchId = el.getAttribute(DATA_ATTRS.slider);
1805
+ if (researchId) {
1806
+ const params = parseParamsAttr(el);
1807
+ const brandConfig = extractBrandConfig(el);
1808
+ styleButton(el, DEFAULT_THEME, brandConfig);
1809
+ el.addEventListener("click", (e) => {
1810
+ e.preventDefault();
1811
+ init({ researchId, type: "slider", params, ...brandConfig });
1812
+ });
1813
+ fetchConfig(researchId).then((config) => {
1814
+ styleButton(el, config, brandConfig);
1815
+ });
1816
+ }
1817
+ });
1818
+ const floatSelector = `[${DATA_ATTRS.float}], [${DATA_ATTRS.chat}]`;
1819
+ const floatEl = document.querySelector(floatSelector);
1820
+ if (floatEl) {
1821
+ const researchId = floatEl.getAttribute(DATA_ATTRS.float) || floatEl.getAttribute(DATA_ATTRS.chat);
1822
+ if (researchId && !instances.has(researchId)) {
1823
+ const params = parseParamsAttr(floatEl);
1824
+ const brandConfig = extractBrandConfig(floatEl);
1825
+ init({
1826
+ researchId,
1827
+ type: "float",
1828
+ params,
1829
+ ...brandConfig,
1830
+ _themeConfig: DEFAULT_THEME
1831
+ });
1832
+ fetchConfig(researchId).then((config) => {
1833
+ const bubble = document.querySelector(
1834
+ '[data-perspective="float-bubble"]'
1835
+ );
1836
+ if (bubble && !floatEl.hasAttribute(DATA_ATTRS.noStyle)) {
1837
+ const isDark = resolveIsDark(brandConfig.theme);
1838
+ const bg = isDark ? brandConfig.brand?.dark?.primary ?? config.darkPrimaryColor : brandConfig.brand?.light?.primary ?? config.primaryColor;
1839
+ bubble.style.setProperty("--perspective-float-bg", bg);
1840
+ bubble.style.setProperty(
1841
+ "--perspective-float-shadow",
1842
+ `0 4px 12px ${bg}66`
1843
+ );
1844
+ bubble.style.setProperty(
1845
+ "--perspective-float-shadow-hover",
1846
+ `0 6px 16px ${bg}80`
1847
+ );
1848
+ bubble.style.backgroundColor = bg;
1849
+ bubble.style.boxShadow = `0 4px 12px ${bg}66`;
1850
+ }
1851
+ });
1852
+ }
1853
+ }
1854
+ }
1855
+ var Perspective = {
1856
+ // Configuration
1857
+ configure,
1858
+ getConfig,
1859
+ // Instance management
1860
+ init,
1861
+ mount,
1862
+ destroy,
1863
+ destroyAll,
1864
+ autoInit,
1865
+ // Direct creation functions (primary API)
1866
+ createWidget,
1867
+ openPopup,
1868
+ openSlider,
1869
+ createFloatBubble,
1870
+ createFullpage,
1871
+ // Legacy alias
1872
+ createChatBubble
1873
+ };
1874
+ if (hasDom() && !window.__PERSPECTIVE_SDK_INITIALIZED__) {
1875
+ window.__PERSPECTIVE_SDK_INITIALIZED__ = true;
1876
+ if (document.readyState === "loading") {
1877
+ document.addEventListener("DOMContentLoaded", autoInit, { once: true });
1878
+ } else {
1879
+ autoInit();
1880
+ }
1881
+ window.Perspective = Perspective;
1882
+ }
1883
+ var browser_default = Perspective;
1884
+ export {
1885
+ autoInit,
1886
+ configure,
1887
+ createChatBubble,
1888
+ createFloatBubble,
1889
+ createFullpage,
1890
+ createWidget,
1891
+ browser_default as default,
1892
+ destroy,
1893
+ destroyAll,
1894
+ getConfig,
1895
+ init,
1896
+ mount,
1897
+ openPopup,
1898
+ openSlider
1899
+ };
1900
+ //# sourceMappingURL=browser.js.map