@drippr/embed-react 0.1.0

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.mjs ADDED
@@ -0,0 +1,591 @@
1
+ // src/DripprFlow.tsx
2
+ import * as React3 from "react";
3
+
4
+ // src/InlineContainer.tsx
5
+ import { jsx } from "react/jsx-runtime";
6
+ function InlineContainer({
7
+ src,
8
+ height,
9
+ iframeRef,
10
+ className,
11
+ style
12
+ }) {
13
+ return /* @__PURE__ */ jsx(
14
+ "div",
15
+ {
16
+ className,
17
+ style: {
18
+ width: "100%",
19
+ ...style
20
+ },
21
+ children: /* @__PURE__ */ jsx(
22
+ "iframe",
23
+ {
24
+ ref: iframeRef,
25
+ src,
26
+ style: {
27
+ width: "100%",
28
+ height: `${height}px`,
29
+ border: "none",
30
+ display: "block"
31
+ },
32
+ loading: "lazy",
33
+ title: "Drippr Feedback"
34
+ }
35
+ )
36
+ }
37
+ );
38
+ }
39
+
40
+ // src/ModalContainer.tsx
41
+ import * as React from "react";
42
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
43
+ var ANIMATION_DURATION = 200;
44
+ var MOBILE_BREAKPOINT = 640;
45
+ function ModalContainer({
46
+ src,
47
+ height,
48
+ iframeRef,
49
+ className,
50
+ style,
51
+ open,
52
+ onOpenChange,
53
+ trigger
54
+ }) {
55
+ const [mounted, setMounted] = React.useState(false);
56
+ const [visible, setVisible] = React.useState(false);
57
+ const [isMobile, setIsMobile] = React.useState(false);
58
+ React.useEffect(() => {
59
+ const checkMobile = () => {
60
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
61
+ };
62
+ checkMobile();
63
+ window.addEventListener("resize", checkMobile);
64
+ return () => window.removeEventListener("resize", checkMobile);
65
+ }, []);
66
+ React.useEffect(() => {
67
+ if (open) {
68
+ setMounted(true);
69
+ requestAnimationFrame(() => {
70
+ requestAnimationFrame(() => {
71
+ setVisible(true);
72
+ });
73
+ });
74
+ } else {
75
+ setVisible(false);
76
+ const timer = setTimeout(() => {
77
+ setMounted(false);
78
+ }, ANIMATION_DURATION);
79
+ return () => clearTimeout(timer);
80
+ }
81
+ }, [open]);
82
+ React.useEffect(() => {
83
+ if (!open) return;
84
+ const handleKeyDown = (e) => {
85
+ if (e.key === "Escape") {
86
+ onOpenChange(false);
87
+ }
88
+ };
89
+ document.addEventListener("keydown", handleKeyDown);
90
+ return () => {
91
+ document.removeEventListener("keydown", handleKeyDown);
92
+ };
93
+ }, [open, onOpenChange]);
94
+ React.useEffect(() => {
95
+ if (open) {
96
+ const originalOverflow = document.body.style.overflow;
97
+ document.body.style.overflow = "hidden";
98
+ return () => {
99
+ document.body.style.overflow = originalOverflow;
100
+ };
101
+ }
102
+ }, [open]);
103
+ const overlayStyles = {
104
+ position: "fixed",
105
+ inset: 0,
106
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
107
+ display: "flex",
108
+ alignItems: isMobile ? "flex-end" : "center",
109
+ justifyContent: "center",
110
+ zIndex: 9999,
111
+ padding: isMobile ? 0 : "16px",
112
+ // Animation
113
+ opacity: visible ? 1 : 0,
114
+ transition: `opacity ${ANIMATION_DURATION}ms ease-out`
115
+ };
116
+ const dialogStyles = isMobile ? {
117
+ // Mobile: bottom sheet
118
+ backgroundColor: "#fff",
119
+ borderRadius: "16px 16px 0 0",
120
+ width: "100%",
121
+ // Use dvh for dynamic viewport on iOS, fallback to vh
122
+ maxHeight: "calc(100dvh - 32px)",
123
+ overflow: "hidden",
124
+ position: "relative",
125
+ boxShadow: "0 -4px 25px -5px rgba(0, 0, 0, 0.1)",
126
+ // Animation - slide from bottom
127
+ transform: visible ? "translateY(0)" : "translateY(100%)",
128
+ transition: `transform ${ANIMATION_DURATION}ms cubic-bezier(0.32, 0.72, 0, 1)`
129
+ } : {
130
+ // Desktop: centered modal
131
+ backgroundColor: "#fff",
132
+ borderRadius: "12px",
133
+ width: "100%",
134
+ maxWidth: "600px",
135
+ maxHeight: "90vh",
136
+ overflow: "hidden",
137
+ position: "relative",
138
+ boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
139
+ // Animation - scale + fade
140
+ opacity: visible ? 1 : 0,
141
+ transform: visible ? "scale(1)" : "scale(0.95)",
142
+ transition: `opacity ${ANIMATION_DURATION}ms ease-out, transform ${ANIMATION_DURATION}ms ease-out`
143
+ };
144
+ const closeButtonStyles = {
145
+ position: "absolute",
146
+ top: "8px",
147
+ right: "8px",
148
+ width: "32px",
149
+ height: "32px",
150
+ border: "none",
151
+ background: "transparent",
152
+ cursor: "pointer",
153
+ display: "flex",
154
+ alignItems: "center",
155
+ justifyContent: "center",
156
+ borderRadius: "6px",
157
+ color: "#666",
158
+ fontSize: "28px",
159
+ lineHeight: 1,
160
+ zIndex: 1
161
+ };
162
+ const handleStyles = {
163
+ width: "36px",
164
+ height: "4px",
165
+ backgroundColor: "#d1d5db",
166
+ borderRadius: "2px",
167
+ margin: "6px auto 2px"
168
+ };
169
+ if (!mounted) {
170
+ return /* @__PURE__ */ jsx2(Fragment, { children: trigger });
171
+ }
172
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
173
+ trigger,
174
+ /* @__PURE__ */ jsx2(
175
+ "div",
176
+ {
177
+ style: overlayStyles,
178
+ onClick: (e) => {
179
+ if (e.target === e.currentTarget) {
180
+ onOpenChange(false);
181
+ }
182
+ },
183
+ role: "dialog",
184
+ "aria-modal": "true",
185
+ children: /* @__PURE__ */ jsxs(
186
+ "div",
187
+ {
188
+ className,
189
+ style: {
190
+ ...dialogStyles,
191
+ ...style
192
+ },
193
+ children: [
194
+ isMobile && /* @__PURE__ */ jsx2("div", { style: handleStyles }),
195
+ /* @__PURE__ */ jsx2(
196
+ "button",
197
+ {
198
+ type: "button",
199
+ style: closeButtonStyles,
200
+ onClick: () => onOpenChange(false),
201
+ "aria-label": "Close",
202
+ children: "\xD7"
203
+ }
204
+ ),
205
+ /* @__PURE__ */ jsx2(
206
+ "iframe",
207
+ {
208
+ ref: iframeRef,
209
+ src,
210
+ style: {
211
+ width: "100%",
212
+ height: isMobile ? `${Math.min(height, window.innerHeight - 80)}px` : `${Math.min(height, window.innerHeight * 0.85)}px`,
213
+ border: "none",
214
+ display: "block"
215
+ },
216
+ loading: "lazy",
217
+ title: "Drippr Feedback"
218
+ }
219
+ )
220
+ ]
221
+ }
222
+ )
223
+ }
224
+ )
225
+ ] });
226
+ }
227
+
228
+ // src/DrawerContainer.tsx
229
+ import * as React2 from "react";
230
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
231
+ var ANIMATION_DURATION2 = 250;
232
+ var MOBILE_BREAKPOINT2 = 640;
233
+ function DrawerContainer({
234
+ src,
235
+ height,
236
+ iframeRef,
237
+ className,
238
+ style,
239
+ open,
240
+ onOpenChange,
241
+ trigger
242
+ }) {
243
+ const [mounted, setMounted] = React2.useState(false);
244
+ const [visible, setVisible] = React2.useState(false);
245
+ const [isMobile, setIsMobile] = React2.useState(false);
246
+ React2.useEffect(() => {
247
+ const checkMobile = () => {
248
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT2);
249
+ };
250
+ checkMobile();
251
+ window.addEventListener("resize", checkMobile);
252
+ return () => window.removeEventListener("resize", checkMobile);
253
+ }, []);
254
+ React2.useEffect(() => {
255
+ if (open) {
256
+ setMounted(true);
257
+ requestAnimationFrame(() => {
258
+ requestAnimationFrame(() => {
259
+ setVisible(true);
260
+ });
261
+ });
262
+ } else {
263
+ setVisible(false);
264
+ const timer = setTimeout(() => {
265
+ setMounted(false);
266
+ }, ANIMATION_DURATION2);
267
+ return () => clearTimeout(timer);
268
+ }
269
+ }, [open]);
270
+ React2.useEffect(() => {
271
+ if (!open) return;
272
+ const handleKeyDown = (e) => {
273
+ if (e.key === "Escape") {
274
+ onOpenChange(false);
275
+ }
276
+ };
277
+ document.addEventListener("keydown", handleKeyDown);
278
+ return () => {
279
+ document.removeEventListener("keydown", handleKeyDown);
280
+ };
281
+ }, [open, onOpenChange]);
282
+ React2.useEffect(() => {
283
+ if (open) {
284
+ const originalOverflow = document.body.style.overflow;
285
+ document.body.style.overflow = "hidden";
286
+ return () => {
287
+ document.body.style.overflow = originalOverflow;
288
+ };
289
+ }
290
+ }, [open]);
291
+ const overlayStyles = {
292
+ position: "fixed",
293
+ inset: 0,
294
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
295
+ zIndex: 9999,
296
+ // Animation
297
+ opacity: visible ? 1 : 0,
298
+ transition: `opacity ${ANIMATION_DURATION2}ms ease-out`
299
+ };
300
+ const drawerStyles = isMobile ? {
301
+ // Mobile: bottom sheet
302
+ position: "fixed",
303
+ left: 0,
304
+ right: 0,
305
+ top: 32,
306
+ bottom: 0,
307
+ width: "100%",
308
+ backgroundColor: "#fff",
309
+ borderRadius: "16px 16px 0 0",
310
+ boxShadow: "0 -4px 25px -5px rgba(0, 0, 0, 0.1)",
311
+ zIndex: 1e4,
312
+ display: "flex",
313
+ flexDirection: "column",
314
+ overflow: "hidden",
315
+ // Animation - slide from bottom
316
+ transform: visible ? "translateY(0)" : "translateY(100%)",
317
+ transition: `transform ${ANIMATION_DURATION2}ms cubic-bezier(0.32, 0.72, 0, 1)`
318
+ } : {
319
+ // Desktop: right drawer
320
+ position: "fixed",
321
+ top: 0,
322
+ right: 0,
323
+ bottom: 0,
324
+ width: "100%",
325
+ maxWidth: "480px",
326
+ backgroundColor: "#fff",
327
+ boxShadow: "-4px 0 25px -5px rgba(0, 0, 0, 0.1)",
328
+ zIndex: 1e4,
329
+ display: "flex",
330
+ flexDirection: "column",
331
+ overflow: "hidden",
332
+ // Animation - slide from right
333
+ transform: visible ? "translateX(0)" : "translateX(100%)",
334
+ transition: `transform ${ANIMATION_DURATION2}ms cubic-bezier(0.32, 0.72, 0, 1)`
335
+ };
336
+ const headerStyles = {
337
+ display: "flex",
338
+ alignItems: "center",
339
+ justifyContent: "flex-end",
340
+ padding: isMobile ? "4px 8px" : "8px",
341
+ borderBottom: isMobile ? "none" : "1px solid #e5e5e5",
342
+ flexShrink: 0
343
+ };
344
+ const closeButtonStyles = {
345
+ width: "32px",
346
+ height: "32px",
347
+ border: "none",
348
+ background: "transparent",
349
+ cursor: "pointer",
350
+ display: "flex",
351
+ alignItems: "center",
352
+ justifyContent: "center",
353
+ borderRadius: "6px",
354
+ color: "#666",
355
+ fontSize: "28px",
356
+ lineHeight: 1
357
+ };
358
+ const contentStyles = {
359
+ flex: 1,
360
+ overflow: "auto"
361
+ };
362
+ const handleStyles = {
363
+ width: "36px",
364
+ height: "4px",
365
+ backgroundColor: "#d1d5db",
366
+ borderRadius: "2px",
367
+ margin: "6px auto 2px"
368
+ };
369
+ if (!mounted) {
370
+ return /* @__PURE__ */ jsx3(Fragment2, { children: trigger });
371
+ }
372
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
373
+ trigger,
374
+ /* @__PURE__ */ jsx3(
375
+ "div",
376
+ {
377
+ style: overlayStyles,
378
+ onClick: () => onOpenChange(false),
379
+ "aria-hidden": "true"
380
+ }
381
+ ),
382
+ /* @__PURE__ */ jsxs2(
383
+ "div",
384
+ {
385
+ className,
386
+ style: {
387
+ ...drawerStyles,
388
+ ...style
389
+ },
390
+ role: "dialog",
391
+ "aria-modal": "true",
392
+ children: [
393
+ isMobile && /* @__PURE__ */ jsx3("div", { style: handleStyles }),
394
+ /* @__PURE__ */ jsx3("div", { style: headerStyles, children: /* @__PURE__ */ jsx3(
395
+ "button",
396
+ {
397
+ type: "button",
398
+ style: closeButtonStyles,
399
+ onClick: () => onOpenChange(false),
400
+ "aria-label": "Close",
401
+ children: "\xD7"
402
+ }
403
+ ) }),
404
+ /* @__PURE__ */ jsx3("div", { style: contentStyles, children: /* @__PURE__ */ jsx3(
405
+ "iframe",
406
+ {
407
+ ref: iframeRef,
408
+ src,
409
+ style: {
410
+ width: "100%",
411
+ height: isMobile ? `${Math.min(height, window.innerHeight - 80)}px` : `${height}px`,
412
+ minHeight: isMobile ? void 0 : "100%",
413
+ border: "none",
414
+ display: "block"
415
+ },
416
+ loading: "lazy",
417
+ title: "Drippr Feedback"
418
+ }
419
+ ) })
420
+ ]
421
+ }
422
+ )
423
+ ] });
424
+ }
425
+
426
+ // src/DripprFlow.tsx
427
+ import { jsx as jsx4 } from "react/jsx-runtime";
428
+ var DEFAULT_BASE_URL = "https://app.drippr.io";
429
+ var DEFAULT_HEIGHT = 700;
430
+ function generateInstanceId() {
431
+ return `drippr_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
432
+ }
433
+ function DripprFlow({
434
+ flowId,
435
+ mode = "inline",
436
+ open: controlledOpen,
437
+ onOpenChange,
438
+ prefill,
439
+ children,
440
+ triggerLabel = "Give Feedback",
441
+ onSubmitted,
442
+ className,
443
+ style,
444
+ height: fallbackHeight = DEFAULT_HEIGHT,
445
+ baseUrl = DEFAULT_BASE_URL
446
+ }) {
447
+ const [instanceId] = React3.useState(generateInstanceId);
448
+ const [height, setHeight] = React3.useState(fallbackHeight);
449
+ const [internalOpen, setInternalOpen] = React3.useState(false);
450
+ const iframeRef = React3.useRef(null);
451
+ const readyRef = React3.useRef(false);
452
+ const [expectedOrigin, setExpectedOrigin] = React3.useState("");
453
+ const isControlled = controlledOpen !== void 0;
454
+ const isOpen = isControlled ? controlledOpen : internalOpen;
455
+ const handleOpenChange = React3.useCallback(
456
+ (newOpen) => {
457
+ if (isControlled) {
458
+ onOpenChange?.(newOpen);
459
+ } else {
460
+ setInternalOpen(newOpen);
461
+ }
462
+ },
463
+ [isControlled, onOpenChange]
464
+ );
465
+ const src = React3.useMemo(() => {
466
+ const url = new URL(`/submit/${flowId}`, baseUrl);
467
+ url.searchParams.set("embed", "1");
468
+ url.searchParams.set("instanceId", instanceId);
469
+ setExpectedOrigin(url.origin);
470
+ return url.toString();
471
+ }, [flowId, baseUrl, instanceId]);
472
+ const api = React3.useMemo(
473
+ () => ({
474
+ open: () => handleOpenChange(true),
475
+ close: () => handleOpenChange(false)
476
+ }),
477
+ [handleOpenChange]
478
+ );
479
+ const sendPrefill = React3.useCallback(() => {
480
+ if (!prefill || !iframeRef.current?.contentWindow) return;
481
+ const message = {
482
+ type: "drippr:prefill",
483
+ instanceId,
484
+ payload: prefill
485
+ };
486
+ iframeRef.current.contentWindow.postMessage(message, expectedOrigin);
487
+ }, [prefill, instanceId, expectedOrigin]);
488
+ const handleMessage = React3.useCallback(
489
+ (event) => {
490
+ if (event.origin !== expectedOrigin) return;
491
+ const data = event.data;
492
+ if (!data || typeof data !== "object" || !data.type) return;
493
+ if (!data.type.startsWith("drippr:")) return;
494
+ if (data.instanceId !== instanceId) return;
495
+ switch (data.type) {
496
+ case "drippr:ready":
497
+ readyRef.current = true;
498
+ sendPrefill();
499
+ break;
500
+ case "drippr:resize":
501
+ if (typeof data.height === "number" && data.height > 0) {
502
+ setHeight(data.height);
503
+ }
504
+ break;
505
+ case "drippr:submitted":
506
+ onSubmitted?.();
507
+ if (mode !== "inline" && !isControlled) {
508
+ handleOpenChange(false);
509
+ }
510
+ break;
511
+ }
512
+ },
513
+ [expectedOrigin, instanceId, sendPrefill, onSubmitted, mode, isControlled, handleOpenChange]
514
+ );
515
+ React3.useEffect(() => {
516
+ window.addEventListener("message", handleMessage);
517
+ return () => {
518
+ window.removeEventListener("message", handleMessage);
519
+ };
520
+ }, [handleMessage]);
521
+ React3.useEffect(() => {
522
+ if (readyRef.current && prefill) {
523
+ sendPrefill();
524
+ }
525
+ }, [prefill, sendPrefill]);
526
+ const trigger = mode !== "inline" && children ? children(api) : mode !== "inline" ? /* @__PURE__ */ jsx4(
527
+ "button",
528
+ {
529
+ type: "button",
530
+ onClick: () => handleOpenChange(true),
531
+ style: {
532
+ padding: "8px 16px",
533
+ borderRadius: "6px",
534
+ border: "1px solid #e5e5e5",
535
+ background: "#fff",
536
+ cursor: "pointer",
537
+ fontSize: "14px"
538
+ },
539
+ children: triggerLabel
540
+ }
541
+ ) : null;
542
+ if (mode === "inline") {
543
+ return /* @__PURE__ */ jsx4(
544
+ InlineContainer,
545
+ {
546
+ src,
547
+ height,
548
+ onMessage: handleMessage,
549
+ iframeRef,
550
+ className,
551
+ style
552
+ }
553
+ );
554
+ }
555
+ if (mode === "modal") {
556
+ return /* @__PURE__ */ jsx4(
557
+ ModalContainer,
558
+ {
559
+ src,
560
+ height,
561
+ onMessage: handleMessage,
562
+ iframeRef,
563
+ className,
564
+ style,
565
+ open: isOpen,
566
+ onOpenChange: handleOpenChange,
567
+ trigger
568
+ }
569
+ );
570
+ }
571
+ if (mode === "drawer") {
572
+ return /* @__PURE__ */ jsx4(
573
+ DrawerContainer,
574
+ {
575
+ src,
576
+ height,
577
+ onMessage: handleMessage,
578
+ iframeRef,
579
+ className,
580
+ style,
581
+ open: isOpen,
582
+ onOpenChange: handleOpenChange,
583
+ trigger
584
+ }
585
+ );
586
+ }
587
+ return null;
588
+ }
589
+ export {
590
+ DripprFlow
591
+ };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@drippr/embed-react",
3
+ "version": "0.1.0",
4
+ "description": "React component for embedding Drippr feedback flows",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
20
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
21
+ "lint": "eslint src/",
22
+ "typecheck": "tsc --noEmit",
23
+ "prepublishOnly": "npm run build",
24
+ "publish": "npm publish --access public"
25
+ },
26
+ "peerDependencies": {
27
+ "react": ">=18.0.0",
28
+ "react-dom": ">=18.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/react": "^18.2.0",
32
+ "@types/react-dom": "^18.2.0",
33
+ "react": "^18.2.0",
34
+ "react-dom": "^18.2.0",
35
+ "tsup": "^8.0.0",
36
+ "typescript": "^5.0.0"
37
+ },
38
+ "keywords": [
39
+ "drippr",
40
+ "feedback",
41
+ "embed",
42
+ "react",
43
+ "iframe"
44
+ ],
45
+ "license": "MIT",
46
+ "author": "Drippr",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/drippr/embed-react"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ }
54
+ }