@perspective-ai/sdk-react 1.0.0-alpha.2 → 1.0.0-alpha.3

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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { useRef, useLayoutEffect, useEffect, useCallback, useState, useMemo } from 'react';
2
- import { createWidget, openPopup, openSlider, createFloatBubble, createFullpage } from '@perspective-ai/sdk';
1
+ import { useRef, useLayoutEffect, useEffect, useCallback, useState } from 'react';
2
+ import { openPopup, openSlider, createFloatBubble, createWidget, createFullpage } from '@perspective-ai/sdk';
3
3
  import { jsx } from 'react/jsx-runtime';
4
4
 
5
5
  var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
@@ -13,33 +13,52 @@ function useStableCallback(callback) {
13
13
  []
14
14
  );
15
15
  }
16
- function Widget({
17
- researchId,
18
- params,
19
- brand,
20
- theme,
21
- host,
22
- onReady,
23
- onSubmit,
24
- onNavigate,
25
- onClose,
26
- onError,
27
- embedRef,
28
- className,
29
- style,
30
- ...divProps
31
- }) {
32
- const containerRef = useRef(null);
16
+
17
+ // src/hooks/usePopup.ts
18
+ function usePopup(options) {
19
+ const {
20
+ researchId,
21
+ params,
22
+ brand,
23
+ theme,
24
+ host,
25
+ onReady,
26
+ onSubmit,
27
+ onNavigate,
28
+ onClose,
29
+ onError,
30
+ open: controlledOpen,
31
+ onOpenChange
32
+ } = options;
33
+ const [handle, setHandle] = useState(null);
34
+ const [internalOpen, setInternalOpen] = useState(false);
33
35
  const handleRef = useRef(null);
36
+ const isControlled = controlledOpen !== void 0;
37
+ const isOpen = isControlled ? controlledOpen : internalOpen;
34
38
  const stableOnReady = useStableCallback(onReady);
35
39
  const stableOnSubmit = useStableCallback(onSubmit);
36
40
  const stableOnNavigate = useStableCallback(onNavigate);
37
- const stableOnClose = useStableCallback(onClose);
38
41
  const stableOnError = useStableCallback(onError);
39
- useEffect(() => {
40
- const container = containerRef.current;
41
- if (!container) return;
42
- const handle = createWidget(container, {
42
+ const setOpen = useCallback(
43
+ (value) => {
44
+ if (isControlled) {
45
+ onOpenChange?.(value);
46
+ } else {
47
+ setInternalOpen(value);
48
+ }
49
+ },
50
+ [isControlled, onOpenChange]
51
+ );
52
+ const handleClose = useCallback(() => {
53
+ handleRef.current = null;
54
+ setHandle(null);
55
+ setOpen(false);
56
+ onClose?.();
57
+ }, [setOpen, onClose]);
58
+ const stableOnClose = useStableCallback(handleClose);
59
+ const createPopup = useCallback(() => {
60
+ if (handleRef.current) return handleRef.current;
61
+ const newHandle = openPopup({
43
62
  researchId,
44
63
  params,
45
64
  brand,
@@ -51,17 +70,9 @@ function Widget({
51
70
  onClose: stableOnClose,
52
71
  onError: stableOnError
53
72
  });
54
- handleRef.current = handle;
55
- if (embedRef) {
56
- embedRef.current = handle;
57
- }
58
- return () => {
59
- handle.unmount();
60
- handleRef.current = null;
61
- if (embedRef) {
62
- embedRef.current = null;
63
- }
64
- };
73
+ handleRef.current = newHandle;
74
+ setHandle(newHandle);
75
+ return newHandle;
65
76
  }, [
66
77
  researchId,
67
78
  params,
@@ -72,42 +83,82 @@ function Widget({
72
83
  stableOnSubmit,
73
84
  stableOnNavigate,
74
85
  stableOnClose,
75
- stableOnError,
76
- embedRef
86
+ stableOnError
77
87
  ]);
78
- return /* @__PURE__ */ jsx(
79
- "div",
80
- {
81
- ref: containerRef,
82
- className,
83
- style: { minHeight: 500, ...style },
84
- "data-testid": "perspective-widget",
85
- ...divProps
88
+ const destroyPopup = useCallback(() => {
89
+ if (handleRef.current) {
90
+ handleRef.current.destroy();
91
+ handleRef.current = null;
92
+ setHandle(null);
86
93
  }
87
- );
94
+ }, []);
95
+ const openFn = useCallback(() => {
96
+ if (isControlled) {
97
+ onOpenChange?.(true);
98
+ } else {
99
+ createPopup();
100
+ setInternalOpen(true);
101
+ }
102
+ }, [isControlled, onOpenChange, createPopup]);
103
+ const closeFn = useCallback(() => {
104
+ if (isControlled) {
105
+ onOpenChange?.(false);
106
+ } else {
107
+ destroyPopup();
108
+ setInternalOpen(false);
109
+ }
110
+ }, [isControlled, onOpenChange, destroyPopup]);
111
+ const toggleFn = useCallback(() => {
112
+ if (isOpen) {
113
+ closeFn();
114
+ } else {
115
+ openFn();
116
+ }
117
+ }, [isOpen, openFn, closeFn]);
118
+ useEffect(() => {
119
+ if (!isControlled) return;
120
+ if (controlledOpen && !handleRef.current) {
121
+ createPopup();
122
+ } else if (!controlledOpen && handleRef.current) {
123
+ destroyPopup();
124
+ }
125
+ }, [controlledOpen, isControlled, createPopup, destroyPopup]);
126
+ useEffect(() => {
127
+ return () => {
128
+ if (handleRef.current) {
129
+ handleRef.current.destroy();
130
+ handleRef.current = null;
131
+ }
132
+ };
133
+ }, []);
134
+ return {
135
+ open: openFn,
136
+ close: closeFn,
137
+ toggle: toggleFn,
138
+ isOpen,
139
+ handle
140
+ };
88
141
  }
89
- function PopupButton({
90
- researchId,
91
- params,
92
- brand,
93
- theme,
94
- host,
95
- onReady,
96
- onSubmit,
97
- onNavigate,
98
- onClose,
99
- onError,
100
- children,
101
- open,
102
- onOpenChange,
103
- embedRef,
104
- onClick,
105
- ...buttonProps
106
- }) {
107
- const handleRef = useRef(null);
142
+ function useSlider(options) {
143
+ const {
144
+ researchId,
145
+ params,
146
+ brand,
147
+ theme,
148
+ host,
149
+ onReady,
150
+ onSubmit,
151
+ onNavigate,
152
+ onClose,
153
+ onError,
154
+ open: controlledOpen,
155
+ onOpenChange
156
+ } = options;
157
+ const [handle, setHandle] = useState(null);
108
158
  const [internalOpen, setInternalOpen] = useState(false);
109
- const isControlled = open !== void 0;
110
- const isOpen = isControlled ? open : internalOpen;
159
+ const handleRef = useRef(null);
160
+ const isControlled = controlledOpen !== void 0;
161
+ const isOpen = isControlled ? controlledOpen : internalOpen;
111
162
  const stableOnReady = useStableCallback(onReady);
112
163
  const stableOnSubmit = useStableCallback(onSubmit);
113
164
  const stableOnNavigate = useStableCallback(onNavigate);
@@ -124,13 +175,14 @@ function PopupButton({
124
175
  );
125
176
  const handleClose = useCallback(() => {
126
177
  handleRef.current = null;
178
+ setHandle(null);
127
179
  setOpen(false);
128
180
  onClose?.();
129
181
  }, [setOpen, onClose]);
130
182
  const stableOnClose = useStableCallback(handleClose);
131
- const createPopup = useCallback(() => {
183
+ const createSlider = useCallback(() => {
132
184
  if (handleRef.current) return handleRef.current;
133
- const handle = openPopup({
185
+ const newHandle = openSlider({
134
186
  researchId,
135
187
  params,
136
188
  brand,
@@ -142,8 +194,9 @@ function PopupButton({
142
194
  onClose: stableOnClose,
143
195
  onError: stableOnError
144
196
  });
145
- handleRef.current = handle;
146
- return handle;
197
+ handleRef.current = newHandle;
198
+ setHandle(newHandle);
199
+ return newHandle;
147
200
  }, [
148
201
  researchId,
149
202
  params,
@@ -156,129 +209,93 @@ function PopupButton({
156
209
  stableOnClose,
157
210
  stableOnError
158
211
  ]);
159
- const proxyHandle = useMemo(
160
- () => ({
161
- open: () => {
162
- createPopup();
163
- setOpen(true);
164
- },
165
- close: () => {
166
- handleRef.current?.destroy();
167
- handleRef.current = null;
168
- setOpen(false);
169
- },
170
- toggle: () => {
171
- if (handleRef.current) {
172
- handleRef.current.destroy();
173
- handleRef.current = null;
174
- setOpen(false);
175
- } else {
176
- createPopup();
177
- setOpen(true);
178
- }
179
- },
180
- unmount: () => {
181
- handleRef.current?.unmount();
182
- handleRef.current = null;
183
- setOpen(false);
184
- },
185
- get isOpen() {
186
- return isOpen;
187
- },
188
- researchId
189
- }),
190
- [createPopup, setOpen, researchId, isOpen]
191
- );
192
- useEffect(() => {
193
- if (embedRef) {
194
- embedRef.current = proxyHandle;
212
+ const destroySlider = useCallback(() => {
213
+ if (handleRef.current) {
214
+ handleRef.current.destroy();
215
+ handleRef.current = null;
216
+ setHandle(null);
195
217
  }
196
- return () => {
197
- if (embedRef) {
198
- embedRef.current = null;
199
- }
200
- };
201
- }, [embedRef, proxyHandle]);
218
+ }, []);
219
+ const openFn = useCallback(() => {
220
+ if (isControlled) {
221
+ onOpenChange?.(true);
222
+ } else {
223
+ createSlider();
224
+ setInternalOpen(true);
225
+ }
226
+ }, [isControlled, onOpenChange, createSlider]);
227
+ const closeFn = useCallback(() => {
228
+ if (isControlled) {
229
+ onOpenChange?.(false);
230
+ } else {
231
+ destroySlider();
232
+ setInternalOpen(false);
233
+ }
234
+ }, [isControlled, onOpenChange, destroySlider]);
235
+ const toggleFn = useCallback(() => {
236
+ if (isOpen) {
237
+ closeFn();
238
+ } else {
239
+ openFn();
240
+ }
241
+ }, [isOpen, openFn, closeFn]);
202
242
  useEffect(() => {
203
243
  if (!isControlled) return;
204
- if (open && !handleRef.current) {
205
- createPopup();
206
- } else if (!open && handleRef.current) {
207
- handleRef.current.destroy();
208
- handleRef.current = null;
244
+ if (controlledOpen && !handleRef.current) {
245
+ createSlider();
246
+ } else if (!controlledOpen && handleRef.current) {
247
+ destroySlider();
209
248
  }
210
- }, [open, isControlled, createPopup]);
211
- const handleClick = useCallback(
212
- (e) => {
213
- onClick?.(e);
214
- if (e.defaultPrevented) return;
215
- if (isOpen && handleRef.current) {
249
+ }, [controlledOpen, isControlled, createSlider, destroySlider]);
250
+ useEffect(() => {
251
+ return () => {
252
+ if (handleRef.current) {
216
253
  handleRef.current.destroy();
217
254
  handleRef.current = null;
218
- setOpen(false);
219
- } else {
220
- createPopup();
221
- setOpen(true);
222
255
  }
223
- },
224
- [onClick, isOpen, createPopup, setOpen]
225
- );
226
- return /* @__PURE__ */ jsx(
227
- "button",
228
- {
229
- type: "button",
230
- onClick: handleClick,
231
- "data-testid": "perspective-popup-button",
232
- ...buttonProps,
233
- children
234
- }
235
- );
256
+ };
257
+ }, []);
258
+ return {
259
+ open: openFn,
260
+ close: closeFn,
261
+ toggle: toggleFn,
262
+ isOpen,
263
+ handle
264
+ };
236
265
  }
237
- function SliderButton({
238
- researchId,
239
- params,
240
- brand,
241
- theme,
242
- host,
243
- onReady,
244
- onSubmit,
245
- onNavigate,
246
- onClose,
247
- onError,
248
- children,
249
- open,
250
- onOpenChange,
251
- embedRef,
252
- onClick,
253
- ...buttonProps
254
- }) {
255
- const handleRef = useRef(null);
266
+ function useFloatBubble(options) {
267
+ const {
268
+ researchId,
269
+ params,
270
+ brand,
271
+ theme,
272
+ host,
273
+ onReady,
274
+ onSubmit,
275
+ onNavigate,
276
+ onClose,
277
+ onError,
278
+ open: controlledOpen,
279
+ onOpenChange
280
+ } = options;
281
+ const [handle, setHandle] = useState(null);
256
282
  const [internalOpen, setInternalOpen] = useState(false);
257
- const isControlled = open !== void 0;
258
- const isOpen = isControlled ? open : internalOpen;
283
+ const handleRef = useRef(null);
284
+ const isControlled = controlledOpen !== void 0;
259
285
  const stableOnReady = useStableCallback(onReady);
260
286
  const stableOnSubmit = useStableCallback(onSubmit);
261
287
  const stableOnNavigate = useStableCallback(onNavigate);
262
288
  const stableOnError = useStableCallback(onError);
263
- const setOpen = useCallback(
264
- (value) => {
265
- if (isControlled) {
266
- onOpenChange?.(value);
267
- } else {
268
- setInternalOpen(value);
269
- }
270
- },
271
- [isControlled, onOpenChange]
272
- );
273
289
  const handleClose = useCallback(() => {
274
- handleRef.current = null;
275
- setOpen(false);
290
+ setInternalOpen(false);
291
+ if (isControlled) {
292
+ onOpenChange?.(false);
293
+ }
276
294
  onClose?.();
277
- }, [setOpen, onClose]);
295
+ }, [isControlled, onOpenChange, onClose]);
278
296
  const stableOnClose = useStableCallback(handleClose);
279
- const createSlider = useCallback(() => {
280
- if (handleRef.current) return handleRef.current;
281
- const handle = openSlider({
297
+ useEffect(() => {
298
+ const newHandle = createFloatBubble({
282
299
  researchId,
283
300
  params,
284
301
  brand,
@@ -290,8 +307,15 @@ function SliderButton({
290
307
  onClose: stableOnClose,
291
308
  onError: stableOnError
292
309
  });
293
- handleRef.current = handle;
294
- return handle;
310
+ handleRef.current = newHandle;
311
+ setHandle(newHandle);
312
+ return () => {
313
+ if (handleRef.current === newHandle) {
314
+ newHandle.unmount();
315
+ handleRef.current = null;
316
+ setHandle(null);
317
+ }
318
+ };
295
319
  }, [
296
320
  researchId,
297
321
  params,
@@ -304,85 +328,72 @@ function SliderButton({
304
328
  stableOnClose,
305
329
  stableOnError
306
330
  ]);
307
- const proxyHandle = useMemo(
308
- () => ({
309
- open: () => {
310
- createSlider();
311
- setOpen(true);
312
- },
313
- close: () => {
314
- handleRef.current?.destroy();
315
- handleRef.current = null;
316
- setOpen(false);
317
- },
318
- toggle: () => {
319
- if (handleRef.current) {
320
- handleRef.current.destroy();
321
- handleRef.current = null;
322
- setOpen(false);
323
- } else {
324
- createSlider();
325
- setOpen(true);
326
- }
327
- },
328
- unmount: () => {
329
- handleRef.current?.unmount();
330
- handleRef.current = null;
331
- setOpen(false);
332
- },
333
- get isOpen() {
334
- return isOpen;
335
- },
336
- researchId
337
- }),
338
- [createSlider, setOpen, researchId, isOpen]
339
- );
340
331
  useEffect(() => {
341
- if (embedRef) {
342
- embedRef.current = proxyHandle;
332
+ if (!isControlled || !handle) return;
333
+ if (controlledOpen && !handle.isOpen) {
334
+ handle.open();
335
+ } else if (!controlledOpen && handle.isOpen) {
336
+ handle.close();
343
337
  }
344
- return () => {
345
- if (embedRef) {
346
- embedRef.current = null;
347
- }
348
- };
349
- }, [embedRef, proxyHandle]);
350
- useEffect(() => {
351
- if (!isControlled) return;
352
- if (open && !handleRef.current) {
353
- createSlider();
354
- } else if (!open && handleRef.current) {
355
- handleRef.current.destroy();
356
- handleRef.current = null;
338
+ }, [controlledOpen, isControlled, handle]);
339
+ const openFn = useCallback(() => {
340
+ if (isControlled) {
341
+ onOpenChange?.(true);
342
+ } else {
343
+ handleRef.current?.open();
344
+ setInternalOpen(true);
357
345
  }
358
- }, [open, isControlled, createSlider]);
359
- const handleClick = useCallback(
360
- (e) => {
361
- onClick?.(e);
362
- if (e.defaultPrevented) return;
363
- if (isOpen && handleRef.current) {
364
- handleRef.current.destroy();
365
- handleRef.current = null;
366
- setOpen(false);
367
- } else {
368
- createSlider();
369
- setOpen(true);
370
- }
371
- },
372
- [onClick, isOpen, createSlider, setOpen]
373
- );
374
- return /* @__PURE__ */ jsx(
375
- "button",
376
- {
377
- type: "button",
378
- onClick: handleClick,
379
- "data-testid": "perspective-slider-button",
380
- ...buttonProps,
381
- children
346
+ }, [isControlled, onOpenChange]);
347
+ const closeFn = useCallback(() => {
348
+ if (isControlled) {
349
+ onOpenChange?.(false);
350
+ } else {
351
+ handleRef.current?.close();
352
+ setInternalOpen(false);
353
+ }
354
+ }, [isControlled, onOpenChange]);
355
+ const toggleFn = useCallback(() => {
356
+ const currentlyOpen = handleRef.current?.isOpen ?? internalOpen;
357
+ if (currentlyOpen) {
358
+ closeFn();
359
+ } else {
360
+ openFn();
382
361
  }
362
+ }, [internalOpen, openFn, closeFn]);
363
+ const unmountFn = useCallback(() => {
364
+ handleRef.current?.unmount();
365
+ handleRef.current = null;
366
+ setHandle(null);
367
+ setInternalOpen(false);
368
+ }, []);
369
+ const isOpen = isControlled ? controlledOpen : handle?.isOpen ?? internalOpen;
370
+ return {
371
+ open: openFn,
372
+ close: closeFn,
373
+ toggle: toggleFn,
374
+ unmount: unmountFn,
375
+ isOpen,
376
+ handle
377
+ };
378
+ }
379
+ function useThemeSync(theme = "system") {
380
+ const [resolved, setResolved] = useState(
381
+ theme !== "system" ? theme : "light"
383
382
  );
383
+ useEffect(() => {
384
+ if (theme !== "system") {
385
+ setResolved(theme);
386
+ return;
387
+ }
388
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
389
+ setResolved(mq.matches ? "dark" : "light");
390
+ const handler = (e) => setResolved(e.matches ? "dark" : "light");
391
+ mq.addEventListener("change", handler);
392
+ return () => mq.removeEventListener("change", handler);
393
+ }, [theme]);
394
+ return resolved;
384
395
  }
385
- function FloatBubble({
396
+ function Widget({
386
397
  researchId,
387
398
  params,
388
399
  brand,
@@ -393,8 +404,12 @@ function FloatBubble({
393
404
  onNavigate,
394
405
  onClose,
395
406
  onError,
396
- embedRef
407
+ embedRef,
408
+ className,
409
+ style,
410
+ ...divProps
397
411
  }) {
412
+ const containerRef = useRef(null);
398
413
  const handleRef = useRef(null);
399
414
  const stableOnReady = useStableCallback(onReady);
400
415
  const stableOnSubmit = useStableCallback(onSubmit);
@@ -402,7 +417,9 @@ function FloatBubble({
402
417
  const stableOnClose = useStableCallback(onClose);
403
418
  const stableOnError = useStableCallback(onError);
404
419
  useEffect(() => {
405
- const handle = createFloatBubble({
420
+ const container = containerRef.current;
421
+ if (!container) return;
422
+ const handle = createWidget(container, {
406
423
  researchId,
407
424
  params,
408
425
  brand,
@@ -438,7 +455,16 @@ function FloatBubble({
438
455
  stableOnError,
439
456
  embedRef
440
457
  ]);
441
- return null;
458
+ return /* @__PURE__ */ jsx(
459
+ "div",
460
+ {
461
+ ref: containerRef,
462
+ className,
463
+ style: { minHeight: 500, ...style },
464
+ "data-testid": "perspective-widget",
465
+ ...divProps
466
+ }
467
+ );
442
468
  }
443
469
  function Fullpage({
444
470
  researchId,
@@ -498,24 +524,44 @@ function Fullpage({
498
524
  ]);
499
525
  return null;
500
526
  }
501
- function useThemeSync(theme = "system") {
502
- const [resolved, setResolved] = useState(
503
- theme !== "system" ? theme : "light"
504
- );
527
+ function FloatBubble({
528
+ researchId,
529
+ params,
530
+ brand,
531
+ theme,
532
+ host,
533
+ onReady,
534
+ onSubmit,
535
+ onNavigate,
536
+ onClose,
537
+ onError,
538
+ embedRef
539
+ }) {
540
+ const { handle } = useFloatBubble({
541
+ researchId,
542
+ params,
543
+ brand,
544
+ theme,
545
+ host,
546
+ onReady,
547
+ onSubmit,
548
+ onNavigate,
549
+ onClose,
550
+ onError
551
+ });
505
552
  useEffect(() => {
506
- if (theme !== "system") {
507
- setResolved(theme);
508
- return;
553
+ if (embedRef) {
554
+ embedRef.current = handle;
509
555
  }
510
- const mq = window.matchMedia("(prefers-color-scheme: dark)");
511
- setResolved(mq.matches ? "dark" : "light");
512
- const handler = (e) => setResolved(e.matches ? "dark" : "light");
513
- mq.addEventListener("change", handler);
514
- return () => mq.removeEventListener("change", handler);
515
- }, [theme]);
516
- return resolved;
556
+ return () => {
557
+ if (embedRef) {
558
+ embedRef.current = null;
559
+ }
560
+ };
561
+ }, [embedRef, handle]);
562
+ return null;
517
563
  }
518
564
 
519
- export { FloatBubble, Fullpage, PopupButton, SliderButton, Widget, useStableCallback, useThemeSync };
565
+ export { FloatBubble, Fullpage, Widget, useFloatBubble, usePopup, useSlider, useStableCallback, useThemeSync };
520
566
  //# sourceMappingURL=index.js.map
521
567
  //# sourceMappingURL=index.js.map