@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.
- package/README.md +333 -0
- package/dist/browser.cjs +1939 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.cts +213 -0
- package/dist/browser.d.ts +213 -0
- package/dist/browser.js +1900 -0
- package/dist/browser.js.map +1 -0
- package/dist/cdn/perspective.global.js +406 -0
- package/dist/cdn/perspective.global.js.map +1 -0
- package/dist/constants.cjs +142 -0
- package/dist/constants.cjs.map +1 -0
- package/dist/constants.d.cts +104 -0
- package/dist/constants.d.ts +104 -0
- package/dist/constants.js +127 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.cjs +1596 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +155 -0
- package/dist/index.d.ts +155 -0
- package/dist/index.js +1579 -0
- package/dist/index.js.map +1 -0
- package/package.json +83 -0
- package/src/browser.test.ts +388 -0
- package/src/browser.ts +509 -0
- package/src/config.test.ts +81 -0
- package/src/config.ts +95 -0
- package/src/constants.ts +214 -0
- package/src/float.test.ts +332 -0
- package/src/float.ts +231 -0
- package/src/fullpage.test.ts +224 -0
- package/src/fullpage.ts +126 -0
- package/src/iframe.test.ts +1037 -0
- package/src/iframe.ts +421 -0
- package/src/index.ts +61 -0
- package/src/loading.ts +90 -0
- package/src/popup.test.ts +344 -0
- package/src/popup.ts +157 -0
- package/src/slider.test.ts +277 -0
- package/src/slider.ts +158 -0
- package/src/styles.ts +395 -0
- package/src/types.ts +148 -0
- package/src/utils.test.ts +162 -0
- package/src/utils.ts +86 -0
- package/src/widget.test.ts +375 -0
- package/src/widget.ts +195 -0
package/src/iframe.ts
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iframe creation and postMessage communication
|
|
3
|
+
* SSR-safe - all DOM access is guarded
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
BrandColors,
|
|
8
|
+
EmbedConfig,
|
|
9
|
+
EmbedMessage,
|
|
10
|
+
EmbedType,
|
|
11
|
+
} from "./types";
|
|
12
|
+
import { hasDom } from "./config";
|
|
13
|
+
import {
|
|
14
|
+
UTM_PARAMS,
|
|
15
|
+
RESERVED_PARAMS,
|
|
16
|
+
PARAM_KEYS,
|
|
17
|
+
BRAND_KEYS,
|
|
18
|
+
MESSAGE_TYPES,
|
|
19
|
+
THEME_VALUES,
|
|
20
|
+
PARAM_VALUES,
|
|
21
|
+
STORAGE_KEYS,
|
|
22
|
+
SDK_VERSION,
|
|
23
|
+
CURRENT_FEATURES,
|
|
24
|
+
} from "./constants";
|
|
25
|
+
import { normalizeHex } from "./utils";
|
|
26
|
+
|
|
27
|
+
/** Validate redirect URL - allow https, http localhost, and relative URLs */
|
|
28
|
+
function isAllowedRedirectUrl(url: string): boolean {
|
|
29
|
+
if (!url || typeof url !== "string") return false;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = new URL(url, window.location.origin);
|
|
32
|
+
const protocol = parsed.protocol.toLowerCase();
|
|
33
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
34
|
+
|
|
35
|
+
if (protocol === "https:") return true;
|
|
36
|
+
if (
|
|
37
|
+
protocol === "http:" &&
|
|
38
|
+
(hostname === "localhost" || hostname === "127.0.0.1")
|
|
39
|
+
)
|
|
40
|
+
return true;
|
|
41
|
+
|
|
42
|
+
return false;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Get or create persistent anonymous ID */
|
|
49
|
+
function getOrCreateAnonId(): string {
|
|
50
|
+
if (!hasDom()) return "";
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
let id = localStorage.getItem(STORAGE_KEYS.anonId);
|
|
54
|
+
if (!id) {
|
|
55
|
+
id = crypto.randomUUID();
|
|
56
|
+
localStorage.setItem(STORAGE_KEYS.anonId, id);
|
|
57
|
+
}
|
|
58
|
+
return id;
|
|
59
|
+
} catch {
|
|
60
|
+
// localStorage might be blocked
|
|
61
|
+
return crypto.randomUUID();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Collect UTM params from current page URL */
|
|
66
|
+
function getUtmParams(): Record<string, string> {
|
|
67
|
+
if (!hasDom()) return {};
|
|
68
|
+
|
|
69
|
+
const params: Record<string, string> = {};
|
|
70
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
71
|
+
|
|
72
|
+
for (const key of UTM_PARAMS) {
|
|
73
|
+
const value = searchParams.get(key);
|
|
74
|
+
if (value) {
|
|
75
|
+
params[key] = value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return params;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Build iframe URL with all params */
|
|
83
|
+
function buildIframeUrl(
|
|
84
|
+
researchId: string,
|
|
85
|
+
type: EmbedType,
|
|
86
|
+
host: string,
|
|
87
|
+
customParams?: Record<string, string>,
|
|
88
|
+
brand?: { light?: BrandColors; dark?: BrandColors },
|
|
89
|
+
themeOverride?: "dark" | "light" | "system"
|
|
90
|
+
): string {
|
|
91
|
+
const url = new URL(`${host}/interview/${researchId}`);
|
|
92
|
+
|
|
93
|
+
// Base embed params
|
|
94
|
+
url.searchParams.set(PARAM_KEYS.embed, PARAM_VALUES.true);
|
|
95
|
+
url.searchParams.set(PARAM_KEYS.embedType, type === "float" ? "chat" : type);
|
|
96
|
+
|
|
97
|
+
// Detect and pass system theme preference (can be overridden)
|
|
98
|
+
if (hasDom()) {
|
|
99
|
+
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
100
|
+
if (themeOverride && themeOverride !== THEME_VALUES.system) {
|
|
101
|
+
url.searchParams.set(PARAM_KEYS.theme, themeOverride);
|
|
102
|
+
} else {
|
|
103
|
+
url.searchParams.set(
|
|
104
|
+
PARAM_KEYS.theme,
|
|
105
|
+
isDark ? THEME_VALUES.dark : THEME_VALUES.light
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
// SSR fallback
|
|
110
|
+
url.searchParams.set(PARAM_KEYS.theme, themeOverride || THEME_VALUES.light);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Auto-forward UTM params from parent
|
|
114
|
+
const utmParams = getUtmParams();
|
|
115
|
+
for (const [key, value] of Object.entries(utmParams)) {
|
|
116
|
+
url.searchParams.set(key, value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Helper to set param only if color is valid
|
|
120
|
+
const setColor = (key: string, color: string | undefined) => {
|
|
121
|
+
if (!color) return;
|
|
122
|
+
const normalized = normalizeHex(color);
|
|
123
|
+
if (normalized) url.searchParams.set(key, normalized);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Add brand colors using short keys
|
|
127
|
+
if (brand?.light) {
|
|
128
|
+
setColor(BRAND_KEYS.primary, brand.light.primary);
|
|
129
|
+
setColor(BRAND_KEYS.secondary, brand.light.secondary);
|
|
130
|
+
setColor(BRAND_KEYS.bg, brand.light.bg);
|
|
131
|
+
setColor(BRAND_KEYS.text, brand.light.text);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Add dark mode brand colors
|
|
135
|
+
if (brand?.dark) {
|
|
136
|
+
setColor(BRAND_KEYS.darkPrimary, brand.dark.primary);
|
|
137
|
+
setColor(BRAND_KEYS.darkSecondary, brand.dark.secondary);
|
|
138
|
+
setColor(BRAND_KEYS.darkBg, brand.dark.bg);
|
|
139
|
+
setColor(BRAND_KEYS.darkText, brand.dark.text);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Add custom params, filtering out reserved keys
|
|
143
|
+
if (customParams) {
|
|
144
|
+
for (const [key, value] of Object.entries(customParams)) {
|
|
145
|
+
if (!RESERVED_PARAMS.has(key)) {
|
|
146
|
+
url.searchParams.set(key, value);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return url.toString();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function createIframe(
|
|
155
|
+
researchId: string,
|
|
156
|
+
type: EmbedType,
|
|
157
|
+
host: string,
|
|
158
|
+
params?: Record<string, string>,
|
|
159
|
+
brand?: { light?: BrandColors; dark?: BrandColors },
|
|
160
|
+
themeOverride?: "dark" | "light" | "system"
|
|
161
|
+
): HTMLIFrameElement {
|
|
162
|
+
if (!hasDom()) {
|
|
163
|
+
// Return a stub for SSR
|
|
164
|
+
return {} as HTMLIFrameElement;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const iframe = document.createElement("iframe");
|
|
168
|
+
iframe.src = buildIframeUrl(
|
|
169
|
+
researchId,
|
|
170
|
+
type,
|
|
171
|
+
host,
|
|
172
|
+
params,
|
|
173
|
+
brand,
|
|
174
|
+
themeOverride
|
|
175
|
+
);
|
|
176
|
+
iframe.setAttribute("allow", "microphone; camera");
|
|
177
|
+
iframe.setAttribute("allowfullscreen", "true");
|
|
178
|
+
iframe.setAttribute(
|
|
179
|
+
"sandbox",
|
|
180
|
+
"allow-scripts allow-same-origin allow-forms allow-popups allow-modals allow-top-navigation"
|
|
181
|
+
);
|
|
182
|
+
iframe.setAttribute("data-perspective", "true");
|
|
183
|
+
iframe.style.cssText = "border:none;";
|
|
184
|
+
|
|
185
|
+
return iframe;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function setupMessageListener(
|
|
189
|
+
researchId: string,
|
|
190
|
+
config: Partial<EmbedConfig>,
|
|
191
|
+
iframe: HTMLIFrameElement,
|
|
192
|
+
host: string,
|
|
193
|
+
options?: { skipResize?: boolean }
|
|
194
|
+
): () => void {
|
|
195
|
+
if (!hasDom()) {
|
|
196
|
+
return () => {};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const handler = (event: MessageEvent<EmbedMessage>) => {
|
|
200
|
+
// Security: Only accept messages from our embed host and from the expected iframe
|
|
201
|
+
if (event.origin !== host) return;
|
|
202
|
+
if (event.source !== iframe.contentWindow) return;
|
|
203
|
+
|
|
204
|
+
// Only process messages from our embed
|
|
205
|
+
if (typeof event.data?.type !== "string") return;
|
|
206
|
+
if (!event.data.type.startsWith("perspective:")) return;
|
|
207
|
+
if (event.data.researchId !== researchId) return;
|
|
208
|
+
|
|
209
|
+
switch (event.data.type) {
|
|
210
|
+
case MESSAGE_TYPES.ready:
|
|
211
|
+
// Send scrollbar styles when iframe is ready
|
|
212
|
+
sendScrollbarStyles(iframe, host);
|
|
213
|
+
// Send anon_id for anonymous auth
|
|
214
|
+
sendMessage(iframe, host, {
|
|
215
|
+
type: MESSAGE_TYPES.anonId,
|
|
216
|
+
anonId: getOrCreateAnonId(),
|
|
217
|
+
});
|
|
218
|
+
// Send init message with version/features for handshake
|
|
219
|
+
sendMessage(iframe, host, {
|
|
220
|
+
type: MESSAGE_TYPES.init,
|
|
221
|
+
version: SDK_VERSION,
|
|
222
|
+
features: CURRENT_FEATURES,
|
|
223
|
+
researchId,
|
|
224
|
+
});
|
|
225
|
+
config.onReady?.();
|
|
226
|
+
break;
|
|
227
|
+
|
|
228
|
+
case MESSAGE_TYPES.resize:
|
|
229
|
+
// Auto-resize iframe height (skip for fixed-container embeds)
|
|
230
|
+
if (!options?.skipResize) {
|
|
231
|
+
iframe.style.height = `${event.data.height}px`;
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
|
|
235
|
+
case MESSAGE_TYPES.submit:
|
|
236
|
+
config.onSubmit?.({ researchId });
|
|
237
|
+
break;
|
|
238
|
+
|
|
239
|
+
case MESSAGE_TYPES.close:
|
|
240
|
+
config.onClose?.();
|
|
241
|
+
break;
|
|
242
|
+
|
|
243
|
+
case MESSAGE_TYPES.error:
|
|
244
|
+
const error = new Error(
|
|
245
|
+
event.data.error
|
|
246
|
+
) as import("./types").EmbedError;
|
|
247
|
+
error.code =
|
|
248
|
+
(event.data.code as import("./types").ErrorCode) || "UNKNOWN";
|
|
249
|
+
config.onError?.(error);
|
|
250
|
+
break;
|
|
251
|
+
|
|
252
|
+
case MESSAGE_TYPES.redirect:
|
|
253
|
+
const redirectUrl = event.data.url;
|
|
254
|
+
// Security: Only allow http(s) and localhost URLs
|
|
255
|
+
if (!isAllowedRedirectUrl(redirectUrl)) {
|
|
256
|
+
console.warn(
|
|
257
|
+
"[Perspective] Blocked unsafe redirect URL:",
|
|
258
|
+
redirectUrl
|
|
259
|
+
);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (config.onNavigate) {
|
|
263
|
+
config.onNavigate(redirectUrl);
|
|
264
|
+
} else {
|
|
265
|
+
// Fallback: auto-navigate parent page
|
|
266
|
+
window.location.href = redirectUrl;
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
window.addEventListener("message", handler);
|
|
273
|
+
return () => window.removeEventListener("message", handler);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Send a message to the embed iframe */
|
|
277
|
+
export function sendMessage(
|
|
278
|
+
iframe: HTMLIFrameElement,
|
|
279
|
+
host: string,
|
|
280
|
+
message: { type: string; [key: string]: unknown }
|
|
281
|
+
): void {
|
|
282
|
+
if (!hasDom()) return;
|
|
283
|
+
iframe.contentWindow?.postMessage(message, host);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Track all active iframes for theme change notifications */
|
|
287
|
+
const activeIframes = new Map<HTMLIFrameElement, string>();
|
|
288
|
+
|
|
289
|
+
export function registerIframe(
|
|
290
|
+
iframe: HTMLIFrameElement,
|
|
291
|
+
host: string
|
|
292
|
+
): () => void {
|
|
293
|
+
activeIframes.set(iframe, host);
|
|
294
|
+
return () => {
|
|
295
|
+
activeIframes.delete(iframe);
|
|
296
|
+
if (activeIframes.size === 0) {
|
|
297
|
+
teardownGlobalListeners();
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Get scrollbar CSS styles */
|
|
303
|
+
function getScrollbarStyles(): string {
|
|
304
|
+
if (!hasDom()) return "";
|
|
305
|
+
|
|
306
|
+
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
307
|
+
const borderColor = isDark ? "hsl(217 33% 17%)" : "hsl(240 6% 90%)";
|
|
308
|
+
|
|
309
|
+
return `
|
|
310
|
+
* {
|
|
311
|
+
scrollbar-width: thin;
|
|
312
|
+
scrollbar-color: ${borderColor} transparent;
|
|
313
|
+
}
|
|
314
|
+
*::-webkit-scrollbar {
|
|
315
|
+
width: 10px;
|
|
316
|
+
height: 10px;
|
|
317
|
+
}
|
|
318
|
+
*::-webkit-scrollbar-track {
|
|
319
|
+
background: transparent;
|
|
320
|
+
}
|
|
321
|
+
*::-webkit-scrollbar-thumb {
|
|
322
|
+
background-color: ${borderColor};
|
|
323
|
+
border-radius: 9999px;
|
|
324
|
+
border: 2px solid transparent;
|
|
325
|
+
background-clip: padding-box;
|
|
326
|
+
}
|
|
327
|
+
*::-webkit-scrollbar-thumb:hover {
|
|
328
|
+
background-color: color-mix(in srgb, ${borderColor} 80%, currentColor);
|
|
329
|
+
}
|
|
330
|
+
`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Send scrollbar styles to an iframe */
|
|
334
|
+
export function sendScrollbarStyles(
|
|
335
|
+
iframe: HTMLIFrameElement,
|
|
336
|
+
host: string
|
|
337
|
+
): void {
|
|
338
|
+
const styles = getScrollbarStyles();
|
|
339
|
+
sendMessage(iframe, host, {
|
|
340
|
+
type: MESSAGE_TYPES.injectStyles,
|
|
341
|
+
styles,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Notify all active iframes of theme change */
|
|
346
|
+
export function notifyThemeChange(): void {
|
|
347
|
+
if (!hasDom()) return;
|
|
348
|
+
|
|
349
|
+
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
350
|
+
|
|
351
|
+
activeIframes.forEach((host, iframe) => {
|
|
352
|
+
const message = {
|
|
353
|
+
type: MESSAGE_TYPES.themeChange,
|
|
354
|
+
theme: isDark ? THEME_VALUES.dark : THEME_VALUES.light,
|
|
355
|
+
};
|
|
356
|
+
sendMessage(iframe, host, message);
|
|
357
|
+
sendScrollbarStyles(iframe, host);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
let themeListener: ((e: MediaQueryListEvent) => void) | null = null;
|
|
362
|
+
let themeMediaQuery: MediaQueryList | null = null;
|
|
363
|
+
let globalMessageHandler: ((event: MessageEvent) => void) | null = null;
|
|
364
|
+
let globalListenersInitialized = false;
|
|
365
|
+
|
|
366
|
+
function setupThemeListener(): void {
|
|
367
|
+
if (themeListener || !hasDom()) return;
|
|
368
|
+
|
|
369
|
+
themeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
370
|
+
themeListener = () => notifyThemeChange();
|
|
371
|
+
themeMediaQuery.addEventListener("change", themeListener);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function teardownThemeListener(): void {
|
|
375
|
+
if (themeListener && themeMediaQuery) {
|
|
376
|
+
themeMediaQuery.removeEventListener("change", themeListener);
|
|
377
|
+
themeListener = null;
|
|
378
|
+
themeMediaQuery = null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function setupGlobalListeners(): void {
|
|
383
|
+
if (!hasDom() || globalMessageHandler) return;
|
|
384
|
+
|
|
385
|
+
setupThemeListener();
|
|
386
|
+
|
|
387
|
+
globalMessageHandler = (event: MessageEvent) => {
|
|
388
|
+
if (!event.data?.type?.startsWith("perspective:")) return;
|
|
389
|
+
if (event.data.type === MESSAGE_TYPES.requestScrollbarStyles) {
|
|
390
|
+
const iframes = Array.from(
|
|
391
|
+
document.querySelectorAll("iframe[data-perspective]")
|
|
392
|
+
);
|
|
393
|
+
const sourceIframe = iframes.find(
|
|
394
|
+
(iframe) => (iframe as HTMLIFrameElement).contentWindow === event.source
|
|
395
|
+
) as HTMLIFrameElement | undefined;
|
|
396
|
+
if (sourceIframe) {
|
|
397
|
+
const host = activeIframes.get(sourceIframe);
|
|
398
|
+
if (host && event.origin === host) {
|
|
399
|
+
sendScrollbarStyles(sourceIframe, host);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
window.addEventListener("message", globalMessageHandler);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function teardownGlobalListeners(): void {
|
|
409
|
+
if (globalMessageHandler) {
|
|
410
|
+
window.removeEventListener("message", globalMessageHandler);
|
|
411
|
+
globalMessageHandler = null;
|
|
412
|
+
}
|
|
413
|
+
teardownThemeListener();
|
|
414
|
+
globalListenersInitialized = false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function ensureGlobalListeners(): void {
|
|
418
|
+
if (globalListenersInitialized) return;
|
|
419
|
+
globalListenersInitialized = true;
|
|
420
|
+
setupGlobalListeners();
|
|
421
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Perspective Embed SDK
|
|
3
|
+
*
|
|
4
|
+
* NPM Entry Point - clean exports, no auto-init, SSR-safe
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import { createWidget, openPopup, openSlider, createFloatBubble } from '@perspective-ai/embed';
|
|
8
|
+
*
|
|
9
|
+
* // Inline widget
|
|
10
|
+
* const widget = createWidget(container, { researchId: 'xxx' });
|
|
11
|
+
*
|
|
12
|
+
* // Popup modal
|
|
13
|
+
* const popup = openPopup({ researchId: 'xxx' });
|
|
14
|
+
*
|
|
15
|
+
* // Slider panel
|
|
16
|
+
* const slider = openSlider({ researchId: 'xxx' });
|
|
17
|
+
*
|
|
18
|
+
* // Floating bubble
|
|
19
|
+
* const bubble = createFloatBubble({ researchId: 'xxx' });
|
|
20
|
+
*
|
|
21
|
+
* For SSR-safe constants and types:
|
|
22
|
+
* import { DATA_ATTRS, MESSAGE_TYPES } from '@perspective-ai/embed/constants';
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// Core embed functions
|
|
26
|
+
export { createWidget } from "./widget";
|
|
27
|
+
export { openPopup } from "./popup";
|
|
28
|
+
export { openSlider } from "./slider";
|
|
29
|
+
export { createFloatBubble, createChatBubble } from "./float";
|
|
30
|
+
export { createFullpage } from "./fullpage";
|
|
31
|
+
|
|
32
|
+
// Configuration
|
|
33
|
+
export { configure, getConfig } from "./config";
|
|
34
|
+
|
|
35
|
+
// Types
|
|
36
|
+
export type {
|
|
37
|
+
BrandColors,
|
|
38
|
+
EmbedConfig,
|
|
39
|
+
EmbedHandle,
|
|
40
|
+
FloatHandle,
|
|
41
|
+
ModalHandle,
|
|
42
|
+
EmbedInstance,
|
|
43
|
+
EmbedError,
|
|
44
|
+
EmbedType,
|
|
45
|
+
ThemeConfig,
|
|
46
|
+
SDKConfig,
|
|
47
|
+
} from "./types";
|
|
48
|
+
|
|
49
|
+
// Re-export commonly used constants and types
|
|
50
|
+
export {
|
|
51
|
+
SDK_VERSION,
|
|
52
|
+
FEATURES,
|
|
53
|
+
CURRENT_FEATURES,
|
|
54
|
+
DATA_ATTRS,
|
|
55
|
+
PARAM_KEYS,
|
|
56
|
+
BRAND_KEYS,
|
|
57
|
+
MESSAGE_TYPES,
|
|
58
|
+
THEME_VALUES,
|
|
59
|
+
} from "./constants";
|
|
60
|
+
|
|
61
|
+
export type { ThemeValue, ParamKey, BrandKey, MessageType } from "./constants";
|
package/src/loading.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading indicator for embed iframes
|
|
3
|
+
* SSR-safe - returns no-op on server
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { BrandColors, ThemeValue } from "./types";
|
|
7
|
+
import { hasDom } from "./config";
|
|
8
|
+
import { resolveTheme, hexToRgba } from "./utils";
|
|
9
|
+
|
|
10
|
+
/** Default colors matching codebase theme */
|
|
11
|
+
const DEFAULT_COLORS = {
|
|
12
|
+
light: {
|
|
13
|
+
bg: "#ffffff",
|
|
14
|
+
primary: "#7629C8",
|
|
15
|
+
},
|
|
16
|
+
dark: {
|
|
17
|
+
bg: "#02040a",
|
|
18
|
+
primary: "#B170FF",
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface LoadingOptions {
|
|
23
|
+
/** Theme override: 'dark', 'light', or 'system' (uses system preference) */
|
|
24
|
+
theme?: ThemeValue;
|
|
25
|
+
/** Brand colors - uses primary color for spinner */
|
|
26
|
+
brand?: {
|
|
27
|
+
light?: BrandColors;
|
|
28
|
+
dark?: BrandColors;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Get colors for loading indicator based on theme and brand */
|
|
33
|
+
function getLoadingColors(options?: LoadingOptions): {
|
|
34
|
+
bg: string;
|
|
35
|
+
primary: string;
|
|
36
|
+
} {
|
|
37
|
+
const theme = resolveTheme(options?.theme);
|
|
38
|
+
const isDark = theme === "dark";
|
|
39
|
+
|
|
40
|
+
// Get brand colors for current theme
|
|
41
|
+
const brandColors = isDark ? options?.brand?.dark : options?.brand?.light;
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
bg:
|
|
45
|
+
brandColors?.bg ||
|
|
46
|
+
(isDark ? DEFAULT_COLORS.dark.bg : DEFAULT_COLORS.light.bg),
|
|
47
|
+
primary:
|
|
48
|
+
brandColors?.primary ||
|
|
49
|
+
(isDark ? DEFAULT_COLORS.dark.primary : DEFAULT_COLORS.light.primary),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createLoadingIndicator(options?: LoadingOptions): HTMLElement {
|
|
54
|
+
// SSR safety - return empty div on server
|
|
55
|
+
if (!hasDom()) {
|
|
56
|
+
return { remove: () => {}, style: {} } as unknown as HTMLElement;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const colors = getLoadingColors(options);
|
|
60
|
+
|
|
61
|
+
const container = document.createElement("div");
|
|
62
|
+
container.className = "perspective-loading";
|
|
63
|
+
container.style.cssText = `
|
|
64
|
+
position: absolute;
|
|
65
|
+
top: 0;
|
|
66
|
+
left: 0;
|
|
67
|
+
right: 0;
|
|
68
|
+
bottom: 0;
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
justify-content: center;
|
|
72
|
+
background: ${colors.bg};
|
|
73
|
+
transition: opacity 0.3s ease;
|
|
74
|
+
z-index: 1;
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
// Create spinner (keyframes defined in styles.ts)
|
|
78
|
+
const spinner = document.createElement("div");
|
|
79
|
+
spinner.style.cssText = `
|
|
80
|
+
width: 2.5rem;
|
|
81
|
+
height: 2.5rem;
|
|
82
|
+
border: 3px solid ${hexToRgba(colors.primary, 0.15)};
|
|
83
|
+
border-top-color: ${colors.primary};
|
|
84
|
+
border-radius: 50%;
|
|
85
|
+
animation: perspective-spin 0.8s linear infinite;
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
container.appendChild(spinner);
|
|
89
|
+
return container;
|
|
90
|
+
}
|