@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
package/src/utils.ts ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Shared utilities for the Perspective Embed SDK
3
+ * SSR-safe - DOM access is guarded
4
+ */
5
+
6
+ import { THEME_VALUES, type ThemeValue } from "./constants";
7
+ import { hasDom } from "./config";
8
+
9
+ /**
10
+ * Join class names, filtering out falsy values
11
+ */
12
+ export function cn(...classes: (string | false | null | undefined)[]): string {
13
+ return classes
14
+ .map((c) => (c || "").split(" "))
15
+ .flat()
16
+ .filter(Boolean)
17
+ .join(" ");
18
+ }
19
+
20
+ /**
21
+ * Get the perspective theme class based on the theme value
22
+ * Returns "perspective-{theme}-theme" when theme is available, undefined otherwise
23
+ */
24
+ export function getThemeClass(theme: string | undefined): string | undefined {
25
+ return theme && theme !== THEME_VALUES.system
26
+ ? `perspective-${theme}-theme`
27
+ : undefined;
28
+ }
29
+
30
+ /**
31
+ * Resolve whether dark mode should be used.
32
+ * Priority: 1) explicit theme override, 2) system preference
33
+ * SSR-safe: defaults to light theme on server
34
+ */
35
+ export function resolveIsDark(theme?: ThemeValue | string): boolean {
36
+ if (theme === THEME_VALUES.dark) return true;
37
+ if (theme === THEME_VALUES.light) return false;
38
+ // system or undefined → use system preference (or light on server)
39
+ if (!hasDom()) return false;
40
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
41
+ }
42
+
43
+ /**
44
+ * Resolve effective theme based on override and system preference
45
+ * Returns the theme string ('light' or 'dark')
46
+ */
47
+ export function resolveTheme(themeOverride?: ThemeValue): "light" | "dark" {
48
+ return resolveIsDark(themeOverride) ? "dark" : "light";
49
+ }
50
+
51
+ /**
52
+ * Normalize and validate hex color. Returns undefined for invalid colors.
53
+ */
54
+ export function normalizeHex(color: string): string | undefined {
55
+ const trimmed = color.trim();
56
+ if (!trimmed) return undefined;
57
+
58
+ const normalized = trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
59
+
60
+ // Validate hex format (#RGB, #RRGGBB, or #RRGGBBAA)
61
+ if (/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(normalized)) {
62
+ return normalized;
63
+ }
64
+
65
+ // Try to extract valid hex chars
66
+ const hexChars = normalized.slice(1).replace(/[^0-9a-fA-F]/g, "");
67
+ if (hexChars.length >= 6) return `#${hexChars.slice(0, 6)}`;
68
+ if (hexChars.length >= 3) return `#${hexChars.slice(0, 3)}`;
69
+
70
+ return undefined;
71
+ }
72
+
73
+ /**
74
+ * Convert hex to rgba for spinner track
75
+ */
76
+ export function hexToRgba(hex: string, alpha: number): string {
77
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
78
+ if (!result || !result[1] || !result[2] || !result[3]) {
79
+ return `rgba(118, 41, 200, ${alpha})`;
80
+ }
81
+
82
+ const r = parseInt(result[1], 16);
83
+ const g = parseInt(result[2], 16);
84
+ const b = parseInt(result[3], 16);
85
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
86
+ }
@@ -0,0 +1,375 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { createWidget } from "./widget";
3
+ import * as config from "./config";
4
+ import * as iframe from "./iframe";
5
+
6
+ describe("createWidget", () => {
7
+ let container: HTMLDivElement;
8
+
9
+ beforeEach(() => {
10
+ container = document.createElement("div");
11
+ document.body.appendChild(container);
12
+ });
13
+
14
+ afterEach(() => {
15
+ container.remove();
16
+ vi.restoreAllMocks();
17
+ });
18
+
19
+ it("creates widget in container", () => {
20
+ const handle = createWidget(container, {
21
+ researchId: "test-research-id",
22
+ });
23
+
24
+ expect(handle.researchId).toBe("test-research-id");
25
+ expect(handle.type).toBe("widget");
26
+ expect(handle.container).toBe(container);
27
+ expect(handle.iframe).toBeInstanceOf(HTMLIFrameElement);
28
+ expect(container.querySelector("iframe[data-perspective]")).toBeTruthy();
29
+
30
+ handle.unmount();
31
+ });
32
+
33
+ it("creates loading indicator", () => {
34
+ const handle = createWidget(container, {
35
+ researchId: "test-research-id",
36
+ });
37
+
38
+ const loading = container.querySelector(".perspective-loading");
39
+ expect(loading).toBeTruthy();
40
+
41
+ handle.unmount();
42
+ });
43
+
44
+ it("returns no-op handle when no DOM", () => {
45
+ vi.spyOn(config, "hasDom").mockReturnValue(false);
46
+
47
+ const handle = createWidget(container, {
48
+ researchId: "test-research-id",
49
+ });
50
+
51
+ expect(handle.iframe).toBeNull();
52
+ expect(handle.container).toBeNull();
53
+ expect(handle.unmount).toBeInstanceOf(Function);
54
+ expect(() => handle.unmount()).not.toThrow();
55
+ });
56
+
57
+ it("returns no-op handle when container is null", () => {
58
+ const handle = createWidget(null, {
59
+ researchId: "test-research-id",
60
+ });
61
+
62
+ expect(handle.iframe).toBeNull();
63
+ expect(handle.container).toBeNull();
64
+ });
65
+
66
+ it("is idempotent (React Strict Mode safe)", () => {
67
+ const handle1 = createWidget(container, {
68
+ researchId: "test-research-id",
69
+ });
70
+
71
+ const handle2 = createWidget(container, {
72
+ researchId: "test-research-id",
73
+ });
74
+
75
+ expect(handle1.iframe).toBeInstanceOf(HTMLIFrameElement);
76
+ expect(handle2.iframe).toBeInstanceOf(HTMLIFrameElement);
77
+ expect(handle2.iframe).toBe(handle1.iframe);
78
+
79
+ const iframes = container.querySelectorAll("iframe[data-perspective]");
80
+ expect(iframes.length).toBe(1);
81
+
82
+ handle1.unmount();
83
+ });
84
+
85
+ it("idempotent handle can clean up existing widget", () => {
86
+ createWidget(container, {
87
+ researchId: "test-research-id",
88
+ });
89
+
90
+ expect(container.querySelector("iframe[data-perspective]")).toBeTruthy();
91
+
92
+ const handle2 = createWidget(container, {
93
+ researchId: "test-research-id",
94
+ });
95
+
96
+ handle2.unmount();
97
+
98
+ expect(container.querySelector("iframe[data-perspective]")).toBeFalsy();
99
+ expect(container.querySelector(".perspective-embed-root")).toBeFalsy();
100
+ });
101
+
102
+ it("idempotent handle cleans up message listeners and iframe registry", () => {
103
+ const cleanupSpy = vi.fn();
104
+ const unregisterSpy = vi.fn();
105
+
106
+ vi.spyOn(iframe, "setupMessageListener").mockReturnValue(cleanupSpy);
107
+ vi.spyOn(iframe, "registerIframe").mockReturnValue(unregisterSpy);
108
+
109
+ createWidget(container, {
110
+ researchId: "test-research-id",
111
+ });
112
+
113
+ const handle2 = createWidget(container, {
114
+ researchId: "test-research-id",
115
+ });
116
+
117
+ expect(cleanupSpy).not.toHaveBeenCalled();
118
+ expect(unregisterSpy).not.toHaveBeenCalled();
119
+
120
+ handle2.unmount();
121
+
122
+ expect(cleanupSpy).toHaveBeenCalledTimes(1);
123
+ expect(unregisterSpy).toHaveBeenCalledTimes(1);
124
+ });
125
+
126
+ it("unmount removes elements", () => {
127
+ const handle = createWidget(container, {
128
+ researchId: "test-research-id",
129
+ });
130
+
131
+ expect(container.querySelector("iframe[data-perspective]")).toBeTruthy();
132
+
133
+ handle.unmount();
134
+
135
+ expect(container.querySelector("iframe[data-perspective]")).toBeFalsy();
136
+ expect(container.querySelector(".perspective-embed-root")).toBeFalsy();
137
+ });
138
+
139
+ it("destroy is alias for unmount", () => {
140
+ const handle = createWidget(container, {
141
+ researchId: "test-research-id",
142
+ });
143
+
144
+ expect(container.querySelector("iframe[data-perspective]")).toBeTruthy();
145
+
146
+ handle.destroy();
147
+
148
+ expect(container.querySelector("iframe[data-perspective]")).toBeFalsy();
149
+ });
150
+
151
+ it("update modifies config", () => {
152
+ const onSubmit1 = vi.fn();
153
+ const onSubmit2 = vi.fn();
154
+
155
+ const handle = createWidget(container, {
156
+ researchId: "test-research-id",
157
+ onSubmit: onSubmit1,
158
+ });
159
+
160
+ handle.update({ onSubmit: onSubmit2 });
161
+
162
+ // The update should not throw
163
+ expect(() => handle.update({ onSubmit: onSubmit2 })).not.toThrow();
164
+
165
+ handle.unmount();
166
+ });
167
+
168
+ it("applies theme class", () => {
169
+ const handle = createWidget(container, {
170
+ researchId: "test-research-id",
171
+ theme: "dark",
172
+ });
173
+
174
+ const wrapper = container.querySelector(".perspective-embed-root");
175
+ expect(wrapper?.classList.contains("perspective-dark-theme")).toBe(true);
176
+
177
+ handle.unmount();
178
+ });
179
+
180
+ it("uses custom host", () => {
181
+ const handle = createWidget(container, {
182
+ researchId: "test-research-id",
183
+ host: "https://custom.example.com",
184
+ });
185
+
186
+ const iframe = handle.iframe as HTMLIFrameElement;
187
+ expect(iframe.src).toContain("https://custom.example.com");
188
+
189
+ handle.unmount();
190
+ });
191
+
192
+ it("passes params to iframe", () => {
193
+ const handle = createWidget(container, {
194
+ researchId: "test-research-id",
195
+ params: { source: "test", campaign: "demo" },
196
+ });
197
+
198
+ const iframe = handle.iframe as HTMLIFrameElement;
199
+ const url = new URL(iframe.src);
200
+ expect(url.searchParams.get("source")).toBe("test");
201
+ expect(url.searchParams.get("campaign")).toBe("demo");
202
+
203
+ handle.unmount();
204
+ });
205
+
206
+ it("unmount is idempotent - calling twice does not throw", () => {
207
+ const cleanupSpy = vi.fn();
208
+ const unregisterSpy = vi.fn();
209
+
210
+ vi.spyOn(iframe, "setupMessageListener").mockReturnValue(cleanupSpy);
211
+ vi.spyOn(iframe, "registerIframe").mockReturnValue(unregisterSpy);
212
+
213
+ const handle = createWidget(container, {
214
+ researchId: "test-research-id",
215
+ });
216
+
217
+ expect(() => {
218
+ handle.unmount();
219
+ handle.unmount();
220
+ }).not.toThrow();
221
+
222
+ // Cleanup functions should only be called once
223
+ expect(cleanupSpy).toHaveBeenCalledTimes(1);
224
+ expect(unregisterSpy).toHaveBeenCalledTimes(1);
225
+ });
226
+
227
+ it("destroy is idempotent - calling twice does not throw", () => {
228
+ const cleanupSpy = vi.fn();
229
+ const unregisterSpy = vi.fn();
230
+
231
+ vi.spyOn(iframe, "setupMessageListener").mockReturnValue(cleanupSpy);
232
+ vi.spyOn(iframe, "registerIframe").mockReturnValue(unregisterSpy);
233
+
234
+ const handle = createWidget(container, {
235
+ researchId: "test-research-id",
236
+ });
237
+
238
+ expect(() => {
239
+ handle.destroy();
240
+ handle.destroy();
241
+ }).not.toThrow();
242
+
243
+ // Cleanup functions should only be called once
244
+ expect(cleanupSpy).toHaveBeenCalledTimes(1);
245
+ expect(unregisterSpy).toHaveBeenCalledTimes(1);
246
+ });
247
+
248
+ it("mixed unmount then destroy does not double-cleanup", () => {
249
+ const cleanupSpy = vi.fn();
250
+ const unregisterSpy = vi.fn();
251
+
252
+ vi.spyOn(iframe, "setupMessageListener").mockReturnValue(cleanupSpy);
253
+ vi.spyOn(iframe, "registerIframe").mockReturnValue(unregisterSpy);
254
+
255
+ const handle = createWidget(container, {
256
+ researchId: "test-research-id",
257
+ });
258
+
259
+ expect(() => {
260
+ handle.unmount();
261
+ handle.destroy();
262
+ }).not.toThrow();
263
+
264
+ // Cleanup functions should only be called once
265
+ expect(cleanupSpy).toHaveBeenCalledTimes(1);
266
+ expect(unregisterSpy).toHaveBeenCalledTimes(1);
267
+ });
268
+
269
+ it("idempotent handle unmount is also idempotent", () => {
270
+ const cleanupSpy = vi.fn();
271
+ const unregisterSpy = vi.fn();
272
+
273
+ vi.spyOn(iframe, "setupMessageListener").mockReturnValue(cleanupSpy);
274
+ vi.spyOn(iframe, "registerIframe").mockReturnValue(unregisterSpy);
275
+
276
+ // Create first handle
277
+ createWidget(container, {
278
+ researchId: "test-research-id",
279
+ });
280
+
281
+ // Get idempotent handle
282
+ const handle2 = createWidget(container, {
283
+ researchId: "test-research-id",
284
+ });
285
+
286
+ expect(() => {
287
+ handle2.unmount();
288
+ handle2.unmount();
289
+ }).not.toThrow();
290
+
291
+ // Cleanup functions should only be called once
292
+ expect(cleanupSpy).toHaveBeenCalledTimes(1);
293
+ expect(unregisterSpy).toHaveBeenCalledTimes(1);
294
+ });
295
+
296
+ describe("update() behavior", () => {
297
+ const host = "https://getperspective.ai";
298
+ const researchId = "test-research-id";
299
+
300
+ const sendMessage = (
301
+ iframeEl: HTMLIFrameElement,
302
+ type: string,
303
+ extra?: Record<string, unknown>
304
+ ) => {
305
+ window.dispatchEvent(
306
+ new MessageEvent("message", {
307
+ data: { type, researchId, ...extra },
308
+ origin: host,
309
+ source: iframeEl.contentWindow,
310
+ })
311
+ );
312
+ };
313
+
314
+ it("update changes which callback is invoked", () => {
315
+ const onSubmit1 = vi.fn();
316
+ const onSubmit2 = vi.fn();
317
+
318
+ const handle = createWidget(container, {
319
+ researchId,
320
+ host,
321
+ onSubmit: onSubmit1,
322
+ });
323
+
324
+ handle.update({ onSubmit: onSubmit2 });
325
+
326
+ sendMessage(handle.iframe!, "perspective:submit");
327
+
328
+ expect(onSubmit2).toHaveBeenCalledTimes(1);
329
+ expect(onSubmit1).not.toHaveBeenCalled();
330
+
331
+ handle.unmount();
332
+ });
333
+
334
+ it("sequential updates only use latest callback", () => {
335
+ const fn1 = vi.fn();
336
+ const fn2 = vi.fn();
337
+ const fn3 = vi.fn();
338
+
339
+ const handle = createWidget(container, {
340
+ researchId,
341
+ host,
342
+ onSubmit: fn1,
343
+ });
344
+
345
+ handle.update({ onSubmit: fn2 });
346
+ handle.update({ onSubmit: fn3 });
347
+
348
+ sendMessage(handle.iframe!, "perspective:submit");
349
+
350
+ expect(fn3).toHaveBeenCalledTimes(1);
351
+ expect(fn2).not.toHaveBeenCalled();
352
+ expect(fn1).not.toHaveBeenCalled();
353
+
354
+ handle.unmount();
355
+ });
356
+
357
+ it("destroy prevents further callback invocations", () => {
358
+ const onSubmit = vi.fn();
359
+
360
+ const handle = createWidget(container, {
361
+ researchId,
362
+ host,
363
+ onSubmit,
364
+ });
365
+
366
+ const iframeEl = handle.iframe!;
367
+
368
+ handle.unmount();
369
+
370
+ sendMessage(iframeEl, "perspective:submit");
371
+
372
+ expect(onSubmit).not.toHaveBeenCalled();
373
+ });
374
+ });
375
+ });
package/src/widget.ts ADDED
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Inline widget embed - renders directly in a container element
3
+ * SSR-safe - returns no-op handle on server
4
+ */
5
+
6
+ import type { EmbedConfig, EmbedHandle } from "./types";
7
+ import { hasDom, getHost } from "./config";
8
+ import {
9
+ createIframe,
10
+ setupMessageListener,
11
+ registerIframe,
12
+ ensureGlobalListeners,
13
+ } from "./iframe";
14
+ import { createLoadingIndicator } from "./loading";
15
+ import { injectStyles } from "./styles";
16
+ import { cn, getThemeClass } from "./utils";
17
+
18
+ type WidgetResources = {
19
+ cleanup: () => void;
20
+ unregister: () => void;
21
+ wrapper: HTMLElement;
22
+ };
23
+
24
+ const widgetResources = new WeakMap<HTMLIFrameElement, WidgetResources>();
25
+
26
+ function createNoOpHandle(researchId: string, type: "widget"): EmbedHandle {
27
+ return {
28
+ unmount: () => {},
29
+ update: () => {},
30
+ destroy: () => {},
31
+ researchId,
32
+ type,
33
+ iframe: null,
34
+ container: null,
35
+ };
36
+ }
37
+
38
+ function createExistingWidgetHandle(
39
+ container: HTMLElement,
40
+ researchId: string
41
+ ): EmbedHandle {
42
+ const existingWrapper = container.querySelector<HTMLElement>(
43
+ ".perspective-embed-root"
44
+ );
45
+ const existingIframe = container.querySelector<HTMLIFrameElement>(
46
+ "iframe[data-perspective]"
47
+ );
48
+
49
+ let destroyed = false;
50
+
51
+ const unmount = () => {
52
+ if (destroyed) return;
53
+ destroyed = true;
54
+
55
+ if (existingIframe) {
56
+ const resources = widgetResources.get(existingIframe);
57
+ if (resources) {
58
+ resources.cleanup();
59
+ resources.unregister();
60
+ widgetResources.delete(existingIframe);
61
+ }
62
+ }
63
+ existingWrapper?.remove();
64
+ };
65
+
66
+ return {
67
+ unmount,
68
+ update: () => {},
69
+ destroy: unmount,
70
+ researchId,
71
+ type: "widget" as const,
72
+ iframe: existingIframe,
73
+ container,
74
+ };
75
+ }
76
+
77
+ export function createWidget(
78
+ container: HTMLElement | null,
79
+ config: EmbedConfig
80
+ ): EmbedHandle {
81
+ const { researchId } = config;
82
+
83
+ // SSR safety: return no-op handle
84
+ if (!hasDom() || !container) {
85
+ return createNoOpHandle(researchId, "widget");
86
+ }
87
+
88
+ // Idempotency check for React Strict Mode
89
+ if (container.querySelector("iframe[data-perspective]")) {
90
+ return createExistingWidgetHandle(container, researchId);
91
+ }
92
+
93
+ const host = getHost(config.host);
94
+
95
+ injectStyles();
96
+ ensureGlobalListeners();
97
+
98
+ // Create wrapper for positioning
99
+ const wrapper = document.createElement("div");
100
+ wrapper.className = cn("perspective-embed-root", getThemeClass(config.theme));
101
+ wrapper.style.cssText =
102
+ "position:relative;width:100%;height:100%;min-height:500px;";
103
+
104
+ // Create loading indicator with theme and brand colors
105
+ const loading = createLoadingIndicator({
106
+ theme: config.theme,
107
+ brand: config.brand,
108
+ });
109
+ wrapper.appendChild(loading);
110
+
111
+ // Create iframe (hidden initially)
112
+ const iframe = createIframe(
113
+ researchId,
114
+ "widget",
115
+ host,
116
+ config.params,
117
+ config.brand,
118
+ config.theme
119
+ );
120
+ iframe.style.width = "100%";
121
+ iframe.style.height = "100%";
122
+ iframe.style.minHeight = "500px";
123
+ iframe.style.opacity = "0";
124
+ iframe.style.transition = "opacity 0.3s ease";
125
+
126
+ wrapper.appendChild(iframe);
127
+ container.appendChild(wrapper);
128
+
129
+ // Mutable config reference for updates
130
+ let currentConfig = { ...config };
131
+
132
+ // Set up message listener with loading state handling
133
+ const cleanup = setupMessageListener(
134
+ researchId,
135
+ {
136
+ get onReady() {
137
+ return () => {
138
+ // Hide loading, show iframe
139
+ loading.style.opacity = "0";
140
+ iframe.style.opacity = "1";
141
+ setTimeout(() => loading.remove(), 300);
142
+ currentConfig.onReady?.();
143
+ };
144
+ },
145
+ get onSubmit() {
146
+ return currentConfig.onSubmit;
147
+ },
148
+ get onNavigate() {
149
+ return currentConfig.onNavigate;
150
+ },
151
+ get onClose() {
152
+ return currentConfig.onClose;
153
+ },
154
+ get onError() {
155
+ return currentConfig.onError;
156
+ },
157
+ },
158
+ iframe,
159
+ host,
160
+ { skipResize: true }
161
+ );
162
+
163
+ // Register iframe for theme change notifications
164
+ const unregisterIframe = registerIframe(iframe, host);
165
+
166
+ widgetResources.set(iframe, {
167
+ cleanup,
168
+ unregister: unregisterIframe,
169
+ wrapper,
170
+ });
171
+
172
+ let destroyed = false;
173
+
174
+ const unmount = () => {
175
+ if (destroyed) return;
176
+ destroyed = true;
177
+
178
+ cleanup();
179
+ unregisterIframe();
180
+ widgetResources.delete(iframe);
181
+ wrapper.remove();
182
+ };
183
+
184
+ return {
185
+ unmount,
186
+ update: (options) => {
187
+ currentConfig = { ...currentConfig, ...options };
188
+ },
189
+ destroy: unmount,
190
+ researchId,
191
+ type: "widget" as const,
192
+ iframe,
193
+ container,
194
+ };
195
+ }