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