@perspective-ai/sdk 1.0.1 → 1.1.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.
package/src/constants.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  // ============================================================================
10
10
 
11
11
  /** SDK version for handshake protocol */
12
- export const SDK_VERSION = "1.0.0";
12
+ export const SDK_VERSION = "1.1.2";
13
13
 
14
14
  /** Feature flags as bitset for version negotiation */
15
15
  export const FEATURES = {
@@ -60,36 +60,12 @@ export const BRAND_KEYS = {
60
60
  export type BrandKey = (typeof BRAND_KEYS)[keyof typeof BRAND_KEYS];
61
61
 
62
62
  // ============================================================================
63
- // UTM Parameters (auto-forwarded from parent URL)
64
- // ============================================================================
65
-
66
- export const UTM_PARAMS = [
67
- "utm_source",
68
- "utm_medium",
69
- "utm_campaign",
70
- "utm_term",
71
- "utm_content",
72
- ] as const;
73
-
74
- export type UtmParam = (typeof UTM_PARAMS)[number];
75
-
76
- // ============================================================================
77
- // Reserved Parameters (cannot be overridden via custom params)
63
+ // Reserved Parameters (cannot be overridden via custom params or parent URL)
78
64
  // ============================================================================
79
65
 
80
66
  export const RESERVED_PARAMS: Set<string> = new Set([
81
- PARAM_KEYS.embed,
82
- PARAM_KEYS.embedType,
83
- PARAM_KEYS.theme,
84
- BRAND_KEYS.primary,
85
- BRAND_KEYS.secondary,
86
- BRAND_KEYS.bg,
87
- BRAND_KEYS.text,
88
- BRAND_KEYS.darkPrimary,
89
- BRAND_KEYS.darkSecondary,
90
- BRAND_KEYS.darkBg,
91
- BRAND_KEYS.darkText,
92
- ...UTM_PARAMS,
67
+ ...Object.values(PARAM_KEYS),
68
+ ...Object.values(BRAND_KEYS),
93
69
  ]);
94
70
 
95
71
  // ============================================================================
@@ -108,6 +84,8 @@ export const DATA_ATTRS = {
108
84
  brandDark: "data-perspective-brand-dark",
109
85
  theme: "data-perspective-theme",
110
86
  noStyle: "data-perspective-no-style",
87
+ autoOpen: "data-perspective-auto-open",
88
+ showOnce: "data-perspective-show-once",
111
89
  } as const;
112
90
 
113
91
  export type DataAttr = (typeof DATA_ATTRS)[keyof typeof DATA_ATTRS];
@@ -191,4 +169,5 @@ export type ModeValue = (typeof MODE_VALUES)[keyof typeof MODE_VALUES];
191
169
 
192
170
  export const STORAGE_KEYS = {
193
171
  anonId: "perspective-anon-id",
172
+ triggerShown: "perspective-trigger-shown",
194
173
  } as const;
@@ -62,6 +62,121 @@ describe("createIframe", () => {
62
62
  expect(src.searchParams.get(PARAM_KEYS.theme)).toBe("dark");
63
63
  });
64
64
 
65
+ it("forwards parent page search params to iframe", () => {
66
+ // Simulate parent page URL with search params
67
+ const originalLocation = window.location;
68
+ Object.defineProperty(window, "location", {
69
+ value: { ...originalLocation, search: "?ref=pricing-enterprise&foo=bar" },
70
+ writable: true,
71
+ configurable: true,
72
+ });
73
+
74
+ const iframe = createIframe(
75
+ "test-research-id",
76
+ "widget",
77
+ "https://getperspective.ai"
78
+ );
79
+
80
+ const src = new URL(iframe.src);
81
+ expect(src.searchParams.get("ref")).toBe("pricing-enterprise");
82
+ expect(src.searchParams.get("foo")).toBe("bar");
83
+
84
+ Object.defineProperty(window, "location", {
85
+ value: originalLocation,
86
+ writable: true,
87
+ configurable: true,
88
+ });
89
+ });
90
+
91
+ it("does not forward reserved params from parent URL", () => {
92
+ const originalLocation = window.location;
93
+ Object.defineProperty(window, "location", {
94
+ value: {
95
+ ...originalLocation,
96
+ search: "?embed=false&theme=dark&ref=test",
97
+ },
98
+ writable: true,
99
+ configurable: true,
100
+ });
101
+
102
+ const iframe = createIframe(
103
+ "test-research-id",
104
+ "widget",
105
+ "https://getperspective.ai"
106
+ );
107
+
108
+ const src = new URL(iframe.src);
109
+ // Reserved params should be set by SDK, not from parent URL
110
+ expect(src.searchParams.get(PARAM_KEYS.embed)).toBe("true");
111
+ // Non-reserved params should be forwarded
112
+ expect(src.searchParams.get("ref")).toBe("test");
113
+
114
+ Object.defineProperty(window, "location", {
115
+ value: originalLocation,
116
+ writable: true,
117
+ configurable: true,
118
+ });
119
+ });
120
+
121
+ it("custom params override parent URL params", () => {
122
+ const originalLocation = window.location;
123
+ Object.defineProperty(window, "location", {
124
+ value: { ...originalLocation, search: "?ref=from-url&source=parent" },
125
+ writable: true,
126
+ configurable: true,
127
+ });
128
+
129
+ const iframe = createIframe(
130
+ "test-research-id",
131
+ "widget",
132
+ "https://getperspective.ai",
133
+ { ref: "from-custom", extra: "value" }
134
+ );
135
+
136
+ const src = new URL(iframe.src);
137
+ // Custom params should override parent URL params
138
+ expect(src.searchParams.get("ref")).toBe("from-custom");
139
+ // Parent-only params should still be forwarded
140
+ expect(src.searchParams.get("source")).toBe("parent");
141
+ // Custom-only params should be present
142
+ expect(src.searchParams.get("extra")).toBe("value");
143
+
144
+ Object.defineProperty(window, "location", {
145
+ value: originalLocation,
146
+ writable: true,
147
+ configurable: true,
148
+ });
149
+ });
150
+
151
+ it("forwards UTM params from parent URL", () => {
152
+ const originalLocation = window.location;
153
+ Object.defineProperty(window, "location", {
154
+ value: {
155
+ ...originalLocation,
156
+ search: "?utm_source=google&utm_campaign=summer&ref=pricing",
157
+ },
158
+ writable: true,
159
+ configurable: true,
160
+ });
161
+
162
+ const iframe = createIframe(
163
+ "test-research-id",
164
+ "widget",
165
+ "https://getperspective.ai"
166
+ );
167
+
168
+ const src = new URL(iframe.src);
169
+ expect(src.searchParams.get("utm_source")).toBe("google");
170
+ expect(src.searchParams.get("utm_campaign")).toBe("summer");
171
+ expect(src.searchParams.get("ref")).toBe("pricing");
172
+
173
+ Object.defineProperty(window, "location", {
174
+ value: originalLocation,
175
+ writable: true,
176
+ configurable: true,
177
+ });
178
+ });
179
+
65
180
  it("includes brand colors", () => {
66
181
  const iframe = createIframe(
67
182
  "test-research-id",
package/src/iframe.ts CHANGED
@@ -11,7 +11,6 @@ import type {
11
11
  } from "./types";
12
12
  import { hasDom } from "./config";
13
13
  import {
14
- UTM_PARAMS,
15
14
  RESERVED_PARAMS,
16
15
  PARAM_KEYS,
17
16
  BRAND_KEYS,
@@ -63,16 +62,15 @@ function getOrCreateAnonId(): string {
63
62
  }
64
63
  }
65
64
 
66
- /** Collect UTM params from current page URL */
67
- function getUtmParams(): Record<string, string> {
65
+ /** Collect all search params from the parent page URL (excluding reserved SDK params) */
66
+ function getParentSearchParams(): Record<string, string> {
68
67
  if (!hasDom()) return {};
69
68
 
70
69
  const params: Record<string, string> = {};
71
70
  const searchParams = new URLSearchParams(window.location.search);
72
71
 
73
- for (const key of UTM_PARAMS) {
74
- const value = searchParams.get(key);
75
- if (value) {
72
+ for (const [key, value] of searchParams.entries()) {
73
+ if (!RESERVED_PARAMS.has(key)) {
76
74
  params[key] = value;
77
75
  }
78
76
  }
@@ -111,9 +109,10 @@ function buildIframeUrl(
111
109
  url.searchParams.set(PARAM_KEYS.theme, themeOverride || THEME_VALUES.light);
112
110
  }
113
111
 
114
- // Auto-forward UTM params from parent
115
- const utmParams = getUtmParams();
116
- for (const [key, value] of Object.entries(utmParams)) {
112
+ // Auto-forward all parent page search params (e.g. ?ref=pricing-enterprise)
113
+ // These are added first so that explicit customParams can override them
114
+ const parentParams = getParentSearchParams();
115
+ for (const [key, value] of Object.entries(parentParams)) {
117
116
  url.searchParams.set(key, value);
118
117
  }
119
118
 
package/src/index.ts CHANGED
@@ -29,6 +29,15 @@ export { openSlider } from "./slider";
29
29
  export { createFloatBubble, createChatBubble } from "./float";
30
30
  export { createFullpage } from "./fullpage";
31
31
 
32
+ // Auto-open triggers
33
+ export {
34
+ setupTrigger,
35
+ parseTriggerAttr,
36
+ parseShowOnceAttr,
37
+ shouldShow,
38
+ markShown,
39
+ } from "./triggers";
40
+
32
41
  // Configuration
33
42
  export { configure, getConfig } from "./config";
34
43
 
@@ -44,6 +53,10 @@ export type {
44
53
  EmbedType,
45
54
  ThemeConfig,
46
55
  SDKConfig,
56
+ TriggerType,
57
+ TriggerConfig,
58
+ ShowOnce,
59
+ AutoOpenConfig,
47
60
  } from "./types";
48
61
 
49
62
  // Re-export commonly used constants and types
@@ -0,0 +1,272 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import {
3
+ parseTriggerAttr,
4
+ parseShowOnceAttr,
5
+ setupTrigger,
6
+ shouldShow,
7
+ markShown,
8
+ } from "./triggers";
9
+
10
+ describe("triggers", () => {
11
+ describe("parseTriggerAttr", () => {
12
+ it("parses timeout with delay", () => {
13
+ expect(parseTriggerAttr("timeout:5000")).toEqual({
14
+ type: "timeout",
15
+ delay: 5000,
16
+ });
17
+ });
18
+
19
+ it("parses timeout with zero delay", () => {
20
+ expect(parseTriggerAttr("timeout:0")).toEqual({
21
+ type: "timeout",
22
+ delay: 0,
23
+ });
24
+ });
25
+
26
+ it("parses bare timeout as 5000ms default", () => {
27
+ expect(parseTriggerAttr("timeout")).toEqual({
28
+ type: "timeout",
29
+ delay: 5000,
30
+ });
31
+ });
32
+
33
+ it("parses exit-intent", () => {
34
+ expect(parseTriggerAttr("exit-intent")).toEqual({
35
+ type: "exit-intent",
36
+ });
37
+ });
38
+
39
+ it("trims whitespace", () => {
40
+ expect(parseTriggerAttr(" timeout:3000 ")).toEqual({
41
+ type: "timeout",
42
+ delay: 3000,
43
+ });
44
+ });
45
+
46
+ it("throws on invalid timeout delay", () => {
47
+ expect(() => parseTriggerAttr("timeout:abc")).toThrow(
48
+ "Invalid timeout delay"
49
+ );
50
+ });
51
+
52
+ it("throws on negative timeout delay", () => {
53
+ expect(() => parseTriggerAttr("timeout:-1")).toThrow(
54
+ "Invalid timeout delay"
55
+ );
56
+ });
57
+
58
+ it("throws on unknown trigger type", () => {
59
+ expect(() => parseTriggerAttr("scroll:50")).toThrow(
60
+ "Unknown trigger type"
61
+ );
62
+ });
63
+ });
64
+
65
+ describe("setupTrigger", () => {
66
+ beforeEach(() => {
67
+ vi.useFakeTimers();
68
+ });
69
+
70
+ afterEach(() => {
71
+ vi.useRealTimers();
72
+ });
73
+
74
+ it("calls callback after timeout delay", () => {
75
+ const callback = vi.fn();
76
+ setupTrigger({ type: "timeout", delay: 3000 }, callback);
77
+
78
+ expect(callback).not.toHaveBeenCalled();
79
+ vi.advanceTimersByTime(2999);
80
+ expect(callback).not.toHaveBeenCalled();
81
+ vi.advanceTimersByTime(1);
82
+ expect(callback).toHaveBeenCalledTimes(1);
83
+ });
84
+
85
+ it("returns cleanup that clears timeout", () => {
86
+ const callback = vi.fn();
87
+ const cleanup = setupTrigger({ type: "timeout", delay: 3000 }, callback);
88
+
89
+ cleanup();
90
+ vi.advanceTimersByTime(5000);
91
+ expect(callback).not.toHaveBeenCalled();
92
+ });
93
+
94
+ it("sets up exit-intent on mouseleave", () => {
95
+ const callback = vi.fn();
96
+ const addSpy = vi.spyOn(document, "addEventListener");
97
+
98
+ setupTrigger({ type: "exit-intent" }, callback);
99
+
100
+ expect(addSpy).toHaveBeenCalledWith("mouseleave", expect.any(Function));
101
+ addSpy.mockRestore();
102
+ });
103
+
104
+ it("fires exit-intent callback when clientY <= 0", () => {
105
+ const callback = vi.fn();
106
+ setupTrigger({ type: "exit-intent" }, callback);
107
+
108
+ // Simulate mouse leaving the document upward
109
+ const event = new MouseEvent("mouseleave", { clientY: -1 });
110
+ document.dispatchEvent(event);
111
+
112
+ expect(callback).toHaveBeenCalledTimes(1);
113
+ });
114
+
115
+ it("does not fire exit-intent when clientY > 0", () => {
116
+ const callback = vi.fn();
117
+ setupTrigger({ type: "exit-intent" }, callback);
118
+
119
+ const event = new MouseEvent("mouseleave", { clientY: 100 });
120
+ document.dispatchEvent(event);
121
+
122
+ expect(callback).not.toHaveBeenCalled();
123
+ });
124
+
125
+ it("removes exit-intent listener after firing", () => {
126
+ const callback = vi.fn();
127
+ setupTrigger({ type: "exit-intent" }, callback);
128
+
129
+ const event1 = new MouseEvent("mouseleave", { clientY: -1 });
130
+ document.dispatchEvent(event1);
131
+ expect(callback).toHaveBeenCalledTimes(1);
132
+
133
+ // Second event should not fire
134
+ const event2 = new MouseEvent("mouseleave", { clientY: -1 });
135
+ document.dispatchEvent(event2);
136
+ expect(callback).toHaveBeenCalledTimes(1);
137
+ });
138
+
139
+ it("returns cleanup that removes exit-intent listener", () => {
140
+ const callback = vi.fn();
141
+ const removeSpy = vi.spyOn(document, "removeEventListener");
142
+
143
+ const cleanup = setupTrigger({ type: "exit-intent" }, callback);
144
+ cleanup();
145
+
146
+ expect(removeSpy).toHaveBeenCalledWith(
147
+ "mouseleave",
148
+ expect.any(Function)
149
+ );
150
+
151
+ // Event should not fire after cleanup
152
+ const event = new MouseEvent("mouseleave", { clientY: -1 });
153
+ document.dispatchEvent(event);
154
+ expect(callback).not.toHaveBeenCalled();
155
+
156
+ removeSpy.mockRestore();
157
+ });
158
+ });
159
+
160
+ describe("parseShowOnceAttr", () => {
161
+ it('returns "session" for null', () => {
162
+ expect(parseShowOnceAttr(null)).toBe("session");
163
+ });
164
+
165
+ it('returns "session" for empty string', () => {
166
+ expect(parseShowOnceAttr("")).toBe("session");
167
+ });
168
+
169
+ it('returns "session" for "session"', () => {
170
+ expect(parseShowOnceAttr("session")).toBe("session");
171
+ });
172
+
173
+ it('returns "visitor" for "visitor"', () => {
174
+ expect(parseShowOnceAttr("visitor")).toBe("visitor");
175
+ });
176
+
177
+ it('returns false for "false"', () => {
178
+ expect(parseShowOnceAttr("false")).toBe(false);
179
+ });
180
+
181
+ it('defaults unknown values to "session"', () => {
182
+ expect(parseShowOnceAttr("always")).toBe("session");
183
+ expect(parseShowOnceAttr("never")).toBe("session");
184
+ });
185
+
186
+ it("trims whitespace", () => {
187
+ expect(parseShowOnceAttr(" visitor ")).toBe("visitor");
188
+ });
189
+ });
190
+
191
+ describe("shouldShow / markShown", () => {
192
+ beforeEach(() => {
193
+ sessionStorage.clear();
194
+ localStorage.clear();
195
+ });
196
+
197
+ it("returns true when no prior shown (session)", () => {
198
+ expect(shouldShow("test-id", "session")).toBe(true);
199
+ });
200
+
201
+ it("returns true when no prior shown (visitor)", () => {
202
+ expect(shouldShow("test-id", "visitor")).toBe(true);
203
+ });
204
+
205
+ it("returns true when showOnce is false", () => {
206
+ expect(shouldShow("test-id", false)).toBe(true);
207
+ });
208
+
209
+ it("returns false after markShown (session)", () => {
210
+ markShown("test-id", "session");
211
+ expect(shouldShow("test-id", "session")).toBe(false);
212
+ });
213
+
214
+ it("returns false after markShown (visitor)", () => {
215
+ markShown("test-id", "visitor");
216
+ expect(shouldShow("test-id", "visitor")).toBe(false);
217
+ });
218
+
219
+ it("always returns true when showOnce is false, even after markShown", () => {
220
+ markShown("test-id", false);
221
+ expect(shouldShow("test-id", false)).toBe(true);
222
+ });
223
+
224
+ it("uses sessionStorage for session mode", () => {
225
+ markShown("test-id", "session");
226
+ expect(sessionStorage.getItem("perspective-trigger-shown:test-id")).toBe(
227
+ "1"
228
+ );
229
+ expect(
230
+ localStorage.getItem("perspective-trigger-shown:test-id")
231
+ ).toBeNull();
232
+ });
233
+
234
+ it("uses localStorage for visitor mode", () => {
235
+ markShown("test-id", "visitor");
236
+ expect(localStorage.getItem("perspective-trigger-shown:test-id")).toBe(
237
+ "1"
238
+ );
239
+ expect(
240
+ sessionStorage.getItem("perspective-trigger-shown:test-id")
241
+ ).toBeNull();
242
+ });
243
+
244
+ it("deduplicates per researchId", () => {
245
+ markShown("id-1", "session");
246
+ expect(shouldShow("id-1", "session")).toBe(false);
247
+ expect(shouldShow("id-2", "session")).toBe(true);
248
+ });
249
+
250
+ it("shouldShow returns true when storage throws", () => {
251
+ const spy = vi
252
+ .spyOn(Storage.prototype, "getItem")
253
+ .mockImplementation(() => {
254
+ throw new Error("Storage disabled");
255
+ });
256
+ expect(shouldShow("test-id", "session")).toBe(true);
257
+ expect(shouldShow("test-id", "visitor")).toBe(true);
258
+ spy.mockRestore();
259
+ });
260
+
261
+ it("markShown does not throw when storage throws", () => {
262
+ const spy = vi
263
+ .spyOn(Storage.prototype, "setItem")
264
+ .mockImplementation(() => {
265
+ throw new Error("Storage disabled");
266
+ });
267
+ expect(() => markShown("test-id", "session")).not.toThrow();
268
+ expect(() => markShown("test-id", "visitor")).not.toThrow();
269
+ spy.mockRestore();
270
+ });
271
+ });
272
+ });
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Auto-open trigger system for popup embeds.
3
+ *
4
+ * Supports:
5
+ * - Timeout: Open after a delay (ms)
6
+ * - Exit intent: Open when user moves cursor above viewport
7
+ *
8
+ * Show-once dedup:
9
+ * - "session" -> sessionStorage
10
+ * - "visitor" -> localStorage
11
+ * - false -> always show
12
+ */
13
+
14
+ import { STORAGE_KEYS } from "./constants";
15
+ import type { TriggerConfig, ShowOnce } from "./types";
16
+
17
+ /**
18
+ * Parse a trigger attribute value into a TriggerConfig.
19
+ *
20
+ * Formats:
21
+ * - "timeout:5000" -> { type: "timeout", delay: 5000 }
22
+ * - "exit-intent" -> { type: "exit-intent" }
23
+ */
24
+ export function parseTriggerAttr(value: string): TriggerConfig {
25
+ const trimmed = value.trim();
26
+
27
+ if (trimmed.startsWith("timeout:")) {
28
+ const delay = parseInt(trimmed.slice("timeout:".length), 10);
29
+ if (isNaN(delay) || delay < 0) {
30
+ throw new Error(`Invalid timeout delay: "${value}"`);
31
+ }
32
+ return { type: "timeout", delay };
33
+ }
34
+
35
+ if (trimmed === "timeout") {
36
+ return { type: "timeout", delay: 5000 };
37
+ }
38
+
39
+ if (trimmed === "exit-intent") {
40
+ return { type: "exit-intent" };
41
+ }
42
+
43
+ throw new Error(
44
+ `Unknown trigger type: "${value}". Expected "timeout:<ms>" or "exit-intent".`
45
+ );
46
+ }
47
+
48
+ /**
49
+ * Set up a trigger that calls `callback` when fired.
50
+ * Returns a cleanup function to teardown the trigger.
51
+ */
52
+ export function setupTrigger(
53
+ config: TriggerConfig,
54
+ callback: () => void
55
+ ): () => void {
56
+ if (config.type === "timeout") {
57
+ const timer = setTimeout(callback, config.delay);
58
+ return () => clearTimeout(timer);
59
+ }
60
+
61
+ if (config.type === "exit-intent") {
62
+ const handler = (e: MouseEvent) => {
63
+ if (e.clientY <= 0) {
64
+ callback();
65
+ document.removeEventListener("mouseleave", handler);
66
+ }
67
+ };
68
+ document.addEventListener("mouseleave", handler);
69
+ return () => document.removeEventListener("mouseleave", handler);
70
+ }
71
+
72
+ // Exhaustive check
73
+ const _exhaustive: never = config;
74
+ throw new Error(
75
+ `Unknown trigger type: ${(_exhaustive as TriggerConfig).type}`
76
+ );
77
+ }
78
+
79
+ function storageKey(researchId: string): string {
80
+ return `${STORAGE_KEYS.triggerShown}:${researchId}`;
81
+ }
82
+
83
+ /**
84
+ * Parse a show-once attribute value into a ShowOnce.
85
+ *
86
+ * Formats:
87
+ * - "session" -> "session"
88
+ * - "visitor" -> "visitor"
89
+ * - "false" -> false
90
+ * - anything else -> defaults to "session"
91
+ */
92
+ export function parseShowOnceAttr(value: string | null): ShowOnce {
93
+ if (!value) return "session";
94
+ const trimmed = value.trim();
95
+ if (trimmed === "visitor") return "visitor";
96
+ if (trimmed === "false") return false;
97
+ return "session";
98
+ }
99
+
100
+ /**
101
+ * Check if the popup should be shown based on show-once dedup.
102
+ */
103
+ export function shouldShow(researchId: string, showOnce: ShowOnce): boolean {
104
+ if (showOnce === false) return true;
105
+
106
+ try {
107
+ const storage = showOnce === "visitor" ? localStorage : sessionStorage;
108
+ return storage.getItem(storageKey(researchId)) === null;
109
+ } catch {
110
+ // Storage unavailable (private browsing, etc.) — show anyway
111
+ return true;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Mark the popup as shown for dedup purposes.
117
+ */
118
+ export function markShown(researchId: string, showOnce: ShowOnce): void {
119
+ if (showOnce === false) return;
120
+
121
+ try {
122
+ const storage = showOnce === "visitor" ? localStorage : sessionStorage;
123
+ storage.setItem(storageKey(researchId), "1");
124
+ } catch {
125
+ // Storage unavailable — ignore
126
+ }
127
+ }
package/src/types.ts CHANGED
@@ -23,6 +23,23 @@ export type EmbedType =
23
23
  | "fullpage"
24
24
  | "chat";
25
25
 
26
+ // ============================================================================
27
+ // Auto-open Trigger Types
28
+ // ============================================================================
29
+
30
+ export type TriggerConfig =
31
+ | { type: "timeout"; delay: number }
32
+ | { type: "exit-intent" };
33
+
34
+ export type TriggerType = TriggerConfig["type"];
35
+
36
+ export type ShowOnce = "session" | "visitor" | false;
37
+
38
+ export interface AutoOpenConfig {
39
+ trigger: TriggerConfig;
40
+ showOnce?: ShowOnce; // default: "session"
41
+ }
42
+
26
43
  /** Brand colors that can be passed via embed code */
27
44
  export interface BrandColors {
28
45
  /** Primary accent color (buttons, links, focus states) */
@@ -51,6 +68,8 @@ export interface EmbedConfig {
51
68
  theme?: ThemeValue;
52
69
  /** Override the default host (defaults to https://getperspective.ai) */
53
70
  host?: string;
71
+ /** Auto-open trigger configuration (popup only) */
72
+ autoOpen?: AutoOpenConfig;
54
73
  /** Callback when embed is ready */
55
74
  onReady?: () => void;
56
75
  /** Callback when interview is submitted/completed */